@c9up/ream 0.1.26 → 0.1.27

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 (179) hide show
  1. package/README.md +65 -0
  2. package/dist/Ignitor.d.ts +115 -7
  3. package/dist/Ignitor.d.ts.map +1 -1
  4. package/dist/Ignitor.js +233 -37
  5. package/dist/Ignitor.js.map +1 -1
  6. package/dist/console/BaseCommand.d.ts +217 -0
  7. package/dist/console/BaseCommand.d.ts.map +1 -0
  8. package/dist/console/BaseCommand.js +340 -0
  9. package/dist/console/BaseCommand.js.map +1 -0
  10. package/dist/console/Console.d.ts +86 -0
  11. package/dist/console/Console.d.ts.map +1 -0
  12. package/dist/console/Console.js +134 -0
  13. package/dist/console/Console.js.map +1 -0
  14. package/dist/console/ExceptionHandler.d.ts +28 -0
  15. package/dist/console/ExceptionHandler.d.ts.map +1 -0
  16. package/dist/console/ExceptionHandler.js +76 -0
  17. package/dist/console/ExceptionHandler.js.map +1 -0
  18. package/dist/console/HelpCommand.d.ts +16 -0
  19. package/dist/console/HelpCommand.d.ts.map +1 -0
  20. package/dist/console/HelpCommand.js +40 -0
  21. package/dist/console/HelpCommand.js.map +1 -0
  22. package/dist/console/IndexGenerator.d.ts +24 -0
  23. package/dist/console/IndexGenerator.d.ts.map +1 -0
  24. package/dist/console/IndexGenerator.js +65 -0
  25. package/dist/console/IndexGenerator.js.map +1 -0
  26. package/dist/console/Kernel.d.ts +298 -0
  27. package/dist/console/Kernel.d.ts.map +1 -0
  28. package/dist/console/Kernel.js +1120 -0
  29. package/dist/console/Kernel.js.map +1 -0
  30. package/dist/console/ListCommand.d.ts +18 -0
  31. package/dist/console/ListCommand.d.ts.map +1 -0
  32. package/dist/console/ListCommand.js +60 -0
  33. package/dist/console/ListCommand.js.map +1 -0
  34. package/dist/console/assertions.d.ts +20 -0
  35. package/dist/console/assertions.d.ts.map +1 -0
  36. package/dist/console/assertions.js +84 -0
  37. package/dist/console/assertions.js.map +1 -0
  38. package/dist/console/cliui.d.ts +248 -0
  39. package/dist/console/cliui.d.ts.map +1 -0
  40. package/dist/console/cliui.js +587 -0
  41. package/dist/console/cliui.js.map +1 -0
  42. package/dist/console/decorators.d.ts +90 -0
  43. package/dist/console/decorators.d.ts.map +1 -0
  44. package/dist/console/decorators.js +142 -0
  45. package/dist/console/decorators.js.map +1 -0
  46. package/dist/console/index.d.ts +32 -0
  47. package/dist/console/index.d.ts.map +1 -0
  48. package/dist/console/index.js +24 -0
  49. package/dist/console/index.js.map +1 -0
  50. package/dist/console/loaders.d.ts +52 -0
  51. package/dist/console/loaders.d.ts.map +1 -0
  52. package/dist/console/loaders.js +131 -0
  53. package/dist/console/loaders.js.map +1 -0
  54. package/dist/console/parser.d.ts +86 -0
  55. package/dist/console/parser.d.ts.map +1 -0
  56. package/dist/console/parser.js +440 -0
  57. package/dist/console/parser.js.map +1 -0
  58. package/dist/console/prompts.d.ts +124 -0
  59. package/dist/console/prompts.d.ts.map +1 -0
  60. package/dist/console/prompts.js +356 -0
  61. package/dist/console/prompts.js.map +1 -0
  62. package/dist/console/tracing.d.ts +17 -0
  63. package/dist/console/tracing.d.ts.map +1 -0
  64. package/dist/console/tracing.js +11 -0
  65. package/dist/console/tracing.js.map +1 -0
  66. package/dist/console/types.d.ts +255 -0
  67. package/dist/console/types.d.ts.map +1 -0
  68. package/dist/console/types.js +25 -0
  69. package/dist/console/types.js.map +1 -0
  70. package/dist/console/ui.d.ts +20 -0
  71. package/dist/console/ui.d.ts.map +1 -0
  72. package/dist/console/ui.js +31 -0
  73. package/dist/console/ui.js.map +1 -0
  74. package/dist/console/utils.d.ts +27 -0
  75. package/dist/console/utils.d.ts.map +1 -0
  76. package/dist/console/utils.js +56 -0
  77. package/dist/console/utils.js.map +1 -0
  78. package/dist/container/Container.d.ts.map +1 -1
  79. package/dist/container/Container.js +40 -12
  80. package/dist/container/Container.js.map +1 -1
  81. package/dist/container/types.d.ts +14 -2
  82. package/dist/container/types.d.ts.map +1 -1
  83. package/dist/decorators/Service.d.ts +7 -0
  84. package/dist/decorators/Service.d.ts.map +1 -1
  85. package/dist/decorators/Service.js +23 -3
  86. package/dist/decorators/Service.js.map +1 -1
  87. package/dist/errors/ReamError.d.ts +25 -0
  88. package/dist/errors/ReamError.d.ts.map +1 -1
  89. package/dist/errors/ReamError.js +5 -0
  90. package/dist/errors/ReamError.js.map +1 -1
  91. package/dist/errors/prettyPrintError.d.ts +9 -0
  92. package/dist/errors/prettyPrintError.d.ts.map +1 -0
  93. package/dist/errors/prettyPrintError.js +22 -0
  94. package/dist/errors/prettyPrintError.js.map +1 -0
  95. package/dist/index.d.ts +14 -4
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +16 -2
  98. package/dist/index.js.map +1 -1
  99. package/dist/middleware/Pipeline.d.ts +29 -8
  100. package/dist/middleware/Pipeline.d.ts.map +1 -1
  101. package/dist/middleware/Pipeline.js +39 -1
  102. package/dist/middleware/Pipeline.js.map +1 -1
  103. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  104. package/dist/rpc/RpcRouter.js +13 -2
  105. package/dist/rpc/RpcRouter.js.map +1 -1
  106. package/dist/services/app.d.ts +1 -1
  107. package/dist/services/app.js +1 -1
  108. package/dist/services/console.d.ts +21 -0
  109. package/dist/services/console.d.ts.map +1 -0
  110. package/dist/services/console.js +35 -0
  111. package/dist/services/console.js.map +1 -0
  112. package/dist/testing/ApiResponse.d.ts +218 -0
  113. package/dist/testing/ApiResponse.d.ts.map +1 -0
  114. package/dist/testing/ApiResponse.js +576 -0
  115. package/dist/testing/ApiResponse.js.map +1 -0
  116. package/dist/testing/RequestBuilder.d.ts +132 -72
  117. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  118. package/dist/testing/RequestBuilder.js +389 -222
  119. package/dist/testing/RequestBuilder.js.map +1 -1
  120. package/dist/testing/TestClient.d.ts +34 -39
  121. package/dist/testing/TestClient.d.ts.map +1 -1
  122. package/dist/testing/TestClient.js +48 -56
  123. package/dist/testing/TestClient.js.map +1 -1
  124. package/dist/utils/Macroable.js +3 -3
  125. package/dist/utils/Macroable.js.map +1 -1
  126. package/events.darwin-arm64.node +0 -0
  127. package/events.darwin-x64.node +0 -0
  128. package/events.linux-arm64-gnu.node +0 -0
  129. package/events.linux-x64-gnu.node +0 -0
  130. package/events.win32-x64-msvc.node +0 -0
  131. package/index.darwin-arm64.node +0 -0
  132. package/index.darwin-x64.node +0 -0
  133. package/index.linux-arm64-gnu.node +0 -0
  134. package/index.linux-x64-gnu.node +0 -0
  135. package/index.win32-x64-msvc.node +0 -0
  136. package/package.json +15 -2
  137. package/scheduler.darwin-arm64.node +0 -0
  138. package/scheduler.darwin-x64.node +0 -0
  139. package/scheduler.linux-arm64-gnu.node +0 -0
  140. package/scheduler.linux-x64-gnu.node +0 -0
  141. package/scheduler.win32-x64-msvc.node +0 -0
  142. package/src/Ignitor.ts +340 -42
  143. package/src/console/BaseCommand.ts +467 -0
  144. package/src/console/Console.ts +174 -0
  145. package/src/console/ExceptionHandler.ts +91 -0
  146. package/src/console/HelpCommand.ts +37 -0
  147. package/src/console/IndexGenerator.ts +79 -0
  148. package/src/console/Kernel.ts +1382 -0
  149. package/src/console/ListCommand.ts +55 -0
  150. package/src/console/assertions.ts +114 -0
  151. package/src/console/cliui.ts +756 -0
  152. package/src/console/decorators.ts +199 -0
  153. package/src/console/index.ts +67 -0
  154. package/src/console/loaders.ts +145 -0
  155. package/src/console/parser.ts +598 -0
  156. package/src/console/prompts.ts +548 -0
  157. package/src/console/tracing.ts +22 -0
  158. package/src/console/types.ts +280 -0
  159. package/src/console/ui.ts +37 -0
  160. package/src/console/utils.ts +69 -0
  161. package/src/container/Container.ts +55 -13
  162. package/src/container/types.ts +15 -2
  163. package/src/decorators/Service.ts +28 -3
  164. package/src/errors/ReamError.ts +31 -0
  165. package/src/errors/prettyPrintError.ts +20 -0
  166. package/src/index.ts +30 -4
  167. package/src/middleware/Pipeline.ts +76 -5
  168. package/src/rpc/RpcRouter.ts +16 -2
  169. package/src/services/app.ts +1 -1
  170. package/src/services/console.ts +42 -0
  171. package/src/testing/ApiResponse.ts +691 -0
  172. package/src/testing/RequestBuilder.ts +498 -275
  173. package/src/testing/TestClient.ts +63 -86
  174. package/src/utils/Macroable.ts +3 -3
  175. package/dist/console/CommandRunner.d.ts +0 -18
  176. package/dist/console/CommandRunner.d.ts.map +0 -1
  177. package/dist/console/CommandRunner.js +0 -65
  178. package/dist/console/CommandRunner.js.map +0 -1
  179. package/src/console/CommandRunner.ts +0 -82
