@guanghechen/commander 4.3.0 → 4.4.1
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 +14 -0
- package/lib/cjs/index.cjs +109 -57
- package/lib/esm/index.mjs +109 -57
- package/lib/types/index.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 4.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix(commander): centralize option policy and enforce version flag semantics — subcommands with
|
|
8
|
+
their own `version` now correctly expose `--version`; commands without `version` reject
|
|
9
|
+
`--version` instead of treating it as a boolean option.
|
|
10
|
+
|
|
11
|
+
## 4.4.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- enforce per-node help subcommand semantics
|
|
16
|
+
|
|
3
17
|
## 4.3.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -368,30 +368,29 @@ class Command {
|
|
|
368
368
|
const routeResult = this.#route(processedArgv);
|
|
369
369
|
const { chain, remaining } = routeResult;
|
|
370
370
|
const leafCommand = chain[chain.length - 1];
|
|
371
|
-
const rootCommand = chain[0];
|
|
372
371
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
373
372
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
373
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
374
|
+
const leafPolicy = this.#mustGetOptionPolicy(optionPolicyMap, leafCommand);
|
|
375
|
+
if (leafPolicy.enableBuiltinHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
376
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
378
377
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
379
378
|
return;
|
|
380
379
|
}
|
|
381
|
-
if (
|
|
380
|
+
if (leafPolicy.enableBuiltinVersion) {
|
|
382
381
|
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
383
382
|
console.log(leafCommand.#version);
|
|
384
383
|
return;
|
|
385
384
|
}
|
|
386
385
|
}
|
|
387
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
386
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
388
387
|
const ctx = {
|
|
389
388
|
cmd: leafCommand,
|
|
390
389
|
envs,
|
|
391
390
|
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
392
391
|
argv,
|
|
393
392
|
};
|
|
394
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
393
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
395
394
|
const actionParams = {
|
|
396
395
|
ctx: parseResult.ctx,
|
|
397
396
|
opts: parseResult.opts,
|
|
@@ -402,7 +401,7 @@ class Command {
|
|
|
402
401
|
await leafCommand.#runAction(actionParams);
|
|
403
402
|
}
|
|
404
403
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
405
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
404
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
406
405
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
407
406
|
}
|
|
408
407
|
else {
|
|
@@ -426,14 +425,15 @@ class Command {
|
|
|
426
425
|
const leafCommand = chain[chain.length - 1];
|
|
427
426
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
428
427
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
429
|
-
const
|
|
428
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
429
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
430
430
|
const ctx = {
|
|
431
431
|
cmd: leafCommand,
|
|
432
432
|
envs,
|
|
433
433
|
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
434
434
|
argv,
|
|
435
435
|
};
|
|
436
|
-
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
436
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
437
437
|
}
|
|
438
438
|
formatHelp() {
|
|
439
439
|
return this.#renderHelpPlain(this.#buildHelpData());
|
|
@@ -450,7 +450,7 @@ class Command {
|
|
|
450
450
|
return color && process.stdout.isTTY === true;
|
|
451
451
|
}
|
|
452
452
|
#buildHelpData() {
|
|
453
|
-
const allOptions = this.#
|
|
453
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
454
454
|
const commandPath = this.#getCommandPath();
|
|
455
455
|
let usage = `Usage: ${commandPath}`;
|
|
456
456
|
if (allOptions.length > 0)
|
|
@@ -587,7 +587,7 @@ class Command {
|
|
|
587
587
|
return lines.join('\n');
|
|
588
588
|
}
|
|
589
589
|
getCompletionMeta() {
|
|
590
|
-
const allOptions = this.#
|
|
590
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
591
591
|
const options = [];
|
|
592
592
|
for (const opt of allOptions) {
|
|
593
593
|
options.push({
|
|
@@ -613,18 +613,48 @@ class Command {
|
|
|
613
613
|
}),
|
|
614
614
|
};
|
|
615
615
|
}
|
|
616
|
+
#findSubcommandEntry(token) {
|
|
617
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
618
|
+
}
|
|
619
|
+
#createUnknownSubcommandError(subcommand) {
|
|
620
|
+
const commandPath = this.#getCommandPath();
|
|
621
|
+
return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
|
|
622
|
+
}
|
|
616
623
|
#processHelpSubcommand(argv) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
624
|
+
let current = this;
|
|
625
|
+
for (let i = 0; i < argv.length; ++i) {
|
|
626
|
+
const token = argv[i];
|
|
627
|
+
if (token.startsWith('-')) {
|
|
628
|
+
return argv;
|
|
629
|
+
}
|
|
630
|
+
if (token === 'help') {
|
|
631
|
+
if (!current.#builtin.command.help) {
|
|
632
|
+
if (current.#subcommandsList.length > 0) {
|
|
633
|
+
throw current.#createUnknownSubcommandError('help');
|
|
634
|
+
}
|
|
635
|
+
return argv;
|
|
636
|
+
}
|
|
637
|
+
if (current.#subcommandsList.length === 0) {
|
|
638
|
+
return argv;
|
|
639
|
+
}
|
|
640
|
+
const target = argv[i + 1];
|
|
641
|
+
if (target === undefined) {
|
|
642
|
+
return [...argv.slice(0, i), '--help'];
|
|
643
|
+
}
|
|
644
|
+
const targetEntry = current.#findSubcommandEntry(target);
|
|
645
|
+
if (targetEntry === undefined) {
|
|
646
|
+
throw current.#createUnknownSubcommandError(target);
|
|
647
|
+
}
|
|
648
|
+
if (argv[i + 2] !== undefined) {
|
|
649
|
+
throw new CommanderError('UnexpectedArgument', 'help subcommand accepts at most one subcommand argument', current.#getCommandPath());
|
|
650
|
+
}
|
|
651
|
+
return [...argv.slice(0, i), target, '--help'];
|
|
652
|
+
}
|
|
653
|
+
const entry = current.#findSubcommandEntry(token);
|
|
654
|
+
if (entry === undefined) {
|
|
655
|
+
return argv;
|
|
656
|
+
}
|
|
657
|
+
current = entry.command;
|
|
628
658
|
}
|
|
629
659
|
return argv;
|
|
630
660
|
}
|
|
@@ -636,7 +666,7 @@ class Command {
|
|
|
636
666
|
const token = argv[idx];
|
|
637
667
|
if (token.startsWith('-'))
|
|
638
668
|
break;
|
|
639
|
-
const entry = current.#
|
|
669
|
+
const entry = current.#findSubcommandEntry(token);
|
|
640
670
|
if (!entry)
|
|
641
671
|
break;
|
|
642
672
|
current = entry.command;
|
|
@@ -645,14 +675,14 @@ class Command {
|
|
|
645
675
|
}
|
|
646
676
|
return { chain, remaining: argv.slice(idx) };
|
|
647
677
|
}
|
|
648
|
-
#resolve(chain, tokens) {
|
|
678
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
649
679
|
const consumedTokens = new Map();
|
|
650
680
|
let remaining = [...tokens];
|
|
651
681
|
const shadowed = new Set();
|
|
652
682
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
653
683
|
const cmd = chain[i];
|
|
654
|
-
const
|
|
655
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
684
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
685
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
656
686
|
consumedTokens.set(cmd, result.consumed);
|
|
657
687
|
remaining = result.remaining;
|
|
658
688
|
for (const opt of cmd.#options) {
|
|
@@ -669,8 +699,7 @@ class Command {
|
|
|
669
699
|
}
|
|
670
700
|
return { consumedTokens, argTokens };
|
|
671
701
|
}
|
|
672
|
-
#shift(tokens, shadowed,
|
|
673
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
702
|
+
#shift(tokens, shadowed, allOptions) {
|
|
674
703
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
675
704
|
const optionByLong = new Map();
|
|
676
705
|
const optionByShort = new Map();
|
|
@@ -738,18 +767,17 @@ class Command {
|
|
|
738
767
|
}
|
|
739
768
|
return { consumed, remaining };
|
|
740
769
|
}
|
|
741
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
770
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
742
771
|
const { consumedTokens, argTokens } = resolveResult;
|
|
743
772
|
const leafCommand = chain[chain.length - 1];
|
|
744
|
-
this.#validateMergedShortOptions(chain);
|
|
773
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
745
774
|
const optsMap = new Map();
|
|
746
|
-
for (
|
|
747
|
-
const
|
|
748
|
-
const includeVersion = i === 0;
|
|
775
|
+
for (const cmd of chain) {
|
|
776
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
749
777
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
750
|
-
const opts = cmd.#parseOptions(tokens,
|
|
778
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
751
779
|
optsMap.set(cmd, opts);
|
|
752
|
-
for (const opt of
|
|
780
|
+
for (const opt of policy.mergedOptions) {
|
|
753
781
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
754
782
|
opt.apply(opts[opt.long], ctx);
|
|
755
783
|
}
|
|
@@ -763,8 +791,7 @@ class Command {
|
|
|
763
791
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
764
792
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
765
793
|
}
|
|
766
|
-
#parseOptions(tokens,
|
|
767
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
794
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
768
795
|
const opts = {};
|
|
769
796
|
let sawColorToken = false;
|
|
770
797
|
for (const opt of allOptions) {
|
|
@@ -946,22 +973,30 @@ class Command {
|
|
|
946
973
|
}
|
|
947
974
|
return raw;
|
|
948
975
|
}
|
|
949
|
-
#
|
|
976
|
+
#hasUserOption(long) {
|
|
977
|
+
return this.#options.some(option => option.long === long);
|
|
978
|
+
}
|
|
979
|
+
#canUseBuiltinVersion() {
|
|
980
|
+
return this.#version !== undefined;
|
|
981
|
+
}
|
|
982
|
+
#resolveOptionPolicy() {
|
|
950
983
|
const optionMap = new Map();
|
|
951
|
-
const hasUserColor = this.#
|
|
952
|
-
const hasUserHelp = this.#
|
|
953
|
-
const hasUserVersion = this.#
|
|
954
|
-
const hasUserLogLevel = this.#
|
|
955
|
-
const hasUserSilent = this.#
|
|
956
|
-
const hasUserLogDate = this.#
|
|
957
|
-
const hasUserLogColorful = this.#
|
|
984
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
985
|
+
const hasUserHelp = this.#hasUserOption('help');
|
|
986
|
+
const hasUserVersion = this.#hasUserOption('version');
|
|
987
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
988
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
989
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
990
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
991
|
+
const enableBuiltinHelp = !hasUserHelp;
|
|
992
|
+
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
958
993
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
959
994
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
960
995
|
}
|
|
961
|
-
if (
|
|
996
|
+
if (enableBuiltinHelp) {
|
|
962
997
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
963
998
|
}
|
|
964
|
-
if (
|
|
999
|
+
if (enableBuiltinVersion) {
|
|
965
1000
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
966
1001
|
}
|
|
967
1002
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
@@ -979,14 +1014,31 @@ class Command {
|
|
|
979
1014
|
for (const opt of this.#options) {
|
|
980
1015
|
optionMap.set(opt.long, opt);
|
|
981
1016
|
}
|
|
982
|
-
return
|
|
1017
|
+
return {
|
|
1018
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
1019
|
+
enableBuiltinHelp,
|
|
1020
|
+
enableBuiltinVersion,
|
|
1021
|
+
};
|
|
983
1022
|
}
|
|
984
|
-
#
|
|
1023
|
+
#buildOptionPolicyMap(chain) {
|
|
1024
|
+
const optionPolicyMap = new Map();
|
|
1025
|
+
for (const cmd of chain) {
|
|
1026
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1027
|
+
}
|
|
1028
|
+
return optionPolicyMap;
|
|
1029
|
+
}
|
|
1030
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1031
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1032
|
+
if (policy !== undefined) {
|
|
1033
|
+
return policy;
|
|
1034
|
+
}
|
|
1035
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
1036
|
+
}
|
|
1037
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
985
1038
|
const mergedByLong = new Map();
|
|
986
|
-
for (
|
|
987
|
-
const
|
|
988
|
-
const
|
|
989
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1039
|
+
for (const cmd of chain) {
|
|
1040
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1041
|
+
for (const opt of policy.mergedOptions) {
|
|
990
1042
|
mergedByLong.set(opt.long, opt);
|
|
991
1043
|
}
|
|
992
1044
|
}
|
|
@@ -1082,8 +1134,8 @@ class Command {
|
|
|
1082
1134
|
process.exit(1);
|
|
1083
1135
|
}
|
|
1084
1136
|
}
|
|
1085
|
-
#resolveHelpColorOption(tokens, envs) {
|
|
1086
|
-
const colorOption =
|
|
1137
|
+
#resolveHelpColorOption(tokens, envs, policy = this.#resolveOptionPolicy()) {
|
|
1138
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1087
1139
|
let color = !isNoColorEnabled(envs);
|
|
1088
1140
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1089
1141
|
return color;
|
package/lib/esm/index.mjs
CHANGED
|
@@ -346,30 +346,29 @@ class Command {
|
|
|
346
346
|
const routeResult = this.#route(processedArgv);
|
|
347
347
|
const { chain, remaining } = routeResult;
|
|
348
348
|
const leafCommand = chain[chain.length - 1];
|
|
349
|
-
const rootCommand = chain[0];
|
|
350
349
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
351
350
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
351
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
352
|
+
const leafPolicy = this.#mustGetOptionPolicy(optionPolicyMap, leafCommand);
|
|
353
|
+
if (leafPolicy.enableBuiltinHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
354
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
356
355
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
357
356
|
return;
|
|
358
357
|
}
|
|
359
|
-
if (
|
|
358
|
+
if (leafPolicy.enableBuiltinVersion) {
|
|
360
359
|
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
361
360
|
console.log(leafCommand.#version);
|
|
362
361
|
return;
|
|
363
362
|
}
|
|
364
363
|
}
|
|
365
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
364
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
366
365
|
const ctx = {
|
|
367
366
|
cmd: leafCommand,
|
|
368
367
|
envs,
|
|
369
368
|
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
370
369
|
argv,
|
|
371
370
|
};
|
|
372
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
371
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
373
372
|
const actionParams = {
|
|
374
373
|
ctx: parseResult.ctx,
|
|
375
374
|
opts: parseResult.opts,
|
|
@@ -380,7 +379,7 @@ class Command {
|
|
|
380
379
|
await leafCommand.#runAction(actionParams);
|
|
381
380
|
}
|
|
382
381
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
383
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
382
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
384
383
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
385
384
|
}
|
|
386
385
|
else {
|
|
@@ -404,14 +403,15 @@ class Command {
|
|
|
404
403
|
const leafCommand = chain[chain.length - 1];
|
|
405
404
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
406
405
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
407
|
-
const
|
|
406
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
407
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
408
408
|
const ctx = {
|
|
409
409
|
cmd: leafCommand,
|
|
410
410
|
envs,
|
|
411
411
|
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
412
412
|
argv,
|
|
413
413
|
};
|
|
414
|
-
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
414
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
415
415
|
}
|
|
416
416
|
formatHelp() {
|
|
417
417
|
return this.#renderHelpPlain(this.#buildHelpData());
|
|
@@ -428,7 +428,7 @@ class Command {
|
|
|
428
428
|
return color && process.stdout.isTTY === true;
|
|
429
429
|
}
|
|
430
430
|
#buildHelpData() {
|
|
431
|
-
const allOptions = this.#
|
|
431
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
432
432
|
const commandPath = this.#getCommandPath();
|
|
433
433
|
let usage = `Usage: ${commandPath}`;
|
|
434
434
|
if (allOptions.length > 0)
|
|
@@ -565,7 +565,7 @@ class Command {
|
|
|
565
565
|
return lines.join('\n');
|
|
566
566
|
}
|
|
567
567
|
getCompletionMeta() {
|
|
568
|
-
const allOptions = this.#
|
|
568
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
569
569
|
const options = [];
|
|
570
570
|
for (const opt of allOptions) {
|
|
571
571
|
options.push({
|
|
@@ -591,18 +591,48 @@ class Command {
|
|
|
591
591
|
}),
|
|
592
592
|
};
|
|
593
593
|
}
|
|
594
|
+
#findSubcommandEntry(token) {
|
|
595
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
596
|
+
}
|
|
597
|
+
#createUnknownSubcommandError(subcommand) {
|
|
598
|
+
const commandPath = this.#getCommandPath();
|
|
599
|
+
return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
|
|
600
|
+
}
|
|
594
601
|
#processHelpSubcommand(argv) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
602
|
+
let current = this;
|
|
603
|
+
for (let i = 0; i < argv.length; ++i) {
|
|
604
|
+
const token = argv[i];
|
|
605
|
+
if (token.startsWith('-')) {
|
|
606
|
+
return argv;
|
|
607
|
+
}
|
|
608
|
+
if (token === 'help') {
|
|
609
|
+
if (!current.#builtin.command.help) {
|
|
610
|
+
if (current.#subcommandsList.length > 0) {
|
|
611
|
+
throw current.#createUnknownSubcommandError('help');
|
|
612
|
+
}
|
|
613
|
+
return argv;
|
|
614
|
+
}
|
|
615
|
+
if (current.#subcommandsList.length === 0) {
|
|
616
|
+
return argv;
|
|
617
|
+
}
|
|
618
|
+
const target = argv[i + 1];
|
|
619
|
+
if (target === undefined) {
|
|
620
|
+
return [...argv.slice(0, i), '--help'];
|
|
621
|
+
}
|
|
622
|
+
const targetEntry = current.#findSubcommandEntry(target);
|
|
623
|
+
if (targetEntry === undefined) {
|
|
624
|
+
throw current.#createUnknownSubcommandError(target);
|
|
625
|
+
}
|
|
626
|
+
if (argv[i + 2] !== undefined) {
|
|
627
|
+
throw new CommanderError('UnexpectedArgument', 'help subcommand accepts at most one subcommand argument', current.#getCommandPath());
|
|
628
|
+
}
|
|
629
|
+
return [...argv.slice(0, i), target, '--help'];
|
|
630
|
+
}
|
|
631
|
+
const entry = current.#findSubcommandEntry(token);
|
|
632
|
+
if (entry === undefined) {
|
|
633
|
+
return argv;
|
|
634
|
+
}
|
|
635
|
+
current = entry.command;
|
|
606
636
|
}
|
|
607
637
|
return argv;
|
|
608
638
|
}
|
|
@@ -614,7 +644,7 @@ class Command {
|
|
|
614
644
|
const token = argv[idx];
|
|
615
645
|
if (token.startsWith('-'))
|
|
616
646
|
break;
|
|
617
|
-
const entry = current.#
|
|
647
|
+
const entry = current.#findSubcommandEntry(token);
|
|
618
648
|
if (!entry)
|
|
619
649
|
break;
|
|
620
650
|
current = entry.command;
|
|
@@ -623,14 +653,14 @@ class Command {
|
|
|
623
653
|
}
|
|
624
654
|
return { chain, remaining: argv.slice(idx) };
|
|
625
655
|
}
|
|
626
|
-
#resolve(chain, tokens) {
|
|
656
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
627
657
|
const consumedTokens = new Map();
|
|
628
658
|
let remaining = [...tokens];
|
|
629
659
|
const shadowed = new Set();
|
|
630
660
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
631
661
|
const cmd = chain[i];
|
|
632
|
-
const
|
|
633
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
662
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
663
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
634
664
|
consumedTokens.set(cmd, result.consumed);
|
|
635
665
|
remaining = result.remaining;
|
|
636
666
|
for (const opt of cmd.#options) {
|
|
@@ -647,8 +677,7 @@ class Command {
|
|
|
647
677
|
}
|
|
648
678
|
return { consumedTokens, argTokens };
|
|
649
679
|
}
|
|
650
|
-
#shift(tokens, shadowed,
|
|
651
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
680
|
+
#shift(tokens, shadowed, allOptions) {
|
|
652
681
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
653
682
|
const optionByLong = new Map();
|
|
654
683
|
const optionByShort = new Map();
|
|
@@ -716,18 +745,17 @@ class Command {
|
|
|
716
745
|
}
|
|
717
746
|
return { consumed, remaining };
|
|
718
747
|
}
|
|
719
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
748
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
720
749
|
const { consumedTokens, argTokens } = resolveResult;
|
|
721
750
|
const leafCommand = chain[chain.length - 1];
|
|
722
|
-
this.#validateMergedShortOptions(chain);
|
|
751
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
723
752
|
const optsMap = new Map();
|
|
724
|
-
for (
|
|
725
|
-
const
|
|
726
|
-
const includeVersion = i === 0;
|
|
753
|
+
for (const cmd of chain) {
|
|
754
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
727
755
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
728
|
-
const opts = cmd.#parseOptions(tokens,
|
|
756
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
729
757
|
optsMap.set(cmd, opts);
|
|
730
|
-
for (const opt of
|
|
758
|
+
for (const opt of policy.mergedOptions) {
|
|
731
759
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
732
760
|
opt.apply(opts[opt.long], ctx);
|
|
733
761
|
}
|
|
@@ -741,8 +769,7 @@ class Command {
|
|
|
741
769
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
742
770
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
743
771
|
}
|
|
744
|
-
#parseOptions(tokens,
|
|
745
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
772
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
746
773
|
const opts = {};
|
|
747
774
|
let sawColorToken = false;
|
|
748
775
|
for (const opt of allOptions) {
|
|
@@ -924,22 +951,30 @@ class Command {
|
|
|
924
951
|
}
|
|
925
952
|
return raw;
|
|
926
953
|
}
|
|
927
|
-
#
|
|
954
|
+
#hasUserOption(long) {
|
|
955
|
+
return this.#options.some(option => option.long === long);
|
|
956
|
+
}
|
|
957
|
+
#canUseBuiltinVersion() {
|
|
958
|
+
return this.#version !== undefined;
|
|
959
|
+
}
|
|
960
|
+
#resolveOptionPolicy() {
|
|
928
961
|
const optionMap = new Map();
|
|
929
|
-
const hasUserColor = this.#
|
|
930
|
-
const hasUserHelp = this.#
|
|
931
|
-
const hasUserVersion = this.#
|
|
932
|
-
const hasUserLogLevel = this.#
|
|
933
|
-
const hasUserSilent = this.#
|
|
934
|
-
const hasUserLogDate = this.#
|
|
935
|
-
const hasUserLogColorful = this.#
|
|
962
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
963
|
+
const hasUserHelp = this.#hasUserOption('help');
|
|
964
|
+
const hasUserVersion = this.#hasUserOption('version');
|
|
965
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
966
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
967
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
968
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
969
|
+
const enableBuiltinHelp = !hasUserHelp;
|
|
970
|
+
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
936
971
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
937
972
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
938
973
|
}
|
|
939
|
-
if (
|
|
974
|
+
if (enableBuiltinHelp) {
|
|
940
975
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
941
976
|
}
|
|
942
|
-
if (
|
|
977
|
+
if (enableBuiltinVersion) {
|
|
943
978
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
944
979
|
}
|
|
945
980
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
@@ -957,14 +992,31 @@ class Command {
|
|
|
957
992
|
for (const opt of this.#options) {
|
|
958
993
|
optionMap.set(opt.long, opt);
|
|
959
994
|
}
|
|
960
|
-
return
|
|
995
|
+
return {
|
|
996
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
997
|
+
enableBuiltinHelp,
|
|
998
|
+
enableBuiltinVersion,
|
|
999
|
+
};
|
|
961
1000
|
}
|
|
962
|
-
#
|
|
1001
|
+
#buildOptionPolicyMap(chain) {
|
|
1002
|
+
const optionPolicyMap = new Map();
|
|
1003
|
+
for (const cmd of chain) {
|
|
1004
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1005
|
+
}
|
|
1006
|
+
return optionPolicyMap;
|
|
1007
|
+
}
|
|
1008
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1009
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1010
|
+
if (policy !== undefined) {
|
|
1011
|
+
return policy;
|
|
1012
|
+
}
|
|
1013
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
1014
|
+
}
|
|
1015
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
963
1016
|
const mergedByLong = new Map();
|
|
964
|
-
for (
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1017
|
+
for (const cmd of chain) {
|
|
1018
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1019
|
+
for (const opt of policy.mergedOptions) {
|
|
968
1020
|
mergedByLong.set(opt.long, opt);
|
|
969
1021
|
}
|
|
970
1022
|
}
|
|
@@ -1060,8 +1112,8 @@ class Command {
|
|
|
1060
1112
|
process.exit(1);
|
|
1061
1113
|
}
|
|
1062
1114
|
}
|
|
1063
|
-
#resolveHelpColorOption(tokens, envs) {
|
|
1064
|
-
const colorOption =
|
|
1115
|
+
#resolveHelpColorOption(tokens, envs, policy = this.#resolveOptionPolicy()) {
|
|
1116
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1065
1117
|
let color = !isNoColorEnabled(envs);
|
|
1066
1118
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1067
1119
|
return color;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ interface ICommandConfig {
|
|
|
129
129
|
name?: string;
|
|
130
130
|
/** Command description */
|
|
131
131
|
desc: string;
|
|
132
|
-
/** Version (for
|
|
132
|
+
/** Version (for built-in --version on this command) */
|
|
133
133
|
version?: string;
|
|
134
134
|
/** Built-in features configuration */
|
|
135
135
|
builtin?: boolean | ICommandBuiltinConfig;
|
|
@@ -224,7 +224,7 @@ interface ICommandParseResult {
|
|
|
224
224
|
rawArgs: string[];
|
|
225
225
|
}
|
|
226
226
|
/** Error kinds for command parsing */
|
|
227
|
-
type ICommanderErrorKind = 'InvalidOptionFormat' | 'InvalidNegativeOption' | 'NegativeOptionWithValue' | 'NegativeOptionType' | 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
227
|
+
type ICommanderErrorKind = 'InvalidOptionFormat' | 'InvalidNegativeOption' | 'NegativeOptionWithValue' | 'NegativeOptionType' | 'UnknownOption' | 'UnknownSubcommand' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
228
228
|
/** Commander error with structured information */
|
|
229
229
|
declare class CommanderError extends Error {
|
|
230
230
|
readonly kind: ICommanderErrorKind;
|