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/setup.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import { section, pwshProfilePath, bashRcPath, configFilePath } from "./shell.js"
2
- import { confirm, ask, checkbox, secret, closePrompts, c } from "./prompts.js"
2
+ import { confirm, ask, checkbox, select, secret, closePrompts, c } from "./prompts.js"
3
+ import { ui } from "./i18n.js"
3
4
  import { loadConfig, saveConfig } from "./config.js"
4
5
  import {
5
6
  SECTIONS,
6
7
  PLUGIN_CHOICES,
8
+ COMMAND_CHOICES,
9
+ MCP_CHOICES,
7
10
  getSection,
8
11
  defaultSections,
9
12
  renderSection,
10
13
  applyLocalSection,
11
14
  applyRemoteSections,
15
+ neededConfigs,
12
16
  } from "./sections.js"
13
17
  import {
14
18
  PROVIDER_META,
@@ -19,157 +23,311 @@ import {
19
23
  ensureSshConfigAlias,
20
24
  installKeyOnServer,
21
25
  verifyConnection,
26
+ MCP_PRESETS,
22
27
  } from "./server.js"
23
28
  import { installClient } from "./client.js"
24
29
 
25
30
  const SAMPLE_ENTRY = { server: "remote-server", host: "server.example.com", user: "you", dir: "~" }
26
- const REMOTE_SECTIONS = new Set(["server", "commands", "providers", "plugins", "claude-mem"])
31
+ const REMOTE_SECTIONS = new Set(["server", "commands", "providers", "plugins", "claude-mem", "mcp"])
32
+
33
+ /** Scenari preimpostati: dicono quali sezioni attivare prima di iniziare. */
34
+ const SCENARIOS = [
35
+ {
36
+ value: "client-server",
37
+ label: ui("Client + Server", "Client + Server"),
38
+ desc: ui("SSH, client oc-* e bootstrap completo del server (consigliato).", "SSH, oc-* clients and full server bootstrap (recommended)."),
39
+ sections: ["ssh", "client-pwsh", "client-bash", "server", "commands", "mcp", "providers", "plugins", "claude-mem"],
40
+ },
41
+ {
42
+ value: "client-only",
43
+ label: ui("Solo client", "Client only"),
44
+ desc: ui("SSH + comandi oc-* nei profili locali (il server opencode resta com'è).", "SSH + oc-* commands in local profiles (the opencode server stays untouched)."),
45
+ sections: ["ssh", "client-pwsh", "client-bash"],
46
+ },
47
+ {
48
+ value: "server-only",
49
+ label: ui("Solo server", "Server only"),
50
+ desc: ui("Bootstrap e config del server remoto via SSH (nessun profilo client).", "Remote server bootstrap & config over SSH (no local profiles)."),
51
+ sections: ["server", "commands", "mcp", "providers", "plugins", "claude-mem"],
52
+ },
53
+ {
54
+ value: "custom",
55
+ label: ui("Personalizzato", "Custom"),
56
+ desc: ui("Scelgo io le sezioni una a una.", "I'll pick the sections one by one."),
57
+ sections: null,
58
+ },
59
+ ]
27
60
 
28
61
  /** Coordina (dove, user, porta) e alias solo per le sezioni che le richiedono. */
29
62
  async function collectEntrySmart(prev, needs) {
30
63
  const d = prev || {}
31
64
  const out = {}
32
65
  if (needs.host) {
33
- out.host = await ask("IP o hostname del server remoto", {
34
- hint: "es. server.example.com o 192.0.2.1 (solo esempio)",
66
+ out.host = await ask(ui("IP o hostname del server remoto", "Remote server IP or hostname"), {
67
+ hint: ui("es. server.example.com o 192.0.2.1 (solo esempio)", "e.g. server.example.com or 192.0.2.1 (example only)"),
35
68
  defaultValue: d.host || "",
36
69
  validate: (v) => v.length > 0,
37
70
  })
38
- out.user = await ask("Utente SSH sul server", {
39
- hint: "es. root, ubuntu (invio = solo hostname)",
71
+ out.user = await ask(ui("Utente SSH sul server", "SSH user on the server"), {
72
+ hint: ui("es. root, ubuntu (invio = solo hostname)", "e.g. root, ubuntu (blank = hostname only)"),
40
73
  defaultValue: d.user || "",
41
74
  })
42
- out.port = await ask("Porta SSH (invio = 22)", {
75
+ out.port = await ask(ui("Porta SSH (invio = 22)", "SSH port (blank = 22)"), {
43
76
  defaultValue: d.port || "",
44
77
  validate: (v) => isPortValid(v || ""),
45
78
  })
46
79
  }
47
80
  if (needs.alias) {
48
- out.server = await ask("Nome con cui i comandi oc-* chiamano il server (alias SSH)", {
49
- hint: "qualsiasi nome, es. server-casa — finisce in ~/.ssh/config e in OC_SERVER",
81
+ out.server = await ask(ui("Nome con cui i comandi oc-* chiamano il server (alias SSH)", "Name the oc-* commands use to reach the server (SSH alias)"), {
82
+ hint: ui("qualsiasi nome, es. server-casa — finisce in ~/.ssh/config e in OC_SERVER", "any name, e.g. home-server — stored in ~/.ssh/config and OC_SERVER"),
50
83
  defaultValue: d.server || "remote-server",
51
84
  validate: (v) => /^[a-zA-Z0-9._-]+$/.test(v),
52
85
  })
53
86
  }
54
87
  if (needs.dir) {
55
- out.dir = await ask("Cartella remota di default (dir sessione)", {
56
- hint: "es. ~ o ~/projects",
57
- defaultValue: d.dir || "~",
58
- })
88
+ const raw = await ask(ui(
89
+ "Cartella remota di default (dir sessione, parte già dalla home dell'utente)",
90
+ "Default remote folder (session dir, already rooted at the user's home)"),
91
+ {
92
+ hint: ui(
93
+ "lascia vuoto = ~ (es. projects/foo → ~/projects/foo; /assoluto resta assoluto)",
94
+ "leave empty = ~ (e.g. projects/foo → ~/projects/foo; /absolute stays absolute)"),
95
+ defaultValue: d.dir && d.dir !== "~" ? d.dir : "",
96
+ })
97
+ out.dir = normalizeDir(raw || (d.dir && d.dir !== "~" ? d.dir : "") || "~")
59
98
  }
60
99
  return out
61
100
  }
62
101
 
102
+ /**
103
+ * Normalizza la cartella remota: vuoto → `~`; relativo (o `./x`) → relativo
104
+ * alla home (`~/x`); già `~/...`/`~` resta com'è; assoluto `/...` resta tale.
105
+ * Evita il path doppio tipo `/home/user/home/user` se l'input incappa sul default.
106
+ */
107
+ function normalizeDir(raw) {
108
+ const v = (raw ?? "").trim()
109
+ if (!v) return "~"
110
+ if (v.startsWith("/")) return v.length === 1 ? "/" : v.replace(/\/+$/, "")
111
+ if (v === "~" || v.startsWith("~/")) return v
112
+ return `~/${v.replace(/^\.?\//, "").replace(/\/+$/, "")}`
113
+ }
114
+
63
115
  /** Raccoglie i provider attivi, gli endpoint e le chiavi (che restano solo sul server). */
64
116
  async function collectProvidersCfg(prev) {
65
117
  const choices = PROVIDER_ORDER.map((id) => ({
66
118
  label: PROVIDER_META[id].name + (PROVIDER_META[id].key ? " (key in .env)" : ""),
67
119
  value: id,
68
120
  }))
69
- const sel = await checkbox("Quali provider abilitare sul server?", choices)
121
+ const sel = await checkbox(ui("Quali provider abilitare sul server?", "Which providers do you want to enable on the server?"), choices)
70
122
  const providers = Object.fromEntries(PROVIDER_ORDER.map((id) => [id, false]))
71
123
  sel.forEach((s) => (providers[s.value] = true))
72
124
 
73
125
  let omnirouteUrl = (prev && prev.omnirouteUrl) || ""
126
+ const baseUrls = { ...(prev && prev.baseUrls) }
74
127
  if (providers.omniroute) {
75
- omnirouteUrl = await ask("URL gateway OmniRoute (senza /v1)", {
128
+ omnirouteUrl = await ask(ui("URL gateway OmniRoute (senza /v1)", "OmniRoute gateway URL (without /v1)"), {
76
129
  hint: "es. http://127.0.0.1:20128",
77
130
  defaultValue: omnirouteUrl || defaultOmniRouteBase(),
78
131
  validate: (v) => /^https?:\/\/\S+$/.test(v),
79
132
  })
80
133
  }
134
+ for (const id of PROVIDER_ORDER) {
135
+ if (id !== "omniroute" && providers[id] && PROVIDER_META[id].baseURLVar) {
136
+ baseUrls[id] = await ask(ui(
137
+ `Base URL ${PROVIDER_META[id].name}`,
138
+ `Base URL ${PROVIDER_META[id].name}`,
139
+ ), {
140
+ hint: ui("endpoint OpenAI-compatible (mantiene /v1)", "OpenAI-compatible endpoint (keeps /v1)"),
141
+ defaultValue: baseUrls[id] || PROVIDER_META[id].defBase,
142
+ validate: (v) => /^https?:\/\/\S+$/.test(v),
143
+ })
144
+ }
145
+ }
81
146
 
82
147
  const envKeys = {}
83
148
  for (const id of PROVIDER_ORDER) {
84
149
  const key = PROVIDER_META[id].key
85
150
  if (providers[id] && key) {
86
- envKeys[key] = await secret(`API key ${PROVIDER_META[id].name} (${key}) — resta sul server`, { allowEmpty: true })
151
+ envKeys[key] = await secret(ui(
152
+ `API key ${PROVIDER_META[id].name} (${key}) — resta sul server`,
153
+ `API key ${PROVIDER_META[id].name} (${key}) — stays on the server`,
154
+ ), { allowEmpty: true })
87
155
  }
88
156
  }
89
157
  if (Object.values(envKeys).some(Boolean)) {
90
- console.log(c.dim(" (le chiavi NON vengono salvate in locale; finiscono solo in ~/.config/opencode/.env sul server, chmod 600)"))
158
+ console.log(c.dim(ui(
159
+ " (le chiavi NON vengono salvate in locale; finiscono solo in ~/.config/opencode/.env sul server, chmod 600)",
160
+ " (keys are NOT stored locally; they only end up in ~/.config/opencode/.env on the server, chmod 600)",
161
+ )))
91
162
  }
92
163
 
93
164
  const models = Object.fromEntries(
94
165
  PROVIDER_ORDER.filter((id) => providers[id]).map((id) => [id, (PROVIDER_META[id].defModels || []).join(", ")]),
95
166
  )
96
- return { providers, models, envKeys, omnirouteUrl }
167
+ return { providers, models, envKeys, omnirouteUrl, baseUrls }
168
+ }
169
+
170
+ /** Comandi custom (file command/*.md sul server) selezionati. */
171
+ async function collectCommandsCfg(prev) {
172
+ const prevList = (prev && prev.customCommands) || ["baseline-ui"]
173
+ const defaultIndices = COMMAND_CHOICES
174
+ .map((ch, i) => (prevList.includes(ch.value) || ch.default ? i : -1))
175
+ .filter((i) => i >= 0)
176
+ const sel = await checkbox(ui("Quali comandi custom installare sul server?", "Which custom commands to install on the server?"), COMMAND_CHOICES.map((m) => ({ label: m.label, value: m.value })), { defaultIndices })
177
+ return { customCommands: sel.map((s) => s.value) }
178
+ }
179
+
180
+ /** Server MCP abilitati; token non salvati in locale (solo .env sul server). */
181
+ async function collectMcpCfg(prev) {
182
+ const prevList = (prev && prev.mcpList) || []
183
+ const defaultIndices = MCP_CHOICES.map((m, i) => (prevList.includes(m.value) ? i : -1)).filter((i) => i >= 0)
184
+ const sel = await checkbox(ui("Quali server MCP abilitare nel config server?", "Which MCP servers to enable in the server config?"), MCP_CHOICES.map((m) => ({ label: m.label, value: m.value })), { defaultIndices })
185
+ const mcpList = sel.map((s) => s.value)
186
+ const envKeys = {}
187
+ const tokenIds = mcpList.filter((id) => MCP_PRESETS[id]?.envKey)
188
+ if (tokenIds.length) {
189
+ envKeys.FIGMA_API_KEY = await secret(ui(
190
+ "Figma personal access token (FIGMA_API_KEY) — resta sul server",
191
+ "Figma personal access token (FIGMA_API_KEY) — stays on the server",
192
+ ), { allowEmpty: true })
193
+ }
194
+ if (Object.values(envKeys).some(Boolean)) {
195
+ console.log(c.dim(ui(
196
+ " (il token NON viene salvato in locale; finisce solo in ~/.config/opencode/.env sul server, chmod 600)",
197
+ " (token is NOT stored locally; it only ends up in ~/.config/opencode/.env on the server, chmod 600)",
198
+ )))
199
+ }
200
+ return { mcpList, envKeys }
97
201
  }
98
202
 
99
203
  export async function run(argv = []) {
100
204
  console.log(c.bold(c.cyan("oc-setup — OpenCode Wyvern "))
101
- + c.dim("(setup modulare: installi tutto, attivi quello che vuoi — nessun dato privato nel pacchetto)"))
102
- section("Moduli")
103
-
205
+ + c.dim(ui(
206
+ "(setup modulare: installi tutto, attivi quello che vuoi — nessun dato privato nel pacchetto)",
207
+ "(modular setup: install everything, activate what you want — no private data in the package)",
208
+ )))
104
209
  const cfg = loadConfig()
105
- const active = await checkbox("Quali sezioni vuoi attivare ora?", SECTIONS.map((s) => ({
106
- label: s.label,
107
- value: s.id,
108
- })))
109
- const activeIds = new Set(active.map((s) => s.value))
210
+
211
+ section(ui("Scenario", "Scenario"))
212
+ const scenario = await select(
213
+ ui("Quale scenario configuri?", "Which scenario are you configuring?"),
214
+ SCENARIOS.map((s) => ({ label: `${s.label} — ${s.desc}`, value: s.value, sections: s.sections })),
215
+ )
216
+
217
+ let applyMode = cfg?.applyMode || "ssh"
218
+ if (scenario.value === "server-only") {
219
+ const modeSel = await select(
220
+ ui("Come applichi lo script sul server?", "How do you apply the script on the server?"),
221
+ [
222
+ { label: ui("Via SSH da questo client", "Over SSH from this client"), value: "ssh" },
223
+ { label: ui("In locale, su questa macchina (localhost)", "Locally, on this machine (localhost)"), value: "local" },
224
+ ],
225
+ )
226
+ applyMode = modeSel.value
227
+ }
228
+
229
+ section(ui("Moduli", "Modules"))
230
+ let activeIds
231
+ if (scenario.sections) {
232
+ activeIds = new Set(scenario.sections)
233
+ console.log(c.dim(` ${ui("attiverò:", "will activate:")} ${SECTIONS.filter((s) => activeIds.has(s.id)).map((s) => s.id).join(", ")}`))
234
+ } else {
235
+ const active = await checkbox(ui("Quali sezioni vuoi attivare ora?", "Which sections do you want to activate now?"), SECTIONS.map((s) => ({
236
+ label: s.label,
237
+ value: s.id,
238
+ })))
239
+ activeIds = new Set(active.map((s) => s.value))
240
+ }
110
241
 
111
242
  if (!activeIds.size) {
112
- console.log(c.yellow(" nessun modulo attivo: niente da installare. Esci e riavvia con \`oc-setup\` per sceglierne almeno uno."))
243
+ console.log(c.yellow(ui(
244
+ " nessun modulo attivo: niente da installare. Esci e riavvia con \`oc-setup\` per sceglierne almeno uno.",
245
+ " no active module: nothing to install. Exit and re-run \`oc-setup\` to pick at least one.",
246
+ )))
113
247
  closePrompts()
114
248
  return
115
249
  }
116
250
 
117
- const needs = {
118
- host: activeIds.has("ssh") || [...REMOTE_SECTIONS].some((id) => activeIds.has(id)),
119
- alias: activeIds.has("ssh") || activeIds.has("client-pwsh") || activeIds.has("client-bash"),
120
- dir: activeIds.has("client-pwsh") || activeIds.has("client-bash"),
121
- }
251
+ // Ogni sezione porta con sé le sue configurazioni (vedi SECTION_CONFIGS):
252
+ // chiediamo solo ciò che serve alle sezioni scelte, mai altro.
253
+ const nc = neededConfigs(activeIds)
254
+ const localOnly = applyMode === "local"
122
255
 
123
- section("Connessione")
124
- console.log(c.dim(" solo i dati necessari ai moduli che hai scelto."))
125
- const entry = await collectEntrySmart(cfg.entry || {}, needs)
256
+ let entry = cfg.entry || {}
257
+ if (!localOnly) {
258
+ section(ui("Connessione", "Connection"))
259
+ console.log(c.dim(ui(" solo i dati necessari ai moduli che hai scelto.", " only the data needed by the modules you picked.")))
260
+ entry = await collectEntrySmart(entry, { host: nc.has("host"), alias: nc.has("alias"), dir: nc.has("dir") })
261
+ }
126
262
 
127
263
  let providers = cfg.providers || Object.fromEntries(PROVIDER_ORDER.map((id) => [id, false]))
128
264
  let models = cfg.models || {}
129
265
  let envKeys = {}
130
266
  let omnirouteUrl = cfg.omnirouteUrl || ""
267
+ let baseUrls = cfg.baseUrls || {}
131
268
  if (activeIds.has("providers")) {
132
269
  const p = await collectProvidersCfg(cfg)
133
270
  providers = p.providers
134
271
  models = p.models
135
272
  omnirouteUrl = p.omnirouteUrl
136
- envKeys = p.envKeys
273
+ baseUrls = { ...baseUrls, ...p.baseUrls }
274
+ envKeys = { ...envKeys, ...p.envKeys }
137
275
  }
138
276
 
139
277
  let plugins = cfg.plugins || []
140
278
  if (activeIds.has("plugins")) {
141
- const sel = await checkbox("Plugin opencode da includere nel config server?", PLUGIN_CHOICES.map((p) => ({ label: p.label, value: p.value })))
279
+ const sel = await checkbox(ui("Plugin opencode da includere nel config server?", "opencode plugins to include in the server config?"), PLUGIN_CHOICES.map((p) => ({ label: p.label, value: p.value })))
142
280
  plugins = sel.map((s) => s.value)
143
281
  }
144
282
 
283
+ let customCommands = cfg.customCommands || []
284
+ if (activeIds.has("commands")) {
285
+ customCommands = (await collectCommandsCfg(cfg)).customCommands
286
+ }
287
+
288
+ let mcpList = cfg.mcpList || []
289
+ if (activeIds.has("mcp")) {
290
+ const mcp = await collectMcpCfg(cfg)
291
+ mcpList = mcp.mcpList
292
+ envKeys = { ...envKeys, ...mcp.envKeys }
293
+ }
294
+
145
295
  let tuning = cfg.tuning !== false
146
- if (activeIds.has("providers") || activeIds.has("plugins") || activeIds.has("claude-mem") || activeIds.has("server")) {
147
- tuning = await confirm("Aggiungo al config tool_output + compaction (default robusti)?", cfg.tuning !== false)
296
+ if (nc.has("tuning")) {
297
+ tuning = await confirm(ui("Aggiungo al config tool_output + compaction (default robusti)?", "Add tool_output + compaction to the config (robust defaults)?"), cfg.tuning !== false)
148
298
  }
149
299
 
150
300
  const sections = Object.fromEntries(SECTIONS.map((s) => [s.id, activeIds.has(s.id)]))
151
- saveConfig({ entry, sections, providers, models, plugins, omnirouteUrl, tuning, configuredAt: new Date().toISOString() })
301
+ saveConfig({ entry, sections, providers, models, plugins, omnirouteUrl, baseUrls, customCommands, mcpList, tuning, applyMode, configuredAt: new Date().toISOString() })
152
302
 
153
- section("Applicazione")
303
+ section(ui("Applicazione", "Application"))
154
304
  const anyClient = activeIds.has("client-pwsh") || activeIds.has("client-bash")
155
305
  if (!activeIds.has("ssh") && anyClient && !entry.host) {
156
- console.log(c.dim(` client senza host: oc-* userà l'alias \`${entry.server}\`. Assicurati che ~/.ssh/config lo definisca.`))
306
+ console.log(c.dim(ui(
307
+ ` client senza host: oc-* userà l'alias \`${entry.server}\`. Assicurati che ~/.ssh/config lo definisca.`,
308
+ ` client without host: oc-* will use the alias \`${entry.server}\`. Make sure ~/.ssh/config defines it.`,
309
+ )))
157
310
  }
158
311
 
159
- const { key } = ensureLocalKey()
160
- console.log(c.dim(` chiave: ${key}`))
312
+ if (!localOnly) {
313
+ const { key } = ensureLocalKey()
314
+ console.log(c.dim(ui(" chiave:", " key:") + ` ${key}`))
315
+ }
161
316
 
162
- if (activeIds.has("ssh")) {
317
+ if (!localOnly && activeIds.has("ssh")) {
163
318
  ensureSshConfigAlias(entry)
164
- const doInstall = await confirm(`Installo la chiave pubblica su ${entry.user ? entry.user + "@" : ""}${entry.host} (password una volta)?`, true)
319
+ const doInstall = await confirm(ui(
320
+ `Installo la chiave pubblica su ${entry.user ? entry.user + "@" : ""}${entry.host} (password una volta)?`,
321
+ `Install the public key on ${entry.user ? entry.user + "@" : ""}${entry.host} (one-time password)?`,
322
+ ), true)
165
323
  if (doInstall) {
166
324
  try {
167
325
  installKeyOnServer(entry)
168
326
  } catch (err) {
169
- console.log(c.yellow(` attenzione: ${err.message}`))
327
+ console.log(c.yellow(` ${ui("attenzione:", "warning:")} ${err.message}`))
170
328
  }
171
329
  }
172
- } else if (entry.host && (activeIds.has("client-pwsh") || activeIds.has("client-bash") || [...REMOTE_SECTIONS].some((id) => activeIds.has(id)))) {
330
+ } else if (!localOnly && entry.host && (activeIds.has("client-pwsh") || activeIds.has("client-bash"))) {
173
331
  ensureSshConfigAlias(entry)
174
332
  }
175
333
 
@@ -182,16 +340,30 @@ export async function run(argv = []) {
182
340
  }
183
341
 
184
342
  if ([...REMOTE_SECTIONS].some((id) => activeIds.has(id))) {
185
- const ok = verifyConnection(entry)
186
- if (!ok) console.log(c.yellow(" server non raggiungibile ora: script pronto, esegui `oc-setup generate` quando torna"))
187
343
  const saved = loadConfig()
188
- applyRemoteSections(saved, { envKeys })
344
+ if (!localOnly) {
345
+ const ok = verifyConnection(entry)
346
+ if (!ok) console.log(c.yellow(ui(
347
+ " server non raggiungibile ora: script pronto, esegui `oc-setup generate` quando torna",
348
+ " server unreachable now: script ready, run `oc-setup generate` when it's back",
349
+ )))
350
+ applyRemoteSections(saved, { envKeys, applyMode: "ssh" })
351
+ } else {
352
+ try {
353
+ applyRemoteSections(saved, { envKeys, applyMode: "local" })
354
+ } catch (err) {
355
+ console.log(c.yellow(` ${ui("attenzione:", "warning:")} ${err.message}`))
356
+ }
357
+ }
189
358
  }
190
359
 
191
- console.log(c.green("\nFatto."))
192
- console.log(` config : ${configFilePath()} (solo dati non sensibili)`)
360
+ console.log(c.green(`\n${ui("Fatto.", "Done.")}`))
361
+ console.log(` config : ${configFilePath()} ${ui("(solo dati non sensibili)", "(non-sensitive data only)")}`)
193
362
  if (localClientTargets.length) {
194
- console.log(` client : ${localClientTargets.map((t) => t === "client-pwsh" ? "PowerShell" : "bash").join(", ")} → ricarica profilo per attivare i comandi oc-*`)
363
+ console.log(ui(
364
+ ` client : ${localClientTargets.map((t) => t === "client-pwsh" ? "PowerShell" : "bash").join(", ")} → ricarica profilo per attivare i comandi oc-*`,
365
+ ` client : ${localClientTargets.map((t) => t === "client-pwsh" ? "PowerShell" : "bash").join(", ")} → reload profile to activate the oc-* commands`,
366
+ ))
195
367
  }
196
368
  closePrompts()
197
369
  }
@@ -202,61 +374,68 @@ export async function cmdStatus() {
202
374
  const cfg = loadConfig()
203
375
  const e = cfg.entry
204
376
  const providers = cfg.providers && Object.entries(cfg.providers).filter(([, v]) => v).map(([k]) => k)
205
- console.log(c.bold(c.cyan("OpenCode Wyvern — stato")))
377
+ console.log(c.bold(c.cyan(ui("OpenCode Wyvern — stato", "OpenCode Wyvern — status"))))
206
378
  console.log(` config: ${configFilePath()}`)
207
379
  console.log(` shell : PowerShell ${pwshProfilePath()} | bash ${bashRcPath()}`)
208
- console.log(` entry : ${e ? `${e.user ? e.user + "@" : ""}${e.host || e.server}${e.port && e.port !== "22" ? ":" + e.port : ""} (alias: ${e.server || "-"}, dir: ${e.dir || "~"})` : "non configurata (esegui oc-setup)"}`)
209
- if (providers?.length) console.log(` server: provider ${providers.join(", ")}${cfg.omnirouteUrl ? ` · omniroute ${cfg.omnirouteUrl}` : ""}${cfg.tuning ? " · tuning on" : ""}`)
380
+ console.log(` entry : ${e ? `${e.user ? e.user + "@" : ""}${e.host || e.server}${e.port && e.port !== "22" ? ":" + e.port : ""} (alias: ${e.server || "-"}, dir: ${e.dir || "~"})` : ui("non configurata (esegui oc-setup)", "not configured (run oc-setup)")}`)
381
+ if (cfg.applyMode === "local") console.log(` apply : ${ui("in locale (localhost, senza SSH)", "local (localhost, no SSH)")}`)
382
+ if (providers?.length) {
383
+ const urls = providers.includes("omniroute") ? [`omniroute ${cfg.omnirouteUrl}`] : []
384
+ for (const [id, url] of Object.entries(cfg.baseUrls || {})) urls.push(`${id} ${url}`)
385
+ console.log(` server: provider ${providers.join(", ")}${urls.length ? ` · ${urls.join(" · ")}` : ""}${cfg.tuning ? ui(" · tuning on", " · tuning on") : ""}`)
386
+ }
387
+ if (cfg.customCommands?.length) console.log(` cmds : ${cfg.customCommands.map((x) => "/" + x).join(", ")}`)
388
+ if (cfg.mcpList?.length) console.log(` mcp : ${cfg.mcpList.join(", ")}`)
210
389
  console.log("")
211
- console.log(" moduli:")
390
+ console.log(ui(" moduli:", " modules:"))
212
391
  for (const s of SECTIONS) {
213
392
  const active = cfg.sections?.[s.id]
214
- console.log(` ${active ? c.green("●") : c.dim("○")} ${s.id.padEnd(12)} ${active ? c.dim(s.label) : c.dim(s.label + " (disattivo)")}`)
393
+ console.log(` ${active ? c.green("●") : c.dim("○")} ${s.id.padEnd(12)} ${active ? c.dim(s.label) : c.dim(s.label + ui(" (disattivo)", " (inactive)"))}`)
215
394
  }
216
395
  }
217
396
 
218
397
  export async function cmdActivate(id) {
219
- if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
398
+ if (!getSection(id)) return fail(ui("sezione sconosciuta:", "unknown section:") + ` ${id}`)
220
399
  const cfg = loadConfig()
221
400
  cfg.sections = { ...defaultSections(), ...(cfg.sections || {}), [id]: true }
222
401
  saveConfig(cfg)
223
- console.log(c.green(` sezione attivata: ${id}`))
402
+ console.log(c.green(` ${ui("sezione attivata:", "section activated:")} ${id}`))
224
403
  if (getSection(id).kind === "remote") {
225
- console.log(c.dim(` per applicarla sul server: oc-setup generate`))
404
+ console.log(c.dim(` ${ui("per applicarla sul server: oc-setup generate", "to apply it on the server: oc-setup generate")}`))
226
405
  } else if (id === "client-pwsh" || id === "client-bash") {
227
406
  await installClient(cfg.entry || SAMPLE_ENTRY, { targets: [id === "client-bash" ? "bash" : "pwsh"] })
228
407
  }
229
408
  }
230
409
 
231
410
  export async function cmdDeactivate(id) {
232
- if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
411
+ if (!getSection(id)) return fail(ui("sezione sconosciuta:", "unknown section:") + ` ${id}`)
233
412
  const cfg = loadConfig()
234
413
  cfg.sections = { ...defaultSections(), ...(cfg.sections || {}), [id]: false }
235
414
  saveConfig(cfg)
236
- console.log(c.yellow(` sezione disattivata: ${id}`))
415
+ console.log(c.yellow(` ${ui("sezione disattivata:", "section deactivated:")} ${id}`))
237
416
  }
238
417
 
239
418
  export async function cmdGenerate() {
240
419
  const cfg = loadConfig()
241
- if (!cfg.entry) return fail("nessuna config: esegui prima `oc-setup`")
420
+ if (!cfg.entry) return fail(ui("nessuna config: esegui prima `oc-setup`", "no config: run `oc-setup` first"))
242
421
  const act = new Set(Object.entries(cfg.sections || {}).filter(([, v]) => v).map(([k]) => k))
243
422
  for (const id of ["client-pwsh", "client-bash"]) {
244
423
  if (act.has(id)) await applyLocalSection(id, { entry: cfg.entry })
245
424
  }
246
425
  if (act.has("ssh")) {
247
426
  ensureSshConfigAlias(cfg.entry)
248
- console.log(c.dim(" alias SSH assicurato."))
249
- console.log(c.dim(" chiave: install via `oc-setup` se server raggiungibile."))
427
+ console.log(c.dim(ui(" alias SSH assicurato.", " SSH alias ensured.")))
428
+ console.log(c.dim(ui(" chiave: install via `oc-setup` se server raggiungibile.", " key: install via `oc-setup` if the server is reachable.")))
250
429
  }
251
430
  const remote = [...act].filter((id) => getSection(id)?.kind === "remote").length
252
431
  if (remote) applyRemoteSections(cfg)
253
432
  }
254
433
 
255
434
  export async function cmdPrint(id) {
256
- if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
435
+ if (!getSection(id)) return fail(ui("sezione sconosciuta:", "unknown section:") + ` ${id}`)
257
436
  const cfg = loadConfig()
258
437
  const rendered = renderSection(id, cfg.entry ? cfg : SAMPLE_ENTRY)
259
- if (!rendered) return fail(`sezione non stampabile: ${id}`)
438
+ if (!rendered) return fail(ui("sezione non stampabile:", "section not printable:") + ` ${id}`)
260
439
  console.log(rendered.content)
261
440
  }
262
441
 
package/src/shell.js CHANGED
@@ -70,13 +70,18 @@ export function run(cmd, args = [], { silent = false, stdio = false, timeout = 1
70
70
 
71
71
  /** Trova un eseguibile nel PATH (Windows incluso). */
72
72
  export function which(bin) {
73
+ const winRoot = (process.env.SystemRoot || "C:\\Windows").toLowerCase()
73
74
  const scan = isWindows() ? process.env.PATH.split(";") : process.env.PATH.split(":")
74
75
  for (const dir of scan) {
75
76
  try {
76
77
  const candidates = isWindows() ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.ps1`] : [bin]
77
78
  for (const c of candidates) {
78
79
  const p = path.join(dir, c)
79
- if (fs.existsSync(p)) return p
80
+ if (fs.existsSync(p)) {
81
+ // Su Windows, `C:\Windows\System32\bash.exe` è WSL (non eredita env)
82
+ if (isWindows() && p.toLowerCase().startsWith(winRoot) && c === "bash.exe") continue
83
+ return p
84
+ }
80
85
  }
81
86
  } catch {
82
87
  /* ignora */