@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix commander completion metadata and negative option rules.
8
+ - Include built-in control options (help/version) in completion metadata.
9
+ - Prevent generating negative completions for reserved controls (--no-help/--no-version).
10
+ - Align completion option metadata with explicit type/args semantics.
11
+
12
+ ## 4.7.3
13
+
14
+ ### Patch Changes
15
+
16
+ - Align commander optional/subcommand parsing with spec and keep release notes in sync. Upgrade
17
+ tooling dependencies and pin changesets packages to fixed versions.
18
+
3
19
  ## 4.7.2
4
20
 
5
21
  ### 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;
@@ -837,7 +864,15 @@ class Command {
837
864
  return ` ${outputLabel} ${desc}`;
838
865
  }
839
866
  getCompletionMeta() {
840
- const allOptions = this.#resolveOptionPolicy().mergedOptions;
867
+ const optionMap = new Map();
868
+ for (const option of this.#resolveOptionPolicy().mergedOptions) {
869
+ optionMap.set(option.long, option);
870
+ }
871
+ optionMap.set('help', BUILTIN_HELP_OPTION);
872
+ if (this.#supportsBuiltinVersion()) {
873
+ optionMap.set('version', BUILTIN_VERSION_OPTION);
874
+ }
875
+ const allOptions = Array.from(optionMap.values());
841
876
  const options = [];
842
877
  const argumentsMeta = [];
843
878
  for (const opt of allOptions) {
@@ -845,7 +880,8 @@ class Command {
845
880
  long: opt.long,
846
881
  short: opt.short,
847
882
  desc: opt.desc,
848
- takesValue: opt.args !== 'none',
883
+ type: opt.type,
884
+ args: opt.args,
849
885
  choices: opt.choices?.map(choice => String(choice)),
850
886
  });
851
887
  }
@@ -1315,6 +1351,14 @@ class Command {
1315
1351
  consumed.push(tokens[i]);
1316
1352
  }
1317
1353
  }
1354
+ else if (opt.args === 'optional') {
1355
+ if (!token.resolved.includes('=') &&
1356
+ i + 1 < tokens.length &&
1357
+ tokens[i + 1].type === 'none') {
1358
+ i += 1;
1359
+ consumed.push(tokens[i]);
1360
+ }
1361
+ }
1318
1362
  else if (opt.args === 'variadic') {
1319
1363
  if (!token.resolved.includes('=')) {
1320
1364
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1340,6 +1384,12 @@ class Command {
1340
1384
  consumed.push(tokens[i]);
1341
1385
  }
1342
1386
  }
1387
+ else if (opt.args === 'optional') {
1388
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1389
+ i += 1;
1390
+ consumed.push(tokens[i]);
1391
+ }
1392
+ }
1343
1393
  else if (opt.args === 'variadic') {
1344
1394
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1345
1395
  i += 1;
@@ -1381,6 +1431,7 @@ class Command {
1381
1431
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1382
1432
  }
1383
1433
  }
