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 +45 -11
- package/bin/oc-setup.js +36 -8
- package/package.json +3 -2
- package/src/client.js +7 -3
- package/src/i18n.js +19 -0
- package/src/prompts.js +132 -34
- package/src/sections.js +130 -28
- package/src/server.js +127 -19
- package/src/setup.js +248 -69
- package/src/shell.js +6 -1
- package/src/uninstall.js +62 -23
- package/templates/command-commit.md +18 -0
- package/templates/command-explain.md +16 -0
- package/templates/command-omniroute-restart.md +19 -0
- package/templates/command-refactor.md +18 -0
- package/templates/command-review.md +17 -0
- package/templates/command-tests.md +16 -0
package/README.md
CHANGED
|
@@ -15,12 +15,18 @@ oc-setup
|
|
|
15
15
|
|
|
16
16
|
La procedura guidata:
|
|
17
17
|
|
|
18
|
-
1. **
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
endpoint e chiavi
|
|
23
|
-
|
|
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 |
|
|
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
|
|
56
|
-
(gateway multi-modello con supporto `auto/*`)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
50
|
-
process.exit(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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(
|
|
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) =>
|
|
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) ?
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|