@guanghechen/commander 4.5.1 → 4.7.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 +86 -32
- package/lib/cjs/browser.cjs +1779 -0
- package/lib/cjs/{index.cjs → node.cjs} +620 -166
- package/lib/esm/browser.mjs +1764 -0
- package/lib/esm/{index.mjs → node.mjs} +618 -148
- package/lib/types/browser.d.ts +551 -0
- package/lib/types/{index.d.ts → node.d.ts} +159 -69
- package/package.json +15 -11
|
@@ -1,28 +1,98 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var promises = require('node:fs/promises');
|
|
4
|
+
var path = require('node:path');
|
|
5
|
+
var env = require('@guanghechen/env');
|
|
3
6
|
var reporter = require('@guanghechen/reporter');
|
|
4
7
|
var fs = require('node:fs');
|
|
5
|
-
var path = require('node:path');
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
const WINDOWS_DRIVE_ABSOLUTE_REGEX = /^[a-zA-Z]:[\\/]/;
|
|
10
|
+
function isAbsolutePath(filepath) {
|
|
11
|
+
return (filepath.startsWith('/') ||
|
|
12
|
+
filepath.startsWith('\\\\') ||
|
|
13
|
+
WINDOWS_DRIVE_ABSOLUTE_REGEX.test(filepath));
|
|
14
|
+
}
|
|
15
|
+
function resolvePathFrom(base, fragment) {
|
|
16
|
+
const useWindowsStyle = WINDOWS_DRIVE_ABSOLUTE_REGEX.test(base);
|
|
17
|
+
const normalizedBase = base.replace(/\\/g, '/');
|
|
18
|
+
const normalizedFragment = fragment.replace(/\\/g, '/');
|
|
19
|
+
const source = isAbsolutePath(normalizedFragment)
|
|
20
|
+
? normalizedFragment
|
|
21
|
+
: `${normalizedBase.replace(/\/$/, '')}/${normalizedFragment}`;
|
|
22
|
+
const prefix = useWindowsStyle ? source.slice(0, 2) : '';
|
|
23
|
+
const body = useWindowsStyle ? source.slice(2) : source;
|
|
24
|
+
const stack = [];
|
|
25
|
+
for (const token of body.split('/')) {
|
|
26
|
+
if (token === '' || token === '.') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (token === '..') {
|
|
30
|
+
if (stack.length > 0) {
|
|
31
|
+
stack.pop();
|
|
17
32
|
}
|
|
18
|
-
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
stack.push(token);
|
|
36
|
+
}
|
|
37
|
+
if (useWindowsStyle) {
|
|
38
|
+
const resolved = `${prefix}/${stack.join('/')}`;
|
|
39
|
+
return resolved.endsWith('/') ? resolved.slice(0, -1) : resolved;
|
|
40
|
+
}
|
|
41
|
+
return `/${stack.join('/')}`;
|
|
42
|
+
}
|
|
43
|
+
function createUnsupportedFsError(operation) {
|
|
44
|
+
return new Error(`runtime does not support file-system operation: ${operation}`);
|
|
45
|
+
}
|
|
46
|
+
function getFallbackCwd() {
|
|
47
|
+
const proc = globalThis.process;
|
|
48
|
+
if (proc && typeof proc.cwd === 'function') {
|
|
49
|
+
return proc.cwd();
|
|
19
50
|
}
|
|
20
|
-
|
|
21
|
-
|
|
51
|
+
return '/';
|
|
52
|
+
}
|
|
53
|
+
function createBrowserCommandRuntime() {
|
|
54
|
+
return {
|
|
55
|
+
cwd: () => getFallbackCwd(),
|
|
56
|
+
isAbsolute: filepath => isAbsolutePath(filepath),
|
|
57
|
+
resolve: (...paths) => {
|
|
58
|
+
if (paths.length === 0) {
|
|
59
|
+
return getFallbackCwd();
|
|
60
|
+
}
|
|
61
|
+
let resolved = getFallbackCwd();
|
|
62
|
+
for (const path of paths) {
|
|
63
|
+
if (path.length === 0) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
resolved = resolvePathFrom(resolved, path);
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
},
|
|
70
|
+
readFile: async () => {
|
|
71
|
+
throw createUnsupportedFsError('readFile');
|
|
72
|
+
},
|
|
73
|
+
stat: async () => {
|
|
74
|
+
throw createUnsupportedFsError('stat');
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let defaultRuntime = createBrowserCommandRuntime();
|
|
80
|
+
function getDefaultCommandRuntime() {
|
|
81
|
+
return defaultRuntime;
|
|
82
|
+
}
|
|
83
|
+
function setDefaultCommandRuntime(runtime) {
|
|
84
|
+
defaultRuntime = runtime;
|
|
22
85
|
}
|
|
23
86
|
|
|
24
|
-
|
|
25
|
-
|
|
87
|
+
function createNodeCommandRuntime() {
|
|
88
|
+
return {
|
|
89
|
+
cwd: () => process.cwd(),
|
|
90
|
+
isAbsolute: filepath => path.isAbsolute(filepath),
|
|
91
|
+
resolve: (...paths) => path.resolve(...paths),
|
|
92
|
+
readFile: filepath => promises.readFile(filepath, 'utf8'),
|
|
93
|
+
stat: filepath => promises.stat(filepath),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
26
96
|
|
|
27
97
|
const TERMINAL_STYLE = {
|
|
28
98
|
bold: '\x1b[1m',
|
|
@@ -36,22 +106,35 @@ function styleText(text, ...styles) {
|
|
|
36
106
|
return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
|
|
37
107
|
}
|
|
38
108
|
|
|
109
|
+
const BUILTIN_LOG_LEVELS = ['debug', 'info', 'hint', 'warn', 'error'];
|
|
110
|
+
function resolveReporterLogLevel(raw) {
|
|
111
|
+
const normalized = raw.trim().toLowerCase();
|
|
112
|
+
return BUILTIN_LOG_LEVELS.find(level => level === normalized);
|
|
113
|
+
}
|
|
114
|
+
function setReporterLevel(ctx, level) {
|
|
115
|
+
const reporter = ctx.reporter;
|
|
116
|
+
reporter?.setLevel?.(level);
|
|
117
|
+
}
|
|
118
|
+
function setReporterFlight(ctx, flight) {
|
|
119
|
+
const reporter = ctx.reporter;
|
|
120
|
+
reporter?.setFlight?.(flight);
|
|
121
|
+
}
|
|
39
122
|
const logLevelOption = {
|
|
40
123
|
long: 'logLevel',
|
|
41
124
|
type: 'string',
|
|
42
125
|
args: 'required',
|
|
43
126
|
desc: 'Set log level',
|
|
44
127
|
default: 'info',
|
|
45
|
-
choices:
|
|
128
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
46
129
|
coerce: (raw) => {
|
|
47
|
-
const level =
|
|
130
|
+
const level = resolveReporterLogLevel(raw);
|
|
48
131
|
if (level === undefined) {
|
|
49
132
|
throw new Error(`Invalid log level: ${raw}`);
|
|
50
133
|
}
|
|
51
134
|
return level;
|
|
52
135
|
},
|
|
53
136
|
apply: (value, ctx) => {
|
|
54
|
-
ctx
|
|
137
|
+
setReporterLevel(ctx, value);
|
|
55
138
|
},
|
|
56
139
|
};
|
|
57
140
|
const logDateOption = {
|
|
@@ -61,7 +144,7 @@ const logDateOption = {
|
|
|
61
144
|
desc: 'Enable log timestamp',
|
|
62
145
|
default: true,
|
|
63
146
|
apply: (value, ctx) => {
|
|
64
|
-
ctx
|
|
147
|
+
setReporterFlight(ctx, { date: Boolean(value) });
|
|
65
148
|
},
|
|
66
149
|
};
|
|
67
150
|
const logColorfulOption = {
|
|
@@ -71,7 +154,7 @@ const logColorfulOption = {
|
|
|
71
154
|
desc: 'Enable colorful log output',
|
|
72
155
|
default: true,
|
|
73
156
|
apply: (value, ctx) => {
|
|
74
|
-
ctx
|
|
157
|
+
setReporterFlight(ctx, { color: Boolean(value) });
|
|
75
158
|
},
|
|
76
159
|
};
|
|
77
160
|
const silentOption = {
|
|
@@ -82,7 +165,7 @@ const silentOption = {
|
|
|
82
165
|
default: false,
|
|
83
166
|
apply: (value, ctx) => {
|
|
84
167
|
if (value) {
|
|
85
|
-
ctx
|
|
168
|
+
setReporterLevel(ctx, 'error');
|
|
86
169
|
}
|
|
87
170
|
},
|
|
88
171
|
};
|
|
@@ -103,6 +186,11 @@ class CommanderError extends Error {
|
|
|
103
186
|
|
|
104
187
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
105
188
|
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
189
|
+
const PRESET_OPTS_FLAG = '--preset-opts';
|
|
190
|
+
const PRESET_ENVS_FLAG = '--preset-envs';
|
|
191
|
+
const PRESET_ROOT_FLAG = '--preset-root';
|
|
192
|
+
const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
|
|
193
|
+
const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
|
|
106
194
|
function kebabToCamelCase(str) {
|
|
107
195
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
108
196
|
}
|
|
@@ -190,14 +278,12 @@ function tokenize(argv, commandPath) {
|
|
|
190
278
|
}
|
|
191
279
|
const BUILTIN_HELP_OPTION = {
|
|
192
280
|
long: 'help',
|
|
193
|
-
short: 'h',
|
|
194
281
|
type: 'boolean',
|
|
195
282
|
args: 'none',
|
|
196
283
|
desc: 'Show help information',
|
|
197
284
|
};
|
|
198
285
|
const BUILTIN_VERSION_OPTION = {
|
|
199
286
|
long: 'version',
|
|
200
|
-
short: 'V',
|
|
201
287
|
type: 'boolean',
|
|
202
288
|
args: 'none',
|
|
203
289
|
desc: 'Show version number',
|
|
@@ -211,6 +297,7 @@ const BUILTIN_COLOR_OPTION = {
|
|
|
211
297
|
};
|
|
212
298
|
function createBuiltinOptionState(enabled) {
|
|
213
299
|
return {
|
|
300
|
+
version: enabled,
|
|
214
301
|
color: enabled,
|
|
215
302
|
logLevel: enabled,
|
|
216
303
|
silent: enabled,
|
|
@@ -224,9 +311,6 @@ function isNoColorEnabled(envs) {
|
|
|
224
311
|
function normalizeBuiltinConfig(builtin) {
|
|
225
312
|
const resolved = {
|
|
226
313
|
option: createBuiltinOptionState(true),
|
|
227
|
-
command: {
|
|
228
|
-
help: false,
|
|
229
|
-
},
|
|
230
314
|
};
|
|
231
315
|
if (builtin === undefined) {
|
|
232
316
|
return resolved;
|
|
@@ -234,13 +318,11 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
234
318
|
if (builtin === true) {
|
|
235
319
|
return {
|
|
236
320
|
option: createBuiltinOptionState(true),
|
|
237
|
-
command: { help: true },
|
|
238
321
|
};
|
|
239
322
|
}
|
|
240
323
|
if (builtin === false) {
|
|
241
324
|
return {
|
|
242
325
|
option: createBuiltinOptionState(false),
|
|
243
|
-
command: { help: false },
|
|
244
326
|
};
|
|
245
327
|
}
|
|
246
328
|
if (builtin.option !== undefined) {
|
|
@@ -251,6 +333,8 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
251
333
|
resolved.option = createBuiltinOptionState(true);
|
|
252
334
|
}
|
|
253
335
|
else {
|
|
336
|
+
if (builtin.option.version !== undefined)
|
|
337
|
+
resolved.option.version = builtin.option.version;
|
|
254
338
|
if (builtin.option.color !== undefined)
|
|
255
339
|
resolved.option.color = builtin.option.color;
|
|
256
340
|
if (builtin.option.logLevel !== undefined) {
|
|
@@ -265,25 +349,17 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
265
349
|
}
|
|
266
350
|
}
|
|
267
351
|
}
|
|
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
352
|
return resolved;
|
|
280
353
|
}
|
|
281
354
|
class Command {
|
|
282
355
|
#name;
|
|
283
356
|
#desc;
|
|
284
357
|
#version;
|
|
358
|
+
#builtinConfig;
|
|
285
359
|
#builtin;
|
|
360
|
+
#presetConfig;
|
|
286
361
|
#reporter;
|
|
362
|
+
#runtime;
|
|
287
363
|
#parent;
|
|
288
364
|
#options = [];
|
|
289
365
|
#arguments = [];
|
|
@@ -295,8 +371,11 @@ class Command {
|
|
|
295
371
|
this.#name = config.name ?? '';
|
|
296
372
|
this.#desc = config.desc;
|
|
297
373
|
this.#version = config.version;
|
|
374
|
+
this.#builtinConfig = config.builtin;
|
|
298
375
|
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
376
|
+
this.#presetConfig = config.preset;
|
|
299
377
|
this.#reporter = config.reporter;
|
|
378
|
+
this.#runtime = config.runtime ?? getDefaultCommandRuntime();
|
|
300
379
|
}
|
|
301
380
|
get name() {
|
|
302
381
|
return this.#name || undefined;
|
|
@@ -307,6 +386,12 @@ class Command {
|
|
|
307
386
|
get version() {
|
|
308
387
|
return this.#version;
|
|
309
388
|
}
|
|
389
|
+
get builtin() {
|
|
390
|
+
return this.#builtinConfig;
|
|
391
|
+
}
|
|
392
|
+
get preset() {
|
|
393
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
394
|
+
}
|
|
310
395
|
get parent() {
|
|
311
396
|
return this.#parent;
|
|
312
397
|
}
|
|
@@ -342,14 +427,17 @@ class Command {
|
|
|
342
427
|
return this;
|
|
343
428
|
}
|
|
344
429
|
subcommand(name, cmd) {
|
|
345
|
-
if (
|
|
346
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name
|
|
430
|
+
if (name === 'help') {
|
|
431
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
|
|
347
432
|
}
|
|
348
433
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
349
434
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
350
435
|
}
|
|
351
436
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
352
437
|
if (existing) {
|
|
438
|
+
if (existing.aliases.includes(name)) {
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
353
441
|
existing.aliases.push(name);
|
|
354
442
|
this.#subcommandsMap.set(name, cmd);
|
|
355
443
|
}
|
|
@@ -362,34 +450,37 @@ class Command {
|
|
|
362
450
|
return this;
|
|
363
451
|
}
|
|
364
452
|
async run(params) {
|
|
365
|
-
const { argv, envs, reporter
|
|
453
|
+
const { argv, envs, reporter } = params;
|
|
366
454
|
try {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const { chain, remaining } = routeResult;
|
|
455
|
+
const routeResult = this.#route(argv);
|
|
456
|
+
const { chain } = routeResult;
|
|
370
457
|
const leafCommand = chain[chain.length - 1];
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
458
|
+
const ctx = this.#createContext({
|
|
459
|
+
chain,
|
|
460
|
+
cmds: routeResult.cmds,
|
|
461
|
+
envs,
|
|
462
|
+
reporter,
|
|
463
|
+
});
|
|
464
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
465
|
+
ctx.controls = controlScanResult.controls;
|
|
466
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
467
|
+
if (ctx.controls.help) {
|
|
468
|
+
const helpCommand = this.#resolveHelpCommand(leafCommand, controlScanResult.helpTarget);
|
|
469
|
+
const helpColor = helpCommand.#resolveHelpColorFromTailArgv(controlScanResult.remaining, ctx.envs);
|
|
470
|
+
console.log(helpCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
378
471
|
return;
|
|
379
472
|
}
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
473
|
+
if (ctx.controls.version) {
|
|
474
|
+
console.log(leafCommand.#version);
|
|
475
|
+
return;
|
|
385
476
|
}
|
|
477
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
478
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
479
|
+
ctx.sources = presetResult.sources;
|
|
480
|
+
ctx.envs = presetResult.envs;
|
|
481
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
482
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
386
483
|
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
484
|
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
394
485
|
const actionParams = {
|
|
395
486
|
ctx: parseResult.ctx,
|
|
@@ -401,7 +492,7 @@ class Command {
|
|
|
401
492
|
await leafCommand.#runAction(actionParams);
|
|
402
493
|
}
|
|
403
494
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
404
|
-
const helpColor = leafCommand.#
|
|
495
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
405
496
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
406
497
|
}
|
|
407
498
|
else {
|
|
@@ -417,22 +508,27 @@ class Command {
|
|
|
417
508
|
throw err;
|
|
418
509
|
}
|
|
419
510
|
}
|
|
420
|
-
parse(params) {
|
|
421
|
-
const { argv, envs, reporter
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const { chain, remaining } = routeResult;
|
|
511
|
+
async parse(params) {
|
|
512
|
+
const { argv, envs, reporter } = params;
|
|
513
|
+
const routeResult = this.#route(argv);
|
|
514
|
+
const { chain } = routeResult;
|
|
425
515
|
const leafCommand = chain[chain.length - 1];
|
|
426
|
-
const
|
|
427
|
-
|
|
516
|
+
const ctx = this.#createContext({
|
|
517
|
+
chain,
|
|
518
|
+
cmds: routeResult.cmds,
|
|
519
|
+
envs,
|
|
520
|
+
reporter,
|
|
521
|
+
});
|
|
522
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
523
|
+
ctx.controls = controlScanResult.controls;
|
|
524
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
428
525
|
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
526
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
527
|
+
ctx.sources = presetResult.sources;
|
|
528
|
+
ctx.envs = presetResult.envs;
|
|
529
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
530
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
429
531
|
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
532
|
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
437
533
|
}
|
|
438
534
|
formatHelp() {
|
|
@@ -450,7 +546,11 @@ class Command {
|
|
|
450
546
|
return color && process.stdout.isTTY === true;
|
|
451
547
|
}
|
|
452
548
|
#buildHelpData() {
|
|
453
|
-
const
|
|
549
|
+
const parseOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
550
|
+
const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
|
|
551
|
+
if (this.#supportsBuiltinVersion()) {
|
|
552
|
+
allOptions.push(BUILTIN_VERSION_OPTION);
|
|
553
|
+
}
|
|
454
554
|
const commandPath = this.#getCommandPath();
|
|
455
555
|
let usage = `Usage: ${commandPath}`;
|
|
456
556
|
if (allOptions.length > 0)
|
|
@@ -484,7 +584,10 @@ class Command {
|
|
|
484
584
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
485
585
|
}
|
|
486
586
|
options.push({ sig, desc });
|
|
487
|
-
if (opt.type === 'boolean' &&
|
|
587
|
+
if (opt.type === 'boolean' &&
|
|
588
|
+
opt.args === 'none' &&
|
|
589
|
+
opt.long !== 'help' &&
|
|
590
|
+
opt.long !== 'version') {
|
|
488
591
|
options.push({
|
|
489
592
|
sig: ` --no-${kebabLong}`,
|
|
490
593
|
desc: `Negate --${kebabLong}`,
|
|
@@ -492,8 +595,7 @@ class Command {
|
|
|
492
595
|
}
|
|
493
596
|
}
|
|
494
597
|
const commands = [];
|
|
495
|
-
|
|
496
|
-
if (showHelpSubcommand) {
|
|
598
|
+
if (this.#subcommandsList.length > 0) {
|
|
497
599
|
commands.push({ name: 'help', desc: 'Show help for a command' });
|
|
498
600
|
}
|
|
499
601
|
for (const entry of this.#subcommandsList) {
|
|
@@ -616,50 +718,9 @@ class Command {
|
|
|
616
718
|
#findSubcommandEntry(token) {
|
|
617
719
|
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
618
720
|
}
|
|
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
721
|
#route(argv) {
|
|
662
722
|
const chain = [this];
|
|
723
|
+
const cmds = [];
|
|
663
724
|
let current = this;
|
|
664
725
|
let idx = 0;
|
|
665
726
|
while (idx < argv.length) {
|
|
@@ -670,10 +731,382 @@ class Command {
|
|
|
670
731
|
if (!entry)
|
|
671
732
|
break;
|
|
672
733
|
current = entry.command;
|
|
734
|
+
cmds.push(token);
|
|
673
735
|
chain.push(current);
|
|
674
736
|
idx += 1;
|
|
675
737
|
}
|
|
676
|
-
return { chain, remaining: argv.slice(idx) };
|
|
738
|
+
return { chain, remaining: argv.slice(idx), cmds };
|
|
739
|
+
}
|
|
740
|
+
#controlScan(tailArgv, leafCommand) {
|
|
741
|
+
const controls = { help: false, version: false };
|
|
742
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
743
|
+
const beforeSeparator = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
744
|
+
const afterSeparator = separatorIndex === -1 ? [] : tailArgv.slice(separatorIndex + 1);
|
|
745
|
+
let helpTarget;
|
|
746
|
+
let scanStartIndex = 0;
|
|
747
|
+
if (beforeSeparator[0] === 'help') {
|
|
748
|
+
controls.help = true;
|
|
749
|
+
scanStartIndex = 1;
|
|
750
|
+
const candidate = beforeSeparator[1];
|
|
751
|
+
if (candidate !== undefined && !candidate.startsWith('-')) {
|
|
752
|
+
helpTarget = candidate;
|
|
753
|
+
scanStartIndex = 2;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const remainingBeforeSeparator = [];
|
|
757
|
+
for (let i = scanStartIndex; i < beforeSeparator.length; i += 1) {
|
|
758
|
+
const token = beforeSeparator[i];
|
|
759
|
+
if (token === '--help') {
|
|
760
|
+
controls.help = true;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (token === '--version' && leafCommand.#supportsBuiltinVersion()) {
|
|
764
|
+
controls.version = true;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
remainingBeforeSeparator.push(token);
|
|
768
|
+
}
|
|
769
|
+
const remaining = separatorIndex === -1
|
|
770
|
+
? remainingBeforeSeparator
|
|
771
|
+
: [...remainingBeforeSeparator, '--', ...afterSeparator];
|
|
772
|
+
return {
|
|
773
|
+
controls,
|
|
774
|
+
remaining,
|
|
775
|
+
helpTarget,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
#createContext(params) {
|
|
779
|
+
const { chain, cmds, envs, reporter: reporter$1 } = params;
|
|
780
|
+
const leafCommand = chain[chain.length - 1];
|
|
781
|
+
const envSnapshot = { ...envs };
|
|
782
|
+
return {
|
|
783
|
+
cmd: leafCommand,
|
|
784
|
+
chain,
|
|
785
|
+
envs: envSnapshot,
|
|
786
|
+
controls: { help: false, version: false },
|
|
787
|
+
sources: {
|
|
788
|
+
preset: {
|
|
789
|
+
argv: [],
|
|
790
|
+
envs: {},
|
|
791
|
+
},
|
|
792
|
+
user: {
|
|
793
|
+
cmds: [...cmds],
|
|
794
|
+
argv: [],
|
|
795
|
+
envs: envSnapshot,
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
#resolveHelpCommand(leafCommand, helpTarget) {
|
|
802
|
+
if (helpTarget === undefined) {
|
|
803
|
+
return leafCommand;
|
|
804
|
+
}
|
|
805
|
+
const target = leafCommand.#findSubcommandEntry(helpTarget);
|
|
806
|
+
if (target === undefined) {
|
|
807
|
+
return leafCommand;
|
|
808
|
+
}
|
|
809
|
+
return target.command;
|
|
810
|
+
}
|
|
811
|
+
async #preset(controlTailArgv, ctx, optionPolicyMap) {
|
|
812
|
+
const commandPath = ctx.chain[ctx.chain.length - 1].#getCommandPath();
|
|
813
|
+
const separatorIndex = controlTailArgv.indexOf('--');
|
|
814
|
+
const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
|
|
815
|
+
const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
|
|
816
|
+
const rootScanResult = this.#scanPresetRootDirectives(beforeSeparator, commandPath);
|
|
817
|
+
const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
|
|
818
|
+
const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
|
|
819
|
+
const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
|
|
820
|
+
const cleanArgv = separatorIndex === -1
|
|
821
|
+
? fileScanResult.cleanArgv
|
|
822
|
+
: [...fileScanResult.cleanArgv, '--', ...afterSeparator];
|
|
823
|
+
const presetOptsFiles = this.#resolvePresetFileSources({
|
|
824
|
+
cliFiles: fileScanResult.cliPresetOptsFiles,
|
|
825
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.opt),
|
|
826
|
+
presetRoot,
|
|
827
|
+
defaultFilename: DEFAULT_PRESET_OPTS_FILENAME,
|
|
828
|
+
});
|
|
829
|
+
const presetEnvsFiles = this.#resolvePresetFileSources({
|
|
830
|
+
cliFiles: fileScanResult.cliPresetEnvsFiles,
|
|
831
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.env),
|
|
832
|
+
presetRoot,
|
|
833
|
+
defaultFilename: DEFAULT_PRESET_ENVS_FILENAME,
|
|
834
|
+
});
|
|
835
|
+
const userSources = {
|
|
836
|
+
cmds: [...ctx.sources.user.cmds],
|
|
837
|
+
argv: [...cleanArgv],
|
|
838
|
+
envs: { ...ctx.sources.user.envs },
|
|
839
|
+
};
|
|
840
|
+
const presetArgv = [];
|
|
841
|
+
for (const file of presetOptsFiles) {
|
|
842
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
843
|
+
if (content === undefined) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const tokens = this.#tokenizePresetOptions(content);
|
|
847
|
+
this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
|
|
848
|
+
this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
|
|
849
|
+
presetArgv.push(...tokens);
|
|
850
|
+
}
|
|
851
|
+
const presetEnvs = {};
|
|
852
|
+
for (const file of presetEnvsFiles) {
|
|
853
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
854
|
+
if (content === undefined) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
let parsed;
|
|
858
|
+
try {
|
|
859
|
+
parsed = env.parse(content);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset envs file "${file.displayPath}": ${error.message}`, commandPath);
|
|
863
|
+
}
|
|
864
|
+
Object.assign(presetEnvs, parsed);
|
|
865
|
+
}
|
|
866
|
+
const sources = {
|
|
867
|
+
user: userSources,
|
|
868
|
+
preset: {
|
|
869
|
+
argv: presetArgv,
|
|
870
|
+
envs: presetEnvs,
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
const envs = { ...sources.user.envs, ...sources.preset.envs };
|
|
874
|
+
const tailArgv = [...sources.preset.argv, ...sources.user.argv];
|
|
875
|
+
return { tailArgv, envs, sources };
|
|
876
|
+
}
|
|
877
|
+
#resolveCommandPresetFromChain(chain) {
|
|
878
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
879
|
+
const preset = chain[index].#presetConfig;
|
|
880
|
+
if (preset?.root !== undefined) {
|
|
881
|
+
return preset;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
async #resolveEffectivePresetRoot(cliPresetRoots, commandPreset, commandPath) {
|
|
887
|
+
if (cliPresetRoots.length > 0) {
|
|
888
|
+
const root = cliPresetRoots[cliPresetRoots.length - 1];
|
|
889
|
+
return await this.#assertPresetRoot(root, PRESET_ROOT_FLAG, commandPath);
|
|
890
|
+
}
|
|
891
|
+
if (commandPreset?.root === undefined) {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
return await this.#assertPresetRoot(commandPreset.root, 'command.preset.root', commandPath);
|
|
895
|
+
}
|
|
896
|
+
async #assertPresetRoot(root, sourceName, commandPath) {
|
|
897
|
+
if (!this.#runtime.isAbsolute(root)) {
|
|
898
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not an absolute directory`, commandPath);
|
|
899
|
+
}
|
|
900
|
+
let stats;
|
|
901
|
+
try {
|
|
902
|
+
stats = await this.#runtime.stat(root);
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" cannot be accessed (${error.message})`, commandPath);
|
|
906
|
+
}
|
|
907
|
+
if (!stats.isDirectory()) {
|
|
908
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not a directory`, commandPath);
|
|
909
|
+
}
|
|
910
|
+
return root;
|
|
911
|
+
}
|
|
912
|
+
#normalizeCommandPresetFile(filepath) {
|
|
913
|
+
if (filepath === undefined) {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
if (!this.#isValidPresetFileValue(filepath)) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
return filepath;
|
|
920
|
+
}
|
|
921
|
+
#resolvePresetFileSources(params) {
|
|
922
|
+
const { cliFiles, commandPresetFile, presetRoot, defaultFilename } = params;
|
|
923
|
+
if (cliFiles.length > 0) {
|
|
924
|
+
return cliFiles.map(filepath => ({
|
|
925
|
+
displayPath: filepath,
|
|
926
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(filepath, presetRoot),
|
|
927
|
+
explicit: true,
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
if (presetRoot === undefined) {
|
|
931
|
+
return [];
|
|
932
|
+
}
|
|
933
|
+
if (commandPresetFile !== undefined) {
|
|
934
|
+
return [
|
|
935
|
+
{
|
|
936
|
+
displayPath: commandPresetFile,
|
|
937
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
|
|
938
|
+
explicit: true,
|
|
939
|
+
},
|
|
940
|
+
];
|
|
941
|
+
}
|
|
942
|
+
const absolutePath = this.#runtime.resolve(presetRoot, defaultFilename);
|
|
943
|
+
return [
|
|
944
|
+
{
|
|
945
|
+
displayPath: absolutePath,
|
|
946
|
+
absolutePath,
|
|
947
|
+
explicit: false,
|
|
948
|
+
},
|
|
949
|
+
];
|
|
950
|
+
}
|
|
951
|
+
#resolvePresetFileAbsolutePath(filepath, presetRoot) {
|
|
952
|
+
if (this.#runtime.isAbsolute(filepath)) {
|
|
953
|
+
return filepath;
|
|
954
|
+
}
|
|
955
|
+
if (presetRoot !== undefined) {
|
|
956
|
+
return this.#runtime.resolve(presetRoot, filepath);
|
|
957
|
+
}
|
|
958
|
+
return this.#runtime.resolve(this.#runtime.cwd(), filepath);
|
|
959
|
+
}
|
|
960
|
+
#assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
|
|
961
|
+
if (tokens.length === 0) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const commandPath = chain[chain.length - 1].#getCommandPath();
|
|
965
|
+
try {
|
|
966
|
+
const { optionTokens, restArgs } = tokenize(tokens, commandPath);
|
|
967
|
+
void restArgs;
|
|
968
|
+
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
969
|
+
if (argTokens.length > 0) {
|
|
970
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch (error) {
|
|
974
|
+
if (error instanceof CommanderError) {
|
|
975
|
+
if (error.kind === 'ConfigurationError') {
|
|
976
|
+
throw error;
|
|
977
|
+
}
|
|
978
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
#scanPresetRootDirectives(argv, commandPath) {
|
|
984
|
+
const cleanArgv = [];
|
|
985
|
+
const cliPresetRoots = [];
|
|
986
|
+
let index = 0;
|
|
987
|
+
while (index < argv.length) {
|
|
988
|
+
const token = argv[index];
|
|
989
|
+
if (token === PRESET_ROOT_FLAG) {
|
|
990
|
+
const value = argv[index + 1];
|
|
991
|
+
if (value === undefined || value.length === 0) {
|
|
992
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
993
|
+
}
|
|
994
|
+
cliPresetRoots.push(value);
|
|
995
|
+
index += 2;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (token.startsWith(`${PRESET_ROOT_FLAG}=`)) {
|
|
999
|
+
const value = token.slice(PRESET_ROOT_FLAG.length + 1);
|
|
1000
|
+
if (value.length === 0) {
|
|
1001
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
1002
|
+
}
|
|
1003
|
+
cliPresetRoots.push(value);
|
|
1004
|
+
index += 1;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
cleanArgv.push(token);
|
|
1008
|
+
index += 1;
|
|
1009
|
+
}
|
|
1010
|
+
return { cleanArgv, cliPresetRoots };
|
|
1011
|
+
}
|
|
1012
|
+
#scanPresetFileDirectives(argv, commandPath) {
|
|
1013
|
+
const cleanArgv = [];
|
|
1014
|
+
const cliPresetOptsFiles = [];
|
|
1015
|
+
const cliPresetEnvsFiles = [];
|
|
1016
|
+
const assertAndPush = (flag, value) => {
|
|
1017
|
+
this.#assertPresetFileValue(value, flag, commandPath);
|
|
1018
|
+
if (flag === PRESET_OPTS_FLAG) {
|
|
1019
|
+
cliPresetOptsFiles.push(value);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
cliPresetEnvsFiles.push(value);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
let index = 0;
|
|
1026
|
+
while (index < argv.length) {
|
|
1027
|
+
const token = argv[index];
|
|
1028
|
+
if (token === PRESET_OPTS_FLAG || token === PRESET_ENVS_FLAG) {
|
|
1029
|
+
const value = argv[index + 1];
|
|
1030
|
+
if (value === undefined || value.length === 0) {
|
|
1031
|
+
throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
|
|
1032
|
+
}
|
|
1033
|
+
assertAndPush(token, value);
|
|
1034
|
+
index += 2;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (token.startsWith(`${PRESET_OPTS_FLAG}=`)) {
|
|
1038
|
+
const value = token.slice(PRESET_OPTS_FLAG.length + 1);
|
|
1039
|
+
if (value.length === 0) {
|
|
1040
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_OPTS_FLAG}"`, commandPath);
|
|
1041
|
+
}
|
|
1042
|
+
assertAndPush(PRESET_OPTS_FLAG, value);
|
|
1043
|
+
index += 1;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1047
|
+
const value = token.slice(PRESET_ENVS_FLAG.length + 1);
|
|
1048
|
+
if (value.length === 0) {
|
|
1049
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ENVS_FLAG}"`, commandPath);
|
|
1050
|
+
}
|
|
1051
|
+
assertAndPush(PRESET_ENVS_FLAG, value);
|
|
1052
|
+
index += 1;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
cleanArgv.push(token);
|
|
1056
|
+
index += 1;
|
|
1057
|
+
}
|
|
1058
|
+
return { cleanArgv, cliPresetOptsFiles, cliPresetEnvsFiles };
|
|
1059
|
+
}
|
|
1060
|
+
#isValidPresetFileValue(filepath) {
|
|
1061
|
+
return filepath.length > 0 && !filepath.startsWith('..');
|
|
1062
|
+
}
|
|
1063
|
+
#assertPresetFileValue(filepath, directive, commandPath) {
|
|
1064
|
+
if (this.#isValidPresetFileValue(filepath)) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${directive}": "${filepath}" (must be non-empty and must not start with "..")`, commandPath);
|
|
1068
|
+
}
|
|
1069
|
+
async #readPresetFile(file, commandPath) {
|
|
1070
|
+
try {
|
|
1071
|
+
return await this.#runtime.readFile(file.absolutePath);
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
const ioError = error;
|
|
1075
|
+
if (!file.explicit && ioError.code === 'ENOENT') {
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${error.message}`, commandPath);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
#tokenizePresetOptions(content) {
|
|
1082
|
+
return content
|
|
1083
|
+
.split(/\s+/)
|
|
1084
|
+
.map(token => token.trim())
|
|
1085
|
+
.filter(token => token.length > 0);
|
|
1086
|
+
}
|
|
1087
|
+
#validatePresetOptionTokens(tokens, filepath, commandPath) {
|
|
1088
|
+
if (tokens.length === 0) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (!tokens[0].startsWith('-')) {
|
|
1092
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": bare token "${tokens[0]}" cannot appear before any option token`, commandPath);
|
|
1093
|
+
}
|
|
1094
|
+
for (const token of tokens) {
|
|
1095
|
+
if (token === '--') {
|
|
1096
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": "--" is not allowed`, commandPath);
|
|
1097
|
+
}
|
|
1098
|
+
if (token === 'help' || token === '--help' || token === '--version') {
|
|
1099
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
|
|
1100
|
+
}
|
|
1101
|
+
if (token === PRESET_ROOT_FLAG ||
|
|
1102
|
+
token.startsWith(`${PRESET_ROOT_FLAG}=`) ||
|
|
1103
|
+
token === PRESET_OPTS_FLAG ||
|
|
1104
|
+
token.startsWith(`${PRESET_OPTS_FLAG}=`) ||
|
|
1105
|
+
token === PRESET_ENVS_FLAG ||
|
|
1106
|
+
token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1107
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
677
1110
|
}
|
|
678
1111
|
#resolve(chain, tokens, optionPolicyMap) {
|
|
679
1112
|
const consumedTokens = new Map();
|
|
@@ -783,13 +1216,20 @@ class Command {
|
|
|
783
1216
|
}
|
|
784
1217
|
}
|
|
785
1218
|
}
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
1219
|
+
const leafLocalOpts = {};
|
|
1220
|
+
const leafParsedOpts = optsMap.get(leafCommand) ?? {};
|
|
1221
|
+
for (const opt of leafCommand.#options) {
|
|
1222
|
+
if (Object.prototype.hasOwnProperty.call(leafParsedOpts, opt.long)) {
|
|
1223
|
+
leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
|
|
1224
|
+
}
|
|
789
1225
|
}
|
|
790
1226
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
791
1227
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
792
|
-
|
|
1228
|
+
const parseCtx = {
|
|
1229
|
+
...ctx,
|
|
1230
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1231
|
+
};
|
|
1232
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
793
1233
|
}
|
|
794
1234
|
#parseOptions(tokens, allOptions, envs) {
|
|
795
1235
|
const opts = {};
|
|
@@ -976,29 +1416,19 @@ class Command {
|
|
|
976
1416
|
#hasUserOption(long) {
|
|
977
1417
|
return this.#options.some(option => option.long === long);
|
|
978
1418
|
}
|
|
979
|
-
#
|
|
980
|
-
return this.#version !== undefined;
|
|
1419
|
+
#supportsBuiltinVersion() {
|
|
1420
|
+
return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
|
|
981
1421
|
}
|
|
982
1422
|
#resolveOptionPolicy() {
|
|
983
1423
|
const optionMap = new Map();
|
|
984
1424
|
const hasUserColor = this.#hasUserOption('color');
|
|
985
|
-
const hasUserHelp = this.#hasUserOption('help');
|
|
986
|
-
const hasUserVersion = this.#hasUserOption('version');
|
|
987
1425
|
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
988
1426
|
const hasUserSilent = this.#hasUserOption('silent');
|
|
989
1427
|
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
990
1428
|
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
991
|
-
const enableBuiltinHelp = !hasUserHelp;
|
|
992
|
-
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
993
1429
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
994
1430
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
995
1431
|
}
|
|
996
|
-
if (enableBuiltinHelp) {
|
|
997
|
-
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
998
|
-
}
|
|
999
|
-
if (enableBuiltinVersion) {
|
|
1000
|
-
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
1001
|
-
}
|
|
1002
1432
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
1003
1433
|
optionMap.set('logLevel', logLevelOption);
|
|
1004
1434
|
}
|
|
@@ -1016,8 +1446,6 @@ class Command {
|
|
|
1016
1446
|
}
|
|
1017
1447
|
return {
|
|
1018
1448
|
mergedOptions: Array.from(optionMap.values()),
|
|
1019
|
-
enableBuiltinHelp,
|
|
1020
|
-
enableBuiltinVersion,
|
|
1021
1449
|
};
|
|
1022
1450
|
}
|
|
1023
1451
|
#buildOptionPolicyMap(chain) {
|
|
@@ -1054,6 +1482,9 @@ class Command {
|
|
|
1054
1482
|
}
|
|
1055
1483
|
}
|
|
1056
1484
|
#validateOptionConfig(opt) {
|
|
1485
|
+
if (opt.long === 'help' || opt.long === 'version') {
|
|
1486
|
+
throw new CommanderError('ConfigurationError', `option long name "${opt.long}" is reserved`, this.#getCommandPath());
|
|
1487
|
+
}
|
|
1057
1488
|
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
1058
1489
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
1059
1490
|
}
|
|
@@ -1134,22 +1565,27 @@ class Command {
|
|
|
1134
1565
|
process.exit(1);
|
|
1135
1566
|
}
|
|
1136
1567
|
}
|
|
1137
|
-
#
|
|
1568
|
+
#resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
|
|
1138
1569
|
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1139
1570
|
let color = !isNoColorEnabled(envs);
|
|
1140
1571
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1141
1572
|
return color;
|
|
1142
1573
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1574
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
1575
|
+
const scanTokens = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
1576
|
+
for (const token of scanTokens) {
|
|
1577
|
+
if (token === '--color') {
|
|
1578
|
+
color = true;
|
|
1145
1579
|
continue;
|
|
1146
1580
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
color = true;
|
|
1581
|
+
if (token === '--no-color') {
|
|
1582
|
+
color = false;
|
|
1150
1583
|
continue;
|
|
1151
1584
|
}
|
|
1152
|
-
|
|
1585
|
+
if (!token.startsWith('--color=')) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const value = token.slice('--color='.length);
|
|
1153
1589
|
if (value === 'true') {
|
|
1154
1590
|
color = true;
|
|
1155
1591
|
}
|
|
@@ -1162,16 +1598,18 @@ class Command {
|
|
|
1162
1598
|
}
|
|
1163
1599
|
return color;
|
|
1164
1600
|
}
|
|
1165
|
-
#
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1601
|
+
#freezeInputSources(sources) {
|
|
1602
|
+
return Object.freeze({
|
|
1603
|
+
preset: Object.freeze({
|
|
1604
|
+
argv: Object.freeze([...sources.preset.argv]),
|
|
1605
|
+
envs: Object.freeze({ ...sources.preset.envs }),
|
|
1606
|
+
}),
|
|
1607
|
+
user: Object.freeze({
|
|
1608
|
+
cmds: Object.freeze([...sources.user.cmds]),
|
|
1609
|
+
argv: Object.freeze([...sources.user.argv]),
|
|
1610
|
+
envs: Object.freeze({ ...sources.user.envs }),
|
|
1611
|
+
}),
|
|
1612
|
+
});
|
|
1175
1613
|
}
|
|
1176
1614
|
#getCommandPath() {
|
|
1177
1615
|
const parts = [];
|
|
@@ -1340,9 +1778,12 @@ function camelToKebabCase(str) {
|
|
|
1340
1778
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1341
1779
|
}
|
|
1342
1780
|
class CompletionCommand extends Command {
|
|
1343
|
-
constructor(root, config) {
|
|
1344
|
-
const paths = config.paths;
|
|
1781
|
+
constructor(root, config = {}) {
|
|
1345
1782
|
const programName = config.programName ?? root.name ?? 'program';
|
|
1783
|
+
const paths = {
|
|
1784
|
+
...createDefaultCompletionPaths(programName),
|
|
1785
|
+
...config.paths,
|
|
1786
|
+
};
|
|
1346
1787
|
super({ desc: 'Generate shell completion script' });
|
|
1347
1788
|
this.option({
|
|
1348
1789
|
long: 'bash',
|
|
@@ -1404,11 +1845,11 @@ class CompletionCommand extends Command {
|
|
|
1404
1845
|
if (writeOpt !== undefined) {
|
|
1405
1846
|
const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
|
|
1406
1847
|
const expandedPath = expandHome(filePath);
|
|
1407
|
-
const dir =
|
|
1408
|
-
if (!
|
|
1409
|
-
|
|
1848
|
+
const dir = path.dirname(expandedPath);
|
|
1849
|
+
if (!fs.existsSync(dir)) {
|
|
1850
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1410
1851
|
}
|
|
1411
|
-
|
|
1852
|
+
fs.writeFileSync(expandedPath, script, 'utf-8');
|
|
1412
1853
|
console.log(`Completion script written to: ${expandedPath}`);
|
|
1413
1854
|
}
|
|
1414
1855
|
else {
|
|
@@ -1417,6 +1858,13 @@ class CompletionCommand extends Command {
|
|
|
1417
1858
|
});
|
|
1418
1859
|
}
|
|
1419
1860
|
}
|
|
1861
|
+
function createDefaultCompletionPaths(programName) {
|
|
1862
|
+
return {
|
|
1863
|
+
bash: `~/.local/share/bash-completion/completions/${programName}`,
|
|
1864
|
+
fish: `~/.config/fish/completions/${programName}.fish`,
|
|
1865
|
+
pwsh: '~/.config/powershell/Microsoft.PowerShell_profile.ps1',
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1420
1868
|
function expandHome(filepath) {
|
|
1421
1869
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
1422
1870
|
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
@@ -1694,6 +2142,8 @@ class PwshCompletion {
|
|
|
1694
2142
|
}
|
|
1695
2143
|
}
|
|
1696
2144
|
|
|
2145
|
+
setDefaultCommandRuntime(createNodeCommandRuntime());
|
|
2146
|
+
|
|
1697
2147
|
exports.BashCompletion = BashCompletion;
|
|
1698
2148
|
exports.Coerce = Coerce;
|
|
1699
2149
|
exports.Command = Command;
|
|
@@ -1701,6 +2151,9 @@ exports.CommanderError = CommanderError;
|
|
|
1701
2151
|
exports.CompletionCommand = CompletionCommand;
|
|
1702
2152
|
exports.FishCompletion = FishCompletion;
|
|
1703
2153
|
exports.PwshCompletion = PwshCompletion;
|
|
2154
|
+
exports.createBrowserCommandRuntime = createBrowserCommandRuntime;
|
|
2155
|
+
exports.createNodeCommandRuntime = createNodeCommandRuntime;
|
|
2156
|
+
exports.getDefaultCommandRuntime = getDefaultCommandRuntime;
|
|
1704
2157
|
exports.isDomain = isDomain;
|
|
1705
2158
|
exports.isIp = isIp;
|
|
1706
2159
|
exports.isIpv4 = isIpv4;
|
|
@@ -1708,4 +2161,5 @@ exports.isIpv6 = isIpv6;
|
|
|
1708
2161
|
exports.logColorfulOption = logColorfulOption;
|
|
1709
2162
|
exports.logDateOption = logDateOption;
|
|
1710
2163
|
exports.logLevelOption = logLevelOption;
|
|
2164
|
+
exports.setDefaultCommandRuntime = setDefaultCommandRuntime;
|
|
1711
2165
|
exports.silentOption = silentOption;
|