@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/cjs/index.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var env = require('@guanghechen/env');
|
|
3
4
|
var reporter = require('@guanghechen/reporter');
|
|
4
|
-
var
|
|
5
|
+
var promises = require('node:fs/promises');
|
|
5
6
|
var path = require('node:path');
|
|
7
|
+
var fs = require('node:fs');
|
|
6
8
|
|
|
7
9
|
function _interopNamespaceDefault(e) {
|
|
8
10
|
var n = Object.create(null);
|
|
@@ -21,8 +23,84 @@ function _interopNamespaceDefault(e) {
|
|
|
21
23
|
return Object.freeze(n);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
25
26
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
27
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
28
|
+
|
|
29
|
+
const TERMINAL_STYLE = {
|
|
30
|
+
bold: '\x1b[1m',
|
|
31
|
+
italic: '\x1b[3m',
|
|
32
|
+
underline: '\x1b[4m',
|
|
33
|
+
cyan: '\x1b[36m',
|
|
34
|
+
dim: '\x1b[2m',
|
|
35
|
+
reset: '\x1b[0m',
|
|
36
|
+
};
|
|
37
|
+
function styleText(text, ...styles) {
|
|
38
|
+
return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BUILTIN_LOG_LEVELS = ['debug', 'info', 'hint', 'warn', 'error'];
|
|
42
|
+
function resolveReporterLogLevel(raw) {
|
|
43
|
+
const normalized = raw.trim().toLowerCase();
|
|
44
|
+
return BUILTIN_LOG_LEVELS.find(level => level === normalized);
|
|
45
|
+
}
|
|
46
|
+
function setReporterLevel(ctx, level) {
|
|
47
|
+
const reporter = ctx.reporter;
|
|
48
|
+
reporter?.setLevel?.(level);
|
|
49
|
+
}
|
|
50
|
+
function setReporterFlight(ctx, flight) {
|
|
51
|
+
const reporter = ctx.reporter;
|
|
52
|
+
reporter?.setFlight?.(flight);
|
|
53
|
+
}
|
|
54
|
+
const logLevelOption = {
|
|
55
|
+
long: 'logLevel',
|
|
56
|
+
type: 'string',
|
|
57
|
+
args: 'required',
|
|
58
|
+
desc: 'Set log level',
|
|
59
|
+
default: 'info',
|
|
60
|
+
choices: [...BUILTIN_LOG_LEVELS],
|
|
61
|
+
coerce: (raw) => {
|
|
62
|
+
const level = resolveReporterLogLevel(raw);
|
|
63
|
+
if (level === undefined) {
|
|
64
|
+
throw new Error(`Invalid log level: ${raw}`);
|
|
65
|
+
}
|
|
66
|
+
return level;
|
|
67
|
+
},
|
|
68
|
+
apply: (value, ctx) => {
|
|
69
|
+
setReporterLevel(ctx, value);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const logDateOption = {
|
|
73
|
+
long: 'logDate',
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
args: 'none',
|
|
76
|
+
desc: 'Enable log timestamp',
|
|
77
|
+
default: true,
|
|
78
|
+
apply: (value, ctx) => {
|
|
79
|
+
setReporterFlight(ctx, { date: Boolean(value) });
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const logColorfulOption = {
|
|
83
|
+
long: 'logColorful',
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
args: 'none',
|
|
86
|
+
desc: 'Enable colorful log output',
|
|
87
|
+
default: true,
|
|
88
|
+
apply: (value, ctx) => {
|
|
89
|
+
setReporterFlight(ctx, { color: Boolean(value) });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const silentOption = {
|
|
93
|
+
long: 'silent',
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
args: 'none',
|
|
96
|
+
desc: 'Suppress non-error output',
|
|
97
|
+
default: false,
|
|
98
|
+
apply: (value, ctx) => {
|
|
99
|
+
if (value) {
|
|
100
|
+
setReporterLevel(ctx, 'error');
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
26
104
|
|
|
27
105
|
class CommanderError extends Error {
|
|
28
106
|
kind;
|
|
@@ -40,6 +118,11 @@ class CommanderError extends Error {
|
|
|
40
118
|
|
|
41
119
|
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
42
120
|
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
121
|
+
const PRESET_OPTS_FLAG = '--preset-opts';
|
|
122
|
+
const PRESET_ENVS_FLAG = '--preset-envs';
|
|
123
|
+
const PRESET_ROOT_FLAG = '--preset-root';
|
|
124
|
+
const DEFAULT_PRESET_OPTS_FILENAME = '.opt.local';
|
|
125
|
+
const DEFAULT_PRESET_ENVS_FILENAME = '.env.local';
|
|
43
126
|
function kebabToCamelCase(str) {
|
|
44
127
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
45
128
|
}
|
|
@@ -127,27 +210,91 @@ function tokenize(argv, commandPath) {
|
|
|
127
210
|
}
|
|
128
211
|
const BUILTIN_HELP_OPTION = {
|
|
129
212
|
long: 'help',
|
|
130
|
-
short: 'h',
|
|
131
213
|
type: 'boolean',
|
|
132
214
|
args: 'none',
|
|
133
215
|
desc: 'Show help information',
|
|
134
216
|
};
|
|
135
217
|
const BUILTIN_VERSION_OPTION = {
|
|
136
218
|
long: 'version',
|
|
137
|
-
short: 'V',
|
|
138
219
|
type: 'boolean',
|
|
139
220
|
args: 'none',
|
|
140
221
|
desc: 'Show version number',
|
|
141
222
|
};
|
|
223
|
+
const BUILTIN_COLOR_OPTION = {
|
|
224
|
+
long: 'color',
|
|
225
|
+
type: 'boolean',
|
|
226
|
+
args: 'none',
|
|
227
|
+
desc: 'Enable colored help output',
|
|
228
|
+
default: true,
|
|
229
|
+
};
|
|
230
|
+
function createBuiltinOptionState(enabled) {
|
|
231
|
+
return {
|
|
232
|
+
version: enabled,
|
|
233
|
+
color: enabled,
|
|
234
|
+
logLevel: enabled,
|
|
235
|
+
silent: enabled,
|
|
236
|
+
logDate: enabled,
|
|
237
|
+
logColorful: enabled,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function isNoColorEnabled(envs) {
|
|
241
|
+
return envs['NO_COLOR'] !== undefined;
|
|
242
|
+
}
|
|
243
|
+
function normalizeBuiltinConfig(builtin) {
|
|
244
|
+
const resolved = {
|
|
245
|
+
option: createBuiltinOptionState(true),
|
|
246
|
+
};
|
|
247
|
+
if (builtin === undefined) {
|
|
248
|
+
return resolved;
|
|
249
|
+
}
|
|
250
|
+
if (builtin === true) {
|
|
251
|
+
return {
|
|
252
|
+
option: createBuiltinOptionState(true),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (builtin === false) {
|
|
256
|
+
return {
|
|
257
|
+
option: createBuiltinOptionState(false),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (builtin.option !== undefined) {
|
|
261
|
+
if (builtin.option === false) {
|
|
262
|
+
resolved.option = createBuiltinOptionState(false);
|
|
263
|
+
}
|
|
264
|
+
else if (builtin.option === true) {
|
|
265
|
+
resolved.option = createBuiltinOptionState(true);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
if (builtin.option.version !== undefined)
|
|
269
|
+
resolved.option.version = builtin.option.version;
|
|
270
|
+
if (builtin.option.color !== undefined)
|
|
271
|
+
resolved.option.color = builtin.option.color;
|
|
272
|
+
if (builtin.option.logLevel !== undefined) {
|
|
273
|
+
resolved.option.logLevel = builtin.option.logLevel;
|
|
274
|
+
}
|
|
275
|
+
if (builtin.option.silent !== undefined)
|
|
276
|
+
resolved.option.silent = builtin.option.silent;
|
|
277
|
+
if (builtin.option.logDate !== undefined)
|
|
278
|
+
resolved.option.logDate = builtin.option.logDate;
|
|
279
|
+
if (builtin.option.logColorful !== undefined) {
|
|
280
|
+
resolved.option.logColorful = builtin.option.logColorful;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return resolved;
|
|
285
|
+
}
|
|
142
286
|
class Command {
|
|
143
287
|
#name;
|
|
144
288
|
#desc;
|
|
145
289
|
#version;
|
|
146
|
-
#
|
|
290
|
+
#builtinConfig;
|
|
291
|
+
#builtin;
|
|
292
|
+
#presetConfig;
|
|
147
293
|
#reporter;
|
|
148
294
|
#parent;
|
|
149
295
|
#options = [];
|
|
150
296
|
#arguments = [];
|
|
297
|
+
#examples = [];
|
|
151
298
|
#subcommandsList = [];
|
|
152
299
|
#subcommandsMap = new Map();
|
|
153
300
|
#action = undefined;
|
|
@@ -155,7 +302,9 @@ class Command {
|
|
|
155
302
|
this.#name = config.name ?? '';
|
|
156
303
|
this.#desc = config.desc;
|
|
157
304
|
this.#version = config.version;
|
|
158
|
-
this.#
|
|
305
|
+
this.#builtinConfig = config.builtin;
|
|
306
|
+
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
307
|
+
this.#presetConfig = config.preset;
|
|
159
308
|
this.#reporter = config.reporter;
|
|
160
309
|
}
|
|
161
310
|
get name() {
|
|
@@ -167,6 +316,12 @@ class Command {
|
|
|
167
316
|
get version() {
|
|
168
317
|
return this.#version;
|
|
169
318
|
}
|
|
319
|
+
get builtin() {
|
|
320
|
+
return this.#builtinConfig;
|
|
321
|
+
}
|
|
322
|
+
get preset() {
|
|
323
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
324
|
+
}
|
|
170
325
|
get parent() {
|
|
171
326
|
return this.#parent;
|
|
172
327
|
}
|
|
@@ -176,6 +331,9 @@ class Command {
|
|
|
176
331
|
get arguments() {
|
|
177
332
|
return [...this.#arguments];
|
|
178
333
|
}
|
|
334
|
+
get examples() {
|
|
335
|
+
return this.#examples.map(example => ({ ...example }));
|
|
336
|
+
}
|
|
179
337
|
get subcommands() {
|
|
180
338
|
return new Map(this.#subcommandsMap);
|
|
181
339
|
}
|
|
@@ -194,15 +352,22 @@ class Command {
|
|
|
194
352
|
this.#action = fn;
|
|
195
353
|
return this;
|
|
196
354
|
}
|
|
355
|
+
example(title, usage, desc) {
|
|
356
|
+
this.#examples.push(this.#normalizeExample({ title, usage, desc }));
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
197
359
|
subcommand(name, cmd) {
|
|
198
|
-
if (
|
|
199
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name
|
|
360
|
+
if (name === 'help') {
|
|
361
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
|
|
200
362
|
}
|
|
201
363
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
202
364
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
203
365
|
}
|
|
204
366
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
205
367
|
if (existing) {
|
|
368
|
+
if (existing.aliases.includes(name)) {
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
206
371
|
existing.aliases.push(name);
|
|
207
372
|
this.#subcommandsMap.set(name, cmd);
|
|
208
373
|
}
|
|
@@ -215,35 +380,38 @@ class Command {
|
|
|
215
380
|
return this;
|
|
216
381
|
}
|
|
217
382
|
async run(params) {
|
|
218
|
-
const { argv, envs, reporter
|
|
383
|
+
const { argv, envs, reporter } = params;
|
|
219
384
|
try {
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const { chain, remaining } = routeResult;
|
|
385
|
+
const routeResult = this.#route(argv);
|
|
386
|
+
const { chain } = routeResult;
|
|
223
387
|
const leafCommand = chain[chain.length - 1];
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
388
|
+
const ctx = this.#createContext({
|
|
389
|
+
chain,
|
|
390
|
+
cmds: routeResult.cmds,
|
|
391
|
+
envs,
|
|
392
|
+
reporter,
|
|
393
|
+
});
|
|
394
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
395
|
+
ctx.controls = controlScanResult.controls;
|
|
396
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
397
|
+
if (ctx.controls.help) {
|
|
398
|
+
const helpCommand = this.#resolveHelpCommand(leafCommand, controlScanResult.helpTarget);
|
|
399
|
+
const helpColor = helpCommand.#resolveHelpColorFromTailArgv(controlScanResult.remaining, ctx.envs);
|
|
400
|
+
console.log(helpCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
231
401
|
return;
|
|
232
402
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
403
|
+
if (ctx.controls.version) {
|
|
404
|
+
console.log(leafCommand.#version);
|
|
405
|
+
return;
|
|
238
406
|
}
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
407
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
408
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
409
|
+
ctx.sources = presetResult.sources;
|
|
410
|
+
ctx.envs = presetResult.envs;
|
|
411
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
412
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
413
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
414
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
247
415
|
const actionParams = {
|
|
248
416
|
ctx: parseResult.ctx,
|
|
249
417
|
opts: parseResult.opts,
|
|
@@ -254,7 +422,8 @@ class Command {
|
|
|
254
422
|
await leafCommand.#runAction(actionParams);
|
|
255
423
|
}
|
|
256
424
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
257
|
-
|
|
425
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
426
|
+
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
258
427
|
}
|
|
259
428
|
else {
|
|
260
429
|
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
@@ -269,28 +438,49 @@ class Command {
|
|
|
269
438
|
throw err;
|
|
270
439
|
}
|
|
271
440
|
}
|
|
272
|
-
parse(params) {
|
|
273
|
-
const { argv, envs, reporter
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const { chain, remaining } = routeResult;
|
|
441
|
+
async parse(params) {
|
|
442
|
+
const { argv, envs, reporter } = params;
|
|
443
|
+
const routeResult = this.#route(argv);
|
|
444
|
+
const { chain } = routeResult;
|
|
277
445
|
const leafCommand = chain[chain.length - 1];
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const ctx = {
|
|
282
|
-
cmd: leafCommand,
|
|
446
|
+
const ctx = this.#createContext({
|
|
447
|
+
chain,
|
|
448
|
+
cmds: routeResult.cmds,
|
|
283
449
|
envs,
|
|
284
|
-
reporter
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
450
|
+
reporter,
|
|
451
|
+
});
|
|
452
|
+
const controlScanResult = this.#controlScan(routeResult.remaining, leafCommand);
|
|
453
|
+
ctx.controls = controlScanResult.controls;
|
|
454
|
+
ctx.sources.user.argv = [...controlScanResult.remaining];
|
|
455
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
456
|
+
const presetResult = await this.#preset(controlScanResult.remaining, ctx, optionPolicyMap);
|
|
457
|
+
ctx.sources = presetResult.sources;
|
|
458
|
+
ctx.envs = presetResult.envs;
|
|
459
|
+
const tokenizeResult = tokenize(presetResult.tailArgv, leafCommand.#getCommandPath());
|
|
460
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
461
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
462
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
288
463
|
}
|
|
289
464
|
formatHelp() {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
465
|
+
return this.#renderHelpPlain(this.#buildHelpData());
|
|
466
|
+
}
|
|
467
|
+
#formatHelpForDisplay(params = {}) {
|
|
468
|
+
const { color = true } = params;
|
|
469
|
+
const helpData = this.#buildHelpData();
|
|
470
|
+
if (!this.#shouldRenderStyledHelp(color)) {
|
|
471
|
+
return this.#renderHelpPlain(helpData);
|
|
472
|
+
}
|
|
473
|
+
return this.#renderHelpTerminal(helpData);
|
|
474
|
+
}
|
|
475
|
+
#shouldRenderStyledHelp(color) {
|
|
476
|
+
return color && process.stdout.isTTY === true;
|
|
477
|
+
}
|
|
478
|
+
#buildHelpData() {
|
|
479
|
+
const parseOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
480
|
+
const allOptions = [...parseOptions, BUILTIN_HELP_OPTION];
|
|
481
|
+
if (this.#supportsBuiltinVersion()) {
|
|
482
|
+
allOptions.push(BUILTIN_VERSION_OPTION);
|
|
483
|
+
}
|
|
294
484
|
const commandPath = this.#getCommandPath();
|
|
295
485
|
let usage = `Usage: ${commandPath}`;
|
|
296
486
|
if (allOptions.length > 0)
|
|
@@ -308,65 +498,128 @@ class Command {
|
|
|
308
498
|
usage += ` [${arg.name}...]`;
|
|
309
499
|
}
|
|
310
500
|
}
|
|
311
|
-
|
|
501
|
+
const options = [];
|
|
502
|
+
for (const opt of allOptions) {
|
|
503
|
+
const kebabLong = camelToKebabCase$1(opt.long);
|
|
504
|
+
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
505
|
+
sig += `--${kebabLong}`;
|
|
506
|
+
if (opt.args !== 'none') {
|
|
507
|
+
sig += ' <value>';
|
|
508
|
+
}
|
|
509
|
+
let desc = opt.desc;
|
|
510
|
+
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
511
|
+
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
512
|
+
}
|
|
513
|
+
if (opt.choices) {
|
|
514
|
+
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
515
|
+
}
|
|
516
|
+
options.push({ sig, desc });
|
|
517
|
+
if (opt.type === 'boolean' &&
|
|
518
|
+
opt.args === 'none' &&
|
|
519
|
+
opt.long !== 'help' &&
|
|
520
|
+
opt.long !== 'version') {
|
|
521
|
+
options.push({
|
|
522
|
+
sig: ` --no-${kebabLong}`,
|
|
523
|
+
desc: `Negate --${kebabLong}`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const commands = [];
|
|
528
|
+
if (this.#subcommandsList.length > 0) {
|
|
529
|
+
commands.push({ name: 'help', desc: 'Show help for a command' });
|
|
530
|
+
}
|
|
531
|
+
for (const entry of this.#subcommandsList) {
|
|
532
|
+
let name = entry.name;
|
|
533
|
+
if (entry.aliases.length > 0) {
|
|
534
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
535
|
+
}
|
|
536
|
+
commands.push({ name, desc: entry.command.#desc });
|
|
537
|
+
}
|
|
538
|
+
const examples = this.#examples.map(example => ({
|
|
539
|
+
title: example.title,
|
|
540
|
+
usage: commandPath ? `${commandPath} ${example.usage}` : example.usage,
|
|
541
|
+
desc: example.desc,
|
|
542
|
+
}));
|
|
543
|
+
return {
|
|
544
|
+
desc: this.#desc,
|
|
545
|
+
usage,
|
|
546
|
+
options,
|
|
547
|
+
commands,
|
|
548
|
+
examples,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
#renderHelpPlain(helpData) {
|
|
552
|
+
const lines = [];
|
|
553
|
+
lines.push(helpData.desc);
|
|
554
|
+
lines.push('');
|
|
555
|
+
lines.push(helpData.usage);
|
|
312
556
|
lines.push('');
|
|
313
|
-
if (
|
|
557
|
+
if (helpData.options.length > 0) {
|
|
314
558
|
lines.push('Options:');
|
|
315
|
-
const
|
|
316
|
-
for (const
|
|
317
|
-
const kebabLong = camelToKebabCase$1(opt.long);
|
|
318
|
-
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
319
|
-
sig += `--${kebabLong}`;
|
|
320
|
-
if (opt.args !== 'none') {
|
|
321
|
-
sig += ' <value>';
|
|
322
|
-
}
|
|
323
|
-
let desc = opt.desc;
|
|
324
|
-
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
325
|
-
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
326
|
-
}
|
|
327
|
-
if (opt.choices) {
|
|
328
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
329
|
-
}
|
|
330
|
-
optLines.push({ sig, desc });
|
|
331
|
-
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
332
|
-
optLines.push({
|
|
333
|
-
sig: ` --no-${kebabLong}`,
|
|
334
|
-
desc: `Negate --${kebabLong}`,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
|
|
339
|
-
for (const { sig, desc } of optLines) {
|
|
559
|
+
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
560
|
+
for (const { sig, desc } of helpData.options) {
|
|
340
561
|
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
341
562
|
lines.push(` ${sig}${padding}${desc}`);
|
|
342
563
|
}
|
|
343
564
|
lines.push('');
|
|
344
565
|
}
|
|
345
|
-
|
|
346
|
-
if (this.#subcommandsList.length > 0) {
|
|
566
|
+
if (helpData.commands.length > 0) {
|
|
347
567
|
lines.push('Commands:');
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
351
|
-
}
|
|
352
|
-
for (const entry of this.#subcommandsList) {
|
|
353
|
-
let name = entry.name;
|
|
354
|
-
if (entry.aliases.length > 0) {
|
|
355
|
-
name += `, ${entry.aliases.join(', ')}`;
|
|
356
|
-
}
|
|
357
|
-
cmdLines.push({ name, desc: entry.command.#desc });
|
|
358
|
-
}
|
|
359
|
-
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
360
|
-
for (const { name, desc } of cmdLines) {
|
|
568
|
+
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
569
|
+
for (const { name, desc } of helpData.commands) {
|
|
361
570
|
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
362
571
|
lines.push(` ${name}${padding}${desc}`);
|
|
363
572
|
}
|
|
364
573
|
lines.push('');
|
|
365
574
|
}
|
|
575
|
+
if (helpData.examples.length > 0) {
|
|
576
|
+
lines.push('Examples:');
|
|
577
|
+
for (const example of helpData.examples) {
|
|
578
|
+
lines.push(` - ${example.title}`);
|
|
579
|
+
lines.push(` ${example.usage}`);
|
|
580
|
+
lines.push(` ${example.desc}`);
|
|
581
|
+
lines.push('');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return lines.join('\n');
|
|
585
|
+
}
|
|
586
|
+
#renderHelpTerminal(helpData) {
|
|
587
|
+
const lines = [];
|
|
588
|
+
lines.push(helpData.desc);
|
|
589
|
+
lines.push('');
|
|
590
|
+
lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
|
|
591
|
+
lines.push('');
|
|
592
|
+
if (helpData.options.length > 0) {
|
|
593
|
+
lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
594
|
+
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
595
|
+
for (const { sig, desc } of helpData.options) {
|
|
596
|
+
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
597
|
+
lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
598
|
+
}
|
|
599
|
+
lines.push('');
|
|
600
|
+
}
|
|
601
|
+
if (helpData.commands.length > 0) {
|
|
602
|
+
lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
603
|
+
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
604
|
+
for (const { name, desc } of helpData.commands) {
|
|
605
|
+
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
606
|
+
lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
607
|
+
}
|
|
608
|
+
lines.push('');
|
|
609
|
+
}
|
|
610
|
+
if (helpData.examples.length > 0) {
|
|
611
|
+
lines.push(styleText('Examples:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
612
|
+
for (const example of helpData.examples) {
|
|
613
|
+
lines.push(` - ${styleText(example.title, TERMINAL_STYLE.bold)}`);
|
|
614
|
+
lines.push(` ${styleText(example.usage, TERMINAL_STYLE.cyan)}`);
|
|
615
|
+
lines.push(` ${styleText(example.desc, TERMINAL_STYLE.italic, TERMINAL_STYLE.dim)}`);
|
|
616
|
+
lines.push('');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
366
619
|
return lines.join('\n');
|
|
367
620
|
}
|
|
368
621
|
getCompletionMeta() {
|
|
369
|
-
const allOptions = this.#
|
|
622
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
370
623
|
const options = [];
|
|
371
624
|
for (const opt of allOptions) {
|
|
372
625
|
options.push({
|
|
@@ -392,46 +645,407 @@ class Command {
|
|
|
392
645
|
}),
|
|
393
646
|
};
|
|
394
647
|
}
|
|
395
|
-
#
|
|
396
|
-
|
|
397
|
-
return argv;
|
|
398
|
-
if (argv.length < 1 || argv[0] !== 'help')
|
|
399
|
-
return argv;
|
|
400
|
-
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
401
|
-
return ['--help'];
|
|
402
|
-
}
|
|
403
|
-
const subName = argv[1];
|
|
404
|
-
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
405
|
-
if (entry) {
|
|
406
|
-
return [subName, '--help', ...argv.slice(2)];
|
|
407
|
-
}
|
|
408
|
-
return argv;
|
|
648
|
+
#findSubcommandEntry(token) {
|
|
649
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
409
650
|
}
|
|
410
651
|
#route(argv) {
|
|
411
652
|
const chain = [this];
|
|
653
|
+
const cmds = [];
|
|
412
654
|
let current = this;
|
|
413
655
|
let idx = 0;
|
|
414
656
|
while (idx < argv.length) {
|
|
415
657
|
const token = argv[idx];
|
|
416
658
|
if (token.startsWith('-'))
|
|
417
659
|
break;
|
|
418
|
-
const entry = current.#
|
|
660
|
+
const entry = current.#findSubcommandEntry(token);
|
|
419
661
|
if (!entry)
|
|
420
662
|
break;
|
|
421
663
|
current = entry.command;
|
|
664
|
+
cmds.push(token);
|
|
422
665
|
chain.push(current);
|
|
423
666
|
idx += 1;
|
|
424
667
|
}
|
|
425
|
-
return { chain, remaining: argv.slice(idx) };
|
|
668
|
+
return { chain, remaining: argv.slice(idx), cmds };
|
|
669
|
+
}
|
|
670
|
+
#controlScan(tailArgv, leafCommand) {
|
|
671
|
+
const controls = { help: false, version: false };
|
|
672
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
673
|
+
const beforeSeparator = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
674
|
+
const afterSeparator = separatorIndex === -1 ? [] : tailArgv.slice(separatorIndex + 1);
|
|
675
|
+
let helpTarget;
|
|
676
|
+
let scanStartIndex = 0;
|
|
677
|
+
if (beforeSeparator[0] === 'help') {
|
|
678
|
+
controls.help = true;
|
|
679
|
+
scanStartIndex = 1;
|
|
680
|
+
const candidate = beforeSeparator[1];
|
|
681
|
+
if (candidate !== undefined && !candidate.startsWith('-')) {
|
|
682
|
+
helpTarget = candidate;
|
|
683
|
+
scanStartIndex = 2;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const remainingBeforeSeparator = [];
|
|
687
|
+
for (let i = scanStartIndex; i < beforeSeparator.length; i += 1) {
|
|
688
|
+
const token = beforeSeparator[i];
|
|
689
|
+
if (token === '--help') {
|
|
690
|
+
controls.help = true;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (token === '--version' && leafCommand.#supportsBuiltinVersion()) {
|
|
694
|
+
controls.version = true;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
remainingBeforeSeparator.push(token);
|
|
698
|
+
}
|
|
699
|
+
const remaining = separatorIndex === -1
|
|
700
|
+
? remainingBeforeSeparator
|
|
701
|
+
: [...remainingBeforeSeparator, '--', ...afterSeparator];
|
|
702
|
+
return {
|
|
703
|
+
controls,
|
|
704
|
+
remaining,
|
|
705
|
+
helpTarget,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
#createContext(params) {
|
|
709
|
+
const { chain, cmds, envs, reporter: reporter$1 } = params;
|
|
710
|
+
const leafCommand = chain[chain.length - 1];
|
|
711
|
+
const envSnapshot = { ...envs };
|
|
712
|
+
return {
|
|
713
|
+
cmd: leafCommand,
|
|
714
|
+
chain,
|
|
715
|
+
envs: envSnapshot,
|
|
716
|
+
controls: { help: false, version: false },
|
|
717
|
+
sources: {
|
|
718
|
+
preset: {
|
|
719
|
+
argv: [],
|
|
720
|
+
envs: {},
|
|
721
|
+
},
|
|
722
|
+
user: {
|
|
723
|
+
cmds: [...cmds],
|
|
724
|
+
argv: [],
|
|
725
|
+
envs: envSnapshot,
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
#resolveHelpCommand(leafCommand, helpTarget) {
|
|
732
|
+
if (helpTarget === undefined) {
|
|
733
|
+
return leafCommand;
|
|
734
|
+
}
|
|
735
|
+
const target = leafCommand.#findSubcommandEntry(helpTarget);
|
|
736
|
+
if (target === undefined) {
|
|
737
|
+
return leafCommand;
|
|
738
|
+
}
|
|
739
|
+
return target.command;
|
|
740
|
+
}
|
|
741
|
+
async #preset(controlTailArgv, ctx, optionPolicyMap) {
|
|
742
|
+
const commandPath = ctx.chain[ctx.chain.length - 1].#getCommandPath();
|
|
743
|
+
const separatorIndex = controlTailArgv.indexOf('--');
|
|
744
|
+
const beforeSeparator = separatorIndex === -1 ? controlTailArgv : controlTailArgv.slice(0, separatorIndex);
|
|
745
|
+
const afterSeparator = separatorIndex === -1 ? [] : controlTailArgv.slice(separatorIndex + 1);
|
|
746
|
+
const rootScanResult = this.#scanPresetRootDirectives(beforeSeparator, commandPath);
|
|
747
|
+
const commandPreset = this.#resolveCommandPresetFromChain(ctx.chain);
|
|
748
|
+
const presetRoot = await this.#resolveEffectivePresetRoot(rootScanResult.cliPresetRoots, commandPreset, commandPath);
|
|
749
|
+
const fileScanResult = this.#scanPresetFileDirectives(rootScanResult.cleanArgv, commandPath);
|
|
750
|
+
const cleanArgv = separatorIndex === -1
|
|
751
|
+
? fileScanResult.cleanArgv
|
|
752
|
+
: [...fileScanResult.cleanArgv, '--', ...afterSeparator];
|
|
753
|
+
const presetOptsFiles = this.#resolvePresetFileSources({
|
|
754
|
+
cliFiles: fileScanResult.cliPresetOptsFiles,
|
|
755
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.opt),
|
|
756
|
+
presetRoot,
|
|
757
|
+
defaultFilename: DEFAULT_PRESET_OPTS_FILENAME,
|
|
758
|
+
});
|
|
759
|
+
const presetEnvsFiles = this.#resolvePresetFileSources({
|
|
760
|
+
cliFiles: fileScanResult.cliPresetEnvsFiles,
|
|
761
|
+
commandPresetFile: this.#normalizeCommandPresetFile(commandPreset?.env),
|
|
762
|
+
presetRoot,
|
|
763
|
+
defaultFilename: DEFAULT_PRESET_ENVS_FILENAME,
|
|
764
|
+
});
|
|
765
|
+
const userSources = {
|
|
766
|
+
cmds: [...ctx.sources.user.cmds],
|
|
767
|
+
argv: [...cleanArgv],
|
|
768
|
+
envs: { ...ctx.sources.user.envs },
|
|
769
|
+
};
|
|
770
|
+
const presetArgv = [];
|
|
771
|
+
for (const file of presetOptsFiles) {
|
|
772
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
773
|
+
if (content === undefined) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const tokens = this.#tokenizePresetOptions(content);
|
|
777
|
+
this.#validatePresetOptionTokens(tokens, file.displayPath, commandPath);
|
|
778
|
+
this.#assertPresetOptionFragments(tokens, file.displayPath, ctx.chain, optionPolicyMap);
|
|
779
|
+
presetArgv.push(...tokens);
|
|
780
|
+
}
|
|
781
|
+
const presetEnvs = {};
|
|
782
|
+
for (const file of presetEnvsFiles) {
|
|
783
|
+
const content = await this.#readPresetFile(file, commandPath);
|
|
784
|
+
if (content === undefined) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
let parsed;
|
|
788
|
+
try {
|
|
789
|
+
parsed = env.parse(content);
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
throw new CommanderError('ConfigurationError', `failed to parse preset envs file "${file.displayPath}": ${error.message}`, commandPath);
|
|
793
|
+
}
|
|
794
|
+
Object.assign(presetEnvs, parsed);
|
|
795
|
+
}
|
|
796
|
+
const sources = {
|
|
797
|
+
user: userSources,
|
|
798
|
+
preset: {
|
|
799
|
+
argv: presetArgv,
|
|
800
|
+
envs: presetEnvs,
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
const envs = { ...sources.user.envs, ...sources.preset.envs };
|
|
804
|
+
const tailArgv = [...sources.preset.argv, ...sources.user.argv];
|
|
805
|
+
return { tailArgv, envs, sources };
|
|
806
|
+
}
|
|
807
|
+
#resolveCommandPresetFromChain(chain) {
|
|
808
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
809
|
+
const preset = chain[index].#presetConfig;
|
|
810
|
+
if (preset?.root !== undefined) {
|
|
811
|
+
return preset;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
async #resolveEffectivePresetRoot(cliPresetRoots, commandPreset, commandPath) {
|
|
817
|
+
if (cliPresetRoots.length > 0) {
|
|
818
|
+
const root = cliPresetRoots[cliPresetRoots.length - 1];
|
|
819
|
+
return await this.#assertPresetRoot(root, PRESET_ROOT_FLAG, commandPath);
|
|
820
|
+
}
|
|
821
|
+
if (commandPreset?.root === undefined) {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
return await this.#assertPresetRoot(commandPreset.root, 'command.preset.root', commandPath);
|
|
825
|
+
}
|
|
826
|
+
async #assertPresetRoot(root, sourceName, commandPath) {
|
|
827
|
+
if (!path.isAbsolute(root)) {
|
|
828
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not an absolute directory`, commandPath);
|
|
829
|
+
}
|
|
830
|
+
let stats;
|
|
831
|
+
try {
|
|
832
|
+
stats = await promises.stat(root);
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" cannot be accessed (${error.message})`, commandPath);
|
|
836
|
+
}
|
|
837
|
+
if (!stats.isDirectory()) {
|
|
838
|
+
throw new CommanderError('ConfigurationError', `invalid preset root from "${sourceName}": "${root}" is not a directory`, commandPath);
|
|
839
|
+
}
|
|
840
|
+
return root;
|
|
426
841
|
}
|
|
427
|
-
#
|
|
842
|
+
#normalizeCommandPresetFile(filepath) {
|
|
843
|
+
if (filepath === undefined) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
if (!this.#isValidPresetFileValue(filepath)) {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
return filepath;
|
|
850
|
+
}
|
|
851
|
+
#resolvePresetFileSources(params) {
|
|
852
|
+
const { cliFiles, commandPresetFile, presetRoot, defaultFilename } = params;
|
|
853
|
+
if (cliFiles.length > 0) {
|
|
854
|
+
return cliFiles.map(filepath => ({
|
|
855
|
+
displayPath: filepath,
|
|
856
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(filepath, presetRoot),
|
|
857
|
+
explicit: true,
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
if (presetRoot === undefined) {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
if (commandPresetFile !== undefined) {
|
|
864
|
+
return [
|
|
865
|
+
{
|
|
866
|
+
displayPath: commandPresetFile,
|
|
867
|
+
absolutePath: this.#resolvePresetFileAbsolutePath(commandPresetFile, presetRoot),
|
|
868
|
+
explicit: true,
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
const absolutePath = path.resolve(presetRoot, defaultFilename);
|
|
873
|
+
return [
|
|
874
|
+
{
|
|
875
|
+
displayPath: absolutePath,
|
|
876
|
+
absolutePath,
|
|
877
|
+
explicit: false,
|
|
878
|
+
},
|
|
879
|
+
];
|
|
880
|
+
}
|
|
881
|
+
#resolvePresetFileAbsolutePath(filepath, presetRoot) {
|
|
882
|
+
if (path.isAbsolute(filepath)) {
|
|
883
|
+
return filepath;
|
|
884
|
+
}
|
|
885
|
+
if (presetRoot !== undefined) {
|
|
886
|
+
return path.resolve(presetRoot, filepath);
|
|
887
|
+
}
|
|
888
|
+
return path.resolve(process.cwd(), filepath);
|
|
889
|
+
}
|
|
890
|
+
#assertPresetOptionFragments(tokens, filepath, chain, optionPolicyMap) {
|
|
891
|
+
if (tokens.length === 0) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const commandPath = chain[chain.length - 1].#getCommandPath();
|
|
895
|
+
try {
|
|
896
|
+
const { optionTokens, restArgs } = tokenize(tokens, commandPath);
|
|
897
|
+
void restArgs;
|
|
898
|
+
const { argTokens } = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
899
|
+
if (argTokens.length > 0) {
|
|
900
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": token "${argTokens[0].original}" cannot be resolved as an option fragment`, commandPath);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
if (error instanceof CommanderError) {
|
|
905
|
+
if (error.kind === 'ConfigurationError') {
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": ${error.message}`, commandPath);
|
|
909
|
+
}
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
#scanPresetRootDirectives(argv, commandPath) {
|
|
914
|
+
const cleanArgv = [];
|
|
915
|
+
const cliPresetRoots = [];
|
|
916
|
+
let index = 0;
|
|
917
|
+
while (index < argv.length) {
|
|
918
|
+
const token = argv[index];
|
|
919
|
+
if (token === PRESET_ROOT_FLAG) {
|
|
920
|
+
const value = argv[index + 1];
|
|
921
|
+
if (value === undefined || value.length === 0) {
|
|
922
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
923
|
+
}
|
|
924
|
+
cliPresetRoots.push(value);
|
|
925
|
+
index += 2;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (token.startsWith(`${PRESET_ROOT_FLAG}=`)) {
|
|
929
|
+
const value = token.slice(PRESET_ROOT_FLAG.length + 1);
|
|
930
|
+
if (value.length === 0) {
|
|
931
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ROOT_FLAG}"`, commandPath);
|
|
932
|
+
}
|
|
933
|
+
cliPresetRoots.push(value);
|
|
934
|
+
index += 1;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
cleanArgv.push(token);
|
|
938
|
+
index += 1;
|
|
939
|
+
}
|
|
940
|
+
return { cleanArgv, cliPresetRoots };
|
|
941
|
+
}
|
|
942
|
+
#scanPresetFileDirectives(argv, commandPath) {
|
|
943
|
+
const cleanArgv = [];
|
|
944
|
+
const cliPresetOptsFiles = [];
|
|
945
|
+
const cliPresetEnvsFiles = [];
|
|
946
|
+
const assertAndPush = (flag, value) => {
|
|
947
|
+
this.#assertPresetFileValue(value, flag, commandPath);
|
|
948
|
+
if (flag === PRESET_OPTS_FLAG) {
|
|
949
|
+
cliPresetOptsFiles.push(value);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
cliPresetEnvsFiles.push(value);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
let index = 0;
|
|
956
|
+
while (index < argv.length) {
|
|
957
|
+
const token = argv[index];
|
|
958
|
+
if (token === PRESET_OPTS_FLAG || token === PRESET_ENVS_FLAG) {
|
|
959
|
+
const value = argv[index + 1];
|
|
960
|
+
if (value === undefined || value.length === 0) {
|
|
961
|
+
throw new CommanderError('ConfigurationError', `missing value for "${token}"`, commandPath);
|
|
962
|
+
}
|
|
963
|
+
assertAndPush(token, value);
|
|
964
|
+
index += 2;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (token.startsWith(`${PRESET_OPTS_FLAG}=`)) {
|
|
968
|
+
const value = token.slice(PRESET_OPTS_FLAG.length + 1);
|
|
969
|
+
if (value.length === 0) {
|
|
970
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_OPTS_FLAG}"`, commandPath);
|
|
971
|
+
}
|
|
972
|
+
assertAndPush(PRESET_OPTS_FLAG, value);
|
|
973
|
+
index += 1;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
977
|
+
const value = token.slice(PRESET_ENVS_FLAG.length + 1);
|
|
978
|
+
if (value.length === 0) {
|
|
979
|
+
throw new CommanderError('ConfigurationError', `missing value for "${PRESET_ENVS_FLAG}"`, commandPath);
|
|
980
|
+
}
|
|
981
|
+
assertAndPush(PRESET_ENVS_FLAG, value);
|
|
982
|
+
index += 1;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
cleanArgv.push(token);
|
|
986
|
+
index += 1;
|
|
987
|
+
}
|
|
988
|
+
return { cleanArgv, cliPresetOptsFiles, cliPresetEnvsFiles };
|
|
989
|
+
}
|
|
990
|
+
#isValidPresetFileValue(filepath) {
|
|
991
|
+
return filepath.length > 0 && !filepath.startsWith('..');
|
|
992
|
+
}
|
|
993
|
+
#assertPresetFileValue(filepath, directive, commandPath) {
|
|
994
|
+
if (this.#isValidPresetFileValue(filepath)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
throw new CommanderError('ConfigurationError', `invalid value for "${directive}": "${filepath}" (must be non-empty and must not start with "..")`, commandPath);
|
|
998
|
+
}
|
|
999
|
+
async #readPresetFile(file, commandPath) {
|
|
1000
|
+
try {
|
|
1001
|
+
return await promises.readFile(file.absolutePath, 'utf8');
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
const ioError = error;
|
|
1005
|
+
if (!file.explicit && ioError.code === 'ENOENT') {
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
throw new CommanderError('ConfigurationError', `failed to read preset file "${file.displayPath}": ${ioError.message}`, commandPath);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
#tokenizePresetOptions(content) {
|
|
1012
|
+
return content
|
|
1013
|
+
.split(/\s+/)
|
|
1014
|
+
.map(token => token.trim())
|
|
1015
|
+
.filter(token => token.length > 0);
|
|
1016
|
+
}
|
|
1017
|
+
#validatePresetOptionTokens(tokens, filepath, commandPath) {
|
|
1018
|
+
if (tokens.length === 0) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (!tokens[0].startsWith('-')) {
|
|
1022
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": bare token "${tokens[0]}" cannot appear before any option token`, commandPath);
|
|
1023
|
+
}
|
|
1024
|
+
for (const token of tokens) {
|
|
1025
|
+
if (token === '--') {
|
|
1026
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": "--" is not allowed`, commandPath);
|
|
1027
|
+
}
|
|
1028
|
+
if (token === 'help' || token === '--help' || token === '--version') {
|
|
1029
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": control token "${token}" is not allowed`, commandPath);
|
|
1030
|
+
}
|
|
1031
|
+
if (token === PRESET_ROOT_FLAG ||
|
|
1032
|
+
token.startsWith(`${PRESET_ROOT_FLAG}=`) ||
|
|
1033
|
+
token === PRESET_OPTS_FLAG ||
|
|
1034
|
+
token.startsWith(`${PRESET_OPTS_FLAG}=`) ||
|
|
1035
|
+
token === PRESET_ENVS_FLAG ||
|
|
1036
|
+
token.startsWith(`${PRESET_ENVS_FLAG}=`)) {
|
|
1037
|
+
throw new CommanderError('ConfigurationError', `invalid preset options in "${filepath}": preset directive "${token}" is not allowed`, commandPath);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
428
1042
|
const consumedTokens = new Map();
|
|
429
1043
|
let remaining = [...tokens];
|
|
430
1044
|
const shadowed = new Set();
|
|
431
1045
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
432
1046
|
const cmd = chain[i];
|
|
433
|
-
const
|
|
434
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
1047
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1048
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
435
1049
|
consumedTokens.set(cmd, result.consumed);
|
|
436
1050
|
remaining = result.remaining;
|
|
437
1051
|
for (const opt of cmd.#options) {
|
|
@@ -448,8 +1062,7 @@ class Command {
|
|
|
448
1062
|
}
|
|
449
1063
|
return { consumedTokens, argTokens };
|
|
450
1064
|
}
|
|
451
|
-
#shift(tokens, shadowed,
|
|
452
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
1065
|
+
#shift(tokens, shadowed, allOptions) {
|
|
453
1066
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
454
1067
|
const optionByLong = new Map();
|
|
455
1068
|
const optionByShort = new Map();
|
|
@@ -517,34 +1130,40 @@ class Command {
|
|
|
517
1130
|
}
|
|
518
1131
|
return { consumed, remaining };
|
|
519
1132
|
}
|
|
520
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
1133
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
521
1134
|
const { consumedTokens, argTokens } = resolveResult;
|
|
522
1135
|
const leafCommand = chain[chain.length - 1];
|
|
523
|
-
this.#validateMergedShortOptions(chain);
|
|
1136
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
524
1137
|
const optsMap = new Map();
|
|
525
|
-
for (
|
|
526
|
-
const
|
|
527
|
-
const includeVersion = i === 0;
|
|
1138
|
+
for (const cmd of chain) {
|
|
1139
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
528
1140
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
529
|
-
const opts = cmd.#parseOptions(tokens,
|
|
1141
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
530
1142
|
optsMap.set(cmd, opts);
|
|
531
|
-
for (const opt of
|
|
1143
|
+
for (const opt of policy.mergedOptions) {
|
|
532
1144
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
533
1145
|
opt.apply(opts[opt.long], ctx);
|
|
534
1146
|
}
|
|
535
1147
|
}
|
|
536
1148
|
}
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
1149
|
+
const leafLocalOpts = {};
|
|
1150
|
+
const leafParsedOpts = optsMap.get(leafCommand) ?? {};
|
|
1151
|
+
for (const opt of leafCommand.#options) {
|
|
1152
|
+
if (Object.prototype.hasOwnProperty.call(leafParsedOpts, opt.long)) {
|
|
1153
|
+
leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
|
|
1154
|
+
}
|
|
540
1155
|
}
|
|
541
1156
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
542
1157
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
543
|
-
|
|
1158
|
+
const parseCtx = {
|
|
1159
|
+
...ctx,
|
|
1160
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1161
|
+
};
|
|
1162
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
544
1163
|
}
|
|
545
|
-
#parseOptions(tokens,
|
|
546
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
1164
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
547
1165
|
const opts = {};
|
|
1166
|
+
let sawColorToken = false;
|
|
548
1167
|
for (const opt of allOptions) {
|
|
549
1168
|
if (opt.default !== undefined) {
|
|
550
1169
|
opts[opt.long] = opt.default;
|
|
@@ -572,6 +1191,9 @@ class Command {
|
|
|
572
1191
|
i += 1;
|
|
573
1192
|
continue;
|
|
574
1193
|
}
|
|
1194
|
+
if (opt.long === 'color') {
|
|
1195
|
+
sawColorToken = true;
|
|
1196
|
+
}
|
|
575
1197
|
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
576
1198
|
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
577
1199
|
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
@@ -648,6 +1270,9 @@ class Command {
|
|
|
648
1270
|
}
|
|
649
1271
|
}
|
|
650
1272
|
}
|
|
1273
|
+
if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
|
|
1274
|
+
opts['color'] = false;
|
|
1275
|
+
}
|
|
651
1276
|
return opts;
|
|
652
1277
|
}
|
|
653
1278
|
#convertValue(opt, rawValue) {
|
|
@@ -718,27 +1343,60 @@ class Command {
|
|
|
718
1343
|
}
|
|
719
1344
|
return raw;
|
|
720
1345
|
}
|
|
721
|
-
#
|
|
1346
|
+
#hasUserOption(long) {
|
|
1347
|
+
return this.#options.some(option => option.long === long);
|
|
1348
|
+
}
|
|
1349
|
+
#supportsBuiltinVersion() {
|
|
1350
|
+
return this.#parent === undefined && this.#version !== undefined && this.#builtin.option.version;
|
|
1351
|
+
}
|
|
1352
|
+
#resolveOptionPolicy() {
|
|
722
1353
|
const optionMap = new Map();
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
1354
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
1355
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
1356
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
1357
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
1358
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
1359
|
+
if (this.#builtin.option.color && !hasUserColor) {
|
|
1360
|
+
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
1361
|
+
}
|
|
1362
|
+
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
1363
|
+
optionMap.set('logLevel', logLevelOption);
|
|
1364
|
+
}
|
|
1365
|
+
if (this.#builtin.option.silent && !hasUserSilent) {
|
|
1366
|
+
optionMap.set('silent', silentOption);
|
|
1367
|
+
}
|
|
1368
|
+
if (this.#builtin.option.logDate && !hasUserLogDate) {
|
|
1369
|
+
optionMap.set('logDate', logDateOption);
|
|
727
1370
|
}
|
|
728
|
-
if (
|
|
729
|
-
optionMap.set('
|
|
1371
|
+
if (this.#builtin.option.logColorful && !hasUserLogColorful) {
|
|
1372
|
+
optionMap.set('logColorful', logColorfulOption);
|
|
730
1373
|
}
|
|
731
1374
|
for (const opt of this.#options) {
|
|
732
1375
|
optionMap.set(opt.long, opt);
|
|
733
1376
|
}
|
|
734
|
-
return
|
|
1377
|
+
return {
|
|
1378
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
#buildOptionPolicyMap(chain) {
|
|
1382
|
+
const optionPolicyMap = new Map();
|
|
1383
|
+
for (const cmd of chain) {
|
|
1384
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1385
|
+
}
|
|
1386
|
+
return optionPolicyMap;
|
|
735
1387
|
}
|
|
736
|
-
#
|
|
1388
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1389
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1390
|
+
if (policy !== undefined) {
|
|
1391
|
+
return policy;
|
|
1392
|
+
}
|
|
1393
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
1394
|
+
}
|
|
1395
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
737
1396
|
const mergedByLong = new Map();
|
|
738
|
-
for (
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1397
|
+
for (const cmd of chain) {
|
|
1398
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1399
|
+
for (const opt of policy.mergedOptions) {
|
|
742
1400
|
mergedByLong.set(opt.long, opt);
|
|
743
1401
|
}
|
|
744
1402
|
}
|
|
@@ -754,6 +1412,9 @@ class Command {
|
|
|
754
1412
|
}
|
|
755
1413
|
}
|
|
756
1414
|
#validateOptionConfig(opt) {
|
|
1415
|
+
if (opt.long === 'help' || opt.long === 'version') {
|
|
1416
|
+
throw new CommanderError('ConfigurationError', `option long name "${opt.long}" is reserved`, this.#getCommandPath());
|
|
1417
|
+
}
|
|
757
1418
|
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
758
1419
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
759
1420
|
}
|
|
@@ -803,6 +1464,21 @@ class Command {
|
|
|
803
1464
|
}
|
|
804
1465
|
}
|
|
805
1466
|
}
|
|
1467
|
+
#normalizeExample(example) {
|
|
1468
|
+
const title = example.title.trim();
|
|
1469
|
+
const usage = example.usage.trim();
|
|
1470
|
+
const desc = example.desc.trim();
|
|
1471
|
+
if (!title) {
|
|
1472
|
+
throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
|
|
1473
|
+
}
|
|
1474
|
+
if (!usage) {
|
|
1475
|
+
throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
|
|
1476
|
+
}
|
|
1477
|
+
if (!desc) {
|
|
1478
|
+
throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
|
|
1479
|
+
}
|
|
1480
|
+
return { title, usage, desc };
|
|
1481
|
+
}
|
|
806
1482
|
async #runAction(params) {
|
|
807
1483
|
if (!this.#action)
|
|
808
1484
|
return;
|
|
@@ -819,16 +1495,51 @@ class Command {
|
|
|
819
1495
|
process.exit(1);
|
|
820
1496
|
}
|
|
821
1497
|
}
|
|
822
|
-
#
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1498
|
+
#resolveHelpColorFromTailArgv(tailArgv, envs, policy = this.#resolveOptionPolicy()) {
|
|
1499
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1500
|
+
let color = !isNoColorEnabled(envs);
|
|
1501
|
+
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1502
|
+
return color;
|
|
1503
|
+
}
|
|
1504
|
+
const separatorIndex = tailArgv.indexOf('--');
|
|
1505
|
+
const scanTokens = separatorIndex === -1 ? tailArgv : tailArgv.slice(0, separatorIndex);
|
|
1506
|
+
for (const token of scanTokens) {
|
|
1507
|
+
if (token === '--color') {
|
|
1508
|
+
color = true;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (token === '--no-color') {
|
|
1512
|
+
color = false;
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (!token.startsWith('--color=')) {
|
|
1516
|
+
continue;
|
|
826
1517
|
}
|
|
827
|
-
|
|
828
|
-
|
|
1518
|
+
const value = token.slice('--color='.length);
|
|
1519
|
+
if (value === 'true') {
|
|
1520
|
+
color = true;
|
|
1521
|
+
}
|
|
1522
|
+
else if (value === 'false') {
|
|
1523
|
+
color = false;
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
|
|
829
1527
|
}
|
|
830
1528
|
}
|
|
831
|
-
return
|
|
1529
|
+
return color;
|
|
1530
|
+
}
|
|
1531
|
+
#freezeInputSources(sources) {
|
|
1532
|
+
return Object.freeze({
|
|
1533
|
+
preset: Object.freeze({
|
|
1534
|
+
argv: Object.freeze([...sources.preset.argv]),
|
|
1535
|
+
envs: Object.freeze({ ...sources.preset.envs }),
|
|
1536
|
+
}),
|
|
1537
|
+
user: Object.freeze({
|
|
1538
|
+
cmds: Object.freeze([...sources.user.cmds]),
|
|
1539
|
+
argv: Object.freeze([...sources.user.argv]),
|
|
1540
|
+
envs: Object.freeze({ ...sources.user.envs }),
|
|
1541
|
+
}),
|
|
1542
|
+
});
|
|
832
1543
|
}
|
|
833
1544
|
#getCommandPath() {
|
|
834
1545
|
const parts = [];
|
|
@@ -843,13 +1554,166 @@ class Command {
|
|
|
843
1554
|
}
|
|
844
1555
|
}
|
|
845
1556
|
|
|
1557
|
+
function isIpv4(rawValue) {
|
|
1558
|
+
const parts = rawValue.split('.');
|
|
1559
|
+
if (parts.length !== 4) {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
for (const part of parts) {
|
|
1563
|
+
if (part.length < 1 || !/^\d+$/.test(part)) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
if (part.length > 1 && part.startsWith('0')) {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
const value = Number(part);
|
|
1570
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
function countIpv6Segments(part, allowIpv4Tail) {
|
|
1577
|
+
if (!part) {
|
|
1578
|
+
return { count: 0, hasIpv4Tail: false };
|
|
1579
|
+
}
|
|
1580
|
+
const segments = part.split(':');
|
|
1581
|
+
let count = 0;
|
|
1582
|
+
let hasIpv4Tail = false;
|
|
1583
|
+
for (let i = 0; i < segments.length; ++i) {
|
|
1584
|
+
const segment = segments[i];
|
|
1585
|
+
const isLastSegment = i === segments.length - 1;
|
|
1586
|
+
if (!segment) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
if (segment.includes('.')) {
|
|
1590
|
+
if (!allowIpv4Tail || !isLastSegment || hasIpv4Tail || !isIpv4(segment)) {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
hasIpv4Tail = true;
|
|
1594
|
+
count += 2;
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
count += 1;
|
|
1601
|
+
}
|
|
1602
|
+
return { count, hasIpv4Tail };
|
|
1603
|
+
}
|
|
1604
|
+
function isIpv6(rawValue) {
|
|
1605
|
+
if (!rawValue || !/^[0-9A-Fa-f:.]+$/.test(rawValue)) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
const doubleColonCount = rawValue.split('::').length - 1;
|
|
1609
|
+
if (doubleColonCount > 1) {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
if (doubleColonCount === 0) {
|
|
1613
|
+
const full = countIpv6Segments(rawValue, true);
|
|
1614
|
+
return full !== null && full.count === 8;
|
|
1615
|
+
}
|
|
1616
|
+
const [left, right] = rawValue.split('::');
|
|
1617
|
+
const leftPart = countIpv6Segments(left, right.length === 0);
|
|
1618
|
+
const rightPart = countIpv6Segments(right, true);
|
|
1619
|
+
if (!leftPart || !rightPart) {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
const totalSegments = leftPart.count + rightPart.count;
|
|
1623
|
+
return totalSegments < 8;
|
|
1624
|
+
}
|
|
1625
|
+
function isIp(rawValue) {
|
|
1626
|
+
return isIpv4(rawValue) || isIpv6(rawValue);
|
|
1627
|
+
}
|
|
1628
|
+
function isDomain(rawValue) {
|
|
1629
|
+
if (rawValue.length < 1 || rawValue.length > 253 || rawValue.endsWith('.')) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
const labels = rawValue.split('.');
|
|
1633
|
+
if (labels.length < 2) {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
if (labels.some(label => label.length < 1 || label.length > 63)) {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
const labelPattern = /^[A-Za-z0-9-]+$/;
|
|
1640
|
+
if (labels.some(label => !labelPattern.test(label) || label.startsWith('-') || label.endsWith('-'))) {
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
const topLevelLabel = labels[labels.length - 1];
|
|
1644
|
+
return /[A-Za-z]/.test(topLevelLabel);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
class Coerce {
|
|
1648
|
+
constructor() { }
|
|
1649
|
+
static create(name, expectedType, validator, errorMessage) {
|
|
1650
|
+
return (rawValue) => {
|
|
1651
|
+
const value = Number(rawValue);
|
|
1652
|
+
if (!validator(value)) {
|
|
1653
|
+
throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
|
|
1654
|
+
}
|
|
1655
|
+
return value;
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
static choice(name, values, errorMessage) {
|
|
1659
|
+
return (rawValue) => {
|
|
1660
|
+
if (values.includes(rawValue)) {
|
|
1661
|
+
return rawValue;
|
|
1662
|
+
}
|
|
1663
|
+
throw new Error(errorMessage ?? `${name} is expected as one of [${values.join(', ')}], but got ${rawValue}`);
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
static domain(name, errorMessage) {
|
|
1667
|
+
return (rawValue) => {
|
|
1668
|
+
if (isDomain(rawValue)) {
|
|
1669
|
+
return rawValue;
|
|
1670
|
+
}
|
|
1671
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid domain, but got ${rawValue}`);
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
static host(name, errorMessage) {
|
|
1675
|
+
return (rawValue) => {
|
|
1676
|
+
if (isIp(rawValue) || isDomain(rawValue)) {
|
|
1677
|
+
return rawValue;
|
|
1678
|
+
}
|
|
1679
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid host (IP or domain), but got ${rawValue}`);
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
static integer(name, errorMessage) {
|
|
1683
|
+
return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
|
|
1684
|
+
}
|
|
1685
|
+
static ip(name, errorMessage) {
|
|
1686
|
+
return (rawValue) => {
|
|
1687
|
+
if (isIp(rawValue)) {
|
|
1688
|
+
return rawValue;
|
|
1689
|
+
}
|
|
1690
|
+
throw new Error(errorMessage ?? `${name} is expected as a valid IP address, but got ${rawValue}`);
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
static number(name, errorMessage) {
|
|
1694
|
+
return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
|
|
1695
|
+
}
|
|
1696
|
+
static port(name, errorMessage) {
|
|
1697
|
+
return this.create(name, 'a valid port number (0-65535)', value => Number.isInteger(value) && value >= 0 && value <= 65535, errorMessage);
|
|
1698
|
+
}
|
|
1699
|
+
static positiveInteger(name, errorMessage) {
|
|
1700
|
+
return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
|
|
1701
|
+
}
|
|
1702
|
+
static positiveNumber(name, errorMessage) {
|
|
1703
|
+
return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
846
1707
|
function camelToKebabCase(str) {
|
|
847
1708
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
848
1709
|
}
|
|
849
1710
|
class CompletionCommand extends Command {
|
|
850
|
-
constructor(root, config) {
|
|
851
|
-
const paths = config.paths;
|
|
1711
|
+
constructor(root, config = {}) {
|
|
852
1712
|
const programName = config.programName ?? root.name ?? 'program';
|
|
1713
|
+
const paths = {
|
|
1714
|
+
...createDefaultCompletionPaths(programName),
|
|
1715
|
+
...config.paths,
|
|
1716
|
+
};
|
|
853
1717
|
super({ desc: 'Generate shell completion script' });
|
|
854
1718
|
this.option({
|
|
855
1719
|
long: 'bash',
|
|
@@ -924,6 +1788,13 @@ class CompletionCommand extends Command {
|
|
|
924
1788
|
});
|
|
925
1789
|
}
|
|
926
1790
|
}
|
|
1791
|
+
function createDefaultCompletionPaths(programName) {
|
|
1792
|
+
return {
|
|
1793
|
+
bash: `~/.local/share/bash-completion/completions/${programName}`,
|
|
1794
|
+
fish: `~/.config/fish/completions/${programName}.fish`,
|
|
1795
|
+
pwsh: '~/.config/powershell/Microsoft.PowerShell_profile.ps1',
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
927
1798
|
function expandHome(filepath) {
|
|
928
1799
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
929
1800
|
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
@@ -1201,37 +2072,18 @@ class PwshCompletion {
|
|
|
1201
2072
|
}
|
|
1202
2073
|
}
|
|
1203
2074
|
|
|
1204
|
-
const logLevelOption = {
|
|
1205
|
-
long: 'logLevel',
|
|
1206
|
-
type: 'string',
|
|
1207
|
-
args: 'required',
|
|
1208
|
-
desc: 'Set log level',
|
|
1209
|
-
default: 'info',
|
|
1210
|
-
choices: reporter.LOG_LEVELS,
|
|
1211
|
-
coerce: (raw) => {
|
|
1212
|
-
const level = reporter.resolveLogLevel(raw);
|
|
1213
|
-
if (level === undefined) {
|
|
1214
|
-
throw new Error(`Invalid log level: ${raw}`);
|
|
1215
|
-
}
|
|
1216
|
-
return level;
|
|
1217
|
-
},
|
|
1218
|
-
apply: (value, ctx) => {
|
|
1219
|
-
ctx.reporter.setLevel(value);
|
|
1220
|
-
},
|
|
1221
|
-
};
|
|
1222
|
-
const silentOption = {
|
|
1223
|
-
long: 'silent',
|
|
1224
|
-
type: 'boolean',
|
|
1225
|
-
args: 'none',
|
|
1226
|
-
desc: 'Suppress non-error output',
|
|
1227
|
-
default: false,
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
2075
|
exports.BashCompletion = BashCompletion;
|
|
2076
|
+
exports.Coerce = Coerce;
|
|
1231
2077
|
exports.Command = Command;
|
|
1232
2078
|
exports.CommanderError = CommanderError;
|
|
1233
2079
|
exports.CompletionCommand = CompletionCommand;
|
|
1234
2080
|
exports.FishCompletion = FishCompletion;
|
|
1235
2081
|
exports.PwshCompletion = PwshCompletion;
|
|
2082
|
+
exports.isDomain = isDomain;
|
|
2083
|
+
exports.isIp = isIp;
|
|
2084
|
+
exports.isIpv4 = isIpv4;
|
|
2085
|
+
exports.isIpv6 = isIpv6;
|
|
2086
|
+
exports.logColorfulOption = logColorfulOption;
|
|
2087
|
+
exports.logDateOption = logDateOption;
|
|
1236
2088
|
exports.logLevelOption = logLevelOption;
|
|
1237
2089
|
exports.silentOption = silentOption;
|