@guanghechen/commander 4.5.1 → 4.6.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/lib/cjs/index.cjs CHANGED
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ var env = require('@guanghechen/env');
3
4
  var reporter = require('@guanghechen/reporter');
4
- var fs = require('node:fs');
5
+ var promises = require('node:fs/promises');
5
6
  var path = require('node:path');
7
+ var fs = require('node:fs');
6
8
 
7
9
  function _interopNamespaceDefault(e) {
8
10
  var n = Object.create(null);
@@ -21,8 +23,8 @@ function _interopNamespaceDefault(e) {
21
23
  return Object.freeze(n);
22
24
  }
23
25
 
24
- var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
25
26
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
27
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
26
28
 
27
29
  const TERMINAL_STYLE = {
28
30
  bold: '\x1b[1m',
@@ -36,22 +38,35 @@ function styleText(text, ...styles) {
36
38
  return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
37
39
  }
38
40
 
41
+ const BUILTIN_LOG_LEVELS = ['debug', 'info', 'hint', 'warn', 'error'];
42
+ function resolveReporterLogLevel(raw) {
43
+ const normalized = raw.trim().toLowerCase();
44
+ return BUILTIN_LOG_LEVELS.find(level => level === normalized);
45
+ }
46
+ function setReporterLevel(ctx, level) {
47
+ const reporter = ctx.reporter;
48
+ reporter?.setLevel?.(level);
49
+ }
50
+ function setReporterFlight(ctx, flight) {
51
+ const reporter = ctx.reporter;
52
+ reporter?.setFlight?.(flight);
53
+ }
39
54
  const logLevelOption = {
40
55
  long: 'logLevel',
41
56
  type: 'string',
42
57
  args: 'required',
43
58
  desc: 'Set log level',
44
59
  default: 'info',
45
- choices: reporter.LOG_LEVELS,
60
+ choices: [...BUILTIN_LOG_LEVELS],
46
61
  coerce: (raw) => {
47
- const level = reporter.resolveLogLevel(raw);
62
+ const level = resolveReporterLogLevel(raw);
48
63
  if (level === undefined) {
49
64
  throw new Error(`Invalid log level: ${raw}`);
50
65
  }
51
66
  return level;
52
67
  },
53
68
  apply: (value, ctx) => {
54
- ctx.reporter.setLevel(value);
69
+ setReporterLevel(ctx, value);
55
70
  },
56
71
  };
57
72
  const logDateOption = {
@@ -61,7 +76,7 @@ const logDateOption = {
61
76
  desc: 'Enable log timestamp',
62
77
  default: true,
63
78
  apply: (value, ctx) => {
64
- ctx.reporter.setFlight({ date: Boolean(value) });
79
+ setReporterFlight(ctx, { date: Boolean(value) });
65
80
  },
66
81
  };
67
82
  const logColorfulOption = {
@@ -71,7 +86,7 @@ const logColorfulOption = {
71
86
  desc: 'Enable colorful log output',
72
87
  default: true,
73
88
  apply: (value, ctx) => {
74
- ctx.reporter.setFlight({ color: Boolean(value) });
89
+ setReporterFlight(ctx, { color: Boolean(value) });
75
90
  },
76
91
  };
77
92
  const silentOption = {
@@ -82,7 +97,7 @@ const silentOption = {
82
97
  default: false,
83
98
  apply: (value, ctx) => {
84
99
  if (value) {
85
- ctx.reporter.setLevel('error');
100
+ setReporterLevel(ctx, 'error');
86
101
  }
87
102
  },
88
103
  };
@@ -103,6 +118,11 @@ class CommanderError extends Error {
103
118
 
104
119
  const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
105
120
  const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
121
+ const PRESET_OPTS_FLAG = '--preset-opts';
122
+ const PRESET_ENVS_FLAG = '--preset-envs';
123
+ const PRESET_ROOT_FLAG = '--preset-root';
124
+ const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
125
+ const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
106
126
  function kebabToCamelCase(str) {
107
127
  return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
108
128
  }
@@ -190,14 +210,12 @@ function tokenize(argv, commandPath) {
190
210
  }
191
211
  const BUILTIN_HELP_OPTION = {
192
212
  long: 'help',
193
- short: 'h',
194
213
  type: 'boolean',
195
214
  args: 'none',
196
215
  desc: 'Show help information',
197
216
  };
198
217
  const BUILTIN_VERSION_OPTION = {
199
218
  long: 'version',
200
- short: 'V',
201
219
  type: 'boolean',
202
220
  args: 'none',
203
221
  desc: 'Show version number',
@@ -211,6 +229,7 @@ const BUILTIN_COLOR_OPTION = {
211
229
  };
212
230
  function createBuiltinOptionState(enabled) {
213
231
  return {
232
+ version: enabled,
214
233
  color: enabled,
215
234
  logLevel: enabled,
216
235
  silent: enabled,
@@ -224,9 +243,6 @@ function isNoColorEnabled(envs) {
224
243
  function normalizeBuiltinConfig(builtin) {
225
244
  const resolved = {
226
245
  option: createBuiltinOptionState(true),
227
- command: {
228
- help: false,
229
- },
230
246
  };
231
247
  if (builtin === undefined) {
232
248
  return resolved;
@@ -234,13 +250,11 @@ function normalizeBuiltinConfig(builtin) {
234
250
  if (builtin === true) {
235
251
  return {
236
252
  option: createBuiltinOptionState(true),
237
- command: { help: true },
238
253
  };
239
254
  }
240
255
  if (builtin === false) {
241
256
  return {
242
257
  option: createBuiltinOptionState(false),
243
- command: { help: false },
244
258
  };
245
259
  }
246
260
  if (builtin.option !== undefined) {
@@ -251,6 +265,8 @@ function normalizeBuiltinConfig(builtin) {
251
265
  resolved.option = createBuiltinOptionState(true);
252
266
  }
253
267
  else {
268
+ if (builtin.option.version !== undefined)
269
+ resolved.option.version = builtin.option.version;
254
270
  if (builtin.option.color !== undefined)
255
271
  resolved.option.color = builtin.option.color;
256
272
  if (builtin.option.logLevel !== undefined) {
@@ -265,24 +281,15 @@ function normalizeBuiltinConfig(builtin) {
265
281
  }
266
282
  }
267
283
  }
268
- if (builtin.command !== undefined) {
269
- if (builtin.command === false) {
270
- resolved.command = { help: false };
271
- }
272
- else if (builtin.command === true) {
273
- resolved.command = { help: true };
274
- }
275
- else if (builtin.command.help !== undefined) {
276
- resolved.command.help = builtin.command.help;
277
- }
278
- }
279
284
  return resolved;
280
285
  }
281
286
  class Command {
282
287
  #name;
283
288
  #desc;
284
289
  #version;
290
+ #builtinConfig;
285
291
  #builtin;
292
+ #presetConfig;
286
293
  #reporter;
287
294
  #parent;
288
295
  #options = [];
@@ -295,7 +302,9 @@ class Command {
295
302
  this.#name = config.name ?? '';
296
303
  this.#desc = config.desc;
297
304
  this.#version = config.version;
305
+ this.#builtinConfig = config.builtin;
298
306
  this.#builtin = normalizeBuiltinConfig(config.builtin);
307
+ this.#presetConfig = config.preset;
299
308
  this.#reporter = config.reporter;
300
309
  }
301
310
  get name() {
@@ -307,6 +316,12 @@ class Command {
307
316
  get version() {
308
317
  return this.#version;
309
318
  }
319
+ get builtin() {
320
+ return this.#builtinConfig;
321
+ }
322
+ get preset() {
323
+ return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
324
+ }
310
325
  get parent() {
311
326
  return this.#parent;
312
327
  }
@@ -342,14 +357,17 @@ class Command {
342
357
  return this;
343
358
  }
344
359
  subcommand(name, cmd) {
345
- if (this.#builtin.command.help && name === 'help') {
346
- throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
360
+ if (name === 'help') {
361
+ throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
347
362
  }
348
363
  if (cmd.#parent && cmd.#parent !== this) {
349
364
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
350
365
  }
351
366
  const existing = this.#subcommandsList.find(e => e.command === cmd);
352
367
  if (existing) {
368
+ if (existing.aliases.includes(name)) {
369
+ return this;
370
+ }
353
371
  existing.aliases.push(name);
354
372
  this.#subcommandsMap.set(name, cmd);
355
373
  }
@@ -362,34 +380,37 @@ class Command {
362
380
  return this;
363
381
  }
364
382
  async run(params) {
365
- const { argv, envs, reporter: reporter$1 } = params;
383
+ const { argv, envs, reporter } = params;
366
384
  try {
367
- const processedArgv = this.#processHelpSubcommand(argv);
368
- const routeResult = this.#route(processedArgv);
369
- const { chain, remaining } = routeResult;
385
+ const routeResult = this.#route(argv);
386
+ const { chain } = routeResult;
370
387
  const leafCommand = chain[chain.length - 1];
371
- const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
372
- const { optionTokens, restArgs } = tokenizeResult;
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);
377
- console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
388
+ const ctx = this.#createContext({
389
+ chain,
390
+ cmds: routeResult.cmds,
391
+ envs,
392
+ reporter,
393
+ });
394
+ const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
395
+ ctx.controls = controlScanResult.controls;
396
+ ctx.sources.user.argv = [...controlScanResult.remaining];
397
+ if (ctx.controls.help) {
398
+ const helpCommand = this.#resolveHelpCommand(leafCommand, controlScanResult.helpTarget);
399
+ const helpColor = helpCommand.#resolveHelpColorFromTailArgv(controlScanResult.remaining, ctx.envs);
400
+ console.log(helpCommand.#formatHelpForDisplay({ color: helpColor }));
378
401
  return;
379
402
  }
380
- if (leafPolicy.enableBuiltinVersion) {
381
- if (this.#hasFlag(optionTokens, 'version', 'V')) {
382
- console.log(leafCommand.#version);
383
- return;
384
- }
403
+ if (ctx.controls.version) {
404
+ console.log(leafCommand.#version);
405
+ return;
385
406
  }
407
+ const optionPolicyMap = this.#buildOptionPolicyMap(chain);
408
+ const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
409
+ ctx.sources = presetResult.sources;
410
+ ctx.envs = presetResult.envs;
411
+ const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
412
+ const { optionTokens, restArgs } = tokenizeResult;
386
413
  const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
387
- const ctx = {
388
- cmd: leafCommand,
389
- envs,
390
- reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
391
- argv,
392
- };
393
414
  const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
394
415
  const actionParams = {
395
416
  ctx: parseResult.ctx,
@@ -401,7 +422,7 @@ class Command {
401
422
  await leafCommand.#runAction(actionParams);
402
423
  }
403
424
  else if (leafCommand.#subcommandsList.length > 0) {
404
- const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
425
+ const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
405
426
  console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
406
427
  }
407
428
  else {
@@ -417,22 +438,27 @@ class Command {
417
438
  throw err;
418
439
  }
419
440
  }
420
- parse(params) {
421
- const { argv, envs, reporter: reporter$1 } = params;
422
- const processedArgv = this.#processHelpSubcommand(argv);
423
- const routeResult = this.#route(processedArgv);
424
- const { chain, remaining } = routeResult;
441
+ async parse(params) {
442
+ const { argv, envs, reporter } = params;
443
+ const routeResult = this.#route(argv);
444
+ const { chain } = routeResult;
425
445
  const leafCommand = chain[chain.length - 1];
426
- const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
427
- const { optionTokens, restArgs } = tokenizeResult;
446
+ const ctx = this.#createContext({
447
+ chain,
448
+ cmds: routeResult.cmds,
449
+ envs,
450
+ reporter,
451
+ });
452
+ const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
453
+ ctx.controls = controlScanResult.controls;
454
+ ctx.sources.user.argv = [...controlScanResult.remaining];
428
455
  const optionPolicyMap = this.#buildOptionPolicyMap(chain);
456
+ const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
457
+ ctx.sources = presetResult.sources;
458
+ ctx.envs = presetResult.envs;
459
+ const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
460
+ const { optionTokens, restArgs } = tokenizeResult;
429
461
  const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
430
- const ctx = {
431
- cmd: leafCommand,
432
- envs,
433
- reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
434
- argv,
435
- };
436
462
  return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
437
463
  }
438
464
  formatHelp() {
@@ -450,7 +476,11 @@ class Command {
450
476
  return color && process.stdout.isTTY === true;
451
477
  }
452
478
  #buildHelpData() {
453
- const allOptions = this.#resolveOptionPolicy().mergedOptions;
479
+ const parseOptions = this.#resolveOptionPolicy().mergedOptions;
480
+ const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
481
+ if (this.#supportsBuiltinVersion()) {
482
+ allOptions.push(BUILTIN_VERSION_OPTION);
483
+ }
454
484
  const commandPath = this.#getCommandPath();
455
485
  let usage = `Usage: ${commandPath}`;
456
486
  if (allOptions.length > 0)
@@ -484,7 +514,10 @@ class Command {
484
514
  desc += ` [choices: ${opt.choices.join(', ')}]`;
485
515
  }
486
516
  options.push({ sig, desc });
487
- if (opt.type === 'boolean' && opt.args === 'none') {
517
+ if (opt.type === 'boolean' &&
518
+ opt.args === 'none' &&
519
+ opt.long !== 'help' &&
520
+ opt.long !== 'version') {
488
521
  options.push({
489
522
  sig: ` --no-${kebabLong}`,
490
523
  desc: `Negate --${kebabLong}`,
@@ -492,8 +525,7 @@ class Command {
492
525
  }
493
526
  }
494
527
  const commands = [];
495
- const showHelpSubcommand = this.#builtin.command.help && this.#subcommandsList.length > 0;
496
- if (showHelpSubcommand) {
528
+ if (this.#subcommandsList.length > 0) {
497
529
  commands.push({ name: 'help', desc: 'Show help for a command' });
498
530
  }
499
531
  for (const entry of this.#subcommandsList) {
@@ -616,50 +648,9 @@ class Command {
616
648
  #findSubcommandEntry(token) {
617
649
  return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
618
650
  }
619
- #createUnknownSubcommandError(subcommand) {
620
- const commandPath = this.#getCommandPath();
621
- return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
622
- }
623
- #processHelpSubcommand(argv) {
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;
658
- }
659
- return argv;
660
- }
661
651
  #route(argv) {
662
652
  const chain = [this];
653
+ const cmds = [];
663
654
  let current = this;
664
655
  let idx = 0;
665
656
  while (idx < argv.length) {
@@ -670,10 +661,382 @@ class Command {
670
661
  if (!entry)
671
662
  break;
672
663
  current = entry.command;
664
+ cmds.push(token);
673
665
  chain.push(current);
674
666
  idx += 1;
675
667
  }
676
- return { chain, remaining: argv.slice(idx) };
668
+ return { chain, remaining: argv.slice(idx), cmds };
669
+ }
670
+ #controlScan(tailArgv, leafCommand) {
671
+ const controls = { help: false, version: false };
672
+ const separatorIndex = tailArgv.indexOf('--');
673
+ const beforeSeparator = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
674
+ const afterSeparator = separatorIndex === -1 ? [] : tailArgv.slice(separatorIndex + 1);
675
+ let helpTarget;
676
+ let scanStartIndex = 0;
677
+ if (beforeSeparator[0] === 'help') {
678
+ controls.help = true;
679
+ scanStartIndex = 1;
680
+ const candidate = beforeSeparator[1];
681
+ if (candidate !== undefined && !candidate.startsWith('-')) {
682
+ helpTarget = candidate;
683
+ scanStartIndex = 2;
684
+ }
685
+ }
686
+ const remainingBeforeSeparator = [];
687
+ for (let i = scanStartIndex; i < beforeSeparator.length; i += 1) {
688
+ const token = beforeSeparator[i];
689
+ if (token === '--help') {
690
+ controls.help = true;
691
+ continue;
692
+ }
693
+ if (token === '--version' && leafCommand.#supportsBuiltinVersion()) {
694
+ controls.version = true;
695
+ continue;
696
+ }
697
+ remainingBeforeSeparator.push(token);
698
+ }
699
+ const remaining = separatorIndex === -1
700
+ ? remainingBeforeSeparator
701
+ : [...remainingBeforeSeparator, '--', ...afterSeparator];
702
+ return {
703
+ controls,
704
+ remaining,
705
+ helpTarget,
706
+ };
707
+ }
708
+ #createContext(params) {
709
+ const { chain, cmds, envs, reporter: reporter$1 } = params;
710
+ const leafCommand = chain[chain.length - 1];
711
+ const envSnapshot = { ...envs };
712
+ return {
713
+ cmd: leafCommand,
714
+ chain,
715
+ envs: envSnapshot,
716
+ controls: { help: false, version: false },
717
+ sources: {
718
+ preset: {
719
+ argv: [],
720
+ envs: {},
721
+ },
722
+ user: {
723
+ cmds: [...cmds],
724
+ argv: [],
725
+ envs: envSnapshot,
726
+ },
727
+ },
728
+ reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
729
+ };
730
+ }
731
+ #resolveHelpCommand(leafCommand, helpTarget) {
732
+ if (helpTarget === undefined) {
733
+ return leafCommand;
734
+ }
735
+ const target = leafCommand.#findSubcommandEntry(helpTarget);
736
+ if (target === undefined) {
737
+ return leafCommand;
738
+ }
739
+ return target.command;
740
+ }
741
+ async #preset(controlTailArgv, ctx, optionPolicyMap) {
742
+ const commandPath = ctx.chain[ctx.chain.length - 1].#getCommandPath();
743
+ const separatorIndex = controlTailArgv.indexOf('--');
744
+ const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
745
+ const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
746
+ const rootScanResult = this.#scanPresetRootDirectives(beforeSeparator, commandPath);
747
+ const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
748
+ const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
749
+ const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
750
+ const cleanArgv = separatorIndex === -1
751
+ ? fileScanResult.cleanArgv
752
+ : [...fileScanResult.cleanArgv, '--', ...afterSeparator];
753
+ const presetOptsFiles = this.#resolvePresetFileSources({
754
+ cliFiles: fileScanResult.cliPresetOptsFiles,
755
+ commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.opt),
756
+ presetRoot,
757
+ defaultFilename: DEFAULT_PRESET_OPTS_FILENAME,
758
+ });
759
+ const presetEnvsFiles = this.#resolvePresetFileSources({
760
+ cliFiles: fileScanResult.cliPresetEnvsFiles,
761
+ commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.env),
762
+ presetRoot,
763
+ defaultFilename: DEFAULT_PRESET_ENVS_FILENAME,
764
+ });
765
+ const userSources = {
766
+ cmds: [...ctx.sources.user.cmds],
767
+ argv: [...cleanArgv],
768
+ envs: { ...ctx.sources.user.envs },
769
+ };
770
+ const presetArgv = [];
771
+ for (const file of presetOptsFiles) {
772
+ const content = await this.#readPresetFile(file, commandPath);
773
+ if (content === undefined) {
774
+ continue;
775
+ }
776
+ const tokens = this.#tokenizePresetOptions(content);
777
+ this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
778
+ this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
779
+ presetArgv.push(...tokens);
780
+ }
781
+ const presetEnvs = {};
782
+ for (const file of presetEnvsFiles) {
783
+ const content = await this.#readPresetFile(file, commandPath);
784
+ if (content === undefined) {
785
+ continue;
786
+ }
787
+ let parsed;
788
+ try {
789
+ parsed = env.parse(content);
790
+ }
791
+ catch (error) {
792
+ throw new CommanderError('ConfigurationError', `failed to parse preset envs file "${file.displayPath}": ${error.message}`, commandPath);
793
+ }
794
+ Object.assign(presetEnvs, parsed);
795
+ }
796
+ const sources = {
797
+ user: userSources,
798
+ preset: {
799
+ argv: presetArgv,
800
+ envs: presetEnvs,
801
+ },
802
+ };
803
+ const envs = { ...sources.user.envs, ...sources.preset.envs };
804
+ const tailArgv = [...sources.preset.argv, ...sources.user.argv];
805
+ return { tailArgv, envs, sources };
806
+ }
807
+ #resolveCommandPresetFromChain(chain) {
808
+ for (let index = chain.length - 1; index >= 0; index -= 1) {
809
+ const preset = chain[index].#presetConfig;
810
+ if (preset?.root !== undefined) {
811
+ return preset;
812
+ }
813
+ }
814
+ return undefined;
815
+ }
816
+ async #resolveEffectivePresetRoot(cliPresetRoots, commandPreset, commandPath) {
817
+ if (cliPresetRoots.length > 0) {
818
+ const root = cliPresetRoots[cliPresetRoots.length - 1];
819
+ return await this.#assertPresetRoot(root, PRESET_ROOT_FLAG, commandPath);
820
+ }
821
+ if (commandPreset?.root === undefined) {
822
+ return undefined;
823
+ }
824
+ return await this.#assertPresetRoot(commandPreset.root, 'command.preset.root', commandPath);
825
+ }
826
+ async #assertPresetRoot(root, sourceName, commandPath) {
827
+ if (!path.isAbsolute(root)) {
828
+ throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not an absolute directory`, commandPath);
829
+ }
830
+ let stats;
831
+ try {
832
+ stats = await promises.stat(root);
833
+ }
834
+ catch (error) {
835
+ throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" cannot be accessed (${error.message})`, commandPath);
836
+ }
837
+ if (!stats.isDirectory()) {
838
+ throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not a directory`, commandPath);
839
+ }
840
+ return root;
841
+ }
842
+ #normalizeCommandPresetFile(filepath) {
843
+ if (filepath === undefined) {
844
+ return undefined;
845
+ }
846
+ if (!this.#isValidPresetFileValue(filepath)) {
847
+ return undefined;
848
+ }
849
+ return filepath;
850
+ }
851
+ #resolvePresetFileSources(params) {
852
+ const { cliFiles, commandPresetFile, presetRoot, defaultFilename } = params;
853
+ if (cliFiles.length > 0) {
854
+ return cliFiles.map(filepath => ({
855
+ displayPath: filepath,
856
+ absolutePath: this.#resolvePresetFileAbsolutePath(filepath, presetRoot),
857
+ explicit: true,
858
+ }));
859
+ }
860
+ if (presetRoot === undefined) {
861
+ return [];
862
+ }
863
+ if (commandPresetFile !== undefined) {
864
+ return [
865
+ {
866
+ displayPath: commandPresetFile,
867
+ absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
868
+ explicit: true,
869
+ },
870
+ ];
871
+ }
872
+ const absolutePath = path.resolve(presetRoot, defaultFilename);
873
+ return [
874
+ {
875
+ displayPath: absolutePath,
876
+ absolutePath,
877
+ explicit: false,
878
+ },
879
+ ];
880
+ }
881
+ #resolvePresetFileAbsolutePath(filepath, presetRoot) {
882
+ if (path.isAbsolute(filepath)) {
883
+ return filepath;
884
+ }
885
+ if (presetRoot !== undefined) {
886
+ return path.resolve(presetRoot, filepath);
887
+ }
888
+ return path.resolve(process.cwd(), filepath);
889
+ }
890
+ #assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
891
+ if (tokens.length === 0) {
892
+ return;
893
+ }
894
+ const commandPath = chain[chain.length - 1].#getCommandPath();
895
+ try {
896
+ const { optionTokens, restArgs } = tokenize(tokens, commandPath);
897
+ void restArgs;
898
+ const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
899
+ if (argTokens.length > 0) {
900
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
901
+ }
902
+ }
903
+ catch (error) {
904
+ if (error instanceof CommanderError) {
905
+ if (error.kind === 'ConfigurationError') {
906
+ throw error;
907
+ }
908
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
909
+ }
910
+ throw error;
911
+ }
912
+ }
913
+ #scanPresetRootDirectives(argv, commandPath) {
914
+ const cleanArgv = [];
915
+ const cliPresetRoots = [];
916
+ let index = 0;
917
+ while (index < argv.length) {
918
+ const token = argv[index];
919
+ if (token === PRESET_ROOT_FLAG) {
920
+ const value = argv[index + 1];
921
+ if (value === undefined || value.length === 0) {
922
+ throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
923
+ }
924
+ cliPresetRoots.push(value);
925
+ index += 2;
926
+ continue;
927
+ }
928
+ if (token.startsWith(`${PRESET_ROOT_FLAG}=`)) {
929
+ const value = token.slice(PRESET_ROOT_FLAG.length + 1);
930
+ if (value.length === 0) {
931
+ throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
932
+ }
933
+ cliPresetRoots.push(value);
934
+ index += 1;
935
+ continue;
936
+ }
937
+ cleanArgv.push(token);
938
+ index += 1;
939
+ }
940
+ return { cleanArgv, cliPresetRoots };
941
+ }
942
+ #scanPresetFileDirectives(argv, commandPath) {
943
+ const cleanArgv = [];
944
+ const cliPresetOptsFiles = [];
945
+ const cliPresetEnvsFiles = [];
946
+ const assertAndPush = (flag, value) => {
947
+ this.#assertPresetFileValue(value, flag, commandPath);
948
+ if (flag === PRESET_OPTS_FLAG) {
949
+ cliPresetOptsFiles.push(value);
950
+ }
951
+ else {
952
+ cliPresetEnvsFiles.push(value);
953
+ }
954
+ };
955
+ let index = 0;
956
+ while (index < argv.length) {
957
+ const token = argv[index];
958
+ if (token === PRESET_OPTS_FLAG || token === PRESET_ENVS_FLAG) {
959
+ const value = argv[index + 1];
960
+ if (value === undefined || value.length === 0) {
961
+ throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
962
+ }
963
+ assertAndPush(token, value);
964
+ index += 2;
965
+ continue;
966
+ }
967
+ if (token.startsWith(`${PRESET_OPTS_FLAG}=`)) {
968
+ const value = token.slice(PRESET_OPTS_FLAG.length + 1);
969
+ if (value.length === 0) {
970
+ throw new CommanderError('ConfigurationError', `missing value for "${PRESET_OPTS_FLAG}"`, commandPath);
971
+ }
972
+ assertAndPush(PRESET_OPTS_FLAG, value);
973
+ index += 1;
974
+ continue;
975
+ }
976
+ if (token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
977
+ const value = token.slice(PRESET_ENVS_FLAG.length + 1);
978
+ if (value.length === 0) {
979
+ throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ENVS_FLAG}"`, commandPath);
980
+ }
981
+ assertAndPush(PRESET_ENVS_FLAG, value);
982
+ index += 1;
983
+ continue;
984
+ }
985
+ cleanArgv.push(token);
986
+ index += 1;
987
+ }
988
+ return { cleanArgv, cliPresetOptsFiles, cliPresetEnvsFiles };
989
+ }
990
+ #isValidPresetFileValue(filepath) {
991
+ return filepath.length > 0 && !filepath.startsWith('..');
992
+ }
993
+ #assertPresetFileValue(filepath, directive, commandPath) {
994
+ if (this.#isValidPresetFileValue(filepath)) {
995
+ return;
996
+ }
997
+ throw new CommanderError('ConfigurationError', `invalid value for "${directive}": "${filepath}" (must be non-empty and must not start with "..")`, commandPath);
998
+ }
999
+ async #readPresetFile(file, commandPath) {
1000
+ try {
1001
+ return await promises.readFile(file.absolutePath, 'utf8');
1002
+ }
1003
+ catch (error) {
1004
+ const ioError = error;
1005
+ if (!file.explicit && ioError.code === 'ENOENT') {
1006
+ return undefined;
1007
+ }
1008
+ throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${ioError.message}`, commandPath);
1009
+ }
1010
+ }
1011
+ #tokenizePresetOptions(content) {
1012
+ return content
1013
+ .split(/\s+/)
1014
+ .map(token => token.trim())
1015
+ .filter(token => token.length > 0);
1016
+ }
1017
+ #validatePresetOptionTokens(tokens, filepath, commandPath) {
1018
+ if (tokens.length === 0) {
1019
+ return;
1020
+ }
1021
+ if (!tokens[0].startsWith('-')) {
1022
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": bare token "${tokens[0]}" cannot appear before any option token`, commandPath);
1023
+ }
1024
+ for (const token of tokens) {
1025
+ if (token === '--') {
1026
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": "--" is not allowed`, commandPath);
1027
+ }
1028
+ if (token === 'help' || token === '--help' || token === '--version') {
1029
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
1030
+ }
1031
+ if (token === PRESET_ROOT_FLAG ||
1032
+ token.startsWith(`${PRESET_ROOT_FLAG}=`) ||
1033
+ token === PRESET_OPTS_FLAG ||
1034
+ token.startsWith(`${PRESET_OPTS_FLAG}=`) ||
1035
+ token === PRESET_ENVS_FLAG ||
1036
+ token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
1037
+ throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
1038
+ }
1039
+ }
677
1040
  }
678
1041
  #resolve(chain, tokens, optionPolicyMap) {
679
1042
  const consumedTokens = new Map();
@@ -783,13 +1146,20 @@ class Command {
783
1146
  }
784
1147
  }
785
1148
  }
786
- const mergedOpts = {};
787
- for (const cmd of chain) {
788
- Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
1149
+ const leafLocalOpts = {};
1150
+ const leafParsedOpts = optsMap.get(leafCommand) ?? {};
1151
+ for (const opt of leafCommand.#options) {
1152
+ if (Object.prototype.hasOwnProperty.call(leafParsedOpts, opt.long)) {
1153
+ leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1154
+ }
789
1155
  }
790
1156
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
791
1157
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
792
- return { ctx, opts: mergedOpts, args, rawArgs };
1158
+ const parseCtx = {
1159
+ ...ctx,
1160
+ sources: this.#freezeInputSources(ctx.sources),
1161
+ };
1162
+ return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
793
1163
  }
794
1164
  #parseOptions(tokens, allOptions, envs) {
795
1165
  const opts = {};
@@ -976,29 +1346,19 @@ class Command {
976
1346
  #hasUserOption(long) {
977
1347
  return this.#options.some(option => option.long === long);
978
1348
  }
979
- #canUseBuiltinVersion() {
980
- return this.#version !== undefined;
1349
+ #supportsBuiltinVersion() {
1350
+ return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
981
1351
  }
