@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/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;
@@ -850,7 +877,15 @@ class Command {
850
877
  return ` ${outputLabel} ${desc}`;
851
878
  }
852
879
  getCompletionMeta() {
853
- const allOptions = this.#resolveOptionPolicy().mergedOptions;
880
+ const optionMap = new Map();
881
+ for (const option of this.#resolveOptionPolicy().mergedOptions) {
882
+ optionMap.set(option.long, option);
883
+ }
884
+ optionMap.set('help', BUILTIN_HELP_OPTION);
885
+ if (this.#supportsBuiltinVersion()) {
886
+ optionMap.set('version', BUILTIN_VERSION_OPTION);
887
+ }
888
+ const allOptions = Array.from(optionMap.values());
854
889
  const options = [];
855
890
  const argumentsMeta = [];
856
891
  for (const opt of allOptions) {
@@ -858,7 +893,8 @@ class Command {
858
893
  long: opt.long,
859
894
  short: opt.short,
860
895
  desc: opt.desc,
861
- takesValue: opt.args !== 'none',
896
+ type: opt.type,
897
+ args: opt.args,
862
898
  choices: opt.choices?.map(choice => String(choice)),
863
899
  });
864
900
  }
@@ -1328,6 +1364,14 @@ class Command {
1328
1364
  consumed.push(tokens[i]);
1329
1365
  }
1330
1366
  }
1367
+ else if (opt.args === 'optional') {
1368
+ if (!token.resolved.includes('=') &&
1369
+ i + 1 < tokens.length &&
1370
+ tokens[i + 1].type === 'none') {
1371
+ i += 1;
1372
+ consumed.push(tokens[i]);
1373
+ }
1374
+ }
1331
1375
  else if (opt.args === 'variadic') {
1332
1376
  if (!token.resolved.includes('=')) {
1333
1377
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1353,6 +1397,12 @@ class Command {
1353
1397
  consumed.push(tokens[i]);
1354
1398
  }
1355
1399
  }
1400
+ else if (opt.args === 'optional') {
1401
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1402
+ i += 1;
1403
+ consumed.push(tokens[i]);
1404
+ }
1405
+ }
1356
1406
  else if (opt.args === 'variadic') {
1357
1407
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1358
1408
  i += 1;
@@ -1394,6 +1444,7 @@ class Command {
1394
1444
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1395
1445
  }
1396
1446
  }
