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