opencode-wyvern 0.2.2 → 0.2.4

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 CHANGED
@@ -20,7 +20,7 @@ npm install -g opencode-wyvern
20
20
  Il comando si chiama **`oc-setup`** (non `oc-help`, non `oc`). Verifica:
21
21
 
22
22
  ```bash
23
- oc-setup --version # → 0.2.1
23
+ oc-setup --version # → 0.2.4
24
24
  ```
25
25
 
26
26
  Se compare `oc-setup: command not found` subito dopo l'installazione, il
@@ -96,7 +96,7 @@ accodata al valore precedente.
96
96
  | `client-bash` | locale | stesso blocco in `~/.bashrc` |
97
97
  | `server` | remoto | controlla node/npm, installa opencode se manca, crea `~/.config/opencode` + `AGENTS.md` |
98
98
  | `commands` | remoto | comandi custom in `command/*.md` (`/baseline-ui`, `/omniroute-restart`, `/review`, ...) |
99
- | `mcp` | remoto | server MCP preset (Figma Desktop, Figma Developer MCP); token in `.env` |
99
+ | `mcp` | remoto | server MCP preset (Figma Developer MCP); token in `.env` |
100
100
  | `providers` | remoto | provider opencode in `opencode.json`; chiavi in `.env` |
101
101
  | `plugins` | remoto | plugin npm installati nella dir config e listati in `opencode.json` |
102
102
  | `claude-mem` | remoto | memoria: wrapper `plugins/claude-mem-plugin.js` |
@@ -125,10 +125,9 @@ Docker OmniRoute e attende che sia `healthy`), `/review`, `/refactor`, `/tests`,
125
125
 
126
126
  ## MCP
127
127
 
128
- La sezione `mcp` scrive il blocco `mcp` di `opencode.json` con i preset:
129
- **Figma Desktop** (Dev Mode, `http://127.0.0.1:3845/mcp`, nessun segreto) e
130
- **Figma Developer MCP** (stdio via `npx figma-developer-mcp`; il Figma personal
131
- token finisce in `.env` sul server, mai in `config.json`).
128
+ La sezione `mcp` configura **Figma Developer MCP** (stdio via
129
+ `npx figma-developer-mcp`). Il Figma personal access token finisce in `.env`
130
+ sul server, mai in `config.json`.
132
131
 
133
132
  ## Client
134
133
 
@@ -149,4 +148,4 @@ node bin/oc-setup.js --help
149
148
  - Nessuna chiave/token/password nel pacchetto o in `config.json` locale.
150
149
  - Le API key sono scritte dal bootstrap remoto in `~/.config/opencode/.env` (`chmod 600`).
151
150
  - I file remoti viaggiano come base64 dentro lo script; con server non raggiungibile
152
- lo script non viene stampato a video (evita leak delle chiavi).
151
+ lo script non viene stampato a video (evita leak delle chiavi).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-wyvern",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Setup modulare per OpenCode: SSH, client PowerShell/bash (comandi oc-*), configurazione server opencode, provider, plugin e comandi custom. Installi tutto, attivi quello che vuoi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/sections.js CHANGED
@@ -155,7 +155,6 @@ export const COMMAND_CHOICES = [
155
155
  ]
156
156
 
157
157
  export const MCP_CHOICES = [
158
- { label: "Figma Desktop — Dev Mode (localhost, " + ui("nessun segreto", "no secret") + ")", value: "figma-desktop" },
159
158
  { label: "Figma Developer MCP — npx + " + ui("token personale Figma", "personal Figma token"), value: "figma-developer" },
160
159
  ]
161
160
 
