@nan0web/ui-cli 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -207
- package/package.json +22 -12
- package/src/CLI.js +22 -29
- package/src/CLiMessage.js +2 -3
- package/src/Command.js +26 -24
- package/src/CommandError.js +3 -5
- package/src/CommandHelp.js +40 -36
- package/src/CommandMessage.js +56 -40
- package/src/CommandParser.js +27 -25
- package/src/InputAdapter.js +630 -90
- package/src/OutputAdapter.js +7 -8
- package/src/README.md.js +190 -316
- package/src/components/Alert.js +3 -6
- package/src/components/prompt/Autocomplete.js +12 -0
- package/src/components/prompt/Confirm.js +29 -0
- package/src/components/prompt/DateTime.js +26 -0
- package/src/components/prompt/Input.js +15 -0
- package/src/components/prompt/Mask.js +12 -0
- package/src/components/prompt/Multiselect.js +26 -0
- package/src/components/prompt/Next.js +8 -0
- package/src/components/prompt/Password.js +13 -0
- package/src/components/prompt/Pause.js +9 -0
- package/src/components/prompt/ProgressBar.js +16 -0
- package/src/components/prompt/Select.js +29 -0
- package/src/components/prompt/Slider.js +16 -0
- package/src/components/prompt/Spinner.js +29 -0
- package/src/components/prompt/Toggle.js +13 -0
- package/src/components/prompt/Tree.js +17 -0
- package/src/components/view/Alert.js +78 -0
- package/src/components/view/Badge.js +11 -0
- package/src/components/view/Nav.js +23 -0
- package/src/components/view/Table.js +12 -0
- package/src/components/view/Toast.js +9 -0
- package/src/core/Component.js +79 -0
- package/src/core/PropValidation.js +138 -0
- package/src/core/render.js +37 -0
- package/src/index.js +80 -41
- package/src/test/PlaygroundTest.js +37 -25
- package/src/test/index.js +2 -4
- package/src/ui/alert.js +58 -0
- package/src/ui/autocomplete.js +86 -0
- package/src/ui/badge.js +35 -0
- package/src/ui/confirm.js +49 -0
- package/src/ui/date-time.js +45 -0
- package/src/ui/form.js +120 -55
- package/src/ui/index.js +18 -4
- package/src/ui/input.js +79 -152
- package/src/ui/mask.js +132 -0
- package/src/ui/multiselect.js +59 -0
- package/src/ui/nav.js +74 -0
- package/src/ui/next.js +18 -13
- package/src/ui/progress.js +88 -0
- package/src/ui/select.js +49 -72
- package/src/ui/slider.js +154 -0
- package/src/ui/spinner.js +65 -0
- package/src/ui/table.js +163 -0
- package/src/ui/toast.js +34 -0
- package/src/ui/toggle.js +34 -0
- package/src/ui/tree.js +393 -0
- package/src/utils/parse.js +1 -1
- package/types/CLI.d.ts +5 -5
- package/types/CLiMessage.d.ts +1 -1
- package/types/Command.d.ts +2 -2
- package/types/CommandHelp.d.ts +3 -3
- package/types/CommandMessage.d.ts +8 -8
- package/types/CommandParser.d.ts +3 -3
- package/types/InputAdapter.d.ts +149 -15
- package/types/OutputAdapter.d.ts +1 -1
- package/types/README.md.d.ts +1 -1
- package/types/UiMessage.d.ts +31 -29
- package/types/components/prompt/Autocomplete.d.ts +6 -0
- package/types/components/prompt/Confirm.d.ts +6 -0
- package/types/components/prompt/DateTime.d.ts +6 -0
- package/types/components/prompt/Input.d.ts +6 -0
- package/types/components/prompt/Mask.d.ts +6 -0
- package/types/components/prompt/Multiselect.d.ts +6 -0
- package/types/components/prompt/Next.d.ts +6 -0
- package/types/components/prompt/Password.d.ts +6 -0
- package/types/components/prompt/Pause.d.ts +6 -0
- package/types/components/prompt/ProgressBar.d.ts +12 -0
- package/types/components/prompt/Select.d.ts +18 -0
- package/types/components/prompt/Slider.d.ts +6 -0
- package/types/components/prompt/Spinner.d.ts +21 -0
- package/types/components/prompt/Toggle.d.ts +6 -0
- package/types/components/prompt/Tree.d.ts +6 -0
- package/types/components/view/Alert.d.ts +21 -0
- package/types/components/view/Badge.d.ts +5 -0
- package/types/components/view/Nav.d.ts +15 -0
- package/types/components/view/Table.d.ts +10 -0
- package/types/components/view/Toast.d.ts +5 -0
- package/types/core/Component.d.ts +34 -0
- package/types/core/PropValidation.d.ts +48 -0
- package/types/core/render.d.ts +6 -0
- package/types/index.d.ts +47 -15
- package/types/test/PlaygroundTest.d.ts +12 -8
- package/types/test/index.d.ts +1 -1
- package/types/ui/alert.d.ts +14 -0
- package/types/ui/autocomplete.d.ts +20 -0
- package/types/ui/badge.d.ts +8 -0
- package/types/ui/confirm.d.ts +21 -0
- package/types/ui/date-time.d.ts +19 -0
- package/types/ui/form.d.ts +43 -12
- package/types/ui/index.d.ts +17 -2
- package/types/ui/input.d.ts +31 -74
- package/types/ui/mask.d.ts +29 -0
- package/types/ui/multiselect.d.ts +25 -0
- package/types/ui/nav.d.ts +27 -0
- package/types/ui/progress.d.ts +43 -0
- package/types/ui/select.d.ts +25 -64
- package/types/ui/slider.d.ts +23 -0
- package/types/ui/spinner.d.ts +28 -0
- package/types/ui/table.d.ts +28 -0
- package/types/ui/toast.d.ts +8 -0
- package/types/ui/toggle.d.ts +17 -0
- package/types/ui/tree.d.ts +48 -0
package/src/Command.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* @module Command
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Message } from
|
|
8
|
-
import CommandMessage from
|
|
9
|
-
import CommandError from
|
|
7
|
+
import { Message } from '@nan0web/co'
|
|
8
|
+
import CommandMessage from './CommandMessage.js'
|
|
9
|
+
import CommandError from './CommandError.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Represents a command definition.
|
|
@@ -15,8 +15,8 @@ import CommandError from "./CommandError.js"
|
|
|
15
15
|
* @deprecated Use CLI instead
|
|
16
16
|
*/
|
|
17
17
|
export default class Command {
|
|
18
|
-
/** @type {string} */ name =
|
|
19
|
-
/** @type {string} */ help =
|
|
18
|
+
/** @type {string} */ name = ''
|
|
19
|
+
/** @type {string} */ help = ''
|
|
20
20
|
/** @type {Object} */ options = {}
|
|
21
21
|
/** @type {Function} */ run = async function* () {}
|
|
22
22
|
/** @type {Command[]} */ children = []
|
|
@@ -30,10 +30,10 @@ export default class Command {
|
|
|
30
30
|
* @param {Command[]} [config.children] - Sub‑commands.
|
|
31
31
|
*/
|
|
32
32
|
constructor(config) {
|
|
33
|
-
this.name = config.name ||
|
|
34
|
-
this.help = config.help ||
|
|
33
|
+
this.name = config.name || ''
|
|
34
|
+
this.help = config.help || ''
|
|
35
35
|
this.options = config.options || {}
|
|
36
|
-
this.run = config.run ||
|
|
36
|
+
this.run = config.run || async function* () {}
|
|
37
37
|
this.children = config.children || []
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -55,7 +55,7 @@ export default class Command {
|
|
|
55
55
|
* @returns {Command|null}
|
|
56
56
|
*/
|
|
57
57
|
findSubcommand(name) {
|
|
58
|
-
return this.children.find(c => c.name === name) || null
|
|
58
|
+
return this.children.find((c) => c.name === name) || null
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -66,14 +66,14 @@ export default class Command {
|
|
|
66
66
|
*/
|
|
67
67
|
parse(argv) {
|
|
68
68
|
const args = Array.isArray(argv) ? argv : [argv]
|
|
69
|
-
const msg = new CommandMessage({ name:
|
|
69
|
+
const msg = new CommandMessage({ name: '', argv: [], opts: {} })
|
|
70
70
|
let i = 0
|
|
71
71
|
while (i < args.length) {
|
|
72
72
|
const cur = args[i]
|
|
73
|
-
if (cur.startsWith(
|
|
73
|
+
if (cur.startsWith('--')) {
|
|
74
74
|
handleLongOption(msg, args, i)
|
|
75
75
|
i = updateIndexAfterOption(args, i)
|
|
76
|
-
} else if (cur.startsWith(
|
|
76
|
+
} else if (cur.startsWith('-')) {
|
|
77
77
|
handleShortOption(msg, args, i)
|
|
78
78
|
i = updateIndexAfterOption(args, i)
|
|
79
79
|
} else {
|
|
@@ -84,7 +84,7 @@ export default class Command {
|
|
|
84
84
|
}
|
|
85
85
|
if (msg.name === this.name) {
|
|
86
86
|
msg.argv = [...msg.argv]
|
|
87
|
-
msg.name =
|
|
87
|
+
msg.name = ''
|
|
88
88
|
}
|
|
89
89
|
if (msg.argv.length > 0) {
|
|
90
90
|
const subName = msg.argv[0]
|
|
@@ -108,9 +108,11 @@ export default class Command {
|
|
|
108
108
|
generateHelp() {
|
|
109
109
|
const parts = []
|
|
110
110
|
if (this.help) parts.push(this.help)
|
|
111
|
-
const optFlags = Object.keys(this.options)
|
|
111
|
+
const optFlags = Object.keys(this.options)
|
|
112
|
+
.map((k) => `--${k}`)
|
|
113
|
+
.join(' ')
|
|
112
114
|
parts.push(optFlags ? `Usage: ${this.name} ${optFlags}` : `Usage: ${this.name}`)
|
|
113
|
-
return parts.join(
|
|
115
|
+
return parts.join('\n')
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/**
|
|
@@ -120,13 +122,13 @@ export default class Command {
|
|
|
120
122
|
* @yields {any}
|
|
121
123
|
* @throws {CommandError}
|
|
122
124
|
*/
|
|
123
|
-
async *
|
|
125
|
+
async *execute(message) {
|
|
124
126
|
try {
|
|
125
|
-
if (typeof this.run ===
|
|
127
|
+
if (typeof this.run === 'function') yield* this.run(message)
|
|
126
128
|
} catch (e) {
|
|
127
129
|
if (e instanceof CommandError) throw e
|
|
128
130
|
/** @ts-ignore */
|
|
129
|
-
throw new CommandError(
|
|
131
|
+
throw new CommandError('Command execution failed', { message: e.message, stack: e.stack })
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -139,7 +141,7 @@ export default class Command {
|
|
|
139
141
|
_applyDefaults(msg) {
|
|
140
142
|
for (const [opt, [type, def]] of Object.entries(this.options)) {
|
|
141
143
|
if (!(opt in msg.opts)) {
|
|
142
|
-
msg.opts[opt] = def !== undefined ? def : type === Boolean ? false :
|
|
144
|
+
msg.opts[opt] = def !== undefined ? def : type === Boolean ? false : ''
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
}
|
|
@@ -157,14 +159,14 @@ export default class Command {
|
|
|
157
159
|
*/
|
|
158
160
|
function handleLongOption(msg, argv, index) {
|
|
159
161
|
const cur = argv[index]
|
|
160
|
-
const eq = cur.indexOf(
|
|
162
|
+
const eq = cur.indexOf('=')
|
|
161
163
|
if (eq > -1) {
|
|
162
164
|
const k = cur.slice(2, eq)
|
|
163
165
|
const v = cur.slice(eq + 1)
|
|
164
166
|
msg.opts[k] = v
|
|
165
167
|
} else {
|
|
166
168
|
const k = cur.slice(2)
|
|
167
|
-
if (index + 1 < argv.length && !argv[index + 1].startsWith(
|
|
169
|
+
if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
|
|
168
170
|
msg.opts[k] = argv[index + 1]
|
|
169
171
|
} else {
|
|
170
172
|
msg.opts[k] = true
|
|
@@ -186,7 +188,7 @@ function handleShortOption(msg, argv, index) {
|
|
|
186
188
|
for (const ch of cur) msg.opts[ch] = true
|
|
187
189
|
} else {
|
|
188
190
|
const k = cur
|
|
189
|
-
if (index + 1 < argv.length && !argv[index + 1].startsWith(
|
|
191
|
+
if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
|
|
190
192
|
msg.opts[k] = argv[index + 1]
|
|
191
193
|
} else {
|
|
192
194
|
msg.opts[k] = true
|
|
@@ -204,7 +206,7 @@ function handleShortOption(msg, argv, index) {
|
|
|
204
206
|
*/
|
|
205
207
|
function updateIndexAfterOption(argv, index) {
|
|
206
208
|
const cur = argv[index]
|
|
207
|
-
if (cur.includes(
|
|
208
|
-
const hasVal = index + 1 < argv.length && !argv[index + 1].startsWith(
|
|
209
|
+
if (cur.includes('=')) return index + 1
|
|
210
|
+
const hasVal = index + 1 < argv.length && !argv[index + 1].startsWith('-')
|
|
209
211
|
return hasVal ? index + 2 : index + 1
|
|
210
212
|
}
|
package/src/CommandError.js
CHANGED
|
@@ -17,7 +17,7 @@ export default class CommandError extends Error {
|
|
|
17
17
|
*/
|
|
18
18
|
constructor(message, data = null) {
|
|
19
19
|
super(message)
|
|
20
|
-
this.name =
|
|
20
|
+
this.name = 'CommandError'
|
|
21
21
|
this.data = data
|
|
22
22
|
Error.captureStackTrace(this, CommandError)
|
|
23
23
|
}
|
|
@@ -28,8 +28,6 @@ export default class CommandError extends Error {
|
|
|
28
28
|
* @returns {string}
|
|
29
29
|
*/
|
|
30
30
|
toString() {
|
|
31
|
-
return this.data
|
|
32
|
-
? `${this.message}\n${JSON.stringify(this.data, null, 2)}`
|
|
33
|
-
: this.message
|
|
31
|
+
return this.data ? `${this.message}\n${JSON.stringify(this.data, null, 2)}` : this.message
|
|
34
32
|
}
|
|
35
|
-
}
|
|
33
|
+
}
|
package/src/CommandHelp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Message } from
|
|
2
|
-
import Logger from
|
|
1
|
+
import { Message } from '@nan0web/co'
|
|
2
|
+
import Logger from '@nan0web/log'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {Object} CommandHelpField MessageBodySchema
|
|
@@ -55,7 +55,7 @@ export default class CommandHelp {
|
|
|
55
55
|
this.#usage(lines)
|
|
56
56
|
this.#options(lines)
|
|
57
57
|
this.#subcommands(lines)
|
|
58
|
-
return lines.join(
|
|
58
|
+
return lines.join('\n')
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -72,12 +72,13 @@ export default class CommandHelp {
|
|
|
72
72
|
*/
|
|
73
73
|
#header(lines) {
|
|
74
74
|
const name = this.MessageClass.name.toLowerCase()
|
|
75
|
-
const help = this.MessageClass['help'] ||
|
|
76
|
-
lines.push(
|
|
77
|
-
`${this.Logger.style(name, { color: this.Logger.MAGENTA })}`,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const help = this.MessageClass['help'] || ''
|
|
76
|
+
lines.push(
|
|
77
|
+
[`${this.Logger.style(name, { color: this.Logger.MAGENTA })}`, help]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(' • ')
|
|
80
|
+
)
|
|
81
|
+
lines.push('')
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
@@ -92,17 +93,17 @@ export default class CommandHelp {
|
|
|
92
93
|
|
|
93
94
|
if (bodyProps.length === 0) {
|
|
94
95
|
lines.push(`Usage: ${name}`)
|
|
95
|
-
lines.push(
|
|
96
|
+
lines.push('')
|
|
96
97
|
return
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
const placeholderParts = []
|
|
100
101
|
const flagParts = []
|
|
101
102
|
|
|
102
|
-
bodyProps.forEach(prop => {
|
|
103
|
+
bodyProps.forEach((prop) => {
|
|
103
104
|
/** @type {CommandHelpField} */
|
|
104
105
|
const schema = this.BodyClass[prop] || {}
|
|
105
|
-
const alias = schema.alias ? `-${schema.alias}, ` :
|
|
106
|
+
const alias = schema.alias ? `-${schema.alias}, ` : ''
|
|
106
107
|
const placeholder = schema.placeholder || schema.defaultValue
|
|
107
108
|
if (placeholder) {
|
|
108
109
|
placeholderParts.push(`[${alias}--${prop}=${placeholder}]`)
|
|
@@ -116,22 +117,22 @@ export default class CommandHelp {
|
|
|
116
117
|
// * when placeholders exist:
|
|
117
118
|
// – if exactly ONE flag part → prepend with ", "
|
|
118
119
|
// – otherwise just space‑separate all parts.
|
|
119
|
-
let usage =
|
|
120
|
+
let usage = ''
|
|
120
121
|
if (placeholderParts.length) {
|
|
121
|
-
usage = placeholderParts.join(
|
|
122
|
+
usage = placeholderParts.join(' ')
|
|
122
123
|
if (flagParts.length) {
|
|
123
124
|
if (flagParts.length === 1) {
|
|
124
125
|
usage = `${usage}, ${flagParts[0]}`
|
|
125
126
|
} else {
|
|
126
|
-
usage = `${usage} ${flagParts.join(
|
|
127
|
+
usage = `${usage} ${flagParts.join(' ')}`
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
} else {
|
|
130
131
|
// only flag parts
|
|
131
|
-
usage = flagParts.join(
|
|
132
|
+
usage = flagParts.join(' , ')
|
|
132
133
|
}
|
|
133
134
|
lines.push(`Usage: ${name} ${usage}`)
|
|
134
|
-
lines.push(
|
|
135
|
+
lines.push('')
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
@@ -144,27 +145,30 @@ export default class CommandHelp {
|
|
|
144
145
|
const bodyProps = Object.keys(bodyInstance)
|
|
145
146
|
if (bodyProps.length === 0) return
|
|
146
147
|
|
|
147
|
-
lines.push(
|
|
148
|
-
bodyProps.forEach(prop => {
|
|
148
|
+
lines.push('Options:')
|
|
149
|
+
bodyProps.forEach((prop) => {
|
|
149
150
|
/** @type {CommandHelpField} */
|
|
150
151
|
const schema = this.BodyClass[prop] || {}
|
|
151
|
-
if (typeof schema !==
|
|
152
|
-
|
|
153
|
-
const flags = schema.alias
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
if (typeof schema !== 'object') return
|
|
153
|
+
|
|
154
|
+
const flags = schema.alias ? `--${prop}, -${schema.alias}` : `--${prop}`
|
|
155
|
+
|
|
156
|
+
const type =
|
|
157
|
+
undefined !== schema.type
|
|
158
|
+
? String(schema.type)
|
|
159
|
+
: undefined !== schema.defaultValue
|
|
160
|
+
? typeof schema.defaultValue
|
|
161
|
+
: undefined !== schema.placeholder
|
|
162
|
+
? typeof schema.placeholder
|
|
163
|
+
: 'any'
|
|
164
|
+
const required =
|
|
165
|
+
schema.required || schema.pattern || schema.defaultValue === undefined ? ' *' : ' '
|
|
166
|
+
const description = schema.help || 'No description'
|
|
163
167
|
|
|
164
168
|
// Pad flags to align the type column with the expectations.
|
|
165
169
|
lines.push(` ${flags.padEnd(30)} ${type.padEnd(9)}${required} ${description}`)
|
|
166
170
|
})
|
|
167
|
-
lines.push(
|
|
171
|
+
lines.push('')
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
@@ -185,12 +189,12 @@ export default class CommandHelp {
|
|
|
185
189
|
const children = this.MessageClass['Children'] || []
|
|
186
190
|
if (children.length === 0) return
|
|
187
191
|
|
|
188
|
-
lines.push(
|
|
189
|
-
children.forEach(ChildClass => {
|
|
192
|
+
lines.push('Subcommands:')
|
|
193
|
+
children.forEach((ChildClass) => {
|
|
190
194
|
const childName = ChildClass.name.toLowerCase()
|
|
191
|
-
const childHelp = ChildClass.help ||
|
|
195
|
+
const childHelp = ChildClass.help || 'No description'
|
|
192
196
|
lines.push(` ${childName.padEnd(20)} ${childHelp}`)
|
|
193
197
|
})
|
|
194
|
-
lines.push(
|
|
198
|
+
lines.push('')
|
|
195
199
|
}
|
|
196
200
|
}
|
package/src/CommandMessage.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* @module CommandMessage
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Message } from
|
|
8
|
-
import { str2argv } from
|
|
9
|
-
import CommandError from
|
|
7
|
+
import { Message } from '@nan0web/co'
|
|
8
|
+
import { str2argv } from './utils/parse.js'
|
|
9
|
+
import CommandError from './CommandError.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @class
|
|
@@ -14,7 +14,7 @@ import CommandError from "./CommandError.js"
|
|
|
14
14
|
* @extends Message
|
|
15
15
|
*/
|
|
16
16
|
export default class CommandMessage extends Message {
|
|
17
|
-
#name =
|
|
17
|
+
#name = ''
|
|
18
18
|
#argv = []
|
|
19
19
|
#opts = {}
|
|
20
20
|
#children = []
|
|
@@ -30,14 +30,8 @@ export default class CommandMessage extends Message {
|
|
|
30
30
|
constructor(input = {}) {
|
|
31
31
|
super(input)
|
|
32
32
|
/** @type {any} */
|
|
33
|
-
const data = typeof input ===
|
|
34
|
-
const {
|
|
35
|
-
name = "",
|
|
36
|
-
argv = [],
|
|
37
|
-
opts = {},
|
|
38
|
-
children = [],
|
|
39
|
-
body = {},
|
|
40
|
-
} = data
|
|
33
|
+
const data = typeof input === 'object' && !Array.isArray(input) ? input : {}
|
|
34
|
+
const { name = '', argv = [], opts = {}, children = [], body = {} } = data
|
|
41
35
|
|
|
42
36
|
const fullBody = { ...body, ...opts }
|
|
43
37
|
this.body = fullBody
|
|
@@ -45,9 +39,9 @@ export default class CommandMessage extends Message {
|
|
|
45
39
|
this.#name = String(name)
|
|
46
40
|
this.#argv = argv.map(String)
|
|
47
41
|
this.#opts = opts
|
|
48
|
-
this.#children = children.map(c => CommandMessage.from(c))
|
|
42
|
+
this.#children = children.map((c) => CommandMessage.from(c))
|
|
49
43
|
|
|
50
|
-
if (typeof input ===
|
|
44
|
+
if (typeof input === 'string' || Array.isArray(input)) {
|
|
51
45
|
const parsed = CommandMessage.parse(input)
|
|
52
46
|
this.#name = parsed.name
|
|
53
47
|
this.#argv = parsed.argv
|
|
@@ -57,38 +51,60 @@ export default class CommandMessage extends Message {
|
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
/** @returns {string} */
|
|
60
|
-
get name() {
|
|
54
|
+
get name() {
|
|
55
|
+
return this.#name
|
|
56
|
+
}
|
|
61
57
|
/** @param {string} v */
|
|
62
|
-
set name(v) {
|
|
58
|
+
set name(v) {
|
|
59
|
+
this.#name = String(v)
|
|
60
|
+
}
|
|
63
61
|
|
|
64
62
|
/** @returns {string[]} */
|
|
65
|
-
get argv() {
|
|
63
|
+
get argv() {
|
|
64
|
+
return this.#argv
|
|
65
|
+
}
|
|
66
66
|
/** @param {string[]} v */
|
|
67
|
-
set argv(v) {
|
|
67
|
+
set argv(v) {
|
|
68
|
+
this.#argv = v.map(String)
|
|
69
|
+
}
|
|
68
70
|
|
|
69
71
|
/** @returns {Object} */
|
|
70
|
-
get opts() {
|
|
72
|
+
get opts() {
|
|
73
|
+
return this.#opts
|
|
74
|
+
}
|
|
71
75
|
/** @param {Object} v */
|
|
72
|
-
set opts(v) {
|
|
76
|
+
set opts(v) {
|
|
77
|
+
this.#opts = v
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
/** @returns {Array<CommandMessage>} */
|
|
75
|
-
get children() {
|
|
81
|
+
get children() {
|
|
82
|
+
return this.#children
|
|
83
|
+
}
|
|
76
84
|
|
|
77
85
|
/** @returns {Array<string>} Full command line (name + args). */
|
|
78
|
-
get args() {
|
|
86
|
+
get args() {
|
|
87
|
+
return [this.name, ...this.argv].filter(Boolean)
|
|
88
|
+
}
|
|
79
89
|
|
|
80
90
|
/** @returns {string} Sub‑command name of the first child, or empty string. */
|
|
81
|
-
get subCommand() {
|
|
91
|
+
get subCommand() {
|
|
92
|
+
return this.children[0]?.name || ''
|
|
93
|
+
}
|
|
82
94
|
|
|
83
95
|
/** @returns {CommandMessage|null} First child message, or null. */
|
|
84
|
-
get subCommandMessage() {
|
|
96
|
+
get subCommandMessage() {
|
|
97
|
+
return this.children[0] || null
|
|
98
|
+
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
87
101
|
* Append a child {@link CommandMessage}.
|
|
88
102
|
*
|
|
89
103
|
* @param {CommandMessage|Object} msg
|
|
90
104
|
*/
|
|
91
|
-
add(msg) {
|
|
105
|
+
add(msg) {
|
|
106
|
+
this.#children.push(CommandMessage.from(msg))
|
|
107
|
+
}
|
|
92
108
|
|
|
93
109
|
/**
|
|
94
110
|
* Convert the message back to a command‑line string.
|
|
@@ -98,9 +114,9 @@ export default class CommandMessage extends Message {
|
|
|
98
114
|
toString() {
|
|
99
115
|
const optsStr = Object.entries(this.opts)
|
|
100
116
|
.map(([k, v]) => (v === true ? `--${k}` : `--${k} ${String(v)}`))
|
|
101
|
-
.join(
|
|
102
|
-
const argsStr = this.argv.join(
|
|
103
|
-
return [this.name, argsStr, optsStr].filter(Boolean).join(
|
|
117
|
+
.join(' ')
|
|
118
|
+
const argsStr = this.argv.join(' ')
|
|
119
|
+
return [this.name, argsStr, optsStr].filter(Boolean).join(' ')
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
@@ -112,37 +128,37 @@ export default class CommandMessage extends Message {
|
|
|
112
128
|
* @throws {CommandError} If no input is supplied.
|
|
113
129
|
*/
|
|
114
130
|
static parse(argv, BodyClass) {
|
|
115
|
-
if (typeof argv ===
|
|
116
|
-
if (argv.length === 0) throw new CommandError(
|
|
117
|
-
const result = { name:
|
|
131
|
+
if (typeof argv === 'string') argv = str2argv(argv)
|
|
132
|
+
if (argv.length === 0) throw new CommandError('No input provided')
|
|
133
|
+
const result = { name: '', argv: [], opts: {} }
|
|
118
134
|
let i = 0
|
|
119
|
-
if (!argv[0].startsWith(
|
|
135
|
+
if (!argv[0].startsWith('-')) {
|
|
120
136
|
result.name = argv[0]
|
|
121
137
|
i = 1
|
|
122
138
|
}
|
|
123
139
|
while (i < argv.length) {
|
|
124
140
|
const cur = argv[i]
|
|
125
|
-
if (cur.startsWith(
|
|
126
|
-
const eq = cur.indexOf(
|
|
141
|
+
if (cur.startsWith('--')) {
|
|
142
|
+
const eq = cur.indexOf('=')
|
|
127
143
|
if (eq > -1) {
|
|
128
144
|
const k = cur.slice(2, eq)
|
|
129
145
|
const v = cur.slice(eq + 1)
|
|
130
146
|
result.opts[k] = v
|
|
131
147
|
} else {
|
|
132
148
|
const k = cur.slice(2)
|
|
133
|
-
if (i + 1 < argv.length && !argv[i + 1].startsWith(
|
|
149
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
|
134
150
|
result.opts[k] = argv[++i]
|
|
135
151
|
} else {
|
|
136
152
|
result.opts[k] = true
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
|
-
} else if (cur.startsWith(
|
|
155
|
+
} else if (cur.startsWith('-') && cur.length > 1) {
|
|
140
156
|
const shorts = cur.slice(1)
|
|
141
157
|
if (shorts.length > 1) {
|
|
142
|
-
shorts.split(
|
|
158
|
+
shorts.split('').forEach((s) => (result.opts[s] = true))
|
|
143
159
|
} else {
|
|
144
160
|
const k = shorts
|
|
145
|
-
if (i + 1 < argv.length && !argv[i + 1].startsWith(
|
|
161
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
|
146
162
|
result.opts[k] = argv[++i]
|
|
147
163
|
} else {
|
|
148
164
|
result.opts[k] = true
|
|
@@ -160,7 +176,7 @@ export default class CommandMessage extends Message {
|
|
|
160
176
|
msg.body = body
|
|
161
177
|
/** @ts-ignore */
|
|
162
178
|
const errors = body.getErrors?.() || {}
|
|
163
|
-
if (Object.keys(errors).length) throw new CommandError(
|
|
179
|
+
if (Object.keys(errors).length) throw new CommandError('Validation failed', { errors })
|
|
164
180
|
}
|
|
165
181
|
return msg
|
|
166
182
|
}
|
|
@@ -175,7 +191,7 @@ export default class CommandMessage extends Message {
|
|
|
175
191
|
if (input instanceof CommandMessage) return input
|
|
176
192
|
if (input instanceof Message) {
|
|
177
193
|
/** @ts-ignore */
|
|
178
|
-
return new CommandMessage({ body: input.body, name: input.name ||
|
|
194
|
+
return new CommandMessage({ body: input.body, name: input.name || '' })
|
|
179
195
|
}
|
|
180
196
|
return new CommandMessage(input)
|
|
181
197
|
}
|
package/src/CommandParser.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @module CommandParser
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Message } from
|
|
8
|
-
import CommandHelp from
|
|
9
|
-
import CommandError from
|
|
10
|
-
import { str2argv } from
|
|
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
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @class
|
|
@@ -31,18 +31,16 @@ export default class CommandParser {
|
|
|
31
31
|
* @throws {Error} If no command is supplied or unknown root command.
|
|
32
32
|
*/
|
|
33
33
|
parse(input = process.argv.slice(2)) {
|
|
34
|
-
const argv = typeof input ===
|
|
35
|
-
if (argv.length === 0) throw new Error(
|
|
34
|
+
const argv = typeof input === 'string' ? str2argv(input) : input
|
|
35
|
+
if (argv.length === 0) throw new Error('No command provided')
|
|
36
36
|
let rootName = null
|
|
37
37
|
let remaining = argv
|
|
38
38
|
|
|
39
|
-
if (!argv[0].startsWith(
|
|
39
|
+
if (!argv[0].startsWith('-')) {
|
|
40
40
|
rootName = argv[0]
|
|
41
41
|
remaining = argv.slice(1)
|
|
42
42
|
|
|
43
|
-
let RootClass = this.Messages.find(
|
|
44
|
-
cls => cls.name.toLowerCase() === rootName.toLowerCase(),
|
|
45
|
-
)
|
|
43
|
+
let RootClass = this.Messages.find((cls) => cls.name.toLowerCase() === rootName.toLowerCase())
|
|
46
44
|
if (!RootClass) {
|
|
47
45
|
if (this.Messages.length === 1) RootClass = this.Messages[0]
|
|
48
46
|
else throw new Error(`Unknown root command: ${rootName}`)
|
|
@@ -56,7 +54,7 @@ export default class CommandParser {
|
|
|
56
54
|
return this.#processMessageTree(rootMessage, remaining)
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
if (this.Messages.length !== 1) throw new Error(
|
|
57
|
+
if (this.Messages.length !== 1) throw new Error('Unable to infer root command from options')
|
|
60
58
|
// @ts-ignore – see comment above
|
|
61
59
|
const RootClass = this.Messages[0]
|
|
62
60
|
// @ts-ignore
|
|
@@ -81,7 +79,7 @@ export default class CommandParser {
|
|
|
81
79
|
const subName = remaining[0]
|
|
82
80
|
// @ts-ignore
|
|
83
81
|
const SubClass = currentMessage.constructor.Children.find(
|
|
84
|
-
cls => cls.name.toLowerCase() === subName.toLowerCase()
|
|
82
|
+
(cls) => cls.name.toLowerCase() === subName.toLowerCase()
|
|
85
83
|
)
|
|
86
84
|
if (!SubClass) break
|
|
87
85
|
|
|
@@ -97,14 +95,14 @@ export default class CommandParser {
|
|
|
97
95
|
const parsedBody = this.#parseLeafBody(
|
|
98
96
|
remaining,
|
|
99
97
|
// @ts-ignore – `Body` may be undefined on some classes
|
|
100
|
-
currentMessage.constructor.Body
|
|
98
|
+
currentMessage.constructor.Body
|
|
101
99
|
)
|
|
102
100
|
currentMessage.body = { ...currentMessage.body, ...parsedBody }
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
if (
|
|
106
104
|
rootMessage.body.subCommand &&
|
|
107
|
-
typeof rootMessage.body.subCommand.assertValid ===
|
|
105
|
+
typeof rootMessage.body.subCommand.assertValid === 'function'
|
|
108
106
|
) {
|
|
109
107
|
rootMessage.body.subCommand.assertValid()
|
|
110
108
|
}
|
|
@@ -125,23 +123,23 @@ export default class CommandParser {
|
|
|
125
123
|
for (let i = 0; i < tokens.length; i++) {
|
|
126
124
|
const token = tokens[i]
|
|
127
125
|
|
|
128
|
-
if (token.startsWith(
|
|
126
|
+
if (token.startsWith('--')) {
|
|
129
127
|
let key = token.slice(2)
|
|
130
128
|
/** @type {boolean | string} */
|
|
131
129
|
let value = true
|
|
132
|
-
const eqIdx = key.indexOf(
|
|
130
|
+
const eqIdx = key.indexOf('=')
|
|
133
131
|
if (eqIdx > -1) {
|
|
134
132
|
value = key.slice(eqIdx + 1)
|
|
135
133
|
key = key.slice(0, eqIdx)
|
|
136
|
-
} else if (i + 1 < tokens.length && !tokens[i + 1].startsWith(
|
|
134
|
+
} else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
|
|
137
135
|
value = tokens[++i]
|
|
138
136
|
}
|
|
139
137
|
const realKey = this.#resolveAlias(key, BodyClass) || key
|
|
140
138
|
if (props.includes(realKey)) body[realKey] = this.#convertType(body[realKey], value)
|
|
141
|
-
} else if (token.startsWith(
|
|
139
|
+
} else if (token.startsWith('-') && token.length > 1) {
|
|
142
140
|
const short = token.slice(1)
|
|
143
141
|
if (short.length > 1) {
|
|
144
|
-
short.split(
|
|
142
|
+
short.split('').forEach((ch) => {
|
|
145
143
|
const realKey = this.#resolveAlias(ch, BodyClass)
|
|
146
144
|
if (realKey && props.includes(realKey)) body[realKey] = true
|
|
147
145
|
})
|
|
@@ -150,10 +148,15 @@ export default class CommandParser {
|
|
|
150
148
|
if (props.includes(realKey)) {
|
|
151
149
|
/** @type {boolean | string} */
|
|
152
150
|
let value = true
|
|
153
|
-
if (i + 1 < tokens.length && !tokens[i + 1].startsWith(
|
|
151
|
+
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
|
|
154
152
|
value = tokens[++i]
|
|
155
153
|
}
|
|
156
|
-
|
|
154
|
+
const type = typeof body[realKey]
|
|
155
|
+
if ('object' === type && Array.isArray(body[realKey])) {
|
|
156
|
+
body[realKey].push(this.#convertType(body[realKey], value))
|
|
157
|
+
} else {
|
|
158
|
+
body[realKey] = this.#convertType(body[realKey], value)
|
|
159
|
+
}
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
} else {
|
|
@@ -162,7 +165,7 @@ export default class CommandParser {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
props.forEach(prop => {
|
|
168
|
+
props.forEach((prop) => {
|
|
166
169
|
const schema = BodyClass[prop]
|
|
167
170
|
if (schema?.default !== undefined && body[prop] === undefined) {
|
|
168
171
|
body[prop] = schema.default
|
|
@@ -199,9 +202,8 @@ export default class CommandParser {
|
|
|
199
202
|
*/
|
|
200
203
|
#convertType(defaultVal, value) {
|
|
201
204
|
const type = typeof defaultVal
|
|
202
|
-
if (type ===
|
|
203
|
-
|
|
204
|
-
if (type === "number") return Number(value) || 0
|
|
205
|
+
if (type === 'boolean') return Boolean(value !== 'false' && value !== false && value !== '')
|
|
206
|
+
if (type === 'number') return Number(value) || 0
|
|
205
207
|
return String(value)
|
|
206
208
|
}
|
|
207
209
|
|