opencode-wyvern 0.2.0 → 0.2.2

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
@@ -8,19 +8,59 @@ 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.1
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
 
16
50
  La procedura guidata:
17
51
 
18
- 1. **Moduli** — scegli quali sezioni attivare (default: tutte).
19
- 2. **Connessione** solo i dati necessari ai moduli scelti
52
+ 1. **Scenario** — scegli subito il contesto: **Client + Server** (tutto),
53
+ **Solo client**, **Solo server** o **Personalizzato** (sezioni singole).
54
+ 2. Con **Solo server** viene chiesto come applicare lo script: **via SSH da questo
55
+ client** o **in locale su questa macchina (localhost)** — in quest'ultimo caso
56
+ non serve host/porta né la chiave SSH.
57
+ 3. **Moduli** — con uno scenario preimpostato le sezioni sono già selezionate;
58
+ con "Personalizzato" le scegli una a una.
59
+ 4. **Connessione** — solo i dati necessari ai moduli scelti
20
60
  (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.
61
+ 5. **Provider / plugin / claude-mem / comandi / MCP** — solo se le sezioni
62
+ corrispondenti sono attive; endpoint e chiavi vengono richieste al volo.
63
+ 6. **Applicazione** — chiave SSH, alias, profili client e bootstrap (remoto o locale).
24
64
 
25
65
  Le sezioni restano salvate in `~/.config/opencode-wyvern/config.json`
26
66
  (solo dati non sensibili) e puoi attivarle/disattivarle in seguito.
@@ -36,6 +76,17 @@ oc-setup deactivate <sezione> disattiva un modulo
36
76
  oc-setup print <sezione> stampa l'artefatto di una sezione
37
77
  ```
38
78
 
79
+ Nella procedura guidata: frecce/Spazio per navigare, `Invio` conferma,
80
+ `Esc`/`Ctrl+C` annullano, `q` esce. **CTRL+SHIFT+C** in un terminale vero copia
81
+ la selezione (gestito dal terminale, mai visto dal wizard) e **CTRL+SHIFT+V**
82
+ incolla: un token con la lettera `q` o con un `a-capo` finale viene accettato
83
+ senza chiudere il setup e senza caratteri spuri.
84
+
85
+ La **cartella remota di default** (del menu Connessione) parte già dalla home
86
+ dell'utente sul server: lasciala **vuota** per `~`, scrivi un percorso relativo
87
+ (es. `projects/foo` → `~/projects/foo`) o assoluto (`/srv/data`). Non viene mai
88
+ accodata al valore precedente.
89
+
39
90
  ## Moduli
40
91
 
41
92
  | Sezione | Dove | Cosa fa |
@@ -44,7 +95,8 @@ oc-setup print <sezione> stampa l'artefatto di una sezione
44
95
  | `client-pwsh` | locale | comandi `oc-*` nel profilo PowerShell (`oc-sessions`, `oc-go`, `oc-resume`, `oc-recap`, ...) |
45
96
  | `client-bash` | locale | stesso blocco in `~/.bashrc` |
46
97
  | `server` | remoto | controlla node/npm, installa opencode se manca, crea `~/.config/opencode` + `AGENTS.md` |
47
- | `commands` | remoto | comando `/baseline-ui` (`command/baseline-ui.md`) |
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` |
48
100
  | `providers` | remoto | provider opencode in `opencode.json`; chiavi in `.env` |
49
101
  | `plugins` | remoto | plugin npm installati nella dir config e listati in `opencode.json` |
50
102
  | `claude-mem` | remoto | memoria: wrapper `plugins/claude-mem-plugin.js` |
@@ -52,16 +104,32 @@ oc-setup print <sezione> stampa l'artefatto di una sezione
52
104
  ## Provider presets
53
105
 
54
106
  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`).
107
+ **OpenCode Zen**, **Anthropic Claude**, **OpenAI / Codex**, **OmniRoute**
108
+ (gateway multi-modello con supporto `auto/*`) e **Ollama / vLLM** (modelli locali
109
+ OpenAI-compatible: viene chiesto il base URL, default `http://127.0.0.1:11434/v1`).
110
+ Le chiavi vengono chieste al setup solo per i provider che le richiedono
111
+ (`GOOGLE_GENERATIVE_AI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) e
112
+ finiscono solo nel `.env` remoto. Per OmniRoute viene chiesto l'URL del gateway
113
+ (default `http://127.0.0.1:20128`).
60
114
 
61
115
  Il modello default è scelto automaticamente (con OmniRoute: `omniroute/auto/best-coding`,
62
116
  altrimenti il primo modello del primo provider attivo), insieme a `small_model`.
63
117
  Un'opzione del setup aggiunge i tuning collaudati `tool_output` e `compaction`.
64
118
 
119
+ ## Comandi custom
120
+
121
+ La sezione `commands` installa agent in `~/.config/opencode/command/*.md`:
122
+ `/baseline-ui` (baseline interfacce), `/omniroute-restart` (riavvia il container
123
+ Docker OmniRoute e attende che sia `healthy`), `/review`, `/refactor`, `/tests`,
124
+ `/commit`, `/explain`.
125
+
126
+ ## MCP
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`).
132
+
65
133
  ## Client
66
134
 
67
135
  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.2",
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() {
@@ -67,28 +153,38 @@ function enableRaw() {
67
153
  * piping/test e ambiente completamente non interattivo).
68
154
  */
69
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
+ }
70
166
 
