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 CHANGED
@@ -15,12 +15,18 @@ oc-setup
15
15
 
16
16
  La procedura guidata:
17
17
 
18
- 1. **Moduli** — scegli quali sezioni attivare (default: tutte).
19
- 2. **Connessione** solo i dati necessari ai moduli scelti
18
+ 1. **Scenario** — scegli subito il contesto: **Client + Server** (tutto),
19
+ **Solo client**, **Solo server** o **Personalizzato** (sezioni singole).
20
+ 2. Con **Solo server** viene chiesto come applicare lo script: **via SSH da questo
21
+ client** o **in locale su questa macchina (localhost)** — in quest'ultimo caso
22
+ non serve host/porta né la chiave SSH.
23
+ 3. **Moduli** — con uno scenario preimpostato le sezioni sono già selezionate;
24
+ con "Personalizzato" le scegli una a una.
25
+ 4. **Connessione** — solo i dati necessari ai moduli scelti
20
26
  (host obbligatorio solo se usi SSH/sezioni remote; coi soli client basta l'alias).
21
- 3. **Provider / plugin / claude-mem** — solo se le sezioni corrispondenti sono attive;
22
- endpoint e chiavi sono richieste al volo.
23
- 4. **Applicazione** — chiave SSH, alias, profili client e bootstrap remoto.
27
+ 5. **Provider / plugin / claude-mem / comandi / MCP** — solo se le sezioni
28
+ corrispondenti sono attive; endpoint e chiavi vengono richieste al volo.
29
+ 6. **Applicazione** — chiave SSH, alias, profili client e bootstrap (remoto o locale).
24
30
 
25
31
  Le sezioni restano salvate in `~/.config/opencode-wyvern/config.json`
26
32
  (solo dati non sensibili) e puoi attivarle/disattivarle in seguito.
@@ -36,6 +42,17 @@ oc-setup deactivate <sezione> disattiva un modulo
36
42
  oc-setup print <sezione> stampa l'artefatto di una sezione
37
43
  ```
38
44
 
45
+ Nella procedura guidata: frecce/Spazio per navigare, `Invio` conferma,
46
+ `Esc`/`Ctrl+C` annullano, `q` esce. **CTRL+SHIFT+C** in un terminale vero copia
47
+ la selezione (gestito dal terminale, mai visto dal wizard) e **CTRL+SHIFT+V**
48
+ incolla: un token con la lettera `q` o con un `a-capo` finale viene accettato
49
+ senza chiudere il setup e senza caratteri spuri.
50
+
51
+ La **cartella remota di default** (del menu Connessione) parte già dalla home
52
+ dell'utente sul server: lasciala **vuota** per `~`, scrivi un percorso relativo
53
+ (es. `projects/foo` → `~/projects/foo`) o assoluto (`/srv/data`). Non viene mai
54
+ accodata al valore precedente.
55
+
39
56
  ## Moduli
40
57
 
41
58
  | Sezione | Dove | Cosa fa |
@@ -44,7 +61,8 @@ oc-setup print <sezione> stampa l'artefatto di una sezione
44
61
  | `client-pwsh` | locale | comandi `oc-*` nel profilo PowerShell (`oc-sessions`, `oc-go`, `oc-resume`, `oc-recap`, ...) |
45
62
  | `client-bash` | locale | stesso blocco in `~/.bashrc` |
46
63
  | `server` | remoto | controlla node/npm, installa opencode se manca, crea `~/.config/opencode` + `AGENTS.md` |
47
- | `commands` | remoto | comando `/baseline-ui` (`command/baseline-ui.md`) |
64
+ | `commands` | remoto | comandi custom in `command/*.md` (`/baseline-ui`, `/omniroute-restart`, `/review`, ...) |
65
+ | `mcp` | remoto | server MCP preset (Figma Desktop, Figma Developer MCP); token in `.env` |
48
66
  | `providers` | remoto | provider opencode in `opencode.json`; chiavi in `.env` |
49
67
  | `plugins` | remoto | plugin npm installati nella dir config e listati in `opencode.json` |
50
68
  | `claude-mem` | remoto | memoria: wrapper `plugins/claude-mem-plugin.js` |
@@ -52,16 +70,32 @@ oc-setup print <sezione> stampa l'artefatto di una sezione
52
70
  ## Provider presets
53
71
 
54
72
  La sezione `providers` propone (checkbox): **GitHub Copilot**, **Google Gemini**,
55
- **OpenCode Zen**, **Anthropic Claude**, **OpenAI / Codex** e **OmniRoute**
56
- (gateway multi-modello con supporto `auto/*`). Le chiavi vengono chieste al setup
57
- solo per i provider che le richiedono (`GOOGLE_GENERATIVE_AI_API_KEY`,
58
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) e finiscono solo nel `.env` remoto.
59
- Per OmniRoute viene chiesto l'URL del gateway (default `http://127.0.0.1:20128`).
73
+ **OpenCode Zen**, **Anthropic Claude**, **OpenAI / Codex**, **OmniRoute**
74
+ (gateway multi-modello con supporto `auto/*`) e **Ollama / vLLM** (modelli locali
75
+ OpenAI-compatible: viene chiesto il base URL, default `http://127.0.0.1:11434/v1`).
76
+ Le chiavi vengono chieste al setup solo per i provider che le richiedono
77
+ (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) e
78
+ finiscono solo nel `.env` remoto. Per OmniRoute viene chiesto l'URL del gateway
79
+ (default `http://127.0.0.1:20128`).
60
80
 
61
81
  Il modello default è scelto automaticamente (con OmniRoute: `omniroute/auto/best-coding`,
62
82
  altrimenti il primo modello del primo provider attivo), insieme a `small_model`.
63
83
  Un'opzione del setup aggiunge i tuning collaudati `tool_output` e `compaction`.
64
84
 
85
+ ## Comandi custom
86
+
87
+ La sezione `commands` installa agent in `~/.config/opencode/command/*.md`:
88
+ `/baseline-ui` (baseline interfacce), `/omniroute-restart` (riavvia il container
89
+ Docker OmniRoute e attende che sia `healthy`), `/review`, `/refactor`, `/tests`,
90
+ `/commit`, `/explain`.
91
+
92
+ ## MCP
93
+
94
+ La sezione `mcp` scrive il blocco `mcp` di `opencode.json` con i preset:
95
+ **Figma Desktop** (Dev Mode, `http://127.0.0.1:3845/mcp`, nessun segreto) e
96
+ **Figma Developer MCP** (stdio via `npx figma-developer-mcp`; il Figma personal
97
+ token finisce in `.env` sul server, mai in `config.json`).
98
+
65
99
  ## Client
66
100
 
67
101
  I client espongono i comandi `oc-*` e mostrano le **sessioni delle ultime 24 ore**
package/bin/oc-setup.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { run, cmdStatus, cmdActivate, cmdDeactivate, cmdGenerate, cmdPrint, runPrintClient } from "../src/setup.js"
3
3
  import { runUninstall } from "../src/uninstall.js"
4
+ import { ui } from "../src/i18n.js"
4
5
 
5
6
  const argv = process.argv.slice(2)
6
7
 
7
- const HELP = `oc-setup — OpenCode Wyvern
8
+ const HELP = ui(`oc-setup — OpenCode Wyvern
8
9
 
9
10
  Setup modulare per OpenCode: installi tutto, attivi quello che vuoi.
10
11
  Nessun dato privato è incluso nel pacchetto: host, utente, chiavi e
@@ -22,8 +23,35 @@ Uso:
22
23
  oc-setup --help | -h questo aiuto
23
24
 
24
25
  Sezioni:
25
- ssh, client-pwsh, client-bash, server, commands, providers, plugins, claude-mem
26
- `
26
+ ssh, client-pwsh, client-bash, server, commands, mcp, providers, plugins, claude-mem
27
+
28
+ Uscita dal wizard:
29
+ TUI: Esc / Ctrl+C Fallback (NON terminale): rispondi 'q'
30
+ Lingua: rilevata dalla locale di sistema; override con OC_LANG=it o OC_LANG=en
31
+ `, `oc-setup — OpenCode Wyvern
32
+
33
+ Modular setup for OpenCode: install everything, activate what you want.
34
+ No private data is included in the package: host, user, keys and API
35
+ keys are requested/created locally during execution.
36
+
37
+ Usage:
38
+ oc-setup starts the interactive wizard
39
+ oc-setup generate re-applies active sections from config
40
+ oc-setup uninstall complete uninstall (client, config, cache, SSH alias)
41
+ oc-setup status shows modules and status
42
+ oc-setup activate <section> activates a module
43
+ oc-setup deactivate <section> deactivates a module
44
+ oc-setup print <section> prints the artifact of a section
45
+ oc-setup --version | -v shows the version
46
+ oc-setup --help | -h this help
47
+
48
+ Sections:
49
+ ssh, client-pwsh, client-bash, server, commands, mcp, providers, plugins, claude-mem
50
+
51
+ Leaving the wizard:
52
+ TUI: Esc / Ctrl+C Fallback (NON terminal): answer 'q'
53
+ Language: detected from system locale; override with OC_LANG=it or OC_LANG=en
54
+ `)
27
55
 
28
56
  if (argv.includes("--help") || argv.includes("-h")) {
29
57
  console.log(HELP)
@@ -39,7 +67,7 @@ const printIdx = argv.indexOf("--print-client")
39
67
  if (printIdx !== -1) {
40
68
  const kind = argv[printIdx + 1]
41
69
  if (kind !== "pwsh" && kind !== "bash") {
42
- console.error("uso: oc-setup --print-client <pwsh|bash>")
70
+ console.error(ui("uso: oc-setup --print-client <pwsh|bash>", "usage: oc-setup --print-client <pwsh|bash>"))
43
71
  process.exit(2)
44
72
  }
45
73
  await runPrintClient(kind)
@@ -53,11 +81,11 @@ try {
53
81
  await cmdStatus()
54
82
  break
55
83
  case "activate":
56
- if (!arg1) fail("uso: oc-setup activate <sezione>")
84
+ if (!arg1) fail(ui("uso: oc-setup activate <sezione>", "usage: oc-setup activate <section>"))
57
85
  await cmdActivate(arg1)
58
86
  break
59
87
  case "deactivate":
60
- if (!arg1) fail("uso: oc-setup deactivate <sezione>")
88
+ if (!arg1) fail(ui("uso: oc-setup deactivate <sezione>", "usage: oc-setup deactivate <section>"))
61
89
  await cmdDeactivate(arg1)
62
90
  break
63
91
  case "generate":
@@ -67,14 +95,14 @@ try {
67
95
  await runUninstall()
68
96
  break
69
97
  case "print":
70
- if (!arg1) fail("uso: oc-setup print <sezione>")
98
+ if (!arg1) fail(ui("uso: oc-setup print <sezione>", "usage: oc-setup print <section>"))
71
99
  await cmdPrint(arg1)
72
100
  break
73
101
  case undefined:
74
102
  await run(argv)
75
103
  break
76
104
  default:
77
- fail(`comando sconosciuto: ${sub}`)
105
+ fail(ui("comando sconosciuto:", "unknown command:") + ` ${sub}`)
78
106
  }
79
107
  } catch (err) {
80
108
  console.error(`\n[error] ${err && err.message ? err.message : err}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-wyvern",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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"
@@ -36,6 +36,10 @@
36
36
  "url": "git+https://github.com/Rhaegal222/opencode-wyvern.git"
37
37
  },
38
38
  "scripts": {
39
- "test": "node bin/oc-setup.js --help"
39
+ "test": "node bin/oc-setup.js --help",
40
+ "prepack": "node scripts/check-secrets.mjs"
41
+ },
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.8.0"
40
44
  }
41
- }
45
+ }
package/src/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import fs from "node:fs"
2
+ import path from "node:path"
2
3
  import crypto from "node:crypto"
3
- import { render, readTemplate } from "./config.js"
4
+ import { render, readTemplate, ensureDir } from "./config.js"
4
5
  import { pwshProfilePath, bashRcPath } from "./shell.js"
5
6
  import { c } from "./prompts.js"
7
+ import { ui } from "./i18n.js"
6
8
 
7
9
  export const MARKER = "# ---- OpenCode Wyvern (auto-generato) ----"
8
10
  /** Marker storici: un blocco generato in passato va comunque rimosso. */
@@ -31,6 +33,7 @@ export function renderClient(kind, { server, host, user, dir }) {
31
33
 
32
34
  /** Sostituisce/crea il blocco generato in un profilo, con backup. */
33
35
  function upsertBlock(filePath, block, { dryRun = false } = {}) {
36
+ ensureDir(path.dirname(filePath)) // la dir del profilo può non esistere (machina nuova)
34
37
  const existed = fs.existsSync(filePath)
35
38
  let content = existed ? fs.readFileSync(filePath, "utf8") : ""
36
39
 
@@ -100,10 +103,11 @@ export async function installClient(cfg, { targets = ["pwsh", "bash"], onlyPrint
100
103
  results.push(upsertBlock(target.path(), block))
101
104
  }
102
105
 
103
- console.log(c.green("\nClient installato:"))
106
+ console.log(c.green(`${ui("\nClient installato:", "\nClient installed:")}`))
104
107
  for (const r of results) {
105
108
  console.log(` - ${r.filePath}${r.backup ? ` (backup: ${r.backup})` : ""}`)
106
109
  }
107
- console.log(c.dim("\nRicarica il profilo con: . $PROFILE (PowerShell) / source ~/.bashrc (bash)"))
110
+ console.log(c.dim(ui("\nRicarica il profilo con: . $PROFILE (PowerShell) / source ~/.bashrc (bash)",
111
+ "\nReload your profile with: . $PROFILE (PowerShell) / source ~/.bashrc (bash)")))
108
112
  return { installed: results }
109
113
  }
package/src/i18n.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Localizzazione interfacce.
3
+ *
4
+ * Rileva la locale di sistema e rende coerente TUTTA la GUI:
5
+ * - locale che inizia con "it" → italiano
6
+ * - altrimenti → inglese
7
+ *
8
+ * Override: `OC_LANG=it` oppure `OC_LANG=en`.
9
+ */
10
+ const raw = process.env.OC_LANG || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || ""
11
+ const sys = raw || (Intl?.DateTimeFormat?.().resolvedOptions?.().locale ?? "") || "en"
12
+
13
+ export const LANG = /^it\b/i.test(sys) ? "it" : "en"
14
+ export const isIt = LANG === "it"
15
+ export const ui = (it, en) => (isIt ? it : en)
16
+
17
+ export const qHint = ui("q=esci", "q=quit")
18
+ export const qLegend = ui("q=Esci", "q=Exit")
19
+ export const cancelMsg = ui("annullato.", "cancelled.")
package/src/prompts.js CHANGED
@@ -1,4 +1,5 @@
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
+ import { ui, isIt, qHint, qLegend, cancelMsg } from "./i18n.js"
2
3
 
3
4
  const color = (code, s) => `\x1b[${code}m${s}\x1b[0m`
4
5
  export const c = {
@@ -10,6 +11,131 @@ export const c = {
10
11
  bold: (s) => color(1, s),
11
12
  }
12
13
 
14
+ // --------------------------------------------------------------------------
15
+ // Backend TUI nativo Node (@clack/prompts) — cross-platform.
16
+ // Quando stdin/stdout/stderr sono un terminale reale, i prompt diventano
17
+ // dialoghi interattivi identici su Windows/pwsh, WSL, Linux e macOS
18
+ // (frecce, Spazio, invio, Esc/Ctrl+C per annullare, exit code 0).
19
+ // Negli altri casi (pipe, CI, non-interattivo) si usa il fallback a righe.
20
+ // `secret` mantiene l'input mascherato (via clack password o raw mode).
21
+ //
22
+ // Il modulo @clack/prompts è importato dinamicamente: se manca (repo senza
23
+ // `npm install`) si cade automaticamente sul fallback a righe.
24
+ // --------------------------------------------------------------------------
25
+
26
+ let cpModule = null
27
+ let cpTried = false
28
+
29
+ async function clack() {
30
+ if (!cpTried) {
31
+ cpTried = true
32
+ try {
33
+ cpModule = await import("@clack/prompts")
34
+ } catch {
35
+ cpModule = null // dipendenza non installata: fallback a righe
36
+ }
37
+ }
38
+ return cpModule
39
+ }
40
+
41
+ const realTty = () => !!input.isTTY && !!output.isTTY && !!stderrStream.isTTY
42
+
43
+ // --------------------------------------------------------------------------
44
+ // Footer (@clack/prompts): localizziamo gli array di istruzioni esportati
45
+ // (MULTISELECT_INSTRUCTIONS/SELECT_INSTRUCTIONS) PRIMA che il prompt li usi.
46
+ // Non intercettiamo mai output.write: clack salva il frame reso per calcolare
47
+ // cursore/cancellazione, quindi un contenuto modificato dopo lo write lo
48
+ // desincronizza (le righe si duplicano senza refresh).
49
+ // --------------------------------------------------------------------------
50
+ function applyInstructions(cp) {
51
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`
52
+ const multi = [
53
+ `${dim("\u2191/\u2193")} ${ui("per navigare", "to navigate")}`,
54
+ `${dim(ui("Spazio:", "Space:"))} ${ui("seleziona", "select")}`,
55
+ `${dim(ui("Invio:", "Enter:"))} ${ui("conferma", "confirm")}`,
56
+ `${dim("q:")} ${qLegend}`,
57
+ ]
58
+ const sel = [
59
+ `${dim("\u2191/\u2193")} ${ui("per navigare", "to navigate")}`,
60
+ `${dim(ui("Invio:", "Enter:"))} ${ui("conferma", "confirm")}`,
61
+ `${dim("q:")} ${qLegend}`,
62
+ ]
63
+ if (Array.isArray(cp.MULTISELECT_INSTRUCTIONS)) cp.MULTISELECT_INSTRUCTIONS.splice(0, cp.MULTISELECT_INSTRUCTIONS.length, ...multi)
64
+ if (Array.isArray(cp.SELECT_INSTRUCTIONS)) cp.SELECT_INSTRUCTIONS.splice(0, cp.SELECT_INSTRUCTIONS.length, ...sel)
65
+ }
66
+
67
+ async function tuiOn() {
68
+ const cp = await clack()
69
+ const ok = !!(cp && realTty())
70
+ if (ok) applyInstructions(cp)
71
+ return ok
72
+ }
73
+
74
+ // --------------------------------------------------------------------------
75
+ // Uscita con "q" anche in TUI: @clack/prompts supporta `signal` (abort → il
76
+ // prompt si risolve in CANCEL_SYMBOL). Ascoltiamo il keypress 'q' su stdin e
77
+ // abortiamo il segnale condiviso; Esc/Ctrl+C restano gestiti da clack.
78
+ // NB: un incolla (CTRL+SHIFT+V) arriva come raffica di keypress: se il testo
79
+ // contiene una 'q' non deve chiudere il wizard. Riconosciamo la raffica dalla
80
+ // frequenza dei caratteri e mettiamo una tolleranza per la prima lettera.
81
+ // --------------------------------------------------------------------------
82
+ const quitKey = new AbortController()
83
+ let quitArmed = false
84
+ let lastKeyTs = 0
85
+ let burstLen = 0
86
+ let quitTimer = null
87
+
88
+ function cancelQuit() {
89
+ if (quitTimer) {
90
+ clearTimeout(quitTimer)
91
+ quitTimer = null
92
+ }
93
+ }
94
+
95
+ function armQuitKey() {
96
+ if (quitArmed) return
97
+ quitArmed = true
98
+ try {
99
+ input.on("keypress", (str) => {
100
+ const now = Date.now()
101
+ burstLen = now - lastKeyTs > 400 ? 1 : burstLen + 1
102
+ lastKeyTs = now
103
+
104
+ // qualunque altro carattere cancella un'eventuale 'q' in sospeso
105
+ if (str !== "q" && str !== "Q") {
106
+ cancelQuit()
107
+ return
108
+ }
109
+ // 'q' nel mezzo di una raffica (incolla / tasto ripetuto): ignora
110
+ if (burstLen > 1) {
111
+ cancelQuit()
112
+ return
113
+ }
114
+ // 'q' isolata: aspetta ~200ms: se arrivano altri caratteri era un
115
+ // incolla e la prima lettera coincideva con la "q", altrimenti esce.
116
+ if (quitTimer) return
117
+ quitTimer = setTimeout(() => {
118
+ quitTimer = null
119
+ quitKey.abort()
120
+ }, 200)
121
+ })
122
+ } catch {
123
+ /* stdin senza keypress: resta Esc/Ctrl+C */
124
+ }
125
+ }
126
+
127
+ /** Opzioni clack con signal condiviso e suffisso "q=esci/q=quit" nel messaggio. */
128
+ function clackOpts(message, opts = {}) {
129
+ armQuitKey()
130
+ return { ...opts, signal: quitKey.signal, message: `${message} ${color(90, `(${qHint})`)}` }
131
+ }
132
+
133
+ function abortSetup() {
134
+ closePrompts()
135
+ console.log(c.yellow(`\n ${cancelMsg}`))
136
+ process.exit(0)
137
+ }
138
+
13
139
  function enableRaw() {
14
140
  try {
15
141
  input.setRawMode(true)
@@ -48,7 +174,7 @@ function readLineFallback(done) {
48
174
  }
49
175
 
50
176
  /** Legge una riga da tastiera. `masked` oscura la digitazione (per segreti); */
51
- /** gestisce backspace e Ctrl+C. */
177
+ /** gestisce backspace, Ctrl+C (annullamento pulito) e incolla (CTRL+SHIFT+V). */
52
178
  export function readLine({ masked = false, allowEmpty = false } = {}, done) {
53
179
  if (!enableRaw()) {
54
180
  readLineFallback(done)
@@ -57,6 +183,8 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
57
183
 
58
184
  let buf = ""
59
185
  const onData = (chunk) => {
186
+ // marcatori "bracketed paste" del terminale: se presenti vanno ignorati
187
+ chunk = chunk.replace(/\x1b\[200~|\x1b\[201~/g, "")
60
188
  for (const ch of chunk) {
61
189
  if (ch === "\r" || ch === "\n") {
62
190
  if (!allowEmpty && buf === "") continue
@@ -64,12 +192,13 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
64
192
  input.setRawMode(false)
65
193
  input.pause()
66
194
  output.write("\n")
67
- return done(buf)
195
+ // Windows e alcuni editor incollano con \r\n finale: scarta il \r
196
+ return done(buf.replace(/\r+$/u, ""))
68
197
  }
69
- if (ch === "\u0003") {
70
- input.setRawMode(false)
71
- input.pause()
72
- process.exit(130)
198
+ if (ch === "\u0003" || ch === "\u0004") {
199
+ // Ctrl+C / Ctrl+D: annullamento pulito (uguale a clack isCancel),
200
+ // MAI un'uscita "muta" che lascia il terminale sporco.
201
+ abortSetup()
73
202
  }
74
203
  if (ch === "\u007f" || ch === "\b") {
75
204
  if (buf.length) {
@@ -85,57 +214,61 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
85
214
  input.on("data", onData)
86
215
  }
87
216
 
88
- /** Domanda a testo semplice con default opzionale. */
89
- export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
217
+ // --------------------------------------------------------------------------
218
+ // Implementazioni classiche (fallback a righe, stesse API precedenti).
219
+ // --------------------------------------------------------------------------
220
+
221
+ async function askClassic(question, { defaultValue = "", hint = "", validate } = {}) {
90
222
  const suffix = hint ? ` ${c.dim(`(${hint})`)}` : ""
91
223
  const def = defaultValue ? c.dim(`[${defaultValue}]`) : ""
224
+ const err = ui("risposta non valida, riprova.", "invalid answer, try again.")
92
225
  // eslint-disable-next-line no-constant-condition
93
226
  for (;;) {
94
- output.write(`${c.cyan("?")} ${question}${suffix} ${def}\n> `)
227
+ output.write(`${c.cyan("?")} ${question}${suffix} ${def} ${c.dim(`(${qHint})`)}\n> `)
95
228
  const raw = await new Promise((resolve) => readLine({}, resolve))
229
+ if (/^q$/i.test(raw.trim())) abortSetup()
96
230
  const value = raw.trim() === "" ? defaultValue : raw.trim()
97
231
  if (validate && !validate(value)) {
98
- console.log(c.yellow(" risposta non valida, riprova."))
232
+ console.log(c.yellow(` ${err}`))
99
233
  continue
100
234
  }
101
235
  return value
102
236
  }
103
237
  }
104
238
 
105
- /** Domanda sì/no con default indicato in maiuscolo. */
106
- export async function confirm(question, defaultValue = false) {
107
- const hint = defaultValue ? "S/n" : "s/N"
239
+ async function confirmClassic(question, defaultValue = false) {
240
+ const hint = defaultValue ? ui("S/n", "Y/n") : ui("s/N", "y/N")
241
+ const err = ui("rispondi s o n.", "answer y or n.")
108
242
  // eslint-disable-next-line no-constant-condition
109
243
  for (;;) {
110
- output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint})`)}\n> `)
244
+ output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint}, ${qHint})`)}\n> `)
111
245
  const a = (await new Promise((resolve) => readLine({}, resolve))).trim().toLowerCase()
246
+ if (a === "q") abortSetup()
112
247
  if (a === "") return defaultValue
113
248
  if (/^(s|si|sì|y|yes)$/.test(a)) return true
114
249
  if (/^(n|no)$/.test(a)) return false
115
- console.log(c.yellow(" rispondi s o n."))
250
+ console.log(c.yellow(` ${err}`))
116
251
  }
117
252
  }
118
253
 
119
- /** Selezione singola (indice numerico). */
120
- export async function select(question, choices, { defaultValue = 0 } = {}) {
254
+ async function selectClassic(question, choices, { defaultValue = 0 } = {}) {
121
255
  console.log(`\n${c.cyan("?")} ${question}`)
122
256
  choices.forEach((choice, i) => {
123
257
  console.log(` ${i + 1}. ${choice.label}`)
124
258
  })
125
- return choices[Number(await ask("Scelta (numero)", {
259
+ return choices[Number(await askClassic(ui("Scelta (numero)", "Choice (number)"), {
126
260
  defaultValue: String(defaultValue + 1),
127
261
  validate: (v) => /^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= choices.length,
128
262
  })) - 1]
129
263
  }
130
264
 
131
- /** Selezione multipla: numeri separati da virgola/spazio (es. "1,3,5"); invio = nessuna. */
132
- export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
133
- console.log(`\n${c.cyan("?")} ${question} ${c.dim("(invio = nessuno; es. 1,3,5)")}`)
265
+ async function checkboxClassic(question, choices, { defaultIndices = [] } = {}) {
266
+ console.log(`\n${c.cyan("?")} ${question} ${c.dim(ui("(invio = nessuno; es. 1,3,5)", "(blank = none; e.g. 1,3,5)"))}`)
134
267
  choices.forEach((choice, i) => {
135
268
  const sel = defaultIndices.includes(i) ? "•" : " "
136
269
  console.log(` ${sel} ${i + 1}. ${choice.label}`)
137
270
  })
138
- const answer = await ask("Seleziona (es. 1,3,5)", {
271
+ const answer = await askClassic(ui("Seleziona (es. 1,3,5)", "Select (e.g. 1,3,5)"), {
139
272
  validate: (v) => {
140
273
  const nums = v.split(/[\s,]+/).filter(Boolean)
141
274
  if (!nums.length) return true // invio = nessuno (salta)
@@ -147,10 +280,89 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
147
280
  return [...new Set(nums.map((n) => Number(n) - 1))].map((i) => choices[i])
148
281
  }
149
282
 
283
+ async function secretClassic(question, { allowEmpty = false } = {}) {
284
+ output.write(`${c.cyan("?")} ${question} ${c.dim(`(${qHint})`)}\n> `)
285
+ return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => {
286
+ if (/^q$/i.test(v.trim())) abortSetup()
287
+ resolve(v)
288
+ }))
289
+ }
290
+
291
+ // --------------------------------------------------------------------------
292
+ // API pubbliche: TUI @clack/prompts (TTY) con fallback classico.
293
+ // --------------------------------------------------------------------------
294
+
295
+ /** Domanda a testo semplice con default opzionale (clack text). */
296
+ export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
297
+ if (!(await tuiOn())) return askClassic(question, { defaultValue, hint, validate })
298
+ const cp = await clack()
299
+ const message = hint ? `${question} (${hint})` : question
300
+ const errMsg = ui("risposta non valida, riprova.", "invalid answer, try again.")
301
+ // eslint-disable-next-line no-constant-condition
302
+ for (;;) {
303
+ const raw = await cp.text(clackOpts(message, {
304
+ initialValue: defaultValue || undefined,
305
+ validate: (val) => {
306
+ const value = String(val ?? "").trim() === "" ? defaultValue : String(val).trim()
307
+ return validate && !validate(value) ? errMsg : undefined
308
+ },
309
+ }))
310
+ if (cp.isCancel(raw)) abortSetup()
311
+ const value = String(raw ?? "").trim() === "" ? defaultValue : String(raw).trim()
312
+ if (validate && !validate(value)) {
313
+ cp.log.error(errMsg)
314
+ continue
315
+ }
316
+ return value
317
+ }
318
+ }
319
+
320
+ /** Domanda sì/no (clack confirm). */
321
+ export async function confirm(question, defaultValue = false) {
322
+ if (!(await tuiOn())) return confirmClassic(question, defaultValue)
323
+ const cp = await clack()
324
+ const v = await cp.confirm(clackOpts(question, {
325
+ initialValue: !!defaultValue,
326
+ active: isIt ? "sì" : "Yes",
327
+ inactive: isIt ? "no" : "No",
328
+ }))
329
+ if (cp.isCancel(v)) abortSetup()
330
+ return v === true
331
+ }
332
+
333
+ /** Selezione singola (clack select, frecce). */
334
+ export async function select(question, choices, { defaultValue = 0 } = {}) {
335
+ if (!(await tuiOn())) return selectClassic(question, choices, { defaultValue })
336
+ const cp = await clack()
337
+ const idx = await cp.select(clackOpts(question, {
338
+ options: choices.map((ch, i) => ({ value: i, label: ch.label })),
339
+ initialValue: defaultValue ?? 0,
340
+ }))
341
+ if (cp.isCancel(idx)) abortSetup()
342
+ return choices[idx]
343
+ }
344
+
345
+ /** Selezione multipla (clack multiselect, frecce + Spazio). */
346
+ export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
347
+ if (!(await tuiOn())) return checkboxClassic(question, choices, { defaultIndices })
348
+ const cp = await clack()
349
+ const options = choices.map((ch) => ({ value: ch.value, label: ch.label }))
350
+ const initialValues = defaultIndices.map((i) => choices[i]?.value).filter((v) => v !== undefined)
351
+ const vals = await cp.multiselect(clackOpts(question, { options, initialValues, required: false }))
352
+ if (cp.isCancel(vals)) abortSetup()
353
+ const sel = new Set(vals)
354
+ return choices.filter((ch) => sel.has(ch.value))
355
+ }
356
+
150
357
  /** Input mascherato per segreti (API key ecc.). */
151
358
  export async function secret(question, { allowEmpty = false } = {}) {
152
- output.write(`${c.cyan("?")} ${question}\n> `)
153
- return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => resolve(v)))
359
+ if (!(await tuiOn())) return secretClassic(question, { allowEmpty })
360
+ const cp = await clack()
361
+ const v = await cp.password(clackOpts(question, {
362
+ validate: allowEmpty ? () => undefined : undefined,
363
+ }))
364
+ if (cp.isCancel(v)) abortSetup()
365
+ return String(v ?? "")
154
366
  }
155
367
 
156
368
  export function closePrompts() {
@@ -161,4 +373,4 @@ export function closePrompts() {
161
373
  /* ok */
162
374
  }
163
375
  input.pause()
164
- }
376
+ }