@nan0web/ui-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -11
  2. package/package.json +17 -7
  3. package/src/CLI.js +141 -0
  4. package/src/Command.js +210 -0
  5. package/src/CommandError.js +35 -0
  6. package/src/CommandHelp.js +204 -0
  7. package/src/CommandMessage.js +181 -0
  8. package/src/CommandParser.js +217 -0
  9. package/src/InputAdapter.js +78 -149
  10. package/src/README.md.js +7 -6
  11. package/src/index.js +36 -14
  12. package/src/ui/index.js +3 -3
  13. package/src/ui/input.js +68 -11
  14. package/src/ui/next.js +30 -26
  15. package/src/ui/select.js +41 -31
  16. package/src/utils/parse.js +41 -0
  17. package/types/CLI.d.ts +44 -0
  18. package/types/Command.d.ts +72 -0
  19. package/types/CommandError.d.ts +19 -0
  20. package/types/CommandHelp.d.ts +85 -0
  21. package/types/CommandMessage.d.ts +65 -0
  22. package/types/CommandParser.d.ts +28 -0
  23. package/types/InputAdapter.d.ts +28 -85
  24. package/types/index.d.ts +12 -9
  25. package/types/ui/index.d.ts +2 -3
  26. package/types/ui/input.d.ts +50 -6
  27. package/types/ui/next.d.ts +11 -8
  28. package/types/ui/select.d.ts +50 -20
  29. package/types/utils/parse.d.ts +13 -0
  30. package/.editorconfig +0 -20
  31. package/CONTRIBUTING.md +0 -42
  32. package/docs/uk/README.md +0 -294
  33. package/playground/forms/addressForm.js +0 -37
  34. package/playground/forms/ageForm.js +0 -26
  35. package/playground/forms/profileForm.js +0 -33
  36. package/playground/forms/userForm.js +0 -36
  37. package/playground/main.js +0 -81
  38. package/playground/vocabs/en.js +0 -25
  39. package/playground/vocabs/index.js +0 -12
  40. package/playground/vocabs/uk.js +0 -25
  41. package/src/InputAdapter.test.js +0 -117
  42. package/src/ui/input.test.js +0 -27
  43. package/src/ui/select.test.js +0 -34
  44. package/system.md +0 -99
  45. package/tsconfig.json +0 -23
  46. package/types/test/ReadLine.d.ts +0 -1
  47. package/types/ui/errors.d.ts +0 -3
