@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.
Files changed (115) hide show
  1. package/README.md +114 -207
  2. package/package.json +22 -12
  3. package/src/CLI.js +22 -30
  4. package/src/CLiMessage.js +2 -3
  5. package/src/Command.js +26 -24
  6. package/src/CommandError.js +3 -5
  7. package/src/CommandHelp.js +40 -36
  8. package/src/CommandMessage.js +56 -40
  9. package/src/CommandParser.js +27 -25
  10. package/src/InputAdapter.js +630 -90
  11. package/src/OutputAdapter.js +7 -8
  12. package/src/README.md.js +190 -316
  13. package/src/components/Alert.js +3 -6
  14. package/src/components/prompt/Autocomplete.js +12 -0
  15. package/src/components/prompt/Confirm.js +29 -0
  16. package/src/components/prompt/DateTime.js +26 -0
  17. package/src/components/prompt/Input.js +15 -0
  18. package/src/components/prompt/Mask.js +12 -0
  19. package/src/components/prompt/Multiselect.js +26 -0
  20. package/src/components/prompt/Next.js +8 -0
  21. package/src/components/prompt/Password.js +13 -0
  22. package/src/components/prompt/Pause.js +9 -0
  23. package/src/components/prompt/ProgressBar.js +16 -0
  24. package/src/components/prompt/Select.js +29 -0
  25. package/src/components/prompt/Slider.js +16 -0
  26. package/src/components/prompt/Spinner.js +29 -0
  27. package/src/components/prompt/Toggle.js +13 -0
  28. package/src/components/prompt/Tree.js +17 -0
  29. package/src/components/view/Alert.js +78 -0
  30. package/src/components/view/Badge.js +11 -0
  31. package/src/components/view/Nav.js +23 -0
  32. package/src/components/view/Table.js +12 -0
  33. package/src/components/view/Toast.js +9 -0
  34. package/src/core/Component.js +79 -0
  35. package/src/core/PropValidation.js +138 -0
  36. package/src/core/render.js +37 -0
  37. package/src/index.js +80 -41
  38. package/src/test/PlaygroundTest.js +37 -25
  39. package/src/test/index.js +2 -4
  40. package/src/ui/alert.js +58 -0
  41. package/src/ui/autocomplete.js +86 -0
  42. package/src/ui/badge.js +35 -0
  43. package/src/ui/confirm.js +49 -0
  44. package/src/ui/date-time.js +45 -0
  45. package/src/ui/form.js +120 -55
  46. package/src/ui/index.js +18 -4
  47. package/src/ui/input.js +79 -152
  48. package/src/ui/mask.js +132 -0
  49. package/src/ui/multiselect.js +59 -0
  50. package/src/ui/nav.js +74 -0
  51. package/src/ui/next.js +18 -13
  52. package/src/ui/progress.js +88 -0
  53. package/src/ui/select.js +49 -72
  54. package/src/ui/slider.js +154 -0
  55. package/src/ui/spinner.js +65 -0
  56. package/src/ui/table.js +163 -0
  57. package/src/ui/toast.js +34 -0
  58. package/src/ui/toggle.js +34 -0
  59. package/src/ui/tree.js +393 -0
  60. package/src/utils/parse.js +1 -1
  61. package/types/CLI.d.ts +5 -5
  62. package/types/CLiMessage.d.ts +1 -1
  63. package/types/Command.d.ts +2 -2
  64. package/types/CommandHelp.d.ts +3 -3
  65. package/types/CommandMessage.d.ts +8 -8
  66. package/types/CommandParser.d.ts +3 -3
  67. package/types/InputAdapter.d.ts +149 -15
  68. package/types/OutputAdapter.d.ts +1 -1
  69. package/types/README.md.d.ts +1 -1
  70. package/types/UiMessage.d.ts +31 -29
  71. package/types/components/prompt/Autocomplete.d.ts +6 -0
  72. package/types/components/prompt/Confirm.d.ts +6 -0
  73. package/types/components/prompt/DateTime.d.ts +6 -0
  74. package/types/components/prompt/Input.d.ts +6 -0
  75. package/types/components/prompt/Mask.d.ts +6 -0
  76. package/types/components/prompt/Multiselect.d.ts +6 -0
  77. package/types/components/prompt/Next.d.ts +6 -0
  78. package/types/components/prompt/Password.d.ts +6 -0
  79. package/types/components/prompt/Pause.d.ts +6 -0
  80. package/types/components/prompt/ProgressBar.d.ts +12 -0
  81. package/types/components/prompt/Select.d.ts +18 -0
  82. package/types/components/prompt/Slider.d.ts +6 -0
  83. package/types/components/prompt/Spinner.d.ts +21 -0
  84. package/types/components/prompt/Toggle.d.ts +6 -0
  85. package/types/components/prompt/Tree.d.ts +6 -0
  86. package/types/components/view/Alert.d.ts +21 -0
  87. package/types/components/view/Badge.d.ts +5 -0
  88. package/types/components/view/Nav.d.ts +15 -0
  89. package/types/components/view/Table.d.ts +10 -0
  90. package/types/components/view/Toast.d.ts +5 -0
  91. package/types/core/Component.d.ts +34 -0
  92. package/types/core/PropValidation.d.ts +48 -0
  93. package/types/core/render.d.ts +6 -0
  94. package/types/index.d.ts +47 -15
  95. package/types/test/PlaygroundTest.d.ts +12 -8
  96. package/types/test/index.d.ts +1 -1
  97. package/types/ui/alert.d.ts +14 -0
  98. package/types/ui/autocomplete.d.ts +20 -0
  99. package/types/ui/badge.d.ts +8 -0
  100. package/types/ui/confirm.d.ts +21 -0
  101. package/types/ui/date-time.d.ts +19 -0
  102. package/types/ui/form.d.ts +43 -12
  103. package/types/ui/index.d.ts +17 -2
  104. package/types/ui/input.d.ts +31 -74
  105. package/types/ui/mask.d.ts +29 -0
  106. package/types/ui/multiselect.d.ts +25 -0
  107. package/types/ui/nav.d.ts +27 -0
  108. package/types/ui/progress.d.ts +43 -0
  109. package/types/ui/select.d.ts +25 -64
  110. package/types/ui/slider.d.ts +23 -0
  111. package/types/ui/spinner.d.ts +28 -0
  112. package/types/ui/table.d.ts +28 -0
  113. package/types/ui/toast.d.ts +8 -0
  114. package/types/ui/toggle.d.ts +17 -0
  115. 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 "@nan0web/ui/core"
