@nan0web/ui-cli 1.1.1 → 2.0.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.
- package/README.md +114 -207
- package/package.json +22 -12
- package/src/CLI.js +22 -30
- package/src/CLiMessage.js +2 -3
- package/src/Command.js +26 -24
- package/src/CommandError.js +3 -5
- package/src/CommandHelp.js +40 -36
- package/src/CommandMessage.js +56 -40
- package/src/CommandParser.js +27 -25
- package/src/InputAdapter.js +630 -90
- package/src/OutputAdapter.js +7 -8
- package/src/README.md.js +190 -316
- package/src/components/Alert.js +3 -6
- package/src/components/prompt/Autocomplete.js +12 -0
- package/src/components/prompt/Confirm.js +29 -0
- package/src/components/prompt/DateTime.js +26 -0
- package/src/components/prompt/Input.js +15 -0
- package/src/components/prompt/Mask.js +12 -0
- package/src/components/prompt/Multiselect.js +26 -0
- package/src/components/prompt/Next.js +8 -0
- package/src/components/prompt/Password.js +13 -0
- package/src/components/prompt/Pause.js +9 -0
- package/src/components/prompt/ProgressBar.js +16 -0
- package/src/components/prompt/Select.js +29 -0
- package/src/components/prompt/Slider.js +16 -0
- package/src/components/prompt/Spinner.js +29 -0
- package/src/components/prompt/Toggle.js +13 -0
- package/src/components/prompt/Tree.js +17 -0
- package/src/components/view/Alert.js +78 -0
- package/src/components/view/Badge.js +11 -0
- package/src/components/view/Nav.js +23 -0
- package/src/components/view/Table.js +12 -0
- package/src/components/view/Toast.js +9 -0
- package/src/core/Component.js +79 -0
- package/src/core/PropValidation.js +138 -0
- package/src/core/render.js +37 -0
- package/src/index.js +80 -41
- package/src/test/PlaygroundTest.js +37 -25
- package/src/test/index.js +2 -4
- package/src/ui/alert.js +58 -0
- package/src/ui/autocomplete.js +86 -0
- package/src/ui/badge.js +35 -0
- package/src/ui/confirm.js +49 -0
- package/src/ui/date-time.js +45 -0
- package/src/ui/form.js +120 -55
- package/src/ui/index.js +18 -4
- package/src/ui/input.js +79 -152
- package/src/ui/mask.js +132 -0
- package/src/ui/multiselect.js +59 -0
- package/src/ui/nav.js +74 -0
- package/src/ui/next.js +18 -13
- package/src/ui/progress.js +88 -0
- package/src/ui/select.js +49 -72
- package/src/ui/slider.js +154 -0
- package/src/ui/spinner.js +65 -0
- package/src/ui/table.js +163 -0
- package/src/ui/toast.js +34 -0
- package/src/ui/toggle.js +34 -0
- package/src/ui/tree.js +393 -0
- package/src/utils/parse.js +1 -1
- package/types/CLI.d.ts +5 -5
- package/types/CLiMessage.d.ts +1 -1
- package/types/Command.d.ts +2 -2
- package/types/CommandHelp.d.ts +3 -3
- package/types/CommandMessage.d.ts +8 -8
- package/types/CommandParser.d.ts +3 -3
- package/types/InputAdapter.d.ts +149 -15
- package/types/OutputAdapter.d.ts +1 -1
- package/types/README.md.d.ts +1 -1
- package/types/UiMessage.d.ts +31 -29
- package/types/components/prompt/Autocomplete.d.ts +6 -0
- package/types/components/prompt/Confirm.d.ts +6 -0
- package/types/components/prompt/DateTime.d.ts +6 -0
- package/types/components/prompt/Input.d.ts +6 -0
- package/types/components/prompt/Mask.d.ts +6 -0
- package/types/components/prompt/Multiselect.d.ts +6 -0
- package/types/components/prompt/Next.d.ts +6 -0
- package/types/components/prompt/Password.d.ts +6 -0
- package/types/components/prompt/Pause.d.ts +6 -0
- package/types/components/prompt/ProgressBar.d.ts +12 -0
- package/types/components/prompt/Select.d.ts +18 -0
- package/types/components/prompt/Slider.d.ts +6 -0
- package/types/components/prompt/Spinner.d.ts +21 -0
- package/types/components/prompt/Toggle.d.ts +6 -0
- package/types/components/prompt/Tree.d.ts +6 -0
- package/types/components/view/Alert.d.ts +21 -0
- package/types/components/view/Badge.d.ts +5 -0
- package/types/components/view/Nav.d.ts +15 -0
- package/types/components/view/Table.d.ts +10 -0
- package/types/components/view/Toast.d.ts +5 -0
- package/types/core/Component.d.ts +34 -0
- package/types/core/PropValidation.d.ts +48 -0
- package/types/core/render.d.ts +6 -0
- package/types/index.d.ts +47 -15
- package/types/test/PlaygroundTest.d.ts +12 -8
- package/types/test/index.d.ts +1 -1
- package/types/ui/alert.d.ts +14 -0
- package/types/ui/autocomplete.d.ts +20 -0
- package/types/ui/badge.d.ts +8 -0
- package/types/ui/confirm.d.ts +21 -0
- package/types/ui/date-time.d.ts +19 -0
- package/types/ui/form.d.ts +43 -12
- package/types/ui/index.d.ts +17 -2
- package/types/ui/input.d.ts +31 -74
- package/types/ui/mask.d.ts +29 -0
- package/types/ui/multiselect.d.ts +25 -0
- package/types/ui/nav.d.ts +27 -0
- package/types/ui/progress.d.ts +43 -0
- package/types/ui/select.d.ts +25 -64
- package/types/ui/slider.d.ts +23 -0
- package/types/ui/spinner.d.ts +28 -0
- package/types/ui/table.d.ts +28 -0
- package/types/ui/toast.d.ts +8 -0
- package/types/ui/toggle.d.ts +17 -0
- package/types/ui/tree.d.ts +48 -0
package/src/ui/form.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* @module ui/form
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { CancelError } from
|
|
8
|
-
import { createInput, Input } from
|
|
9
|
-
import { select } from
|
|
10
|
-
import {
|
|
7
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
8
|
+
import { createInput, Input } from './input.js'
|
|
9
|
+
import { select } from './select.js'
|
|
10
|
+
import { autocomplete } from './autocomplete.js'
|
|
11
|
+
import { UiForm, FormInput } from '@nan0web/ui'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Generates a UiForm instance from a Body class static schema.
|
|
@@ -27,33 +28,33 @@ export function generateForm(BodyClass, options = {}) {
|
|
|
27
28
|
const fields = []
|
|
28
29
|
|
|
29
30
|
for (const [name, schema] of Object.entries(BodyClass)) {
|
|
30
|
-
if (typeof schema !==
|
|
31
|
+
if (typeof schema !== 'object' || schema === null) continue
|
|
31
32
|
|
|
32
|
-
const translate = (value) => (typeof t ===
|
|
33
|
+
const translate = (value) => (typeof t === 'function' ? t(value) : value)
|
|
33
34
|
|
|
34
35
|
fields.push(
|
|
35
36
|
new FormInput({
|
|
36
37
|
name,
|
|
37
38
|
label: translate(schema.help || name),
|
|
38
|
-
type: schema.type ||
|
|
39
|
+
type: schema.type || 'text',
|
|
39
40
|
required: Boolean(schema.required),
|
|
40
|
-
placeholder: translate(schema.placeholder || schema.defaultValue ||
|
|
41
|
+
placeholder: translate(schema.placeholder || schema.defaultValue || ''),
|
|
41
42
|
options: schema.options
|
|
42
43
|
? schema.options.map((opt) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
typeof opt === 'string'
|
|
45
|
+
? opt
|
|
46
|
+
: opt.label
|
|
47
|
+
? { label: opt.label, value: opt.value }
|
|
48
|
+
: opt
|
|
49
|
+
)
|
|
49
50
|
: [],
|
|
50
51
|
validation: schema.validate
|
|
51
52
|
? (value) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const res = schema.validate(value)
|
|
54
|
+
return res === true ? true : typeof res === 'string' ? res : `Invalid ${name}`
|
|
55
|
+
}
|
|
55
56
|
: () => true,
|
|
56
|
-
})
|
|
57
|
+
})
|
|
57
58
|
)
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -82,15 +83,22 @@ export default class Form {
|
|
|
82
83
|
* @param {Object} [options={}] - Options.
|
|
83
84
|
* @param {string[]} [options.stops=["quit", "cancel", "exit"]] - Stop words.
|
|
84
85
|
* @param {(prompt: string) => Promise<Input>} [options.inputFn] - Custom input function.
|
|
86
|
+
* @param {(config: object) => Promise<{index:number, value:any}>} [options.selectFn] - Custom select function.
|
|
87
|
+
* @param {(config: object) => Promise<{value: number, cancelled: boolean}>} [options.sliderFn] - Custom slider function.
|
|
88
|
+
* @param {(config: object) => Promise<{value: boolean, cancelled: boolean}>} [options.toggleFn] - Custom toggle function.
|
|
89
|
+
* @param {Function} [options.t] - Optional translation function.
|
|
85
90
|
* @throws {TypeError} If model is not an object with a constructor.
|
|
86
91
|
*/
|
|
87
92
|
constructor(model, options = {}) {
|
|
88
|
-
if (!model || typeof model !==
|
|
89
|
-
throw new TypeError(
|
|
93
|
+
if (!model || typeof model !== 'object' || !model.constructor) {
|
|
94
|
+
throw new TypeError('Form requires a model instance with a constructor')
|
|
90
95
|
}
|
|
91
96
|
this.#model = model
|
|
92
|
-
|
|
97
|
+
this.options = options
|
|
98
|
+
const { stops = ['quit', 'cancel', 'exit'], inputFn, selectFn, t = (k) => k } = options
|
|
99
|
+
this.t = t
|
|
93
100
|
this.handler = inputFn || createInput(stops)
|
|
101
|
+
this.select = selectFn || select
|
|
94
102
|
this.#fields = this.#generateFields()
|
|
95
103
|
}
|
|
96
104
|
|
|
@@ -103,25 +111,41 @@ export default class Form {
|
|
|
103
111
|
const Class = this.#model.constructor
|
|
104
112
|
const fields = []
|
|
105
113
|
for (const [name, schema] of Object.entries(Class)) {
|
|
106
|
-
if (typeof schema !==
|
|
114
|
+
if (typeof schema !== 'object' || schema === null) continue
|
|
107
115
|
const isRequired = schema.required === true || schema.defaultValue === undefined
|
|
108
|
-
const placeholder = schema.placeholder || schema.defaultValue ||
|
|
116
|
+
const placeholder = this.t(String(schema.placeholder || schema.defaultValue || ''))
|
|
109
117
|
const options = schema.options || []
|
|
110
|
-
|
|
118
|
+
|
|
119
|
+
// Support both 'validate' and 'validator' (validator takes precedence)
|
|
120
|
+
const validateFn = schema.validator || schema.validate
|
|
121
|
+
const validation = validateFn
|
|
111
122
|
? (value) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
try {
|
|
124
|
+
const res = validateFn(value)
|
|
125
|
+
if (res === true) return true
|
|
126
|
+
if (res === false) return `${this.t('validate.error')} ${name}`
|
|
127
|
+
if (typeof res === 'string') return this.t(res)
|
|
128
|
+
return `${this.t('validate.error')} ${name}`
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const err = /** @type {Error} */ (error)
|
|
131
|
+
return err.message || `${this.t('validate.error')} ${name}`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
117
134
|
: () => true
|
|
135
|
+
|
|
118
136
|
fields.push({
|
|
119
137
|
name,
|
|
120
|
-
label: schema.help || name,
|
|
121
|
-
type: schema.type ||
|
|
138
|
+
label: this.t(schema.help || name),
|
|
139
|
+
type: schema.type || 'text',
|
|
122
140
|
required: isRequired,
|
|
123
141
|
placeholder,
|
|
124
|
-
|
|
142
|
+
min: schema.min,
|
|
143
|
+
max: schema.max,
|
|
144
|
+
step: schema.step,
|
|
145
|
+
options: options.map((opt) => {
|
|
146
|
+
if (typeof opt === 'string') return { label: this.t(opt), value: opt }
|
|
147
|
+
return { ...opt, label: this.t(opt.label) }
|
|
148
|
+
}),
|
|
125
149
|
validation,
|
|
126
150
|
})
|
|
127
151
|
}
|
|
@@ -144,14 +168,8 @@ export default class Form {
|
|
|
144
168
|
return new Form(model, options)
|
|
145
169
|
}
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
* @param {Object} config - Selection configuration.
|
|
151
|
-
* @returns {Promise<{index:number, value:any}>} Selected option.
|
|
152
|
-
*/
|
|
153
|
-
async select(config) {
|
|
154
|
-
return select(config)
|
|
171
|
+
get fields() {
|
|
172
|
+
return this.#fields
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
/**
|
|
@@ -177,20 +195,54 @@ export default class Form {
|
|
|
177
195
|
while (idx < this.#fields.length) {
|
|
178
196
|
const field = this.#fields[idx]
|
|
179
197
|
const currentValue = this.#model[field.name] ?? field.placeholder
|
|
180
|
-
const prompt = `${field.label}${field.required ?
|
|
198
|
+
const prompt = `${field.label}${field.required ? ' *' : ''} [${currentValue}]: `
|
|
181
199
|
try {
|
|
182
|
-
if (field.options.length > 0) {
|
|
200
|
+
if (field.options.length > 0 || field.type === 'autocomplete') {
|
|
183
201
|
const selConfig = {
|
|
184
202
|
title: field.label,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
203
|
+
message: field.label,
|
|
204
|
+
prompt: `${this.t('Select')}: `,
|
|
205
|
+
options: field.options,
|
|
206
|
+
console: /** @type {any} */ (this.options).console || {
|
|
207
|
+
info: (msg) => process.stdout.write(msg + '\n'),
|
|
208
|
+
},
|
|
190
209
|
ask: this.handler,
|
|
191
210
|
}
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
let val
|
|
212
|
+
if (field.type === 'autocomplete') {
|
|
213
|
+
const autoResult = await autocomplete(selConfig)
|
|
214
|
+
val = autoResult.value
|
|
215
|
+
} else {
|
|
216
|
+
const selResult = await this.select(selConfig)
|
|
217
|
+
val = selResult.value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const validRes = field.validation(val)
|
|
221
|
+
if (validRes !== true) {
|
|
222
|
+
console.error(`\n${validRes}`)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
this.#model[field.name] = this.convertValue(field, val)
|
|
226
|
+
idx++
|
|
227
|
+
} else if (
|
|
228
|
+
field.type === 'number' &&
|
|
229
|
+
field.min !== undefined &&
|
|
230
|
+
field.max !== undefined &&
|
|
231
|
+
/** @type {any} */ (this.options).sliderFn
|
|
232
|
+
) {
|
|
233
|
+
// Use slider for number fields with range
|
|
234
|
+
const sliderConfig = {
|
|
235
|
+
message: field.label,
|
|
236
|
+
min: field.min,
|
|
237
|
+
max: field.max,
|
|
238
|
+
step: field.step || 1,
|
|
239
|
+
initial: Number(currentValue) || field.min,
|
|
240
|
+
}
|
|
241
|
+
const sliderResult = await /** @type {any} */ (this.options).sliderFn(sliderConfig)
|
|
242
|
+
if (sliderResult && sliderResult.cancelled) {
|
|
243
|
+
return { cancelled: true }
|
|
244
|
+
}
|
|
245
|
+
const val = sliderResult ? sliderResult.value : currentValue
|
|
194
246
|
const validRes = field.validation(val)
|
|
195
247
|
if (validRes !== true) {
|
|
196
248
|
console.error(`\n${validRes}`)
|
|
@@ -198,14 +250,27 @@ export default class Form {
|
|
|
198
250
|
}
|
|
199
251
|
this.#model[field.name] = this.convertValue(field, val)
|
|
200
252
|
idx++
|
|
253
|
+
} else if (field.type === 'toggle' && /** @type {any} */ (this.options).toggleFn) {
|
|
254
|
+
// Use toggle for boolean fields
|
|
255
|
+
const toggleConfig = {
|
|
256
|
+
message: field.label,
|
|
257
|
+
initial: currentValue === 'true' || currentValue === true,
|
|
258
|
+
}
|
|
259
|
+
const toggleResult = await /** @type {any} */ (this.options).toggleFn(toggleConfig)
|
|
260
|
+
if (toggleResult && toggleResult.cancelled) {
|
|
261
|
+
return { cancelled: true }
|
|
262
|
+
}
|
|
263
|
+
const val = toggleResult ? toggleResult.value : currentValue
|
|
264
|
+
idx++ // Toggles don't fail validation usually
|
|
265
|
+
this.#model[field.name] = val
|
|
201
266
|
} else {
|
|
202
267
|
const inputObj = await this.input(prompt)
|
|
203
268
|
if (inputObj.cancelled) {
|
|
204
269
|
return { cancelled: true }
|
|
205
270
|
}
|
|
206
271
|
let answer = inputObj.value.trim()
|
|
207
|
-
if (answer ===
|
|
208
|
-
this.#model[field.name] =
|
|
272
|
+
if (answer === '' && !field.required) {
|
|
273
|
+
this.#model[field.name] = ''
|
|
209
274
|
idx++
|
|
210
275
|
continue
|
|
211
276
|
}
|
|
@@ -236,12 +301,12 @@ export default class Form {
|
|
|
236
301
|
*/
|
|
237
302
|
convertValue(field, value) {
|
|
238
303
|
const schema = this.#model.constructor[field.name]
|
|
239
|
-
const type = schema?.type || typeof (schema?.defaultValue ??
|
|
304
|
+
const type = schema?.type || typeof (schema?.defaultValue ?? 'string')
|
|
240
305
|
switch (type) {
|
|
241
|
-
case
|
|
306
|
+
case 'number':
|
|
242
307
|
return Number(value) || 0
|
|
243
|
-
case
|
|
244
|
-
return value.toLowerCase() ===
|
|
308
|
+
case 'boolean':
|
|
309
|
+
return value.toLowerCase() === 'true'
|
|
245
310
|
default:
|
|
246
311
|
return String(value)
|
|
247
312
|
}
|
package/src/ui/index.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
export { Input } from
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { next, pause } from
|
|
1
|
+
export { Input, text, createInput, ask } from './input.js'
|
|
2
|
+
export { select, select as baseSelect } from './select.js'
|
|
3
|
+
export { confirm, confirm as baseConfirm } from './confirm.js'
|
|
4
|
+
export { next, pause } from './next.js'
|
|
5
|
+
export { autocomplete, autocomplete as baseAutocomplete } from './autocomplete.js'
|
|
6
|
+
export { table, table as baseTable } from './table.js'
|
|
7
|
+
export { multiselect, multiselect as baseMultiselect } from './multiselect.js'
|
|
8
|
+
export { mask, mask as baseMask } from './mask.js'
|
|
9
|
+
export { toggle, toggle as baseToggle } from './toggle.js'
|
|
10
|
+
export { slider, slider as baseSlider } from './slider.js'
|
|
11
|
+
export { progress, ProgressBar, progress as baseProgress } from './progress.js'
|
|
12
|
+
export { spinner, Spinner, spinner as baseSpinner } from './spinner.js'
|
|
13
|
+
export { badge } from './badge.js'
|
|
14
|
+
export { alert } from './alert.js'
|
|
15
|
+
export { toast } from './toast.js'
|
|
16
|
+
export { breadcrumbs, tabs, steps } from './nav.js'
|
|
17
|
+
export { tree } from './tree.js'
|
|
18
|
+
export { datetime } from './date-time.js'
|
package/src/ui/input.js
CHANGED
|
@@ -4,32 +4,16 @@
|
|
|
4
4
|
* @module ui/input
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
/** @typedef {import("./select.js").ConsoleLike} ConsoleLike */
|
|
12
|
-
/** @typedef {(input: Input) => Promise<boolean>} LoopFn */
|
|
13
|
-
/** @typedef {(input: Input) => string} NextQuestionFn */
|
|
7
|
+
import prompts from 'prompts'
|
|
8
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
9
|
+
import process from 'node:process'
|
|
10
|
+
import { validateString, validateFunction } from '../core/PropValidation.js'
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
17
|
-
* ---
|
|
18
|
-
* Must be used only as a type — typedef does not work with full arguments description for functions.
|
|
19
|
-
* ---
|
|
20
|
-
* @param {string} question - Prompt displayed to the user.
|
|
21
|
-
* @param {boolean | LoopFn} [loop=false] - Loop‑control flag, validator or boolean that forces a single answer.
|
|
22
|
-
* @param {string | NextQuestionFn} [nextQuestion] - When `false` the prompt ends after one answer.
|
|
23
|
-
* When a `function` is supplied it receives the current {@link Input}
|
|
24
|
-
* and must return a new question string for the next iteration.
|
|
25
|
-
*
|
|
26
|
-
* @returns {Promise<Input>} Resolves with an {@link Input} instance that contains the final answer,
|
|
27
|
-
* the raw value and cancellation state.
|
|
28
|
-
*
|
|
29
|
-
* @throws {Error} May propagate errors from the underlying readline interface.
|
|
13
|
+
* Triggers a system beep (ASCII Bell).
|
|
30
14
|
*/
|
|
31
|
-
export
|
|
32
|
-
|
|
15
|
+
export function beep() {
|
|
16
|
+
process.stdout.write('\u0007')
|
|
33
17
|
}
|
|
34
18
|
|
|
35
19
|
/**
|
|
@@ -42,181 +26,124 @@ export async function InputFn(question, loop = false, nextQuestion = undefined)
|
|
|
42
26
|
*/
|
|
43
27
|
export class Input {
|
|
44
28
|
/** @type {string} */
|
|
45
|
-
value =
|
|
29
|
+
value = ''
|
|
46
30
|
/** @type {string[]} */
|
|
47
31
|
stops = []
|
|
48
32
|
#cancelled = false
|
|
49
33
|
|
|
50
|
-
/**
|
|
51
|
-
* Create a new {@link Input} instance.
|
|
52
|
-
*
|
|
53
|
-
* @param {Object} [input={}] - Optional initial values.
|
|
54
|
-
* @param {string} [input.value] - Initial answer string.
|
|
55
|
-
* @param {boolean} [input.cancelled] - Initial cancel flag.
|
|
56
|
-
* @param {string|string[]} [input.stops] - Words that trigger cancellation.
|
|
57
|
-
*/
|
|
58
34
|
constructor(input = {}) {
|
|
59
|
-
const {
|
|
60
|
-
value = this.value,
|
|
61
|
-
cancelled = this.#cancelled,
|
|
62
|
-
stops = [],
|
|
63
|
-
} = input
|
|
64
|
-
|
|
35
|
+
const { value = this.value, cancelled = this.#cancelled, stops = [] } = input
|
|
65
36
|
this.value = String(value)
|
|
66
|
-
|
|
67
|
-
const normalizedStops = Array.isArray(stops) ? stops : [stops].filter(Boolean)
|
|
68
|
-
this.stops = normalizedStops.map(String)
|
|
69
|
-
|
|
37
|
+
this.stops = stops
|
|
70
38
|
this.#cancelled = Boolean(cancelled)
|
|
71
39
|
}
|
|
72
40
|
|
|
73
|
-
/**
|
|
74
|
-
* Returns whether the input has been cancelled either explicitly or via a stop word.
|
|
75
|
-
*
|
|
76
|
-
* @returns {boolean}
|
|
77
|
-
*/
|
|
78
41
|
get cancelled() {
|
|
79
42
|
return this.#cancelled || this.stops.includes(this.value)
|
|
80
43
|
}
|
|
81
44
|
|
|
82
|
-
/** @returns {string} The raw answer value. */
|
|
83
45
|
toString() {
|
|
84
46
|
return this.value
|
|
85
47
|
}
|
|
86
48
|
}
|
|
87
49
|
|
|
88
50
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* @param {Object} input
|
|
92
|
-
* @param {string} input.question - Text displayed as a prompt.
|
|
93
|
-
* @param {string} [input.predef] - Optional predefined answer (useful for testing).
|
|
94
|
-
* @param {ConsoleLike} [input.console] - Optional console to show predefined value
|
|
95
|
-
* @param {import("node:readline").Interface} [input.rl] - Readline interface instnace
|
|
96
|
-
* @returns {Promise<string>} The answer without surrounding whitespace.
|
|
51
|
+
* Modern text input with validation and default value.
|
|
97
52
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
53
|
+
* @param {Object} config
|
|
54
|
+
* @param {string} config.message - Prompt question
|
|
55
|
+
* @param {string} [config.initial] - Default value
|
|
56
|
+
* @param {string} [config.type] - Prompt type (text, password, etc)
|
|
57
|
+
* @param {(value:string)=>boolean|string|Promise<boolean|string>} [config.validate] - Validator
|
|
58
|
+
* @param {(value:string)=>string} [config.format] - Formatter
|
|
59
|
+
* @returns {Promise<{value:string, cancelled:boolean}>}
|
|
100
60
|
*/
|
|
101
|
-
export async function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
61
|
+
export async function text(config) {
|
|
62
|
+
validateString(config.message, 'message', 'Input.text', true)
|
|
63
|
+
validateString(config.initial, 'initial', 'Input.text')
|
|
64
|
+
validateFunction(config.validate, 'validate', 'Input.text')
|
|
65
|
+
validateFunction(config.format, 'format', 'Input.text')
|
|
66
|
+
|
|
67
|
+
const { message, initial, validate, type = 'text', format } = config
|
|
68
|
+
const response = await prompts(
|
|
69
|
+
{
|
|
70
|
+
type: type,
|
|
71
|
+
name: 'value',
|
|
72
|
+
message,
|
|
73
|
+
initial,
|
|
74
|
+
validate,
|
|
75
|
+
format,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
onCancel: () => {
|
|
79
|
+
throw new CancelError()
|
|
80
|
+
},
|
|
114
81
|
}
|
|
115
|
-
|
|
116
|
-
}
|
|
82
|
+
)
|
|
117
83
|
|
|
118
|
-
return
|
|
119
|
-
const rl = initialRL ?? createInterface({ input: stdin, output: stdout, terminal: true })
|
|
120
|
-
rl.question(question, answer => {
|
|
121
|
-
rl.close()
|
|
122
|
-
resolve(answer.trim())
|
|
123
|
-
})
|
|
124
|
-
})
|
|
84
|
+
return { value: response.value, cancelled: response.value === undefined }
|
|
125
85
|
}
|
|
126
86
|
|
|
127
87
|
/**
|
|
128
88
|
* Factory that creates a reusable async input handler.
|
|
89
|
+
* Adapter for legacy ask() signature.
|
|
129
90
|
*
|
|
130
91
|
* @param {string[]} [stops=[]] Words that trigger cancellation.
|
|
131
92
|
* @param {string|undefined} [predef] Optional predefined answer for testing.
|
|
132
|
-
* @param {
|
|
133
|
-
* @
|
|
93
|
+
* @param {Object} [console] Optional console instance.
|
|
94
|
+
* @param {(input: Input) => Promise<boolean>|boolean} [loop] Optional loop validator.
|
|
95
|
+
* @returns {Function} Async function that resolves to an {@link Input}.
|
|
134
96
|
*/
|
|
135
|
-
export function createInput(stops = [], predef = undefined, console = undefined) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!cont) return input
|
|
97
|
+
export function createInput(stops = [], predef = undefined, console = undefined, loop = undefined) {
|
|
98
|
+
return async function ask(question, loopVal = loop, nextQuestion = undefined) {
|
|
99
|
+
const currentLoop = typeof loopVal === 'function' ? loopVal : loop
|
|
100
|
+
if (predef !== undefined) {
|
|
101
|
+
prompts.inject([predef])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Map options to prompts config
|
|
105
|
+
let validationFn = undefined
|
|
106
|
+
|
|
107
|
+
if (typeof currentLoop === 'function') {
|
|
108
|
+
validationFn = async (val) => {
|
|
109
|
+
if (stops.includes(val)) return true
|
|
110
|
+
// Loop returns true to CONTINUE (invalid), false to STOP (valid).
|
|
111
|
+
// prompts returns true for VALID, string/false for INVALID.
|
|
112
|
+
const inputObj = new Input({ value: val, stops })
|
|
113
|
+
if (inputObj.cancelled) return true
|
|
114
|
+
|
|
115
|
+
const shouldContinue = await currentLoop(inputObj)
|
|
116
|
+
return shouldContinue ? 'Invalid input' : true
|
|
156
117
|
}
|
|
157
|
-
if (typeof nextQuestion === "string") currentQuestion = nextQuestion
|
|
158
|
-
if (typeof nextQuestion === "function") currentQuestion = nextQuestion(input)
|
|
159
118
|
}
|
|
119
|
+
|
|
120
|
+
const result = await text({
|
|
121
|
+
message: question,
|
|
122
|
+
validate: validationFn,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return new Input({ value: result.value, stops })
|
|
160
126
|
}
|
|
161
|
-
return fn
|
|
162
127
|
}
|
|
163
128
|
|
|
164
129
|
/**
|
|
165
130
|
* High‑level input helper `ask`.
|
|
166
|
-
*
|
|
167
|
-
* This constant inherits the full {@link InputFn} signature **and** the
|
|
168
|
-
* detailed JSDoc description for each argument, as defined in {@link InputFn}.
|
|
169
|
-
*
|
|
170
|
-
* @type {InputFn}
|
|
131
|
+
* Use this for simple string prompts.
|
|
171
132
|
*/
|
|
172
133
|
export const ask = createInput()
|
|
173
134
|
|
|
174
135
|
/**
|
|
175
|
-
*
|
|
176
|
-
* @param {ConsoleLike} console
|
|
177
|
-
* @param {string[]} [stops=[]]
|
|
178
|
-
* @returns {import("./select.js").InputFn}
|
|
179
|
-
* @throws {CancelError}
|
|
136
|
+
* Mock helper for predefined inputs (Testing).
|
|
180
137
|
*/
|
|
181
138
|
export function createPredefinedInput(predefined, console, stops = []) {
|
|
182
139
|
const strPredefined = predefined.map(String)
|
|
183
|
-
const input = new Input({ stops })
|
|
184
140
|
let index = 0
|
|
185
|
-
return async function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (index >= strPredefined.length) {
|
|
189
|
-
throw new CancelError("No more predefined answers")
|
|
190
|
-
}
|
|
191
|
-
const predef = strPredefined[index++]
|
|
192
|
-
if (console) {
|
|
193
|
-
console.info(`${currentQuestion}${predef}\n`)
|
|
194
|
-
} else {
|
|
195
|
-
process.stdout.write(`${currentQuestion}${predef}\n`)
|
|
196
|
-
}
|
|
197
|
-
input.value = predef.trim()
|
|
198
|
-
if (input.cancelled) {
|
|
199
|
-
return input
|
|
200
|
-
}
|
|
201
|
-
if (false === loop) {
|
|
202
|
-
return input
|
|
203
|
-
}
|
|
204
|
-
if (true === loop && input.value) {
|
|
205
|
-
return input
|
|
206
|
-
}
|
|
207
|
-
if (typeof loop === "function") {
|
|
208
|
-
const cont = await loop(input)
|
|
209
|
-
if (!cont) {
|
|
210
|
-
return input
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (typeof nextQuestion === "string") {
|
|
214
|
-
currentQuestion = nextQuestion
|
|
215
|
-
} else if (typeof nextQuestion === "function") {
|
|
216
|
-
currentQuestion = nextQuestion(input)
|
|
217
|
-
}
|
|
141
|
+
return async function (question) {
|
|
142
|
+
if (index >= strPredefined.length) {
|
|
143
|
+
throw new CancelError('No more predefined answers')
|
|
218
144
|
}
|
|
145
|
+
const val = strPredefined[index++]
|
|
146
|
+
if (console) console.info(`${question}${val}`)
|
|
147
|
+
return new Input({ value: val, stops })
|
|
219
148
|
}
|
|
220
149
|
}
|
|
221
|
-
|
|
222
|
-
export default createInput
|