diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index 48f52283..281c7e3c 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -389,7 +389,7 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean // Build + reload is async; fire-and-forget from the sync handler. void (async () => { - const { exec } = await import("child_process"); + const { exec } = await import("child_process"); const buildResult = await new Promise<{ ok: boolean; error?: string }>((resolve) => { exec("bun run build", { cwd: amicodePath, timeout: 120_000 }, (err, _stdout, stderr) => { if (err) { @@ -680,6 +680,123 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean return true; } + // Devcontainer VSIX build: builds both repos using the pnpm-based workflow + // and emits a .vsix to the configured output path for manual installation. + if (msg.kind === "dev-tools-build-vsix") { + const opencodePath = typeof (msg as { opencodePath?: unknown }).opencodePath === "string" + ? (msg as unknown as { opencodePath: string }).opencodePath.trim().replace(/^~/, os.homedir()) + : ""; + const amicodePath = typeof (msg as { amicodePath?: unknown }).amicodePath === "string" + ? (msg as unknown as { amicodePath: string }).amicodePath.trim().replace(/^~/, os.homedir()) + : ""; + const outputPath = typeof (msg as { outputPath?: unknown }).outputPath === "string" + ? (msg as unknown as { outputPath: string }).outputPath.trim().replace(/^~/, os.homedir()) + : ""; + + if (!opencodePath || !amicodePath || !outputPath) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: "All three paths (opencode, amicode, output) must be set", + }); + return true; + } + + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "building", + }); + + void (async () => { + const { exec, execFile } = await import("child_process"); + const run = (cmd: string, cwd: string): Promise<{ ok: boolean; error?: string; stdout?: string }> => + new Promise((resolve) => { + exec(cmd, { cwd, timeout: 300_000, env: { ...process.env, NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=4096", AMICODE_OPENCODE_SRC: opencodePath } }, + (err, stdout, stderr) => { + if (err) resolve({ ok: false, error: stderr?.trim() || err.message }); + else resolve({ ok: true, stdout: stdout?.trim() }); + }); + }); + + try { + // ── Step 1: Install opencode deps (bun-based repo) ── + const installOc = await run("bun install", opencodePath); + if (!installOc.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `bun install (opencode) failed: ${installOc.error?.slice(0, 200)}`, + }); + return; + } + + // ── Step 2: Install amicode deps (pnpm-based repo) ── + const installAc = await run("pnpm install", amicodePath); + if (!installAc.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `pnpm install (amicode) failed: ${installAc.error?.slice(0, 200)}`, + }); + return; + } + + // ── Step 3: Build extension bundle (esbuild) ── + const buildExt = await run("pnpm --filter amicode build", amicodePath); + if (!buildExt.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `pnpm build failed: ${buildExt.error?.slice(0, 200)}`, + }); + return; + } + + // ── Step 4: Build opencode binary + vendor it ── + const buildOc = await run("pnpm --filter amicode opencode:build", amicodePath); + if (!buildOc.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `opencode:build failed: ${buildOc.error?.slice(0, 200)}`, + }); + return; + } + + // ── Step 5: Package vsix ── + const extDir = path.join(amicodePath, "packages", "extension"); + const vsixName = `amicode-${Date.now()}.vsix`; + const vsixDest = path.join(outputPath, vsixName); + + fs.mkdirSync(outputPath, { recursive: true }); + + // Use execFile (no shell) to avoid command injection via outputPath + const packResult = await new Promise<{ ok: boolean; error?: string }>((resolve) => { + execFile("pnpm", ["exec", "vsce", "package", "--no-dependencies", "--allow-missing-repository", "-o", vsixDest], + { cwd: extDir, timeout: 300_000, env: { ...process.env, NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=4096", AMICODE_OPENCODE_SRC: opencodePath } }, + (err, _stdout, stderr) => { + if (err) resolve({ ok: false, error: stderr?.trim() || err.message }); + else resolve({ ok: true }); + }); + }); + if (!packResult.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `vsce package failed: ${packResult.error?.slice(0, 200)}`, + }); + return; + } + + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "done", vsixPath: vsixDest, + }); + } catch (e: unknown) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-build-vsix-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: e instanceof Error ? e.message : "Unknown error", + }); + } + })(); + + return true; + } + // #351 reverse: Work Column Device Inspector → extension refresh if (msg.kind === "device:refresh" && typeof (msg as { device?: unknown }).device === "string") { void vscode.commands.executeCommand("amicode.device.refresh"); diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index c4c41be1..a34adcfb 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -378,7 +378,7 @@ export class ChatPanel { replyClipboardImage(d.nonce); return; } - if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "app-ready")) { + if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "app-ready")) { vscode.postMessage(d); } return; @@ -387,7 +387,7 @@ export class ChatPanel { // (webview-internal origin, never the opencode origin). Forward only // our own envelopes, pinned to the opencode origin. #351 adds // run:*/device:* envelopes for the Work Column inspector tabs. - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, ${origin}); } @@ -567,12 +567,12 @@ export class ChatPanel { replyClipboardImage(d.nonce); return; } - if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "app-ready")) { + if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "app-ready")) { vscode.postMessage(d); } return; } - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, origin); }