1447
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1397
1448
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1398
1449
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1399
1450
  const parseCtx = {
@@ -1476,6 +1527,23 @@ class Command {
1476
1527
  i += 1;
1477
1528
  continue;
1478
1529
  }
1530
+ if (opt.args === 'optional') {
1531
+ const eqIdx = token.resolved.indexOf('=');
1532
+ if (eqIdx !== -1) {
1533
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1534
+ i += 1;
1535
+ continue;
1536
+ }
1537
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1538
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1539
+ i += 1;
1540
+ }
1541
+ else {
1542
+ opts[opt.long] = undefined;
1543
+ }
1544
+ i += 1;
1545
+ continue;
1546
+ }
1479
1547
  if (opt.args === 'variadic') {
1480
1548
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1481
1549
  const eqIdx = token.resolved.indexOf('=');
@@ -1495,7 +1563,7 @@ class Command {
1495
1563
  i += 1;
1496
1564
  }
1497
1565
  for (const opt of allOptions) {
1498
- if (opt.required && opts[opt.long] === undefined) {
1566
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1499
1567
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
1500
1568
  }
1501
1569
  }
@@ -1532,6 +1600,9 @@ class Command {
1532
1600
  #parseArguments(rawArgs) {
1533
1601
  const argumentDefs = this.#arguments;
1534
1602
  const args = {};
1603
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1604
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1605
+ }
1535
1606
  const missing = [];
1536
1607
  let remaining = rawArgs.length;
1537
1608
  for (const def of argumentDefs) {
@@ -1572,25 +1643,23 @@ class Command {
1572
1643
  }
1573
1644
  if (def.kind === 'some') {
1574
1645
  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
1646
  args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1579
1647
  index = rawArgs.length;
1580
1648
  break;
1581
1649
  }
1582
- const raw = rawArgs[index];
1583
- if (raw === undefined) {
1584
- if (def.kind === 'optional') {
1650
+ if (def.kind === 'optional') {
1651
+ const raw = rawArgs[index];
1652
+ if (raw === undefined) {
1585
1653
  args[def.name] = def.default ?? undefined;
1586
1654
  continue;
1587
1655
  }
1588
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1589
- }
1590
- else {
1591
1656
  args[def.name] = this.#convertArgument(def, raw);
1592
1657
  index += 1;
1658
+ continue;
1593
1659
  }
1660
+ const raw = rawArgs[index];
1661
+ args[def.name] = this.#convertArgument(def, raw);
1662
+ index += 1;
1594
1663
  }
1595
1664
  const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1596
1665
  if (!hasRestArgument && index < rawArgs.length) {
@@ -1624,6 +1693,50 @@ class Command {
1624
1693
  }
1625
1694
  return value;
1626
1695
  }
1696
+ #assertUnknownSubcommand(userTailArgv) {
1697
+ if (this.#subcommandsList.length === 0) {
1698
+ return;
1699
+ }
1700
+ const token = userTailArgv[0];
1701
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1702
+ return;
1703
+ }
1704
+ if (this.#findSubcommandEntry(token) !== undefined) {
1705
+ return;
1706
+ }
1707
+ const hints = [];
1708
+ if (this.#arguments.length === 0) {
1709
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1710
+ }
1711
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1712
+ if (candidate !== undefined) {
1713
+ hints.push(`Hint: did you mean "${candidate}"?`);
1714
+ }
1715
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1716
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1717
+ }
1718
+ #resolveDidYouMeanSubcommandName(token) {
1719
+ const source = normalizeSubcommandNameForDistance(token);
1720
+ let minDistance = Number.POSITIVE_INFINITY;
1721
+ let bestName;
1722
+ let isUniqueBest = false;
1723
+ for (const entry of this.#subcommandsList) {
1724
+ const target = normalizeSubcommandNameForDistance(entry.name);
1725
+ const distance = levenshteinDistance(source, target);
1726
+ if (distance < minDistance) {
1727
+ minDistance = distance;
1728
+ bestName = entry.name;
1729
+ isUniqueBest = true;
1730
+ }
1731
+ else if (distance === minDistance) {
1732
+ isUniqueBest = false;
1733
+ }
1734
+ }
1735
+ if (minDistance <= 2 && isUniqueBest) {
1736
+ return bestName;
1737
+ }
1738
+ return undefined;
1739
+ }
1627
1740
  #hasUserOption(long) {
1628
1741
  return this.#options.some(option => option.long === long);
1629
1742
  }
@@ -1667,11 +1780,9 @@ class Command {
1667
1780
  return optionPolicyMap;
1668
1781
  }
1669
1782
  #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());
1783
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1784
+ optionPolicyMap.set(cmd, policy);
1785
+ return policy;
1675
1786
  }
1676
1787
  #validateMergedShortOptions(chain, optionPolicyMap) {
1677
1788
  const mergedByLong = new Map();
@@ -1700,7 +1811,10 @@ class Command {
1700
1811
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1701
1812
  }
1702
1813
  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());
1814
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1815
+ }
1816
+ if (opt.type === 'number' && opt.args === 'optional') {
1817
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1704
1818
  }
