@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/CHANGELOG.md +7 -0
- package/README.md +81 -25
- package/lib/cjs/index.cjs +526 -148
- package/lib/esm/index.mjs +525 -146
- package/lib/types/index.d.ts +70 -24
- package/package.json +2 -1
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
|
|
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:
|
|
60
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
46
61
|
coerce: (raw) => {
|
|
47
|
-
const level =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
346
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name
|
|
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
|
|
383
|
+
const { argv, envs, reporter } = params;
|
|
366
384
|
try {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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 (
|
|
381
|
-
|
|
382
|
-
|
|
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.#
|
|
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
|
|
422
|
-
const
|
|
423
|
-
const
|
|
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
|
|
427
|
-
|
|
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
|
|
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' &&
|
|
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
|
-
|
|
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
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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.
|
|
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
|
-
#
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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'] || '';
|