@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 +7 -0
- package/lib/cjs/browser.cjs +125 -17
- package/lib/cjs/index.cjs +176 -1028
- package/lib/cjs/node.cjs +177 -39
- package/lib/esm/browser.mjs +125 -17
- package/lib/esm/index.mjs +175 -1021
- package/lib/esm/node.mjs +177 -39
- package/lib/types/browser.d.ts +2 -1
- package/lib/types/index.d.ts +15 -191
- package/lib/types/node.d.ts +2 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
1583
|
-
|
|
1584
|
-
if (
|
|
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
|
-
|
|
1672
|
-
|
|
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: '
|
|
2059
|
-
desc: 'Write to file (use shell default path
|
|
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
|
|
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
|
|
2093
|
-
if (
|
|
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);
|
package/lib/esm/browser.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
1568
|
-
|
|
1569
|
-
if (
|
|
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
|
-
|
|
1657
|
-
|
|
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)) {
|