8
- import { createInput, Input } from "./input.js"
9
- import { select } from "./select.js"
10
- import { UiForm, FormInput } from "@nan0web/ui"
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 !== "object" || schema === null) continue
31
+ if (typeof schema !== 'object' || schema === null) continue
31
32
 
32
- const translate = (value) => (typeof t === "function" ? t(value) : value)
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 || "text",
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
- typeof opt === "string"
44
- ? opt
45
- : opt.label
46
- ? { label: opt.label, value: opt.value }
47
- : opt,
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
- const res = schema.validate(value)
53
- return res === true ? true : typeof res === "string" ? res : `Invalid ${name}`
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 !== "object" || !model.constructor) {
89
- throw new TypeError("Form requires a model instance with a constructor")
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
- const { stops = ["quit", "cancel", "exit"], inputFn } = options
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 !== "object" || schema === null) continue
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
- const validation = schema.validate
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
- const res = schema.validate(value)
113
- if (res === true) return true
114
- if (typeof res === "string") return res
115
- return `Invalid ${name}`
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 || "text",
138
+ label: this.t(schema.help || name),
139
+ type: schema.type || 'text',
122
140
  required: isRequired,
123
141
  placeholder,
124
- options,
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
- * Prompts for selection using the provided configuration.
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 ? " *" : ""} [${currentValue}]: `
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
- prompt: "Choose (number): ",
186
- options: field.options.map((opt) =>
187
- typeof opt === "string" ? opt : opt.label ? { label: opt.label, value: opt.value } : opt,
188
- ),
189
- console: { info: console.info.bind(console) },
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
- const selResult = await this.select(selConfig)
193
- const val = selResult.value
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 === "" && !field.required) {
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 ?? "string")
304
+ const type = schema?.type || typeof (schema?.defaultValue ?? 'string')
240
305
  switch (type) {
241
- case "number":
306
+ case 'number':
242
307
  return Number(value) || 0
243
- case "boolean":
244
- return value.toLowerCase() === "true"
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 "./input.js"
2
- export { createInput, ask } from "./input.js"
3
- export { select, default as baseSelect } from "./select.js"
4
- export { next, pause } from "./next.js"
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 { createInterface } from "node:readline"
8
- import { stdin, stdout } from "node:process"
9
- import { CancelError } from "@nan0web/ui"
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
- * Input function.
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 async function InputFn(question, loop = false, nextQuestion = undefined) {
32
- return new Input()
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
- * Low‑level prompt that returns a trimmed string.
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
- * When `predef` is supplied the function mimics the usual readline output
99
- * (`question + answer + newline`) and returns the trimmed value.
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 _askRaw(input) {
102
- const {
103
- question,
104
- predef = undefined,
105
- console,
106
- rl: initialRL,
107
- } = input
108
- // Fast‑path for predefined answers – mimic readline output.
109
- if (typeof predef === "string") {
110
- if (console) {
111
- console.info(`${question}${predef}\n`)
112
- } else {
113
- process.stdout.write(`${question}${predef}\n`)
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
- return predef.trim()
116
- }
82
+ )
117
83
 
118
- return new Promise(resolve => {
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 {ConsoleLike} [console] Optional console instance.
133
- * @returns {InputFn} Async function that resolves to an {@link Input}.
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
- const input = new Input({ stops })
137
-
138
- /**
139
- * Internal handler used by the factory.
140
- *
141
- * @param {string} question - Prompt displayed to the user.
142
- * @param {boolean | LoopFn} [loop=false] - Loop‑control flag or validator.
143
- * @param {string | NextQuestionFn} [nextQuestion=false] - Next prompt generator or its value.
144
- * @returns {Promise<Input>}
145
- */
146
- async function fn(question, loop = false, nextQuestion = undefined) {
147
- let currentQuestion = question
148
- while (true) {
149
- input.value = await _askRaw({ question: currentQuestion, predef, console })
150
-
151
- if (false === loop || input.cancelled) return input
152
- if (true === loop && input.value) return input
153
- if (typeof loop === "function") {
154
- const cont = await loop(input)
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
- * @param {string[]} predefined
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 predefinedHandler(question, loop = false, nextQuestion = undefined) {
186
- let currentQuestion = question
187
- while (true) {
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