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