@guanghechen/commander 3.1.0 → 3.3.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 +20 -0
- package/README.md +2 -5
- package/lib/cjs/index.cjs +144 -172
- package/lib/esm/index.mjs +144 -172
- package/lib/types/index.d.ts +28 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 3.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Apply code review fixes:
|
|
8
|
+
- Fix `#getCommandPath` to return full command path (e.g., "cli sub" instead of just "sub")
|
|
9
|
+
- Remove unused `#parseLongOption` and `#parseShortOption` dead code
|
|
10
|
+
- Improve `--no-{option}` help description to "Negate --{option}"
|
|
11
|
+
- Document short option negative value limitation in spec
|
|
12
|
+
|
|
13
|
+
## 3.2.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Change args from string[] to Record<string, unknown> with type/coerce/default support:
|
|
18
|
+
- `args` is now `Record<string, unknown>` keyed by argument name
|
|
19
|
+
- Add `rawArgs: string[]` for original argument strings before type conversion
|
|
20
|
+
- IArgument now supports `type`, `default`, and `coerce` properties
|
|
21
|
+
- Add `TooManyArguments` error kind for extra arguments validation
|
|
22
|
+
|
|
3
23
|
## 3.1.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -124,7 +124,6 @@ const root = new Command({
|
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
const clone = new Command({
|
|
127
|
-
name: 'clone',
|
|
128
127
|
description: 'Clone a repository',
|
|
129
128
|
})
|
|
130
129
|
.argument({ name: 'url', kind: 'required', description: 'Repository URL' })
|
|
@@ -134,8 +133,6 @@ const clone = new Command({
|
|
|
134
133
|
})
|
|
135
134
|
|
|
136
135
|
const commit = new Command({
|
|
137
|
-
name: 'commit',
|
|
138
|
-
aliases: ['ci'],
|
|
139
136
|
description: 'Record changes to the repository',
|
|
140
137
|
})
|
|
141
138
|
.option({ long: 'message', short: 'm', type: 'string', required: true, description: 'Commit message' })
|
|
@@ -144,7 +141,7 @@ const commit = new Command({
|
|
|
144
141
|
console.log(`Committing: ${opts['message']}`)
|
|
145
142
|
})
|
|
146
143
|
|
|
147
|
-
root.subcommand(clone).subcommand(commit)
|
|
144
|
+
root.subcommand('clone', clone).subcommand('commit', commit).subcommand('ci', commit)
|
|
148
145
|
|
|
149
146
|
root.run({ argv: process.argv.slice(2), envs: process.env })
|
|
150
147
|
```
|
|
@@ -161,7 +158,7 @@ const root = new Command({
|
|
|
161
158
|
})
|
|
162
159
|
|
|
163
160
|
// Add completion subcommand
|
|
164
|
-
root.subcommand(new CompletionCommand(root))
|
|
161
|
+
root.subcommand('completion', new CompletionCommand(root))
|
|
165
162
|
|
|
166
163
|
// Generate completion scripts:
|
|
167
164
|
// mycli completion --bash > ~/.local/share/bash-completion/completions/mycli
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -68,6 +68,8 @@ class Command {
|
|
|
68
68
|
#description;
|
|
69
69
|
#version;
|
|
70
70
|
#helpSubcommandEnabled;
|
|
71
|
+
#reporter;
|
|
72
|
+
#parent;
|
|
71
73
|
#options = [];
|
|
72
74
|
#arguments = [];
|
|
73
75
|
#subcommands = [];
|
|
@@ -77,6 +79,7 @@ class Command {
|
|
|
77
79
|
this.#description = config.description;
|
|
78
80
|
this.#version = config.version;
|
|
79
81
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
82
|
+
this.#reporter = config.reporter;
|
|
80
83
|
}
|
|
81
84
|
get name() {
|
|
82
85
|
return this.#name;
|
|
@@ -87,6 +90,9 @@ class Command {
|
|
|
87
90
|
get version() {
|
|
88
91
|
return this.#version;
|
|
89
92
|
}
|
|
93
|
+
get parent() {
|
|
94
|
+
return this.#parent;
|
|
95
|
+
}
|
|
90
96
|
get options() {
|
|
91
97
|
return [...this.#options];
|
|
92
98
|
}
|
|
@@ -112,12 +118,16 @@ class Command {
|
|
|
112
118
|
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
113
119
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
114
120
|
}
|
|
121
|
+
if (cmd.#parent && cmd.#parent !== this) {
|
|
122
|
+
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
123
|
+
}
|
|
115
124
|
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
116
125
|
if (existing) {
|
|
117
126
|
existing.aliases.push(name);
|
|
118
127
|
}
|
|
119
128
|
else {
|
|
120
129
|
cmd.#name = name;
|
|
130
|
+
cmd.#parent = this;
|
|
121
131
|
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
122
132
|
}
|
|
123
133
|
return this;
|
|
@@ -128,34 +138,35 @@ class Command {
|
|
|
128
138
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
129
139
|
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
130
140
|
const leafCommand = chain[chain.length - 1];
|
|
141
|
+
const rootCommand = chain[0];
|
|
142
|
+
const includeRootVersion = chain.length === 1;
|
|
143
|
+
this.#validateMergedShortOptions(chain, includeRootVersion);
|
|
131
144
|
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
132
|
-
const leafOptions = leafCommand.#getMergedOptions();
|
|
145
|
+
const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
|
|
133
146
|
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
134
147
|
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
135
148
|
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
136
149
|
console.log(leafCommand.formatHelp());
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
139
|
-
if (!hasUserVersion && leafCommand
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
if (!hasUserVersion && leafCommand === rootCommand) {
|
|
153
|
+
if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
154
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
142
157
|
}
|
|
143
|
-
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
158
|
+
const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
|
|
144
159
|
const ctx = {
|
|
145
160
|
cmd: leafCommand,
|
|
146
161
|
envs,
|
|
147
|
-
reporter: reporter ?? new DefaultReporter(),
|
|
162
|
+
reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
|
|
148
163
|
argv,
|
|
149
164
|
};
|
|
150
165
|
this.#applyChain(chain, optsMap, ctx);
|
|
151
166
|
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
156
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
|
|
157
|
-
}
|
|
158
|
-
const actionParams = { ctx, opts: mergedOpts, args };
|
|
167
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
168
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
169
|
+
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
159
170
|
if (leafCommand.#action) {
|
|
160
171
|
try {
|
|
161
172
|
await leafCommand.#action(actionParams);
|
|
@@ -187,76 +198,23 @@ class Command {
|
|
|
187
198
|
}
|
|
188
199
|
}
|
|
189
200
|
parse(argv) {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
opts[opt.long] = [];
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
let remaining = [...argv];
|
|
205
|
-
const resolverOptions = allOptions.filter(o => o.resolver);
|
|
206
|
-
for (const opt of resolverOptions) {
|
|
207
|
-
const result = opt.resolver(remaining);
|
|
208
|
-
opts[opt.long] = result.value;
|
|
209
|
-
remaining = result.remaining;
|
|
210
|
-
}
|
|
211
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
212
|
-
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
213
|
-
let i = 0;
|
|
214
|
-
while (i < remaining.length) {
|
|
215
|
-
const token = remaining[i];
|
|
216
|
-
if (token === '--') {
|
|
217
|
-
args.push(...remaining.slice(i + 1));
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
if (token.startsWith('--')) {
|
|
221
|
-
i = this.#parseLongOption(remaining, i, optionByLong, opts);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
225
|
-
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
args.push(token);
|
|
229
|
-
i += 1;
|
|
230
|
-
}
|
|
231
|
-
for (const opt of allOptions) {
|
|
232
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
233
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
for (const opt of allOptions) {
|
|
237
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
238
|
-
const value = opts[opt.long];
|
|
239
|
-
const values = Array.isArray(value) ? value : [value];
|
|
240
|
-
const choices = opt.choices;
|
|
241
|
-
for (const v of values) {
|
|
242
|
-
if (!choices.includes(v)) {
|
|
243
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const requiredArgs = this.#arguments.filter(a => a.kind === 'required');
|
|
249
|
-
if (args.length < requiredArgs.length) {
|
|
250
|
-
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
251
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
252
|
-
}
|
|
253
|
-
return { opts, args };
|
|
201
|
+
const processedArgv = this.#processHelpSubcommand(argv);
|
|
202
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
203
|
+
const leafCommand = chain[chain.length - 1];
|
|
204
|
+
const includeRootVersion = chain.length === 1;
|
|
205
|
+
this.#validateMergedShortOptions(chain, includeRootVersion);
|
|
206
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
207
|
+
const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
|
|
208
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
209
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
210
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
211
|
+
return { opts: mergedOpts, args, rawArgs };
|
|
254
212
|
}
|
|
255
213
|
shift(tokens) {
|
|
256
214
|
return this.#shiftWithShadowed(tokens, new Set());
|
|
257
215
|
}
|
|
258
|
-
#shiftWithShadowed(tokens, shadowed) {
|
|
259
|
-
const allDirectOptions = this.#getMergedOptions();
|
|
216
|
+
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
217
|
+
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
260
218
|
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
261
219
|
const opts = {};
|
|
262
220
|
for (const opt of directOptions) {
|
|
@@ -373,7 +331,7 @@ class Command {
|
|
|
373
331
|
if (effectiveType === 'boolean') {
|
|
374
332
|
optLines.push({
|
|
375
333
|
sig: ` --no-${opt.long}`,
|
|
376
|
-
desc: opt.
|
|
334
|
+
desc: `Negate --${opt.long}`,
|
|
377
335
|
});
|
|
378
336
|
}
|
|
379
337
|
}
|
|
@@ -436,11 +394,11 @@ class Command {
|
|
|
436
394
|
};
|
|
437
395
|
}
|
|
438
396
|
#processHelpSubcommand(argv) {
|
|
439
|
-
if (!this.#helpSubcommandEnabled
|
|
397
|
+
if (!this.#helpSubcommandEnabled)
|
|
440
398
|
return argv;
|
|
441
399
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
442
400
|
return argv;
|
|
443
|
-
if (argv.length === 1) {
|
|
401
|
+
if (argv.length === 1 || this.#subcommands.length === 0) {
|
|
444
402
|
return ['--help'];
|
|
445
403
|
}
|
|
446
404
|
const subName = argv[1];
|
|
@@ -477,30 +435,34 @@ class Command {
|
|
|
477
435
|
restArgs: tokens.slice(ddIdx + 1),
|
|
478
436
|
};
|
|
479
437
|
}
|
|
480
|
-
#shiftChain(chain, tokens) {
|
|
438
|
+
#shiftChain(chain, tokens, includeRootVersion) {
|
|
481
439
|
const optsMap = new Map();
|
|
482
440
|
let remaining = [...tokens];
|
|
441
|
+
const rootCommand = chain[0];
|
|
483
442
|
const shadowed = new Set();
|
|
484
443
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
485
444
|
const cmd = chain[i];
|
|
486
|
-
const
|
|
445
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
446
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
|
|
487
447
|
optsMap.set(cmd, result.opts);
|
|
488
448
|
remaining = result.remaining;
|
|
489
449
|
for (const opt of cmd.#options) {
|
|
490
450
|
shadowed.add(opt.long);
|
|
491
451
|
}
|
|
492
452
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
453
|
+
const positionalArgs = [];
|
|
454
|
+
for (const token of remaining) {
|
|
455
|
+
if (token.startsWith('-')) {
|
|
456
|
+
const leafCommand = chain[chain.length - 1];
|
|
457
|
+
if (!token.startsWith('--') && token.length > 2) {
|
|
458
|
+
const flag = token[1];
|
|
459
|
+
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
460
|
+
}
|
|
461
|
+
throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
501
462
|
}
|
|
463
|
+
positionalArgs.push(token);
|
|
502
464
|
}
|
|
503
|
-
return optsMap;
|
|
465
|
+
return { optsMap, positionalArgs };
|
|
504
466
|
}
|
|
505
467
|
#applyChain(chain, optsMap, ctx) {
|
|
506
468
|
for (const cmd of chain) {
|
|
@@ -519,82 +481,6 @@ class Command {
|
|
|
519
481
|
}
|
|
520
482
|
return merged;
|
|
521
483
|
}
|
|
522
|
-
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
523
|
-
const token = argv[idx];
|
|
524
|
-
const eqIdx = token.indexOf('=');
|
|
525
|
-
let optName;
|
|
526
|
-
let inlineValue;
|
|
527
|
-
if (eqIdx !== -1) {
|
|
528
|
-
optName = token.slice(2, eqIdx);
|
|
529
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
optName = token.slice(2);
|
|
533
|
-
}
|
|
534
|
-
const opt = optionByLong.get(optName);
|
|
535
|
-
if (!opt) {
|
|
536
|
-
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
537
|
-
}
|
|
538
|
-
if (opt.type === 'boolean') {
|
|
539
|
-
if (inlineValue !== undefined) {
|
|
540
|
-
if (inlineValue === 'true') {
|
|
541
|
-
opts[optName] = true;
|
|
542
|
-
}
|
|
543
|
-
else if (inlineValue === 'false') {
|
|
544
|
-
opts[optName] = false;
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
opts[optName] = true;
|
|
552
|
-
}
|
|
553
|
-
return idx + 1;
|
|
554
|
-
}
|
|
555
|
-
let value;
|
|
556
|
-
let nextIdx = idx;
|
|
557
|
-
if (inlineValue !== undefined) {
|
|
558
|
-
value = inlineValue;
|
|
559
|
-
}
|
|
560
|
-
else if (idx + 1 < argv.length) {
|
|
561
|
-
value = argv[idx + 1];
|
|
562
|
-
nextIdx += 1;
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
566
|
-
}
|
|
567
|
-
this.#applyValue(opt, value, opts);
|
|
568
|
-
return nextIdx + 1;
|
|
569
|
-
}
|
|
570
|
-
#parseShortOption(argv, idx, optionByShort, opts) {
|
|
571
|
-
const token = argv[idx];
|
|
572
|
-
if (token.includes('=')) {
|
|
573
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
574
|
-
}
|
|
575
|
-
const flags = token.slice(1);
|
|
576
|
-
for (let j = 0; j < flags.length; j++) {
|
|
577
|
-
const flag = flags[j];
|
|
578
|
-
const opt = optionByShort.get(flag);
|
|
579
|
-
if (!opt) {
|
|
580
|
-
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
581
|
-
}
|
|
582
|
-
if (opt.type === 'boolean') {
|
|
583
|
-
opts[opt.long] = true;
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
if (j < flags.length - 1) {
|
|
587
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
588
|
-
}
|
|
589
|
-
if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
|
|
590
|
-
const value = argv[idx + 1];
|
|
591
|
-
this.#applyValue(opt, value, opts);
|
|
592
|
-
return idx + 2;
|
|
593
|
-
}
|
|
594
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
595
|
-
}
|
|
596
|
-
return idx + 1;
|
|
597
|
-
}
|
|
598
484
|
#applyValue(opt, rawValue, opts) {
|
|
599
485
|
const type = opt.type ?? 'string';
|
|
600
486
|
let parsedValue = rawValue;
|
|
@@ -627,14 +513,14 @@ class Command {
|
|
|
627
513
|
opts[opt.long] = parsedValue;
|
|
628
514
|
}
|
|
629
515
|
}
|
|
630
|
-
#getMergedOptions() {
|
|
516
|
+
#getMergedOptions(includeVersion = !this.#parent) {
|
|
631
517
|
const optionMap = new Map();
|
|
632
518
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
633
519
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
634
520
|
if (!hasUserHelp) {
|
|
635
521
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
636
522
|
}
|
|
637
|
-
if (!hasUserVersion) {
|
|
523
|
+
if (!hasUserVersion && includeVersion) {
|
|
638
524
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
639
525
|
}
|
|
640
526
|
for (const opt of this.#options) {
|
|
@@ -642,6 +528,26 @@ class Command {
|
|
|
642
528
|
}
|
|
643
529
|
return Array.from(optionMap.values());
|
|
644
530
|
}
|
|
531
|
+
#validateMergedShortOptions(chain, includeRootVersion) {
|
|
532
|
+
const mergedByLong = new Map();
|
|
533
|
+
const rootCommand = chain[0];
|
|
534
|
+
for (const cmd of chain) {
|
|
535
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
536
|
+
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
537
|
+
mergedByLong.set(opt.long, opt);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const shortMap = new Map();
|
|
541
|
+
for (const opt of mergedByLong.values()) {
|
|
542
|
+
if (!opt.short)
|
|
543
|
+
continue;
|
|
544
|
+
const existingLong = shortMap.get(opt.short);
|
|
545
|
+
if (existingLong && existingLong !== opt.long) {
|
|
546
|
+
throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
|
|
547
|
+
}
|
|
548
|
+
shortMap.set(opt.short, opt.long);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
645
551
|
#validateOptionConfig(opt) {
|
|
646
552
|
if (opt.long.startsWith('no-')) {
|
|
647
553
|
throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
|
|
@@ -662,6 +568,9 @@ class Command {
|
|
|
662
568
|
}
|
|
663
569
|
}
|
|
664
570
|
#validateArgumentConfig(arg) {
|
|
571
|
+
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
572
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
573
|
+
}
|
|
665
574
|
if (arg.kind === 'variadic') {
|
|
666
575
|
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
667
576
|
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
@@ -680,6 +589,61 @@ class Command {
|
|
|
680
589
|
}
|
|
681
590
|
}
|
|
682
591
|
}
|
|
592
|
+
#parseArguments(rawArgs) {
|
|
593
|
+
const argumentDefs = this.#arguments;
|
|
594
|
+
const args = {};
|
|
595
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
596
|
+
if (rawArgs.length < requiredCount) {
|
|
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());
|
|
602
|
+
}
|
|
603
|
+
let index = 0;
|
|
604
|
+
for (const def of argumentDefs) {
|
|
605
|
+
if (def.kind === 'variadic') {
|
|
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
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
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
|
+
}
|
|
683
647
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
684
648
|
const optionByLong = new Map();
|
|
685
649
|
const optionByShort = new Map();
|
|
@@ -774,7 +738,15 @@ class Command {
|
|
|
774
738
|
return result;
|
|
775
739
|
}
|
|
776
740
|
#getCommandPath() {
|
|
777
|
-
|
|
741
|
+
const parts = [];
|
|
742
|
+
let current = this;
|
|
743
|
+
while (current) {
|
|
744
|
+
if (current.#name) {
|
|
745
|
+
parts.unshift(current.#name);
|
|
746
|
+
}
|
|
747
|
+
current = current.#parent;
|
|
748
|
+
}
|
|
749
|
+
return parts.join(' ') || this.#name;
|
|
778
750
|
}
|
|
779
751
|
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
780
752
|
const token = tokens[idx];
|
package/lib/esm/index.mjs
CHANGED
|
@@ -46,6 +46,8 @@ class Command {
|
|
|
46
46
|
#description;
|
|
47
47
|
#version;
|
|
48
48
|
#helpSubcommandEnabled;
|
|
49
|
+
#reporter;
|
|
50
|
+
#parent;
|
|
49
51
|
#options = [];
|
|
50
52
|
#arguments = [];
|
|
51
53
|
#subcommands = [];
|
|
@@ -55,6 +57,7 @@ class Command {
|
|
|
55
57
|
this.#description = config.description;
|
|
56
58
|
this.#version = config.version;
|
|
57
59
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
60
|
+
this.#reporter = config.reporter;
|
|
58
61
|
}
|
|
59
62
|
get name() {
|
|
60
63
|
return this.#name;
|
|
@@ -65,6 +68,9 @@ class Command {
|
|
|
65
68
|
get version() {
|
|
66
69
|
return this.#version;
|
|
67
70
|
}
|
|
71
|
+
get parent() {
|
|
72
|
+
return this.#parent;
|
|
73
|
+
}
|
|
68
74
|
get options() {
|
|
69
75
|
return [...this.#options];
|
|
70
76
|
}
|
|
@@ -90,12 +96,16 @@ class Command {
|
|
|
90
96
|
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
91
97
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
92
98
|
}
|
|
99
|
+
if (cmd.#parent && cmd.#parent !== this) {
|
|
100
|
+
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
101
|
+
}
|
|
93
102
|
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
94
103
|
if (existing) {
|
|
95
104
|
existing.aliases.push(name);
|
|
96
105
|
}
|
|
97
106
|
else {
|
|
98
107
|
cmd.#name = name;
|
|
108
|
+
cmd.#parent = this;
|
|
99
109
|
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
100
110
|
}
|
|
101
111
|
return this;
|
|
@@ -106,34 +116,35 @@ class Command {
|
|
|
106
116
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
107
117
|
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
108
118
|
const leafCommand = chain[chain.length - 1];
|
|
119
|
+
const rootCommand = chain[0];
|
|
120
|
+
const includeRootVersion = chain.length === 1;
|
|
121
|
+
this.#validateMergedShortOptions(chain, includeRootVersion);
|
|
109
122
|
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
110
|
-
const leafOptions = leafCommand.#getMergedOptions();
|
|
123
|
+
const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
|
|
111
124
|
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
112
125
|
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
113
126
|
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
114
127
|
console.log(leafCommand.formatHelp());
|
|
115
128
|
return;
|
|
116
129
|
}
|
|
117
|
-
if (!hasUserVersion && leafCommand
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
if (!hasUserVersion && leafCommand === rootCommand) {
|
|
131
|
+
if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
132
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
120
135
|
}
|
|
121
|
-
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
136
|
+
const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
|
|
122
137
|
const ctx = {
|
|
123
138
|
cmd: leafCommand,
|
|
124
139
|
envs,
|
|
125
|
-
reporter: reporter ?? new DefaultReporter(),
|
|
140
|
+
reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
|
|
126
141
|
argv,
|
|
127
142
|
};
|
|
128
143
|
this.#applyChain(chain, optsMap, ctx);
|
|
129
144
|
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
134
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
|
|
135
|
-
}
|
|
136
|
-
const actionParams = { ctx, opts: mergedOpts, args };
|
|
145
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
146
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
147
|
+
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
137
148
|
if (leafCommand.#action) {
|
|
138
149
|
try {
|
|
139
150
|
await leafCommand.#action(actionParams);
|
|
@@ -165,76 +176,23 @@ class Command {
|
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
parse(argv) {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
opts[opt.long] = [];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
let remaining = [...argv];
|
|
183
|
-
const resolverOptions = allOptions.filter(o => o.resolver);
|
|
184
|
-
for (const opt of resolverOptions) {
|
|
185
|
-
const result = opt.resolver(remaining);
|
|
186
|
-
opts[opt.long] = result.value;
|
|
187
|
-
remaining = result.remaining;
|
|
188
|
-
}
|
|
189
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
190
|
-
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
191
|
-
let i = 0;
|
|
192
|
-
while (i < remaining.length) {
|
|
193
|
-
const token = remaining[i];
|
|
194
|
-
if (token === '--') {
|
|
195
|
-
args.push(...remaining.slice(i + 1));
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
if (token.startsWith('--')) {
|
|
199
|
-
i = this.#parseLongOption(remaining, i, optionByLong, opts);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
203
|
-
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
args.push(token);
|
|
207
|
-
i += 1;
|
|
208
|
-
}
|
|
209
|
-
for (const opt of allOptions) {
|
|
210
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
211
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
for (const opt of allOptions) {
|
|
215
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
216
|
-
const value = opts[opt.long];
|
|
217
|
-
const values = Array.isArray(value) ? value : [value];
|
|
218
|
-
const choices = opt.choices;
|
|
219
|
-
for (const v of values) {
|
|
220
|
-
if (!choices.includes(v)) {
|
|
221
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const requiredArgs = this.#arguments.filter(a => a.kind === 'required');
|
|
227
|
-
if (args.length < requiredArgs.length) {
|
|
228
|
-
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
229
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
230
|
-
}
|
|
231
|
-
return { opts, args };
|
|
179
|
+
const processedArgv = this.#processHelpSubcommand(argv);
|
|
180
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
181
|
+
const leafCommand = chain[chain.length - 1];
|
|
182
|
+
const includeRootVersion = chain.length === 1;
|
|
183
|
+
this.#validateMergedShortOptions(chain, includeRootVersion);
|
|
184
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
185
|
+
const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
|
|
186
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
187
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
188
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
189
|
+
return { opts: mergedOpts, args, rawArgs };
|
|
232
190
|
}
|
|
233
191
|
shift(tokens) {
|
|
234
192
|
return this.#shiftWithShadowed(tokens, new Set());
|
|
235
193
|
}
|
|
236
|
-
#shiftWithShadowed(tokens, shadowed) {
|
|
237
|
-
const allDirectOptions = this.#getMergedOptions();
|
|
194
|
+
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
195
|
+
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
238
196
|
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
239
197
|
const opts = {};
|
|
240
198
|
for (const opt of directOptions) {
|
|
@@ -351,7 +309,7 @@ class Command {
|
|
|
351
309
|
if (effectiveType === 'boolean') {
|
|
352
310
|
optLines.push({
|
|
353
311
|
sig: ` --no-${opt.long}`,
|
|
354
|
-
desc: opt.
|
|
312
|
+
desc: `Negate --${opt.long}`,
|
|
355
313
|
});
|
|
356
314
|
}
|
|
357
315
|
}
|
|
@@ -414,11 +372,11 @@ class Command {
|
|
|
414
372
|
};
|
|
415
373
|
}
|
|
416
374
|
#processHelpSubcommand(argv) {
|
|
417
|
-
if (!this.#helpSubcommandEnabled
|
|
375
|
+
if (!this.#helpSubcommandEnabled)
|
|
418
376
|
return argv;
|
|
419
377
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
420
378
|
return argv;
|
|
421
|
-
if (argv.length === 1) {
|
|
379
|
+
if (argv.length === 1 || this.#subcommands.length === 0) {
|
|
422
380
|
return ['--help'];
|
|
423
381
|
}
|
|
424
382
|
const subName = argv[1];
|
|
@@ -455,30 +413,34 @@ class Command {
|
|
|
455
413
|
restArgs: tokens.slice(ddIdx + 1),
|
|
456
414
|
};
|
|
457
415
|
}
|
|
458
|
-
#shiftChain(chain, tokens) {
|
|
416
|
+
#shiftChain(chain, tokens, includeRootVersion) {
|
|
459
417
|
const optsMap = new Map();
|
|
460
418
|
let remaining = [...tokens];
|
|
419
|
+
const rootCommand = chain[0];
|
|
461
420
|
const shadowed = new Set();
|
|
462
421
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
463
422
|
const cmd = chain[i];
|
|
464
|
-
const
|
|
423
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
424
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
|
|
465
425
|
optsMap.set(cmd, result.opts);
|
|
466
426
|
remaining = result.remaining;
|
|
467
427
|
for (const opt of cmd.#options) {
|
|
468
428
|
shadowed.add(opt.long);
|
|
469
429
|
}
|
|
470
430
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
431
|
+
const positionalArgs = [];
|
|
432
|
+
for (const token of remaining) {
|
|
433
|
+
if (token.startsWith('-')) {
|
|
434
|
+
const leafCommand = chain[chain.length - 1];
|
|
435
|
+
if (!token.startsWith('--') && token.length > 2) {
|
|
436
|
+
const flag = token[1];
|
|
437
|
+
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
438
|
+
}
|
|
439
|
+
throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
479
440
|
}
|
|
441
|
+
positionalArgs.push(token);
|
|
480
442
|
}
|
|
481
|
-
return optsMap;
|
|
443
|
+
return { optsMap, positionalArgs };
|
|
482
444
|
}
|
|
483
445
|
#applyChain(chain, optsMap, ctx) {
|
|
484
446
|
for (const cmd of chain) {
|
|
@@ -497,82 +459,6 @@ class Command {
|
|
|
497
459
|
}
|
|
498
460
|
return merged;
|
|
499
461
|
}
|
|
500
|
-
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
501
|
-
const token = argv[idx];
|
|
502
|
-
const eqIdx = token.indexOf('=');
|
|
503
|
-
let optName;
|
|
504
|
-
let inlineValue;
|
|
505
|
-
if (eqIdx !== -1) {
|
|
506
|
-
optName = token.slice(2, eqIdx);
|
|
507
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
optName = token.slice(2);
|
|
511
|
-
}
|
|
512
|
-
const opt = optionByLong.get(optName);
|
|
513
|
-
if (!opt) {
|
|
514
|
-
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
515
|
-
}
|
|
516
|
-
if (opt.type === 'boolean') {
|
|
517
|
-
if (inlineValue !== undefined) {
|
|
518
|
-
if (inlineValue === 'true') {
|
|
519
|
-
opts[optName] = true;
|
|
520
|
-
}
|
|
521
|
-
else if (inlineValue === 'false') {
|
|
522
|
-
opts[optName] = false;
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
opts[optName] = true;
|
|
530
|
-
}
|
|
531
|
-
return idx + 1;
|
|
532
|
-
}
|
|
533
|
-
let value;
|
|
534
|
-
let nextIdx = idx;
|
|
535
|
-
if (inlineValue !== undefined) {
|
|
536
|
-
value = inlineValue;
|
|
537
|
-
}
|
|
538
|
-
else if (idx + 1 < argv.length) {
|
|
539
|
-
value = argv[idx + 1];
|
|
540
|
-
nextIdx += 1;
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
544
|
-
}
|
|
545
|
-
this.#applyValue(opt, value, opts);
|
|
546
|
-
return nextIdx + 1;
|
|
547
|
-
}
|
|
548
|
-
#parseShortOption(argv, idx, optionByShort, opts) {
|
|
549
|
-
const token = argv[idx];
|
|
550
|
-
if (token.includes('=')) {
|
|
551
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
552
|
-
}
|
|
553
|
-
const flags = token.slice(1);
|
|
554
|
-
for (let j = 0; j < flags.length; j++) {
|
|
555
|
-
const flag = flags[j];
|
|
556
|
-
const opt = optionByShort.get(flag);
|
|
557
|
-
if (!opt) {
|
|
558
|
-
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
559
|
-
}
|
|
560
|
-
if (opt.type === 'boolean') {
|
|
561
|
-
opts[opt.long] = true;
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
if (j < flags.length - 1) {
|
|
565
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
566
|
-
}
|
|
567
|
-
if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
|
|
568
|
-
const value = argv[idx + 1];
|
|
569
|
-
this.#applyValue(opt, value, opts);
|
|
570
|
-
return idx + 2;
|
|
571
|
-
}
|
|
572
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
573
|
-
}
|
|
574
|
-
return idx + 1;
|
|
575
|
-
}
|
|
576
462
|
#applyValue(opt, rawValue, opts) {
|
|
577
463
|
const type = opt.type ?? 'string';
|
|
578
464
|
let parsedValue = rawValue;
|
|
@@ -605,14 +491,14 @@ class Command {
|
|
|
605
491
|
opts[opt.long] = parsedValue;
|
|
606
492
|
}
|
|
607
493
|
}
|
|
608
|
-
#getMergedOptions() {
|
|
494
|
+
#getMergedOptions(includeVersion = !this.#parent) {
|
|
609
495
|
const optionMap = new Map();
|
|
610
496
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
611
497
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
612
498
|
if (!hasUserHelp) {
|
|
613
499
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
614
500
|
}
|
|
615
|
-
if (!hasUserVersion) {
|
|
501
|
+
if (!hasUserVersion && includeVersion) {
|
|
616
502
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
617
503
|
}
|
|
618
504
|
for (const opt of this.#options) {
|
|
@@ -620,6 +506,26 @@ class Command {
|
|
|
620
506
|
}
|
|
621
507
|
return Array.from(optionMap.values());
|
|
622
508
|
}
|
|
509
|
+
#validateMergedShortOptions(chain, includeRootVersion) {
|
|
510
|
+
const mergedByLong = new Map();
|
|
511
|
+
const rootCommand = chain[0];
|
|
512
|
+
for (const cmd of chain) {
|
|
513
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
514
|
+
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
515
|
+
mergedByLong.set(opt.long, opt);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const shortMap = new Map();
|
|
519
|
+
for (const opt of mergedByLong.values()) {
|
|
520
|
+
if (!opt.short)
|
|
521
|
+
continue;
|
|
522
|
+
const existingLong = shortMap.get(opt.short);
|
|
523
|
+
if (existingLong && existingLong !== opt.long) {
|
|
524
|
+
throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
|
|
525
|
+
}
|
|
526
|
+
shortMap.set(opt.short, opt.long);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
623
529
|
#validateOptionConfig(opt) {
|
|
624
530
|
if (opt.long.startsWith('no-')) {
|
|
625
531
|
throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
|
|
@@ -640,6 +546,9 @@ class Command {
|
|
|
640
546
|
}
|
|
641
547
|
}
|
|
642
548
|
#validateArgumentConfig(arg) {
|
|
549
|
+
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
550
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
551
|
+
}
|
|
643
552
|
if (arg.kind === 'variadic') {
|
|
644
553
|
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
645
554
|
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
@@ -658,6 +567,61 @@ class Command {
|
|
|
658
567
|
}
|
|
659
568
|
}
|
|
660
569
|
}
|
|
570
|
+
#parseArguments(rawArgs) {
|
|
571
|
+
const argumentDefs = this.#arguments;
|
|
572
|
+
const args = {};
|
|
573
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
574
|
+
if (rawArgs.length < requiredCount) {
|
|
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());
|
|
580
|
+
}
|
|
581
|
+
let index = 0;
|
|
582
|
+
for (const def of argumentDefs) {
|
|
583
|
+
if (def.kind === 'variadic') {
|
|
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
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
598
|
+
index += 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
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
|
+
}
|
|
607
|
+
#convertArgument(def, raw) {
|
|
608
|
+
if (def.coerce) {
|
|
609
|
+
try {
|
|
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
|
+
}
|
|
661
625
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
662
626
|
const optionByLong = new Map();
|
|
663
627
|
const optionByShort = new Map();
|
|
@@ -752,7 +716,15 @@ class Command {
|
|
|
752
716
|
return result;
|
|
753
717
|
}
|
|
754
718
|
#getCommandPath() {
|
|
755
|
-
|
|
719
|
+
const parts = [];
|
|
720
|
+
let current = this;
|
|
721
|
+
while (current) {
|
|
722
|
+
if (current.#name) {
|
|
723
|
+
parts.unshift(current.#name);
|
|
724
|
+
}
|
|
725
|
+
current = current.#parent;
|
|
726
|
+
}
|
|
727
|
+
return parts.join(' ') || this.#name;
|
|
756
728
|
}
|
|
757
729
|
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
758
730
|
const token = tokens[idx];
|
package/lib/types/index.d.ts
CHANGED
|
@@ -46,14 +46,25 @@ interface IOption<T = unknown> {
|
|
|
46
46
|
}
|
|
47
47
|
/** Argument kind */
|
|
48
48
|
type IArgumentKind = 'required' | 'optional' | 'variadic';
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/** Argument value type */
|
|
50
|
+
type IArgumentType = 'string' | 'number';
|
|
51
|
+
/**
|
|
52
|
+
* Positional argument definition.
|
|
53
|
+
* @template T - The type of the argument value
|
|
54
|
+
*/
|
|
55
|
+
interface IArgument<T = unknown> {
|
|
51
56
|
/** Argument name */
|
|
52
57
|
name: string;
|
|
53
58
|
/** Argument description */
|
|
54
59
|
description: string;
|
|
55
60
|
/** Argument kind: required / optional / variadic */
|
|
56
61
|
kind: IArgumentKind;
|
|
62
|
+
/** Value type, defaults to 'string' */
|
|
63
|
+
type?: IArgumentType;
|
|
64
|
+
/** Default value when not provided (only effective for optional arguments) */
|
|
65
|
+
default?: T;
|
|
66
|
+
/** Custom value transformation (takes precedence over type conversion) */
|
|
67
|
+
coerce?: (rawValue: string) => T;
|
|
57
68
|
}
|
|
58
69
|
/** Command configuration */
|
|
59
70
|
interface ICommandConfig {
|
|
@@ -61,16 +72,19 @@ interface ICommandConfig {
|
|
|
61
72
|
name?: string;
|
|
62
73
|
/** Command description */
|
|
63
74
|
description: string;
|
|
64
|
-
/** Version (only effective for root
|
|
75
|
+
/** Version (only effective for built-in root --version) */
|
|
65
76
|
version?: string;
|
|
66
77
|
/** Enable built-in "help" subcommand (only effective when command has subcommands) */
|
|
67
78
|
help?: boolean;
|
|
79
|
+
/** Default reporter for this command (can be overridden by run params) */
|
|
80
|
+
reporter?: IReporter;
|
|
68
81
|
}
|
|
69
82
|
/** Forward declaration for Command class */
|
|
70
83
|
interface ICommand {
|
|
71
84
|
readonly name: string;
|
|
72
85
|
readonly description: string;
|
|
73
86
|
readonly version: string | undefined;
|
|
87
|
+
readonly parent?: ICommand;
|
|
74
88
|
readonly options: IOption[];
|
|
75
89
|
readonly arguments: IArgument[];
|
|
76
90
|
}
|
|
@@ -91,8 +105,10 @@ interface IActionParams {
|
|
|
91
105
|
ctx: ICommandContext;
|
|
92
106
|
/** Parsed options */
|
|
93
107
|
opts: Record<string, unknown>;
|
|
94
|
-
/** Parsed positional arguments */
|
|
95
|
-
args: string
|
|
108
|
+
/** Parsed positional arguments (keyed by argument name) */
|
|
109
|
+
args: Record<string, unknown>;
|
|
110
|
+
/** Raw positional argument strings (before type conversion) */
|
|
111
|
+
rawArgs: string[];
|
|
96
112
|
}
|
|
97
113
|
/** Action handler function */
|
|
98
114
|
type IAction = (params: IActionParams) => void | Promise<void>;
|
|
@@ -102,15 +118,17 @@ interface IRunParams {
|
|
|
102
118
|
argv: string[];
|
|
103
119
|
/** Environment variables (usually process.env) */
|
|
104
120
|
envs: Record<string, string | undefined>;
|
|
105
|
-
/** Optional reporter
|
|
121
|
+
/** Optional reporter override (defaults to command's reporter or console reporter) */
|
|
106
122
|
reporter?: IReporter;
|
|
107
123
|
}
|
|
108
124
|
/** parse() method result */
|
|
109
125
|
interface IParseResult {
|
|
110
126
|
/** Parsed options */
|
|
111
127
|
opts: Record<string, unknown>;
|
|
112
|
-
/** Parsed positional arguments */
|
|
113
|
-
args: string
|
|
128
|
+
/** Parsed positional arguments (keyed by argument name) */
|
|
129
|
+
args: Record<string, unknown>;
|
|
130
|
+
/** Raw positional argument strings (before type conversion) */
|
|
131
|
+
rawArgs: string[];
|
|
114
132
|
}
|
|
115
133
|
/** shift() method result */
|
|
116
134
|
interface IShiftResult {
|
|
@@ -120,7 +138,7 @@ interface IShiftResult {
|
|
|
120
138
|
remaining: string[];
|
|
121
139
|
}
|
|
122
140
|
/** Error kinds for command parsing */
|
|
123
|
-
type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
141
|
+
type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
124
142
|
/** Commander error with structured information */
|
|
125
143
|
declare class CommanderError extends Error {
|
|
126
144
|
readonly kind: ICommanderErrorKind;
|
|
@@ -176,6 +194,7 @@ declare class Command implements ICommand {
|
|
|
176
194
|
get name(): string;
|
|
177
195
|
get description(): string;
|
|
178
196
|
get version(): string | undefined;
|
|
197
|
+
get parent(): Command | undefined;
|
|
179
198
|
get options(): IOption[];
|
|
180
199
|
get arguments(): IArgument[];
|
|
181
200
|
option(opt: IOption): this;
|