982
1352
  #resolveOptionPolicy() {
983
1353
  const optionMap = new Map();
984
1354
  const hasUserColor = this.#hasUserOption('color');
985
- const hasUserHelp = this.#hasUserOption('help');
986
- const hasUserVersion = this.#hasUserOption('version');
987
1355
  const hasUserLogLevel = this.#hasUserOption('logLevel');
988
1356
  const hasUserSilent = this.#hasUserOption('silent');
989
1357
  const hasUserLogDate = this.#hasUserOption('logDate');
990
1358
  const hasUserLogColorful = this.#hasUserOption('logColorful');
991
- const enableBuiltinHelp = !hasUserHelp;
992
- const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
993
1359
  if (this.#builtin.option.color && !hasUserColor) {
994
1360
  optionMap.set('color', BUILTIN_COLOR_OPTION);
995
1361
  }
996
- if (enableBuiltinHelp) {
997
- optionMap.set('help', BUILTIN_HELP_OPTION);
998
- }
999
- if (enableBuiltinVersion) {
1000
- optionMap.set('version', BUILTIN_VERSION_OPTION);
1001
- }
1002
1362
  if (this.#builtin.option.logLevel && !hasUserLogLevel) {
1003
1363
  optionMap.set('logLevel', logLevelOption);
1004
1364
  }
