clap-ts 0.3.0 → 0.4.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 (43) hide show
  1. package/dist/parser.js +5 -5
  2. package/dist/types.d.ts +14 -1
  3. package/package.json +17 -1
  4. package/src/__tests__/arg-options.test.ts +687 -0
  5. package/src/__tests__/argfile.test.ts +127 -0
  6. package/src/__tests__/clap-parity.test.ts +682 -0
  7. package/src/__tests__/command-options.test.ts +713 -0
  8. package/src/__tests__/completions.test.ts +423 -0
  9. package/src/__tests__/config.test.ts +261 -0
  10. package/src/__tests__/deprecation.test.ts +104 -0
  11. package/src/__tests__/help.test.ts +312 -0
  12. package/src/__tests__/install.test.ts +120 -0
  13. package/src/__tests__/log.test.ts +189 -0
  14. package/src/__tests__/man.test.ts +135 -0
  15. package/src/__tests__/markdown.test.ts +114 -0
  16. package/src/__tests__/output.test.ts +249 -0
  17. package/src/__tests__/parser.test.ts +627 -0
  18. package/src/__tests__/plugins.test.ts +182 -0
  19. package/src/__tests__/progress.test.ts +221 -0
  20. package/src/__tests__/prompt.test.ts +265 -0
  21. package/src/__tests__/runner.test.ts +459 -0
  22. package/src/__tests__/spec.test.ts +107 -0
  23. package/src/__tests__/testing.test.ts +93 -0
  24. package/src/__tests__/validation.test.ts +267 -0
  25. package/src/argfile.ts +188 -0
  26. package/src/completions.ts +865 -0
  27. package/src/config.ts +184 -0
  28. package/src/help.ts +779 -0
  29. package/src/index.ts +58 -0
  30. package/src/install.ts +226 -0
  31. package/src/log.ts +225 -0
  32. package/src/man.ts +289 -0
  33. package/src/markdown.ts +210 -0
  34. package/src/output.ts +453 -0
  35. package/src/parser.ts +1240 -0
  36. package/src/plugins.ts +193 -0
  37. package/src/progress.ts +295 -0
  38. package/src/prompt.ts +388 -0
  39. package/src/runner.ts +769 -0
  40. package/src/spec.ts +197 -0
  41. package/src/testing.ts +159 -0
  42. package/src/types.ts +618 -0
  43. package/src/validation.ts +627 -0
