opencode-wyvern 0.1.1 → 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/README.md +45 -11
- package/bin/oc-setup.js +36 -8
- package/package.json +8 -4
- package/src/client.js +7 -3
- package/src/i18n.js +19 -0
- package/src/prompts.js +238 -26
- package/src/sections.js +130 -28
- package/src/server.js +127 -19
- package/src/setup.js +248 -69
- package/src/shell.js +6 -1
- package/src/uninstall.js +62 -23
- package/templates/command-commit.md +18 -0
- package/templates/command-explain.md +16 -0
- package/templates/command-omniroute-restart.md +19 -0
- package/templates/command-refactor.md +18 -0
- package/templates/command-review.md +17 -0
- package/templates/command-tests.md +16 -0
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
160
|
-
|
|
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(
|
|
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")
|
|
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
|
-
|
|
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("
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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))
|
|
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 */
|