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