opencode-wyvern 0.1.1 → 0.2.0

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.
Files changed (2) hide show
  1. package/package.json +6 -3
  2. package/src/prompts.js +127 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-wyvern",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Setup modulare per OpenCode: SSH, client PowerShell/bash (comandi oc-*), configurazione server opencode, provider, plugin e comandi custom. Installi tutto, attivi quello che vuoi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "url": "https://github.com/Rhaegal222"
10
10
  },
11
11
  "engines": {
12
- "node": ">=18"
12
+ "node": ">=20.12.0"
13
13
  },
14
14
  "bin": {
15
15
  "oc-setup": "bin/oc-setup.js"
@@ -37,5 +37,8 @@
37
37
  },
38
38
  "scripts": {
39
39
  "test": "node bin/oc-setup.js --help"
40
+ },
41
+ "dependencies": {
42
+ "@clack/prompts": "^1.8.0"
40
43
  }
41
- }
44
+ }
package/src/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { stdin as input, stdout as output } from "node:process"
1
+ import { stdin as input, stdout as output, stderr as stderrStream } from "node:process"
2
2
 
3
3
  const color = (code, s) => `\x1b[${code}m${s}\x1b[0m`
4
4
  export const c = {
@@ -10,6 +10,46 @@ export const c = {
10
10
  bold: (s) => color(1, s),
11
11
  }
12
12
 
13
+ // --------------------------------------------------------------------------
14
+ // Backend TUI nativo Node (@clack/prompts) — cross-platform.
15
+ // Quando stdin/stdout/stderr sono un terminale reale, i prompt diventano
16
+ // dialoghi interattivi identici su Windows/pwsh, WSL, Linux e macOS
17
+ // (frecce, Spazio, invio, Esc/Ctrl+C per annullare, exit code 1).
18
+ // Negli altri casi (pipe, CI, non-interattivo) si usa il fallback a righe.
19
+ // `secret` mantiene l'input mascherato (via clack password o raw mode).
20
+ //
21
+ // Il modulo @clack/prompts è importato dinamicamente: se manca (repo senza
22
+ // `npm install`) si cade automaticamente sul fallback a righe.
23
+ // --------------------------------------------------------------------------
24
+
25
+ let cpModule = null
26
+ let cpTried = false
27
+
28
+ async function clack() {
29
+ if (!cpTried) {
30
+ cpTried = true
31
+ try {
32
+ cpModule = await import("@clack/prompts")
33
+ } catch {
34
+ cpModule = null // dipendenza non installata: fallback a righe
35
+ }
36
+ }
37
+ return cpModule
38
+ }
39
+
40
+ const realTty = () => !!input.isTTY && !!output.isTTY && !!stderrStream.isTTY
41
+
42
+ async function tuiOn() {
43
+ const cp = await clack()
44
+ return !!(cp && realTty())
45
+ }
46
+
47
+ function abortSetup() {
48
+ closePrompts()
49
+ console.log(c.yellow("\n annullato (exit 1)."))
50
+ process.exit(1)
51
+ }
52
+
13
53
  function enableRaw() {
14
54
  try {
15
55
  input.setRawMode(true)
@@ -85,8 +125,11 @@ export function readLine({ masked = false, allowEmpty = false } = {}, done) {
85
125
  input.on("data", onData)
86
126
  }
87
127
 
88
- /** Domanda a testo semplice con default opzionale. */
89
- export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
128
+ // --------------------------------------------------------------------------
129
+ // Implementazioni classiche (fallback a righe, stesse API precedenti).
130
+ // --------------------------------------------------------------------------
131
+
132
+ async function askClassic(question, { defaultValue = "", hint = "", validate } = {}) {
90
133
  const suffix = hint ? ` ${c.dim(`(${hint})`)}` : ""
91
134
  const def = defaultValue ? c.dim(`[${defaultValue}]`) : ""
92
135
  // eslint-disable-next-line no-constant-condition
@@ -102,8 +145,7 @@ export async function ask(question, { defaultValue = "", hint = "", validate } =
102
145
  }
103
146
  }
104
147
 
105
- /** Domanda sì/no con default indicato in maiuscolo. */
106
- export async function confirm(question, defaultValue = false) {
148
+ async function confirmClassic(question, defaultValue = false) {
107
149
  const hint = defaultValue ? "S/n" : "s/N"
108
150
  // eslint-disable-next-line no-constant-condition
109
151
  for (;;) {
@@ -116,26 +158,24 @@ export async function confirm(question, defaultValue = false) {
116
158
  }
117
159
  }
118
160
 
119
- /** Selezione singola (indice numerico). */
120
- export async function select(question, choices, { defaultValue = 0 } = {}) {
161
+ async function selectClassic(question, choices, { defaultValue = 0 } = {}) {
121
162
  console.log(`\n${c.cyan("?")} ${question}`)
122
163
  choices.forEach((choice, i) => {
123
164
  console.log(` ${i + 1}. ${choice.label}`)
124
165
  })
125
- return choices[Number(await ask("Scelta (numero)", {
166
+ return choices[Number(await askClassic("Scelta (numero)", {
126
167
  defaultValue: String(defaultValue + 1),
127
168
  validate: (v) => /^\d+$/.test(v) && Number(v) >= 1 && Number(v) <= choices.length,
128
169
  })) - 1]
129
170
  }
130
171
 
131
- /** Selezione multipla: numeri separati da virgola/spazio (es. "1,3,5"); invio = nessuna. */
132
- export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
172
+ async function checkboxClassic(question, choices, { defaultIndices = [] } = {}) {
133
173
  console.log(`\n${c.cyan("?")} ${question} ${c.dim("(invio = nessuno; es. 1,3,5)")}`)
134
174
  choices.forEach((choice, i) => {
135
175
  const sel = defaultIndices.includes(i) ? "•" : " "
136
176
  console.log(` ${sel} ${i + 1}. ${choice.label}`)
137
177
  })
138
- const answer = await ask("Seleziona (es. 1,3,5)", {
178
+ const answer = await askClassic("Seleziona (es. 1,3,5)", {
139
179
  validate: (v) => {
140
180
  const nums = v.split(/[\s,]+/).filter(Boolean)
141
181
  if (!nums.length) return true // invio = nessuno (salta)
@@ -147,12 +187,86 @@ export async function checkbox(question, choices, { defaultIndices = [] } = {})
147
187
  return [...new Set(nums.map((n) => Number(n) - 1))].map((i) => choices[i])
148
188
  }
149
189
 
150
- /** Input mascherato per segreti (API key ecc.). */
151
- export async function secret(question, { allowEmpty = false } = {}) {
190
+ async function secretClassic(question, { allowEmpty = false } = {}) {
152
191
  output.write(`${c.cyan("?")} ${question}\n> `)
153
192
  return new Promise((resolve) => readLine({ masked: true, allowEmpty }, (v) => resolve(v)))
154
193
  }
155
194
 
195
+ // --------------------------------------------------------------------------
196
+ // API pubbliche: TUI @clack/prompts (TTY) con fallback classico.
197
+ // --------------------------------------------------------------------------
198
+
199
+ /** Domanda a testo semplice con default opzionale (clack text). */
200
+ export async function ask(question, { defaultValue = "", hint = "", validate } = {}) {
201
+ if (!(await tuiOn())) return askClassic(question, { defaultValue, hint, validate })
202
+ const cp = await clack()
203
+ const message = hint ? `${question} (${hint})` : question
204
+ // eslint-disable-next-line no-constant-condition
205
+ for (;;) {
206
+ const raw = await cp.text({
207
+ message,
208
+ initialValue: defaultValue || undefined,
209
+ validate: (val) => {
210
+ const value = String(val ?? "").trim() === "" ? defaultValue : String(val).trim()
211
+ return validate && !validate(value) ? "risposta non valida, riprova." : undefined
212
+ },
213
+ })
214
+ if (cp.isCancel(raw)) abortSetup()
215
+ const value = String(raw ?? "").trim() === "" ? defaultValue : String(raw).trim()
216
+ if (validate && !validate(value)) {
217
+ cp.log.error("risposta non valida, riprova.")
218
+ continue
219
+ }
220
+ return value
221
+ }
222
+ }
223
+
224
+ /** Domanda sì/no (clack confirm). */
225
+ export async function confirm(question, defaultValue = false) {
226
+ if (!(await tuiOn())) return confirmClassic(question, defaultValue)
227
+ const cp = await clack()
228
+ const v = await cp.confirm({ message: question, initialValue: !!defaultValue })
229
+ if (cp.isCancel(v)) abortSetup()
230
+ return v === true
231
+ }
232
+
233
+ /** Selezione singola (clack select, frecce). */
234
+ export async function select(question, choices, { defaultValue = 0 } = {}) {
235
+ if (!(await tuiOn())) return selectClassic(question, choices, { defaultValue })
236
+ const cp = await clack()
237
+ const idx = await cp.select({
238
+ message: question,
239
+ options: choices.map((ch, i) => ({ value: i, label: ch.label })),
240
+ initialValue: defaultValue ?? 0,
241
+ })
242
+ if (cp.isCancel(idx)) abortSetup()
243
+ return choices[idx]
244
+ }
245
+
246
+ /** Selezione multipla (clack multiselect, frecce + Spazio). */
247
+ export async function checkbox(question, choices, { defaultIndices = [] } = {}) {
248
+ if (!(await tuiOn())) return checkboxClassic(question, choices, { defaultIndices })
249
+ const cp = await clack()
250
+ const options = choices.map((ch) => ({ value: ch.value, label: ch.label }))
251
+ const initialValues = defaultIndices.map((i) => choices[i]?.value).filter((v) => v !== undefined)
252
+ const vals = await cp.multiselect({ message: question, options, initialValues, required: false })
253
+ if (cp.isCancel(vals)) abortSetup()
254
+ const sel = new Set(vals)
255
+ return choices.filter((ch) => sel.has(ch.value))
256
+ }
257
+
258
+ /** Input mascherato per segreti (API key ecc.). */
259
+ export async function secret(question, { allowEmpty = false } = {}) {
260
+ if (!(await tuiOn())) return secretClassic(question, { allowEmpty })
261
+ const cp = await clack()
262
+ const v = await cp.password({
263
+ message: question,
264
+ validate: allowEmpty ? () => undefined : undefined,
265
+ })
266
+ if (cp.isCancel(v)) abortSetup()
267
+ return String(v ?? "")
268
+ }
269
+
156
270
  export function closePrompts() {
157
271
  input.removeAllListeners("data")
158
272
  try {