@guanghechen/commander 3.2.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 +10 -0
- package/README.md +2 -5
- package/lib/cjs/index.cjs +85 -162
- package/lib/esm/index.mjs +85 -162
- package/lib/types/index.d.ts +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 3.2.0
|
|
4
14
|
|
|
5
15
|
### 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,28 +138,34 @@ 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
|
|
167
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
168
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
153
169
|
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
154
170
|
if (leafCommand.#action) {
|
|
155
171
|
try {
|
|
@@ -182,72 +198,23 @@ class Command {
|
|
|
182
198
|
}
|
|
183
199
|
}
|
|
184
200
|
parse(argv) {
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
opts[opt.long] = [];
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let remaining = [...argv];
|
|
200
|
-
const resolverOptions = allOptions.filter(o => o.resolver);
|
|
201
|
-
for (const opt of resolverOptions) {
|
|
202
|
-
const result = opt.resolver(remaining);
|
|
203
|
-
opts[opt.long] = result.value;
|
|
204
|
-
remaining = result.remaining;
|
|
205
|
-
}
|
|
206
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
207
|
-
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
208
|
-
let i = 0;
|
|
209
|
-
while (i < remaining.length) {
|
|
210
|
-
const token = remaining[i];
|
|
211
|
-
if (token === '--') {
|
|
212
|
-
rawArgs.push(...remaining.slice(i + 1));
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
if (token.startsWith('--')) {
|
|
216
|
-
i = this.#parseLongOption(remaining, i, optionByLong, opts);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
220
|
-
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
rawArgs.push(token);
|
|
224
|
-
i += 1;
|
|
225
|
-
}
|
|
226
|
-
for (const opt of allOptions) {
|
|
227
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
228
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
for (const opt of allOptions) {
|
|
232
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
233
|
-
const value = opts[opt.long];
|
|
234
|
-
const values = Array.isArray(value) ? value : [value];
|
|
235
|
-
const choices = opt.choices;
|
|
236
|
-
for (const v of values) {
|
|
237
|
-
if (!choices.includes(v)) {
|
|
238
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const { args } = this.#parseArguments(rawArgs);
|
|
244
|
-
return { opts, args, rawArgs };
|
|
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 };
|
|
245
212
|
}
|
|
246
213
|
shift(tokens) {
|
|
247
214
|
return this.#shiftWithShadowed(tokens, new Set());
|
|
248
215
|
}
|
|
249
|
-
#shiftWithShadowed(tokens, shadowed) {
|
|
250
|
-
const allDirectOptions = this.#getMergedOptions();
|
|
216
|
+
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
217
|
+
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
251
218
|
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
252
219
|
const opts = {};
|
|
253
220
|
for (const opt of directOptions) {
|
|
@@ -364,7 +331,7 @@ class Command {
|
|
|
364
331
|
if (effectiveType === 'boolean') {
|
|
365
332
|
optLines.push({
|
|
366
333
|
sig: ` --no-${opt.long}`,
|
|
367
|
-
desc: opt.
|
|
334
|
+
desc: `Negate --${opt.long}`,
|
|
368
335
|
});
|
|
369
336
|
}
|
|
370
337
|
}
|
|
@@ -427,11 +394,11 @@ class Command {
|
|
|
427
394
|
};
|
|
428
395
|
}
|
|
429
396
|
#processHelpSubcommand(argv) {
|
|
430
|
-
if (!this.#helpSubcommandEnabled
|
|
397
|
+
if (!this.#helpSubcommandEnabled)
|
|
431
398
|
return argv;
|
|
432
399
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
433
400
|
return argv;
|
|
434
|
-
if (argv.length === 1) {
|
|
401
|
+
if (argv.length === 1 || this.#subcommands.length === 0) {
|
|
435
402
|
return ['--help'];
|
|
436
403
|
}
|
|
437
404
|
const subName = argv[1];
|
|
@@ -468,30 +435,34 @@ class Command {
|
|
|
468
435
|
restArgs: tokens.slice(ddIdx + 1),
|
|
469
436
|
};
|
|
470
437
|
}
|
|
471
|
-
#shiftChain(chain, tokens) {
|
|
438
|
+
#shiftChain(chain, tokens, includeRootVersion) {
|
|
472
439
|
const optsMap = new Map();
|
|
473
440
|
let remaining = [...tokens];
|
|
441
|
+
const rootCommand = chain[0];
|
|
474
442
|
const shadowed = new Set();
|
|
475
443
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
476
444
|
const cmd = chain[i];
|
|
477
|
-
const
|
|
445
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
446
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
|
|
478
447
|
optsMap.set(cmd, result.opts);
|
|
479
448
|
remaining = result.remaining;
|
|
480
449
|
for (const opt of cmd.#options) {
|
|
481
450
|
shadowed.add(opt.long);
|
|
482
451
|
}
|
|
483
452
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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());
|
|
492
462
|
}
|
|
463
|
+
positionalArgs.push(token);
|
|
493
464
|
}
|
|
494
|
-
return optsMap;
|
|
465
|
+
return { optsMap, positionalArgs };
|
|
495
466
|
}
|
|
496
467
|
#applyChain(chain, optsMap, ctx) {
|
|
497
468
|
for (const cmd of chain) {
|
|
@@ -510,82 +481,6 @@ class Command {
|
|
|
510
481
|
}
|
|
511
482
|
return merged;
|
|
512
483
|
}
|
|
513
|
-
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
514
|
-
const token = argv[idx];
|
|
515
|
-
const eqIdx = token.indexOf('=');
|
|
516
|
-
let optName;
|
|
517
|
-
let inlineValue;
|
|
518
|
-
if (eqIdx !== -1) {
|
|
519
|
-
optName = token.slice(2, eqIdx);
|
|
520
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
optName = token.slice(2);
|
|
524
|
-
}
|
|
525
|
-
const opt = optionByLong.get(optName);
|
|
526
|
-
if (!opt) {
|
|
527
|
-
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
528
|
-
}
|
|
529
|
-
if (opt.type === 'boolean') {
|
|
530
|
-
if (inlineValue !== undefined) {
|
|
531
|
-
if (inlineValue === 'true') {
|
|
532
|
-
opts[optName] = true;
|
|
533
|
-
}
|
|
534
|
-
else if (inlineValue === 'false') {
|
|
535
|
-
opts[optName] = false;
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
opts[optName] = true;
|
|
543
|
-
}
|
|
544
|
-
return idx + 1;
|
|
545
|
-
}
|
|
546
|
-
let value;
|
|
547
|
-
let nextIdx = idx;
|
|
548
|
-
if (inlineValue !== undefined) {
|
|
549
|
-
value = inlineValue;
|
|
550
|
-
}
|
|
551
|
-
else if (idx + 1 < argv.length) {
|
|
552
|
-
value = argv[idx + 1];
|
|
553
|
-
nextIdx += 1;
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
557
|
-
}
|
|
558
|
-
this.#applyValue(opt, value, opts);
|
|
559
|
-
return nextIdx + 1;
|
|
560
|
-
}
|
|
561
|
-
#parseShortOption(argv, idx, optionByShort, opts) {
|
|
562
|
-
const token = argv[idx];
|
|
563
|
-
if (token.includes('=')) {
|
|
564
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
565
|
-
}
|
|
566
|
-
const flags = token.slice(1);
|
|
567
|
-
for (let j = 0; j < flags.length; j++) {
|
|
568
|
-
const flag = flags[j];
|
|
569
|
-
const opt = optionByShort.get(flag);
|
|
570
|
-
if (!opt) {
|
|
571
|
-
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
572
|
-
}
|
|
573
|
-
if (opt.type === 'boolean') {
|
|
574
|
-
opts[opt.long] = true;
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
if (j < flags.length - 1) {
|
|
578
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
579
|
-
}
|
|
580
|
-
if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
|
|
581
|
-
const value = argv[idx + 1];
|
|
582
|
-
this.#applyValue(opt, value, opts);
|
|
583
|
-
return idx + 2;
|
|
584
|
-
}
|
|
585
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
586
|
-
}
|
|
587
|
-
return idx + 1;
|
|
588
|
-
}
|
|
589
484
|
#applyValue(opt, rawValue, opts) {
|
|
590
485
|
const type = opt.type ?? 'string';
|
|
591
486
|
let parsedValue = rawValue;
|
|
@@ -618,14 +513,14 @@ class Command {
|
|
|
618
513
|
opts[opt.long] = parsedValue;
|
|
619
514
|
}
|
|
620
515
|
}
|
|
621
|
-
#getMergedOptions() {
|
|
516
|
+
#getMergedOptions(includeVersion = !this.#parent) {
|
|
622
517
|
const optionMap = new Map();
|
|
623
518
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
624
519
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
625
520
|
if (!hasUserHelp) {
|
|
626
521
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
627
522
|
}
|
|
628
|
-
if (!hasUserVersion) {
|
|
523
|
+
if (!hasUserVersion && includeVersion) {
|
|
629
524
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
630
525
|
}
|
|
631
526
|
for (const opt of this.#options) {
|
|
@@ -633,6 +528,26 @@ class Command {
|
|
|
633
528
|
}
|
|
634
529
|
return Array.from(optionMap.values());
|
|
635
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
|
+
}
|
|
636
551
|
#validateOptionConfig(opt) {
|
|
637
552
|
if (opt.long.startsWith('no-')) {
|
|
638
553
|
throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
|
|
@@ -823,7 +738,15 @@ class Command {
|
|
|
823
738
|
return result;
|
|
824
739
|
}
|
|
825
740
|
#getCommandPath() {
|
|
826
|
-
|
|
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;
|
|
827
750
|
}
|
|
828
751
|
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
829
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,28 +116,34 @@ 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
|
|
145
|
+
const allArgs = [...positionalArgs, ...restArgs];
|
|
146
|
+
const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
|
|
131
147
|
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
132
148
|
if (leafCommand.#action) {
|
|
133
149
|
try {
|
|
@@ -160,72 +176,23 @@ class Command {
|
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
parse(argv) {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
opts[opt.long] = [];
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
let remaining = [...argv];
|
|
178
|
-
const resolverOptions = allOptions.filter(o => o.resolver);
|
|
179
|
-
for (const opt of resolverOptions) {
|
|
180
|
-
const result = opt.resolver(remaining);
|
|
181
|
-
opts[opt.long] = result.value;
|
|
182
|
-
remaining = result.remaining;
|
|
183
|
-
}
|
|
184
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
185
|
-
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
186
|
-
let i = 0;
|
|
187
|
-
while (i < remaining.length) {
|
|
188
|
-
const token = remaining[i];
|
|
189
|
-
if (token === '--') {
|
|
190
|
-
rawArgs.push(...remaining.slice(i + 1));
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
if (token.startsWith('--')) {
|
|
194
|
-
i = this.#parseLongOption(remaining, i, optionByLong, opts);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
198
|
-
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
rawArgs.push(token);
|
|
202
|
-
i += 1;
|
|
203
|
-
}
|
|
204
|
-
for (const opt of allOptions) {
|
|
205
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
206
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
for (const opt of allOptions) {
|
|
210
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
211
|
-
const value = opts[opt.long];
|
|
212
|
-
const values = Array.isArray(value) ? value : [value];
|
|
213
|
-
const choices = opt.choices;
|
|
214
|
-
for (const v of values) {
|
|
215
|
-
if (!choices.includes(v)) {
|
|
216
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
const { args } = this.#parseArguments(rawArgs);
|
|
222
|
-
return { opts, args, rawArgs };
|
|
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 };
|
|
223
190
|
}
|
|
224
191
|
shift(tokens) {
|
|
225
192
|
return this.#shiftWithShadowed(tokens, new Set());
|
|
226
193
|
}
|
|
227
|
-
#shiftWithShadowed(tokens, shadowed) {
|
|
228
|
-
const allDirectOptions = this.#getMergedOptions();
|
|
194
|
+
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
195
|
+
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
229
196
|
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
230
197
|
const opts = {};
|
|
231
198
|
for (const opt of directOptions) {
|
|
@@ -342,7 +309,7 @@ class Command {
|
|
|
342
309
|
if (effectiveType === 'boolean') {
|
|
343
310
|
optLines.push({
|
|
344
311
|
sig: ` --no-${opt.long}`,
|
|
345
|
-
desc: opt.
|
|
312
|
+
desc: `Negate --${opt.long}`,
|
|
346
313
|
});
|
|
347
314
|
}
|
|
348
315
|
}
|
|
@@ -405,11 +372,11 @@ class Command {
|
|
|
405
372
|
};
|
|
406
373
|
}
|
|
407
374
|
#processHelpSubcommand(argv) {
|
|
408
|
-
if (!this.#helpSubcommandEnabled
|
|
375
|
+
if (!this.#helpSubcommandEnabled)
|
|
409
376
|
return argv;
|
|
410
377
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
411
378
|
return argv;
|
|
412
|
-
if (argv.length === 1) {
|
|
379
|
+
if (argv.length === 1 || this.#subcommands.length === 0) {
|
|
413
380
|
return ['--help'];
|
|
414
381
|
}
|
|
415
382
|
const subName = argv[1];
|
|
@@ -446,30 +413,34 @@ class Command {
|
|
|
446
413
|
restArgs: tokens.slice(ddIdx + 1),
|
|
447
414
|
};
|
|
448
415
|
}
|
|
449
|
-
#shiftChain(chain, tokens) {
|
|
416
|
+
#shiftChain(chain, tokens, includeRootVersion) {
|
|
450
417
|
const optsMap = new Map();
|
|
451
418
|
let remaining = [...tokens];
|
|
419
|
+
const rootCommand = chain[0];
|
|
452
420
|
const shadowed = new Set();
|
|
453
421
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
454
422
|
const cmd = chain[i];
|
|
455
|
-
const
|
|
423
|
+
const includeVersion = cmd === rootCommand && includeRootVersion;
|
|
424
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
|
|
456
425
|
optsMap.set(cmd, result.opts);
|
|
457
426
|
remaining = result.remaining;
|
|
458
427
|
for (const opt of cmd.#options) {
|
|
459
428
|
shadowed.add(opt.long);
|
|
460
429
|
}
|
|
461
430
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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());
|
|
470
440
|
}
|
|
441
|
+
positionalArgs.push(token);
|
|
471
442
|
}
|
|
472
|
-
return optsMap;
|
|
443
|
+
return { optsMap, positionalArgs };
|
|
473
444
|
}
|
|
474
445
|
#applyChain(chain, optsMap, ctx) {
|
|
475
446
|
for (const cmd of chain) {
|
|
@@ -488,82 +459,6 @@ class Command {
|
|
|
488
459
|
}
|
|
489
460
|
return merged;
|
|
490
461
|
}
|
|
491
|
-
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
492
|
-
const token = argv[idx];
|
|
493
|
-
const eqIdx = token.indexOf('=');
|
|
494
|
-
let optName;
|
|
495
|
-
let inlineValue;
|
|
496
|
-
if (eqIdx !== -1) {
|
|
497
|
-
optName = token.slice(2, eqIdx);
|
|
498
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
499
|
-
}
|
|
500
|
-
else {
|
|
501
|
-
optName = token.slice(2);
|
|
502
|
-
}
|
|
503
|
-
const opt = optionByLong.get(optName);
|
|
504
|
-
if (!opt) {
|
|
505
|
-
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
506
|
-
}
|
|
507
|
-
if (opt.type === 'boolean') {
|
|
508
|
-
if (inlineValue !== undefined) {
|
|
509
|
-
if (inlineValue === 'true') {
|
|
510
|
-
opts[optName] = true;
|
|
511
|
-
}
|
|
512
|
-
else if (inlineValue === 'false') {
|
|
513
|
-
opts[optName] = false;
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
opts[optName] = true;
|
|
521
|
-
}
|
|
522
|
-
return idx + 1;
|
|
523
|
-
}
|
|
524
|
-
let value;
|
|
525
|
-
let nextIdx = idx;
|
|
526
|
-
if (inlineValue !== undefined) {
|
|
527
|
-
value = inlineValue;
|
|
528
|
-
}
|
|
529
|
-
else if (idx + 1 < argv.length) {
|
|
530
|
-
value = argv[idx + 1];
|
|
531
|
-
nextIdx += 1;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
535
|
-
}
|
|
536
|
-
this.#applyValue(opt, value, opts);
|
|
537
|
-
return nextIdx + 1;
|
|
538
|
-
}
|
|
539
|
-
#parseShortOption(argv, idx, optionByShort, opts) {
|
|
540
|
-
const token = argv[idx];
|
|
541
|
-
if (token.includes('=')) {
|
|
542
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
543
|
-
}
|
|
544
|
-
const flags = token.slice(1);
|
|
545
|
-
for (let j = 0; j < flags.length; j++) {
|
|
546
|
-
const flag = flags[j];
|
|
547
|
-
const opt = optionByShort.get(flag);
|
|
548
|
-
if (!opt) {
|
|
549
|
-
throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
550
|
-
}
|
|
551
|
-
if (opt.type === 'boolean') {
|
|
552
|
-
opts[opt.long] = true;
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (j < flags.length - 1) {
|
|
556
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
557
|
-
}
|
|
558
|
-
if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
|
|
559
|
-
const value = argv[idx + 1];
|
|
560
|
-
this.#applyValue(opt, value, opts);
|
|
561
|
-
return idx + 2;
|
|
562
|
-
}
|
|
563
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
564
|
-
}
|
|
565
|
-
return idx + 1;
|
|
566
|
-
}
|
|
567
462
|
#applyValue(opt, rawValue, opts) {
|
|
568
463
|
const type = opt.type ?? 'string';
|
|
569
464
|
let parsedValue = rawValue;
|
|
@@ -596,14 +491,14 @@ class Command {
|
|
|
596
491
|
opts[opt.long] = parsedValue;
|
|
597
492
|
}
|
|
598
493
|
}
|
|
599
|
-
#getMergedOptions() {
|
|
494
|
+
#getMergedOptions(includeVersion = !this.#parent) {
|
|
600
495
|
const optionMap = new Map();
|
|
601
496
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
602
497
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
603
498
|
if (!hasUserHelp) {
|
|
604
499
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
605
500
|
}
|
|
606
|
-
if (!hasUserVersion) {
|
|
501
|
+
if (!hasUserVersion && includeVersion) {
|
|
607
502
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
608
503
|
}
|
|
609
504
|
for (const opt of this.#options) {
|
|
@@ -611,6 +506,26 @@ class Command {
|
|
|
611
506
|
}
|
|
612
507
|
return Array.from(optionMap.values());
|
|
613
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
|
+
}
|
|
614
529
|
#validateOptionConfig(opt) {
|
|
615
530
|
if (opt.long.startsWith('no-')) {
|
|
616
531
|
throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
|
|
@@ -801,7 +716,15 @@ class Command {
|
|
|
801
716
|
return result;
|
|
802
717
|
}
|
|
803
718
|
#getCommandPath() {
|
|
804
|
-
|
|
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;
|
|
805
728
|
}
|
|
806
729
|
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
807
730
|
const token = tokens[idx];
|
package/lib/types/index.d.ts
CHANGED
|
@@ -72,16 +72,19 @@ interface ICommandConfig {
|
|
|
72
72
|
name?: string;
|
|
73
73
|
/** Command description */
|
|
74
74
|
description: string;
|
|
75
|
-
/** Version (only effective for root
|
|
75
|
+
/** Version (only effective for built-in root --version) */
|
|
76
76
|
version?: string;
|
|
77
77
|
/** Enable built-in "help" subcommand (only effective when command has subcommands) */
|
|
78
78
|
help?: boolean;
|
|
79
|
+
/** Default reporter for this command (can be overridden by run params) */
|
|
80
|
+
reporter?: IReporter;
|
|
79
81
|
}
|
|
80
82
|
/** Forward declaration for Command class */
|
|
81
83
|
interface ICommand {
|
|
82
84
|
readonly name: string;
|
|
83
85
|
readonly description: string;
|
|
84
86
|
readonly version: string | undefined;
|
|
87
|
+
readonly parent?: ICommand;
|
|
85
88
|
readonly options: IOption[];
|
|
86
89
|
readonly arguments: IArgument[];
|
|
87
90
|
}
|
|
@@ -115,7 +118,7 @@ interface IRunParams {
|
|
|
115
118
|
argv: string[];
|
|
116
119
|
/** Environment variables (usually process.env) */
|
|
117
120
|
envs: Record<string, string | undefined>;
|
|
118
|
-
/** Optional reporter
|
|
121
|
+
/** Optional reporter override (defaults to command's reporter or console reporter) */
|
|
119
122
|
reporter?: IReporter;
|
|
120
123
|
}
|
|
121
124
|
/** parse() method result */
|
|
@@ -191,6 +194,7 @@ declare class Command implements ICommand {
|
|
|
191
194
|
get name(): string;
|
|
192
195
|
get description(): string;
|
|
193
196
|
get version(): string | undefined;
|
|
197
|
+
get parent(): Command | undefined;
|
|
194
198
|
get options(): IOption[];
|
|
195
199
|
get arguments(): IArgument[];
|
|
196
200
|
option(opt: IOption): this;
|