@guanghechen/commander 4.7.1 → 4.7.2

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/lib/esm/node.mjs CHANGED
@@ -195,6 +195,116 @@ function kebabToCamelCase(str) {
195
195
  function camelToKebabCase$1(str) {
196
196
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
197
197
  }
198
+ const ANSI_ESCAPE_REGEX = new RegExp(String.raw `\\x1B\\[[0-?]*[ -/]*[@-~]`, 'g');
199
+ const DECIMAL_INTEGER_REGEX = /^\d(?:_?\d)*$/;
200
+ const DECIMAL_FRACTION_REGEX = /^\d(?:_?\d)*$/;
201
+ const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
202
+ const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
203
+ const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
204
+ const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
205
+ function stripAnsi(value) {
206
+ return value.replace(ANSI_ESCAPE_REGEX, '');
207
+ }
208
+ function isCombiningMark(codePoint) {
209
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
210
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
211
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
212
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
213
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
214
+ }
215
+ function isWideCodePoint(codePoint) {
216
+ if (codePoint < 0x1100) {
217
+ return false;
218
+ }
219
+ return (codePoint <= 0x115f ||
220
+ codePoint === 0x2329 ||
221
+ codePoint === 0x232a ||
222
+ (codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
223
+ (codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
224
+ (codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
225
+ (codePoint >= 0xa960 && codePoint <= 0xa97c) ||
226
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
227
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
228
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
229
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
230
+ (codePoint >= 0xff01 && codePoint <= 0xff60) ||
231
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
232
+ (codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
233
+ (codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
234
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
235
+ }
236
+ function getDisplayWidth(value) {
237
+ const normalized = stripAnsi(value).normalize('NFC');
238
+ let width = 0;
239
+ for (const char of normalized) {
240
+ const codePoint = char.codePointAt(0);
241
+ if (codePoint === undefined || isCombiningMark(codePoint)) {
242
+ continue;
243
+ }
244
+ width += isWideCodePoint(codePoint) ? 2 : 1;
245
+ }
246
+ return width;
247
+ }
248
+ function padDisplayEnd(value, targetWidth) {
249
+ const width = getDisplayWidth(value);
250
+ if (width >= targetWidth) {
251
+ return value;
252
+ }
253
+ return value + ' '.repeat(targetWidth - width);
254
+ }
255
+ function isValidPrimitiveNumberLiteral(rawValue) {
256
+ if (rawValue.trim() !== rawValue || rawValue.length === 0) {
257
+ return false;
258
+ }
259
+ if (rawValue === 'NaN' || rawValue === 'Infinity' || rawValue === '-Infinity') {
260
+ return false;
261
+ }
262
+ if (BINARY_LITERAL_REGEX.test(rawValue)) {
263
+ return true;
264
+ }
265
+ if (OCTAL_LITERAL_REGEX.test(rawValue)) {
266
+ return true;
267
+ }
268
+ if (HEX_LITERAL_REGEX.test(rawValue)) {
269
+ return true;
270
+ }
271
+ const sign = rawValue[0] === '+' || rawValue[0] === '-' ? rawValue[0] : '';
272
+ const body = sign ? rawValue.slice(1) : rawValue;
273
+ if (body.length === 0) {
274
+ return false;
275
+ }
276
+ const expIndex = body.search(/[eE]/);
277
+ const basePart = expIndex === -1 ? body : body.slice(0, expIndex);
278
+ const expPart = expIndex === -1 ? '' : body.slice(expIndex);
279
+ if (expPart && !DECIMAL_EXPONENT_REGEX.test(expPart)) {
280
+ return false;
281
+ }
282
+ if (basePart.includes('.')) {
283
+ const decimalParts = basePart.split('.');
284
+ if (decimalParts.length !== 2) {
285
+ return false;
286
+ }
287
+ const [intPart, fracPart] = decimalParts;
288
+ const intOk = intPart.length === 0 || DECIMAL_INTEGER_REGEX.test(intPart);
289
+ const fracOk = fracPart.length === 0 || DECIMAL_FRACTION_REGEX.test(fracPart);
290
+ if (!intOk || !fracOk) {
291
+ return false;
292
+ }
293
+ return intPart.length > 0 || fracPart.length > 0;
294
+ }
295
+ return DECIMAL_INTEGER_REGEX.test(basePart);
296
+ }
297
+ function parsePrimitiveNumber(rawValue) {
298
+ if (!isValidPrimitiveNumberLiteral(rawValue)) {
299
+ return undefined;
300
+ }
301
+ const normalized = rawValue.replaceAll('_', '');
302
+ const value = Number(normalized);
303
+ if (!Number.isFinite(value)) {
304
+ return undefined;
305
+ }
306
+ return value;
307
+ }
198
308
  function tokenizeLongOption(arg, commandPath) {
199
309
  const eqIdx = arg.indexOf('=');
200
310
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -431,9 +541,13 @@ class Command {
431
541
  if (cmd.#parent && cmd.#parent !== this) {
432
542
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
433
543
  }
544
+ const occupied = this.#subcommandsMap.get(name);
545
+ if (occupied && occupied !== cmd) {
546
+ throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
547
+ }
434
548
  const existing = this.#subcommandsList.find(e => e.command === cmd);
435
549
  if (existing) {
436
- if (existing.aliases.includes(name)) {
550
+ if (existing.name === name || existing.aliases.includes(name)) {
437
551
  return this;
438
552
  }
439
553
  existing.aliases.push(name);
@@ -562,10 +676,32 @@ class Command {
562
676
  else if (arg.kind === 'optional') {
563
677
  usage += ` [${arg.name}]`;
564
678
  }
679
+ else if (arg.kind === 'some') {
680
+ usage += ` <${arg.name}...>`;
681
+ }
565
682
  else {
566
683
  usage += ` [${arg.name}...]`;
567
684
  }
568
685
  }
686
+ const argumentsLines = [];
687
+ for (const arg of this.#arguments) {
688
+ const sig = arg.kind === 'required'
689
+ ? `<${arg.name}>`
690
+ : arg.kind === 'optional'
691
+ ? `[${arg.name}]`
692
+ : arg.kind === 'some'
693
+ ? `<${arg.name}...>`
694
+ : `[${arg.name}...]`;
695
+ const metadata = [`[type: ${arg.type}]`];
696
+ if (arg.kind === 'optional' && arg.default !== undefined) {
697
+ metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
698
+ }
699
+ if (arg.choices && arg.choices.length > 0) {
700
+ metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
701
+ }
702
+ const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
703
+ argumentsLines.push({ sig, desc });
704
+ }
569
705
  const options = [];
570
706
  for (const opt of allOptions) {
571
707
  const kebabLong = camelToKebabCase$1(opt.long);
@@ -579,7 +715,7 @@ class Command {
579
715
  desc += ` (default: ${JSON.stringify(opt.default)})`;
580
716
  }
581
717
  if (opt.choices) {
582
- desc += ` [choices: ${opt.choices.join(', ')}]`;
718
+ desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
583
719
  }
584
720
  options.push({ sig, desc });
585
721
  if (opt.type === 'boolean' &&
@@ -611,6 +747,7 @@ class Command {
611
747
  return {
612
748
  desc: this.#desc,
613
749
  usage,
750
+ arguments: argumentsLines,
614
751
  options,
615
752
  commands,
616
753
  examples,
@@ -618,25 +755,29 @@ class Command {
618
755
  }
619
756
  #renderHelpPlain(helpData) {
620
757
  const lines = [];
758
+ const labelWidth = this.#getHelpLabelWidth(helpData);
621
759
  lines.push(helpData.desc);
622
760
  lines.push('');
623
761
  lines.push(helpData.usage);
624
762
  lines.push('');
763
+ if (helpData.arguments.length > 0) {
764
+ lines.push('Arguments:');
765
+ for (const { sig, desc } of helpData.arguments) {
766
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
767
+ }
768
+ lines.push('');
769
+ }
625
770
  if (helpData.options.length > 0) {
626
771
  lines.push('Options:');
627
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
628
772
  for (const { sig, desc } of helpData.options) {
629
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
630
- lines.push(` ${sig}${padding}${desc}`);
773
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
631
774
  }
632
775
  lines.push('');
633
776
  }
634
777
  if (helpData.commands.length > 0) {
635
778
  lines.push('Commands:');
636
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
637
779
  for (const { name, desc } of helpData.commands) {
638
- const padding = ' '.repeat(maxNameLen - name.length + 2);
639
- lines.push(` ${name}${padding}${desc}`);
780
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
640
781
  }
641
782
  lines.push('');
642
783
  }
@@ -653,25 +794,29 @@ class Command {
653
794
  }
654
795
  #renderHelpTerminal(helpData) {
655
796
  const lines = [];
797
+ const labelWidth = this.#getHelpLabelWidth(helpData);
656
798
  lines.push(helpData.desc);
657
799
  lines.push('');
658
800
  lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
659
801
  lines.push('');
802
+ if (helpData.arguments.length > 0) {
803
+ lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
804
+ for (const { sig, desc } of helpData.arguments) {
805
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
806
+ }
807
+ lines.push('');
808
+ }
660
809
  if (helpData.options.length > 0) {
661
810
  lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
662
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
663
811
  for (const { sig, desc } of helpData.options) {
664
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
665
- lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
812
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
666
813
  }
667
814
  lines.push('');
668
815
  }
669
816
  if (helpData.commands.length > 0) {
670
817
  lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
671
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
672
818
  for (const { name, desc } of helpData.commands) {
673
- const padding = ' '.repeat(maxNameLen - name.length + 2);
674
- lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
819
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
675
820
  }
676
821
  lines.push('');
677
822
  }
@@ -686,16 +831,41 @@ class Command {
686
831
  }
687
832
  return lines.join('\n');
688
833
  }
834
+ #getHelpLabelWidth(helpData) {
835
+ const labels = [
836
+ ...helpData.arguments.map(line => line.sig),
837
+ ...helpData.options.map(line => line.sig),
838
+ ...helpData.commands.map(line => line.name),
839
+ ];
840
+ if (labels.length === 0) {
841
+ return 0;
842
+ }
843
+ return Math.max(...labels.map(getDisplayWidth));
844
+ }
845
+ #renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
846
+ const paddedLabel = padDisplayEnd(label, labelWidth);
847
+ const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
848
+ return ` ${outputLabel} ${desc}`;
849
+ }
689
850
  getCompletionMeta() {
690
851
  const allOptions = this.#resolveOptionPolicy().mergedOptions;
691
852
  const options = [];
853
+ const argumentsMeta = [];
692
854
  for (const opt of allOptions) {
693
855
  options.push({
694
856
  long: opt.long,
695
857
  short: opt.short,
696
858
  desc: opt.desc,
697
859
  takesValue: opt.args !== 'none',
698
- choices: opt.choices,
860
+ choices: opt.choices?.map(choice => String(choice)),
861
+ });
862
+ }
863
+ for (const arg of this.#arguments) {
864
+ argumentsMeta.push({
865
+ name: arg.name,
866
+ kind: arg.kind,
867
+ type: arg.type,
868
+ choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
699
869
  });
700
870
  }
701
871
  return {
@@ -703,6 +873,7 @@ class Command {
703
873
  desc: this.#desc,
704
874
  aliases: [],
705
875
  options,
876
+ arguments: argumentsMeta,
706
877
  subcommands: this.#subcommandsList.map(entry => {
707
878
  const subMeta = entry.command.getCompletionMeta();
708
879
  return {
@@ -1348,8 +1519,8 @@ class Command {
1348
1519
  return opt.coerce(rawValue);
1349
1520
  }
1350
1521
  if (opt.type === 'number') {
1351
- const num = Number(rawValue);
1352
- if (Number.isNaN(num)) {
1522
+ const num = parsePrimitiveNumber(rawValue);
1523
+ if (num === undefined) {
1353
1524
  throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
1354
1525
  }
1355
1526
  return num;
@@ -1359,12 +1530,34 @@ class Command {
1359
1530
  #parseArguments(rawArgs) {
1360
1531
  const argumentDefs = this.#arguments;
1361
1532
  const args = {};
1362
- const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
1363
- if (rawArgs.length < requiredCount) {
1364
- const missing = argumentDefs
1365
- .filter(a => a.kind === 'required')
1366
- .slice(rawArgs.length)
1367
- .map(a => a.name);
1533
+ const missing = [];
1534
+ let remaining = rawArgs.length;
1535
+ for (const def of argumentDefs) {
1536
+ if (def.kind === 'required') {
1537
+ if (remaining === 0) {
1538
+ missing.push(def.name);
1539
+ }
1540
+ else {
1541
+ remaining -= 1;
1542
+ }
1543
+ continue;
1544
+ }
1545
+ if (def.kind === 'optional') {
1546
+ if (remaining > 0) {
1547
+ remaining -= 1;
1548
+ }
1549
+ continue;
1550
+ }
1551
+ if (def.kind === 'some') {
1552
+ if (remaining === 0) {
1553
+ missing.push(def.name);
1554
+ }
1555
+ remaining = 0;
1556
+ continue;
1557
+ }
1558
+ remaining = 0;
1559
+ }
1560
+ if (missing.length > 0) {
1368
1561
  throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
1369
1562
  }
1370
1563
  let index = 0;
@@ -1375,41 +1568,59 @@ class Command {
1375
1568
  index = rawArgs.length;
1376
1569
  break;
1377
1570
  }
1571
+ if (def.kind === 'some') {
1572
+ const rest = rawArgs.slice(index);
1573
+ if (rest.length === 0) {
1574
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1575
+ }
1576
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1577
+ index = rawArgs.length;
1578
+ break;
1579
+ }
1378
1580
  const raw = rawArgs[index];
1379
1581
  if (raw === undefined) {
1380
1582
  if (def.kind === 'optional') {
1381
1583
  args[def.name] = def.default ?? undefined;
1382
1584
  continue;
1383
1585
  }
1586
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
1384
1587
  }
1385
1588
  else {
1386
1589
  args[def.name] = this.#convertArgument(def, raw);
1387
1590
  index += 1;
1388
1591
  }
1389
1592
  }
1390
- const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
1391
- if (!hasVariadic && index < rawArgs.length) {
1593
+ const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1594
+ if (!hasRestArgument && index < rawArgs.length) {
1392
1595
  throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
1393
1596
  }
1394
1597
  return { args, rawArgs };
1395
1598
  }
1396
1599
  #convertArgument(def, raw) {
1600
+ let value;
1397
1601
  if (def.coerce) {
1398
1602
  try {
1399
- return def.coerce(raw);
1603
+ value = def.coerce(raw);
1400
1604
  }
1401
1605
  catch {
1402
1606
  throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1403
1607
  }
1404
1608
  }
1405
- if (def.type === 'number') {
1406
- const n = Number(raw);
1407
- if (Number.isNaN(n)) {
1408
- throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1609
+ else {
1610
+ value = raw;
1611
+ }
1612
+ if (typeof value !== 'string') {
1613
+ throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
1614
+ }
1615
+ if (def.type === 'choice') {
1616
+ const choices = def.choices ?? [];
1617
+ if (!choices.includes(value)) {
1618
+ throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
1619
+ .map(choice => JSON.stringify(choice))
1620
+ .join(', ')}`, this.#getCommandPath());
1409
1621
  }
1410
- return n;
1411
1622
  }
1412
- return raw;
1623
+ return value;
1413
1624
  }
1414
1625
  #hasUserOption(long) {
1415
1626
  return this.#options.some(option => option.long === long);
@@ -1495,6 +1706,9 @@ class Command {
1495
1706
  if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
1496
1707
  throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
1497
1708
  }
1709
+ if (opt.short !== undefined && opt.short.length !== 1) {
1710
+ throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
1711
+ }
1498
1712
  if (opt.required && opt.default !== undefined) {
1499
1713
  throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
1500
1714
  }
@@ -1511,24 +1725,58 @@ class Command {
1511
1725
  }
1512
1726
  }
1513
1727
  #validateArgumentConfig(arg) {
1514
- if (arg.kind === 'required' && arg.default !== undefined) {
1515
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
1728
+ if (arg.kind !== 'required' &&
1729
+ arg.kind !== 'optional' &&
1730
+ arg.kind !== 'variadic' &&
1731
+ arg.kind !== 'some') {
1732
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
1733
+ }
1734
+ if (arg.type !== 'string' && arg.type !== 'choice') {
1735
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
1736
+ }
1737
+ if (arg.default !== undefined && arg.kind !== 'optional') {
1738
+ throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
1739
+ }
1740
+ if (arg.type === 'string' && arg.choices !== undefined) {
1741
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
1742
+ }
1743
+ if (arg.type === 'choice') {
1744
+ if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
1745
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
1746
+ }
1747
+ if (arg.choices.some(choice => typeof choice !== 'string')) {
1748
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
1749
+ }
1750
+ }
1751
+ if (arg.default !== undefined) {
1752
+ this.#validateArgumentDefaultValue(arg);
1516
1753
  }
1517
- if (arg.kind === 'variadic') {
1518
- if (this.#arguments.some(a => a.kind === 'variadic')) {
1519
- throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
1754
+ if (arg.kind === 'variadic' || arg.kind === 'some') {
1755
+ if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
1756
+ throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
1520
1757
  }
1521
1758
  }
1522
1759
  if (this.#arguments.length > 0) {
1523
1760
  const last = this.#arguments[this.#arguments.length - 1];
1524
- if (last.kind === 'variadic') {
1525
- throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
1761
+ if (last.kind === 'variadic' || last.kind === 'some') {
1762
+ throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
1526
1763
  }
1527
1764
  }
1528
1765
  if (arg.kind === 'required') {
1529
- const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
1766
+ const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
1530
1767
  if (hasOptional) {
1531
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
1768
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
1769
+ }
1770
+ }
1771
+ }
1772
+ #validateArgumentDefaultValue(arg) {
1773
+ if (typeof arg.default !== 'string') {
1774
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
1775
+ }
1776
+ if (arg.type === 'choice') {
1777
+ const choices = arg.choices ?? [];
1778
+ if (!choices.includes(arg.default)) {
1779
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
1532
1780
  }
1533
1781
  }
1534
1782
  }
@@ -1885,6 +2133,7 @@ class BashCompletion {
1885
2133
  '',
1886
2134
  `${funcName}() {`,
1887
2135
  ' local cur prev words cword',
2136
+ ' local opts arg_choices prefer_value_choices',
1888
2137
  ' _init_completion || return',
1889
2138
  '',
1890
2139
  ...this.#generateCommandCase(this.#meta, 1),
@@ -1904,13 +2153,15 @@ class BashCompletion {
1904
2153
  for (const opt of cmd.options) {
1905
2154
  const kebabLong = camelToKebabCase(opt.long);
1906
2155
  if (opt.short)
1907
- optParts.push(`-${opt.short}`);
1908
- optParts.push(`--${kebabLong}`);
2156
+ optParts.push(this.#escapeWord(`-${opt.short}`));
2157
+ optParts.push(this.#escapeWord(`--${kebabLong}`));
1909
2158
  if (!opt.takesValue) {
1910
- optParts.push(`--no-${kebabLong}`);
2159
+ optParts.push(this.#escapeWord(`--no-${kebabLong}`));
1911
2160
  }
1912
2161
  }
1913
- const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
2162
+ const subParts = cmd.subcommands
2163
+ .flatMap(sub => [sub.name, ...sub.aliases])
2164
+ .map(value => this.#escapeWord(value));
1914
2165
  const allOpts = [...optParts, ...subParts].join(' ');
1915
2166
  if (cmd.subcommands.length > 0) {
1916
2167
  lines.push(`${indent}case "\${words[${depth}]}" in`);
@@ -1922,14 +2173,105 @@ class BashCompletion {
1922
2173
  }
1923
2174
  lines.push(`${indent} *)`);
1924
2175
  lines.push(`${indent} opts="${allOpts}"`);
2176
+ this.#appendChoiceLogicForCommand(lines, `${indent} `, cmd, depth);
1925
2177
  lines.push(`${indent} ;;`);
1926
2178
  lines.push(`${indent}esac`);
1927
2179
  }
1928
2180
  else {
1929
2181
  lines.push(`${indent}opts="${allOpts}"`);
2182
+ this.#appendChoiceLogicForCommand(lines, indent, cmd, depth);
1930
2183
  }
1931
2184
  return lines;
1932
2185
  }
2186
+ #serializeWordList(words) {
2187
+ return words.map(choice => this.#escapeWord(choice)).join(' ');
2188
+ }
2189
+ #appendChoiceLogicForCommand(lines, indent, cmd, depth) {
2190
+ const valueOptions = cmd.options.filter(opt => opt.takesValue);
2191
+ const valueOptionsWithChoices = valueOptions.filter(opt => opt.choices && opt.choices.length > 0);
2192
+ const valueLongPatterns = valueOptions.map(opt => `--${camelToKebabCase(opt.long)}`);
2193
+ const valueShortPatterns = valueOptions
2194
+ .map(opt => opt.short)
2195
+ .filter((short) => typeof short === 'string');
2196
+ lines.push(`${indent}prefer_value_choices=0`);
2197
+ if (valueOptionsWithChoices.length > 0) {
2198
+ lines.push(`${indent}if [[ "$cur" != -* ]]; then`);
2199
+ lines.push(`${indent} case "$prev" in`);
2200
+ for (const opt of valueOptionsWithChoices) {
2201
+ const patterns = [`--${camelToKebabCase(opt.long)}`];
2202
+ if (opt.short) {
2203
+ patterns.push(`-${opt.short}`);
2204
+ }
2205
+ lines.push(`${indent} ${patterns.join('|')})`);
2206
+ lines.push(`${indent} opts="${this.#serializeWordList(opt.choices ?? [])}"`);
2207
+ lines.push(`${indent} prefer_value_choices=1`);
2208
+ lines.push(`${indent} ;;`);
2209
+ }
2210
+ lines.push(`${indent} esac`);
2211
+ lines.push(`${indent}fi`);
2212
+ }
2213
+ lines.push(`${indent}if [[ $prefer_value_choices -eq 0 ]]; then`);
2214
+ lines.push(`${indent} positional_count=0`);
2215
+ lines.push(`${indent} expect_value=0`);
2216
+ lines.push(`${indent} for ((idx=${depth}; idx<cword; idx++)); do`);
2217
+ lines.push(`${indent} token="\${words[idx]}"`);
2218
+ lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
2219
+ lines.push(`${indent} expect_value=0`);
2220
+ lines.push(`${indent} continue`);
2221
+ lines.push(`${indent} fi`);
2222
+ lines.push(`${indent} if [[ "$token" == --* ]]; then`);
2223
+ lines.push(`${indent} if [[ "$token" == *=* ]]; then`);
2224
+ lines.push(`${indent} continue`);
2225
+ lines.push(`${indent} fi`);
2226
+ if (valueLongPatterns.length > 0) {
2227
+ lines.push(`${indent} case "$token" in`);
2228
+ lines.push(`${indent} ${valueLongPatterns.join('|')}) expect_value=1 ;;`);
2229
+ lines.push(`${indent} esac`);
2230
+ }
2231
+ lines.push(`${indent} continue`);
2232
+ lines.push(`${indent} fi`);
2233
+ lines.push(`${indent} if [[ "$token" == -* && "$token" != "-" ]]; then`);
2234
+ lines.push(`${indent} if [[ \${#token} -eq 2 ]]; then`);
2235
+ if (valueShortPatterns.length > 0) {
2236
+ lines.push(`${indent} case "\${token:1:1}" in`);
2237
+ lines.push(`${indent} ${valueShortPatterns.join('|')}) expect_value=1 ;;`);
2238
+ lines.push(`${indent} esac`);
2239
+ }
2240
+ lines.push(`${indent} fi`);
2241
+ lines.push(`${indent} continue`);
2242
+ lines.push(`${indent} fi`);
2243
+ lines.push(`${indent} positional_count=$((positional_count + 1))`);
2244
+ lines.push(`${indent} done`);
2245
+ lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
2246
+ lines.push(`${indent} opts=""`);
2247
+ lines.push(`${indent} prefer_value_choices=1`);
2248
+ lines.push(`${indent} elif [[ "$cur" != -* ]]; then`);
2249
+ lines.push(`${indent} arg_slot=-1`);
2250
+ lines.push(`${indent} arg_count=${cmd.arguments.length}`);
2251
+ const hasRestArgument = cmd.arguments.length > 0 &&
2252
+ (cmd.arguments[cmd.arguments.length - 1].kind === 'variadic' ||
2253
+ cmd.arguments[cmd.arguments.length - 1].kind === 'some');
2254
+ lines.push(`${indent} has_rest=${hasRestArgument ? 1 : 0}`);
2255
+ lines.push(`${indent} if [[ $has_rest -eq 1 && $positional_count -ge $((arg_count - 1)) ]]; then`);
2256
+ lines.push(`${indent} arg_slot=$((arg_count - 1))`);
2257
+ lines.push(`${indent} elif [[ $positional_count -lt $arg_count ]]; then`);
2258
+ lines.push(`${indent} arg_slot=$positional_count`);
2259
+ lines.push(`${indent} fi`);
2260
+ lines.push(`${indent} case "$arg_slot" in`);
2261
+ for (let index = 0; index < cmd.arguments.length; index += 1) {
2262
+ const arg = cmd.arguments[index];
2263
+ if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
2264
+ continue;
2265
+ }
2266
+ lines.push(`${indent} ${index}) opts="${this.#serializeWordList(arg.choices)}" ;;`);
2267
+ }
2268
+ lines.push(`${indent} esac`);
2269
+ lines.push(`${indent} fi`);
2270
+ lines.push(`${indent}fi`);
2271
+ }
2272
+ #escapeWord(word) {
2273
+ return word.replace(/([\\\s'"`$!])/g, '\\$1');
2274
+ }
1933
2275
  #sanitizeName(name) {
1934
2276
  return name.replace(/[^a-zA-Z0-9]/g, '_');
1935
2277
  }
@@ -1937,15 +2279,19 @@ class BashCompletion {
1937
2279
  class FishCompletion {
1938
2280
  #meta;
1939
2281
  #programName;
2282
+ #slotMatcherName;
1940
2283
  constructor(meta, programName) {
1941
2284
  this.#meta = meta;
1942
2285
  this.#programName = programName;
2286
+ this.#slotMatcherName = `__${this.#sanitizeName(programName)}_match_arg_slot`;
1943
2287
  }
1944
2288
  generate() {
1945
2289
  const lines = [
1946
2290
  `# Fish completion for ${this.#programName}`,
1947
2291
  '# Generated by @guanghechen/commander',
1948
2292
  '',
2293
+ ...this.#generateSlotMatcherFunction(),
2294
+ '',
1949
2295
  ...this.#generateCommandCompletions(this.#meta, []),
1950
2296
  '',
1951
2297
  ];
@@ -1965,7 +2311,7 @@ class FishCompletion {
1965
2311
  line += ` -l ${kebabLong}`;
1966
2312
  line += ` -d '${this.#escape(opt.desc)}'`;
1967
2313
  if (opt.choices && opt.choices.length > 0) {
1968
- line += ` -xa '${opt.choices.join(' ')}'`;
2314
+ line += ` -xa '${opt.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
1969
2315
  }
1970
2316
  lines.push(line);
1971
2317
  if (!opt.takesValue) {
@@ -1977,6 +2323,36 @@ class FishCompletion {
1977
2323
  lines.push(noLine);
1978
2324
  }
1979
2325
  }
2326
+ const valueOptionLongs = cmd.options
2327
+ .filter(opt => opt.takesValue)
2328
+ .map(opt => camelToKebabCase(opt.long))
2329
+ .join(',');
2330
+ const valueOptionShorts = cmd.options
2331
+ .filter(opt => opt.takesValue && opt.short)
2332
+ .map(opt => opt.short)
2333
+ .join(',');
2334
+ const argCount = cmd.arguments.length;
2335
+ const hasRestArgument = argCount > 0 &&
2336
+ (cmd.arguments[argCount - 1].kind === 'variadic' ||
2337
+ cmd.arguments[argCount - 1].kind === 'some');
2338
+ for (let index = 0; index < cmd.arguments.length; index += 1) {
2339
+ const arg = cmd.arguments[index];
2340
+ if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
2341
+ continue;
2342
+ }
2343
+ let line = `complete -c ${this.#programName}`;
2344
+ const slotCondition = `${this.#slotMatcherName} ${parentPath.length} ${argCount} ${hasRestArgument ? 1 : 0} ${index} '${valueOptionLongs}' '${valueOptionShorts}'`;
2345
+ if (condition) {
2346
+ line += ` -n '${condition}; and ${slotCondition}'`;
2347
+ }
2348
+ else {
2349
+ line += ` -n '${slotCondition}'`;
2350
+ }
2351
+ line += ` -f`;
2352
+ line += ` -a '${arg.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
2353
+ line += ` -d '${this.#escape(`Argument: ${arg.name}`)}'`;
2354
+ lines.push(line);
2355
+ }
1980
2356
  for (const sub of cmd.subcommands) {
1981
2357
  let line = `complete -c ${this.#programName}`;
1982
2358
  if (isRoot) {
@@ -2000,7 +2376,7 @@ class FishCompletion {
2000
2376
  aliasLine += ` -d 'Alias for ${sub.name}'`;
2001
2377
  lines.push(aliasLine);
2002
2378
  }
2003
- const newPath = [...parentPath, sub.name];
2379
+ const newPath = [...parentPath, [sub.name, ...sub.aliases]];
2004
2380
  lines.push(...this.#generateCommandCompletions(sub, newPath));
2005
2381
  }
2006
2382
  return lines;
@@ -2008,7 +2384,7 @@ class FishCompletion {
2008
2384
  #buildCondition(path) {
2009
2385
  if (path.length === 0)
2010
2386
  return '';
2011
- return `__fish_seen_subcommand_from ${path[path.length - 1]}`;
2387
+ return path.map(level => `__fish_seen_subcommand_from ${level.join(' ')}`).join('; and ');
2012
2388
  }
2013
2389
  #getSubcommandNames(cmd) {
2014
2390
  return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
@@ -2016,6 +2392,68 @@ class FishCompletion {
2016
2392
  #escape(s) {
2017
2393
  return s.replace(/'/g, "\\'");
2018
2394
  }
2395
+ #escapeChoice(s) {
2396
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\s/g, '\\ ');
2397
+ }
2398
+ #sanitizeName(name) {
2399
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
2400
+ }
2401
+ #generateSlotMatcherFunction() {
2402
+ return [
2403
+ `function ${this.#slotMatcherName} --argument-names depth arg_count has_rest target_index long_opts short_opts`,
2404
+ ' set -l tokens (commandline -opc)',
2405
+ ' set -l start (math $depth + 2)',
2406
+ ' set -l positional 0',
2407
+ ' set -l expect_value 0',
2408
+ ' set -l i $start',
2409
+ ' set -l token_count (count $tokens)',
2410
+ ' set -l long_list (string split "," -- $long_opts)',
2411
+ ' set -l short_list (string split "," -- $short_opts)',
2412
+ ' while test $i -le $token_count',
2413
+ ' set -l token $tokens[$i]',
2414
+ ' if test $expect_value -eq 1',
2415
+ ' set expect_value 0',
2416
+ ' set i (math $i + 1)',
2417
+ ' continue',
2418
+ ' end',
2419
+ ' if string match -q -- "--*" $token',
2420
+ ' if string match -q -- "*=*" $token',
2421
+ ' set i (math $i + 1)',
2422
+ ' continue',
2423
+ ' end',
2424
+ ' set -l opt_name (string replace -r "^--" "" -- $token)',
2425
+ ' if contains -- $opt_name $long_list',
2426
+ ' set expect_value 1',
2427
+ ' end',
2428
+ ' set i (math $i + 1)',
2429
+ ' continue',
2430
+ ' end',
2431
+ ' if test "$token" != "-"; and string match -q -- "-*" $token',
2432
+ ' set -l raw_short (string replace -r "^-" "" -- $token)',
2433
+ ' if test (string length -- $raw_short) -eq 1',
2434
+ ' if contains -- $raw_short $short_list',
2435
+ ' set expect_value 1',
2436
+ ' end',
2437
+ ' end',
2438
+ ' set i (math $i + 1)',
2439
+ ' continue',
2440
+ ' end',
2441
+ ' set positional (math $positional + 1)',
2442
+ ' set i (math $i + 1)',
2443
+ ' end',
2444
+ ' if test $expect_value -eq 1',
2445
+ ' return 1',
2446
+ ' end',
2447
+ ' set -l slot -1',
2448
+ ' if test $has_rest -eq 1; and test $positional -ge (math $arg_count - 1)',
2449
+ ' set slot (math $arg_count - 1)',
2450
+ ' else if test $positional -lt $arg_count',
2451
+ ' set slot $positional',
2452
+ ' end',
2453
+ ' test $slot -eq $target_index',
2454
+ 'end',
2455
+ ];
2456
+ }
2019
2457
  }
2020
2458
  class PwshCompletion {
2021
2459
  #meta;
@@ -2041,16 +2479,105 @@ class PwshCompletion {
2041
2479
  '',
2042
2480
  ' # Find current command context',
2043
2481
  ' $cmd = $commands',
2482
+ ' $commandDepth = 1',
2044
2483
  ' foreach ($word in $words[1..($words.Count - 1)]) {',
2045
2484
  ' if ($word.StartsWith("-")) { continue }',
2046
2485
  ' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
2047
2486
  ' $cmd = $cmd.subcommands[$word]',
2487
+ ' $commandDepth += 1',
2048
2488
  ' }',
2049
2489
  ' }',
2050
2490
  '',
2051
2491
  ' # Generate completions',
2052
2492
  ' $completions = @()',
2053
2493
  '',
2494
+ ' # Option value slot (always higher priority than arguments)',
2495
+ ' $previous = if ($words.Count -ge 2) { $words[$words.Count - 2] } else { $null }',
2496
+ ' if ($previous) {',
2497
+ ' foreach ($opt in $cmd.options) {',
2498
+ ' $isLong = $previous -eq "--$($opt.long)"',
2499
+ ' $isShort = $opt.short -and $previous -eq "-$($opt.short)"',
2500
+ ' if ($isLong -or $isShort) {',
2501
+ ' if ($opt.choices) {',
2502
+ ' foreach ($choice in $opt.choices) {',
2503
+ ' if ($choice -like "$current*") {',
2504
+ ' $completions += [System.Management.Automation.CompletionResult]::new(',
2505
+ ' $choice,',
2506
+ ' $choice,',
2507
+ ' "ParameterValue",',
2508
+ ' $choice',
2509
+ ' )',
2510
+ ' }',
2511
+ ' }',
2512
+ ' }',
2513
+ ' return $completions',
2514
+ ' }',
2515
+ ' }',
2516
+ ' }',
2517
+ '',
2518
+ ' # Determine argument slot',
2519
+ ' $positionalCount = 0',
2520
+ ' $expectValue = $false',
2521
+ ' for ($i = $commandDepth; $i -lt ($words.Count - 1); $i += 1) {',
2522
+ ' $token = $words[$i]',
2523
+ ' if ($expectValue) {',
2524
+ ' $expectValue = $false',
2525
+ ' continue',
2526
+ ' }',
2527
+ ' if ($token.StartsWith("--")) {',
2528
+ ' if ($token.Contains("=")) { continue }',
2529
+ ' foreach ($opt in $cmd.options) {',
2530
+ ' if ($token -eq "--$($opt.long)" -and $opt.takesValue) {',
2531
+ ' $expectValue = $true',
2532
+ ' break',
2533
+ ' }',
2534
+ ' }',
2535
+ ' continue',
2536
+ ' }',
2537
+ ' if ($token.StartsWith("-") -and $token -ne "-") {',
2538
+ ' if ($token.Length -eq 2) {',
2539
+ ' foreach ($opt in $cmd.options) {',
2540
+ ' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.takesValue) {',
2541
+ ' $expectValue = $true',
2542
+ ' break',
2543
+ ' }',
2544
+ ' }',
2545
+ ' }',
2546
+ ' continue',
2547
+ ' }',
2548
+ ' $positionalCount += 1',
2549
+ ' }',
2550
+ ' if ($expectValue) {',
2551
+ ' return $completions',
2552
+ ' }',
2553
+ ' if (-not $current.StartsWith("-") -and $cmd.arguments -and $cmd.arguments.Count -gt 0) {',
2554
+ ' $argSlot = -1',
2555
+ ' $argCount = $cmd.arguments.Count',
2556
+ ' $lastArg = $cmd.arguments[$argCount - 1]',
2557
+ ' $hasRest = $lastArg.kind -eq "variadic" -or $lastArg.kind -eq "some"',
2558
+ ' if ($hasRest -and $positionalCount -ge ($argCount - 1)) {',
2559
+ ' $argSlot = $argCount - 1',
2560
+ ' } elseif ($positionalCount -lt $argCount) {',
2561
+ ' $argSlot = $positionalCount',
2562
+ ' }',
2563
+ ' if ($argSlot -ge 0) {',
2564
+ ' $argMeta = $cmd.arguments[$argSlot]',
2565
+ ' if ($argMeta.choices) {',
2566
+ ' foreach ($choice in $argMeta.choices) {',
2567
+ ' if ($choice -like "$current*") {',
2568
+ ' $completions += [System.Management.Automation.CompletionResult]::new(',
2569
+ ' $choice,',
2570
+ ' $choice,',
2571
+ ' "ParameterValue",',
2572
+ ' $choice',
2573
+ ' )',
2574
+ ' }',
2575
+ ' }',
2576
+ ' return $completions',
2577
+ ' }',
2578
+ ' }',
2579
+ ' }',
2580
+ '',
2054
2581
  ' # Options',
2055
2582
  ' if ($current.StartsWith("-")) {',
2056
2583
  ' foreach ($opt in $cmd.options) {',
@@ -2059,7 +2586,7 @@ class PwshCompletion {
2059
2586
  ' "--$($opt.long)",',
2060
2587
  ' $opt.long,',
2061
2588
  ' "ParameterName",',
2062
- ' $opt.desc',
2589
+ ' $opt.description',
2063
2590
  ' )',
2064
2591
  ' }',
2065
2592
  ' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
@@ -2067,7 +2594,7 @@ class PwshCompletion {
2067
2594
  ' "--no-$($opt.long)",',
2068
2595
  ' "no-$($opt.long)",',
2069
2596
  ' "ParameterName",',
2070
- ' $opt.desc',
2597
+ ' $opt.description',
2071
2598
  ' )',
2072
2599
  ' }',
2073
2600
  ' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
@@ -2075,7 +2602,7 @@ class PwshCompletion {
2075
2602
  ' "-$($opt.short)",',
2076
2603
  ' $opt.short,',
2077
2604
  ' "ParameterName",',
2078
- ' $opt.desc',
2605
+ ' $opt.description',
2079
2606
  ' )',
2080
2607
  ' }',
2081
2608
  ' }',
@@ -2089,7 +2616,7 @@ class PwshCompletion {
2089
2616
  ' $sub,',
2090
2617
  ' $sub,',
2091
2618
  ' "Command",',
2092
- ' $cmd.subcommands[$sub].desc',
2619
+ ' $cmd.subcommands[$sub].description',
2093
2620
  ' )',
2094
2621
  ' }',
2095
2622
  ' }',
@@ -2113,8 +2640,25 @@ class PwshCompletion {
2113
2640
  lines.push(`${indent} long = '${kebabLong}'`);
2114
2641
  lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
2115
2642
  lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
2643
+ lines.push(`${indent} takesValue = $${opt.takesValue}`);
2116
2644
  if (opt.choices) {
2117
- lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
2645
+ lines.push(`${indent} choices = @('${opt.choices
2646
+ .map(choice => this.#escape(choice))
2647
+ .join("', '")}')`);
2648
+ }
2649
+ lines.push(`${indent} }`);
2650
+ }
2651
+ lines.push(`${indent})`);
2652
+ lines.push(`${indent}arguments = @(`);
2653
+ for (const arg of cmd.arguments) {
2654
+ lines.push(`${indent} @{`);
2655
+ lines.push(`${indent} name = '${this.#escape(arg.name)}'`);
2656
+ lines.push(`${indent} kind = '${arg.kind}'`);
2657
+ lines.push(`${indent} type = '${arg.type}'`);
2658
+ if (arg.choices && arg.choices.length > 0) {
2659
+ lines.push(`${indent} choices = @('${arg.choices
2660
+ .map(choice => this.#escape(choice))
2661
+ .join("', '")}')`);
2118
2662
  }
2119
2663
  lines.push(`${indent} }`);
2120
2664
  }