@@ -0,0 +1,598 @@
1
+ /**
2
+ * argv parser for console commands.
3
+ *
4
+ * Metadata-driven, unlike the naive splitter it replaces: knowing a flag's
5
+ * declared type is what makes `--email hugo@example.com` work. Without it the
6
+ * parser cannot tell a value-taking flag from a boolean one, so it has to treat
7
+ * every `--flag` as `true` and let the value fall through to the positionals —
8
+ * which is exactly how the previous implementation silently dropped arguments.
9
+ *
10
+ * Supported forms (Console parity):
11
+ * --flag=value --flag value --flag (boolean) --no-flag (boolean false)
12
+ * -f value -f -abc (grouped boolean aliases)
13
+ * -- everything after it is positional, never a flag
14
+ * repeated flags accumulate for `@flags.array()`
15
+ */
16
+
17
+ import { ReamError } from '../errors/ReamError.js'
18
+ import type { ArgumentMetaData, FlagMetaData, ParsedInput } from './types.js'
19
+
20
+ export interface ParseOptions {
21
+ args?: readonly ArgumentMetaData[]
22
+ flags?: readonly FlagMetaData[]
23
+ allowUnknownFlags?: boolean
24
+ /** Command name, used to make error messages point at the right command. */
25
+ commandName?: string
26
+ /**
27
+ * Check the required inputs while parsing. `false` parses leniently and
28
+ * leaves that to {@link validateParsed}.
29
+ *
30
+ * The kernel parses leniently on purpose: Console validates AFTER the global-flag
31
+ * listeners have run, which is what lets `--help` work on a command whose
32
+ * flags are required.
33
+ */
34
+ validate?: boolean
35
+ }
36
+
37
+ /** A flag token resolved against the declared metadata. */
38
+ interface RawFlag {
39
+ meta: FlagMetaData
40
+ /** `undefined` means "no inline value" — a boolean, or a value still to come. */
41
+ value?: string
42
+ }
43
+
44
+ /**
45
+ * The parser as an object (Console `Parser`).
46
+ *
47
+ * Same work as {@link parseArgv}, held in a shape that can be passed around and
48
+ * reused — which is how Console's kernel and its ported code hold it.
49
+ */
50
+ export class Parser {
51
+ readonly #options: ParseOptions
52
+
53
+ constructor(options: ParseOptions = {}) {
54
+ this.#options = options
55
+ }
56
+
57
+ parse(argv: readonly string[] | string): ParsedInput {
58
+ return parseArgv(
59
+ typeof argv === 'string' ? argv.split(' ').filter(Boolean) : argv,
60
+ this.#options,
61
+ )
62
+ }
63
+ }
64
+
65
+ export function parseArgv(argv: readonly string[], options: ParseOptions = {}): ParsedInput {
66
+ const declaredFlags = options.flags ?? []
67
+ const declaredArgs = options.args ?? []
68
+ const where = options.commandName ? ` for "${options.commandName}"` : ''
69
+
70
+ const byName = new Map<string, FlagMetaData>()
71
+ const byAlias = new Map<string, FlagMetaData>()
72
+ for (const flag of declaredFlags) {
73
+ byName.set(flag.flagName, flag)
74
+ for (const alias of flag.alias) byAlias.set(alias, flag)
75
+ }
76
+
77
+ const positionals: string[] = []
78
+ /** Collected per property so repeated flags can accumulate into arrays. */
79
+ const collected = new Map<string, Array<string | boolean>>()
80
+ const unknown: Record<string, string | boolean> = {}
81
+
82
+ const push = (meta: FlagMetaData, value: string | boolean): void => {
83
+ const existing = collected.get(meta.propertyName)
84
+ if (existing) existing.push(value)
85
+ else collected.set(meta.propertyName, [value])
86
+ }
87
+
88
+ /** Record the flag as seen without attaching a value to it. */
89
+ const mention = (meta: FlagMetaData): void => {
90
+ if (!collected.has(meta.propertyName)) collected.set(meta.propertyName, [])
91
+ }
92
+
93
+ let sawTerminator = false
94
+
95
+ for (let i = 0; i < argv.length; i++) {
96
+ const token = argv[i] as string
97
+
98
+ if (sawTerminator) {
99
+ positionals.push(token)
100
+ continue
101
+ }
102
+ if (token === '--') {
103
+ sawTerminator = true
104
+ continue
105
+ }
106
+
107
+ // --long, --long=value, --no-long
108
+ if (token.startsWith('--')) {
109
+ const body = token.slice(2)
110
+ const eq = body.indexOf('=')
111
+ const name = eq === -1 ? body : body.slice(0, eq)
112
+ const inline = eq === -1 ? undefined : body.slice(eq + 1)
113
+
114
+ // `--no-x` negates a declared boolean. Checked before the plain lookup so
115
+ // a genuine flag literally named `no-cache` still wins.
116
+ const negated =
117
+ !byName.has(name) && name.startsWith('no-') ? byName.get(name.slice(3)) : undefined
118
+ if (negated && negated.type === 'boolean') {
119
+ if (inline !== undefined) {
120
+ throw invalidNegation(token, where)
121
+ }
122
+ push(negated, false)
123
+ continue
124
+ }
125
+
126
+ const meta = byName.get(name)
127
+ if (!meta) {
128
+ if (!options.allowUnknownFlags) throw unknownFlag(`--${name}`, where, declaredFlags)
129
+ // An undeclared flag has no type to go on, so `--foo bar` is read the
130
+ // way a shell parser does: the next token is its value unless it looks
131
+ // like another flag. Treating it as a bare boolean would drop `bar`
132
+ // entirely — a proxy command would silently lose its arguments.
133
+ if (inline !== undefined) {
134
+ unknown[name] = inline
135
+ } else {
136
+ const next = argv[i + 1]
137
+ if (next !== undefined && (!next.startsWith('-') || next === '-')) {
138
+ unknown[name] = next
139
+ i++
140
+ } else {
141
+ unknown[name] = true
142
+ }
143
+ }
144
+ continue
145
+ }
146
+ i = consume({ meta, value: inline }, argv, i, { push, mention }, where)
147
+ continue
148
+ }
149
+
150
+ // -a, -a value, -abc (grouped booleans)
151
+ if (token.startsWith('-') && token.length > 1) {
152
+ const body = token.slice(1)
153
+ const eq = body.indexOf('=')
154
+ const chars = eq === -1 ? body : body.slice(0, eq)
155
+ const inline = eq === -1 ? undefined : body.slice(eq + 1)
156
+
157
+ if (chars.length === 1) {
158
+ const meta = byAlias.get(chars)
159
+ if (!meta) {
160
+ if (!options.allowUnknownFlags) throw unknownFlag(`-${chars}`, where, declaredFlags)
161
+ if (inline !== undefined) {
162
+ unknown[chars] = inline
163
+ } else {
164
+ const next = argv[i + 1]
165
+ if (next !== undefined && (!next.startsWith('-') || next === '-')) {
166
+ unknown[chars] = next
167
+ i++
168
+ } else {
169
+ unknown[chars] = true
170
+ }
171
+ }
172
+ continue
173
+ }
174
+ i = consume({ meta, value: inline }, argv, i, { push, mention }, where)
175
+ continue
176
+ }
177
+
178
+ // A group like `-rs` can only be booleans: there is no way to attach a
179
+ // value to more than one flag at a time.
180
+ for (const char of chars) {
181
+ const meta = byAlias.get(char)
182
+ if (!meta) {
183
+ if (!options.allowUnknownFlags) throw unknownFlag(`-${char}`, where, declaredFlags)
184
+ unknown[char] = true
185
+ continue
186
+ }
187
+ if (meta.type !== 'boolean') {
188
+ throw new ReamError(
189
+ 'E_CONSOLE_INVALID_FLAG_GROUP',
190
+ `Cannot group "-${char}" with other flags${where} — "--${meta.flagName}" expects a value.`,
191
+ { hint: `Pass it on its own: -${char} <value>` },
192
+ )
193
+ }
194
+ push(meta, true)
195
+ }
196
+ continue
197
+ }
198
+
199
+ positionals.push(token)
200
+ }
201
+
202
+ const extraArgs: string[] = []
203
+ const strict = options.validate !== false
204
+ const flags = applyParse(
205
+ declaredFlags,
206
+ materialiseFlags(declaredFlags, collected, unknown, where, strict),
207
+ )
208
+ const args = applyParse(
209
+ declaredArgs,
210
+ materialiseArgs(
211
+ declaredArgs,
212
+ positionals,
213
+ where,
214
+ options.allowUnknownFlags === true,
215
+ extraArgs,
216
+ strict,
217
+ ),
218
+ )
219
+
220
+ // Rekeyed into Console's shape only here: defaults and `parse` are applied per
221
+ // DECLARATION, which needs the property name, while what comes out describes
222
+ // what was typed — positionals as a list, flags under their CLI name.
223
+ const byFlagName: Record<string, unknown> = { ...unknown }
224
+ for (const meta of declaredFlags) {
225
+ if (meta.propertyName in flags) byFlagName[meta.flagName] = flags[meta.propertyName]
226
+ }
227
+
228
+ return {
229
+ unknownFlags: Object.keys(unknown),
230
+ flags: byFlagName,
231
+ args: declaredArgs.map((meta) => args[meta.propertyName]),
232
+ extraArgs,
233
+ _: extraArgs,
234
+ nodeArgs: [],
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Run each declared input's `parse` callback over the materialised value.
240
+ *
241
+ * Applied last, on the already-typed value, so `parse` receives a number for a
242
+ * number flag rather than the raw token — and only for inputs that were
243
+ * actually provided or defaulted, never for an absent optional one.
244
+ */
245
+ function applyParse<
246
+ M extends { propertyName: string; parse?: (value: never) => unknown },
247
+ V extends Record<string, unknown>,
248
+ >(declared: readonly M[], values: V): V {
249
+ for (const meta of declared) {
250
+ if (meta.parse === undefined) continue
251
+ const current = values[meta.propertyName]
252
+ if (current === undefined) continue
253
+ Reflect.set(values, meta.propertyName, Reflect.apply(meta.parse, undefined, [current]))
254
+ }
255
+ return values
256
+ }
257
+
258
+ /**
259
+ * Attach a value to a flag, pulling the next argv token when the value was not
260
+ * written inline. Returns the new cursor position.
261
+ */
262
+ function consume(
263
+ raw: RawFlag,
264
+ argv: readonly string[],
265
+ index: number,
266
+ collect: {
267
+ push: (meta: FlagMetaData, value: string | boolean) => void
268
+ mention: (meta: FlagMetaData) => void
269
+ },
270
+ where: string,
271
+ ): number {
272
+ const { push, mention } = collect
273
+ const { meta, value } = raw
274
+
275
+ if (meta.type === 'boolean') {
276
+ if (value === undefined) {
277
+ push(meta, true)
278
+ return index
279
+ }
280
+ // `--force=false` is explicit enough to honour; anything else is a typo.
281
+ if (value === 'true' || value === 'false') {
282
+ push(meta, value === 'true')
283
+ return index
284
+ }
285
+ throw new ReamError(
286
+ 'E_CONSOLE_INVALID_FLAG_VALUE',
287
+ `Flag "--${meta.flagName}"${where} is a boolean and cannot take the value "${value}".`,
288
+ { hint: `Use --${meta.flagName} or --no-${meta.flagName}` },
289
+ )
290
+ }
291
+
292
+ if (value !== undefined) {
293
+ push(meta, value)
294
+ return index
295
+ }
296
+
297
+ // Value-taking flag with no inline value: the next token is it. A token that
298
+ // itself looks like a flag is NOT consumed — `--email --name x` is a mistake
299
+ // worth reporting, not an email of "--name".
300
+ const next = argv[index + 1]
301
+ if (next === undefined || (next.startsWith('-') && next !== '-')) {
302
+ // Unless the flag says an empty value is meaningful (Console
303
+ // `allowEmptyValue`), in which case the mention itself is the value.
304
+ if (meta.allowEmptyValue === true) {
305
+ // An array flag with nothing behind it is an EMPTY list, not a list
306
+ // holding one empty string — `mention` records it without a value.
307
+ if (meta.type === 'array') mention(meta)
308
+ else push(meta, '')
309
+ return index
310
+ }
311
+ throw new ReamError(
312
+ 'E_CONSOLE_MISSING_FLAG_VALUE',
313
+ `Flag "--${meta.flagName}"${where} expects a value.`,
314
+ { hint: `Example: --${meta.flagName}=<value>` },
315
+ )
316
+ }
317
+ push(meta, next)
318
+ return index + 1
319
+ }
320
+
321
+ /** Apply types, defaults and required checks to the collected flag values. */
322
+ function materialiseFlags(
323
+ declared: readonly FlagMetaData[],
324
+ collected: Map<string, Array<string | boolean>>,
325
+ unknown: Record<string, string | boolean>,
326
+ where: string,
327
+ strict: boolean,
328
+ ): Record<string, string | string[] | number | boolean> {
329
+ const out: Record<string, string | string[] | number | boolean> = { ...unknown }
330
+
331
+ for (const meta of declared) {
332
+ const values = collected.get(meta.propertyName)
333
+
334
+ if (values === undefined) {
335
+ if (meta.default !== undefined) out[meta.propertyName] = meta.default
336
+ else if (meta.required && strict) {
337
+ throw new ReamError(
338
+ 'E_CONSOLE_MISSING_FLAG',
339
+ `Missing required flag "--${meta.flagName}"${where}.`,
340
+ )
341
+ }
342
+ continue
343
+ }
344
+
345
+ switch (meta.type) {
346
+ case 'array':
347
+ out[meta.propertyName] = values.map(String)
348
+ break
349
+ case 'number': {
350
+ // Last one wins for scalars, matching how shells are normally used.
351
+ const last = String(values[values.length - 1])
352
+ const parsed = Number(last)
353
+ if (last.trim() === '' || Number.isNaN(parsed)) {
354
+ throw new ReamError(
355
+ 'E_CONSOLE_INVALID_FLAG_VALUE',
356
+ `Flag "--${meta.flagName}"${where} expects a number, got "${last}".`,
357
+ )
358
+ }
359
+ out[meta.propertyName] = parsed
360
+ break
361
+ }
362
+ case 'boolean':
363
+ out[meta.propertyName] = Boolean(values[values.length - 1])
364
+ break
365
+ default:
366
+ out[meta.propertyName] = String(values[values.length - 1])
367
+ }
368
+ }
369
+
370
+ return out
371
+ }
372
+
373
+ /** Map positionals onto declared arguments, honouring `@args.spread()`. */
374
+ function materialiseArgs(
375
+ declared: readonly ArgumentMetaData[],
376
+ positionals: readonly string[],
377
+ where: string,
378
+ allowExtra: boolean,
379
+ extraArgs: string[],
380
+ strict: boolean,
381
+ ): Record<string, string | string[]> {
382
+ const out: Record<string, string | string[]> = {}
383
+ let cursor = 0
384
+
385
+ for (const meta of declared) {
386
+ if (meta.type === 'spread') {
387
+ const rest = positionals.slice(cursor)
388
+ cursor = positionals.length
389
+ if (rest.length === 0) {
390
+ // No default and nothing to collect: the argument is UNDEFINED, not an
391
+ // empty list (Console). A command that distinguishes "not given" from
392
+ // "given empty" must be able to tell them apart.
393
+ if (meta.default !== undefined) out[meta.propertyName] = meta.default
394
+ else if (meta.required && strict) throw missingArgument(meta, where)
395
+ continue
396
+ }
397
+ out[meta.propertyName] = rest
398
+ continue
399
+ }
400
+
401
+ const value = positionals[cursor]
402
+ cursor++
403
+ if (value === undefined) {
404
+ if (meta.default !== undefined) out[meta.propertyName] = meta.default
405
+ else if (meta.required && strict) throw missingArgument(meta, where)
406
+ continue
407
+ }
408
+ // An empty positional is nearly always a shell variable that did not
409
+ // expand, so it is reported rather than accepted (Console) — unless the
410
+ // argument declares that an empty value means something.
411
+ if (value === '' && meta.allowEmptyValue !== true && strict) {
412
+ throw new ReamError(
413
+ 'E_CONSOLE_MISSING_ARGUMENT_VALUE',
414
+ `Missing value for argument "${meta.argumentName}"${where}.`,
415
+ )
416
+ }
417
+ out[meta.propertyName] = value
418
+ }
419
+
420
+ // Anything left over was never asked for. Swallowing it hides a typo:
421
+ // `greet john extra` would run as `greet john`, silently dropping the rest.
422
+ // A `spread` argument consumes the tail, so this only fires without one.
423
+ if (cursor < positionals.length) {
424
+ const extra = positionals.slice(cursor)
425
+ // A proxy command (allowUnknownFlags) keeps them — forwarding them is its
426
+ // whole purpose. Anything else reports them, because swallowing a
427
+ // positional hides a typo.
428
+ if (allowExtra) {
429
+ extraArgs.push(...extra)
430
+ return out
431
+ }
432
+ throw new ReamError(
433
+ 'E_CONSOLE_UNEXPECTED_ARGUMENT',
434
+ `Unexpected argument${extra.length > 1 ? 's' : ''} ${extra.map((value) => `"${value}"`).join(', ')}${where}.`,
435
+ {
436
+ hint:
437
+ declared.length === 0
438
+ ? 'This command takes no positional arguments.'
439
+ : `It takes ${declared.length}: ${declared.map((arg) => arg.argumentName).join(', ')}.`,
440
+ },
441
+ )
442
+ }
443
+
444
+ return out
445
+ }
446
+
447
+ /**
448
+ * Check a parsed input against a command's declarations (Console `validate`).
449
+ *
450
+ * Separate from parsing because the ORDER matters: Console runs the global-flag
451
+ * listeners between the two, which is what lets `--help` work on a command
452
+ * whose flags are required. One implementation, called by the kernel and by
453
+ * `BaseCommand.validate()`.
454
+ *
455
+ * Positionals are accepted as a list (Console's shape) or keyed by property name
456
+ * (what a hand-built Ream input holds).
457
+ */
458
+ export function validateParsed(
459
+ parsed: {
460
+ args: readonly unknown[] | Record<string, unknown>
461
+ flags?: Record<string, unknown>
462
+ unknownFlags?: readonly string[]
463
+ },
464
+ declarations: {
465
+ args?: readonly ArgumentMetaData[]
466
+ flags?: readonly FlagMetaData[]
467
+ allowUnknownFlags?: boolean
468
+ commandName?: string
469
+ },
470
+ ): void {
471
+ const where = declarations.commandName ? ` for "${declarations.commandName}"` : ''
472
+ const byPosition = Array.isArray(parsed.args) ? parsed.args : undefined
473
+ const byName: Record<string, unknown> = byPosition === undefined ? { ...parsed.args } : {}
474
+
475
+ ;(declarations.args ?? []).forEach((arg, index) => {
476
+ const value = byPosition === undefined ? byName[arg.propertyName] : byPosition[index]
477
+
478
+ if (arg.required && value === undefined) throw missingArgument(arg, where)
479
+ if (value === undefined || arg.allowEmptyValue === true) return
480
+ if (value === '' || (Array.isArray(value) && value.length === 0)) {
481
+ throw new ReamError(
482
+ 'E_CONSOLE_MISSING_ARGUMENT_VALUE',
483
+ `Missing value for argument "${arg.argumentName}"${where}.`,
484
+ )
485
+ }
486
+ })
487
+
488
+ const unknown = parsed.unknownFlags ?? []
489
+ if (declarations.allowUnknownFlags !== true && unknown.length > 0) {
490
+ const flag = unknown[0] ?? ''
491
+ throw new ReamError(
492
+ 'E_CONSOLE_UNKNOWN_FLAG',
493
+ `Unknown flag "${flag.length === 1 ? `-${flag}` : `--${flag}`}"${where}.`,
494
+ )
495
+ }
496
+
497
+ const flags: Record<string, unknown> = parsed.flags ?? {}
498
+ for (const flag of declarations.flags ?? []) {
499
+ // Keyed by flag name (Console, and what the parser returns) or by property
500
+ // name — a hand-built input must validate whichever shape it was written in.
501
+ const key = Object.hasOwn(flags, flag.flagName) ? flag.flagName : flag.propertyName
502
+ const mentioned = Object.hasOwn(flags, key)
503
+ const value = flags[key]
504
+
505
+ if (flag.required && !mentioned) {
506
+ throw new ReamError(
507
+ 'E_CONSOLE_MISSING_FLAG',
508
+ `Missing required flag "--${flag.flagName}"${where}.`,
509
+ )
510
+ }
511
+ if (!mentioned || flag.allowEmptyValue === true) continue
512
+
513
+ if (flag.type === 'number') {
514
+ if (value === undefined || Number.isNaN(value)) {
515
+ throw new ReamError(
516
+ 'E_CONSOLE_INVALID_FLAG_VALUE',
517
+ `Flag "--${flag.flagName}"${where} expects a number, got "${String(value)}".`,
518
+ )
519
+ }
520
+ continue
521
+ }
522
+
523
+ // A mentioned string/array flag with nothing behind it: `--name` alone
524
+ // yields '' from a parser, and an empty array for a repeatable flag.
525
+ if (flag.type === 'boolean') continue
526
+ if (value === '' || (Array.isArray(value) && value.length === 0)) {
527
+ throw new ReamError(
528
+ 'E_CONSOLE_MISSING_FLAG_VALUE',
529
+ `Missing value for flag "--${flag.flagName}"${where}.`,
530
+ )
531
+ }
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Assign the parsed values to the properties they were declared on.
537
+ *
538
+ * The single hydration path: `BaseCommand.hydrate()` calls it, and so does the
539
+ * kernel for a command declared structurally, which has no `hydrate()` of its
540
+ * own. Both input shapes are accepted — positionals as a list (what the parser
541
+ * now returns, and Console's shape) or keyed by property name, flags under their
542
+ * flag name or their property name — because a hand-built input is a documented
543
+ * use case.
544
+ */
545
+ export function assignParsedValues(
546
+ target: object,
547
+ declaredArgs: readonly ArgumentMetaData[],
548
+ declaredFlags: readonly FlagMetaData[],
549
+ parsed: { args: unknown[] | Record<string, unknown>; flags: Record<string, unknown> },
550
+ ): void {
551
+ const byPosition = Array.isArray(parsed.args) ? parsed.args : undefined
552
+ const byArgName: Record<string, unknown> = byPosition === undefined ? { ...parsed.args } : {}
553
+
554
+ declaredArgs.forEach((meta, index) => {
555
+ define(target, meta.propertyName, byPosition?.[index] ?? byArgName[meta.propertyName])
556
+ })
557
+
558
+ for (const meta of declaredFlags) {
559
+ const key = Object.hasOwn(parsed.flags, meta.flagName) ? meta.flagName : meta.propertyName
560
+ define(target, meta.propertyName, parsed.flags[key])
561
+ }
562
+ }
563
+
564
+ /** Writable and enumerable, as Console's `hydrate` defines them. */
565
+ function define(target: object, property: string, value: unknown): void {
566
+ Object.defineProperty(target, property, {
567
+ value,
568
+ enumerable: true,
569
+ writable: true,
570
+ configurable: true,
571
+ })
572
+ }
573
+
574
+ /**
575
+ * Exported so `BaseCommand.validate()` reports a missing argument with the very
576
+ * message the parser uses — two wordings for one rule is how they drift apart.
577
+ */
578
+ export function missingArgument(meta: ArgumentMetaData, where = ''): ReamError {
579
+ return new ReamError(
580
+ 'E_CONSOLE_MISSING_ARGUMENT',
581
+ `Missing required argument "${meta.argumentName}"${where}.`,
582
+ meta.description ? { hint: meta.description } : undefined,
583
+ )
584
+ }
585
+
586
+ function unknownFlag(token: string, where: string, declared: readonly FlagMetaData[]): ReamError {
587
+ const known = declared.map((f) => `--${f.flagName}`).join(', ')
588
+ return new ReamError('E_CONSOLE_UNKNOWN_FLAG', `Unknown flag "${token}"${where}.`, {
589
+ hint: known.length > 0 ? `Known flags: ${known}` : 'This command declares no flags.',
590
+ })
591
+ }
592
+
593
+ function invalidNegation(token: string, where: string): ReamError {
594
+ return new ReamError(
595
+ 'E_CONSOLE_INVALID_FLAG_VALUE',
596
+ `"${token}"${where} negates a boolean flag and cannot carry a value.`,
597
+ )
598
+ }