@guanghechen/commander 4.7.1 → 4.7.3

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.
@@ -182,6 +182,140 @@ function kebabToCamelCase(str) {
182
182
  function camelToKebabCase(str) {
183
183
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
184
184
  }
185
+ const ANSI_ESCAPE_REGEX = new RegExp(String.raw `\\x1B\\[[0-?]*[ -/]*[@-~]`, 'g');
186
+ const DECIMAL_INTEGER_REGEX = /^\d(?:_?\d)*$/;
187
+ const DECIMAL_FRACTION_REGEX = /^\d(?:_?\d)*$/;
188
+ const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
189
+ const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
190
+ const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
191
+ const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
192
+ function stripAnsi(value) {
193
+ return value.replace(ANSI_ESCAPE_REGEX, '');
194
+ }
195
+ function isCombiningMark(codePoint) {
196
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
197
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
198
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
199
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
200
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
201
+ }
202
+ function isWideCodePoint(codePoint) {
203
+ if (codePoint < 0x1100) {
204
+ return false;
205
+ }
206
+ return (codePoint <= 0x115f ||
207
+ codePoint === 0x2329 ||
208
+ codePoint === 0x232a ||
209
+ (codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
210
+ (codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
211
+ (codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
212
+ (codePoint >= 0xa960 && codePoint <= 0xa97c) ||
213
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
214
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
215
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
216
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
217
+ (codePoint >= 0xff01 && codePoint <= 0xff60) ||
218
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
219
+ (codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
220
+ (codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
221
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
222
+ }
223
+ function getDisplayWidth(value) {
224
+ const normalized = stripAnsi(value).normalize('NFC');
225
+ let width = 0;
226
+ for (const char of normalized) {
227
+ const codePoint = char.codePointAt(0);
228
+ if (codePoint === undefined || isCombiningMark(codePoint)) {
229
+ continue;
230
+ }
231
+ width += isWideCodePoint(codePoint) ? 2 : 1;
232
+ }
233
+ return width;
234
+ }
235
+ function padDisplayEnd(value, targetWidth) {
236
+ const width = getDisplayWidth(value);
237
+ if (width >= targetWidth) {
238
+ return value;
239
+ }
240
+ return value + ' '.repeat(targetWidth - width);
241
+ }
242
+ function isValidPrimitiveNumberLiteral(rawValue) {
243
+ if (rawValue.trim() !== rawValue || rawValue.length === 0) {
244
+ return false;
245
+ }
246
+ if (rawValue === 'NaN' || rawValue === 'Infinity' || rawValue === '-Infinity') {
247
+ return false;
248
+ }
249
+ if (BINARY_LITERAL_REGEX.test(rawValue)) {
250
+ return true;
251
+ }
252
+ if (OCTAL_LITERAL_REGEX.test(rawValue)) {
253
+ return true;
254
+ }
255
+ if (HEX_LITERAL_REGEX.test(rawValue)) {
256
+ return true;
257
+ }
258
+ const sign = rawValue[0] === '+' || rawValue[0] === '-' ? rawValue[0] : '';
259
+ const body = sign ? rawValue.slice(1) : rawValue;
260
+ if (body.length === 0) {
261
+ return false;
262
+ }
263
+ const expIndex = body.search(/[eE]/);
264
+ const basePart = expIndex === -1 ? body : body.slice(0, expIndex);
265
+ const expPart = expIndex === -1 ? '' : body.slice(expIndex);
266
+ if (expPart && !DECIMAL_EXPONENT_REGEX.test(expPart)) {
267
+ return false;
268
+ }
269
+ if (basePart.includes('.')) {
270
+ const decimalParts = basePart.split('.');
271
+ if (decimalParts.length !== 2) {
272
+ return false;
273
+ }
274
+ const [intPart, fracPart] = decimalParts;
275
+ const intOk = intPart.length === 0 || DECIMAL_INTEGER_REGEX.test(intPart);
276
+ const fracOk = fracPart.length === 0 || DECIMAL_FRACTION_REGEX.test(fracPart);
277
+ if (!intOk || !fracOk) {
278
+ return false;
279
+ }
280
+ return intPart.length > 0 || fracPart.length > 0;
281
+ }
282
+ return DECIMAL_INTEGER_REGEX.test(basePart);
283
+ }
284
+ function parsePrimitiveNumber(rawValue) {
285
+ if (!isValidPrimitiveNumberLiteral(rawValue)) {
286
+ return undefined;
287
+ }
288
+ const normalized = rawValue.replaceAll('_', '');
289
+ const value = Number(normalized);
290
+ if (!Number.isFinite(value)) {
291
+ return undefined;
292
+ }
293
+ return value;
294
+ }
295
+ function normalizeSubcommandNameForDistance(name) {
296
+ return camelToKebabCase(name).toLowerCase();
297
+ }
298
+ function levenshteinDistance(left, right) {
299
+ if (left === right) {
300
+ return 0;
301
+ }
302
+ if (left.length === 0) {
303
+ return right.length;
304
+ }
305
+ if (right.length === 0) {
306
+ return left.length;
307
+ }
308
+ let prev = Array.from({ length: right.length + 1 }, (_, i) => i);
309
+ for (let i = 0; i < left.length; i += 1) {
310
+ const current = [i + 1];
311
+ for (let j = 0; j < right.length; j += 1) {
312
+ const substitutionCost = left[i] === right[j] ? 0 : 1;
313
+ current[j + 1] = Math.min(current[j] + 1, prev[j + 1] + 1, prev[j] + substitutionCost);
314
+ }
315
+ prev = current;
316
+ }
317
+ return prev[right.length];
318
+ }
185
319
  function tokenizeLongOption(arg, commandPath) {
186
320
  const eqIdx = arg.indexOf('=');
187
321
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -418,9 +552,13 @@ class Command {
418
552
  if (cmd.#parent && cmd.#parent !== this) {
419
553
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
420
554
  }
555
+ const occupied = this.#subcommandsMap.get(name);
556
+ if (occupied && occupied !== cmd) {
557
+ throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
558
+ }
421
559
  const existing = this.#subcommandsList.find(e => e.command === cmd);
422
560
  if (existing) {
423
- if (existing.aliases.includes(name)) {
561
+ if (existing.name === name || existing.aliases.includes(name)) {
424
562
  return this;
425
563
  }
426
564
  existing.aliases.push(name);
@@ -549,16 +687,41 @@ class Command {
549
687
  else if (arg.kind === 'optional') {
550
688
  usage += ` [${arg.name}]`;
551
689
  }
690
+ else if (arg.kind === 'some') {
691
+ usage += ` <${arg.name}...>`;
692
+ }
552
693
  else {
553
694
  usage += ` [${arg.name}...]`;
554
695
  }
555
696
  }
697
+ const argumentsLines = [];
698
+ for (const arg of this.#arguments) {
699
+ const sig = arg.kind === 'required'
700
+ ? `<${arg.name}>`
701
+ : arg.kind === 'optional'
702
+ ? `[${arg.name}]`
703
+ : arg.kind === 'some'
704
+ ? `<${arg.name}...>`
705
+ : `[${arg.name}...]`;
706
+ const metadata = [`[type: ${arg.type}]`];
707
+ if (arg.kind === 'optional' && arg.default !== undefined) {
708
+ metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
709
+ }
710
+ if (arg.choices && arg.choices.length > 0) {
711
+ metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
712
+ }
713
+ const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
714
+ argumentsLines.push({ sig, desc });
715
+ }
556
716
  const options = [];
557
717
  for (const opt of allOptions) {
558
718
  const kebabLong = camelToKebabCase(opt.long);
559
719
  let sig = opt.short ? `-${opt.short}, ` : ' ';
560
720
  sig += `--${kebabLong}`;
561
- if (opt.args !== 'none') {
721
+ if (opt.args === 'optional') {
722
+ sig += ' [value]';
723
+ }
724
+ else if (opt.args !== 'none') {
562
725
  sig += ' <value>';
563
726
  }
564
727
  let desc = opt.desc;
@@ -566,7 +729,7 @@ class Command {
566
729
  desc += ` (default: ${JSON.stringify(opt.default)})`;
567
730
  }
568
731
  if (opt.choices) {
569
- desc += ` [choices: ${opt.choices.join(', ')}]`;
732
+ desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
570
733
  }
571
734
  options.push({ sig, desc });
572
735
  if (opt.type === 'boolean' &&
@@ -598,6 +761,7 @@ class Command {
598
761
  return {
599
762
  desc: this.#desc,
600
763
  usage,
764
+ arguments: argumentsLines,
601
765
  options,
602
766
  commands,
603
767
  examples,
@@ -605,25 +769,29 @@ class Command {
605
769
  }
606
770
  #renderHelpPlain(helpData) {
607
771
  const lines = [];
772
+ const labelWidth = this.#getHelpLabelWidth(helpData);
608
773
  lines.push(helpData.desc);
609
774
  lines.push('');
610
775
  lines.push(helpData.usage);
611
776
  lines.push('');
777
+ if (helpData.arguments.length > 0) {
778
+ lines.push('Arguments:');
779
+ for (const { sig, desc } of helpData.arguments) {
780
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
781
+ }
782
+ lines.push('');
783
+ }
612
784
  if (helpData.options.length > 0) {
613
785
  lines.push('Options:');
614
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
615
786
  for (const { sig, desc } of helpData.options) {
616
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
617
- lines.push(` ${sig}${padding}${desc}`);
787
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
618
788
  }
619
789
  lines.push('');
620
790
  }
621
791
  if (helpData.commands.length > 0) {
622
792
  lines.push('Commands:');
623
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
624
793
  for (const { name, desc } of helpData.commands) {
625
- const padding = ' '.repeat(maxNameLen - name.length + 2);
626
- lines.push(` ${name}${padding}${desc}`);
794
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
627
795
  }
628
796
  lines.push('');
629
797
  }
@@ -640,25 +808,29 @@ class Command {
640
808
  }
641
809
  #renderHelpTerminal(helpData) {
642
810
  const lines = [];
811
+ const labelWidth = this.#getHelpLabelWidth(helpData);
643
812
  lines.push(helpData.desc);
644
813
  lines.push('');
645
814
  lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
646
815
  lines.push('');
816
+ if (helpData.arguments.length > 0) {
817
+ lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
818
+ for (const { sig, desc } of helpData.arguments) {
819
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
820
+ }
821
+ lines.push('');
822
+ }
647
823
  if (helpData.options.length > 0) {
648
824
  lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
649
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
650
825
  for (const { sig, desc } of helpData.options) {
651
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
652
- lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
826
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
653
827
  }
654
828
  lines.push('');
655
829
  }
656
830
  if (helpData.commands.length > 0) {
657
831
  lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
658
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
659
832
  for (const { name, desc } of helpData.commands) {
660
- const padding = ' '.repeat(maxNameLen - name.length + 2);
661
- lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
833
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
662
834
  }
663
835
  lines.push('');
664
836
  }
@@ -673,16 +845,41 @@ class Command {
673
845
  }
674
846
  return lines.join('\n');
675
847
  }
848
+ #getHelpLabelWidth(helpData) {
849
+ const labels = [
850
+ ...helpData.arguments.map(line => line.sig),
851
+ ...helpData.options.map(line => line.sig),
852
+ ...helpData.commands.map(line => line.name),
853
+ ];
854
+ if (labels.length === 0) {
855
+ return 0;
856
+ }
857
+ return Math.max(...labels.map(getDisplayWidth));
858
+ }
859
+ #renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
860
+ const paddedLabel = padDisplayEnd(label, labelWidth);
861
+ const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
862
+ return ` ${outputLabel} ${desc}`;
863
+ }
676
864
  getCompletionMeta() {
677
865
  const allOptions = this.#resolveOptionPolicy().mergedOptions;
678
866
  const options = [];
867
+ const argumentsMeta = [];
679
868
  for (const opt of allOptions) {
680
869
  options.push({
681
870
  long: opt.long,
682
871
  short: opt.short,
683
872
  desc: opt.desc,
684
873
  takesValue: opt.args !== 'none',
685
- choices: opt.choices,
874
+ choices: opt.choices?.map(choice => String(choice)),
875
+ });
876
+ }
877
+ for (const arg of this.#arguments) {
878
+ argumentsMeta.push({
879
+ name: arg.name,
880
+ kind: arg.kind,
881
+ type: arg.type,
882
+ choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
686
883
  });
687
884
  }
688
885
  return {
@@ -690,6 +887,7 @@ class Command {
690
887
  desc: this.#desc,
691
888
  aliases: [],
692
889
  options,
890
+ arguments: argumentsMeta,
693
891
  subcommands: this.#subcommandsList.map(entry => {
694
892
  const subMeta = entry.command.getCompletionMeta();
695
893
  return {
@@ -1142,6 +1340,14 @@ class Command {
1142
1340
  consumed.push(tokens[i]);
1143
1341
  }
1144
1342
  }
1343
+ else if (opt.args === 'optional') {
1344
+ if (!token.resolved.includes('=') &&
1345
+ i + 1 < tokens.length &&
1346
+ tokens[i + 1].type === 'none') {
1347
+ i += 1;
1348
+ consumed.push(tokens[i]);
1349
+ }
1350
+ }
1145
1351
  else if (opt.args === 'variadic') {
1146
1352
  if (!token.resolved.includes('=')) {
1147
1353
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1167,6 +1373,12 @@ class Command {
1167
1373
  consumed.push(tokens[i]);
1168
1374
  }
1169
1375
  }
1376
+ else if (opt.args === 'optional') {
1377
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1378
+ i += 1;
1379
+ consumed.push(tokens[i]);
1380
+ }
1381
+ }
1170
1382
  else if (opt.args === 'variadic') {
1171
1383
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1172
1384
  i += 1;
@@ -1208,6 +1420,7 @@ class Command {
1208
1420
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1209
1421
  }
1210
1422
  }
1423
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1211
1424
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1212
1425
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1213
1426
  const parseCtx = {
@@ -1290,6 +1503,23 @@ class Command {
1290
1503
  i += 1;
1291
1504
  continue;
1292
1505
  }
1506
+ if (opt.args === 'optional') {
1507
+ const eqIdx = token.resolved.indexOf('=');
1508
+ if (eqIdx !== -1) {
1509
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1510
+ i += 1;
1511
+ continue;
1512
+ }
1513
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1514
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1515
+ i += 1;
1516
+ }
1517
+ else {
1518
+ opts[opt.long] = undefined;
1519
+ }
1520
+ i += 1;
1521
+ continue;
1522
+ }
1293
1523
  if (opt.args === 'variadic') {
1294
1524
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1295
1525
  const eqIdx = token.resolved.indexOf('=');
@@ -1309,7 +1539,7 @@ class Command {
1309
1539
  i += 1;
1310
1540
  }
1311
1541
  for (const opt of allOptions) {
1312
- if (opt.required && opts[opt.long] === undefined) {
1542
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1313
1543
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1314
1544
  }
1315
1545
  }
@@ -1335,8 +1565,8 @@ class Command {
1335
1565
  return opt.coerce(rawValue);
1336
1566
  }
1337
1567
  if (opt.type === 'number') {
1338
- const num = Number(rawValue);
1339
- if (Number.isNaN(num)) {
1568
+ const num = parsePrimitiveNumber(rawValue);
1569
+ if (num === undefined) {
1340
1570
  throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1341
1571
  }
1342
1572
  return num;
@@ -1346,12 +1576,37 @@ class Command {
1346
1576
  #parseArguments(rawArgs) {
1347
1577
  const argumentDefs = this.#arguments;
1348
1578
  const args = {};
1349
- const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
1350
- if (rawArgs.length < requiredCount) {
1351
- const missing = argumentDefs
1352
- .filter(a => a.kind === 'required')
1353
- .slice(rawArgs.length)
1354
- .map(a => a.name);
1579
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1580
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1581
+ }
1582
+ const missing = [];
1583
+ let remaining = rawArgs.length;
1584
+ for (const def of argumentDefs) {
1585
+ if (def.kind === 'required') {
1586
+ if (remaining === 0) {
1587
+ missing.push(def.name);
1588
+ }
1589
+ else {
1590
+ remaining -= 1;
1591
+ }
1592
+ continue;
1593
+ }
1594
+ if (def.kind === 'optional') {
1595
+ if (remaining > 0) {
1596
+ remaining -= 1;
1597
+ }
1598
+ continue;
1599
+ }
1600
+ if (def.kind === 'some') {
1601
+ if (remaining === 0) {
1602
+ missing.push(def.name);
1603
+ }
1604
+ remaining = 0;
1605
+ continue;
1606
+ }
1607
+ remaining = 0;
1608
+ }
1609
+ if (missing.length > 0) {
1355
1610
  throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
1356
1611
  }
1357
1612
  let index = 0;
@@ -1362,41 +1617,101 @@ class Command {
1362
1617
  index = rawArgs.length;
1363
1618
  break;
1364
1619
  }
1365
- const raw = rawArgs[index];
1366
- if (raw === undefined) {
1367
- if (def.kind === 'optional') {
1620
+ if (def.kind === 'some') {
1621
+ const rest = rawArgs.slice(index);
1622
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1623
+ index = rawArgs.length;
1624
+ break;
1625
+ }
1626
+ if (def.kind === 'optional') {
1627
+ const raw = rawArgs[index];
1628
+ if (raw === undefined) {
1368
1629
  args[def.name] = def.default ?? undefined;
1369
1630
  continue;
1370
1631
  }
1371
- }
1372
- else {
1373
1632
  args[def.name] = this.#convertArgument(def, raw);
1374
1633
  index += 1;
1634
+ continue;
1375
1635
  }
1636
+ const raw = rawArgs[index];
1637
+ args[def.name] = this.#convertArgument(def, raw);
1638
+ index += 1;
1376
1639
  }
1377
- const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
1378
- if (!hasVariadic && index < rawArgs.length) {
1640
+ const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1641
+ if (!hasRestArgument && index < rawArgs.length) {
1379
1642
  throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
1380
1643
  }
1381
1644
  return { args, rawArgs };
1382
1645
  }
1383
1646
  #convertArgument(def, raw) {
1647
+ let value;
1384
1648
  if (def.coerce) {
1385
1649
  try {
1386
- return def.coerce(raw);
1650
+ value = def.coerce(raw);
1387
1651
  }
1388
1652
  catch {
1389
1653
  throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1390
1654
  }
1391
1655
  }
1392
- if (def.type === 'number') {
1393
- const n = Number(raw);
1394
- if (Number.isNaN(n)) {
1395
- throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1656
+ else {
1657
+ value = raw;
1658
+ }
1659
+ if (typeof value !== 'string') {
1660
+ throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
1661
+ }
1662
+ if (def.type === 'choice') {
1663
+ const choices = def.choices ?? [];
1664
+ if (!choices.includes(value)) {
1665
+ throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
1666
+ .map(choice => JSON.stringify(choice))
1667
+ .join(', ')}`, this.#getCommandPath());
1668
+ }
1669
+ }
1670
+ return value;
1671
+ }
1672
+ #assertUnknownSubcommand(userTailArgv) {
1673
+ if (this.#subcommandsList.length === 0) {
1674
+ return;
1675
+ }
1676
+ const token = userTailArgv[0];
1677
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1678
+ return;
1679
+ }
1680
+ if (this.#findSubcommandEntry(token) !== undefined) {
1681
+ return;
1682
+ }
1683
+ const hints = [];
1684
+ if (this.#arguments.length === 0) {
1685
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1686
+ }
1687
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1688
+ if (candidate !== undefined) {
1689
+ hints.push(`Hint: did you mean "${candidate}"?`);
1690
+ }
1691
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1692
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1693
+ }
1694
+ #resolveDidYouMeanSubcommandName(token) {
1695
+ const source = normalizeSubcommandNameForDistance(token);
1696
+ let minDistance = Number.POSITIVE_INFINITY;
1697
+ let bestName;
1698
+ let isUniqueBest = false;
1699
+ for (const entry of this.#subcommandsList) {
1700
+ const target = normalizeSubcommandNameForDistance(entry.name);
1701
+ const distance = levenshteinDistance(source, target);
1702
+ if (distance < minDistance) {
1703
+ minDistance = distance;
1704
+ bestName = entry.name;
1705
+ isUniqueBest = true;
1706
+ }
1707
+ else if (distance === minDistance) {
1708
+ isUniqueBest = false;
1396
1709
  }
1397
- return n;
1398
1710
  }
1399
- return raw;
1711
+ if (minDistance <= 2 && isUniqueBest) {
1712
+ return bestName;
1713
+ }
1714
+ return undefined;
1400
1715
  }
1401
1716
  #hasUserOption(long) {
1402
1717
  return this.#options.some(option => option.long === long);
@@ -1441,11 +1756,9 @@ class Command {
1441
1756
  return optionPolicyMap;
1442
1757
  }
1443
1758
  #mustGetOptionPolicy(optionPolicyMap, cmd) {
1444
- const policy = optionPolicyMap.get(cmd);
1445
- if (policy !== undefined) {
1446
- return policy;
1447
- }
1448
- throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
1759
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1760
+ optionPolicyMap.set(cmd, policy);
1761
+ return policy;
1449
1762
  }
1450
1763
  #validateMergedShortOptions(chain, optionPolicyMap) {
1451
1764
  const mergedByLong = new Map();
@@ -1474,7 +1787,10 @@ class Command {
1474
1787
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1475
1788
  }
1476
1789
  if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
1477
- throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
1790
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1791
+ }
1792
+ if (opt.type === 'number' && opt.args === 'optional') {
1793
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1478
1794
  }
1479
1795
  if (opt.long.startsWith('no')) {
1480
1796
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1482,12 +1798,18 @@ class Command {
1482
1798
  if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
1483
1799
  throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
1484
1800
  }
1801
+ if (opt.short !== undefined && opt.short.length !== 1) {
1802
+ throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
1803
+ }
1485
1804
  if (opt.required && opt.default !== undefined) {
1486
1805
  throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
1487
1806
  }
1488
1807
  if (opt.type === 'boolean' && opt.required) {
1489
1808
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1490
1809
  }
1810
+ if (opt.required && opt.args !== 'required') {
1811
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1812
+ }
1491
1813
  }
1492
1814
  #checkOptionUniqueness(opt) {
1493
1815
  if (this.#options.some(o => o.long === opt.long)) {
@@ -1498,24 +1820,58 @@ class Command {
1498
1820
  }
1499
1821
  }
1500
1822
  #validateArgumentConfig(arg) {
1501
- if (arg.kind === 'required' && arg.default !== undefined) {
1502
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
1823
+ if (arg.kind !== 'required' &&
1824
+ arg.kind !== 'optional' &&
1825
+ arg.kind !== 'variadic' &&
1826
+ arg.kind !== 'some') {
1827
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
1828
+ }
1829
+ if (arg.type !== 'string' && arg.type !== 'choice') {
1830
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
1831
+ }
1832
+ if (arg.default !== undefined && arg.kind !== 'optional') {
1833
+ throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
1834
+ }
1835
+ if (arg.type === 'string' && arg.choices !== undefined) {
1836
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
1503
1837
  }
1504
- if (arg.kind === 'variadic') {
1505
- if (this.#arguments.some(a => a.kind === 'variadic')) {
1506
- throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
1838
+ if (arg.type === 'choice') {
1839
+ if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
1840
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
1841
+ }
1842
+ if (arg.choices.some(choice => typeof choice !== 'string')) {
1843
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
1844
+ }
1845
+ }
1846
+ if (arg.default !== undefined) {
1847
+ this.#validateArgumentDefaultValue(arg);
1848
+ }
1849
+ if (arg.kind === 'variadic' || arg.kind === 'some') {
1850
+ if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
1851
+ throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
1507
1852
  }
1508
1853
  }
1509
1854
  if (this.#arguments.length > 0) {
1510
1855
  const last = this.#arguments[this.#arguments.length - 1];
1511
- if (last.kind === 'variadic') {
1512
- throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
1856
+ if (last.kind === 'variadic' || last.kind === 'some') {
1857
+ throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
1513
1858
  }
1514
1859
  }
1515
1860
  if (arg.kind === 'required') {
1516
- const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
1861
+ const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
1517
1862
  if (hasOptional) {
1518
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
1863
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
1864
+ }
1865
+ }
1866
+ }
1867
+ #validateArgumentDefaultValue(arg) {
1868
+ if (typeof arg.default !== 'string') {
1869
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
1870
+ }
1871
+ if (arg.type === 'choice') {
1872
+ const choices = arg.choices ?? [];
1873
+ if (!choices.includes(arg.default)) {
1874
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
1519
1875
  }
1520
1876
  }
1521
1877
  }