@guanghechen/commander 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/index.mjs CHANGED
@@ -29,41 +29,131 @@ class DefaultReporter {
29
29
  console.error(message, ...args);
30
30
  }
31
31
  }
32
+ const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
33
+ const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
34
+ function kebabToCamelCase(str) {
35
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
36
+ }
37
+ function camelToKebabCase$1(str) {
38
+ return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
39
+ }
40
+ function tokenizeLongOption(arg, commandPath) {
41
+ const eqIdx = arg.indexOf('=');
42
+ const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
43
+ const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
44
+ if (namePart.includes('_')) {
45
+ throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
46
+ }
47
+ const lowerName = namePart.toLowerCase();
48
+ if (lowerName === '--no' || lowerName === '--no-') {
49
+ throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
50
+ }
51
+ if (lowerName.startsWith('--no-')) {
52
+ if (valuePart !== '') {
53
+ throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
54
+ }
55
+ if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
56
+ throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
57
+ }
58
+ const camelName = kebabToCamelCase(lowerName.slice(5));
59
+ return {
60
+ original: arg,
61
+ resolved: `--${camelName}=false`,
62
+ name: camelName,
63
+ type: 'long',
64
+ };
65
+ }
66
+ if (!LONG_OPTION_REGEX.test(lowerName)) {
67
+ throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
68
+ }
69
+ const camelName = kebabToCamelCase(lowerName.slice(2));
70
+ return {
71
+ original: arg,
72
+ resolved: `--${camelName}${valuePart}`,
73
+ name: camelName,
74
+ type: 'long',
75
+ };
76
+ }
77
+ function tokenizeShortOptions(arg, commandPath) {
78
+ if (arg.includes('=')) {
79
+ throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
80
+ }
81
+ const flags = arg.slice(1);
82
+ return flags.split('').map(flag => ({
83
+ original: `-${flag}`,
84
+ resolved: `-${flag}`,
85
+ name: flag,
86
+ type: 'short',
87
+ }));
88
+ }
89
+ function tokenize(argv, commandPath) {
90
+ const optionTokens = [];
91
+ const restArgs = [];
92
+ let passThrough = false;
93
+ for (const arg of argv) {
94
+ if (arg === '--') {
95
+ passThrough = true;
96
+ continue;
97
+ }
98
+ if (passThrough) {
99
+ restArgs.push(arg);
100
+ continue;
101
+ }
102
+ if (arg.startsWith('--')) {
103
+ optionTokens.push(tokenizeLongOption(arg, commandPath));
104
+ continue;
105
+ }
106
+ if (arg.startsWith('-') && arg.length > 1) {
107
+ optionTokens.push(...tokenizeShortOptions(arg, commandPath));
108
+ continue;
109
+ }
110
+ optionTokens.push({
111
+ original: arg,
112
+ resolved: arg,
113
+ name: '',
114
+ type: 'none',
115
+ });
116
+ }
117
+ return { optionTokens, restArgs };
118
+ }
32
119
  const BUILTIN_HELP_OPTION = {
33
120
  long: 'help',
34
121
  short: 'h',
35
122
  type: 'boolean',
36
- description: 'Show help information',
123
+ args: 'none',
124
+ desc: 'Show help information',
37
125
  };
38
126
  const BUILTIN_VERSION_OPTION = {
39
127
  long: 'version',
40
128
  short: 'V',
41
129
  type: 'boolean',
42
- description: 'Show version number',
130
+ args: 'none',
131
+ desc: 'Show version number',
43
132
  };
