opencode-wyvern 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-wyvern",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
@@ -9,7 +9,7 @@
9
9
  "url": "https://github.com/Rhaegal222"
10
10
  },
11
11
  "engines": {
12
- "node": ">=18"
12
+ "node": ">=20.12.0"
13
13
  },
14
14
  "bin": {
15
15
  "oc-setup": "bin/oc-setup.js"
@@ -37,5 +37,8 @@
37
37
  },
38
38
  "scripts": {
39
39
  "test": "node bin/oc-setup.js --help"
40
+ },
41
+ "dependencies": {
42
+ "@clack/prompts": "^1.8.0"
40
43
  }
41
- }
44
+ }
package/src/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { stdin as input, stdout as output } from "node:process"
1
+ import { stdin as input, stdout as output, stderr as stderrStream } from "node:process"
2
2
 
3
3
  const color = (code, s) => `\x1b[${code}m${s}\x1b[0m`
4
4
  export const c = {
@@ -10,6 +10,46 @@ export const c = {
10
10
  bold: (s) => color(1, s),
11
11
  }
12
12
 
13
+ // --------------------------------------------------------------------------
14
+ // Backend TUI nativo Node (@clack/prompts) — cross-platform.
15
+ // Quando stdin/stdout/stderr sono un terminale reale, i prompt diventano
16
+ // dialoghi interattivi identici su Windows/pwsh, WSL, Linux e macOS
17
+ // (frecce, Spazio, invio, Esc/Ctrl+C per annullare, exit code 1).
18
+ // Negli altri casi (pipe, CI, non-interattivo) si usa il fallback a righe.
19
+ // `secret` mantiene l'input mascherato (via clack password o raw mode).
20
+ //
21
+ // Il modulo @clack/prompts è importato dinamicamente: se manca (repo senza
22
+ // `npm install`) si cade automaticamente sul fallback a righe.
23
+ // --------------------------------------------------------------------------
24
+
25
+ let cpModule = null
26
+ let cpTried = false
27
+
28
+ async function clack() {
29
+ if (!cpTried) {
30
+ cpTried = true
31
+ try {
32
+ cpModule = await import("@clack/prompts")
33
+ } catch {
34
+ cpModule = null // dipendenza non installata: fallback a righe
35
+ }
36
+ }
37
+ return cpModule
38
+ }
39
+
40
+ const realTty = () => !!input.isTTY && !!output.isTTY && !!stderrStream.isTTY
41
+
42
+ async function tuiOn() {
43
+ const cp = await clack()
44
+ return !!(cp && realTty())
45
+ }
46
+
47
+ function abortSetup() {
48
+ closePrompts()
49
+ console.log(c.yellow("\n annullato (exit 1)."))
50
+ process.exit(1)
51
+ }
52
+
13
53
  function enableRaw() {
14
54
  try {
15
55
  input.setRawMode(true)
@@ -85,8 +125,11 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
85
125
  input.on("data", onData)
86
126
  }
87
127
 
88
- /** Domanda a testo semplice con default opzionale. */
89
- export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
128
+ // --------------------------------------------------------------------------
129
+ // Implementazioni classiche (fallback a righe, stesse API precedenti).
130
+ // --------------------------------------------------------------------------
131
+
132
+ async function askClassic(question, { defaultValue = "", hint = "", validate } = {}) {
90
133
  const suffix = hint ? ` ${c.dim(`(${hint})`)}` : ""
91
134
  const def = defaultValue ? c.dim(`[${defaultValue}]`) : ""
92
135
  // eslint-disable-next-line no-constant-condition
@@ -102,8 +145,7 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
102
145
  }
103
146
  }
104
147
 
105
- /** Domanda sì/no con default indicato in maiuscolo. */
106
- export async function confirm(question, defaultValue = false) {
148
+ async function confirmClassic(question, defaultValue = false) {
107
149
  const hint = defaultValue ? "S/n" : "s/N"
108
150
  // eslint-disable-next-line no-constant-condition
109
151
  for (;;) {
@@ -116,26 +158,24 @@ export async function confirm(question, defaultValue = false) {
116
158
  }
117
159
  }
118
160
 
119
- /** Selezione singola (indice numerico). */
120
- export async function select(question, choices, { defaultValue = 0 } = {}) {
161
+ async function selectClassic(question, choices, { defaultValue = 0 } = {}) {
121
162
  console.log(`\n${c.cyan("?")} ${question}`)
122
163
  choices.forEach((choice, i) => {
123
164
  console.log(` ${i + 1}. ${choice.label}`)
124
165
  })
125
- return choices[Number(await ask("Scelta (numero)", {
166
+ return choices[Number(await askClassic("Scelta (numero)", {
126
167
  defaultValue: String(defaultValue + 1),
127
168
  validate: (v) => /^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= choices.length,
128
169
  })) - 1]
129
170
  }
130
171
 
131
- /** Selezione multipla: numeri separati da virgola/spazio (es. "1,3,5"); invio = nessuna. */
132
- export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
172
+ async function checkboxClassic(question, choices, { defaultIndices = [] } = {}) {
133
173
  console.log(`\n${c.cyan("?")} ${question} ${c.dim("(invio = nessuno; es. 1,3,5)")}`)
134
174
  choices.forEach((choice, i) => {
135
175
  const sel = defaultIndices.includes(i) ? "•" : " "
136
176
  console.log(` ${sel} ${i + 1}. ${choice.label}`)
137
177
  })
138
- const answer = await ask("Seleziona (es. 1,3,5)", {
178
+ const answer = await askClassic("Seleziona (es. 1,3,5)", {
139
179
  validate: (v) => {
140
180
  const nums = v.split(/[\s,]+/).filter(Boolean)
141
181
  if (!nums.length) return true // invio = nessuno (salta)
@@ -147,12 +187,86 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
147
187
  return [...new Set(nums.map((n) => Number(n) - 1))].map((i) => choices[i])
148
188
  }
149
189
 
150
- /** Input mascherato per segreti (API key ecc.). */
151
- export async function secret(question, { allowEmpty = false } = {}) {
190
+ async function secretClassic(question, { allowEmpty = false } = {}) {
152
191
  output.write(`${c.cyan("?")} ${question}\n> `)
153
192
  return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => resolve(v)))
154
193
  }
155
194
 
195
+ // --------------------------------------------------------------------------
196
+ // API pubbliche: TUI @clack/prompts (TTY) con fallback classico.
197
+ // --------------------------------------------------------------------------
198
+
199
+ /** Domanda a testo semplice con default opzionale (clack text). */
200
+ export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
201
+ if (!(await tuiOn())) return askClassic(question, { defaultValue, hint, validate })
202
+ const cp = await clack()
203
+ const message = hint ? `${question} (${hint})` : question
204
+ // eslint-disable-next-line no-constant-condition
205
+ for (;;) {
206
+ const raw = await cp.text({
207
+ message,
208
+ initialValue: defaultValue || undefined,
209
+ validate: (val) => {
210
+ const value = String(val ?? "").trim() === "" ? defaultValue : String(val).trim()
211
+ return validate && !validate(value) ? "risposta non valida, riprova." : undefined
212
+ },
213
+ })
214
+ if (cp.isCancel(raw)) abortSetup()
215
+ const value = String(raw ?? "").trim() === "" ? defaultValue : String(raw).trim()
216
+ if (validate && !validate(value)) {
217
+ cp.log.error("risposta non valida, riprova.")
218
+ continue
219
+ }
220
+ return value
221
+ }
222
+ }
223
+
224
+ /** Domanda sì/no (clack confirm). */
225
+ export async function confirm(question, defaultValue = false) {
226
+ if (!(await tuiOn())) return confirmClassic(question, defaultValue)
227
+ const cp = await clack()
228
+ const v = await cp.confirm({ message: question, initialValue: !!defaultValue })
229
+ if (cp.isCancel(v)) abortSetup()
230
+ return v === true
231
+ }
232
+
233
+ /** Selezione singola (clack select, frecce). */
234
+ export async function select(question, choices, { defaultValue = 0 } = {}) {
235
+ if (!(await tuiOn())) return selectClassic(question, choices, { defaultValue })
236
+ const cp = await clack()
237
+ const idx = await cp.select({
238
+ message: question,
239
+ options: choices.map((ch, i) => ({ value: i, label: ch.label })),
240
+ initialValue: defaultValue ?? 0,
241
+ })
242
+ if (cp.isCancel(idx)) abortSetup()
243
+ return choices[idx]
244
+ }
245
+
246
+ /** Selezione multipla (clack multiselect, frecce + Spazio). */
247
+ export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
248
+ if (!(await tuiOn())) return checkboxClassic(question, choices, { defaultIndices })
249
+ const cp = await clack()
250
+ const options = choices.map((ch) => ({ value: ch.value, label: ch.label }))
251
+ const initialValues = defaultIndices.map((i) => choices[i]?.value).filter((v) => v !== undefined)
252
+ const vals = await cp.multiselect({ message: question, options, initialValues, required: false })
253
+ if (cp.isCancel(vals)) abortSetup()
254
+ const sel = new Set(vals)
255
+ return choices.filter((ch) => sel.has(ch.value))
256
+ }
257
+
258
+ /** Input mascherato per segreti (API key ecc.). */
259
+ export async function secret(question, { allowEmpty = false } = {}) {
260
+ if (!(await tuiOn())) return secretClassic(question, { allowEmpty })
261
+ const cp = await clack()
262
+ const v = await cp.password({
263
+ message: question,
264
+ validate: allowEmpty ? () => undefined : undefined,
265
+ })
266
+ if (cp.isCancel(v)) abortSetup()
267
+ return String(v ?? "")
268
+ }
269
+
156
270
  export function closePrompts() {
157
271
  input.removeAllListeners("data")
158
272
  try {
@@ -3,7 +3,31 @@
3
3
  export OC_SERVER='__OC_SERVER__'
4
4
  export OC_DIR='__OC_DIR__'
5
5
 
6
+ oc-sync-env() {
7
+ local cfg="${XDG_CONFIG_HOME:-$HOME/.config}/opencode-wyvern/config.json"
8
+ [ -f "$cfg" ] || return 0
9
+ local out k v
10
+ if command -v node >/dev/null 2>&1; then
11
+ out="$(node -e 'const c=require(process.argv[1]).entry||{};for(const k of ["host","user","server","dir"]){if(c[k]!=null)console.log(k+"="+c[k])}' "$cfg" 2>/dev/null)" || return 0
12
+ elif command -v python3 >/dev/null 2>&1; then
13
+ out="$(python3 -c 'import json,sys;e=json.load(open(sys.argv[1]))["entry"];[print(k+"="+str(e[k])) for k in ("host","user","server","dir") if e.get(k)]' "$cfg" 2>/dev/null)" || return 0
14
+ else
15
+ return 0
16
+ fi
17
+ while IFS='=' read -r k v; do
18
+ [ -n "$k" ] || continue
19
+ case "$k" in
20
+ host) export OC_HOST="$v" ;;
21
+ user) export OC_USER="$v" ;;
22
+ server) export OC_SERVER="$v" ;;
23
+ dir) export OC_DIR="$v" ;;
24
+ esac
25
+ done <<< "$out"
26
+ }
27
+ oc-sync-env
28
+
6
29
  oc-path() {
30
+ oc-sync-env
7
31
  if [ -z "$OC_OPENCODE" ]; then
8
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)
9
33
  [ -n "$OC_OPENCODE" ] || OC_OPENCODE='opencode'
@@ -12,6 +36,7 @@ oc-path() {
12
36
  }
13
37
 
14
38
  oc-connect() {
39
+ oc-sync-env
15
40
  local key="$HOME/.ssh/id_ed25519"
16
41
  local pub="$key.pub"
17
42
  if [ ! -f "$key" ]; then
@@ -29,11 +54,14 @@ oc-connect() {
29
54
  }
30
55
 
31
56
  oc() {
57
+ oc-sync-env
32
58
  local oc; oc="$(oc-path)"
59
+ oc-sessions-all >/dev/null 2>&1
33
60
  ssh -t "$OC_SERVER" "cd $OC_DIR && $oc"
34
61
  }
35
62
 
36
63
  oc-ssh() {
64
+ oc-sync-env
37
65
  ssh "$OC_SERVER"
38
66
  }
39
67
 
@@ -58,6 +86,7 @@ oc-cache-load() {
58
86
  }
59
87
 
60
88
  oc-sessions-all() {
89
+ oc-sync-env
61
90
  local oc b64 out
62
91
  py='import json,sys
63
92
  raw=sys.stdin.read()
@@ -171,9 +200,11 @@ oc-delete() {
171
200
  }
172
201
 
173
202
  oc-go() {
203
+ oc-sync-env
174
204
  local id="$1" dir="${2:-~}" norecap=0 oc exit_code
175
205
  [ "$3" = "--no-recap" ] && norecap=1
176
206
  oc="$(oc-path)"
207
+ oc-sessions-all >/dev/null 2>&1
177
208
  mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/opencode-wyvern"
178
209
  printf '%s\t%s\n' "$id" "${dir:-~}" > "${XDG_CACHE_HOME:-$HOME/.cache}/opencode-wyvern/last.tsv"
179
210
  if [ "$dir" = "~" ] || [ -z "$dir" ]; then
@@ -187,6 +218,7 @@ oc-go() {
187
218
  }
188
219
 
189
220
  oc-recap() {
221
+ oc-sync-env
190
222
  local last_exit="${1:--1}" err_time live cached list bar back m i id title dir updated last choice ss cid ctitle cdir
191
223
  err_time="$(date '+%Y-%m-%d %H:%M:%S')"
192
224
  bar="=============================================="
@@ -274,6 +306,7 @@ oc-open-tab() {
274
306
  }
275
307
 
276
308
  oc-resume() {
309
+ oc-sync-env
277
310
  local list rest first_term=1 mode="$1" term
278
311
  if [ "$mode" = "--all-tabs" ] || [ "$mode" = "-a" ]; then
279
312
  first_term=0
@@ -9,6 +9,22 @@ $env:OC_DIR = '__OC_DIR__' # cartella remota di default
9
9
 
10
10
  $global:OC_OPENCODE = $null # cache path, si azzera a ogni reload
11
11
 
12
+ function Sync-OcEnv {
13
+ # riseleziona OC_* dal config (fonte di verità), così anche in un
14
+ # ambiente con variabili stale i comandi usano server/dir corretti.
15
+ $cfg = Join-Path $env:USERPROFILE ".config\opencode-wyvern\config.json"
16
+ if (-not (Test-Path $cfg)) { return }
17
+ try {
18
+ $j = Get-Content $cfg -Raw | ConvertFrom-Json
19
+ if ($j.entry.host) { $env:OC_HOST = [string]$j.entry.host }
20
+ if ($j.entry.user) { $env:OC_USER = [string]$j.entry.user }
21
+ if ($j.entry.server) { $env:OC_SERVER = [string]$j.entry.server }
22
+ if ($j.entry.dir) { $env:OC_DIR = [string]$j.entry.dir }
23
+ } catch { }
24
+ }
25
+
26
+ Sync-OcEnv
27
+
12
28
  function Get-OcPath {
13
29
  if (-not $global:OC_OPENCODE) {
14
30
  $remotePath = "(command -v opencode || ls -t ~/.nvm/versions/node/*/bin/opencode 2>/dev/null | head -n1)"
@@ -20,6 +36,7 @@ function Get-OcPath {
20
36
  }
21
37
 
22
38
  function oc-connect {
39
+ Sync-OcEnv
23
40
  $key = "$env:USERPROFILE\.ssh\id_ed25519"
24
41
  $pub = "$env:USERPROFILE\.ssh\id_ed25519.pub"
25
42
 
@@ -42,11 +59,14 @@ function oc-connect {
42
59
  }
43
60
 
44
61
  function oc {
62
+ Sync-OcEnv
63
+ $null = @(Get-OcAllSessions)
45
64
  $oc = Get-OcPath
46
65
  ssh -t $env:OC_SERVER "cd $env:OC_DIR && $oc"
47
66
  }
48
67
 
49
68
  function oc-ssh {
69
+ Sync-OcEnv
50
70
  ssh $env:OC_SERVER
51
71
  }
52
72
 
@@ -154,6 +174,7 @@ function Get-OcRecentSessions {
154
174
  }
155
175
 
156
176
  function oc-sessions {
177
+ Sync-OcEnv
157
178
  $sessions = Get-OcRecentSessions
158
179
  if (-not $sessions) {
159
180
  Write-Host "Nessuna sessione nelle ultime 24 ore (o SSH a chiave non configurato - esegui oc-connect)." -ForegroundColor Yellow
@@ -192,6 +213,7 @@ function oc-find {
192
213
  [string]$Search = '',
193
214
  [int]$Limit = 30
194
215
  )
216
+ Sync-OcEnv
195
217
  $all = Get-OcAllSessions
196
218
  if (-not $all) {
197
219
  Write-Host "Nessuna sessione trovata (o SSH a chiave non configurato)." -ForegroundColor Yellow
@@ -234,6 +256,7 @@ function oc-delete {
234
256
  param(
235
257
  [string]$Search = ''
236
258
  )
259
+ Sync-OcEnv
237
260
  $all = Get-OcAllSessions
238
261
  if (-not $all) {
239
262
  Write-Host "Nessuna sessione trovata." -ForegroundColor Yellow
@@ -280,6 +303,8 @@ function oc-go {
280
303
  [string]$Dir = "~",
281
304
  [switch]$NoRecap
282
305
  )
306
+ Sync-OcEnv
307
+ $null = @(Get-OcAllSessions)
283
308
  $oc = Get-OcPath
284
309
  if ($Dir -eq '~' -or [string]::IsNullOrEmpty($Dir)) {
285
310
  $remoteCmd = "cd ~ && $oc -s $Id"
@@ -298,6 +323,7 @@ function Show-OcRecap {
298
323
  [int]$LastExit = -1,
299
324
  [datetime]$ErrTime = (Get-Date)
300
325
  )
326
+ Sync-OcEnv
301
327
  Write-Host ""
302
328
  Write-Host ("{0}" -f ('=' * 56)) -ForegroundColor DarkCyan
303
329
  Write-Host "Connessione terminata. Riepilogo per riprendere:" -ForegroundColor Cyan
@@ -411,6 +437,7 @@ function Get-OcTerminal {
411
437
 
412
438
  function Invoke-OcWarpResume {
413
439
  param([Parameter(Mandatory)]$Sessions)
440
+ Sync-OcEnv
414
441
  $dir = Get-WarpConfigDir
415
442
  if (-not $dir) { return $false }
416
443
  $oc = Get-OcPath
@@ -460,6 +487,7 @@ function oc-resume {
460
487
  [switch]$Warp,
461
488
  [switch]$AllTabs
462
489
  )
490
+ Sync-OcEnv
463
491
  $sessions = @(Get-OcRecentSessions)
464
492
 
465
493
  if ($sessions.Count -eq 0) {