@nan0web/ui-cli 1.1.1 → 2.1.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 +153 -203
- package/bin/cli.js +11 -0
- package/bin/nan0cli.js +86 -0
- package/package.json +27 -13
- 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 +241 -312
- 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 +85 -40
- 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/InputAdapter.js
CHANGED
|
@@ -4,19 +4,44 @@
|
|
|
4
4
|
* @module InputAdapter
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@nan0web/ui"
|
|
12
|
-
import { CancelError } from "@nan0web/ui/core"
|
|
7
|
+
import { UiForm, InputAdapter as BaseInputAdapter, UiMessage } from '@nan0web/ui'
|
|
8
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
9
|
+
import prompts from 'prompts'
|
|
10
|
+
import readline from 'node:readline'
|
|
13
11
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
import {
|
|
13
|
+
ask as baseAsk,
|
|
14
|
+
text as baseText,
|
|
15
|
+
beep,
|
|
16
|
+
createInput,
|
|
17
|
+
createPredefinedInput,
|
|
18
|
+
Input,
|
|
19
|
+
} from './ui/input.js'
|
|
20
|
+
import { next } from './ui/next.js'
|
|
21
|
+
import { select as baseSelect } from './ui/select.js'
|
|
22
|
+
import { confirm as baseConfirm } from './ui/confirm.js'
|
|
23
|
+
import { autocomplete as baseAutocomplete } from './ui/autocomplete.js'
|
|
24
|
+
import { table as baseTable } from './ui/table.js'
|
|
25
|
+
import { multiselect as baseMultiselect } from './ui/multiselect.js'
|
|
26
|
+
import { mask as baseMask } from './ui/mask.js'
|
|
27
|
+
import { toggle as baseToggle } from './ui/toggle.js'
|
|
28
|
+
import { slider as baseSlider } from './ui/slider.js'
|
|
29
|
+
import { progress as baseProgress } from './ui/progress.js'
|
|
30
|
+
import { spinner as baseSpinner } from './ui/spinner.js'
|
|
31
|
+
import { tree as baseTree } from './ui/tree.js'
|
|
32
|
+
import { datetime as baseDateTime } from './ui/date-time.js'
|
|
33
|
+
import { generateForm } from './ui/form.js'
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MAX_RETRIES = 100
|
|
17
36
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} ConsoleLike
|
|
39
|
+
* @property {(...args: any[]) => void} debug
|
|
40
|
+
* @property {(...args: any[]) => void} log
|
|
41
|
+
* @property {(...args: any[]) => void} info
|
|
42
|
+
* @property {(...args: any[]) => void} warn
|
|
43
|
+
* @property {(...args: any[]) => void} error
|
|
44
|
+
*/
|
|
20
45
|
|
|
21
46
|
/**
|
|
22
47
|
* Extends the generic {@link BaseInputAdapter} with CLI‑specific behaviour.
|
|
@@ -32,29 +57,43 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
32
57
|
/** @type {ConsoleLike} */
|
|
33
58
|
#console
|
|
34
59
|
#stdout
|
|
35
|
-
/** @type {Map<string, () => Promise<
|
|
60
|
+
/** @type {Map<string, () => Promise<any>>} */
|
|
36
61
|
#components = new Map()
|
|
62
|
+
/** @type {number} */
|
|
63
|
+
#maxRetries
|
|
64
|
+
/** @type {Function} */
|
|
65
|
+
#t
|
|
37
66
|
|
|
38
67
|
constructor(options = {}) {
|
|
39
68
|
super()
|
|
40
69
|
const {
|
|
41
70
|
predefined = process.env.PLAY_DEMO_SEQUENCE ?? [],
|
|
42
|
-
divider = process.env.PLAY_DEMO_DIVIDER ??
|
|
71
|
+
divider = process.env.PLAY_DEMO_DIVIDER ?? ',',
|
|
43
72
|
console: initialConsole = console,
|
|
44
73
|
stdout = process.stdout,
|
|
45
74
|
components = new Map(),
|
|
75
|
+
t = (key) => key,
|
|
46
76
|
} = options
|
|
47
77
|
|
|
78
|
+
let { maxRetries = process.env.UI_CLI_MAX_RETRIES ?? DEFAULT_MAX_RETRIES } = options
|
|
79
|
+
|
|
80
|
+
// Increase default retries for automated environments to prevent flakiness
|
|
81
|
+
if (process.env.PLAY_DEMO_SEQUENCE && maxRetries === DEFAULT_MAX_RETRIES) {
|
|
82
|
+
maxRetries = 1000
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
this.#console = initialConsole
|
|
49
86
|
this.#stdout = stdout
|
|
50
87
|
this.#components = components
|
|
88
|
+
this.#maxRetries = Number(maxRetries)
|
|
89
|
+
this.#t = t
|
|
51
90
|
|
|
52
91
|
if (Array.isArray(predefined)) {
|
|
53
|
-
this.#answers = predefined.map(v => String(v))
|
|
54
|
-
} else if (typeof predefined ===
|
|
92
|
+
this.#answers = predefined.map((v) => String(v))
|
|
93
|
+
} else if (typeof predefined === 'string') {
|
|
55
94
|
this.#answers = predefined
|
|
56
95
|
.split(divider)
|
|
57
|
-
.map(v => v.trim())
|
|
96
|
+
.map((v) => v.trim())
|
|
58
97
|
.filter(Boolean)
|
|
59
98
|
} else {
|
|
60
99
|
this.#answers = []
|
|
@@ -62,9 +101,21 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
/** @returns {ConsoleLike} */
|
|
65
|
-
get console() {
|
|
104
|
+
get console() {
|
|
105
|
+
return this.#console
|
|
106
|
+
}
|
|
107
|
+
/** @returns {Function} */
|
|
108
|
+
get t() {
|
|
109
|
+
return this.#t
|
|
110
|
+
}
|
|
111
|
+
/** @param {Function} val */
|
|
112
|
+
set t(val) {
|
|
113
|
+
this.#t = val
|
|
114
|
+
}
|
|
66
115
|
/** @returns {NodeJS.WriteStream} */
|
|
67
|
-
get stdout() {
|
|
116
|
+
get stdout() {
|
|
117
|
+
return this.#stdout
|
|
118
|
+
}
|
|
68
119
|
|
|
69
120
|
#nextAnswer() {
|
|
70
121
|
if (this.#cursor < this.#answers.length) {
|
|
@@ -75,6 +126,11 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
75
126
|
return null
|
|
76
127
|
}
|
|
77
128
|
|
|
129
|
+
/** @returns {string[]} */
|
|
130
|
+
getRemainingAnswers() {
|
|
131
|
+
return this.#answers.slice(this.#cursor)
|
|
132
|
+
}
|
|
133
|
+
|
|
78
134
|
/**
|
|
79
135
|
* Normalise a value that can be either a raw string or an {@link Input}
|
|
80
136
|
* instance (which carries the actual value in its `value` property).
|
|
@@ -83,17 +139,41 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
83
139
|
* @returns {string} Plain string value.
|
|
84
140
|
*/
|
|
85
141
|
#normalise(val) {
|
|
86
|
-
if (val && typeof val ===
|
|
142
|
+
if (val && typeof val === 'object' && 'value' in val) {
|
|
87
143
|
return String(val.value)
|
|
88
144
|
}
|
|
89
|
-
return String(val ??
|
|
145
|
+
return String(val ?? '')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Private helper to format value according to mask
|
|
150
|
+
* @param {string|number} value
|
|
151
|
+
* @param {string} mask
|
|
152
|
+
*/
|
|
153
|
+
#applyMask(value, mask) {
|
|
154
|
+
if (!mask) return String(value)
|
|
155
|
+
const cleanValue = String(value).replace(/[^a-zA-Z0-9]/g, '')
|
|
156
|
+
let i = 0
|
|
157
|
+
let v = 0
|
|
158
|
+
let result = ''
|
|
159
|
+
while (i < mask.length && v < cleanValue.length) {
|
|
160
|
+
const maskChar = mask[i]
|
|
161
|
+
if (maskChar === '#' || maskChar === '0' || maskChar === 'A') {
|
|
162
|
+
result += cleanValue[v]
|
|
163
|
+
v++
|
|
164
|
+
} else {
|
|
165
|
+
result += maskChar
|
|
166
|
+
}
|
|
167
|
+
i++
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
90
170
|
}
|
|
91
171
|
|
|
92
172
|
/**
|
|
93
173
|
* Create a handler with stop words that supports predefined answers.
|
|
94
174
|
*
|
|
95
175
|
* @param {string[]} stops - Stop words for cancellation.
|
|
96
|
-
* @returns {
|
|
176
|
+
* @returns {Function}
|
|
97
177
|
*/
|
|
98
178
|
createHandler(stops = []) {
|
|
99
179
|
const self = this
|
|
@@ -103,7 +183,7 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
103
183
|
this.stdout.write(`${question}${predefined}\n`)
|
|
104
184
|
const input = new Input({ value: predefined, stops })
|
|
105
185
|
if (input.cancelled) {
|
|
106
|
-
throw new CancelError(
|
|
186
|
+
throw new CancelError('Cancelled via stop word')
|
|
107
187
|
}
|
|
108
188
|
return input
|
|
109
189
|
}
|
|
@@ -112,6 +192,61 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
112
192
|
}
|
|
113
193
|
}
|
|
114
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Create a select handler that supports predefined answers.
|
|
197
|
+
* @returns {Function}
|
|
198
|
+
*/
|
|
199
|
+
createSelectHandler() {
|
|
200
|
+
const self = this
|
|
201
|
+
return async (config) => {
|
|
202
|
+
const predefined = self.#nextAnswer()
|
|
203
|
+
if (predefined !== null) {
|
|
204
|
+
// Normalize options to find value
|
|
205
|
+
let choices = []
|
|
206
|
+
const options = config.options || []
|
|
207
|
+
if (options instanceof Map) {
|
|
208
|
+
choices = Array.from(options.entries()).map(([value, label]) => ({ label, value }))
|
|
209
|
+
} else if (Array.isArray(options)) {
|
|
210
|
+
choices = options.map((el) => (typeof el === 'string' ? { label: el, value: el } : el))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const idx = Number(predefined) - 1
|
|
214
|
+
let valToInject = predefined
|
|
215
|
+
if (!isNaN(idx) && idx >= 0 && idx < choices.length) {
|
|
216
|
+
valToInject = choices[idx].value
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (config.console) {
|
|
220
|
+
if (config.title) config.console.info(config.title)
|
|
221
|
+
choices.forEach((c, i) => {
|
|
222
|
+
config.console.info(` ${i + 1}) ${c.label}`)
|
|
223
|
+
})
|
|
224
|
+
const p = config.prompt ?? ''
|
|
225
|
+
config.console.info(`${p}${predefined}`)
|
|
226
|
+
}
|
|
227
|
+
prompts.inject([valToInject])
|
|
228
|
+
}
|
|
229
|
+
return self.select(config)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pause execution and wait for user input (Press any key).
|
|
235
|
+
*
|
|
236
|
+
* @param {string} [message] - Message to display.
|
|
237
|
+
* @returns {Promise<void>}
|
|
238
|
+
*/
|
|
239
|
+
async pause(message) {
|
|
240
|
+
const predefined = this.#nextAnswer()
|
|
241
|
+
if (message) this.console.info(message)
|
|
242
|
+
if (predefined !== null) {
|
|
243
|
+
// Automated mode: proceed immediately
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
// Interactive mode
|
|
247
|
+
await next()
|
|
248
|
+
}
|
|
249
|
+
|
|
115
250
|
/**
|
|
116
251
|
* Prompt the user for a full form, handling navigation and validation.
|
|
117
252
|
*
|
|
@@ -122,101 +257,205 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
122
257
|
*/
|
|
123
258
|
async requestForm(form, options = {}) {
|
|
124
259
|
const { silent = true } = options
|
|
125
|
-
if (!silent) this.console.info(`\n${form.title}\n`)
|
|
260
|
+
if (!silent) this.console.info(`\n${this.#t(form.title)}\n`)
|
|
126
261
|
|
|
127
262
|
let formData = { ...form.state }
|
|
128
263
|
let idx = 0
|
|
264
|
+
let retries = 0
|
|
129
265
|
|
|
130
266
|
while (idx < form.fields.length) {
|
|
131
|
-
const field = form.fields[idx]
|
|
132
|
-
|
|
267
|
+
const field = /** @type {any} */ (form.fields[idx])
|
|
268
|
+
if (retries > this.#maxRetries) {
|
|
269
|
+
return {
|
|
270
|
+
body: { action: 'form-cancel', cancelled: true, error: 'Infinite loop detected' },
|
|
271
|
+
cancelled: true,
|
|
272
|
+
action: 'form-cancel',
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const label = this.#t(field.label || field.name)
|
|
276
|
+
const hasPunctuation = label.trim().endsWith('?') || label.trim().endsWith(':')
|
|
277
|
+
const promptMsg = `${label}${field.required ? ' *' : ''}${hasPunctuation ? '' : ':'}`
|
|
133
278
|
|
|
134
279
|
// ---- Handle select fields (options) ----------------------------------------
|
|
135
|
-
if (
|
|
280
|
+
if (
|
|
281
|
+
field.type === 'select' ||
|
|
282
|
+
(Array.isArray(field.options ?? 0) &&
|
|
283
|
+
field.options.length &&
|
|
284
|
+
field.type !== 'multiselect' &&
|
|
285
|
+
field.type !== 'autocomplete')
|
|
286
|
+
) {
|
|
136
287
|
const options = /** @type {any[]} */ (field.options)
|
|
137
288
|
|
|
138
|
-
const predefined = this.#nextAnswer()
|
|
139
289
|
const selConfig = {
|
|
140
|
-
title: field.label,
|
|
141
|
-
prompt:
|
|
142
|
-
options: options.map(opt =>
|
|
143
|
-
typeof opt ===
|
|
144
|
-
? { label: opt, value: opt }
|
|
145
|
-
: opt
|
|
290
|
+
title: this.#t(field.label),
|
|
291
|
+
prompt: this.#t('Choose (number): '),
|
|
292
|
+
options: options.map((opt) =>
|
|
293
|
+
typeof opt === 'string'
|
|
294
|
+
? { label: this.#t(opt), value: opt }
|
|
295
|
+
: { ...opt, label: this.#t(opt.label) }
|
|
146
296
|
),
|
|
147
297
|
console: { info: this.console.info.bind(this.console) },
|
|
148
298
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
selResult = await this.select(selConfig)
|
|
299
|
+
const chosen = await this.requestSelect(selConfig)
|
|
300
|
+
if (!chosen || chosen.cancelled) {
|
|
301
|
+
return {
|
|
302
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
303
|
+
cancelled: true,
|
|
304
|
+
action: 'form-cancel',
|
|
305
|
+
}
|
|
157
306
|
}
|
|
158
307
|
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
161
|
-
this.console.error(
|
|
308
|
+
const values = options.map((o) => (typeof o === 'string' ? o : o.value))
|
|
309
|
+
if (!values.includes(chosen.value)) {
|
|
310
|
+
this.console.error('\nEnumeration must have one value')
|
|
311
|
+
retries++
|
|
162
312
|
continue
|
|
163
313
|
}
|
|
164
|
-
formData[field.name] = String(chosen)
|
|
314
|
+
formData[field.name] = String(chosen.value)
|
|
315
|
+
idx++
|
|
316
|
+
retries = 0
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
// ---- Handle confirm / toggle fields --------------------------------------
|
|
320
|
+
if (field.type === 'confirm' || field.type === 'toggle') {
|
|
321
|
+
const res = await this.requestConfirm({ message: promptMsg })
|
|
322
|
+
if (!res || res.cancelled) {
|
|
323
|
+
return {
|
|
324
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
325
|
+
cancelled: true,
|
|
326
|
+
action: 'form-cancel',
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
formData[field.name] = res.value
|
|
165
330
|
idx++
|
|
166
331
|
continue
|
|
167
332
|
}
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
333
|
|
|
170
|
-
|
|
171
|
-
|
|
334
|
+
// ---- Handle multiselect fields -------------------------------------------
|
|
335
|
+
if (field.type === 'multiselect') {
|
|
336
|
+
const res = await this.requestMultiselect({
|
|
337
|
+
message: promptMsg,
|
|
338
|
+
options: Array.isArray(field.options) ? field.options : [],
|
|
339
|
+
initial: [],
|
|
340
|
+
})
|
|
341
|
+
if (!res || res.cancelled) {
|
|
342
|
+
return {
|
|
343
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
344
|
+
cancelled: true,
|
|
345
|
+
action: 'form-cancel',
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
formData[field.name] = res.value
|
|
349
|
+
idx++
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
172
352
|
|
|
173
|
-
|
|
353
|
+
// ---- Handle mask fields --------------------------------------------------
|
|
354
|
+
if (field.type === 'mask') {
|
|
355
|
+
const res = await this.requestMask({
|
|
356
|
+
message: promptMsg,
|
|
357
|
+
mask: field.mask || '',
|
|
358
|
+
placeholder: field.placeholder || '',
|
|
359
|
+
})
|
|
360
|
+
if (!res || res.cancelled) {
|
|
361
|
+
return {
|
|
362
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
363
|
+
cancelled: true,
|
|
364
|
+
action: 'form-cancel',
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
formData[field.name] = res.value
|
|
368
|
+
idx++
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- Handle date / datetime fields ---------------------------------------
|
|
373
|
+
if (field.type === 'date' || field.type === 'datetime') {
|
|
374
|
+
const res = await this.requestDateTime({
|
|
375
|
+
message: promptMsg,
|
|
376
|
+
initial: field.value instanceof Date ? field.value : new Date(),
|
|
377
|
+
mask: field.mask,
|
|
378
|
+
})
|
|
379
|
+
if (!res || res.cancelled) {
|
|
380
|
+
return {
|
|
381
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
382
|
+
cancelled: true,
|
|
383
|
+
action: 'form-cancel',
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
formData[field.name] = res.value
|
|
387
|
+
idx++
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---- Handle text / password / number fields ------------------------------
|
|
392
|
+
|
|
393
|
+
const res = await this.requestInput({
|
|
394
|
+
prompt: promptMsg,
|
|
395
|
+
initial: field.placeholder,
|
|
396
|
+
type: field.type === 'password' || field.type === 'secret' ? 'password' : 'text',
|
|
397
|
+
validation: (val) => {
|
|
398
|
+
if (!field.validation) return true
|
|
399
|
+
const result = field.validation(val)
|
|
400
|
+
if (result !== true) beep()
|
|
401
|
+
return result
|
|
402
|
+
},
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
if (!res || res.cancelled) {
|
|
174
406
|
return {
|
|
175
|
-
body: { action:
|
|
176
|
-
form: {},
|
|
407
|
+
body: { action: 'form-cancel', cancelled: true },
|
|
177
408
|
cancelled: true,
|
|
178
|
-
action:
|
|
409
|
+
action: 'form-cancel',
|
|
179
410
|
}
|
|
180
411
|
}
|
|
181
412
|
|
|
182
|
-
const trimmed =
|
|
183
|
-
|
|
413
|
+
const trimmed = String(res.value || '').trim()
|
|
414
|
+
|
|
415
|
+
if (trimmed === '::prev' || trimmed === '::back') {
|
|
184
416
|
idx = Math.max(0, idx - 1)
|
|
417
|
+
retries = 0
|
|
185
418
|
continue
|
|
186
419
|
}
|
|
187
|
-
if (trimmed ===
|
|
420
|
+
if (trimmed === '::next' || trimmed === '::skip') {
|
|
188
421
|
idx++
|
|
422
|
+
retries = 0
|
|
189
423
|
continue
|
|
190
424
|
}
|
|
191
|
-
if (trimmed ===
|
|
425
|
+
if (trimmed === '' && !field.required) {
|
|
192
426
|
idx++
|
|
427
|
+
retries = 0
|
|
193
428
|
continue
|
|
194
429
|
}
|
|
195
|
-
if (field.required && trimmed ===
|
|
196
|
-
this.console.info(
|
|
430
|
+
if (field.required && trimmed === '') {
|
|
431
|
+
this.console.info(`\n${this.#t('Field is required.')}`)
|
|
432
|
+
retries++
|
|
197
433
|
continue
|
|
198
434
|
}
|
|
435
|
+
|
|
199
436
|
const schema = field.constructor
|
|
200
437
|
const { isValid, errors } = form.validateValue(field.name, trimmed, schema)
|
|
201
438
|
if (!isValid) {
|
|
202
|
-
this.console.warn(
|
|
439
|
+
this.console.warn('\n' + Object.values(errors).join('\n'))
|
|
440
|
+
retries++
|
|
203
441
|
continue
|
|
204
442
|
}
|
|
205
443
|
formData[field.name] = trimmed
|
|
206
444
|
idx++
|
|
445
|
+
retries = 0
|
|
207
446
|
}
|
|
208
447
|
|
|
209
448
|
const finalForm = form.setData(formData)
|
|
210
449
|
const errors = finalForm.validate()
|
|
211
450
|
if (errors.size) {
|
|
212
|
-
this.console.warn(
|
|
451
|
+
this.console.warn('\n' + Object.values(errors).join('\n'))
|
|
213
452
|
return await this.requestForm(form, options)
|
|
214
453
|
}
|
|
215
454
|
return {
|
|
216
|
-
body: { action:
|
|
455
|
+
body: { action: 'form-submit', cancelled: false, form: finalForm },
|
|
217
456
|
form: finalForm,
|
|
218
457
|
cancelled: false,
|
|
219
|
-
action:
|
|
458
|
+
action: 'form-submit',
|
|
220
459
|
}
|
|
221
460
|
}
|
|
222
461
|
|
|
@@ -233,8 +472,11 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
233
472
|
const compLoader = this.#components.get(component)
|
|
234
473
|
if (compLoader) {
|
|
235
474
|
try {
|
|
236
|
-
|
|
237
|
-
if (typeof compFn ===
|
|
475
|
+
let compFn = await compLoader()
|
|
476
|
+
if (compFn && typeof compFn === 'object' && compFn.default) {
|
|
477
|
+
compFn = compFn.default
|
|
478
|
+
}
|
|
479
|
+
if (typeof compFn === 'function') {
|
|
238
480
|
compFn.call(this, props)
|
|
239
481
|
return
|
|
240
482
|
}
|
|
@@ -243,9 +485,9 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
243
485
|
this.console.debug?.(err.stack)
|
|
244
486
|
}
|
|
245
487
|
}
|
|
246
|
-
if (props && typeof props ===
|
|
488
|
+
if (props && typeof props === 'object') {
|
|
247
489
|
const { variant, content } = props
|
|
248
|
-
const prefix = variant ? `[${variant}]` :
|
|
490
|
+
const prefix = variant ? `[${variant}]` : ''
|
|
249
491
|
this.console.info(`${prefix} ${String(content)}`)
|
|
250
492
|
} else {
|
|
251
493
|
this.console.info(String(component))
|
|
@@ -267,21 +509,52 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
267
509
|
* Prompt the user to select an option from a list.
|
|
268
510
|
*
|
|
269
511
|
* @param {Object} config - Configuration object.
|
|
270
|
-
* @returns {Promise<string>} Selected value (or
|
|
512
|
+
* @returns {Promise<{value: string|undefined, cancelled: boolean}>} Selected value (or undefined on cancel).
|
|
271
513
|
*/
|
|
272
514
|
async requestSelect(config) {
|
|
515
|
+
config.limit = config.limit ?? Math.max(5, (this.stdout.rows || 24) - 4)
|
|
273
516
|
try {
|
|
274
517
|
const predefined = this.#nextAnswer()
|
|
275
|
-
if (!config.console) config.console =
|
|
518
|
+
if (!config.console) config.console = this.#console
|
|
276
519
|
|
|
277
520
|
if (predefined !== null) {
|
|
278
|
-
|
|
279
|
-
|
|
521
|
+
// Normalize options to find value
|
|
522
|
+
let choices = []
|
|
523
|
+
const options = config.options || []
|
|
524
|
+
if (options instanceof Map) {
|
|
525
|
+
choices = Array.from(options.entries()).map(([value, label]) => ({ label, value }))
|
|
526
|
+
} else if (Array.isArray(options)) {
|
|
527
|
+
choices = options.map((el) => {
|
|
528
|
+
if (typeof el === 'string') return { label: el, value: el }
|
|
529
|
+
return { label: el.label || el.title, value: el.value }
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const idx = Number(predefined) - 1
|
|
534
|
+
let valToInject = predefined
|
|
535
|
+
if (!isNaN(idx) && idx >= 0 && idx < choices.length) {
|
|
536
|
+
valToInject = choices[idx].value
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (config.console) {
|
|
540
|
+
if (config.title) config.console.info(config.title)
|
|
541
|
+
const limit = config.limit || choices.length
|
|
542
|
+
choices.slice(0, limit).forEach((c, i) => {
|
|
543
|
+
config.console.info(` ${i + 1}) ${this.#t(c.label)}`)
|
|
544
|
+
})
|
|
545
|
+
if (choices.length > limit) {
|
|
546
|
+
config.console.info(` ↓ (${choices.length - limit} ${this.#t('more')})`)
|
|
547
|
+
}
|
|
548
|
+
const p = config.prompt ?? ''
|
|
549
|
+
config.console.info(`✔ ${p} ${predefined}`)
|
|
550
|
+
}
|
|
551
|
+
return { value: valToInject, cancelled: false }
|
|
280
552
|
}
|
|
281
|
-
|
|
282
|
-
|
|
553
|
+
config.t = this.t
|
|
554
|
+
const res = await this.select(config)
|
|
555
|
+
return { value: res.value, cancelled: res.cancelled ?? false }
|
|
283
556
|
} catch (e) {
|
|
284
|
-
if (e instanceof CancelError) return
|
|
557
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
285
558
|
throw e
|
|
286
559
|
}
|
|
287
560
|
}
|
|
@@ -290,18 +563,285 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
290
563
|
* Prompt for a single string input.
|
|
291
564
|
*
|
|
292
565
|
* @param {Object} config - Prompt configuration.
|
|
293
|
-
* @returns {Promise<string>} User response string.
|
|
566
|
+
* @returns {Promise<{value: string|undefined, cancelled: boolean}>} User response string or undefined on cancel.
|
|
294
567
|
*/
|
|
295
568
|
async requestInput(config) {
|
|
296
569
|
const predefined = this.#nextAnswer()
|
|
297
|
-
|
|
570
|
+
const promptOrig =
|
|
571
|
+
config.prompt ?? config.message ?? `${config.label ?? config.name ?? 'Input'}: `
|
|
572
|
+
const prompt = this.#t(promptOrig)
|
|
573
|
+
if (predefined !== null) {
|
|
574
|
+
if (config.type !== 'password' && config.type !== 'secret') {
|
|
575
|
+
this.console.info(`✔ ${prompt} ${predefined}`)
|
|
576
|
+
} else {
|
|
577
|
+
this.console.info(`✔ ${prompt}${'*'.repeat(predefined.length)}`)
|
|
578
|
+
}
|
|
579
|
+
return { value: predefined, cancelled: false }
|
|
580
|
+
} else if (config.yes === true && config.value !== undefined) {
|
|
581
|
+
return { value: config.value, cancelled: false }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const res = await baseText({
|
|
586
|
+
message: prompt,
|
|
587
|
+
initial: config.initial ?? config.placeholder,
|
|
588
|
+
type: config.type === 'password' || config.type === 'secret' ? 'password' : 'text',
|
|
589
|
+
validate: config.validate || config.validation,
|
|
590
|
+
})
|
|
591
|
+
return res
|
|
592
|
+
} catch (e) {
|
|
593
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
594
|
+
throw e
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Prompt the user for an autocomplete selection.
|
|
600
|
+
*
|
|
601
|
+
* @param {Object} config - Configuration object.
|
|
602
|
+
* @returns {Promise<{value: any, cancelled: boolean}>} Selected value.
|
|
603
|
+
*/
|
|
604
|
+
async requestAutocomplete(config) {
|
|
605
|
+
config.limit = config.limit ?? Math.max(5, (this.stdout.rows || 24) - 4)
|
|
606
|
+
const predefined = this.#nextAnswer()
|
|
607
|
+
const prompt = config.message || config.title || 'Search: '
|
|
608
|
+
if (predefined !== null) {
|
|
609
|
+
this.console.info(`${prompt} ${predefined}`)
|
|
610
|
+
return { value: predefined, cancelled: false }
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
return await baseAutocomplete(config)
|
|
614
|
+
} catch (e) {
|
|
615
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
616
|
+
throw e
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Requests confirmation (yes/no).
|
|
622
|
+
*
|
|
623
|
+
* @param {Object} config - Confirmation configuration.
|
|
624
|
+
* @returns {Promise<{value: boolean|undefined, cancelled: boolean}>} User confirmation.
|
|
625
|
+
*/
|
|
626
|
+
async requestConfirm(config) {
|
|
627
|
+
const predefined = this.#nextAnswer()
|
|
628
|
+
const prompt = config.message || 'Confirm: '
|
|
629
|
+
if (predefined !== null) {
|
|
630
|
+
const val = ['y', 'yes', 'true', '1', 'так', '+'].includes(predefined.toLowerCase())
|
|
631
|
+
const display = val ? config.active || this.#t('yes') : config.inactive || this.#t('no')
|
|
632
|
+
this.console.info(`✔ ${prompt} ${display}`)
|
|
633
|
+
return { value: val, cancelled: false }
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const active = config.active || (this.#t ? this.#t('yes') : 'Yes')
|
|
637
|
+
const inactive = config.inactive || (this.#t ? this.#t('no') : 'No')
|
|
638
|
+
if (typeof config === 'object' && config !== null) config.t = this.t
|
|
639
|
+
return await baseConfirm({ ...config, message: prompt, active, inactive, t: this.t })
|
|
640
|
+
} catch (e) {
|
|
641
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
642
|
+
throw e
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Display an interactive table.
|
|
648
|
+
*
|
|
649
|
+
* @param {Object} config - Table configuration.
|
|
650
|
+
* @returns {Promise<{value: any, cancelled: boolean}>}
|
|
651
|
+
*/
|
|
652
|
+
async requestTable(config) {
|
|
653
|
+
config.t = this.t
|
|
654
|
+
config.prompt = (c) => this.requestInput(c)
|
|
655
|
+
config.logger = config.logger || this.console
|
|
656
|
+
try {
|
|
657
|
+
return await baseTable(config)
|
|
658
|
+
} catch (e) {
|
|
659
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
660
|
+
throw e
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Requests multiple selection.
|
|
666
|
+
*
|
|
667
|
+
* @param {Object} config - Multiselect configuration.
|
|
668
|
+
* @returns {Promise<{value: any[]|undefined, cancelled: boolean}>} Selected values.
|
|
669
|
+
*/
|
|
670
|
+
async requestMultiselect(config) {
|
|
671
|
+
const predefined = this.#nextAnswer()
|
|
672
|
+
const prompt = config.message || 'Select: '
|
|
673
|
+
if (predefined !== null) {
|
|
674
|
+
this.console.info(`${prompt} ${predefined}`)
|
|
675
|
+
return { value: predefined.split(',').map((v) => v.trim()), cancelled: false }
|
|
676
|
+
}
|
|
677
|
+
const instructions =
|
|
678
|
+
`\n${this.#t('Instructions')}:\n` +
|
|
679
|
+
` ↑/↓: ${this.#t('Highlight option')}\n` +
|
|
680
|
+
` ←/→/[space]: ${this.#t('Toggle selection')}\n` +
|
|
681
|
+
` a: ${this.#t('Toggle all')}\n` +
|
|
682
|
+
` enter/return: ${this.#t('Complete answer')}`
|
|
683
|
+
|
|
684
|
+
// Pass instructions to multiselect config
|
|
685
|
+
config.instructions = instructions
|
|
686
|
+
config.t = this.t
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
return await baseMultiselect(config)
|
|
690
|
+
} catch (e) {
|
|
691
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
692
|
+
throw e
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Requests masked input.
|
|
698
|
+
*
|
|
699
|
+
* @param {Object} config - Mask configuration.
|
|
700
|
+
* @returns {Promise<{value: string|undefined, cancelled: boolean}>} Masked value.
|
|
701
|
+
*/
|
|
702
|
+
async requestMask(config) {
|
|
703
|
+
const predefined = this.#nextAnswer()
|
|
704
|
+
const prompt = config.message || 'Input: '
|
|
705
|
+
if (predefined !== null) {
|
|
706
|
+
const display = this.#applyMask(predefined, config.mask)
|
|
707
|
+
this.console.info(`✔ ${prompt} ${display}`)
|
|
708
|
+
return { value: predefined, cancelled: false }
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const maskImpl = this.#components.get('mask') || baseMask
|
|
712
|
+
const res = await maskImpl({ ...config, message: prompt })
|
|
713
|
+
if (res && !res.cancelled) {
|
|
714
|
+
try {
|
|
715
|
+
readline.moveCursor(this.#stdout, 0, -1)
|
|
716
|
+
readline.clearLine(this.#stdout, 0)
|
|
717
|
+
this.console.info(`✔ ${prompt} ${res.value}`)
|
|
718
|
+
} catch (e) {
|
|
719
|
+
this.console.error('Cursor op failed:', e)
|
|
720
|
+
// Ignore cursor errors in non-TTY environments, but ensure output is logged
|
|
721
|
+
// If cursor moved failed, we might have duplicate log.
|
|
722
|
+
// But we prioritize showing correct value.
|
|
723
|
+
// If readline failed, we assume console.info needs to run?
|
|
724
|
+
// If readline failed, console.info might have been cleared? No.
|
|
725
|
+
// We only print if we tried to clear.
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return res
|
|
729
|
+
} catch (e) {
|
|
730
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
731
|
+
throw e
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Request a toggle switch.
|
|
737
|
+
* @param {Object} config
|
|
738
|
+
* @returns {Promise<{value: boolean|undefined, cancelled: boolean}>}
|
|
739
|
+
*/
|
|
740
|
+
async requestToggle(config) {
|
|
741
|
+
const predefined = this.#nextAnswer()
|
|
742
|
+
const prompt = config.message || 'Confirm: '
|
|
743
|
+
if (predefined !== null) {
|
|
744
|
+
this.console.info(`✔ ${prompt} ${predefined}`)
|
|
745
|
+
return {
|
|
746
|
+
value: ['y', 'yes', 'true', '1', 'так'].includes(predefined.toLowerCase()),
|
|
747
|
+
cancelled: false,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
return await baseToggle(config)
|
|
752
|
+
} catch (e) {
|
|
753
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
754
|
+
throw e
|
|
755
|
+
}
|
|
756
|
+
}
|
|
298
757
|
|
|
299
|
-
|
|
300
|
-
|
|
758
|
+
/**
|
|
759
|
+
* Request a numeric slider.
|
|
760
|
+
* @param {Object} config
|
|
761
|
+
* @returns {Promise<{value: number|undefined, cancelled: boolean}>}
|
|
762
|
+
*/
|
|
763
|
+
async requestSlider(config) {
|
|
764
|
+
const predefined = this.#nextAnswer()
|
|
765
|
+
const prompt = config.message || 'Value: '
|
|
766
|
+
if (predefined !== null) {
|
|
767
|
+
this.console.info(`${prompt} ${predefined}`)
|
|
768
|
+
return { value: Number(predefined), cancelled: false }
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
config.t = this.t
|
|
772
|
+
return await baseSlider(config)
|
|
773
|
+
} catch (e) {
|
|
774
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
775
|
+
throw e
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Create a progress bar.
|
|
781
|
+
* @param {Object} options
|
|
782
|
+
* @returns {import('./ui/progress.js').ProgressBar}
|
|
783
|
+
*/
|
|
784
|
+
requestProgress(options) {
|
|
785
|
+
return baseProgress(options)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Create and start a spinner.
|
|
790
|
+
* @param {string} message
|
|
791
|
+
* @returns {import('./ui/spinner.js').Spinner}
|
|
792
|
+
*/
|
|
793
|
+
requestSpinner(message) {
|
|
794
|
+
return baseSpinner(message)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Request a selection from a tree view.
|
|
799
|
+
* @param {Object} config
|
|
800
|
+
* @returns {Promise<{value: any, cancelled: boolean}>} Selected node(s).
|
|
801
|
+
*/
|
|
802
|
+
async requestTree(config) {
|
|
803
|
+
const predefined = this.#nextAnswer()
|
|
804
|
+
if (predefined !== null) {
|
|
805
|
+
const prompt = config.message || 'Select: '
|
|
806
|
+
this.console.info(`${prompt} ${predefined}`)
|
|
807
|
+
return { value: predefined, cancelled: false }
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
config.t = this.t
|
|
811
|
+
return await baseTree(config)
|
|
812
|
+
} catch (e) {
|
|
813
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
814
|
+
throw e
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Request a date or time from the user.
|
|
820
|
+
* @param {Object} config
|
|
821
|
+
* @returns {Promise<{value: Date|undefined, cancelled: boolean}>}
|
|
822
|
+
*/
|
|
823
|
+
async requestDateTime(config) {
|
|
824
|
+
const predefined = this.#nextAnswer()
|
|
825
|
+
const promptOrig = config.message || config.label || config.name || 'Date: '
|
|
826
|
+
const prompt = this.#t(promptOrig)
|
|
827
|
+
if (predefined !== null) {
|
|
828
|
+
this.console.info(`${prompt} ${predefined}`)
|
|
829
|
+
let val = new Date(predefined)
|
|
830
|
+
if (isNaN(val.getTime())) {
|
|
831
|
+
if (/^\d{1,2}:\d{2}/.test(predefined)) {
|
|
832
|
+
const today = new Date().toISOString().split('T')[0]
|
|
833
|
+
val = new Date(`${today}T${predefined}`)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { value: isNaN(val.getTime()) ? undefined : val, cancelled: false }
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const res = await baseDateTime({ t: this.t, ...config, message: prompt })
|
|
840
|
+
return res
|
|
841
|
+
} catch (e) {
|
|
842
|
+
if (e instanceof CancelError) return { value: undefined, cancelled: true }
|
|
843
|
+
throw e
|
|
301
844
|
}
|
|
302
|
-
const prompt = config.prompt ?? `${config.label ?? config.name}: `
|
|
303
|
-
const answer = await this.ask(prompt)
|
|
304
|
-
return this.#normalise(answer)
|
|
305
845
|
}
|
|
306
846
|
|
|
307
847
|
/**
|
|
@@ -317,10 +857,10 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
317
857
|
const predefined = this.#nextAnswer()
|
|
318
858
|
if (predefined !== null) {
|
|
319
859
|
this.stdout.write(`${question}${predefined}\n`)
|
|
320
|
-
|
|
860
|
+
prompts.inject([predefined])
|
|
321
861
|
}
|
|
322
|
-
const
|
|
323
|
-
return
|
|
862
|
+
const result = await baseAsk(question)
|
|
863
|
+
return result
|
|
324
864
|
}
|
|
325
865
|
|
|
326
866
|
/** @inheritDoc */
|
|
@@ -342,22 +882,22 @@ export default class CLiInputAdapter extends BaseInputAdapter {
|
|
|
342
882
|
*/
|
|
343
883
|
async requireInput(msg) {
|
|
344
884
|
if (!msg) {
|
|
345
|
-
throw new Error(
|
|
885
|
+
throw new Error('Message instance is required')
|
|
346
886
|
}
|
|
347
887
|
if (!(msg instanceof UiMessage)) {
|
|
348
|
-
throw new TypeError(
|
|
888
|
+
throw new TypeError('Message must be an instance UiMessage')
|
|
349
889
|
}
|
|
350
890
|
/** @type {Map<string,string>} */
|
|
351
891
|
let errors = msg.validate()
|
|
352
892
|
while (errors.size > 0) {
|
|
353
|
-
const form = generateForm(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)
|
|
893
|
+
const form = generateForm(/** @type {any} */ (msg.constructor).Body, {
|
|
894
|
+
initialState: msg.body,
|
|
895
|
+
t: this.#t,
|
|
896
|
+
})
|
|
357
897
|
|
|
358
898
|
const formResult = await this.processForm(form, msg.body)
|
|
359
899
|
if (formResult.cancelled) {
|
|
360
|
-
throw new CancelError(
|
|
900
|
+
throw new CancelError('User cancelled form')
|
|
361
901
|
}
|
|
362
902
|
|
|
363
903
|
const updatedBody = { ...msg.body, ...formResult.form.state }
|