@guanghechen/commander 4.7.2 → 4.7.4

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.
@@ -292,6 +292,30 @@ function parsePrimitiveNumber(rawValue) {
292
292
  }
293
293
  return value;
294
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
+ }
295
319
  function tokenizeLongOption(arg, commandPath) {
296
320
  const eqIdx = arg.indexOf('=');
297
321
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -694,7 +718,10 @@ class Command {
694
718
  const kebabLong = camelToKebabCase(opt.long);
695
719
  let sig = opt.short ? `-${opt.short}, ` : ' ';
696
720
  sig += `--${kebabLong}`;
697
- if (opt.args !== 'none') {
721
+ if (opt.args === 'optional') {
722
+ sig += ' [value]';
723
+ }
724
+ else if (opt.args !== 'none') {
698
725
  sig += ' <value>';
699
726
  }
700
727
  let desc = opt.desc;
@@ -835,7 +862,15 @@ class Command {
835
862
  return ` ${outputLabel} ${desc}`;
836
863
  }
837
864
  getCompletionMeta() {
838
- const allOptions = this.#resolveOptionPolicy().mergedOptions;
865
+ const optionMap = new Map();
866
+ for (const option of this.#resolveOptionPolicy().mergedOptions) {
867
+ optionMap.set(option.long, option);
868
+ }
869
+ optionMap.set('help', BUILTIN_HELP_OPTION);
870
+ if (this.#supportsBuiltinVersion()) {
871
+ optionMap.set('version', BUILTIN_VERSION_OPTION);
872
+ }
873
+ const allOptions = Array.from(optionMap.values());
839
874
  const options = [];
840
875
  const argumentsMeta = [];
841
876
  for (const opt of allOptions) {
@@ -843,7 +878,8 @@ class Command {
843
878
  long: opt.long,
844
879
  short: opt.short,
845
880
  desc: opt.desc,
846
- takesValue: opt.args !== 'none',
881
+ type: opt.type,
882
+ args: opt.args,
847
883
  choices: opt.choices?.map(choice => String(choice)),
848
884
  });
849
885
  }
@@ -1313,6 +1349,14 @@ class Command {
1313
1349
  consumed.push(tokens[i]);
1314
1350
  }
1315
1351
  }
1352
+ else if (opt.args === 'optional') {
1353
+ if (!token.resolved.includes('=') &&
1354
+ i + 1 < tokens.length &&
1355
+ tokens[i + 1].type === 'none') {
1356
+ i += 1;
1357
+ consumed.push(tokens[i]);
1358
+ }
1359
+ }
1316
1360
  else if (opt.args === 'variadic') {
1317
1361
  if (!token.resolved.includes('=')) {
1318
1362
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1338,6 +1382,12 @@ class Command {
1338
1382
  consumed.push(tokens[i]);
1339
1383
  }
1340
1384
  }
1385
+ else if (opt.args === 'optional') {
1386
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1387
+ i += 1;
1388
+ consumed.push(tokens[i]);
1389
+ }
1390
+ }
1341
1391
  else if (opt.args === 'variadic') {
1342
1392
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1343
1393
  i += 1;
@@ -1379,6 +1429,7 @@ class Command {
1379
1429
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1380
1430
  }
1381
1431
  }
1432
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1382
1433
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1383
1434
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1384
1435
  const parseCtx = {
@@ -1461,6 +1512,23 @@ class Command {
1461
1512
  i += 1;
1462
1513
  continue;
1463
1514
  }
1515
+ if (opt.args === 'optional') {
1516
+ const eqIdx = token.resolved.indexOf('=');
1517
+ if (eqIdx !== -1) {
1518
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1519
+ i += 1;
1520
+ continue;
1521
+ }
1522
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1523
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1524
+ i += 1;
1525
+ }
1526
+ else {
1527
+ opts[opt.long] = undefined;
1528
+ }
1529
+ i += 1;
1530
+ continue;
1531
+ }
1464
1532
  if (opt.args === 'variadic') {
1465
1533
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1466
1534
  const eqIdx = token.resolved.indexOf('=');
@@ -1480,7 +1548,7 @@ class Command {
1480
1548
  i += 1;
1481
1549
  }
1482
1550
  for (const opt of allOptions) {
1483
- if (opt.required && opts[opt.long] === undefined) {
1551
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1484
1552
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1485
1553
  }
1486
1554
  }
@@ -1517,6 +1585,9 @@ class Command {
1517
1585
  #parseArguments(rawArgs) {
1518
1586
  const argumentDefs = this.#arguments;
1519
1587
  const args = {};
1588
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1589
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1590
+ }
1520
1591
  const missing = [];
1521
1592
  let remaining = rawArgs.length;
1522
1593
  for (const def of argumentDefs) {
@@ -1557,25 +1628,23 @@ class Command {
1557
1628
  }
1558
1629
  if (def.kind === 'some') {
1559
1630
  const rest = rawArgs.slice(index);
1560
- if (rest.length === 0) {
1561
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1562
- }
1563
1631
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1564
1632
  index = rawArgs.length;
1565
1633
  break;
1566
1634
  }
1567
- const raw = rawArgs[index];
1568
- if (raw === undefined) {
1569
- if (def.kind === 'optional') {
1635
+ if (def.kind === 'optional') {
1636
+ const raw = rawArgs[index];
1637
+ if (raw === undefined) {
1570
1638
  args[def.name] = def.default ?? undefined;
1571
1639
  continue;
1572
1640
  }
1573
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1574
- }
1575
- else {
1576
1641
  args[def.name] = this.#convertArgument(def, raw);
1577
1642
  index += 1;
1643
+ continue;
1578
1644
  }
1645
+ const raw = rawArgs[index];
1646
+ args[def.name] = this.#convertArgument(def, raw);
1647
+ index += 1;
1579
1648
  }
1580
1649
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1581
1650
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1609,6 +1678,50 @@ class Command {
1609
1678
  }
1610
1679
  return value;
1611
1680
  }
1681
+ #assertUnknownSubcommand(userTailArgv) {
1682
+ if (this.#subcommandsList.length === 0) {
1683
+ return;
1684
+ }
1685
+ const token = userTailArgv[0];
1686
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1687
+ return;
1688
+ }
1689
+ if (this.#findSubcommandEntry(token) !== undefined) {
1690
+ return;
1691
+ }
1692
+ const hints = [];
1693
+ if (this.#arguments.length === 0) {
1694
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1695
+ }
1696
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1697
+ if (candidate !== undefined) {
1698
+ hints.push(`Hint: did you mean "${candidate}"?`);
1699
+ }
1700
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1701
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1702
+ }
1703
+ #resolveDidYouMeanSubcommandName(token) {
1704
+ const source = normalizeSubcommandNameForDistance(token);
1705
+ let minDistance = Number.POSITIVE_INFINITY;
1706
+ let bestName;
1707
+ let isUniqueBest = false;
1708
+ for (const entry of this.#subcommandsList) {
1709
+ const target = normalizeSubcommandNameForDistance(entry.name);
1710
+ const distance = levenshteinDistance(source, target);
1711
+ if (distance < minDistance) {
1712
+ minDistance = distance;
1713
+ bestName = entry.name;
1714
+ isUniqueBest = true;
1715
+ }
1716
+ else if (distance === minDistance) {
1717
+ isUniqueBest = false;
1718
+ }
1719
+ }
1720
+ if (minDistance <= 2 && isUniqueBest) {
1721
+ return bestName;
1722
+ }
1723
+ return undefined;
1724
+ }
1612
1725
  #hasUserOption(long) {
1613
1726
  return this.#options.some(option => option.long === long);
1614
1727
  }
