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 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.2.0",
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",
@@ -36,7 +36,8 @@
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"
40
41
  },
41
42
  "dependencies": {
42
43
  "@clack/prompts": "^1.8.0"
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
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 = {
@@ -14,7 +15,7 @@ export const c = {
14
15
  // Backend TUI nativo Node (@clack/prompts) — cross-platform.
15
16
  // Quando stdin/stdout/stderr sono un terminale reale, i prompt diventano
16
17
  // dialoghi interattivi identici su Windows/pwsh, WSL, Linux e macOS
17
- // (frecce, Spazio, invio, Esc/Ctrl+C per annullare, exit code 1).
18
+ // (frecce, Spazio, invio, Esc/Ctrl+C per annullare, exit code 0).
18
19
  // Negli altri casi (pipe, CI, non-interattivo) si usa il fallback a righe.
19
20
  // `secret` mantiene l'input mascherato (via clack password o raw mode).
20
21
  //
@@ -39,15 +40,100 @@ async function clack() {
39
40
 
40
41
  const realTty = () => !!input.isTTY && !!output.isTTY && !!stderrStream.isTTY
41
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
+
42
67
  async function tuiOn() {
43
68
  const cp = await clack()
44
- return !!(cp && realTty())
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})`)}` }
45
131
  }
46
132
 
47
133
  function abortSetup() {
48
134
  closePrompts()
49
- console.log(c.yellow("\n annullato (exit 1)."))
50
- process.exit(1)
135
+ console.log(c.yellow(`\n ${cancelMsg}`))
136
+ process.exit(0)
51
137
  }
52
138
 
53
139
  function enableRaw() {
@@ -88,7 +174,7 @@ function readLineFallback(done) {
88
174
  }
89
175
 
90
176
  /** Legge una riga da tastiera. `masked` oscura la digitazione (per segreti); */
91
- /** gestisce backspace e Ctrl+C. */
177
+ /** gestisce backspace, Ctrl+C (annullamento pulito) e incolla (CTRL+SHIFT+V). */
92
178
  export function readLine({ masked = false, allowEmpty = false } = {}, done) {
93
179
  if (!enableRaw()) {
94
180
  readLineFallback(done)
@@ -97,6 +183,8 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
97
183
 
98
184
  let buf = ""
99
185
  const onData = (chunk) => {
186
+ // marcatori "bracketed paste" del terminale: se presenti vanno ignorati
187
+ chunk = chunk.replace(/\x1b\[200~|\x1b\[201~/g, "")
100
188
  for (const ch of chunk) {
101
189
  if (ch === "\r" || ch === "\n") {
102
190
  if (!allowEmpty && buf === "") continue
@@ -104,12 +192,13 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
104
192
  input.setRawMode(false)
105
193
  input.pause()
106
194
  output.write("\n")
107
- return done(buf)
195
+ // Windows e alcuni editor incollano con \r\n finale: scarta il \r
196
+ return done(buf.replace(/\r+$/u, ""))
108
197
  }
109
- if (ch === "\u0003") {
110
- input.setRawMode(false)
111
- input.pause()
112
- 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()
113
202
  }
114
203
  if (ch === "\u007f" || ch === "\b") {
115
204
  if (buf.length) {
@@ -132,13 +221,15 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
132
221
  async function askClassic(question, { defaultValue = "", hint = "", validate } = {}) {
133
222
  const suffix = hint ? ` ${c.dim(`(${hint})`)}` : ""
134
223
  const def = defaultValue ? c.dim(`[${defaultValue}]`) : ""
224
+ const err = ui("risposta non valida, riprova.", "invalid answer, try again.")
135
225
  // eslint-disable-next-line no-constant-condition
136
226
  for (;;) {
137
- output.write(`${c.cyan("?")} ${question}${suffix} ${def}\n> `)
227
+ output.write(`${c.cyan("?")} ${question}${suffix} ${def} ${c.dim(`(${qHint})`)}\n> `)
138
228
  const raw = await new Promise((resolve) => readLine({}, resolve))
229
+ if (/^q$/i.test(raw.trim())) abortSetup()
139
230
  const value = raw.trim() === "" ? defaultValue : raw.trim()
140
231
  if (validate && !validate(value)) {
141
- console.log(c.yellow(" risposta non valida, riprova."))
232
+ console.log(c.yellow(` ${err}`))
142
233
  continue
143
234
  }
144
235
  return value
@@ -146,15 +237,17 @@ async function askClassic(question, { defaultValue = "", hint = "", validate } =
146
237
  }
147
238
 
148
239
  async function confirmClassic(question, defaultValue = false) {
149
- const hint = defaultValue ? "S/n" : "s/N"
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.")
150
242
  // eslint-disable-next-line no-constant-condition
151
243
  for (;;) {
152
- output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint})`)}\n> `)
244
+ output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint}, ${qHint})`)}\n> `)
153
245
  const a = (await new Promise((resolve) => readLine({}, resolve))).trim().toLowerCase()
246
+ if (a === "q") abortSetup()
154
247
  if (a === "") return defaultValue
155
248
  if (/^(s|si|sì|y|yes)$/.test(a)) return true
156
249
  if (/^(n|no)$/.test(a)) return false
157
- console.log(c.yellow(" rispondi s o n."))
250
+ console.log(c.yellow(` ${err}`))
158
251
  }
159
252
  }
160
253
 
@@ -163,19 +256,19 @@ async function selectClassic(question, choices, { defaultValue = 0 } = {}) {
163
256
  choices.forEach((choice, i) => {
164
257
  console.log(` ${i + 1}. ${choice.label}`)
165
258
  })
166
- return choices[Number(await askClassic("Scelta (numero)", {
259
+ return choices[Number(await askClassic(ui("Scelta (numero)", "Choice (number)"), {
167
260
  defaultValue: String(defaultValue + 1),
168
261
  validate: (v) => /^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= choices.length,
169
262
  })) - 1]
170
263
  }
171
264
 
172
265
  async function checkboxClassic(question, choices, { defaultIndices = [] } = {}) {
173
- console.log(`\n${c.cyan("?")} ${question} ${c.dim("(invio = nessuno; es. 1,3,5)")}`)
266
+ console.log(`\n${c.cyan("?")} ${question} ${c.dim(ui("(invio = nessuno; es. 1,3,5)", "(blank = none; e.g. 1,3,5)"))}`)
174
267
  choices.forEach((choice, i) => {
175
268
  const sel = defaultIndices.includes(i) ? "•" : " "
176
269
  console.log(` ${sel} ${i + 1}. ${choice.label}`)
177
270
  })
178
- const answer = await askClassic("Seleziona (es. 1,3,5)", {
271
+ const answer = await askClassic(ui("Seleziona (es. 1,3,5)", "Select (e.g. 1,3,5)"), {
179
272
  validate: (v) => {
180
273
  const nums = v.split(/[\s,]+/).filter(Boolean)
181
274
  if (!nums.length) return true // invio = nessuno (salta)
@@ -188,8 +281,11 @@ async function checkboxClassic(question, choices, { defaultIndices = [] } = {})
188
281
  }
189
282
 
190
283
  async function secretClassic(question, { allowEmpty = false } = {}) {
191
- output.write(`${c.cyan("?")} ${question}\n> `)
192
- return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => resolve(v)))
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
+ }))
193
289
  }
194
290
 
195
291
  // --------------------------------------------------------------------------
@@ -201,20 +297,20 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
201
297
  if (!(await tuiOn())) return askClassic(question, { defaultValue, hint, validate })
202
298
  const cp = await clack()
203
299
  const message = hint ? `${question} (${hint})` : question
300
+ const errMsg = ui("risposta non valida, riprova.", "invalid answer, try again.")
204
301
  // eslint-disable-next-line no-constant-condition
205
302
  for (;;) {
206
- const raw = await cp.text({
207
- message,
303
+ const raw = await cp.text(clackOpts(message, {
208
304
  initialValue: defaultValue || undefined,
209
305
  validate: (val) => {
210
306
  const value = String(val ?? "").trim() === "" ? defaultValue : String(val).trim()
211
- return validate && !validate(value) ? "risposta non valida, riprova." : undefined
307
+ return validate && !validate(value) ? errMsg : undefined
212
308
  },
213
- })
309
+ }))
214
310
  if (cp.isCancel(raw)) abortSetup()
215
311
  const value = String(raw ?? "").trim() === "" ? defaultValue : String(raw).trim()
216
312
  if (validate && !validate(value)) {
217
- cp.log.error("risposta non valida, riprova.")
313
+ cp.log.error(errMsg)
218
314
  continue
219
315
  }
220
316
  return value
@@ -225,7 +321,11 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
225
321
  export async function confirm(question, defaultValue = false) {
226
322
  if (!(await tuiOn())) return confirmClassic(question, defaultValue)
227
323
  const cp = await clack()
228
- const v = await cp.confirm({ message: question, initialValue: !!defaultValue })
324
+ const v = await cp.confirm(clackOpts(question, {
325
+ initialValue: !!defaultValue,
326
+ active: isIt ? "sì" : "Yes",
327
+ inactive: isIt ? "no" : "No",
328
+ }))
229
329
  if (cp.isCancel(v)) abortSetup()
230
330
  return v === true
231
331
  }
@@ -234,11 +334,10 @@ export async function confirm(question, defaultValue = false) {
234
334
  export async function select(question, choices, { defaultValue = 0 } = {}) {
235
335
  if (!(await tuiOn())) return selectClassic(question, choices, { defaultValue })
236
336
  const cp = await clack()
237
- const idx = await cp.select({
238
- message: question,
337
+ const idx = await cp.select(clackOpts(question, {
239
338
  options: choices.map((ch, i) => ({ value: i, label: ch.label })),
240
339
  initialValue: defaultValue ?? 0,
241
- })
340
+ }))
242
341
  if (cp.isCancel(idx)) abortSetup()
243
342
  return choices[idx]
244
343
  }
@@ -249,7 +348,7 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
249
348
  const cp = await clack()
250
349
  const options = choices.map((ch) => ({ value: ch.value, label: ch.label }))
251
350
  const initialValues = defaultIndices.map((i) => choices[i]?.value).filter((v) => v !== undefined)
252
- const vals = await cp.multiselect({ message: question, options, initialValues, required: false })
351
+ const vals = await cp.multiselect(clackOpts(question, { options, initialValues, required: false }))
253
352
  if (cp.isCancel(vals)) abortSetup()
254
353
  const sel = new Set(vals)
255
354
  return choices.filter((ch) => sel.has(ch.value))
@@ -259,10 +358,9 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
259
358
  export async function secret(question, { allowEmpty = false } = {}) {
260
359
  if (!(await tuiOn())) return secretClassic(question, { allowEmpty })
261
360
  const cp = await clack()
262
- const v = await cp.password({
263
- message: question,
361
+ const v = await cp.password(clackOpts(question, {
264
362
  validate: allowEmpty ? () => undefined : undefined,
265
- })
363
+ }))
266
364
  if (cp.isCancel(v)) abortSetup()
267
365
  return String(v ?? "")
268
366
  }
@@ -275,4 +373,4 @@ export function closePrompts() {
275
373
  /* ok */
276
374
  }
277
375
  input.pause()
278
- }
376
+ }