@nan0web/ui-cli 1.0.2 → 1.1.1
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 +10 -10
- package/package.json +18 -3
- package/src/CLI.js +51 -41
- package/src/CLiMessage.js +9 -0
- package/src/CommandHelp.js +2 -10
- package/src/CommandMessage.js +1 -0
- package/src/InputAdapter.js +275 -48
- package/src/OutputAdapter.js +61 -0
- package/src/README.md.js +15 -16
- package/src/components/Alert.js +22 -0
- package/src/index.js +9 -3
- package/src/test/PlaygroundTest.js +157 -0
- package/src/test/index.js +7 -0
- package/src/ui/Adapter.js +3 -0
- package/src/ui/form.js +254 -0
- package/src/ui/input.js +119 -17
- package/src/ui/select.js +64 -27
- package/types/CLI.d.ts +11 -10
- package/types/CLiMessage.d.ts +8 -0
- package/types/CommandMessage.d.ts +1 -0
- package/types/InputAdapter.d.ts +64 -17
- package/types/OutputAdapter.d.ts +29 -0
- package/types/UiMessage.d.ts +40 -0
- package/types/components/Alert.d.ts +15 -0
- package/types/index.d.ts +5 -3
- package/types/test/PlaygroundTest.d.ts +80 -0
- package/types/test/index.d.ts +3 -0
- package/types/ui/Adapter.d.ts +1 -0
- package/types/ui/form.d.ts +90 -0
- package/types/ui/input.d.ts +58 -11
- package/types/ui/select.d.ts +32 -18
package/src/InputAdapter.js
CHANGED
|
@@ -1,36 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CLiInputAdapter – bridges UI‑CLI utilities with generic UI core.
|
|
3
3
|
*
|
|
4
4
|
* @module InputAdapter
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Message } from "@nan0web/co"
|
|
8
7
|
import {
|
|
9
|
-
|
|
8
|
+
UiForm,
|
|
10
9
|
InputAdapter as BaseInputAdapter,
|
|
10
|
+
UiMessage,
|
|
11
11
|
} from "@nan0web/ui"
|
|
12
|
-
import { ask } from "./ui/input.js"
|
|
13
|
-
import { select } from "./ui/select.js"
|
|
14
12
|
import { CancelError } from "@nan0web/ui/core"
|
|
15
13
|
|
|
14
|
+
import { ask as baseAsk, createInput, createPredefinedInput, Input } from "./ui/input.js"
|
|
15
|
+
import { select as baseSelect } from "./ui/select.js"
|
|
16
|
+
import { generateForm } from "./ui/form.js"
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("./ui/select.js").InputFn} InputFn */
|
|
19
|
+
/** @typedef {import("./ui/select.js").ConsoleLike} ConsoleLike */
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* Extends the generic {@link BaseInputAdapter} with CLI‑specific behaviour.
|
|
18
23
|
*
|
|
19
24
|
* @class
|
|
20
25
|
* @extends BaseInputAdapter
|
|
21
26
|
*/
|
|
22
|
-
export default class
|
|
27
|
+
export default class CLiInputAdapter extends BaseInputAdapter {
|
|
28
|
+
/** @type {string[]} Queue of predefined answers. */
|
|
29
|
+
#answers = []
|
|
30
|
+
/** @type {number} Current position in the answers queue. */
|
|
31
|
+
#cursor = 0
|
|
32
|
+
/** @type {ConsoleLike} */
|
|
33
|
+
#console
|
|
34
|
+
#stdout
|
|
35
|
+
/** @type {Map<string, () => Promise<Function>>} */
|
|
36
|
+
#components = new Map()
|
|
37
|
+
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
super()
|
|
40
|
+
const {
|
|
41
|
+
predefined = process.env.PLAY_DEMO_SEQUENCE ?? [],
|
|
42
|
+
divider = process.env.PLAY_DEMO_DIVIDER ?? ",",
|
|
43
|
+
console: initialConsole = console,
|
|
44
|
+
stdout = process.stdout,
|
|
45
|
+
components = new Map(),
|
|
46
|
+
} = options
|
|
47
|
+
|
|
48
|
+
this.#console = initialConsole
|
|
49
|
+
this.#stdout = stdout
|
|
50
|
+
this.#components = components
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(predefined)) {
|
|
53
|
+
this.#answers = predefined.map(v => String(v))
|
|
54
|
+
} else if (typeof predefined === "string") {
|
|
55
|
+
this.#answers = predefined
|
|
56
|
+
.split(divider)
|
|
57
|
+
.map(v => v.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
} else {
|
|
60
|
+
this.#answers = []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @returns {ConsoleLike} */
|
|
65
|
+
get console() { return this.#console }
|
|
66
|
+
/** @returns {NodeJS.WriteStream} */
|
|
67
|
+
get stdout() { return this.#stdout }
|
|
68
|
+
|
|
69
|
+
#nextAnswer() {
|
|
70
|
+
if (this.#cursor < this.#answers.length) {
|
|
71
|
+
const val = this.#answers[this.#cursor]
|
|
72
|
+
this.#cursor++
|
|
73
|
+
return val
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalise a value that can be either a raw string or an {@link Input}
|
|
80
|
+
* instance (which carries the actual value in its `value` property).
|
|
81
|
+
*
|
|
82
|
+
* @param {any} val – Raw value or {@link Input}.
|
|
83
|
+
* @returns {string} Plain string value.
|
|
84
|
+
*/
|
|
85
|
+
#normalise(val) {
|
|
86
|
+
if (val && typeof val === "object" && "value" in val) {
|
|
87
|
+
return String(val.value)
|
|
88
|
+
}
|
|
89
|
+
return String(val ?? "")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a handler with stop words that supports predefined answers.
|
|
94
|
+
*
|
|
95
|
+
* @param {string[]} stops - Stop words for cancellation.
|
|
96
|
+
* @returns {InputFn}
|
|
97
|
+
*/
|
|
98
|
+
createHandler(stops = []) {
|
|
99
|
+
const self = this
|
|
100
|
+
return async (question, loop = false, nextQuestion = undefined) => {
|
|
101
|
+
const predefined = self.#nextAnswer()
|
|
102
|
+
if (predefined !== null) {
|
|
103
|
+
this.stdout.write(`${question}${predefined}\n`)
|
|
104
|
+
const input = new Input({ value: predefined, stops })
|
|
105
|
+
if (input.cancelled) {
|
|
106
|
+
throw new CancelError("Cancelled via stop word")
|
|
107
|
+
}
|
|
108
|
+
return input
|
|
109
|
+
}
|
|
110
|
+
const interactive = createInput(stops)
|
|
111
|
+
return interactive(question, loop, nextQuestion)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
23
115
|
/**
|
|
24
116
|
* Prompt the user for a full form, handling navigation and validation.
|
|
25
117
|
*
|
|
26
|
-
* @param {
|
|
118
|
+
* @param {UiForm} form - Form definition to present.
|
|
27
119
|
* @param {Object} [options={}]
|
|
28
120
|
* @param {boolean} [options.silent=true] - Suppress console output if `true`.
|
|
29
121
|
* @returns {Promise<Object>} Result object containing form data and meta‑information.
|
|
30
122
|
*/
|
|
31
123
|
async requestForm(form, options = {}) {
|
|
32
124
|
const { silent = true } = options
|
|
33
|
-
if (!silent) console.
|
|
125
|
+
if (!silent) this.console.info(`\n${form.title}\n`)
|
|
34
126
|
|
|
35
127
|
let formData = { ...form.state }
|
|
36
128
|
let idx = 0
|
|
@@ -38,23 +130,56 @@ export default class CLIInputAdapter extends BaseInputAdapter {
|
|
|
38
130
|
while (idx < form.fields.length) {
|
|
39
131
|
const field = form.fields[idx]
|
|
40
132
|
const prompt = `${field.label || field.name}${field.required ? " *" : ""}: `
|
|
41
|
-
const answer = await this.ask(prompt)
|
|
42
133
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
134
|
+
// ---- Handle select fields (options) ----------------------------------------
|
|
135
|
+
if (Array.isArray(field.options ?? 0) && field.options.length) {
|
|
136
|
+
const options = /** @type {any[]} */ (field.options)
|
|
137
|
+
|
|
138
|
+
const predefined = this.#nextAnswer()
|
|
139
|
+
const selConfig = {
|
|
140
|
+
title: field.label,
|
|
141
|
+
prompt: "Choose (number): ",
|
|
142
|
+
options: options.map(opt =>
|
|
143
|
+
typeof opt === "string"
|
|
144
|
+
? { label: opt, value: opt }
|
|
145
|
+
: opt
|
|
146
|
+
),
|
|
147
|
+
console: { info: this.console.info.bind(this.console) },
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let selResult
|
|
151
|
+
if (predefined !== null) {
|
|
152
|
+
selConfig.ask = createPredefinedInput([predefined], this.console)
|
|
153
|
+
selResult = await this.select(selConfig)
|
|
154
|
+
} else {
|
|
155
|
+
selConfig.ask = this.createHandler(["cancel", "quit", "exit"])
|
|
156
|
+
selResult = await this.select(selConfig)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const chosen = selResult?.value
|
|
160
|
+
if (!options.includes(chosen)) {
|
|
161
|
+
this.console.error("\nEnumeration must have one value")
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
formData[field.name] = String(chosen)
|
|
165
|
+
idx++
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const rawAnswer = await this.ask(prompt)
|
|
171
|
+
const answerStr = this.#normalise(rawAnswer)
|
|
172
|
+
|
|
173
|
+
if (["", "esc"].includes(answerStr)) {
|
|
46
174
|
return {
|
|
47
|
-
|
|
48
|
-
body: { action: "form-cancel", escaped: true, form: {}, id: form.id },
|
|
175
|
+
body: { action: "form-cancel", cancelled: true, form: {} },
|
|
49
176
|
form: {},
|
|
50
|
-
|
|
177
|
+
cancelled: true,
|
|
51
178
|
action: "form-cancel",
|
|
52
|
-
// @ts-ignore
|
|
53
|
-
id: form.id,
|
|
54
179
|
}
|
|
55
180
|
}
|
|
56
181
|
|
|
57
|
-
const trimmed =
|
|
182
|
+
const trimmed = answerStr.trim()
|
|
58
183
|
if (trimmed === "::prev" || trimmed === "::back") {
|
|
59
184
|
idx = Math.max(0, idx - 1)
|
|
60
185
|
continue
|
|
@@ -68,13 +193,13 @@ export default class CLIInputAdapter extends BaseInputAdapter {
|
|
|
68
193
|
continue
|
|
69
194
|
}
|
|
70
195
|
if (field.required && trimmed === "") {
|
|
71
|
-
console.
|
|
196
|
+
this.console.info("\nField is required.")
|
|
72
197
|
continue
|
|
73
198
|
}
|
|
74
199
|
const schema = field.constructor
|
|
75
200
|
const { isValid, errors } = form.validateValue(field.name, trimmed, schema)
|
|
76
201
|
if (!isValid) {
|
|
77
|
-
console.
|
|
202
|
+
this.console.warn("\n" + Object.values(errors).join("\n"))
|
|
78
203
|
continue
|
|
79
204
|
}
|
|
80
205
|
formData[field.name] = trimmed
|
|
@@ -82,39 +207,78 @@ export default class CLIInputAdapter extends BaseInputAdapter {
|
|
|
82
207
|
}
|
|
83
208
|
|
|
84
209
|
const finalForm = form.setData(formData)
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
console.
|
|
210
|
+
const errors = finalForm.validate()
|
|
211
|
+
if (errors.size) {
|
|
212
|
+
this.console.warn("\n" + Object.values(errors).join("\n"))
|
|
88
213
|
return await this.requestForm(form, options)
|
|
89
214
|
}
|
|
90
|
-
|
|
91
|
-
// @ts-ignore – `UIForm` may not expose `id`
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
93
215
|
return {
|
|
94
|
-
|
|
95
|
-
body: { action: "form-submit", escaped: false, form: finalForm, id: form.id },
|
|
216
|
+
body: { action: "form-submit", cancelled: false, form: finalForm },
|
|
96
217
|
form: finalForm,
|
|
97
|
-
|
|
218
|
+
cancelled: false,
|
|
98
219
|
action: "form-submit",
|
|
99
|
-
// @ts-ignore
|
|
100
|
-
id: form.id,
|
|
101
220
|
}
|
|
102
221
|
}
|
|
103
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Render a UI component in the CLI environment.
|
|
225
|
+
*
|
|
226
|
+
* The current CLI adapter only supports simple textual rendering.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} component - Component name (e.g. `"Alert"`).
|
|
229
|
+
* @param {object} props - Props object passed to the component.
|
|
230
|
+
* @returns {Promise<void>}
|
|
231
|
+
*/
|
|
232
|
+
async render(component, props) {
|
|
233
|
+
const compLoader = this.#components.get(component)
|
|
234
|
+
if (compLoader) {
|
|
235
|
+
try {
|
|
236
|
+
const compFn = await compLoader()
|
|
237
|
+
if (typeof compFn === "function") {
|
|
238
|
+
compFn.call(this, props)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
} catch (/** @type {any} */ err) {
|
|
242
|
+
this.console.error(`Failed to render component "${component}": ${err.message}`)
|
|
243
|
+
this.console.debug?.(err.stack)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (props && typeof props === "object") {
|
|
247
|
+
const { variant, content } = props
|
|
248
|
+
const prefix = variant ? `[${variant}]` : ""
|
|
249
|
+
this.console.info(`${prefix} ${String(content)}`)
|
|
250
|
+
} else {
|
|
251
|
+
this.console.info(String(component))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Process a full form – thin wrapper around {@link requestForm}.
|
|
257
|
+
*
|
|
258
|
+
* @param {UiForm} form - Form definition.
|
|
259
|
+
* @param {object} [_state] - Unused, kept for compatibility with `CLiMessage`.
|
|
260
|
+
* @returns {Promise<Object>} Same shape as {@link requestForm} result.
|
|
261
|
+
*/
|
|
262
|
+
async processForm(form, _state) {
|
|
263
|
+
return this.requestForm(form)
|
|
264
|
+
}
|
|
265
|
+
|
|
104
266
|
/**
|
|
105
267
|
* Prompt the user to select an option from a list.
|
|
106
268
|
*
|
|
107
|
-
* @param {Object} config - Configuration
|
|
108
|
-
* @returns {Promise<
|
|
269
|
+
* @param {Object} config - Configuration object.
|
|
270
|
+
* @returns {Promise<string>} Selected value (or empty string on cancel).
|
|
109
271
|
*/
|
|
110
272
|
async requestSelect(config) {
|
|
111
273
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
274
|
+
const predefined = this.#nextAnswer()
|
|
275
|
+
if (!config.console) config.console = { info: () => { } }
|
|
276
|
+
|
|
277
|
+
if (predefined !== null) {
|
|
278
|
+
config.yes = true
|
|
279
|
+
config.ask = createPredefinedInput([predefined], config.console)
|
|
280
|
+
}
|
|
281
|
+
const result = await this.select(config)
|
|
118
282
|
return result.value
|
|
119
283
|
} catch (e) {
|
|
120
284
|
if (e instanceof CancelError) return ""
|
|
@@ -126,23 +290,86 @@ export default class CLIInputAdapter extends BaseInputAdapter {
|
|
|
126
290
|
* Prompt for a single string input.
|
|
127
291
|
*
|
|
128
292
|
* @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
293
|
* @returns {Promise<string>} User response string.
|
|
133
294
|
*/
|
|
134
295
|
async requestInput(config) {
|
|
296
|
+
const predefined = this.#nextAnswer()
|
|
297
|
+
if (predefined !== null) return predefined
|
|
298
|
+
|
|
299
|
+
if (config.yes === true && config.value !== undefined) {
|
|
300
|
+
return config.value
|
|
301
|
+
}
|
|
135
302
|
const prompt = config.prompt ?? `${config.label ?? config.name}: `
|
|
136
|
-
const
|
|
137
|
-
return
|
|
303
|
+
const answer = await this.ask(prompt)
|
|
304
|
+
return this.#normalise(answer)
|
|
138
305
|
}
|
|
139
306
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Asks user a question or form and returns the completed form
|
|
309
|
+
* @param {string | UiForm} question
|
|
310
|
+
* @param {object} [options={}]
|
|
311
|
+
*
|
|
312
|
+
*/
|
|
313
|
+
async ask(question, options = {}) {
|
|
314
|
+
if (question instanceof UiForm) {
|
|
315
|
+
return await this.requestForm(question, options)
|
|
316
|
+
}
|
|
317
|
+
const predefined = this.#nextAnswer()
|
|
318
|
+
if (predefined !== null) {
|
|
319
|
+
this.stdout.write(`${question}${predefined}\n`)
|
|
320
|
+
return predefined
|
|
321
|
+
}
|
|
322
|
+
const input = await baseAsk(question)
|
|
323
|
+
return input.value
|
|
143
324
|
}
|
|
325
|
+
|
|
144
326
|
/** @inheritDoc */
|
|
145
327
|
async select(cfg) {
|
|
146
|
-
return
|
|
328
|
+
return baseSelect(cfg)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* **New API** – Require input for a {@link UiMessage} instance.
|
|
333
|
+
*
|
|
334
|
+
* This method mirrors the previous `UiMessage.requireInput` logic, but is now
|
|
335
|
+
* owned by the UI adapter. It validates the message according to its static
|
|
336
|
+
* {@link UiMessage.Body} schema, presents a generated {@link UiForm} and
|
|
337
|
+
* returns the updated body. Cancellation results in a {@link CancelError}.
|
|
338
|
+
*
|
|
339
|
+
* @param {UiMessage} msg - Message instance needing input.
|
|
340
|
+
* @returns {Promise<any>} Updated message body.
|
|
341
|
+
* @throws {CancelError} When user cancels the input process.
|
|
342
|
+
*/
|
|
343
|
+
async requireInput(msg) {
|
|
344
|
+
if (!msg) {
|
|
345
|
+
throw new Error("Message instance is required")
|
|
346
|
+
}
|
|
347
|
+
if (!(msg instanceof UiMessage)) {
|
|
348
|
+
throw new TypeError("Message must be an instance UiMessage")
|
|
349
|
+
}
|
|
350
|
+
/** @type {Map<string,string>} */
|
|
351
|
+
let errors = msg.validate()
|
|
352
|
+
while (errors.size > 0) {
|
|
353
|
+
const form = generateForm(
|
|
354
|
+
/** @type {any} */(msg.constructor).Body,
|
|
355
|
+
{ initialState: msg.body }
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
const formResult = await this.processForm(form, msg.body)
|
|
359
|
+
if (formResult.cancelled) {
|
|
360
|
+
throw new CancelError("User cancelled form")
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const updatedBody = { ...msg.body, ...formResult.form.state }
|
|
364
|
+
const updatedErrors = msg.validate(updatedBody)
|
|
365
|
+
|
|
366
|
+
if (updatedErrors.size > 0) {
|
|
367
|
+
errors = updatedErrors
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
msg.body = updatedBody
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
return /** @type {any} */ (msg.body)
|
|
147
374
|
}
|
|
148
375
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutputAdapter handles UI output operations in command-line environment.
|
|
3
|
+
* @class
|
|
4
|
+
*/
|
|
5
|
+
export default class OutputAdapter {
|
|
6
|
+
/** @type {any} */
|
|
7
|
+
#console
|
|
8
|
+
/** @type {Map<string, () => Promise<Function>>} */
|
|
9
|
+
#components = new Map()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates new output adapter.
|
|
13
|
+
* @param {Object} [options] - Configuration options.
|
|
14
|
+
* @param {any} [options.console] - Console implementation.
|
|
15
|
+
* @param {Map<string, () => Promise<Function>>} [options.components] - Component loaders.
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
console: initialConsole = console,
|
|
20
|
+
components = new Map(),
|
|
21
|
+
} = options
|
|
22
|
+
|
|
23
|
+
this.#console = initialConsole
|
|
24
|
+
this.#components = components
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns {any} */
|
|
28
|
+
get console() { return this.#console; }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a UI component in the CLI environment.
|
|
32
|
+
*
|
|
33
|
+
* The current implementation supports simple textual rendering.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} component - Component name (e.g. `"Alert"`).
|
|
36
|
+
* @param {object} props - Props object passed to the component.
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async render(component, props) {
|
|
40
|
+
const loader = this.#components.get(component)
|
|
41
|
+
if (loader) {
|
|
42
|
+
try {
|
|
43
|
+
const compFn = await loader()
|
|
44
|
+
if (typeof compFn === "function") {
|
|
45
|
+
compFn.call(this, props)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
} catch (/** @type {any} */ err) {
|
|
49
|
+
this.#console.error(`Failed to render component "${component}": ${err.message}`)
|
|
50
|
+
this.#console.debug?.(err.stack)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (props && typeof props === "object") {
|
|
54
|
+
const { variant, content } = props
|
|
55
|
+
const prefix = variant ? `[${variant}]` : ""
|
|
56
|
+
this.#console.info(`${prefix} ${String(content)}`)
|
|
57
|
+
} else {
|
|
58
|
+
this.#console.info(String(component))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/README.md.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { describe, it, before, beforeEach } from "node:test"
|
|
3
3
|
import assert from "node:assert/strict"
|
|
4
4
|
import FS from "@nan0web/db-fs"
|
|
5
|
-
import
|
|
6
|
-
import { UIForm } from "@nan0web/ui"
|
|
5
|
+
import { UiForm } from "@nan0web/ui"
|
|
7
6
|
import { NoConsole } from "@nan0web/log"
|
|
8
7
|
import {
|
|
9
8
|
DatasetParser,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
runSpawn,
|
|
12
11
|
} from "@nan0web/test"
|
|
13
12
|
import {
|
|
14
|
-
|
|
13
|
+
CLiInputAdapter as BaseCLiInputAdapter,
|
|
15
14
|
CancelError,
|
|
16
15
|
createInput,
|
|
17
16
|
Input,
|
|
@@ -39,7 +38,7 @@ async function next() {
|
|
|
39
38
|
return Promise.resolve(" ")
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
class
|
|
41
|
+
class CLiInputAdapter extends BaseCLiInputAdapter {
|
|
43
42
|
async ask(question) {
|
|
44
43
|
return await ask(question)
|
|
45
44
|
}
|
|
@@ -89,7 +88,7 @@ function testRender() {
|
|
|
89
88
|
*
|
|
90
89
|
* Core classes:
|
|
91
90
|
*
|
|
92
|
-
* - `
|
|
91
|
+
* - `CLiInputAdapter` — handles form, input, and select requests in CLI.
|
|
93
92
|
* - `Input` — wraps user input with value and cancellation status.
|
|
94
93
|
* - `CancelError` — thrown when a user cancels an operation.
|
|
95
94
|
*
|
|
@@ -133,7 +132,7 @@ function testRender() {
|
|
|
133
132
|
* @docs
|
|
134
133
|
* ## Usage
|
|
135
134
|
*
|
|
136
|
-
* ###
|
|
135
|
+
* ### CLiInputAdapter
|
|
137
136
|
*
|
|
138
137
|
* The adapter provides methods to handle form, input, and select requests.
|
|
139
138
|
*
|
|
@@ -141,9 +140,9 @@ function testRender() {
|
|
|
141
140
|
*
|
|
142
141
|
* Displays a form and collects user input field-by-field with validation.
|
|
143
142
|
*/
|
|
144
|
-
it("How to request form input via
|
|
145
|
-
//import {
|
|
146
|
-
const adapter = new
|
|
143
|
+
it("How to request form input via CLiInputAdapter?", async () => {
|
|
144
|
+
//import { CLiInputAdapter } from '@nan0web/ui-cli'
|
|
145
|
+
const adapter = new CLiInputAdapter()
|
|
147
146
|
const fields = [
|
|
148
147
|
{ name: "name", label: "Full Name", required: true },
|
|
149
148
|
{ name: "email", label: "Email", type: "email", required: true },
|
|
@@ -159,7 +158,7 @@ function testRender() {
|
|
|
159
158
|
newForm.state = data
|
|
160
159
|
return newForm
|
|
161
160
|
}
|
|
162
|
-
const form =
|
|
161
|
+
const form = UiForm.from({
|
|
163
162
|
title: "User Profile",
|
|
164
163
|
fields,
|
|
165
164
|
id: "user-profile-form",
|
|
@@ -177,9 +176,9 @@ function testRender() {
|
|
|
177
176
|
/**
|
|
178
177
|
* @docs
|
|
179
178
|
*/
|
|
180
|
-
it("How to request select input via
|
|
181
|
-
//import {
|
|
182
|
-
const adapter = new
|
|
179
|
+
it("How to request select input via CLiInputAdapter?", async () => {
|
|
180
|
+
//import { CLiInputAdapter } from '@nan0web/ui-cli'
|
|
181
|
+
const adapter = new CLiInputAdapter()
|
|
183
182
|
const config = {
|
|
184
183
|
title: "Choose Language:",
|
|
185
184
|
prompt: "Language (1-2): ",
|
|
@@ -313,7 +312,7 @@ function testRender() {
|
|
|
313
312
|
* @docs
|
|
314
313
|
* ## API
|
|
315
314
|
*
|
|
316
|
-
* ###
|
|
315
|
+
* ### CLiInputAdapter
|
|
317
316
|
*
|
|
318
317
|
* * **Methods**
|
|
319
318
|
* * `requestForm(form, options)` — (async) handles form request
|
|
@@ -368,7 +367,7 @@ function testRender() {
|
|
|
368
367
|
* Extends `Error`, thrown when an input is cancelled.
|
|
369
368
|
*/
|
|
370
369
|
it("All exported classes and functions should pass basic tests", () => {
|
|
371
|
-
assert.ok(
|
|
370
|
+
assert.ok(CLiInputAdapter)
|
|
372
371
|
assert.ok(CancelError)
|
|
373
372
|
assert.ok(createInput)
|
|
374
373
|
assert.ok(baseAsk)
|
|
@@ -404,7 +403,7 @@ function testRender() {
|
|
|
404
403
|
assert.ok(String(pkg.scripts?.playground))
|
|
405
404
|
const response = await runSpawn("git", ["remote", "get-url", "origin"], { timeout: 2_000 })
|
|
406
405
|
if (response.code === 0) {
|
|
407
|
-
assert.ok(response.text.trim().endsWith("
|
|
406
|
+
assert.ok(response.text.trim().endsWith("nan0web/ui-cli.git"))
|
|
408
407
|
} else {
|
|
409
408
|
// git command may fail if not in a repo or no remote, skip assertion
|
|
410
409
|
console.warn("Git command skipped due to non-zero exit code or timeout.")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @typedef {import("../InputAdapter.js").default} InputAdapter */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Alert component for CLI rendering.
|
|
5
|
+
*
|
|
6
|
+
* @this {InputAdapter}
|
|
7
|
+
* @param {Object} input - Component props.
|
|
8
|
+
* @param {string} [input.variant="info"] - Alert variant (maps to console method).
|
|
9
|
+
* @param {string} [input.content=""] - Alert message content.
|
|
10
|
+
* @throws {Error} If variant maps to undefined console method.
|
|
11
|
+
*/
|
|
12
|
+
export default function (input = {}) {
|
|
13
|
+
const {
|
|
14
|
+
variant = "info",
|
|
15
|
+
content = "",
|
|
16
|
+
} = input
|
|
17
|
+
const fn = variant === "success" ? "info" : variant
|
|
18
|
+
if (typeof this.console[fn] !== "function") {
|
|
19
|
+
throw new Error(`Undefined variant: ${variant}`)
|
|
20
|
+
}
|
|
21
|
+
this.console[fn](content)
|
|
22
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import CLIInputAdapter from "./InputAdapter.js"
|
|
2
1
|
import { CancelError } from "@nan0web/ui/core"
|
|
2
|
+
|
|
3
|
+
import CLiInputAdapter from "./InputAdapter.js"
|
|
4
|
+
import OutputAdapter from "./OutputAdapter.js"
|
|
3
5
|
import CLI from "./CLI.js"
|
|
4
6
|
import Command from "./Command.js"
|
|
5
7
|
import CommandError from "./CommandError.js"
|
|
@@ -21,8 +23,9 @@ export {
|
|
|
21
23
|
|
|
22
24
|
export {
|
|
23
25
|
CLI,
|
|
24
|
-
|
|
26
|
+
CLiInputAdapter,
|
|
25
27
|
CancelError,
|
|
28
|
+
OutputAdapter,
|
|
26
29
|
|
|
27
30
|
/** @deprecated */
|
|
28
31
|
Command,
|
|
@@ -35,6 +38,9 @@ export {
|
|
|
35
38
|
CommandHelp,
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/* New public API */
|
|
42
|
+
export { generateForm } from "./ui/form.js"
|
|
43
|
+
|
|
38
44
|
export const renderers = new Map([
|
|
39
45
|
[
|
|
40
46
|
"UIProcess",
|
|
@@ -44,4 +50,4 @@ export const renderers = new Map([
|
|
|
44
50
|
],
|
|
45
51
|
])
|
|
46
52
|
|
|
47
|
-
export default
|
|
53
|
+
export default CLiInputAdapter
|