@guanghechen/commander 4.7.1 → 4.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/cjs/browser.cjs +289 -41
- package/lib/cjs/index.cjs +1028 -176
- package/lib/cjs/node.cjs +597 -53
- package/lib/esm/browser.mjs +289 -41
- package/lib/esm/index.mjs +1021 -175
- package/lib/esm/node.mjs +597 -53
- package/lib/types/browser.d.ts +26 -5
- package/lib/types/index.d.ts +191 -15
- package/lib/types/node.d.ts +26 -5
- package/package.json +1 -1
package/lib/esm/index.mjs
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
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';
|
|
7
|
+
|
|
8
|
+
const TERMINAL_STYLE = {
|
|
9
|
+
bold: '\x1b[1m',
|
|
10
|
+
italic: '\x1b[3m',
|
|
11
|
+
underline: '\x1b[4m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
dim: '\x1b[2m',
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
};
|
|
16
|
+
function styleText(text, ...styles) {
|
|
17
|
+
return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
|
|
18
|
+
}
|
|
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
|
+
}
|
|
33
|
+
const logLevelOption = {
|
|
34
|
+
long: 'logLevel',
|
|
35
|
+
type: 'string',
|
|
36
|
+
args: 'required',
|
|
37
|
+
desc: 'Set log level',
|
|
38
|
+
default: 'info',
|
|
39
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
40
|
+
coerce: (raw) => {
|
|
41
|
+
const level = resolveReporterLogLevel(raw);
|
|
42
|
+
if (level === undefined) {
|
|
43
|
+
throw new Error(`Invalid log level: ${raw}`);
|
|
44
|
+
}
|
|
45
|
+
return level;
|
|
46
|
+
},
|
|
47
|
+
apply: (value, ctx) => {
|
|
48
|
+
setReporterLevel(ctx, value);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const logDateOption = {
|
|
52
|
+
long: 'logDate',
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
args: 'none',
|
|
55
|
+
desc: 'Enable log timestamp',
|
|
56
|
+
default: true,
|
|
57
|
+
apply: (value, ctx) => {
|
|
58
|
+
setReporterFlight(ctx, { date: Boolean(value) });
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const logColorfulOption = {
|
|
62
|
+
long: 'logColorful',
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
args: 'none',
|
|
65
|
+
desc: 'Enable colorful log output',
|
|
66
|
+
default: true,
|
|
67
|
+
apply: (value, ctx) => {
|
|
68
|
+
setReporterFlight(ctx, { color: Boolean(value) });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const silentOption = {
|
|
72
|
+
long: 'silent',
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
args: 'none',
|
|
75
|
+
desc: 'Suppress non-error output',
|
|
76
|
+
default: false,
|
|
77
|
+
apply: (value, ctx) => {
|
|
78
|
+
if (value) {
|
|
79
|
+
setReporterLevel(ctx, 'error');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
4
83
|
|
|
5
84
|
class CommanderError extends Error {
|
|
6
85
|
kind;
|
|
@@ -18,6 +97,11 @@ class CommanderError extends Error {
|
|
|
18
97
|
|
|
19
98
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
20
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';
|
|
21
105
|
function kebabToCamelCase(str) {
|
|
22
106
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
23
107
|
}
|
|
@@ -105,27 +189,91 @@ function tokenize(argv, commandPath) {
|
|
|
105
189
|
}
|
|
106
190
|
const BUILTIN_HELP_OPTION = {
|
|
107
191
|
long: 'help',
|
|
108
|
-
short: 'h',
|
|
109
192
|
type: 'boolean',
|
|
110
193
|
args: 'none',
|
|
111
194
|
desc: 'Show help information',
|
|
112
195
|
};
|
|
113
196
|
const BUILTIN_VERSION_OPTION = {
|
|
114
197
|
long: 'version',
|
|
115
|
-
short: 'V',
|
|
116
198
|
type: 'boolean',
|
|
117
199
|
args: 'none',
|
|
118
200
|
desc: 'Show version number',
|
|
119
201
|
};
|
|
202
|
+
const BUILTIN_COLOR_OPTION = {
|
|
203
|
+
long: 'color',
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
args: 'none',
|
|
206
|
+
desc: 'Enable colored help output',
|
|
207
|
+
default: true,
|
|
208
|
+
};
|
|
209
|
+
function createBuiltinOptionState(enabled) {
|
|
210
|
+
return {
|
|
211
|
+
version: enabled,
|
|
212
|
+
color: enabled,
|
|
213
|
+
logLevel: enabled,
|
|
214
|
+
silent: enabled,
|
|
215
|
+
logDate: enabled,
|
|
216
|
+
logColorful: enabled,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function isNoColorEnabled(envs) {
|
|
220
|
+
return envs['NO_COLOR'] !== undefined;
|
|
221
|
+
}
|
|
222
|
+
function normalizeBuiltinConfig(builtin) {
|
|
223
|
+
const resolved = {
|
|
224
|
+
option: createBuiltinOptionState(true),
|
|
225
|
+
};
|
|
226
|
+
if (builtin === undefined) {
|
|
227
|
+
return resolved;
|
|
228
|
+
}
|
|
229
|
+
if (builtin === true) {
|
|
230
|
+
return {
|
|
231
|
+
option: createBuiltinOptionState(true),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (builtin === false) {
|
|
235
|
+
return {
|
|
236
|
+
option: createBuiltinOptionState(false),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (builtin.option !== undefined) {
|
|
240
|
+
if (builtin.option === false) {
|
|
241
|
+
resolved.option = createBuiltinOptionState(false);
|
|
242
|
+
}
|
|
243
|
+
else if (builtin.option === true) {
|
|
244
|
+
resolved.option = createBuiltinOptionState(true);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
if (builtin.option.version !== undefined)
|
|
248
|
+
resolved.option.version = builtin.option.version;
|
|
249
|
+
if (builtin.option.color !== undefined)
|
|
250
|
+
resolved.option.color = builtin.option.color;
|
|
251
|
+
if (builtin.option.logLevel !== undefined) {
|
|
252
|
+
resolved.option.logLevel = builtin.option.logLevel;
|
|
253
|
+
}
|
|
254
|
+
if (builtin.option.silent !== undefined)
|
|
255
|
+
resolved.option.silent = builtin.option.silent;
|
|
256
|
+
if (builtin.option.logDate !== undefined)
|
|
257
|
+
resolved.option.logDate = builtin.option.logDate;
|
|
258
|
+
if (builtin.option.logColorful !== undefined) {
|
|
259
|
+
resolved.option.logColorful = builtin.option.logColorful;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
120
265
|
class Command {
|
|
121
266
|
#name;
|
|
122
267
|
#desc;
|
|
123
268
|
#version;
|
|
124
|
-
#
|
|
269
|
+
#builtinConfig;
|
|
270
|
+
#builtin;
|
|
271
|
+
#presetConfig;
|
|
125
272
|
#reporter;
|
|
126
273
|
#parent;
|
|
127
274
|
#options = [];
|
|
128
275
|
#arguments = [];
|
|
276
|
+
#examples = [];
|
|
129
277
|
#subcommandsList = [];
|
|
130
278
|
#subcommandsMap = new Map();
|
|
131
279
|
#action = undefined;
|
|
@@ -133,7 +281,9 @@ class Command {
|
|
|
133
281
|
this.#name = config.name ?? '';
|
|
134
282
|
this.#desc = config.desc;
|
|
135
283
|
this.#version = config.version;
|
|
136
|
-
this.#
|
|
284
|
+
this.#builtinConfig = config.builtin;
|
|
285
|
+
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
286
|
+
this.#presetConfig = config.preset;
|
|
137
287
|
this.#reporter = config.reporter;
|
|
138
288
|
}
|
|
139
289
|
get name() {
|
|
@@ -145,6 +295,12 @@ class Command {
|
|
|
145
295
|
get version() {
|
|
146
296
|
return this.#version;
|
|
147
297
|
}
|
|
298
|
+
get builtin() {
|
|
299
|
+
return this.#builtinConfig;
|
|
300
|
+
}
|
|
301
|
+
get preset() {
|
|
302
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
303
|
+
}
|
|
148
304
|
get parent() {
|
|
149
305
|
return this.#parent;
|
|
150
306
|
}
|
|
@@ -154,6 +310,9 @@ class Command {
|
|
|
154
310
|
get arguments() {
|
|
155
311
|
return [...this.#arguments];
|
|
156
312
|
}
|
|
313
|
+
get examples() {
|
|
314
|
+
return this.#examples.map(example => ({ ...example }));
|
|
315
|
+
}
|
|
157
316
|
get subcommands() {
|
|
158
317
|
return new Map(this.#subcommandsMap);
|
|
159
318
|
}
|
|
@@ -172,15 +331,22 @@ class Command {
|
|
|
172
331
|
this.#action = fn;
|
|
173
332
|
return this;
|
|
174
333
|
}
|
|
334
|
+
example(title, usage, desc) {
|
|
335
|
+
this.#examples.push(this.#normalizeExample({ title, usage, desc }));
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
175
338
|
subcommand(name, cmd) {
|
|
176
|
-
if (
|
|
177
|
-
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());
|
|
178
341
|
}
|
|
179
342
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
180
343
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
181
344
|
}
|
|
182
345
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
183
346
|
if (existing) {
|
|
347
|
+
if (existing.aliases.includes(name)) {
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
184
350
|
existing.aliases.push(name);
|
|
185
351
|
this.#subcommandsMap.set(name, cmd);
|
|
186
352
|
}
|
|
@@ -195,33 +361,36 @@ class Command {
|
|
|
195
361
|
async run(params) {
|
|
196
362
|
const { argv, envs, reporter } = params;
|
|
197
363
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const { chain, remaining } = routeResult;
|
|
364
|
+
const routeResult = this.#route(argv);
|
|
365
|
+
const { chain } = routeResult;
|
|
201
366
|
const leafCommand = chain[chain.length - 1];
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 }));
|
|
209
380
|
return;
|
|
210
381
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
382
|
+
if (ctx.controls.version) {
|
|
383
|
+
console.log(leafCommand.#version);
|
|
384
|
+
return;
|
|
216
385
|
}
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
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;
|
|
392
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
393
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
225
394
|
const actionParams = {
|
|
226
395
|
ctx: parseResult.ctx,
|
|
227
396
|
opts: parseResult.opts,
|
|
@@ -232,7 +401,8 @@ class Command {
|
|
|
232
401
|
await leafCommand.#runAction(actionParams);
|
|
233
402
|
}
|
|
234
403
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
235
|
-
|
|
404
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
405
|
+
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
236
406
|
}
|
|
237
407
|
else {
|
|
238
408
|
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
@@ -247,28 +417,49 @@ class Command {
|
|
|
247
417
|
throw err;
|
|
248
418
|
}
|
|
249
419
|
}
|
|
250
|
-
parse(params) {
|
|
420
|
+
async parse(params) {
|
|
251
421
|
const { argv, envs, reporter } = params;
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const { chain, remaining } = routeResult;
|
|
422
|
+
const routeResult = this.#route(argv);
|
|
423
|
+
const { chain } = routeResult;
|
|
255
424
|
const leafCommand = chain[chain.length - 1];
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const ctx = {
|
|
260
|
-
cmd: leafCommand,
|
|
425
|
+
const ctx = this.#createContext({
|
|
426
|
+
chain,
|
|
427
|
+
cmds: routeResult.cmds,
|
|
261
428
|
envs,
|
|
262
|
-
reporter
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
429
|
+
reporter,
|
|
430
|
+
});
|
|
431
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
432
|
+
ctx.controls = controlScanResult.controls;
|
|
433
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
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;
|
|
440
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
441
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
266
442
|
}
|
|
267
443
|
formatHelp() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
444
|
+
return this.#renderHelpPlain(this.#buildHelpData());
|
|
445
|
+
}
|
|
446
|
+
#formatHelpForDisplay(params = {}) {
|
|
447
|
+
const { color = true } = params;
|
|
448
|
+
const helpData = this.#buildHelpData();
|
|
449
|
+
if (!this.#shouldRenderStyledHelp(color)) {
|
|
450
|
+
return this.#renderHelpPlain(helpData);
|
|
451
|
+
}
|
|
452
|
+
return this.#renderHelpTerminal(helpData);
|
|
453
|
+
}
|
|
454
|
+
#shouldRenderStyledHelp(color) {
|
|
455
|
+
return color && process.stdout.isTTY === true;
|
|
456
|
+
}
|
|
457
|
+
#buildHelpData() {
|
|
458
|
+
const parseOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
459
|
+
const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
|
|
460
|
+
if (this.#supportsBuiltinVersion()) {
|
|
461
|
+
allOptions.push(BUILTIN_VERSION_OPTION);
|
|
462
|
+
}
|
|
272
463
|
const commandPath = this.#getCommandPath();
|
|
273
464
|
let usage = `Usage: ${commandPath}`;
|
|
274
465
|
if (allOptions.length > 0)
|
|
@@ -286,65 +477,128 @@ class Command {
|
|
|
286
477
|
usage += ` [${arg.name}...]`;
|
|
287
478
|
}
|
|
288
479
|
}
|
|
289
|
-
|
|
480
|
+
const options = [];
|
|
481
|
+
for (const opt of allOptions) {
|
|
482
|
+
const kebabLong = camelToKebabCase$1(opt.long);
|
|
483
|
+
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
484
|
+
sig += `--${kebabLong}`;
|
|
485
|
+
if (opt.args !== 'none') {
|
|
486
|
+
sig += ' <value>';
|
|
487
|
+
}
|
|
488
|
+
let desc = opt.desc;
|
|
489
|
+
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
490
|
+
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
491
|
+
}
|
|
492
|
+
if (opt.choices) {
|
|
493
|
+
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
494
|
+
}
|
|
495
|
+
options.push({ sig, desc });
|
|
496
|
+
if (opt.type === 'boolean' &&
|
|
497
|
+
opt.args === 'none' &&
|
|
498
|
+
opt.long !== 'help' &&
|
|
499
|
+
opt.long !== 'version') {
|
|
500
|
+
options.push({
|
|
501
|
+
sig: ` --no-${kebabLong}`,
|
|
502
|
+
desc: `Negate --${kebabLong}`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const commands = [];
|
|
507
|
+
if (this.#subcommandsList.length > 0) {
|
|
508
|
+
commands.push({ name: 'help', desc: 'Show help for a command' });
|
|
509
|
+
}
|
|
510
|
+
for (const entry of this.#subcommandsList) {
|
|
511
|
+
let name = entry.name;
|
|
512
|
+
if (entry.aliases.length > 0) {
|
|
513
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
514
|
+
}
|
|
515
|
+
commands.push({ name, desc: entry.command.#desc });
|
|
516
|
+
}
|
|
517
|
+
const examples = this.#examples.map(example => ({
|
|
518
|
+
title: example.title,
|
|
519
|
+
usage: commandPath ? `${commandPath} ${example.usage}` : example.usage,
|
|
520
|
+
desc: example.desc,
|
|
521
|
+
}));
|
|
522
|
+
return {
|
|
523
|
+
desc: this.#desc,
|
|
524
|
+
usage,
|
|
525
|
+
options,
|
|
526
|
+
commands,
|
|
527
|
+
examples,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
#renderHelpPlain(helpData) {
|
|
531
|
+
const lines = [];
|
|
532
|
+
lines.push(helpData.desc);
|
|
533
|
+
lines.push('');
|
|
534
|
+
lines.push(helpData.usage);
|
|
290
535
|
lines.push('');
|
|
291
|
-
if (
|
|
536
|
+
if (helpData.options.length > 0) {
|
|
292
537
|
lines.push('Options:');
|
|
293
|
-
const
|
|
294
|
-
for (const
|
|
295
|
-
const kebabLong = camelToKebabCase$1(opt.long);
|
|
296
|
-
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
297
|
-
sig += `--${kebabLong}`;
|
|
298
|
-
if (opt.args !== 'none') {
|
|
299
|
-
sig += ' <value>';
|
|
300
|
-
}
|
|
301
|
-
let desc = opt.desc;
|
|
302
|
-
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
303
|
-
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
304
|
-
}
|
|
305
|
-
if (opt.choices) {
|
|
306
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
307
|
-
}
|
|
308
|
-
optLines.push({ sig, desc });
|
|
309
|
-
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
310
|
-
optLines.push({
|
|
311
|
-
sig: ` --no-${kebabLong}`,
|
|
312
|
-
desc: `Negate --${kebabLong}`,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
|
|
317
|
-
for (const { sig, desc } of optLines) {
|
|
538
|
+
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
539
|
+
for (const { sig, desc } of helpData.options) {
|
|
318
540
|
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
319
541
|
lines.push(` ${sig}${padding}${desc}`);
|
|
320
542
|
}
|
|
321
543
|
lines.push('');
|
|
322
544
|
}
|
|
323
|
-
|
|
324
|
-
if (this.#subcommandsList.length > 0) {
|
|
545
|
+
if (helpData.commands.length > 0) {
|
|
325
546
|
lines.push('Commands:');
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
329
|
-
}
|
|
330
|
-
for (const entry of this.#subcommandsList) {
|
|
331
|
-
let name = entry.name;
|
|
332
|
-
if (entry.aliases.length > 0) {
|
|
333
|
-
name += `, ${entry.aliases.join(', ')}`;
|
|
334
|
-
}
|
|
335
|
-
cmdLines.push({ name, desc: entry.command.#desc });
|
|
336
|
-
}
|
|
337
|
-
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
338
|
-
for (const { name, desc } of cmdLines) {
|
|
547
|
+
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
548
|
+
for (const { name, desc } of helpData.commands) {
|
|
339
549
|
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
340
550
|
lines.push(` ${name}${padding}${desc}`);
|
|
341
551
|
}
|
|
342
552
|
lines.push('');
|
|
343
553
|
}
|
|
554
|
+
if (helpData.examples.length > 0) {
|
|
555
|
+
lines.push('Examples:');
|
|
556
|
+
for (const example of helpData.examples) {
|
|
557
|
+
lines.push(` - ${example.title}`);
|
|
558
|
+
lines.push(` ${example.usage}`);
|
|
559
|
+
lines.push(` ${example.desc}`);
|
|
560
|
+
lines.push('');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return lines.join('\n');
|
|
564
|
+
}
|
|
565
|
+
#renderHelpTerminal(helpData) {
|
|
566
|
+
const lines = [];
|
|
567
|
+
lines.push(helpData.desc);
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
|
|
570
|
+
lines.push('');
|
|
571
|
+
if (helpData.options.length > 0) {
|
|
572
|
+
lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
573
|
+
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
574
|
+
for (const { sig, desc } of helpData.options) {
|
|
575
|
+
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
576
|
+
lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
577
|
+
}
|
|
578
|
+
lines.push('');
|
|
579
|
+
}
|
|
580
|
+
if (helpData.commands.length > 0) {
|
|
581
|
+
lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
582
|
+
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
583
|
+
for (const { name, desc } of helpData.commands) {
|
|
584
|
+
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
585
|
+
lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
586
|
+
}
|
|
587
|
+
lines.push('');
|
|
588
|
+
}
|
|
589
|
+
if (helpData.examples.length > 0) {
|
|
590
|
+
lines.push(styleText('Examples:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
591
|
+
for (const example of helpData.examples) {
|
|
592
|
+
lines.push(` - ${styleText(example.title, TERMINAL_STYLE.bold)}`);
|
|
593
|
+
lines.push(` ${styleText(example.usage, TERMINAL_STYLE.cyan)}`);
|
|
594
|
+
lines.push(` ${styleText(example.desc, TERMINAL_STYLE.italic, TERMINAL_STYLE.dim)}`);
|
|
595
|
+
lines.push('');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
344
598
|
return lines.join('\n');
|
|
345
599
|
}
|
|
346
600
|
getCompletionMeta() {
|
|
347
|
-
const allOptions = this.#
|
|
601
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
348
602
|
const options = [];
|
|
349
603
|
for (const opt of allOptions) {
|
|
350
604
|
options.push({
|
|
@@ -370,46 +624,407 @@ class Command {
|
|
|
370
624
|
}),
|
|
371
625
|
};
|
|
372
626
|
}
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
return argv;
|
|
376
|
-
if (argv.length < 1 || argv[0] !== 'help')
|
|
377
|
-
return argv;
|
|
378
|
-
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
379
|
-
return ['--help'];
|
|
380
|
-
}
|
|
381
|
-
const subName = argv[1];
|
|
382
|
-
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
383
|
-
if (entry) {
|
|
384
|
-
return [subName, '--help', ...argv.slice(2)];
|
|
385
|
-
}
|
|
386
|
-
return argv;
|
|
627
|
+
#findSubcommandEntry(token) {
|
|
628
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
387
629
|
}
|
|
388
630
|
#route(argv) {
|
|
389
631
|
const chain = [this];
|
|
632
|
+
const cmds = [];
|
|
390
633
|
let current = this;
|
|
391
634
|
let idx = 0;
|
|
392
635
|
while (idx < argv.length) {
|
|
393
636
|
const token = argv[idx];
|
|
394
637
|
if (token.startsWith('-'))
|
|
395
638
|
break;
|
|
396
|
-
const entry = current.#
|
|
639
|
+
const entry = current.#findSubcommandEntry(token);
|
|
397
640
|
if (!entry)
|
|
398
641
|
break;
|
|
399
642
|
current = entry.command;
|
|
643
|
+
cmds.push(token);
|
|
400
644
|
chain.push(current);
|
|
401
645
|
idx += 1;
|
|
402
646
|
}
|
|
403
|
-
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;
|
|
404
794
|
}
|
|
405
|
-
#
|
|
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
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
406
1021
|
const consumedTokens = new Map();
|
|
407
1022
|
let remaining = [...tokens];
|
|
408
1023
|
const shadowed = new Set();
|
|
409
1024
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
410
1025
|
const cmd = chain[i];
|
|
411
|
-
const
|
|
412
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
1026
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1027
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
413
1028
|
consumedTokens.set(cmd, result.consumed);
|
|
414
1029
|
remaining = result.remaining;
|
|
415
1030
|
for (const opt of cmd.#options) {
|
|
@@ -426,8 +1041,7 @@ class Command {
|
|
|
426
1041
|
}
|
|
427
1042
|
return { consumedTokens, argTokens };
|
|
428
1043
|
}
|
|
429
|
-
#shift(tokens, shadowed,
|
|
430
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
1044
|
+
#shift(tokens, shadowed, allOptions) {
|
|
431
1045
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
432
1046
|
const optionByLong = new Map();
|
|
433
1047
|
const optionByShort = new Map();
|
|
@@ -495,34 +1109,40 @@ class Command {
|
|
|
495
1109
|
}
|
|
496
1110
|
return { consumed, remaining };
|
|
497
1111
|
}
|
|
498
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
1112
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
499
1113
|
const { consumedTokens, argTokens } = resolveResult;
|
|
500
1114
|
const leafCommand = chain[chain.length - 1];
|
|
501
|
-
this.#validateMergedShortOptions(chain);
|
|
1115
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
502
1116
|
const optsMap = new Map();
|
|
503
|
-
for (
|
|
504
|
-
const
|
|
505
|
-
const includeVersion = i === 0;
|
|
1117
|
+
for (const cmd of chain) {
|
|
1118
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
506
1119
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
507
|
-
const opts = cmd.#parseOptions(tokens,
|
|
1120
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
508
1121
|
optsMap.set(cmd, opts);
|
|
509
|
-
for (const opt of
|
|
1122
|
+
for (const opt of policy.mergedOptions) {
|
|
510
1123
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
511
1124
|
opt.apply(opts[opt.long], ctx);
|
|
512
1125
|
}
|
|
513
1126
|
}
|
|
514
1127
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
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
|
+
}
|
|
518
1134
|
}
|
|
519
1135
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
520
1136
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
521
|
-
|
|
1137
|
+
const parseCtx = {
|
|
1138
|
+
...ctx,
|
|
1139
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1140
|
+
};
|
|
1141
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
522
1142
|
}
|
|
523
|
-
#parseOptions(tokens,
|
|
524
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
1143
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
525
1144
|
const opts = {};
|
|
1145
|
+
let sawColorToken = false;
|
|
526
1146
|
for (const opt of allOptions) {
|
|
527
1147
|
if (opt.default !== undefined) {
|
|
528
1148
|
opts[opt.long] = opt.default;
|
|
@@ -550,6 +1170,9 @@ class Command {
|
|
|
550
1170
|
i += 1;
|
|
551
1171
|
continue;
|
|
552
1172
|
}
|
|
1173
|
+
if (opt.long === 'color') {
|
|
1174
|
+
sawColorToken = true;
|
|
1175
|
+
}
|
|
553
1176
|
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
554
1177
|
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
555
1178
|
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
@@ -626,6 +1249,9 @@ class Command {
|
|
|
626
1249
|
}
|
|
627
1250
|
}
|
|
628
1251
|
}
|
|
1252
|
+
if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
|
|
1253
|
+
opts['color'] = false;
|
|
1254
|
+
}
|
|
629
1255
|
return opts;
|
|
630
1256
|
}
|
|
631
1257
|
#convertValue(opt, rawValue) {
|
|
@@ -696,27 +1322,60 @@ class Command {
|
|
|
696
1322
|
}
|
|
697
1323
|
return raw;
|
|
698
1324
|
}
|
|
699
|
-
#
|
|
1325
|
+
#hasUserOption(long) {
|
|
1326
|
+
return this.#options.some(option => option.long === long);
|
|
1327
|
+
}
|
|
1328
|
+
#supportsBuiltinVersion() {
|
|
1329
|
+
return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
|
|
1330
|
+
}
|
|
1331
|
+
#resolveOptionPolicy() {
|
|
700
1332
|
const optionMap = new Map();
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
1333
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
1334
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
1335
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
1336
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
1337
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
1338
|
+
if (this.#builtin.option.color && !hasUserColor) {
|
|
1339
|
+
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
1340
|
+
}
|
|
1341
|
+
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
1342
|
+
optionMap.set('logLevel', logLevelOption);
|
|
705
1343
|
}
|
|
706
|
-
if (
|
|
707
|
-
optionMap.set('
|
|
1344
|
+
if (this.#builtin.option.silent && !hasUserSilent) {
|
|
1345
|
+
optionMap.set('silent', silentOption);
|
|
1346
|
+
}
|
|
1347
|
+
if (this.#builtin.option.logDate && !hasUserLogDate) {
|
|
1348
|
+
optionMap.set('logDate', logDateOption);
|
|
1349
|
+
}
|
|
1350
|
+
if (this.#builtin.option.logColorful && !hasUserLogColorful) {
|
|
1351
|
+
optionMap.set('logColorful', logColorfulOption);
|
|
708
1352
|
}
|
|
709
1353
|
for (const opt of this.#options) {
|
|
710
1354
|
optionMap.set(opt.long, opt);
|
|
711
1355
|
}
|
|
712
|
-
return
|
|
1356
|
+
return {
|
|
1357
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
#buildOptionPolicyMap(chain) {
|
|
1361
|
+
const optionPolicyMap = new Map();
|
|
1362
|
+
for (const cmd of chain) {
|
|
1363
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1364
|
+
}
|
|
1365
|
+
return optionPolicyMap;
|
|
1366
|
+
}
|
|
1367
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1368
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1369
|
+
if (policy !== undefined) {
|
|
1370
|
+
return policy;
|
|
1371
|
+
}
|
|
1372
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
713
1373
|
}
|
|
714
|
-
#validateMergedShortOptions(chain) {
|
|
1374
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
715
1375
|
const mergedByLong = new Map();
|
|
716
|
-
for (
|
|
717
|
-
const
|
|
718
|
-
const
|
|
719
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1376
|
+
for (const cmd of chain) {
|
|
1377
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1378
|
+
for (const opt of policy.mergedOptions) {
|
|
720
1379
|
mergedByLong.set(opt.long, opt);
|
|
721
1380
|
}
|
|
722
1381
|
}
|
|
@@ -732,6 +1391,9 @@ class Command {
|
|
|
732
1391
|
}
|
|
733
1392
|
}
|
|
734
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
|
+
}
|
|
735
1397
|
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
736
1398
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
737
1399
|
}
|
|
@@ -781,6 +1443,21 @@ class Command {
|
|
|
781
1443
|
}
|
|
782
1444
|
}
|
|
783
1445
|
}
|
|
1446
|
+
#normalizeExample(example) {
|
|
1447
|
+
const title = example.title.trim();
|
|
1448
|
+
const usage = example.usage.trim();
|
|
1449
|
+
const desc = example.desc.trim();
|
|
1450
|
+
if (!title) {
|
|
1451
|
+
throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
|
|
1452
|
+
}
|
|
1453
|
+
if (!usage) {
|
|
1454
|
+
throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
|
|
1455
|
+
}
|
|
1456
|
+
if (!desc) {
|
|
1457
|
+
throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
|
|
1458
|
+
}
|
|
1459
|
+
return { title, usage, desc };
|
|
1460
|
+
}
|
|
784
1461
|
async #runAction(params) {
|
|
785
1462
|
if (!this.#action)
|
|
786
1463
|
return;
|
|
@@ -797,16 +1474,51 @@ class Command {
|
|
|
797
1474
|
process.exit(1);
|
|
798
1475
|
}
|
|
799
1476
|
}
|
|
800
|
-
#
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1477
|
+
#resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
|
|
1478
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1479
|
+
let color = !isNoColorEnabled(envs);
|
|
1480
|
+
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1481
|
+
return color;
|
|
1482
|
+
}
|
|
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;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (token === '--no-color') {
|
|
1491
|
+
color = false;
|
|
1492
|
+
continue;
|
|
804
1493
|
}
|
|
805
|
-
if (token.
|
|
806
|
-
|
|
1494
|
+
if (!token.startsWith('--color=')) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const value = token.slice('--color='.length);
|
|
1498
|
+
if (value === 'true') {
|
|
1499
|
+
color = true;
|
|
1500
|
+
}
|
|
1501
|
+
else if (value === 'false') {
|
|
1502
|
+
color = false;
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
|
|
807
1506
|
}
|
|
808
1507
|
}
|
|
809
|
-
return
|
|
1508
|
+
return color;
|
|
1509
|
+
}
|
|
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
|
+
});
|
|
810
1522
|
}
|
|
811
1523
|
#getCommandPath() {
|
|
812
1524
|
const parts = [];
|
|
@@ -821,13 +1533,166 @@ class Command {
|
|
|
821
1533
|
}
|
|
822
1534
|
}
|
|
823
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
|
+
|
|
1626
|
+
class Coerce {
|
|
1627
|
+
constructor() { }
|
|
1628
|
+
static create(name, expectedType, validator, errorMessage) {
|
|
1629
|
+
return (rawValue) => {
|
|
1630
|
+
const value = Number(rawValue);
|
|
1631
|
+
if (!validator(value)) {
|
|
1632
|
+
throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
|
|
1633
|
+
}
|
|
1634
|
+
return value;
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
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
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
static integer(name, errorMessage) {
|
|
1662
|
+
return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
|
|
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
|
+
}
|
|
1678
|
+
static positiveInteger(name, errorMessage) {
|
|
1679
|
+
return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
|
|
1680
|
+
}
|
|
1681
|
+
static positiveNumber(name, errorMessage) {
|
|
1682
|
+
return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
824
1686
|
function camelToKebabCase(str) {
|
|
825
1687
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
826
1688
|
}
|
|
827
1689
|
class CompletionCommand extends Command {
|
|
828
|
-
constructor(root, config) {
|
|
829
|
-
const paths = config.paths;
|
|
1690
|
+
constructor(root, config = {}) {
|
|
830
1691
|
const programName = config.programName ?? root.name ?? 'program';
|
|
1692
|
+
const paths = {
|
|
1693
|
+
...createDefaultCompletionPaths(programName),
|
|
1694
|
+
...config.paths,
|
|
1695
|
+
};
|
|
831
1696
|
super({ desc: 'Generate shell completion script' });
|
|
832
1697
|
this.option({
|
|
833
1698
|
long: 'bash',
|
|
@@ -902,6 +1767,13 @@ class CompletionCommand extends Command {
|
|
|
902
1767
|
});
|
|
903
1768
|
}
|
|
904
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
|
+
}
|
|
905
1777
|
function expandHome(filepath) {
|
|
906
1778
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
907
1779
|
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
@@ -1179,30 +2051,4 @@ class PwshCompletion {
|
|
|
1179
2051
|
}
|
|
1180
2052
|
}
|
|
1181
2053
|
|
|
1182
|
-
|
|
1183
|
-
long: 'logLevel',
|
|
1184
|
-
type: 'string',
|
|
1185
|
-
args: 'required',
|
|
1186
|
-
desc: 'Set log level',
|
|
1187
|
-
default: 'info',
|
|
1188
|
-
choices: LOG_LEVELS,
|
|
1189
|
-
coerce: (raw) => {
|
|
1190
|
-
const level = resolveLogLevel(raw);
|
|
1191
|
-
if (level === undefined) {
|
|
1192
|
-
throw new Error(`Invalid log level: ${raw}`);
|
|
1193
|
-
}
|
|
1194
|
-
return level;
|
|
1195
|
-
},
|
|
1196
|
-
apply: (value, ctx) => {
|
|
1197
|
-
ctx.reporter.setLevel(value);
|
|
1198
|
-
},
|
|
1199
|
-
};
|
|
1200
|
-
const silentOption = {
|
|
1201
|
-
long: 'silent',
|
|
1202
|
-
type: 'boolean',
|
|
1203
|
-
args: 'none',
|
|
1204
|
-
desc: 'Suppress non-error output',
|
|
1205
|
-
default: false,
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logLevelOption, silentOption };
|
|
2054
|
+
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, isDomain, isIp, isIpv4, isIpv6, logColorfulOption, logDateOption, logLevelOption, silentOption };
|