opencode-wyvern 0.2.1 → 0.2.3
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 +35 -1
- package/package.json +1 -1
- package/src/prompts.js +23 -13
- package/src/server.js +29 -8
- package/src/setup.js +9 -1
- package/templates/client-bash.sh +26 -8
- package/templates/client-pwsh.ps1 +22 -9
package/README.md
CHANGED
|
@@ -8,8 +8,42 @@ API key vengono **richiesti/creati durante il setup**, mai salvati in locale
|
|
|
8
8
|
|
|
9
9
|
## Installazione
|
|
10
10
|
|
|
11
|
+
Prerequisiti: **Node.js 18+** e **npm** (su Ubuntu/WSL si consiglia `nvm`).
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
14
|
npm install -g opencode-wyvern
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> Attenzione: `npm rm -g opencode-wyvern` **disinstalla**. Per installare
|
|
18
|
+
> si usa solo `npm install -g opencode-wyvern`.
|
|
19
|
+
|
|
20
|
+
Il comando si chiama **`oc-setup`** (non `oc-help`, non `oc`). Verifica:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
oc-setup --version # → 0.2.3
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Se compare `oc-setup: command not found` subito dopo l'installazione, il
|
|
27
|
+
binario è appena fuori dal PATH della shell corrente. Ricaricalo:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
hash -r # svuota la cache dei comandi (immediato) e riprova
|
|
31
|
+
source ~/.bashrc # oppure ricarica il profilo
|
|
32
|
+
# oppure: chiudi e riapri il terminale WSL
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Con **nvm** (Linux/WSL) il binario finisce in
|
|
36
|
+
`~/.nvm/versions/node/<versione>/bin`; quell'alias deve essere attivo
|
|
37
|
+
(`nvm use default`). Controlla che il file ci sia:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
nvm use default
|
|
41
|
+
ls -l ~/.nvm/versions/node/$(nvm current)/bin/oc-setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Poi avvia la procedura guidata:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
13
47
|
oc-setup
|
|
14
48
|
```
|
|
15
49
|
|
|
@@ -115,4 +149,4 @@ node bin/oc-setup.js --help
|
|
|
115
149
|
- Nessuna chiave/token/password nel pacchetto o in `config.json` locale.
|
|
116
150
|
- Le API key sono scritte dal bootstrap remoto in `~/.config/opencode/.env` (`chmod 600`).
|
|
117
151
|
- I file remoti viaggiano come base64 dentro lo script; con server non raggiungibile
|
|
118
|
-
lo script non viene stampato a video (evita leak delle chiavi).
|
|
152
|
+
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.
|
|
3
|
+
"version": "0.2.3",
|
|
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/prompts.js
CHANGED
|
@@ -153,24 +153,34 @@ function enableRaw() {
|
|
|
153
153
|
* piping/test e ambiente completamente non interattivo).
|
|
154
154
|
*/
|
|
155
155
|
const fallbackQueue = []
|
|
156
|
+
let fallbackEof = false
|
|
157
|
+
let fallbackBound = false
|
|
158
|
+
const fallbackWaiters = []
|
|
159
|
+
|
|
160
|
+
function pumpFallback() {
|
|
161
|
+
while (fallbackWaiters.length && (fallbackQueue.length || fallbackEof)) {
|
|
162
|
+
const done = fallbackWaiters.shift()
|
|
163
|
+
done(fallbackEof ? "q" : fallbackQueue.shift())
|
|
164
|
+
}
|
|
165
|
+
}
|
|
156
166
|
|
|
157
167
|
function readLineFallback(done) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
input.removeListener("data", onData)
|
|
168
|
-
next()
|
|
168
|
+
if (!fallbackBound) {
|
|
169
|
+
fallbackBound = true
|
|
170
|
+
input.on("data", (chunk) => {
|
|
171
|
+
fallbackQueue.push(...chunk.toString().split(/\r\n|\r|\n/))
|
|
172
|
+
pumpFallback()
|
|
173
|
+
})
|
|
174
|
+
const onEof = () => {
|
|
175
|
+
fallbackEof = true
|
|
176
|
+
pumpFallback()
|
|
169
177
|
}
|
|
170
|
-
input.on("
|
|
178
|
+
input.on("end", onEof)
|
|
179
|
+
input.on("close", onEof)
|
|
171
180
|
input.resume()
|
|
172
181
|
}
|
|
173
|
-
|
|
182
|
+
fallbackWaiters.push(done)
|
|
183
|
+
pumpFallback()
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
/** Legge una riga da tastiera. `masked` oscura la digitazione (per segreti); */
|
package/src/server.js
CHANGED
|
@@ -244,11 +244,12 @@ export function ensureLocalKey() {
|
|
|
244
244
|
export function installKeyOnServer(cfg) {
|
|
245
245
|
const { pub } = keyPaths()
|
|
246
246
|
const pubContent = fs.readFileSync(pub, "utf8").trim()
|
|
247
|
+
const pubB64 = Buffer.from(pubContent, "utf8").toString("base64")
|
|
247
248
|
const target = sshTargetArgs(cfg)
|
|
248
249
|
const remoteCmd =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
250
|
+
`umask 077; mkdir -p ~/.ssh; touch ~/.ssh/authorized_keys; ` +
|
|
251
|
+
`PUB="$(printf '%s' '${pubB64}' | base64 -d)"; ` +
|
|
252
|
+
`grep -qxF "$PUB" ~/.ssh/authorized_keys 2>/dev/null || printf '%s\\n' "$PUB" >> ~/.ssh/authorized_keys; ` +
|
|
252
253
|
"chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys; echo OK"
|
|
253
254
|
|
|
254
255
|
console.log(c.cyan(ui(
|
|
@@ -278,6 +279,8 @@ export function hostAliasBlock(cfg) {
|
|
|
278
279
|
if (cfg.host) parts.push(` HostName ${cfg.host}`)
|
|
279
280
|
if (cfg.user) parts.push(` User ${cfg.user}`)
|
|
280
281
|
if (cfg.port && String(cfg.port) !== "22") parts.push(` Port ${cfg.port}`)
|
|
282
|
+
parts.push(" IdentityFile ~/.ssh/id_ed25519")
|
|
283
|
+
parts.push(" IdentitiesOnly yes")
|
|
281
284
|
parts.push(" ServerAliveInterval 30")
|
|
282
285
|
parts.push(" ServerAliveCountMax 5")
|
|
283
286
|
parts.push(" StrictHostKeyChecking accept-new")
|
|
@@ -289,19 +292,37 @@ export function ensureSshConfigAlias(cfg) {
|
|
|
289
292
|
const p = sshConfigPath()
|
|
290
293
|
const target = `Host ${cfg.server}`
|
|
291
294
|
let content = ""
|
|
295
|
+
const nextBlock = hostAliasBlock(cfg)
|
|
292
296
|
if (fs.existsSync(p)) {
|
|
293
297
|
content = fs.readFileSync(p, "utf8")
|
|
294
|
-
|
|
298
|
+
const re = new RegExp(`(^|\\n)Host\\s+${cfg.server.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n(?:[ \\t].*(?:\\n|$))*`, "m")
|
|
299
|
+
if (re.test(content)) {
|
|
300
|
+
const updated = content.replace(re, (m, lead = "") => `${lead}${nextBlock}\n`)
|
|
301
|
+
if (updated === content) return false
|
|
302
|
+
fs.writeFileSync(p, updated, "utf8")
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
295
305
|
if (content && !content.endsWith("\n")) content += "\n"
|
|
296
306
|
}
|
|
297
307
|
ensureDir(path.dirname(p))
|
|
298
|
-
fs.writeFileSync(p, content +
|
|
308
|
+
fs.writeFileSync(p, content + nextBlock + "\n", "utf8")
|
|
299
309
|
return true
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
/** Verifica la connessione SSH senza password (BatchMode). */
|
|
303
313
|
export function verifyConnection(cfg) {
|
|
304
|
-
const
|
|
314
|
+
const { key } = keyPaths()
|
|
315
|
+
const target = cfg.server || sshTargetArgs(cfg).at(-1)
|
|
316
|
+
const targetArgs = cfg.server ? [] : sshTargetArgs(cfg).slice(0, -1)
|
|
317
|
+
const keyArgs = fs.existsSync(key) ? ["-i", key, "-o", "IdentitiesOnly=yes"] : []
|
|
318
|
+
const res = run("ssh", [
|
|
319
|
+
...targetArgs,
|
|
320
|
+
...keyArgs,
|
|
321
|
+
"-o", "BatchMode=yes",
|
|
322
|
+
"-o", "ConnectTimeout=8",
|
|
323
|
+
target,
|
|
324
|
+
"echo CONN_OK",
|
|
325
|
+
], {
|
|
305
326
|
silent: true,
|
|
306
327
|
timeout: 20000,
|
|
307
328
|
})
|
|
@@ -335,7 +356,7 @@ export function buildRemoteScript({ sections = new Set(), providers = new Set(),
|
|
|
335
356
|
"if [ -z \"$OPENCODE_BIN\" ]; then",
|
|
336
357
|
" log 'opencode mancante: lo installo (npm i -g opencode-ai)'",
|
|
337
358
|
" npm install -g opencode-ai >/dev/null 2>&1 || true",
|
|
338
|
-
" OPENCODE_BIN=\"$(command -v opencode 2>/dev/null)\"",
|
|
359
|
+
" OPENCODE_BIN=\"$(command -v opencode 2>/dev/null || ls -t $HOME/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)\"",
|
|
339
360
|
"fi",
|
|
340
361
|
'[ -f package.json ] || printf \'{\\n "private": true,\\n "dependencies": {}\\n}\\n\' > package.json',
|
|
341
362
|
"[ -f .gitignore ] || printf 'node_modules\\n.env\\n' > .gitignore",
|
|
@@ -448,4 +469,4 @@ export function runScriptLocal(script) {
|
|
|
448
469
|
|
|
449
470
|
export function isPortValid(v) {
|
|
450
471
|
return v === "" || (/^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= 65535)
|
|
451
|
-
}
|
|
472
|
+
}
|
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
|
}
|
|
@@ -447,4 +455,4 @@ function fail(msg) {
|
|
|
447
455
|
export async function runPrintClient(kind) {
|
|
448
456
|
const id = kind === "bash" ? "client-bash" : "client-pwsh"
|
|
449
457
|
await cmdPrint(id)
|
|
450
|
-
}
|
|
458
|
+
}
|
package/templates/client-bash.sh
CHANGED
|
@@ -28,11 +28,10 @@ oc-sync-env
|
|
|
28
28
|
|
|
29
29
|
oc-path() {
|
|
30
30
|
oc-sync-env
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
$
|
|
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
|