From 24349bd66e2526de20b52292faaa6908b0fb93ab Mon Sep 17 00:00:00 2001 From: dave horner Date: Fri, 13 Jun 2025 08:54:25 -0400 Subject: [PATCH] feat(extension): add update/install and cargo-e sync functionality - Added commands to update, package, install, and broadcast extension updates across machines - Enabled cargo project detection with Cargo.toml and cargo-e target selection during sync - Added support for handling incoming cargo-e sync events via WebSocket - Improved status bar UI and notifications for room ID and WebSocket events - Updated version to 0.0.57 and enhanced package-lock with glob and related dependencies - Bumped changelog for version 0.0.6 --- CHANGELOG.md | 1 + README.md | 1 + package-lock.json | 122 ++++----- package.json | 8 + src/extension.ts | 654 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 697 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984437e..483ead0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.6] - 2025-06-12 - Add command to show/edit room ID +- Auto-detect cargo and cmake projects on sync, add cargo-e target selection ## [0.0.5] - 2025-05-14 diff --git a/README.md b/README.md index 92aac5d..b638264 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Happy coding. ## How to use **Prerequisites:** +- To package you will need `npm install -g @vscode/vsce` - The extension should automatically create an entry in `settings.json`, `multiBuild.server.roomId`, and if you have your VS Code setup to sync your settings, this room ID will automatically be shared on all your computers. You may need to force-sync as VS Code can have a mind of its own. diff --git a/package-lock.json b/package-lock.json index a5d654b..5fdc0d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -650,6 +650,67 @@ "node": ">=18" } }, + "node_modules/@vscode/test-cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/test-cli/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@vscode/test-cli/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vscode/test-cli/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/test-electron": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", @@ -2081,27 +2142,6 @@ "dev": true, "license": "MIT" }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2557,22 +2597,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2726,13 +2750,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -3484,23 +3501,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index 2243916..bc67901 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,14 @@ { "command": "multiBuild.showRoomId", "title": "Multi-Build: Show/Edit Room ID" + }, + { + "command": "multiBuild.updateAndInstall", + "title": "Multi-Build: Update, Package, and Install Extension" + }, + { + "command": "multiBuild.broadcastUpdateAndInstall", + "title": "Multi-Build: Broadcast Update/Install to All Machines" } ], "configuration": { diff --git a/src/extension.ts b/src/extension.ts index 9fb6275..615afc8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,14 +4,19 @@ import { API as GitAPI, GitExtension } from "../typings/git"; import WebSocket from "ws"; import { randomUUID } from "crypto"; import assert from "assert"; +import { exec } from "child_process"; +import { promisify } from "util"; +const execAsync = promisify(exec); const extensionName = "Multi-Build"; const logTag = "[multi-build]"; +const cargoLogTag = "[cargo-e]"; const serverConfigKey = "multiBuild.server"; const syncDataConfigKey = "multiBuild.syncData"; const reconnectCommand = `multiBuild.reconnect`; const syncCommand = `multiBuild.sync`; const showRoomIdCommand = "multiBuild.showRoomId"; +const updateAndInstallCommand = "multiBuild.updateAndInstall"; const defaultBaseUrl = "wss://multi-build-server.symless.workers.dev"; const keepAliveIntervalMillis = 10000; // 10 seconds @@ -21,6 +26,9 @@ var keepAlive: NodeJS.Timeout | undefined; var connected = false; export function activate(context: vscode.ExtensionContext) { + console.log(`${logTag} Activating ${extensionName} v${context.extension.packageJSON.version}`); + vscode.window.showInformationMessage(`${extensionName} v${context.extension.packageJSON.version} loaded successfully.`); + init().catch((error) => { handleError(error); }); @@ -59,6 +67,99 @@ export function activate(context: vscode.ExtensionContext) { } }), ); + + // Register command to update, package, and install the extension + context.subscriptions.push( + vscode.commands.registerCommand(updateAndInstallCommand, async () => { + try { + // Use a visible terminal for all steps + const terminal = vscode.window.createTerminal({ name: "Multi-Build Update" }); + terminal.show(); + // Use VS Code Git API to pull latest code instead of terminal command + try { + const git = getGitAPI(); + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + const repo = git.repositories.find(r => r.rootUri.fsPath === workspacePath); + if (repo) { + // Pulls the currently checked out branch + await repo.pull(); + vscode.window.showInformationMessage("Multi-Build: Pulled latest code using VS Code Git API."); + } else { + vscode.window.showWarningMessage("Multi-Build: No Git repository found for workspace, skipping pull."); + } + } catch (err) { + vscode.window.showWarningMessage(`Multi-Build: Git pull failed: ${err}`); + } + + const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + const pkg = require(path.join(workspacePath, "package.json")); + const version = pkg.version; + const vsixName = `multi-build-${version}.vsix`; + const vsixPath = path.join(workspacePath, vsixName); + const fs = require("fs"); + let prevMtime = 0; + if (fs.existsSync(vsixPath)) { + prevMtime = fs.statSync(vsixPath).mtime.getTime(); + } + vscode.window.showInformationMessage(`Multi-Build v${version}: Pulling latest code in terminal...`); + + // Clean up old VSIX files before packaging (remove glob, just delete known file) + if (fs.existsSync(vsixPath)) { + try { fs.unlinkSync(vsixPath); } catch (e) { /* ignore */ } + } + vscode.window.showInformationMessage("Multi-Build: Deleted previous VSIX file before packaging."); + + terminal.sendText("npm run package"); + vscode.window.showInformationMessage(`Multi-Build v${version}: Packaging extension in terminal...`); + // Wait for the new .vsix file to be created/updated (remove glob, just check vsixPath) + const waitForVsix = async () => { + for (let i = 0; i < 30; ++i) { // up to ~15 seconds + if (fs.existsSync(vsixPath)) { + const mtime = fs.statSync(vsixPath).mtime.getTime(); + if (mtime > prevMtime) { + return vsixPath; + } + } + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + return undefined; + }; + const foundVsix = await waitForVsix(); + if (!foundVsix) { + vscode.window.showErrorMessage(`Multi-Build: ${vsixName} not found or not updated after packaging. Make sure packaging succeeded.`); + return; + } + vscode.window.showInformationMessage(`Multi-Build v${version}: Installing extension from VSIX...`); + // Install the correct VSIX using VS Code's API + await vscode.commands.executeCommand('workbench.extensions.installExtension', vscode.Uri.file(foundVsix)); + // Broadcast a refresh command to all connected instances after successful upgrade + vscode.commands.executeCommand('workbench.action.reloadWindow'); + if (roomSocket) { + sendMessage({ type: "refresh-all-windows" }); + vscode.window.showInformationMessage("Multi-Build: Refresh command sent to all connected instances."); + } else { + vscode.window.showInformationMessage("Multi-Build: No WebSocket connection (roomSocket is 0), not refreshing other code instances."); + } + vscode.window.showInformationMessage(`Multi-Build: Pulled, packaged, and installed ${vsixName} (v${version})`); + } catch (err) { + vscode.window.showErrorMessage(`Multi-Build: Update/install failed: ${err}`); + } + }) + ); + // Add a command to broadcast update-and-install to all machines + context.subscriptions.push( + vscode.commands.registerCommand("multiBuild.broadcastUpdateAndInstall", async () => { + sendMessage({ type: "update-and-install" }); + vscode.window.showInformationMessage("Multi-Build: Sent update/install command to all machines in the room."); + }) + ); + + // Create a status bar item to display the current version + const versionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); + versionStatusBarItem.text = `$(tag) ${extensionName} v${context.extension.packageJSON.version}`; + versionStatusBarItem.tooltip = "Current version of Multi-Build extension"; + versionStatusBarItem.show(); + context.subscriptions.push(versionStatusBarItem); } export function deactivate() { @@ -253,18 +354,73 @@ async function pushRepoSettings() { } // Always ask the branch name, as this is what changes most often. - // Copy pasting this from the PR isn't a big deal, and it's not often one we've used before. - const branch = await vscode.window.showInputBox({ + // Try to get the current branch from the selected repo + let branch: string | undefined = config?.branch; + if (!branch) { + try { + const git = getGitAPI(); + const repoObj = git.repositories.find((r) => path.basename(r.rootUri.fsPath) === repo); + branch = repoObj?.state.HEAD?.name; + } catch (e) { + // ignore error, fallback to default + } + } + if (!branch) { + branch = "master"; + } + branch = await vscode.window.showInputBox({ prompt: "Enter the branch name", placeHolder: "hello-branch", - value: config?.branch || "master", + value: branch || "master", }); if (!branch) { vscode.window.showErrorMessage(`${extensionName}: Cannot sync, no branch specified`); return; } - const data = { repo, remote, branch }; + // Check for Cargo.toml files + const cargoFiles = await vscode.workspace.findFiles("**/Cargo.toml"); + let manifestPath: string | undefined = undefined; + let target: string | undefined = undefined; + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + let cargoSelected = false; + if (cargoFiles.length > 0 && workspaceFolder) { + vscode.window.showInformationMessage(`Multi-Build: Found ${cargoFiles.length} Cargo.toml file(s) in the workspace.`); + // Sort the files alphabetically by their paths + const sortedCargoFiles = cargoFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath)); + + console.log(`${cargoLogTag} Found and sorted Cargo.toml files:`, sortedCargoFiles.map(f => f.fsPath)); + + const selectedFile = await vscode.window.showQuickPick( + sortedCargoFiles.map((file) => ({ + label: path.basename(file.fsPath), + description: file.fsPath, + filePath: file.fsPath, + })), + { + placeHolder: "Select a Cargo.toml file (Esc to skip)", + }, + ); + if (selectedFile) { + // Store manifestPath as relative to workspace root + manifestPath = path.relative(workspaceFolder, selectedFile.filePath); + const selectedTarget = await listCargoETargets(selectedFile.filePath).catch((error) => { + vscode.window.showErrorMessage(`${cargoLogTag} Error listing Cargo-e targets: ${error}`); + return null; + }); + if (!selectedTarget) { + console.warn(`${cargoLogTag} No target selected, running default Cargo-e command`); + } + target = selectedTarget ? selectedTarget.label : undefined; + cargoSelected = true; + } + } + + // Only include manifestPath/target if a Cargo.toml was selected + const data: any = { repo, remote, branch }; + if (cargoSelected && manifestPath) { data.manifestPath = manifestPath; } + if (cargoSelected && target) { data.target = target; } + console.log(`${logTag} Saving changes to config:`, data); await vscode.workspace.getConfiguration().update(syncDataConfigKey, data, true); @@ -284,7 +440,9 @@ function getSyncData() { const repo = config.get("repo"); const remote = config.get("remote"); const branch = config.get("branch"); - return { repo, remote, branch }; + const manifestPath = config.get("manifestPath"); + const target = config.get("target"); + return { repo, remote, branch, manifestPath, target }; } async function getAuthToken() { @@ -303,39 +461,337 @@ function sendMessage({ type, data }: { type: string; data?: unknown }) { if (!activeSocket) { throw new Error("No WebSocket connection found"); } + // Don't show or send keep-alive messages with data + if (type === "keep-alive") { + console.debug(`${logTag} Sending message: ${type}`, { data }); + roomSocket.send(JSON.stringify({ type, data })); + return; + } + vscode.window.showInformationMessage(`Multi-Build: Sending message: ${type}${data ? ", data: " + JSON.stringify(data) : ""}`); console.debug(`${logTag} Sending message: ${type}`, { data }); activeSocket.send(JSON.stringify({ type, data })); } -async function handleSyncData(data: { repo: string; remote: string; branch: string }) { - const { repo, remote, branch } = data; +async function handleSyncData(data: { repo: string; remote: string; branch: string; manifestPath?: string; target?: string }) { + const { repo, remote, branch, manifestPath, target } = data; if (!repo || !remote || !branch) { console.error(`${logTag} Invalid sync message:`, data); throw new Error("Invalid sync message"); } - + try { + const git = getGitAPI(); + const currentRepoObj = git.repositories.find((r) => path.basename(r.rootUri.fsPath) === repo); + if (!currentRepoObj) { + vscode.window.showWarningMessage(`${extensionName}: The repository '${repo}' does not match any open repository in this workspace.`); + return; + } console.log(`${logTag} Syncing repo: ${repo}, remote: ${remote}, branch: ${branch}`); - const checkoutResult = await checkoutBranch(repo, remote, branch); if (!checkoutResult) { // Not necessarily an error; maybe the repo doesn't exist in this workspace. console.debug(`${logTag} No Git checkout happened, skipping build`); return; } - + vscode.window.showInformationMessage(`${extensionName}: Synced to branch '${branch}' in repository '${repo}'.`); + // Check if Cargo.toml exists in the repo root + const repoObj = git.repositories.find((r) => path.basename(r.rootUri.fsPath) === repo); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + // Info: repoObj is the VS Code Git repository object for the selected repo, or undefined if not found. + // Info: workspaceFolder is the absolute path to the first workspace folder, or undefined if not open. vscode.window.showInformationMessage( - `${extensionName}: Checked out branch '${branch}' from '${repo}/${remote}'`, + `${extensionName}: Synced to branch '${branch}' in repository '${repo}' '${workspaceFolder}'.` ); + if (repoObj && workspaceFolder) { + let selectedFilePath = manifestPath ? path.resolve(workspaceFolder, ...manifestPath.split(/[\\\/]/)) : undefined; + let targetName = target; + if (manifestPath && targetName) { + await vscode.window.showInformationMessage( + `${extensionName}: Using Cargo manifest: ${manifestPath}, target: ${targetName} selectedPath: ${selectedFilePath}` + ); + } else if (manifestPath) { + await vscode.window.showInformationMessage( + `${extensionName}: Using Cargo manifest: ${manifestPath}` + ); + } else if (targetName) { + await vscode.window.showInformationMessage( + `${extensionName}: Using Cargo target: ${targetName}` + ); + } + if (!selectedFilePath) { + let selectedFile: { label: string; description: string; filePath: string } | undefined = undefined; + + // If manifestPath and target are already defined, use those directly + if (manifestPath && target) { + selectedFile = { + label: path.basename(manifestPath), + description: manifestPath, + filePath: manifestPath, + }; + // No need to prompt user, just use provided values + } else { + const cargoFiles = await vscode.workspace.findFiles("**/Cargo.toml"); + console.debug(`${logTag} Found Cargo.toml files:`, cargoFiles.map(f => f.fsPath)); + if (cargoFiles.length > 0) { + // Sort the files alphabetically by their paths + const sortedCargoFiles = cargoFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath)); + + console.log(`${cargoLogTag} Found and sorted Cargo.toml files:`, sortedCargoFiles.map(f => f.fsPath)); + + selectedFile = await vscode.window.showQuickPick( + sortedCargoFiles.map((file) => ({ + label: path.basename(file.fsPath), + description: file.fsPath, + filePath: file.fsPath, + })), + { + placeHolder: "Select a Cargo.toml file", + }, + ); + if (!selectedFile) { + // User cancelled selection, silently continue to CMake/package.json handling + } + } else { + // No Cargo.toml files found, silently continue to CMake/package.json handling + } + } + if (selectedFile) { + selectedFilePath = selectedFile.filePath; + // Prompt for target if not provided + const selectedTarget = await listCargoETargets(selectedFilePath).catch((error) => { + vscode.window.showErrorMessage(`${cargoLogTag} Error listing Cargo-e targets: ${error}`); + return null; + }); + if (!selectedTarget) { + console.warn(`${cargoLogTag} No target selected, running default Cargo-e command`); + } + targetName = selectedTarget ? selectedTarget.label : undefined; + handleCargoECommand(selectedFilePath, targetName, workspaceFolder); + // Send WebSocket message to synchronize with other systems + sendMessage({ + type: "cargo-e", + data: { + repo, + remote, + branch, + manifestPath: selectedFilePath, + target: targetName, + }, + }); + + + } else { + await vscode.window.showErrorMessage(`${extensionName}: No Cargo.toml selected, skipping Cargo build.`); + } + } else { + const posixManifestPath = selectedFilePath.split(path.sep).join(path.posix.sep); + const selectedDir = path.dirname(path.resolve(workspaceFolder, selectedFilePath)); + console.log(`${cargoLogTag} Preparing to run 'cargo-e' in ${selectedDir}`); + + const terminal = vscode.window.createTerminal({ + name: targetName ? `${targetName}` : "Cargo Build", + cwd: selectedDir, + }); + terminal.show(); + + const cargoCommand = targetName + ? `cargo-e --manifest-path "${posixManifestPath}" --target ${targetName}` + : `cargo-e --manifest-path "${posixManifestPath}"`; + console.log(`${cargoLogTag} Executing command: ${cargoCommand}`); + terminal.sendText(cargoCommand); + } + } else { + console.warn(`${logTag} No Git repository found for workspace, skipping Cargo build.`); + await vscode.window.showWarningMessage(`${extensionName}: No Git repository found for workspace, skipping Cargo build.`); + } + + // Check if CMakeLists.txt exists in the repo root + const cmakeFiles = await vscode.workspace.findFiles("**/CMakeLists.txt"); + if (cmakeFiles.length > 0) { + // Filter out CMakeLists.txt files in 'target' directories + const filteredCmakeFiles = cmakeFiles.filter(f => !/[/\\]target[/\\]/.test(f.fsPath)); + if (filteredCmakeFiles.length === 0) { + console.log(`${logTag} All CMakeLists.txt files are in 'target' directories, skipping CMake build.`); + return; + } + // If there are multiple, let the user pick which one to use + let cmakeFileToUse = filteredCmakeFiles[0]; + if (filteredCmakeFiles.length > 1) { + const picked = await vscode.window.showQuickPick( + filteredCmakeFiles.map(f => ({ + label: path.basename(f.fsPath), + description: f.fsPath, + file: f + })), + { placeHolder: "Select a CMakeLists.txt file to use for build" } + ); + if (!picked) { + vscode.window.showInformationMessage(`${logTag} No CMakeLists.txt selected, skipping CMake build.`); + return; + } + cmakeFileToUse = picked.file; + } + // Optionally, set the workspace folder to the directory containing the selected CMakeLists.txt + const cmakeDir = path.dirname(cmakeFileToUse.fsPath); + console.log(`${logTag} Using CMakeLists.txt in: ${cmakeDir}`); + console.debug(`${logTag} Using CMakeLists.txt in: ${cmakeDir}`); + vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(cmakeDir), false); + + console.log(`${logTag} Found CMakeLists.txt files:`, cmakeFiles.map(f => f.fsPath)); + console.log(`${logTag} CMake configure`); + await vscode.commands.executeCommand("cmake.configure"); + + // Wait a moment for CMake to finish up (or we get "already running" errors). + console.log(`${logTag} Waiting for CMake`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + console.log(`${logTag} CMake build`); + await vscode.commands.executeCommand("cmake.build"); + } else { + // No Cargo.toml and no CMakeLists.txt + // Check for package.json with engines.vscode + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (workspaceFolder) { + const fs = require("fs"); + const pkgPath = path.join(workspaceFolder, "package.json"); + if (fs.existsSync(pkgPath)) { + const pkg = require(pkgPath); + if (pkg.engines && pkg.engines.vscode) { + vscode.window.showInformationMessage("Multi-Build: Detected VS Code extension project. Running npm install, packaging, and installing..."); + const terminal = vscode.window.createTerminal({ name: "Multi-Build Extension Install" }); + terminal.show(); + terminal.sendText("npm install"); + vscode.window.showInformationMessage("Multi-Build: Running npm install in terminal..."); + await new Promise((resolve) => setTimeout(resolve, 15000)); + terminal.sendText("npm run package"); + vscode.window.showInformationMessage("Multi-Build: Packaging extension in terminal..."); + // Wait for the new .vsix file to be created/updated + const version = pkg.version; + const vsixName = `multi-build-${version}.vsix`; + const vsixPath = path.join(workspaceFolder, vsixName); + let prevMtime = 0; + if (fs.existsSync(vsixPath)) { + prevMtime = fs.statSync(vsixPath).mtime.getTime(); + } + const waitForVsix = async () => { + for (let i = 0; i < 30; ++i) { // up to ~15 seconds + if (fs.existsSync(vsixPath)) { + const mtime = fs.statSync(vsixPath).mtime.getTime(); + if (mtime > prevMtime) { + return true; + } + } + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + return false; + }; + const found = await waitForVsix(); + if (!found) { + vscode.window.showErrorMessage(`Multi-Build: ${vsixName} not found or not updated after packaging. Make sure packaging succeeded.`); + return; + } + vscode.window.showInformationMessage(`Multi-Build: Installing extension from VSIX...`); + await vscode.commands.executeCommand('workbench.extensions.installExtension', vscode.Uri.file(vsixPath)); + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + vscode.window.showInformationMessage(`Multi-Build: Installed and reloaded VS Code extension from ${vsixName}`); + return; + } + } + } + vscode.window.showErrorMessage(`${extensionName}: No Cargo.toml, CMakeLists.txt, or VS Code extension (package.json with engines.vscode) found in the workspace.`); + } + } catch (error) { + console.error(`${logTag} Error handling synced message:`, error); + vscode.window.showErrorMessage(`${extensionName}: Error handling synced message: ${error}`); + } +} + +// Add logic to run `cargo-e --json-all-targets` and list targets +async function listCargoETargets(manifestPath: string): Promise<{ label: string; description: string; detail: string } | null> { + try { + console.log(`${cargoLogTag} Listing Cargo-e targets for manifest: ${manifestPath}`); + const { exec } = await import("child_process"); + const output = await new Promise((resolve, reject) => { + exec( + `cargo-e --json-all-targets --manifest-path "${manifestPath}"`, + { cwd: path.dirname(manifestPath) }, + (error, stdout, stderr) => { + if (error) { + reject(stderr || error.message); + } else { + resolve(stdout); + } + } + ); + }); - console.log(`${logTag} CMake configure`); - await vscode.commands.executeCommand("cmake.configure"); + if (!output) { + console.warn(`${cargoLogTag} No output from cargo-e, skipping target listing.`); + return null; + } - // Wait a moment for CMake to finish up (or we get "already running" errors). - console.log(`${logTag} Waiting for CMake`); - await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log(`${cargoLogTag} Raw output from cargo-e:`, output); - console.log(`${logTag} CMake build`); - await vscode.commands.executeCommand("cmake.build"); + // Parse the JSON output + let targets: any[]; + try { + targets = JSON.parse(output); + console.log(`${cargoLogTag} Parsed JSON targets:`, targets); + if (!Array.isArray(targets) || targets.length === 0) { + vscode.window.showWarningMessage(`${cargoLogTag} No targets found in output.`); + return null; + } + } catch (error) { + vscode.window.showErrorMessage(`${cargoLogTag} Failed to parse JSON output: ${error}`); + return null; + } + + // Extract target display names + const targetOptions: { label: string; description: string; detail: string }[] = targets.map((target: any) => ({ + label: target.name || "unknown", + description: target.kind || "", + detail: target.manifest_path || "", + })); + // Sort targets alphabetically by label + targetOptions.sort((a, b) => a.label.localeCompare(b.label)); + console.log(`${cargoLogTag} Target options for Quick Pick:`, targetOptions); + + // Show the targets in a Quick Pick menu + // Add lifecycle logs and a delay for debugging + console.log(`${cargoLogTag} Showing Quick Pick menu.`); + try { + // Use showQuickPick and keep the menu open until user selects or cancels. + // Prevent auto-closing by awaiting the promise and not triggering any other UI. + const selectedTarget = await vscode.window.showQuickPick(targetOptions, { + placeHolder: "Select a target to execute", + ignoreFocusOut: true, // Keeps the picker open if focus is lost + }); + + console.log(`${cargoLogTag} Quick Pick menu dismissed.`); + + if (!selectedTarget) { + // Only show info if there were options but user cancelled + if (targetOptions.length > 0) { + vscode.window.showInformationMessage(`${cargoLogTag} No target selected.`); + } + console.log(`${cargoLogTag} Quick Pick cancelled by user.`); + return null; + } + + console.log(`${cargoLogTag} User selected target:`, selectedTarget); + return selectedTarget; + } catch (error) { + console.error(`${cargoLogTag} Error during Quick Pick:`, error); + vscode.window.showErrorMessage(`${cargoLogTag} Error during target selection: ${error}`); + return null; + } finally { + // Add a delay to observe the Quick Pick menu behavior + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } catch (error) { + vscode.window.showErrorMessage(`${cargoLogTag} Error listing targets: ${error}`); + console.error(`${cargoLogTag} Error details:`, error); + return null; + } } async function connectWebSocket() { @@ -365,6 +821,7 @@ async function connectWebSocket() { newSocket.on("open", () => { assert(newSocket, "WebSocket is not defined on open"); console.log(`${logTag} WebSocket connection opened`); + vscode.window.showInformationMessage(`${logTag} WebSocket connection opened`); sendMessage({ type: "hello" }); keepAlive = setInterval(() => sendMessage({ type: "keep-alive" }), keepAliveIntervalMillis); }); @@ -383,12 +840,61 @@ async function connectWebSocket() { if (message.type === "hello") { console.log(`${logTag} Hello back message received`); } else if (message.type === "ack") { - console.debug(`${logTag} Ack message received`); + //console.debug(`${logTag} Ack message received`); } else if (message.type === "error") { console.error(`${logTag} Error message received: ${message.message}`); } else if (message.type === "sync") { console.log(`${logTag} Sync message received:`, message.data); await handleSyncData(message.data); + } else if (message.type === "update-and-install") { + // Show notification when update/install message is received + vscode.window.showInformationMessage("Multi-Build: Received update/install command from room. Running update..."); + // Only run update/install if this is the multi-build repo + const pkg = require(path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '', "package.json")); + if (pkg.name === "multi-build") { + vscode.commands.executeCommand(updateAndInstallCommand); + } else { + vscode.window.showWarningMessage("Multi-Build: Ignored update/install command (not multi-build repo)"); + } + } else if (message.type === "refresh-all-windows") { + console.log(`${logTag} Received refresh-all-windows command.`); + + // Refresh the current window + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } else if (message.type === "cargo-e") { + const { manifestPath, target } = message.data; + if (!manifestPath) { + console.warn(`${cargoLogTag} No manifestPath provided in cargo-e message`); + vscode.window.showErrorMessage(`${cargoLogTag} No manifestPath provided in cargo-e message`); + return; + } + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceFolder || !manifestPath) { + vscode.window.showErrorMessage(`${cargoLogTag} Cannot run cargo-e: workspaceFolder or manifestPath is undefined.`); + return; + } + // const posixManifestPath = manifestPath.split(path.sep).join(path.posix.sep); + // const selectedDir = path.dirname(path.resolve(workspaceFolder, manifestPath)); + + // console.log(`${cargoLogTag} Preparing to run 'cargo-e' in ${selectedDir}`); + // const terminal = vscode.window.createTerminal({ + // name: target ? `${target}` : "Cargo Build", + // cwd: selectedDir, + // }); + // terminal.show(); + + // const cargoCommand = target + // ? `cargo-e --manifest-path "${posixManifestPath}" --target ${target}` + // : `cargo-e --manifest-path "${posixManifestPath}"`; + // console.log(`${cargoLogTag} Executing command: ${cargoCommand}`); + // terminal.sendText(cargoCommand); + if (workspaceFolder && manifestPath) { + handleCargoECommand(manifestPath, target, workspaceFolder); + } else { + vscode.window.showErrorMessage(`${cargoLogTag} Cannot run cargo-e: workspaceFolder or manifestPath is undefined.`); + console.error(`${cargoLogTag} Cannot run cargo-e: workspaceFolder or manifestPath is undefined.`); + } } else { throw new Error(`Unknown message type: ${message.type}`); } @@ -464,17 +970,109 @@ async function checkoutBranch( return false; } - if (repo.getBranch(branchName) !== undefined) { - console.debug(`${logTag} Branch '${branchName}' already exists`); - await repo.checkout(branchName); - await repo.pull(); - } else { - console.debug(`${logTag} Branch '${branchName}' does not exist, creating new branch`); - await repo.checkout(ref); - await repo.createBranch(branchName, true, ref); - await repo.setBranchUpstream(branchName, ref); + try { + if (repo.getBranch(branchName) !== undefined) { + console.debug(`${logTag} Branch '${branchName}' already exists`); + await repo.checkout(branchName); + await repo.pull(); + } else { + console.debug(`${logTag} Branch '${branchName}' does not exist, creating new branch`); + await repo.checkout(ref); + await repo.createBranch(branchName, true, ref); + await repo.setBranchUpstream(branchName, ref); + } + } catch (error) { + vscode.window.showErrorMessage( + `${extensionName}: Error checking out or creating branch '${branchName}': ${error}`, + ); + console.error(`${logTag} Error during branch checkout/create:`, error); + return false; } console.log(`${logTag} Checked out branch '${branchName}' in repository ${repoName}`); + vscode.window.showInformationMessage( + `${extensionName}: Checked out branch '${branchName}' from '${repoName}' (remote: ${remoteName})`, + ); return true; } + +async function handleCargoE(data: { manifestPath: string; target?: string }) { + const { manifestPath, target } = data; + + if (!manifestPath) { + console.warn(`${cargoLogTag} No manifestPath provided in cargo-e message`); + vscode.window.showErrorMessage(`${cargoLogTag} No manifestPath provided in cargo-e message`); + return; + } + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!workspaceFolder) { + console.error(`${cargoLogTag} Workspace folder is undefined. Cannot run cargo-e.`); + vscode.window.showErrorMessage(`${cargoLogTag} Cannot run cargo-e: workspaceFolder is undefined.`); + return; + } + + console.log(`${cargoLogTag} Resolved workspace folder: ${workspaceFolder}`); + + const posixManifestPath = manifestPath.split(path.sep).join(path.posix.sep); + const selectedDir = path.dirname(path.resolve(workspaceFolder, manifestPath)); + + console.log(`${cargoLogTag} Preparing to run 'cargo-e' in ${selectedDir}`); + const terminal = vscode.window.createTerminal({ + name: target ? `${target}` : "Cargo Build", + cwd: selectedDir, + }); + terminal.show(); + + const cargoCommand = target + ? `cargo-e --manifest-path "${posixManifestPath}" --target ${target}` + : `cargo-e --manifest-path "${posixManifestPath}"`; + console.log(`${cargoLogTag} Executing command: ${cargoCommand}`); + terminal.sendText(cargoCommand); +} + +function handleCargoECommand(selectedFilePath: string, targetName: string | undefined, workspaceFolder: string) { + try { + if (!selectedFilePath || !workspaceFolder) { + vscode.window.showErrorMessage(`${cargoLogTag} Cannot run cargo-e: missing manifest path or workspace folder.`); + return; + } + + const posixManifestPath = selectedFilePath.split(path.sep).join(path.posix.sep); + const selectedDir = path.dirname(path.resolve(workspaceFolder, selectedFilePath)); + + console.log(`${cargoLogTag} Preparing to run 'cargo-e' in ${selectedDir}`); + const terminal = vscode.window.createTerminal({ + name: targetName ? `${targetName}` : "Cargo Build", + cwd: selectedDir, + }); + terminal.show(); + + const cargoCommand = targetName + ? `cargo-e --manifest-path "${posixManifestPath}" --target ${targetName}` + : `cargo-e --manifest-path "${posixManifestPath}"`; + console.log(`${cargoLogTag} Executing command: ${cargoCommand}`); + terminal.sendText(cargoCommand); + } catch (error) { + vscode.window.showErrorMessage(`${cargoLogTag} Error running cargo-e: ${error}`); + console.error(`${cargoLogTag} Error running cargo-e:`, error); + } + + + // // Run cargo-e with the selected or received manifestPath and target + // // Normalize to POSIX path for --manifest-path argument + // const posixManifestPath = selectedFilePath.split(path.sep).join(path.posix.sep); + // const selectedDir = path.dirname(path.resolve(workspaceFolder, selectedFilePath)); + // console.log(`${cargoLogTag} Preparing to run 'cargo-e' in ${selectedDir}`); + + // const terminal = vscode.window.createTerminal({ + // name: targetName ? `${targetName}` : "Cargo Build", + // cwd: selectedDir, + // }); + // terminal.show(); + + // const cargoCommand = targetName ? `cargo-e --manifest-path "${posixManifestPath}" --target ${targetName}` : `cargo-e --manifest-path "${posixManifestPath}"`; + // console.log(`${cargoLogTag} Executing command: ${cargoCommand}`); + // terminal.sendText(cargoCommand); +} +