opencode-wyvern 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sections.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { c } from "./prompts.js"
2
+ import { ui } from "./i18n.js"
2
3
  import { renderClient, installClient } from "./client.js"
3
- import { buildRemoteScript, runRemote, verifyConnection, chooseDefaultModels } from "./server.js"
4
+ import { buildRemoteScript, runRemote, runScriptLocal, verifyConnection, chooseDefaultModels } from "./server.js"
4
5
 
5
6
  /**
6
7
  * Catalogo delle sezioni del setup. Ogni sezione è un modulo indipendente:
@@ -13,65 +14,149 @@ export const SECTIONS = [
13
14
  id: "ssh",
14
15
  kind: "local",
15
16
  group: "core",
16
- label: "SSH — chiave, alias, install sul server",
17
- desc: "Genera/riusa ~/.ssh/id_ed25519 (senza passphrase), aggiunge l'alias a ~/.ssh/config e installa la chiave sul server (password una volta).",
17
+ label: ui("SSH — chiave, alias, install sul server", "SSH — key, alias, server install"),
18
+ desc: ui(
19
+ "Genera/riusa ~/.ssh/id_ed25519 (senza passphrase), aggiunge l'alias a ~/.ssh/config e installa la chiave sul server (password una volta).",
20
+ "Generates/reuses ~/.ssh/id_ed25519 (no passphrase), adds the alias to ~/.ssh/config and installs the key on the server (one-time password).",
21
+ ),
18
22
  },
19
23
  {
20
24
  id: "client-pwsh",
21
25
  kind: "local",
22
26
  group: "client",
23
- label: "Client PowerShell — comandi oc-*",
24
- desc: "Blocco oc-*, oc-go, oc-resume, oc-recap nel profilo PowerShell.",
27
+ label: ui("Client PowerShell — comandi oc-*", "PowerShell client — oc-* commands"),
28
+ desc: ui(
29
+ "Blocco oc-*, oc-go, oc-resume, oc-recap nel profilo PowerShell.",
30
+ "oc-*, oc-go, oc-resume, oc-recap block in the PowerShell profile.",
31
+ ),
25
32
  },
26
33
  {
27
34
  id: "client-bash",
28
35
  kind: "local",
29
36
  group: "client",
30
- label: "Client bash — comandi oc-*",
31
- desc: "Stesso blocco in ~/.bashrc.",
37
+ label: ui("Client bash — comandi oc-*", "bash client — oc-* commands"),
38
+ desc: ui(
39
+ "Stesso blocco in ~/.bashrc.",
40
+ "Same block in ~/.bashrc.",
41
+ ),
32
42
  },
33
43
  {
34
44
  id: "server",
35
45
  kind: "remote",
36
46
  group: "server",
37
- label: "Server — bootstrap opencode",
38
- desc: "Controlla node/npm, installa opencode se manca, crea ~/.config/opencode e AGENTS.md.",
47
+ label: ui("Server — bootstrap opencode", "Server — opencode bootstrap"),
48
+ desc: ui(
49
+ "Controlla node/npm, installa opencode se manca, crea ~/.config/opencode e AGENTS.md.",
50
+ "Checks node/npm, installs opencode if missing, creates ~/.config/opencode and AGENTS.md.",
51
+ ),
39
52
  },
40
53
  {
41
54
  id: "commands",
42
55
  kind: "remote",
43
56
  group: "server",
44
- label: "Server — comando /baseline-ui",
45
- desc: "Scrive command/baseline-ui.md (baseline di interfaccia).",
57
+ label: ui(
58
+ "Server — comandi custom (/baseline-ui, /review, /omniroute-restart…)",
59
+ "Server — custom commands (/baseline-ui, /review, /omniroute-restart…)",
60
+ ),
61
+ desc: ui(
62
+ "Scrive i comandi selezionati in ~/.config/opencode/command/*.md (agent in opencode).",
63
+ "Writes the selected commands to ~/.config/opencode/command/*.md (opencode agents).",
64
+ ),
65
+ },
66
+ {
67
+ id: "mcp",
68
+ kind: "remote",
69
+ group: "server",
70
+ label: ui("Server — MCP (Figma Design…)", "Server — MCP (Figma Design…)"),
71
+ desc: ui(
72
+ "Blocco mcp in opencode.json per server figma; i token finiscono in .env sul server (chmod 600).",
73
+ "mcp block in opencode.json for figma servers; tokens go to .env on the server (chmod 600).",
74
+ ),
46
75
  },
47
76
  {
48
77
  id: "providers",
49
78
  kind: "remote",
50
79
  group: "server",
51
- label: "Server — provider (Copilot, Gemini, Zen, Anthropic, OpenAI, OmniRoute)",
52
- desc: "SDK + blocco provider in opencode.json; API key in .env sul server (chmod 600).",
80
+ label: ui(
81
+ "Server provider (Copilot, Gemini, Zen, Anthropic, OpenAI, OmniRoute, Ollama)",
82
+ "Server — providers (Copilot, Gemini, Zen, Anthropic, OpenAI, OmniRoute, Ollama)",
83
+ ),
84
+ desc: ui(
85
+ "SDK + blocco provider in opencode.json; API key in .env sul server (chmod 600).",
86
+ "SDK + provider block in opencode.json; API keys in .env on the server (chmod 600).",
87
+ ),
53
88
  },
54
89
  {
55
90
  id: "plugins",
56
91
  kind: "remote",
57
92
  group: "server",
58
- label: "Server — plugin (copilot-auth, claude-auth, kimi, omniroute)",
59
- desc: "Plugin npm installati in ~/.config/opencode e listati in opencode.json.",
93
+ label: ui(
94
+ "Server plugin (copilot-auth, claude-auth, kimi, omniroute)",
95
+ "Server — plugins (copilot-auth, claude-auth, kimi, omniroute)",
96
+ ),
97
+ desc: ui(
98
+ "Plugin npm installati in ~/.config/opencode e listati in opencode.json.",
99
+ "npm plugins installed in ~/.config/opencode and listed in opencode.json.",
100
+ ),
60
101
  },
61
102
  {
62
103
  id: "claude-mem",
63
104
  kind: "remote",
64
105
  group: "server",
65
- label: "Server — claude-mem (memoria)",
66
- desc: "Installa opcode-mem + wrapper plugins/claude-mem-plugin.js.",
106
+ label: ui("Server — claude-mem (memoria)", "Server — claude-mem (memory)"),
107
+ desc: ui(
108
+ "Installa opcode-mem + wrapper plugins/claude-mem-plugin.js.",
109
+ "Installs opcode-mem + plugins/claude-mem-plugin.js wrapper.",
110
+ ),
67
111
  },
68
112
  ]
69
113
 
114
+ /**
115
+ * Configurazioni associate a ciascuna sezione: il wizard chiede SOLO quelle
116
+ * delle sezioni selezionate (principio "ogni sezione → le sue config").
117
+ * Le config di connessione (host/user/port) sono condivise: chiunque le
118
+ * richieda le riceve una volta sola.
119
+ */
120
+ const SSH_CONN = ["host", "user", "port"]
121
+ export const SECTION_CONFIGS = {
122
+ ssh: [...SSH_CONN, "alias", "keyInstall"],
123
+ "client-pwsh": ["alias", "dir"],
124
+ "client-bash": ["alias", "dir"],
125
+ server: [...SSH_CONN, "tuning"],
126
+ commands: [...SSH_CONN, "customCommands"],
127
+ mcp: [...SSH_CONN, "mcpList"],
128
+ providers: [...SSH_CONN, "providersList", "omnirouteUrl", "apiKeys", "tuning"],
129
+ plugins: [...SSH_CONN, "pluginsList", "tuning"],
130
+ "claude-mem": [...SSH_CONN, "tuning"],
131
+ }
132
+
133
+ /** Unione delle configurazioni richieste da un insieme di sezioni. */
134
+ export function neededConfigs(sectionIds) {
135
+ const set = new Set()
136
+ for (const id of sectionIds) for (const k of SECTION_CONFIGS[id] || []) set.add(k)
137
+ return set
138
+ }
139
+
70
140
  export const PLUGIN_CHOICES = [
71
- { label: "copilot-auth (auth GitHub Copilot)", value: "copilot-auth" },
72
- { label: "claude-auth (auth Claude)", value: "claude-auth" },
141
+ { label: ui("copilot-auth (auth GitHub Copilot)", "copilot-auth (GitHub Copilot auth)"), value: "copilot-auth" },
142
+ { label: ui("claude-auth (auth Claude)", "claude-auth (Claude auth)"), value: "claude-auth" },
73
143
  { label: "kimi-subscription", value: "kimi" },
74
- { label: "omniroute (gateway multi-modello)", value: "omniroute" },
144
+ { label: ui("omniroute (gateway multi-modello)", "omniroute (multi-model gateway)"), value: "omniroute" },
145
+ ]
146
+
147
+ export const COMMAND_CHOICES = [
148
+ { label: "/baseline-ui — " + ui("baseline interfacce/strutture", "interface/structure baseline"), value: "baseline-ui", default: true },
149
+ { label: "/omniroute-restart — " + ui("riavvia il container Docker OmniRoute", "restarts the OmniRoute Docker container"), value: "omniroute-restart" },
150
+ { label: "/review — " + ui("checklist code review", "code review checklist"), value: "review" },
151
+ { label: "/refactor — " + ui("piano di refactoring", "refactoring plan"), value: "refactor" },
152
+ { label: "/tests — " + ui("genera casi di test", "generates test cases"), value: "tests" },
153
+ { label: "/commit — " + ui("messaggio commit convenzionale", "conventional commit message"), value: "commit" },
154
+ { label: "/explain — " + ui("spiega un blocco di codice", "explains a code block"), value: "explain" },
155
+ ]
156
+
157
+ export const MCP_CHOICES = [
158
+ { label: "Figma Desktop — Dev Mode (localhost, " + ui("nessun segreto", "no secret") + ")", value: "figma-desktop" },
159
+ { label: "Figma Developer MCP — npx + " + ui("token personale Figma", "personal Figma token"), value: "figma-developer" },
75
160
  ]
