@guanghechen/commander 4.7.0 → 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 +14 -0
- package/lib/cjs/browser.cjs +290 -42
- package/lib/cjs/index.cjs +2089 -0
- package/lib/cjs/node.cjs +598 -54
- package/lib/esm/browser.mjs +290 -42
- package/lib/esm/index.mjs +2054 -0
- package/lib/esm/node.mjs +598 -54
- package/lib/types/browser.d.ts +27 -6
- package/lib/types/index.d.ts +560 -0
- package/lib/types/node.d.ts +27 -6
- package/package.json +3 -3
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var env = require('@guanghechen/env');
|
|
4
|
+
var reporter = require('@guanghechen/reporter');
|
|
5
|
+
var promises = require('node:fs/promises');
|
|
6
|
+
var path = require('node:path');
|
|
7
|
+
var fs = require('node:fs');
|
|
8
|
+
|
|
9
|
+
function _interopNamespaceDefault(e) {
|
|
10
|
+
var n = Object.create(null);
|
|
11
|
+
if (e) {
|
|
12
|
+
Object.keys(e).forEach(function (k) {
|
|
13
|
+
if (k !== 'default') {
|
|
14
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: function () { return e[k]; }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
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
|
+
};
|
|
104
|
+
|
|
105
|
+
class CommanderError extends Error {
|
|
106
|
+
kind;
|
|
107
|
+
commandPath;
|
|
108
|
+
constructor(kind, message, commandPath) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = 'CommanderError';
|
|
111
|
+
this.kind = kind;
|
|
112
|
+
this.commandPath = commandPath;
|
|
113
|
+
}
|
|
114
|
+
format() {
|
|
115
|
+
return `Error: ${this.message}\nRun "${this.commandPath} --help" for usage.`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
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';
|
|
126
|
+
function kebabToCamelCase(str) {
|
|
127
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
128
|
+
}
|
|
129
|
+
function camelToKebabCase$1(str) {
|
|
130
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
131
|
+
}
|
|
132
|
+
function tokenizeLongOption(arg, commandPath) {
|
|
133
|
+
const eqIdx = arg.indexOf('=');
|
|
134
|
+
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
135
|
+
const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
|
|
136
|
+
if (namePart.includes('_')) {
|
|
137
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
|
|
138
|
+
}
|
|
139
|
+
const lowerName = namePart.toLowerCase();
|
|
140
|
+
if (lowerName === '--no' || lowerName === '--no-') {
|
|
141
|
+
throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
|
|
142
|
+
}
|
|
143
|
+
if (lowerName.startsWith('--no-')) {
|
|
144
|
+
if (valuePart !== '') {
|
|
145
|
+
throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
|
|
146
|
+
}
|
|
147
|
+
if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
|
|
148
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
149
|
+
}
|
|
150
|
+
const camelName = kebabToCamelCase(lowerName.slice(5));
|
|
151
|
+
return {
|
|
152
|
+
original: arg,
|
|
153
|
+
resolved: `--${camelName}=false`,
|
|
154
|
+
name: camelName,
|
|
155
|
+
type: 'long',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (!LONG_OPTION_REGEX.test(lowerName)) {
|
|
159
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
160
|
+
}
|
|
161
|
+
const camelName = kebabToCamelCase(lowerName.slice(2));
|
|
162
|
+
return {
|
|
163
|
+
original: arg,
|
|
164
|
+
resolved: `--${camelName}${valuePart}`,
|
|
165
|
+
name: camelName,
|
|
166
|
+
type: 'long',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function tokenizeShortOptions(arg, commandPath) {
|
|
170
|
+
if (arg.includes('=')) {
|
|
171
|
+
throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
|
|
172
|
+
}
|
|
173
|
+
const flags = arg.slice(1);
|
|
174
|
+
return flags.split('').map(flag => ({
|
|
175
|
+
original: `-${flag}`,
|
|
176
|
+
resolved: `-${flag}`,
|
|
177
|
+
name: flag,
|
|
178
|
+
type: 'short',
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
function tokenize(argv, commandPath) {
|
|
182
|
+
const optionTokens = [];
|
|
183
|
+
const restArgs = [];
|
|
184
|
+
let passThrough = false;
|
|
185
|
+
for (const arg of argv) {
|
|
186
|
+
if (arg === '--') {
|
|
187
|
+
passThrough = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (passThrough) {
|
|
191
|
+
restArgs.push(arg);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg.startsWith('--')) {
|
|
195
|
+
optionTokens.push(tokenizeLongOption(arg, commandPath));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
199
|
+
optionTokens.push(...tokenizeShortOptions(arg, commandPath));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
optionTokens.push({
|
|
203
|
+
original: arg,
|
|
204
|
+
resolved: arg,
|
|
205
|
+
name: '',
|
|
206
|
+
type: 'none',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return { optionTokens, restArgs };
|
|
210
|
+
}
|
|
211
|
+
const BUILTIN_HELP_OPTION = {
|
|
212
|
+
long: 'help',
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
args: 'none',
|
|
215
|
+
desc: 'Show help information',
|
|
216
|
+
};
|
|
217
|
+
const BUILTIN_VERSION_OPTION = {
|
|
218
|
+
long: 'version',
|
|
219
|
+
type: 'boolean',
|
|
220
|
+
args: 'none',
|
|
221
|
+
desc: 'Show version number',
|
|
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
|
+
}
|
|
286
|
+
class Command {
|
|
287
|
+
#name;
|
|
288
|
+
#desc;
|
|
289
|
+
#version;
|
|
290
|
+
#builtinConfig;
|
|
291
|
+
#builtin;
|
|
292
|
+
#presetConfig;
|
|
293
|
+
#reporter;
|
|
294
|
+
#parent;
|
|
295
|
+
#options = [];
|
|
296
|
+
#arguments = [];
|
|
297
|
+
#examples = [];
|
|
298
|
+
#subcommandsList = [];
|
|
299
|
+
#subcommandsMap = new Map();
|
|
300
|
+
#action = undefined;
|
|
301
|
+
constructor(config) {
|
|
302
|
+
this.#name = config.name ?? '';
|
|
303
|
+
this.#desc = config.desc;
|
|
304
|
+
this.#version = config.version;
|
|
305
|
+
this.#builtinConfig = config.builtin;
|
|
306
|
+
this.#builtin = normalizeBuiltinConfig(config.builtin);
|
|
307
|
+
this.#presetConfig = config.preset;
|
|
308
|
+
this.#reporter = config.reporter;
|
|
309
|
+
}
|
|
310
|
+
get name() {
|
|
311
|
+
return this.#name || undefined;
|
|
312
|
+
}
|
|
313
|
+
get description() {
|
|
314
|
+
return this.#desc;
|
|
315
|
+
}
|
|
316
|
+
get version() {
|
|
317
|
+
return this.#version;
|
|
318
|
+
}
|
|
319
|
+
get builtin() {
|
|
320
|
+
return this.#builtinConfig;
|
|
321
|
+
}
|
|
322
|
+
get preset() {
|
|
323
|
+
return this.#presetConfig === undefined ? undefined : { ...this.#presetConfig };
|
|
324
|
+
}
|
|
325
|
+
get parent() {
|
|
326
|
+
return this.#parent;
|
|
327
|
+
}
|
|
328
|
+
get options() {
|
|
329
|
+
return [...this.#options];
|
|
330
|
+
}
|
|
331
|
+
get arguments() {
|
|
332
|
+
return [...this.#arguments];
|
|
333
|
+
}
|
|
334
|
+
get examples() {
|
|
335
|
+
return this.#examples.map(example => ({ ...example }));
|
|
336
|
+
}
|
|
337
|
+
get subcommands() {
|
|
338
|
+
return new Map(this.#subcommandsMap);
|
|
339
|
+
}
|
|
340
|
+
option(opt) {
|
|
341
|
+
this.#validateOptionConfig(opt);
|
|
342
|
+
this.#checkOptionUniqueness(opt);
|
|
343
|
+
this.#options.push(opt);
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
argument(arg) {
|
|
347
|
+
this.#validateArgumentConfig(arg);
|
|
348
|
+
this.#arguments.push(arg);
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
action(fn) {
|
|
352
|
+
this.#action = fn;
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
example(title, usage, desc) {
|
|
356
|
+
this.#examples.push(this.#normalizeExample({ title, usage, desc }));
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
subcommand(name, cmd) {
|
|
360
|
+
if (name === 'help') {
|
|
361
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name', this.#getCommandPath());
|
|
362
|
+
}
|
|
363
|
+
if (cmd.#parent && cmd.#parent !== this) {
|
|
364
|
+
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
365
|
+
}
|
|
366
|
+
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
367
|
+
if (existing) {
|
|
368
|
+
if (existing.aliases.includes(name)) {
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
existing.aliases.push(name);
|
|
372
|
+
this.#subcommandsMap.set(name, cmd);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
cmd.#name = name;
|
|
376
|
+
cmd.#parent = this;
|
|
377
|
+
this.#subcommandsList.push({ name, aliases: [], command: cmd });
|
|
378
|
+
this.#subcommandsMap.set(name, cmd);
|
|
379
|
+
}
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
async run(params) {
|
|
383
|
+
const { argv, envs, reporter } = params;
|
|
384
|
+
try {
|
|
385
|
+
const routeResult = this.#route(argv);
|
|
386
|
+
const { chain } = routeResult;
|
|
387
|
+
const leafCommand = chain[chain.length - 1];
|
|
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 }));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (ctx.controls.version) {
|
|
404
|
+
console.log(leafCommand.#version);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
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);
|
|
415
|
+
const actionParams = {
|
|
416
|
+
ctx: parseResult.ctx,
|
|
417
|
+
opts: parseResult.opts,
|
|
418
|
+
args: parseResult.args,
|
|
419
|
+
rawArgs: parseResult.rawArgs,
|
|
420
|
+
};
|
|
421
|
+
if (leafCommand.#action) {
|
|
422
|
+
await leafCommand.#runAction(actionParams);
|
|
423
|
+
}
|
|
424
|
+
else if (leafCommand.#subcommandsList.length > 0) {
|
|
425
|
+
const helpColor = leafCommand.#resolveHelpColorFromTailArgv(presetResult.tailArgv, ctx.envs);
|
|
426
|
+
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
if (err instanceof CommanderError) {
|
|
434
|
+
console.error(err.format());
|
|
435
|
+
process.exit(2);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
throw err;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async parse(params) {
|
|
442
|
+
const { argv, envs, reporter } = params;
|
|
443
|
+
const routeResult = this.#route(argv);
|
|
444
|
+
const { chain } = routeResult;
|
|
445
|
+
const leafCommand = chain[chain.length - 1];
|
|
446
|
+
const ctx = this.#createContext({
|
|
447
|
+
chain,
|
|
448
|
+
cmds: routeResult.cmds,
|
|
449
|
+
envs,
|
|
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);
|
|
463
|
+
}
|
|
464
|
+
formatHelp() {
|
|
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
|
+
}
|
|
484
|
+
const commandPath = this.#getCommandPath();
|
|
485
|
+
let usage = `Usage: ${commandPath}`;
|
|
486
|
+
if (allOptions.length > 0)
|
|
487
|
+
usage += ' [options]';
|
|
488
|
+
if (this.#subcommandsList.length > 0)
|
|
489
|
+
usage += ' [command]';
|
|
490
|
+
for (const arg of this.#arguments) {
|
|
491
|
+
if (arg.kind === 'required') {
|
|
492
|
+
usage += ` <${arg.name}>`;
|
|
493
|
+
}
|
|
494
|
+
else if (arg.kind === 'optional') {
|
|
495
|
+
usage += ` [${arg.name}]`;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
usage += ` [${arg.name}...]`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
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);
|
|
556
|
+
lines.push('');
|
|
557
|
+
if (helpData.options.length > 0) {
|
|
558
|
+
lines.push('Options:');
|
|
559
|
+
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
560
|
+
for (const { sig, desc } of helpData.options) {
|
|
561
|
+
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
562
|
+
lines.push(` ${sig}${padding}${desc}`);
|
|
563
|
+
}
|
|
564
|
+
lines.push('');
|
|
565
|
+
}
|
|
566
|
+
if (helpData.commands.length > 0) {
|
|
567
|
+
lines.push('Commands:');
|
|
568
|
+
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
569
|
+
for (const { name, desc } of helpData.commands) {
|
|
570
|
+
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
571
|
+
lines.push(` ${name}${padding}${desc}`);
|
|
572
|
+
}
|
|
573
|
+
lines.push('');
|
|
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
|
+
}
|
|
619
|
+
return lines.join('\n');
|
|
620
|
+
}
|
|
621
|
+
getCompletionMeta() {
|
|
622
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
623
|
+
const options = [];
|
|
624
|
+
for (const opt of allOptions) {
|
|
625
|
+
options.push({
|
|
626
|
+
long: opt.long,
|
|
627
|
+
short: opt.short,
|
|
628
|
+
desc: opt.desc,
|
|
629
|
+
takesValue: opt.args !== 'none',
|
|
630
|
+
choices: opt.choices,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
name: this.#name,
|
|
635
|
+
desc: this.#desc,
|
|
636
|
+
aliases: [],
|
|
637
|
+
options,
|
|
638
|
+
subcommands: this.#subcommandsList.map(entry => {
|
|
639
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
640
|
+
return {
|
|
641
|
+
...subMeta,
|
|
642
|
+
name: entry.name,
|
|
643
|
+
aliases: entry.aliases,
|
|
644
|
+
};
|
|
645
|
+
}),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
#findSubcommandEntry(token) {
|
|
649
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
650
|
+
}
|
|
651
|
+
#route(argv) {
|
|
652
|
+
const chain = [this];
|
|
653
|
+
const cmds = [];
|
|
654
|
+
let current = this;
|
|
655
|
+
let idx = 0;
|
|
656
|
+
while (idx < argv.length) {
|
|
657
|
+
const token = argv[idx];
|
|
658
|
+
if (token.startsWith('-'))
|
|
659
|
+
break;
|
|
660
|
+
const entry = current.#findSubcommandEntry(token);
|
|
661
|
+
if (!entry)
|
|
662
|
+
break;
|
|
663
|
+
current = entry.command;
|
|
664
|
+
cmds.push(token);
|
|
665
|
+
chain.push(current);
|
|
666
|
+
idx += 1;
|
|
667
|
+
}
|
|
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;
|
|
841
|
+
}
|
|
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) {
|
|
1042
|
+
const consumedTokens = new Map();
|
|
1043
|
+
let remaining = [...tokens];
|
|
1044
|
+
const shadowed = new Set();
|
|
1045
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
1046
|
+
const cmd = chain[i];
|
|
1047
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1048
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
1049
|
+
consumedTokens.set(cmd, result.consumed);
|
|
1050
|
+
remaining = result.remaining;
|
|
1051
|
+
for (const opt of cmd.#options) {
|
|
1052
|
+
shadowed.add(opt.long);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const argTokens = [];
|
|
1056
|
+
for (const token of remaining) {
|
|
1057
|
+
if (token.type !== 'none') {
|
|
1058
|
+
const leafCommand = chain[chain.length - 1];
|
|
1059
|
+
throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
1060
|
+
}
|
|
1061
|
+
argTokens.push(token);
|
|
1062
|
+
}
|
|
1063
|
+
return { consumedTokens, argTokens };
|
|
1064
|
+
}
|
|
1065
|
+
#shift(tokens, shadowed, allOptions) {
|
|
1066
|
+
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
1067
|
+
const optionByLong = new Map();
|
|
1068
|
+
const optionByShort = new Map();
|
|
1069
|
+
for (const opt of effectiveOptions) {
|
|
1070
|
+
optionByLong.set(opt.long, opt);
|
|
1071
|
+
if (opt.short) {
|
|
1072
|
+
optionByShort.set(opt.short, opt);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const consumed = [];
|
|
1076
|
+
const remaining = [];
|
|
1077
|
+
let i = 0;
|
|
1078
|
+
while (i < tokens.length) {
|
|
1079
|
+
const token = tokens[i];
|
|
1080
|
+
if (token.type === 'long') {
|
|
1081
|
+
const opt = optionByLong.get(token.name);
|
|
1082
|
+
if (opt) {
|
|
1083
|
+
consumed.push(token);
|
|
1084
|
+
if (opt.args === 'required') {
|
|
1085
|
+
if (!token.resolved.includes('=') && i + 1 < tokens.length) {
|
|
1086
|
+
i += 1;
|
|
1087
|
+
consumed.push(tokens[i]);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
else if (opt.args === 'variadic') {
|
|
1091
|
+
if (!token.resolved.includes('=')) {
|
|
1092
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1093
|
+
i += 1;
|
|
1094
|
+
consumed.push(tokens[i]);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
i += 1;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
remaining.push(token);
|
|
1102
|
+
i += 1;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (token.type === 'short') {
|
|
1106
|
+
const opt = optionByShort.get(token.name);
|
|
1107
|
+
if (opt) {
|
|
1108
|
+
consumed.push(token);
|
|
1109
|
+
if (opt.args === 'required') {
|
|
1110
|
+
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1111
|
+
i += 1;
|
|
1112
|
+
consumed.push(tokens[i]);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
else if (opt.args === 'variadic') {
|
|
1116
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1117
|
+
i += 1;
|
|
1118
|
+
consumed.push(tokens[i]);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
i += 1;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
remaining.push(token);
|
|
1125
|
+
i += 1;
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
remaining.push(token);
|
|
1129
|
+
i += 1;
|
|
1130
|
+
}
|
|
1131
|
+
return { consumed, remaining };
|
|
1132
|
+
}
|
|
1133
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
1134
|
+
const { consumedTokens, argTokens } = resolveResult;
|
|
1135
|
+
const leafCommand = chain[chain.length - 1];
|
|
1136
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
1137
|
+
const optsMap = new Map();
|
|
1138
|
+
for (const cmd of chain) {
|
|
1139
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1140
|
+
const tokens = consumedTokens.get(cmd) ?? [];
|
|
1141
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
1142
|
+
optsMap.set(cmd, opts);
|
|
1143
|
+
for (const opt of policy.mergedOptions) {
|
|
1144
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
1145
|
+
opt.apply(opts[opt.long], ctx);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
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
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
1157
|
+
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
1158
|
+
const parseCtx = {
|
|
1159
|
+
...ctx,
|
|
1160
|
+
sources: this.#freezeInputSources(ctx.sources),
|
|
1161
|
+
};
|
|
1162
|
+
return { ctx: parseCtx, opts: leafLocalOpts, args, rawArgs };
|
|
1163
|
+
}
|
|
1164
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
1165
|
+
const opts = {};
|
|
1166
|
+
let sawColorToken = false;
|
|
1167
|
+
for (const opt of allOptions) {
|
|
1168
|
+
if (opt.default !== undefined) {
|
|
1169
|
+
opts[opt.long] = opt.default;
|
|
1170
|
+
}
|
|
1171
|
+
else if (opt.type === 'boolean' && opt.args === 'none') {
|
|
1172
|
+
opts[opt.long] = false;
|
|
1173
|
+
}
|
|
1174
|
+
else if (opt.args === 'variadic') {
|
|
1175
|
+
opts[opt.long] = [];
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const optionByLong = new Map();
|
|
1179
|
+
const optionByShort = new Map();
|
|
1180
|
+
for (const opt of allOptions) {
|
|
1181
|
+
optionByLong.set(opt.long, opt);
|
|
1182
|
+
if (opt.short) {
|
|
1183
|
+
optionByShort.set(opt.short, opt);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
let i = 0;
|
|
1187
|
+
while (i < tokens.length) {
|
|
1188
|
+
const token = tokens[i];
|
|
1189
|
+
const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
|
|
1190
|
+
if (!opt) {
|
|
1191
|
+
i += 1;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (opt.long === 'color') {
|
|
1195
|
+
sawColorToken = true;
|
|
1196
|
+
}
|
|
1197
|
+
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
1198
|
+
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
1199
|
+
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
1200
|
+
}
|
|
1201
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
1202
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1203
|
+
if (eqIdx !== -1) {
|
|
1204
|
+
const value = token.resolved.slice(eqIdx + 1);
|
|
1205
|
+
if (value === 'true') {
|
|
1206
|
+
opts[opt.long] = true;
|
|
1207
|
+
}
|
|
1208
|
+
else if (value === 'false') {
|
|
1209
|
+
opts[opt.long] = false;
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
opts[opt.long] = true;
|
|
1217
|
+
}
|
|
1218
|
+
i += 1;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (opt.args === 'required') {
|
|
1222
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1223
|
+
let rawValue;
|
|
1224
|
+
if (eqIdx !== -1) {
|
|
1225
|
+
rawValue = token.resolved.slice(eqIdx + 1);
|
|
1226
|
+
}
|
|
1227
|
+
else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1228
|
+
rawValue = tokens[i + 1].original;
|
|
1229
|
+
i += 1;
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
|
|
1233
|
+
}
|
|
1234
|
+
opts[opt.long] = this.#convertValue(opt, rawValue);
|
|
1235
|
+
i += 1;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (opt.args === 'variadic') {
|
|
1239
|
+
const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
|
|
1240
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1241
|
+
if (eqIdx !== -1) {
|
|
1242
|
+
values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1246
|
+
i += 1;
|
|
1247
|
+
values.push(this.#convertValue(opt, tokens[i].original));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
opts[opt.long] = values;
|
|
1251
|
+
i += 1;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
i += 1;
|
|
1255
|
+
}
|
|
1256
|
+
for (const opt of allOptions) {
|
|
1257
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
1258
|
+
throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
for (const opt of allOptions) {
|
|
1262
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
1263
|
+
const value = opts[opt.long];
|
|
1264
|
+
const values = Array.isArray(value) ? value : [value];
|
|
1265
|
+
const choices = opt.choices;
|
|
1266
|
+
for (const v of values) {
|
|
1267
|
+
if (!choices.includes(v)) {
|
|
1268
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
|
|
1274
|
+
opts['color'] = false;
|
|
1275
|
+
}
|
|
1276
|
+
return opts;
|
|
1277
|
+
}
|
|
1278
|
+
#convertValue(opt, rawValue) {
|
|
1279
|
+
if (opt.coerce) {
|
|
1280
|
+
return opt.coerce(rawValue);
|
|
1281
|
+
}
|
|
1282
|
+
if (opt.type === 'number') {
|
|
1283
|
+
const num = Number(rawValue);
|
|
1284
|
+
if (Number.isNaN(num)) {
|
|
1285
|
+
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
1286
|
+
}
|
|
1287
|
+
return num;
|
|
1288
|
+
}
|
|
1289
|
+
return rawValue;
|
|
1290
|
+
}
|
|
1291
|
+
#parseArguments(rawArgs) {
|
|
1292
|
+
const argumentDefs = this.#arguments;
|
|
1293
|
+
const args = {};
|
|
1294
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
1295
|
+
if (rawArgs.length < requiredCount) {
|
|
1296
|
+
const missing = argumentDefs
|
|
1297
|
+
.filter(a => a.kind === 'required')
|
|
1298
|
+
.slice(rawArgs.length)
|
|
1299
|
+
.map(a => a.name);
|
|
1300
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
1301
|
+
}
|
|
1302
|
+
let index = 0;
|
|
1303
|
+
for (const def of argumentDefs) {
|
|
1304
|
+
if (def.kind === 'variadic') {
|
|
1305
|
+
const rest = rawArgs.slice(index);
|
|
1306
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
1307
|
+
index = rawArgs.length;
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
const raw = rawArgs[index];
|
|
1311
|
+
if (raw === undefined) {
|
|
1312
|
+
if (def.kind === 'optional') {
|
|
1313
|
+
args[def.name] = def.default ?? undefined;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
1319
|
+
index += 1;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
1323
|
+
if (!hasVariadic && index < rawArgs.length) {
|
|
1324
|
+
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
1325
|
+
}
|
|
1326
|
+
return { args, rawArgs };
|
|
1327
|
+
}
|
|
1328
|
+
#convertArgument(def, raw) {
|
|
1329
|
+
if (def.coerce) {
|
|
1330
|
+
try {
|
|
1331
|
+
return def.coerce(raw);
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (def.type === 'number') {
|
|
1338
|
+
const n = Number(raw);
|
|
1339
|
+
if (Number.isNaN(n)) {
|
|
1340
|
+
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
1341
|
+
}
|
|
1342
|
+
return n;
|
|
1343
|
+
}
|
|
1344
|
+
return raw;
|
|
1345
|
+
}
|
|
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() {
|
|
1353
|
+
const optionMap = new Map();
|
|
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);
|
|
1370
|
+
}
|
|
1371
|
+
if (this.#builtin.option.logColorful && !hasUserLogColorful) {
|
|
1372
|
+
optionMap.set('logColorful', logColorfulOption);
|
|
1373
|
+
}
|
|
1374
|
+
for (const opt of this.#options) {
|
|
1375
|
+
optionMap.set(opt.long, opt);
|
|
1376
|
+
}
|
|
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;
|
|
1387
|
+
}
|
|
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) {
|
|
1396
|
+
const mergedByLong = new Map();
|
|
1397
|
+
for (const cmd of chain) {
|
|
1398
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1399
|
+
for (const opt of policy.mergedOptions) {
|
|
1400
|
+
mergedByLong.set(opt.long, opt);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const shortMap = new Map();
|
|
1404
|
+
for (const opt of mergedByLong.values()) {
|
|
1405
|
+
if (!opt.short)
|
|
1406
|
+
continue;
|
|
1407
|
+
const existingLong = shortMap.get(opt.short);
|
|
1408
|
+
if (existingLong && existingLong !== opt.long) {
|
|
1409
|
+
throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
|
|
1410
|
+
}
|
|
1411
|
+
shortMap.set(opt.short, opt.long);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
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
|
+
}
|
|
1418
|
+
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
1419
|
+
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
1420
|
+
}
|
|
1421
|
+
if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
|
|
1422
|
+
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
|
|
1423
|
+
}
|
|
1424
|
+
if (opt.long.startsWith('no')) {
|
|
1425
|
+
throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
|
|
1426
|
+
}
|
|
1427
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
1428
|
+
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
1429
|
+
}
|
|
1430
|
+
if (opt.required && opt.default !== undefined) {
|
|
1431
|
+
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
1432
|
+
}
|
|
1433
|
+
if (opt.type === 'boolean' && opt.required) {
|
|
1434
|
+
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
#checkOptionUniqueness(opt) {
|
|
1438
|
+
if (this.#options.some(o => o.long === opt.long)) {
|
|
1439
|
+
throw new CommanderError('OptionConflict', `option "--${opt.long}" is already defined`, this.#getCommandPath());
|
|
1440
|
+
}
|
|
1441
|
+
if (opt.short && this.#options.some(o => o.short === opt.short)) {
|
|
1442
|
+
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is already defined`, this.#getCommandPath());
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
#validateArgumentConfig(arg) {
|
|
1446
|
+
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
1447
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
1448
|
+
}
|
|
1449
|
+
if (arg.kind === 'variadic') {
|
|
1450
|
+
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
1451
|
+
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (this.#arguments.length > 0) {
|
|
1455
|
+
const last = this.#arguments[this.#arguments.length - 1];
|
|
1456
|
+
if (last.kind === 'variadic') {
|
|
1457
|
+
throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (arg.kind === 'required') {
|
|
1461
|
+
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
|
|
1462
|
+
if (hasOptional) {
|
|
1463
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
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
|
+
}
|
|
1482
|
+
async #runAction(params) {
|
|
1483
|
+
if (!this.#action)
|
|
1484
|
+
return;
|
|
1485
|
+
try {
|
|
1486
|
+
await this.#action(params);
|
|
1487
|
+
}
|
|
1488
|
+
catch (err) {
|
|
1489
|
+
if (err instanceof Error) {
|
|
1490
|
+
console.error(`Error: ${err.message}`);
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
console.error('Error: action failed');
|
|
1494
|
+
}
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
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;
|
|
1517
|
+
}
|
|
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());
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
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
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
#getCommandPath() {
|
|
1545
|
+
const parts = [];
|
|
1546
|
+
let current = this;
|
|
1547
|
+
while (current) {
|
|
1548
|
+
if (current.#name) {
|
|
1549
|
+
parts.unshift(current.#name);
|
|
1550
|
+
}
|
|
1551
|
+
current = current.#parent;
|
|
1552
|
+
}
|
|
1553
|
+
return parts.join(' ') || this.#name;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
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
|
+
|
|
1707
|
+
function camelToKebabCase(str) {
|
|
1708
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1709
|
+
}
|
|
1710
|
+
class CompletionCommand extends Command {
|
|
1711
|
+
constructor(root, config = {}) {
|
|
1712
|
+
const programName = config.programName ?? root.name ?? 'program';
|
|
1713
|
+
const paths = {
|
|
1714
|
+
...createDefaultCompletionPaths(programName),
|
|
1715
|
+
...config.paths,
|
|
1716
|
+
};
|
|
1717
|
+
super({ desc: 'Generate shell completion script' });
|
|
1718
|
+
this.option({
|
|
1719
|
+
long: 'bash',
|
|
1720
|
+
type: 'boolean',
|
|
1721
|
+
args: 'none',
|
|
1722
|
+
desc: 'Generate Bash completion script',
|
|
1723
|
+
})
|
|
1724
|
+
.option({
|
|
1725
|
+
long: 'fish',
|
|
1726
|
+
type: 'boolean',
|
|
1727
|
+
args: 'none',
|
|
1728
|
+
desc: 'Generate Fish completion script',
|
|
1729
|
+
})
|
|
1730
|
+
.option({
|
|
1731
|
+
long: 'pwsh',
|
|
1732
|
+
type: 'boolean',
|
|
1733
|
+
args: 'none',
|
|
1734
|
+
desc: 'Generate PowerShell completion script',
|
|
1735
|
+
})
|
|
1736
|
+
.option({
|
|
1737
|
+
long: 'write',
|
|
1738
|
+
short: 'w',
|
|
1739
|
+
type: 'string',
|
|
1740
|
+
args: 'required',
|
|
1741
|
+
desc: 'Write to file (use shell default path if empty)',
|
|
1742
|
+
default: undefined,
|
|
1743
|
+
})
|
|
1744
|
+
.action(({ opts }) => {
|
|
1745
|
+
const meta = root.getCompletionMeta();
|
|
1746
|
+
const selectedShells = [
|
|
1747
|
+
opts['bash'] && 'bash',
|
|
1748
|
+
opts['fish'] && 'fish',
|
|
1749
|
+
opts['pwsh'] && 'pwsh',
|
|
1750
|
+
].filter(Boolean);
|
|
1751
|
+
if (selectedShells.length === 0) {
|
|
1752
|
+
console.error('Please specify a shell: --bash, --fish, or --pwsh');
|
|
1753
|
+
process.exit(1);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (selectedShells.length > 1) {
|
|
1757
|
+
console.error('Please specify only one shell option');
|
|
1758
|
+
process.exit(1);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const shell = selectedShells[0];
|
|
1762
|
+
let script;
|
|
1763
|
+
switch (shell) {
|
|
1764
|
+
case 'bash':
|
|
1765
|
+
script = new BashCompletion(meta, programName).generate();
|
|
1766
|
+
break;
|
|
1767
|
+
case 'fish':
|
|
1768
|
+
script = new FishCompletion(meta, programName).generate();
|
|
1769
|
+
break;
|
|
1770
|
+
case 'pwsh':
|
|
1771
|
+
script = new PwshCompletion(meta, programName).generate();
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
const writeOpt = opts['write'];
|
|
1775
|
+
if (writeOpt !== undefined) {
|
|
1776
|
+
const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
|
|
1777
|
+
const expandedPath = expandHome(filePath);
|
|
1778
|
+
const dir = path__namespace.dirname(expandedPath);
|
|
1779
|
+
if (!fs__namespace.existsSync(dir)) {
|
|
1780
|
+
fs__namespace.mkdirSync(dir, { recursive: true });
|
|
1781
|
+
}
|
|
1782
|
+
fs__namespace.writeFileSync(expandedPath, script, 'utf-8');
|
|
1783
|
+
console.log(`Completion script written to: ${expandedPath}`);
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
console.log(script);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
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
|
+
}
|
|
1798
|
+
function expandHome(filepath) {
|
|
1799
|
+
if (filepath.startsWith('~/') || filepath === '~') {
|
|
1800
|
+
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
1801
|
+
return filepath.replace(/^~/, home);
|
|
1802
|
+
}
|
|
1803
|
+
return filepath;
|
|
1804
|
+
}
|
|
1805
|
+
class BashCompletion {
|
|
1806
|
+
#meta;
|
|
1807
|
+
#programName;
|
|
1808
|
+
constructor(meta, programName) {
|
|
1809
|
+
this.#meta = meta;
|
|
1810
|
+
this.#programName = programName;
|
|
1811
|
+
}
|
|
1812
|
+
generate() {
|
|
1813
|
+
const funcName = `_${this.#sanitizeName(this.#programName)}_completions`;
|
|
1814
|
+
const lines = [
|
|
1815
|
+
`# Bash completion for ${this.#programName}`,
|
|
1816
|
+
'# Generated by @guanghechen/commander',
|
|
1817
|
+
'',
|
|
1818
|
+
`${funcName}() {`,
|
|
1819
|
+
' local cur prev words cword',
|
|
1820
|
+
' _init_completion || return',
|
|
1821
|
+
'',
|
|
1822
|
+
...this.#generateCommandCase(this.#meta, 1),
|
|
1823
|
+
'',
|
|
1824
|
+
' COMPREPLY=($(compgen -W "$opts" -- "$cur"))',
|
|
1825
|
+
'}',
|
|
1826
|
+
'',
|
|
1827
|
+
`complete -F ${funcName} ${this.#programName}`,
|
|
1828
|
+
'',
|
|
1829
|
+
];
|
|
1830
|
+
return lines.join('\n');
|
|
1831
|
+
}
|
|
1832
|
+
#generateCommandCase(cmd, depth) {
|
|
1833
|
+
const indent = ' '.repeat(depth);
|
|
1834
|
+
const lines = [];
|
|
1835
|
+
const optParts = [];
|
|
1836
|
+
for (const opt of cmd.options) {
|
|
1837
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1838
|
+
if (opt.short)
|
|
1839
|
+
optParts.push(`-${opt.short}`);
|
|
1840
|
+
optParts.push(`--${kebabLong}`);
|
|
1841
|
+
if (!opt.takesValue) {
|
|
1842
|
+
optParts.push(`--no-${kebabLong}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
1846
|
+
const allOpts = [...optParts, ...subParts].join(' ');
|
|
1847
|
+
if (cmd.subcommands.length > 0) {
|
|
1848
|
+
lines.push(`${indent}case "\${words[${depth}]}" in`);
|
|
1849
|
+
for (const sub of cmd.subcommands) {
|
|
1850
|
+
const pattern = [sub.name, ...sub.aliases].join('|');
|
|
1851
|
+
lines.push(`${indent} ${pattern})`);
|
|
1852
|
+
lines.push(...this.#generateCommandCase(sub, depth + 1));
|
|
1853
|
+
lines.push(`${indent} ;;`);
|
|
1854
|
+
}
|
|
1855
|
+
lines.push(`${indent} *)`);
|
|
1856
|
+
lines.push(`${indent} opts="${allOpts}"`);
|
|
1857
|
+
lines.push(`${indent} ;;`);
|
|
1858
|
+
lines.push(`${indent}esac`);
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
lines.push(`${indent}opts="${allOpts}"`);
|
|
1862
|
+
}
|
|
1863
|
+
return lines;
|
|
1864
|
+
}
|
|
1865
|
+
#sanitizeName(name) {
|
|
1866
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
class FishCompletion {
|
|
1870
|
+
#meta;
|
|
1871
|
+
#programName;
|
|
1872
|
+
constructor(meta, programName) {
|
|
1873
|
+
this.#meta = meta;
|
|
1874
|
+
this.#programName = programName;
|
|
1875
|
+
}
|
|
1876
|
+
generate() {
|
|
1877
|
+
const lines = [
|
|
1878
|
+
`# Fish completion for ${this.#programName}`,
|
|
1879
|
+
'# Generated by @guanghechen/commander',
|
|
1880
|
+
'',
|
|
1881
|
+
...this.#generateCommandCompletions(this.#meta, []),
|
|
1882
|
+
'',
|
|
1883
|
+
];
|
|
1884
|
+
return lines.join('\n');
|
|
1885
|
+
}
|
|
1886
|
+
#generateCommandCompletions(cmd, parentPath) {
|
|
1887
|
+
const lines = [];
|
|
1888
|
+
const isRoot = parentPath.length === 0;
|
|
1889
|
+
const condition = this.#buildCondition(parentPath);
|
|
1890
|
+
for (const opt of cmd.options) {
|
|
1891
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1892
|
+
let line = `complete -c ${this.#programName}`;
|
|
1893
|
+
if (condition)
|
|
1894
|
+
line += ` -n '${condition}'`;
|
|
1895
|
+
if (opt.short)
|
|
1896
|
+
line += ` -s ${opt.short}`;
|
|
1897
|
+
line += ` -l ${kebabLong}`;
|
|
1898
|
+
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1899
|
+
if (opt.choices && opt.choices.length > 0) {
|
|
1900
|
+
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
1901
|
+
}
|
|
1902
|
+
lines.push(line);
|
|
1903
|
+
if (!opt.takesValue) {
|
|
1904
|
+
let noLine = `complete -c ${this.#programName}`;
|
|
1905
|
+
if (condition)
|
|
1906
|
+
noLine += ` -n '${condition}'`;
|
|
1907
|
+
noLine += ` -l no-${kebabLong}`;
|
|
1908
|
+
noLine += ` -d '${this.#escape(opt.desc)}'`;
|
|
1909
|
+
lines.push(noLine);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
for (const sub of cmd.subcommands) {
|
|
1913
|
+
let line = `complete -c ${this.#programName}`;
|
|
1914
|
+
if (isRoot) {
|
|
1915
|
+
line += ' -n __fish_use_subcommand';
|
|
1916
|
+
}
|
|
1917
|
+
else if (condition) {
|
|
1918
|
+
line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1919
|
+
}
|
|
1920
|
+
line += ` -a ${sub.name}`;
|
|
1921
|
+
line += ` -d '${this.#escape(sub.desc)}'`;
|
|
1922
|
+
lines.push(line);
|
|
1923
|
+
for (const alias of sub.aliases) {
|
|
1924
|
+
let aliasLine = `complete -c ${this.#programName}`;
|
|
1925
|
+
if (isRoot) {
|
|
1926
|
+
aliasLine += ' -n __fish_use_subcommand';
|
|
1927
|
+
}
|
|
1928
|
+
else if (condition) {
|
|
1929
|
+
aliasLine += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1930
|
+
}
|
|
1931
|
+
aliasLine += ` -a ${alias}`;
|
|
1932
|
+
aliasLine += ` -d 'Alias for ${sub.name}'`;
|
|
1933
|
+
lines.push(aliasLine);
|
|
1934
|
+
}
|
|
1935
|
+
const newPath = [...parentPath, sub.name];
|
|
1936
|
+
lines.push(...this.#generateCommandCompletions(sub, newPath));
|
|
1937
|
+
}
|
|
1938
|
+
return lines;
|
|
1939
|
+
}
|
|
1940
|
+
#buildCondition(path) {
|
|
1941
|
+
if (path.length === 0)
|
|
1942
|
+
return '';
|
|
1943
|
+
return `__fish_seen_subcommand_from ${path[path.length - 1]}`;
|
|
1944
|
+
}
|
|
1945
|
+
#getSubcommandNames(cmd) {
|
|
1946
|
+
return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
1947
|
+
}
|
|
1948
|
+
#escape(s) {
|
|
1949
|
+
return s.replace(/'/g, "\\'");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
class PwshCompletion {
|
|
1953
|
+
#meta;
|
|
1954
|
+
#programName;
|
|
1955
|
+
constructor(meta, programName) {
|
|
1956
|
+
this.#meta = meta;
|
|
1957
|
+
this.#programName = programName;
|
|
1958
|
+
}
|
|
1959
|
+
generate() {
|
|
1960
|
+
const lines = [
|
|
1961
|
+
`# PowerShell completion for ${this.#programName}`,
|
|
1962
|
+
'# Generated by @guanghechen/commander',
|
|
1963
|
+
'',
|
|
1964
|
+
`Register-ArgumentCompleter -Native -CommandName ${this.#programName} -ScriptBlock {`,
|
|
1965
|
+
' param($wordToComplete, $commandAst, $cursorPosition)',
|
|
1966
|
+
'',
|
|
1967
|
+
' $commands = @{',
|
|
1968
|
+
this.#generateCommandHash(this.#meta, ' '),
|
|
1969
|
+
' }',
|
|
1970
|
+
'',
|
|
1971
|
+
' $words = $commandAst.CommandElements | ForEach-Object { $_.ToString() }',
|
|
1972
|
+
' $current = $wordToComplete',
|
|
1973
|
+
'',
|
|
1974
|
+
' # Find current command context',
|
|
1975
|
+
' $cmd = $commands',
|
|
1976
|
+
' foreach ($word in $words[1..($words.Count - 1)]) {',
|
|
1977
|
+
' if ($word.StartsWith("-")) { continue }',
|
|
1978
|
+
' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
|
|
1979
|
+
' $cmd = $cmd.subcommands[$word]',
|
|
1980
|
+
' }',
|
|
1981
|
+
' }',
|
|
1982
|
+
'',
|
|
1983
|
+
' # Generate completions',
|
|
1984
|
+
' $completions = @()',
|
|
1985
|
+
'',
|
|
1986
|
+
' # Options',
|
|
1987
|
+
' if ($current.StartsWith("-")) {',
|
|
1988
|
+
' foreach ($opt in $cmd.options) {',
|
|
1989
|
+
' if ("--$($opt.long)" -like "$current*") {',
|
|
1990
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1991
|
+
' "--$($opt.long)",',
|
|
1992
|
+
' $opt.long,',
|
|
1993
|
+
' "ParameterName",',
|
|
1994
|
+
' $opt.desc',
|
|
1995
|
+
' )',
|
|
1996
|
+
' }',
|
|
1997
|
+
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
1998
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1999
|
+
' "--no-$($opt.long)",',
|
|
2000
|
+
' "no-$($opt.long)",',
|
|
2001
|
+
' "ParameterName",',
|
|
2002
|
+
' $opt.desc',
|
|
2003
|
+
' )',
|
|
2004
|
+
' }',
|
|
2005
|
+
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
2006
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2007
|
+
' "-$($opt.short)",',
|
|
2008
|
+
' $opt.short,',
|
|
2009
|
+
' "ParameterName",',
|
|
2010
|
+
' $opt.desc',
|
|
2011
|
+
' )',
|
|
2012
|
+
' }',
|
|
2013
|
+
' }',
|
|
2014
|
+
' }',
|
|
2015
|
+
'',
|
|
2016
|
+
' # Subcommands',
|
|
2017
|
+
' if ($cmd.subcommands) {',
|
|
2018
|
+
' foreach ($sub in $cmd.subcommands.Keys) {',
|
|
2019
|
+
' if ($sub -like "$current*") {',
|
|
2020
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2021
|
+
' $sub,',
|
|
2022
|
+
' $sub,',
|
|
2023
|
+
' "Command",',
|
|
2024
|
+
' $cmd.subcommands[$sub].desc',
|
|
2025
|
+
' )',
|
|
2026
|
+
' }',
|
|
2027
|
+
' }',
|
|
2028
|
+
' }',
|
|
2029
|
+
'',
|
|
2030
|
+
' return $completions',
|
|
2031
|
+
'}',
|
|
2032
|
+
'',
|
|
2033
|
+
];
|
|
2034
|
+
return lines.join('\n');
|
|
2035
|
+
}
|
|
2036
|
+
#generateCommandHash(cmd, indent) {
|
|
2037
|
+
const lines = [];
|
|
2038
|
+
lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
|
|
2039
|
+
lines.push(`${indent}options = @(`);
|
|
2040
|
+
for (const opt of cmd.options) {
|
|
2041
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
2042
|
+
lines.push(`${indent} @{`);
|
|
2043
|
+
if (opt.short)
|
|
2044
|
+
lines.push(`${indent} short = '${opt.short}'`);
|
|
2045
|
+
lines.push(`${indent} long = '${kebabLong}'`);
|
|
2046
|
+
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
2047
|
+
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
2048
|
+
if (opt.choices) {
|
|
2049
|
+
lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
|
|
2050
|
+
}
|
|
2051
|
+
lines.push(`${indent} }`);
|
|
2052
|
+
}
|
|
2053
|
+
lines.push(`${indent})`);
|
|
2054
|
+
if (cmd.subcommands.length > 0) {
|
|
2055
|
+
lines.push(`${indent}subcommands = @{`);
|
|
2056
|
+
for (const sub of cmd.subcommands) {
|
|
2057
|
+
lines.push(`${indent} '${sub.name}' = @{`);
|
|
2058
|
+
lines.push(this.#generateCommandHash(sub, `${indent} `));
|
|
2059
|
+
lines.push(`${indent} }`);
|
|
2060
|
+
for (const alias of sub.aliases) {
|
|
2061
|
+
lines.push(`${indent} '${alias}' = @{`);
|
|
2062
|
+
lines.push(this.#generateCommandHash(sub, `${indent} `));
|
|
2063
|
+
lines.push(`${indent} }`);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
lines.push(`${indent}}`);
|
|
2067
|
+
}
|
|
2068
|
+
return lines.join('\n');
|
|
2069
|
+
}
|
|
2070
|
+
#escape(s) {
|
|
2071
|
+
return s.replace(/'/g, "''");
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
exports.BashCompletion = BashCompletion;
|
|
2076
|
+
exports.Coerce = Coerce;
|
|
2077
|
+
exports.Command = Command;
|
|
2078
|
+
exports.CommanderError = CommanderError;
|
|
2079
|
+
exports.CompletionCommand = CompletionCommand;
|
|
2080
|
+
exports.FishCompletion = FishCompletion;
|
|
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;
|
|
2088
|
+
exports.logLevelOption = logLevelOption;
|
|
2089
|
+
exports.silentOption = silentOption;
|