@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.
Files changed (115) hide show
  1. package/README.md +114 -207
  2. package/package.json +22 -12
  3. package/src/CLI.js +22 -29
  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/Command.js CHANGED
@@ -4,9 +4,9 @@
4
4
  * @module Command
5
5
  */
6
6
 
7
- import { Message } from "@nan0web/co"
8
- import CommandMessage from "./CommandMessage.js"
9
- import CommandError from "./CommandError.js"
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 || (async function* () {})
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: "", argv: [], opts: {} })
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).map(k => `--${k}`).join(" ")
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("\n")
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 * execute(message) {
125
+ async *execute(message) {
124
126
  try {
125
- if (typeof this.run === "function") yield* this.run(message)
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("Command execution failed", { message: e.message, stack: e.stack })
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("=")) return index + 1
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
  }
@@ -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 = "CommandError"
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
+ }
@@ -1,5 +1,5 @@
1
- import { Message } from "@nan0web/co"
2
- import Logger from "@nan0web/log"
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("\n")
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
- help
79
- ].filter(Boolean).join(""))
80
- lines.push("")
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("Options:")
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 !== "object") return
152
-
153
- const flags = schema.alias
154
- ? `--${prop}, -${schema.alias}`
155
- : `--${prop}`
156
-
157
- const type = undefined !== schema.type ? String(schema.type)
158
- : undefined !== schema.defaultValue ? typeof schema.defaultValue
159
- : undefined !== schema.placeholder ? typeof schema.placeholder
160
- : "any"
161
- const required = schema.required || schema.pattern || schema.defaultValue === undefined ? " *" : " "
162
- const description = schema.help || "No description"
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("Subcommands:")
189
- children.forEach(ChildClass => {
192
+ lines.push('Subcommands:')
193
+ children.forEach((ChildClass) => {
190
194
  const childName = ChildClass.name.toLowerCase()
191
- const childHelp = ChildClass.help || "No description"
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
  }
@@ -4,9 +4,9 @@
4
4
  * @module CommandMessage
5
5
  */
6
6
 
7
- import { Message } from "@nan0web/co"
8
- import { str2argv } from "./utils/parse.js"
9
- import CommandError from "./CommandError.js"
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 === "object" && !Array.isArray(input) ? 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 === "string" || Array.isArray(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() { return this.#name }
54
+ get name() {
55
+ return this.#name
56
+ }
61
57
  /** @param {string} v */
62
- set name(v) { this.#name = String(v) }
58
+ set name(v) {
59
+ this.#name = String(v)
60
+ }
63
61
 
64
62
  /** @returns {string[]} */
65
- get argv() { return this.#argv }
63
+ get argv() {
64
+ return this.#argv
65
+ }
66
66
  /** @param {string[]} v */
67
- set argv(v) { this.#argv = v.map(String) }
67
+ set argv(v) {
68
+ this.#argv = v.map(String)
69
+ }
68
70
 
69
71
  /** @returns {Object} */
70
- get opts() { return this.#opts }
72
+ get opts() {
73
+ return this.#opts
74
+ }
71
75
  /** @param {Object} v */
72
- set opts(v) { this.#opts = v }
76
+ set opts(v) {
77
+ this.#opts = v
78
+ }
73
79
 
74
80
  /** @returns {Array<CommandMessage>} */
75
- get children() { return this.#children }
81
+ get children() {
82
+ return this.#children
83
+ }
76
84
 
77
85
  /** @returns {Array<string>} Full command line (name + args). */
78
- get args() { return [this.name, ...this.argv].filter(Boolean) }
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() { return this.children[0]?.name || "" }
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() { return this.children[0] || null }
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) { this.#children.push(CommandMessage.from(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 === "string") argv = str2argv(argv)
116
- if (argv.length === 0) throw new CommandError("No input provided")
117
- const result = { name: "", argv: [], opts: {} }
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("-") && cur.length > 1) {
155
+ } else if (cur.startsWith('-') && cur.length > 1) {
140
156
  const shorts = cur.slice(1)
141
157
  if (shorts.length > 1) {
142
- shorts.split("").forEach(s => (result.opts[s] = true))
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("Validation failed", { errors })
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
  }
@@ -4,10 +4,10 @@
4
4
  * @module CommandParser
5
5
  */
6
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"
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 === "string" ? str2argv(input) : input
35
- if (argv.length === 0) throw new Error("No command provided")
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("Unable to infer root command from options")
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 === "function"
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("-") && token.length > 1) {
139
+ } else if (token.startsWith('-') && token.length > 1) {
142
140
  const short = token.slice(1)
143
141
  if (short.length > 1) {
144
- short.split("").forEach(ch => {
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
- body[realKey] = this.#convertType(body[realKey], value)
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 === "boolean")
203
- return Boolean(value !== "false" && value !== false && value !== "")
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