@@ -196,7 +195,8 @@ export function serverState(cfg) {
196
195
  omnirouteUrl,
197
196
  baseUrls: cfg.baseUrls || {},
198
197
  customCommands: cfg.customCommands || [],
199
- mcpList: cfg.mcpList || [],
198
+ // Ignora preset rimossi: `oc-setup generate` pulisce anche le vecchie config.
199
+ mcpList: (cfg.mcpList || []).filter((id) => MCP_PRESETS[id]),
200
200
  defaultModel,
201
201
  smallModel,
202
202
  tuning: cfg.tuning,
@@ -259,4 +259,4 @@ export function applyRemoteSections(cfg, { dryRun = false, envKeys, applyMode }
259
259
  }
260
260
  runRemote(cfg.entry || cfg, script)
261
261
  return { applied: true, script }
262
- }
262
+ }
package/src/server.js CHANGED
@@ -158,16 +158,14 @@ function providerBlock(id, models, baseUrls = {}, omnirouteUrl = "") {
158
158
  * `envKey` = chiave d'ambiente da chiedere al setup (finisce in .env sul server).
159
159
  */
160
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
161
  "figma-developer": {
167
162
  label: "Figma Developer MCP — npx + Figma personal token",
168
163
  type: "local",
169
- command: ["npx", "-y", "figma-developer-mcp", "--stdio"],
170
- environment: { FIGMA_API_KEY: "{env:FIGMA_API_KEY}" },
164
+ command: ["npx", "-y", "figma-developer-mcp", "--stdio", "--no-telemetry"],
165
+ environment: {
166
+ FIGMA_API_KEY: "{env:FIGMA_API_KEY}",
167
+ PATH: "{env:PATH}",
168
+ },
171
169
  timeout: 15000,
172
170
  envKey: "FIGMA_API_KEY",
173
171
  },
@@ -175,14 +173,14 @@ export const MCP_PRESETS = {
175
173
 
176
174
  /** Raggruppa i preset MCP scelti nel blocco `mcp` di opencode.json. */
177
175
  export function buildMcpBlock(mcpList = []) {
178
- const mcp = {}
176
+ const servers = {}
179
177
  for (const id of mcpList) {
180
178
  const p = MCP_PRESETS[id]
181
179
  if (!p) continue
182
180
  const { label, envKey, ...serv } = p
183
- mcp[id] = serv
181
+ servers[id] = serv
184
182
  }
185
- return mcp
183
+ return Object.keys(servers).length ? { servers } : {}
186
184
  }
187
185
 
188
186
  /** Modello default (inner ID) per un preset MCP che richiede un token. */
@@ -244,11 +242,12 @@ export function ensureLocalKey() {
244
242
  export function installKeyOnServer(cfg) {
245
243
  const { pub } = keyPaths()
246
244
  const pubContent = fs.readFileSync(pub, "utf8").trim()
245
+ const pubB64 = Buffer.from(pubContent, "utf8").toString("base64")
247
246
  const target = sshTargetArgs(cfg)
248
247
  const remoteCmd =
249
- "umask 077; mkdir -p ~/.ssh && grep -qF '" +
250
- pubContent.split(" ")[0] +
251
- "' ~/.ssh/authorized_keys 2>/dev/null || (cat >> ~/.ssh/authorized_keys); " +
248
+ `umask 077; mkdir -p ~/.ssh; touch ~/.ssh/authorized_keys; ` +
249
+ `PUB="$(printf '%s' '${pubB64}' | base64 -d)"; ` +
250
+ `grep -qxF "$PUB" ~/.ssh/authorized_keys 2>/dev/null || printf '%s\\n' "$PUB" >> ~/.ssh/authorized_keys; ` +
252
251
  "chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys; echo OK"
253
252
 
254
253
  console.log(c.cyan(ui(
@@ -278,6 +277,8 @@ export function hostAliasBlock(cfg) {
278
277
  if (cfg.host) parts.push(` HostName ${cfg.host}`)
279
278
  if (cfg.user) parts.push(` User ${cfg.user}`)
280
279
  if (cfg.port && String(cfg.port) !== "22") parts.push(` Port ${cfg.port}`)
280
+ parts.push(" IdentityFile ~/.ssh/id_ed25519")
281
+ parts.push(" IdentitiesOnly yes")
281
282
  parts.push(" ServerAliveInterval 30")
282
283
  parts.push(" ServerAliveCountMax 5")
283
284
  parts.push(" StrictHostKeyChecking accept-new")
@@ -289,19 +290,37 @@ export function ensureSshConfigAlias(cfg) {
289
290
  const p = sshConfigPath()
290
291
  const target = `Host ${cfg.server}`
291
292
  let content = ""
293
+ const nextBlock = hostAliasBlock(cfg)
292
294
  if (fs.existsSync(p)) {
293
295
  content = fs.readFileSync(p, "utf8")
294
- if (content.includes(target)) return false
296
+ const re = new RegExp(`(^|\\n)Host\\s+${cfg.server.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n(?:[ \\t].*(?:\\n|$))*`, "m")
297
+ if (re.test(content)) {
298
+ const updated = content.replace(re, (m, lead = "") => `${lead}${nextBlock}\n`)
299
+ if (updated === content) return false
300
+ fs.writeFileSync(p, updated, "utf8")
301
+ return true
302
+ }
295
303
  if (content && !content.endsWith("\n")) content += "\n"
296
304
  }
297
305
  ensureDir(path.dirname(p))
298
- fs.writeFileSync(p, content + hostAliasBlock(cfg) + "\n", "utf8")
306
+ fs.writeFileSync(p, content + nextBlock + "\n", "utf8")
299
307
  return true
300
308
  }
301
309
 
302
310
  /** Verifica la connessione SSH senza password (BatchMode). */
303
311
  export function verifyConnection(cfg) {
304
- const res = run("ssh", [...sshTargetArgs(cfg), "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", "echo CONN_OK"], {
312
+ const { key } = keyPaths()
313
+ const target = cfg.server || sshTargetArgs(cfg).at(-1)
314
+ const targetArgs = cfg.server ? [] : sshTargetArgs(cfg).slice(0, -1)
315
+ const keyArgs = fs.existsSync(key) ? ["-i", key, "-o", "IdentitiesOnly=yes"] : []
316
+ const res = run("ssh", [
317
+ ...targetArgs,
318
+ ...keyArgs,
319
+ "-o", "BatchMode=yes",
320
+ "-o", "ConnectTimeout=8",
321
+ target,
322
+ "echo CONN_OK",
323
+ ], {
305
324
  silent: true,
306
325
  timeout: 20000,
307
326
  })
@@ -335,7 +354,7 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
335
354
  "if [ -z \"$OPENCODE_BIN\" ]; then",
336
355
  " log 'opencode mancante: lo installo (npm i -g opencode-ai)'",
337
356
  " npm install -g opencode-ai >/dev/null 2>&1 || true",
338
- " OPENCODE_BIN=\"$(command -v opencode 2>/dev/null)\"",
357
+ " OPENCODE_BIN=\"$(command -v opencode 2>/dev/null || ls -t $HOME/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)\"",
339
358
  "fi",
340
359
  '[ -f package.json ] || printf \'{\\n "private": true,\\n "dependencies": {}\\n}\\n\' > package.json',
341
360
  "[ -f .gitignore ] || printf 'node_modules\\n.env\\n' > .gitignore",
@@ -392,6 +411,19 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
392
411
  lines.push("umask 077; [ -f \"$CFG_DIR/.env\" ] || : > \"$CFG_DIR/.env\"")
393
412
  }
394
413
 
414
+ if (mcpList.includes("figma-developer")) {
415
+ lines.push(
416
+ 'NVM_NODE_BIN="$(ls -dt \"$HOME\"/.nvm/versions/node/*/bin 2>/dev/null | head -n1)"',
417
+ 'if [ -n "$NVM_NODE_BIN" ] && [ -x "$NVM_NODE_BIN/node" ]; then',
418
+ ' touch "$CFG_DIR/.env"',
419
+ ' grep -v "^PATH=" "$CFG_DIR/.env" > "$CFG_DIR/.env.tmp" || true',
420
+ ' printf \'PATH="%s:%s"\\n\' "$NVM_NODE_BIN" "$PATH" >> "$CFG_DIR/.env.tmp"',
421
+ ' mv "$CFG_DIR/.env.tmp" "$CFG_DIR/.env"',
422
+ ' chmod 600 "$CFG_DIR/.env"',
423
+ 'fi',
424
+ )
425
+ }
426
+
395
427
  lines.push('log "config opencode scritta in: $CFG_DIR"')
396
428
  lines.push("[ -n \"$OPENCODE_BIN\" ] && \"$OPENCODE_BIN\" --version && echo REMOTE_OK || echo REMOTE_NO_OPENCODE")
397
429
  return lines.join("\n")
@@ -448,4 +480,4 @@ export function runScriptLocal(script) {
448
480
 
449
481
  export function isPortValid(v) {
450
482
  return v === "" || (/^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= 65535)
451
- }
483
+ }
package/src/setup.js CHANGED
@@ -323,6 +323,14 @@ if (!localOnly) {
323
323
  if (doInstall) {
324
324
  try {
325
325
  installKeyOnServer(entry)
326
+ if (verifyConnection(entry)) {
327
+ console.log(c.green(ui(" autenticazione SSH a chiave verificata.", " SSH key authentication verified.")))
328
+ } else {
329
+ console.log(c.yellow(ui(
330
+ " attenzione: la chiave e' stata copiata, ma l'accesso senza password non e' stato verificato. Controlla sshd e ~/.ssh/authorized_keys sul server.",
331
+ " warning: the key was copied, but passwordless login could not be verified. Check sshd and ~/.ssh/authorized_keys on the server.",
332
+ )))
333
+ }
326
334
  } catch (err) {
327
335
  console.log(c.yellow(` ${ui("attenzione:", "warning:")} ${err.message}`))
328
336
  }
@@ -385,7 +393,8 @@ export async function cmdStatus() {
385
393
  console.log(` server: provider ${providers.join(", ")}${urls.length ? ` · ${urls.join(" · ")}` : ""}${cfg.tuning ? ui(" · tuning on", " · tuning on") : ""}`)
386
394
  }
387
395
  if (cfg.customCommands?.length) console.log(` cmds : ${cfg.customCommands.map((x) => "/" + x).join(", ")}`)
388
- if (cfg.mcpList?.length) console.log(` mcp : ${cfg.mcpList.join(", ")}`)
396
+ const mcpList = (cfg.mcpList || []).filter((id) => MCP_PRESETS[id])
397
+ if (mcpList.length) console.log(` mcp : ${mcpList.join(", ")}`)
389
398
  console.log("")
390
399
  console.log(ui(" moduli:", " modules:"))
391
400
  for (const s of SECTIONS) {
@@ -447,4 +456,4 @@ function fail(msg) {
447
456
  export async function runPrintClient(kind) {
448
457
  const id = kind === "bash" ? "client-bash" : "client-pwsh"
449
458
  await cmdPrint(id)
450
- }
459
+ }
@@ -28,11 +28,10 @@ oc-sync-env
28
28
 
29
29
  oc-path() {
30
30
  oc-sync-env
31
- if [ -z "$OC_OPENCODE" ]; then
32
- OC_OPENCODE=$(ssh -o BatchMode=yes "$OC_SERVER" "(command -v opencode || ls -t \$HOME/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)" 2>/dev/null | head -n1)
33
- [ -n "$OC_OPENCODE" ] || OC_OPENCODE='opencode'
34
- fi
35
- echo "$OC_OPENCODE"
31
+ # Risolve opencode SUL SERVER al momento dell'esecuzione: funziona anche
32
+ # senza chiave SSH configurata (BatchMode non tenuto) e senza opencode nel
33
+ # PATH del server (cade sulle installazioni nvm `~/.nvm/versions/node/*/bin`).
34
+ printf '%s' '__oc_wyvern(){ OPENCODE_BIN="$(command -v opencode 2>/dev/null || ls -t $HOME/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)"; if [ -z "$OPENCODE_BIN" ]; then echo "[oc] ERRORE: opencode non trovato sul server. Esegui oc-setup scegliendo Client + Server, oppure installa sul server: npm install -g opencode-ai" >&2; return 127; fi; "$OPENCODE_BIN" "$@"; }; __oc_wyvern'
36
35
  }
37
36
 
38
37
  oc-connect() {
@@ -201,10 +200,23 @@ oc-delete() {
201
200
 
202
201
  oc-go() {
203
202
  oc-sync-env
204
- local id="$1" dir="${2:-~}" norecap=0 oc exit_code
203
+ local id="$1" dir="${2:-}" norecap=0 oc exit_code session_dir
205
204
  [ "$3" = "--no-recap" ] && norecap=1
205
+ if [ -z "$id" ]; then
206
+ echo -e "${RED}[FAIL] Specifica l'ID della sessione.${NC}"
207
+ return 2
208
+ fi
209
+ case "$id" in
210
+ *[![:alnum:]_-]*) echo -e "${RED}[FAIL] ID sessione non valido.${NC}"; return 2 ;;
211
+ esac
206
212
  oc="$(oc-path)"
207
- oc-sessions-all >/dev/null 2>&1
213
+ if [ -z "$dir" ]; then
214
+ session_dir="$(oc-cache-load | awk -F '\t' -v wanted="$id" '$2 == wanted { print $4; exit }')"
215
+ if [ -z "$session_dir" ]; then
216
+ session_dir="$(oc-sessions-all | awk -F '\t' -v wanted="$id" '$2 == wanted { print $4; exit }')"
217
+ fi
218
+ dir="${session_dir:-~}"
219
+ fi
208
220
  mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/opencode-wyvern"
209
221
  printf '%s\t%s\n' "$id" "${dir:-~}" > "${XDG_CACHE_HOME:-$HOME/.cache}/opencode-wyvern/last.tsv"
210
222
  if [ "$dir" = "~" ] || [ -z "$dir" ]; then
@@ -229,6 +241,12 @@ oc-recap() {
229
241
  live="$(oc-sessions-today 2>/dev/null)"
230
242
  if [ -z "$live" ]; then
231
243
  cached="$(oc-cache-load)"
244
+ if [ "$last_exit" = "127" ]; then
245
+ echo -e "${RED} [!] Comando remoto non trovato (SSH exit 127).${NC}"
246
+ echo -e "${YELLOW} Probabile causa: opencode non e' installato sul server o non e' nel PATH della shell SSH.${NC}"
247
+ echo -e "${DARKGRAY:-} Risolvi con: oc-setup scegliendo Client + Server, oppure sul server: npm install -g opencode-ai${NC}"
248
+ return 127
249
+ fi
232
250
  if [ -n "$cached" ]; then
233
251
  m="$(stat -c %y "${XDG_CACHE_HOME:-$HOME/.cache}/opencode-wyvern/sessions.tsv" 2>/dev/null | cut -d'.' -f1)"
234
252
  echo -e "${RED} [!] Connessione persa alle $err_time (SSH exit $last_exit).${NC}"
@@ -448,4 +466,4 @@ oc-help() {
448
466
  echo ""
449
467
  echo -e "${DARKGRAY:-}Nota: oc-ssh, oc-sessions, oc-resume richiedono SSH key configurata"
450
468
  echo ""
451
- }
469
+ }
@@ -26,13 +26,10 @@ function Sync-OcEnv {
26
26
  Sync-OcEnv
27
27
 
28
28
  function Get-OcPath {
29
- if (-not $global:OC_OPENCODE) {
30
- $remotePath = "(command -v opencode || ls -t ~/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)"
31
- $found = ssh -o BatchMode=yes $env:OC_SERVER $remotePath 2>$null | Select-Object -First 1
32
- if ($found) { $global:OC_OPENCODE = $found.Trim() }
33
- else { $global:OC_OPENCODE = 'opencode' }
34
- }
35
- return $global:OC_OPENCODE
29
+ # Risolve opencode SUL SERVER al momento dell'esecuzione: funziona anche
30
+ # senza chiave SSH configurata (niente BatchMode) e senza opencode nel PATH
31
+ # del server (fallback sulle installazioni nvm ~/.nvm/versions/node/*/bin).
32
+ return '__oc_wyvern(){ OPENCODE_BIN="$(command -v opencode 2>/dev/null || ls -t $HOME/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)"; if [ -z "$OPENCODE_BIN" ]; then echo "[oc] ERRORE: opencode non trovato sul server. Esegui oc-setup scegliendo Client + Server, oppure installa sul server: npm install -g opencode-ai" >&2; return 127; fi; "$OPENCODE_BIN" "$@"; }; __oc_wyvern'
36
33
  }
37
34
 
38
35
  function oc-connect {
@@ -300,11 +297,21 @@ function oc-go {
300
297
  param(
301
298
  [Parameter(Mandatory)]
302
299
  [string]$Id,
303
- [string]$Dir = "~",
300
+ [string]$Dir = "",
304
301
  [switch]$NoRecap
305
302
  )
306
303
  Sync-OcEnv
307
- $null = @(Get-OcAllSessions)
304
+ if ($Id -notmatch '^[A-Za-z0-9_-]+$') {
305
+ Write-Host "ID sessione non valido." -ForegroundColor Red
306
+ return
307
+ }
308
+ if ([string]::IsNullOrWhiteSpace($Dir)) {
309
+ $found = @(Get-OcCachedSessions | Where-Object { $_.Id -eq $Id } | Select-Object -First 1)
310
+ if (-not $found.Count) {
311
+ $found = @(Get-OcAllSessions | Where-Object { $_.Id -eq $Id } | Select-Object -First 1)
312
+ }
313
+ $Dir = if ($found.Count) { $found[0].Dir } else { "~" }
314
+ }
308
315
  $oc = Get-OcPath
309
316
  if ($Dir -eq '~' -or [string]::IsNullOrEmpty($Dir)) {
310
317
  $remoteCmd = "cd ~ && $oc -s $Id"
@@ -334,6 +341,12 @@ function Show-OcRecap {
334
341
  $sessions = @(Get-OcRecentSessions)
335
342
  $fromCache = $false
336
343
  if ($sessions.Count -eq 0) {
344
+ if ($LastExit -eq 127) {
345
+ Write-Host " [!] Comando remoto non trovato (SSH exit 127)." -ForegroundColor Red
346
+ Write-Host " Probabile causa: opencode non e' installato sul server o non e' nel PATH della shell SSH." -ForegroundColor Yellow
347
+ Write-Host " Risolvi con: oc-setup scegliendo Client + Server, oppure sul server: npm install -g opencode-ai" -ForegroundColor DarkGray
348
+ return
349
+ }
337
350
  $status = Get-OcServerStatus
338
351
  if ($status.Up) {
339
352
  Write-Host " Server raggiungibile ma nessuna sessione nelle ultime 24 ore." -ForegroundColor Yellow