From 9d8b0551aaba045b4c49f78c7339da447dd5afb6 Mon Sep 17 00:00:00 2001 From: Nabi Bukhsh Date: Tue, 20 Oct 2026 20:48:08 +0500 Subject: [PATCH 1/3] Add code-opencode VS Code extension support (#37) Add support for installing the OpenCode VS Code extension (sst-dev.opencode) in code-server, matching the existing code-claude, code-codex, and code-vibe options. Closes #37. --- CHANGELOG.md | 2 ++ agent-vm.setup.sh | 13 ++++++++----- internal/cli/setup.go | 23 ++++++++++++++--------- internal/cli/setup_test.go | 14 ++++++++++++++ tests/setup-script.sh | 24 ++++++++++++------------ www/src/i18n/en.ts | 2 +- www/src/i18n/fr.ts | 2 +- 7 files changed, 52 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d4faae5..2185b144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- `agent-vm setup`: Added `code-opencode` preinstall option to install the OpenCode VS Code extension (`sst-dev.opencode`) into code-server (#37). + ## 0.3.0 agent-vm 0.3 is one Go binary with Lima built in. The diff --git a/agent-vm.setup.sh b/agent-vm.setup.sh index 5a78a15b..32ec57af 100644 --- a/agent-vm.setup.sh +++ b/agent-vm.setup.sh @@ -49,11 +49,12 @@ INSTALL_PI="${AGENT_VM_INSTALL_PI:-0}" INSTALL_CODE_SERVER="${AGENT_VM_INSTALL_CODE_SERVER:-0}" # Each agent's code-server extension, which brings its own copy of the agent. INSTALL_CODE_CLAUDE="${AGENT_VM_INSTALL_CODE_CLAUDE:-0}" +INSTALL_CODE_OPENCODE="${AGENT_VM_INSTALL_CODE_OPENCODE:-0}" INSTALL_CODE_CODEX="${AGENT_VM_INSTALL_CODE_CODEX:-0}" INSTALL_CODE_VIBE="${AGENT_VM_INSTALL_CODE_VIBE:-0}" # An extension needs the editor: `agent-vm setup` says so already, a run of # this script on its own may not. -if [[ "$INSTALL_CODE_CLAUDE$INSTALL_CODE_CODEX$INSTALL_CODE_VIBE" == *1* ]]; then +if [[ "$INSTALL_CODE_CLAUDE$INSTALL_CODE_OPENCODE$INSTALL_CODE_CODEX$INSTALL_CODE_VIBE" == *1* ]]; then INSTALL_CODE_SERVER=1 fi # MCP servers wired into every installed agent's config (see `agent-vm setup --help`). @@ -62,8 +63,9 @@ INSTALL_MCP_PLAYWRIGHT="${AGENT_VM_INSTALL_MCP_PLAYWRIGHT:-0}" # An agent there in either form: its command line and its extension read the # same config, which is written for both. -HAS_CLAUDE=0 HAS_CODEX=0 HAS_VIBE=0 +HAS_CLAUDE=0 HAS_OPENCODE=0 HAS_CODEX=0 HAS_VIBE=0 [[ "$INSTALL_CLAUDE$INSTALL_CODE_CLAUDE" == *1* ]] && HAS_CLAUDE=1 +[[ "$INSTALL_OPENCODE$INSTALL_CODE_OPENCODE" == *1* ]] && HAS_OPENCODE=1 [[ "$INSTALL_CODEX$INSTALL_CODE_CODEX" == *1* ]] && HAS_CODEX=1 [[ "$INSTALL_VIBE$INSTALL_CODE_VIBE" == *1* ]] && HAS_VIBE=1 @@ -266,7 +268,7 @@ if [[ "$HAS_CLAUDE" == "1" ]]; then JSON fi -if [[ "$INSTALL_OPENCODE" == "1" ]]; then +if [[ "$INSTALL_OPENCODE" == "1" || "$INSTALL_CODE_OPENCODE" == "1" ]]; then echo "Installing OpenCode..." curl -fsSL https://opencode.ai/install | bash echo 'export PATH=$HOME/.opencode/bin:$PATH' >> ~/.zshenv @@ -425,6 +427,7 @@ if [[ "$INSTALL_CODE_SERVER" == "1" ]]; then curl -fsSL https://code-server.dev/install.sh | sh extensions=() [[ "$INSTALL_CODE_CLAUDE" == "1" ]] && extensions+=(--install-extension anthropic.claude-code) + [[ "$INSTALL_CODE_OPENCODE" == "1" ]] && extensions+=(--install-extension sst-dev.opencode) [[ "$INSTALL_CODE_CODEX" == "1" ]] && extensions+=(--install-extension openai.chatgpt) [[ "$INSTALL_CODE_VIBE" == "1" ]] && extensions+=(--install-extension mistralai.mistral-vibe-code) if [[ ${#extensions[@]} -gt 0 ]]; then @@ -537,7 +540,7 @@ configure_mcp() { fi fi - if [[ "$INSTALL_OPENCODE" == "1" ]]; then + if [[ "$HAS_OPENCODE" == "1" ]]; then echo "Configuring $name MCP server for OpenCode..." mkdir -p "$HOME/.config/opencode" local config="$HOME/.config/opencode/opencode.json" @@ -587,7 +590,7 @@ configure_mcp() { # True when at least one agent is installed: nothing to configure otherwise, # and no reason to print a "skipping" notice either. any_agent_installed() { - [[ "$HAS_CLAUDE" == "1" || "$INSTALL_OPENCODE" == "1" \ + [[ "$HAS_CLAUDE" == "1" || "$HAS_OPENCODE" == "1" \ || "$HAS_CODEX" == "1" || "$HAS_VIBE" == "1" ]] } diff --git a/internal/cli/setup.go b/internal/cli/setup.go index 0709e07f..3f4b8b32 100644 --- a/internal/cli/setup.go +++ b/internal/cli/setup.go @@ -49,13 +49,13 @@ Options: python, node, ruby, rust, golang, docker, chromium, gh, claude, opencode, codex, vibe, pi, mcp-chrome, mcp-playwright, code-server, code-claude, - code-codex, code-vibe + code-opencode, code-codex, code-vibe 'code-server' is VS Code in the browser, for 'agent-vm code', with GitHub Copilot turned off. - 'code-claude', 'code-codex' and 'code-vibe' add that + 'code-claude', 'code-opencode', 'code-codex' and 'code-vibe' add that agent's extension, and code-server with it. Each extension brings its own copy of the agent: 'claude', - 'codex' and 'vibe' are the command-line ones, for + 'opencode', 'codex' and 'vibe' are the command-line ones, for 'agent-vm claude' and the others. Selecting codex or pi also installs node (npm). So does mcp-chrome when chromium and an agent are selected @@ -81,7 +81,7 @@ Options: // The software of the base, as agent-vm.setup.sh names it // (AGENT_VM_INSTALL_). var software = []string{"python", "node", "ruby", "rust", "golang", "docker", "chromium", "gh", "claude", "opencode", "codex", "vibe", "pi", - "code-server", "code-claude", "code-codex", "code-vibe", "mcp-chrome", "mcp-playwright"} + "code-server", "code-claude", "code-opencode", "code-codex", "code-vibe", "mcp-chrome", "mcp-playwright"} // The default set. Opt-in: Ruby, Rust, Go, Pi (released several times a // week), Playwright MCP (a second browser-driving server, whose tools cost @@ -108,7 +108,7 @@ func (in install) nodeNeededBy() string { return "Codex CLI requires npm" case in["pi"]: return "Pi requires npm" - case in["chromium"] && in["mcp-chrome"] && (in["claude"] || in["opencode"] || in["vibe"] || in["code-claude"] || in["code-codex"] || in["code-vibe"]): + case in["chromium"] && in["mcp-chrome"] && (in["claude"] || in["opencode"] || in["vibe"] || in["code-claude"] || in["code-opencode"] || in["code-codex"] || in["code-vibe"]): return "Chrome DevTools MCP uses npx" } return "" @@ -192,7 +192,7 @@ func (e *app) setupCmd(ctx context.Context, args []string) int { case f == "default": in.set(defaultSoftware...) case f == "none": - case f == "code-claude" || f == "code-codex" || f == "code-vibe": + case f == "code-claude" || f == "code-opencode" || f == "code-codex" || f == "code-vibe": // An extension needs the editor. in.set(f, "code-server") case contains(software, f): @@ -343,7 +343,12 @@ func (e *app) askCodeExtensions(in install) { return } var names []string - for _, a := range []struct{ key, name string }{{"claude", "Claude Code"}, {"codex", "Codex"}, {"vibe", "Mistral Vibe"}} { + for _, a := range []struct{ key, name string }{ + {"claude", "Claude Code"}, + {"opencode", "OpenCode"}, + {"codex", "Codex"}, + {"vibe", "Mistral Vibe"}, + } { if in[a.key] { names = append(names, a.name) } @@ -356,9 +361,9 @@ func (e *app) askCodeExtensions(in install) { if choice == 3 { return } - in["code-claude"], in["code-codex"], in["code-vibe"] = in["claude"], in["codex"], in["vibe"] + in["code-claude"], in["code-opencode"], in["code-codex"], in["code-vibe"] = in["claude"], in["opencode"], in["codex"], in["vibe"] if choice == 2 { - in["claude"], in["codex"], in["vibe"] = false, false, false + in["claude"], in["opencode"], in["codex"], in["vibe"] = false, false, false, false } } diff --git a/internal/cli/setup_test.go b/internal/cli/setup_test.go index eb18cc40..de91c834 100644 --- a/internal/cli/setup_test.go +++ b/internal/cli/setup_test.go @@ -80,6 +80,7 @@ func TestSetupPreinstall(t *testing.T) { {[]string{"--preinstall=none"}, ""}, {[]string{"--preinstall=all"}, strings.Join(software, ",")}, {[]string{"--preinstall=python, docker,claude"}, "python,docker,claude"}, + {[]string{"--preinstall=code-opencode"}, "code-server,code-opencode"}, {[]string{"--preinstall=code-vibe"}, "code-server,code-vibe"}, // Codex needs npm: node goes in, and it is said. {[]string{"--preinstall=codex"}, "node,codex"}, @@ -164,6 +165,19 @@ func TestSetupWizard(t *testing.T) { strings.Contains(te.out(), " agent-vm claude\n") { t.Errorf("output: %s", te.out()) } + + // OpenCode as extension only + te, stdin = wizardRun(t, + "n", // the default set + "n", "y", "n", "n", "", // Claude, OpenCode, Codex, Vibe, Pi + "y", "2", // code-server, extension only + "n", "n", "n", // Docker, Chromium, gh + "n", "n", "", "", "", // Python, Node, Ruby, Rust, Go + "", // resources + ) + if got := on(exportsOf(t, stdin)); got != "code-server,code-opencode" { + t.Errorf("opencode extension: %s", got) + } } // code-server makes the default memory 4, but --memory wins. No terminal: diff --git a/tests/setup-script.sh b/tests/setup-script.sh index 719eb354..f84d010a 100755 --- a/tests/setup-script.sh +++ b/tests/setup-script.sh @@ -109,12 +109,12 @@ check "sshfs wrapper: an older sshfs gets the arguments unchanged" \ # An agent is there as a command, as an extension, or both: either way its # config (managed settings, MCP servers) is written. The lines setup derives # it with, run as setup does. -has_block="$(awk '/^HAS_CLAUDE=0 HAS_CODEX=0 HAS_VIBE=0$/,/HAS_VIBE=1$/' "$SETUP_SH")" +has_block="$(awk '/^HAS_CLAUDE=0 HAS_OPENCODE=0 HAS_CODEX=0 HAS_VIBE=0$/,/HAS_VIBE=1$/' "$SETUP_SH")" # shellcheck disable=SC2034 # read by the eval'd block -has() { ( INSTALL_CLAUDE="$1" INSTALL_CODE_CLAUDE="$2" INSTALL_CODEX="$3" INSTALL_CODE_CODEX="$4" INSTALL_VIBE="$5" INSTALL_CODE_VIBE="$6" - eval "$has_block"; echo "$HAS_CLAUDE$HAS_CODEX$HAS_VIBE" ); } +has() { ( INSTALL_CLAUDE="$1" INSTALL_CODE_CLAUDE="$2" INSTALL_OPENCODE="$3" INSTALL_CODE_OPENCODE="$4" INSTALL_CODEX="$5" INSTALL_CODE_CODEX="$6" INSTALL_VIBE="$7" INSTALL_CODE_VIBE="$8" + eval "$has_block"; echo "$HAS_CLAUDE$HAS_OPENCODE$HAS_CODEX$HAS_VIBE" ); } check "setup: the agents' config, for a command or an extension" \ - "$(has 1 0 0 0 0 0) $(has 0 1 0 0 0 0) $(has 0 0 0 1 0 0) $(has 0 0 0 0 1 1) $(has 0 0 0 0 0 0)" "100 100 010 001 000" + "$(has 1 0 0 0 0 0 0 0) $(has 0 1 0 0 0 0 0 0) $(has 0 0 1 0 0 0 0 0) $(has 0 0 0 1 0 0 0 0) $(has 0 0 0 0 1 0 0 0) $(has 0 0 0 0 0 1 0 0) $(has 0 0 0 0 0 0 1 1) $(has 0 0 0 0 0 0 0 0)" "1000 1000 0100 0100 0010 0010 0001 0000" # ============================================================================= section "MCP config writer" @@ -128,7 +128,7 @@ else ( HOME="$MCPHOME" eval "$(awk '/^configure_mcp\(\) \{/,/^\}/' "$SETUP_SH")" - HAS_CLAUDE=1 INSTALL_OPENCODE=1 HAS_VIBE=1 HAS_CODEX=1 + HAS_CLAUDE=1 HAS_OPENCODE=1 HAS_VIBE=1 HAS_CODEX=1 for _ in 1 2; do # twice: the writer must be idempotent configure_mcp chrome-devtools npx -y chrome-devtools-mcp@latest --headless=true configure_mcp playwright env PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npx -y @playwright/mcp@latest @@ -253,7 +253,7 @@ else cs_block="$(awk '/^toml_prepend\(\) \{/,/^\}/' "$SETUP_SH") $(awk '/^if \[\[ "\$INSTALL_CODE_SERVER" == "1" \]\]; then/,/^fi$/' "$SETUP_SH") $(awk '/^configure_mcp\(\) \{/,/^\}/' "$SETUP_SH")" - # : the extensions asked for. + # : the extensions asked for. # Under set -e, as the script runs: a failing step must show here. Twice: # what it writes must not pile up when it runs again in the same VM. The # first run's state is kept in /first. @@ -261,8 +261,8 @@ $(awk '/^configure_mcp\(\) \{/,/^\}/' "$SETUP_SH")" run_cs_block() { mkdir -p "$1/.config/code-server"; echo "password: from-the-base" > "$1/.config/code-server/config.yaml" ( set -e - HOME="$1"; INSTALL_CODE_SERVER=1 INSTALL_CODE_CLAUDE="$2" INSTALL_CODE_CODEX="$3" INSTALL_CODE_VIBE="$4" - HAS_CLAUDE="$2" HAS_CODEX="$3" HAS_VIBE="$4" INSTALL_OPENCODE=0 + HOME="$1"; INSTALL_CODE_SERVER=1 INSTALL_CODE_CLAUDE="$2" INSTALL_CODE_OPENCODE="$3" INSTALL_CODE_CODEX="$4" INSTALL_CODE_VIBE="$5" + HAS_CLAUDE="$2" HAS_OPENCODE="$3" HAS_CODEX="$4" HAS_VIBE="$5" curl() { echo "curl $*" >> "$HOME/calls.log"; } code-server() { echo "code-server $*" >> "$HOME/calls.log"; } code_server_schemas() { echo "schemas $*" >> "$HOME/calls.log"; mkdir -p "$(dirname "$2")"; echo '{"json.schemas":[]}' > "$2"; } @@ -275,7 +275,7 @@ $(awk '/^configure_mcp\(\) \{/,/^\}/' "$SETUP_SH")" echo 'done' > "$HOME/finished" ) >/dev/null 2>&1 } CSB="$SB/cs-bare"; mkdir -p "$CSB" - run_cs_block "$CSB" 0 0 0 + run_cs_block "$CSB" 0 0 0 0 check "code-server alone: no extension installed" "$(grep -c '^code-server' "$CSB/calls.log")" "0" check "code-server alone: Copilot off" \ "$(jq -r '."chat.disableAIFeatures"' "$CSB/.local/share/code-server/User/settings.json")" "true" @@ -297,14 +297,14 @@ $(awk '/^configure_mcp\(\) \{/,/^\}/' "$SETUP_SH")" check "code-server alone: runs to the end under set -e" "$(cat "$CSB/finished" 2>/dev/null)" "done" CSA="$SB/cs-all"; mkdir -p "$CSA" - run_cs_block "$CSA" 1 1 1 + run_cs_block "$CSA" 1 1 1 1 check "with the extensions: runs to the end under set -e" "$(cat "$CSA/finished" 2>/dev/null)" "done" check "codex: written on a fresh home" "$(head -n 2 "$CSA/first/.codex" 2>/dev/null | sort | tr '\n' ' ')" \ 'approval_policy = "never" sandbox_mode = "danger-full-access" ' check "vibe: written on a fresh home" "$(cat "$CSA/first/.vibe" 2>/dev/null)" 'default_agent = "auto-approve"' - check "the three extensions, in one call" \ + check "the four extensions, in one call" \ "$(grep '^code-server' "$CSA/calls.log" | head -n 1)" \ - "code-server --install-extension anthropic.claude-code --install-extension openai.chatgpt --install-extension mistralai.mistral-vibe-code" + "code-server --install-extension anthropic.claude-code --install-extension sst-dev.opencode --install-extension openai.chatgpt --install-extension mistralai.mistral-vibe-code" # The permission keys are machine-scoped: code-server reads them from the # machine settings only. check "claude: bypass mode allowed and picked, in the machine settings" \ diff --git a/www/src/i18n/en.ts b/www/src/i18n/en.ts index cd7f2dc7..f5f6602c 100644 --- a/www/src/i18n/en.ts +++ b/www/src/i18n/en.ts @@ -669,7 +669,7 @@ export const en = { ['MCP', 'Chrome DevTools MCP, wired into every installed agent but Pi (no MCP support)', 'mcp-chrome', 'yes'], ['MCP', 'Playwright MCP, reusing the same Chromium', 'mcp-playwright', 'no'], ['Editor', 'code-server (VS Code in the browser, for `agent-vm code`), dark theme, GitHub Copilot off, no telemetry', 'code-server', 'no'], - ['Editor', 'The Claude Code, Codex or Mistral Vibe extension, each starting with no permission prompts, with code-server', 'code-claude, code-codex, code-vibe', 'no'], + ['Editor', 'The Claude Code, OpenCode, Codex or Mistral Vibe extension, each starting with no permission prompts, with code-server', 'code-claude, code-opencode, code-codex, code-vibe', 'no'], ], contentsCode: 'agent-vm setup --preinstall=default # no prompts\nagent-vm setup --preinstall=default,rust # plus Rust\nagent-vm setup --preinstall=python,docker,claude # minimal Claude setup\nagent-vm setup --preinstall=default,code-claude # plus the editor\nagent-vm setup --disk 50 --memory 16 --cpus 8 # a bigger template', diff --git a/www/src/i18n/fr.ts b/www/src/i18n/fr.ts index 3c46f794..a121fdf7 100644 --- a/www/src/i18n/fr.ts +++ b/www/src/i18n/fr.ts @@ -671,7 +671,7 @@ export const fr: Dictionary = { ['MCP', 'Chrome DevTools MCP, câblé dans chaque agent installé sauf Pi (pas de support MCP)', 'mcp-chrome', 'oui'], ['MCP', 'Playwright MCP, réutilisant le même Chromium', 'mcp-playwright', 'non'], ['Éditeur', 'code-server (VS Code dans le navigateur, pour `agent-vm code`), thème sombre, GitHub Copilot désactivé, sans télémétrie', 'code-server', 'non'], - ['Éditeur', 'L\'extension Claude Code, Codex ou Mistral Vibe, chacune démarrant sans demande de permission, avec code-server', 'code-claude, code-codex, code-vibe', 'non'], + ['Éditeur', 'L\'extension Claude Code, OpenCode, Codex ou Mistral Vibe, chacune démarrant sans demande de permission, avec code-server', 'code-claude, code-opencode, code-codex, code-vibe', 'non'], ], contentsCode: 'agent-vm setup --preinstall=default # sans question\nagent-vm setup --preinstall=default,rust # avec Rust en plus\nagent-vm setup --preinstall=python,docker,claude # Claude seul, minimal\nagent-vm setup --preinstall=default,code-claude # avec l\'éditeur\nagent-vm setup --disk 50 --memory 16 --cpus 8 # une image plus grosse', From 793e7b510afe2f802b8e75a821adce18fee71511 Mon Sep 17 00:00:00 2001 From: Nabi Bukhsh Date: Tue, 20 Oct 2026 21:06:00 +0500 Subject: [PATCH 2/3] Clarify OpenCode CLI requirement for extension-only setup Keep OpenCode CLI selected when extension is chosen, since sst-dev.opencode requires the CLI binary. Update setup help, wizard choice prompt, tests, and documentation accordingly. --- internal/cli/setup.go | 18 +++++++++++++----- internal/cli/setup_test.go | 8 +++++--- tests/setup-script.sh | 1 + www/src/i18n/en.ts | 2 +- www/src/i18n/fr.ts | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/cli/setup.go b/internal/cli/setup.go index 3f4b8b32..07a23003 100644 --- a/internal/cli/setup.go +++ b/internal/cli/setup.go @@ -55,8 +55,9 @@ Options: 'code-claude', 'code-opencode', 'code-codex' and 'code-vibe' add that agent's extension, and code-server with it. Each extension brings its own copy of the agent: 'claude', - 'opencode', 'codex' and 'vibe' are the command-line ones, for - 'agent-vm claude' and the others. + 'codex' and 'vibe' are the command-line ones, for + 'agent-vm claude' and the others (OpenCode's extension + requires its command-line tool, which is installed with it). Selecting codex or pi also installs node (npm). So does mcp-chrome when chromium and an agent are selected (npx). mcp-playwright does not: list node yourself. @@ -192,9 +193,12 @@ func (e *app) setupCmd(ctx context.Context, args []string) int { case f == "default": in.set(defaultSoftware...) case f == "none": - case f == "code-claude" || f == "code-opencode" || f == "code-codex" || f == "code-vibe": + case f == "code-claude" || f == "code-codex" || f == "code-vibe": // An extension needs the editor. in.set(f, "code-server") + case f == "code-opencode": + // OpenCode's extension requires both the editor and the OpenCode CLI. + in.set(f, "code-server", "opencode") case contains(software, f): in.set(f) default: @@ -357,13 +361,17 @@ func (e *app) askCodeExtensions(in install) { return } fmt.Fprintf(e.io.Stderr, " %s in the editor:\n", strings.Join(names, ", ")) - choice := e.ui.AskChoice("Pick one", 1, "Extension and command line (agent-vm claude...)", "Extension only", "Command line only") + promptExtOnly := "Extension only" + if in["opencode"] { + promptExtOnly = "Extension only (OpenCode keeps the CLI, required by its extension)" + } + choice := e.ui.AskChoice("Pick one", 1, "Extension and command line (agent-vm claude...)", promptExtOnly, "Command line only") if choice == 3 { return } in["code-claude"], in["code-opencode"], in["code-codex"], in["code-vibe"] = in["claude"], in["opencode"], in["codex"], in["vibe"] if choice == 2 { - in["claude"], in["opencode"], in["codex"], in["vibe"] = false, false, false, false + in["claude"], in["codex"], in["vibe"] = false, false, false } } diff --git a/internal/cli/setup_test.go b/internal/cli/setup_test.go index de91c834..193d3a2b 100644 --- a/internal/cli/setup_test.go +++ b/internal/cli/setup_test.go @@ -70,6 +70,7 @@ func on(m map[string]string) string { return strings.Join(out, ",") } +// TestSetupPreinstall verifies --preinstall flag combinations. func TestSetupPreinstall(t *testing.T) { for _, c := range []struct { args []string @@ -80,7 +81,7 @@ func TestSetupPreinstall(t *testing.T) { {[]string{"--preinstall=none"}, ""}, {[]string{"--preinstall=all"}, strings.Join(software, ",")}, {[]string{"--preinstall=python, docker,claude"}, "python,docker,claude"}, - {[]string{"--preinstall=code-opencode"}, "code-server,code-opencode"}, + {[]string{"--preinstall=code-opencode"}, "opencode,code-server,code-opencode"}, {[]string{"--preinstall=code-vibe"}, "code-server,code-vibe"}, // Codex needs npm: node goes in, and it is said. {[]string{"--preinstall=codex"}, "node,codex"}, @@ -133,6 +134,7 @@ func wizardRun(t *testing.T, answers ...string) (*testEnv, []string) { return te, stdin } +// TestSetupWizard verifies interactive setup wizard choices and defaults. func TestSetupWizard(t *testing.T) { // Enter everywhere: the default set and resources. te, stdin := wizardRun(t) @@ -166,7 +168,7 @@ func TestSetupWizard(t *testing.T) { t.Errorf("output: %s", te.out()) } - // OpenCode as extension only + // OpenCode as extension only (keeps the required OpenCode CLI) te, stdin = wizardRun(t, "n", // the default set "n", "y", "n", "n", "", // Claude, OpenCode, Codex, Vibe, Pi @@ -175,7 +177,7 @@ func TestSetupWizard(t *testing.T) { "n", "n", "", "", "", // Python, Node, Ruby, Rust, Go "", // resources ) - if got := on(exportsOf(t, stdin)); got != "code-server,code-opencode" { + if got := on(exportsOf(t, stdin)); got != "opencode,code-server,code-opencode" { t.Errorf("opencode extension: %s", got) } } diff --git a/tests/setup-script.sh b/tests/setup-script.sh index f84d010a..c6b3420c 100755 --- a/tests/setup-script.sh +++ b/tests/setup-script.sh @@ -110,6 +110,7 @@ check "sshfs wrapper: an older sshfs gets the arguments unchanged" \ # config (managed settings, MCP servers) is written. The lines setup derives # it with, run as setup does. has_block="$(awk '/^HAS_CLAUDE=0 HAS_OPENCODE=0 HAS_CODEX=0 HAS_VIBE=0$/,/HAS_VIBE=1$/' "$SETUP_SH")" +# has derives the HAS_* flags for the given set of agent install flags. # shellcheck disable=SC2034 # read by the eval'd block has() { ( INSTALL_CLAUDE="$1" INSTALL_CODE_CLAUDE="$2" INSTALL_OPENCODE="$3" INSTALL_CODE_OPENCODE="$4" INSTALL_CODEX="$5" INSTALL_CODE_CODEX="$6" INSTALL_VIBE="$7" INSTALL_CODE_VIBE="$8" eval "$has_block"; echo "$HAS_CLAUDE$HAS_OPENCODE$HAS_CODEX$HAS_VIBE" ); } diff --git a/www/src/i18n/en.ts b/www/src/i18n/en.ts index f5f6602c..7e603e1e 100644 --- a/www/src/i18n/en.ts +++ b/www/src/i18n/en.ts @@ -674,7 +674,7 @@ export const en = { contentsCode: 'agent-vm setup --preinstall=default # no prompts\nagent-vm setup --preinstall=default,rust # plus Rust\nagent-vm setup --preinstall=python,docker,claude # minimal Claude setup\nagent-vm setup --preinstall=default,code-claude # plus the editor\nagent-vm setup --disk 50 --memory 16 --cpus 8 # a bigger template', contentsNote: - '`codex` and `pi` pull in `node`, and so does `mcp-chrome` with `chromium` and an agent. Both MCP servers are skipped without `node` and `chromium`. Each editor extension ships its own copy of its agent (200 to 600 MB): the wizard asks whether to keep the command-line one too. Without it, `agent-vm claude` says it is not installed. This is only what the template ships: the agent is root in the VM and reaches the internet, so it installs whatever else it needs.', + '`codex` and `pi` pull in `node`, and so does `mcp-chrome` with `chromium` and an agent. Both MCP servers are skipped without `node` and `chromium`. Each editor extension ships its own copy of its agent (200 to 600 MB): the wizard asks whether to keep the command-line one too (OpenCode’s extension requires the CLI, which is always installed with it). Without it, `agent-vm claude` says it is not installed. This is only what the template ships: the agent is root in the VM and reaches the internet, so it installs whatever else it needs.', dockerTitle: 'Why not Docker', dockerLede: 'On Linux, containers share the host kernel, so a compromised dependency that finds a kernel bug is on your host. On macOS, Docker Desktop already runs a VM, which narrows the gap, but it is one VM shared by every container rather than one per project. A VM brings its own kernel, and root inside it still has the hypervisor between it and your machine. The practical differences matter as much as the security one.', diff --git a/www/src/i18n/fr.ts b/www/src/i18n/fr.ts index a121fdf7..18ecc773 100644 --- a/www/src/i18n/fr.ts +++ b/www/src/i18n/fr.ts @@ -676,7 +676,7 @@ export const fr: Dictionary = { contentsCode: 'agent-vm setup --preinstall=default # sans question\nagent-vm setup --preinstall=default,rust # avec Rust en plus\nagent-vm setup --preinstall=python,docker,claude # Claude seul, minimal\nagent-vm setup --preinstall=default,code-claude # avec l\'éditeur\nagent-vm setup --disk 50 --memory 16 --cpus 8 # une image plus grosse', contentsNote: - '`codex` et `pi` entraînent `node`, tout comme `mcp-chrome` avec `chromium` et un agent. Les deux serveurs MCP sont ignorés sans `node` et `chromium`. Chaque extension de l\'éditeur apporte sa propre copie de son agent (200 à 600 Mo) : l\'assistant demande s\'il faut garder aussi celle en ligne de commande. Sans elle, `agent-vm claude` dit qu\'il n\'est pas installé. Ce n\'est que ce que l\'image contient au départ : l\'agent est root dans la VM et atteint Internet, il installe lui-même ce qui lui manque.', + '`codex` et `pi` entraînent `node`, tout comme `mcp-chrome` avec `chromium` et un agent. Les deux serveurs MCP sont ignorés sans `node` et `chromium`. Chaque extension de l\'éditeur apporte sa propre copie de son agent (200 à 600 Mo) : l\'assistant demande s\'il faut garder aussi celle en ligne de commande (l\'extension d\'OpenCode nécessite l\'outil en ligne de commande, qui est toujours installé avec elle). Sans elle, `agent-vm claude` dit qu\'il n\'est pas installé. Ce n\'est que ce que l\'image contient au départ : l\'agent est root dans la VM et atteint Internet, il installe lui-même ce qui lui manque.', dockerTitle: 'Pourquoi pas Docker', dockerLede: 'Sous Linux, les conteneurs partagent le noyau de l\'hôte : une dépendance compromise qui trouve une faille dans le noyau se retrouve sur ta machine. Sous macOS, Docker Desktop fait déjà tourner une VM, ce qui réduit l\'écart, mais c\'est une seule VM partagée par tous tes conteneurs, pas une par projet. Une VM a son propre noyau, et root dans la VM a encore l\'hyperviseur entre lui et ton poste. Les différences pratiques comptent autant que celle-ci.', From e8533a6c7381c59a626e44538dbf064daeb9ec72 Mon Sep 17 00:00:00 2001 From: Nabi Bukhsh Date: Tue, 20 Oct 2026 21:18:59 +0500 Subject: [PATCH 3/3] Assert OpenCode-specific prompt in setup wizard test --- internal/cli/setup_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/cli/setup_test.go b/internal/cli/setup_test.go index 193d3a2b..c66d908c 100644 --- a/internal/cli/setup_test.go +++ b/internal/cli/setup_test.go @@ -180,6 +180,9 @@ func TestSetupWizard(t *testing.T) { if got := on(exportsOf(t, stdin)); got != "opencode,code-server,code-opencode" { t.Errorf("opencode extension: %s", got) } + if !strings.Contains(te.out(), "Extension only (OpenCode keeps the CLI, required by its extension)") { + t.Errorf("prompt not shown: %s", te.out()) + } } // code-server makes the default memory 4, but --memory wins. No terminal: