@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/lib/cjs/node.cjs CHANGED
@@ -307,6 +307,30 @@ function parsePrimitiveNumber(rawValue) {
307
307
  }
308
308
  return value;
309
309
  }
310
+ function normalizeSubcommandNameForDistance(name) {
311
+ return camelToKebabCase$1(name).toLowerCase();
312
+ }
313
+ function levenshteinDistance(left, right) {
314
+ if (left === right) {
315
+ return 0;
316
+ }
317
+ if (left.length === 0) {
318
+ return right.length;
319
+ }
320
+ if (right.length === 0) {
321
+ return left.length;
322
+ }
323
+ let prev = Array.from({ length: right.length + 1 }, (_, i) => i);
324
+ for (let i = 0; i < left.length; i += 1) {
325
+ const current = [i + 1];
326
+ for (let j = 0; j < right.length; j += 1) {
327
+ const substitutionCost = left[i] === right[j] ? 0 : 1;
328
+ current[j + 1] = Math.min(current[j] + 1, prev[j + 1] + 1, prev[j] + substitutionCost);
329
+ }
330
+ prev = current;
331
+ }
332
+ return prev[right.length];
333
+ }
310
334
  function tokenizeLongOption(arg, commandPath) {
311
335
  const eqIdx = arg.indexOf('=');
312
336
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -709,7 +733,10 @@ class Command {
709
733
  const kebabLong = camelToKebabCase$1(opt.long);
710
734
  let sig = opt.short ? `-${opt.short}, ` : ' ';
711
735
  sig += `--${kebabLong}`;
712
- if (opt.args !== 'none') {
736
+ if (opt.args === 'optional') {
737
+ sig += ' [value]';
738
+ }
739
+ else if (opt.args !== 'none') {
713
740
  sig += ' <value>';
714
741
  }
715
742
  let desc = opt.desc;
@@ -1328,6 +1355,14 @@ class Command {
1328
1355
  consumed.push(tokens[i]);
1329
1356
  }
1330
1357
  }
1358
+ else if (opt.args === 'optional') {
1359
+ if (!token.resolved.includes('=') &&
1360
+ i + 1 < tokens.length &&
1361
+ tokens[i + 1].type === 'none') {
1362
+ i += 1;
1363
+ consumed.push(tokens[i]);
1364
+ }
1365
+ }
1331
1366
  else if (opt.args === 'variadic') {
1332
1367
  if (!token.resolved.includes('=')) {
1333
1368
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1353,6 +1388,12 @@ class Command {
1353
1388
  consumed.push(tokens[i]);
1354
1389
  }
1355
1390
  }
1391
+ else if (opt.args === 'optional') {
1392
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1393
+ i += 1;
1394
+ consumed.push(tokens[i]);
1395
+ }
1396
+ }
1356
1397
  else if (opt.args === 'variadic') {
1357
1398
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1358
1399
  i += 1;
@@ -1394,6 +1435,7 @@ class Command {
1394
1435
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1395
1436
  }
1396
1437
  }
1438
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1397
1439
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1398
1440
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1399
1441
  const parseCtx = {
@@ -1476,6 +1518,23 @@ class Command {
1476
1518
  i += 1;
1477
1519
  continue;
1478
1520
  }
1521
+ if (opt.args === 'optional') {
1522
+ const eqIdx = token.resolved.indexOf('=');
1523
+ if (eqIdx !== -1) {
1524
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1525
+ i += 1;
1526
+ continue;
1527
+ }
1528
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1529
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1530
+ i += 1;
1531
+ }
1532
+ else {
1533
+ opts[opt.long] = undefined;
1534
+ }
1535
+ i += 1;
1536
+ continue;
1537
+ }
1479
1538
  if (opt.args === 'variadic') {
1480
1539
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1481
1540
  const eqIdx = token.resolved.indexOf('=');
@@ -1495,7 +1554,7 @@ class Command {
1495
1554
  i += 1;
1496
1555
  }
1497
1556
  for (const opt of allOptions) {
1498
- if (opt.required && opts[opt.long] === undefined) {
1557
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1499
1558
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
1500
1559
  }
1501
1560
  }
@@ -1532,6 +1591,9 @@ class Command {
1532
1591
  #parseArguments(rawArgs) {
1533
1592
  const argumentDefs = this.#arguments;
1534
1593
  const args = {};
1594
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1595
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1596
+ }
1535
1597
  const missing = [];
1536
1598
  let remaining = rawArgs.length;
1537
1599
  for (const def of argumentDefs) {
@@ -1572,25 +1634,23 @@ class Command {
1572
1634
  }
1573
1635
  if (def.kind === 'some') {
1574
1636
  const rest = rawArgs.slice(index);
1575
- if (rest.length === 0) {
1576
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1577
- }
1578
1637
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1579
1638
  index = rawArgs.length;
1580
1639
  break;
1581
1640
  }
1582
- const raw = rawArgs[index];
1583
- if (raw === undefined) {
1584
- if (def.kind === 'optional') {
1641
+ if (def.kind === 'optional') {
1642
+ const raw = rawArgs[index];
1643
+ if (raw === undefined) {
1585
1644
  args[def.name] = def.default ?? undefined;
1586
1645
  continue;
1587
1646
  }
1588
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1589
- }
1590
- else {
1591
1647
  args[def.name] = this.#convertArgument(def, raw);
1592
1648
  index += 1;
1649
+ continue;
1593
1650
  }
1651
+ const raw = rawArgs[index];
1652
+ args[def.name] = this.#convertArgument(def, raw);
1653
+ index += 1;
1594
1654
  }
1595
1655
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1596
1656
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1624,6 +1684,50 @@ class Command {
1624
1684
  }
1625
1685
  return value;
1626
1686
  }
1687
+ #assertUnknownSubcommand(userTailArgv) {
1688
+ if (this.#subcommandsList.length === 0) {
1689
+ return;
1690
+ }
1691
+ const token = userTailArgv[0];
1692
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1693
+ return;
1694
+ }
1695
+ if (this.#findSubcommandEntry(token) !== undefined) {
1696
+ return;
1697
+ }
1698
+ const hints = [];
1699
+ if (this.#arguments.length === 0) {
1700
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1701
+ }
1702
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1703
+ if (candidate !== undefined) {
1704
+ hints.push(`Hint: did you mean "${candidate}"?`);
1705
+ }
1706
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1707
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1708
+ }
1709
+ #resolveDidYouMeanSubcommandName(token) {
1710
+ const source = normalizeSubcommandNameForDistance(token);
1711
+ let minDistance = Number.POSITIVE_INFINITY;
1712
+ let bestName;
1713
+ let isUniqueBest = false;
1714
+ for (const entry of this.#subcommandsList) {
1715
+ const target = normalizeSubcommandNameForDistance(entry.name);
1716
+ const distance = levenshteinDistance(source, target);
1717
+ if (distance < minDistance) {
1718
+ minDistance = distance;
1719
+ bestName = entry.name;
1720
+ isUniqueBest = true;
1721
+ }
1722
+ else if (distance === minDistance) {
1723
+ isUniqueBest = false;
1724
+ }
1725
+ }
1726
+ if (minDistance <= 2 && isUniqueBest) {
1727
+ return bestName;
1728
+ }
1729
+ return undefined;
1730
+ }
1627
1731
  #hasUserOption(long) {
1628
1732
  return this.#options.some(option => option.long === long);
1629
1733
  }
