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 +79 -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 +155 -47
- 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
|
@@ -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. **
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
endpoint e chiavi
|
|
23
|
-
|
|
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 |
|
|
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
|
|
56
|
-
(gateway multi-modello con supporto `auto/*`)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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.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
|
|
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() {
|
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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("
|
|
178
|
+
input.on("end", onEof)
|
|
179
|
+
input.on("close", onEof)
|
|
85
180
|
input.resume()
|
|
86
181
|
}
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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(
|
|
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) =>
|
|
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) ?
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|