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
+ * Argument validation - enforces constraints after parsing.
3
+ * Matches clap's validation: required, exclusive, conflicts, requires,
4
+ * valueParser, numArgs, requiredUnlessPresent, requiredIfEq, groups.
5
+ * Includes typo suggestion via Levenshtein distance.
6
+ */
7
+
8
+ import type { ArgDef, ArgsDef, ArgGroup, CommandDef, ParseResult } from './types.js';
9
+ import {
10
+ CliParseError,
11
+ matchesPossibleValue,
12
+ possibleValues,
13
+ subCommandsOf,
14
+ } from './parser.js';
15
+
16
+ // ---- Compiled Validation Spec ----
17
+
18
+ /**
19
+ * Most commands use only a few of the constraint kinds, so the subsets are
20
+ * computed once per command rather than walking every arg nine times per run.
21
+ */
22
+ interface ValidationSpec {
23
+ readonly entries: readonly (readonly [string, ArgDef])[];
24
+ readonly exclusive: readonly (readonly [string, ArgDef])[];
25
+ readonly required: readonly (readonly [string, ArgDef])[];
26
+ readonly requiredIfEq: readonly (readonly [string, ArgDef])[];
27
+ readonly valueParser: readonly (readonly [string, ArgDef])[];
28
+ readonly conflicts: readonly (readonly [string, ArgDef])[];
29
+ readonly requires: readonly (readonly [string, ArgDef])[];
30
+ readonly numArgs: readonly (readonly [string, ArgDef])[];
31
+ }
32
+
33
+ const specCache = new WeakMap<CommandDef, ValidationSpec>();
34
+
35
+ function buildValidationSpec(command: CommandDef): ValidationSpec {
36
+ const entries: (readonly [string, ArgDef])[] = [];
37
+ const exclusive: (readonly [string, ArgDef])[] = [];
38
+ const required: (readonly [string, ArgDef])[] = [];
39
+ const requiredIfEq: (readonly [string, ArgDef])[] = [];
40
+ const valueParser: (readonly [string, ArgDef])[] = [];
41
+ const conflicts: (readonly [string, ArgDef])[] = [];
42
+ const requires: (readonly [string, ArgDef])[] = [];
43
+ const numArgs: (readonly [string, ArgDef])[] = [];
44
+
45
+ const argsDef = command.args ?? {};
46
+ for (const key of Object.keys(argsDef)) {
47
+ const def = argsDef[key]!;
48
+ const entry = [key, def] as const;
49
+ entries.push(entry);
50
+
51
+ if (def.exclusive) {
52
+ exclusive.push(entry);
53
+ }
54
+ if (def.required && def.default === undefined) {
55
+ required.push(entry);
56
+ }
57
+ if (def.requiredIfEq || def.requiredIfEqAny || def.requiredIfEqAll) {
58
+ requiredIfEq.push(entry);
59
+ }
60
+ if (def.valueParser) {
61
+ valueParser.push(entry);
62
+ }
63
+ if (def.conflictsWith && def.conflictsWith.length > 0) {
64
+ conflicts.push(entry);
65
+ }
66
+ if (
67
+ (def.requires && def.requires.length > 0) ||
68
+ def.requiresIf !== undefined ||
69
+ def.requiresIfs !== undefined
70
+ ) {
71
+ requires.push(entry);
72
+ }
73
+ // The parser enforces numArgs per occurrence while consuming tokens, the
74
+ // way clap does. Only positionals and delimiter-split values reach here
75
+ // without having been counted.
76
+ if (def.numArgs && (def.type === 'positional' || def.valueDelimiter !== undefined)) {
77
+ numArgs.push(entry);
78
+ }
79
+ }
80
+
81
+ return { entries, exclusive, required, requiredIfEq, valueParser, conflicts, requires, numArgs };
82
+ }
83
+
84
+ function getValidationSpec(command: CommandDef): ValidationSpec {
85
+ let spec = specCache.get(command);
86
+ if (spec === undefined) {
87
+ spec = buildValidationSpec(command);
88
+ specCache.set(command, spec);
89
+ }
90
+ return spec;
91
+ }
92
+
93
+ // ---- Levenshtein Distance ----
94
+
95
+ /**
96
+ * Calculate Levenshtein distance between two strings.
97
+ * Used for "did you mean?" typo suggestions (like clap).
98
+ */
99
+ function levenshteinDistance(a: string, b: string): number {
100
+ const aLen = a.length;
101
+ const bLen = b.length;
102
+
103
+ if (aLen === 0) {
104
+ return bLen;
105
+ }
106
+ if (bLen === 0) {
107
+ return aLen;
108
+ }
109
+
110
+ // Use single-row DP for memory efficiency
111
+ const row: number[] = Array.from({ length: bLen + 1 });
112
+ for (let j = 0; j <= bLen; j++) {
113
+ row[j] = j;
114
+ }
115
+
116
+ for (let i = 1; i <= aLen; i++) {
117
+ let prev = i - 1;
118
+ row[0] = i;
119
+ for (let j = 1; j <= bLen; j++) {
120
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
121
+ const current = row[j]!;
122
+ const val = Math.min(
123
+ current + 1, // deletion
124
+ row[j - 1]! + 1, // insertion
125
+ prev + cost, // substitution
126
+ );
127
+ prev = current;
128
+ row[j] = val;
129
+ }
130
+ }
131
+
132
+ return row[bLen]!;
133
+ }
134
+
135
+ /**
136
+ * Find the closest match for a string from candidates.
137
+ * Returns undefined if no candidate is within maxDistance.
138
+ */
139
+ function findClosestMatch(
140
+ target: string,
141
+ candidates: readonly string[],
142
+ maxDistance = 3,
143
+ ): string | undefined {
144
+ let bestMatch: string | undefined;
145
+ let bestDist = maxDistance + 1;
146
+
147
+ for (const candidate of candidates) {
148
+ const dist = levenshteinDistance(target, candidate);
149
+ if (dist < bestDist) {
150
+ bestDist = dist;
151
+ bestMatch = candidate;
152
+ }
153
+ }
154
+
155
+ return bestMatch;
156
+ }
157
+
158
+ // ---- Build Known Flags ----
159
+
160
+ /**
161
+ * Collect all known long flag names for a command (for typo suggestions).
162
+ */
163
+ function collectKnownFlags(argsDef: ArgsDef): string[] {
164
+ const flags: string[] = [];
165
+ for (const [key, def] of Object.entries(argsDef)) {
166
+ const longName = def.long ?? key;
167
+ flags.push(longName);
168
+ if (def.type === 'boolean') {
169
+ flags.push(`no-${longName}`);
170
+ }
171
+ if (def.alias) {
172
+ for (const alias of def.alias) {
173
+ if (alias.length > 1) {
174
+ flags.push(alias);
175
+ }
176
+ }
177
+ }
178
+ if (def.visibleAlias) {
179
+ for (const alias of def.visibleAlias) {
180
+ if (alias.length > 1) {
181
+ flags.push(alias);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ // Always include built-in flags
187
+ flags.push('help', 'version');
188
+ return flags;
189
+ }
190
+
191
+ /**
192
+ * Collect all known subcommand names (including aliases) for typo suggestions.
193
+ */
194
+ function collectKnownSubcommands(command: CommandDef): string[] {
195
+ const names: string[] = [];
196
+ for (const [name, def] of Object.entries(subCommandsOf(command))) {
197
+ names.push(name);
198
+ if (def.meta.aliases) {
199
+ for (const alias of def.meta.aliases) {
200
+ names.push(alias);
201
+ }
202
+ }
203
+ }
204
+ return names;
205
+ }
206
+
207
+ /** Check if an arg value is "set" (not undefined, and not false for booleans). */
208
+ function isArgSet(value: unknown, typeName: string | undefined): boolean {
209
+ if (value === undefined) {
210
+ return false;
211
+ }
212
+ if (typeName === 'boolean' && value === false) {
213
+ return false;
214
+ }
215
+ return true;
216
+ }
217
+
218
+ // ---- Validation ----
219
+
220
+ /** Validate unknown flags and suggest corrections. */
221
+ function validateUnknownFlags(
222
+ unknown: readonly string[],
223
+ argsDef: ArgsDef,
224
+ command: CommandDef,
225
+ ): void {
226
+ if (unknown.length === 0) {
227
+ return;
228
+ }
229
+
230
+ const knownFlags = collectKnownFlags(argsDef);
231
+ const knownSubcommands = collectKnownSubcommands(command);
232
+ const allKnown = [...knownFlags, ...knownSubcommands];
233
+
234
+ for (const flag of unknown) {
235
+ const stripped = flag.replace(/^-+/, '');
236
+ const suggestion = findClosestMatch(stripped, allKnown);
237
+ let msg = `unexpected argument '${flag}' found`;
238
+ if (suggestion) {
239
+ const prefix = suggestion.length === 1 ? '-' : '--';
240
+ msg += `\n\n tip: a similar argument exists: '${prefix}${suggestion}'`;
241
+ }
242
+ throw new CliParseError(msg);
243
+ }
244
+ }
245
+
246
+ /** Validate exclusive args -- cannot be used with any other arg. */
247
+ function validateExclusive(
248
+ spec: ValidationSpec,
249
+ argsDef: ArgsDef,
250
+ explicitlySet: ReadonlySet<string>,
251
+ ): void {
252
+ for (const [key, def] of spec.exclusive) {
253
+ if (!explicitlySet.has(key)) {
254
+ continue;
255
+ }
256
+
257
+ // Check if any other arg was explicitly set
258
+ for (const otherKey of explicitlySet) {
259
+ if (otherKey === key) {
260
+ continue;
261
+ }
262
+ const otherDef = argsDef[otherKey];
263
+ if (!otherDef) {
264
+ continue;
265
+ }
266
+ const flagName = formatFlagForError(key, def);
267
+ const otherFlagName = formatFlagForError(otherKey, otherDef);
268
+ throw new CliParseError(
269
+ `the argument ${flagName} cannot be used with ${otherFlagName}`,
270
+ );
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Validate that all required args are present.
277
+ * Respects requiredUnlessPresent and subcommandNegatesReqs.
278
+ */
279
+ function validateRequired(
280
+ spec: ValidationSpec,
281
+ argsDef: ArgsDef,
282
+ args: Record<string, string | number | boolean | string[]>,
283
+ skipRequired: boolean,
284
+ ): void {
285
+ if (skipRequired) {
286
+ return;
287
+ }
288
+
289
+ const missing: string[] = [];
290
+
291
+ for (const [key, def] of spec.required) {
292
+ // requiredUnlessPresent: skip if any of the named args is present
293
+ if (def.requiredUnlessPresent) {
294
+ const unlessArgs = Array.isArray(def.requiredUnlessPresent)
295
+ ? def.requiredUnlessPresent
296
+ : [def.requiredUnlessPresent];
297
+ if (unlessArgs.some((name) => isArgSet(args[name], argsDef[name]?.type))) {
298
+ continue;
299
+ }
300
+ }
301
+
302
+ // requiredUnlessPresentAll: skip only when every named arg is present
303
+ if (
304
+ def.requiredUnlessPresentAll &&
305
+ def.requiredUnlessPresentAll.length > 0 &&
306
+ def.requiredUnlessPresentAll.every((name) => isArgSet(args[name], argsDef[name]?.type))
307
+ ) {
308
+ continue;
309
+ }
310
+
311
+ const value = args[key];
312
+ if (value === undefined) {
313
+ const displayName =
314
+ def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
315
+ missing.push(displayName);
316
+ }
317
+ }
318
+
319
+ if (missing.length > 0) {
320
+ const lines = missing.map((f) => ` ${f}`).join('\n');
321
+ throw new CliParseError(`the following required arguments were not provided:\n${lines}`);
322
+ }
323
+ }
324
+
325
+ /** Validate requiredIfEq -- arg required when another arg equals a specific value. */
326
+ function validateRequiredIfEq(
327
+ spec: ValidationSpec,
328
+ args: Record<string, string | number | boolean | string[]>,
329
+ ): void {
330
+ const holds = (
331
+ args: Record<string, string | number | boolean | string[]>,
332
+ [otherKey, otherValue]: readonly [string, string],
333
+ ): boolean => {
334
+ const actual = args[otherKey];
335
+ return actual !== undefined && String(actual) === otherValue;
336
+ };
337
+
338
+ for (const [key, def] of spec.requiredIfEq) {
339
+ let isRequired = def.requiredIfEq !== undefined && holds(args, def.requiredIfEq);
340
+
341
+ if (!isRequired && def.requiredIfEqAny) {
342
+ isRequired = def.requiredIfEqAny.some((condition) => holds(args, condition));
343
+ }
344
+ if (!isRequired && def.requiredIfEqAll && def.requiredIfEqAll.length > 0) {
345
+ isRequired = def.requiredIfEqAll.every((condition) => holds(args, condition));
346
+ }
347
+
348
+ if (isRequired && !isArgSet(args[key], def.type)) {
349
+ const displayName =
350
+ def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
351
+ throw new CliParseError(
352
+ `the following required arguments were not provided:\n ${displayName}`,
353
+ );
354
+ }
355
+ }
356
+ }
357
+
358
+ /** Validate valueParser (enum-like restricted values). */
359
+ function validateValueParser(
360
+ spec: ValidationSpec,
361
+ args: Record<string, string | number | boolean | string[]>,
362
+ ): void {
363
+ for (const [key, def] of spec.valueParser) {
364
+ const value = args[key];
365
+ if (value === undefined) {
366
+ continue;
367
+ }
368
+
369
+ const longName = def.long ?? key;
370
+
371
+ // Function-based value parser: already applied in parser, skip enum check
372
+ if (typeof def.valueParser === 'function') {
373
+ continue;
374
+ }
375
+
376
+ // Enum-style value parser: validate against allowed values
377
+ const possible = possibleValues(def);
378
+ if (possible.length === 0) {
379
+ continue;
380
+ }
381
+
382
+ const ignoreCase = def.ignoreCase === true;
383
+ const valueName = def.valueName ?? longName.toUpperCase();
384
+ // A positional has no flag to name, so it is shown as just its placeholder.
385
+ const target =
386
+ def.type === 'positional' ? `<${valueName}>` : `--${longName} <${valueName}>`;
387
+ const values = Array.isArray(value) ? value : [String(value)];
388
+ for (const v of values) {
389
+ if (!possible.some((candidate) => matchesPossibleValue(candidate, v, ignoreCase))) {
390
+ const possibleStr = possible
391
+ .filter((candidate) => !candidate.hidden)
392
+ .map((candidate) => candidate.name)
393
+ .join(', ');
394
+ throw new CliParseError(
395
+ `invalid value '${v}' for '${target}'\n [possible values: ${possibleStr}]`,
396
+ );
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ /** Format a flag name for error display, including value name for non-booleans. */
403
+ function formatFlagForError(key: string, def: ArgDef): string {
404
+ const longName = def.long ?? key;
405
+ if (def.type !== 'boolean') {
406
+ const valueName = def.valueName ?? longName.toUpperCase();
407
+ return `'--${longName} <${valueName}>'`;
408
+ }
409
+ return `'--${longName}'`;
410
+ }
411
+
412
+ /** Validate conflictsWith constraints. */
413
+ function validateConflicts(
414
+ spec: ValidationSpec,
415
+ argsDef: ArgsDef,
416
+ args: Record<string, string | number | boolean | string[]>,
417
+ ): void {
418
+ for (const [key, def] of spec.conflicts) {
419
+ if (!isArgSet(args[key], def.type)) {
420
+ continue;
421
+ }
422
+
423
+ for (const conflictKey of def.conflictsWith!) {
424
+ const conflictDef = argsDef[conflictKey];
425
+ if (!isArgSet(args[conflictKey], conflictDef?.type)) {
426
+ continue;
427
+ }
428
+
429
+ const flagA = formatFlagForError(key, def);
430
+ const flagB = conflictDef
431
+ ? formatFlagForError(conflictKey, conflictDef)
432
+ : `'--${conflictKey}'`;
433
+ throw new CliParseError(`the argument ${flagA} cannot be used with ${flagB}`);
434
+ }
435
+ }
436
+ }
437
+
438
+ /** Validate requires constraints. */
439
+ function validateRequires(
440
+ spec: ValidationSpec,
441
+ argsDef: ArgsDef,
442
+ args: Record<string, string | number | boolean | string[]>,
443
+ ): void {
444
+ for (const [key, def] of spec.requires) {
445
+ const own = args[key];
446
+ if (!isArgSet(own, def.type)) {
447
+ continue;
448
+ }
449
+
450
+ // requires applies whenever the arg is present; requiresIf only when it
451
+ // holds a particular value.
452
+ const needed: string[] = [...(def.requires ?? [])];
453
+ const ownValue = String(own);
454
+ if (def.requiresIf && def.requiresIf[0] === ownValue) {
455
+ needed.push(def.requiresIf[1]);
456
+ }
457
+ if (def.requiresIfs) {
458
+ for (const [whenValue, requiredKey] of def.requiresIfs) {
459
+ if (whenValue === ownValue) {
460
+ needed.push(requiredKey);
461
+ }
462
+ }
463
+ }
464
+
465
+ const missing: string[] = [];
466
+ for (const requiredKey of needed) {
467
+ const requiredDef = argsDef[requiredKey];
468
+ if (!isArgSet(args[requiredKey], requiredDef?.type)) {
469
+ missing.push(`--${requiredDef?.long ?? requiredKey}`);
470
+ }
471
+ }
472
+ if (missing.length > 0) {
473
+ const lines = missing.map((f) => ` ${f}`).join('\n');
474
+ throw new CliParseError(`the following required arguments were not provided:\n${lines}`);
475
+ }
476
+ }
477
+ }
478
+
479
+ /** Validate numArgs constraints. */
480
+ function validateNumArgs(
481
+ spec: ValidationSpec,
482
+ args: Record<string, string | number | boolean | string[]>,
483
+ ): void {
484
+ for (const [key, def] of spec.numArgs) {
485
+ const value = args[key];
486
+ if (value === undefined) {
487
+ continue;
488
+ }
489
+
490
+ const count = Array.isArray(value) ? value.length : 1;
491
+ if (count < def.numArgs!.min) {
492
+ throw new CliParseError(
493
+ `the argument '--${def.long ?? key}' requires at least ${String(def.numArgs!.min)} values but ${String(count)} were provided`,
494
+ );
495
+ }
496
+ if (count > def.numArgs!.max) {
497
+ throw new CliParseError(
498
+ `the argument '--${def.long ?? key}' accepts at most ${String(def.numArgs!.max)} values but ${String(count)} were provided`,
499
+ );
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Collect the effective groups: those declared on the command, widened by any
506
+ * arg that names the group itself via `groups`.
507
+ */
508
+ function collectGroups(command: CommandDef, argsDef: ArgsDef): readonly ArgGroup[] {
509
+ const declared = command.groups ?? [];
510
+ const byName = new Map<string, string[]>();
511
+
512
+ for (const key of Object.keys(argsDef)) {
513
+ const def = argsDef[key]!;
514
+ if (def.group === undefined && def.groups === undefined) {
515
+ continue;
516
+ }
517
+ const memberships = def.group === undefined ? def.groups! : [def.group, ...(def.groups ?? [])];
518
+ for (const name of memberships) {
519
+ const members = byName.get(name);
520
+ if (members === undefined) {
521
+ byName.set(name, [key]);
522
+ } else {
523
+ members.push(key);
524
+ }
525
+ }
526
+ }
527
+
528
+ if (byName.size === 0) {
529
+ return declared;
530
+ }
531
+
532
+ const merged: ArgGroup[] = [];
533
+ for (const group of declared) {
534
+ const extra = byName.get(group.name);
535
+ if (extra === undefined) {
536
+ merged.push(group);
537
+ continue;
538
+ }
539
+ byName.delete(group.name);
540
+ merged.push({ ...group, args: [...group.args, ...extra.filter((a) => !group.args.includes(a))] });
541
+ }
542
+ for (const [name, members] of byName) {
543
+ merged.push({ name, args: members });
544
+ }
545
+ return merged;
546
+ }
547
+
548
+ /** Validate argument groups. */
549
+ function validateGroups(
550
+ command: CommandDef,
551
+ argsDef: ArgsDef,
552
+ args: Record<string, string | number | boolean | string[]>,
553
+ ): void {
554
+ const groups = collectGroups(command, argsDef);
555
+ if (groups.length === 0) {
556
+ return;
557
+ }
558
+
559
+ const flagName = (name: string): string => `'--${argsDef[name]?.long ?? name}'`;
560
+
561
+ for (const group of groups) {
562
+ const setArgs = group.args.filter((argName) => isArgSet(args[argName], argsDef[argName]?.type));
563
+
564
+ if (group.required && setArgs.length === 0) {
565
+ const argList = group.args.map(flagName).join(', ');
566
+ throw new CliParseError(`one of the following arguments must be provided: ${argList}`);
567
+ }
568
+
569
+ if (!group.multiple && setArgs.length > 1) {
570
+ const argList = setArgs.map(flagName).join(', ');
571
+ throw new CliParseError(`the following arguments cannot be used together: ${argList}`);
572
+ }
573
+
574
+ if (setArgs.length === 0) {
575
+ continue;
576
+ }
577
+
578
+ if (group.conflictsWith) {
579
+ for (const other of group.conflictsWith) {
580
+ if (isArgSet(args[other], argsDef[other]?.type)) {
581
+ throw new CliParseError(
582
+ `the argument ${flagName(setArgs[0]!)} cannot be used with ${flagName(other)}`,
583
+ );
584
+ }
585
+ }
586
+ }
587
+
588
+ if (group.requires) {
589
+ const missing = group.requires.filter(
590
+ (other) => !isArgSet(args[other], argsDef[other]?.type),
591
+ );
592
+ if (missing.length > 0) {
593
+ const lines = missing.map((m) => ` --${argsDef[m]?.long ?? m}`).join('\n');
594
+ throw new CliParseError(`the following required arguments were not provided:\n${lines}`);
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Validate parsed results against the command definition.
602
+ * Throws CliParseError with clap-style error messages.
603
+ */
604
+ export function validate(parseResult: ParseResult, command: CommandDef): void {
605
+ const argsDef = command.args ?? {};
606
+ const { args, unknown, explicitlySet } = parseResult;
607
+
608
+ // Determine if required validation should be skipped (subcommandNegatesReqs)
609
+ const skipRequired =
610
+ command.meta.subcommandNegatesReqs === true && parseResult.subCommand !== undefined;
611
+
612
+ const spec = getValidationSpec(command);
613
+
614
+ if (unknown.length > 0) {
615
+ validateUnknownFlags(unknown, argsDef, command);
616
+ }
617
+ validateExclusive(spec, argsDef, explicitlySet);
618
+ validateRequired(spec, argsDef, args, skipRequired);
619
+ if (!skipRequired) {
620
+ validateRequiredIfEq(spec, args);
621
+ }
622
+ validateValueParser(spec, args);
623
+ validateConflicts(spec, argsDef, args);
624
+ validateRequires(spec, argsDef, args);
625
+ validateNumArgs(spec, args);
626
+ validateGroups(command, argsDef, args);
627
+ }