@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 +16 -0
- package/lib/cjs/browser.cjs +136 -19
- package/lib/cjs/index.cjs +176 -1028
- package/lib/cjs/node.cjs +205 -51
- package/lib/esm/browser.mjs +136 -19
- package/lib/esm/index.mjs +175 -1021
- package/lib/esm/node.mjs +205 -51
- package/lib/types/browser.d.ts +6 -3
- package/lib/types/index.d.ts +15 -191
- package/lib/types/node.d.ts +6 -3
- package/package.json +3 -3
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
|
package/lib/cjs/browser.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1570
|
-
|
|
1571
|
-
if (
|
|
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
|
-
|
|
1659
|
-
|
|
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)) {
|