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
@@ -0,0 +1,627 @@
1
+ /**
2
+ * Parser tests - comprehensive coverage of clap-ts argument parsing.
3
+ */
4
+
5
+ import { describe, test, expect, afterEach } from 'bun:test';
6
+ import { parseArgs, CliParseError } from '../parser.js';
7
+ import type { CommandDef, ArgsDef } from '../types.js';
8
+
9
+ /** Helper to create a CommandDef from ArgsDef for testing. */
10
+ function cmd(args: ArgsDef, subCommands?: Record<string, CommandDef>): CommandDef {
11
+ return { meta: { name: 'test' }, args, subCommands };
12
+ }
13
+
14
+ // ---- Basic Parsing ----
15
+
16
+ describe('basic parsing', () => {
17
+ test('boolean flags', () => {
18
+ const result = parseArgs(['--verbose'], cmd({ verbose: { type: 'boolean' } }));
19
+ expect(result.args.verbose).toBe(true);
20
+ });
21
+
22
+ test('boolean flag absent defaults to undefined', () => {
23
+ const result = parseArgs([], cmd({ verbose: { type: 'boolean' } }));
24
+ expect(result.args.verbose).toBeUndefined();
25
+ });
26
+
27
+ test('--no-<flag> negation', () => {
28
+ const result = parseArgs(['--no-verbose'], cmd({ verbose: { type: 'boolean' } }));
29
+ expect(result.args.verbose).toBe(false);
30
+ });
31
+
32
+ test('string args', () => {
33
+ const result = parseArgs(['--name', 'alice'], cmd({ name: { type: 'string' } }));
34
+ expect(result.args.name).toBe('alice');
35
+ });
36
+
37
+ test('string args with = syntax', () => {
38
+ const result = parseArgs(['--name=alice'], cmd({ name: { type: 'string' } }));
39
+ expect(result.args.name).toBe('alice');
40
+ });
41
+
42
+ test('number args', () => {
43
+ const result = parseArgs(['--port', '3003'], cmd({ port: { type: 'number' } }));
44
+ expect(result.args.port).toBe(3003);
45
+ });
46
+
47
+ test('number args with float', () => {
48
+ const result = parseArgs(['--rate', '1.5'], cmd({ rate: { type: 'number' } }));
49
+ expect(result.args.rate).toBe(1.5);
50
+ });
51
+
52
+ test('number args rejects non-numeric', () => {
53
+ expect(() => {
54
+ parseArgs(['--port', 'abc'], cmd({ port: { type: 'number' } }));
55
+ }).toThrow(CliParseError);
56
+ });
57
+
58
+ test('short flags', () => {
59
+ const result = parseArgs(['-v'], cmd({ verbose: { type: 'boolean', short: 'v' } }));
60
+ expect(result.args.verbose).toBe(true);
61
+ });
62
+
63
+ test('short flags with value', () => {
64
+ const result = parseArgs(['-p', '3003'], cmd({ port: { type: 'number', short: 'p' } }));
65
+ expect(result.args.port).toBe(3003);
66
+ });
67
+
68
+ test('combined short boolean flags', () => {
69
+ const result = parseArgs(
70
+ ['-vd'],
71
+ cmd({
72
+ verbose: { type: 'boolean', short: 'v' },
73
+ debug: { type: 'boolean', short: 'd' },
74
+ }),
75
+ );
76
+ expect(result.args.verbose).toBe(true);
77
+ expect(result.args.debug).toBe(true);
78
+ });
79
+ });
80
+
81
+ // ---- Default Values ----
82
+
83
+ describe('defaults', () => {
84
+ test('default value applied when arg not provided', () => {
85
+ const result = parseArgs([], cmd({ port: { type: 'number', default: 3000 } }));
86
+ expect(result.args.port).toBe(3000);
87
+ });
88
+
89
+ test('default string value', () => {
90
+ const result = parseArgs([], cmd({ env: { type: 'string', default: 'dev' } }));
91
+ expect(result.args.env).toBe('dev');
92
+ });
93
+
94
+ test('default boolean value', () => {
95
+ const result = parseArgs([], cmd({ verbose: { type: 'boolean', default: false } }));
96
+ expect(result.args.verbose).toBe(false);
97
+ });
98
+
99
+ test('default array value for append', () => {
100
+ const result = parseArgs(
101
+ [],
102
+ cmd({ headers: { type: 'string', action: 'append', default: ['X-Default: 1'] } }),
103
+ );
104
+ expect(result.args.headers).toEqual(['X-Default: 1']);
105
+ });
106
+
107
+ test('explicit value overrides default', () => {
108
+ const result = parseArgs(['--port', '8080'], cmd({ port: { type: 'number', default: 3000 } }));
109
+ expect(result.args.port).toBe(8080);
110
+ });
111
+ });
112
+
113
+ // ---- Enum Validation (valueParser) ----
114
+
115
+ describe('valueParser (enum validation)', () => {
116
+ test('valid value passes', () => {
117
+ const result = parseArgs(
118
+ ['--env', 'dev'],
119
+ cmd({ env: { type: 'string', valueParser: ['dev', 'staging', 'prod'] } }),
120
+ );
121
+ expect(result.args.env).toBe('dev');
122
+ });
123
+
124
+ test('all valid values pass', () => {
125
+ for (const val of ['dev', 'staging', 'prod']) {
126
+ const result = parseArgs(
127
+ ['--env', val],
128
+ cmd({ env: { type: 'string', valueParser: ['dev', 'staging', 'prod'] } }),
129
+ );
130
+ expect(result.args.env).toBe(val);
131
+ }
132
+ });
133
+ });
134
+
135
+ // ---- Env Fallback ----
136
+
137
+ describe('env fallback', () => {
138
+ const originalEnv = process.env['TEST_PORT'];
139
+
140
+ afterEach(() => {
141
+ if (originalEnv === undefined) {
142
+ delete process.env['TEST_PORT'];
143
+ } else {
144
+ process.env['TEST_PORT'] = originalEnv;
145
+ }
146
+ });
147
+
148
+ test('falls back to env var when arg not on CLI', () => {
149
+ process.env['TEST_PORT'] = '9090';
150
+ const result = parseArgs([], cmd({ port: { type: 'number', env: 'TEST_PORT' } }));
151
+ expect(result.args.port).toBe(9090);
152
+ });
153
+
154
+ test('CLI arg takes precedence over env var', () => {
155
+ process.env['TEST_PORT'] = '9090';
156
+ const result = parseArgs(
157
+ ['--port', '3003'],
158
+ cmd({ port: { type: 'number', env: 'TEST_PORT' } }),
159
+ );
160
+ expect(result.args.port).toBe(3003);
161
+ });
162
+
163
+ test('empty env var is ignored', () => {
164
+ process.env['TEST_PORT'] = '';
165
+ const result = parseArgs([], cmd({ port: { type: 'number', env: 'TEST_PORT' } }));
166
+ expect(result.args.port).toBeUndefined();
167
+ });
168
+ });
169
+
170
+ // ---- numArgs + defaultMissingValue ----
171
+
172
+ describe('numArgs + defaultMissingValue', () => {
173
+ test('flag present without value uses defaultMissingValue', () => {
174
+ const result = parseArgs(
175
+ ['--level'],
176
+ cmd({
177
+ level: {
178
+ type: 'string',
179
+ numArgs: { min: 0, max: 1 },
180
+ defaultMissingValue: 'info',
181
+ },
182
+ }),
183
+ );
184
+ expect(result.args.level).toBe('info');
185
+ });
186
+
187
+ test('flag present with value uses the value', () => {
188
+ const result = parseArgs(
189
+ ['--level', 'debug'],
190
+ cmd({
191
+ level: {
192
+ type: 'string',
193
+ numArgs: { min: 0, max: 1 },
194
+ defaultMissingValue: 'info',
195
+ },
196
+ }),
197
+ );
198
+ expect(result.args.level).toBe('debug');
199
+ });
200
+ });
201
+
202
+ // ---- action: 'append' ----
203
+
204
+ describe("action: 'append'", () => {
205
+ test('multiple values collected into array', () => {
206
+ const result = parseArgs(
207
+ ['-H', 'X-A: 1', '-H', 'X-B: 2'],
208
+ cmd({ header: { type: 'string', short: 'H', action: 'append' } }),
209
+ );
210
+ expect(result.args.header).toEqual(['X-A: 1', 'X-B: 2']);
211
+ });
212
+
213
+ test('single value becomes array', () => {
214
+ const result = parseArgs(
215
+ ['-H', 'X-A: 1'],
216
+ cmd({ header: { type: 'string', short: 'H', action: 'append' } }),
217
+ );
218
+ expect(result.args.header).toEqual(['X-A: 1']);
219
+ });
220
+ });
221
+
222
+ // ---- action: 'count' ----
223
+
224
+ describe("action: 'count'", () => {
225
+ test('multiple flags counted', () => {
226
+ const result = parseArgs(
227
+ ['-v', '-v', '-v'],
228
+ cmd({ verbose: { type: 'boolean', short: 'v', action: 'count' } }),
229
+ );
230
+ expect(result.args.verbose).toBe(3);
231
+ });
232
+
233
+ test('single flag counts to 1', () => {
234
+ const result = parseArgs(
235
+ ['-v'],
236
+ cmd({ verbose: { type: 'boolean', short: 'v', action: 'count' } }),
237
+ );
238
+ expect(result.args.verbose).toBe(1);
239
+ });
240
+ });
241
+
242
+ // ---- Global Args ----
243
+
244
+ describe('global args', () => {
245
+ test('global args merged into subcommand parsing', () => {
246
+ const parentCmd = cmd(
247
+ {
248
+ verbose: { type: 'boolean', short: 'v', global: true },
249
+ },
250
+ {
251
+ serve: {
252
+ meta: { name: 'serve' },
253
+ args: { port: { type: 'number', short: 'p' } },
254
+ },
255
+ },
256
+ );
257
+
258
+ // Global args are merged by the runner, but we can test mergeGlobalArgs directly
259
+ const { collectGlobalArgs, mergeGlobalArgs } = require('../parser.js');
260
+ const globals = collectGlobalArgs(parentCmd);
261
+ expect(globals.verbose).toBeDefined();
262
+
263
+ const childArgs = parentCmd.subCommands!['serve']!.args!;
264
+ const merged = mergeGlobalArgs(globals, childArgs);
265
+ expect(merged.verbose).toBeDefined();
266
+ expect(merged.port).toBeDefined();
267
+ });
268
+ });
269
+
270
+ // ---- Positionals ----
271
+
272
+ describe('positionals', () => {
273
+ test('positional args parsed in order', () => {
274
+ const result = parseArgs(
275
+ ['input.txt', 'output.txt'],
276
+ cmd({
277
+ source: { type: 'positional', valueName: 'SOURCE' },
278
+ dest: { type: 'positional', valueName: 'DEST' },
279
+ }),
280
+ );
281
+ expect(result.args.source).toBe('input.txt');
282
+ expect(result.args.dest).toBe('output.txt');
283
+ });
284
+
285
+ test('positional mixed with flags', () => {
286
+ const result = parseArgs(
287
+ ['--verbose', 'input.txt'],
288
+ cmd({
289
+ verbose: { type: 'boolean' },
290
+ file: { type: 'positional' },
291
+ }),
292
+ );
293
+ expect(result.args.verbose).toBe(true);
294
+ expect(result.args.file).toBe('input.txt');
295
+ });
296
+ });
297
+
298
+ // ---- -- Separator ----
299
+
300
+ describe('-- separator', () => {
301
+ test('everything after -- goes to rest', () => {
302
+ const result = parseArgs(
303
+ ['--verbose', '--', 'extra1', 'extra2'],
304
+ cmd({ verbose: { type: 'boolean' } }),
305
+ );
306
+ expect(result.args.verbose).toBe(true);
307
+ expect(result.rest).toEqual(['extra1', 'extra2']);
308
+ });
309
+
310
+ test('empty rest when no -- present', () => {
311
+ const result = parseArgs(['--verbose'], cmd({ verbose: { type: 'boolean' } }));
312
+ expect(result.rest).toEqual([]);
313
+ });
314
+ });
315
+
316
+ // ---- Kebab to CamelCase ----
317
+
318
+ describe('kebab-to-camelCase', () => {
319
+ test('--config-path accessible as configPath', () => {
320
+ const result = parseArgs(
321
+ ['--config-path', './config.yaml'],
322
+ cmd({ 'config-path': { type: 'string' } }),
323
+ );
324
+ expect(result.args['configPath']).toBe('./config.yaml');
325
+ // Also accessible with original key
326
+ expect(result.args['config-path']).toBe('./config.yaml');
327
+ });
328
+
329
+ test('--mocks-dir accessible as mocksDir', () => {
330
+ const result = parseArgs(['--mocks-dir', './mocks'], cmd({ 'mocks-dir': { type: 'string' } }));
331
+ expect(result.args['mocksDir']).toBe('./mocks');
332
+ });
333
+ });
334
+
335
+ // ---- type: 'number' ----
336
+
337
+ describe("type: 'number'", () => {
338
+ test('auto parseInt for integer strings', () => {
339
+ const result = parseArgs(['--port', '3003'], cmd({ port: { type: 'number' } }));
340
+ expect(result.args.port).toBe(3003);
341
+ expect(typeof result.args.port).toBe('number');
342
+ });
343
+
344
+ test('auto parseFloat for decimal strings', () => {
345
+ const result = parseArgs(['--rate', '0.5'], cmd({ rate: { type: 'number' } }));
346
+ expect(result.args.rate).toBe(0.5);
347
+ expect(typeof result.args.rate).toBe('number');
348
+ });
349
+ });
350
+
351
+ // ---- Unknown Args (strict: false) ----
352
+
353
+ describe('unknown args', () => {
354
+ test('unknown args should NOT throw (collected in unknown array)', () => {
355
+ const result = parseArgs(
356
+ ['--verbose', '--unknown-flag'],
357
+ cmd({ verbose: { type: 'boolean' } }),
358
+ );
359
+ expect(result.args.verbose).toBe(true);
360
+ expect(result.unknown).toContain('--unknown-flag');
361
+ });
362
+
363
+ test('multiple unknown args collected', () => {
364
+ const result = parseArgs(['--foo', '--bar'], cmd({}));
365
+ expect(result.unknown).toContain('--foo');
366
+ expect(result.unknown).toContain('--bar');
367
+ });
368
+ });
369
+
370
+ // ---- Long name different from key ----
371
+
372
+ describe('long name different from key', () => {
373
+ test('arg with long name different from key is recognized', () => {
374
+ const result = parseArgs(
375
+ ['--port', '3003'],
376
+ cmd({ portNum: { type: 'number', long: 'port' } }),
377
+ );
378
+ expect(result.args.portNum).toBe(3003);
379
+ });
380
+
381
+ test('arg with long name and short flag', () => {
382
+ const result = parseArgs(
383
+ ['-p', '3003'],
384
+ cmd({ portNum: { type: 'number', long: 'port', short: 'p' } }),
385
+ );
386
+ expect(result.args.portNum).toBe(3003);
387
+ });
388
+ });
389
+
390
+ // ---- Help and Version ----
391
+
392
+ describe('help and version', () => {
393
+ test('--help sets helpRequested', () => {
394
+ const result = parseArgs(['--help'], cmd({}));
395
+ expect(result.helpRequested).toBe(true);
396
+ });
397
+
398
+ test('-h sets helpRequested', () => {
399
+ const result = parseArgs(['-h'], cmd({}));
400
+ expect(result.helpRequested).toBe(true);
401
+ });
402
+
403
+ test('--version sets versionRequested', () => {
404
+ const versioned: CommandDef = { meta: { name: 'test', version: '1.0.0' } };
405
+ expect(parseArgs(['--version'], versioned).versionRequested).toBe(true);
406
+ });
407
+
408
+ test('-V sets versionRequested', () => {
409
+ const versioned: CommandDef = { meta: { name: 'test', version: '1.0.0' } };
410
+ expect(parseArgs(['-V'], versioned).versionRequested).toBe(true);
411
+ });
412
+
413
+ test('no --version flag without a declared version, as in clap', () => {
414
+ const result = parseArgs(['--version'], cmd({}));
415
+ expect(result.versionRequested).toBe(false);
416
+ expect(result.unknown).toEqual(['--version']);
417
+ });
418
+ });
419
+
420
+ // ---- Aliases ----
421
+
422
+ describe('aliases', () => {
423
+ test('long alias recognized', () => {
424
+ const result = parseArgs(
425
+ ['--directory', './mocks'],
426
+ cmd({ dir: { type: 'string', long: 'dir', alias: ['directory'] } }),
427
+ );
428
+ expect(result.args.dir).toBe('./mocks');
429
+ });
430
+
431
+ test('single-char alias as short flag', () => {
432
+ const result = parseArgs(
433
+ ['-d', './mocks'],
434
+ cmd({ dir: { type: 'string', long: 'dir', alias: ['d'] } }),
435
+ );
436
+ expect(result.args.dir).toBe('./mocks');
437
+ });
438
+ });
439
+
440
+ // ---- Tokenizer regressions ----
441
+
442
+ describe('boolean values via =', () => {
443
+ test('--flag=true is true', () => {
444
+ const result = parseArgs(['--verbose=true'], cmd({ verbose: { type: 'boolean' } }));
445
+ expect(result.args.verbose).toBe(true);
446
+ });
447
+
448
+ test('--flag=1 and --flag=yes are true', () => {
449
+ const c = cmd({ verbose: { type: 'boolean' } });
450
+ expect(parseArgs(['--verbose=1'], c).args.verbose).toBe(true);
451
+ expect(parseArgs(['--verbose=yes'], c).args.verbose).toBe(true);
452
+ });
453
+
454
+ test('--flag=false and --flag=junk are false', () => {
455
+ const c = cmd({ verbose: { type: 'boolean' } });
456
+ expect(parseArgs(['--verbose=false'], c).args.verbose).toBe(false);
457
+ expect(parseArgs(['--verbose=junk'], c).args.verbose).toBe(false);
458
+ });
459
+ });
460
+
461
+ describe('missing values are rejected', () => {
462
+ test('flag at end of argv', () => {
463
+ expect(() => parseArgs(['--name'], cmd({ name: { type: 'string' } }))).toThrow(
464
+ "a value is required for '--name' but none was supplied",
465
+ );
466
+ });
467
+
468
+ test('flag followed by another flag', () => {
469
+ const c = cmd({ name: { type: 'string' }, verbose: { type: 'boolean' } });
470
+ expect(() => parseArgs(['--name', '--verbose'], c)).toThrow(
471
+ "a value is required for '--name' but none was supplied",
472
+ );
473
+ });
474
+
475
+ test('append flag with no value', () => {
476
+ const c = cmd({ header: { type: 'string', short: 'H', action: 'append' } });
477
+ expect(() => parseArgs(['-H'], c)).toThrow(CliParseError);
478
+ });
479
+ });
480
+
481
+ describe('short flag values', () => {
482
+ test('-p=80 strips the equals like clap', () => {
483
+ const result = parseArgs(['-p=80'], cmd({ port: { type: 'number', short: 'p' } }));
484
+ expect(result.args.port).toBe(80);
485
+ });
486
+
487
+ test('-c= yields an empty string', () => {
488
+ const result = parseArgs(['-c='], cmd({ config: { type: 'string', short: 'c' } }));
489
+ expect(result.args.config).toBe('');
490
+ });
491
+
492
+ test('unknown short flag is reported with a single dash', () => {
493
+ const result = parseArgs(['-Z'], cmd({ verbose: { type: 'boolean', short: 'v' } }));
494
+ expect(result.unknown).toEqual(['-Z']);
495
+ });
496
+ });
497
+
498
+ describe('numArgs consumes multiple tokens', () => {
499
+ const multi = cmd({
500
+ verbose: { type: 'boolean' },
501
+ files: { type: 'string', action: 'append', numArgs: { min: 2, max: 5 } },
502
+ });
503
+
504
+ test('collects up to max values', () => {
505
+ expect(parseArgs(['--files', 'a', 'b'], multi).args.files).toEqual(['a', 'b']);
506
+ });
507
+
508
+ test('stops at the next flag', () => {
509
+ const result = parseArgs(['--files', 'a', 'b', '--verbose'], multi);
510
+ expect(result.args.files).toEqual(['a', 'b']);
511
+ expect(result.args.verbose).toBe(true);
512
+ expect(result.positionals).toEqual([]);
513
+ });
514
+
515
+ test('too few values is an error', () => {
516
+ expect(() => parseArgs(['--files', 'a'], multi)).toThrow(
517
+ "the argument '--files' requires at least 2 values but 1 were provided",
518
+ );
519
+ });
520
+
521
+ test('valueTerminator ends collection', () => {
522
+ const c = cmd({
523
+ cmds: {
524
+ type: 'string',
525
+ action: 'append',
526
+ numArgs: { min: 1, max: 99 },
527
+ allowHyphenValues: true,
528
+ valueTerminator: ';',
529
+ },
530
+ location: { type: 'positional' },
531
+ });
532
+ const result = parseArgs(['--cmds', 'ls', '-la', ';', '/tmp'], c);
533
+ expect(result.args.cmds).toEqual(['ls', '-la']);
534
+ expect(result.args.location).toBe('/tmp');
535
+ });
536
+ });
537
+
538
+ describe('requireEquals', () => {
539
+ const c = cmd({ config: { type: 'string', short: 'c', requireEquals: true } });
540
+
541
+ test('accepts --config=value', () => {
542
+ expect(parseArgs(['--config=x'], c).args.config).toBe('x');
543
+ });
544
+
545
+ test('rejects --config value', () => {
546
+ expect(() => parseArgs(['--config', 'x'], c)).toThrow(
547
+ "equal sign is needed when assigning values to '--config'",
548
+ );
549
+ });
550
+
551
+ test('rejects -c value', () => {
552
+ expect(() => parseArgs(['-c', 'x'], c)).toThrow(CliParseError);
553
+ });
554
+ });
555
+
556
+ describe('negative numbers as positionals', () => {
557
+ test('allowNegativeNumbers lets a positional take -5', () => {
558
+ const c = cmd({
559
+ n: { type: 'positional', allowNegativeNumbers: true },
560
+ m: { type: 'positional' },
561
+ });
562
+ const result = parseArgs(['-5', 'x'], c);
563
+ expect(result.args.n).toBe('-5');
564
+ expect(result.args.m).toBe('x');
565
+ expect(result.unknown).toEqual([]);
566
+ });
567
+ });
568
+
569
+ describe('subcommand boundary', () => {
570
+ const c = cmd(
571
+ { verbose: { type: 'boolean', short: 'v' }, name: { type: 'string' } },
572
+ { serve: { meta: { name: 'serve' } } },
573
+ );
574
+
575
+ test('tokens after the subcommand are handed on untouched', () => {
576
+ const result = parseArgs(['-v', 'serve', '--port', '9'], c);
577
+ expect(result.subCommand).toBe('serve');
578
+ expect(result.args.verbose).toBe(true);
579
+ expect(result.subCommandArgs).toEqual(['--port', '9']);
580
+ });
581
+
582
+ test('a flag value is never mistaken for a subcommand', () => {
583
+ const result = parseArgs(['--name', 'serve'], c);
584
+ expect(result.subCommand).toBeUndefined();
585
+ expect(result.args.name).toBe('serve');
586
+ });
587
+ });
588
+
589
+ describe('inferLongArgs ambiguity', () => {
590
+ test('an ambiguous prefix is not resolved', () => {
591
+ const c: CommandDef = {
592
+ meta: { name: 'test', version: '1.0.0', inferLongArgs: true },
593
+ args: { verbose: { type: 'boolean' } },
594
+ };
595
+ // --v is a prefix of both --verbose and the built-in --version.
596
+ expect(parseArgs(['--v'], c).unknown).toEqual(['--v']);
597
+ expect(parseArgs(['--verb'], c).args.verbose).toBe(true);
598
+ });
599
+ });
600
+
601
+ describe('explicitlySet', () => {
602
+ test('tracks which args were explicitly provided', () => {
603
+ const command = cmd({
604
+ port: { type: 'number', long: 'port', default: 3000 },
605
+ verbose: { type: 'boolean', long: 'verbose' },
606
+ });
607
+ const result = parseArgs(['--verbose'], command);
608
+ expect(result.explicitlySet.has('verbose')).toBe(true);
609
+ expect(result.explicitlySet.has('port')).toBe(false);
610
+ });
611
+ });
612
+
613
+ describe('helpIsShort', () => {
614
+ test('detects -h as short help', () => {
615
+ const command = cmd({});
616
+ const result = parseArgs(['-h'], command);
617
+ expect(result.helpRequested).toBe(true);
618
+ expect(result.helpIsShort).toBe(true);
619
+ });
620
+
621
+ test('detects --help as long help', () => {
622
+ const command = cmd({});
623
+ const result = parseArgs(['--help'], command);
624
+ expect(result.helpRequested).toBe(true);
625
+ expect(result.helpIsShort).toBe(false);
626
+ });
627
+ });