@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/esm/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Reporter, LOG_LEVELS, resolveLogLevel } from '@guanghechen/reporter';
|
|
1
2
|
import * as fs from 'node:fs';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
|
|
@@ -15,55 +16,131 @@ class CommanderError extends Error {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
};
|
|
21
52
|
}
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
if (!LONG_OPTION_REGEX.test(lowerName)) {
|
|
54
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
24
55
|
}
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
67
|
}
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
});
|
|
30
103
|
}
|
|
104
|
+
return { optionTokens, restArgs };
|
|
31
105
|
}
|
|
32
106
|
const BUILTIN_HELP_OPTION = {
|
|
33
107
|
long: 'help',
|
|
34
108
|
short: 'h',
|
|
35
109
|
type: 'boolean',
|
|
36
|
-
|
|
110
|
+
args: 'none',
|
|
111
|
+
desc: 'Show help information',
|
|
37
112
|
};
|
|
38
113
|
const BUILTIN_VERSION_OPTION = {
|
|
39
114
|
long: 'version',
|
|
40
115
|
short: 'V',
|
|
41
116
|
type: 'boolean',
|
|
42
|
-
|
|
117
|
+
args: 'none',
|
|
118
|
+
desc: 'Show version number',
|
|
43
119
|
};
|
|
44
120
|
class Command {
|
|
45
121
|
#name;
|
|
46
|
-
#
|
|
122
|
+
#desc;
|
|
47
123
|
#version;
|
|
48
124
|
#helpSubcommandEnabled;
|
|
49
125
|
#reporter;
|
|
50
126
|
#parent;
|
|
51
127
|
#options = [];
|
|
52
128
|
#arguments = [];
|
|
53
|
-
#
|
|
54
|
-
#
|
|
129
|
+
#subcommandsList = [];
|
|
130
|
+
#subcommandsMap = new Map();
|
|
131
|
+
#action = undefined;
|
|
55
132
|
constructor(config) {
|
|
56
133
|
this.#name = config.name ?? '';
|
|
57
|
-
this.#
|
|
134
|
+
this.#desc = config.desc;
|
|
58
135
|
this.#version = config.version;
|
|
59
136
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
60
137
|
this.#reporter = config.reporter;
|
|
61
138
|
}
|
|
62
139
|
get name() {
|
|
63
|
-
return this.#name;
|
|
140
|
+
return this.#name || undefined;
|
|
64
141
|
}
|
|
65
142
|
get description() {
|
|
66
|
-
return this.#
|
|
143
|
+
return this.#desc;
|
|
67
144
|
}
|
|
68
145
|
get version() {
|
|
69
146
|
return this.#version;
|
|
@@ -77,6 +154,9 @@ class Command {
|
|
|
77
154
|
get arguments() {
|
|
78
155
|
return [...this.#arguments];
|
|
79
156
|
}
|
|
157
|
+
get subcommands() {
|
|
158
|
+
return new Map(this.#subcommandsMap);
|
|
159
|
+
}
|
|
80
160
|
option(opt) {
|
|
81
161
|
this.#validateOptionConfig(opt);
|
|
82
162
|
this.#checkOptionUniqueness(opt);
|
|
@@ -99,14 +179,16 @@ class Command {
|
|
|
99
179
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
100
180
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
101
181
|
}
|
|
102
|
-
const existing = this.#
|
|
182
|
+
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
103
183
|
if (existing) {
|
|
104
184
|
existing.aliases.push(name);
|
|
185
|
+
this.#subcommandsMap.set(name, cmd);
|
|
105
186
|
}
|
|
106
187
|
else {
|
|
107
188
|
cmd.#name = name;
|
|
108
189
|
cmd.#parent = this;
|
|
109
|
-
this.#
|
|
190
|
+
this.#subcommandsList.push({ name, aliases: [], command: cmd });
|
|
191
|
+
this.#subcommandsMap.set(name, cmd);
|
|
110
192
|
}
|
|
111
193
|
return this;
|
|
112
194
|
}
|
|
@@ -114,52 +196,42 @@ class Command {
|
|
|
114
196
|
const { argv, envs, reporter } = params;
|
|
115
197
|
try {
|
|
116
198
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
117
|
-
const
|
|
199
|
+
const routeResult = this.#route(processedArgv);
|
|
200
|
+
const { chain, remaining } = routeResult;
|
|
118
201
|
const leafCommand = chain[chain.length - 1];
|
|
119
202
|
const rootCommand = chain[0];
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
123
|
-
const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
|
|
203
|
+
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
204
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
124
205
|
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
125
206
|
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
126
|
-
if (!hasUserHelp &&
|
|
207
|
+
if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
127
208
|
console.log(leafCommand.formatHelp());
|
|
128
209
|
return;
|
|
129
210
|
}
|
|
130
|
-
if (!hasUserVersion && leafCommand === rootCommand) {
|
|
131
|
-
if (
|
|
132
|
-
console.log(leafCommand
|
|
211
|
+
if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
|
|
212
|
+
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
213
|
+
console.log(leafCommand.#version);
|
|
133
214
|
return;
|
|
134
215
|
}
|
|
135
216
|
}
|
|
136
|
-
const
|
|
217
|
+
const resolveResult = this.#resolve(chain, optionTokens);
|
|
137
218
|
const ctx = {
|
|
138
219
|
cmd: leafCommand,
|
|
139
220
|
envs,
|
|
140
|
-
reporter: reporter ?? this.#reporter ?? new
|
|
221
|
+
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
141
222
|
argv,
|
|
142
223
|
};
|
|
143
|
-
this.#
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
};
|
|
148
231
|
if (leafCommand.#action) {
|
|
149
|
-
|
|
150
|
-
await leafCommand.#action(actionParams);
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
if (err instanceof Error) {
|
|
154
|
-
console.error(`Error: ${err.message}`);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
console.error('Error: action failed');
|
|
158
|
-
}
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
232
|
+
await leafCommand.#runAction(actionParams);
|
|
161
233
|
}
|
|
162
|
-
else if (leafCommand.#
|
|
234
|
+
else if (leafCommand.#subcommandsList.length > 0) {
|
|
163
235
|
console.log(leafCommand.formatHelp());
|
|
164
236
|
}
|
|
165
237
|
else {
|
|
@@ -175,105 +247,33 @@ class Command {
|
|
|
175
247
|
throw err;
|
|
176
248
|
}
|
|
177
249
|
}
|
|
178
|
-
parse(
|
|
250
|
+
parse(params) {
|
|
251
|
+
const { argv, envs, reporter } = params;
|
|
179
252
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
180
|
-
const
|
|
253
|
+
const routeResult = this.#route(processedArgv);
|
|
254
|
+
const { chain, remaining } = routeResult;
|
|
181
255
|
const leafCommand = chain[chain.length - 1];
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return this.#shiftWithShadowed(tokens, new Set());
|
|
193
|
-
}
|
|
194
|
-
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
195
|
-
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
196
|
-
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
197
|
-
const opts = {};
|
|
198
|
-
for (const opt of directOptions) {
|
|
199
|
-
if (opt.default !== undefined) {
|
|
200
|
-
opts[opt.long] = opt.default;
|
|
201
|
-
}
|
|
202
|
-
else if (opt.type === 'boolean') {
|
|
203
|
-
opts[opt.long] = false;
|
|
204
|
-
}
|
|
205
|
-
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
206
|
-
opts[opt.long] = [];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
let remaining = [...tokens];
|
|
210
|
-
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
211
|
-
for (const opt of resolverOptions) {
|
|
212
|
-
const result = opt.resolver(remaining);
|
|
213
|
-
opts[opt.long] = result.value;
|
|
214
|
-
remaining = result.remaining;
|
|
215
|
-
}
|
|
216
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
217
|
-
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
218
|
-
const finalRemaining = [];
|
|
219
|
-
let i = 0;
|
|
220
|
-
while (i < normalizedTokens.length) {
|
|
221
|
-
const token = normalizedTokens[i];
|
|
222
|
-
if (token.startsWith('--')) {
|
|
223
|
-
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
224
|
-
if (consumed > 0) {
|
|
225
|
-
i += consumed;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
finalRemaining.push(token);
|
|
229
|
-
i += 1;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
233
|
-
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
234
|
-
if (result.consumed) {
|
|
235
|
-
i = result.nextIdx;
|
|
236
|
-
if (result.remainingToken) {
|
|
237
|
-
finalRemaining.push(result.remainingToken);
|
|
238
|
-
}
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
finalRemaining.push(token);
|
|
242
|
-
i += 1;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
finalRemaining.push(token);
|
|
246
|
-
i += 1;
|
|
247
|
-
}
|
|
248
|
-
for (const opt of directOptions) {
|
|
249
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
250
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
for (const opt of directOptions) {
|
|
254
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
255
|
-
const value = opts[opt.long];
|
|
256
|
-
const values = Array.isArray(value) ? value : [value];
|
|
257
|
-
const choices = opt.choices;
|
|
258
|
-
for (const v of values) {
|
|
259
|
-
if (!choices.includes(v)) {
|
|
260
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return { opts, remaining: finalRemaining };
|
|
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
266
|
}
|
|
267
267
|
formatHelp() {
|
|
268
268
|
const lines = [];
|
|
269
269
|
const allOptions = this.#getMergedOptions();
|
|
270
|
-
lines.push(this.#
|
|
270
|
+
lines.push(this.#desc);
|
|
271
271
|
lines.push('');
|
|
272
272
|
const commandPath = this.#getCommandPath();
|
|
273
273
|
let usage = `Usage: ${commandPath}`;
|
|
274
274
|
if (allOptions.length > 0)
|
|
275
275
|
usage += ' [options]';
|
|
276
|
-
if (this.#
|
|
276
|
+
if (this.#subcommandsList.length > 0)
|
|
277
277
|
usage += ' [command]';
|
|
278
278
|
for (const arg of this.#arguments) {
|
|
279
279
|
if (arg.kind === 'required') {
|
|
@@ -292,24 +292,24 @@ class Command {
|
|
|
292
292
|
lines.push('Options:');
|
|
293
293
|
const optLines = [];
|
|
294
294
|
for (const opt of allOptions) {
|
|
295
|
+
const kebabLong = camelToKebabCase$1(opt.long);
|
|
295
296
|
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
296
|
-
sig += `--${
|
|
297
|
-
|
|
298
|
-
if (effectiveType !== 'boolean') {
|
|
297
|
+
sig += `--${kebabLong}`;
|
|
298
|
+
if (opt.args !== 'none') {
|
|
299
299
|
sig += ' <value>';
|
|
300
300
|
}
|
|
301
|
-
let desc = opt.
|
|
302
|
-
if (opt.default !== undefined &&
|
|
301
|
+
let desc = opt.desc;
|
|
302
|
+
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
303
303
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
304
304
|
}
|
|
305
305
|
if (opt.choices) {
|
|
306
306
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
307
307
|
}
|
|
308
308
|
optLines.push({ sig, desc });
|
|
309
|
-
if (
|
|
309
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
310
310
|
optLines.push({
|
|
311
|
-
sig: ` --no-${
|
|
312
|
-
desc: `Negate --${
|
|
311
|
+
sig: ` --no-${kebabLong}`,
|
|
312
|
+
desc: `Negate --${kebabLong}`,
|
|
313
313
|
});
|
|
314
314
|
}
|
|
315
315
|
}
|
|
@@ -320,19 +320,19 @@ class Command {
|
|
|
320
320
|
}
|
|
321
321
|
lines.push('');
|
|
322
322
|
}
|
|
323
|
-
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#
|
|
324
|
-
if (this.#
|
|
323
|
+
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
|
|
324
|
+
if (this.#subcommandsList.length > 0) {
|
|
325
325
|
lines.push('Commands:');
|
|
326
326
|
const cmdLines = [];
|
|
327
327
|
if (showHelpSubcommand) {
|
|
328
328
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
329
329
|
}
|
|
330
|
-
for (const entry of this.#
|
|
330
|
+
for (const entry of this.#subcommandsList) {
|
|
331
331
|
let name = entry.name;
|
|
332
332
|
if (entry.aliases.length > 0) {
|
|
333
333
|
name += `, ${entry.aliases.join(', ')}`;
|
|
334
334
|
}
|
|
335
|
-
cmdLines.push({ name, desc: entry.command.#
|
|
335
|
+
cmdLines.push({ name, desc: entry.command.#desc });
|
|
336
336
|
}
|
|
337
337
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
338
338
|
for (const { name, desc } of cmdLines) {
|
|
@@ -347,21 +347,20 @@ class Command {
|
|
|
347
347
|
const allOptions = this.#getMergedOptions();
|
|
348
348
|
const options = [];
|
|
349
349
|
for (const opt of allOptions) {
|
|
350
|
-
const effectiveType = opt.type ?? 'string';
|
|
351
350
|
options.push({
|
|
352
351
|
long: opt.long,
|
|
353
352
|
short: opt.short,
|
|
354
|
-
|
|
355
|
-
takesValue:
|
|
353
|
+
desc: opt.desc,
|
|
354
|
+
takesValue: opt.args !== 'none',
|
|
356
355
|
choices: opt.choices,
|
|
357
356
|
});
|
|
358
357
|
}
|
|
359
358
|
return {
|
|
360
359
|
name: this.#name,
|
|
361
|
-
|
|
360
|
+
desc: this.#desc,
|
|
362
361
|
aliases: [],
|
|
363
362
|
options,
|
|
364
|
-
subcommands: this.#
|
|
363
|
+
subcommands: this.#subcommandsList.map(entry => {
|
|
365
364
|
const subMeta = entry.command.getCompletionMeta();
|
|
366
365
|
return {
|
|
367
366
|
...subMeta,
|
|
@@ -376,17 +375,17 @@ class Command {
|
|
|
376
375
|
return argv;
|
|
377
376
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
378
377
|
return argv;
|
|
379
|
-
if (argv.length === 1 || this.#
|
|
378
|
+
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
380
379
|
return ['--help'];
|
|
381
380
|
}
|
|
382
381
|
const subName = argv[1];
|
|
383
|
-
const entry = this.#
|
|
382
|
+
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
384
383
|
if (entry) {
|
|
385
384
|
return [subName, '--help', ...argv.slice(2)];
|
|
386
385
|
}
|
|
387
386
|
return argv;
|
|
388
387
|
}
|
|
389
|
-
#
|
|
388
|
+
#route(argv) {
|
|
390
389
|
const chain = [this];
|
|
391
390
|
let current = this;
|
|
392
391
|
let idx = 0;
|
|
@@ -394,7 +393,7 @@ class Command {
|
|
|
394
393
|
const token = argv[idx];
|
|
395
394
|
if (token.startsWith('-'))
|
|
396
395
|
break;
|
|
397
|
-
const entry = current.#
|
|
396
|
+
const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
398
397
|
if (!entry)
|
|
399
398
|
break;
|
|
400
399
|
current = entry.command;
|
|
@@ -403,93 +402,299 @@ class Command {
|
|
|
403
402
|
}
|
|
404
403
|
return { chain, remaining: argv.slice(idx) };
|
|
405
404
|
}
|
|
406
|
-
#
|
|
407
|
-
const
|
|
408
|
-
if (ddIdx === -1) {
|
|
409
|
-
return { optionTokens: tokens, restArgs: [] };
|
|
410
|
-
}
|
|
411
|
-
return {
|
|
412
|
-
optionTokens: tokens.slice(0, ddIdx),
|
|
413
|
-
restArgs: tokens.slice(ddIdx + 1),
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
#shiftChain(chain, tokens, includeRootVersion) {
|
|
417
|
-
const optsMap = new Map();
|
|
405
|
+
#resolve(chain, tokens) {
|
|
406
|
+
const consumedTokens = new Map();
|
|
418
407
|
let remaining = [...tokens];
|
|
419
|
-
const rootCommand = chain[0];
|
|
420
408
|
const shadowed = new Set();
|
|
421
409
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
422
410
|
const cmd = chain[i];
|
|
423
|
-
const includeVersion =
|
|
424
|
-
const result = cmd.#
|
|
425
|
-
|
|
411
|
+
const includeVersion = i === 0;
|
|
412
|
+
const result = cmd.#shift(remaining, shadowed, includeVersion);
|
|
413
|
+
consumedTokens.set(cmd, result.consumed);
|
|
426
414
|
remaining = result.remaining;
|
|
427
415
|
for (const opt of cmd.#options) {
|
|
428
416
|
shadowed.add(opt.long);
|
|
429
417
|
}
|
|
430
418
|
}
|
|
431
|
-
const
|
|
419
|
+
const argTokens = [];
|
|
432
420
|
for (const token of remaining) {
|
|
433
|
-
if (token.
|
|
421
|
+
if (token.type !== 'none') {
|
|
434
422
|
const leafCommand = chain[chain.length - 1];
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
438
488
|
}
|
|
439
|
-
|
|
489
|
+
remaining.push(token);
|
|
490
|
+
i += 1;
|
|
491
|
+
continue;
|
|
440
492
|
}
|
|
441
|
-
|
|
493
|
+
remaining.push(token);
|
|
494
|
+
i += 1;
|
|
442
495
|
}
|
|
443
|
-
return {
|
|
496
|
+
return { consumed, remaining };
|
|
444
497
|
}
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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)) {
|
|
449
510
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
450
511
|
opt.apply(opts[opt.long], ctx);
|
|
451
512
|
}
|
|
452
513
|
}
|
|
453
514
|
}
|
|
454
|
-
|
|
455
|
-
#mergeOpts(chain, optsMap) {
|
|
456
|
-
const merged = {};
|
|
515
|
+
const mergedOpts = {};
|
|
457
516
|
for (const cmd of chain) {
|
|
458
|
-
Object.assign(
|
|
517
|
+
Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
|
|
459
518
|
}
|
|
460
|
-
|
|
519
|
+
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
520
|
+
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
521
|
+
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
461
522
|
}
|
|
462
|
-
#
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}
|
|
467
536
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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;
|
|
479
563
|
}
|
|
480
|
-
|
|
481
|
-
|
|
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;
|
|
482
573
|
}
|
|
574
|
+
i += 1;
|
|
575
|
+
continue;
|
|
483
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;
|
|
484
611
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
}
|
|
489
616
|
}
|
|
490
|
-
|
|
491
|
-
opts[opt.long]
|
|
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;
|
|
492
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;
|
|
493
698
|
}
|
|
494
699
|
#getMergedOptions(includeVersion = !this.#parent) {
|
|
495
700
|
const optionMap = new Map();
|
|
@@ -506,11 +711,11 @@ class Command {
|
|
|
506
711
|
}
|
|
507
712
|
return Array.from(optionMap.values());
|
|
508
713
|
}
|
|
509
|
-
#validateMergedShortOptions(chain
|
|
714
|
+
#validateMergedShortOptions(chain) {
|
|
510
715
|
const mergedByLong = new Map();
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const includeVersion =
|
|
716
|
+
for (let i = 0; i < chain.length; i++) {
|
|
717
|
+
const cmd = chain[i];
|
|
718
|
+
const includeVersion = i === 0;
|
|
514
719
|
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
515
720
|
mergedByLong.set(opt.long, opt);
|
|
516
721
|
}
|
|
@@ -527,8 +732,17 @@ class Command {
|
|
|
527
732
|
}
|
|
528
733
|
}
|
|
529
734
|
#validateOptionConfig(opt) {
|
|
530
|
-
if (opt.
|
|
531
|
-
throw new CommanderError('ConfigurationError', `option
|
|
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());
|
|
532
746
|
}
|
|
533
747
|
if (opt.required && opt.default !== undefined) {
|
|
534
748
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
@@ -567,154 +781,33 @@ class Command {
|
|
|
567
781
|
}
|
|
568
782
|
}
|
|
569
783
|
}
|
|
570
|
-
#
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const missing = argumentDefs
|
|
576
|
-
.filter(a => a.kind === 'required')
|
|
577
|
-
.slice(rawArgs.length)
|
|
578
|
-
.map(a => a.name);
|
|
579
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
784
|
+
async #runAction(params) {
|
|
785
|
+
if (!this.#action)
|
|
786
|
+
return;
|
|
787
|
+
try {
|
|
788
|
+
await this.#action(params);
|
|
580
789
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const rest = rawArgs.slice(index);
|
|
585
|
-
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
586
|
-
index = rawArgs.length;
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
const raw = rawArgs[index];
|
|
590
|
-
if (raw === undefined) {
|
|
591
|
-
if (def.kind === 'optional') {
|
|
592
|
-
args[def.name] = def.default ?? undefined;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
if (err instanceof Error) {
|
|
792
|
+
console.error(`Error: ${err.message}`);
|
|
595
793
|
}
|
|
596
794
|
else {
|
|
597
|
-
|
|
598
|
-
index += 1;
|
|
795
|
+
console.error('Error: action failed');
|
|
599
796
|
}
|
|
797
|
+
process.exit(1);
|
|
600
798
|
}
|
|
601
|
-
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
602
|
-
if (!hasVariadic && index < rawArgs.length) {
|
|
603
|
-
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
604
|
-
}
|
|
605
|
-
return { args, rawArgs };
|
|
606
799
|
}
|
|
607
|
-
#
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return def.coerce(raw);
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (def.type === 'number') {
|
|
617
|
-
const n = Number(raw);
|
|
618
|
-
if (Number.isNaN(n)) {
|
|
619
|
-
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
620
|
-
}
|
|
621
|
-
return n;
|
|
622
|
-
}
|
|
623
|
-
return raw;
|
|
624
|
-
}
|
|
625
|
-
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
626
|
-
const optionByLong = new Map();
|
|
627
|
-
const optionByShort = new Map();
|
|
628
|
-
const booleanOptions = new Set();
|
|
629
|
-
for (const opt of allOptions) {
|
|
630
|
-
if (excludeResolver && opt.resolver)
|
|
631
|
-
continue;
|
|
632
|
-
optionByLong.set(opt.long, opt);
|
|
633
|
-
if (opt.short) {
|
|
634
|
-
optionByShort.set(opt.short, opt);
|
|
635
|
-
}
|
|
636
|
-
if (opt.type === 'boolean') {
|
|
637
|
-
booleanOptions.add(opt.long);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return { optionByLong, optionByShort, booleanOptions };
|
|
641
|
-
}
|
|
642
|
-
#hasHelpFlag(argv, allOptions) {
|
|
643
|
-
return this.#hasBuiltinFlag(argv, 'help', 'h', allOptions);
|
|
644
|
-
}
|
|
645
|
-
#hasVersionFlag(argv, allOptions) {
|
|
646
|
-
return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
|
|
647
|
-
}
|
|
648
|
-
#hasBuiltinFlag(argv, flagLong, flagShort, allOptions) {
|
|
649
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions);
|
|
650
|
-
const normalizedArgv = this.#normalizeArgv(argv, booleanOptions);
|
|
651
|
-
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
652
|
-
const arg = normalizedArgv[i];
|
|
653
|
-
if (arg === '--') {
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
|
|
800
|
+
#hasFlag(tokens, longName, shortName) {
|
|
801
|
+
for (const token of tokens) {
|
|
802
|
+
if (token.type === 'long' && token.name === longName) {
|
|
657
803
|
return true;
|
|
658
804
|
}
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
#optionConsumesNextValue(arg, optionByLong, optionByShort) {
|
|
666
|
-
if (arg.startsWith('--')) {
|
|
667
|
-
const eqIdx = arg.indexOf('=');
|
|
668
|
-
if (eqIdx !== -1) {
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
const optName = arg.slice(2);
|
|
672
|
-
const opt = optionByLong.get(optName);
|
|
673
|
-
if (!opt) {
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
const type = opt.type ?? 'string';
|
|
677
|
-
return type !== 'boolean';
|
|
678
|
-
}
|
|
679
|
-
if (arg.startsWith('-') && arg.length === 2) {
|
|
680
|
-
const opt = optionByShort.get(arg[1]);
|
|
681
|
-
if (!opt) {
|
|
682
|
-
return false;
|
|
805
|
+
if (token.type === 'short' && token.name === shortName) {
|
|
806
|
+
return true;
|
|
683
807
|
}
|
|
684
|
-
const type = opt.type ?? 'string';
|
|
685
|
-
return type !== 'boolean';
|
|
686
808
|
}
|
|
687
809
|
return false;
|
|
688
810
|
}
|
|
689
|
-
#normalizeArgv(argv, booleanOptions) {
|
|
690
|
-
const result = [];
|
|
691
|
-
let seenDoubleDash = false;
|
|
692
|
-
for (const arg of argv) {
|
|
693
|
-
if (arg === '--') {
|
|
694
|
-
seenDoubleDash = true;
|
|
695
|
-
result.push(arg);
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (!seenDoubleDash && arg.startsWith('--no-')) {
|
|
699
|
-
const eqIdx = arg.indexOf('=');
|
|
700
|
-
if (eqIdx !== -1) {
|
|
701
|
-
const optName = arg.slice(5, eqIdx);
|
|
702
|
-
if (booleanOptions.has(optName)) {
|
|
703
|
-
throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
const optName = arg.slice(5);
|
|
708
|
-
if (booleanOptions.has(optName)) {
|
|
709
|
-
result.push(`--${optName}=false`);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
result.push(arg);
|
|
715
|
-
}
|
|
716
|
-
return result;
|
|
717
|
-
}
|
|
718
811
|
#getCommandPath() {
|
|
719
812
|
const parts = [];
|
|
720
813
|
let current = this;
|
|
@@ -726,130 +819,41 @@ class Command {
|
|
|
726
819
|
}
|
|
727
820
|
return parts.join(' ') || this.#name;
|
|
728
821
|
}
|
|
729
|
-
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
730
|
-
const token = tokens[idx];
|
|
731
|
-
const eqIdx = token.indexOf('=');
|
|
732
|
-
let optName;
|
|
733
|
-
let inlineValue;
|
|
734
|
-
if (eqIdx !== -1) {
|
|
735
|
-
optName = token.slice(2, eqIdx);
|
|
736
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
optName = token.slice(2);
|
|
740
|
-
}
|
|
741
|
-
const opt = optionByLong.get(optName);
|
|
742
|
-
if (!opt) {
|
|
743
|
-
return 0;
|
|
744
|
-
}
|
|
745
|
-
if (opt.type === 'boolean') {
|
|
746
|
-
if (inlineValue !== undefined) {
|
|
747
|
-
if (inlineValue === 'true') {
|
|
748
|
-
opts[optName] = true;
|
|
749
|
-
}
|
|
750
|
-
else if (inlineValue === 'false') {
|
|
751
|
-
opts[optName] = false;
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
opts[optName] = true;
|
|
759
|
-
}
|
|
760
|
-
return 1;
|
|
761
|
-
}
|
|
762
|
-
let value;
|
|
763
|
-
let consumed = 1;
|
|
764
|
-
if (inlineValue !== undefined) {
|
|
765
|
-
value = inlineValue;
|
|
766
|
-
}
|
|
767
|
-
else if (idx + 1 < tokens.length) {
|
|
768
|
-
value = tokens[idx + 1];
|
|
769
|
-
consumed = 2;
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
773
|
-
}
|
|
774
|
-
this.#applyValue(opt, value, opts);
|
|
775
|
-
return consumed;
|
|
776
|
-
}
|
|
777
|
-
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
778
|
-
const token = tokens[idx];
|
|
779
|
-
if (token.includes('=')) {
|
|
780
|
-
const firstFlag = token[1];
|
|
781
|
-
if (!optionByShort.has(firstFlag)) {
|
|
782
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
783
|
-
}
|
|
784
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
785
|
-
}
|
|
786
|
-
const flags = token.slice(1);
|
|
787
|
-
let j = 0;
|
|
788
|
-
const consumedFlags = [];
|
|
789
|
-
const unconsumedFlags = [];
|
|
790
|
-
let nextIdx = idx + 1;
|
|
791
|
-
while (j < flags.length) {
|
|
792
|
-
const flag = flags[j];
|
|
793
|
-
const opt = optionByShort.get(flag);
|
|
794
|
-
if (!opt) {
|
|
795
|
-
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
consumedFlags.push(flag);
|
|
799
|
-
if (opt.type === 'boolean') {
|
|
800
|
-
opts[opt.long] = true;
|
|
801
|
-
j += 1;
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
if (j < flags.length - 1) {
|
|
805
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
806
|
-
}
|
|
807
|
-
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
808
|
-
const value = tokens[idx + 1];
|
|
809
|
-
this.#applyValue(opt, value, opts);
|
|
810
|
-
nextIdx = idx + 2;
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
814
|
-
}
|
|
815
|
-
j += 1;
|
|
816
|
-
}
|
|
817
|
-
if (consumedFlags.length > 0) {
|
|
818
|
-
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
819
|
-
return { consumed: true, nextIdx, remainingToken };
|
|
820
|
-
}
|
|
821
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
822
|
-
}
|
|
823
822
|
}
|
|
824
823
|
|
|
824
|
+
function camelToKebabCase(str) {
|
|
825
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
826
|
+
}
|
|
825
827
|
class CompletionCommand extends Command {
|
|
826
828
|
constructor(root, config) {
|
|
827
829
|
const paths = config.paths;
|
|
828
|
-
const programName = config.programName ?? root.name;
|
|
829
|
-
super({
|
|
830
|
-
description: 'Generate shell completion script',
|
|
831
|
-
});
|
|
830
|
+
const programName = config.programName ?? root.name ?? 'program';
|
|
831
|
+
super({ desc: 'Generate shell completion script' });
|
|
832
832
|
this.option({
|
|
833
833
|
long: 'bash',
|
|
834
834
|
type: 'boolean',
|
|
835
|
-
|
|
835
|
+
args: 'none',
|
|
836
|
+
desc: 'Generate Bash completion script',
|
|
836
837
|
})
|
|
837
838
|
.option({
|
|
838
839
|
long: 'fish',
|
|
839
840
|
type: 'boolean',
|
|
840
|
-
|
|
841
|
+
args: 'none',
|
|
842
|
+
desc: 'Generate Fish completion script',
|
|
841
843
|
})
|
|
842
844
|
.option({
|
|
843
845
|
long: 'pwsh',
|
|
844
846
|
type: 'boolean',
|
|
845
|
-
|
|
847
|
+
args: 'none',
|
|
848
|
+
desc: 'Generate PowerShell completion script',
|
|
846
849
|
})
|
|
847
850
|
.option({
|
|
848
851
|
long: 'write',
|
|
849
852
|
short: 'w',
|
|
850
853
|
type: 'string',
|
|
851
|
-
|
|
852
|
-
|
|
854
|
+
args: 'required',
|
|
855
|
+
desc: 'Write to file (use shell default path if empty)',
|
|
856
|
+
default: undefined,
|
|
853
857
|
})
|
|
854
858
|
.action(({ opts }) => {
|
|
855
859
|
const meta = root.getCompletionMeta();
|
|
@@ -905,45 +909,6 @@ function expandHome(filepath) {
|
|
|
905
909
|
}
|
|
906
910
|
return filepath;
|
|
907
911
|
}
|
|
908
|
-
function resolveOptionalStringOption(argv, longName, shortName) {
|
|
909
|
-
const remaining = [];
|
|
910
|
-
let value;
|
|
911
|
-
for (let i = 0; i < argv.length; i++) {
|
|
912
|
-
const arg = argv[i];
|
|
913
|
-
if (arg.startsWith(`--${longName}=`)) {
|
|
914
|
-
value = arg.slice(`--${longName}=`.length);
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
if (arg === `--${longName}`) {
|
|
918
|
-
const next = argv[i + 1];
|
|
919
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
920
|
-
value = next;
|
|
921
|
-
i += 1;
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
value = '';
|
|
925
|
-
}
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
if (arg.startsWith(`-${shortName}=`)) {
|
|
929
|
-
value = arg.slice(`-${shortName}=`.length);
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
if (arg === `-${shortName}`) {
|
|
933
|
-
const next = argv[i + 1];
|
|
934
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
935
|
-
value = next;
|
|
936
|
-
i += 1;
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
value = '';
|
|
940
|
-
}
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
remaining.push(arg);
|
|
944
|
-
}
|
|
945
|
-
return { value, remaining };
|
|
946
|
-
}
|
|
947
912
|
class BashCompletion {
|
|
948
913
|
#meta;
|
|
949
914
|
#programName;
|
|
@@ -976,11 +941,12 @@ class BashCompletion {
|
|
|
976
941
|
const lines = [];
|
|
977
942
|
const optParts = [];
|
|
978
943
|
for (const opt of cmd.options) {
|
|
944
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
979
945
|
if (opt.short)
|
|
980
946
|
optParts.push(`-${opt.short}`);
|
|
981
|
-
optParts.push(`--${
|
|
947
|
+
optParts.push(`--${kebabLong}`);
|
|
982
948
|
if (!opt.takesValue) {
|
|
983
|
-
optParts.push(`--no-${
|
|
949
|
+
optParts.push(`--no-${kebabLong}`);
|
|
984
950
|
}
|
|
985
951
|
}
|
|
986
952
|
const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
@@ -1029,13 +995,14 @@ class FishCompletion {
|
|
|
1029
995
|
const isRoot = parentPath.length === 0;
|
|
1030
996
|
const condition = this.#buildCondition(parentPath);
|
|
1031
997
|
for (const opt of cmd.options) {
|
|
998
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1032
999
|
let line = `complete -c ${this.#programName}`;
|
|
1033
1000
|
if (condition)
|
|
1034
1001
|
line += ` -n '${condition}'`;
|
|
1035
1002
|
if (opt.short)
|
|
1036
1003
|
line += ` -s ${opt.short}`;
|
|
1037
|
-
line += ` -l ${
|
|
1038
|
-
line += ` -d '${this.#escape(opt.
|
|
1004
|
+
line += ` -l ${kebabLong}`;
|
|
1005
|
+
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1039
1006
|
if (opt.choices && opt.choices.length > 0) {
|
|
1040
1007
|
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
1041
1008
|
}
|
|
@@ -1044,8 +1011,8 @@ class FishCompletion {
|
|
|
1044
1011
|
let noLine = `complete -c ${this.#programName}`;
|
|
1045
1012
|
if (condition)
|
|
1046
1013
|
noLine += ` -n '${condition}'`;
|
|
1047
|
-
noLine += ` -l no-${
|
|
1048
|
-
noLine += ` -d '${this.#escape(opt.
|
|
1014
|
+
noLine += ` -l no-${kebabLong}`;
|
|
1015
|
+
noLine += ` -d '${this.#escape(opt.desc)}'`;
|
|
1049
1016
|
lines.push(noLine);
|
|
1050
1017
|
}
|
|
1051
1018
|
}
|
|
@@ -1058,7 +1025,7 @@ class FishCompletion {
|
|
|
1058
1025
|
line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1059
1026
|
}
|
|
1060
1027
|
line += ` -a ${sub.name}`;
|
|
1061
|
-
line += ` -d '${this.#escape(sub.
|
|
1028
|
+
line += ` -d '${this.#escape(sub.desc)}'`;
|
|
1062
1029
|
lines.push(line);
|
|
1063
1030
|
for (const alias of sub.aliases) {
|
|
1064
1031
|
let aliasLine = `complete -c ${this.#programName}`;
|
|
@@ -1131,7 +1098,7 @@ class PwshCompletion {
|
|
|
1131
1098
|
' "--$($opt.long)",',
|
|
1132
1099
|
' $opt.long,',
|
|
1133
1100
|
' "ParameterName",',
|
|
1134
|
-
' $opt.
|
|
1101
|
+
' $opt.desc',
|
|
1135
1102
|
' )',
|
|
1136
1103
|
' }',
|
|
1137
1104
|
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
@@ -1139,7 +1106,7 @@ class PwshCompletion {
|
|
|
1139
1106
|
' "--no-$($opt.long)",',
|
|
1140
1107
|
' "no-$($opt.long)",',
|
|
1141
1108
|
' "ParameterName",',
|
|
1142
|
-
' $opt.
|
|
1109
|
+
' $opt.desc',
|
|
1143
1110
|
' )',
|
|
1144
1111
|
' }',
|
|
1145
1112
|
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
@@ -1147,7 +1114,7 @@ class PwshCompletion {
|
|
|
1147
1114
|
' "-$($opt.short)",',
|
|
1148
1115
|
' $opt.short,',
|
|
1149
1116
|
' "ParameterName",',
|
|
1150
|
-
' $opt.
|
|
1117
|
+
' $opt.desc',
|
|
1151
1118
|
' )',
|
|
1152
1119
|
' }',
|
|
1153
1120
|
' }',
|
|
@@ -1161,7 +1128,7 @@ class PwshCompletion {
|
|
|
1161
1128
|
' $sub,',
|
|
1162
1129
|
' $sub,',
|
|
1163
1130
|
' "Command",',
|
|
1164
|
-
' $cmd.subcommands[$sub].
|
|
1131
|
+
' $cmd.subcommands[$sub].desc',
|
|
1165
1132
|
' )',
|
|
1166
1133
|
' }',
|
|
1167
1134
|
' }',
|
|
@@ -1175,14 +1142,15 @@ class PwshCompletion {
|
|
|
1175
1142
|
}
|
|
1176
1143
|
#generateCommandHash(cmd, indent) {
|
|
1177
1144
|
const lines = [];
|
|
1178
|
-
lines.push(`${indent}description = '${this.#escape(cmd.
|
|
1145
|
+
lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
|
|
1179
1146
|
lines.push(`${indent}options = @(`);
|
|
1180
1147
|
for (const opt of cmd.options) {
|
|
1148
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1181
1149
|
lines.push(`${indent} @{`);
|
|
1182
1150
|
if (opt.short)
|
|
1183
1151
|
lines.push(`${indent} short = '${opt.short}'`);
|
|
1184
|
-
lines.push(`${indent} long = '${
|
|
1185
|
-
lines.push(`${indent} description = '${this.#escape(opt.
|
|
1152
|
+
lines.push(`${indent} long = '${kebabLong}'`);
|
|
1153
|
+
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
1186
1154
|
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
1187
1155
|
if (opt.choices) {
|
|
1188
1156
|
lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
|
|
@@ -1211,4 +1179,30 @@ class PwshCompletion {
|
|
|
1211
1179
|
}
|
|
1212
1180
|
}
|
|
1213
1181
|
|
|
1214
|
-
|
|
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 };
|