@@ -1016,8 +1376,6 @@ class Command {
1016
1376
  }
1017
1377
  return {
1018
1378
  mergedOptions: Array.from(optionMap.values()),
1019
- enableBuiltinHelp,
1020
- enableBuiltinVersion,
1021
1379
  };
1022
1380
  }
1023
1381
  #buildOptionPolicyMap(chain) {
@@ -1054,6 +1412,9 @@ class Command {
1054
1412
  }
1055
1413
  }
1056
1414
  #validateOptionConfig(opt) {
1415
+ if (opt.long === 'help' || opt.long === 'version') {
1416
+ throw new CommanderError('ConfigurationError', `option long name "${opt.long}" is reserved`, this.#getCommandPath());
1417
+ }
1057
1418
  if (opt.type === 'boolean' && opt.args !== 'none') {
1058
1419
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1059
1420
  }
@@ -1134,22 +1495,27 @@ class Command {
1134
1495
  process.exit(1);
1135
1496
  }
1136
1497
  }
1137
- #resolveHelpColorOption(tokens, envs, policy = this.#resolveOptionPolicy()) {
1498
+ #resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
1138
1499
  const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
1139
1500
  let color = !isNoColorEnabled(envs);
1140
1501
  if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
1141
1502
  return color;
1142
1503
  }
