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