clap-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parser.js ADDED
@@ -0,0 +1,646 @@
1
+ /**
2
+ * Argument parser - delegates core tokenizing to node:util parseArgs,
3
+ * then layers on: env fallback, type coercion, count/append actions,
4
+ * numArgs with defaultMissingValue, global args, kebab-to-camel mapping,
5
+ * subcommand detection, and default values.
6
+ *
7
+ * node:util parseArgs handles:
8
+ * --flag, --flag=value, --flag value, -f, -fvalue, -abc (combined booleans),
9
+ * -- separator, positionals
10
+ *
11
+ * We handle on top:
12
+ * conflictsWith / requires (in validation.ts),
13
+ * env variable fallback, action: 'append' (via multiple:true), action: 'count',
14
+ * numArgs with defaultMissingValue, global args merge, valueParser enum validation
15
+ * (in validation.ts), number type coercion, kebab-to-camel mapping,
16
+ * required arg validation (in validation.ts), typo suggestions (in validation.ts),
17
+ * valueDelimiter splitting, function valueParser, trailingVarArg, last,
18
+ * allowHyphenValues, allowNegativeNumbers, inferLongArgs, defaultValueIf.
19
+ */
20
+ import { parseArgs as nodeParseArgs } from 'node:util';
21
+ // ---- Helpers ----
22
+ /** Convert kebab-case to camelCase: --config-path -> configPath */
23
+ function kebabToCamel(s) {
24
+ return s.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
25
+ }
26
+ /** Get the raw argv slice (after the binary/script path). */
27
+ export function getRawArgs(argv) {
28
+ if (argv) {
29
+ return [...argv];
30
+ }
31
+ // Bun.argv includes [bun, script, ...args], same as process.argv
32
+ const source = globalThis.Bun === undefined ? process.argv : globalThis.Bun.argv;
33
+ return source.slice(2);
34
+ }
35
+ // ---- Error ----
36
+ export class CliParseError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = 'CliParseError';
40
+ }
41
+ }
42
+ // ---- Flag Lookup Maps ----
43
+ /**
44
+ * Build lookup maps from all possible flag forms to the canonical arg name.
45
+ * Used for post-processing parseArgs output back to our ArgDef keys.
46
+ * Registers both hidden aliases and visible aliases.
47
+ */
48
+ function buildFlagMaps(argsDef) {
49
+ const longMap = new Map();
50
+ const shortMap = new Map();
51
+ let hasHyphenOrNegative = false;
52
+ for (const [key, def] of Object.entries(argsDef)) {
53
+ const longName = def.long ?? key;
54
+ longMap.set(longName, { key, def, negated: false });
55
+ if (def.type === 'boolean') {
56
+ longMap.set(`no-${longName}`, { key, def, negated: true });
57
+ }
58
+ if (def.allowHyphenValues || def.allowNegativeNumbers) {
59
+ hasHyphenOrNegative = true;
60
+ }
61
+ // Register hidden aliases
62
+ if (def.alias) {
63
+ for (const alias of def.alias) {
64
+ if (alias.length === 1) {
65
+ shortMap.set(alias, { key, def });
66
+ }
67
+ else {
68
+ longMap.set(alias, { key, def, negated: false });
69
+ }
70
+ }
71
+ }
72
+ // Register visible aliases (also work as real aliases at parse time)
73
+ if (def.visibleAlias) {
74
+ for (const alias of def.visibleAlias) {
75
+ if (alias.length === 1) {
76
+ shortMap.set(alias, { key, def });
77
+ }
78
+ else {
79
+ longMap.set(alias, { key, def, negated: false });
80
+ }
81
+ }
82
+ }
83
+ if (def.short && def.short.length === 1) {
84
+ shortMap.set(def.short, { key, def });
85
+ }
86
+ }
87
+ return { longMap, shortMap, hasHyphenOrNegative };
88
+ }
89
+ // ---- Build parseArgs options config ----
90
+ /**
91
+ * Build the `options` config that node:util parseArgs expects from our ArgDef definitions.
92
+ */
93
+ function buildParseArgsOptions(argsDef) {
94
+ const options = {};
95
+ const optionalValueFlags = new Set();
96
+ for (const [key, def] of Object.entries(argsDef)) {
97
+ if (def.type === 'positional') {
98
+ continue;
99
+ }
100
+ const longName = def.long ?? key;
101
+ if (def.action === 'count') {
102
+ options[longName] = { type: 'boolean', multiple: true };
103
+ }
104
+ else if (def.type === 'boolean') {
105
+ options[longName] = { type: 'boolean' };
106
+ options[`no-${longName}`] = { type: 'boolean' };
107
+ }
108
+ else if (def.action === 'append') {
109
+ options[longName] = { type: 'string', multiple: true };
110
+ }
111
+ else {
112
+ options[longName] = { type: 'string' };
113
+ }
114
+ if (def.numArgs && def.numArgs.min === 0 && def.type !== 'boolean') {
115
+ optionalValueFlags.add(longName);
116
+ }
117
+ if (def.short && def.short.length === 1) {
118
+ options[longName].short = def.short;
119
+ }
120
+ // Register hidden and visible aliases
121
+ const registerAliases = (aliases) => {
122
+ const opt = options[longName];
123
+ for (const alias of aliases) {
124
+ if (alias.length > 1 || opt.short) {
125
+ options[alias] = { type: opt.type, multiple: opt.multiple };
126
+ }
127
+ else {
128
+ opt.short = alias;
129
+ }
130
+ }
131
+ };
132
+ if (def.alias) {
133
+ registerAliases(def.alias);
134
+ }
135
+ if (def.visibleAlias) {
136
+ registerAliases(def.visibleAlias);
137
+ }
138
+ }
139
+ // Built-in flags
140
+ options['help'] = { type: 'boolean', short: 'h' };
141
+ options['version'] = { type: 'boolean', short: 'V' };
142
+ return { options, optionalValueFlags };
143
+ }
144
+ // ---- Pre-scan / Preprocessing ----
145
+ /** Check if the next token looks like a flag (not a value). */
146
+ function isNextTokenAFlag(nextToken) {
147
+ return (nextToken === undefined ||
148
+ (nextToken.startsWith('-') && nextToken !== '-' && nextToken !== '--'));
149
+ }
150
+ /** Check if a token looks like a negative number (e.g., -1, -3.14, -0). */
151
+ function looksLikeNegativeNumber(token) {
152
+ return /^-\d/.test(token);
153
+ }
154
+ /** Try to match a long flag token as an optional-value flag without a value. */
155
+ function tryStripOptionalLong(token, nextToken, optionalValueFlags, longMap) {
156
+ if (!token.startsWith('--') || token.length <= 2 || token.includes('=')) {
157
+ return undefined;
158
+ }
159
+ const flagName = token.slice(2);
160
+ if (!optionalValueFlags.has(flagName) || !isNextTokenAFlag(nextToken)) {
161
+ return undefined;
162
+ }
163
+ const entry = longMap.get(flagName);
164
+ if (!entry) {
165
+ return undefined;
166
+ }
167
+ return { key: entry.key, defaultVal: entry.def.defaultMissingValue ?? true };
168
+ }
169
+ /** Try to match a short flag token as an optional-value flag without a value. */
170
+ function tryStripOptionalShort(token, nextToken, optionalValueFlags, shortMap) {
171
+ if (!token.startsWith('-') || token.length !== 2 || token[1] === '-') {
172
+ return undefined;
173
+ }
174
+ const c = token[1];
175
+ const entry = shortMap.get(c);
176
+ if (!entry) {
177
+ return undefined;
178
+ }
179
+ const entryLong = entry.def.long ?? entry.key;
180
+ if (!optionalValueFlags.has(entryLong) || !isNextTokenAFlag(nextToken)) {
181
+ return undefined;
182
+ }
183
+ return { key: entry.key, defaultVal: entry.def.defaultMissingValue ?? true };
184
+ }
185
+ /**
186
+ * Resolve a flag token to its key and ArgDef, checking both long and short maps.
187
+ * Returns the canonical key (used as fallback long name) and the def.
188
+ */
189
+ function resolveFlag(token, longMap, shortMap) {
190
+ if (token.startsWith('--') && token.length > 2) {
191
+ const flagName = token.includes('=') ? token.slice(2, token.indexOf('=')) : token.slice(2);
192
+ const entry = longMap.get(flagName);
193
+ return entry ? { key: entry.key, def: entry.def } : undefined;
194
+ }
195
+ if (token.startsWith('-') && token.length === 2 && token[1] !== '-') {
196
+ return shortMap.get(token[1]) ?? undefined;
197
+ }
198
+ return undefined;
199
+ }
200
+ /**
201
+ * Pre-process raw args before passing to node:util parseArgs.
202
+ * Handles:
203
+ * 1. Optional-value flags (strip bare flags, record defaults)
204
+ * 2. allowHyphenValues / allowNegativeNumbers (rewrite --flag -value to --flag=-value)
205
+ */
206
+ function preprocessArgs(rawArgs, optionalValueFlags, longMap, shortMap, hasHyphenOrNegative) {
207
+ if (optionalValueFlags.size === 0 && !hasHyphenOrNegative) {
208
+ return { processedArgs: [...rawArgs], optionalDefaults: new Map() };
209
+ }
210
+ const optionalDefaults = new Map();
211
+ const processedArgs = [];
212
+ let stopParsing = false;
213
+ for (let i = 0; i < rawArgs.length; i++) {
214
+ const token = rawArgs[i];
215
+ if (stopParsing || token === '--') {
216
+ if (token === '--') {
217
+ stopParsing = true;
218
+ }
219
+ processedArgs.push(token);
220
+ continue;
221
+ }
222
+ const nextToken = rawArgs[i + 1];
223
+ // Optional-value flag handling (long)
224
+ const stripped = tryStripOptionalLong(token, nextToken, optionalValueFlags, longMap);
225
+ if (stripped) {
226
+ optionalDefaults.set(stripped.key, stripped.defaultVal);
227
+ continue;
228
+ }
229
+ // Optional-value flag handling (short)
230
+ const strippedShort = tryStripOptionalShort(token, nextToken, optionalValueFlags, shortMap);
231
+ if (strippedShort) {
232
+ optionalDefaults.set(strippedShort.key, strippedShort.defaultVal);
233
+ continue;
234
+ }
235
+ // allowHyphenValues / allowNegativeNumbers:
236
+ // If this token is a known flag and the next token starts with - but the flag's
237
+ // def allows hyphen values or negative numbers, rewrite to --flag=-value
238
+ if (nextToken && nextToken.startsWith('-') && nextToken !== '-' && nextToken !== '--') {
239
+ const resolved = resolveFlag(token, longMap, shortMap);
240
+ if (resolved && resolved.def.type !== 'boolean' && resolved.def.type !== 'positional') {
241
+ const shouldRewrite = resolved.def.allowHyphenValues ||
242
+ (resolved.def.allowNegativeNumbers && looksLikeNegativeNumber(nextToken));
243
+ if (shouldRewrite && !token.includes('=')) {
244
+ // Rewrite to --longName=value form so node:util parseArgs treats it as a value
245
+ const longName = resolved.def.long ?? resolved.key;
246
+ processedArgs.push(`--${longName}=${nextToken}`);
247
+ i++; // skip the next token (consumed as value)
248
+ continue;
249
+ }
250
+ }
251
+ }
252
+ processedArgs.push(token);
253
+ }
254
+ return { processedArgs, optionalDefaults };
255
+ }
256
+ // ---- Coerce Value ----
257
+ function coerceValue(value, def, argName) {
258
+ // If a function valueParser is defined, use it for coercion
259
+ if (typeof def.valueParser === 'function') {
260
+ try {
261
+ const parsed = def.valueParser(value);
262
+ // Ensure the result is a valid ParseResult value type
263
+ if (typeof parsed === 'string' ||
264
+ typeof parsed === 'number' ||
265
+ typeof parsed === 'boolean') {
266
+ return parsed;
267
+ }
268
+ return String(parsed);
269
+ }
270
+ catch (error) {
271
+ const msg = error instanceof Error ? error.message : String(error);
272
+ throw new CliParseError(`invalid value '${value}' for '${argName}': ${msg}`);
273
+ }
274
+ }
275
+ switch (def.type) {
276
+ case 'boolean': {
277
+ const lower = value.toLowerCase();
278
+ return lower === 'true' || lower === '1' || lower === 'yes';
279
+ }
280
+ case 'number': {
281
+ const num = value.includes('.') ? Number.parseFloat(value) : Number.parseInt(value, 10);
282
+ if (Number.isNaN(num) || !Number.isFinite(num)) {
283
+ throw new CliParseError(`invalid value '${value}' for '${argName}': expected a finite number`);
284
+ }
285
+ return num;
286
+ }
287
+ case 'enum':
288
+ case 'string':
289
+ case 'positional': {
290
+ return value;
291
+ }
292
+ }
293
+ }
294
+ // ---- Subcommand Detection ----
295
+ /**
296
+ * Build a lookup map from subcommand names and aliases to canonical names.
297
+ * Used for O(1) subcommand detection.
298
+ */
299
+ function buildSubcommandMap(subCommands) {
300
+ const map = new Map();
301
+ for (const [name, def] of Object.entries(subCommands)) {
302
+ map.set(name, name);
303
+ if (def.meta.aliases) {
304
+ for (const alias of def.meta.aliases) {
305
+ map.set(alias, name);
306
+ }
307
+ }
308
+ }
309
+ return map;
310
+ }
311
+ /**
312
+ * Scan positionals for a subcommand name or alias using O(1) map lookup.
313
+ * Returns the canonical name and the index in the positionals array.
314
+ */
315
+ function detectSubcommand(positionals, command) {
316
+ if (!command.subCommands) {
317
+ return undefined;
318
+ }
319
+ const subMap = buildSubcommandMap(command.subCommands);
320
+ for (let i = 0; i < positionals.length; i++) {
321
+ const canonical = subMap.get(positionals[i]);
322
+ if (canonical) {
323
+ return { name: canonical, index: i };
324
+ }
325
+ }
326
+ return undefined;
327
+ }
328
+ // ---- Separate Rest Args ----
329
+ /**
330
+ * parseArgs lumps everything (before and after --) into its `positionals` array.
331
+ * We need to split them into positionals (before --) and rest (after --).
332
+ */
333
+ function splitPositionalsAndRest(rawArgs, parseArgsPositionals) {
334
+ let dashDashIdx = -1;
335
+ for (let i = 0; i < rawArgs.length; i++) {
336
+ if (rawArgs[i] === '--') {
337
+ dashDashIdx = i;
338
+ break;
339
+ }
340
+ }
341
+ if (dashDashIdx === -1) {
342
+ return { positionals: [...parseArgsPositionals], rest: [] };
343
+ }
344
+ const restCount = rawArgs.length - dashDashIdx - 1;
345
+ const totalPositionals = parseArgsPositionals.length;
346
+ const beforeCount = totalPositionals - restCount;
347
+ return {
348
+ positionals: parseArgsPositionals.slice(0, Math.max(0, beforeCount)),
349
+ rest: parseArgsPositionals.slice(Math.max(0, beforeCount)),
350
+ };
351
+ }
352
+ // ---- Infer Long Args ----
353
+ /**
354
+ * Try to resolve an unknown long flag by prefix matching.
355
+ * Returns the matched entry if exactly one match, undefined otherwise.
356
+ */
357
+ function tryInferLongArg(flagName, longMap) {
358
+ const matches = [];
359
+ for (const [name, entry] of longMap) {
360
+ if (name.startsWith(flagName) && !entry.negated) {
361
+ matches.push(entry);
362
+ }
363
+ }
364
+ return matches.length === 1 ? matches[0] : undefined;
365
+ }
366
+ // ---- Value Delimiter Splitting ----
367
+ /**
368
+ * Split a string value by the delimiter and return as array.
369
+ * Applied after initial parsing for args with valueDelimiter.
370
+ */
371
+ function splitByDelimiter(value, delimiter) {
372
+ if (Array.isArray(value)) {
373
+ const result = [];
374
+ for (const v of value) {
375
+ result.push(...v.split(delimiter));
376
+ }
377
+ return result;
378
+ }
379
+ return value.split(delimiter);
380
+ }
381
+ // ---- Main Parser ----
382
+ /**
383
+ * Parse raw argument tokens against a command definition.
384
+ *
385
+ * Uses node:util parseArgs for core tokenizing, then layers on all clap-ts features.
386
+ */
387
+ export function parseArgs(rawArgs, command) {
388
+ const argsDef = command.args ?? {};
389
+ const inferLong = command.meta.inferLongArgs === true;
390
+ // Build parseArgs config from our ArgDef definitions
391
+ const { options, optionalValueFlags } = buildParseArgsOptions(argsDef);
392
+ // Build flag maps once and reuse throughout parsing
393
+ const { longMap, shortMap, hasHyphenOrNegative } = buildFlagMaps(argsDef);
394
+ // Pre-process: handle optional-value flags, allowHyphenValues, allowNegativeNumbers
395
+ const { processedArgs, optionalDefaults } = preprocessArgs(rawArgs, optionalValueFlags, longMap, shortMap, hasHyphenOrNegative);
396
+ // Run node:util parseArgs (strict: false so unknown flags don't throw)
397
+ let parsed;
398
+ try {
399
+ parsed = nodeParseArgs({
400
+ args: processedArgs,
401
+ options,
402
+ allowPositionals: true,
403
+ strict: false,
404
+ });
405
+ }
406
+ catch (error) {
407
+ const msg = error instanceof Error ? error.message : String(error);
408
+ throw new CliParseError(msg);
409
+ }
410
+ const { values, positionals: rawPositionals } = parsed;
411
+ const result = {};
412
+ const unknown = [];
413
+ const explicitlySet = new Set();
414
+ // Extract help/version and detect short vs long help
415
+ const helpRequested = values['help'] === true;
416
+ const helpIsShort = helpRequested && rawArgs.some((a) => a === '-h');
417
+ const versionRequested = values['version'] === true;
418
+ // Process each parsed value back through our ArgDef system
419
+ for (const [parsedKey, rawValue] of Object.entries(values)) {
420
+ if (parsedKey === 'help' || parsedKey === 'version') {
421
+ continue;
422
+ }
423
+ // Handle --no-<flag> negation
424
+ if (parsedKey.startsWith('no-') && rawValue === true) {
425
+ const positiveName = parsedKey.slice(3);
426
+ const entry = longMap.get(positiveName);
427
+ if (entry && entry.def.type === 'boolean') {
428
+ result[entry.key] = false;
429
+ explicitlySet.add(entry.key);
430
+ continue;
431
+ }
432
+ }
433
+ // Look up canonical key via long map
434
+ const entry = longMap.get(parsedKey);
435
+ if (entry) {
436
+ applyValue(entry.key, entry.def, rawValue, result, explicitlySet);
437
+ continue;
438
+ }
439
+ // Check short map
440
+ const shortEntry = shortMap.get(parsedKey);
441
+ if (shortEntry) {
442
+ applyValue(shortEntry.key, shortEntry.def, rawValue, result, explicitlySet);
443
+ continue;
444
+ }
445
+ // inferLongArgs: try prefix matching on unknown long flags
446
+ if (inferLong) {
447
+ const inferred = tryInferLongArg(parsedKey, longMap);
448
+ if (inferred) {
449
+ applyValue(inferred.key, inferred.def, rawValue, result, explicitlySet);
450
+ continue;
451
+ }
452
+ }
453
+ unknown.push(`--${parsedKey}`);
454
+ }
455
+ // Apply optional-value defaults from pre-scan
456
+ for (const [key, defaultVal] of optionalDefaults) {
457
+ if (!explicitlySet.has(key)) {
458
+ result[key] = defaultVal;
459
+ explicitlySet.add(key);
460
+ }
461
+ }
462
+ // Split positionals from rest (tokens after --)
463
+ const { positionals: allPositionals, rest } = splitPositionalsAndRest(rawArgs, rawPositionals);
464
+ // Detect subcommand in positionals
465
+ const subCmdResult = detectSubcommand(allPositionals, command);
466
+ const subCommand = subCmdResult?.name;
467
+ // Remove subcommand token from positionals
468
+ const positionals = [];
469
+ for (let i = 0; i < allPositionals.length; i++) {
470
+ if (subCmdResult && i === subCmdResult.index) {
471
+ continue;
472
+ }
473
+ positionals.push(allPositionals[i]);
474
+ }
475
+ // Assign positionals to positional arg defs
476
+ const positionalDefs = [];
477
+ for (const [key, def] of Object.entries(argsDef)) {
478
+ if (def.type === 'positional') {
479
+ positionalDefs.push({ key, def });
480
+ }
481
+ }
482
+ for (let p = 0; p < positionalDefs.length; p++) {
483
+ const { key, def } = positionalDefs[p];
484
+ // last: this positional only gets values from rest (after --)
485
+ if (def.last) {
486
+ if (rest.length > 0) {
487
+ result[key] = rest[0];
488
+ explicitlySet.add(key);
489
+ }
490
+ continue;
491
+ }
492
+ // trailingVarArg: consume all remaining positionals as an array
493
+ if (def.trailingVarArg) {
494
+ if (p < positionals.length) {
495
+ result[key] = positionals.slice(p);
496
+ explicitlySet.add(key);
497
+ }
498
+ break; // no more positional defs to process
499
+ }
500
+ // Normal positional assignment
501
+ if (p < positionals.length) {
502
+ result[key] = coerceValue(positionals[p], def, key);
503
+ explicitlySet.add(key);
504
+ }
505
+ }
506
+ // Single pass: valueDelimiter splitting, env fallback, conditional defaults, static defaults
507
+ const argsDefEntries = Object.entries(argsDef);
508
+ for (let a = 0; a < argsDefEntries.length; a++) {
509
+ const [key, def] = argsDefEntries[a];
510
+ if (explicitlySet.has(key)) {
511
+ // Apply valueDelimiter splitting for explicitly set args
512
+ if (def.valueDelimiter) {
513
+ const value = result[key];
514
+ if (value !== undefined && (typeof value === 'string' || Array.isArray(value))) {
515
+ result[key] = splitByDelimiter(value, def.valueDelimiter);
516
+ }
517
+ }
518
+ continue;
519
+ }
520
+ // Env var fallback
521
+ if (def.env) {
522
+ const envValue = globalThis.Bun === undefined
523
+ ? process.env[def.env]
524
+ : globalThis.Bun.env[def.env];
525
+ if (envValue !== undefined && envValue !== '') {
526
+ result[key] = def.valueDelimiter
527
+ ? envValue.split(def.valueDelimiter)
528
+ : coerceValue(envValue, def, `env:${def.env}`);
529
+ explicitlySet.add(key);
530
+ continue;
531
+ }
532
+ }
533
+ // Conditional default (defaultValueIf)
534
+ if (def.defaultValueIf) {
535
+ const [otherKey, otherValue, conditionalDefault] = def.defaultValueIf;
536
+ if (String(result[otherKey]) === String(otherValue)) {
537
+ result[key] = conditionalDefault;
538
+ continue;
539
+ }
540
+ }
541
+ // Static default
542
+ if (def.default !== undefined) {
543
+ if (Array.isArray(def.default)) {
544
+ result[key] = [...def.default];
545
+ }
546
+ else if (typeof def.default === 'string' ||
547
+ typeof def.default === 'number' ||
548
+ typeof def.default === 'boolean') {
549
+ result[key] = def.default;
550
+ }
551
+ }
552
+ }
553
+ // Convert keys to camelCase for the result
554
+ const camelResult = {};
555
+ for (const [key, value] of Object.entries(result)) {
556
+ const camelKey = kebabToCamel(key);
557
+ camelResult[camelKey] = value;
558
+ if (key !== camelKey) {
559
+ camelResult[key] = value;
560
+ }
561
+ }
562
+ return {
563
+ args: camelResult,
564
+ positionals,
565
+ rest,
566
+ subCommand,
567
+ helpRequested,
568
+ helpIsShort,
569
+ versionRequested,
570
+ unknown,
571
+ explicitlySet,
572
+ };
573
+ }
574
+ // ---- Value Application Helper ----
575
+ /**
576
+ * Apply a raw value from parseArgs into the result, handling count, append,
577
+ * number coercion, boolean conversion, and function valueParser.
578
+ */
579
+ function applyValue(key, def, rawValue, result, explicitlySet) {
580
+ if (def.action === 'count') {
581
+ if (Array.isArray(rawValue)) {
582
+ result[key] = rawValue.length;
583
+ }
584
+ else {
585
+ result[key] = rawValue === true ? 1 : 0;
586
+ }
587
+ explicitlySet.add(key);
588
+ return;
589
+ }
590
+ if (def.type === 'boolean') {
591
+ result[key] = rawValue === true;
592
+ explicitlySet.add(key);
593
+ return;
594
+ }
595
+ if (def.action === 'append') {
596
+ // node:util parseArgs returns string[] for multiple:true string options
597
+ if (Array.isArray(rawValue)) {
598
+ result[key] = rawValue;
599
+ }
600
+ else if (typeof rawValue === 'string') {
601
+ result[key] = [rawValue];
602
+ }
603
+ explicitlySet.add(key);
604
+ return;
605
+ }
606
+ // Single string/number/enum value
607
+ if (typeof rawValue === 'string') {
608
+ result[key] = coerceValue(rawValue, def, `--${def.long ?? key}`);
609
+ explicitlySet.add(key);
610
+ return;
611
+ }
612
+ // Boolean value for a string-typed arg (edge case from parseArgs)
613
+ if (typeof rawValue === 'boolean' && rawValue) {
614
+ result[key] = def.defaultMissingValue ?? true;
615
+ explicitlySet.add(key);
616
+ }
617
+ }
618
+ // ---- Global Args ----
619
+ /**
620
+ * Collect global args from a command.
621
+ * Returns a merged ArgsDef of all global args.
622
+ */
623
+ export function collectGlobalArgs(command) {
624
+ const globals = {};
625
+ const args = command.args ?? {};
626
+ for (const [key, def] of Object.entries(args)) {
627
+ if (def.global) {
628
+ globals[key] = def;
629
+ }
630
+ }
631
+ return globals;
632
+ }
633
+ /**
634
+ * Merge global args into a subcommand's args.
635
+ * Global args from the parent are added to the child unless the child
636
+ * already defines an arg with the same name.
637
+ */
638
+ export function mergeGlobalArgs(parentGlobals, childArgs) {
639
+ const merged = { ...childArgs };
640
+ for (const [key, def] of Object.entries(parentGlobals)) {
641
+ if (!(key in merged)) {
642
+ merged[key] = def;
643
+ }
644
+ }
645
+ return merged;
646
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Command runner - entry point for CLI execution.
3
+ * Handles subcommand resolution, lifecycle hooks, error handling.
4
+ * Supports inferSubcommands, subcommandRequired, allowExternalSubcommands,
5
+ * argsConflictsWithSubcommands, argRequiredElseHelp, and custom styles.
6
+ */
7
+ import type { ArgDef, ArgsDef, CommandDef, ParsedArgs, RunOptions } from './types.js';
8
+ /**
9
+ * Define a command with full type inference on arguments.
10
+ * This is the primary API for creating commands.
11
+ *
12
+ * ```ts
13
+ * const cmd = defineCommand({
14
+ * meta: { name: 'my-tool', version: '1.0.0', description: 'My tool' },
15
+ * args: {
16
+ * verbose: { type: 'boolean', short: 'v', description: 'Verbose output' },
17
+ * port: { type: 'number', short: 'p', default: 3000, description: 'Port' },
18
+ * },
19
+ * run({ args }) {
20
+ * console.log(args.verbose, args.port);
21
+ * },
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function defineCommand<const T extends ArgsDef>(def: CommandDef<T>): CommandDef<T>;
26
+ /**
27
+ * Define a reusable argument group with full type inference.
28
+ * Use this for shared args that are spread into multiple commands.
29
+ *
30
+ * ```ts
31
+ * const envArgs = defineArgs({
32
+ * env: { type: 'string', valueParser: ['dev', 'staging', 'prod'] },
33
+ * dev: { type: 'boolean', conflictsWith: ['env', 'staging', 'prod'] },
34
+ * });
35
+ * ```
36
+ */
37
+ export declare function defineArgs<const T extends ArgsDef>(args: T): T;
38
+ /**
39
+ * Define a single argument with full type inference.
40
+ *
41
+ * ```ts
42
+ * const portArg = defineArg({ type: 'number', short: 'p', default: 3003 });
43
+ * ```
44
+ */
45
+ export declare function defineArg<const T extends ArgDef>(arg: T): T;
46
+ /**
47
+ * Run a specific command with pre-parsed arguments.
48
+ * Executes the setup -> run -> cleanup lifecycle.
49
+ */
50
+ export declare function runCommand<T extends ArgsDef>(command: CommandDef<T>, args: ParsedArgs<T>, rawArgs?: readonly string[], subCommand?: string): Promise<void>;
51
+ /**
52
+ * Main entry point for CLI applications.
53
+ * Parses args, resolves subcommands, validates, and runs.
54
+ *
55
+ * ```ts
56
+ * const main = defineCommand({ ... });
57
+ * runMain(main);
58
+ * ```
59
+ */
60
+ export declare function runMain(rootCommand: CommandDef<any>, opts?: RunOptions): Promise<void>;