1434
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1384
1435
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1385
1436
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1386
1437
  const parseCtx = {
@@ -1463,6 +1514,23 @@ class Command {
1463
1514
  i += 1;
1464
1515
  continue;
1465
1516
  }
1517
+ if (opt.args === 'optional') {
1518
+ const eqIdx = token.resolved.indexOf('=');
1519
+ if (eqIdx !== -1) {
1520
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1521
+ i += 1;
1522
+ continue;
1523
+ }
1524
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1525
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1526
+ i += 1;
1527
+ }
1528
+ else {
1529
+ opts[opt.long] = undefined;
1530
+ }
1531
+ i += 1;
1532
+ continue;
1533
+ }
1466
1534
  if (opt.args === 'variadic') {
1467
1535
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1468
1536
  const eqIdx = token.resolved.indexOf('=');
@@ -1482,7 +1550,7 @@ class Command {
1482
1550
  i += 1;
1483
1551
  }
1484
1552
  for (const opt of allOptions) {
1485
- if (opt.required && opts[opt.long] === undefined) {
1553
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1486
1554
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1487
1555
  }
1488
1556
  }
@@ -1519,6 +1587,9 @@ class Command {
1519
1587
  #parseArguments(rawArgs) {
1520
1588
  const argumentDefs = this.#arguments;
1521
1589
  const args = {};
1590
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1591
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1592
+ }
1522
1593
  const missing = [];
1523
1594
  let remaining = rawArgs.length;
1524
1595
  for (const def of argumentDefs) {
@@ -1559,25 +1630,23 @@ class Command {
1559
1630
  }
1560
1631
  if (def.kind === 'some') {
1561
1632
  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
1633
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1566
1634
  index = rawArgs.length;
1567
1635
  break;
1568
1636
  }
1569
- const raw = rawArgs[index];
1570
- if (raw === undefined) {
1571
- if (def.kind === 'optional') {
1637
+ if (def.kind === 'optional') {
1638
+ const raw = rawArgs[index];
1639
+ if (raw === undefined) {
1572
1640
  args[def.name] = def.default ?? undefined;
1573
1641
  continue;
1574
1642
  }
1575
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1576
- }
1577
- else {
1578
1643
  args[def.name] = this.#convertArgument(def, raw);
1579
1644
  index += 1;
1645
+ continue;
1580
1646
  }
1647
+ const raw = rawArgs[index];
1648
+ args[def.name] = this.#convertArgument(def, raw);
1649
+ index += 1;
1581
1650
  }
1582
1651
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1583
1652
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1611,6 +1680,50 @@ class Command {
1611
1680
  }
1612
1681
  return value;
1613
1682
  }
1683
+ #assertUnknownSubcommand(userTailArgv) {
1684
+ if (this.#subcommandsList.length === 0) {
1685
+ return;
1686
+ }
1687
+ const token = userTailArgv[0];
1688
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1689
+ return;
1690
+ }
1691
+ if (this.#findSubcommandEntry(token) !== undefined) {
1692
+ return;
1693
+ }
1694
+ const hints = [];
1695
+ if (this.#arguments.length === 0) {
1696
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1697
+ }
1698
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1699
+ if (candidate !== undefined) {
1700
+ hints.push(`Hint: did you mean "${candidate}"?`);
1701
+ }
1702
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1703
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1704
+ }
1705
+ #resolveDidYouMeanSubcommandName(token) {
1706
+ const source = normalizeSubcommandNameForDistance(token);
1707
+ let minDistance = Number.POSITIVE_INFINITY;
1708
+ let bestName;
1709
+ let isUniqueBest = false;
1710
+ for (const entry of this.#subcommandsList) {
1711
+ const target = normalizeSubcommandNameForDistance(entry.name);
1712
+ const distance = levenshteinDistance(source, target);
1713
+ if (distance < minDistance) {
1714
+ minDistance = distance;
1715
+ bestName = entry.name;
1716
+ isUniqueBest = true;
1717
+ }
1718
+ else if (distance === minDistance) {
1719
+ isUniqueBest = false;
1720
+ }
1721
+ }
1722
+ if (minDistance <= 2 && isUniqueBest) {
1723
+ return bestName;
1724
+ }
1725
+ return undefined;
1726
+ }
1614
1727
  #hasUserOption(long) {
1615
1728
  return this.#options.some(option => option.long === long);
1616
1729
  }
@@ -1654,11 +1767,9 @@ class Command {
1654
1767
  return optionPolicyMap;
1655
1768
  }
1656
1769
  #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());
1770
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1771
+ optionPolicyMap.set(cmd, policy);
1772
+ return policy;
1662
1773
  }
1663
1774
  #validateMergedShortOptions(chain, optionPolicyMap) {
1664
1775
  const mergedByLong = new Map();
@@ -1687,7 +1798,10 @@ class Command {
1687
1798
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1688
1799
  }
1689
1800
  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());
1801
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1802
+ }
1803
+ if (opt.type === 'number' && opt.args === 'optional') {
1804
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1691
1805
  }
1692
1806
  if (opt.long.startsWith('no')) {
1693
1807
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1704,6 +1818,9 @@ class Command {
1704
1818
  if (opt.type === 'boolean' && opt.required) {
1705
1819
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1706
1820
  }
1821
+ if (opt.required && opt.args !== 'required') {
1822
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1823
+ }
1707
1824
  }
1708
1825
  #checkOptionUniqueness(opt) {
1709
1826
  if (this.#options.some(o => o.long === opt.long)) {