@guanghechen/commander 4.7.1 → 4.7.3
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 +14 -0
- package/lib/cjs/browser.cjs +410 -54
- package/lib/cjs/node.cjs +770 -88
- package/lib/esm/browser.mjs +410 -54
- package/lib/esm/node.mjs +770 -88
- package/lib/types/browser.d.ts +28 -6
- package/lib/types/node.d.ts +28 -6
- package/package.json +1 -1
package/lib/esm/browser.mjs
CHANGED
|
@@ -182,6 +182,140 @@ function kebabToCamelCase(str) {
|
|
|
182
182
|
function camelToKebabCase(str) {
|
|
183
183
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
184
184
|
}
|
|
185
|
+
const ANSI_ESCAPE_REGEX = new RegExp(String.raw `\\x1B\\[[0-?]*[ -/]*[@-~]`, 'g');
|
|
186
|
+
const DECIMAL_INTEGER_REGEX = /^\d(?:_?\d)*$/;
|
|
187
|
+
const DECIMAL_FRACTION_REGEX = /^\d(?:_?\d)*$/;
|
|
188
|
+
const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
|
|
189
|
+
const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
|
|
190
|
+
const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
|
|
191
|
+
const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
|
|
192
|
+
function stripAnsi(value) {
|
|
193
|
+
return value.replace(ANSI_ESCAPE_REGEX, '');
|
|
194
|
+
}
|
|
195
|
+
function isCombiningMark(codePoint) {
|
|
196
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
197
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
198
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
199
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
200
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
201
|
+
}
|
|
202
|
+
function isWideCodePoint(codePoint) {
|
|
203
|
+
if (codePoint < 0x1100) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return (codePoint <= 0x115f ||
|
|
207
|
+
codePoint === 0x2329 ||
|
|
208
|
+
codePoint === 0x232a ||
|
|
209
|
+
(codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
|
|
210
|
+
(codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
|
|
211
|
+
(codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
|
|
212
|
+
(codePoint >= 0xa960 && codePoint <= 0xa97c) ||
|
|
213
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
214
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
215
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
216
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
|
|
217
|
+
(codePoint >= 0xff01 && codePoint <= 0xff60) ||
|
|
218
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
219
|
+
(codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
|
|
220
|
+
(codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
|
|
221
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
222
|
+
}
|
|
223
|
+
function getDisplayWidth(value) {
|
|
224
|
+
const normalized = stripAnsi(value).normalize('NFC');
|
|
225
|
+
let width = 0;
|
|
226
|
+
for (const char of normalized) {
|
|
227
|
+
const codePoint = char.codePointAt(0);
|
|
228
|
+
if (codePoint === undefined || isCombiningMark(codePoint)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
width += isWideCodePoint(codePoint) ? 2 : 1;
|
|
232
|
+
}
|
|
233
|
+
return width;
|
|
234
|
+
}
|
|
235
|
+
function padDisplayEnd(value, targetWidth) {
|
|
236
|
+
const width = getDisplayWidth(value);
|
|
237
|
+
if (width >= targetWidth) {
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
return value + ' '.repeat(targetWidth - width);
|
|
241
|
+
}
|
|
242
|
+
function isValidPrimitiveNumberLiteral(rawValue) {
|
|
243
|
+
if (rawValue.trim() !== rawValue || rawValue.length === 0) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (rawValue === 'NaN' || rawValue === 'Infinity' || rawValue === '-Infinity') {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (BINARY_LITERAL_REGEX.test(rawValue)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (OCTAL_LITERAL_REGEX.test(rawValue)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (HEX_LITERAL_REGEX.test(rawValue)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const sign = rawValue[0] === '+' || rawValue[0] === '-' ? rawValue[0] : '';
|
|
259
|
+
const body = sign ? rawValue.slice(1) : rawValue;
|
|
260
|
+
if (body.length === 0) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const expIndex = body.search(/[eE]/);
|
|
264
|
+
const basePart = expIndex === -1 ? body : body.slice(0, expIndex);
|
|
265
|
+
const expPart = expIndex === -1 ? '' : body.slice(expIndex);
|
|
266
|
+
if (expPart && !DECIMAL_EXPONENT_REGEX.test(expPart)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (basePart.includes('.')) {
|
|
270
|
+
const decimalParts = basePart.split('.');
|
|
271
|
+
if (decimalParts.length !== 2) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const [intPart, fracPart] = decimalParts;
|
|
275
|
+
const intOk = intPart.length === 0 || DECIMAL_INTEGER_REGEX.test(intPart);
|
|
276
|
+
const fracOk = fracPart.length === 0 || DECIMAL_FRACTION_REGEX.test(fracPart);
|
|
277
|
+
if (!intOk || !fracOk) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return intPart.length > 0 || fracPart.length > 0;
|
|
281
|
+
}
|
|
282
|
+
return DECIMAL_INTEGER_REGEX.test(basePart);
|
|
283
|
+
}
|
|
284
|
+
function parsePrimitiveNumber(rawValue) {
|
|
285
|
+
if (!isValidPrimitiveNumberLiteral(rawValue)) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const normalized = rawValue.replaceAll('_', '');
|
|
289
|
+
const value = Number(normalized);
|
|
290
|
+
if (!Number.isFinite(value)) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function normalizeSubcommandNameForDistance(name) {
|
|
296
|
+
return camelToKebabCase(name).toLowerCase();
|
|
297
|
+
}
|
|
298
|
+
function levenshteinDistance(left, right) {
|
|
299
|
+
if (left === right) {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
if (left.length === 0) {
|
|
303
|
+
return right.length;
|
|
304
|
+
}
|
|
305
|
+
if (right.length === 0) {
|
|
306
|
+
return left.length;
|
|
307
|
+
}
|
|
308
|
+
let prev = Array.from({ length: right.length + 1 }, (_, i) => i);
|
|
309
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
310
|
+
const current = [i + 1];
|
|
311
|
+
for (let j = 0; j < right.length; j += 1) {
|
|
312
|
+
const substitutionCost = left[i] === right[j] ? 0 : 1;
|
|
313
|
+
current[j + 1] = Math.min(current[j] + 1, prev[j + 1] + 1, prev[j] + substitutionCost);
|
|
314
|
+
}
|
|
315
|
+
prev = current;
|
|
316
|
+
}
|
|
317
|
+
return prev[right.length];
|
|
318
|
+
}
|
|
185
319
|
function tokenizeLongOption(arg, commandPath) {
|
|
186
320
|
const eqIdx = arg.indexOf('=');
|
|
187
321
|
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
@@ -418,9 +552,13 @@ class Command {
|
|
|
418
552
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
419
553
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
420
554
|
}
|
|
555
|
+
const occupied = this.#subcommandsMap.get(name);
|
|
556
|
+
if (occupied && occupied !== cmd) {
|
|
557
|
+
throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
|
|
558
|
+
}
|
|
421
559
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
422
560
|
if (existing) {
|
|
423
|
-
if (existing.aliases.includes(name)) {
|
|
561
|
+
if (existing.name === name || existing.aliases.includes(name)) {
|
|
424
562
|
return this;
|
|
425
563
|
}
|
|
426
564
|
existing.aliases.push(name);
|
|
@@ -549,16 +687,41 @@ class Command {
|
|
|
549
687
|
else if (arg.kind === 'optional') {
|
|
550
688
|
usage += ` [${arg.name}]`;
|
|
551
689
|
}
|
|
690
|
+
else if (arg.kind === 'some') {
|
|
691
|
+
usage += ` <${arg.name}...>`;
|
|
692
|
+
}
|
|
552
693
|
else {
|
|
553
694
|
usage += ` [${arg.name}...]`;
|
|
554
695
|
}
|
|
555
696
|
}
|
|
697
|
+
const argumentsLines = [];
|
|
698
|
+
for (const arg of this.#arguments) {
|
|
699
|
+
const sig = arg.kind === 'required'
|
|
700
|
+
? `<${arg.name}>`
|
|
701
|
+
: arg.kind === 'optional'
|
|
702
|
+
? `[${arg.name}]`
|
|
703
|
+
: arg.kind === 'some'
|
|
704
|
+
? `<${arg.name}...>`
|
|
705
|
+
: `[${arg.name}...]`;
|
|
706
|
+
const metadata = [`[type: ${arg.type}]`];
|
|
707
|
+
if (arg.kind === 'optional' && arg.default !== undefined) {
|
|
708
|
+
metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
|
|
709
|
+
}
|
|
710
|
+
if (arg.choices && arg.choices.length > 0) {
|
|
711
|
+
metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
|
|
712
|
+
}
|
|
713
|
+
const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
|
|
714
|
+
argumentsLines.push({ sig, desc });
|
|
715
|
+
}
|
|
556
716
|
const options = [];
|
|
557
717
|
for (const opt of allOptions) {
|
|
558
718
|
const kebabLong = camelToKebabCase(opt.long);
|
|
559
719
|
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
560
720
|
sig += `--${kebabLong}`;
|
|
561
|
-
if (opt.args
|
|
721
|
+
if (opt.args === 'optional') {
|
|
722
|
+
sig += ' [value]';
|
|
723
|
+
}
|
|
724
|
+
else if (opt.args !== 'none') {
|
|
562
725
|
sig += ' <value>';
|
|
563
726
|
}
|
|
564
727
|
let desc = opt.desc;
|
|
@@ -566,7 +729,7 @@ class Command {
|
|
|
566
729
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
567
730
|
}
|
|
568
731
|
if (opt.choices) {
|
|
569
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
732
|
+
desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
|
|
570
733
|
}
|
|
571
734
|
options.push({ sig, desc });
|
|
572
735
|
if (opt.type === 'boolean' &&
|
|
@@ -598,6 +761,7 @@ class Command {
|
|
|
598
761
|
return {
|
|
599
762
|
desc: this.#desc,
|
|
600
763
|
usage,
|
|
764
|
+
arguments: argumentsLines,
|
|
601
765
|
options,
|
|
602
766
|
commands,
|
|
603
767
|
examples,
|
|
@@ -605,25 +769,29 @@ class Command {
|
|
|
605
769
|
}
|
|
606
770
|
#renderHelpPlain(helpData) {
|
|
607
771
|
const lines = [];
|
|
772
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
608
773
|
lines.push(helpData.desc);
|
|
609
774
|
lines.push('');
|
|
610
775
|
lines.push(helpData.usage);
|
|
611
776
|
lines.push('');
|
|
777
|
+
if (helpData.arguments.length > 0) {
|
|
778
|
+
lines.push('Arguments:');
|
|
779
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
780
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
781
|
+
}
|
|
782
|
+
lines.push('');
|
|
783
|
+
}
|
|
612
784
|
if (helpData.options.length > 0) {
|
|
613
785
|
lines.push('Options:');
|
|
614
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
615
786
|
for (const { sig, desc } of helpData.options) {
|
|
616
|
-
|
|
617
|
-
lines.push(` ${sig}${padding}${desc}`);
|
|
787
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
618
788
|
}
|
|
619
789
|
lines.push('');
|
|
620
790
|
}
|
|
621
791
|
if (helpData.commands.length > 0) {
|
|
622
792
|
lines.push('Commands:');
|
|
623
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
624
793
|
for (const { name, desc } of helpData.commands) {
|
|
625
|
-
|
|
626
|
-
lines.push(` ${name}${padding}${desc}`);
|
|
794
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
|
|
627
795
|
}
|
|
628
796
|
lines.push('');
|
|
629
797
|
}
|
|
@@ -640,25 +808,29 @@ class Command {
|
|
|
640
808
|
}
|
|
641
809
|
#renderHelpTerminal(helpData) {
|
|
642
810
|
const lines = [];
|
|
811
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
643
812
|
lines.push(helpData.desc);
|
|
644
813
|
lines.push('');
|
|
645
814
|
lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
|
|
646
815
|
lines.push('');
|
|
816
|
+
if (helpData.arguments.length > 0) {
|
|
817
|
+
lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
818
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
819
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
820
|
+
}
|
|
821
|
+
lines.push('');
|
|
822
|
+
}
|
|
647
823
|
if (helpData.options.length > 0) {
|
|
648
824
|
lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
649
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
650
825
|
for (const { sig, desc } of helpData.options) {
|
|
651
|
-
|
|
652
|
-
lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
826
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
653
827
|
}
|
|
654
828
|
lines.push('');
|
|
655
829
|
}
|
|
656
830
|
if (helpData.commands.length > 0) {
|
|
657
831
|
lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
658
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
659
832
|
for (const { name, desc } of helpData.commands) {
|
|
660
|
-
|
|
661
|
-
lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
833
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
662
834
|
}
|
|
663
835
|
lines.push('');
|
|
664
836
|
}
|
|
@@ -673,16 +845,41 @@ class Command {
|
|
|
673
845
|
}
|
|
674
846
|
return lines.join('\n');
|
|
675
847
|
}
|
|
848
|
+
#getHelpLabelWidth(helpData) {
|
|
849
|
+
const labels = [
|
|
850
|
+
...helpData.arguments.map(line => line.sig),
|
|
851
|
+
...helpData.options.map(line => line.sig),
|
|
852
|
+
...helpData.commands.map(line => line.name),
|
|
853
|
+
];
|
|
854
|
+
if (labels.length === 0) {
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
return Math.max(...labels.map(getDisplayWidth));
|
|
858
|
+
}
|
|
859
|
+
#renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
|
|
860
|
+
const paddedLabel = padDisplayEnd(label, labelWidth);
|
|
861
|
+
const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
|
|
862
|
+
return ` ${outputLabel} ${desc}`;
|
|
863
|
+
}
|
|
676
864
|
getCompletionMeta() {
|
|
677
865
|
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
678
866
|
const options = [];
|
|
867
|
+
const argumentsMeta = [];
|
|
679
868
|
for (const opt of allOptions) {
|
|
680
869
|
options.push({
|
|
681
870
|
long: opt.long,
|
|
682
871
|
short: opt.short,
|
|
683
872
|
desc: opt.desc,
|
|
684
873
|
takesValue: opt.args !== 'none',
|
|
685
|
-
choices: opt.choices,
|
|
874
|
+
choices: opt.choices?.map(choice => String(choice)),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
for (const arg of this.#arguments) {
|
|
878
|
+
argumentsMeta.push({
|
|
879
|
+
name: arg.name,
|
|
880
|
+
kind: arg.kind,
|
|
881
|
+
type: arg.type,
|
|
882
|
+
choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
|
|
686
883
|
});
|
|
687
884
|
}
|
|
688
885
|
return {
|
|
@@ -690,6 +887,7 @@ class Command {
|
|
|
690
887
|
desc: this.#desc,
|
|
691
888
|
aliases: [],
|
|
692
889
|
options,
|
|
890
|
+
arguments: argumentsMeta,
|
|
693
891
|
subcommands: this.#subcommandsList.map(entry => {
|
|
694
892
|
const subMeta = entry.command.getCompletionMeta();
|
|
695
893
|
return {
|
|
@@ -1142,6 +1340,14 @@ class Command {
|
|
|
1142
1340
|
consumed.push(tokens[i]);
|
|
1143
1341
|
}
|
|
1144
1342
|
}
|
|
1343
|
+
else if (opt.args === 'optional') {
|
|
1344
|
+
if (!token.resolved.includes('=') &&
|
|
1345
|
+
i + 1 < tokens.length &&
|
|
1346
|
+
tokens[i + 1].type === 'none') {
|
|
1347
|
+
i += 1;
|
|
1348
|
+
consumed.push(tokens[i]);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1145
1351
|
else if (opt.args === 'variadic') {
|
|
1146
1352
|
if (!token.resolved.includes('=')) {
|
|
1147
1353
|
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
@@ -1167,6 +1373,12 @@ class Command {
|
|
|
1167
1373
|
consumed.push(tokens[i]);
|
|
1168
1374
|
}
|
|
1169
1375
|
}
|
|
1376
|
+
else if (opt.args === 'optional') {
|
|
1377
|
+
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1378
|
+
i += 1;
|
|
1379
|
+
consumed.push(tokens[i]);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1170
1382
|
else if (opt.args === 'variadic') {
|
|
1171
1383
|
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1172
1384
|
i += 1;
|
|
@@ -1208,6 +1420,7 @@ class Command {
|
|
|
1208
1420
|
leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
|
|
1209
1421
|
}
|
|
1210
1422
|
}
|
|
1423
|
+
leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
|
|
1211
1424
|
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
1212
1425
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
1213
1426
|
const parseCtx = {
|
|
@@ -1290,6 +1503,23 @@ class Command {
|
|
|
1290
1503
|
i += 1;
|
|
1291
1504
|
continue;
|
|
1292
1505
|
}
|
|
1506
|
+
if (opt.args === 'optional') {
|
|
1507
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1508
|
+
if (eqIdx !== -1) {
|
|
1509
|
+
opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
|
|
1510
|
+
i += 1;
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
1514
|
+
opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
|
|
1515
|
+
i += 1;
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
opts[opt.long] = undefined;
|
|
1519
|
+
}
|
|
1520
|
+
i += 1;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1293
1523
|
if (opt.args === 'variadic') {
|
|
1294
1524
|
const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
|
|
1295
1525
|
const eqIdx = token.resolved.indexOf('=');
|
|
@@ -1309,7 +1539,7 @@ class Command {
|
|
|
1309
1539
|
i += 1;
|
|
1310
1540
|
}
|
|
1311
1541
|
for (const opt of allOptions) {
|
|
1312
|
-
if (opt.required && opts
|
|
1542
|
+
if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
|
|
1313
1543
|
throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
|
|
1314
1544
|
}
|
|
1315
1545
|
}
|
|
@@ -1335,8 +1565,8 @@ class Command {
|
|
|
1335
1565
|
return opt.coerce(rawValue);
|
|
1336
1566
|
}
|
|
1337
1567
|
if (opt.type === 'number') {
|
|
1338
|
-
const num =
|
|
1339
|
-
if (
|
|
1568
|
+
const num = parsePrimitiveNumber(rawValue);
|
|
1569
|
+
if (num === undefined) {
|
|
1340
1570
|
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase(opt.long)}"`, this.#getCommandPath());
|
|
1341
1571
|
}
|
|
1342
1572
|
return num;
|
|
@@ -1346,12 +1576,37 @@ class Command {
|
|
|
1346
1576
|
#parseArguments(rawArgs) {
|
|
1347
1577
|
const argumentDefs = this.#arguments;
|
|
1348
1578
|
const args = {};
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1579
|
+
if (argumentDefs.length === 0 && rawArgs.length > 0) {
|
|
1580
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
|
|
1581
|
+
}
|
|
1582
|
+
const missing = [];
|
|
1583
|
+
let remaining = rawArgs.length;
|
|
1584
|
+
for (const def of argumentDefs) {
|
|
1585
|
+
if (def.kind === 'required') {
|
|
1586
|
+
if (remaining === 0) {
|
|
1587
|
+
missing.push(def.name);
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
remaining -= 1;
|
|
1591
|
+
}
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (def.kind === 'optional') {
|
|
1595
|
+
if (remaining > 0) {
|
|
1596
|
+
remaining -= 1;
|
|
1597
|
+
}
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (def.kind === 'some') {
|
|
1601
|
+
if (remaining === 0) {
|
|
1602
|
+
missing.push(def.name);
|
|
1603
|
+
}
|
|
1604
|
+
remaining = 0;
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
remaining = 0;
|
|
1608
|
+
}
|
|
1609
|
+
if (missing.length > 0) {
|
|
1355
1610
|
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
1356
1611
|
}
|
|
1357
1612
|
let index = 0;
|
|
@@ -1362,41 +1617,101 @@ class Command {
|
|
|
1362
1617
|
index = rawArgs.length;
|
|
1363
1618
|
break;
|
|
1364
1619
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1620
|
+
if (def.kind === 'some') {
|
|
1621
|
+
const rest = rawArgs.slice(index);
|
|
1622
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
1623
|
+
index = rawArgs.length;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
if (def.kind === 'optional') {
|
|
1627
|
+
const raw = rawArgs[index];
|
|
1628
|
+
if (raw === undefined) {
|
|
1368
1629
|
args[def.name] = def.default ?? undefined;
|
|
1369
1630
|
continue;
|
|
1370
1631
|
}
|
|
1371
|
-
}
|
|
1372
|
-
else {
|
|
1373
1632
|
args[def.name] = this.#convertArgument(def, raw);
|
|
1374
1633
|
index += 1;
|
|
1634
|
+
continue;
|
|
1375
1635
|
}
|
|
1636
|
+
const raw = rawArgs[index];
|
|
1637
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
1638
|
+
index += 1;
|
|
1376
1639
|
}
|
|
1377
|
-
const
|
|
1378
|
-
if (!
|
|
1640
|
+
const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
|
|
1641
|
+
if (!hasRestArgument && index < rawArgs.length) {
|
|
1379
1642
|
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
1380
1643
|
}
|
|
1381
1644
|
return { args, rawArgs };
|
|
1382
1645
|
}
|
|
1383
1646
|
#convertArgument(def, raw) {
|
|
1647
|
+
let value;
|
|
1384
1648
|
if (def.coerce) {
|
|
1385
1649
|
try {
|
|
1386
|
-
|
|
1650
|
+
value = def.coerce(raw);
|
|
1387
1651
|
}
|
|
1388
1652
|
catch {
|
|
1389
1653
|
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
1390
1654
|
}
|
|
1391
1655
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1656
|
+
else {
|
|
1657
|
+
value = raw;
|
|
1658
|
+
}
|
|
1659
|
+
if (typeof value !== 'string') {
|
|
1660
|
+
throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
|
|
1661
|
+
}
|
|
1662
|
+
if (def.type === 'choice') {
|
|
1663
|
+
const choices = def.choices ?? [];
|
|
1664
|
+
if (!choices.includes(value)) {
|
|
1665
|
+
throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
|
|
1666
|
+
.map(choice => JSON.stringify(choice))
|
|
1667
|
+
.join(', ')}`, this.#getCommandPath());
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return value;
|
|
1671
|
+
}
|
|
1672
|
+
#assertUnknownSubcommand(userTailArgv) {
|
|
1673
|
+
if (this.#subcommandsList.length === 0) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const token = userTailArgv[0];
|
|
1677
|
+
if (token === undefined || token.startsWith('-') || token === 'help') {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (this.#findSubcommandEntry(token) !== undefined) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const hints = [];
|
|
1684
|
+
if (this.#arguments.length === 0) {
|
|
1685
|
+
hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
|
|
1686
|
+
}
|
|
1687
|
+
const candidate = this.#resolveDidYouMeanSubcommandName(token);
|
|
1688
|
+
if (candidate !== undefined) {
|
|
1689
|
+
hints.push(`Hint: did you mean "${candidate}"?`);
|
|
1690
|
+
}
|
|
1691
|
+
const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
|
|
1692
|
+
throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
|
|
1693
|
+
}
|
|
1694
|
+
#resolveDidYouMeanSubcommandName(token) {
|
|
1695
|
+
const source = normalizeSubcommandNameForDistance(token);
|
|
1696
|
+
let minDistance = Number.POSITIVE_INFINITY;
|
|
1697
|
+
let bestName;
|
|
1698
|
+
let isUniqueBest = false;
|
|
1699
|
+
for (const entry of this.#subcommandsList) {
|
|
1700
|
+
const target = normalizeSubcommandNameForDistance(entry.name);
|
|
1701
|
+
const distance = levenshteinDistance(source, target);
|
|
1702
|
+
if (distance < minDistance) {
|
|
1703
|
+
minDistance = distance;
|
|
1704
|
+
bestName = entry.name;
|
|
1705
|
+
isUniqueBest = true;
|
|
1706
|
+
}
|
|
1707
|
+
else if (distance === minDistance) {
|
|
1708
|
+
isUniqueBest = false;
|
|
1396
1709
|
}
|
|
1397
|
-
return n;
|
|
1398
1710
|
}
|
|
1399
|
-
|
|
1711
|
+
if (minDistance <= 2 && isUniqueBest) {
|
|
1712
|
+
return bestName;
|
|
1713
|
+
}
|
|
1714
|
+
return undefined;
|
|
1400
1715
|
}
|
|
1401
1716
|
#hasUserOption(long) {
|
|
1402
1717
|
return this.#options.some(option => option.long === long);
|
|
@@ -1441,11 +1756,9 @@ class Command {
|
|
|
1441
1756
|
return optionPolicyMap;
|
|
1442
1757
|
}
|
|
1443
1758
|
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1444
|
-
const policy = optionPolicyMap.get(cmd);
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
1759
|
+
const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
|
|
1760
|
+
optionPolicyMap.set(cmd, policy);
|
|
1761
|
+
return policy;
|
|
1449
1762
|
}
|
|
1450
1763
|
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
1451
1764
|
const mergedByLong = new Map();
|
|
@@ -1474,7 +1787,10 @@ class Command {
|
|
|
1474
1787
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
1475
1788
|
}
|
|
1476
1789
|
if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
|
|
1477
|
-
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
|
|
1790
|
+
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
|
|
1791
|
+
}
|
|
1792
|
+
if (opt.type === 'number' && opt.args === 'optional') {
|
|
1793
|
+
throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
|
|
1478
1794
|
}
|
|
1479
1795
|
if (opt.long.startsWith('no')) {
|
|
1480
1796
|
throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
|
|
@@ -1482,12 +1798,18 @@ class Command {
|
|
|
1482
1798
|
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
1483
1799
|
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
1484
1800
|
}
|
|
1801
|
+
if (opt.short !== undefined && opt.short.length !== 1) {
|
|
1802
|
+
throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
|
|
1803
|
+
}
|
|
1485
1804
|
if (opt.required && opt.default !== undefined) {
|
|
1486
1805
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
1487
1806
|
}
|
|
1488
1807
|
if (opt.type === 'boolean' && opt.required) {
|
|
1489
1808
|
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
|
|
1490
1809
|
}
|
|
1810
|
+
if (opt.required && opt.args !== 'required') {
|
|
1811
|
+
throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
|
|
1812
|
+
}
|
|
1491
1813
|
}
|
|
1492
1814
|
#checkOptionUniqueness(opt) {
|
|
1493
1815
|
if (this.#options.some(o => o.long === opt.long)) {
|
|
@@ -1498,24 +1820,58 @@ class Command {
|
|
|
1498
1820
|
}
|
|
1499
1821
|
}
|
|
1500
1822
|
#validateArgumentConfig(arg) {
|
|
1501
|
-
if (arg.kind
|
|
1502
|
-
|
|
1823
|
+
if (arg.kind !== 'required' &&
|
|
1824
|
+
arg.kind !== 'optional' &&
|
|
1825
|
+
arg.kind !== 'variadic' &&
|
|
1826
|
+
arg.kind !== 'some') {
|
|
1827
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
|
|
1828
|
+
}
|
|
1829
|
+
if (arg.type !== 'string' && arg.type !== 'choice') {
|
|
1830
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
|
|
1831
|
+
}
|
|
1832
|
+
if (arg.default !== undefined && arg.kind !== 'optional') {
|
|
1833
|
+
throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
|
|
1834
|
+
}
|
|
1835
|
+
if (arg.type === 'string' && arg.choices !== undefined) {
|
|
1836
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
|
|
1503
1837
|
}
|
|
1504
|
-
if (arg.
|
|
1505
|
-
if (
|
|
1506
|
-
throw new CommanderError('ConfigurationError',
|
|
1838
|
+
if (arg.type === 'choice') {
|
|
1839
|
+
if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
|
|
1840
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
|
|
1841
|
+
}
|
|
1842
|
+
if (arg.choices.some(choice => typeof choice !== 'string')) {
|
|
1843
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (arg.default !== undefined) {
|
|
1847
|
+
this.#validateArgumentDefaultValue(arg);
|
|
1848
|
+
}
|
|
1849
|
+
if (arg.kind === 'variadic' || arg.kind === 'some') {
|
|
1850
|
+
if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
|
|
1851
|
+
throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
|
|
1507
1852
|
}
|
|
1508
1853
|
}
|
|
1509
1854
|
if (this.#arguments.length > 0) {
|
|
1510
1855
|
const last = this.#arguments[this.#arguments.length - 1];
|
|
1511
|
-
if (last.kind === 'variadic') {
|
|
1512
|
-
throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
|
|
1856
|
+
if (last.kind === 'variadic' || last.kind === 'some') {
|
|
1857
|
+
throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
|
|
1513
1858
|
}
|
|
1514
1859
|
}
|
|
1515
1860
|
if (arg.kind === 'required') {
|
|
1516
|
-
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
|
|
1861
|
+
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
|
|
1517
1862
|
if (hasOptional) {
|
|
1518
|
-
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
|
|
1863
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
#validateArgumentDefaultValue(arg) {
|
|
1868
|
+
if (typeof arg.default !== 'string') {
|
|
1869
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
|
|
1870
|
+
}
|
|
1871
|
+
if (arg.type === 'choice') {
|
|
1872
|
+
const choices = arg.choices ?? [];
|
|
1873
|
+
if (!choices.includes(arg.default)) {
|
|
1874
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
|
|
1519
1875
|
}
|
|
1520
1876
|
}
|
|
1521
1877
|
}
|