@guanghechen/commander 2.1.0 → 3.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 +16 -2
- package/lib/cjs/index.cjs +288 -80
- package/lib/esm/index.mjs +287 -79
- package/lib/types/index.d.ts +20 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Implement option bubbling with shift/apply flow
|
|
8
|
+
- Add `shift()` method for bottom-up option consumption (leaf → root)
|
|
9
|
+
- Refactor `run()` with new flow: route → split → shift → apply → action
|
|
10
|
+
- Add `UnexpectedArgument` error type for positional args before `--`
|
|
11
|
+
|
|
3
12
|
All notable changes to this project will be documented in this file. See
|
|
4
13
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
14
|
|
|
6
|
-
##
|
|
15
|
+
## 3.0.0 (2026-02-08)
|
|
16
|
+
|
|
17
|
+
### BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
- Change subcommand registration API to `subcommand(name, cmd)`
|
|
20
|
+
|
|
21
|
+
## 2.1.0 (2026-02-08)
|
|
7
22
|
|
|
8
23
|
### Features
|
|
9
24
|
|
|
@@ -30,4 +45,3 @@ All notable changes to this project will be documented in this file. See
|
|
|
30
45
|
### Features
|
|
31
46
|
|
|
32
47
|
- Initial stable release: A minimal, type-safe command-line interface builder with fluent API
|
|
33
|
-
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -67,34 +67,25 @@ class Command {
|
|
|
67
67
|
#name;
|
|
68
68
|
#description;
|
|
69
69
|
#version;
|
|
70
|
-
#aliases;
|
|
71
70
|
#helpSubcommandEnabled;
|
|
72
71
|
#options = [];
|
|
73
72
|
#arguments = [];
|
|
74
73
|
#subcommands = [];
|
|
75
74
|
#action;
|
|
76
|
-
#parent;
|
|
77
75
|
constructor(config) {
|
|
78
|
-
this.#name = config.name;
|
|
76
|
+
this.#name = config.name ?? '';
|
|
79
77
|
this.#description = config.description;
|
|
80
78
|
this.#version = config.version;
|
|
81
|
-
this.#aliases = config.aliases ?? [];
|
|
82
79
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
83
80
|
}
|
|
84
81
|
get name() {
|
|
85
82
|
return this.#name;
|
|
86
83
|
}
|
|
87
|
-
get aliases() {
|
|
88
|
-
return this.#aliases;
|
|
89
|
-
}
|
|
90
84
|
get description() {
|
|
91
85
|
return this.#description;
|
|
92
86
|
}
|
|
93
87
|
get version() {
|
|
94
|
-
return this.#version
|
|
95
|
-
}
|
|
96
|
-
get parent() {
|
|
97
|
-
return this.#parent;
|
|
88
|
+
return this.#version;
|
|
98
89
|
}
|
|
99
90
|
get options() {
|
|
100
91
|
return [...this.#options];
|
|
@@ -117,46 +108,57 @@ class Command {
|
|
|
117
108
|
this.#action = fn;
|
|
118
109
|
return this;
|
|
119
110
|
}
|
|
120
|
-
subcommand(cmd) {
|
|
121
|
-
if (this.#helpSubcommandEnabled &&
|
|
111
|
+
subcommand(name, cmd) {
|
|
112
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
122
113
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
123
114
|
}
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
116
|
+
if (existing) {
|
|
117
|
+
existing.aliases.push(name);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
cmd.#name = name;
|
|
121
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
122
|
+
}
|
|
126
123
|
return this;
|
|
127
124
|
}
|
|
128
125
|
async run(params) {
|
|
129
126
|
const { argv, envs, reporter } = params;
|
|
130
127
|
try {
|
|
131
128
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
132
|
-
const {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
130
|
+
const leafCommand = chain[chain.length - 1];
|
|
131
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
132
|
+
const leafOptions = leafCommand.#getMergedOptions();
|
|
133
|
+
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
134
|
+
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
135
|
+
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
136
|
+
console.log(leafCommand.formatHelp());
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
|
-
if (!hasUserVersion &&
|
|
141
|
-
console.log(
|
|
139
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
140
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
142
141
|
return;
|
|
143
142
|
}
|
|
144
|
-
const
|
|
143
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
145
144
|
const ctx = {
|
|
146
|
-
cmd:
|
|
145
|
+
cmd: leafCommand,
|
|
147
146
|
envs,
|
|
148
147
|
reporter: reporter ?? new DefaultReporter(),
|
|
149
148
|
argv,
|
|
150
149
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
151
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
152
|
+
const args = restArgs;
|
|
153
|
+
const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
|
|
154
|
+
if (args.length < requiredArgs.length) {
|
|
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 };
|
|
159
|
+
if (leafCommand.#action) {
|
|
158
160
|
try {
|
|
159
|
-
await
|
|
161
|
+
await leafCommand.#action(actionParams);
|
|
160
162
|
}
|
|
161
163
|
catch (err) {
|
|
162
164
|
if (err instanceof Error) {
|
|
@@ -168,11 +170,11 @@ class Command {
|
|
|
168
170
|
process.exit(1);
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
|
-
else if (
|
|
172
|
-
console.log(
|
|
173
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
174
|
+
console.log(leafCommand.formatHelp());
|
|
173
175
|
}
|
|
174
176
|
else {
|
|
175
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
177
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
catch (err) {
|
|
@@ -250,6 +252,82 @@ class Command {
|
|
|
250
252
|
}
|
|
251
253
|
return { opts, args };
|
|
252
254
|
}
|
|
255
|
+
shift(tokens) {
|
|
256
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
257
|
+
}
|
|
258
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
259
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
260
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
261
|
+
const opts = {};
|
|
262
|
+
for (const opt of directOptions) {
|
|
263
|
+
if (opt.default !== undefined) {
|
|
264
|
+
opts[opt.long] = opt.default;
|
|
265
|
+
}
|
|
266
|
+
else if (opt.type === 'boolean') {
|
|
267
|
+
opts[opt.long] = false;
|
|
268
|
+
}
|
|
269
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
270
|
+
opts[opt.long] = [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let remaining = [...tokens];
|
|
274
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
275
|
+
for (const opt of resolverOptions) {
|
|
276
|
+
const result = opt.resolver(remaining);
|
|
277
|
+
opts[opt.long] = result.value;
|
|
278
|
+
remaining = result.remaining;
|
|
279
|
+
}
|
|
280
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
281
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
282
|
+
const finalRemaining = [];
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < normalizedTokens.length) {
|
|
285
|
+
const token = normalizedTokens[i];
|
|
286
|
+
if (token.startsWith('--')) {
|
|
287
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
288
|
+
if (consumed > 0) {
|
|
289
|
+
i += consumed;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
finalRemaining.push(token);
|
|
293
|
+
i += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
297
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
298
|
+
if (result.consumed) {
|
|
299
|
+
i = result.nextIdx;
|
|
300
|
+
if (result.remainingToken) {
|
|
301
|
+
finalRemaining.push(result.remainingToken);
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
finalRemaining.push(token);
|
|
306
|
+
i += 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
finalRemaining.push(token);
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
for (const opt of directOptions) {
|
|
313
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
314
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const opt of directOptions) {
|
|
318
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
319
|
+
const value = opts[opt.long];
|
|
320
|
+
const values = Array.isArray(value) ? value : [value];
|
|
321
|
+
const choices = opt.choices;
|
|
322
|
+
for (const v of values) {
|
|
323
|
+
if (!choices.includes(v)) {
|
|
324
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { opts, remaining: finalRemaining };
|
|
330
|
+
}
|
|
253
331
|
formatHelp() {
|
|
254
332
|
const lines = [];
|
|
255
333
|
const allOptions = this.#getMergedOptions();
|
|
@@ -313,12 +391,12 @@ class Command {
|
|
|
313
391
|
if (showHelpSubcommand) {
|
|
314
392
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
315
393
|
}
|
|
316
|
-
for (const
|
|
317
|
-
let name =
|
|
318
|
-
if (
|
|
319
|
-
name += `, ${
|
|
394
|
+
for (const entry of this.#subcommands) {
|
|
395
|
+
let name = entry.name;
|
|
396
|
+
if (entry.aliases.length > 0) {
|
|
397
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
320
398
|
}
|
|
321
|
-
cmdLines.push({ name, desc:
|
|
399
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
322
400
|
}
|
|
323
401
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
324
402
|
for (const { name, desc } of cmdLines) {
|
|
@@ -345,9 +423,16 @@ class Command {
|
|
|
345
423
|
return {
|
|
346
424
|
name: this.#name,
|
|
347
425
|
description: this.#description,
|
|
348
|
-
aliases:
|
|
426
|
+
aliases: [],
|
|
349
427
|
options,
|
|
350
|
-
subcommands: this.#subcommands.map(
|
|
428
|
+
subcommands: this.#subcommands.map(entry => {
|
|
429
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
430
|
+
return {
|
|
431
|
+
...subMeta,
|
|
432
|
+
name: entry.name,
|
|
433
|
+
aliases: entry.aliases,
|
|
434
|
+
};
|
|
435
|
+
}),
|
|
351
436
|
};
|
|
352
437
|
}
|
|
353
438
|
#processHelpSubcommand(argv) {
|
|
@@ -359,26 +444,80 @@ class Command {
|
|
|
359
444
|
return ['--help'];
|
|
360
445
|
}
|
|
361
446
|
const subName = argv[1];
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
447
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
448
|
+
if (entry) {
|
|
364
449
|
return [subName, '--help', ...argv.slice(2)];
|
|
365
450
|
}
|
|
366
451
|
return argv;
|
|
367
452
|
}
|
|
368
|
-
#
|
|
453
|
+
#routeChain(argv) {
|
|
454
|
+
const chain = [this];
|
|
369
455
|
let current = this;
|
|
370
456
|
let idx = 0;
|
|
371
457
|
while (idx < argv.length) {
|
|
372
458
|
const token = argv[idx];
|
|
373
459
|
if (token.startsWith('-'))
|
|
374
460
|
break;
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
461
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
462
|
+
if (!entry)
|
|
377
463
|
break;
|
|
378
|
-
current =
|
|
464
|
+
current = entry.command;
|
|
465
|
+
chain.push(current);
|
|
379
466
|
idx += 1;
|
|
380
467
|
}
|
|
381
|
-
return {
|
|
468
|
+
return { chain, remaining: argv.slice(idx) };
|
|
469
|
+
}
|
|
470
|
+
#splitAtDoubleDash(tokens) {
|
|
471
|
+
const ddIdx = tokens.indexOf('--');
|
|
472
|
+
if (ddIdx === -1) {
|
|
473
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
477
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
#shiftChain(chain, tokens) {
|
|
481
|
+
const optsMap = new Map();
|
|
482
|
+
let remaining = [...tokens];
|
|
483
|
+
const shadowed = new Set();
|
|
484
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
485
|
+
const cmd = chain[i];
|
|
486
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
487
|
+
optsMap.set(cmd, result.opts);
|
|
488
|
+
remaining = result.remaining;
|
|
489
|
+
for (const opt of cmd.#options) {
|
|
490
|
+
shadowed.add(opt.long);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (remaining.length > 0) {
|
|
494
|
+
const leafCommand = chain[chain.length - 1];
|
|
495
|
+
const firstToken = remaining[0];
|
|
496
|
+
if (firstToken.startsWith('-')) {
|
|
497
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return optsMap;
|
|
504
|
+
}
|
|
505
|
+
#applyChain(chain, optsMap, ctx) {
|
|
506
|
+
for (const cmd of chain) {
|
|
507
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
508
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
509
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
510
|
+
opt.apply(opts[opt.long], ctx);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
#mergeOpts(chain, optsMap) {
|
|
516
|
+
const merged = {};
|
|
517
|
+
for (const cmd of chain) {
|
|
518
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
519
|
+
}
|
|
520
|
+
return merged;
|
|
382
521
|
}
|
|
383
522
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
384
523
|
const token = argv[idx];
|
|
@@ -489,33 +628,17 @@ class Command {
|
|
|
489
628
|
}
|
|
490
629
|
}
|
|
491
630
|
#getMergedOptions() {
|
|
492
|
-
const ancestors = [];
|
|
493
|
-
for (let node = this; node; node = node.#parent) {
|
|
494
|
-
ancestors.unshift(node);
|
|
495
|
-
}
|
|
496
631
|
const optionMap = new Map();
|
|
497
|
-
const hasUserHelp =
|
|
498
|
-
const hasUserVersion =
|
|
632
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
633
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
499
634
|
if (!hasUserHelp) {
|
|
500
635
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
501
636
|
}
|
|
502
637
|
if (!hasUserVersion) {
|
|
503
638
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
504
639
|
}
|
|
505
|
-
for (const
|
|
506
|
-
|
|
507
|
-
optionMap.set(opt.long, opt);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const shortToLong = new Map();
|
|
511
|
-
for (const [long, opt] of optionMap) {
|
|
512
|
-
if (opt.short) {
|
|
513
|
-
const existing = shortToLong.get(opt.short);
|
|
514
|
-
if (existing && existing !== long) {
|
|
515
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
516
|
-
}
|
|
517
|
-
shortToLong.set(opt.short, long);
|
|
518
|
-
}
|
|
640
|
+
for (const opt of this.#options) {
|
|
641
|
+
optionMap.set(opt.long, opt);
|
|
519
642
|
}
|
|
520
643
|
return Array.from(optionMap.values());
|
|
521
644
|
}
|
|
@@ -557,9 +680,6 @@ class Command {
|
|
|
557
680
|
}
|
|
558
681
|
}
|
|
559
682
|
}
|
|
560
|
-
#isBuiltinOption(opt) {
|
|
561
|
-
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
562
|
-
}
|
|
563
683
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
564
684
|
const optionByLong = new Map();
|
|
565
685
|
const optionByShort = new Map();
|
|
@@ -654,20 +774,109 @@ class Command {
|
|
|
654
774
|
return result;
|
|
655
775
|
}
|
|
656
776
|
#getCommandPath() {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
777
|
+
return this.#name;
|
|
778
|
+
}
|
|
779
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
780
|
+
const token = tokens[idx];
|
|
781
|
+
const eqIdx = token.indexOf('=');
|
|
782
|
+
let optName;
|
|
783
|
+
let inlineValue;
|
|
784
|
+
if (eqIdx !== -1) {
|
|
785
|
+
optName = token.slice(2, eqIdx);
|
|
786
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
optName = token.slice(2);
|
|
790
|
+
}
|
|
791
|
+
const opt = optionByLong.get(optName);
|
|
792
|
+
if (!opt) {
|
|
793
|
+
return 0;
|
|
794
|
+
}
|
|
795
|
+
if (opt.type === 'boolean') {
|
|
796
|
+
if (inlineValue !== undefined) {
|
|
797
|
+
if (inlineValue === 'true') {
|
|
798
|
+
opts[optName] = true;
|
|
799
|
+
}
|
|
800
|
+
else if (inlineValue === 'false') {
|
|
801
|
+
opts[optName] = false;
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
opts[optName] = true;
|
|
809
|
+
}
|
|
810
|
+
return 1;
|
|
811
|
+
}
|
|
812
|
+
let value;
|
|
813
|
+
let consumed = 1;
|
|
814
|
+
if (inlineValue !== undefined) {
|
|
815
|
+
value = inlineValue;
|
|
816
|
+
}
|
|
817
|
+
else if (idx + 1 < tokens.length) {
|
|
818
|
+
value = tokens[idx + 1];
|
|
819
|
+
consumed = 2;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
823
|
+
}
|
|
824
|
+
this.#applyValue(opt, value, opts);
|
|
825
|
+
return consumed;
|
|
826
|
+
}
|
|
827
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
828
|
+
const token = tokens[idx];
|
|
829
|
+
if (token.includes('=')) {
|
|
830
|
+
const firstFlag = token[1];
|
|
831
|
+
if (!optionByShort.has(firstFlag)) {
|
|
832
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
833
|
+
}
|
|
834
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
835
|
+
}
|
|
836
|
+
const flags = token.slice(1);
|
|
837
|
+
let j = 0;
|
|
838
|
+
const consumedFlags = [];
|
|
839
|
+
const unconsumedFlags = [];
|
|
840
|
+
let nextIdx = idx + 1;
|
|
841
|
+
while (j < flags.length) {
|
|
842
|
+
const flag = flags[j];
|
|
843
|
+
const opt = optionByShort.get(flag);
|
|
844
|
+
if (!opt) {
|
|
845
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
consumedFlags.push(flag);
|
|
849
|
+
if (opt.type === 'boolean') {
|
|
850
|
+
opts[opt.long] = true;
|
|
851
|
+
j += 1;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (j < flags.length - 1) {
|
|
855
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
856
|
+
}
|
|
857
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
858
|
+
const value = tokens[idx + 1];
|
|
859
|
+
this.#applyValue(opt, value, opts);
|
|
860
|
+
nextIdx = idx + 2;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
864
|
+
}
|
|
865
|
+
j += 1;
|
|
866
|
+
}
|
|
867
|
+
if (consumedFlags.length > 0) {
|
|
868
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
869
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
660
870
|
}
|
|
661
|
-
return
|
|
871
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
662
872
|
}
|
|
663
873
|
}
|
|
664
874
|
|
|
665
875
|
class CompletionCommand extends Command {
|
|
666
876
|
constructor(root, config) {
|
|
667
|
-
const name = config.name ?? 'completion';
|
|
668
877
|
const paths = config.paths;
|
|
878
|
+
const programName = config.programName ?? root.name;
|
|
669
879
|
super({
|
|
670
|
-
name,
|
|
671
880
|
description: 'Generate shell completion script',
|
|
672
881
|
});
|
|
673
882
|
this.option({
|
|
@@ -694,7 +903,6 @@ class CompletionCommand extends Command {
|
|
|
694
903
|
})
|
|
695
904
|
.action(({ opts }) => {
|
|
696
905
|
const meta = root.getCompletionMeta();
|
|
697
|
-
const programName = root.name;
|
|
698
906
|
const selectedShells = [
|
|
699
907
|
opts['bash'] && 'bash',
|
|
700
908
|
opts['fish'] && 'fish',
|
package/lib/esm/index.mjs
CHANGED
|
@@ -45,34 +45,25 @@ class Command {
|
|
|
45
45
|
#name;
|
|
46
46
|
#description;
|
|
47
47
|
#version;
|
|
48
|
-
#aliases;
|
|
49
48
|
#helpSubcommandEnabled;
|
|
50
49
|
#options = [];
|
|
51
50
|
#arguments = [];
|
|
52
51
|
#subcommands = [];
|
|
53
52
|
#action;
|
|
54
|
-
#parent;
|
|
55
53
|
constructor(config) {
|
|
56
|
-
this.#name = config.name;
|
|
54
|
+
this.#name = config.name ?? '';
|
|
57
55
|
this.#description = config.description;
|
|
58
56
|
this.#version = config.version;
|
|
59
|
-
this.#aliases = config.aliases ?? [];
|
|
60
57
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
61
58
|
}
|
|
62
59
|
get name() {
|
|
63
60
|
return this.#name;
|
|
64
61
|
}
|
|
65
|
-
get aliases() {
|
|
66
|
-
return this.#aliases;
|
|
67
|
-
}
|
|
68
62
|
get description() {
|
|
69
63
|
return this.#description;
|
|
70
64
|
}
|
|
71
65
|
get version() {
|
|
72
|
-
return this.#version
|
|
73
|
-
}
|
|
74
|
-
get parent() {
|
|
75
|
-
return this.#parent;
|
|
66
|
+
return this.#version;
|
|
76
67
|
}
|
|
77
68
|
get options() {
|
|
78
69
|
return [...this.#options];
|
|
@@ -95,46 +86,57 @@ class Command {
|
|
|
95
86
|
this.#action = fn;
|
|
96
87
|
return this;
|
|
97
88
|
}
|
|
98
|
-
subcommand(cmd) {
|
|
99
|
-
if (this.#helpSubcommandEnabled &&
|
|
89
|
+
subcommand(name, cmd) {
|
|
90
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
100
91
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.aliases.push(name);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
cmd.#name = name;
|
|
99
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
100
|
+
}
|
|
104
101
|
return this;
|
|
105
102
|
}
|
|
106
103
|
async run(params) {
|
|
107
104
|
const { argv, envs, reporter } = params;
|
|
108
105
|
try {
|
|
109
106
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
110
|
-
const {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
108
|
+
const leafCommand = chain[chain.length - 1];
|
|
109
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
110
|
+
const leafOptions = leafCommand.#getMergedOptions();
|
|
111
|
+
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
112
|
+
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
113
|
+
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
114
|
+
console.log(leafCommand.formatHelp());
|
|
116
115
|
return;
|
|
117
116
|
}
|
|
118
|
-
if (!hasUserVersion &&
|
|
119
|
-
console.log(
|
|
117
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
118
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
120
119
|
return;
|
|
121
120
|
}
|
|
122
|
-
const
|
|
121
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
123
122
|
const ctx = {
|
|
124
|
-
cmd:
|
|
123
|
+
cmd: leafCommand,
|
|
125
124
|
envs,
|
|
126
125
|
reporter: reporter ?? new DefaultReporter(),
|
|
127
126
|
argv,
|
|
128
127
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
129
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
130
|
+
const args = restArgs;
|
|
131
|
+
const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
|
|
132
|
+
if (args.length < requiredArgs.length) {
|
|
133
|
+
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
134
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
|
|
133
135
|
}
|
|
134
|
-
const actionParams = { ctx, opts, args };
|
|
135
|
-
if (
|
|
136
|
+
const actionParams = { ctx, opts: mergedOpts, args };
|
|
137
|
+
if (leafCommand.#action) {
|
|
136
138
|
try {
|
|
137
|
-
await
|
|
139
|
+
await leafCommand.#action(actionParams);
|
|
138
140
|
}
|
|
139
141
|
catch (err) {
|
|
140
142
|
if (err instanceof Error) {
|
|
@@ -146,11 +148,11 @@ class Command {
|
|
|
146
148
|
process.exit(1);
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
|
-
else if (
|
|
150
|
-
console.log(
|
|
151
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
152
|
+
console.log(leafCommand.formatHelp());
|
|
151
153
|
}
|
|
152
154
|
else {
|
|
153
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
155
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
catch (err) {
|
|
@@ -228,6 +230,82 @@ class Command {
|
|
|
228
230
|
}
|
|
229
231
|
return { opts, args };
|
|
230
232
|
}
|
|
233
|
+
shift(tokens) {
|
|
234
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
235
|
+
}
|
|
236
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
237
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
238
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
239
|
+
const opts = {};
|
|
240
|
+
for (const opt of directOptions) {
|
|
241
|
+
if (opt.default !== undefined) {
|
|
242
|
+
opts[opt.long] = opt.default;
|
|
243
|
+
}
|
|
244
|
+
else if (opt.type === 'boolean') {
|
|
245
|
+
opts[opt.long] = false;
|
|
246
|
+
}
|
|
247
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
248
|
+
opts[opt.long] = [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let remaining = [...tokens];
|
|
252
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
253
|
+
for (const opt of resolverOptions) {
|
|
254
|
+
const result = opt.resolver(remaining);
|
|
255
|
+
opts[opt.long] = result.value;
|
|
256
|
+
remaining = result.remaining;
|
|
257
|
+
}
|
|
258
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
259
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
260
|
+
const finalRemaining = [];
|
|
261
|
+
let i = 0;
|
|
262
|
+
while (i < normalizedTokens.length) {
|
|
263
|
+
const token = normalizedTokens[i];
|
|
264
|
+
if (token.startsWith('--')) {
|
|
265
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
266
|
+
if (consumed > 0) {
|
|
267
|
+
i += consumed;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
finalRemaining.push(token);
|
|
271
|
+
i += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
275
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
276
|
+
if (result.consumed) {
|
|
277
|
+
i = result.nextIdx;
|
|
278
|
+
if (result.remainingToken) {
|
|
279
|
+
finalRemaining.push(result.remainingToken);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
finalRemaining.push(token);
|
|
284
|
+
i += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
finalRemaining.push(token);
|
|
288
|
+
i += 1;
|
|
289
|
+
}
|
|
290
|
+
for (const opt of directOptions) {
|
|
291
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
292
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const opt of directOptions) {
|
|
296
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
297
|
+
const value = opts[opt.long];
|
|
298
|
+
const values = Array.isArray(value) ? value : [value];
|
|
299
|
+
const choices = opt.choices;
|
|
300
|
+
for (const v of values) {
|
|
301
|
+
if (!choices.includes(v)) {
|
|
302
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { opts, remaining: finalRemaining };
|
|
308
|
+
}
|
|
231
309
|
formatHelp() {
|
|
232
310
|
const lines = [];
|
|
233
311
|
const allOptions = this.#getMergedOptions();
|
|
@@ -291,12 +369,12 @@ class Command {
|
|
|
291
369
|
if (showHelpSubcommand) {
|
|
292
370
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
293
371
|
}
|
|
294
|
-
for (const
|
|
295
|
-
let name =
|
|
296
|
-
if (
|
|
297
|
-
name += `, ${
|
|
372
|
+
for (const entry of this.#subcommands) {
|
|
373
|
+
let name = entry.name;
|
|
374
|
+
if (entry.aliases.length > 0) {
|
|
375
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
298
376
|
}
|
|
299
|
-
cmdLines.push({ name, desc:
|
|
377
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
300
378
|
}
|
|
301
379
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
302
380
|
for (const { name, desc } of cmdLines) {
|
|
@@ -323,9 +401,16 @@ class Command {
|
|
|
323
401
|
return {
|
|
324
402
|
name: this.#name,
|
|
325
403
|
description: this.#description,
|
|
326
|
-
aliases:
|
|
404
|
+
aliases: [],
|
|
327
405
|
options,
|
|
328
|
-
subcommands: this.#subcommands.map(
|
|
406
|
+
subcommands: this.#subcommands.map(entry => {
|
|
407
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
408
|
+
return {
|
|
409
|
+
...subMeta,
|
|
410
|
+
name: entry.name,
|
|
411
|
+
aliases: entry.aliases,
|
|
412
|
+
};
|
|
413
|
+
}),
|
|
329
414
|
};
|
|
330
415
|
}
|
|
331
416
|
#processHelpSubcommand(argv) {
|
|
@@ -337,26 +422,80 @@ class Command {
|
|
|
337
422
|
return ['--help'];
|
|
338
423
|
}
|
|
339
424
|
const subName = argv[1];
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
425
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
426
|
+
if (entry) {
|
|
342
427
|
return [subName, '--help', ...argv.slice(2)];
|
|
343
428
|
}
|
|
344
429
|
return argv;
|
|
345
430
|
}
|
|
346
|
-
#
|
|
431
|
+
#routeChain(argv) {
|
|
432
|
+
const chain = [this];
|
|
347
433
|
let current = this;
|
|
348
434
|
let idx = 0;
|
|
349
435
|
while (idx < argv.length) {
|
|
350
436
|
const token = argv[idx];
|
|
351
437
|
if (token.startsWith('-'))
|
|
352
438
|
break;
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
439
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
440
|
+
if (!entry)
|
|
355
441
|
break;
|
|
356
|
-
current =
|
|
442
|
+
current = entry.command;
|
|
443
|
+
chain.push(current);
|
|
357
444
|
idx += 1;
|
|
358
445
|
}
|
|
359
|
-
return {
|
|
446
|
+
return { chain, remaining: argv.slice(idx) };
|
|
447
|
+
}
|
|
448
|
+
#splitAtDoubleDash(tokens) {
|
|
449
|
+
const ddIdx = tokens.indexOf('--');
|
|
450
|
+
if (ddIdx === -1) {
|
|
451
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
455
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
#shiftChain(chain, tokens) {
|
|
459
|
+
const optsMap = new Map();
|
|
460
|
+
let remaining = [...tokens];
|
|
461
|
+
const shadowed = new Set();
|
|
462
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
463
|
+
const cmd = chain[i];
|
|
464
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
465
|
+
optsMap.set(cmd, result.opts);
|
|
466
|
+
remaining = result.remaining;
|
|
467
|
+
for (const opt of cmd.#options) {
|
|
468
|
+
shadowed.add(opt.long);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (remaining.length > 0) {
|
|
472
|
+
const leafCommand = chain[chain.length - 1];
|
|
473
|
+
const firstToken = remaining[0];
|
|
474
|
+
if (firstToken.startsWith('-')) {
|
|
475
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return optsMap;
|
|
482
|
+
}
|
|
483
|
+
#applyChain(chain, optsMap, ctx) {
|
|
484
|
+
for (const cmd of chain) {
|
|
485
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
486
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
487
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
488
|
+
opt.apply(opts[opt.long], ctx);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
#mergeOpts(chain, optsMap) {
|
|
494
|
+
const merged = {};
|
|
495
|
+
for (const cmd of chain) {
|
|
496
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
497
|
+
}
|
|
498
|
+
return merged;
|
|
360
499
|
}
|
|
361
500
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
362
501
|
const token = argv[idx];
|
|
@@ -467,33 +606,17 @@ class Command {
|
|
|
467
606
|
}
|
|
468
607
|
}
|
|
469
608
|
#getMergedOptions() {
|
|
470
|
-
const ancestors = [];
|
|
471
|
-
for (let node = this; node; node = node.#parent) {
|
|
472
|
-
ancestors.unshift(node);
|
|
473
|
-
}
|
|
474
609
|
const optionMap = new Map();
|
|
475
|
-
const hasUserHelp =
|
|
476
|
-
const hasUserVersion =
|
|
610
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
611
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
477
612
|
if (!hasUserHelp) {
|
|
478
613
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
479
614
|
}
|
|
480
615
|
if (!hasUserVersion) {
|
|
481
616
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
482
617
|
}
|
|
483
|
-
for (const
|
|
484
|
-
|
|
485
|
-
optionMap.set(opt.long, opt);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
const shortToLong = new Map();
|
|
489
|
-
for (const [long, opt] of optionMap) {
|
|
490
|
-
if (opt.short) {
|
|
491
|
-
const existing = shortToLong.get(opt.short);
|
|
492
|
-
if (existing && existing !== long) {
|
|
493
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
494
|
-
}
|
|
495
|
-
shortToLong.set(opt.short, long);
|
|
496
|
-
}
|
|
618
|
+
for (const opt of this.#options) {
|
|
619
|
+
optionMap.set(opt.long, opt);
|
|
497
620
|
}
|
|
498
621
|
return Array.from(optionMap.values());
|
|
499
622
|
}
|
|
@@ -535,9 +658,6 @@ class Command {
|
|
|
535
658
|
}
|
|
536
659
|
}
|
|
537
660
|
}
|
|
538
|
-
#isBuiltinOption(opt) {
|
|
539
|
-
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
540
|
-
}
|
|
541
661
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
542
662
|
const optionByLong = new Map();
|
|
543
663
|
const optionByShort = new Map();
|
|
@@ -632,20 +752,109 @@ class Command {
|
|
|
632
752
|
return result;
|
|
633
753
|
}
|
|
634
754
|
#getCommandPath() {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
755
|
+
return this.#name;
|
|
756
|
+
}
|
|
757
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
758
|
+
const token = tokens[idx];
|
|
759
|
+
const eqIdx = token.indexOf('=');
|
|
760
|
+
let optName;
|
|
761
|
+
let inlineValue;
|
|
762
|
+
if (eqIdx !== -1) {
|
|
763
|
+
optName = token.slice(2, eqIdx);
|
|
764
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
optName = token.slice(2);
|
|
768
|
+
}
|
|
769
|
+
const opt = optionByLong.get(optName);
|
|
770
|
+
if (!opt) {
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
if (opt.type === 'boolean') {
|
|
774
|
+
if (inlineValue !== undefined) {
|
|
775
|
+
if (inlineValue === 'true') {
|
|
776
|
+
opts[optName] = true;
|
|
777
|
+
}
|
|
778
|
+
else if (inlineValue === 'false') {
|
|
779
|
+
opts[optName] = false;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
opts[optName] = true;
|
|
787
|
+
}
|
|
788
|
+
return 1;
|
|
789
|
+
}
|
|
790
|
+
let value;
|
|
791
|
+
let consumed = 1;
|
|
792
|
+
if (inlineValue !== undefined) {
|
|
793
|
+
value = inlineValue;
|
|
794
|
+
}
|
|
795
|
+
else if (idx + 1 < tokens.length) {
|
|
796
|
+
value = tokens[idx + 1];
|
|
797
|
+
consumed = 2;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
801
|
+
}
|
|
802
|
+
this.#applyValue(opt, value, opts);
|
|
803
|
+
return consumed;
|
|
804
|
+
}
|
|
805
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
806
|
+
const token = tokens[idx];
|
|
807
|
+
if (token.includes('=')) {
|
|
808
|
+
const firstFlag = token[1];
|
|
809
|
+
if (!optionByShort.has(firstFlag)) {
|
|
810
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
811
|
+
}
|
|
812
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
813
|
+
}
|
|
814
|
+
const flags = token.slice(1);
|
|
815
|
+
let j = 0;
|
|
816
|
+
const consumedFlags = [];
|
|
817
|
+
const unconsumedFlags = [];
|
|
818
|
+
let nextIdx = idx + 1;
|
|
819
|
+
while (j < flags.length) {
|
|
820
|
+
const flag = flags[j];
|
|
821
|
+
const opt = optionByShort.get(flag);
|
|
822
|
+
if (!opt) {
|
|
823
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
consumedFlags.push(flag);
|
|
827
|
+
if (opt.type === 'boolean') {
|
|
828
|
+
opts[opt.long] = true;
|
|
829
|
+
j += 1;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (j < flags.length - 1) {
|
|
833
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
834
|
+
}
|
|
835
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
836
|
+
const value = tokens[idx + 1];
|
|
837
|
+
this.#applyValue(opt, value, opts);
|
|
838
|
+
nextIdx = idx + 2;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
842
|
+
}
|
|
843
|
+
j += 1;
|
|
844
|
+
}
|
|
845
|
+
if (consumedFlags.length > 0) {
|
|
846
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
847
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
638
848
|
}
|
|
639
|
-
return
|
|
849
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
640
850
|
}
|
|
641
851
|
}
|
|
642
852
|
|
|
643
853
|
class CompletionCommand extends Command {
|
|
644
854
|
constructor(root, config) {
|
|
645
|
-
const name = config.name ?? 'completion';
|
|
646
855
|
const paths = config.paths;
|
|
856
|
+
const programName = config.programName ?? root.name;
|
|
647
857
|
super({
|
|
648
|
-
name,
|
|
649
858
|
description: 'Generate shell completion script',
|
|
650
859
|
});
|
|
651
860
|
this.option({
|
|
@@ -672,7 +881,6 @@ class CompletionCommand extends Command {
|
|
|
672
881
|
})
|
|
673
882
|
.action(({ opts }) => {
|
|
674
883
|
const meta = root.getCompletionMeta();
|
|
675
|
-
const programName = root.name;
|
|
676
884
|
const selectedShells = [
|
|
677
885
|
opts['bash'] && 'bash',
|
|
678
886
|
opts['fish'] && 'fish',
|
package/lib/types/index.d.ts
CHANGED
|
@@ -57,10 +57,8 @@ interface IArgument {
|
|
|
57
57
|
}
|
|
58
58
|
/** Command configuration */
|
|
59
59
|
interface ICommandConfig {
|
|
60
|
-
/** Command name (
|
|
61
|
-
name
|
|
62
|
-
/** Command aliases */
|
|
63
|
-
aliases?: string[];
|
|
60
|
+
/** Command name (only effective for root command) */
|
|
61
|
+
name?: string;
|
|
64
62
|
/** Command description */
|
|
65
63
|
description: string;
|
|
66
64
|
/** Version (only effective for root command) */
|
|
@@ -71,10 +69,8 @@ interface ICommandConfig {
|
|
|
71
69
|
/** Forward declaration for Command class */
|
|
72
70
|
interface ICommand {
|
|
73
71
|
readonly name: string;
|
|
74
|
-
readonly aliases: string[];
|
|
75
72
|
readonly description: string;
|
|
76
73
|
readonly version: string | undefined;
|
|
77
|
-
readonly parent: ICommand | undefined;
|
|
78
74
|
readonly options: IOption[];
|
|
79
75
|
readonly arguments: IArgument[];
|
|
80
76
|
}
|
|
@@ -116,8 +112,15 @@ interface IParseResult {
|
|
|
116
112
|
/** Parsed positional arguments */
|
|
117
113
|
args: string[];
|
|
118
114
|
}
|
|
115
|
+
/** shift() method result */
|
|
116
|
+
interface IShiftResult {
|
|
117
|
+
/** Options consumed by this command */
|
|
118
|
+
opts: Record<string, unknown>;
|
|
119
|
+
/** Tokens not consumed, to be passed to parent */
|
|
120
|
+
remaining: string[];
|
|
121
|
+
}
|
|
119
122
|
/** Error kinds for command parsing */
|
|
120
|
-
type ICommanderErrorKind = 'UnknownOption' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
123
|
+
type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
121
124
|
/** Commander error with structured information */
|
|
122
125
|
declare class CommanderError extends Error {
|
|
123
126
|
readonly kind: ICommanderErrorKind;
|
|
@@ -155,8 +158,8 @@ interface ICompletionPaths {
|
|
|
155
158
|
}
|
|
156
159
|
/** CompletionCommand configuration */
|
|
157
160
|
interface ICompletionCommandConfig {
|
|
158
|
-
/**
|
|
159
|
-
|
|
161
|
+
/** Program name for completion scripts (defaults to root.name) */
|
|
162
|
+
programName?: string;
|
|
160
163
|
/** Default completion file paths for each shell (required for --write support) */
|
|
161
164
|
paths: ICompletionPaths;
|
|
162
165
|
}
|
|
@@ -171,18 +174,21 @@ declare class Command implements ICommand {
|
|
|
171
174
|
#private;
|
|
172
175
|
constructor(config: ICommandConfig);
|
|
173
176
|
get name(): string;
|
|
174
|
-
get aliases(): string[];
|
|
175
177
|
get description(): string;
|
|
176
178
|
get version(): string | undefined;
|
|
177
|
-
get parent(): Command | undefined;
|
|
178
179
|
get options(): IOption[];
|
|
179
180
|
get arguments(): IArgument[];
|
|
180
181
|
option(opt: IOption): this;
|
|
181
182
|
argument(arg: IArgument): this;
|
|
182
183
|
action(fn: IAction): this;
|
|
183
|
-
subcommand(cmd: Command): this;
|
|
184
|
+
subcommand(name: string, cmd: Command): this;
|
|
184
185
|
run(params: IRunParams): Promise<void>;
|
|
185
186
|
parse(argv: string[]): IParseResult;
|
|
187
|
+
/**
|
|
188
|
+
* Shift options from tokens that this command recognizes.
|
|
189
|
+
* Unrecognized tokens are returned in `remaining` for parent commands.
|
|
190
|
+
*/
|
|
191
|
+
shift(tokens: string[]): IShiftResult;
|
|
186
192
|
formatHelp(): string;
|
|
187
193
|
getCompletionMeta(): ICompletionMeta;
|
|
188
194
|
}
|
|
@@ -199,7 +205,7 @@ declare class Command implements ICommand {
|
|
|
199
205
|
* @example
|
|
200
206
|
* ```typescript
|
|
201
207
|
* const root = new Command({ name: 'mycli', description: 'My CLI' })
|
|
202
|
-
* root.subcommand(new CompletionCommand(root, {
|
|
208
|
+
* root.subcommand('completion', new CompletionCommand(root, {
|
|
203
209
|
* paths: {
|
|
204
210
|
* bash: `~/.local/share/bash-completion/completions/mycli`,
|
|
205
211
|
* fish: `~/.config/fish/completions/mycli.fish`,
|
|
@@ -233,4 +239,4 @@ declare class PwshCompletion {
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion };
|
|
236
|
-
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType };
|
|
242
|
+
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType, IShiftResult };
|