@guanghechen/commander 3.0.0 → 3.2.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 +19 -0
- package/lib/cjs/index.cjs +308 -33
- package/lib/esm/index.mjs +308 -33
- package/lib/types/index.d.ts +35 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 3.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Change args from string[] to Record<string, unknown> with type/coerce/default support:
|
|
8
|
+
- `args` is now `Record<string, unknown>` keyed by argument name
|
|
9
|
+
- Add `rawArgs: string[]` for original argument strings before type conversion
|
|
10
|
+
- IArgument now supports `type`, `default`, and `coerce` properties
|
|
11
|
+
- Add `TooManyArguments` error kind for extra arguments validation
|
|
12
|
+
|
|
13
|
+
## 3.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Implement option bubbling with shift/apply flow
|
|
18
|
+
- Add `shift()` method for bottom-up option consumption (leaf → root)
|
|
19
|
+
- Refactor `run()` with new flow: route → split → shift → apply → action
|
|
20
|
+
- Add `UnexpectedArgument` error type for positional args before `--`
|
|
21
|
+
|
|
3
22
|
All notable changes to this project will be documented in this file. See
|
|
4
23
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
24
|
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -126,34 +126,34 @@ class Command {
|
|
|
126
126
|
const { argv, envs, reporter } = params;
|
|
127
127
|
try {
|
|
128
128
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
129
|
-
const {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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());
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
137
|
-
if (!hasUserVersion &&
|
|
138
|
-
console.log(
|
|
139
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
140
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
141
|
-
const
|
|
143
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
142
144
|
const ctx = {
|
|
143
|
-
cmd:
|
|
145
|
+
cmd: leafCommand,
|
|
144
146
|
envs,
|
|
145
147
|
reporter: reporter ?? new DefaultReporter(),
|
|
146
148
|
argv,
|
|
147
149
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const actionParams = { ctx, opts, args };
|
|
154
|
-
if (command.#action) {
|
|
150
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
151
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
152
|
+
const { args, rawArgs } = leafCommand.#parseArguments(restArgs);
|
|
153
|
+
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
154
|
+
if (leafCommand.#action) {
|
|
155
155
|
try {
|
|
156
|
-
await
|
|
156
|
+
await leafCommand.#action(actionParams);
|
|
157
157
|
}
|
|
158
158
|
catch (err) {
|
|
159
159
|
if (err instanceof Error) {
|
|
@@ -165,11 +165,11 @@ class Command {
|
|
|
165
165
|
process.exit(1);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
else if (
|
|
169
|
-
console.log(
|
|
168
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
169
|
+
console.log(leafCommand.formatHelp());
|
|
170
170
|
}
|
|
171
171
|
else {
|
|
172
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
172
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
catch (err) {
|
|
@@ -184,7 +184,7 @@ class Command {
|
|
|
184
184
|
parse(argv) {
|
|
185
185
|
const allOptions = this.#getMergedOptions();
|
|
186
186
|
const opts = {};
|
|
187
|
-
const
|
|
187
|
+
const rawArgs = [];
|
|
188
188
|
for (const opt of allOptions) {
|
|
189
189
|
if (opt.default !== undefined) {
|
|
190
190
|
opts[opt.long] = opt.default;
|
|
@@ -209,7 +209,7 @@ class Command {
|
|
|
209
209
|
while (i < remaining.length) {
|
|
210
210
|
const token = remaining[i];
|
|
211
211
|
if (token === '--') {
|
|
212
|
-
|
|
212
|
+
rawArgs.push(...remaining.slice(i + 1));
|
|
213
213
|
break;
|
|
214
214
|
}
|
|
215
215
|
if (token.startsWith('--')) {
|
|
@@ -220,7 +220,7 @@ class Command {
|
|
|
220
220
|
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
|
-
|
|
223
|
+
rawArgs.push(token);
|
|
224
224
|
i += 1;
|
|
225
225
|
}
|
|
226
226
|
for (const opt of allOptions) {
|
|
@@ -240,12 +240,84 @@ class Command {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
const { args } = this.#parseArguments(rawArgs);
|
|
244
|
+
return { opts, args, rawArgs };
|
|
245
|
+
}
|
|
246
|
+
shift(tokens) {
|
|
247
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
248
|
+
}
|
|
249
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
250
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
251
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
252
|
+
const opts = {};
|
|
253
|
+
for (const opt of directOptions) {
|
|
254
|
+
if (opt.default !== undefined) {
|
|
255
|
+
opts[opt.long] = opt.default;
|
|
256
|
+
}
|
|
257
|
+
else if (opt.type === 'boolean') {
|
|
258
|
+
opts[opt.long] = false;
|
|
259
|
+
}
|
|
260
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
261
|
+
opts[opt.long] = [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let remaining = [...tokens];
|
|
265
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
266
|
+
for (const opt of resolverOptions) {
|
|
267
|
+
const result = opt.resolver(remaining);
|
|
268
|
+
opts[opt.long] = result.value;
|
|
269
|
+
remaining = result.remaining;
|
|
270
|
+
}
|
|
271
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
272
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
273
|
+
const finalRemaining = [];
|
|
274
|
+
let i = 0;
|
|
275
|
+
while (i < normalizedTokens.length) {
|
|
276
|
+
const token = normalizedTokens[i];
|
|
277
|
+
if (token.startsWith('--')) {
|
|
278
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
279
|
+
if (consumed > 0) {
|
|
280
|
+
i += consumed;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
finalRemaining.push(token);
|
|
284
|
+
i += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
288
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
289
|
+
if (result.consumed) {
|
|
290
|
+
i = result.nextIdx;
|
|
291
|
+
if (result.remainingToken) {
|
|
292
|
+
finalRemaining.push(result.remainingToken);
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
finalRemaining.push(token);
|
|
297
|
+
i += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
finalRemaining.push(token);
|
|
301
|
+
i += 1;
|
|
302
|
+
}
|
|
303
|
+
for (const opt of directOptions) {
|
|
304
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
305
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
306
|
+
}
|
|
247
307
|
}
|
|
248
|
-
|
|
308
|
+
for (const opt of directOptions) {
|
|
309
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
310
|
+
const value = opts[opt.long];
|
|
311
|
+
const values = Array.isArray(value) ? value : [value];
|
|
312
|
+
const choices = opt.choices;
|
|
313
|
+
for (const v of values) {
|
|
314
|
+
if (!choices.includes(v)) {
|
|
315
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { opts, remaining: finalRemaining };
|
|
249
321
|
}
|
|
250
322
|
formatHelp() {
|
|
251
323
|
const lines = [];
|
|
@@ -369,7 +441,8 @@ class Command {
|
|
|
369
441
|
}
|
|
370
442
|
return argv;
|
|
371
443
|
}
|
|
372
|
-
#
|
|
444
|
+
#routeChain(argv) {
|
|
445
|
+
const chain = [this];
|
|
373
446
|
let current = this;
|
|
374
447
|
let idx = 0;
|
|
375
448
|
while (idx < argv.length) {
|
|
@@ -380,9 +453,62 @@ class Command {
|
|
|
380
453
|
if (!entry)
|
|
381
454
|
break;
|
|
382
455
|
current = entry.command;
|
|
456
|
+
chain.push(current);
|
|
383
457
|
idx += 1;
|
|
384
458
|
}
|
|
385
|
-
return {
|
|
459
|
+
return { chain, remaining: argv.slice(idx) };
|
|
460
|
+
}
|
|
461
|
+
#splitAtDoubleDash(tokens) {
|
|
462
|
+
const ddIdx = tokens.indexOf('--');
|
|
463
|
+
if (ddIdx === -1) {
|
|
464
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
468
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
#shiftChain(chain, tokens) {
|
|
472
|
+
const optsMap = new Map();
|
|
473
|
+
let remaining = [...tokens];
|
|
474
|
+
const shadowed = new Set();
|
|
475
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
476
|
+
const cmd = chain[i];
|
|
477
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
478
|
+
optsMap.set(cmd, result.opts);
|
|
479
|
+
remaining = result.remaining;
|
|
480
|
+
for (const opt of cmd.#options) {
|
|
481
|
+
shadowed.add(opt.long);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (remaining.length > 0) {
|
|
485
|
+
const leafCommand = chain[chain.length - 1];
|
|
486
|
+
const firstToken = remaining[0];
|
|
487
|
+
if (firstToken.startsWith('-')) {
|
|
488
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return optsMap;
|
|
495
|
+
}
|
|
496
|
+
#applyChain(chain, optsMap, ctx) {
|
|
497
|
+
for (const cmd of chain) {
|
|
498
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
499
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
500
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
501
|
+
opt.apply(opts[opt.long], ctx);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
#mergeOpts(chain, optsMap) {
|
|
507
|
+
const merged = {};
|
|
508
|
+
for (const cmd of chain) {
|
|
509
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
510
|
+
}
|
|
511
|
+
return merged;
|
|
386
512
|
}
|
|
387
513
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
388
514
|
const token = argv[idx];
|
|
@@ -527,6 +653,9 @@ class Command {
|
|
|
527
653
|
}
|
|
528
654
|
}
|
|
529
655
|
#validateArgumentConfig(arg) {
|
|
656
|
+
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
657
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
658
|
+
}
|
|
530
659
|
if (arg.kind === 'variadic') {
|
|
531
660
|
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
532
661
|
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
@@ -545,8 +674,60 @@ class Command {
|
|
|
545
674
|
}
|
|
546
675
|
}
|
|
547
676
|
}
|
|
548
|
-
#
|
|
549
|
-
|
|
677
|
+
#parseArguments(rawArgs) {
|
|
678
|
+
const argumentDefs = this.#arguments;
|
|
679
|
+
const args = {};
|
|
680
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
681
|
+
if (rawArgs.length < requiredCount) {
|
|
682
|
+
const missing = argumentDefs
|
|
683
|
+
.filter(a => a.kind === 'required')
|
|
684
|
+
.slice(rawArgs.length)
|
|
685
|
+
.map(a => a.name);
|
|
686
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
687
|
+
}
|
|
688
|
+
let index = 0;
|
|
689
|
+
for (const def of argumentDefs) {
|
|
690
|
+
if (def.kind === 'variadic') {
|
|
691
|
+
const rest = rawArgs.slice(index);
|
|
692
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
693
|
+
index = rawArgs.length;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
const raw = rawArgs[index];
|
|
697
|
+
if (raw === undefined) {
|
|
698
|
+
if (def.kind === 'optional') {
|
|
699
|
+
args[def.name] = def.default ?? undefined;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
705
|
+
index += 1;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
709
|
+
if (!hasVariadic && index < rawArgs.length) {
|
|
710
|
+
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
711
|
+
}
|
|
712
|
+
return { args, rawArgs };
|
|
713
|
+
}
|
|
714
|
+
#convertArgument(def, raw) {
|
|
715
|
+
if (def.coerce) {
|
|
716
|
+
try {
|
|
717
|
+
return def.coerce(raw);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (def.type === 'number') {
|
|
724
|
+
const n = Number(raw);
|
|
725
|
+
if (Number.isNaN(n)) {
|
|
726
|
+
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
727
|
+
}
|
|
728
|
+
return n;
|
|
729
|
+
}
|
|
730
|
+
return raw;
|
|
550
731
|
}
|
|
551
732
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
552
733
|
const optionByLong = new Map();
|
|
@@ -644,6 +825,100 @@ class Command {
|
|
|
644
825
|
#getCommandPath() {
|
|
645
826
|
return this.#name;
|
|
646
827
|
}
|
|
828
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
829
|
+
const token = tokens[idx];
|
|
830
|
+
const eqIdx = token.indexOf('=');
|
|
831
|
+
let optName;
|
|
832
|
+
let inlineValue;
|
|
833
|
+
if (eqIdx !== -1) {
|
|
834
|
+
optName = token.slice(2, eqIdx);
|
|
835
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
optName = token.slice(2);
|
|
839
|
+
}
|
|
840
|
+
const opt = optionByLong.get(optName);
|
|
841
|
+
if (!opt) {
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
844
|
+
if (opt.type === 'boolean') {
|
|
845
|
+
if (inlineValue !== undefined) {
|
|
846
|
+
if (inlineValue === 'true') {
|
|
847
|
+
opts[optName] = true;
|
|
848
|
+
}
|
|
849
|
+
else if (inlineValue === 'false') {
|
|
850
|
+
opts[optName] = false;
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
opts[optName] = true;
|
|
858
|
+
}
|
|
859
|
+
return 1;
|
|
860
|
+
}
|
|
861
|
+
let value;
|
|
862
|
+
let consumed = 1;
|
|
863
|
+
if (inlineValue !== undefined) {
|
|
864
|
+
value = inlineValue;
|
|
865
|
+
}
|
|
866
|
+
else if (idx + 1 < tokens.length) {
|
|
867
|
+
value = tokens[idx + 1];
|
|
868
|
+
consumed = 2;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
872
|
+
}
|
|
873
|
+
this.#applyValue(opt, value, opts);
|
|
874
|
+
return consumed;
|
|
875
|
+
}
|
|
876
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
877
|
+
const token = tokens[idx];
|
|
878
|
+
if (token.includes('=')) {
|
|
879
|
+
const firstFlag = token[1];
|
|
880
|
+
if (!optionByShort.has(firstFlag)) {
|
|
881
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
882
|
+
}
|
|
883
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
884
|
+
}
|
|
885
|
+
const flags = token.slice(1);
|
|
886
|
+
let j = 0;
|
|
887
|
+
const consumedFlags = [];
|
|
888
|
+
const unconsumedFlags = [];
|
|
889
|
+
let nextIdx = idx + 1;
|
|
890
|
+
while (j < flags.length) {
|
|
891
|
+
const flag = flags[j];
|
|
892
|
+
const opt = optionByShort.get(flag);
|
|
893
|
+
if (!opt) {
|
|
894
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
consumedFlags.push(flag);
|
|
898
|
+
if (opt.type === 'boolean') {
|
|
899
|
+
opts[opt.long] = true;
|
|
900
|
+
j += 1;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (j < flags.length - 1) {
|
|
904
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
905
|
+
}
|
|
906
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
907
|
+
const value = tokens[idx + 1];
|
|
908
|
+
this.#applyValue(opt, value, opts);
|
|
909
|
+
nextIdx = idx + 2;
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
913
|
+
}
|
|
914
|
+
j += 1;
|
|
915
|
+
}
|
|
916
|
+
if (consumedFlags.length > 0) {
|
|
917
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
918
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
919
|
+
}
|
|
920
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
921
|
+
}
|
|
647
922
|
}
|
|
648
923
|
|
|
649
924
|
class CompletionCommand extends Command {
|
package/lib/esm/index.mjs
CHANGED
|
@@ -104,34 +104,34 @@ class Command {
|
|
|
104
104
|
const { argv, envs, reporter } = params;
|
|
105
105
|
try {
|
|
106
106
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
107
|
-
const {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
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());
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
|
-
if (!hasUserVersion &&
|
|
116
|
-
console.log(
|
|
117
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
118
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
|
-
const
|
|
121
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
120
122
|
const ctx = {
|
|
121
|
-
cmd:
|
|
123
|
+
cmd: leafCommand,
|
|
122
124
|
envs,
|
|
123
125
|
reporter: reporter ?? new DefaultReporter(),
|
|
124
126
|
argv,
|
|
125
127
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const actionParams = { ctx, opts, args };
|
|
132
|
-
if (command.#action) {
|
|
128
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
129
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
130
|
+
const { args, rawArgs } = leafCommand.#parseArguments(restArgs);
|
|
131
|
+
const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
|
|
132
|
+
if (leafCommand.#action) {
|
|
133
133
|
try {
|
|
134
|
-
await
|
|
134
|
+
await leafCommand.#action(actionParams);
|
|
135
135
|
}
|
|
136
136
|
catch (err) {
|
|
137
137
|
if (err instanceof Error) {
|
|
@@ -143,11 +143,11 @@ class Command {
|
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
else if (
|
|
147
|
-
console.log(
|
|
146
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
147
|
+
console.log(leafCommand.formatHelp());
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
150
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
150
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
catch (err) {
|
|
@@ -162,7 +162,7 @@ class Command {
|
|
|
162
162
|
parse(argv) {
|
|
163
163
|
const allOptions = this.#getMergedOptions();
|
|
164
164
|
const opts = {};
|
|
165
|
-
const
|
|
165
|
+
const rawArgs = [];
|
|
166
166
|
for (const opt of allOptions) {
|
|
167
167
|
if (opt.default !== undefined) {
|
|
168
168
|
opts[opt.long] = opt.default;
|
|
@@ -187,7 +187,7 @@ class Command {
|
|
|
187
187
|
while (i < remaining.length) {
|
|
188
188
|
const token = remaining[i];
|
|
189
189
|
if (token === '--') {
|
|
190
|
-
|
|
190
|
+
rawArgs.push(...remaining.slice(i + 1));
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
193
|
if (token.startsWith('--')) {
|
|
@@ -198,7 +198,7 @@ class Command {
|
|
|
198
198
|
i = this.#parseShortOption(remaining, i, optionByShort, opts);
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
rawArgs.push(token);
|
|
202
202
|
i += 1;
|
|
203
203
|
}
|
|
204
204
|
for (const opt of allOptions) {
|
|
@@ -218,12 +218,84 @@ class Command {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
const { args } = this.#parseArguments(rawArgs);
|
|
222
|
+
return { opts, args, rawArgs };
|
|
223
|
+
}
|
|
224
|
+
shift(tokens) {
|
|
225
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
226
|
+
}
|
|
227
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
228
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
229
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
230
|
+
const opts = {};
|
|
231
|
+
for (const opt of directOptions) {
|
|
232
|
+
if (opt.default !== undefined) {
|
|
233
|
+
opts[opt.long] = opt.default;
|
|
234
|
+
}
|
|
235
|
+
else if (opt.type === 'boolean') {
|
|
236
|
+
opts[opt.long] = false;
|
|
237
|
+
}
|
|
238
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
239
|
+
opts[opt.long] = [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let remaining = [...tokens];
|
|
243
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
244
|
+
for (const opt of resolverOptions) {
|
|
245
|
+
const result = opt.resolver(remaining);
|
|
246
|
+
opts[opt.long] = result.value;
|
|
247
|
+
remaining = result.remaining;
|
|
248
|
+
}
|
|
249
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
250
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
251
|
+
const finalRemaining = [];
|
|
252
|
+
let i = 0;
|
|
253
|
+
while (i < normalizedTokens.length) {
|
|
254
|
+
const token = normalizedTokens[i];
|
|
255
|
+
if (token.startsWith('--')) {
|
|
256
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
257
|
+
if (consumed > 0) {
|
|
258
|
+
i += consumed;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
finalRemaining.push(token);
|
|
262
|
+
i += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
266
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
267
|
+
if (result.consumed) {
|
|
268
|
+
i = result.nextIdx;
|
|
269
|
+
if (result.remainingToken) {
|
|
270
|
+
finalRemaining.push(result.remainingToken);
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
finalRemaining.push(token);
|
|
275
|
+
i += 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
finalRemaining.push(token);
|
|
279
|
+
i += 1;
|
|
280
|
+
}
|
|
281
|
+
for (const opt of directOptions) {
|
|
282
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
283
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
284
|
+
}
|
|
225
285
|
}
|
|
226
|
-
|
|
286
|
+
for (const opt of directOptions) {
|
|
287
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
288
|
+
const value = opts[opt.long];
|
|
289
|
+
const values = Array.isArray(value) ? value : [value];
|
|
290
|
+
const choices = opt.choices;
|
|
291
|
+
for (const v of values) {
|
|
292
|
+
if (!choices.includes(v)) {
|
|
293
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { opts, remaining: finalRemaining };
|
|
227
299
|
}
|
|
228
300
|
formatHelp() {
|
|
229
301
|
const lines = [];
|
|
@@ -347,7 +419,8 @@ class Command {
|
|
|
347
419
|
}
|
|
348
420
|
return argv;
|
|
349
421
|
}
|
|
350
|
-
#
|
|
422
|
+
#routeChain(argv) {
|
|
423
|
+
const chain = [this];
|
|
351
424
|
let current = this;
|
|
352
425
|
let idx = 0;
|
|
353
426
|
while (idx < argv.length) {
|
|
@@ -358,9 +431,62 @@ class Command {
|
|
|
358
431
|
if (!entry)
|
|
359
432
|
break;
|
|
360
433
|
current = entry.command;
|
|
434
|
+
chain.push(current);
|
|
361
435
|
idx += 1;
|
|
362
436
|
}
|
|
363
|
-
return {
|
|
437
|
+
return { chain, remaining: argv.slice(idx) };
|
|
438
|
+
}
|
|
439
|
+
#splitAtDoubleDash(tokens) {
|
|
440
|
+
const ddIdx = tokens.indexOf('--');
|
|
441
|
+
if (ddIdx === -1) {
|
|
442
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
446
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
#shiftChain(chain, tokens) {
|
|
450
|
+
const optsMap = new Map();
|
|
451
|
+
let remaining = [...tokens];
|
|
452
|
+
const shadowed = new Set();
|
|
453
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
454
|
+
const cmd = chain[i];
|
|
455
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
456
|
+
optsMap.set(cmd, result.opts);
|
|
457
|
+
remaining = result.remaining;
|
|
458
|
+
for (const opt of cmd.#options) {
|
|
459
|
+
shadowed.add(opt.long);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (remaining.length > 0) {
|
|
463
|
+
const leafCommand = chain[chain.length - 1];
|
|
464
|
+
const firstToken = remaining[0];
|
|
465
|
+
if (firstToken.startsWith('-')) {
|
|
466
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return optsMap;
|
|
473
|
+
}
|
|
474
|
+
#applyChain(chain, optsMap, ctx) {
|
|
475
|
+
for (const cmd of chain) {
|
|
476
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
477
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
478
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
479
|
+
opt.apply(opts[opt.long], ctx);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
#mergeOpts(chain, optsMap) {
|
|
485
|
+
const merged = {};
|
|
486
|
+
for (const cmd of chain) {
|
|
487
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
488
|
+
}
|
|
489
|
+
return merged;
|
|
364
490
|
}
|
|
365
491
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
366
492
|
const token = argv[idx];
|
|
@@ -505,6 +631,9 @@ class Command {
|
|
|
505
631
|
}
|
|
506
632
|
}
|
|
507
633
|
#validateArgumentConfig(arg) {
|
|
634
|
+
if (arg.kind === 'required' && arg.default !== undefined) {
|
|
635
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
|
|
636
|
+
}
|
|
508
637
|
if (arg.kind === 'variadic') {
|
|
509
638
|
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
510
639
|
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
@@ -523,8 +652,60 @@ class Command {
|
|
|
523
652
|
}
|
|
524
653
|
}
|
|
525
654
|
}
|
|
526
|
-
#
|
|
527
|
-
|
|
655
|
+
#parseArguments(rawArgs) {
|
|
656
|
+
const argumentDefs = this.#arguments;
|
|
657
|
+
const args = {};
|
|
658
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
659
|
+
if (rawArgs.length < requiredCount) {
|
|
660
|
+
const missing = argumentDefs
|
|
661
|
+
.filter(a => a.kind === 'required')
|
|
662
|
+
.slice(rawArgs.length)
|
|
663
|
+
.map(a => a.name);
|
|
664
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
665
|
+
}
|
|
666
|
+
let index = 0;
|
|
667
|
+
for (const def of argumentDefs) {
|
|
668
|
+
if (def.kind === 'variadic') {
|
|
669
|
+
const rest = rawArgs.slice(index);
|
|
670
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
671
|
+
index = rawArgs.length;
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
const raw = rawArgs[index];
|
|
675
|
+
if (raw === undefined) {
|
|
676
|
+
if (def.kind === 'optional') {
|
|
677
|
+
args[def.name] = def.default ?? undefined;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
683
|
+
index += 1;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
687
|
+
if (!hasVariadic && index < rawArgs.length) {
|
|
688
|
+
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
689
|
+
}
|
|
690
|
+
return { args, rawArgs };
|
|
691
|
+
}
|
|
692
|
+
#convertArgument(def, raw) {
|
|
693
|
+
if (def.coerce) {
|
|
694
|
+
try {
|
|
695
|
+
return def.coerce(raw);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (def.type === 'number') {
|
|
702
|
+
const n = Number(raw);
|
|
703
|
+
if (Number.isNaN(n)) {
|
|
704
|
+
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
705
|
+
}
|
|
706
|
+
return n;
|
|
707
|
+
}
|
|
708
|
+
return raw;
|
|
528
709
|
}
|
|
529
710
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
530
711
|
const optionByLong = new Map();
|
|
@@ -622,6 +803,100 @@ class Command {
|
|
|
622
803
|
#getCommandPath() {
|
|
623
804
|
return this.#name;
|
|
624
805
|
}
|
|
806
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
807
|
+
const token = tokens[idx];
|
|
808
|
+
const eqIdx = token.indexOf('=');
|
|
809
|
+
let optName;
|
|
810
|
+
let inlineValue;
|
|
811
|
+
if (eqIdx !== -1) {
|
|
812
|
+
optName = token.slice(2, eqIdx);
|
|
813
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
optName = token.slice(2);
|
|
817
|
+
}
|
|
818
|
+
const opt = optionByLong.get(optName);
|
|
819
|
+
if (!opt) {
|
|
820
|
+
return 0;
|
|
821
|
+
}
|
|
822
|
+
if (opt.type === 'boolean') {
|
|
823
|
+
if (inlineValue !== undefined) {
|
|
824
|
+
if (inlineValue === 'true') {
|
|
825
|
+
opts[optName] = true;
|
|
826
|
+
}
|
|
827
|
+
else if (inlineValue === 'false') {
|
|
828
|
+
opts[optName] = false;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
opts[optName] = true;
|
|
836
|
+
}
|
|
837
|
+
return 1;
|
|
838
|
+
}
|
|
839
|
+
let value;
|
|
840
|
+
let consumed = 1;
|
|
841
|
+
if (inlineValue !== undefined) {
|
|
842
|
+
value = inlineValue;
|
|
843
|
+
}
|
|
844
|
+
else if (idx + 1 < tokens.length) {
|
|
845
|
+
value = tokens[idx + 1];
|
|
846
|
+
consumed = 2;
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
850
|
+
}
|
|
851
|
+
this.#applyValue(opt, value, opts);
|
|
852
|
+
return consumed;
|
|
853
|
+
}
|
|
854
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
855
|
+
const token = tokens[idx];
|
|
856
|
+
if (token.includes('=')) {
|
|
857
|
+
const firstFlag = token[1];
|
|
858
|
+
if (!optionByShort.has(firstFlag)) {
|
|
859
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
860
|
+
}
|
|
861
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
862
|
+
}
|
|
863
|
+
const flags = token.slice(1);
|
|
864
|
+
let j = 0;
|
|
865
|
+
const consumedFlags = [];
|
|
866
|
+
const unconsumedFlags = [];
|
|
867
|
+
let nextIdx = idx + 1;
|
|
868
|
+
while (j < flags.length) {
|
|
869
|
+
const flag = flags[j];
|
|
870
|
+
const opt = optionByShort.get(flag);
|
|
871
|
+
if (!opt) {
|
|
872
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
consumedFlags.push(flag);
|
|
876
|
+
if (opt.type === 'boolean') {
|
|
877
|
+
opts[opt.long] = true;
|
|
878
|
+
j += 1;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (j < flags.length - 1) {
|
|
882
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
883
|
+
}
|
|
884
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
885
|
+
const value = tokens[idx + 1];
|
|
886
|
+
this.#applyValue(opt, value, opts);
|
|
887
|
+
nextIdx = idx + 2;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
891
|
+
}
|
|
892
|
+
j += 1;
|
|
893
|
+
}
|
|
894
|
+
if (consumedFlags.length > 0) {
|
|
895
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
896
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
897
|
+
}
|
|
898
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
899
|
+
}
|
|
625
900
|
}
|
|
626
901
|
|
|
627
902
|
class CompletionCommand extends Command {
|
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 {
|
|
@@ -91,8 +102,10 @@ interface IActionParams {
|
|
|
91
102
|
ctx: ICommandContext;
|
|
92
103
|
/** Parsed options */
|
|
93
104
|
opts: Record<string, unknown>;
|
|
94
|
-
/** Parsed positional arguments */
|
|
95
|
-
args: string
|
|
105
|
+
/** Parsed positional arguments (keyed by argument name) */
|
|
106
|
+
args: Record<string, unknown>;
|
|
107
|
+
/** Raw positional argument strings (before type conversion) */
|
|
108
|
+
rawArgs: string[];
|
|
96
109
|
}
|
|
97
110
|
/** Action handler function */
|
|
98
111
|
type IAction = (params: IActionParams) => void | Promise<void>;
|
|
@@ -109,11 +122,20 @@ interface IRunParams {
|
|
|
109
122
|
interface IParseResult {
|
|
110
123
|
/** Parsed options */
|
|
111
124
|
opts: Record<string, unknown>;
|
|
112
|
-
/** Parsed positional arguments */
|
|
113
|
-
args: string
|
|
125
|
+
/** Parsed positional arguments (keyed by argument name) */
|
|
126
|
+
args: Record<string, unknown>;
|
|
127
|
+
/** Raw positional argument strings (before type conversion) */
|
|
128
|
+
rawArgs: string[];
|
|
129
|
+
}
|
|
130
|
+
/** shift() method result */
|
|
131
|
+
interface IShiftResult {
|
|
132
|
+
/** Options consumed by this command */
|
|
133
|
+
opts: Record<string, unknown>;
|
|
134
|
+
/** Tokens not consumed, to be passed to parent */
|
|
135
|
+
remaining: string[];
|
|
114
136
|
}
|
|
115
137
|
/** Error kinds for command parsing */
|
|
116
|
-
type ICommanderErrorKind = 'UnknownOption' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
138
|
+
type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
117
139
|
/** Commander error with structured information */
|
|
118
140
|
declare class CommanderError extends Error {
|
|
119
141
|
readonly kind: ICommanderErrorKind;
|
|
@@ -177,6 +199,11 @@ declare class Command implements ICommand {
|
|
|
177
199
|
subcommand(name: string, cmd: Command): this;
|
|
178
200
|
run(params: IRunParams): Promise<void>;
|
|
179
201
|
parse(argv: string[]): IParseResult;
|
|
202
|
+
/**
|
|
203
|
+
* Shift options from tokens that this command recognizes.
|
|
204
|
+
* Unrecognized tokens are returned in `remaining` for parent commands.
|
|
205
|
+
*/
|
|
206
|
+
shift(tokens: string[]): IShiftResult;
|
|
180
207
|
formatHelp(): string;
|
|
181
208
|
getCompletionMeta(): ICompletionMeta;
|
|
182
209
|
}
|
|
@@ -227,4 +254,4 @@ declare class PwshCompletion {
|
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion };
|
|
230
|
-
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType };
|
|
257
|
+
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType, IShiftResult };
|