@@ -0,0 +1,181 @@
1
+ /**
2
+ * CommandMessage – generalized CLI message representation.
3
+ *
4
+ * @module CommandMessage
5
+ */
6
+
7
+ import { Message } from "@nan0web/co"
8
+ import { str2argv } from "./utils/parse.js"
9
+ import CommandError from "./CommandError.js"
10
+
11
+ /**
12
+ * @class
13
+ * @extends Message
14
+ */
15
+ export default class CommandMessage extends Message {
16
+ #name = ""
17
+ #argv = []
18
+ #opts = {}
19
+ #children = []
20
+
21
+ /**
22
+ * @param {Object} [input={}]
23
+ * @param {string} [input.name] - Command name.
24
+ * @param {string[]} [input.argv] - Positional arguments.
25
+ * @param {Object} [input.opts] - Options map.
26
+ * @param {Array<CommandMessage>} [input.children] - Nested messages.
27
+ * @param {Object} [input.body] - Message body payload.
28
+ */
29
+ constructor(input = {}) {
30
+ super(input)
31
+ /** @type {any} */
32
+ const data = typeof input === "object" && !Array.isArray(input) ? input : {}
33
+ const {
34
+ name = "",
35
+ argv = [],
36
+ opts = {},
37
+ children = [],
38
+ body = {},
39
+ } = data
40
+
41
+ const fullBody = { ...body, ...opts }
42
+ this.body = fullBody
43
+
44
+ this.#name = String(name)
45
+ this.#argv = argv.map(String)
46
+ this.#opts = opts
47
+ this.#children = children.map(c => CommandMessage.from(c))
48
+
49
+ if (typeof input === "string" || Array.isArray(input)) {
50
+ const parsed = CommandMessage.parse(input)
51
+ this.#name = parsed.name
52
+ this.#argv = parsed.argv
53
+ this.#opts = parsed.opts
54
+ this.body = { ...this.body, ...parsed.opts }
55
+ }
56
+ }
57
+
58
+ /** @returns {string} */
59
+ get name() { return this.#name }
60
+ /** @param {string} v */
61
+ set name(v) { this.#name = String(v) }
62
+
63
+ /** @returns {string[]} */
64
+ get argv() { return this.#argv }
65
+ /** @param {string[]} v */
66
+ set argv(v) { this.#argv = v.map(String) }
67
+
68
+ /** @returns {Object} */
69
+ get opts() { return this.#opts }
70
+ /** @param {Object} v */
71
+ set opts(v) { this.#opts = v }
72
+
73
+ /** @returns {Array<CommandMessage>} */
74
+ get children() { return this.#children }
75
+
76
+ /** @returns {Array<string>} Full command line (name + args). */
77
+ get args() { return [this.name, ...this.argv].filter(Boolean) }
78
+
79
+ /** @returns {string} Sub‑command name of the first child, or empty string. */
80
+ get subCommand() { return this.children[0]?.name || "" }
81
+
82
+ /** @returns {CommandMessage|null} First child message, or null. */
83
+ get subCommandMessage() { return this.children[0] || null }
84
+
85
+ /**
86
+ * Append a child {@link CommandMessage}.
87
+ *
88
+ * @param {CommandMessage|Object} msg
89
+ */
90
+ add(msg) { this.#children.push(CommandMessage.from(msg)) }
91
+
92
+ /**
93
+ * Convert the message back to a command‑line string.
94
+ *
95
+ * @returns {string}
96
+ */
97
+ toString() {
98
+ const optsStr = Object.entries(this.opts)
99
+ .map(([k, v]) => (v === true ? `--${k}` : `--${k} ${String(v)}`))
100
+ .join(" ")
101
+ const argsStr = this.argv.join(" ")
102
+ return [this.name, argsStr, optsStr].filter(Boolean).join(" ")
103
+ }
104
+
105
+ /**
106
+ * Parse raw CLI input into a {@link CommandMessage}.
107
+ *
108
+ * @param {string|string[]} argv - Input string or token array.
109
+ * @param {typeof Object} [BodyClass] - Optional class to instantiate the body.
110
+ * @returns {CommandMessage}
111
+ * @throws {CommandError} If no input is supplied.
112
+ */
113
+ static parse(argv, BodyClass) {
114
+ if (typeof argv === "string") argv = str2argv(argv)
115
+ if (argv.length === 0) throw new CommandError("No input provided")
116
+ const result = { name: "", argv: [], opts: {} }
117
+ let i = 0
118
+ if (!argv[0].startsWith("-")) {
119
+ result.name = argv[0]
120
+ i = 1
121
+ }
122
+ while (i < argv.length) {
123
+ const cur = argv[i]
124
+ if (cur.startsWith("--")) {
125
+ const eq = cur.indexOf("=")
126
+ if (eq > -1) {
127
+ const k = cur.slice(2, eq)
128
+ const v = cur.slice(eq + 1)
129
+ result.opts[k] = v
130
+ } else {
131
+ const k = cur.slice(2)
132
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
133
+ result.opts[k] = argv[++i]
134
+ } else {
135
+ result.opts[k] = true
136
+ }
137
+ }
138
+ } else if (cur.startsWith("-") && cur.length > 1) {
139
+ const shorts = cur.slice(1)
140
+ if (shorts.length > 1) {
141
+ shorts.split("").forEach(s => (result.opts[s] = true))
142
+ } else {
143
+ const k = shorts
144
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
145
+ result.opts[k] = argv[++i]
146
+ } else {
147
+ result.opts[k] = true
148
+ }
149
+ }
150
+ } else {
151
+ /** @ts-ignore */
152
+ result.argv.push(cur)
153
+ }
154
+ i++
155
+ }
156
+ const msg = new CommandMessage(result)
157
+ if (BodyClass) {
158
+ const body = new BodyClass(result.opts)
159
+ msg.body = body
160
+ /** @ts-ignore */
161
+ const errors = body.getErrors?.() || {}
162
+ if (Object.keys(errors).length) throw new CommandError("Validation failed", { errors })
163
+ }
164
+ return msg
165
+ }
166
+
167
+ /**
168
+ * Convert a raw input into a {@link CommandMessage} instance.
169
+ *
170
+ * @param {CommandMessage|Message|Object|string|Array<string>} input
171
+ * @returns {CommandMessage}
172
+ */
173
+ static from(input) {
174
+ if (input instanceof CommandMessage) return input
175
+ if (input instanceof Message) {
176
+ /** @ts-ignore */
177
+ return new CommandMessage({ body: input.body, name: input.name || "" })
178
+ }
179
+ return new CommandMessage(input)
180
+ }
181
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * CommandParser – parses argv into a hierarchical message tree supporting sub‑commands.
3
+ *
4
+ * @module CommandParser
5
+ */
6
+
7
+ import { Message } from "@nan0web/co"
8
+ import CommandHelp from "./CommandHelp.js"
9
+ import CommandError from "./CommandError.js"
10
+ import { str2argv } from "./utils/parse.js"
11
+
12
+ /**
13
+ * @class
14
+ */
15
+ export default class CommandParser {
16
+ /** @type {Array<Function>} */
17
+ Messages
18
+
19
+ /**
20
+ * @param {Array<Function>} [Messages=[]] - Root message classes.
21
+ */
22
+ constructor(Messages = []) {
23
+ this.Messages = Array.isArray(Messages) ? Messages : [Messages]
24
+ }
25
+
26
+ /**
27
+ * Parse the provided input into a message hierarchy.
28
+ *
29
+ * @param {string|string[]} [input=process.argv.slice(2)] - CLI arguments.
30
+ * @returns {Message}
31
+ * @throws {Error} If no command is supplied or unknown root command.
32
+ */
33
+ parse(input = process.argv.slice(2)) {
34
+ const argv = typeof input === "string" ? str2argv(input) : input
35
+ if (argv.length === 0) throw new Error("No command provided")
36
+ let rootName = null
37
+ let remaining = argv
38
+
39
+ if (!argv[0].startsWith("-")) {
40
+ rootName = argv[0]
41
+ remaining = argv.slice(1)
42
+
43
+ let RootClass = this.Messages.find(
44
+ cls => cls.name.toLowerCase() === rootName.toLowerCase(),
45
+ )
46
+ if (!RootClass) {
47
+ if (this.Messages.length === 1) RootClass = this.Messages[0]
48
+ else throw new Error(`Unknown root command: ${rootName}`)
49
+ }
50
+ // @ts-ignore – `RootClass` may not be a concrete `Message` subclass from TS view
51
+ const rootMessage = new RootClass({})
52
+ if (rootName) {
53
+ if (!rootMessage.head) rootMessage.head = {}
54
+ rootMessage.head.name = rootName
55
+ }
56
+ return this.#processMessageTree(rootMessage, remaining)
57
+ }
58
+
59
+ if (this.Messages.length !== 1) throw new Error("Unable to infer root command from options")
60
+ // @ts-ignore – see comment above
61
+ const RootClass = this.Messages[0]
62
+ // @ts-ignore
63
+ const rootMessage = new RootClass({})
64
+ if (!rootMessage.head) rootMessage.head = {}
65
+ return this.#processMessageTree(rootMessage, remaining)
66
+ }
67
+
68
+ /**
69
+ * Walk the message tree and attach sub‑commands and leaf arguments.
70
+ *
71
+ * @param {Message} rootMessage - Root message instance.
72
+ * @param {string[]} remainingTokens - Tokens yet to be processed.
73
+ * @returns {Message}
74
+ */
75
+ #processMessageTree(rootMessage, remainingTokens) {
76
+ let currentMessage = rootMessage
77
+ let remaining = remainingTokens
78
+
79
+ // @ts-ignore – `Children` is a static property on concrete message classes
80
+ while (currentMessage.constructor.Children && remaining.length) {
81
+ const subName = remaining[0]
82
+ // @ts-ignore
83
+ const SubClass = currentMessage.constructor.Children.find(
84
+ cls => cls.name.toLowerCase() === subName.toLowerCase(),
85
+ )
86
+ if (!SubClass) break
87
+
88
+ // @ts-ignore
89
+ const subMessage = new SubClass({})
90
+ subMessage.name = subName
91
+ currentMessage.body.subCommand = subMessage
92
+ currentMessage = subMessage
93
+ remaining = remaining.slice(1)
94
+ }
95
+
96
+ if (remaining.length) {
97
+ const parsedBody = this.#parseLeafBody(
98
+ remaining,
99
+ // @ts-ignore – `Body` may be undefined on some classes
100
+ currentMessage.constructor.Body,
101
+ )
102
+ currentMessage.body = { ...currentMessage.body, ...parsedBody }
103
+ }
104
+
105
+ if (
106
+ rootMessage.body.subCommand &&
107
+ typeof rootMessage.body.subCommand.assertValid === "function"
108
+ ) {
109
+ rootMessage.body.subCommand.assertValid()
110
+ }
111
+ return rootMessage
112
+ }
113
+
114
+ /**
115
+ * Parse leaf‑level arguments into the provided body class.
116
+ *
117
+ * @param {string[]} tokens - Remaining CLI tokens.
118
+ * @param {typeof Message} BodyClass - Class defining fields and validation.
119
+ * @returns {Object} Instance of BodyClass populated with parsed values.
120
+ */
121
+ #parseLeafBody(tokens, BodyClass) {
122
+ const body = new BodyClass()
123
+ const props = Object.keys(body)
124
+
125
+ for (let i = 0; i < tokens.length; i++) {
126
+ const token = tokens[i]
127
+
128
+ if (token.startsWith("--")) {
129
+ let key = token.slice(2)
130
+ /** @type {boolean | string} */
131
+ let value = true
132
+ const eqIdx = key.indexOf("=")
133
+ if (eqIdx > -1) {
134
+ value = key.slice(eqIdx + 1)
135
+ key = key.slice(0, eqIdx)
136
+ } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith("-")) {
137
+ value = tokens[++i]
138
+ }
139
+ const realKey = this.#resolveAlias(key, BodyClass) || key
140
+ if (props.includes(realKey)) body[realKey] = this.#convertType(body[realKey], value)
141
+ } else if (token.startsWith("-") && token.length > 1) {
142
+ const short = token.slice(1)
143
+ if (short.length > 1) {
144
+ short.split("").forEach(ch => {
145
+ const realKey = this.#resolveAlias(ch, BodyClass)
146
+ if (realKey && props.includes(realKey)) body[realKey] = true
147
+ })
148
+ } else {
149
+ const realKey = this.#resolveAlias(short, BodyClass) || short
150
+ if (props.includes(realKey)) {
151
+ /** @type {boolean | string} */
152
+ let value = true
153
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith("-")) {
154
+ value = tokens[++i]
155
+ }
156
+ body[realKey] = this.#convertType(body[realKey], value)
157
+ }
158
+ }
159
+ } else {
160
+ const first = props[0]
161
+ if (first) body[first] = token
162
+ }
163
+ }
164
+
165
+ props.forEach(prop => {
166
+ const schema = BodyClass[prop]
167
+ if (schema?.default !== undefined && body[prop] === undefined) {
168
+ body[prop] = schema.default
169
+ }
170
+ const err = schema?.validate?.(body[prop], body)
171
+ if (err !== undefined && err !== null && err !== true) {
172
+ throw new CommandError(`Invalid ${prop}: ${err}`, { [prop]: err })
173
+ }
174
+ })
175
+
176
+ return body
177
+ }
178
+
179
+ /**
180
+ * Resolve an alias to its full property name.
181
+ *
182
+ * @param {string} alias
183
+ * @param {Function} BodyClass
184
+ * @returns {string|null}
185
+ */
186
+ #resolveAlias(alias, BodyClass) {
187
+ for (const [prop, schema] of Object.entries(BodyClass)) {
188
+ if (schema?.alias === alias) return prop
189
+ }
190
+ return null
191
+ }
192
+
193
+ /**
194
+ * Convert a raw CLI string to the appropriate JavaScript type.
195
+ *
196
+ * @param {*} defaultVal - The default value used for type inference.
197
+ * @param {*} value - Raw parsed value.
198
+ * @returns {*}
199
+ */
200
+ #convertType(defaultVal, value) {
201
+ const type = typeof defaultVal
202
+ if (type === "boolean")
203
+ return Boolean(value !== "false" && value !== false && value !== "")
204
+ if (type === "number") return Number(value) || 0
205
+ return String(value)
206
+ }
207
+
208
+ /**
209
+ * Generate help text for a given message class.
210
+ *
211
+ * @param {typeof Message} MessageClass
212
+ * @returns {string}
213
+ */
214
+ generateHelp(MessageClass) {
215
+ return new CommandHelp(MessageClass).generate()
216
+ }
217
+ }
@@ -1,219 +1,148 @@
1
- import { Message } from '@nan0web/co'
2
- import { UIForm, InputAdapter as BaseInputAdapter, InputMessage as BaseInputMessage } from '@nan0web/ui'
3
- import { ask } from './ui/input.js'
4
- import { select } from './ui/select.js'
5
-
6
- /** @typedef {Partial<UIForm>} FormMessageValue */
7
- /** @typedef {Partial<Message> | null} InputMessageValue */
8
-
9
1
  /**
10
- * Extends the generic {@link BaseInputMessage} to carry a {@link UIForm}
11
- * instance alongside the usual input message payload.
2
+ * CLIInputAdapter bridges UI‑CLI utilities with generic UI core.
12
3
  *
13
- * The original {@link BaseInputMessage} expects a `value` of type
14
- * {@link InputMessageValue} (a {@link Message} payload). To remain
15
- * compatible we keep `value` unchanged and store the form data in a
16
- * separate `form` property.
4
+ * @module InputAdapter
17
5
  */
