Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion packages/extension/src/chat_bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down
8 changes: 4 additions & 4 deletions packages/extension/src/chat_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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});
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading