@guanghechen/commander 4.4.0 → 4.5.0

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.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add built-in coerce factories for numeric option parsing in commander.
8
+
9
+ ## 4.4.1
10
+
11
+ ### Patch Changes
12
+
13
+ - fix(commander): centralize option policy and enforce version flag semantics — subcommands with
14
+ their own `version` now correctly expose `--version`; commands without `version` reject
15
+ `--version` instead of treating it as a boolean option.
16
+
3
17
  ## 4.4.0
4
18
 
5
19
  ### Minor Changes
package/README.md CHANGED
@@ -202,6 +202,50 @@ new Command({ name: 'example', description: 'Option types demo' })
202
202
  })
203
203
  ```
204
204
 
205
+ ### Built-in Coerce Factories
206
+
207
+ ```typescript
208
+ import { Coerce, Command } from '@guanghechen/commander'
209
+
210
+ new Command({ name: 'example', desc: 'Coerce demo' })
211
+ .option({
212
+ long: 'offset',
213
+ type: 'number',
214
+ args: 'required',
215
+ coerce: Coerce.integer('--offset'),
216
+ desc: 'Signed offset',
217
+ })
218
+ .option({
219
+ long: 'parallel',
220
+ type: 'number',
221
+ args: 'required',
222
+ coerce: Coerce.positiveInteger('--parallel'),
223
+ desc: 'Parallel workers',
224
+ })
225
+ .option({
226
+ long: 'duration',
227
+ type: 'number',
228
+ args: 'required',
229
+ coerce: Coerce.positiveNumber('--duration'),
230
+ desc: 'Duration in seconds',
231
+ })
232
+ .option({
233
+ long: 'scale',
234
+ type: 'number',
235
+ args: 'required',
236
+ coerce: Coerce.number('--scale'),
237
+ desc: 'Scale factor',
238
+ })
239
+ ```
240
+
241
+ Default error message format:
242
+
243
+ ```text
244
+ {name} is expected as {coerce type}, but got {raw}
245
+ ```
246
+
247
+ You can still override the message via `Coerce.xxx(name, 'custom error message')`.
248
+
205
249
  ### Help Examples
206
250
 
207
251
  ```typescript
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({
@@ -675,14 +675,14 @@ class Command {
675
675
  }
676
676
  return { chain, remaining: argv.slice(idx) };
677
677
  }
678
- #resolve(chain, tokens) {
678
+ #resolve(chain, tokens, optionPolicyMap) {
679
679
  const consumedTokens = new Map();
680
680
  let remaining = [...tokens];
681
681
  const shadowed = new Set();
682
682
  for (let i = chain.length - 1; i >= 0; i--) {
683
683
  const cmd = chain[i];
684
- const includeVersion = i === 0;
685
- const result = cmd.#shift(remaining, shadowed, includeVersion);
684
+ const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
685
+ const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
686
686
  consumedTokens.set(cmd, result.consumed);
687
687
  remaining = result.remaining;
688
688
  for (const opt of cmd.#options) {
@@ -699,8 +699,7 @@ class Command {
699
699
  }
700
700
  return { consumedTokens, argTokens };
701
701
  }
702
- #shift(tokens, shadowed, includeVersion) {
703
- const allOptions = this.#getMergedOptions(includeVersion);
702
+ #shift(tokens, shadowed, allOptions) {
704
703
  const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
705
704
  const optionByLong = new Map();
706
705
  const optionByShort = new Map();
@@ -768,18 +767,17 @@ class Command {
768
767
  }
769
768
  return { consumed, remaining };
770
769
  }
771
- #parse(chain, resolveResult, ctx, restArgs) {
770
+ #parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
772
771
  const { consumedTokens, argTokens } = resolveResult;
773
772
  const leafCommand = chain[chain.length - 1];
774
- this.#validateMergedShortOptions(chain);
773
+ this.#validateMergedShortOptions(chain, optionPolicyMap);
775
774
  const optsMap = new Map();
776
- for (let i = 0; i < chain.length; i++) {
777
- const cmd = chain[i];
778
- const includeVersion = i === 0;
775
+ for (const cmd of chain) {
776
+ const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
779
777
  const tokens = consumedTokens.get(cmd) ?? [];
780
- const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
778
+ const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
781
779
  optsMap.set(cmd, opts);
782
- for (const opt of cmd.#getMergedOptions(includeVersion)) {
780
+ for (const opt of policy.mergedOptions) {
783
781
  if (opt.apply && opts[opt.long] !== undefined) {
784
782
  opt.apply(opts[opt.long], ctx);
785
783
  }
@@ -793,8 +791,7 @@ class Command {
793
791
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
794
792
  return { ctx, opts: mergedOpts, args, rawArgs };
795
793
  }
796
- #parseOptions(tokens, includeVersion, envs) {
797
- const allOptions = this.#getMergedOptions(includeVersion);
794
+ #parseOptions(tokens, allOptions, envs) {
798
795
  const opts = {};
799
796
  let sawColorToken = false;
800
797
  for (const opt of allOptions) {
@@ -976,22 +973,30 @@ class Command {
976
973
  }
977
974
  return raw;
978
975
  }
979
- #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() {
980
983
  const optionMap = new Map();
981
- const hasUserColor = this.#options.some(o => o.long === 'color');
982
- const hasUserHelp = this.#options.some(o => o.long === 'help');
983
- const hasUserVersion = this.#options.some(o => o.long === 'version');
984
- const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
985
- const hasUserSilent = this.#options.some(o => o.long === 'silent');
986
- const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
987
- 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();
988
993
  if (this.#builtin.option.color && !hasUserColor) {
989
994
  optionMap.set('color', BUILTIN_COLOR_OPTION);
990
995
  }
991
- if (!hasUserHelp) {
996
+ if (enableBuiltinHelp) {
992
997
  optionMap.set('help', BUILTIN_HELP_OPTION);
993
998
  }
994
- if (!hasUserVersion && includeVersion) {
999
+ if (enableBuiltinVersion) {
995
1000
  optionMap.set('version', BUILTIN_VERSION_OPTION);
996
1001
  }
997
1002
  if (this.#builtin.option.logLevel && !hasUserLogLevel) {
@@ -1009,14 +1014,31 @@ class Command {
1009
1014
  for (const opt of this.#options) {
1010
1015
  optionMap.set(opt.long, opt);
1011
1016
  }
1012
- return Array.from(optionMap.values());
1017
+ return {
1018
+ mergedOptions: Array.from(optionMap.values()),
1019
+ enableBuiltinHelp,
1020
+ enableBuiltinVersion,
1021
+ };
1022
+ }
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());
1013
1036
  }
1014
- #validateMergedShortOptions(chain) {
1037
+ #validateMergedShortOptions(chain, optionPolicyMap) {
1015
1038
  const mergedByLong = new Map();
1016
- for (let i = 0; i < chain.length; i++) {
1017
- const cmd = chain[i];
1018
- const includeVersion = i === 0;
1019
- 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) {
1020
1042
  mergedByLong.set(opt.long, opt);
1021
1043
  }
1022
1044
  }
@@ -1112,8 +1134,8 @@ class Command {
1112
1134
  process.exit(1);
1113
1135
  }
1114
1136
  }
1115
- #resolveHelpColorOption(tokens, envs) {
1116
- 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');
1117
1139
  let color = !isNoColorEnabled(envs);
1118
1140
  if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
1119
1141
  return color;
@@ -1164,6 +1186,31 @@ class Command {
1164
1186
  }
1165
1187
  }
1166
1188
 
1189
+ class Coerce {
1190
+ constructor() { }
1191
+ static create(name, expectedType, validator, errorMessage) {
1192
+ return (rawValue) => {
1193
+ const value = Number(rawValue);
1194
+ if (!validator(value)) {
1195
+ throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
1196
+ }
1197
+ return value;
1198
+ };
1199
+ }
1200
+ static number(name, errorMessage) {
1201
+ return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1202
+ }
1203
+ static integer(name, errorMessage) {
1204
+ return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
1205
+ }
1206
+ static positiveInteger(name, errorMessage) {
1207
+ return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
1208
+ }
1209
+ static positiveNumber(name, errorMessage) {
1210
+ return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
1211
+ }
1212
+ }
1213
+
1167
1214
  function camelToKebabCase(str) {
1168
1215
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
1169
1216
  }
@@ -1523,6 +1570,7 @@ class PwshCompletion {
1523
1570
  }
1524
1571
 
1525
1572
  exports.BashCompletion = BashCompletion;
1573
+ exports.Coerce = Coerce;
1526
1574
  exports.Command = Command;
1527
1575
  exports.CommanderError = CommanderError;
1528
1576
  exports.CompletionCommand = CompletionCommand;
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({
@@ -653,14 +653,14 @@ class Command {
653
653
  }
654
654
  return { chain, remaining: argv.slice(idx) };
655
655
  }
656
- #resolve(chain, tokens) {
656
+ #resolve(chain, tokens, optionPolicyMap) {
657
657
  const consumedTokens = new Map();
658
658
  let remaining = [...tokens];
659
659
  const shadowed = new Set();
660
660
  for (let i = chain.length - 1; i >= 0; i--) {
661
661
  const cmd = chain[i];
662
- const includeVersion = i === 0;
663
- const result = cmd.#shift(remaining, shadowed, includeVersion);
662
+ const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
663
+ const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
664
664
  consumedTokens.set(cmd, result.consumed);
665
665
  remaining = result.remaining;
666
666
  for (const opt of cmd.#options) {
@@ -677,8 +677,7 @@ class Command {
677
677
  }
678
678
  return { consumedTokens, argTokens };
679
679
  }
680
- #shift(tokens, shadowed, includeVersion) {
681
- const allOptions = this.#getMergedOptions(includeVersion);
680
+ #shift(tokens, shadowed, allOptions) {
682
681
  const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
683
682
  const optionByLong = new Map();
684
683
  const optionByShort = new Map();
@@ -746,18 +745,17 @@ class Command {
746
745
  }
747
746
  return { consumed, remaining };
748
747
  }
749
- #parse(chain, resolveResult, ctx, restArgs) {
748
+ #parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
750
749
  const { consumedTokens, argTokens } = resolveResult;
751
750
  const leafCommand = chain[chain.length - 1];
752
- this.#validateMergedShortOptions(chain);
751
+ this.#validateMergedShortOptions(chain, optionPolicyMap);
753
752
  const optsMap = new Map();
754
- for (let i = 0; i < chain.length; i++) {
755
- const cmd = chain[i];
756
- const includeVersion = i === 0;
753
+ for (const cmd of chain) {
754
+ const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
757
755
  const tokens = consumedTokens.get(cmd) ?? [];
758
- const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
756
+ const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
759
757
  optsMap.set(cmd, opts);
760
- for (const opt of cmd.#getMergedOptions(includeVersion)) {
758
+ for (const opt of policy.mergedOptions) {
761
759
  if (opt.apply && opts[opt.long] !== undefined) {
762
760
  opt.apply(opts[opt.long], ctx);
763
761
  }
@@ -771,8 +769,7 @@ class Command {
771
769
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
772
770
  return { ctx, opts: mergedOpts, args, rawArgs };
773
771
  }
774
- #parseOptions(tokens, includeVersion, envs) {
775
- const allOptions = this.#getMergedOptions(includeVersion);
772
+ #parseOptions(tokens, allOptions, envs) {
776
773
  const opts = {};
777
774
  let sawColorToken = false;
778
775
  for (const opt of allOptions) {
@@ -954,22 +951,30 @@ class Command {
954
951
  }
955
952
  return raw;
956
953
  }
957
- #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() {
958
961
  const optionMap = new Map();
959
- const hasUserColor = this.#options.some(o => o.long === 'color');
960
- const hasUserHelp = this.#options.some(o => o.long === 'help');
961
- const hasUserVersion = this.#options.some(o => o.long === 'version');
962
- const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
963
- const hasUserSilent = this.#options.some(o => o.long === 'silent');
964
- const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
965
- 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();
966
971
  if (this.#builtin.option.color && !hasUserColor) {
967
972
  optionMap.set('color', BUILTIN_COLOR_OPTION);
968
973
  }
969
- if (!hasUserHelp) {
974
+ if (enableBuiltinHelp) {
970
975
  optionMap.set('help', BUILTIN_HELP_OPTION);
971
976
  }
972
- if (!hasUserVersion && includeVersion) {
977
+ if (enableBuiltinVersion) {
973
978
  optionMap.set('version', BUILTIN_VERSION_OPTION);
974
979
  }
975
980
  if (this.#builtin.option.logLevel && !hasUserLogLevel) {
@@ -987,14 +992,31 @@ class Command {
987
992
  for (const opt of this.#options) {
988
993
  optionMap.set(opt.long, opt);
989
994
  }
990
- return Array.from(optionMap.values());
995
+ return {
996
+ mergedOptions: Array.from(optionMap.values()),
997
+ enableBuiltinHelp,
998
+ enableBuiltinVersion,
999
+ };
1000
+ }
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());
991
1014
  }
992
- #validateMergedShortOptions(chain) {
1015
+ #validateMergedShortOptions(chain, optionPolicyMap) {
993
1016
  const mergedByLong = new Map();
994
- for (let i = 0; i < chain.length; i++) {
995
- const cmd = chain[i];
996
- const includeVersion = i === 0;
997
- 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) {
998
1020
  mergedByLong.set(opt.long, opt);
999
1021
  }
1000
1022
  }
@@ -1090,8 +1112,8 @@ class Command {
1090
1112
  process.exit(1);
1091
1113
  }
1092
1114
  }
1093
- #resolveHelpColorOption(tokens, envs) {
1094
- 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');
1095
1117
  let color = !isNoColorEnabled(envs);
1096
1118
  if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
1097
1119
  return color;
@@ -1142,6 +1164,31 @@ class Command {
1142
1164
  }
1143
1165
  }
1144
1166
 
1167
+ class Coerce {
1168
+ constructor() { }
1169
+ static create(name, expectedType, validator, errorMessage) {
1170
+ return (rawValue) => {
1171
+ const value = Number(rawValue);
1172
+ if (!validator(value)) {
1173
+ throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
1174
+ }
1175
+ return value;
1176
+ };
1177
+ }
1178
+ static number(name, errorMessage) {
1179
+ return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
1180
+ }
1181
+ static integer(name, errorMessage) {
1182
+ return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
1183
+ }
1184
+ static positiveInteger(name, errorMessage) {
1185
+ return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
1186
+ }
1187
+ static positiveNumber(name, errorMessage) {
1188
+ return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
1189
+ }
1190
+ }
1191
+
1145
1192
  function camelToKebabCase(str) {
1146
1193
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
1147
1194
  }
@@ -1500,4 +1547,4 @@ class PwshCompletion {
1500
1547
  }
1501
1548
  }
1502
1549
 
1503
- export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
1550
+ export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
@@ -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;
@@ -308,6 +308,20 @@ declare class Command implements ICommand {
308
308
  getCompletionMeta(): ICompletionMeta;
309
309
  }
310
310
 
311
+ /**
312
+ * Pre-defined coerce factory methods for @guanghechen/commander.
313
+ *
314
+ * @module @guanghechen/commander/coerce
315
+ */
316
+ declare class Coerce {
317
+ private constructor();
318
+ private static create;
319
+ static number(name: string, errorMessage?: string): (rawValue: string) => number;
320
+ static integer(name: string, errorMessage?: string): (rawValue: string) => number;
321
+ static positiveInteger(name: string, errorMessage?: string): (rawValue: string) => number;
322
+ static positiveNumber(name: string, errorMessage?: string): (rawValue: string) => number;
323
+ }
324
+
311
325
  /**
312
326
  * Shell completion generators
313
327
  *
@@ -436,5 +450,5 @@ declare const logColorfulOption: ICommandOptionConfig<boolean>;
436
450
  */
437
451
  declare const silentOption: ICommandOptionConfig<boolean>;
438
452
 
439
- export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
453
+ export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
440
454
  export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandConfig, ICommandContext, ICommandExample, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",