18
- class FormMessage extends BaseInputMessage {
19
- /** @type {UIForm} Form data associated with the message */
20
- form
21
6
 
22
- /**
23
- * Creates a new {@link FormMessage}.
24
- *
25
- * @param {object} props - Message properties.
26
- * @param {FormMessageValue} [props.form={}] UIForm instance or data.
27
- * @param {InputMessageValue} [props.value=null] Retained for compatibility.
28
- * @param {string[]|string} [props.options=[]] Available options.
29
- * @param {boolean} [props.waiting=false] Waiting flag.
30
- * @param {boolean} [props.escaped=false] Escape flag.
31
- */
32
- constructor(props = {}) {
33
- const {
34
- form = {},
35
- ...rest
36
- } = props
37
- // Initialise the parent with the remaining properties.
38
- // Cast to `any` to avoid type‑mismatch between duplicated InputMessage
39
- // definitions across packages.
40
- super(/** @type {any} */ (rest))
41
-
42
- // Store the UIForm; accept an object, UIForm or a plain payload.
43
- this.form = UIForm.from(form)
44
- }
45
-
46
- /**
47
- * Creates a {@link FormMessage} from an existing instance or plain data.
48
- *
49
- * @param {FormMessage|object} input – Existing message or raw data.
50
- * @returns {FormMessage}
51
- */
52
- static from(input) {
53
- if (input instanceof FormMessage) return input
54
- const {
55
- form = {},
56
- ...rest
57
- } = input ?? {}
58
- return new FormMessage({ form, ...rest })
59
- }
60
- }
7
+ import { Message } from "@nan0web/co"
8
+ import {
9
+ UIForm,
10
+ InputAdapter as BaseInputAdapter,
11
+ } from "@nan0web/ui"
12
+ import { ask } from "./ui/input.js"
13
+ import { select } from "./ui/select.js"
14
+ import { CancelError } from "@nan0web/ui/core"
61
15
 
