@guanghechen/commander 4.5.0 → 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 +14 -0
- package/README.md +131 -29
- package/lib/cjs/index.cjs +657 -150
- package/lib/esm/index.mjs +653 -149
- package/lib/types/index.d.ts +124 -18
- package/package.json +2 -1
package/lib/esm/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { parse } from '@guanghechen/env';
|
|
2
|
+
import { Reporter } from '@guanghechen/reporter';
|
|
3
|
+
import { stat, readFile } from 'node:fs/promises';
|
|
3
4
|
import * as path from 'node:path';
|
|
5
|
+
import path__default from 'node:path';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
4
7
|
|
|
5
8
|
const TERMINAL_STYLE = {
|
|
6
9
|
bold: '\x1b[1m',
|
|
@@ -14,22 +17,35 @@ function styleText(text, ...styles) {
|
|
|
14
17
|
return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const BUILTIN_LOG_LEVELS = ['debug', 'info', 'hint', 'warn', 'error'];
|
|
21
|
+
function resolveReporterLogLevel(raw) {
|
|
22
|
+
const normalized = raw.trim().toLowerCase();
|
|
23
|
+
return BUILTIN_LOG_LEVELS.find(level => level === normalized);
|
|
24
|
+
}
|
|
25
|
+
function setReporterLevel(ctx, level) {
|
|
26
|
+
const reporter = ctx.reporter;
|
|
27
|
+
reporter?.setLevel?.(level);
|
|
28
|
+
}
|
|
29
|
+
function setReporterFlight(ctx, flight) {
|
|
30
|
+
const reporter = ctx.reporter;
|
|
31
|
+
reporter?.setFlight?.(flight);
|
|
32
|
+
}
|
|
17
33
|
const logLevelOption = {
|
|
18
34
|
long: 'logLevel',
|
|
19
35
|
type: 'string',
|
|
20
36
|
args: 'required',
|
|
21
37
|
desc: 'Set log level',
|
|
22
38
|
default: 'info',
|
|
23
|
-
choices:
|
|
39
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
24
40
|
coerce: (raw) => {
|
|
25
|
-
const level =
|
|
41
|
+
const level = resolveReporterLogLevel(raw);
|
|
26
42
|
if (level === undefined) {
|
|
27
43
|
throw new Error(`Invalid log level: ${raw}`);
|
|
28
44
|
}
|
|
29
45
|
return level;
|
|
30
46
|
},
|
|
31
47
|
apply: (value, ctx) => {
|
|
32
|
-
ctx
|
|
48
|
+
setReporterLevel(ctx, value);
|
|
33
49
|
},
|
|
34
50
|
};
|
|
35
51
|
const logDateOption = {
|
|
@@ -39,7 +55,7 @@ const logDateOption = {
|
|
|
39
55
|
desc: 'Enable log timestamp',
|
|
40
56
|
default: true,
|
|
41
57
|
apply: (value, ctx) => {
|
|
42
|
-
ctx
|
|
58
|
+
setReporterFlight(ctx, { date: Boolean(value) });
|
|
43
59
|
},
|
|
44
60
|
};
|
|
45
61
|
const logColorfulOption = {
|
|
@@ -49,7 +65,7 @@ const logColorfulOption = {
|
|
|
49
65
|
desc: 'Enable colorful log output',
|
|
50
66
|
default: true,
|
|
51
67
|
apply: (value, ctx) => {
|
|
52
|
-
ctx
|
|
68
|
+
setReporterFlight(ctx, { color: Boolean(value) });
|
|
53
69
|
},
|
|
54
70
|
};
|
|
55
71
|
const silentOption = {
|
|
@@ -60,7 +76,7 @@ const silentOption = {
|
|
|
60
76
|
default: false,
|
|
61
77
|
apply: (value, ctx) => {
|
|
62
78
|
if (value) {
|
|
63
|
-
ctx
|
|
79
|
+
setReporterLevel(ctx, 'error');
|
|
64
80
|
}
|
|
65
81
|
},
|
|
66
82
|
};
|
|
@@ -81,6 +97,11 @@ class CommanderError extends Error {
|
|
|
81
97
|
|
|
82
98
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
83
99
|
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
100
|
+
const PRESET_OPTS_FLAG = '--preset-opts';
|
|
101
|
+
const PRESET_ENVS_FLAG = '--preset-envs';
|
|
102
|
+
const PRESET_ROOT_FLAG = '--preset-root';
|
|
103
|
+
const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
|
|
104
|
+
const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
|
|
84
105
|
function kebabToCamelCase(str) {
|
|
85
106
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
86
107
|
}
|
|
@@ -168,14 +189,12 @@ function tokenize(argv, commandPath) {
|
|
|
168
189
|
}
|
|
169
190
|
const BUILTIN_HELP_OPTION = {
|
|
170
191
|
long: 'help',
|
|
171
|
-
short: 'h',
|
|
172
192
|
type: 'boolean',
|
|
173
193
|
args: 'none',
|
|
174
194
|
desc: 'Show help information',
|
|
175
195
|
};
|
|
176
196
|
const BUILTIN_VERSION_OPTION = {
|
|
177
197
|
long: 'version',
|
|
178
|
-
short: 'V',
|
|
179
198
|
type: 'boolean',
|
|
180
199
|
args: 'none',
|
|
181
200
|
desc: 'Show version number',
|
|
@@ -189,6 +208,7 @@ const BUILTIN_COLOR_OPTION = {
|
|
|
189
208
|
};
|
|
190
209
|
function createBuiltinOptionState(enabled) {
|
|
191
210
|
return {
|
|
211
|
+
version: enabled,
|
|
192
212
|
color: enabled,
|
|
193
213
|
logLevel: enabled,
|
|
194
214
|
silent: enabled,
|
|
@@ -202,9 +222,6 @@ function isNoColorEnabled(envs) {
|
|
|
202
222
|
function normalizeBuiltinConfig(builtin) {
|
|
203
223
|
const resolved = {
|
|
204
224
|
option: createBuiltinOptionState(true),
|
|
205
|
-
command: {
|
|
206
|
-
help: false,
|
|
207
|
-
},
|
|
208
225
|
};
|
|
209
226
|
if (builtin === undefined) {
|
|
210
227
|
return resolved;
|
|
@@ -212,13 +229,11 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
212
229
|
if (builtin === true) {
|
|
213
230
|
return {
|
|
214
231
|
option: createBuiltinOptionState(true),
|
|
215
|
-
command: { help: true },
|
|
216
232
|
};
|
|
217
233
|
}
|
|
218
234
|
if (builtin === false) {
|
|
219
235
|
return {
|
|
220
236
|
option: createBuiltinOptionState(false),
|
|
221
|
-
command: { help: false },
|
|
222
237
|
};
|
|
223
238
|
}
|
|
224
239
|
if (builtin.option !== undefined) {
|
|
@@ -229,6 +244,8 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
229
244
|
resolved.option = createBuiltinOptionState(true);
|
|
230
245
|
}
|
|
231
246
|
else {
|
|
247
|
+
if (builtin.option.version !== undefined)
|
|
248
|
+
resolved.option.version = builtin.option.version;
|
|
232
249
|
if (builtin.option.color !== undefined)
|
|
233
250
|
resolved.option.color = builtin.option.color;
|
|
234
251
|
if (builtin.option.logLevel !== undefined) {
|
|
@@ -243,24 +260,15 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
243
260
|
}
|
|
244
261
|
}
|
|
245
262
|
}
|
|
246
|
-
if (builtin.command !== undefined) {
|
|
247
|
-
if (builtin.command === false) {
|
|
248
|
-
resolved.command = { help: false };
|
|
249
|
-
}
|
|
250
|
-
else if (builtin.command === true) {
|
|
251
|
-
resolved.command = { help: true };
|
|
252
|
-
}
|
|
253
|
-
else if (builtin.command.help !== undefined) {
|
|
254
|
-
resolved.command.help = builtin.command.help;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
263
|
return resolved;
|
|
258
264
|
}
|
|
259
265
|
class Command {
|
|
260
266
|
#name;
|
|
261
267
|
#desc;
|
|
262
268
|
#version;
|
|
269
|
+
#builtinConfig;
|
|
263
270
|
#builtin;
|
|
271
|
+
#presetConfig;
|
|
264
272
|
#reporter;
|
|
265
273
|
#parent;
|
|
266
274
|
#options = [];
|
|
@@ -273,7 +281,9 @@ class Command {
|
|
|
273
281
|
this.#name = config.name ?? '';
|
|
274
282
|
this.#desc = config.desc;
|
|
275
283
|
this.#version = config.version;
|
|
284
|
+
this.#builtinConfig = config.builtin;
|
|
276
285
|
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
286
|
+
this.#presetConfig = config.preset;
|
|
277
287
|
this.#reporter = config.reporter;
|
|
278
288
|
}
|
|
279
289
|
get name() {
|
|
@@ -285,6 +295,12 @@ class Command {
|
|
|
285
295
|
get version() {
|
|
286
296
|
return this.#version;
|
|
287
297
|
}
|
|
298
|
+
get builtin() {
|
|
299
|
+
return this.#builtinConfig;
|
|
300
|
+
}
|
|
301
|
+
get preset() {
|
|
302
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
303
|
+
}
|
|
288
304
|
get parent() {
|
|
289
305
|
return this.#parent;
|
|
290
306
|
}
|
|
@@ -320,14 +336,17 @@ class Command {
|
|
|
320
336
|
return this;
|
|
321
337
|
}
|
|
322
338
|
subcommand(name, cmd) {
|
|
323
|
-
if (
|
|
324
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name
|
|
339
|
+
if (name === 'help') {
|
|
340
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
|
|
325
341
|
}
|
|
326
342
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
327
343
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
328
344
|
}
|
|
329
345
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
330
346
|
if (existing) {
|
|
347
|
+
if (existing.aliases.includes(name)) {
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
331
350
|
existing.aliases.push(name);
|
|
332
351
|
this.#subcommandsMap.set(name, cmd);
|
|
333
352
|
}
|
|
@@ -342,32 +361,35 @@ class Command {
|
|
|
342
361
|
async run(params) {
|
|
343
362
|
const { argv, envs, reporter } = params;
|
|
344
363
|
try {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const { chain, remaining } = routeResult;
|
|
364
|
+
const routeResult = this.#route(argv);
|
|
365
|
+
const { chain } = routeResult;
|
|
348
366
|
const leafCommand = chain[chain.length - 1];
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
367
|
+
const ctx = this.#createContext({
|
|
368
|
+
chain,
|
|
369
|
+
cmds: routeResult.cmds,
|
|
370
|
+
envs,
|
|
371
|
+
reporter,
|
|
372
|
+
});
|
|
373
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
374
|
+
ctx.controls = controlScanResult.controls;
|
|
375
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
376
|
+
if (ctx.controls.help) {
|
|
377
|
+
const helpCommand = this.#resolveHelpCommand(leafCommand, controlScanResult.helpTarget);
|
|
378
|
+
const helpColor = helpCommand.#resolveHelpColorFromTailArgv(controlScanResult.remaining, ctx.envs);
|
|
379
|
+
console.log(helpCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
356
380
|
return;
|
|
357
381
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
382
|
+
if (ctx.controls.version) {
|
|
383
|
+
console.log(leafCommand.#version);
|
|
384
|
+
return;
|
|
363
385
|
}
|
|
386
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
387
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
388
|
+
ctx.sources = presetResult.sources;
|
|
389
|
+
ctx.envs = presetResult.envs;
|
|
390
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
391
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
364
392
|
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
365
|
-
const ctx = {
|
|
366
|
-
cmd: leafCommand,
|
|
367
|
-
envs,
|
|
368
|
-
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
369
|
-
argv,
|
|
370
|
-
};
|
|
371
393
|
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
372
394
|
const actionParams = {
|
|
373
395
|
ctx: parseResult.ctx,
|
|
@@ -379,7 +401,7 @@ class Command {
|
|
|
379
401
|
await leafCommand.#runAction(actionParams);
|
|
380
402
|
}
|
|
381
403
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
382
|
-
const helpColor = leafCommand.#
|
|
404
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
383
405
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
384
406
|
}
|
|
385
407
|
else {
|
|
@@ -395,22 +417,27 @@ class Command {
|
|
|
395
417
|
throw err;
|
|
396
418
|
}
|
|
397
419
|
}
|
|
398
|
-
parse(params) {
|
|
420
|
+
async parse(params) {
|
|
399
421
|
const { argv, envs, reporter } = params;
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const { chain, remaining } = routeResult;
|
|
422
|
+
const routeResult = this.#route(argv);
|
|
423
|
+
const { chain } = routeResult;
|
|
403
424
|
const leafCommand = chain[chain.length - 1];
|
|
404
|
-
const
|
|
405
|
-
|
|
425
|
+
const ctx = this.#createContext({
|
|
426
|
+
chain,
|
|
427
|
+
cmds: routeResult.cmds,
|
|
428
|
+
envs,
|
|
429
|
+
reporter,
|
|
430
|
+
});
|
|
431
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
432
|
+
ctx.controls = controlScanResult.controls;
|
|
433
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
406
434
|
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
435
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
436
|
+
ctx.sources = presetResult.sources;
|
|
437
|
+
ctx.envs = presetResult.envs;
|
|
438
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
439
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
407
440
|
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
408
|
-
const ctx = {
|
|
409
|
-
cmd: leafCommand,
|
|
410
|
-
envs,
|
|
411
|
-
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
412
|
-
argv,
|
|
413
|
-
};
|
|
414
441
|
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
415
442
|
}
|
|
416
443
|
formatHelp() {
|
|
@@ -428,7 +455,11 @@ class Command {
|
|
|
428
455
|
return color && process.stdout.isTTY === true;
|
|
429
456
|
}
|
|
430
457
|
#buildHelpData() {
|
|
431
|
-
const
|
|
458
|
+
const parseOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
459
|
+
const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
|
|
460
|
+
if (this.#supportsBuiltinVersion()) {
|
|
461
|
+
allOptions.push(BUILTIN_VERSION_OPTION);
|
|
462
|
+
}
|
|
432
463
|
const commandPath = this.#getCommandPath();
|
|
433
464
|
let usage = `Usage: ${commandPath}`;
|
|
434
465
|
if (allOptions.length > 0)
|
|
@@ -462,7 +493,10 @@ class Command {
|
|
|
462
493
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
463
494
|
}
|
|
464
495
|
options.push({ sig, desc });
|
|
465
|
-
if (opt.type === 'boolean' &&
|
|
496
|
+
if (opt.type === 'boolean' &&
|
|
497
|
+
opt.args === 'none' &&
|
|
498
|
+
opt.long !== 'help' &&
|
|
499
|
+
opt.long !== 'version') {
|
|
466
500
|
options.push({
|
|
467
501
|
sig: ` --no-${kebabLong}`,
|
|
468
502
|
desc: `Negate --${kebabLong}`,
|
|
@@ -470,8 +504,7 @@ class Command {
|
|
|
470
504
|
}
|
|
471
505
|
}
|
|
472
506
|
const commands = [];
|
|
473
|
-
|
|
474
|
-
if (showHelpSubcommand) {
|
|
507
|
+
if (this.#subcommandsList.length > 0) {
|
|
475
508
|
commands.push({ name: 'help', desc: 'Show help for a command' });
|
|
476
509
|
}
|
|
477
510
|
for (const entry of this.#subcommandsList) {
|
|
@@ -594,50 +627,9 @@ class Command {
|
|
|
594
627
|
#findSubcommandEntry(token) {
|
|
595
628
|
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
596
629
|
}
|
|
597
|
-
#createUnknownSubcommandError(subcommand) {
|
|
598
|
-
const commandPath = this.#getCommandPath();
|
|
599
|
-
return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
|
|
600
|
-
}
|
|
601
|
-
#processHelpSubcommand(argv) {
|
|
602
|
-
let current = this;
|
|
603
|
-
for (let i = 0; i < argv.length; ++i) {
|
|
604
|
-
const token = argv[i];
|
|
605
|
-
if (token.startsWith('-')) {
|
|
606
|
-
return argv;
|
|
607
|
-
}
|
|
608
|
-
if (token === 'help') {
|
|
609
|
-
if (!current.#builtin.command.help) {
|
|
610
|
-
if (current.#subcommandsList.length > 0) {
|
|
611
|
-
throw current.#createUnknownSubcommandError('help');
|
|
612
|
-
}
|
|
613
|
-
return argv;
|
|
614
|
-
}
|
|
615
|
-
if (current.#subcommandsList.length === 0) {
|
|
616
|
-
return argv;
|
|
617
|
-
}
|
|
618
|
-
const target = argv[i + 1];
|
|
619
|
-
if (target === undefined) {
|
|
620
|
-
return [...argv.slice(0, i), '--help'];
|
|
621
|
-
}
|
|
622
|
-
const targetEntry = current.#findSubcommandEntry(target);
|
|
623
|
-
if (targetEntry === undefined) {
|
|
624
|
-
throw current.#createUnknownSubcommandError(target);
|
|
625
|
-
}
|
|
626
|
-
if (argv[i + 2] !== undefined) {
|
|
627
|
-
throw new CommanderError('UnexpectedArgument', 'help subcommand accepts at most one subcommand argument', current.#getCommandPath());
|
|
628
|
-
}
|
|
629
|
-
return [...argv.slice(0, i), target, '--help'];
|
|
630
|
-
}
|
|
631
|
-
const entry = current.#findSubcommandEntry(token);
|
|
632
|
-
if (entry === undefined) {
|
|
633
|
-
return argv;
|
|
634
|
-
}
|
|
635
|
-
current = entry.command;
|
|
636
|
-
}
|
|
637
|
-
return argv;
|
|
638
|
-
}
|
|
639
630
|
#route(argv) {
|
|
640
631
|
const chain = [this];
|
|
632
|
+
const cmds = [];
|
|
641
633
|
let current = this;
|
|
642
634
|
let idx = 0;
|
|
643
635
|
while (idx < argv.length) {
|
|
@@ -648,10 +640,382 @@ class Command {
|
|
|
648
640
|
if (!entry)
|
|
649
641
|
break;
|
|
650
642
|
current = entry.command;
|
|
643
|
+
cmds.push(token);
|
|
651
644
|
chain.push(current);
|
|
652
645
|
idx += 1;
|
|
653
646
|
}
|
|
654
|
-
return { chain, remaining: argv.slice(idx) };
|
|
647
|
+
return { chain, remaining: argv.slice(idx), cmds };
|
|
648
|
+
}
|
|
649
|
+
#controlScan(tailArgv, leafCommand) {
|
|
650
|
+
const controls = { help: false, version: false };
|
|
651
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
652
|
+
const beforeSeparator = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
653
|
+
const afterSeparator = separatorIndex === -1 ? [] : tailArgv.slice(separatorIndex + 1);
|
|
654
|
+
let helpTarget;
|
|
655
|
+
let scanStartIndex = 0;
|
|
656
|
+
if (beforeSeparator[0] === 'help') {
|
|
657
|
+
controls.help = true;
|
|
658
|
+
scanStartIndex = 1;
|
|
659
|
+
const candidate = beforeSeparator[1];
|
|
660
|
+
if (candidate !== undefined && !candidate.startsWith('-')) {
|
|
661
|
+
helpTarget = candidate;
|
|
662
|
+
scanStartIndex = 2;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const remainingBeforeSeparator = [];
|
|
666
|
+
for (let i = scanStartIndex; i < beforeSeparator.length; i += 1) {
|
|
667
|
+
const token = beforeSeparator[i];
|
|
668
|
+
if (token === '--help') {
|
|
669
|
+
controls.help = true;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (token === '--version' && leafCommand.#supportsBuiltinVersion()) {
|
|
673
|
+
controls.version = true;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
remainingBeforeSeparator.push(token);
|
|
677
|
+
}
|
|
678
|
+
const remaining = separatorIndex === -1
|
|
679
|
+
? remainingBeforeSeparator
|
|
680
|
+
: [...remainingBeforeSeparator, '--', ...afterSeparator];
|
|
681
|
+
return {
|
|
682
|
+
controls,
|
|
683
|
+
remaining,
|
|
684
|
+
helpTarget,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
#createContext(params) {
|
|
688
|
+
const { chain, cmds, envs, reporter } = params;
|
|
689
|
+
const leafCommand = chain[chain.length - 1];
|
|
690
|
+
const envSnapshot = { ...envs };
|
|
691
|
+
return {
|
|
692
|
+
cmd: leafCommand,
|
|
693
|
+
chain,
|
|
694
|
+
envs: envSnapshot,
|
|
695
|
+
controls: { help: false, version: false },
|
|
696
|
+
sources: {
|
|
697
|
+
preset: {
|
|
698
|
+
argv: [],
|
|
699
|
+
envs: {},
|
|
700
|
+
},
|
|
701
|
+
user: {
|
|
702
|
+
cmds: [...cmds],
|
|
703
|
+
argv: [],
|
|
704
|
+
envs: envSnapshot,
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
#resolveHelpCommand(leafCommand, helpTarget) {
|
|
711
|
+
if (helpTarget === undefined) {
|
|
712
|
+
return leafCommand;
|
|
713
|
+
}
|
|
714
|
+
const target = leafCommand.#findSubcommandEntry(helpTarget);
|
|
715
|
+
if (target === undefined) {
|
|
716
|
+
return leafCommand;
|
|
717
|
+
}
|
|
718
|
+
return target.command;
|
|
719
|
+
}
|
|
720
|
+
async #preset(controlTailArgv, ctx, optionPolicyMap) {
|
|
721
|
+
const commandPath = ctx.chain[ctx.chain.length - 1].#getCommandPath();
|
|
722
|
+
const separatorIndex = controlTailArgv.indexOf('--');
|
|
723
|
+
const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
|
|
724
|
+
const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
|
|
725
|
+
const rootScanResult = this.#scanPresetRootDirectives(beforeSeparator, commandPath);
|
|
726
|
+
const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
|
|
727
|
+
const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
|
|
728
|
+
const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
|
|
729
|
+
const cleanArgv = separatorIndex === -1
|
|
730
|
+
? fileScanResult.cleanArgv
|
|
731
|
+
: [...fileScanResult.cleanArgv, '--', ...afterSeparator];
|
|
732
|
+
const presetOptsFiles = this.#resolvePresetFileSources({
|
|
733
|
+
cliFiles: fileScanResult.cliPresetOptsFiles,
|
|
734
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.opt),
|
|
735
|
+
presetRoot,
|
|
736
|
+
defaultFilename: DEFAULT_PRESET_OPTS_FILENAME,
|
|
737
|
+
});
|
|
738
|
+
const presetEnvsFiles = this.#resolvePresetFileSources({
|
|
739
|
+
cliFiles: fileScanResult.cliPresetEnvsFiles,
|
|
740
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.env),
|
|
741
|
+
presetRoot,
|
|
742
|
+
defaultFilename: DEFAULT_PRESET_ENVS_FILENAME,
|
|
743
|
+
});
|
|
744
|
+
const userSources = {
|
|
745
|
+
cmds: [...ctx.sources.user.cmds],
|
|
746
|
+
argv: [...cleanArgv],
|
|
747
|
+
envs: { ...ctx.sources.user.envs },
|
|
748
|
+
};
|
|
749
|
+
const presetArgv = [];
|
|
750
|
+
for (const file of presetOptsFiles) {
|
|
751
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
752
|
+
if (content === undefined) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const tokens = this.#tokenizePresetOptions(content);
|
|
756
|
+
this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
|
|
757
|
+
this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
|
|
758
|
+
presetArgv.push(...tokens);
|
|
759
|
+
}
|
|
760
|
+
const presetEnvs = {};
|
|
761
|
+
for (const file of presetEnvsFiles) {
|
|
762
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
763
|
+
if (content === undefined) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
let parsed;
|
|
767
|
+
try {
|
|
768
|
+
parsed = parse(content);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset envs file "${file.displayPath}": ${error.message}`, commandPath);
|
|
772
|
+
}
|
|
773
|
+
Object.assign(presetEnvs, parsed);
|
|
774
|
+
}
|
|
775
|
+
const sources = {
|
|
776
|
+
user: userSources,
|
|
777
|
+
preset: {
|
|
778
|
+
argv: presetArgv,
|
|
779
|
+
envs: presetEnvs,
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
const envs = { ...sources.user.envs, ...sources.preset.envs };
|
|
783
|
+
const tailArgv = [...sources.preset.argv, ...sources.user.argv];
|
|
784
|
+
return { tailArgv, envs, sources };
|
|
785
|
+
}
|
|
786
|
+
#resolveCommandPresetFromChain(chain) {
|
|
787
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
788
|
+
const preset = chain[index].#presetConfig;
|
|
789
|
+
if (preset?.root !== undefined) {
|
|
790
|
+
return preset;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
async #resolveEffectivePresetRoot(cliPresetRoots, commandPreset, commandPath) {
|
|
796
|
+
if (cliPresetRoots.length > 0) {
|
|
797
|
+
const root = cliPresetRoots[cliPresetRoots.length - 1];
|
|
798
|
+
return await this.#assertPresetRoot(root, PRESET_ROOT_FLAG, commandPath);
|
|
799
|
+
}
|
|
800
|
+
if (commandPreset?.root === undefined) {
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
return await this.#assertPresetRoot(commandPreset.root, 'command.preset.root', commandPath);
|
|
804
|
+
}
|
|
805
|
+
async #assertPresetRoot(root, sourceName, commandPath) {
|
|
806
|
+
if (!path__default.isAbsolute(root)) {
|
|
807
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not an absolute directory`, commandPath);
|
|
808
|
+
}
|
|
809
|
+
let stats;
|
|
810
|
+
try {
|
|
811
|
+
stats = await stat(root);
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" cannot be accessed (${error.message})`, commandPath);
|
|
815
|
+
}
|
|
816
|
+
if (!stats.isDirectory()) {
|
|
817
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not a directory`, commandPath);
|
|
818
|
+
}
|
|
819
|
+
return root;
|
|
820
|
+
}
|
|
821
|
+
#normalizeCommandPresetFile(filepath) {
|
|
822
|
+
if (filepath === undefined) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
if (!this.#isValidPresetFileValue(filepath)) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
return filepath;
|
|
829
|
+
}
|
|
830
|
+
#resolvePresetFileSources(params) {
|
|
831
|
+
const { cliFiles, commandPresetFile, presetRoot, defaultFilename } = params;
|
|
832
|
+
if (cliFiles.length > 0) {
|
|
833
|
+
return cliFiles.map(filepath => ({
|
|
834
|
+
displayPath: filepath,
|
|
835
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(filepath, presetRoot),
|
|
836
|
+
explicit: true,
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
if (presetRoot === undefined) {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
if (commandPresetFile !== undefined) {
|
|
843
|
+
return [
|
|
844
|
+
{
|
|
845
|
+
displayPath: commandPresetFile,
|
|
846
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
|
|
847
|
+
explicit: true,
|
|
848
|
+
},
|
|
849
|
+
];
|
|
850
|
+
}
|
|
851
|
+
const absolutePath = path__default.resolve(presetRoot, defaultFilename);
|
|
852
|
+
return [
|
|
853
|
+
{
|
|
854
|
+
displayPath: absolutePath,
|
|
855
|
+
absolutePath,
|
|
856
|
+
explicit: false,
|
|
857
|
+
},
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
#resolvePresetFileAbsolutePath(filepath, presetRoot) {
|
|
861
|
+
if (path__default.isAbsolute(filepath)) {
|
|
862
|
+
return filepath;
|
|
863
|
+
}
|
|
864
|
+
if (presetRoot !== undefined) {
|
|
865
|
+
return path__default.resolve(presetRoot, filepath);
|
|
866
|
+
}
|
|
867
|
+
return path__default.resolve(process.cwd(), filepath);
|
|
868
|
+
}
|
|
869
|
+
#assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
|
|
870
|
+
if (tokens.length === 0) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const commandPath = chain[chain.length - 1].#getCommandPath();
|
|
874
|
+
try {
|
|
875
|
+
const { optionTokens, restArgs } = tokenize(tokens, commandPath);
|
|
876
|
+
void restArgs;
|
|
877
|
+
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
878
|
+
if (argTokens.length > 0) {
|
|
879
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
if (error instanceof CommanderError) {
|
|
884
|
+
if (error.kind === 'ConfigurationError') {
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
|
|
888
|
+
}
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
#scanPresetRootDirectives(argv, commandPath) {
|
|
893
|
+
const cleanArgv = [];
|
|
894
|
+
const cliPresetRoots = [];
|
|
895
|
+
let index = 0;
|
|
896
|
+
while (index < argv.length) {
|
|
897
|
+
const token = argv[index];
|
|
898
|
+
if (token === PRESET_ROOT_FLAG) {
|
|
899
|
+
const value = argv[index + 1];
|
|
900
|
+
if (value === undefined || value.length === 0) {
|
|
901
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
902
|
+
}
|
|
903
|
+
cliPresetRoots.push(value);
|
|
904
|
+
index += 2;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (token.startsWith(`${PRESET_ROOT_FLAG}=`)) {
|
|
908
|
+
const value = token.slice(PRESET_ROOT_FLAG.length + 1);
|
|
909
|
+
if (value.length === 0) {
|
|
910
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
911
|
+
}
|
|
912
|
+
cliPresetRoots.push(value);
|
|
913
|
+
index += 1;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
cleanArgv.push(token);
|
|
917
|
+
index += 1;
|
|
918
|
+
}
|
|
919
|
+
return { cleanArgv, cliPresetRoots };
|
|
920
|
+
}
|
|
921
|
+
#scanPresetFileDirectives(argv, commandPath) {
|
|
922
|
+
const cleanArgv = [];
|
|
923
|
+
const cliPresetOptsFiles = [];
|
|
924
|
+
const cliPresetEnvsFiles = [];
|
|
925
|
+
const assertAndPush = (flag, value) => {
|
|
926
|
+
this.#assertPresetFileValue(value, flag, commandPath);
|
|
927
|
+
if (flag === PRESET_OPTS_FLAG) {
|
|
928
|
+
cliPresetOptsFiles.push(value);
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
cliPresetEnvsFiles.push(value);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
let index = 0;
|
|
935
|
+
while (index < argv.length) {
|
|
936
|
+
const token = argv[index];
|
|
937
|
+
if (token === PRESET_OPTS_FLAG || token === PRESET_ENVS_FLAG) {
|
|
938
|
+
const value = argv[index + 1];
|
|
939
|
+
if (value === undefined || value.length === 0) {
|
|
940
|
+
throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
|
|
941
|
+
}
|
|
942
|
+
assertAndPush(token, value);
|
|
943
|
+
index += 2;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (token.startsWith(`${PRESET_OPTS_FLAG}=`)) {
|
|
947
|
+
const value = token.slice(PRESET_OPTS_FLAG.length + 1);
|
|
948
|
+
if (value.length === 0) {
|
|
949
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_OPTS_FLAG}"`, commandPath);
|
|
950
|
+
}
|
|
951
|
+
assertAndPush(PRESET_OPTS_FLAG, value);
|
|
952
|
+
index += 1;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
956
|
+
const value = token.slice(PRESET_ENVS_FLAG.length + 1);
|
|
957
|
+
if (value.length === 0) {
|
|
958
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ENVS_FLAG}"`, commandPath);
|
|
959
|
+
}
|
|
960
|
+
assertAndPush(PRESET_ENVS_FLAG, value);
|
|
961
|
+
index += 1;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
cleanArgv.push(token);
|
|
965
|
+
index += 1;
|
|
966
|
+
}
|
|
967
|
+
return { cleanArgv, cliPresetOptsFiles, cliPresetEnvsFiles };
|
|
968
|
+
}
|
|
969
|
+
#isValidPresetFileValue(filepath) {
|
|
970
|
+
return filepath.length > 0 && !filepath.startsWith('..');
|
|
971
|
+
}
|
|
972
|
+
#assertPresetFileValue(filepath, directive, commandPath) {
|
|
973
|
+
if (this.#isValidPresetFileValue(filepath)) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${directive}": "${filepath}" (must be non-empty and must not start with "..")`, commandPath);
|
|
977
|
+
}
|
|
978
|
+
async #readPresetFile(file, commandPath) {
|
|
979
|
+
try {
|
|
980
|
+
return await readFile(file.absolutePath, 'utf8');
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
const ioError = error;
|
|
984
|
+
if (!file.explicit && ioError.code === 'ENOENT') {
|
|
985
|
+
return undefined;
|
|
986
|
+
}
|
|
987
|
+
throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${ioError.message}`, commandPath);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
#tokenizePresetOptions(content) {
|
|
991
|
+
return content
|
|
992
|
+
.split(/\s+/)
|
|
993
|
+
.map(token => token.trim())
|
|
994
|
+
.filter(token => token.length > 0);
|
|
995
|
+
}
|
|
996
|
+
#validatePresetOptionTokens(tokens, filepath, commandPath) {
|
|
997
|
+
if (tokens.length === 0) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (!tokens[0].startsWith('-')) {
|
|
1001
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": bare token "${tokens[0]}" cannot appear before any option token`, commandPath);
|
|
1002
|
+
}
|
|
1003
|
+
for (const token of tokens) {
|
|
1004
|
+
if (token === '--') {
|
|
1005
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": "--" is not allowed`, commandPath);
|
|
1006
|
+
}
|
|
1007
|
+
if (token === 'help' || token === '--help' || token === '--version') {
|
|
1008
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
|
|
1009
|
+
}
|
|
1010
|
+
if (token === PRESET_ROOT_FLAG ||
|
|
1011
|
+
token.startsWith(`${PRESET_ROOT_FLAG}=`) ||
|
|
1012
|
+
token === PRESET_OPTS_FLAG ||
|
|
1013
|
+
token.startsWith(`${PRESET_OPTS_FLAG}=`) ||
|
|
1014
|
+
token === PRESET_ENVS_FLAG ||
|
|
1015
|
+
token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1016
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
655
1019
|
}
|
|
656
1020
|
#resolve(chain, tokens, optionPolicyMap) {
|
|
657
1021
|
const consumedTokens = new Map();
|
|
@@ -761,13 +1125,20 @@ class Command {
|
|
|
761
1125
|
}
|
|
762
1126
|
}
|
|
763
1127
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
1128
|
+
const leafLocalOpts = {};
|
|
1129
|
+
const leafParsedOpts = optsMap.get(leafCommand) ?? {};
|
|
1130
|
+
for (const opt of leafCommand.#options) {
|
|
1131
|
+
if (Object.prototype.hasOwnProperty.call(leafParsedOpts, opt.long)) {
|
|
1132
|
+
leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
|
|
1133
|
+
}
|
|
767
1134
|
}
|
|
768
1135
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
769
1136
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
770
|
-
|
|
1137
|
+
const parseCtx = {
|
|
1138
|
+
...ctx,
|
|
1139
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1140
|
+
};
|
|
1141
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
771
1142
|
}
|
|
772
1143
|
#parseOptions(tokens, allOptions, envs) {
|
|
773
1144
|
const opts = {};
|
|
@@ -954,29 +1325,19 @@ class Command {
|
|
|
954
1325
|
#hasUserOption(long) {
|
|
955
1326
|
return this.#options.some(option => option.long === long);
|
|
956
1327
|
}
|
|
957
|
-
#
|
|
958
|
-
return this.#version !== undefined;
|
|
1328
|
+
#supportsBuiltinVersion() {
|
|
1329
|
+
return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
|
|
959
1330
|
}
|
|
960
1331
|
#resolveOptionPolicy() {
|
|
961
1332
|
const optionMap = new Map();
|
|
962
1333
|
const hasUserColor = this.#hasUserOption('color');
|
|
963
|
-
const hasUserHelp = this.#hasUserOption('help');
|
|
964
|
-
const hasUserVersion = this.#hasUserOption('version');
|
|
965
1334
|
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
966
1335
|
const hasUserSilent = this.#hasUserOption('silent');
|
|
967
1336
|
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
968
1337
|
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
969
|
-
const enableBuiltinHelp = !hasUserHelp;
|
|
970
|
-
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
971
1338
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
972
1339
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
973
1340
|
}
|
|
974
|
-
if (enableBuiltinHelp) {
|
|
975
|
-
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
976
|
-
}
|
|
977
|
-
if (enableBuiltinVersion) {
|
|
978
|
-
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
979
|
-
}
|
|
980
1341
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
981
1342
|
optionMap.set('logLevel', logLevelOption);
|
|
982
1343
|
}
|
|
@@ -994,8 +1355,6 @@ class Command {
|
|
|
994
1355
|
}
|
|
995
1356
|
return {
|
|
996
1357
|
mergedOptions: Array.from(optionMap.values()),
|
|
997
|
-
enableBuiltinHelp,
|
|
998
|
-
enableBuiltinVersion,
|
|
999
1358
|
};
|
|
1000
1359
|
}
|
|
1001
1360
|
#buildOptionPolicyMap(chain) {
|
|
@@ -1032,6 +1391,9 @@ class Command {
|
|
|
1032
1391
|
}
|
|
1033
1392
|
}
|
|
1034
1393
|
#validateOptionConfig(opt) {
|
|
1394
|
+
if (opt.long === 'help' || opt.long === 'version') {
|
|
1395
|
+
throw new CommanderError('ConfigurationError', `option long name "${opt.long}" is reserved`, this.#getCommandPath());
|
|
1396
|
+
}
|
|
1035
1397
|
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
1036
1398
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
1037
1399
|
}
|
|
@@ -1112,22 +1474,27 @@ class Command {
|
|
|
1112
1474
|
process.exit(1);
|
|
1113
1475
|
}
|
|
1114
1476
|
}
|
|
1115
|
-
#
|
|
1477
|
+
#resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
|
|
1116
1478
|
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1117
1479
|
let color = !isNoColorEnabled(envs);
|
|
1118
1480
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1119
1481
|
return color;
|
|
1120
1482
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1483
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
1484
|
+
const scanTokens = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
1485
|
+
for (const token of scanTokens) {
|
|
1486
|
+
if (token === '--color') {
|
|
1487
|
+
color = true;
|
|
1123
1488
|
continue;
|
|
1124
1489
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1490
|
+
if (token === '--no-color') {
|
|
1491
|
+
color = false;
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (!token.startsWith('--color=')) {
|
|
1128
1495
|
continue;
|
|
1129
1496
|
}
|
|
1130
|
-
const value = token.
|
|
1497
|
+
const value = token.slice('--color='.length);
|
|
1131
1498
|
if (value === 'true') {
|
|
1132
1499
|
color = true;
|
|
1133
1500
|
}
|
|
@@ -1140,16 +1507,18 @@ class Command {
|
|
|
1140
1507
|
}
|
|
1141
1508
|
return color;
|
|
1142
1509
|
}
|
|
1143
|
-
#
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1510
|
+
#freezeInputSources(sources) {
|
|
1511
|
+
return Object.freeze({
|
|
1512
|
+
preset: Object.freeze({
|
|
1513
|
+
argv: Object.freeze([...sources.preset.argv]),
|
|
1514
|
+
envs: Object.freeze({ ...sources.preset.envs }),
|
|
1515
|
+
}),
|
|
1516
|
+
user: Object.freeze({
|
|
1517
|
+
cmds: Object.freeze([...sources.user.cmds]),
|
|
1518
|
+
argv: Object.freeze([...sources.user.argv]),
|
|
1519
|
+
envs: Object.freeze({ ...sources.user.envs }),
|
|
1520
|
+
}),
|
|
1521
|
+
});
|
|
1153
1522
|
}
|
|
1154
1523
|
#getCommandPath() {
|
|
1155
1524
|
const parts = [];
|
|
@@ -1164,6 +1533,96 @@ class Command {
|
|
|
1164
1533
|
}
|
|
1165
1534
|
}
|
|
1166
1535
|
|
|
1536
|
+
function isIpv4(rawValue) {
|
|
1537
|
+
const parts = rawValue.split('.');
|
|
1538
|
+
if (parts.length !== 4) {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
for (const part of parts) {
|
|
1542
|
+
if (part.length < 1 || !/^\d+$/.test(part)) {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
if (part.length > 1 && part.startsWith('0')) {
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
const value = Number(part);
|
|
1549
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
function countIpv6Segments(part, allowIpv4Tail) {
|
|
1556
|
+
if (!part) {
|
|
1557
|
+
return { count: 0, hasIpv4Tail: false };
|
|
1558
|
+
}
|
|
1559
|
+
const segments = part.split(':');
|
|
1560
|
+
let count = 0;
|
|
1561
|
+
let hasIpv4Tail = false;
|
|
1562
|
+
for (let i = 0; i < segments.length; ++i) {
|
|
1563
|
+
const segment = segments[i];
|
|
1564
|
+
const isLastSegment = i === segments.length - 1;
|
|
1565
|
+
if (!segment) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
if (segment.includes('.')) {
|
|
1569
|
+
if (!allowIpv4Tail || !isLastSegment || hasIpv4Tail || !isIpv4(segment)) {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
hasIpv4Tail = true;
|
|
1573
|
+
count += 2;
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) {
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
count += 1;
|
|
1580
|
+
}
|
|
1581
|
+
return { count, hasIpv4Tail };
|
|
1582
|
+
}
|
|
1583
|
+
function isIpv6(rawValue) {
|
|
1584
|
+
if (!rawValue || !/^[0-9A-Fa-f:.]+$/.test(rawValue)) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
const doubleColonCount = rawValue.split('::').length - 1;
|
|
1588
|
+
if (doubleColonCount > 1) {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
if (doubleColonCount === 0) {
|
|
1592
|
+
const full = countIpv6Segments(rawValue, true);
|
|
1593
|
+
return full !== null && full.count === 8;
|
|
1594
|
+
}
|
|
1595
|
+
const [left, right] = rawValue.split('::');
|
|
1596
|
+
const leftPart = countIpv6Segments(left, right.length === 0);
|
|
1597
|
+
const rightPart = countIpv6Segments(right, true);
|
|
1598
|
+
if (!leftPart || !rightPart) {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
const totalSegments = leftPart.count + rightPart.count;
|
|
1602
|
+
return totalSegments < 8;
|
|
1603
|
+
}
|
|
1604
|
+
function isIp(rawValue) {
|
|
1605
|
+
return isIpv4(rawValue) || isIpv6(rawValue);
|
|
1606
|
+
}
|
|
1607
|
+
function isDomain(rawValue) {
|
|
1608
|
+
if (rawValue.length < 1 || rawValue.length > 253 || rawValue.endsWith('.')) {
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
const labels = rawValue.split('.');
|
|
1612
|
+
if (labels.length < 2) {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
if (labels.some(label => label.length < 1 || label.length > 63)) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
const labelPattern = /^[A-Za-z0-9-]+$/;
|
|
1619
|
+
if (labels.some(label => !labelPattern.test(label) || label.startsWith('-') || label.endsWith('-'))) {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
const topLevelLabel = labels[labels.length - 1];
|
|
1623
|
+
return /[A-Za-z]/.test(topLevelLabel);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1167
1626
|
class Coerce {
|
|
1168
1627
|
constructor() { }
|
|
1169
1628
|
static create(name, expectedType, validator, errorMessage) {
|
|
@@ -1175,12 +1634,47 @@ class Coerce {
|
|
|
1175
1634
|
return value;
|
|
1176
1635
|
};
|
|
1177
1636
|
}
|
|
1178
|
-
static
|
|
1179
|
-
return
|
|
1637
|
+
static choice(name, values, errorMessage) {
|
|
1638
|
+
return (rawValue) => {
|
|
1639
|
+
if (values.includes(rawValue)) {
|
|
1640
|
+
return rawValue;
|
|
1641
|
+
}
|
|
1642
|
+
throw new Error(errorMessage ?? `${name} is expected as one of [${values.join(', ')}], but got ${rawValue}`);
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
static domain(name, errorMessage) {
|
|
1646
|
+
return (rawValue) => {
|
|
1647
|
+
if (isDomain(rawValue)) {
|
|
1648
|
+
return rawValue;
|
|
1649
|
+
}
|
|
1650
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid domain, but got ${rawValue}`);
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
static host(name, errorMessage) {
|
|
1654
|
+
return (rawValue) => {
|
|
1655
|
+
if (isIp(rawValue) || isDomain(rawValue)) {
|
|
1656
|
+
return rawValue;
|
|
1657
|
+
}
|
|
1658
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid host (IP or domain), but got ${rawValue}`);
|
|
1659
|
+
};
|
|
1180
1660
|
}
|
|
1181
1661
|
static integer(name, errorMessage) {
|
|
1182
1662
|
return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
|
|
1183
1663
|
}
|
|
1664
|
+
static ip(name, errorMessage) {
|
|
1665
|
+
return (rawValue) => {
|
|
1666
|
+
if (isIp(rawValue)) {
|
|
1667
|
+
return rawValue;
|
|
1668
|
+
}
|
|
1669
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid IP address, but got ${rawValue}`);
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
static number(name, errorMessage) {
|
|
1673
|
+
return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
|
|
1674
|
+
}
|
|
1675
|
+
static port(name, errorMessage) {
|
|
1676
|
+
return this.create(name, 'a valid port number (0-65535)', value => Number.isInteger(value) && value >= 0 && value <= 65535, errorMessage);
|
|
1677
|
+
}
|
|
1184
1678
|
static positiveInteger(name, errorMessage) {
|
|
1185
1679
|
return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
|
|
1186
1680
|
}
|
|
@@ -1193,9 +1687,12 @@ function camelToKebabCase(str) {
|
|
|
1193
1687
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1194
1688
|
}
|
|
1195
1689
|
class CompletionCommand extends Command {
|
|
1196
|
-
constructor(root, config) {
|
|
1197
|
-
const paths = config.paths;
|
|
1690
|
+
constructor(root, config = {}) {
|
|
1198
1691
|
const programName = config.programName ?? root.name ?? 'program';
|
|
1692
|
+
const paths = {
|
|
1693
|
+
...createDefaultCompletionPaths(programName),
|
|
1694
|
+
...config.paths,
|
|
1695
|
+
};
|
|
1199
1696
|
super({ desc: 'Generate shell completion script' });
|
|
1200
1697
|
this.option({
|
|
1201
1698
|
long: 'bash',
|
|
@@ -1270,6 +1767,13 @@ class CompletionCommand extends Command {
|
|
|
1270
1767
|
});
|
|
1271
1768
|
}
|
|
1272
1769
|
}
|
|
1770
|
+
function createDefaultCompletionPaths(programName) {
|
|
1771
|
+
return {
|
|
1772
|
+
bash: `~/.local/share/bash-completion/completions/${programName}`,
|
|
1773
|
+
fish: `~/.config/fish/completions/${programName}.fish`,
|
|
1774
|
+
pwsh: '~/.config/powershell/Microsoft.PowerShell_profile.ps1',
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1273
1777
|
function expandHome(filepath) {
|
|
1274
1778
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
1275
1779
|
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
@@ -1547,4 +2051,4 @@ class PwshCompletion {
|
|
|
1547
2051
|
}
|
|
1548
2052
|
}
|
|
1549
2053
|
|
|
1550
|
-
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
|
|
2054
|
+
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, isDomain, isIp, isIpv4, isIpv6, logColorfulOption, logDateOption, logLevelOption, silentOption };
|