@guanghechen/commander 4.7.5 → 4.7.7
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 +17 -0
- package/README.md +46 -39
- package/lib/cjs/browser.cjs +428 -175
- package/lib/cjs/node.cjs +427 -174
- package/lib/esm/browser.mjs +428 -175
- package/lib/esm/node.mjs +427 -174
- package/lib/schema/preset.config.schema.json +152 -0
- package/lib/schema/preset.schema.json +152 -0
- package/lib/types/browser.d.ts +43 -7
- package/lib/types/node.d.ts +43 -7
- package/package.json +1 -1
- package/lib/cjs/index.cjs +0 -1237
- package/lib/esm/index.mjs +0 -1208
- package/lib/types/index.d.ts +0 -384
package/lib/esm/index.mjs
DELETED
|
@@ -1,1208 +0,0 @@
|
|
|
1
|
-
import { Reporter, LOG_LEVELS, resolveLogLevel } from '@guanghechen/reporter';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
|
|
5
|
-
class CommanderError extends Error {
|
|
6
|
-
kind;
|
|
7
|
-
commandPath;
|
|
8
|
-
constructor(kind, message, commandPath) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = 'CommanderError';
|
|
11
|
-
this.kind = kind;
|
|
12
|
-
this.commandPath = commandPath;
|
|
13
|
-
}
|
|
14
|
-
format() {
|
|
15
|
-
return `Error: ${this.message}\nRun "${this.commandPath} --help" for usage.`;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
20
|
-
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
21
|
-
function kebabToCamelCase(str) {
|
|
22
|
-
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
23
|
-
}
|
|
24
|
-
function camelToKebabCase$1(str) {
|
|
25
|
-
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
26
|
-
}
|
|
27
|
-
function tokenizeLongOption(arg, commandPath) {
|
|
28
|
-
const eqIdx = arg.indexOf('=');
|
|
29
|
-
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
30
|
-
const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
|
|
31
|
-
if (namePart.includes('_')) {
|
|
32
|
-
throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
|
|
33
|
-
}
|
|
34
|
-
const lowerName = namePart.toLowerCase();
|
|
35
|
-
if (lowerName === '--no' || lowerName === '--no-') {
|
|
36
|
-
throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
|
|
37
|
-
}
|
|
38
|
-
if (lowerName.startsWith('--no-')) {
|
|
39
|
-
if (valuePart !== '') {
|
|
40
|
-
throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
|
|
41
|
-
}
|
|
42
|
-
if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
|
|
43
|
-
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
44
|
-
}
|
|
45
|
-
const camelName = kebabToCamelCase(lowerName.slice(5));
|
|
46
|
-
return {
|
|
47
|
-
original: arg,
|
|
48
|
-
resolved: `--${camelName}=false`,
|
|
49
|
-
name: camelName,
|
|
50
|
-
type: 'long',
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
if (!LONG_OPTION_REGEX.test(lowerName)) {
|
|
54
|
-
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
55
|
-
}
|
|
56
|
-
const camelName = kebabToCamelCase(lowerName.slice(2));
|
|
57
|
-
return {
|
|
58
|
-
original: arg,
|
|
59
|
-
resolved: `--${camelName}${valuePart}`,
|
|
60
|
-
name: camelName,
|
|
61
|
-
type: 'long',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function tokenizeShortOptions(arg, commandPath) {
|
|
65
|
-
if (arg.includes('=')) {
|
|
66
|
-
throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
|
|
67
|
-
}
|
|
68
|
-
const flags = arg.slice(1);
|
|
69
|
-
return flags.split('').map(flag => ({
|
|
70
|
-
original: `-${flag}`,
|
|
71
|
-
resolved: `-${flag}`,
|
|
72
|
-
name: flag,
|
|
73
|
-
type: 'short',
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
function tokenize(argv, commandPath) {
|
|
77
|
-
const optionTokens = [];
|
|
78
|
-
const restArgs = [];
|
|
79
|
-
let passThrough = false;
|
|
80
|
-
for (const arg of argv) {
|
|
81
|
-
if (arg === '--') {
|
|
82
|
-
passThrough = true;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (passThrough) {
|
|
86
|
-
restArgs.push(arg);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (arg.startsWith('--')) {
|
|
90
|
-
optionTokens.push(tokenizeLongOption(arg, commandPath));
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (arg.startsWith('-') && arg.length > 1) {
|
|
94
|
-
optionTokens.push(...tokenizeShortOptions(arg, commandPath));
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
optionTokens.push({
|
|
98
|
-
original: arg,
|
|
99
|
-
resolved: arg,
|
|
100
|
-
name: '',
|
|
101
|
-
type: 'none',
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return { optionTokens, restArgs };
|
|
105
|
-
}
|
|
106
|
-
const BUILTIN_HELP_OPTION = {
|
|
107
|
-
long: 'help',
|
|
108
|
-
short: 'h',
|
|
109
|
-
type: 'boolean',
|
|
110
|
-
args: 'none',
|
|
111
|
-
desc: 'Show help information',
|
|
112
|
-
};
|
|
113
|
-
const BUILTIN_VERSION_OPTION = {
|
|
114
|
-
long: 'version',
|
|
115
|
-
short: 'V',
|
|
116
|
-
type: 'boolean',
|
|
117
|
-
args: 'none',
|
|
118
|
-
desc: 'Show version number',
|
|
119
|
-
};
|
|
120
|
-
class Command {
|
|
121
|
-
#name;
|
|
122
|
-
#desc;
|
|
123
|
-
#version;
|
|
124
|
-
#helpSubcommandEnabled;
|
|
125
|
-
#reporter;
|
|
126
|
-
#parent;
|
|
127
|
-
#options = [];
|
|
128
|
-
#arguments = [];
|
|
129
|
-
#subcommandsList = [];
|
|
130
|
-
#subcommandsMap = new Map();
|
|
131
|
-
#action = undefined;
|
|
132
|
-
constructor(config) {
|
|
133
|
-
this.#name = config.name ?? '';
|
|
134
|
-
this.#desc = config.desc;
|
|
135
|
-
this.#version = config.version;
|
|
136
|
-
this.#helpSubcommandEnabled = config.help ?? false;
|
|
137
|
-
this.#reporter = config.reporter;
|
|
138
|
-
}
|
|
139
|
-
get name() {
|
|
140
|
-
return this.#name || undefined;
|
|
141
|
-
}
|
|
142
|
-
get description() {
|
|
143
|
-
return this.#desc;
|
|
144
|
-
}
|
|
145
|
-
get version() {
|
|
146
|
-
return this.#version;
|
|
147
|
-
}
|
|
148
|
-
get parent() {
|
|
149
|
-
return this.#parent;
|
|
150
|
-
}
|
|
151
|
-
get options() {
|
|
152
|
-
return [...this.#options];
|
|
153
|
-
}
|
|
154
|
-
get arguments() {
|
|
155
|
-
return [...this.#arguments];
|
|
156
|
-
}
|
|
157
|
-
get subcommands() {
|
|
158
|
-
return new Map(this.#subcommandsMap);
|
|
159
|
-
}
|
|
160
|
-
option(opt) {
|
|
161
|
-
this.#validateOptionConfig(opt);
|
|
162
|
-
this.#checkOptionUniqueness(opt);
|
|
163
|
-
this.#options.push(opt);
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
argument(arg) {
|
|
167
|
-
this.#validateArgumentConfig(arg);
|
|
168
|
-
this.#arguments.push(arg);
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
action(fn) {
|
|
172
|
-
this.#action = fn;
|
|
173
|
-
return this;
|
|
174
|
-
}
|
|
175
|
-
subcommand(name, cmd) {
|
|
176
|
-
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
177
|
-
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
178
|
-
}
|
|
179
|
-
if (cmd.#parent && cmd.#parent !== this) {
|
|
180
|
-
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
181
|
-
}
|
|
182
|
-
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
183
|
-
if (existing) {
|
|
184
|
-
existing.aliases.push(name);
|
|
185
|
-
this.#subcommandsMap.set(name, cmd);
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
cmd.#name = name;
|
|
189
|
-
cmd.#parent = this;
|
|
190
|
-
this.#subcommandsList.push({ name, aliases: [], command: cmd });
|
|
191
|
-
this.#subcommandsMap.set(name, cmd);
|
|
192
|
-
}
|
|
193
|
-
return this;
|
|
194
|
-
}
|
|
195
|
-
async run(params) {
|
|
196
|
-
const { argv, envs, reporter } = params;
|
|
197
|
-
try {
|
|
198
|
-
const processedArgv = this.#processHelpSubcommand(argv);
|
|
199
|
-
const routeResult = this.#route(processedArgv);
|
|
200
|
-
const { chain, remaining } = routeResult;
|
|
201
|
-
const leafCommand = chain[chain.length - 1];
|
|
202
|
-
const rootCommand = chain[0];
|
|
203
|
-
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
204
|
-
const { optionTokens, restArgs } = tokenizeResult;
|
|
205
|
-
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
206
|
-
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
207
|
-
if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
208
|
-
console.log(leafCommand.formatHelp());
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
|
|
212
|
-
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
213
|
-
console.log(leafCommand.#version);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
218
|
-
const ctx = {
|
|
219
|
-
cmd: leafCommand,
|
|
220
|
-
envs,
|
|
221
|
-
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
222
|
-
argv,
|
|
223
|
-
};
|
|
224
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
225
|
-
const actionParams = {
|
|
226
|
-
ctx: parseResult.ctx,
|
|
227
|
-
opts: parseResult.opts,
|
|
228
|
-
args: parseResult.args,
|
|
229
|
-
rawArgs: parseResult.rawArgs,
|
|
230
|
-
};
|
|
231
|
-
if (leafCommand.#action) {
|
|
232
|
-
await leafCommand.#runAction(actionParams);
|
|
233
|
-
}
|
|
234
|
-
else if (leafCommand.#subcommandsList.length > 0) {
|
|
235
|
-
console.log(leafCommand.formatHelp());
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
if (err instanceof CommanderError) {
|
|
243
|
-
console.error(err.format());
|
|
244
|
-
process.exit(2);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
throw err;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
parse(params) {
|
|
251
|
-
const { argv, envs, reporter } = params;
|
|
252
|
-
const processedArgv = this.#processHelpSubcommand(argv);
|
|
253
|
-
const routeResult = this.#route(processedArgv);
|
|
254
|
-
const { chain, remaining } = routeResult;
|
|
255
|
-
const leafCommand = chain[chain.length - 1];
|
|
256
|
-
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
257
|
-
const { optionTokens, restArgs } = tokenizeResult;
|
|
258
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
259
|
-
const ctx = {
|
|
260
|
-
cmd: leafCommand,
|
|
261
|
-
envs,
|
|
262
|
-
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
263
|
-
argv,
|
|
264
|
-
};
|
|
265
|
-
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
266
|
-
}
|
|
267
|
-
formatHelp() {
|
|
268
|
-
const lines = [];
|
|
269
|
-
const allOptions = this.#getMergedOptions();
|
|
270
|
-
lines.push(this.#desc);
|
|
271
|
-
lines.push('');
|
|
272
|
-
const commandPath = this.#getCommandPath();
|
|
273
|
-
let usage = `Usage: ${commandPath}`;
|
|
274
|
-
if (allOptions.length > 0)
|
|
275
|
-
usage += ' [options]';
|
|
276
|
-
if (this.#subcommandsList.length > 0)
|
|
277
|
-
usage += ' [command]';
|
|
278
|
-
for (const arg of this.#arguments) {
|
|
279
|
-
if (arg.kind === 'required') {
|
|
280
|
-
usage += ` <${arg.name}>`;
|
|
281
|
-
}
|
|
282
|
-
else if (arg.kind === 'optional') {
|
|
283
|
-
usage += ` [${arg.name}]`;
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
usage += ` [${arg.name}...]`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
lines.push(usage);
|
|
290
|
-
lines.push('');
|
|
291
|
-
if (allOptions.length > 0) {
|
|
292
|
-
lines.push('Options:');
|
|
293
|
-
const optLines = [];
|
|
294
|
-
for (const opt of allOptions) {
|
|
295
|
-
const kebabLong = camelToKebabCase$1(opt.long);
|
|
296
|
-
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
297
|
-
sig += `--${kebabLong}`;
|
|
298
|
-
if (opt.args !== 'none') {
|
|
299
|
-
sig += ' <value>';
|
|
300
|
-
}
|
|
301
|
-
let desc = opt.desc;
|
|
302
|
-
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
303
|
-
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
304
|
-
}
|
|
305
|
-
if (opt.choices) {
|
|
306
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
307
|
-
}
|
|
308
|
-
optLines.push({ sig, desc });
|
|
309
|
-
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
310
|
-
optLines.push({
|
|
311
|
-
sig: ` --no-${kebabLong}`,
|
|
312
|
-
desc: `Negate --${kebabLong}`,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
|
|
317
|
-
for (const { sig, desc } of optLines) {
|
|
318
|
-
const padding = ' '.repeat(maxSigLen - sig.length + 2);
|
|
319
|
-
lines.push(` ${sig}${padding}${desc}`);
|
|
320
|
-
}
|
|
321
|
-
lines.push('');
|
|
322
|
-
}
|
|
323
|
-
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
|
|
324
|
-
if (this.#subcommandsList.length > 0) {
|
|
325
|
-
lines.push('Commands:');
|
|
326
|
-
const cmdLines = [];
|
|
327
|
-
if (showHelpSubcommand) {
|
|
328
|
-
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
329
|
-
}
|
|
330
|
-
for (const entry of this.#subcommandsList) {
|
|
331
|
-
let name = entry.name;
|
|
332
|
-
if (entry.aliases.length > 0) {
|
|
333
|
-
name += `, ${entry.aliases.join(', ')}`;
|
|
334
|
-
}
|
|
335
|
-
cmdLines.push({ name, desc: entry.command.#desc });
|
|
336
|
-
}
|
|
337
|
-
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
338
|
-
for (const { name, desc } of cmdLines) {
|
|
339
|
-
const padding = ' '.repeat(maxNameLen - name.length + 2);
|
|
340
|
-
lines.push(` ${name}${padding}${desc}`);
|
|
341
|
-
}
|
|
342
|
-
lines.push('');
|
|
343
|
-
}
|
|
344
|
-
return lines.join('\n');
|
|
345
|
-
}
|
|
346
|
-
getCompletionMeta() {
|
|
347
|
-
const allOptions = this.#getMergedOptions();
|
|
348
|
-
const options = [];
|
|
349
|
-
for (const opt of allOptions) {
|
|
350
|
-
options.push({
|
|
351
|
-
long: opt.long,
|
|
352
|
-
short: opt.short,
|
|
353
|
-
desc: opt.desc,
|
|
354
|
-
takesValue: opt.args !== 'none',
|
|
355
|
-
choices: opt.choices,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
name: this.#name,
|
|
360
|
-
desc: this.#desc,
|
|
361
|
-
aliases: [],
|
|
362
|
-
options,
|
|
363
|
-
subcommands: this.#subcommandsList.map(entry => {
|
|
364
|
-
const subMeta = entry.command.getCompletionMeta();
|
|
365
|
-
return {
|
|
366
|
-
...subMeta,
|
|
367
|
-
name: entry.name,
|
|
368
|
-
aliases: entry.aliases,
|
|
369
|
-
};
|
|
370
|
-
}),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
#processHelpSubcommand(argv) {
|
|
374
|
-
if (!this.#helpSubcommandEnabled)
|
|
375
|
-
return argv;
|
|
376
|
-
if (argv.length < 1 || argv[0] !== 'help')
|
|
377
|
-
return argv;
|
|
378
|
-
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
379
|
-
return ['--help'];
|
|
380
|
-
}
|
|
381
|
-
const subName = argv[1];
|
|
382
|
-
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
383
|
-
if (entry) {
|
|
384
|
-
return [subName, '--help', ...argv.slice(2)];
|
|
385
|
-
}
|
|
386
|
-
return argv;
|
|
387
|
-
}
|
|
388
|
-
#route(argv) {
|
|
389
|
-
const chain = [this];
|
|
390
|
-
let current = this;
|
|
391
|
-
let idx = 0;
|
|
392
|
-
while (idx < argv.length) {
|
|
393
|
-
const token = argv[idx];
|
|
394
|
-
if (token.startsWith('-'))
|
|
395
|
-
break;
|
|
396
|
-
const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
397
|
-
if (!entry)
|
|
398
|
-
break;
|
|
399
|
-
current = entry.command;
|
|
400
|
-
chain.push(current);
|
|
401
|
-
idx += 1;
|
|
402
|
-
}
|
|
403
|
-
return { chain, remaining: argv.slice(idx) };
|
|
404
|
-
}
|
|
405
|
-
#resolve(chain, tokens) {
|
|
406
|
-
const consumedTokens = new Map();
|
|
407
|
-
let remaining = [...tokens];
|
|
408
|
-
const shadowed = new Set();
|
|
409
|
-
for (let i = chain.length - 1; i >= 0; i--) {
|
|
410
|
-
const cmd = chain[i];
|
|
411
|
-
const includeVersion = i === 0;
|
|
412
|
-
const result = cmd.#shift(remaining, shadowed, includeVersion);
|
|
413
|
-
consumedTokens.set(cmd, result.consumed);
|
|
414
|
-
remaining = result.remaining;
|
|
415
|
-
for (const opt of cmd.#options) {
|
|
416
|
-
shadowed.add(opt.long);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const argTokens = [];
|
|
420
|
-
for (const token of remaining) {
|
|
421
|
-
if (token.type !== 'none') {
|
|
422
|
-
const leafCommand = chain[chain.length - 1];
|
|
423
|
-
throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
424
|
-
}
|
|
425
|
-
argTokens.push(token);
|
|
426
|
-
}
|
|
427
|
-
return { consumedTokens, argTokens };
|
|
428
|
-
}
|
|
429
|
-
#shift(tokens, shadowed, includeVersion) {
|
|
430
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
431
|
-
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
432
|
-
const optionByLong = new Map();
|
|
433
|
-
const optionByShort = new Map();
|
|
434
|
-
for (const opt of effectiveOptions) {
|
|
435
|
-
optionByLong.set(opt.long, opt);
|
|
436
|
-
if (opt.short) {
|
|
437
|
-
optionByShort.set(opt.short, opt);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
const consumed = [];
|
|
441
|
-
const remaining = [];
|
|
442
|
-
let i = 0;
|
|
443
|
-
while (i < tokens.length) {
|
|
444
|
-
const token = tokens[i];
|
|
445
|
-
if (token.type === 'long') {
|
|
446
|
-
const opt = optionByLong.get(token.name);
|
|
447
|
-
if (opt) {
|
|
448
|
-
consumed.push(token);
|
|
449
|
-
if (opt.args === 'required') {
|
|
450
|
-
if (!token.resolved.includes('=') && i + 1 < tokens.length) {
|
|
451
|
-
i += 1;
|
|
452
|
-
consumed.push(tokens[i]);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else if (opt.args === 'variadic') {
|
|
456
|
-
if (!token.resolved.includes('=')) {
|
|
457
|
-
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
458
|
-
i += 1;
|
|
459
|
-
consumed.push(tokens[i]);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
i += 1;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
remaining.push(token);
|
|
467
|
-
i += 1;
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (token.type === 'short') {
|
|
471
|
-
const opt = optionByShort.get(token.name);
|
|
472
|
-
if (opt) {
|
|
473
|
-
consumed.push(token);
|
|
474
|
-
if (opt.args === 'required') {
|
|
475
|
-
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
476
|
-
i += 1;
|
|
477
|
-
consumed.push(tokens[i]);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
else if (opt.args === 'variadic') {
|
|
481
|
-
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
482
|
-
i += 1;
|
|
483
|
-
consumed.push(tokens[i]);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
i += 1;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
remaining.push(token);
|
|
490
|
-
i += 1;
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
remaining.push(token);
|
|
494
|
-
i += 1;
|
|
495
|
-
}
|
|
496
|
-
return { consumed, remaining };
|
|
497
|
-
}
|
|
498
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
499
|
-
const { consumedTokens, argTokens } = resolveResult;
|
|
500
|
-
const leafCommand = chain[chain.length - 1];
|
|
501
|
-
this.#validateMergedShortOptions(chain);
|
|
502
|
-
const optsMap = new Map();
|
|
503
|
-
for (let i = 0; i < chain.length; i++) {
|
|
504
|
-
const cmd = chain[i];
|
|
505
|
-
const includeVersion = i === 0;
|
|
506
|
-
const tokens = consumedTokens.get(cmd) ?? [];
|
|
507
|
-
const opts = cmd.#parseOptions(tokens, includeVersion);
|
|
508
|
-
optsMap.set(cmd, opts);
|
|
509
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
510
|
-
if (opt.apply && opts[opt.long] !== undefined) {
|
|
511
|
-
opt.apply(opts[opt.long], ctx);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const mergedOpts = {};
|
|
516
|
-
for (const cmd of chain) {
|
|
517
|
-
Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
|
|
518
|
-
}
|
|
519
|
-
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
520
|
-
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
521
|
-
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
522
|
-
}
|
|
523
|
-
#parseOptions(tokens, includeVersion) {
|
|
524
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
525
|
-
const opts = {};
|
|
526
|
-
for (const opt of allOptions) {
|
|
527
|
-
if (opt.default !== undefined) {
|
|
528
|
-
opts[opt.long] = opt.default;
|
|
529
|
-
}
|
|
530
|
-
else if (opt.type === 'boolean' && opt.args === 'none') {
|
|
531
|
-
opts[opt.long] = false;
|
|
532
|
-
}
|
|
533
|
-
else if (opt.args === 'variadic') {
|
|
534
|
-
opts[opt.long] = [];
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
const optionByLong = new Map();
|
|
538
|
-
const optionByShort = new Map();
|
|
539
|
-
for (const opt of allOptions) {
|
|
540
|
-
optionByLong.set(opt.long, opt);
|
|
541
|
-
if (opt.short) {
|
|
542
|
-
optionByShort.set(opt.short, opt);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
let i = 0;
|
|
546
|
-
while (i < tokens.length) {
|
|
547
|
-
const token = tokens[i];
|
|
548
|
-
const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
|
|
549
|
-
if (!opt) {
|
|
550
|
-
i += 1;
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
554
|
-
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
555
|
-
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
556
|
-
}
|
|
557
|
-
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
558
|
-
const eqIdx = token.resolved.indexOf('=');
|
|
559
|
-
if (eqIdx !== -1) {
|
|
560
|
-
const value = token.resolved.slice(eqIdx + 1);
|
|
561
|
-
if (value === 'true') {
|
|
562
|
-
opts[opt.long] = true;
|
|
563
|
-
}
|
|
564
|
-
else if (value === 'false') {
|
|
565
|
-
opts[opt.long] = false;
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
opts[opt.long] = true;
|
|
573
|
-
}
|
|
574
|
-
i += 1;
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
if (opt.args === 'required') {
|
|
578
|
-
const eqIdx = token.resolved.indexOf('=');
|
|
579
|
-
let rawValue;
|
|
580
|
-
if (eqIdx !== -1) {
|
|
581
|
-
rawValue = token.resolved.slice(eqIdx + 1);
|
|
582
|
-
}
|
|
583
|
-
else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
584
|
-
rawValue = tokens[i + 1].original;
|
|
585
|
-
i += 1;
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
|
|
589
|
-
}
|
|
590
|
-
opts[opt.long] = this.#convertValue(opt, rawValue);
|
|
591
|
-
i += 1;
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
if (opt.args === 'variadic') {
|
|
595
|
-
const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
|
|
596
|
-
const eqIdx = token.resolved.indexOf('=');
|
|
597
|
-
if (eqIdx !== -1) {
|
|
598
|
-
values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
602
|
-
i += 1;
|
|
603
|
-
values.push(this.#convertValue(opt, tokens[i].original));
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
opts[opt.long] = values;
|
|
607
|
-
i += 1;
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
i += 1;
|
|
611
|
-
}
|
|
612
|
-
for (const opt of allOptions) {
|
|
613
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
614
|
-
throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
for (const opt of allOptions) {
|
|
618
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
619
|
-
const value = opts[opt.long];
|
|
620
|
-
const values = Array.isArray(value) ? value : [value];
|
|
621
|
-
const choices = opt.choices;
|
|
622
|
-
for (const v of values) {
|
|
623
|
-
if (!choices.includes(v)) {
|
|
624
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return opts;
|
|
630
|
-
}
|
|
631
|
-
#convertValue(opt, rawValue) {
|
|
632
|
-
if (opt.coerce) {
|
|
633
|
-
return opt.coerce(rawValue);
|
|
634
|
-
}
|
|
635
|
-
if (opt.type === 'number') {
|
|
636
|
-
const num = Number(rawValue);
|
|
637
|
-
if (Number.isNaN(num)) {
|
|
638
|
-
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
639
|
-
}
|
|
640
|
-
return num;
|
|
641
|
-
}
|
|
642
|
-
return rawValue;
|
|
643
|
-
}
|
|
644
|
-
#parseArguments(rawArgs) {
|
|
645
|
-
const argumentDefs = this.#arguments;
|
|
646
|
-
const args = {};
|
|
647
|
-
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
648
|
-
if (rawArgs.length < requiredCount) {
|
|
649
|
-
const missing = argumentDefs
|
|
650
|
-
.filter(a => a.kind === 'required')
|
|
651
|
-
.slice(rawArgs.length)
|
|
652
|
-
.map(a => a.name);
|
|
653
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
654
|
-
}
|
|
655
|
-
let index = 0;
|
|
656
|
-
for (const def of argumentDefs) {
|
|
657
|
-
if (def.kind === 'variadic') {
|
|
658
|
-
const rest = rawArgs.slice(index);
|
|
659
|
-
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
660
|
-
index = rawArgs.length;
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
const raw = rawArgs[index];
|
|
664
|
-
if (raw === undefined) {
|
|
665
|
-
if (def.kind === 'optional') {
|
|
666
|
-
args[def.name] = def.default ?? undefined;
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
args[def.name] = this.#convertArgument(def, raw);
|
|
672
|
-
index += 1;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
676
|
-
if (!hasVariadic && index < rawArgs.length) {
|
|
677
|
-
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
678
|
-
}
|
|
679
|
-
return { args, rawArgs };
|
|
680
|
-
}
|
|
681
|
-
#convertArgument(def, raw) {
|
|
682
|
-
if (def.coerce) {
|
|
683
|
-
try {
|
|
684
|
-
return def.coerce(raw);
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (def.type === 'number') {
|
|
691
|
-
const n = Number(raw);
|
|
692
|
-
if (Number.isNaN(n)) {
|
|
693
|
-
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
694
|
-
}
|
|
695
|
-
return n;
|
|
696
|
-
}
|
|
697
|
-
return raw;
|
|
698
|
-
}
|
|
699
|
-
#getMergedOptions(includeVersion = !this.#parent) {
|
|
700
|
-
const optionMap = new Map();
|
|
701
|
-
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
702
|
-
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
703
|
-
if (!hasUserHelp) {
|
|
704
|
-
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
705
|
-
}
|
|
706
|
-
if (!hasUserVersion && includeVersion) {
|
|
707
|
-
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
708
|
-
}
|
|
709
|
-
for (const opt of this.#options) {
|
|
710
|
-
optionMap.set(opt.long, opt);
|
|
711
|
-
}
|
|
712
|
-
return Array.from(optionMap.values());
|
|
713
|
-
}
|
|
714
|
-
#validateMergedShortOptions(chain) {
|
|
715
|
-
const mergedByLong = new Map();
|
|
716
|
-
for (let i = 0; i < chain.length; i++) {
|
|
717
|
-
const cmd = chain[i];
|
|
718
|
-
const includeVersion = i === 0;
|
|
719
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
720
|
-
mergedByLong.set(opt.long, opt);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const shortMap = new Map();
|
|
724
|
-
for (const opt of mergedByLong.values()) {
|
|
725
|
-
if (!opt.short)
|
|
726
|
-
continue;
|
|
727
|
-
const existingLong = shortMap.get(opt.short);
|
|
728
|
-
if (existingLong && existingLong !== opt.long) {
|
|
729
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
|
|
730
|
-
}
|
|
731
|
-
shortMap.set(opt.short, opt.long);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
#validateOptionConfig(opt) {
|
|
735
|
-
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
736
|
-
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
737
|
-
}
|
|
738
|
-
if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
|
|
739
|
-
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
|
|
740
|
-
}
|
|
741
|
-
if (opt.long.startsWith('no')) {
|
|
742
|
-
throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
|
|
743
|
-
}
|
|
744
|
-
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
745
|
-
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
746
|
-
}
|
|
747
|
-
if (opt.required && opt.default !== undefined) {
|
|
748
|
-
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
749
|
-
}
|
|
750
|
-
if (opt.type === 'boolean' && opt.required) {
|
|
751
|
-
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
#checkOptionUniqueness(opt) {
|
|
755
|
-
if (this.#options.some(o => o.long === opt.long)) {
|
|
756
|
-
throw new CommanderError('OptionConflict', `option "--${opt.long}" is already defined`, this.#getCommandPath());
|
|
757
|
-
}
|
|
758
|
-
if (opt.short && this.#options.some(o => o.short === opt.short)) {
|
|
759
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is already defined`, this.#getCommandPath());
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
#validateArgumentConfig(arg) {
|
|
763
|
-
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
764
|
-
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
765
|
-
}
|
|
766
|
-
if (arg.kind === 'variadic') {
|
|
767
|
-
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
768
|
-
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (this.#arguments.length > 0) {
|
|
772
|
-
const last = this.#arguments[this.#arguments.length - 1];
|
|
773
|
-
if (last.kind === 'variadic') {
|
|
774
|
-
throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
if (arg.kind === 'required') {
|
|
778
|
-
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
|
|
779
|
-
if (hasOptional) {
|
|
780
|
-
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
async #runAction(params) {
|
|
785
|
-
if (!this.#action)
|
|
786
|
-
return;
|
|
787
|
-
try {
|
|
788
|
-
await this.#action(params);
|
|
789
|
-
}
|
|
790
|
-
catch (err) {
|
|
791
|
-
if (err instanceof Error) {
|
|
792
|
-
console.error(`Error: ${err.message}`);
|
|
793
|
-
}
|
|
794
|
-
else {
|
|
795
|
-
console.error('Error: action failed');
|
|
796
|
-
}
|
|
797
|
-
process.exit(1);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
#hasFlag(tokens, longName, shortName) {
|
|
801
|
-
for (const token of tokens) {
|
|
802
|
-
if (token.type === 'long' && token.name === longName) {
|
|
803
|
-
return true;
|
|
804
|
-
}
|
|
805
|
-
if (token.type === 'short' && token.name === shortName) {
|
|
806
|
-
return true;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
#getCommandPath() {
|
|
812
|
-
const parts = [];
|
|
813
|
-
let current = this;
|
|
814
|
-
while (current) {
|
|
815
|
-
if (current.#name) {
|
|
816
|
-
parts.unshift(current.#name);
|
|
817
|
-
}
|
|
818
|
-
current = current.#parent;
|
|
819
|
-
}
|
|
820
|
-
return parts.join(' ') || this.#name;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function camelToKebabCase(str) {
|
|
825
|
-
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
826
|
-
}
|
|
827
|
-
class CompletionCommand extends Command {
|
|
828
|
-
constructor(root, config) {
|
|
829
|
-
const paths = config.paths;
|
|
830
|
-
const programName = config.programName ?? root.name ?? 'program';
|
|
831
|
-
super({ desc: 'Generate shell completion script' });
|
|
832
|
-
this.option({
|
|
833
|
-
long: 'bash',
|
|
834
|
-
type: 'boolean',
|
|
835
|
-
args: 'none',
|
|
836
|
-
desc: 'Generate Bash completion script',
|
|
837
|
-
})
|
|
838
|
-
.option({
|
|
839
|
-
long: 'fish',
|
|
840
|
-
type: 'boolean',
|
|
841
|
-
args: 'none',
|
|
842
|
-
desc: 'Generate Fish completion script',
|
|
843
|
-
})
|
|
844
|
-
.option({
|
|
845
|
-
long: 'pwsh',
|
|
846
|
-
type: 'boolean',
|
|
847
|
-
args: 'none',
|
|
848
|
-
desc: 'Generate PowerShell completion script',
|
|
849
|
-
})
|
|
850
|
-
.option({
|
|
851
|
-
long: 'write',
|
|
852
|
-
short: 'w',
|
|
853
|
-
type: 'string',
|
|
854
|
-
args: 'required',
|
|
855
|
-
desc: 'Write to file (use shell default path if empty)',
|
|
856
|
-
default: undefined,
|
|
857
|
-
})
|
|
858
|
-
.action(({ opts }) => {
|
|
859
|
-
const meta = root.getCompletionMeta();
|
|
860
|
-
const selectedShells = [
|
|
861
|
-
opts['bash'] && 'bash',
|
|
862
|
-
opts['fish'] && 'fish',
|
|
863
|
-
opts['pwsh'] && 'pwsh',
|
|
864
|
-
].filter(Boolean);
|
|
865
|
-
if (selectedShells.length === 0) {
|
|
866
|
-
console.error('Please specify a shell: --bash, --fish, or --pwsh');
|
|
867
|
-
process.exit(1);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
if (selectedShells.length > 1) {
|
|
871
|
-
console.error('Please specify only one shell option');
|
|
872
|
-
process.exit(1);
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const shell = selectedShells[0];
|
|
876
|
-
let script;
|
|
877
|
-
switch (shell) {
|
|
878
|
-
case 'bash':
|
|
879
|
-
script = new BashCompletion(meta, programName).generate();
|
|
880
|
-
break;
|
|
881
|
-
case 'fish':
|
|
882
|
-
script = new FishCompletion(meta, programName).generate();
|
|
883
|
-
break;
|
|
884
|
-
case 'pwsh':
|
|
885
|
-
script = new PwshCompletion(meta, programName).generate();
|
|
886
|
-
break;
|
|
887
|
-
}
|
|
888
|
-
const writeOpt = opts['write'];
|
|
889
|
-
if (writeOpt !== undefined) {
|
|
890
|
-
const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
|
|
891
|
-
const expandedPath = expandHome(filePath);
|
|
892
|
-
const dir = path.dirname(expandedPath);
|
|
893
|
-
if (!fs.existsSync(dir)) {
|
|
894
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
895
|
-
}
|
|
896
|
-
fs.writeFileSync(expandedPath, script, 'utf-8');
|
|
897
|
-
console.log(`Completion script written to: ${expandedPath}`);
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
console.log(script);
|
|
901
|
-
}
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
function expandHome(filepath) {
|
|
906
|
-
if (filepath.startsWith('~/') || filepath === '~') {
|
|
907
|
-
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
908
|
-
return filepath.replace(/^~/, home);
|
|
909
|
-
}
|
|
910
|
-
return filepath;
|
|
911
|
-
}
|
|
912
|
-
class BashCompletion {
|
|
913
|
-
#meta;
|
|
914
|
-
#programName;
|
|
915
|
-
constructor(meta, programName) {
|
|
916
|
-
this.#meta = meta;
|
|
917
|
-
this.#programName = programName;
|
|
918
|
-
}
|
|
919
|
-
generate() {
|
|
920
|
-
const funcName = `_${this.#sanitizeName(this.#programName)}_completions`;
|
|
921
|
-
const lines = [
|
|
922
|
-
`# Bash completion for ${this.#programName}`,
|
|
923
|
-
'# Generated by @guanghechen/commander',
|
|
924
|
-
'',
|
|
925
|
-
`${funcName}() {`,
|
|
926
|
-
' local cur prev words cword',
|
|
927
|
-
' _init_completion || return',
|
|
928
|
-
'',
|
|
929
|
-
...this.#generateCommandCase(this.#meta, 1),
|
|
930
|
-
'',
|
|
931
|
-
' COMPREPLY=($(compgen -W "$opts" -- "$cur"))',
|
|
932
|
-
'}',
|
|
933
|
-
'',
|
|
934
|
-
`complete -F ${funcName} ${this.#programName}`,
|
|
935
|
-
'',
|
|
936
|
-
];
|
|
937
|
-
return lines.join('\n');
|
|
938
|
-
}
|
|
939
|
-
#generateCommandCase(cmd, depth) {
|
|
940
|
-
const indent = ' '.repeat(depth);
|
|
941
|
-
const lines = [];
|
|
942
|
-
const optParts = [];
|
|
943
|
-
for (const opt of cmd.options) {
|
|
944
|
-
const kebabLong = camelToKebabCase(opt.long);
|
|
945
|
-
if (opt.short)
|
|
946
|
-
optParts.push(`-${opt.short}`);
|
|
947
|
-
optParts.push(`--${kebabLong}`);
|
|
948
|
-
if (!opt.takesValue) {
|
|
949
|
-
optParts.push(`--no-${kebabLong}`);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
953
|
-
const allOpts = [...optParts, ...subParts].join(' ');
|
|
954
|
-
if (cmd.subcommands.length > 0) {
|
|
955
|
-
lines.push(`${indent}case "\${words[${depth}]}" in`);
|
|
956
|
-
for (const sub of cmd.subcommands) {
|
|
957
|
-
const pattern = [sub.name, ...sub.aliases].join('|');
|
|
958
|
-
lines.push(`${indent} ${pattern})`);
|
|
959
|
-
lines.push(...this.#generateCommandCase(sub, depth + 1));
|
|
960
|
-
lines.push(`${indent} ;;`);
|
|
961
|
-
}
|
|
962
|
-
lines.push(`${indent} *)`);
|
|
963
|
-
lines.push(`${indent} opts="${allOpts}"`);
|
|
964
|
-
lines.push(`${indent} ;;`);
|
|
965
|
-
lines.push(`${indent}esac`);
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
lines.push(`${indent}opts="${allOpts}"`);
|
|
969
|
-
}
|
|
970
|
-
return lines;
|
|
971
|
-
}
|
|
972
|
-
#sanitizeName(name) {
|
|
973
|
-
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
class FishCompletion {
|
|
977
|
-
#meta;
|
|
978
|
-
#programName;
|
|
979
|
-
constructor(meta, programName) {
|
|
980
|
-
this.#meta = meta;
|
|
981
|
-
this.#programName = programName;
|
|
982
|
-
}
|
|
983
|
-
generate() {
|
|
984
|
-
const lines = [
|
|
985
|
-
`# Fish completion for ${this.#programName}`,
|
|
986
|
-
'# Generated by @guanghechen/commander',
|
|
987
|
-
'',
|
|
988
|
-
...this.#generateCommandCompletions(this.#meta, []),
|
|
989
|
-
'',
|
|
990
|
-
];
|
|
991
|
-
return lines.join('\n');
|
|
992
|
-
}
|
|
993
|
-
#generateCommandCompletions(cmd, parentPath) {
|
|
994
|
-
const lines = [];
|
|
995
|
-
const isRoot = parentPath.length === 0;
|
|
996
|
-
const condition = this.#buildCondition(parentPath);
|
|
997
|
-
for (const opt of cmd.options) {
|
|
998
|
-
const kebabLong = camelToKebabCase(opt.long);
|
|
999
|
-
let line = `complete -c ${this.#programName}`;
|
|
1000
|
-
if (condition)
|
|
1001
|
-
line += ` -n '${condition}'`;
|
|
1002
|
-
if (opt.short)
|
|
1003
|
-
line += ` -s ${opt.short}`;
|
|
1004
|
-
line += ` -l ${kebabLong}`;
|
|
1005
|
-
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1006
|
-
if (opt.choices && opt.choices.length > 0) {
|
|
1007
|
-
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
1008
|
-
}
|
|
1009
|
-
lines.push(line);
|
|
1010
|
-
if (!opt.takesValue) {
|
|
1011
|
-
let noLine = `complete -c ${this.#programName}`;
|
|
1012
|
-
if (condition)
|
|
1013
|
-
noLine += ` -n '${condition}'`;
|
|
1014
|
-
noLine += ` -l no-${kebabLong}`;
|
|
1015
|
-
noLine += ` -d '${this.#escape(opt.desc)}'`;
|
|
1016
|
-
lines.push(noLine);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
for (const sub of cmd.subcommands) {
|
|
1020
|
-
let line = `complete -c ${this.#programName}`;
|
|
1021
|
-
if (isRoot) {
|
|
1022
|
-
line += ' -n __fish_use_subcommand';
|
|
1023
|
-
}
|
|
1024
|
-
else if (condition) {
|
|
1025
|
-
line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1026
|
-
}
|
|
1027
|
-
line += ` -a ${sub.name}`;
|
|
1028
|
-
line += ` -d '${this.#escape(sub.desc)}'`;
|
|
1029
|
-
lines.push(line);
|
|
1030
|
-
for (const alias of sub.aliases) {
|
|
1031
|
-
let aliasLine = `complete -c ${this.#programName}`;
|
|
1032
|
-
if (isRoot) {
|
|
1033
|
-
aliasLine += ' -n __fish_use_subcommand';
|
|
1034
|
-
}
|
|
1035
|
-
else if (condition) {
|
|
1036
|
-
aliasLine += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1037
|
-
}
|
|
1038
|
-
aliasLine += ` -a ${alias}`;
|
|
1039
|
-
aliasLine += ` -d 'Alias for ${sub.name}'`;
|
|
1040
|
-
lines.push(aliasLine);
|
|
1041
|
-
}
|
|
1042
|
-
const newPath = [...parentPath, sub.name];
|
|
1043
|
-
lines.push(...this.#generateCommandCompletions(sub, newPath));
|
|
1044
|
-
}
|
|
1045
|
-
return lines;
|
|
1046
|
-
}
|
|
1047
|
-
#buildCondition(path) {
|
|
1048
|
-
if (path.length === 0)
|
|
1049
|
-
return '';
|
|
1050
|
-
return `__fish_seen_subcommand_from ${path[path.length - 1]}`;
|
|
1051
|
-
}
|
|
1052
|
-
#getSubcommandNames(cmd) {
|
|
1053
|
-
return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
1054
|
-
}
|
|
1055
|
-
#escape(s) {
|
|
1056
|
-
return s.replace(/'/g, "\\'");
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
class PwshCompletion {
|
|
1060
|
-
#meta;
|
|
1061
|
-
#programName;
|
|
1062
|
-
constructor(meta, programName) {
|
|
1063
|
-
this.#meta = meta;
|
|
1064
|
-
this.#programName = programName;
|
|
1065
|
-
}
|
|
1066
|
-
generate() {
|
|
1067
|
-
const lines = [
|
|
1068
|
-
`# PowerShell completion for ${this.#programName}`,
|
|
1069
|
-
'# Generated by @guanghechen/commander',
|
|
1070
|
-
'',
|
|
1071
|
-
`Register-ArgumentCompleter -Native -CommandName ${this.#programName} -ScriptBlock {`,
|
|
1072
|
-
' param($wordToComplete, $commandAst, $cursorPosition)',
|
|
1073
|
-
'',
|
|
1074
|
-
' $commands = @{',
|
|
1075
|
-
this.#generateCommandHash(this.#meta, ' '),
|
|
1076
|
-
' }',
|
|
1077
|
-
'',
|
|
1078
|
-
' $words = $commandAst.CommandElements | ForEach-Object { $_.ToString() }',
|
|
1079
|
-
' $current = $wordToComplete',
|
|
1080
|
-
'',
|
|
1081
|
-
' # Find current command context',
|
|
1082
|
-
' $cmd = $commands',
|
|
1083
|
-
' foreach ($word in $words[1..($words.Count - 1)]) {',
|
|
1084
|
-
' if ($word.StartsWith("-")) { continue }',
|
|
1085
|
-
' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
|
|
1086
|
-
' $cmd = $cmd.subcommands[$word]',
|
|
1087
|
-
' }',
|
|
1088
|
-
' }',
|
|
1089
|
-
'',
|
|
1090
|
-
' # Generate completions',
|
|
1091
|
-
' $completions = @()',
|
|
1092
|
-
'',
|
|
1093
|
-
' # Options',
|
|
1094
|
-
' if ($current.StartsWith("-")) {',
|
|
1095
|
-
' foreach ($opt in $cmd.options) {',
|
|
1096
|
-
' if ("--$($opt.long)" -like "$current*") {',
|
|
1097
|
-
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1098
|
-
' "--$($opt.long)",',
|
|
1099
|
-
' $opt.long,',
|
|
1100
|
-
' "ParameterName",',
|
|
1101
|
-
' $opt.desc',
|
|
1102
|
-
' )',
|
|
1103
|
-
' }',
|
|
1104
|
-
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
1105
|
-
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1106
|
-
' "--no-$($opt.long)",',
|
|
1107
|
-
' "no-$($opt.long)",',
|
|
1108
|
-
' "ParameterName",',
|
|
1109
|
-
' $opt.desc',
|
|
1110
|
-
' )',
|
|
1111
|
-
' }',
|
|
1112
|
-
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
1113
|
-
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1114
|
-
' "-$($opt.short)",',
|
|
1115
|
-
' $opt.short,',
|
|
1116
|
-
' "ParameterName",',
|
|
1117
|
-
' $opt.desc',
|
|
1118
|
-
' )',
|
|
1119
|
-
' }',
|
|
1120
|
-
' }',
|
|
1121
|
-
' }',
|
|
1122
|
-
'',
|
|
1123
|
-
' # Subcommands',
|
|
1124
|
-
' if ($cmd.subcommands) {',
|
|
1125
|
-
' foreach ($sub in $cmd.subcommands.Keys) {',
|
|
1126
|
-
' if ($sub -like "$current*") {',
|
|
1127
|
-
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
1128
|
-
' $sub,',
|
|
1129
|
-
' $sub,',
|
|
1130
|
-
' "Command",',
|
|
1131
|
-
' $cmd.subcommands[$sub].desc',
|
|
1132
|
-
' )',
|
|
1133
|
-
' }',
|
|
1134
|
-
' }',
|
|
1135
|
-
' }',
|
|
1136
|
-
'',
|
|
1137
|
-
' return $completions',
|
|
1138
|
-
'}',
|
|
1139
|
-
'',
|
|
1140
|
-
];
|
|
1141
|
-
return lines.join('\n');
|
|
1142
|
-
}
|
|
1143
|
-
#generateCommandHash(cmd, indent) {
|
|
1144
|
-
const lines = [];
|
|
1145
|
-
lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
|
|
1146
|
-
lines.push(`${indent}options = @(`);
|
|
1147
|
-
for (const opt of cmd.options) {
|
|
1148
|
-
const kebabLong = camelToKebabCase(opt.long);
|
|
1149
|
-
lines.push(`${indent} @{`);
|
|
1150
|
-
if (opt.short)
|
|
1151
|
-
lines.push(`${indent} short = '${opt.short}'`);
|
|
1152
|
-
lines.push(`${indent} long = '${kebabLong}'`);
|
|
1153
|
-
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
1154
|
-
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
1155
|
-
if (opt.choices) {
|
|
1156
|
-
lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
|
|
1157
|
-
}
|
|
1158
|
-
lines.push(`${indent} }`);
|
|
1159
|
-
}
|
|
1160
|
-
lines.push(`${indent})`);
|
|
1161
|
-
if (cmd.subcommands.length > 0) {
|
|
1162
|
-
lines.push(`${indent}subcommands = @{`);
|
|
1163
|
-
for (const sub of cmd.subcommands) {
|
|
1164
|
-
lines.push(`${indent} '${sub.name}' = @{`);
|
|
1165
|
-
lines.push(this.#generateCommandHash(sub, `${indent} `));
|
|
1166
|
-
lines.push(`${indent} }`);
|
|
1167
|
-
for (const alias of sub.aliases) {
|
|
1168
|
-
lines.push(`${indent} '${alias}' = @{`);
|
|
1169
|
-
lines.push(this.#generateCommandHash(sub, `${indent} `));
|
|
1170
|
-
lines.push(`${indent} }`);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
lines.push(`${indent}}`);
|
|
1174
|
-
}
|
|
1175
|
-
return lines.join('\n');
|
|
1176
|
-
}
|
|
1177
|
-
#escape(s) {
|
|
1178
|
-
return s.replace(/'/g, "''");
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const logLevelOption = {
|
|
1183
|
-
long: 'logLevel',
|
|
1184
|
-
type: 'string',
|
|
1185
|
-
args: 'required',
|
|
1186
|
-
desc: 'Set log level',
|
|
1187
|
-
default: 'info',
|
|
1188
|
-
choices: LOG_LEVELS,
|
|
1189
|
-
coerce: (raw) => {
|
|
1190
|
-
const level = resolveLogLevel(raw);
|
|
1191
|
-
if (level === undefined) {
|
|
1192
|
-
throw new Error(`Invalid log level: ${raw}`);
|
|
1193
|
-
}
|
|
1194
|
-
return level;
|
|
1195
|
-
},
|
|
1196
|
-
apply: (value, ctx) => {
|
|
1197
|
-
ctx.reporter.setLevel(value);
|
|
1198
|
-
},
|
|
1199
|
-
};
|
|
1200
|
-
const silentOption = {
|
|
1201
|
-
long: 'silent',
|
|
1202
|
-
type: 'boolean',
|
|
1203
|
-
args: 'none',
|
|
1204
|
-
desc: 'Suppress non-error output',
|
|
1205
|
-
default: false,
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logLevelOption, silentOption };
|