@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 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 hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
375
- const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
376
- if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
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 (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
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 resolveResult = this.#resolve(chain, optionTokens);
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.#getMergedOptions();
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.#getMergedOptions();
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
- if (!this.#builtin.command.help)
618
- return argv;
619
- if (argv.length < 1 || argv[0] !== 'help')
620
- return argv;
621
- if (argv.length === 1 || this.#subcommandsList.length === 0) {
622
- return ['--help'];
623
- }
624
- const subName = argv[1];
625
- const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
626
- if (entry) {
627
- return [subName, '--help', ...argv.slice(2)];
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.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
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 includeVersion = i === 0;
655
- const result = cmd.#shift(remaining, shadowed, includeVersion);
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, includeVersion) {
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 (let i = 0; i < chain.length; i++) {
747
- const cmd = chain[i];
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, includeVersion, ctx.envs);
778
+ const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
751
779
  optsMap.set(cmd, opts);
752
- for (const opt of cmd.#getMergedOptions(includeVersion)) {
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, includeVersion, envs) {
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
- #getMergedOptions(includeVersion = !this.#parent) {
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.#options.some(o => o.long === 'color');
952
- const hasUserHelp = this.#options.some(o => o.long === 'help');
953
- const hasUserVersion = this.#options.some(o => o.long === 'version');
954
- const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
955
- const hasUserSilent = this.#options.some(o => o.long === 'silent');
956
- const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
957
- const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
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 (!hasUserHelp) {
996
+ if (enableBuiltinHelp) {
962
997
  optionMap.set('help', BUILTIN_HELP_OPTION);
963
998
  }
964
- if (!hasUserVersion && includeVersion) {
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 Array.from(optionMap.values());
1017
+ return {
1018
+ mergedOptions: Array.from(optionMap.values()),
1019
+ enableBuiltinHelp,
1020
+ enableBuiltinVersion,
1021
+ };
983
1022
  }
984
- #validateMergedShortOptions(chain) {
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 (let i = 0; i < chain.length; i++) {
987
- const cmd = chain[i];
988
- const includeVersion = i === 0;
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 = this.#getMergedOptions().find(opt => opt.long === 'color');
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 hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
353
- const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
354
- if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
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 (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
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 resolveResult = this.#resolve(chain, optionTokens);
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.#getMergedOptions();
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.#getMergedOptions();
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
- if (!this.#builtin.command.help)
596
- return argv;
597
- if (argv.length < 1 || argv[0] !== 'help')
598
- return argv;
599
- if (argv.length === 1 || this.#subcommandsList.length === 0) {
600
- return ['--help'];
601
- }
602
- const subName = argv[1];
603
- const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
604
- if (entry) {
605
- return [subName, '--help', ...argv.slice(2)];
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.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
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 includeVersion = i === 0;
633
- const result = cmd.#shift(remaining, shadowed, includeVersion);
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, includeVersion) {
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 (let i = 0; i < chain.length; i++) {
725
- const cmd = chain[i];
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, includeVersion, ctx.envs);
756
+ const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
729
757
  optsMap.set(cmd, opts);
730
- for (const opt of cmd.#getMergedOptions(includeVersion)) {
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, includeVersion, envs) {
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
- #getMergedOptions(includeVersion = !this.#parent) {
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.#options.some(o => o.long === 'color');
930
- const hasUserHelp = this.#options.some(o => o.long === 'help');
931
- const hasUserVersion = this.#options.some(o => o.long === 'version');
932
- const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
933
- const hasUserSilent = this.#options.some(o => o.long === 'silent');
934
- const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
935
- const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
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 (!hasUserHelp) {
974
+ if (enableBuiltinHelp) {
940
975
  optionMap.set('help', BUILTIN_HELP_OPTION);
941
976
  }
942
- if (!hasUserVersion && includeVersion) {
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 Array.from(optionMap.values());
995
+ return {
996
+ mergedOptions: Array.from(optionMap.values()),
997
+ enableBuiltinHelp,
998
+ enableBuiltinVersion,
999
+ };
961
1000
  }
962
- #validateMergedShortOptions(chain) {
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 (let i = 0; i < chain.length; i++) {
965
- const cmd = chain[i];
966
- const includeVersion = i === 0;
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 = this.#getMergedOptions().find(opt => opt.long === 'color');
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;
@@ -129,7 +129,7 @@ interface ICommandConfig {
129
129
  name?: string;
130
130
  /** Command description */
131
131
  desc: string;
132
- /** Version (for root --version) */
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",