package/src/parser.ts ADDED
@@ -0,0 +1,1240 @@
1
+ /**
2
+ * Argument parser.
3
+ *
4
+ * Tokenizes argv directly against the command's own arg definitions. This
5
+ * replaced node:util parseArgs, which re-validates its entire `options` object
6
+ * on every call (~170ns per option, regardless of argv length) and cannot
7
+ * express multi-value options, value terminators, or subcommand boundaries.
8
+ *
9
+ * Handled here: long/short/clustered flags, attached and `=` values, boolean
10
+ * negation, count and append actions, multi-token numArgs, optional values via
11
+ * defaultMissingValue, value delimiters, hyphen and negative-number values,
12
+ * positional assignment, trailing var args, `--` escape, subcommand
13
+ * boundaries, env fallback, conditional and static defaults, and
14
+ * kebab-to-camel key mapping.
15
+ *
16
+ * Constraint checks that need the whole picture (conflicts, requires, groups,
17
+ * possible values) live in validation.ts.
18
+ */
19
+
20
+ import type {
21
+ ArgDef,
22
+ ArgsDef,
23
+ ParseResult,
24
+ CommandDef,
25
+ PossibleValue,
26
+ ValueSource,
27
+ } from './types.js';
28
+
29
+ // ---- Error ----
30
+
31
+ export class CliParseError extends Error {
32
+ constructor(message: string) {
33
+ super(message);
34
+ this.name = 'CliParseError';
35
+ }
36
+ }
37
+
38
+ // ---- Helpers ----
39
+
40
+ const HYPHEN = 45;
41
+ const NEGATIVE_NUMBER = /^-\d/;
42
+
43
+ /** Convert kebab-case to camelCase: --config-path -> configPath */
44
+ export function kebabToCamel(s: string): string {
45
+ return s.replaceAll(/-([a-z])/g, (_, c: string) => c.toUpperCase());
46
+ }
47
+
48
+ /**
49
+ * Get the raw argv slice (after the binary/script path). With `noBinaryName`
50
+ * the source is taken as-is, matching clap's Command::no_binary_name.
51
+ */
52
+ export function getRawArgs(argv?: readonly string[], noBinaryName = false): string[] {
53
+ if (argv) {
54
+ return [...argv];
55
+ }
56
+ // `Bun.argv` is `process.argv`, and bun implements `process` in full, so
57
+ // reaching for the one when the other is there was a branch with two
58
+ // spellings of one answer.
59
+ const source = process.argv;
60
+ return noBinaryName ? [...source] : source.slice(2);
61
+ }
62
+
63
+ function readEnv(name: string): string | undefined {
64
+ return process.env[name];
65
+ }
66
+
67
+ // ---- Subcommands ----
68
+
69
+ const resolvedSubCommands = new WeakMap<
70
+ CommandDef<any>,
71
+ Record<string, CommandDef<any>>
72
+ >();
73
+
74
+ /**
75
+ * The command's subcommands, building any lazy ones on first use and caching
76
+ * the result so the thunk runs at most once per command.
77
+ */
78
+ export function subCommandsOf(command: CommandDef<any>): Record<string, CommandDef<any>> {
79
+ if (command.lazySubCommands === undefined) {
80
+ return command.subCommands ?? {};
81
+ }
82
+ let resolved = resolvedSubCommands.get(command);
83
+ if (resolved === undefined) {
84
+ resolved = { ...command.lazySubCommands(), ...command.subCommands };
85
+ resolvedSubCommands.set(command, resolved);
86
+ }
87
+ return resolved;
88
+ }
89
+
90
+ /** Whether the command has any subcommand, without building the lazy ones. */
91
+ export function hasSubCommands(command: CommandDef<any>): boolean {
92
+ if (command.lazySubCommands !== undefined) {
93
+ return true;
94
+ }
95
+ const subs = command.subCommands;
96
+ if (subs === undefined) {
97
+ return false;
98
+ }
99
+ for (const _key in subs) {
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ // ---- Possible Values ----
106
+
107
+ const possibleValueCache = new WeakMap<
108
+ readonly (string | PossibleValue)[],
109
+ readonly PossibleValue[]
110
+ >();
111
+
112
+ /**
113
+ * Normalize an arg's allowed values to PossibleValue records. Plain strings and
114
+ * PossibleValue objects can be mixed in the same list.
115
+ */
116
+ export function possibleValues(def: ArgDef): readonly PossibleValue[] {
117
+ const parser = def.valueParser;
118
+ if (parser === undefined || typeof parser === 'function') {
119
+ return [];
120
+ }
121
+ const cached = possibleValueCache.get(parser);
122
+ if (cached !== undefined) {
123
+ return cached;
124
+ }
125
+ const normalized = parser.map((v) => (typeof v === 'string' ? { name: v } : v));
126
+ possibleValueCache.set(parser, normalized);
127
+ return normalized;
128
+ }
129
+
130
+ /** Whether a raw value matches this possible value, by name or alias. */
131
+ export function matchesPossibleValue(
132
+ candidate: PossibleValue,
133
+ value: string,
134
+ ignoreCase: boolean,
135
+ ): boolean {
136
+ if (ignoreCase) {
137
+ const lowered = value.toLowerCase();
138
+ if (candidate.name.toLowerCase() === lowered) {
139
+ return true;
140
+ }
141
+ return candidate.aliases?.some((a) => a.toLowerCase() === lowered) ?? false;
142
+ }
143
+ return candidate.name === value || (candidate.aliases?.includes(value) ?? false);
144
+ }
145
+
146
+ // ---- Compiled Spec ----
147
+
148
+ /**
149
+ * An arg definition flattened into the shape the tokenizer needs, so the hot
150
+ * loop never re-derives `long ?? key`, value counts, or action booleans.
151
+ */
152
+ interface ArgSpec {
153
+ readonly key: string;
154
+ readonly def: ArgDef;
155
+ readonly long: string;
156
+ readonly isBool: boolean;
157
+ readonly isCount: boolean;
158
+ readonly isAppend: boolean;
159
+ /** Minimum values this arg consumes per occurrence. */
160
+ readonly min: number;
161
+ /** Maximum values this arg consumes per occurrence. */
162
+ readonly max: number;
163
+ /** camelCase form of `key`, precomputed so parsing never re-runs the regex. */
164
+ readonly camel: string;
165
+ /** Whether a second occurrence of this arg should be rejected. */
166
+ readonly repeatIsError: boolean;
167
+ /** Warning to emit when this arg is used, precomputed from `deprecated`. */
168
+ readonly deprecation?: string;
169
+ /** Fixed boolean this flag forces, for the setTrue and setFalse actions. */
170
+ readonly forced?: boolean;
171
+ /** Built-in output this arg triggers, for the help and version actions. */
172
+ readonly triggers?: 'help' | 'helpShort' | 'helpLong' | 'version';
173
+ /** Built-in flag this spec stands for, if any. */
174
+ readonly builtin?: 'help' | 'version';
175
+ }
176
+
177
+ interface LongEntry {
178
+ readonly spec: ArgSpec;
179
+ readonly negated: boolean;
180
+ }
181
+
182
+ interface CommandSpec {
183
+ readonly longs: Map<string, LongEntry>;
184
+ readonly shorts: Map<string, ArgSpec>;
185
+ readonly positionals: readonly ArgSpec[];
186
+ /** Count of positionals fed from argv rather than from after `--`. */
187
+ readonly indexedPositionals: number;
188
+ /** Every declared arg, in declaration order, for the fallback and key passes. */
189
+ readonly all: readonly ArgSpec[];
190
+ /** Declared args by key, for cross-references such as replacedBy. */
191
+ readonly byKey: Record<string, ArgSpec>;
192
+ /**
193
+ * The command itself, so the subcommand maps can be built on demand. A CLI
194
+ * that only ever sees flags never pays for a lazySubCommands thunk.
195
+ */
196
+ readonly command: CommandDef<any>;
197
+ readonly anySubcommands: boolean;
198
+ /** Subcommand name or alias -> canonical name. Built on first lookup. */
199
+ subcommands?: Map<string, string>;
200
+ /** Long or short flag -> canonical subcommand name, for `pacman -S` style. */
201
+ subcommandFlags?: Map<string, string>;
202
+ readonly inferSubcommands: boolean;
203
+ readonly allowExternal: boolean;
204
+ readonly subcommandPrecedence: boolean;
205
+ readonly allowMissingPositional: boolean;
206
+ readonly argsOverrideSelf: boolean;
207
+ readonly ignoreErrors: boolean;
208
+ readonly dontDelimitTrailingValues: boolean;
209
+ /** Whether any arg declares overridesWith, so occurrence order must be kept. */
210
+ readonly hasOverrides: boolean;
211
+ readonly cmdAllowHyphen: boolean;
212
+ /** Any arg accepts negative numbers, so `-5` may be a value rather than a flag. */
213
+ readonly allowsNegative: boolean;
214
+ readonly inferLong: boolean;
215
+ }
216
+
217
+ const specCache = new WeakMap<CommandDef, CommandSpec>();
218
+
219
+ /** Actions that stand alone: the flag carries no value of its own. */
220
+ const VALUELESS_ACTIONS = new Set<string>([
221
+ 'count',
222
+ 'setTrue',
223
+ 'setFalse',
224
+ 'help',
225
+ 'helpShort',
226
+ 'helpLong',
227
+ 'version',
228
+ ]);
229
+
230
+ function valueCounts(def: ArgDef): { min: number; max: number } {
231
+ if (def.numArgs) {
232
+ return { min: def.numArgs.min, max: def.numArgs.max };
233
+ }
234
+ if (def.type === 'boolean' || (def.action !== undefined && VALUELESS_ACTIONS.has(def.action))) {
235
+ return { min: 0, max: 0 };
236
+ }
237
+ return { min: 1, max: 1 };
238
+ }
239
+
240
+ /** The stderr notice for a deprecated arg, or undefined when it is current. */
241
+ function deprecationNotice(key: string, def: ArgDef): string | undefined {
242
+ if (def.deprecated === undefined || def.deprecated === false) {
243
+ return undefined;
244
+ }
245
+ const label = def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
246
+ const reason = typeof def.deprecated === 'string' ? `: ${def.deprecated}` : '';
247
+ const instead = def.replacedBy === undefined ? '' : `; use '--${def.replacedBy}' instead`;
248
+ return `'${label}' is deprecated${reason}${instead}`;
249
+ }
250
+
251
+ function makeSpec(key: string, def: ArgDef, argsOverrideSelf: boolean): ArgSpec {
252
+ const { min, max } = valueCounts(def);
253
+ const isAppend = def.action === 'append';
254
+ const forced =
255
+ def.action === 'setTrue' ? true : def.action === 'setFalse' ? false : undefined;
256
+ const triggers =
257
+ def.action === 'help' || def.action === 'helpShort' || def.action === 'helpLong'
258
+ ? def.action
259
+ : def.action === 'version'
260
+ ? ('version' as const)
261
+ : undefined;
262
+ return {
263
+ forced,
264
+ triggers,
265
+ deprecation: deprecationNotice(key, def),
266
+ key,
267
+ def,
268
+ long: def.long ?? key,
269
+ camel: kebabToCamel(key),
270
+ repeatIsError: !isAppend && !argsOverrideSelf && def.overridesWith === undefined,
271
+ isBool: def.type === 'boolean' && !VALUELESS_ACTIONS.has(def.action ?? ''),
272
+ isCount: def.action === 'count',
273
+ isAppend,
274
+ min,
275
+ max,
276
+ };
277
+ }
278
+
279
+ function registerAliases(
280
+ aliases: readonly string[] | undefined,
281
+ spec: ArgSpec,
282
+ longs: Map<string, LongEntry>,
283
+ shorts: Map<string, ArgSpec>,
284
+ ): void {
285
+ if (!aliases) {
286
+ return;
287
+ }
288
+ for (const alias of aliases) {
289
+ if (alias.length === 1) {
290
+ shorts.set(alias, spec);
291
+ } else {
292
+ longs.set(alias, { spec, negated: false });
293
+ }
294
+ }
295
+ }
296
+
297
+ function buildSpec(command: CommandDef): CommandSpec {
298
+ const argsDef: ArgsDef = command.args ?? {};
299
+ const longs = new Map<string, LongEntry>();
300
+ const shorts = new Map<string, ArgSpec>();
301
+ const positionals: ArgSpec[] = [];
302
+ const all: ArgSpec[] = [];
303
+ const byKey: Record<string, ArgSpec> = {};
304
+ let allowsNegative = false;
305
+
306
+ const cmdAllowHyphen = command.meta.allowHyphenValues === true;
307
+ const argsOverrideSelf = command.meta.argsOverrideSelf === true;
308
+ let hasOverrides = false;
309
+ if (command.meta.allowNegativeNumbers) {
310
+ allowsNegative = true;
311
+ }
312
+
313
+ for (const key of Object.keys(argsDef)) {
314
+ const def = argsDef[key]!;
315
+
316
+ if (def.allowNegativeNumbers) {
317
+ allowsNegative = true;
318
+ }
319
+ if (def.overridesWith !== undefined) {
320
+ hasOverrides = true;
321
+ }
322
+
323
+ const spec = makeSpec(key, def, argsOverrideSelf);
324
+ all.push(spec);
325
+ byKey[key] = spec;
326
+
327
+ if (def.type === 'positional') {
328
+ positionals.push(spec);
329
+ continue;
330
+ }
331
+
332
+ longs.set(spec.long, { spec, negated: false });
333
+ if (spec.isBool) {
334
+ longs.set(`no-${spec.long}`, { spec, negated: true });
335
+ }
336
+ if (def.short && def.short.length === 1) {
337
+ shorts.set(def.short, spec);
338
+ }
339
+ registerAliases(def.alias, spec, longs, shorts);
340
+ registerAliases(def.visibleAlias, spec, longs, shorts);
341
+ }
342
+
343
+ // Built-in flags never displace a user-defined arg of the same name.
344
+ const helpSpec: ArgSpec = {
345
+ key: 'help', def: { type: 'boolean' }, long: 'help', camel: 'help',
346
+ repeatIsError: false,
347
+ isBool: true, isCount: false, isAppend: false, min: 0, max: 0, builtin: 'help',
348
+ };
349
+ const versionSpec: ArgSpec = {
350
+ key: 'version', def: { type: 'boolean' }, long: 'version', camel: 'version',
351
+ repeatIsError: false,
352
+ isBool: true, isCount: false, isAppend: false, min: 0, max: 0, builtin: 'version',
353
+ };
354
+ if (command.meta.disableHelpFlag !== true) {
355
+ if (!longs.has('help')) {
356
+ longs.set('help', { spec: helpSpec, negated: false });
357
+ }
358
+ if (!shorts.has('h')) {
359
+ shorts.set('h', helpSpec);
360
+ }
361
+ }
362
+ // clap only offers --version where a version exists, either on this command
363
+ // or propagated down from an ancestor.
364
+ if (command.meta.disableVersionFlag !== true && command.meta.version !== undefined) {
365
+ if (!longs.has('version')) {
366
+ longs.set('version', { spec: versionSpec, negated: false });
367
+ }
368
+ if (!shorts.has('V')) {
369
+ shorts.set('V', versionSpec);
370
+ }
371
+ }
372
+
373
+ if (command.meta.helpExpected) {
374
+ const undocumented = all
375
+ .filter((spec) => !spec.def.hidden && !spec.def.description && !spec.def.longDescription)
376
+ .map((spec) => spec.key);
377
+ if (undocumented.length > 0) {
378
+ throw new CliParseError(
379
+ `helpExpected is set but these arguments have no description: ${undocumented.join(', ')}`,
380
+ );
381
+ }
382
+ }
383
+
384
+ // An explicit index overrides declaration order; unindexed positionals keep
385
+ // their relative order after the indexed ones are placed.
386
+ if (positionals.some((spec) => spec.def.index !== undefined)) {
387
+ positionals.sort((a, b) => (a.def.index ?? Number.MAX_SAFE_INTEGER) - (b.def.index ?? Number.MAX_SAFE_INTEGER));
388
+ }
389
+
390
+ return {
391
+ longs,
392
+ shorts,
393
+ positionals,
394
+ indexedPositionals: positionals.reduce((n, spec) => (spec.def.last ? n : n + 1), 0),
395
+ all,
396
+ byKey,
397
+ command,
398
+ anySubcommands: hasSubCommands(command),
399
+ inferSubcommands: command.meta.inferSubcommands === true,
400
+ allowExternal: command.meta.allowExternalSubcommands === true,
401
+ subcommandPrecedence: command.meta.subcommandPrecedenceOverArg === true,
402
+ allowMissingPositional: command.meta.allowMissingPositional === true,
403
+ argsOverrideSelf,
404
+ ignoreErrors: command.meta.ignoreErrors === true,
405
+ dontDelimitTrailingValues: command.meta.dontDelimitTrailingValues === true,
406
+ hasOverrides,
407
+ cmdAllowHyphen,
408
+ allowsNegative,
409
+ inferLong: command.meta.inferLongArgs === true,
410
+ };
411
+ }
412
+
413
+ function getSpec(command: CommandDef): CommandSpec {
414
+ let spec = specCache.get(command);
415
+ if (spec === undefined) {
416
+ spec = buildSpec(command);
417
+ specCache.set(command, spec);
418
+ }
419
+ return spec;
420
+ }
421
+
422
+ // ---- Value Coercion ----
423
+
424
+ export function coerceValue(
425
+ value: string,
426
+ def: ArgDef,
427
+ argName: string,
428
+ ): string | number | boolean {
429
+ if (typeof def.valueParser === 'function') {
430
+ try {
431
+ const parsed = def.valueParser(value);
432
+ if (
433
+ typeof parsed === 'string' ||
434
+ typeof parsed === 'number' ||
435
+ typeof parsed === 'boolean'
436
+ ) {
437
+ return parsed;
438
+ }
439
+ return String(parsed);
440
+ } catch (error) {
441
+ const msg = error instanceof Error ? error.message : String(error);
442
+ throw new CliParseError(`invalid value '${value}' for '${argName}': ${msg}`);
443
+ }
444
+ }
445
+
446
+ switch (def.type) {
447
+ case 'boolean': {
448
+ const lower = value.toLowerCase();
449
+ return lower === 'true' || lower === '1' || lower === 'yes';
450
+ }
451
+ case 'number': {
452
+ const num = value.includes('.') ? Number.parseFloat(value) : Number.parseInt(value, 10);
453
+ if (Number.isNaN(num) || !Number.isFinite(num)) {
454
+ throw new CliParseError(
455
+ `invalid value '${value}' for '${argName}': expected a finite number`,
456
+ );
457
+ }
458
+ return num;
459
+ }
460
+ case 'enum':
461
+ case 'string':
462
+ case 'positional': {
463
+ return value;
464
+ }
465
+ }
466
+ }
467
+
468
+ /** Human-readable name for this arg in error messages. */
469
+ function displayName(spec: ArgSpec): string {
470
+ return spec.def.type === 'positional' ? `<${spec.def.valueName ?? spec.key}>` : `--${spec.long}`;
471
+ }
472
+
473
+ // ---- Tokenizer State ----
474
+
475
+ interface ParseState {
476
+ readonly result: Record<string, string | number | boolean | string[]>;
477
+ readonly explicitlySet: Set<string>;
478
+ readonly valueSources: Map<string, ValueSource>;
479
+ readonly errors: string[];
480
+ readonly warnings: string[];
481
+ readonly warned: Set<string>;
482
+ /** Keys whose value came from tokens after `--`. */
483
+ readonly fromRest: Set<string>;
484
+ /**
485
+ * Arg key -> sequence number of its last explicit occurrence. Only built when
486
+ * some arg declares overridesWith, so the common parse allocates no Map.
487
+ */
488
+ order: Map<string, number> | undefined;
489
+ seq: number;
490
+ readonly unknown: string[];
491
+ readonly positionals: string[];
492
+ readonly rest: string[];
493
+ helpRequested: boolean;
494
+ helpIsShort: boolean;
495
+ versionRequested: boolean;
496
+ versionIsShort: boolean;
497
+ subCommand?: string;
498
+ subCommandIsExternal: boolean;
499
+ subCommandArgs: readonly string[];
500
+ }
501
+
502
+ /** Record that an arg was explicitly given, keeping the occurrence order. */
503
+ function markSet(state: ParseState, key: string, spec?: ArgSpec): void {
504
+ state.explicitlySet.add(key);
505
+ state.valueSources.set(key, 'cli');
506
+ state.order?.set(key, state.seq++);
507
+ if (spec?.deprecation !== undefined && !state.warned.has(key)) {
508
+ state.warned.add(key);
509
+ state.warnings.push(spec.deprecation);
510
+ }
511
+ }
512
+
513
+ /** Whether a token can serve as a value for this arg rather than starting a new flag. */
514
+ function isValueToken(token: string, spec: ArgSpec, cmdSpec: CommandSpec): boolean {
515
+ if (token.length === 0 || token.charCodeAt(0) !== HYPHEN) {
516
+ return true;
517
+ }
518
+ if (token === '-') {
519
+ return true;
520
+ }
521
+ if (token === '--') {
522
+ return false;
523
+ }
524
+ if (spec.def.allowHyphenValues || cmdSpec.cmdAllowHyphen) {
525
+ return true;
526
+ }
527
+ return (
528
+ (spec.def.allowNegativeNumbers || cmdSpec.allowsNegative) && NEGATIVE_NUMBER.test(token)
529
+ );
530
+ }
531
+
532
+ /** Record a flag occurrence that carries no value: booleans and counts. */
533
+ function applyFlagOnly(spec: ArgSpec, negated: boolean, state: ParseState): void {
534
+ if (spec.builtin === 'help' || spec.triggers === 'help' || spec.triggers === 'helpLong') {
535
+ state.helpRequested = true;
536
+ return;
537
+ }
538
+ if (spec.triggers === 'helpShort') {
539
+ state.helpRequested = true;
540
+ state.helpIsShort = true;
541
+ return;
542
+ }
543
+ if (spec.builtin === 'version' || spec.triggers === 'version') {
544
+ state.versionRequested = true;
545
+ return;
546
+ }
547
+ if (spec.forced !== undefined) {
548
+ state.result[spec.key] = negated ? !spec.forced : spec.forced;
549
+ markSet(state, spec.key, spec);
550
+ return;
551
+ }
552
+ if (spec.isCount) {
553
+ const previous = state.result[spec.key];
554
+ state.result[spec.key] = (typeof previous === 'number' ? previous : 0) + 1;
555
+ } else {
556
+ state.result[spec.key] = !negated;
557
+ }
558
+ markSet(state, spec.key, spec);
559
+ }
560
+
561
+ /** Store one or more parsed values for an arg, honouring the append action. */
562
+ function applyValues(spec: ArgSpec, values: string[], state: ParseState): void {
563
+ const name = displayName(spec);
564
+
565
+ // A second occurrence of a single-value arg is an error in clap unless the
566
+ // command opted into argsOverrideSelf. Reading result is cheaper than the
567
+ // Set, and defaults have not been applied yet, so a value here means a
568
+ // previous occurrence.
569
+ if (spec.repeatIsError && state.result[spec.key] !== undefined) {
570
+ throw new CliParseError(`the argument '${name}' cannot be used multiple times`);
571
+ }
572
+
573
+ if (spec.isAppend) {
574
+ const previous = state.result[spec.key];
575
+ const list = Array.isArray(previous) ? previous : [];
576
+ for (const value of values) {
577
+ list.push(String(coerceValue(value, spec.def, name)));
578
+ }
579
+ state.result[spec.key] = list;
580
+ } else if (values.length > 1) {
581
+ state.result[spec.key] = values.map((v) => String(coerceValue(v, spec.def, name)));
582
+ } else {
583
+ state.result[spec.key] = coerceValue(values[0]!, spec.def, name);
584
+ }
585
+
586
+ markSet(state, spec.key, spec);
587
+ }
588
+
589
+ /** Apply defaultMissingValue for a flag whose value was omitted (numArgs.min === 0). */
590
+ function applyMissingValue(spec: ArgSpec, state: ParseState): void {
591
+ if (spec.def.defaultMissingValues !== undefined) {
592
+ state.result[spec.key] = [...spec.def.defaultMissingValues];
593
+ markSet(state, spec.key, spec);
594
+ return;
595
+ }
596
+ const fallback = spec.def.defaultMissingValue ?? true;
597
+ if (spec.isAppend) {
598
+ const previous = state.result[spec.key];
599
+ const list = Array.isArray(previous) ? previous : [];
600
+ list.push(String(fallback));
601
+ state.result[spec.key] = list;
602
+ } else {
603
+ state.result[spec.key] = fallback;
604
+ }
605
+ markSet(state, spec.key, spec);
606
+ }
607
+
608
+ /**
609
+ * Consume up to `spec.max` values for a flag starting at argv[from].
610
+ * Returns the index of the last token consumed.
611
+ */
612
+ function consumeValues(
613
+ spec: ArgSpec,
614
+ argv: readonly string[],
615
+ from: number,
616
+ cmdSpec: CommandSpec,
617
+ state: ParseState,
618
+ ): number {
619
+ const values: string[] = [];
620
+ let i = from;
621
+
622
+ while (values.length < spec.max && i < argv.length) {
623
+ const token = argv[i]!;
624
+ if (spec.def.valueTerminator !== undefined && token === spec.def.valueTerminator) {
625
+ i++;
626
+ break;
627
+ }
628
+ if (!isValueToken(token, spec, cmdSpec)) {
629
+ break;
630
+ }
631
+ if (
632
+ cmdSpec.subcommandPrecedence &&
633
+ values.length >= spec.min &&
634
+ resolveSubcommand(cmdSpec, token) !== undefined
635
+ ) {
636
+ break;
637
+ }
638
+ values.push(token);
639
+ i++;
640
+ }
641
+
642
+ if (values.length < spec.min) {
643
+ if (values.length === 0 && spec.min === 0) {
644
+ applyMissingValue(spec, state);
645
+ return i - 1;
646
+ }
647
+ throw new CliParseError(
648
+ spec.min === 1
649
+ ? `a value is required for '${displayName(spec)}' but none was supplied`
650
+ : `the argument '${displayName(spec)}' requires at least ${String(spec.min)} values but ${String(values.length)} were provided`,
651
+ );
652
+ }
653
+
654
+ if (values.length === 0) {
655
+ applyMissingValue(spec, state);
656
+ } else {
657
+ applyValues(spec, values, state);
658
+ }
659
+ return i - 1;
660
+ }
661
+
662
+ /**
663
+ * Resolve an unknown long flag by unique prefix match (inferLongArgs).
664
+ * Negated (`--no-x`) entries are excluded so `--n` cannot silently negate.
665
+ */
666
+ function inferLongEntry(cmdSpec: CommandSpec, name: string): LongEntry | undefined {
667
+ let match: LongEntry | undefined;
668
+ for (const [candidate, entry] of cmdSpec.longs) {
669
+ if (entry.negated || !candidate.startsWith(name)) {
670
+ continue;
671
+ }
672
+ if (match !== undefined) {
673
+ return undefined;
674
+ }
675
+ match = entry;
676
+ }
677
+ return match;
678
+ }
679
+
680
+ /** Build the name and flag lookup maps, once, on the first token that needs them. */
681
+ function buildSubcommandMaps(cmdSpec: CommandSpec): void {
682
+ const names = new Map<string, string>();
683
+ const flags = new Map<string, string>();
684
+
685
+ const subs = subCommandsOf(cmdSpec.command);
686
+ for (const name of Object.keys(subs)) {
687
+ names.set(name, name);
688
+ const subMeta = subs[name]!.meta;
689
+ for (const alias of [...(subMeta.aliases ?? []), ...(subMeta.hiddenAliases ?? [])]) {
690
+ names.set(alias, name);
691
+ }
692
+ for (const form of [
693
+ ...(subMeta.shortFlag === undefined ? [] : [subMeta.shortFlag]),
694
+ ...(subMeta.shortFlagAliases ?? []),
695
+ ...(subMeta.visibleShortFlagAliases ?? []),
696
+ ]) {
697
+ flags.set(`-${form}`, name);
698
+ }
699
+ for (const form of [
700
+ ...(subMeta.longFlag === undefined ? [] : [subMeta.longFlag]),
701
+ ...(subMeta.longFlagAliases ?? []),
702
+ ...(subMeta.visibleLongFlagAliases ?? []),
703
+ ]) {
704
+ flags.set(`--${form}`, name);
705
+ }
706
+ }
707
+
708
+ cmdSpec.subcommands = names;
709
+ cmdSpec.subcommandFlags = flags;
710
+ }
711
+
712
+ /** The subcommand named by a flag form like `-S`, or undefined. */
713
+ function subcommandForFlag(cmdSpec: CommandSpec, token: string): string | undefined {
714
+ if (!cmdSpec.anySubcommands) {
715
+ return undefined;
716
+ }
717
+ if (cmdSpec.subcommandFlags === undefined) {
718
+ buildSubcommandMaps(cmdSpec);
719
+ }
720
+ return cmdSpec.subcommandFlags!.get(token);
721
+ }
722
+
723
+ /**
724
+ * Resolve a bare token to a canonical subcommand name, by exact match on the
725
+ * name or an alias, then by unique prefix when inferSubcommands is on.
726
+ */
727
+ function resolveSubcommand(cmdSpec: CommandSpec, token: string): string | undefined {
728
+ if (!cmdSpec.anySubcommands) {
729
+ return undefined;
730
+ }
731
+ if (cmdSpec.subcommands === undefined) {
732
+ buildSubcommandMaps(cmdSpec);
733
+ }
734
+ const map = cmdSpec.subcommands!;
735
+ const exact = map.get(token);
736
+ if (exact !== undefined) {
737
+ return exact;
738
+ }
739
+ if (!cmdSpec.inferSubcommands) {
740
+ return undefined;
741
+ }
742
+ let match: string | undefined;
743
+ for (const [candidate, canonical] of map) {
744
+ if (!candidate.startsWith(token)) {
745
+ continue;
746
+ }
747
+ if (match !== undefined && match !== canonical) {
748
+ return undefined;
749
+ }
750
+ match = canonical;
751
+ }
752
+ return match;
753
+ }
754
+
755
+ /** Handle a `--long`, `--long=value` or `--no-long` token. Returns the last index consumed. */
756
+ function handleLong(
757
+ token: string,
758
+ argv: readonly string[],
759
+ i: number,
760
+ cmdSpec: CommandSpec,
761
+ state: ParseState,
762
+ ): number {
763
+ const eq = token.indexOf('=');
764
+ const name = eq === -1 ? token.slice(2) : token.slice(2, eq);
765
+
766
+ let entry = cmdSpec.longs.get(name);
767
+ if (entry === undefined && cmdSpec.inferLong) {
768
+ entry = inferLongEntry(cmdSpec, name);
769
+ }
770
+ if (entry === undefined) {
771
+ // Only now is it worth building the subcommand maps: an unrecognised flag
772
+ // may still be a flag-invoked subcommand like `pacman --sync`.
773
+ const flagSub = subcommandForFlag(cmdSpec, token);
774
+ if (flagSub !== undefined) {
775
+ state.subCommand = flagSub;
776
+ state.subCommandArgs = argv.slice(i + 1);
777
+ return i;
778
+ }
779
+ state.unknown.push(`--${name}`);
780
+ return i;
781
+ }
782
+
783
+ const { spec, negated } = entry;
784
+
785
+ if (eq !== -1) {
786
+ const inline = token.slice(eq + 1);
787
+ if (spec.builtin !== undefined || spec.isCount) {
788
+ applyFlagOnly(spec, negated, state);
789
+ return i;
790
+ }
791
+ if (spec.isBool && negated) {
792
+ // `--no-verbose=true` means "negate", so invert whatever was supplied.
793
+ state.result[spec.key] = coerceValue(inline, spec.def, `--${name}`) === false;
794
+ markSet(state, spec.key, spec);
795
+ return i;
796
+ }
797
+ applyValues(spec, [inline], state);
798
+ return i;
799
+ }
800
+
801
+ if (spec.max === 0) {
802
+ applyFlagOnly(spec, negated, state);
803
+ return i;
804
+ }
805
+ if (spec.def.requireEquals) {
806
+ if (spec.min === 0) {
807
+ applyMissingValue(spec, state);
808
+ return i;
809
+ }
810
+ throw new CliParseError(
811
+ `equal sign is needed when assigning values to '${displayName(spec)}'`,
812
+ );
813
+ }
814
+ return consumeValues(spec, argv, i + 1, cmdSpec, state);
815
+ }
816
+
817
+ /** Handle a `-abc`, `-p80`, `-p=80` or `-p 80` token. Returns the last index consumed. */
818
+ function handleShort(
819
+ token: string,
820
+ argv: readonly string[],
821
+ i: number,
822
+ cmdSpec: CommandSpec,
823
+ state: ParseState,
824
+ ): number {
825
+ for (let c = 1; c < token.length; c++) {
826
+ const flag = token[c]!;
827
+ const spec = cmdSpec.shorts.get(flag);
828
+
829
+ if (spec === undefined) {
830
+ const flagSub = subcommandForFlag(cmdSpec, token);
831
+ if (flagSub !== undefined) {
832
+ state.subCommand = flagSub;
833
+ state.subCommandArgs = argv.slice(i + 1);
834
+ return i;
835
+ }
836
+ state.unknown.push(`-${flag}`);
837
+ return i;
838
+ }
839
+
840
+ if (spec.builtin === 'help') {
841
+ state.helpRequested = true;
842
+ state.helpIsShort = true;
843
+ continue;
844
+ }
845
+ if (spec.builtin === 'version') {
846
+ state.versionRequested = true;
847
+ state.versionIsShort = true;
848
+ continue;
849
+ }
850
+ if (spec.max === 0) {
851
+ applyFlagOnly(spec, false, state);
852
+ continue;
853
+ }
854
+
855
+ // A value-taking short flag takes the rest of the cluster, or the next token.
856
+ if (c + 1 < token.length) {
857
+ // clap strips a single leading '=' so `-o=v` and `-ov` agree.
858
+ const attached = token.charCodeAt(c + 1) === 61 ? token.slice(c + 2) : token.slice(c + 1);
859
+ applyValues(spec, [attached], state);
860
+ return i;
861
+ }
862
+ if (spec.def.requireEquals) {
863
+ if (spec.min === 0) {
864
+ applyMissingValue(spec, state);
865
+ return i;
866
+ }
867
+ throw new CliParseError(
868
+ `equal sign is needed when assigning values to '${displayName(spec)}'`,
869
+ );
870
+ }
871
+ return consumeValues(spec, argv, i + 1, cmdSpec, state);
872
+ }
873
+
874
+ return i;
875
+ }
876
+
877
+ /**
878
+ * Consume one token: a flag-invoked subcommand, a long or short flag, a
879
+ * subcommand boundary, or a positional. Returns the last index consumed.
880
+ */
881
+ function handleToken(
882
+ token: string,
883
+ rawArgs: readonly string[],
884
+ i: number,
885
+ cmdSpec: CommandSpec,
886
+ state: ParseState,
887
+ ): number {
888
+ if (token.length > 1 && token.charCodeAt(0) === HYPHEN) {
889
+ if (token.charCodeAt(1) === HYPHEN) {
890
+ return handleLong(token, rawArgs, i, cmdSpec, state);
891
+ }
892
+ // A negative number is a value, not a flag cluster, when nothing claims the
893
+ // leading digit as a short flag.
894
+ const isNegativeValue =
895
+ cmdSpec.allowsNegative && NEGATIVE_NUMBER.test(token) && !cmdSpec.shorts.has(token[1]!);
896
+ if (!isNegativeValue) {
897
+ return handleShort(token, rawArgs, i, cmdSpec, state);
898
+ }
899
+ }
900
+
901
+ if (state.positionals.length === 0) {
902
+ const canonical = resolveSubcommand(cmdSpec, token);
903
+ if (canonical !== undefined) {
904
+ state.subCommand = canonical;
905
+ state.subCommandArgs = rawArgs.slice(i + 1);
906
+ return i;
907
+ }
908
+ if (cmdSpec.allowExternal) {
909
+ state.subCommand = token;
910
+ state.subCommandIsExternal = true;
911
+ state.subCommandArgs = rawArgs.slice(i + 1);
912
+ return i;
913
+ }
914
+ }
915
+
916
+ state.positionals.push(token);
917
+ return i;
918
+ }
919
+
920
+ // ---- Positional Assignment ----
921
+
922
+ function assignPositionals(cmdSpec: CommandSpec, state: ParseState): void {
923
+ let index = 0;
924
+ // Definitions still waiting for a value, so a leading optional one can be
925
+ // skipped when there are not enough values to go round.
926
+ let remainingDefs = cmdSpec.indexedPositionals;
927
+
928
+ for (const spec of cmdSpec.positionals) {
929
+ if (!spec.def.last) {
930
+ remainingDefs--;
931
+ if (
932
+ cmdSpec.allowMissingPositional &&
933
+ !spec.def.required &&
934
+ state.positionals.length - index <= remainingDefs
935
+ ) {
936
+ continue;
937
+ }
938
+ }
939
+
940
+ if (spec.def.last) {
941
+ // `last` positionals are only fed from tokens after `--`.
942
+ if (state.rest.length > 0) {
943
+ state.result[spec.key] = state.rest[0]!;
944
+ state.fromRest.add(spec.key);
945
+ markSet(state, spec.key, spec);
946
+ }
947
+ continue;
948
+ }
949
+
950
+ if (spec.def.trailingVarArg) {
951
+ if (index < state.positionals.length) {
952
+ state.result[spec.key] = state.positionals.slice(index);
953
+ markSet(state, spec.key, spec);
954
+ index = state.positionals.length;
955
+ }
956
+ return;
957
+ }
958
+
959
+ if (index >= state.positionals.length) {
960
+ continue;
961
+ }
962
+
963
+ if (spec.max > 1) {
964
+ const take = Math.min(spec.max, state.positionals.length - index);
965
+ const values = state.positionals.slice(index, index + take);
966
+ state.result[spec.key] = values.map((v) => String(coerceValue(v, spec.def, spec.key)));
967
+ markSet(state, spec.key, spec);
968
+ index += take;
969
+ continue;
970
+ }
971
+
972
+ state.result[spec.key] = coerceValue(state.positionals[index]!, spec.def, spec.key);
973
+ markSet(state, spec.key, spec);
974
+ index++;
975
+ }
976
+ }
977
+
978
+ // ---- Overrides ----
979
+
980
+ /**
981
+ * Drop args displaced by a later `overridesWith`. Only command-line
982
+ * occurrences take part, so an env or default value is never overridden.
983
+ */
984
+ function applyOverrides(cmdSpec: CommandSpec, state: ParseState): void {
985
+ const order = state.order;
986
+ if (order === undefined) {
987
+ return;
988
+ }
989
+ for (const spec of cmdSpec.all) {
990
+ const targets = spec.def.overridesWith;
991
+ if (targets === undefined || !state.explicitlySet.has(spec.key)) {
992
+ continue;
993
+ }
994
+ const mine = order.get(spec.key);
995
+ if (mine === undefined) {
996
+ continue;
997
+ }
998
+ for (const target of targets) {
999
+ if (target === spec.key) {
1000
+ continue;
1001
+ }
1002
+ const theirs = order.get(target);
1003
+ if (theirs === undefined || theirs > mine) {
1004
+ continue;
1005
+ }
1006
+ delete state.result[target];
1007
+ state.explicitlySet.delete(target);
1008
+ order.delete(target);
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Copy each deprecated arg's value to the arg that replaced it, unless that one
1015
+ * was given directly. Lets a rename keep working without the handler caring.
1016
+ */
1017
+ function applyReplacements(cmdSpec: CommandSpec, state: ParseState): void {
1018
+ for (const spec of cmdSpec.all) {
1019
+ const target = spec.def.replacedBy;
1020
+ if (target === undefined || !state.explicitlySet.has(spec.key)) {
1021
+ continue;
1022
+ }
1023
+ if (state.explicitlySet.has(target) || !(target in cmdSpec.byKey)) {
1024
+ continue;
1025
+ }
1026
+ const value = state.result[spec.key];
1027
+ if (value === undefined) {
1028
+ continue;
1029
+ }
1030
+ state.result[target] = value;
1031
+ state.explicitlySet.add(target);
1032
+ state.valueSources.set(target, state.valueSources.get(spec.key) ?? 'cli');
1033
+ }
1034
+ }
1035
+
1036
+ // ---- Defaults, Env, Delimiters ----
1037
+
1038
+ function splitByDelimiter(value: string | string[], delimiter: string): string[] {
1039
+ if (Array.isArray(value)) {
1040
+ const out: string[] = [];
1041
+ for (const v of value) {
1042
+ out.push(...v.split(delimiter));
1043
+ }
1044
+ return out;
1045
+ }
1046
+ return value.split(delimiter);
1047
+ }
1048
+
1049
+ function applyFallbacks(cmdSpec: CommandSpec, state: ParseState): void {
1050
+ const { result, explicitlySet } = state;
1051
+
1052
+ for (const spec of cmdSpec.all) {
1053
+ const { key, def } = spec;
1054
+ if (explicitlySet.has(key)) {
1055
+ if (
1056
+ def.valueDelimiter &&
1057
+ !(cmdSpec.dontDelimitTrailingValues && state.fromRest.has(key))
1058
+ ) {
1059
+ const value = result[key];
1060
+ if (value !== undefined && (typeof value === 'string' || Array.isArray(value))) {
1061
+ result[key] = splitByDelimiter(value, def.valueDelimiter);
1062
+ }
1063
+ }
1064
+ continue;
1065
+ }
1066
+
1067
+ if (def.env) {
1068
+ const envValue = readEnv(def.env);
1069
+ if (envValue !== undefined && envValue !== '') {
1070
+ result[key] = def.valueDelimiter
1071
+ ? envValue.split(def.valueDelimiter)
1072
+ : coerceValue(envValue, def, `env:${def.env}`);
1073
+ explicitlySet.add(key);
1074
+ state.valueSources.set(key, 'env');
1075
+ continue;
1076
+ }
1077
+ }
1078
+
1079
+ if (def.defaultValueIf) {
1080
+ const [otherKey, otherValue, conditionalDefault] = def.defaultValueIf;
1081
+ if (String(result[otherKey]) === String(otherValue)) {
1082
+ result[key] = conditionalDefault;
1083
+ state.valueSources.set(key, 'default');
1084
+ continue;
1085
+ }
1086
+ }
1087
+
1088
+ if (def.defaultValueIfs) {
1089
+ let matched = false;
1090
+ for (const [otherKey, otherValue, conditionalDefault] of def.defaultValueIfs) {
1091
+ if (String(result[otherKey]) === String(otherValue)) {
1092
+ result[key] = conditionalDefault;
1093
+ state.valueSources.set(key, 'default');
1094
+ matched = true;
1095
+ break;
1096
+ }
1097
+ }
1098
+ if (matched) {
1099
+ continue;
1100
+ }
1101
+ }
1102
+
1103
+ if (def.default !== undefined) {
1104
+ result[key] = Array.isArray(def.default)
1105
+ ? [...def.default]
1106
+ : (def.default as string | number | boolean);
1107
+ state.valueSources.set(key, 'default');
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ // ---- Main Parser ----
1113
+
1114
+ /**
1115
+ * Parse raw argument tokens against a command definition.
1116
+ *
1117
+ * Stops at the first bare token matching a subcommand name or alias; the
1118
+ * remaining tokens are returned as `subCommandArgs` for the caller to parse
1119
+ * against that subcommand.
1120
+ */
1121
+ export function parseArgs(rawArgs: readonly string[], command: CommandDef): ParseResult {
1122
+ const cmdSpec = getSpec(command);
1123
+
1124
+ const state: ParseState = {
1125
+ result: {},
1126
+ explicitlySet: new Set<string>(),
1127
+ valueSources: new Map<string, ValueSource>(),
1128
+ errors: [],
1129
+ warnings: [],
1130
+ warned: new Set<string>(),
1131
+ fromRest: new Set<string>(),
1132
+ order: cmdSpec.hasOverrides ? new Map<string, number>() : undefined,
1133
+ seq: 0,
1134
+ unknown: [],
1135
+ positionals: [],
1136
+ rest: [],
1137
+ helpRequested: false,
1138
+ helpIsShort: false,
1139
+ versionRequested: false,
1140
+ versionIsShort: false,
1141
+ subCommandIsExternal: false,
1142
+ subCommandArgs: [],
1143
+ };
1144
+
1145
+ let i = 0;
1146
+ for (; i < rawArgs.length; i++) {
1147
+ const token = rawArgs[i]!;
1148
+
1149
+ if (token === '--') {
1150
+ for (let r = i + 1; r < rawArgs.length; r++) {
1151
+ state.rest.push(rawArgs[r]!);
1152
+ }
1153
+ break;
1154
+ }
1155
+
1156
+ // ignoreErrors keeps going after a bad token, collecting the message, the
1157
+ // way clap's Command::ignore_errors does.
1158
+ if (cmdSpec.ignoreErrors) {
1159
+ try {
1160
+ i = handleToken(token, rawArgs, i, cmdSpec, state);
1161
+ } catch (error) {
1162
+ state.errors.push(error instanceof Error ? error.message : String(error));
1163
+ }
1164
+ } else {
1165
+ i = handleToken(token, rawArgs, i, cmdSpec, state);
1166
+ }
1167
+
1168
+ if (state.subCommand !== undefined) {
1169
+ break;
1170
+ }
1171
+ }
1172
+
1173
+ assignPositionals(cmdSpec, state);
1174
+ applyOverrides(cmdSpec, state);
1175
+ applyReplacements(cmdSpec, state);
1176
+ applyFallbacks(cmdSpec, state);
1177
+
1178
+ // Expose both the declared key and its camelCase form.
1179
+ const args: Record<string, string | number | boolean | string[]> = {};
1180
+ for (const spec of cmdSpec.all) {
1181
+ const value = state.result[spec.key];
1182
+ if (value === undefined) {
1183
+ continue;
1184
+ }
1185
+ args[spec.camel] = value;
1186
+ if (spec.key !== spec.camel) {
1187
+ args[spec.key] = value;
1188
+ }
1189
+ }
1190
+
1191
+ return {
1192
+ args,
1193
+ positionals: state.positionals,
1194
+ rest: state.rest,
1195
+ subCommand: state.subCommand,
1196
+ subCommandIsExternal: state.subCommandIsExternal,
1197
+ subCommandArgs: state.subCommandArgs,
1198
+ helpRequested: state.helpRequested,
1199
+ helpIsShort: state.helpIsShort,
1200
+ versionRequested: state.versionRequested,
1201
+ versionIsShort: state.versionIsShort,
1202
+ unknown: state.unknown,
1203
+ errors: state.errors,
1204
+ warnings: state.warnings,
1205
+ explicitlySet: state.explicitlySet,
1206
+ valueSources: state.valueSources,
1207
+ };
1208
+ }
1209
+
1210
+ // ---- Global Args ----
1211
+
1212
+ /**
1213
+ * Collect global args from a command.
1214
+ * Returns a merged ArgsDef of all global args.
1215
+ */
1216
+ export function collectGlobalArgs(command: CommandDef): ArgsDef {
1217
+ const globals: Record<string, ArgDef> = {};
1218
+ const args = command.args ?? {};
1219
+ for (const [key, def] of Object.entries(args)) {
1220
+ if (def.global) {
1221
+ globals[key] = def;
1222
+ }
1223
+ }
1224
+ return globals;
1225
+ }
1226
+
1227
+ /**
1228
+ * Merge global args into a subcommand's args.
1229
+ * Global args from the parent are added to the child unless the child
1230
+ * already defines an arg with the same name.
1231
+ */
1232
+ export function mergeGlobalArgs(parentGlobals: ArgsDef, childArgs: ArgsDef): ArgsDef {
1233
+ const merged: Record<string, ArgDef> = { ...childArgs };
1234
+ for (const [key, def] of Object.entries(parentGlobals)) {
1235
+ if (!(key in merged)) {
1236
+ merged[key] = def;
1237
+ }
1238
+ }
1239
+ return merged;
1240
+ }