44
133
  class Command {
45
134
  #name;
46
- #description;
135
+ #desc;
47
136
  #version;
48
137
  #helpSubcommandEnabled;
49
138
  #reporter;
50
139
  #parent;
51
140
  #options = [];
52
141
  #arguments = [];
53
- #subcommands = [];
54
- #action;
142
+ #subcommandsList = [];
143
+ #subcommandsMap = new Map();
144
+ #action = undefined;
55
145
  constructor(config) {
56
146
  this.#name = config.name ?? '';
57
- this.#description = config.description;
147
+ this.#desc = config.desc;
58
148
  this.#version = config.version;
59
149
  this.#helpSubcommandEnabled = config.help ?? false;
60
150
  this.#reporter = config.reporter;
61
151
  }
62
152
  get name() {
63
- return this.#name;
153
+ return this.#name || undefined;
64
154
  }
65
155
  get description() {
66
- return this.#description;
156
+ return this.#desc;
67
157
  }
68
158
  get version() {
69
159
  return this.#version;
@@ -77,6 +167,9 @@ class Command {
77
167
  get arguments() {
78
168
  return [...this.#arguments];
79
169
  }
170
+ get subcommands() {
171
+ return new Map(this.#subcommandsMap);
172
+ }
80
173
  option(opt) {
81
174
  this.#validateOptionConfig(opt);
82
175
  this.#checkOptionUniqueness(opt);
@@ -99,14 +192,16 @@ class Command {
99
192
  if (cmd.#parent && cmd.#parent !== this) {
100
193
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
101
194
  }
102
- const existing = this.#subcommands.find(e => e.command === cmd);
195
+ const existing = this.#subcommandsList.find(e => e.command === cmd);
103
196
  if (existing) {
104
197
  existing.aliases.push(name);
198
+ this.#subcommandsMap.set(name, cmd);
105
199
  }
106
200
  else {
107
201
  cmd.#name = name;
108
202
  cmd.#parent = this;
109
- this.#subcommands.push({ name, aliases: [], command: cmd });
203
+ this.#subcommandsList.push({ name, aliases: [], command: cmd });
204
+ this.#subcommandsMap.set(name, cmd);
110
205
  }
111
206
  return this;
112
207
  }
@@ -114,52 +209,42 @@ class Command {
114
209
  const { argv, envs, reporter } = params;
115
210
  try {
116
211
  const processedArgv = this.#processHelpSubcommand(argv);
117
- const { chain, remaining } = this.#routeChain(processedArgv);
212
+ const routeResult = this.#route(processedArgv);
213
+ const { chain, remaining } = routeResult;
118
214
  const leafCommand = chain[chain.length - 1];
119
215
  const rootCommand = chain[0];
120
- const includeRootVersion = chain.length === 1;
121
- this.#validateMergedShortOptions(chain, includeRootVersion);
122
- const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
123
- const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
216
+ const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
217
+ const { optionTokens, restArgs } = tokenizeResult;
124
218
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
125
219
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
126
- if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
220
+ if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
127
221
  console.log(leafCommand.formatHelp());
128
222
  return;
129
223
  }
130
- if (!hasUserVersion && leafCommand === rootCommand) {
131
- if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
132
- console.log(leafCommand.version ?? 'unknown');
224
+ if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
225
+ if (this.#hasFlag(optionTokens, 'version', 'V')) {
226
+ console.log(leafCommand.#version);
133
227
  return;
134
228
  }
135
229
  }
136
- const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
230
+ const resolveResult = this.#resolve(chain, optionTokens);
137
231
  const ctx = {
138
232
  cmd: leafCommand,
139
233
  envs,
140
234
  reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
141
235
  argv,
142
236
  };
143
- this.#applyChain(chain, optsMap, ctx);
144
- const mergedOpts = this.#mergeOpts(chain, optsMap);
145
- const allArgs = [...positionalArgs, ...restArgs];
146
- const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
147
- const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
237
+ const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
238
+ const actionParams = {
239
+ ctx: parseResult.ctx,
240
+ opts: parseResult.opts,
241
+ args: parseResult.args,
242
+ rawArgs: parseResult.rawArgs,
243
+ };
148
244
  if (leafCommand.#action) {
149
- try {
150
- await leafCommand.#action(actionParams);
151
- }
152
- catch (err) {
153
- if (err instanceof Error) {
154
- console.error(`Error: ${err.message}`);
155
- }
156
- else {
157
- console.error('Error: action failed');
158
- }
159
- process.exit(1);
160
- }
245
+ await leafCommand.#runAction(actionParams);
161
246
  }
162
- else if (leafCommand.#subcommands.length > 0) {
247
+ else if (leafCommand.#subcommandsList.length > 0) {
163
248
  console.log(leafCommand.formatHelp());
164
249
  }
165
250
  else {
@@ -175,105 +260,33 @@ class Command {
175
260
  throw err;
176
261
  }
177
262
  }
178
- parse(argv) {
263
+ parse(params) {
264
+ const { argv, envs, reporter } = params;
179
265
  const processedArgv = this.#processHelpSubcommand(argv);
180
- const { chain, remaining } = this.#routeChain(processedArgv);
266
+ const routeResult = this.#route(processedArgv);
267
+ const { chain, remaining } = routeResult;
181
268
  const leafCommand = chain[chain.length - 1];
182
- const includeRootVersion = chain.length === 1;
183
- this.#validateMergedShortOptions(chain, includeRootVersion);
184
- const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
185
- const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
186
- const mergedOpts = this.#mergeOpts(chain, optsMap);
187
- const allArgs = [...positionalArgs, ...restArgs];
188
- const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
189
- return { opts: mergedOpts, args, rawArgs };
190
- }
191
- shift(tokens) {
192
- return this.#shiftWithShadowed(tokens, new Set());
193
- }
194
- #shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
195
- const allDirectOptions = this.#getMergedOptions(includeVersion);
196
- const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
197
- const opts = {};
198
- for (const opt of directOptions) {
199
- if (opt.default !== undefined) {
200
- opts[opt.long] = opt.default;
201
- }
202
- else if (opt.type === 'boolean') {
203
- opts[opt.long] = false;
204
- }
205
- else if (opt.type === 'string[]' || opt.type === 'number[]') {
206
- opts[opt.long] = [];
207
- }
208
- }
209
- let remaining = [...tokens];
210
- const resolverOptions = directOptions.filter(o => o.resolver);
211
- for (const opt of resolverOptions) {
212
- const result = opt.resolver(remaining);
213
- opts[opt.long] = result.value;
214
- remaining = result.remaining;
215
- }
216
- const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
217
- const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
218
- const finalRemaining = [];
219
- let i = 0;
220
- while (i < normalizedTokens.length) {
221
- const token = normalizedTokens[i];
222
- if (token.startsWith('--')) {
223
- const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
224
- if (consumed > 0) {
225
- i += consumed;
226
- continue;
227
- }
228
- finalRemaining.push(token);
229
- i += 1;
230
- continue;
231
- }
232
- if (token.startsWith('-') && token.length > 1) {
233
- const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
234
- if (result.consumed) {
235
- i = result.nextIdx;
236
- if (result.remainingToken) {
237
- finalRemaining.push(result.remainingToken);
238
- }
239
- continue;
240
- }
241
- finalRemaining.push(token);
242
- i += 1;
243
- continue;
244
- }
245
- finalRemaining.push(token);
246
- i += 1;
247
- }
248
- for (const opt of directOptions) {
249
- if (opt.required && opts[opt.long] === undefined) {
250
- throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
251
- }
252
- }
253
- for (const opt of directOptions) {
254
- if (opt.choices && opts[opt.long] !== undefined) {
255
- const value = opts[opt.long];
256
- const values = Array.isArray(value) ? value : [value];
257
- const choices = opt.choices;
258
- for (const v of values) {
259
- if (!choices.includes(v)) {
260
- throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
261
- }
262
- }
263
- }
264
- }
265
- return { opts, remaining: finalRemaining };
269
+ const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
270
+ const { optionTokens, restArgs } = tokenizeResult;
271
+ const resolveResult = this.#resolve(chain, optionTokens);
272
+ const ctx = {
273
+ cmd: leafCommand,
274
+ envs,
275
+ reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
276
+ argv,
277
+ };
278
+ return this.#parse(chain, resolveResult, ctx, restArgs);
266
279
  }
267
280
  formatHelp() {
268
281
  const lines = [];
269
282
  const allOptions = this.#getMergedOptions();
270
- lines.push(this.#description);
283
+ lines.push(this.#desc);
271
284
  lines.push('');
272
285
  const commandPath = this.#getCommandPath();
273
286
  let usage = `Usage: ${commandPath}`;
274
287
  if (allOptions.length > 0)
275
288
  usage += ' [options]';
276
- if (this.#subcommands.length > 0)
289
+ if (this.#subcommandsList.length > 0)
277
290
  usage += ' [command]';
278
291
  for (const arg of this.#arguments) {
279
292
  if (arg.kind === 'required') {
@@ -292,24 +305,24 @@ class Command {
292
305
  lines.push('Options:');
293
306
  const optLines = [];
294
307
  for (const opt of allOptions) {
308
+ const kebabLong = camelToKebabCase$1(opt.long);
295
309
  let sig = opt.short ? `-${opt.short}, ` : ' ';
296
- sig += `--${opt.long}`;
297
- const effectiveType = opt.type ?? 'string';
298
- if (effectiveType !== 'boolean') {
310
+ sig += `--${kebabLong}`;
311
+ if (opt.args !== 'none') {
299
312
  sig += ' <value>';
300
313
  }
301
- let desc = opt.description;
302
- if (opt.default !== undefined && effectiveType !== 'boolean') {
314
+ let desc = opt.desc;
315
+ if (opt.default !== undefined && opt.type !== 'boolean') {
303
316
  desc += ` (default: ${JSON.stringify(opt.default)})`;
304
317
  }
305
318
  if (opt.choices) {
306
319
  desc += ` [choices: ${opt.choices.join(', ')}]`;
307
320
  }
308
321
  optLines.push({ sig, desc });
309
- if (effectiveType === 'boolean') {
322
+ if (opt.type === 'boolean' && opt.args === 'none') {
310
323
  optLines.push({
311
- sig: ` --no-${opt.long}`,
312
- desc: `Negate --${opt.long}`,
324
+ sig: ` --no-${kebabLong}`,
325
+ desc: `Negate --${kebabLong}`,
313
326
  });
314
327
  }
315
328
  }
@@ -320,19 +333,19 @@ class Command {
320
333
  }
321
334
  lines.push('');
322
335
  }
323
- const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommands.length > 0;
324
- if (this.#subcommands.length > 0) {
336
+ const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
337
+ if (this.#subcommandsList.length > 0) {
325
338
  lines.push('Commands:');
326
339
  const cmdLines = [];
327
340
  if (showHelpSubcommand) {
328
341
  cmdLines.push({ name: 'help', desc: 'Show help for a command' });
329
342
  }
330
- for (const entry of this.#subcommands) {
343
+ for (const entry of this.#subcommandsList) {
331
344
  let name = entry.name;
332
345
  if (entry.aliases.length > 0) {
333
346
  name += `, ${entry.aliases.join(', ')}`;
334
347
  }
335
- cmdLines.push({ name, desc: entry.command.#description });
348
+ cmdLines.push({ name, desc: entry.command.#desc });
336
349
  }
337
350
  const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
338
351
  for (const { name, desc } of cmdLines) {
@@ -347,21 +360,20 @@ class Command {
347
360
  const allOptions = this.#getMergedOptions();
348
361
  const options = [];
349
362
  for (const opt of allOptions) {
350
- const effectiveType = opt.type ?? 'string';
351
363
  options.push({
352
364
  long: opt.long,
353
365
  short: opt.short,
354
- description: opt.description,
355
- takesValue: effectiveType !== 'boolean',
366
+ desc: opt.desc,
367
+ takesValue: opt.args !== 'none',
356
368
  choices: opt.choices,
357
369
  });
358
370
  }
359
371
  return {
360
372
  name: this.#name,
361
- description: this.#description,
373
+ desc: this.#desc,
362
374
  aliases: [],
363
375
  options,
364
- subcommands: this.#subcommands.map(entry => {
376
+ subcommands: this.#subcommandsList.map(entry => {
365
377
  const subMeta = entry.command.getCompletionMeta();
366
378
  return {
367
379
  ...subMeta,
@@ -376,17 +388,17 @@ class Command {
376
388
  return argv;
377
389
  if (argv.length < 1 || argv[0] !== 'help')
378
390
  return argv;
379
- if (argv.length === 1 || this.#subcommands.length === 0) {
391
+ if (argv.length === 1 || this.#subcommandsList.length === 0) {
380
392
  return ['--help'];
381
393
  }
382
394
  const subName = argv[1];
383
- const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
395
+ const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
384
396
  if (entry) {
385
397
  return [subName, '--help', ...argv.slice(2)];
386
398
  }
387
399
  return argv;
388
400
  }
389
- #routeChain(argv) {
401
+ #route(argv) {
390
402
  const chain = [this];
391
403
  let current = this;
392
404
  let idx = 0;
@@ -394,7 +406,7 @@ class Command {
394
406
  const token = argv[idx];
395
407
  if (token.startsWith('-'))
396
408
  break;
397
- const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
409
+ const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
398
410
  if (!entry)
399
411
  break;
400
412
  current = entry.command;
@@ -403,93 +415,299 @@ class Command {
403
415
  }
404
416
  return { chain, remaining: argv.slice(idx) };
405
417
  }
406
- #splitAtDoubleDash(tokens) {
407
- const ddIdx = tokens.indexOf('--');
408
- if (ddIdx === -1) {
409
- return { optionTokens: tokens, restArgs: [] };
410
- }
411
- return {
412
- optionTokens: tokens.slice(0, ddIdx),
413
- restArgs: tokens.slice(ddIdx + 1),
414
- };
415
- }
416
- #shiftChain(chain, tokens, includeRootVersion) {
417
- const optsMap = new Map();
418
+ #resolve(chain, tokens) {
419
+ const consumedTokens = new Map();
418
420
  let remaining = [...tokens];
419
- const rootCommand = chain[0];
420
421
  const shadowed = new Set();
421
422
  for (let i = chain.length - 1; i >= 0; i--) {
422
423
  const cmd = chain[i];
423
- const includeVersion = cmd === rootCommand && includeRootVersion;
424
- const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
425
- optsMap.set(cmd, result.opts);
424
+ const includeVersion = i === 0;
425
+ const result = cmd.#shift(remaining, shadowed, includeVersion);
426
+ consumedTokens.set(cmd, result.consumed);
426
427
  remaining = result.remaining;
427
428
  for (const opt of cmd.#options) {
428
429
  shadowed.add(opt.long);
429
430
  }
430
431
  }
431
- const positionalArgs = [];
432
+ const argTokens = [];
432
433
  for (const token of remaining) {
433
- if (token.startsWith('-')) {
434
+ if (token.type !== 'none') {
434
435
  const leafCommand = chain[chain.length - 1];
435
- if (!token.startsWith('--') && token.length > 2) {
436
- const flag = token[1];
437
- throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
436
+ throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
437
+ }
438
+ argTokens.push(token);
439
+ }
440
+ return { consumedTokens, argTokens };
441
+ }
442
+ #shift(tokens, shadowed, includeVersion) {
443
+ const allOptions = this.#getMergedOptions(includeVersion);
444
+ const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
445
+ const optionByLong = new Map();
446
+ const optionByShort = new Map();
447
+ for (const opt of effectiveOptions) {
448
+ optionByLong.set(opt.long, opt);
449
+ if (opt.short) {
450
+ optionByShort.set(opt.short, opt);
451
+ }
452
+ }
453
+ const consumed = [];
454
+ const remaining = [];
455
+ let i = 0;
456
+ while (i < tokens.length) {
457
+ const token = tokens[i];
458
+ if (token.type === 'long') {
459
+ const opt = optionByLong.get(token.name);
460
+ if (opt) {
461
+ consumed.push(token);
462
+ if (opt.args === 'required') {
463
+ if (!token.resolved.includes('=') && i + 1 < tokens.length) {
464
+ i += 1;
465
+ consumed.push(tokens[i]);
466
+ }
467
+ }
468
+ else if (opt.args === 'variadic') {
469
+ if (!token.resolved.includes('=')) {
470
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
471
+ i += 1;
472
+ consumed.push(tokens[i]);
473
+ }
474
+ }
475
+ }
476
+ i += 1;
477
+ continue;
478
+ }
479
+ remaining.push(token);
480
+ i += 1;
481
+ continue;
482
+ }
483
+ if (token.type === 'short') {
484
+ const opt = optionByShort.get(token.name);
485
+ if (opt) {
486
+ consumed.push(token);
487
+ if (opt.args === 'required') {
488
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
489
+ i += 1;
490
+ consumed.push(tokens[i]);
491
+ }
492
+ }
493
+ else if (opt.args === 'variadic') {
494
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
495
+ i += 1;
496
+ consumed.push(tokens[i]);
497
+ }
498
+ }
499
+ i += 1;
500
+ continue;
438
501
  }
439
- throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
502
+ remaining.push(token);
503
+ i += 1;
504
+ continue;
440
505
  }
441
- positionalArgs.push(token);
506
+ remaining.push(token);
507
+ i += 1;
442
508
  }
443
- return { optsMap, positionalArgs };
509
+ return { consumed, remaining };
444
510
  }
445
- #applyChain(chain, optsMap, ctx) {
446
- for (const cmd of chain) {
447
- const opts = optsMap.get(cmd) ?? {};
448
- for (const opt of cmd.#getMergedOptions()) {
511
+ #parse(chain, resolveResult, ctx, restArgs) {
512
+ const { consumedTokens, argTokens } = resolveResult;
513
+ const leafCommand = chain[chain.length - 1];
514
+ this.#validateMergedShortOptions(chain);
515
+ const optsMap = new Map();
516
+ for (let i = 0; i < chain.length; i++) {
517
+ const cmd = chain[i];
518
+ const includeVersion = i === 0;
519
+ const tokens = consumedTokens.get(cmd) ?? [];
520
+ const opts = cmd.#parseOptions(tokens, includeVersion);
521
+ optsMap.set(cmd, opts);
522
+ for (const opt of cmd.#getMergedOptions(includeVersion)) {
449
523
  if (opt.apply && opts[opt.long] !== undefined) {
450
524
  opt.apply(opts[opt.long], ctx);
451
525
  }
452
526
  }
453
527
  }
454
- }
455
- #mergeOpts(chain, optsMap) {
456
- const merged = {};
528
+ const mergedOpts = {};
457
529
  for (const cmd of chain) {
458
- Object.assign(merged, optsMap.get(cmd) ?? {});
530
+ Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
459
531
  }
460
- return merged;
532
+ const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
533
+ const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
534
+ return { ctx, opts: mergedOpts, args, rawArgs };
461
535
  }
462
- #applyValue(opt, rawValue, opts) {
463
- const type = opt.type ?? 'string';
464
- let parsedValue = rawValue;
465
- if (opt.coerce) {
466
- parsedValue = opt.coerce(rawValue);
536
+ #parseOptions(tokens, includeVersion) {
537
+ const allOptions = this.#getMergedOptions(includeVersion);
538
+ const opts = {};
539
+ for (const opt of allOptions) {
540
+ if (opt.default !== undefined) {
541
+ opts[opt.long] = opt.default;
542
+ }
543
+ else if (opt.type === 'boolean' && opt.args === 'none') {
544
+ opts[opt.long] = false;
545
+ }
546
+ else if (opt.args === 'variadic') {
547
+ opts[opt.long] = [];
548
+ }
467
549
  }
468
- else {
469
- switch (type) {
470
- case 'string':
471
- case 'string[]':
472
- parsedValue = rawValue;
473
- break;
474
- case 'number':
475
- case 'number[]': {
476
- const num = Number(rawValue);
477
- if (Number.isNaN(num)) {
478
- throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${opt.long}"`, this.#getCommandPath());
550
+ const optionByLong = new Map();
551
+ const optionByShort = new Map();
552
+ for (const opt of allOptions) {
553
+ optionByLong.set(opt.long, opt);
554
+ if (opt.short) {
555
+ optionByShort.set(opt.short, opt);
556
+ }
557
+ }
558
+ let i = 0;
559
+ while (i < tokens.length) {
560
+ const token = tokens[i];
561
+ const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
562
+ if (!opt) {
563
+ i += 1;
564
+ continue;
565
+ }
566
+ const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
567
+ if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
568
+ throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
569
+ }
570
+ if (opt.type === 'boolean' && opt.args === 'none') {
571
+ const eqIdx = token.resolved.indexOf('=');
572
+ if (eqIdx !== -1) {
573
+ const value = token.resolved.slice(eqIdx + 1);
574
+ if (value === 'true') {
575
+ opts[opt.long] = true;
479
576
  }
480
- parsedValue = num;
481
- break;
577
+ else if (value === 'false') {
578
+ opts[opt.long] = false;
579
+ }
580
+ else {
581
+ throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
582
+ }
583
+ }
584
+ else {
585
+ opts[opt.long] = true;
482
586
  }
587
+ i += 1;
588
+ continue;
589
+ }
590
+ if (opt.args === 'required') {
591
+ const eqIdx = token.resolved.indexOf('=');
592
+ let rawValue;
593
+ if (eqIdx !== -1) {
594
+ rawValue = token.resolved.slice(eqIdx + 1);
595
+ }
596
+ else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
597
+ rawValue = tokens[i + 1].original;
598
+ i += 1;
599
+ }
600
+ else {
601
+ throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
602
+ }
603
+ opts[opt.long] = this.#convertValue(opt, rawValue);
604
+ i += 1;
605
+ continue;
606
+ }
607
+ if (opt.args === 'variadic') {
608
+ const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
609
+ const eqIdx = token.resolved.indexOf('=');
610
+ if (eqIdx !== -1) {
611
+ values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
612
+ }
613
+ else {
614
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
615
+ i += 1;
616
+ values.push(this.#convertValue(opt, tokens[i].original));
617
+ }
618
+ }
619
+ opts[opt.long] = values;
620
+ i += 1;
621
+ continue;
483
622
  }
623
+ i += 1;
484
624
  }
485
- if (type === 'string[]' || type === 'number[]') {
486
- const currentValue = opts[opt.long];
487
- const current = Array.isArray(currentValue) ? currentValue : [];
488
- opts[opt.long] = [...current, parsedValue];
625
+ for (const opt of allOptions) {
626
+ if (opt.required && opts[opt.long] === undefined) {
627
+ throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
628
+ }
489
629
  }
490
- else {
491
- opts[opt.long] = parsedValue;
630
+ for (const opt of allOptions) {
631
+ if (opt.choices && opts[opt.long] !== undefined) {
632
+ const value = opts[opt.long];
633
+ const values = Array.isArray(value) ? value : [value];
634
+ const choices = opt.choices;
635
+ for (const v of values) {
636
+ if (!choices.includes(v)) {
637
+ throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
638
+ }
639
+ }
640
+ }
641
+ }
642
+ return opts;
643
+ }
644
+ #convertValue(opt, rawValue) {
645
+ if (opt.coerce) {
646
+ return opt.coerce(rawValue);
647
+ }
648
+ if (opt.type === 'number') {
649
+ const num = Number(rawValue);
650
+ if (Number.isNaN(num)) {
651
+ throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
652
+ }
653
+ return num;
654
+ }
655
+ return rawValue;
656
+ }
657
+ #parseArguments(rawArgs) {
658
+ const argumentDefs = this.#arguments;
659
+ const args = {};
660
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
661
+ if (rawArgs.length < requiredCount) {
662
+ const missing = argumentDefs
663
+ .filter(a => a.kind === 'required')
664
+ .slice(rawArgs.length)
665
+ .map(a => a.name);
666
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
667
+ }
668
+ let index = 0;
669
+ for (const def of argumentDefs) {
670
+ if (def.kind === 'variadic') {
671
+ const rest = rawArgs.slice(index);
672
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
673
+ index = rawArgs.length;
674
+ break;
675
+ }
676
+ const raw = rawArgs[index];
677
+ if (raw === undefined) {
678
+ if (def.kind === 'optional') {
679
+ args[def.name] = def.default ?? undefined;
680
+ continue;
681
+ }
682
+ }
683
+ else {
684
+ args[def.name] = this.#convertArgument(def, raw);
685
+ index += 1;
686
+ }
687
+ }
688
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
689
+ if (!hasVariadic && index < rawArgs.length) {
690
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
691
+ }
692
+ return { args, rawArgs };
693
+ }
694
+ #convertArgument(def, raw) {
695
+ if (def.coerce) {
696
+ try {
697
+ return def.coerce(raw);
698
+ }
699
+ catch {
700
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
701
+ }
702
+ }
703
+ if (def.type === 'number') {
704
+ const n = Number(raw);
705
+ if (Number.isNaN(n)) {
706
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
707
+ }
708
+ return n;
492
709
  }
710
+ return raw;
493
711
  }
494
712
  #getMergedOptions(includeVersion = !this.#parent) {
495
713
  const optionMap = new Map();
@@ -506,11 +724,11 @@ class Command {
506
724
  }
507
725
  return Array.from(optionMap.values());
508
726
  }
509
- #validateMergedShortOptions(chain, includeRootVersion) {
727
+ #validateMergedShortOptions(chain) {
510
728
  const mergedByLong = new Map();
511
- const rootCommand = chain[0];
512
- for (const cmd of chain) {
513
- const includeVersion = cmd === rootCommand && includeRootVersion;
729
+ for (let i = 0; i < chain.length; i++) {
730
+ const cmd = chain[i];
731
+ const includeVersion = i === 0;
514
732
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
515
733
  mergedByLong.set(opt.long, opt);
516
734
  }
@@ -527,8 +745,17 @@ class Command {
527
745
  }
528
746
  }
529
747
  #validateOptionConfig(opt) {
530
- if (opt.long.startsWith('no-')) {
531
- throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
748
+ if (opt.type === 'boolean' && opt.args !== 'none') {
749
+ throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
750
+ }
751
+ if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
752
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
753
+ }
754
+ if (opt.long.startsWith('no')) {
755
+ throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
756
+ }
757
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
758
+ throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
532
759
  }
533
760
  if (opt.required && opt.default !== undefined) {
534
761
  throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
@@ -567,154 +794,33 @@ class Command {
567
794
  }
568
795
  }
569
796
  }
570
- #parseArguments(rawArgs) {
571
- const argumentDefs = this.#arguments;
572
- const args = {};
573
- const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
574
- if (rawArgs.length < requiredCount) {
575
- const missing = argumentDefs
576
- .filter(a => a.kind === 'required')
577
- .slice(rawArgs.length)
578
- .map(a => a.name);
579
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
797
+ async #runAction(params) {
798
+ if (!this.#action)
799
+ return;
800
+ try {
801
+ await this.#action(params);
580
802
  }
581
- let index = 0;
582
- for (const def of argumentDefs) {
583
- if (def.kind === 'variadic') {
584
- const rest = rawArgs.slice(index);
585
- args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
586
- index = rawArgs.length;
587
- break;
588
- }
589
- const raw = rawArgs[index];
590
- if (raw === undefined) {
591
- if (def.kind === 'optional') {
592
- args[def.name] = def.default ?? undefined;
593
- continue;
594
- }
803
+ catch (err) {
804
+ if (err instanceof Error) {
805
+ console.error(`Error: ${err.message}`);
595
806
  }
596
807
  else {
597
- args[def.name] = this.#convertArgument(def, raw);
598
- index += 1;
599
- }
600
- }
601
- const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
602
- if (!hasVariadic && index < rawArgs.length) {
603
- throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
604
- }
605
- return { args, rawArgs };
606
- }
607
- #convertArgument(def, raw) {
608
- if (def.coerce) {
609
- try {
610
- return def.coerce(raw);
611
- }
612
- catch {
613
- throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
614
- }
615
- }
616
- if (def.type === 'number') {
617
- const n = Number(raw);
618
- if (Number.isNaN(n)) {
619
- throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
620
- }
621
- return n;
622
- }
623
- return raw;
624
- }
625
- #buildOptionMaps(allOptions, excludeResolver = false) {
626
- const optionByLong = new Map();
627
- const optionByShort = new Map();
628
- const booleanOptions = new Set();
629
- for (const opt of allOptions) {
630
- if (excludeResolver && opt.resolver)
631
- continue;
632
- optionByLong.set(opt.long, opt);
633
- if (opt.short) {
634
- optionByShort.set(opt.short, opt);
635
- }
636
- if (opt.type === 'boolean') {
637
- booleanOptions.add(opt.long);
808
+ console.error('Error: action failed');
638
809
  }
810
+ process.exit(1);
639
811
  }
640
- return { optionByLong, optionByShort, booleanOptions };
641
812
  }
642
- #hasHelpFlag(argv, allOptions) {
643
- return this.#hasBuiltinFlag(argv, 'help', 'h', allOptions);
644
- }
645
- #hasVersionFlag(argv, allOptions) {
646
- return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
647
- }
648
- #hasBuiltinFlag(argv, flagLong, flagShort, allOptions) {
649
- const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions);
650
- const normalizedArgv = this.#normalizeArgv(argv, booleanOptions);
651
- for (let i = 0; i < normalizedArgv.length; i++) {
652
- const arg = normalizedArgv[i];
653
- if (arg === '--') {
654
- break;
655
- }
656
- if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
813
+ #hasFlag(tokens, longName, shortName) {
814
+ for (const token of tokens) {
815
+ if (token.type === 'long' && token.name === longName) {
657
816
  return true;
658
817
  }
659
- if (this.#optionConsumesNextValue(arg, optionByLong, optionByShort)) {
660
- i += 1;
661
- }
662
- }
663
- return false;
664
- }
665
- #optionConsumesNextValue(arg, optionByLong, optionByShort) {
666
- if (arg.startsWith('--')) {
667
- const eqIdx = arg.indexOf('=');
668
- if (eqIdx !== -1) {
669
- return false;
670
- }
671
- const optName = arg.slice(2);
672
- const opt = optionByLong.get(optName);
673
- if (!opt) {
674
- return false;
675
- }
676
- const type = opt.type ?? 'string';
677
- return type !== 'boolean';
678
- }
679
- if (arg.startsWith('-') && arg.length === 2) {
680
- const opt = optionByShort.get(arg[1]);
681
- if (!opt) {
682
- return false;
818
+ if (token.type === 'short' && token.name === shortName) {
819
+ return true;
683
820
  }
684
- const type = opt.type ?? 'string';
685
- return type !== 'boolean';
686
821
  }
687
822
  return false;
688
823
  }
689
- #normalizeArgv(argv, booleanOptions) {
690
- const result = [];
691
- let seenDoubleDash = false;
692
- for (const arg of argv) {
693
- if (arg === '--') {
694
- seenDoubleDash = true;
695
- result.push(arg);
696
- continue;
697
- }
698
- if (!seenDoubleDash && arg.startsWith('--no-')) {
699
- const eqIdx = arg.indexOf('=');
700
- if (eqIdx !== -1) {
701
- const optName = arg.slice(5, eqIdx);
702
- if (booleanOptions.has(optName)) {
703
- throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
704
- }
705
- }
706
- else {
707
- const optName = arg.slice(5);
708
- if (booleanOptions.has(optName)) {
709
- result.push(`--${optName}=false`);
710
- continue;
711
- }
712
- }
713
- }
714
- result.push(arg);
715
- }
716
- return result;
717
- }
718
824
  #getCommandPath() {
719
825
  const parts = [];
720
826
  let current = this;
@@ -726,130 +832,41 @@ class Command {
726
832
  }
727
833
  return parts.join(' ') || this.#name;
728
834
  }
729
- #tryConsumeLongOption(tokens, idx, optionByLong, opts) {
730
- const token = tokens[idx];
731
- const eqIdx = token.indexOf('=');
732
- let optName;
733
- let inlineValue;
734
- if (eqIdx !== -1) {
735
- optName = token.slice(2, eqIdx);
736
- inlineValue = token.slice(eqIdx + 1);
737
- }
738
- else {
739
- optName = token.slice(2);
740
- }
741
- const opt = optionByLong.get(optName);
742
- if (!opt) {
743
- return 0;
744
- }
745
- if (opt.type === 'boolean') {
746
- if (inlineValue !== undefined) {
747
- if (inlineValue === 'true') {
748
- opts[optName] = true;
749
- }
750
- else if (inlineValue === 'false') {
751
- opts[optName] = false;
752
- }
753
- else {
754
- throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
755
- }
756
- }
757
- else {
758
- opts[optName] = true;
759
- }
760
- return 1;
761
- }
762
- let value;
763
- let consumed = 1;
764
- if (inlineValue !== undefined) {
765
- value = inlineValue;
766
- }
767
- else if (idx + 1 < tokens.length) {
768
- value = tokens[idx + 1];
769
- consumed = 2;
770
- }
771
- else {
772
- throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
773
- }
774
- this.#applyValue(opt, value, opts);
775
- return consumed;
776
- }
777
- #tryConsumeShortOption(tokens, idx, optionByShort, opts) {
778
- const token = tokens[idx];
779
- if (token.includes('=')) {
780
- const firstFlag = token[1];
781
- if (!optionByShort.has(firstFlag)) {
782
- return { consumed: false, nextIdx: idx + 1 };
783
- }
784
- throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
785
- }
786
- const flags = token.slice(1);
787
- let j = 0;
788
- const consumedFlags = [];
789
- const unconsumedFlags = [];
790
- let nextIdx = idx + 1;
791
- while (j < flags.length) {
792
- const flag = flags[j];
793
- const opt = optionByShort.get(flag);
794
- if (!opt) {
795
- unconsumedFlags.push(...flags.slice(j).split(''));
796
- break;
797
- }
798
- consumedFlags.push(flag);
799
- if (opt.type === 'boolean') {
800
- opts[opt.long] = true;
801
- j += 1;
802
- continue;
803
- }
804
- if (j < flags.length - 1) {
805
- throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
806
- }
807
- if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
808
- const value = tokens[idx + 1];
809
- this.#applyValue(opt, value, opts);
810
- nextIdx = idx + 2;
811
- }
812
- else {
813
- throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
814
- }
815
- j += 1;
816
- }
817
- if (consumedFlags.length > 0) {
818
- const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
819
- return { consumed: true, nextIdx, remainingToken };
820
- }
821
- return { consumed: false, nextIdx: idx + 1 };
822
- }
823
835
  }
824
836
 
837
+ function camelToKebabCase(str) {
838
+ return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
839
+ }
825
840
  class CompletionCommand extends Command {
826
841
  constructor(root, config) {
827
842
  const paths = config.paths;
828
- const programName = config.programName ?? root.name;
829
- super({
830
- description: 'Generate shell completion script',
831
- });
843
+ const programName = config.programName ?? root.name ?? 'program';
844
+ super({ desc: 'Generate shell completion script' });
832
845
  this.option({
833
846
  long: 'bash',
834
847
  type: 'boolean',
835
- description: 'Generate Bash completion script',
848
+ args: 'none',
849
+ desc: 'Generate Bash completion script',
836
850
  })
837
851
  .option({
838
852
  long: 'fish',
839
853
  type: 'boolean',
840
- description: 'Generate Fish completion script',
854
+ args: 'none',
855
+ desc: 'Generate Fish completion script',
841
856
  })
842
857
  .option({
843
858
  long: 'pwsh',
844
859
  type: 'boolean',
845
- description: 'Generate PowerShell completion script',
860
+ args: 'none',
861
+ desc: 'Generate PowerShell completion script',
846
862
  })
847
863
  .option({
848
864
  long: 'write',
849
865
  short: 'w',
850
866
  type: 'string',
851
- description: 'Write to file (default path if no value given)',
852
- resolver: argv => resolveOptionalStringOption(argv, 'write', 'w'),
867
+ args: 'required',
868
+ desc: 'Write to file (use shell default path if empty)',
869
+ default: undefined,
853
870
  })
854
871
  .action(({ opts }) => {
855
872
  const meta = root.getCompletionMeta();
@@ -905,45 +922,6 @@ function expandHome(filepath) {
905
922
  }
906
923
  return filepath;
907
924
  }
908
- function resolveOptionalStringOption(argv, longName, shortName) {
909
- const remaining = [];
910
- let value;
911
- for (let i = 0; i < argv.length; i++) {
912
- const arg = argv[i];
913
- if (arg.startsWith(`--${longName}=`)) {
914
- value = arg.slice(`--${longName}=`.length);
915
- continue;
916
- }
917
- if (arg === `--${longName}`) {
918
- const next = argv[i + 1];
919
- if (next !== undefined && !next.startsWith('-')) {
920
- value = next;
921
- i += 1;
922
- }
923
- else {
924
- value = '';
925
- }
926
- continue;
927
- }
928
- if (arg.startsWith(`-${shortName}=`)) {
929
- value = arg.slice(`-${shortName}=`.length);
930
- continue;
931
- }
932
- if (arg === `-${shortName}`) {
933
- const next = argv[i + 1];
934
- if (next !== undefined && !next.startsWith('-')) {
935
- value = next;
936
- i += 1;
937
- }
938
- else {
939
- value = '';
940
- }
941
- continue;
942
- }
943
- remaining.push(arg);
944
- }
945
- return { value, remaining };
946
- }
947
925
  class BashCompletion {
948
926
  #meta;
949
927
  #programName;
@@ -976,11 +954,12 @@ class BashCompletion {
976
954
  const lines = [];
977
955
  const optParts = [];
978
956
  for (const opt of cmd.options) {
957
+ const kebabLong = camelToKebabCase(opt.long);
979
958
  if (opt.short)
980
959
  optParts.push(`-${opt.short}`);
981
- optParts.push(`--${opt.long}`);
960
+ optParts.push(`--${kebabLong}`);
982
961
  if (!opt.takesValue) {
983
- optParts.push(`--no-${opt.long}`);
962
+ optParts.push(`--no-${kebabLong}`);
984
963
  }
985
964
  }
986
965
  const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
@@ -1029,13 +1008,14 @@ class FishCompletion {
1029
1008
  const isRoot = parentPath.length === 0;
1030
1009
  const condition = this.#buildCondition(parentPath);
1031
1010
  for (const opt of cmd.options) {
1011
+ const kebabLong = camelToKebabCase(opt.long);
1032
1012
  let line = `complete -c ${this.#programName}`;
1033
1013
  if (condition)
1034
1014
  line += ` -n '${condition}'`;
1035
1015
  if (opt.short)
1036
1016
  line += ` -s ${opt.short}`;
1037
- line += ` -l ${opt.long}`;
1038
- line += ` -d '${this.#escape(opt.description)}'`;
1017
+ line += ` -l ${kebabLong}`;
1018
+ line += ` -d '${this.#escape(opt.desc)}'`;
1039
1019
  if (opt.choices && opt.choices.length > 0) {
1040
1020
  line += ` -xa '${opt.choices.join(' ')}'`;
1041
1021
  }
@@ -1044,8 +1024,8 @@ class FishCompletion {
1044
1024
  let noLine = `complete -c ${this.#programName}`;
1045
1025
  if (condition)
1046
1026
  noLine += ` -n '${condition}'`;
1047
- noLine += ` -l no-${opt.long}`;
1048
- noLine += ` -d '${this.#escape(opt.description)}'`;
1027
+ noLine += ` -l no-${kebabLong}`;
1028
+ noLine += ` -d '${this.#escape(opt.desc)}'`;
1049
1029
  lines.push(noLine);
1050
1030
  }
1051
1031
  }
@@ -1058,7 +1038,7 @@ class FishCompletion {
1058
1038
  line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
1059
1039
  }
1060
1040
  line += ` -a ${sub.name}`;
1061
- line += ` -d '${this.#escape(sub.description)}'`;
1041
+ line += ` -d '${this.#escape(sub.desc)}'`;
1062
1042
  lines.push(line);
1063
1043
  for (const alias of sub.aliases) {
1064
1044
  let aliasLine = `complete -c ${this.#programName}`;
@@ -1131,7 +1111,7 @@ class PwshCompletion {
1131
1111
  ' "--$($opt.long)",',
1132
1112
  ' $opt.long,',
1133
1113
  ' "ParameterName",',
1134
- ' $opt.description',
1114
+ ' $opt.desc',
1135
1115
  ' )',
1136
1116
  ' }',
1137
1117
  ' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
@@ -1139,7 +1119,7 @@ class PwshCompletion {
1139
1119
  ' "--no-$($opt.long)",',
1140
1120
  ' "no-$($opt.long)",',
1141
1121
  ' "ParameterName",',
1142
- ' $opt.description',
1122
+ ' $opt.desc',
1143
1123
  ' )',
1144
1124
  ' }',
1145
1125
  ' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
@@ -1147,7 +1127,7 @@ class PwshCompletion {
1147
1127
  ' "-$($opt.short)",',
1148
1128
  ' $opt.short,',
1149
1129
  ' "ParameterName",',
1150
- ' $opt.description',
1130
+ ' $opt.desc',
1151
1131
  ' )',
1152
1132
  ' }',
1153
1133
  ' }',
@@ -1161,7 +1141,7 @@ class PwshCompletion {
1161
1141
  ' $sub,',
1162
1142
  ' $sub,',
1163
1143
  ' "Command",',
1164
- ' $cmd.subcommands[$sub].description',
1144
+ ' $cmd.subcommands[$sub].desc',
1165
1145
  ' )',
1166
1146
  ' }',
1167
1147
  ' }',
@@ -1175,14 +1155,15 @@ class PwshCompletion {
1175
1155
  }
1176
1156
  #generateCommandHash(cmd, indent) {
1177
1157
  const lines = [];
1178
- lines.push(`${indent}description = '${this.#escape(cmd.description)}'`);
1158
+ lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
1179
1159
  lines.push(`${indent}options = @(`);
1180
1160
  for (const opt of cmd.options) {
1161
+ const kebabLong = camelToKebabCase(opt.long);
1181
1162
  lines.push(`${indent} @{`);
1182
1163
  if (opt.short)
1183
1164
  lines.push(`${indent} short = '${opt.short}'`);
1184
- lines.push(`${indent} long = '${opt.long}'`);
1185
- lines.push(`${indent} description = '${this.#escape(opt.description)}'`);
1165
+ lines.push(`${indent} long = '${kebabLong}'`);
1166
+ lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
1186
1167
  lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
1187
1168
  if (opt.choices) {
1188
1169
  lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);