1143
- for (const token of tokens) {
1144
- if (token.type !== 'long' || token.name !== 'color') {
1504
+ const separatorIndex = tailArgv.indexOf('--');
1505
+ const scanTokens = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
1506
+ for (const token of scanTokens) {
1507
+ if (token === '--color') {
1508
+ color = true;
1145
1509
  continue;
1146
1510
  }
1147
- const eqIdx = token.resolved.indexOf('=');
1148
- if (eqIdx === -1) {
1149
- color = true;
1511
+ if (token === '--no-color') {
1512
+ color = false;
1513
+ continue;
1514
+ }
1515
+ if (!token.startsWith('--color=')) {
1150
1516
  continue;
1151
1517
  }
1152
- const value = token.resolved.slice(eqIdx + 1);
1518
+ const value = token.slice('--color='.length);
1153
1519
  if (value === 'true') {
1154
1520
  color = true;
1155
1521
  }
@@ -1162,16 +1528,18 @@ class Command {
1162
1528
  }
1163
1529
  return color;
1164
1530
  }
1165
- #hasFlag(tokens, longName, shortName) {
1166
- for (const token of tokens) {
1167
- if (token.type === 'long' && token.name === longName) {
1168
- return true;
1169
- }
1170
- if (token.type === 'short' && token.name === shortName) {
1171
- return true;
1172
- }
1173
- }
1174
- return false;
1531
+ #freezeInputSources(sources) {
1532
+ return Object.freeze({
1533
+ preset: Object.freeze({
1534
+ argv: Object.freeze([...sources.preset.argv]),
1535
+ envs: Object.freeze({ ...sources.preset.envs }),
1536
+ }),
1537
+ user: Object.freeze({
1538
+ cmds: Object.freeze([...sources.user.cmds]),
1539
+ argv: Object.freeze([...sources.user.argv]),
1540
+ envs: Object.freeze({ ...sources.user.envs }),
1541
+ }),
1542
+ });
1175
1543
  }
1176
1544
  #getCommandPath() {
1177
1545
  const parts = [];
@@ -1340,9 +1708,12 @@ function camelToKebabCase(str) {
1340
1708
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
1341
1709
  }
1342
1710
  class CompletionCommand extends Command {
1343
- constructor(root, config) {
1344
- const paths = config.paths;
1711
+ constructor(root, config = {}) {
1345
1712
  const programName = config.programName ?? root.name ?? 'program';
1713
+ const paths = {
1714
+ ...createDefaultCompletionPaths(programName),
1715
+ ...config.paths,
1716
+ };
1346
1717
  super({ desc: 'Generate shell completion script' });
1347
1718
  this.option({
1348
1719
  long: 'bash',
@@ -1417,6 +1788,13 @@ class CompletionCommand extends Command {
1417
1788
  });
1418
1789
  }
1419
1790
  }
1791
+ function createDefaultCompletionPaths(programName) {
1792
+ return {
1793
+ bash: `~/.local/share/bash-completion/completions/${programName}`,
1794
+ fish: `~/.config/fish/completions/${programName}.fish`,
1795
+ pwsh: '~/.config/powershell/Microsoft.PowerShell_profile.ps1',
1796
+ };
1797
+ }
1420
1798
  function expandHome(filepath) {
1421
1799
  if (filepath.startsWith('~/') || filepath === '~') {
1422
1800
  const home = process.env['HOME'] || process.env['USERPROFILE'] || '';