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/README.md +45 -11
- package/bin/oc-setup.js +36 -8
- package/package.json +3 -2
- package/src/client.js +7 -3
- package/src/i18n.js +19 -0
- package/src/prompts.js +132 -34
- 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/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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
45
|
-
|
|
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:
|
|
52
|
-
|
|
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:
|
|
59
|
-
|
|
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:
|
|
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
|
-
|
|
139
|
-
console.log(c.cyan(
|
|
140
|
-
|
|
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(
|
|
155
|
-
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
}
|