@@ -1652,11 +1765,9 @@ class Command {
1652
1765
  return optionPolicyMap;
1653
1766
  }
1654
1767
  #mustGetOptionPolicy(optionPolicyMap, cmd) {
1655
- const policy = optionPolicyMap.get(cmd);
1656
- if (policy !== undefined) {
1657
- return policy;
1658
- }
1659
- throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
1768
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1769
+ optionPolicyMap.set(cmd, policy);
1770
+ return policy;
1660
1771
  }
1661
1772
  #validateMergedShortOptions(chain, optionPolicyMap) {
1662
1773
  const mergedByLong = new Map();
@@ -1685,7 +1796,10 @@ class Command {
1685
1796
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1686
1797
  }
1687
1798
  if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
1688
- throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
1799
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1800
+ }
1801
+ if (opt.type === 'number' && opt.args === 'optional') {
1802
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1689
1803
  }
1690
1804
  if (opt.long.startsWith('no')) {
1691
1805
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1702,6 +1816,9 @@ class Command {
1702
1816
  if (opt.type === 'boolean' && opt.required) {
1703
1817
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1704
1818
  }
1819
+ if (opt.required && opt.args !== 'required') {
1820
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1821
+ }
1705
1822
  }
1706
1823
  #checkOptionUniqueness(opt) {
1707
1824
  if (this.#options.some(o => o.long === opt.long)) {