1705
1819
  if (opt.long.startsWith('no')) {
1706
1820
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1717,6 +1831,9 @@ class Command {
1717
1831
  if (opt.type === 'boolean' && opt.required) {
1718
1832
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1719
1833
  }
1834
+ if (opt.required && opt.args !== 'required') {
1835
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1836
+ }
1720
1837
  }
1721
1838
  #checkOptionUniqueness(opt) {
1722
1839
  if (this.#options.some(o => o.long === opt.long)) {
@@ -2025,6 +2142,41 @@ class Coerce {
2025
2142
  function camelToKebabCase(str) {
2026
2143
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
2027
2144
  }
2145
+ function canGenerateNegativeCompletion(opt) {
2146
+ return (opt.type === 'boolean' && opt.args === 'none' && opt.long !== 'help' && opt.long !== 'version');
2147
+ }
2148
+ function optionTakesValue(opt) {
2149
+ return opt.args !== 'none';
2150
+ }
2151
+ const COMPLETION_SHELL_STATE = Symbol('completion-shell-state');
2152
+ function getCommandPath(ctx) {
2153
+ const names = ctx.chain
2154
+ .map(command => command.name)
2155
+ .filter((name) => Boolean(name));
2156
+ if (names.length > 0) {
2157
+ return names.join(' ');
2158
+ }
2159
+ return ctx.cmd.name ?? 'command';
2160
+ }
2161
+ function getCompletionShellState(ctx) {
2162
+ const host = ctx;
2163
+ host[COMPLETION_SHELL_STATE] ??= {};
2164
+ return host[COMPLETION_SHELL_STATE];
2165
+ }
2166
+ function registerCompletionShell(ctx, shell) {
2167
+ const state = getCompletionShellState(ctx);
2168
+ if (state.shell !== undefined && state.shell !== shell) {
2169
+ throw new CommanderError('OptionConflict', 'options "--bash", "--fish", and "--pwsh" are mutually exclusive', getCommandPath(ctx));
2170
+ }
2171
+ state.shell = shell;
2172
+ }
2173
+ function mustGetCompletionShell(ctx) {
2174
+ const state = getCompletionShellState(ctx);
2175
+ if (state.shell === undefined) {
2176
+ throw new CommanderError('MissingRequired', 'missing required option: one of "--bash", "--fish", or "--pwsh"', getCommandPath(ctx));
2177
+ }
2178
+ return state.shell;
2179
+ }
2028
2180
  class CompletionCommand extends Command {
2029
2181
  constructor(root, config = {}) {
2030
2182
  const programName = config.programName ?? root.name ?? 'program';
@@ -2038,45 +2190,45 @@ class CompletionCommand extends Command {
2038
2190
  type: 'boolean',
2039
2191
  args: 'none',
2040
2192
  desc: 'Generate Bash completion script',
2193
+ apply: (value, ctx) => {
2194
+ if (value === true) {
2195
+ registerCompletionShell(ctx, 'bash');
2196
+ }
2197
+ },
2041
2198
  })
2042
2199
  .option({
2043
2200
  long: 'fish',
2044
2201
  type: 'boolean',
2045
2202
  args: 'none',
2046
2203
  desc: 'Generate Fish completion script',
2204
+ apply: (value, ctx) => {
2205
+ if (value === true) {
2206
+ registerCompletionShell(ctx, 'fish');
2207
+ }
2208
+ },
2047
2209
  })
2048
2210
  .option({
2049
2211
  long: 'pwsh',
2050
2212
  type: 'boolean',
2051
2213
  args: 'none',
2052
2214
  desc: 'Generate PowerShell completion script',
2215
+ apply: (value, ctx) => {
2216
+ if (value === true) {
2217
+ registerCompletionShell(ctx, 'pwsh');
2218
+ }
2219
+ mustGetCompletionShell(ctx);
2220
+ },
2053
2221
  })
2054
2222
  .option({
2055
2223
  long: 'write',
2056
2224
  short: 'w',
2057
2225
  type: 'string',
2058
- args: 'required',
2059
- desc: 'Write to file (use shell default path if empty)',
2060
- default: undefined,
2226
+ args: 'optional',
2227
+ desc: 'Write to file (use shell default path when value is omitted or empty)',
2061
2228
  })
2062
- .action(({ opts }) => {
2229
+ .action(({ opts, ctx }) => {
2063
2230
  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];
2231
+ const shell = mustGetCompletionShell(ctx);
2080
2232
  let script;
2081
2233
  switch (shell) {
2082
2234
  case 'bash':
@@ -2089,8 +2241,9 @@ class CompletionCommand extends Command {
2089
2241
  script = new PwshCompletion(meta, programName).generate();
2090
2242
  break;
2091
2243
  }
2092
- const writeOpt = opts['write'];
2093
- if (writeOpt !== undefined) {
2244
+ const hasWrite = Object.prototype.hasOwnProperty.call(opts, 'write');
2245
+ if (hasWrite) {
2246
+ const writeOpt = opts['write'];
2094
2247
  const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
2095
2248
  const expandedPath = expandHome(filePath);
2096
2249
  const dir = path.dirname(expandedPath);
@@ -2157,7 +2310,7 @@ class BashCompletion {
2157
2310
  if (opt.short)
2158
2311
  optParts.push(this.#escapeWord(`-${opt.short}`));
2159
2312
  optParts.push(this.#escapeWord(`--${kebabLong}`));
2160
- if (!opt.takesValue) {
2313
+ if (canGenerateNegativeCompletion(opt)) {
2161
2314
  optParts.push(this.#escapeWord(`--no-${kebabLong}`));
2162
2315
  }
2163
2316
  }
@@ -2189,7 +2342,7 @@ class BashCompletion {
2189
2342
  return words.map(choice => this.#escapeWord(choice)).join(' ');
2190
2343
  }
2191
2344
  #appendChoiceLogicForCommand(lines, indent, cmd, depth) {
2192
- const valueOptions = cmd.options.filter(opt => opt.takesValue);
2345
+ const valueOptions = cmd.options.filter(optionTakesValue);
2193
2346
  const valueOptionsWithChoices = valueOptions.filter(opt => opt.choices && opt.choices.length > 0);
2194
2347
  const valueLongPatterns = valueOptions.map(opt => `--${camelToKebabCase(opt.long)}`);
2195
2348
  const valueShortPatterns = valueOptions
@@ -2316,7 +2469,7 @@ class FishCompletion {
2316
2469
  line += ` -xa '${opt.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
2317
2470
  }
2318
2471
  lines.push(line);
2319
- if (!opt.takesValue) {
2472
+ if (canGenerateNegativeCompletion(opt)) {
2320
2473
  let noLine = `complete -c ${this.#programName}`;
2321
2474
  if (condition)
2322
2475
  noLine += ` -n '${condition}'`;
@@ -2326,11 +2479,11 @@ class FishCompletion {
2326
2479
  }
2327
2480
  }
2328
2481
  const valueOptionLongs = cmd.options
2329
- .filter(opt => opt.takesValue)
2482
+ .filter(optionTakesValue)
2330
2483
  .map(opt => camelToKebabCase(opt.long))
2331
2484
  .join(',');
2332
2485
  const valueOptionShorts = cmd.options
2333
- .filter(opt => opt.takesValue && opt.short)
2486
+ .filter(opt => optionTakesValue(opt) && opt.short)
2334
2487
  .map(opt => opt.short)
2335
2488
  .join(',');
2336
2489
  const argCount = cmd.arguments.length;
@@ -2529,7 +2682,7 @@ class PwshCompletion {
2529
2682
  ' if ($token.StartsWith("--")) {',
2530
2683
  ' if ($token.Contains("=")) { continue }',
2531
2684
  ' foreach ($opt in $cmd.options) {',
2532
- ' if ($token -eq "--$($opt.long)" -and $opt.takesValue) {',
2685
+ ' if ($token -eq "--$($opt.long)" -and $opt.args -ne "none") {',
2533
2686
  ' $expectValue = $true',
2534
2687
  ' break',
2535
2688
  ' }',
@@ -2539,7 +2692,7 @@ class PwshCompletion {
2539
2692
  ' if ($token.StartsWith("-") -and $token -ne "-") {',
2540
2693
  ' if ($token.Length -eq 2) {',
2541
2694
  ' foreach ($opt in $cmd.options) {',
2542
- ' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.takesValue) {',
2695
+ ' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.args -ne "none") {',
2543
2696
  ' $expectValue = $true',
2544
2697
  ' break',
2545
2698
  ' }',
@@ -2591,7 +2744,7 @@ class PwshCompletion {
2591
2744
  ' $opt.description',
2592
2745
  ' )',
2593
2746
  ' }',
2594
- ' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
2747
+ ' if ($opt.canNegate -and "--no-$($opt.long)" -like "$current*") {',
2595
2748
  ' $completions += [System.Management.Automation.CompletionResult]::new(',
2596
2749
  ' "--no-$($opt.long)",',
2597
2750
  ' "no-$($opt.long)",',
@@ -2641,8 +2794,9 @@ class PwshCompletion {
2641
2794
  lines.push(`${indent} short = '${opt.short}'`);
2642
2795
  lines.push(`${indent} long = '${kebabLong}'`);
2643
2796
  lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
2644
- lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
2645
- lines.push(`${indent} takesValue = $${opt.takesValue}`);
2797
+ lines.push(`${indent} type = '${opt.type}'`);
2798
+ lines.push(`${indent} args = '${opt.args}'`);
2799
+ lines.push(`${indent} canNegate = $${canGenerateNegativeCompletion(opt)}`);
2646
2800
  if (opt.choices) {
2647
2801
  lines.push(`${indent} choices = @('${opt.choices
2648
2802
  .map(choice => this.#escape(choice))