71
167
  function readLineFallback(done) {
72
- const next = () => {
73
- if (fallbackQueue.length) {
74
- const v = fallbackQueue.shift()
75
- return done(v)
76
- }
77
- const onData = (chunk) => {
78
- const text = chunk.toString()
79
- const parts = text.split(/\r\n|\r|\n/)
80
- fallbackQueue.push(...parts)
81
- input.removeListener("data", onData)
82
- 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()
83
177
  }
84
- input.on("data", onData)
178
+ input.on("end", onEof)
179
+ input.on("close", onEof)
85
180
  input.resume()
86
181
  }
87
- next()
182
+ fallbackWaiters.push(done)
183
+ pumpFallback()
88
184
  }
89
185
 
90
186
  /** Legge una riga da tastiera. `masked` oscura la digitazione (per segreti); */
91
- /** gestisce backspace e Ctrl+C. */
187
+ /** gestisce backspace, Ctrl+C (annullamento pulito) e incolla (CTRL+SHIFT+V). */
92
188
  export function readLine({ masked = false, allowEmpty = false } = {}, done) {
93
189
  if (!enableRaw()) {
94
190
  readLineFallback(done)
@@ -97,6 +193,8 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
97
193
 
98
194
  let buf = ""
99
195
  const onData = (chunk) => {
196
+ // marcatori "bracketed paste" del terminale: se presenti vanno ignorati
197
+ chunk = chunk.replace(/\x1b\[200~|\x1b\[201~/g, "")
100
198
  for (const ch of chunk) {
101
199
  if (ch === "\r" || ch === "\n") {
102
200
  if (!allowEmpty && buf === "") continue
@@ -104,12 +202,13 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
104
202
  input.setRawMode(false)
105
203
  input.pause()
106
204
  output.write("\n")
107
- return done(buf)
205
+ // Windows e alcuni editor incollano con \r\n finale: scarta il \r
206
+ return done(buf.replace(/\r+$/u, ""))
108
207
  }
109
- if (ch === "\u0003") {
110
- input.setRawMode(false)
111
- input.pause()
112
- process.exit(130)
208
+ if (ch === "\u0003" || ch === "\u0004") {
209
+ // Ctrl+C / Ctrl+D: annullamento pulito (uguale a clack isCancel),
210
+ // MAI un'uscita "muta" che lascia il terminale sporco.
211
+ abortSetup()
113
212
  }
114
213
  if (ch === "\u007f" || ch === "\b") {
115
214
  if (buf.length) {
@@ -132,13 +231,15 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
132
231
  async function askClassic(question, { defaultValue = "", hint = "", validate } = {}) {
133
232
  const suffix = hint ? ` ${c.dim(`(${hint})`)}` : ""
134
233
  const def = defaultValue ? c.dim(`[${defaultValue}]`) : ""
234
+ const err = ui("risposta non valida, riprova.", "invalid answer, try again.")
135
235
  // eslint-disable-next-line no-constant-condition
136
236
  for (;;) {
137
- output.write(`${c.cyan("?")} ${question}${suffix} ${def}\n> `)
237
+ output.write(`${c.cyan("?")} ${question}${suffix} ${def} ${c.dim(`(${qHint})`)}\n> `)
138
238
  const raw = await new Promise((resolve) => readLine({}, resolve))
239
+ if (/^q$/i.test(raw.trim())) abortSetup()
139
240
  const value = raw.trim() === "" ? defaultValue : raw.trim()
140
241
  if (validate && !validate(value)) {
141
- console.log(c.yellow(" risposta non valida, riprova."))
242
+ console.log(c.yellow(` ${err}`))
142
243
  continue
143
244
  }
144
245
  return value
@@ -146,15 +247,17 @@ async function askClassic(question, { defaultValue = "", hint = "", validate } =
146
247
  }
147
248
 
148
249
  async function confirmClassic(question, defaultValue = false) {
149
- const hint = defaultValue ? "S/n" : "s/N"
250
+ const hint = defaultValue ? ui("S/n", "Y/n") : ui("s/N", "y/N")
251
+ const err = ui("rispondi s o n.", "answer y or n.")
150
252
  // eslint-disable-next-line no-constant-condition
151
253
  for (;;) {
152
- output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint})`)}\n> `)
254
+ output.write(`${c.cyan("?")} ${question} ${c.dim(`(${hint}, ${qHint})`)}\n> `)
153
255
  const a = (await new Promise((resolve) => readLine({}, resolve))).trim().toLowerCase()
256
+ if (a === "q") abortSetup()
154
257
  if (a === "") return defaultValue
155
258
  if (/^(s|si|sì|y|yes)$/.test(a)) return true
156
259
  if (/^(n|no)$/.test(a)) return false
157
- console.log(c.yellow(" rispondi s o n."))
260
+ console.log(c.yellow(` ${err}`))
158
261
  }
159
262
  }
160
263
 
@@ -163,19 +266,19 @@ async function selectClassic(question, choices, { defaultValue = 0 } = {}) {
163
266
  choices.forEach((choice, i) => {
164
267
  console.log(` ${i + 1}. ${choice.label}`)
165
268
  })
166
- return choices[Number(await askClassic("Scelta (numero)", {
269
+ return choices[Number(await askClassic(ui("Scelta (numero)", "Choice (number)"), {
167
270
  defaultValue: String(defaultValue + 1),
168
271
  validate: (v) => /^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= choices.length,
169
272
  })) - 1]
170
273
  }
171
274
 
172
275
  async function checkboxClassic(question, choices, { defaultIndices = [] } = {}) {
173
- console.log(`\n${c.cyan("?")} ${question} ${c.dim("(invio = nessuno; es. 1,3,5)")}`)
276
+ console.log(`\n${c.cyan("?")} ${question} ${c.dim(ui("(invio = nessuno; es. 1,3,5)", "(blank = none; e.g. 1,3,5)"))}`)
174
277
  choices.forEach((choice, i) => {
175
278
  const sel = defaultIndices.includes(i) ? "•" : " "
176
279
  console.log(` ${sel} ${i + 1}. ${choice.label}`)
177
280
  })
178
- const answer = await askClassic("Seleziona (es. 1,3,5)", {
281
+ const answer = await askClassic(ui("Seleziona (es. 1,3,5)", "Select (e.g. 1,3,5)"), {
179
282
  validate: (v) => {
180
283
  const nums = v.split(/[\s,]+/).filter(Boolean)
181
284
  if (!nums.length) return true // invio = nessuno (salta)
@@ -188,8 +291,11 @@ async function checkboxClassic(question, choices, { defaultIndices = [] } = {})
188
291
  }
189
292
 
190
293
  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)))
294
+ output.write(`${c.cyan("?")} ${question} ${c.dim(`(${qHint})`)}\n> `)
295
+ return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => {
296
+ if (/^q$/i.test(v.trim())) abortSetup()
297
+ resolve(v)
298
+ }))
193
299
  }
194
300
 
195
301
  // --------------------------------------------------------------------------
@@ -201,20 +307,20 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
201
307
  if (!(await tuiOn())) return askClassic(question, { defaultValue, hint, validate })
202
308
  const cp = await clack()
203
309
  const message = hint ? `${question} (${hint})` : question
310
+ const errMsg = ui("risposta non valida, riprova.", "invalid answer, try again.")
204
311
  // eslint-disable-next-line no-constant-condition
205
312
  for (;;) {
206
- const raw = await cp.text({
207
- message,
313
+ const raw = await cp.text(clackOpts(message, {
208
314
  initialValue: defaultValue || undefined,
209
315
  validate: (val) => {
210
316
  const value = String(val ?? "").trim() === "" ? defaultValue : String(val).trim()
211
- return validate && !validate(value) ? "risposta non valida, riprova." : undefined
317
+ return validate && !validate(value) ? errMsg : undefined
212
318
  },
213
- })
319
+ }))
214
320
  if (cp.isCancel(raw)) abortSetup()
215
321
  const value = String(raw ?? "").trim() === "" ? defaultValue : String(raw).trim()
216
322
  if (validate && !validate(value)) {
217
- cp.log.error("risposta non valida, riprova.")
323
+ cp.log.error(errMsg)
218
324
  continue
219
325
  }
220
326
  return value
@@ -225,7 +331,11 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
225
331
  export async function confirm(question, defaultValue = false) {
226
332
  if (!(await tuiOn())) return confirmClassic(question, defaultValue)
227
333
  const cp = await clack()
228
- const v = await cp.confirm({ message: question, initialValue: !!defaultValue })
334
+ const v = await cp.confirm(clackOpts(question, {
335
+ initialValue: !!defaultValue,
336
+ active: isIt ? "sì" : "Yes",
337
+ inactive: isIt ? "no" : "No",
338
+ }))
229
339
  if (cp.isCancel(v)) abortSetup()
230
340
  return v === true
231
341
  }
@@ -234,11 +344,10 @@ export async function confirm(question, defaultValue = false) {
234
344
  export async function select(question, choices, { defaultValue = 0 } = {}) {
235
345
  if (!(await tuiOn())) return selectClassic(question, choices, { defaultValue })
236
346
  const cp = await clack()
237
- const idx = await cp.select({
238
- message: question,
347
+ const idx = await cp.select(clackOpts(question, {
239
348
  options: choices.map((ch, i) => ({ value: i, label: ch.label })),
240
349
  initialValue: defaultValue ?? 0,
241
- })
350
+ }))
242
351
  if (cp.isCancel(idx)) abortSetup()
243
352
  return choices[idx]
244
353
  }
@@ -249,7 +358,7 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
249
358
  const cp = await clack()
250
359
  const options = choices.map((ch) => ({ value: ch.value, label: ch.label }))
251
360
  const initialValues = defaultIndices.map((i) => choices[i]?.value).filter((v) => v !== undefined)
252
- const vals = await cp.multiselect({ message: question, options, initialValues, required: false })
361
+ const vals = await cp.multiselect(clackOpts(question, { options, initialValues, required: false }))
253
362
  if (cp.isCancel(vals)) abortSetup()
254
363
  const sel = new Set(vals)
255
364
  return choices.filter((ch) => sel.has(ch.value))
@@ -259,10 +368,9 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
259
368
  export async function secret(question, { allowEmpty = false } = {}) {
260
369
  if (!(await tuiOn())) return secretClassic(question, { allowEmpty })
261
370
  const cp = await clack()
262
- const v = await cp.password({
263
- message: question,
371
+ const v = await cp.password(clackOpts(question, {
264
372
  validate: allowEmpty ? () => undefined : undefined,
265
- })
373
+ }))
266
374
  if (cp.isCancel(v)) abortSetup()
267
375
  return String(v ?? "")
268
376
  }
@@ -275,4 +383,4 @@ export function closePrompts() {
275
383
  /* ok */
276
384
  }
277
385
  input.pause()
278
- }
386
+ }