@guanghechen/commander 4.7.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Align commander optional/subcommand parsing with spec and keep release notes in sync. Upgrade
8
+ tooling dependencies and pin changesets packages to fixed versions.
9
+
3
10
  ## 4.7.2
4
11
 
5
12
  ### Patch Changes
@@ -294,6 +294,30 @@ function parsePrimitiveNumber(rawValue) {
294
294
  }
295
295
  return value;
296
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
+ }
297
321
  function tokenizeLongOption(arg, commandPath) {
298
322
  const eqIdx = arg.indexOf('=');
299
323
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -696,7 +720,10 @@ class Command {
696
720
  const kebabLong = camelToKebabCase(opt.long);
697
721
  let sig = opt.short ? `-${opt.short}, ` : ' ';
698
722
  sig += `--${kebabLong}`;
699
- if (opt.args !== 'none') {
723
+ if (opt.args === 'optional') {
724
+ sig += ' [value]';
725
+ }
726
+ else if (opt.args !== 'none') {
700
727
  sig += ' <value>';
701
728
  }
702
729
  let desc = opt.desc;
@@ -1315,6 +1342,14 @@ class Command {
1315
1342
  consumed.push(tokens[i]);
1316
1343
  }
1317
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
+ }
1318
1353
  else if (opt.args === 'variadic') {
1319
1354
  if (!token.resolved.includes('=')) {
1320
1355
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1340,6 +1375,12 @@ class Command {
1340
1375
  consumed.push(tokens[i]);
1341
1376
  }
1342
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
+ }
1343
1384
  else if (opt.args === 'variadic') {
1344
1385
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1345
1386
  i += 1;
@@ -1381,6 +1422,7 @@ class Command {
1381
1422
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1382
1423
  }
1383
1424
  }
1425
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1384
1426
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1385
1427
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1386
1428
  const parseCtx = {
@@ -1463,6 +1505,23 @@ class Command {
1463
1505
  i += 1;
1464
1506
  continue;
1465
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
+ }
1466
1525
  if (opt.args === 'variadic') {
1467
1526
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1468
1527
  const eqIdx = token.resolved.indexOf('=');
@@ -1482,7 +1541,7 @@ class Command {
1482
1541
  i += 1;
1483
1542
  }
1484
1543
  for (const opt of allOptions) {
1485
- if (opt.required && opts[opt.long] === undefined) {
1544
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1486
1545
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1487
1546
  }
1488
1547
  }
@@ -1519,6 +1578,9 @@ class Command {
1519
1578
  #parseArguments(rawArgs) {
1520
1579
  const argumentDefs = this.#arguments;
1521
1580
  const args = {};
1581
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1582
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1583
+ }
1522
1584
  const missing = [];
1523
1585
  let remaining = rawArgs.length;
1524
1586
  for (const def of argumentDefs) {
@@ -1559,25 +1621,23 @@ class Command {
1559
1621
  }
1560
1622
  if (def.kind === 'some') {
1561
1623
  const rest = rawArgs.slice(index);
1562
- if (rest.length === 0) {
1563
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1564
- }
1565
1624
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1566
1625
  index = rawArgs.length;
1567
1626
  break;
1568
1627
  }
1569
- const raw = rawArgs[index];
1570
- if (raw === undefined) {
1571
- if (def.kind === 'optional') {
1628
+ if (def.kind === 'optional') {
1629
+ const raw = rawArgs[index];
1630
+ if (raw === undefined) {
1572
1631
  args[def.name] = def.default ?? undefined;
1573
1632
  continue;
1574
1633
  }
1575
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1576
- }
1577
- else {
1578
1634
  args[def.name] = this.#convertArgument(def, raw);
1579
1635
  index += 1;
1636
+ continue;
1580
1637
  }
1638
+ const raw = rawArgs[index];
1639
+ args[def.name] = this.#convertArgument(def, raw);
1640
+ index += 1;
1581
1641
  }
1582
1642
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1583
1643
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1611,6 +1671,50 @@ class Command {
1611
1671
  }
1612
1672
  return value;
1613
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;
1711
+ }
1712
+ }
1713
+ if (minDistance <= 2 && isUniqueBest) {
1714
+ return bestName;
1715
+ }
1716
+ return undefined;
1717
+ }
1614
1718
  #hasUserOption(long) {
1615
1719
  return this.#options.some(option => option.long === long);
1616
1720
  }
@@ -1654,11 +1758,9 @@ class Command {
1654
1758
  return optionPolicyMap;
1655
1759
  }
1656
1760
  #mustGetOptionPolicy(optionPolicyMap, cmd) {
1657
- const policy = optionPolicyMap.get(cmd);
1658
- if (policy !== undefined) {
1659
- return policy;
1660
- }
1661
- 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;
1662
1764
  }
1663
1765
  #validateMergedShortOptions(chain, optionPolicyMap) {
1664
1766
  const mergedByLong = new Map();
@@ -1687,7 +1789,10 @@ class Command {
1687
1789
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1688
1790
  }
1689
1791
  if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
1690
- 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());
1691
1796
  }
1692
1797
  if (opt.long.startsWith('no')) {
1693
1798
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1704,6 +1809,9 @@ class Command {
1704
1809
  if (opt.type === 'boolean' && opt.required) {
1705
1810
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1706
1811
  }
1812
+ if (opt.required && opt.args !== 'required') {
1813
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1814
+ }
1707
1815
  }
1708
1816
  #checkOptionUniqueness(opt) {
1709
1817
  if (this.#options.some(o => o.long === opt.long)) {