76
161
 
77
162
  export function getSection(id) {
@@ -109,6 +194,9 @@ export function serverState(cfg) {
109
194
  claudeMem: sections.has("claude-mem"),
110
195
  models,
111
196
  omnirouteUrl,
197
+ baseUrls: cfg.baseUrls || {},
198
+ customCommands: cfg.customCommands || [],
199
+ mcpList: cfg.mcpList || [],
112
200
  defaultModel,
113
201
  smallModel,
114
202
  tuning: cfg.tuning,
@@ -120,7 +208,7 @@ export function serverState(cfg) {
120
208
  export function renderSection(id, cfg) {
121
209
  if (id === "client-pwsh") return { content: renderClient("pwsh", cfg.entry || cfg) }
122
210
  if (id === "client-bash") return { content: renderClient("bash", cfg.entry || cfg) }
123
- const remoteIds = new Set(["server", "commands", "providers", "plugins", "claude-mem"])
211
+ const remoteIds = new Set(["server", "commands", "providers", "plugins", "claude-mem", "mcp"])
124
212
  if (remoteIds.has(id)) {
125
213
  const state = serverState(cfg)
126
214
  const only = new Set([id])
@@ -135,24 +223,38 @@ export async function applyLocalSection(id, cfg) {
135
223
  await installClient(cfg.entry || cfg, { targets: [id === "client-bash" ? "bash" : "pwsh"] })
136
224
  return
137
225
  }
138
- if (id === "ssh") {
139
- console.log(c.cyan("[..] Chiave SSH + alias (l'installazione sul server richiede la password una volta)..."))
140
- // nulla da fare qui: la chiave/alias vengono gestiti dal wizard
226
+ if (id === "ssh") {
227
+ console.log(c.cyan(ui(
228
+ "[..] Chiave SSH + alias (l'installazione sul server richiede la password una volta)...",
229
+ "[..] SSH key + alias (installing on the server requires the one-time password)...",
230
+ )))
141
231
  }
142
232
  }
143
233
 
144
- /** Applica le sezioni remoto: compone lo script e lo esegue via SSH. */
145
- export function applyRemoteSections(cfg, { dryRun = false, envKeys } = {}) {
234
+ /** Applica le sezioni remoto: compone lo script e lo esegue via SSH o in locale. */
235
+ export function applyRemoteSections(cfg, { dryRun = false, envKeys, applyMode } = {}) {
236
+ const mode = applyMode || cfg.applyMode || "ssh"
146
237
  const state = { ...serverState(cfg), envKeys: envKeys || cfg.envKeys || {} }
147
238
  const script = buildRemoteScript(state)
148
239
  const remoteActive = [...state.sections].filter((id) => getSection(id)?.kind === "remote")
149
240
  if (!remoteActive.length) return { applied: false, script }
150
241
 
242
+ if (mode === "local") {
243
+ runScriptLocal(script)
244
+ return { applied: true, script }
245
+ }
246
+
151
247
  const ok = verifyConnection(cfg.entry || cfg)
152
248
  if (!ok) {
153
249
  // NB: non stampiamo lo script: con le API key dentro sarebbe un leak a video.
154
- console.log(c.yellow(` server non raggiungibile: script pronto (${script.length} byte). Esegui di nuovo \`oc-setup\` o \`oc-setup generate\` quando torna.`))
155
- console.log(c.dim(` sezioni remoto da applicare: ${remoteActive.join(", ")}`))
250
+ console.log(c.yellow(ui(
251
+ ` server non raggiungibile: script pronto (${script.length} byte). Esegui di nuovo \`oc-setup\` o \`oc-setup generate\` quando torna.`,
252
+ ` server unreachable: script ready (${script.length} bytes). Re-run \`oc-setup\` or \`oc-setup generate\` once it's back.`,
253
+ )))
254
+ console.log(c.dim(ui(
255
+ ` sezioni remoto da applicare: ${remoteActive.join(", ")}`,
256
+ ` remote sections to apply: ${remoteActive.join(", ")}`,
257
+ )))
156
258
  return { applied: false, script }
157
259
  }
158
260
  runRemote(cfg.entry || cfg, script)
package/src/server.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
3
  import { spawnSync } from "node:child_process"
4
- import { keyPaths, sshConfigPath, run } from "./shell.js"
5
- import { readTemplate, ensureDir } from "./config.js"
4
+ import { keyPaths, sshConfigPath, run, which } from "./shell.js"
5
+ import { readTemplate, ensureDir, templatePath } from "./config.js"
6
6
  import { c } from "./prompts.js"
7
+ import { ui } from "./i18n.js"
7
8
 
8
9
  /**
9
10
  * Catalogo dei provider opencode supportati dal wizard.
@@ -48,14 +49,23 @@ export const PROVIDER_META = {
48
49
  name: "OmniRoute (aggregator)",
49
50
  key: null,
50
51
  baseURLVar: true,
52
+ defBase: "http://127.0.0.1:20128/v1",
51
53
  defModels: [
52
54
  "auto/best-coding", "auto/best-reasoning", "auto/best-fast",
53
55
  "auto/cheap", "auto/best-free", "felo/felo-search", "felo/felo-scholar",
54
56
  ],
55
57
  },
58
+ ollama: {
59
+ npm: "@ai-sdk/openai-compatible",
60
+ name: "Ollama / vLLM (locale)",
61
+ key: null,
62
+ baseURLVar: true,
63
+ defBase: "http://127.0.0.1:11434/v1",
64
+ defModels: ["qwen2.5-coder:32b", "qwen2.5-coder:7b", "llama3.1:8b", "llama3.3:70b"],
65
+ },
56
66
  }
57
67
 
58
- export const PROVIDER_ORDER = ["copilot", "gemini", "zen", "anthropic", "openai", "omniroute"]
68
+ export const PROVIDER_ORDER = ["copilot", "gemini", "zen", "anthropic", "openai", "omniroute", "ollama"]
59
69
 
60
70
  const MODEL_LABELS = {
61
71
  "gpt-4o": "GPT-4o", "gpt-4o-mini": "GPT-4o mini",
@@ -66,6 +76,8 @@ const MODEL_LABELS = {
66
76
  "auto/best-coding": "Auto: best coding", "auto/best-reasoning": "Auto: best reasoning",
67
77
  "auto/best-fast": "Auto: best fast", "auto/cheap": "Auto: cheap", "auto/best-free": "Auto: best free",
68
78
  "felo/felo-search": "Felo: web search", "felo/felo-scholar": "Felo: academic search",
79
+ "qwen2.5-coder:32b": "Qwen2.5 Coder 32B", "qwen2.5-coder:7b": "Qwen2.5 Coder 7B",
80
+ "llama3.1:8b": "Llama 3.1 8B", "llama3.3:70b": "Llama 3.3 70B",
69
81
  }
70
82
 
71
83
  /** Nome del provider nel JSON opencode (la chiave "opencode" è per OpenCode Zen). */
@@ -125,27 +137,69 @@ function merchantEntry(p, omnirouteUrl) {
125
137
  return PLUGIN_PKG[p]
126
138
  }
127
139
 
128
- function providerBlock(id, models, omnirouteUrl) {
140
+ function providerBlock(id, models, baseUrls = {}, omnirouteUrl = "") {
129
141
  const meta = PROVIDER_META[id]
130
142
  const block = { npm: meta.npm, name: meta.name }
131
143
  if (meta.key) block.env = [meta.key]
132
144
  if (meta.baseURL) block.options = { baseURL: meta.baseURL }
133
- if (meta.baseURLVar) block.options = { baseURL: omnirouteProviderUrl(omnirouteUrl) }
145
+ if (meta.baseURLVar) {
146
+ const url = id === "omniroute"
147
+ ? baseUrls[id] || omnirouteProviderUrl(omnirouteUrl)
148
+ : baseUrls[id] || meta.defBase
149
+ block.options = { baseURL: url }
150
+ }
134
151
  block.models = meta.fixedModels
135
152
  || Object.fromEntries(models.map((m) => [m, { name: MODEL_LABELS[m] || m }]))
136
153
  return block
137
154
  }
138
155
 
156
+ /**
157
+ * Preset MCP pronti da abilitare nel config server.
158
+ * `envKey` = chiave d'ambiente da chiedere al setup (finisce in .env sul server).
159
+ */
160
+ export const MCP_PRESETS = {
161
+ "figma-desktop": {
162
+ label: "Figma Desktop — Dev Mode (localhost, nessun segreto)",
163
+ type: "remote",
164
+ url: "http://127.0.0.1:3845/mcp",
165
+ },
166
+ "figma-developer": {
167
+ label: "Figma Developer MCP — npx + Figma personal token",
168
+ type: "local",
169
+ command: ["npx", "-y", "figma-developer-mcp", "--stdio"],
170
+ environment: { FIGMA_API_KEY: "{env:FIGMA_API_KEY}" },
171
+ timeout: 15000,
172
+ envKey: "FIGMA_API_KEY",
173
+ },
174
+ }
175
+
176
+ /** Raggruppa i preset MCP scelti nel blocco `mcp` di opencode.json. */
177
+ export function buildMcpBlock(mcpList = []) {
178
+ const mcp = {}
179
+ for (const id of mcpList) {
180
+ const p = MCP_PRESETS[id]
181
+ if (!p) continue
182
+ const { label, envKey, ...serv } = p
183
+ mcp[id] = serv
184
+ }
185
+ return mcp
186
+ }
187
+
188
+ /** Modello default (inner ID) per un preset MCP che richiede un token. */
189
+ export function mcpEnvKey(mcpList = []) {
190
+ return mcpList.map((id) => MCP_PRESETS[id]?.envKey).find(Boolean) || null
191
+ }
192
+
139
193
  /**
140
194
  * Costruisce opencode.json dal server in modo programmatico (solo sezioni attive).
141
195
  * Nessun segreto nel JSON: le chiavi restano in .env (dichiarate con "env": [...]).
142
196
  */
143
- export function buildServerConfig({ providers = new Set(), models = {}, omnirouteUrl = "", defaultModel, smallModel, tuning = false, plugins = [], claudeMem = false } = {}) {
197
+ export function buildServerConfig({ providers = new Set(), models = {}, baseUrls = {}, omnirouteUrl = "", defaultModel, smallModel, tuning = false, plugins = [], claudeMem = false, mcpList = [] } = {}) {
144
198
  const provider = {}
145
199
  for (const id of PROVIDER_ORDER) {
146
200
  if (providers.has(id)) {
147
201
  const list = (models[id] || PROVIDER_META[id].defModels.join(", ")).split(",").map((s) => s.trim()).filter(Boolean)
148
- provider[providerJsonKey(id)] = providerBlock(id, list, omnirouteUrl)
202
+ provider[providerJsonKey(id)] = providerBlock(id, list, baseUrls, omnirouteUrl)
149
203
  }
150
204
  }
151
205
 
@@ -157,6 +211,8 @@ export function buildServerConfig({ providers = new Set(), models = {}, omnirout
157
211
  provider,
158
212
  plugin,
159
213
  }
214
+ const mcp = buildMcpBlock(mcpList || [])
215
+ if (Object.keys(mcp).length) out.mcp = mcp
160
216
  if (defaultModel) out.model = defaultModel
161
217
  if (smallModel) out.small_model = smallModel
162
218
  if (tuning) {
@@ -173,7 +229,7 @@ export function ensureLocalKey() {
173
229
  if (fs.existsSync(key) && fs.existsSync(pub)) {
174
230
  return { key, pub, created: false }
175
231
  }
176
- console.log(c.cyan("[..] Genero la SSH key ed25519 (senza passphrase)..."))
232
+ console.log(c.cyan(ui("[..] Genero la SSH key ed25519 (senza passphrase)...", "[..] Generating the ed25519 SSH key (no passphrase)...")))
177
233
  const gen = spawnSync("ssh-keygen", ["-t", "ed25519", "-C", "", "-f", key, "-N", ""], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
178
234
  if (gen.status !== 0) {
179
235
  throw new Error(`ssh-keygen fallito: ${(gen.stderr || "").toString().trim()}`)
@@ -195,7 +251,10 @@ export function installKeyOnServer(cfg) {
195
251
  "' ~/.ssh/authorized_keys 2>/dev/null || (cat >> ~/.ssh/authorized_keys); " +
196
252
  "chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys; echo OK"
197
253
 
198
- console.log(c.cyan(`[..] Installo la chiave su ${c.bold(target.join(" "))} (password una volta)...`))
254
+ console.log(c.cyan(ui(
255
+ `[..] Installo la chiave su ${c.bold(target.join(" "))} (password una volta)...`,
256
+ `[..] Installing the key on ${c.bold(target.join(" "))} (one-time password)...`,
257
+ )))
199
258
  const res = spawnSync("ssh", [...target, remoteCmd], {
200
259
  encoding: "utf8",
201
260
  stdio: ["pipe", "inherit", "inherit"],
@@ -205,7 +264,7 @@ export function installKeyOnServer(cfg) {
205
264
  if (res.status !== 0) {
206
265
  throw new Error(`installazione chiave fallita (exit ${res.status})`)
207
266
  }
208
- console.log(c.green(" chiave installata."))
267
+ console.log(c.green(ui(" chiave installata.", " key installed.")))
209
268
  }
210
269
 
211
270
  export function sshTargetArgs({ user, host, port }) {
@@ -255,7 +314,7 @@ export function verifyConnection(cfg) {
255
314
  * I segreti (API key) finiscono solo in .env (chmod 600) quando la sezione providers
256
315
  * è attiva e l'utente ne ha fornite.
257
316
  */
258
- export function buildRemoteScript({ sections = new Set(), providers = new Set(), models = {}, omnirouteUrl = "", defaultModel, smallModel, tuning = false, plugins = [], claudeMem = false, envKeys = {} } = {}) {
317
+ export function buildRemoteScript({ sections = new Set(), providers = new Set(), models = {}, baseUrls = {}, omnirouteUrl = "", defaultModel, smallModel, tuning = false, plugins = [], claudeMem = false, envKeys = {}, customCommands = [], mcpList = [] } = {}) {
259
318
  const esc = (s) => Buffer.from(s, "utf8").toString("base64")
260
319
  const act = (id) => sections.has(id)
261
320
 
@@ -265,6 +324,9 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
265
324
  "command -v node >/dev/null || { log 'ERRORE: node non trovato sul server'; exit 2; }",
266
325
  "command -v npm >/dev/null || { log 'ERRORE: npm non trovato sul server'; exit 2; }",
267
326
  "log \"node: $(node -v)\"",
327
+ 'CFG_DIR="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"',
328
+ 'mkdir -p "$CFG_DIR/plugins" "$CFG_DIR/command"',
329
+ 'cd "$CFG_DIR"',
268
330
  ]
269
331
 
270
332
  if (act("server")) {
@@ -275,9 +337,6 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
275
337
  " npm install -g opencode-ai >/dev/null 2>&1 || true",
276
338
  " OPENCODE_BIN=\"$(command -v opencode 2>/dev/null)\"",
277
339
  "fi",
278
- 'CFG_DIR="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"',
279
- 'mkdir -p "$CFG_DIR/plugins" "$CFG_DIR/command"',
280
- 'cd "$CFG_DIR"',
281
340
  '[ -f package.json ] || printf \'{\\n "private": true,\\n "dependencies": {}\\n}\\n\' > package.json',
282
341
  "[ -f .gitignore ] || printf 'node_modules\\n.env\\n' > .gitignore",
283
342
  `echo '${esc(readTemplate("AGENTS.md"))}' | base64 -d > "$CFG_DIR/AGENTS.md"`,
@@ -285,7 +344,20 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
285
344
  }
286
345
 
287
346
  if (act("commands")) {
288
- lines.push(`echo '${esc(readTemplate("command-baseline-ui.md"))}' | base64 -d > "$CFG_DIR/command/baseline-ui.md"`)
347
+ const cmds = (customCommands && customCommands.length ? customCommands : ["baseline-ui"])
348
+ for (const id of cmds) {
349
+ const tpl = `command-${id}.md`
350
+ if (fs.existsSync(templatePath(tpl))) {
351
+ lines.push(`echo '${esc(readTemplate(tpl))}' | base64 -d > "$CFG_DIR/command/${id}.md"`)
352
+ }
353
+ }
354
+ }
355
+
356
+ if (act("mcp")) {
357
+ const mcpIds = mcpList || []
358
+ if (mcpIds.length) {
359
+ lines.push(`log "MCP: ${mcpIds.join(", ")}"`)
360
+ }
289
361
  }
290
362
 
291
363
  if (claudeMem) {
@@ -302,14 +374,14 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
302
374
  const pluginPkgs = [...new Set((plugins || []).map((p) => PLUGIN_PKG[p]).filter(Boolean))]
303
375
  if (pluginPkgs.length) lines.push(`npm install ${pluginPkgs.join(" ")} >/dev/null 2>&1 || true`)
304
376
 
305
- const hasServer = act("server") || act("commands") || act("plugins") || claudeMem || providers.size > 0
377
+ const hasServer = act("server") || act("commands") || act("plugins") || claudeMem || providers.size > 0 || (mcpList && mcpList.length > 0)
306
378
  if (hasServer) {
307
- const json = buildServerConfig({ providers, models, omnirouteUrl, defaultModel, smallModel, tuning, plugins, claudeMem })
379
+ const json = buildServerConfig({ providers, models, baseUrls, omnirouteUrl, defaultModel, smallModel, tuning, plugins, claudeMem, mcpList })
308
380
  lines.push(`echo '${esc(JSON.stringify(json, null, 2))}' | base64 -d > "$CFG_DIR/opencode.json"`)
309
381
  }
310
382
 
311
383
  const keyEntries = Object.entries(envKeys || {}).filter(([, v]) => v)
312
- if (act("providers") && keyEntries.length) {
384
+ if ((act("providers") || act("mcp")) && keyEntries.length) {
313
385
  lines.push("umask 077")
314
386
  lines.push("printf '' > \"$CFG_DIR/.env\"")
315
387
  for (const [k, v] of keyEntries) {
@@ -329,7 +401,10 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
329
401
  export function runRemote(cfg, script) {
330
402
  const b64 = Buffer.from(script, "utf8").toString("base64")
331
403
  const remoteCmd = `printf '%s' ${b64} | base64 -d | bash`
332
- console.log(c.cyan("[..] Eseguo bootstrap sul server (può richiedere qualche minuto)..."))
404
+ console.log(c.cyan(ui(
405
+ "[..] Eseguo bootstrap sul server (può richiedere qualche minuto)...",
406
+ "[..] Running bootstrap on the server (may take a few minutes)...",
407
+ )))
333
408
  const res = spawnSync("ssh", [...sshTargetArgs(cfg), remoteCmd], {
334
409
  encoding: "utf8",
335
410
  stdio: ["inherit", "inherit", "inherit"],
@@ -338,6 +413,39 @@ export function runRemote(cfg, script) {
338
413
  return { ok: res.status === 0, status: res.status }
339
414
  }
340
415
 
416
+ /**
417
+ * Esegue lo script di setup in locale, direttamente sulla macchina server
418
+ * ("server-only in localhost"): niente SSH, i file finiscono in ~/.config/opencode.
419
+ * Richiede bash sulla macchina (Linux/macOS, oppure Git Bash/WSL su Windows).
420
+ */
421
+ export function runScriptLocal(script) {
422
+ const bash = which("bash")
423
+ if (!bash) {
424
+ throw new Error(ui(
425
+ "server-only locale richiede bash su questa macchina (esegui il wizard direttamente dentro il server, Linux/macOS o Git Bash/WSL).",
426
+ "server-only local requires bash on this machine (run the wizard directly on the server, Linux/macOS or Git Bash/WSL).",
427
+ ))
428
+ }
429
+ console.log(c.cyan(ui(
430
+ "[..] Eseguo bootstrap in locale su questa macchina (può richiedere qualche minuto)...",
431
+ "[..] Running bootstrap locally on this machine (may take a few minutes)...",
432
+ )))
433
+ const env = { ...process.env }
434
+ // Conversione HOME in path POSIX (Git Bash/MSYS2 su Windows); su Linux resta com'è.
435
+ const rawHome = env.HOME || env.USERPROFILE
436
+ if (rawHome && /^[A-Za-z]:[\\/]/.test(rawHome)) {
437
+ const drive = rawHome[0].toLowerCase()
438
+ env.HOME = `/${drive}/${rawHome.slice(2).replace(/\\/g, "/")}`
439
+ }
440
+ const res = spawnSync(bash, ["-c", script], {
441
+ encoding: "utf8",
442
+ stdio: ["inherit", "inherit", "inherit"],
443
+ timeout: 600000,
444
+ env,
445
+ })
446
+ return { ok: res.status === 0, status: res.status }
447
+ }
448
+
341
449
  export function isPortValid(v) {
342
450
  return v === "" || (/^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= 65535)
343
451
  }