@guanghechen/commander 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/lib/cjs/index.cjs +509 -513
- package/lib/esm/index.mjs +507 -513
- package/lib/types/index.d.ts +211 -88
- package/package.json +4 -1
package/lib/cjs/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var reporter = require('@guanghechen/reporter');
|
|
3
4
|
var fs = require('node:fs');
|
|
4
5
|
var path = require('node:path');
|
|
5
6
|
|
|
@@ -37,55 +38,131 @@ class CommanderError extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
42
|
+
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
43
|
+
function kebabToCamelCase(str) {
|
|
44
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
function camelToKebabCase$1(str) {
|
|
47
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
function tokenizeLongOption(arg, commandPath) {
|
|
50
|
+
const eqIdx = arg.indexOf('=');
|
|
51
|
+
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
52
|
+
const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
|
|
53
|
+
if (namePart.includes('_')) {
|
|
54
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
|
|
55
|
+
}
|
|
56
|
+
const lowerName = namePart.toLowerCase();
|
|
57
|
+
if (lowerName === '--no' || lowerName === '--no-') {
|
|
58
|
+
throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
|
|
43
59
|
}
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
if (lowerName.startsWith('--no-')) {
|
|
61
|
+
if (valuePart !== '') {
|
|
62
|
+
throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
|
|
63
|
+
}
|
|
64
|
+
if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
|
|
65
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
66
|
+
}
|
|
67
|
+
const camelName = kebabToCamelCase(lowerName.slice(5));
|
|
68
|
+
return {
|
|
69
|
+
original: arg,
|
|
70
|
+
resolved: `--${camelName}=false`,
|
|
71
|
+
name: camelName,
|
|
72
|
+
type: 'long',
|
|
73
|
+
};
|
|
46
74
|
}
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
if (!LONG_OPTION_REGEX.test(lowerName)) {
|
|
76
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
49
77
|
}
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
const camelName = kebabToCamelCase(lowerName.slice(2));
|
|
79
|
+
return {
|
|
80
|
+
original: arg,
|
|
81
|
+
resolved: `--${camelName}${valuePart}`,
|
|
82
|
+
name: camelName,
|
|
83
|
+
type: 'long',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function tokenizeShortOptions(arg, commandPath) {
|
|
87
|
+
if (arg.includes('=')) {
|
|
88
|
+
throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
|
|
89
|
+
}
|
|
90
|
+
const flags = arg.slice(1);
|
|
91
|
+
return flags.split('').map(flag => ({
|
|
92
|
+
original: `-${flag}`,
|
|
93
|
+
resolved: `-${flag}`,
|
|
94
|
+
name: flag,
|
|
95
|
+
type: 'short',
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
function tokenize(argv, commandPath) {
|
|
99
|
+
const optionTokens = [];
|
|
100
|
+
const restArgs = [];
|
|
101
|
+
let passThrough = false;
|
|
102
|
+
for (const arg of argv) {
|
|
103
|
+
if (arg === '--') {
|
|
104
|
+
passThrough = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (passThrough) {
|
|
108
|
+
restArgs.push(arg);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (arg.startsWith('--')) {
|
|
112
|
+
optionTokens.push(tokenizeLongOption(arg, commandPath));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
116
|
+
optionTokens.push(...tokenizeShortOptions(arg, commandPath));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
optionTokens.push({
|
|
120
|
+
original: arg,
|
|
121
|
+
resolved: arg,
|
|
122
|
+
name: '',
|
|
123
|
+
type: 'none',
|
|
124
|
+
});
|
|
52
125
|
}
|
|
126
|
+
return { optionTokens, restArgs };
|
|
53
127
|
}
|
|
54
128
|
const BUILTIN_HELP_OPTION = {
|
|
55
129
|
long: 'help',
|
|
56
130
|
short: 'h',
|
|
57
131
|
type: 'boolean',
|
|
58
|
-
|
|
132
|
+
args: 'none',
|
|
133
|
+
desc: 'Show help information',
|
|
59
134
|
};
|
|
60
135
|
const BUILTIN_VERSION_OPTION = {
|
|
61
136
|
long: 'version',
|
|
62
137
|
short: 'V',
|
|
63
138
|
type: 'boolean',
|
|
64
|
-
|
|
139
|
+
args: 'none',
|
|
140
|
+
desc: 'Show version number',
|
|
65
141
|
};
|
|
66
142
|
class Command {
|
|
67
143
|
#name;
|
|
68
|
-
#
|
|
144
|
+
#desc;
|
|
69
145
|
#version;
|
|
70
146
|
#helpSubcommandEnabled;
|
|
71
147
|
#reporter;
|
|
72
148
|
#parent;
|
|
73
149
|
#options = [];
|
|
74
150
|
#arguments = [];
|
|
75
|
-
#
|
|
76
|
-
#
|
|
151
|
+
#subcommandsList = [];
|
|
152
|
+
#subcommandsMap = new Map();
|
|
153
|
+
#action = undefined;
|
|
77
154
|
constructor(config) {
|
|
78
155
|
this.#name = config.name ?? '';
|
|
79
|
-
this.#
|
|
156
|
+
this.#desc = config.desc;
|
|
80
157
|
this.#version = config.version;
|
|
81
158
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
82
159
|
this.#reporter = config.reporter;
|
|
83
160
|
}
|
|
84
161
|
get name() {
|
|
85
|
-
return this.#name;
|
|
162
|
+
return this.#name || undefined;
|
|
86
163
|
}
|
|
87
164
|
get description() {
|
|
88
|
-
return this.#
|
|
165
|
+
return this.#desc;
|
|
89
166
|
}
|
|
90
167
|
get version() {
|
|
91
168
|
return this.#version;
|
|
@@ -99,6 +176,9 @@ class Command {
|
|
|
99
176
|
get arguments() {
|
|
100
177
|
return [...this.#arguments];
|
|
101
178
|
}
|
|
179
|
+
get subcommands() {
|
|
180
|
+
return new Map(this.#subcommandsMap);
|
|
181
|
+
}
|
|
102
182
|
option(opt) {
|
|
103
183
|
this.#validateOptionConfig(opt);
|
|
104
184
|
this.#checkOptionUniqueness(opt);
|
|
@@ -121,67 +201,59 @@ class Command {
|
|
|
121
201
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
122
202
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
123
203
|
}
|
|
124
|
-
const existing = this.#
|
|
204
|
+
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
125
205
|
if (existing) {
|
|
126
206
|
existing.aliases.push(name);
|
|
207
|
+
this.#subcommandsMap.set(name, cmd);
|
|
127
208
|
}
|
|
128
209
|
else {
|
|
129
210
|
cmd.#name = name;
|
|
130
211
|
cmd.#parent = this;
|
|
131
|
-
this.#
|
|
212
|
+
this.#subcommandsList.push({ name, aliases: [], command: cmd });
|
|
213
|
+
this.#subcommandsMap.set(name, cmd);
|
|
132
214
|
}
|
|
133
215
|
return this;
|
|
134
216
|
}
|
|
135
217
|
async run(params) {
|
|
136
|
-
const { argv, envs, reporter } = params;
|
|
218
|
+
const { argv, envs, reporter: reporter$1 } = params;
|
|
137
219
|
try {
|
|
138
220
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
139
|
-
const
|
|
221
|
+
const routeResult = this.#route(processedArgv);
|
|
222
|
+
const { chain, remaining } = routeResult;
|
|
140
223
|
const leafCommand = chain[chain.length - 1];
|
|
141
224
|
const rootCommand = chain[0];
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
145
|
-
const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
|
|
225
|
+
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
226
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
146
227
|
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
147
228
|
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
148
|
-
if (!hasUserHelp &&
|
|
229
|
+
if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
149
230
|
console.log(leafCommand.formatHelp());
|
|
150
231
|
return;
|
|
151
232
|
}
|
|
152
|
-
if (!hasUserVersion && leafCommand === rootCommand) {
|
|
153
|
-
if (
|
|
154
|
-
console.log(leafCommand
|
|
233
|
+
if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
|
|
234
|
+
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
235
|
+
console.log(leafCommand.#version);
|
|
155
236
|
return;
|
|
156
237
|
}
|
|
157
238
|
}
|
|
158
|
-
const
|
|
239
|
+
const resolveResult = this.#resolve(chain, optionTokens);
|
|
159
240
|
const ctx = {
|
|
160
241
|
cmd: leafCommand,
|
|
161
242
|
envs,
|
|
162
|
-
reporter: reporter ?? this.#reporter ?? new
|
|
243
|
+
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
163
244
|
argv,
|
|
164
245
|
};
|
|
165
|
-
this.#
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
247
|
+
const actionParams = {
|
|
248
|
+
ctx: parseResult.ctx,
|
|
249
|
+
opts: parseResult.opts,
|
|
250
|
+
args: parseResult.args,
|
|
251
|
+
rawArgs: parseResult.rawArgs,
|
|
252
|
+
};
|
|
170
253
|
if (leafCommand.#action) {
|
|
171
|
-
|
|
172
|
-
await leafCommand.#action(actionParams);
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
if (err instanceof Error) {
|
|
176
|
-
console.error(`Error: ${err.message}`);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
console.error('Error: action failed');
|
|
180
|
-
}
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
254
|
+
await leafCommand.#runAction(actionParams);
|
|
183
255
|
}
|
|
184
|
-
else if (leafCommand.#
|
|
256
|
+
else if (leafCommand.#subcommandsList.length > 0) {
|
|
185
257
|
console.log(leafCommand.formatHelp());
|
|
186
258
|
}
|
|
187
259
|
else {
|
|
@@ -197,105 +269,33 @@ class Command {
|
|
|
197
269
|
throw err;
|
|
198
270
|
}
|
|
199
271
|
}
|
|
200
|
-
parse(
|
|
272
|
+
parse(params) {
|
|
273
|
+
const { argv, envs, reporter: reporter$1 } = params;
|
|
201
274
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
202
|
-
const
|
|
275
|
+
const routeResult = this.#route(processedArgv);
|
|
276
|
+
const { chain, remaining } = routeResult;
|
|
203
277
|
const leafCommand = chain[chain.length - 1];
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return this.#shiftWithShadowed(tokens, new Set());
|
|
215
|
-
}
|
|
216
|
-
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
217
|
-
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
218
|
-
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
219
|
-
const opts = {};
|
|
220
|
-
for (const opt of directOptions) {
|
|
221
|
-
if (opt.default !== undefined) {
|
|
222
|
-
opts[opt.long] = opt.default;
|
|
223
|
-
}
|
|
224
|
-
else if (opt.type === 'boolean') {
|
|
225
|
-
opts[opt.long] = false;
|
|
226
|
-
}
|
|
227
|
-
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
228
|
-
opts[opt.long] = [];
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
let remaining = [...tokens];
|
|
232
|
-
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
233
|
-
for (const opt of resolverOptions) {
|
|
234
|
-
const result = opt.resolver(remaining);
|
|
235
|
-
opts[opt.long] = result.value;
|
|
236
|
-
remaining = result.remaining;
|
|
237
|
-
}
|
|
238
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
239
|
-
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
240
|
-
const finalRemaining = [];
|
|
241
|
-
let i = 0;
|
|
242
|
-
while (i < normalizedTokens.length) {
|
|
243
|
-
const token = normalizedTokens[i];
|
|
244
|
-
if (token.startsWith('--')) {
|
|
245
|
-
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
246
|
-
if (consumed > 0) {
|
|
247
|
-
i += consumed;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
finalRemaining.push(token);
|
|
251
|
-
i += 1;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
255
|
-
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
256
|
-
if (result.consumed) {
|
|
257
|
-
i = result.nextIdx;
|
|
258
|
-
if (result.remainingToken) {
|
|
259
|
-
finalRemaining.push(result.remainingToken);
|
|
260
|
-
}
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
finalRemaining.push(token);
|
|
264
|
-
i += 1;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
finalRemaining.push(token);
|
|
268
|
-
i += 1;
|
|
269
|
-
}
|
|
270
|
-
for (const opt of directOptions) {
|
|
271
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
272
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
for (const opt of directOptions) {
|
|
276
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
277
|
-
const value = opts[opt.long];
|
|
278
|
-
const values = Array.isArray(value) ? value : [value];
|
|
279
|
-
const choices = opt.choices;
|
|
280
|
-
for (const v of values) {
|
|
281
|
-
if (!choices.includes(v)) {
|
|
282
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return { opts, remaining: finalRemaining };
|
|
278
|
+
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
279
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
280
|
+
const resolveResult = this.#resolve(chain, optionTokens);
|
|
281
|
+
const ctx = {
|
|
282
|
+
cmd: leafCommand,
|
|
283
|
+
envs,
|
|
284
|
+
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
285
|
+
argv,
|
|
286
|
+
};
|
|
287
|
+
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
288
288
|
}
|
|
289
289
|
formatHelp() {
|
|
290
290
|
const lines = [];
|
|
291
291
|
const allOptions = this.#getMergedOptions();
|
|
292
|
-
lines.push(this.#
|
|
292
|
+
lines.push(this.#desc);
|
|
293
293
|
lines.push('');
|
|
294
294
|
const commandPath = this.#getCommandPath();
|
|
295
295
|
let usage = `Usage: ${commandPath}`;
|
|
296
296
|
if (allOptions.length > 0)
|
|
297
297
|
usage += ' [options]';
|
|
298
|
-
if (this.#
|
|
298
|
+
if (this.#subcommandsList.length > 0)
|
|
299
299
|
usage += ' [command]';
|
|
300
300
|
for (const arg of this.#arguments) {
|
|
301
301
|
if (arg.kind === 'required') {
|
|
@@ -314,24 +314,24 @@ class Command {
|
|
|
314
314
|
lines.push('Options:');
|
|
315
315
|
const optLines = [];
|
|
316
316
|
for (const opt of allOptions) {
|
|
317
|
+
const kebabLong = camelToKebabCase$1(opt.long);
|
|
317
318
|
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
318
|
-
sig += `--${
|
|
319
|
-
|
|
320
|
-
if (effectiveType !== 'boolean') {
|
|
319
|
+
sig += `--${kebabLong}`;
|
|
320
|
+
if (opt.args !== 'none') {
|
|
321
321
|
sig += ' <value>';
|
|
322
322
|
}
|
|
323
|
-
let desc = opt.
|
|
324
|
-
if (opt.default !== undefined &&
|
|
323
|
+
let desc = opt.desc;
|
|
324
|
+
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
325
325
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
326
326
|
}
|
|
327
327
|
if (opt.choices) {
|
|
328
328
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
329
329
|
}
|
|
330
330
|
optLines.push({ sig, desc });
|
|
331
|
-
if (
|
|
331
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
332
332
|
optLines.push({
|
|
333
|
-
sig: ` --no-${
|
|
334
|
-
desc: `Negate --${
|
|
333
|
+
sig: ` --no-${kebabLong}`,
|
|
334
|
+
desc: `Negate --${kebabLong}`,
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
337
|
}
|
|
@@ -342,19 +342,19 @@ class Command {
|
|
|
342
342
|
}
|
|
343
343
|
lines.push('');
|
|
344
344
|
}
|
|
345
|
-
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#
|
|
346
|
-
if (this.#
|
|
345
|
+
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
|
|
346
|
+
if (this.#subcommandsList.length > 0) {
|
|
347
347
|
lines.push('Commands:');
|
|
348
348
|
const cmdLines = [];
|
|
349
349
|
if (showHelpSubcommand) {
|
|
350
350
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
351
351
|
}
|
|
352
|
-
for (const entry of this.#
|
|
352
|
+
for (const entry of this.#subcommandsList) {
|
|
353
353
|
let name = entry.name;
|
|
354
354
|
if (entry.aliases.length > 0) {
|
|
355
355
|
name += `, ${entry.aliases.join(', ')}`;
|
|
356
356
|
}
|
|
357
|
-
cmdLines.push({ name, desc: entry.command.#
|
|
357
|
+
cmdLines.push({ name, desc: entry.command.#desc });
|
|
358
358
|
}
|
|
359
359
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
360
360
|
for (const { name, desc } of cmdLines) {
|
|
@@ -369,21 +369,20 @@ class Command {
|
|
|
369
369
|
const allOptions = this.#getMergedOptions();
|
|
370
370
|
const options = [];
|
|
371
371
|
for (const opt of allOptions) {
|
|
372
|
-
const effectiveType = opt.type ?? 'string';
|
|
373
372
|
options.push({
|
|
374
373
|
long: opt.long,
|
|
375
374
|
short: opt.short,
|
|
376
|
-
|
|
377
|
-
takesValue:
|
|
375
|
+
desc: opt.desc,
|
|
376
|
+
takesValue: opt.args !== 'none',
|
|
378
377
|
choices: opt.choices,
|
|
379
378
|
});
|
|
380
379
|
}
|
|
381
380
|
return {
|
|
382
381
|
name: this.#name,
|
|
383
|
-
|
|
382
|
+
desc: this.#desc,
|
|
384
383
|
aliases: [],
|
|
385
384
|
options,
|
|
386
|
-
subcommands: this.#
|
|
385
|
+
subcommands: this.#subcommandsList.map(entry => {
|
|
387
386
|
const subMeta = entry.command.getCompletionMeta();
|
|
388
387
|
return {
|
|
389
388
|
...subMeta,
|
|
@@ -398,17 +397,17 @@ class Command {
|
|
|
398
397
|
return argv;
|
|
399
398
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
400
399
|
return argv;
|
|
401
|
-
if (argv.length === 1 || this.#
|
|
400
|
+
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
402
401
|
return ['--help'];
|
|
403
402
|
}
|
|
404
403
|
const subName = argv[1];
|
|
405
|
-
const entry = this.#
|
|
404
|
+
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
406
405
|
if (entry) {
|
|
407
406
|
return [subName, '--help', ...argv.slice(2)];
|
|
408
407
|
}
|
|
409
408
|
return argv;
|
|
410
409
|
}
|
|
411
|
-
#
|
|
410
|
+
#route(argv) {
|
|
412
411
|
const chain = [this];
|
|
413
412
|
let current = this;
|
|
414
413
|
let idx = 0;
|
|
@@ -416,7 +415,7 @@ class Command {
|
|
|
416
415
|
const token = argv[idx];
|
|
417
416
|
if (token.startsWith('-'))
|
|
418
417
|
break;
|
|
419
|
-
const entry = current.#
|
|
418
|
+
const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
420
419
|
if (!entry)
|
|
421
420
|
break;
|
|
422
421
|
current = entry.command;
|
|
@@ -425,93 +424,299 @@ class Command {
|
|
|
425
424
|
}
|
|
426
425
|
return { chain, remaining: argv.slice(idx) };
|
|
427
426
|
}
|
|
428
|
-
#
|
|
429
|
-
const
|
|
430
|
-
if (ddIdx === -1) {
|
|
431
|
-
return { optionTokens: tokens, restArgs: [] };
|
|
432
|
-
}
|
|
433
|
-
return {
|
|
434
|
-
optionTokens: tokens.slice(0, ddIdx),
|
|
435
|
-
restArgs: tokens.slice(ddIdx + 1),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
#shiftChain(chain, tokens, includeRootVersion) {
|
|
439
|
-
const optsMap = new Map();
|
|
427
|
+
#resolve(chain, tokens) {
|
|
428
|
+
const consumedTokens = new Map();
|
|
440
429
|
let remaining = [...tokens];
|
|
441
|
-
const rootCommand = chain[0];
|
|
442
430
|
const shadowed = new Set();
|
|
443
431
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
444
432
|
const cmd = chain[i];
|
|
445
|
-
const includeVersion =
|
|
446
|
-
const result = cmd.#
|
|
447
|
-
|
|
433
|
+
const includeVersion = i === 0;
|
|
434
|
+
const result = cmd.#shift(remaining, shadowed, includeVersion);
|
|
435
|
+
consumedTokens.set(cmd, result.consumed);
|
|
448
436
|
remaining = result.remaining;
|
|
449
437
|
for (const opt of cmd.#options) {
|
|
450
438
|
shadowed.add(opt.long);
|
|
451
439
|
}
|
|
452
440
|
}
|
|
453
|
-
const
|
|
441
|
+
const argTokens = [];
|
|
454
442
|
for (const token of remaining) {
|
|
455
|
-
if (token.
|
|
443
|
+
if (token.type !== 'none') {
|
|
456
444
|
const leafCommand = chain[chain.length - 1];
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
445
|
+
throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
446
|
+
}
|
|
447
|
+
argTokens.push(token);
|
|
448
|
+
}
|
|
449
|
+
return { consumedTokens, argTokens };
|
|
450
|
+
}
|
|
451
|
+
#shift(tokens, shadowed, includeVersion) {
|
|
452
|
+
const allOptions = this.#getMergedOptions(includeVersion);
|
|
453
|
+
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
454
|
+
const optionByLong = new Map();
|
|
455
|
+
const optionByShort = new Map();
|
|
456
|
+
for (const opt of effectiveOptions) {
|
|
457
|
+
optionByLong.set(opt.long, opt);
|
|
458
|
+
if (opt.short) {
|
|
459
|
+
optionByShort.set(opt.short, opt);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const consumed = [];
|
|
463
|
+
const remaining = [];
|
|
464
|
+
let i = 0;
|
|
465
|
+
while (i < tokens.length) {
|
|
466
|
+
const token = tokens[i];
|
|
467
|
+
if (token.type === 'long') {
|
|
468
|
+
const opt = optionByLong.get(token.name);
|
|
469
|
+
if (opt) {
|
|
470
|
+
consumed.push(token);
|
|
471
|
+
if (opt.args === 'required') {
|
|
472
|
+
if (!token.resolved.includes('=') && i + 1 < tokens.length) {
|
|
473
|
+
i += 1;
|
|
474
|
+
consumed.push(tokens[i]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (opt.args === 'variadic') {
|
|
478
|
+
if (!token.resolved.includes('=')) {
|
|
479
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
480
|
+
i += 1;
|
|
481
|
+
consumed.push(tokens[i]);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
i += 1;
|
|
486
|
+
continue;
|
|
460
487
|
}
|
|
461
|
-
|
|
488
|
+
remaining.push(token);
|
|
489
|
+
i += 1;
|
|
490
|
+
continue;
|
|
462
491
|
}
|
|
463
|
-
|
|
492
|
+
if (token.type === 'short') {
|
|
493
|
+
const opt = optionByShort.get(token.name);
|
|
494
|
+
if (opt) {
|
|
495
|
+
consumed.push(token);
|
|
496
|
+
if (opt.args === 'required') {
|
|
497
|
+
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
498
|
+
i += 1;
|
|
499
|
+
consumed.push(tokens[i]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else if (opt.args === 'variadic') {
|
|
503
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
504
|
+
i += 1;
|
|
505
|
+
consumed.push(tokens[i]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
i += 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
remaining.push(token);
|
|
512
|
+
i += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
remaining.push(token);
|
|
516
|
+
i += 1;
|
|
464
517
|
}
|
|
465
|
-
return {
|
|
518
|
+
return { consumed, remaining };
|
|
466
519
|
}
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
520
|
+
#parse(chain, resolveResult, ctx, restArgs) {
|
|
521
|
+
const { consumedTokens, argTokens } = resolveResult;
|
|
522
|
+
const leafCommand = chain[chain.length - 1];
|
|
523
|
+
this.#validateMergedShortOptions(chain);
|
|
524
|
+
const optsMap = new Map();
|
|
525
|
+
for (let i = 0; i < chain.length; i++) {
|
|
526
|
+
const cmd = chain[i];
|
|
527
|
+
const includeVersion = i === 0;
|
|
528
|
+
const tokens = consumedTokens.get(cmd) ?? [];
|
|
529
|
+
const opts = cmd.#parseOptions(tokens, includeVersion);
|
|
530
|
+
optsMap.set(cmd, opts);
|
|
531
|
+
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
471
532
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
472
533
|
opt.apply(opts[opt.long], ctx);
|
|
473
534
|
}
|
|
474
535
|
}
|
|
475
536
|
}
|
|
476
|
-
|
|
477
|
-
#mergeOpts(chain, optsMap) {
|
|
478
|
-
const merged = {};
|
|
537
|
+
const mergedOpts = {};
|
|
479
538
|
for (const cmd of chain) {
|
|
480
|
-
Object.assign(
|
|
539
|
+
Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
|
|
481
540
|
}
|
|
482
|
-
|
|
541
|
+
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
542
|
+
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
543
|
+
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
483
544
|
}
|
|
484
|
-
#
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
545
|
+
#parseOptions(tokens, includeVersion) {
|
|
546
|
+
const allOptions = this.#getMergedOptions(includeVersion);
|
|
547
|
+
const opts = {};
|
|
548
|
+
for (const opt of allOptions) {
|
|
549
|
+
if (opt.default !== undefined) {
|
|
550
|
+
opts[opt.long] = opt.default;
|
|
551
|
+
}
|
|
552
|
+
else if (opt.type === 'boolean' && opt.args === 'none') {
|
|
553
|
+
opts[opt.long] = false;
|
|
554
|
+
}
|
|
555
|
+
else if (opt.args === 'variadic') {
|
|
556
|
+
opts[opt.long] = [];
|
|
557
|
+
}
|
|
489
558
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
559
|
+
const optionByLong = new Map();
|
|
560
|
+
const optionByShort = new Map();
|
|
561
|
+
for (const opt of allOptions) {
|
|
562
|
+
optionByLong.set(opt.long, opt);
|
|
563
|
+
if (opt.short) {
|
|
564
|
+
optionByShort.set(opt.short, opt);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
let i = 0;
|
|
568
|
+
while (i < tokens.length) {
|
|
569
|
+
const token = tokens[i];
|
|
570
|
+
const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
|
|
571
|
+
if (!opt) {
|
|
572
|
+
i += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
576
|
+
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
577
|
+
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
578
|
+
}
|
|
579
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
580
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
581
|
+
if (eqIdx !== -1) {
|
|
582
|
+
const value = token.resolved.slice(eqIdx + 1);
|
|
583
|
+
if (value === 'true') {
|
|
584
|
+
opts[opt.long] = true;
|
|
501
585
|
}
|
|
502
|
-
|
|
503
|
-
|
|
586
|
+
else if (value === 'false') {
|
|
587
|
+
opts[opt.long] = false;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
opts[opt.long] = true;
|
|
595
|
+
}
|
|
596
|
+
i += 1;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (opt.args === 'required') {
|
|
600
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
601
|
+
let rawValue;
|
|
602
|
+
if (eqIdx !== -1) {
|
|
603
|
+
rawValue = token.resolved.slice(eqIdx + 1);
|
|
604
|
+
}
|
|
605
|
+
else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
606
|
+
rawValue = tokens[i + 1].original;
|
|
607
|
+
i += 1;
|
|
504
608
|
}
|
|
609
|
+
else {
|
|
610
|
+
throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
|
|
611
|
+
}
|
|
612
|
+
opts[opt.long] = this.#convertValue(opt, rawValue);
|
|
613
|
+
i += 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (opt.args === 'variadic') {
|
|
617
|
+
const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
|
|
618
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
619
|
+
if (eqIdx !== -1) {
|
|
620
|
+
values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
624
|
+
i += 1;
|
|
625
|
+
values.push(this.#convertValue(opt, tokens[i].original));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
opts[opt.long] = values;
|
|
629
|
+
i += 1;
|
|
630
|
+
continue;
|
|
505
631
|
}
|
|
632
|
+
i += 1;
|
|
506
633
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
634
|
+
for (const opt of allOptions) {
|
|
635
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
636
|
+
throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
637
|
+
}
|
|
511
638
|
}
|
|
512
|
-
|
|
513
|
-
opts[opt.long]
|
|
639
|
+
for (const opt of allOptions) {
|
|
640
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
641
|
+
const value = opts[opt.long];
|
|
642
|
+
const values = Array.isArray(value) ? value : [value];
|
|
643
|
+
const choices = opt.choices;
|
|
644
|
+
for (const v of values) {
|
|
645
|
+
if (!choices.includes(v)) {
|
|
646
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return opts;
|
|
652
|
+
}
|
|
653
|
+
#convertValue(opt, rawValue) {
|
|
654
|
+
if (opt.coerce) {
|
|
655
|
+
return opt.coerce(rawValue);
|
|
514
656
|
}
|
|
657
|
+
if (opt.type === 'number') {
|
|
658
|
+
const num = Number(rawValue);
|
|
659
|
+
if (Number.isNaN(num)) {
|
|
660
|
+
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
661
|
+
}
|
|
662
|
+
return num;
|
|
663
|
+
}
|
|
664
|
+
return rawValue;
|
|
665
|
+
}
|
|
666
|
+
#parseArguments(rawArgs) {
|
|
667
|
+
const argumentDefs = this.#arguments;
|
|
668
|
+
const args = {};
|
|
669
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
670
|
+
if (rawArgs.length < requiredCount) {
|
|
671
|
+
const missing = argumentDefs
|
|
672
|
+
.filter(a => a.kind === 'required')
|
|
673
|
+
.slice(rawArgs.length)
|
|
674
|
+
.map(a => a.name);
|
|
675
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
676
|
+
}
|
|
677
|
+
let index = 0;
|
|
678
|
+
for (const def of argumentDefs) {
|
|
679
|
+
if (def.kind === 'variadic') {
|
|
680
|
+
const rest = rawArgs.slice(index);
|
|
681
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
682
|
+
index = rawArgs.length;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
const raw = rawArgs[index];
|
|
686
|
+
if (raw === undefined) {
|
|
687
|
+
if (def.kind === 'optional') {
|
|
688
|
+
args[def.name] = def.default ?? undefined;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
694
|
+
index += 1;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
698
|
+
if (!hasVariadic && index < rawArgs.length) {
|
|
699
|
+
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
700
|
+
}
|
|
701
|
+
return { args, rawArgs };
|
|
702
|
+
}
|
|
703
|
+
#convertArgument(def, raw) {
|
|
704
|
+
if (def.coerce) {
|
|
705
|
+
try {
|
|
706
|
+
return def.coerce(raw);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (def.type === 'number') {
|
|
713
|
+
const n = Number(raw);
|
|
714
|
+
if (Number.isNaN(n)) {
|
|
715
|
+
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
716
|
+
}
|
|
717
|
+
return n;
|
|
718
|
+
}
|
|
719
|
+
return raw;
|
|
515
720
|
}
|
|
516
721
|
#getMergedOptions(includeVersion = !this.#parent) {
|
|
517
722
|
const optionMap = new Map();
|
|
@@ -528,11 +733,11 @@ class Command {
|
|
|
528
733
|
}
|
|
529
734
|
return Array.from(optionMap.values());
|
|
530
735
|
}
|
|
531
|
-
#validateMergedShortOptions(chain
|
|
736
|
+
#validateMergedShortOptions(chain) {
|
|
532
737
|
const mergedByLong = new Map();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const includeVersion =
|
|
738
|
+
for (let i = 0; i < chain.length; i++) {
|
|
739
|
+
const cmd = chain[i];
|
|
740
|
+
const includeVersion = i === 0;
|
|
536
741
|
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
537
742
|
mergedByLong.set(opt.long, opt);
|
|
538
743
|
}
|
|
@@ -549,8 +754,17 @@ class Command {
|
|
|
549
754
|
}
|
|
550
755
|
}
|
|
551
756
|
#validateOptionConfig(opt) {
|
|
552
|
-
if (opt.
|
|
553
|
-
throw new CommanderError('ConfigurationError', `option
|
|
757
|
+
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
758
|
+
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
759
|
+
}
|
|
760
|
+
if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
|
|
761
|
+
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
|
|
762
|
+
}
|
|
763
|
+
if (opt.long.startsWith('no')) {
|
|
764
|
+
throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
|
|
765
|
+
}
|
|
766
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
767
|
+
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
554
768
|
}
|
|
555
769
|
if (opt.required && opt.default !== undefined) {
|
|
556
770
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
@@ -589,154 +803,33 @@ class Command {
|
|
|
589
803
|
}
|
|
590
804
|
}
|
|
591
805
|
}
|
|
592
|
-
#
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const missing = argumentDefs
|
|
598
|
-
.filter(a => a.kind === 'required')
|
|
599
|
-
.slice(rawArgs.length)
|
|
600
|
-
.map(a => a.name);
|
|
601
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
806
|
+
async #runAction(params) {
|
|
807
|
+
if (!this.#action)
|
|
808
|
+
return;
|
|
809
|
+
try {
|
|
810
|
+
await this.#action(params);
|
|
602
811
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const rest = rawArgs.slice(index);
|
|
607
|
-
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
608
|
-
index = rawArgs.length;
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
const raw = rawArgs[index];
|
|
612
|
-
if (raw === undefined) {
|
|
613
|
-
if (def.kind === 'optional') {
|
|
614
|
-
args[def.name] = def.default ?? undefined;
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
if (err instanceof Error) {
|
|
814
|
+
console.error(`Error: ${err.message}`);
|
|
617
815
|
}
|
|
618
816
|
else {
|
|
619
|
-
|
|
620
|
-
index += 1;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
624
|
-
if (!hasVariadic && index < rawArgs.length) {
|
|
625
|
-
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
626
|
-
}
|
|
627
|
-
return { args, rawArgs };
|
|
628
|
-
}
|
|
629
|
-
#convertArgument(def, raw) {
|
|
630
|
-
if (def.coerce) {
|
|
631
|
-
try {
|
|
632
|
-
return def.coerce(raw);
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (def.type === 'number') {
|
|
639
|
-
const n = Number(raw);
|
|
640
|
-
if (Number.isNaN(n)) {
|
|
641
|
-
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
642
|
-
}
|
|
643
|
-
return n;
|
|
644
|
-
}
|
|
645
|
-
return raw;
|
|
646
|
-
}
|
|
647
|
-
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
648
|
-
const optionByLong = new Map();
|
|
649
|
-
const optionByShort = new Map();
|
|
650
|
-
const booleanOptions = new Set();
|
|
651
|
-
for (const opt of allOptions) {
|
|
652
|
-
if (excludeResolver && opt.resolver)
|
|
653
|
-
continue;
|
|
654
|
-
optionByLong.set(opt.long, opt);
|
|
655
|
-
if (opt.short) {
|
|
656
|
-
optionByShort.set(opt.short, opt);
|
|
657
|
-
}
|
|
658
|
-
if (opt.type === 'boolean') {
|
|
659
|
-
booleanOptions.add(opt.long);
|
|
817
|
+
console.error('Error: action failed');
|
|
660
818
|
}
|
|
819
|
+
process.exit(1);
|
|
661
820
|
}
|
|
662
|
-
return { optionByLong, optionByShort, booleanOptions };
|
|
663
|
-
}
|
|
664
|
-
#hasHelpFlag(argv, allOptions) {
|
|
665
|
-
return this.#hasBuiltinFlag(argv, 'help', 'h', allOptions);
|
|
666
|
-
}
|
|
667
|
-
#hasVersionFlag(argv, allOptions) {
|
|
668
|
-
return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
|
|
669
821
|
}
|
|
670
|
-
#
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
674
|
-
const arg = normalizedArgv[i];
|
|
675
|
-
if (arg === '--') {
|
|
676
|
-
break;
|
|
677
|
-
}
|
|
678
|
-
if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
|
|
822
|
+
#hasFlag(tokens, longName, shortName) {
|
|
823
|
+
for (const token of tokens) {
|
|
824
|
+
if (token.type === 'long' && token.name === longName) {
|
|
679
825
|
return true;
|
|
680
826
|
}
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
return false;
|
|
686
|
-
}
|
|
687
|
-
#optionConsumesNextValue(arg, optionByLong, optionByShort) {
|
|
688
|
-
if (arg.startsWith('--')) {
|
|
689
|
-
const eqIdx = arg.indexOf('=');
|
|
690
|
-
if (eqIdx !== -1) {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
const optName = arg.slice(2);
|
|
694
|
-
const opt = optionByLong.get(optName);
|
|
695
|
-
if (!opt) {
|
|
696
|
-
return false;
|
|
697
|
-
}
|
|
698
|
-
const type = opt.type ?? 'string';
|
|
699
|
-
return type !== 'boolean';
|
|
700
|
-
}
|
|
701
|
-
if (arg.startsWith('-') && arg.length === 2) {
|
|
702
|
-
const opt = optionByShort.get(arg[1]);
|
|
703
|
-
if (!opt) {
|
|
704
|
-
return false;
|
|
827
|
+
if (token.type === 'short' && token.name === shortName) {
|
|
828
|
+
return true;
|
|
705
829
|
}
|
|
706
|
-
const type = opt.type ?? 'string';
|
|
707
|
-
return type !== 'boolean';
|
|
708
830
|
}
|
|
709
831
|
return false;
|
|
710
832
|
}
|
|
711
|
-
#normalizeArgv(argv, booleanOptions) {
|
|
712
|
-
const result = [];
|
|
713
|
-
let seenDoubleDash = false;
|
|
714
|
-
for (const arg of argv) {
|
|
715
|
-
if (arg === '--') {
|
|
716
|
-
seenDoubleDash = true;
|
|
717
|
-
result.push(arg);
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
if (!seenDoubleDash && arg.startsWith('--no-')) {
|
|
721
|
-
const eqIdx = arg.indexOf('=');
|
|
722
|
-
if (eqIdx !== -1) {
|
|
723
|
-
const optName = arg.slice(5, eqIdx);
|
|
724
|
-
if (booleanOptions.has(optName)) {
|
|
725
|
-
throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
const optName = arg.slice(5);
|
|
730
|
-
if (booleanOptions.has(optName)) {
|
|
731
|
-
result.push(`--${optName}=false`);
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
result.push(arg);
|
|
737
|
-
}
|
|
738
|
-
return result;
|
|
739
|
-
}
|
|
740
833
|
#getCommandPath() {
|
|
741
834
|
const parts = [];
|
|
742
835
|
let current = this;
|
|
@@ -748,130 +841,41 @@ class Command {
|
|
|
748
841
|
}
|
|
749
842
|
return parts.join(' ') || this.#name;
|
|
750
843
|
}
|
|
751
|
-
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
752
|
-
const token = tokens[idx];
|
|
753
|
-
const eqIdx = token.indexOf('=');
|
|
754
|
-
let optName;
|
|
755
|
-
let inlineValue;
|
|
756
|
-
if (eqIdx !== -1) {
|
|
757
|
-
optName = token.slice(2, eqIdx);
|
|
758
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
optName = token.slice(2);
|
|
762
|
-
}
|
|
763
|
-
const opt = optionByLong.get(optName);
|
|
764
|
-
if (!opt) {
|
|
765
|
-
return 0;
|
|
766
|
-
}
|
|
767
|
-
if (opt.type === 'boolean') {
|
|
768
|
-
if (inlineValue !== undefined) {
|
|
769
|
-
if (inlineValue === 'true') {
|
|
770
|
-
opts[optName] = true;
|
|
771
|
-
}
|
|
772
|
-
else if (inlineValue === 'false') {
|
|
773
|
-
opts[optName] = false;
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
else {
|
|
780
|
-
opts[optName] = true;
|
|
781
|
-
}
|
|
782
|
-
return 1;
|
|
783
|
-
}
|
|
784
|
-
let value;
|
|
785
|
-
let consumed = 1;
|
|
786
|
-
if (inlineValue !== undefined) {
|
|
787
|
-
value = inlineValue;
|
|
788
|
-
}
|
|
789
|
-
else if (idx + 1 < tokens.length) {
|
|
790
|
-
value = tokens[idx + 1];
|
|
791
|
-
consumed = 2;
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
795
|
-
}
|
|
796
|
-
this.#applyValue(opt, value, opts);
|
|
797
|
-
return consumed;
|
|
798
|
-
}
|
|
799
|
-
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
800
|
-
const token = tokens[idx];
|
|
801
|
-
if (token.includes('=')) {
|
|
802
|
-
const firstFlag = token[1];
|
|
803
|
-
if (!optionByShort.has(firstFlag)) {
|
|
804
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
805
|
-
}
|
|
806
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
807
|
-
}
|
|
808
|
-
const flags = token.slice(1);
|
|
809
|
-
let j = 0;
|
|
810
|
-
const consumedFlags = [];
|
|
811
|
-
const unconsumedFlags = [];
|
|
812
|
-
let nextIdx = idx + 1;
|
|
813
|
-
while (j < flags.length) {
|
|
814
|
-
const flag = flags[j];
|
|
815
|
-
const opt = optionByShort.get(flag);
|
|
816
|
-
if (!opt) {
|
|
817
|
-
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
consumedFlags.push(flag);
|
|
821
|
-
if (opt.type === 'boolean') {
|
|
822
|
-
opts[opt.long] = true;
|
|
823
|
-
j += 1;
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
if (j < flags.length - 1) {
|
|
827
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
828
|
-
}
|
|
829
|
-
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
830
|
-
const value = tokens[idx + 1];
|
|
831
|
-
this.#applyValue(opt, value, opts);
|
|
832
|
-
nextIdx = idx + 2;
|
|
833
|
-
}
|
|
834
|
-
else {
|
|
835
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
836
|
-
}
|
|
837
|
-
j += 1;
|
|
838
|
-
}
|
|
839
|
-
if (consumedFlags.length > 0) {
|
|
840
|
-
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
841
|
-
return { consumed: true, nextIdx, remainingToken };
|
|
842
|
-
}
|
|
843
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
844
|
-
}
|
|
845
844
|
}
|
|
846
845
|
|
|
846
|
+
function camelToKebabCase(str) {
|
|
847
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
848
|
+
}
|
|
847
849
|
class CompletionCommand extends Command {
|
|
848
850
|
constructor(root, config) {
|
|
849
851
|
const paths = config.paths;
|
|
850
|
-
const programName = config.programName ?? root.name;
|
|
851
|
-
super({
|
|
852
|
-
description: 'Generate shell completion script',
|
|
853
|
-
});
|
|
852
|
+
const programName = config.programName ?? root.name ?? 'program';
|
|
853
|
+
super({ desc: 'Generate shell completion script' });
|
|
854
854
|
this.option({
|
|
855
855
|
long: 'bash',
|
|
856
856
|
type: 'boolean',
|
|
857
|
-
|
|
857
|
+
args: 'none',
|
|
858
|
+
desc: 'Generate Bash completion script',
|
|
858
859
|
})
|
|
859
860
|
.option({
|
|
860
861
|
long: 'fish',
|
|
861
862
|
type: 'boolean',
|
|
862
|
-
|
|
863
|
+
args: 'none',
|
|
864
|
+
desc: 'Generate Fish completion script',
|
|
863
865
|
})
|
|
864
866
|
.option({
|
|
865
867
|
long: 'pwsh',
|
|
866
868
|
type: 'boolean',
|
|
867
|
-
|
|
869
|
+
args: 'none',
|
|
870
|
+
desc: 'Generate PowerShell completion script',
|
|
868
871
|
})
|
|
869
872
|
.option({
|
|
870
873
|
long: 'write',
|
|
871
874
|
short: 'w',
|
|
872
875
|
type: 'string',
|
|
873
|
-
|
|
874
|
-
|
|
876
|
+
args: 'required',
|
|
877
|
+
desc: 'Write to file (use shell default path if empty)',
|
|
878
|
+
default: undefined,
|
|
875
879
|
})
|
|
876
880
|
.action(({ opts }) => {
|
|
877
881
|
const meta = root.getCompletionMeta();
|
|
@@ -927,45 +931,6 @@ function expandHome(filepath) {
|
|
|
927
931
|
}
|
|
928
932
|
return filepath;
|
|
929
933
|
}
|
|
930
|
-
function resolveOptionalStringOption(argv, longName, shortName) {
|
|
931
|
-
const remaining = [];
|
|
932
|
-
let value;
|
|
933
|
-
for (let i = 0; i < argv.length; i++) {
|
|
934
|
-
const arg = argv[i];
|
|
935
|
-
if (arg.startsWith(`--${longName}=`)) {
|
|
936
|
-
value = arg.slice(`--${longName}=`.length);
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
if (arg === `--${longName}`) {
|
|
940
|
-
const next = argv[i + 1];
|
|
941
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
942
|
-
value = next;
|
|
943
|
-
i += 1;
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
value = '';
|
|
947
|
-
}
|
|
948
|
-
continue;
|
|
949
|
-
}
|
|
950
|
-
if (arg.startsWith(`-${shortName}=`)) {
|
|
951
|
-
value = arg.slice(`-${shortName}=`.length);
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
if (arg === `-${shortName}`) {
|
|
955
|
-
const next = argv[i + 1];
|
|
956
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
957
|
-
value = next;
|
|
958
|
-
i += 1;
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
value = '';
|
|
962
|
-
}
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
remaining.push(arg);
|
|
966
|
-
}
|
|
967
|
-
return { value, remaining };
|
|
968
|
-
}
|
|
969
934
|
class BashCompletion {
|
|
970
935
|
#meta;
|
|
971
936
|
#programName;
|
|
@@ -998,11 +963,12 @@ class BashCompletion {
|
|
|
998
963
|
const lines = [];
|
|
999
964
|
const optParts = [];
|
|
1000
965
|
for (const opt of cmd.options) {
|
|
966
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1001
967
|
if (opt.short)
|
|
1002
968
|
optParts.push(`-${opt.short}`);
|
|
1003
|
-
optParts.push(`--${
|
|
969
|
+
optParts.push(`--${kebabLong}`);
|
|
1004
970
|
if (!opt.takesValue) {
|
|
1005
|
-
optParts.push(`--no-${
|
|
971
|
+
optParts.push(`--no-${kebabLong}`);
|
|
1006
972
|
}
|
|
1007
973
|
}
|
|
1008
974
|
const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
@@ -1051,13 +1017,14 @@ class FishCompletion {
|
|
|
1051
1017
|
const isRoot = parentPath.length === 0;
|
|
1052
1018
|
const condition = this.#buildCondition(parentPath);
|
|
1053
1019
|
for (const opt of cmd.options) {
|
|
1020
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1054
1021
|
let line = `complete -c ${this.#programName}`;
|
|
1055
1022
|
if (condition)
|
|
1056
1023
|
line += ` -n '${condition}'`;
|
|
1057
1024
|
if (opt.short)
|
|
1058
1025
|
line += ` -s ${opt.short}`;
|
|
1059
|
-
line += ` -l ${
|
|
1060
|
-
line += ` -d '${this.#escape(opt.
|
|
1026
|
+
line += ` -l ${kebabLong}`;
|
|
1027
|
+
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1061
1028
|
if (opt.choices && opt.choices.length > 0) {
|
|
1062
1029
|
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
1063
1030
|
}
|
|
@@ -1066,8 +1033,8 @@ class FishCompletion {
|
|
|
1066
1033
|
let noLine = `complete -c ${this.#programName}`;
|
|
1067
1034
|
if (condition)
|
|
1068
1035
|
noLine += ` -n '${condition}'`;
|
|
1069
|
-
noLine += ` -l no-${
|
|
1070
|
-
noLine += ` -d '${this.#escape(opt.
|
|
1036
|
+
noLine += ` -l no-${kebabLong}`;
|
|
1037
|
+
noLine += ` -d '${this.#escape(opt.desc)}'`;
|
|
1071
1038
|
lines.push(noLine);
|
|
1072
1039
|
}
|
|
1073
1040
|
}
|
|
@@ -1080,7 +1047,7 @@ class FishCompletion {
|
|
|
1080
1047
|
line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1081
1048
|
}
|
|
1082
1049
|
line += ` -a ${sub.name}`;
|
|
1083
|
-
line += ` -d '${this.#escape(sub.
|
|
1050
|
+
line += ` -d '${this.#escape(sub.desc)}'`;
|
|
1084
1051
|
lines.push(line);
|
|
1085
1052
|
for (const alias of sub.aliases) {
|
|
1086
1053
|
let aliasLine = `complete -c ${this.#programName}`;
|
|
@@ -1153,7 +1120,7 @@ class PwshCompletion {
|
|
|
1153
1120
|
' "--$($opt.long)",',
|
|
1154
1121
|
' $opt.long,',
|
|
1155
1122
|
' "ParameterName",',
|
|
1156
|
-
' $opt.
|
|
1123
|
+
' $opt.desc',
|
|
1157
1124
|
' )',
|
|
1158
1125
|
' }',
|
|
1159
1126
|
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
@@ -1161,7 +1128,7 @@ class PwshCompletion {
|
|
|
1161
1128
|
' "--no-$($opt.long)",',
|
|
1162
1129
|
' "no-$($opt.long)",',
|
|
1163
1130
|
' "ParameterName",',
|
|
1164
|
-
' $opt.
|
|
1131
|
+
' $opt.desc',
|
|
1165
1132
|
' )',
|
|
1166
1133
|
' }',
|
|
1167
1134
|
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
@@ -1169,7 +1136,7 @@ class PwshCompletion {
|
|
|
1169
1136
|
' "-$($opt.short)",',
|
|
1170
1137
|
' $opt.short,',
|
|
1171
1138
|
' "ParameterName",',
|
|
1172
|
-
' $opt.
|
|
1139
|
+
' $opt.desc',
|
|
1173
1140
|
' )',
|
|
1174
1141
|
' }',
|
|
1175
1142
|
' }',
|
|
@@ -1183,7 +1150,7 @@ class PwshCompletion {
|
|
|
1183
1150
|
' $sub,',
|
|
1184
1151
|
' $sub,',
|
|
1185
1152
|
' "Command",',
|
|
1186
|
-
' $cmd.subcommands[$sub].
|
|
1153
|
+
' $cmd.subcommands[$sub].desc',
|
|
1187
1154
|
' )',
|
|
1188
1155
|
' }',
|
|
1189
1156
|
' }',
|
|
@@ -1197,14 +1164,15 @@ class PwshCompletion {
|
|
|
1197
1164
|
}
|
|
1198
1165
|
#generateCommandHash(cmd, indent) {
|
|
1199
1166
|
const lines = [];
|
|
1200
|
-
lines.push(`${indent}description = '${this.#escape(cmd.
|
|
1167
|
+
lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
|
|
1201
1168
|
lines.push(`${indent}options = @(`);
|
|
1202
1169
|
for (const opt of cmd.options) {
|
|
1170
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1203
1171
|
lines.push(`${indent} @{`);
|
|
1204
1172
|
if (opt.short)
|
|
1205
1173
|
lines.push(`${indent} short = '${opt.short}'`);
|
|
1206
|
-
lines.push(`${indent} long = '${
|
|
1207
|
-
lines.push(`${indent} description = '${this.#escape(opt.
|
|
1174
|
+
lines.push(`${indent} long = '${kebabLong}'`);
|
|
1175
|
+
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
1208
1176
|
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
1209
1177
|
if (opt.choices) {
|
|
1210
1178
|
lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
|
|
@@ -1233,9 +1201,37 @@ class PwshCompletion {
|
|
|
1233
1201
|
}
|
|
1234
1202
|
}
|
|
1235
1203
|
|
|
1204
|
+
const logLevelOption = {
|
|
1205
|
+
long: 'logLevel',
|
|
1206
|
+
type: 'string',
|
|
1207
|
+
args: 'required',
|
|
1208
|
+
desc: 'Set log level',
|
|
1209
|
+
default: 'info',
|
|
1210
|
+
choices: reporter.LOG_LEVELS,
|
|
1211
|
+
coerce: (raw) => {
|
|
1212
|
+
const level = reporter.resolveLogLevel(raw);
|
|
1213
|
+
if (level === undefined) {
|
|
1214
|
+
throw new Error(`Invalid log level: ${raw}`);
|
|
1215
|
+
}
|
|
1216
|
+
return level;
|
|
1217
|
+
},
|
|
1218
|
+
apply: (value, ctx) => {
|
|
1219
|
+
ctx.reporter.setLevel(value);
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1222
|
+
const silentOption = {
|
|
1223
|
+
long: 'silent',
|
|
1224
|
+
type: 'boolean',
|
|
1225
|
+
args: 'none',
|
|
1226
|
+
desc: 'Suppress non-error output',
|
|
1227
|
+
default: false,
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1236
1230
|
exports.BashCompletion = BashCompletion;
|
|
1237
1231
|
exports.Command = Command;
|
|
1238
1232
|
exports.CommanderError = CommanderError;
|
|
1239
1233
|
exports.CompletionCommand = CompletionCommand;
|
|
1240
1234
|
exports.FishCompletion = FishCompletion;
|
|
1241
1235
|
exports.PwshCompletion = PwshCompletion;
|
|
1236
|
+
exports.logLevelOption = logLevelOption;
|
|
1237
|
+
exports.silentOption = silentOption;
|