62
16
  /**
63
- * CLI specific adapter extending the generic {@link BaseInputAdapter}.
64
- * Implements concrete `ask` and `select` helpers that rely on the CLI utilities.
17
+ * Extends the generic {@link BaseInputAdapter} with CLI‑specific behaviour.
18
+ *
19
+ * @class
20
+ * @extends BaseInputAdapter
65
21
  */
66
22
  export default class CLIInputAdapter extends BaseInputAdapter {
67
23
  /**
68
- * Interactively fill a {@link UIForm} field‑by‑field.
24
+ * Prompt the user for a full form, handling navigation and validation.
69
25
  *
70
- * @param {UIForm} form Form definition to be filled.
71
- * @param {Object} options – Request options.
72
- * @param {boolean} [options.silent=true] Suppress title output when `true`.
73
- * @returns {Promise<FormMessage>} Message with `escaped` = true on cancel,
74
- * otherwise `escaped` = false and the completed form attached as `form`.
26
+ * @param {UIForm} form - Form definition to present.
27
+ * @param {Object} [options={}]
28
+ * @param {boolean} [options.silent=true] - Suppress console output if `true`.
29
+ * @returns {Promise<Object>} Result object containing form data and meta‑information.
75
30
  */
76
31
  async requestForm(form, options = {}) {
77
32
  const { silent = true } = options
78
-
79
- if (!silent) {
80
- console.log(`\n${form.title}\n`)
81
- }
33
+ if (!silent) console.log(`\n${form.title}\n`)
82
34
 
83
35
  let formData = { ...form.state }
84
- let currentFieldIndex = 0
85
-
86
- while (currentFieldIndex < form.fields.length) {
87
- const field = form.fields[currentFieldIndex]
88
- const prompt = field.label || field.name
89
-
90
- const input = await this.ask(`${prompt}${field.required ? ' *' : ''}: `)
91
-
92
- // Cancel (Esc or empty string)
93
- if ([FormMessage.ESCAPE, ''].includes(input)) {
94
- return FormMessage.from({
36
+ let idx = 0
37
+
38
+ while (idx < form.fields.length) {
39
+ const field = form.fields[idx]
40
+ const prompt = `${field.label || field.name}${field.required ? " *" : ""}: `
41
+ const answer = await this.ask(prompt)
42
+
43
+ if (["", "esc"].includes(answer)) {
44
+ // @ts-ignore `UIForm` may not expose `id` in its TS definition
45
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
46
+ return {
47
+ // @ts-ignore
48
+ body: { action: "form-cancel", escaped: true, form: {}, id: form.id },
95
49
  form: {},
96
50
  escaped: true,
97
- action: 'form-cancel',
51
+ action: "form-cancel",
52
+ // @ts-ignore
98
53
  id: form.id,
99
- })
54
+ }
100
55
  }
101
56
 
102
- // Navigation shortcuts
103
- const trimmed = input.trim()
104
- if (trimmed === '::prev' || trimmed === '::back') {
105
- currentFieldIndex = Math.max(0, currentFieldIndex - 1)
57
+ const trimmed = answer.trim()
58
+ if (trimmed === "::prev" || trimmed === "::back") {
59
+ idx = Math.max(0, idx - 1)
106
60
  continue
107
61
  }
108
- if (trimmed === '::next' || trimmed === '::skip') {
109
- currentFieldIndex++
62
+ if (trimmed === "::next" || trimmed === "::skip") {
63
+ idx++
110
64
  continue
111
65
  }
112
-
113
- // Skip optional fields when empty
114
- if (trimmed === '' && !field.required) {
115
- currentFieldIndex++
66
+ if (trimmed === "" && !field.required) {
67
+ idx++
68
+ continue
69
+ }
70
+ if (field.required && trimmed === "") {
71
+ console.log("\nField is required.")
116
72
  continue
117
73
  }
118
-
119
- // Validate using the form's schema / field definition
120
74
  const schema = field.constructor
121
75
  const { isValid, errors } = form.validateValue(field.name, trimmed, schema)
122
76
  if (!isValid) {
123
- const errorMessages = Object.values(errors)
124
- console.log(`\x1b[31mError: ${errorMessages.join(', ')}\x1b[0m`)
125
- continue // stay on current field
77
+ console.log("\n" + Object.values(errors).join("\n"))
78
+ continue
126
79
  }
127
-
128
- // Store validated value
129
80
  formData[field.name] = trimmed
130
- currentFieldIndex++
81
+ idx++
131
82
  }
132
83
 
133
- // Final form validation
134
84
  const finalForm = form.setData(formData)
135
85
  const { isValid, errors } = finalForm.validate()
136
86
  if (!isValid) {
137
- console.log('\n' + Object.values(errors).join('\n'))
138
- return await this.requestForm(form, options) // retry recursively
87
+ console.log("\n" + Object.values(errors).join("\n"))
88
+ return await this.requestForm(form, options)
139
89
  }
140
90
 
141
- return FormMessage.from({
91
+ // @ts-ignore – `UIForm` may not expose `id`
92
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
93
+ return {
94
+ // @ts-ignore
95
+ body: { action: "form-submit", escaped: false, form: finalForm, id: form.id },
142
96
  form: finalForm,
143
97
  escaped: false,
144
- action: 'form-submit',
98
+ action: "form-submit",
99
+ // @ts-ignore
145
100
  id: form.id,
146
- })
101
+ }
147
102
  }
148
103
 
149
104
  /**
150
- * Request a selection from a list of options.
105
+ * Prompt the user to select an option from a list.
151
106
  *
152
- * @param {Object} config Selection configuration.
153
- * @param {string} config.title Title displayed before the list.
154
- * @param {string} config.prompt – Prompt text.
155
- * @param {Array<string>|Map<string,string>|Array<{label:string,value:string}>} config.options – Options to choose from.
156
- * @param {string} config.id – Identifier for the resulting message.
157
- * @returns {Promise<BaseInputMessage>} Message containing chosen value and metadata.
107
+ * @param {Object} config - Configuration passed to {@link select}.
108
+ * @returns {Promise<any>} Selected value, or empty string on cancellation.
158
109
  */
159
110
  async requestSelect(config) {
160
111
  try {
161
112
  const result = await this.select({
162
- title: config.title ?? 'Select an option:',
163
- prompt: config.prompt ?? 'Choose (1‑N): ',
113
+ title: config.title ?? "Select an option:",
114
+ prompt: config.prompt ?? "Choose (1‑N): ",
164
115
  options: config.options,
165
116
  console: console,
166
117
  })
167
- return BaseInputMessage.from({
168
- value: result.value,
169
- data: result,
170
- id: config.id,
171
- })
172
- } catch (error) {
173
- if (error instanceof this.CancelError) {
174
- return BaseInputMessage.from({
175
- value: '',
176
- id: config.id,
177
- })
178
- }
179
- throw error
118
+ return result.value
119
+ } catch (e) {
120
+ if (e instanceof CancelError) return ""
121
+ throw e
180
122
  }
181
123
  }
182
124
 
183
125
  /**
184
- * Simple string input request.
126
+ * Prompt for a single string input.
185
127
  *
186
- * @param {Object} config Input configuration.
187
- * @param {string} config.prompt Prompt text.
188
- * @param {string} config.id Identifier for the resulting message.
189
- * @param {string} [config.label] Optional label used as fallback.
190
- * @param {string} [config.name] Optional name used as fallback.
191
- * @returns {Promise<BaseInputMessage>} Message containing the entered text.
128
+ * @param {Object} config - Prompt configuration.
129
+ * @param {string} [config.prompt] - Prompt text.
130
+ * @param {string} [config.label] - Optional label.
131
+ * @param {string} [config.name] - Optional identifier.
132
+ * @returns {Promise<string>} User response string.
192
133
  */
193
134
  async requestInput(config) {
194
135
  const prompt = config.prompt ?? `${config.label ?? config.name}: `
195
136
  const input = await this.ask(prompt)
196
-
197
- if (input === '') {
198
- return BaseInputMessage.from({
199
- value: '',
200
- id: config.id,
201
- })
202
- }
203
-
204
- return BaseInputMessage.from({
205
- value: input,
206
- id: config.id,
207
- })
137
+ return input
208
138
  }
209
139
 
210
140
  /** @inheritDoc */
211
141
  async ask(question) {
212
142
  return ask(question)
213
143
  }
214
-
215
144
  /** @inheritDoc */
216
- async select(config) {
217
- return select(config)
145
+ async select(cfg) {
146
+ return select(cfg)
218
147
  }
219
148
  }