@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,6 +1,96 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { stat, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@guanghechen/env';
|
|
4
|
+
import { Reporter } from '@guanghechen/reporter';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const WINDOWS_DRIVE_ABSOLUTE_REGEX = /^[a-zA-Z]:[\\/]/;
|
|
8
|
+
function isAbsolutePath(filepath) {
|
|
9
|
+
return (filepath.startsWith('/') ||
|
|
10
|
+
filepath.startsWith('\\\\') ||
|
|
11
|
+
WINDOWS_DRIVE_ABSOLUTE_REGEX.test(filepath));
|
|
12
|
+
}
|
|
13
|
+
function resolvePathFrom(base, fragment) {
|
|
14
|
+
const useWindowsStyle = WINDOWS_DRIVE_ABSOLUTE_REGEX.test(base);
|
|
15
|
+
const normalizedBase = base.replace(/\\/g, '/');
|
|
16
|
+
const normalizedFragment = fragment.replace(/\\/g, '/');
|
|
17
|
+
const source = isAbsolutePath(normalizedFragment)
|
|
18
|
+
? normalizedFragment
|
|
19
|
+
: `${normalizedBase.replace(/\/$/, '')}/${normalizedFragment}`;
|
|
20
|
+
const prefix = useWindowsStyle ? source.slice(0, 2) : '';
|
|
21
|
+
const body = useWindowsStyle ? source.slice(2) : source;
|
|
22
|
+
const stack = [];
|
|
23
|
+
for (const token of body.split('/')) {
|
|
24
|
+
if (token === '' || token === '.') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (token === '..') {
|
|
28
|
+
if (stack.length > 0) {
|
|
29
|
+
stack.pop();
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
stack.push(token);
|
|
34
|
+
}
|
|
35
|
+
if (useWindowsStyle) {
|
|
36
|
+
const resolved = `${prefix}/${stack.join('/')}`;
|
|
37
|
+
return resolved.endsWith('/') ? resolved.slice(0, -1) : resolved;
|
|
38
|
+
}
|
|
39
|
+
return `/${stack.join('/')}`;
|
|
40
|
+
}
|
|
41
|
+
function createUnsupportedFsError(operation) {
|
|
42
|
+
return new Error(`runtime does not support file-system operation: ${operation}`);
|
|
43
|
+
}
|
|
44
|
+
function getFallbackCwd() {
|
|
45
|
+
const proc = globalThis.process;
|
|
46
|
+
if (proc && typeof proc.cwd === 'function') {
|
|
47
|
+
return proc.cwd();
|
|
48
|
+
}
|
|
49
|
+
return '/';
|
|
50
|
+
}
|
|
51
|
+
function createBrowserCommandRuntime() {
|
|
52
|
+
return {
|
|
53
|
+
cwd: () => getFallbackCwd(),
|
|
54
|
+
isAbsolute: filepath => isAbsolutePath(filepath),
|
|
55
|
+
resolve: (...paths) => {
|
|
56
|
+
if (paths.length === 0) {
|
|
57
|
+
return getFallbackCwd();
|
|
58
|
+
}
|
|
59
|
+
let resolved = getFallbackCwd();
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
if (path.length === 0) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
resolved = resolvePathFrom(resolved, path);
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
},
|
|
68
|
+
readFile: async () => {
|
|
69
|
+
throw createUnsupportedFsError('readFile');
|
|
70
|
+
},
|
|
71
|
+
stat: async () => {
|
|
72
|
+
throw createUnsupportedFsError('stat');
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let defaultRuntime = createBrowserCommandRuntime();
|
|
78
|
+
function getDefaultCommandRuntime() {
|
|
79
|
+
return defaultRuntime;
|
|
80
|
+
}
|
|
81
|
+
function setDefaultCommandRuntime(runtime) {
|
|
82
|
+
defaultRuntime = runtime;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createNodeCommandRuntime() {
|
|
86
|
+
return {
|
|
87
|
+
cwd: () => process.cwd(),
|
|
88
|
+
isAbsolute: filepath => path.isAbsolute(filepath),
|
|
89
|
+
resolve: (...paths) => path.resolve(...paths),
|
|
90
|
+
readFile: filepath => readFile(filepath, 'utf8'),
|
|
91
|
+
stat: filepath => stat(filepath),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
4
94
|
|
|
5
95
|
const TERMINAL_STYLE = {
|
|
6
96
|
bold: '\x1b[1m',
|
|
@@ -14,22 +104,35 @@ function styleText(text, ...styles) {
|
|
|
14
104
|
return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
|
|
15
105
|
}
|
|
16
106
|
|
|
107
|
+
const BUILTIN_LOG_LEVELS = ['debug', 'info', 'hint', 'warn', 'error'];
|
|
108
|
+
function resolveReporterLogLevel(raw) {
|
|
109
|
+
const normalized = raw.trim().toLowerCase();
|
|
110
|
+
return BUILTIN_LOG_LEVELS.find(level => level === normalized);
|
|
111
|
+
}
|
|
112
|
+
function setReporterLevel(ctx, level) {
|
|
113
|
+
const reporter = ctx.reporter;
|
|
114
|
+
reporter?.setLevel?.(level);
|
|
115
|
+
}
|
|
116
|
+
function setReporterFlight(ctx, flight) {
|
|
117
|
+
const reporter = ctx.reporter;
|
|
118
|
+
reporter?.setFlight?.(flight);
|
|
119
|
+
}
|
|
17
120
|
const logLevelOption = {
|
|
18
121
|
long: 'logLevel',
|
|
19
122
|
type: 'string',
|
|
20
123
|
args: 'required',
|
|
21
124
|
desc: 'Set log level',
|
|
22
125
|
default: 'info',
|
|
23
|
-
choices:
|
|
126
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
24
127
|
coerce: (raw) => {
|
|
25
|
-
const level =
|
|
128
|
+
const level = resolveReporterLogLevel(raw);
|
|
26
129
|
if (level === undefined) {
|
|
27
130
|
throw new Error(`Invalid log level: ${raw}`);
|
|
28
131
|
}
|
|
29
132
|
return level;
|
|
30
133
|
},
|
|
31
134
|
apply: (value, ctx) => {
|
|
32
|
-
ctx
|
|
135
|
+
setReporterLevel(ctx, value);
|
|
33
136
|
},
|
|
34
137
|
};
|
|
35
138
|
const logDateOption = {
|
|
@@ -39,7 +142,7 @@ const logDateOption = {
|
|
|
39
142
|
desc: 'Enable log timestamp',
|
|
40
143
|
default: true,
|
|
41
144
|
apply: (value, ctx) => {
|
|
42
|
-
ctx
|
|
145
|
+
setReporterFlight(ctx, { date: Boolean(value) });
|
|
43
146
|
},
|
|
44
147
|
};
|
|
45
148
|
const logColorfulOption = {
|
|
@@ -49,7 +152,7 @@ const logColorfulOption = {
|
|
|
49
152
|
desc: 'Enable colorful log output',
|
|
50
153
|
default: true,
|
|
51
154
|
apply: (value, ctx) => {
|
|
52
|
-
ctx
|
|
155
|
+
setReporterFlight(ctx, { color: Boolean(value) });
|
|
53
156
|
},
|
|
54
157
|
};
|
|
55
158
|
const silentOption = {
|
|
@@ -60,7 +163,7 @@ const silentOption = {
|
|
|
60
163
|
default: false,
|
|
61
164
|
apply: (value, ctx) => {
|
|
62
165
|
if (value) {
|
|
63
|
-
ctx
|
|
166
|
+
setReporterLevel(ctx, 'error');
|
|
64
167
|
}
|
|
65
168
|
},
|
|
66
169
|
};
|
|
@@ -81,6 +184,11 @@ class CommanderError extends Error {
|
|
|
81
184
|
|
|
82
185
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
83
186
|
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
187
|
+
const PRESET_OPTS_FLAG = '--preset-opts';
|
|
188
|
+
const PRESET_ENVS_FLAG = '--preset-envs';
|
|
189
|
+
const PRESET_ROOT_FLAG = '--preset-root';
|
|
190
|
+
const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
|
|
191
|
+
const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
|
|
84
192
|
function kebabToCamelCase(str) {
|
|
85
193
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
86
194
|
}
|
|
@@ -168,14 +276,12 @@ function tokenize(argv, commandPath) {
|
|
|
168
276
|
}
|
|
169
277
|
const BUILTIN_HELP_OPTION = {
|
|
170
278
|
long: 'help',
|
|
171
|
-
short: 'h',
|
|
172
279
|
type: 'boolean',
|
|
173
280
|
args: 'none',
|
|
174
281
|
desc: 'Show help information',
|
|
175
282
|
};
|
|
176
283
|
const BUILTIN_VERSION_OPTION = {
|
|
177
284
|
long: 'version',
|
|
178
|
-
short: 'V',
|
|
179
285
|
type: 'boolean',
|
|
180
286
|
args: 'none',
|
|
181
287
|
desc: 'Show version number',
|
|
@@ -189,6 +295,7 @@ const BUILTIN_COLOR_OPTION = {
|
|
|
189
295
|
};
|
|
190
296
|
function createBuiltinOptionState(enabled) {
|
|
191
297
|
return {
|
|
298
|
+
version: enabled,
|
|
192
299
|
color: enabled,
|
|
193
300
|
logLevel: enabled,
|
|
194
301
|
silent: enabled,
|
|
@@ -202,9 +309,6 @@ function isNoColorEnabled(envs) {
|
|
|
202
309
|
function normalizeBuiltinConfig(builtin) {
|
|
203
310
|
const resolved = {
|
|
204
311
|
option: createBuiltinOptionState(true),
|
|
205
|
-
command: {
|
|
206
|
-
help: false,
|
|
207
|
-
},
|
|
208
312
|
};
|
|
209
313
|
if (builtin === undefined) {
|
|
210
314
|
return resolved;
|
|
@@ -212,13 +316,11 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
212
316
|
if (builtin === true) {
|
|
213
317
|
return {
|
|
214
318
|
option: createBuiltinOptionState(true),
|
|
215
|
-
command: { help: true },
|
|
216
319
|
};
|
|
217
320
|
}
|
|
218
321
|
if (builtin === false) {
|
|
219
322
|
return {
|
|
220
323
|
option: createBuiltinOptionState(false),
|
|
221
|
-
command: { help: false },
|
|
222
324
|
};
|
|
223
325
|
}
|
|
224
326
|
if (builtin.option !== undefined) {
|
|
@@ -229,6 +331,8 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
229
331
|
resolved.option = createBuiltinOptionState(true);
|
|
230
332
|
}
|
|
231
333
|
else {
|
|
334
|
+
if (builtin.option.version !== undefined)
|
|
335
|
+
resolved.option.version = builtin.option.version;
|
|
232
336
|
if (builtin.option.color !== undefined)
|
|
233
337
|
resolved.option.color = builtin.option.color;
|
|
234
338
|
if (builtin.option.logLevel !== undefined) {
|
|
@@ -243,25 +347,17 @@ function normalizeBuiltinConfig(builtin) {
|
|
|
243
347
|
}
|
|
244
348
|
}
|
|
245
349
|
}
|
|
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
350
|
return resolved;
|
|
258
351
|
}
|
|
259
352
|
class Command {
|
|
260
353
|
#name;
|
|
261
354
|
#desc;
|
|
262
355
|
#version;
|
|
356
|
+
#builtinConfig;
|
|
263
357
|
#builtin;
|
|
358
|
+
#presetConfig;
|
|
264
359
|
#reporter;
|
|
360
|
+
#runtime;
|
|
265
361
|
#parent;
|
|
266
362
|
#options = [];
|
|
267
363
|
#arguments = [];
|
|
@@ -273,8 +369,11 @@ class Command {
|
|
|
273
369
|
this.#name = config.name ?? '';
|
|
274
370
|
this.#desc = config.desc;
|
|
275
371
|
this.#version = config.version;
|
|
372
|
+
this.#builtinConfig = config.builtin;
|
|
276
373
|
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
374
|
+
this.#presetConfig = config.preset;
|
|
277
375
|
this.#reporter = config.reporter;
|
|
376
|
+
this.#runtime = config.runtime ?? getDefaultCommandRuntime();
|
|
278
377
|
}
|
|
279
378
|
get name() {
|
|
280
379
|
return this.#name || undefined;
|
|
@@ -285,6 +384,12 @@ class Command {
|
|
|
285
384
|
get version() {
|
|
286
385
|
return this.#version;
|
|
287
386
|
}
|
|
387
|
+
get builtin() {
|
|
388
|
+
return this.#builtinConfig;
|
|
389
|
+
}
|
|
390
|
+
get preset() {
|
|
391
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
392
|
+
}
|
|
288
393
|
get parent() {
|
|
289
394
|
return this.#parent;
|
|
290
395
|
}
|
|
@@ -320,14 +425,17 @@ class Command {
|
|
|
320
425
|
return this;
|
|
321
426
|
}
|
|
322
427
|
subcommand(name, cmd) {
|
|
323
|
-
if (
|
|
324
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name
|
|
428
|
+
if (name === 'help') {
|
|
429
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
|
|
325
430
|
}
|
|
326
431
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
327
432
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
328
433
|
}
|
|
329
434
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
330
435
|
if (existing) {
|
|
436
|
+
if (existing.aliases.includes(name)) {
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
331
439
|
existing.aliases.push(name);
|
|
332
440
|
this.#subcommandsMap.set(name, cmd);
|
|
333
441
|
}
|
|
@@ -342,32 +450,35 @@ class Command {
|
|
|
342
450
|
async run(params) {
|
|
343
451
|
const { argv, envs, reporter } = params;
|
|
344
452
|
try {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const { chain, remaining } = routeResult;
|
|
453
|
+
const routeResult = this.#route(argv);
|
|
454
|
+
const { chain } = routeResult;
|
|
348
455
|
const leafCommand = chain[chain.length - 1];
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
456
|
+
const ctx = this.#createContext({
|
|
457
|
+
chain,
|
|
458
|
+
cmds: routeResult.cmds,
|
|
459
|
+
envs,
|
|
460
|
+
reporter,
|
|
461
|
+
});
|
|
462
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
463
|
+
ctx.controls = controlScanResult.controls;
|
|
464
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
465
|
+
if (ctx.controls.help) {
|
|
466
|
+
const helpCommand = this.#resolveHelpCommand(leafCommand, controlScanResult.helpTarget);
|
|
467
|
+
const helpColor = helpCommand.#resolveHelpColorFromTailArgv(controlScanResult.remaining, ctx.envs);
|
|
468
|
+
console.log(helpCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
356
469
|
return;
|
|
357
470
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
471
|
+
if (ctx.controls.version) {
|
|
472
|
+
console.log(leafCommand.#version);
|
|
473
|
+
return;
|
|
363
474
|
}
|
|
475
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
476
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
477
|
+
ctx.sources = presetResult.sources;
|
|
478
|
+
ctx.envs = presetResult.envs;
|
|
479
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
480
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
364
481
|
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
482
|
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
372
483
|
const actionParams = {
|
|
373
484
|
ctx: parseResult.ctx,
|
|
@@ -379,7 +490,7 @@ class Command {
|
|
|
379
490
|
await leafCommand.#runAction(actionParams);
|
|
380
491
|
}
|
|
381
492
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
382
|
-
const helpColor = leafCommand.#
|
|
493
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
383
494
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
384
495
|
}
|
|
385
496
|
else {
|
|
@@ -395,22 +506,27 @@ class Command {
|
|
|
395
506
|
throw err;
|
|
396
507
|
}
|
|
397
508
|
}
|
|
398
|
-
parse(params) {
|
|
509
|
+
async parse(params) {
|
|
399
510
|
const { argv, envs, reporter } = params;
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const { chain, remaining } = routeResult;
|
|
511
|
+
const routeResult = this.#route(argv);
|
|
512
|
+
const { chain } = routeResult;
|
|
403
513
|
const leafCommand = chain[chain.length - 1];
|
|
404
|
-
const
|
|
405
|
-
|
|
514
|
+
const ctx = this.#createContext({
|
|
515
|
+
chain,
|
|
516
|
+
cmds: routeResult.cmds,
|
|
517
|
+
envs,
|
|
518
|
+
reporter,
|
|
519
|
+
});
|
|
520
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
521
|
+
ctx.controls = controlScanResult.controls;
|
|
522
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
406
523
|
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
524
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
525
|
+
ctx.sources = presetResult.sources;
|
|
526
|
+
ctx.envs = presetResult.envs;
|
|
527
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
528
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
407
529
|
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
530
|
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
415
531
|
}
|
|
416
532
|
formatHelp() {
|
|
@@ -428,7 +544,11 @@ class Command {
|
|
|
428
544
|
return color && process.stdout.isTTY === true;
|
|
429
545
|
}
|
|
430
546
|
#buildHelpData() {
|
|
431
|
-
const
|
|
547
|
+
const parseOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
548
|
+
const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
|
|
549
|
+
if (this.#supportsBuiltinVersion()) {
|
|
550
|
+
allOptions.push(BUILTIN_VERSION_OPTION);
|
|
551
|
+
}
|
|
432
552
|
const commandPath = this.#getCommandPath();
|
|
433
553
|
let usage = `Usage: ${commandPath}`;
|
|
434
554
|
if (allOptions.length > 0)
|
|
@@ -462,7 +582,10 @@ class Command {
|
|
|
462
582
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
463
583
|
}
|
|
464
584
|
options.push({ sig, desc });
|
|
465
|
-
if (opt.type === 'boolean' &&
|
|
585
|
+
if (opt.type === 'boolean' &&
|
|
586
|
+
opt.args === 'none' &&
|
|
587
|
+
opt.long !== 'help' &&
|
|
588
|
+
opt.long !== 'version') {
|
|
466
589
|
options.push({
|
|
467
590
|
sig: ` --no-${kebabLong}`,
|
|
468
591
|
desc: `Negate --${kebabLong}`,
|
|
@@ -470,8 +593,7 @@ class Command {
|
|
|
470
593
|
}
|
|
471
594
|
}
|
|
472
595
|
const commands = [];
|
|
473
|
-
|
|
474
|
-
if (showHelpSubcommand) {
|
|
596
|
+
if (this.#subcommandsList.length > 0) {
|
|
475
597
|
commands.push({ name: 'help', desc: 'Show help for a command' });
|
|
476
598
|
}
|
|
477
599
|
for (const entry of this.#subcommandsList) {
|
|
@@ -594,50 +716,9 @@ class Command {
|
|
|
594
716
|
#findSubcommandEntry(token) {
|
|
595
717
|
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
596
718
|
}
|
|
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
719
|
#route(argv) {
|
|
640
720
|
const chain = [this];
|
|
721
|
+
const cmds = [];
|
|
641
722
|
let current = this;
|
|
642
723
|
let idx = 0;
|
|
643
724
|
while (idx < argv.length) {
|
|
@@ -648,10 +729,382 @@ class Command {
|
|
|
648
729
|
if (!entry)
|
|
649
730
|
break;
|
|
650
731
|
current = entry.command;
|
|
732
|
+
cmds.push(token);
|
|
651
733
|
chain.push(current);
|
|
652
734
|
idx += 1;
|
|
653
735
|
}
|
|
654
|
-
return { chain, remaining: argv.slice(idx) };
|
|
736
|
+
return { chain, remaining: argv.slice(idx), cmds };
|
|
737
|
+
}
|
|
738
|
+
#controlScan(tailArgv, leafCommand) {
|
|
739
|
+
const controls = { help: false, version: false };
|
|
740
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
741
|
+
const beforeSeparator = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
742
|
+
const afterSeparator = separatorIndex === -1 ? [] : tailArgv.slice(separatorIndex + 1);
|
|
743
|
+
let helpTarget;
|
|
744
|
+
let scanStartIndex = 0;
|
|
745
|
+
if (beforeSeparator[0] === 'help') {
|
|
746
|
+
controls.help = true;
|
|
747
|
+
scanStartIndex = 1;
|
|
748
|
+
const candidate = beforeSeparator[1];
|
|
749
|
+
if (candidate !== undefined && !candidate.startsWith('-')) {
|
|
750
|
+
helpTarget = candidate;
|
|
751
|
+
scanStartIndex = 2;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const remainingBeforeSeparator = [];
|
|
755
|
+
for (let i = scanStartIndex; i < beforeSeparator.length; i += 1) {
|
|
756
|
+
const token = beforeSeparator[i];
|
|
757
|
+
if (token === '--help') {
|
|
758
|
+
controls.help = true;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (token === '--version' && leafCommand.#supportsBuiltinVersion()) {
|
|
762
|
+
controls.version = true;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
remainingBeforeSeparator.push(token);
|
|
766
|
+
}
|
|
767
|
+
const remaining = separatorIndex === -1
|
|
768
|
+
? remainingBeforeSeparator
|
|
769
|
+
: [...remainingBeforeSeparator, '--', ...afterSeparator];
|
|
770
|
+
return {
|
|
771
|
+
controls,
|
|
772
|
+
remaining,
|
|
773
|
+
helpTarget,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
#createContext(params) {
|
|
777
|
+
const { chain, cmds, envs, reporter } = params;
|
|
778
|
+
const leafCommand = chain[chain.length - 1];
|
|
779
|
+
const envSnapshot = { ...envs };
|
|
780
|
+
return {
|
|
781
|
+
cmd: leafCommand,
|
|
782
|
+
chain,
|
|
783
|
+
envs: envSnapshot,
|
|
784
|
+
controls: { help: false, version: false },
|
|
785
|
+
sources: {
|
|
786
|
+
preset: {
|
|
787
|
+
argv: [],
|
|
788
|
+
envs: {},
|
|
789
|
+
},
|
|
790
|
+
user: {
|
|
791
|
+
cmds: [...cmds],
|
|
792
|
+
argv: [],
|
|
793
|
+
envs: envSnapshot,
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
#resolveHelpCommand(leafCommand, helpTarget) {
|
|
800
|
+
if (helpTarget === undefined) {
|
|
801
|
+
return leafCommand;
|
|
802
|
+
}
|
|
803
|
+
const target = leafCommand.#findSubcommandEntry(helpTarget);
|
|
804
|
+
if (target === undefined) {
|
|
805
|
+
return leafCommand;
|
|
806
|
+
}
|
|
807
|
+
return target.command;
|
|
808
|
+
}
|
|
809
|
+
async #preset(controlTailArgv, ctx, optionPolicyMap) {
|
|
810
|
+
const commandPath = ctx.chain[ctx.chain.length - 1].#getCommandPath();
|
|
811
|
+
const separatorIndex = controlTailArgv.indexOf('--');
|
|
812
|
+
const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
|
|
813
|
+
const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
|
|
814
|
+
const rootScanResult = this.#scanPresetRootDirectives(beforeSeparator, commandPath);
|
|
815
|
+
const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
|
|
816
|
+
const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
|
|
817
|
+
const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
|
|
818
|
+
const cleanArgv = separatorIndex === -1
|
|
819
|
+
? fileScanResult.cleanArgv
|
|
820
|
+
: [...fileScanResult.cleanArgv, '--', ...afterSeparator];
|
|
821
|
+
const presetOptsFiles = this.#resolvePresetFileSources({
|
|
822
|
+
cliFiles: fileScanResult.cliPresetOptsFiles,
|
|
823
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.opt),
|
|
824
|
+
presetRoot,
|
|
825
|
+
defaultFilename: DEFAULT_PRESET_OPTS_FILENAME,
|
|
826
|
+
});
|
|
827
|
+
const presetEnvsFiles = this.#resolvePresetFileSources({
|
|
828
|
+
cliFiles: fileScanResult.cliPresetEnvsFiles,
|
|
829
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.env),
|
|
830
|
+
presetRoot,
|
|
831
|
+
defaultFilename: DEFAULT_PRESET_ENVS_FILENAME,
|
|
832
|
+
});
|
|
833
|
+
const userSources = {
|
|
834
|
+
cmds: [...ctx.sources.user.cmds],
|
|
835
|
+
argv: [...cleanArgv],
|
|
836
|
+
envs: { ...ctx.sources.user.envs },
|
|
837
|
+
};
|
|
838
|
+
const presetArgv = [];
|
|
839
|
+
for (const file of presetOptsFiles) {
|
|
840
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
841
|
+
if (content === undefined) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const tokens = this.#tokenizePresetOptions(content);
|
|
845
|
+
this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
|
|
846
|
+
this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
|
|
847
|
+
presetArgv.push(...tokens);
|
|
848
|
+
}
|
|
849
|
+
const presetEnvs = {};
|
|
850
|
+
for (const file of presetEnvsFiles) {
|
|
851
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
852
|
+
if (content === undefined) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
let parsed;
|
|
856
|
+
try {
|
|
857
|
+
parsed = parse(content);
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset envs file "${file.displayPath}": ${error.message}`, commandPath);
|
|
861
|
+
}
|
|
862
|
+
Object.assign(presetEnvs, parsed);
|
|
863
|
+
}
|
|
864
|
+
const sources = {
|
|
865
|
+
user: userSources,
|
|
866
|
+
preset: {
|
|
867
|
+
argv: presetArgv,
|
|
868
|
+
envs: presetEnvs,
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
const envs = { ...sources.user.envs, ...sources.preset.envs };
|
|
872
|
+
const tailArgv = [...sources.preset.argv, ...sources.user.argv];
|
|
873
|
+
return { tailArgv, envs, sources };
|
|
874
|
+
}
|
|
875
|
+
#resolveCommandPresetFromChain(chain) {
|
|
876
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
877
|
+
const preset = chain[index].#presetConfig;
|
|
878
|
+
if (preset?.root !== undefined) {
|
|
879
|
+
return preset;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
async #resolveEffectivePresetRoot(cliPresetRoots, commandPreset, commandPath) {
|
|
885
|
+
if (cliPresetRoots.length > 0) {
|
|
886
|
+
const root = cliPresetRoots[cliPresetRoots.length - 1];
|
|
887
|
+
return await this.#assertPresetRoot(root, PRESET_ROOT_FLAG, commandPath);
|
|
888
|
+
}
|
|
889
|
+
if (commandPreset?.root === undefined) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
return await this.#assertPresetRoot(commandPreset.root, 'command.preset.root', commandPath);
|
|
893
|
+
}
|
|
894
|
+
async #assertPresetRoot(root, sourceName, commandPath) {
|
|
895
|
+
if (!this.#runtime.isAbsolute(root)) {
|
|
896
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not an absolute directory`, commandPath);
|
|
897
|
+
}
|
|
898
|
+
let stats;
|
|
899
|
+
try {
|
|
900
|
+
stats = await this.#runtime.stat(root);
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" cannot be accessed (${error.message})`, commandPath);
|
|
904
|
+
}
|
|
905
|
+
if (!stats.isDirectory()) {
|
|
906
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not a directory`, commandPath);
|
|
907
|
+
}
|
|
908
|
+
return root;
|
|
909
|
+
}
|
|
910
|
+
#normalizeCommandPresetFile(filepath) {
|
|
911
|
+
if (filepath === undefined) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
if (!this.#isValidPresetFileValue(filepath)) {
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
return filepath;
|
|
918
|
+
}
|
|
919
|
+
#resolvePresetFileSources(params) {
|
|
920
|
+
const { cliFiles, commandPresetFile, presetRoot, defaultFilename } = params;
|
|
921
|
+
if (cliFiles.length > 0) {
|
|
922
|
+
return cliFiles.map(filepath => ({
|
|
923
|
+
displayPath: filepath,
|
|
924
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(filepath, presetRoot),
|
|
925
|
+
explicit: true,
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
if (presetRoot === undefined) {
|
|
929
|
+
return [];
|
|
930
|
+
}
|
|
931
|
+
if (commandPresetFile !== undefined) {
|
|
932
|
+
return [
|
|
933
|
+
{
|
|
934
|
+
displayPath: commandPresetFile,
|
|
935
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
|
|
936
|
+
explicit: true,
|
|
937
|
+
},
|
|
938
|
+
];
|
|
939
|
+
}
|
|
940
|
+
const absolutePath = this.#runtime.resolve(presetRoot, defaultFilename);
|
|
941
|
+
return [
|
|
942
|
+
{
|
|
943
|
+
displayPath: absolutePath,
|
|
944
|
+
absolutePath,
|
|
945
|
+
explicit: false,
|
|
946
|
+
},
|
|
947
|
+
];
|
|
948
|
+
}
|
|
949
|
+
#resolvePresetFileAbsolutePath(filepath, presetRoot) {
|
|
950
|
+
if (this.#runtime.isAbsolute(filepath)) {
|
|
951
|
+
return filepath;
|
|
952
|
+
}
|
|
953
|
+
if (presetRoot !== undefined) {
|
|
954
|
+
return this.#runtime.resolve(presetRoot, filepath);
|
|
955
|
+
}
|
|
956
|
+
return this.#runtime.resolve(this.#runtime.cwd(), filepath);
|
|
957
|
+
}
|
|
958
|
+
#assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
|
|
959
|
+
if (tokens.length === 0) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const commandPath = chain[chain.length - 1].#getCommandPath();
|
|
963
|
+
try {
|
|
964
|
+
const { optionTokens, restArgs } = tokenize(tokens, commandPath);
|
|
965
|
+
void restArgs;
|
|
966
|
+
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
967
|
+
if (argTokens.length > 0) {
|
|
968
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
if (error instanceof CommanderError) {
|
|
973
|
+
if (error.kind === 'ConfigurationError') {
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
|
|
977
|
+
}
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
#scanPresetRootDirectives(argv, commandPath) {
|
|
982
|
+
const cleanArgv = [];
|
|
983
|
+
const cliPresetRoots = [];
|
|
984
|
+
let index = 0;
|
|
985
|
+
while (index < argv.length) {
|
|
986
|
+
const token = argv[index];
|
|
987
|
+
if (token === PRESET_ROOT_FLAG) {
|
|
988
|
+
const value = argv[index + 1];
|
|
989
|
+
if (value === undefined || value.length === 0) {
|
|
990
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
991
|
+
}
|
|
992
|
+
cliPresetRoots.push(value);
|
|
993
|
+
index += 2;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (token.startsWith(`${PRESET_ROOT_FLAG}=`)) {
|
|
997
|
+
const value = token.slice(PRESET_ROOT_FLAG.length + 1);
|
|
998
|
+
if (value.length === 0) {
|
|
999
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
1000
|
+
}
|
|
1001
|
+
cliPresetRoots.push(value);
|
|
1002
|
+
index += 1;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
cleanArgv.push(token);
|
|
1006
|
+
index += 1;
|
|
1007
|
+
}
|
|
1008
|
+
return { cleanArgv, cliPresetRoots };
|
|
1009
|
+
}
|
|
1010
|
+
#scanPresetFileDirectives(argv, commandPath) {
|
|
1011
|
+
const cleanArgv = [];
|
|
1012
|
+
const cliPresetOptsFiles = [];
|
|
1013
|
+
const cliPresetEnvsFiles = [];
|
|
1014
|
+
const assertAndPush = (flag, value) => {
|
|
1015
|
+
this.#assertPresetFileValue(value, flag, commandPath);
|
|
1016
|
+
if (flag === PRESET_OPTS_FLAG) {
|
|
1017
|
+
cliPresetOptsFiles.push(value);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
cliPresetEnvsFiles.push(value);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
let index = 0;
|
|
1024
|
+
while (index < argv.length) {
|
|
1025
|
+
const token = argv[index];
|
|
1026
|
+
if (token === PRESET_OPTS_FLAG || token === PRESET_ENVS_FLAG) {
|
|
1027
|
+
const value = argv[index + 1];
|
|
1028
|
+
if (value === undefined || value.length === 0) {
|
|
1029
|
+
throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
|
|
1030
|
+
}
|
|
1031
|
+
assertAndPush(token, value);
|
|
1032
|
+
index += 2;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (token.startsWith(`${PRESET_OPTS_FLAG}=`)) {
|
|
1036
|
+
const value = token.slice(PRESET_OPTS_FLAG.length + 1);
|
|
1037
|
+
if (value.length === 0) {
|
|
1038
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_OPTS_FLAG}"`, commandPath);
|
|
1039
|
+
}
|
|
1040
|
+
assertAndPush(PRESET_OPTS_FLAG, value);
|
|
1041
|
+
index += 1;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1045
|
+
const value = token.slice(PRESET_ENVS_FLAG.length + 1);
|
|
1046
|
+
if (value.length === 0) {
|
|
1047
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ENVS_FLAG}"`, commandPath);
|
|
1048
|
+
}
|
|
1049
|
+
assertAndPush(PRESET_ENVS_FLAG, value);
|
|
1050
|
+
index += 1;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
cleanArgv.push(token);
|
|
1054
|
+
index += 1;
|
|
1055
|
+
}
|
|
1056
|
+
return { cleanArgv, cliPresetOptsFiles, cliPresetEnvsFiles };
|
|
1057
|
+
}
|
|
1058
|
+
#isValidPresetFileValue(filepath) {
|
|
1059
|
+
return filepath.length > 0 && !filepath.startsWith('..');
|
|
1060
|
+
}
|
|
1061
|
+
#assertPresetFileValue(filepath, directive, commandPath) {
|
|
1062
|
+
if (this.#isValidPresetFileValue(filepath)) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${directive}": "${filepath}" (must be non-empty and must not start with "..")`, commandPath);
|
|
1066
|
+
}
|
|
1067
|
+
async #readPresetFile(file, commandPath) {
|
|
1068
|
+
try {
|
|
1069
|
+
return await this.#runtime.readFile(file.absolutePath);
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
const ioError = error;
|
|
1073
|
+
if (!file.explicit && ioError.code === 'ENOENT') {
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${error.message}`, commandPath);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
#tokenizePresetOptions(content) {
|
|
1080
|
+
return content
|
|
1081
|
+
.split(/\s+/)
|
|
1082
|
+
.map(token => token.trim())
|
|
1083
|
+
.filter(token => token.length > 0);
|
|
1084
|
+
}
|
|
1085
|
+
#validatePresetOptionTokens(tokens, filepath, commandPath) {
|
|
1086
|
+
if (tokens.length === 0) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (!tokens[0].startsWith('-')) {
|
|
1090
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": bare token "${tokens[0]}" cannot appear before any option token`, commandPath);
|
|
1091
|
+
}
|
|
1092
|
+
for (const token of tokens) {
|
|
1093
|
+
if (token === '--') {
|
|
1094
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": "--" is not allowed`, commandPath);
|
|
1095
|
+
}
|
|
1096
|
+
if (token === 'help' || token === '--help' || token === '--version') {
|
|
1097
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
|
|
1098
|
+
}
|
|
1099
|
+
if (token === PRESET_ROOT_FLAG ||
|
|
1100
|
+
token.startsWith(`${PRESET_ROOT_FLAG}=`) ||
|
|
1101
|
+
token === PRESET_OPTS_FLAG ||
|
|
1102
|
+
token.startsWith(`${PRESET_OPTS_FLAG}=`) ||
|
|
1103
|
+
token === PRESET_ENVS_FLAG ||
|
|
1104
|
+
token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1105
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
655
1108
|
}
|
|
656
1109
|
#resolve(chain, tokens, optionPolicyMap) {
|
|
657
1110
|
const consumedTokens = new Map();
|
|
@@ -761,13 +1214,20 @@ class Command {
|
|
|
761
1214
|
}
|
|
762
1215
|
}
|
|
763
1216
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
1217
|
+
const leafLocalOpts = {};
|
|
1218
|
+
const leafParsedOpts = optsMap.get(leafCommand) ?? {};
|
|
1219
|
+
for (const opt of leafCommand.#options) {
|
|
1220
|
+
if (Object.prototype.hasOwnProperty.call(leafParsedOpts, opt.long)) {
|
|
1221
|
+
leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
|
|
1222
|
+
}
|
|
767
1223
|
}
|
|
768
1224
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
769
1225
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
770
|
-
|
|
1226
|
+
const parseCtx = {
|
|
1227
|
+
...ctx,
|
|
1228
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1229
|
+
};
|
|
1230
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
771
1231
|
}
|
|
772
1232
|
#parseOptions(tokens, allOptions, envs) {
|
|
773
1233
|
const opts = {};
|
|
@@ -954,29 +1414,19 @@ class Command {
|
|
|
954
1414
|
#hasUserOption(long) {
|
|
955
1415
|
return this.#options.some(option => option.long === long);
|
|
956
1416
|
}
|
|
957
|
-
#
|
|
958
|
-
return this.#version !== undefined;
|
|
1417
|
+
#supportsBuiltinVersion() {
|
|
1418
|
+
return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
|
|
959
1419
|
}
|
|
960
1420
|
#resolveOptionPolicy() {
|
|
961
1421
|
const optionMap = new Map();
|
|
962
1422
|
const hasUserColor = this.#hasUserOption('color');
|
|
963
|
-
const hasUserHelp = this.#hasUserOption('help');
|
|
964
|
-
const hasUserVersion = this.#hasUserOption('version');
|
|
965
1423
|
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
966
1424
|
const hasUserSilent = this.#hasUserOption('silent');
|
|
967
1425
|
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
968
1426
|
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
969
|
-
const enableBuiltinHelp = !hasUserHelp;
|
|
970
|
-
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
971
1427
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
972
1428
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
973
1429
|
}
|
|
974
|
-
if (enableBuiltinHelp) {
|
|
975
|
-
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
976
|
-
}
|
|
977
|
-
if (enableBuiltinVersion) {
|
|
978
|
-
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
979
|
-
}
|
|
980
1430
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
981
1431
|
optionMap.set('logLevel', logLevelOption);
|
|
982
1432
|
}
|
|
@@ -994,8 +1444,6 @@ class Command {
|
|
|
994
1444
|
}
|
|
995
1445
|
return {
|
|
996
1446
|
mergedOptions: Array.from(optionMap.values()),
|
|
997
|
-
enableBuiltinHelp,
|
|
998
|
-
enableBuiltinVersion,
|
|
999
1447
|
};
|
|
1000
1448
|
}
|
|
1001
1449
|
#buildOptionPolicyMap(chain) {
|
|
@@ -1032,6 +1480,9 @@ class Command {
|
|
|
1032
1480
|
}
|
|
1033
1481
|
}
|
|
1034
1482
|
#validateOptionConfig(opt) {
|
|
1483
|
+
if (opt.long === 'help' || opt.long === 'version') {
|
|
1484
|
+
throw new CommanderError('ConfigurationError', `option long name "${opt.long}" is reserved`, this.#getCommandPath());
|
|
1485
|
+
}
|
|
1035
1486
|
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
1036
1487
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
1037
1488
|
}
|
|
@@ -1112,22 +1563,27 @@ class Command {
|
|
|
1112
1563
|
process.exit(1);
|
|
1113
1564
|
}
|
|
1114
1565
|
}
|
|
1115
|
-
#
|
|
1566
|
+
#resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
|
|
1116
1567
|
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1117
1568
|
let color = !isNoColorEnabled(envs);
|
|
1118
1569
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1119
1570
|
return color;
|
|
1120
1571
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1572
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
1573
|
+
const scanTokens = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
1574
|
+
for (const token of scanTokens) {
|
|
1575
|
+
if (token === '--color') {
|
|
1576
|
+
color = true;
|
|
1123
1577
|
continue;
|
|
1124
1578
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
color = true;
|
|
1579
|
+
if (token === '--no-color') {
|
|
1580
|
+
color = false;
|
|
1128
1581
|
continue;
|
|
1129
1582
|
}
|
|
1130
|
-
|
|
1583
|
+
if (!token.startsWith('--color=')) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
const value = token.slice('--color='.length);
|
|
1131
1587
|
if (value === 'true') {
|
|
1132
1588
|
color = true;
|
|
1133
1589
|
}
|
|
@@ -1140,16 +1596,18 @@ class Command {
|
|
|
1140
1596
|
}
|
|
1141
1597
|
return color;
|
|
1142
1598
|
}
|
|
1143
|
-
#
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1599
|
+
#freezeInputSources(sources) {
|
|
1600
|
+
return Object.freeze({
|
|
1601
|
+
preset: Object.freeze({
|
|
1602
|
+
argv: Object.freeze([...sources.preset.argv]),
|
|
1603
|
+
envs: Object.freeze({ ...sources.preset.envs }),
|
|
1604
|
+
}),
|
|
1605
|
+
user: Object.freeze({
|
|
1606
|
+
cmds: Object.freeze([...sources.user.cmds]),
|
|
1607
|
+
argv: Object.freeze([...sources.user.argv]),
|
|
1608
|
+
envs: Object.freeze({ ...sources.user.envs }),
|
|
1609
|
+
}),
|
|
1610
|
+
});
|
|
1153
1611
|
}
|
|
1154
1612
|
#getCommandPath() {
|
|
1155
1613
|
const parts = [];
|
|
@@ -1318,9 +1776,12 @@ function camelToKebabCase(str) {
|
|
|
1318
1776
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1319
1777
|
}
|
|
1320
1778
|
class CompletionCommand extends Command {
|
|
1321
|
-
constructor(root, config) {
|
|
1322
|
-
const paths = config.paths;
|
|
1779
|
+
constructor(root, config = {}) {
|
|
1323
1780
|
const programName = config.programName ?? root.name ?? 'program';
|
|
1781
|
+
const paths = {
|
|
1782
|
+
...createDefaultCompletionPaths(programName),
|
|
1783
|
+
...config.paths,
|
|
1784
|
+
};
|
|
1324
1785
|
super({ desc: 'Generate shell completion script' });
|
|
1325
1786
|
this.option({
|
|
1326
1787
|
long: 'bash',
|
|
@@ -1395,6 +1856,13 @@ class CompletionCommand extends Command {
|
|
|
1395
1856
|
});
|
|
1396
1857
|
}
|
|
1397
1858
|
}
|
|
1859
|
+
function createDefaultCompletionPaths(programName) {
|
|
1860
|
+
return {
|
|
1861
|
+
bash: `~/.local/share/bash-completion/completions/${programName}`,
|
|
1862
|
+
fish: `~/.config/fish/completions/${programName}.fish`,
|
|
1863
|
+
pwsh: '~/.config/powershell/Microsoft.PowerShell_profile.ps1',
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1398
1866
|
function expandHome(filepath) {
|
|
1399
1867
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
1400
1868
|
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
@@ -1672,4 +2140,6 @@ class PwshCompletion {
|
|
|
1672
2140
|
}
|
|
1673
2141
|
}
|
|
1674
2142
|
|
|
1675
|
-
|
|
2143
|
+
setDefaultCommandRuntime(createNodeCommandRuntime());
|
|
2144
|
+
|
|
2145
|
+
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, createBrowserCommandRuntime, createNodeCommandRuntime, getDefaultCommandRuntime, isDomain, isIp, isIpv4, isIpv6, logColorfulOption, logDateOption, logLevelOption, setDefaultCommandRuntime, silentOption };
|