@@ -1667,11 +1771,9 @@ class Command {
1667
1771
  return optionPolicyMap;
1668
1772
  }
1669
1773
  #mustGetOptionPolicy(optionPolicyMap, cmd) {
1670
- const policy = optionPolicyMap.get(cmd);
1671
- if (policy !== undefined) {
1672
- return policy;
1673
- }
1674
- throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
1774
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1775
+ optionPolicyMap.set(cmd, policy);
1776
+ return policy;
1675
1777
  }
1676
1778
  #validateMergedShortOptions(chain, optionPolicyMap) {
1677
1779
  const mergedByLong = new Map();
@@ -1700,7 +1802,10 @@ class Command {
1700
1802
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1701
1803
  }
1702
1804
  if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
1703
- throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
1805
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1806
+ }
1807
+ if (opt.type === 'number' && opt.args === 'optional') {
1808
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1704
1809
  }
1705
1810
  if (opt.long.startsWith('no')) {
1706
1811
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1717,6 +1822,9 @@ class Command {
1717
1822
  if (opt.type === 'boolean' && opt.required) {
1718
1823
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1719
1824
  }
1825
+ if (opt.required && opt.args !== 'required') {
1826
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1827
+ }
1720
1828
  }
1721
1829
  #checkOptionUniqueness(opt) {
1722
1830
  if (this.#options.some(o => o.long === opt.long)) {
@@ -2025,6 +2133,35 @@ class Coerce {
2025
2133
  function camelToKebabCase(str) {
2026
2134
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
2027
2135
  }
2136
+ const COMPLETION_SHELL_STATE = Symbol('completion-shell-state');
2137
+ function getCommandPath(ctx) {
2138
+ const names = ctx.chain
2139
+ .map(command => command.name)
2140
+ .filter((name) => Boolean(name));
2141
+ if (names.length > 0) {
2142
+ return names.join(' ');
2143
+ }
2144
+ return ctx.cmd.name ?? 'command';
2145
+ }
2146
+ function getCompletionShellState(ctx) {
2147
+ const host = ctx;
2148
+ host[COMPLETION_SHELL_STATE] ??= {};
2149
+ return host[COMPLETION_SHELL_STATE];
2150
+ }
2151
+ function registerCompletionShell(ctx, shell) {
2152
+ const state = getCompletionShellState(ctx);
2153
+ if (state.shell !== undefined && state.shell !== shell) {
2154
+ throw new CommanderError('OptionConflict', 'options "--bash", "--fish", and "--pwsh" are mutually exclusive', getCommandPath(ctx));
2155
+ }
2156
+ state.shell = shell;
2157
+ }
2158
+ function mustGetCompletionShell(ctx) {
2159
+ const state = getCompletionShellState(ctx);
2160
+ if (state.shell === undefined) {
2161
+ throw new CommanderError('MissingRequired', 'missing required option: one of "--bash", "--fish", or "--pwsh"', getCommandPath(ctx));
2162
+ }
2163
+ return state.shell;
2164
+ }
2028
2165
  class CompletionCommand extends Command {
2029
2166
  constructor(root, config = {}) {
2030
2167
  const programName = config.programName ?? root.name ?? 'program';
@@ -2038,45 +2175,45 @@ class CompletionCommand extends Command {
2038
2175
  type: 'boolean',
2039
2176
  args: 'none',
2040
2177
  desc: 'Generate Bash completion script',
2178
+ apply: (value, ctx) => {
2179
+ if (value === true) {
2180
+ registerCompletionShell(ctx, 'bash');
2181
+ }
2182
+ },
2041
2183
  })
2042
2184
  .option({
2043
2185
  long: 'fish',
2044
2186
  type: 'boolean',
2045
2187
  args: 'none',
2046
2188
  desc: 'Generate Fish completion script',
2189
+ apply: (value, ctx) => {
2190
+ if (value === true) {
2191
+ registerCompletionShell(ctx, 'fish');
2192
+ }
2193
+ },
2047
2194
  })
2048
2195
  .option({
2049
2196
  long: 'pwsh',
2050
2197
  type: 'boolean',
2051
2198
  args: 'none',
2052
2199
  desc: 'Generate PowerShell completion script',
2200
+ apply: (value, ctx) => {
2201
+ if (value === true) {
2202
+ registerCompletionShell(ctx, 'pwsh');
2203
+ }
2204
+ mustGetCompletionShell(ctx);
2205
+ },
2053
2206
  })
2054
2207
  .option({
2055
2208
  long: 'write',
2056
2209
  short: 'w',
2057
2210
  type: 'string',
2058
- args: 'required',
2059
- desc: 'Write to file (use shell default path if empty)',
2060
- default: undefined,
2211
+ args: 'optional',
2212
+ desc: 'Write to file (use shell default path when value is omitted or empty)',
2061
2213
  })
2062
- .action(({ opts }) => {
2214
+ .action(({ opts, ctx }) => {
2063
2215
  const meta = root.getCompletionMeta();
2064
- const selectedShells = [
2065
- opts['bash'] && 'bash',
2066
- opts['fish'] && 'fish',
2067
- opts['pwsh'] && 'pwsh',
2068
- ].filter(Boolean);
2069
- if (selectedShells.length === 0) {
2070
- console.error('Please specify a shell: --bash, --fish, or --pwsh');
2071
- process.exit(1);
2072
- return;
2073
- }
2074
- if (selectedShells.length > 1) {
2075
- console.error('Please specify only one shell option');
2076
- process.exit(1);
2077
- return;
2078
- }
2079
- const shell = selectedShells[0];
2216
+ const shell = mustGetCompletionShell(ctx);
2080
2217
  let script;
2081
2218
  switch (shell) {
2082
2219
  case 'bash':
@@ -2089,8 +2226,9 @@ class CompletionCommand extends Command {
2089
2226
  script = new PwshCompletion(meta, programName).generate();
2090
2227
  break;
2091
2228
  }
2092
- const writeOpt = opts['write'];
2093
- if (writeOpt !== undefined) {
2229
+ const hasWrite = Object.prototype.hasOwnProperty.call(opts, 'write');
2230
+ if (hasWrite) {
2231
+ const writeOpt = opts['write'];
2094
2232
  const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
2095
2233
  const expandedPath = expandHome(filePath);
2096
2234
  const dir = path.dirname(expandedPath);
@@ -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;
@@ -1313,6 +1340,14 @@ class Command {
1313
1340
  consumed.push(tokens[i]);
1314
1341
  }
1315
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
+ }
1316
1351
  else if (opt.args === 'variadic') {
1317
1352
  if (!token.resolved.includes('=')) {
1318
1353
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1338,6 +1373,12 @@ class Command {
1338
1373
  consumed.push(tokens[i]);
1339
1374
  }
1340
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
+ }
1341
1382
  else if (opt.args === 'variadic') {
1342
1383
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1343
1384
  i += 1;
@@ -1379,6 +1420,7 @@ class Command {
1379
1420
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1380
1421
  }
1381
1422
  }
1423
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1382
1424
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1383
1425
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1384
1426
  const parseCtx = {
@@ -1461,6 +1503,23 @@ class Command {
1461
1503
  i += 1;
1462
1504
  continue;
1463
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
+ }
1464
1523
  if (opt.args === 'variadic') {
1465
1524
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1466
1525
  const eqIdx = token.resolved.indexOf('=');
@@ -1480,7 +1539,7 @@ class Command {
1480
1539
  i += 1;
1481
1540
  }
1482
1541
  for (const opt of allOptions) {
1483
- if (opt.required && opts[opt.long] === undefined) {
1542
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1484
1543
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
1485
1544
  }
1486
1545
  }
@@ -1517,6 +1576,9 @@ class Command {
1517
1576
  #parseArguments(rawArgs) {
1518
1577
  const argumentDefs = this.#arguments;
1519
1578
  const args = {};
1579
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1580
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1581
+ }
1520
1582
  const missing = [];
1521
1583
  let remaining = rawArgs.length;
1522
1584
  for (const def of argumentDefs) {
@@ -1557,25 +1619,23 @@ class Command {
1557
1619
  }
1558
1620
  if (def.kind === 'some') {
1559
1621
  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
1622
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1564
1623
  index = rawArgs.length;
1565
1624
  break;
1566
1625
  }
1567
- const raw = rawArgs[index];
1568
- if (raw === undefined) {
1569
- if (def.kind === 'optional') {
1626
+ if (def.kind === 'optional') {
1627
+ const raw = rawArgs[index];
1628
+ if (raw === undefined) {
1570
1629
  args[def.name] = def.default ?? undefined;
1571
1630
  continue;
1572
1631
  }
1573
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1574
- }
1575
- else {
1576
1632
  args[def.name] = this.#convertArgument(def, raw);
1577
1633
  index += 1;
1634
+ continue;
1578
1635
  }
1636
+ const raw = rawArgs[index];
1637
+ args[def.name] = this.#convertArgument(def, raw);
1638
+ index += 1;
1579
1639
  }
1580
1640
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1581
1641
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1609,6 +1669,50 @@ class Command {
1609
1669
  }
1610
1670
  return value;
1611
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;
1709
+ }
1710
+ }
1711
+ if (minDistance <= 2 && isUniqueBest) {
1712
+ return bestName;
1713
+ }
1714
+ return undefined;
1715
+ }
1612
1716
  #hasUserOption(long) {
1613
1717
  return this.#options.some(option => option.long === long);
1614
1718
  }
@@ -1652,11 +1756,9 @@ class Command {
1652
1756
  return optionPolicyMap;
1653
1757
  }
1654
1758
  #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());
1759
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1760
+ optionPolicyMap.set(cmd, policy);
1761
+ return policy;
1660
1762
  }
1661
1763
  #validateMergedShortOptions(chain, optionPolicyMap) {
1662
1764
  const mergedByLong = new Map();
@@ -1685,7 +1787,10 @@ class Command {
1685
1787
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1686
1788
  }
1687
1789
  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());
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());
1689
1794
  }
1690
1795
  if (opt.long.startsWith('no')) {
1691
1796
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1702,6 +1807,9 @@ class Command {
1702
1807
  if (opt.type === 'boolean' && opt.required) {
1703
1808
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1704
1809
  }
1810
+ if (opt.required && opt.args !== 'required') {
1811
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1812
+ }
1705
1813
  }
1706
1814
  #checkOptionUniqueness(opt) {
1707
1815
  if (this.#options.some(o => o.long === opt.long)) {