@guanghechen/commander 3.3.0 → 4.1.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
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var reporter = require('@guanghechen/reporter');
3
4
  var fs = require('node:fs');
4
5
  var path = require('node:path');
5
6
 
@@ -37,55 +38,131 @@ class CommanderError extends Error {
37
38
  }
38
39
  }
39
40
 
40
- class DefaultReporter {
41
- debug(message, ...args) {
42
- console.debug(message, ...args);
41
+ const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
42
+ const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
43
+ function kebabToCamelCase(str) {
44
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
45
+ }
46
+ function camelToKebabCase$1(str) {
47
+ return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
48
+ }
49
+ function tokenizeLongOption(arg, commandPath) {
50
+ const eqIdx = arg.indexOf('=');
51
+ const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
52
+ const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
53
+ if (namePart.includes('_')) {
54
+ throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
55
+ }
56
+ const lowerName = namePart.toLowerCase();
57
+ if (lowerName === '--no' || lowerName === '--no-') {
58
+ throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
43
59
  }
44
- info(message, ...args) {
45
- console.info(message, ...args);
60
+ if (lowerName.startsWith('--no-')) {
61
+ if (valuePart !== '') {
62
+ throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
63
+ }
64
+ if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
65
+ throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
66
+ }
67
+ const camelName = kebabToCamelCase(lowerName.slice(5));
68
+ return {
69
+ original: arg,
70
+ resolved: `--${camelName}=false`,
71
+ name: camelName,
72
+ type: 'long',
73
+ };
46
74
  }
47
- warn(message, ...args) {
48
- console.warn(message, ...args);
75
+ if (!LONG_OPTION_REGEX.test(lowerName)) {
76
+ throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
49
77
  }
50
- error(message, ...args) {
51
- console.error(message, ...args);
78
+ const camelName = kebabToCamelCase(lowerName.slice(2));
79
+ return {
80
+ original: arg,
81
+ resolved: `--${camelName}${valuePart}`,
82
+ name: camelName,
83
+ type: 'long',
84
+ };
85
+ }
86
+ function tokenizeShortOptions(arg, commandPath) {
87
+ if (arg.includes('=')) {
88
+ throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
89
+ }
90
+ const flags = arg.slice(1);
91
+ return flags.split('').map(flag => ({
92
+ original: `-${flag}`,
93
+ resolved: `-${flag}`,
94
+ name: flag,
95
+ type: 'short',
96
+ }));
97
+ }
98
+ function tokenize(argv, commandPath) {
99
+ const optionTokens = [];
100
+ const restArgs = [];
101
+ let passThrough = false;
102
+ for (const arg of argv) {
103
+ if (arg === '--') {
104
+ passThrough = true;
105
+ continue;
106
+ }
107
+ if (passThrough) {
108
+ restArgs.push(arg);
109
+ continue;
110
+ }
111
+ if (arg.startsWith('--')) {
112
+ optionTokens.push(tokenizeLongOption(arg, commandPath));
113
+ continue;
114
+ }
115
+ if (arg.startsWith('-') && arg.length > 1) {
116
+ optionTokens.push(...tokenizeShortOptions(arg, commandPath));
117
+ continue;
118
+ }
119
+ optionTokens.push({
120
+ original: arg,
121
+ resolved: arg,
122
+ name: '',
123
+ type: 'none',
124
+ });
52
125
  }
126
+ return { optionTokens, restArgs };
53
127
  }
54
128
  const BUILTIN_HELP_OPTION = {
55
129
  long: 'help',
56
130
  short: 'h',
57
131
  type: 'boolean',
58
- description: 'Show help information',
132
+ args: 'none',
133
+ desc: 'Show help information',
59
134
  };
60
135
  const BUILTIN_VERSION_OPTION = {
61
136
  long: 'version',
62
137
  short: 'V',
63
138
  type: 'boolean',
64
- description: 'Show version number',
139
+ args: 'none',
140
+ desc: 'Show version number',
65
141
  };
66
142
  class Command {
67
143
  #name;
68
- #description;
144
+ #desc;
69
145
  #version;
70
146
  #helpSubcommandEnabled;
71
147
  #reporter;
72
148
  #parent;
73
149
  #options = [];
74
150
  #arguments = [];
75
- #subcommands = [];
76
- #action;
151
+ #subcommandsList = [];
152
+ #subcommandsMap = new Map();
153
+ #action = undefined;
77
154
  constructor(config) {
78
155
  this.#name = config.name ?? '';
79
- this.#description = config.description;
156
+ this.#desc = config.desc;
80
157
  this.#version = config.version;
81
158
  this.#helpSubcommandEnabled = config.help ?? false;
82
159
  this.#reporter = config.reporter;
83
160
  }
84
161
  get name() {
85
- return this.#name;
162
+ return this.#name || undefined;
86
163
  }
87
164
  get description() {
88
- return this.#description;
165
+ return this.#desc;
89
166
  }
90
167
  get version() {
91
168
  return this.#version;
@@ -99,6 +176,9 @@ class Command {
99
176
  get arguments() {
100
177
  return [...this.#arguments];
101
178
  }
179
+ get subcommands() {
180
+ return new Map(this.#subcommandsMap);
181
+ }
102
182
  option(opt) {
103
183
  this.#validateOptionConfig(opt);
104
184
  this.#checkOptionUniqueness(opt);
@@ -121,67 +201,59 @@ class Command {
121
201
  if (cmd.#parent && cmd.#parent !== this) {
122
202
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
123
203
  }
124
- const existing = this.#subcommands.find(e => e.command === cmd);
204
+ const existing = this.#subcommandsList.find(e => e.command === cmd);
125
205
  if (existing) {
126
206
  existing.aliases.push(name);
207
+ this.#subcommandsMap.set(name, cmd);
127
208
  }
128
209
  else {
129
210
  cmd.#name = name;
130
211
  cmd.#parent = this;
131
- this.#subcommands.push({ name, aliases: [], command: cmd });
212
+ this.#subcommandsList.push({ name, aliases: [], command: cmd });
213
+ this.#subcommandsMap.set(name, cmd);
132
214
  }
133
215
  return this;
134
216
  }
135
217
  async run(params) {
136
- const { argv, envs, reporter } = params;
218
+ const { argv, envs, reporter: reporter$1 } = params;
137
219
  try {
138
220
  const processedArgv = this.#processHelpSubcommand(argv);
139
- const { chain, remaining } = this.#routeChain(processedArgv);
221
+ const routeResult = this.#route(processedArgv);
222
+ const { chain, remaining } = routeResult;
140
223
  const leafCommand = chain[chain.length - 1];
141
224
  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);
225
+ const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
226
+ const { optionTokens, restArgs } = tokenizeResult;
146
227
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
147
228
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
148
- if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
229
+ if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
149
230
  console.log(leafCommand.formatHelp());
150
231
  return;
151
232
  }
152
- if (!hasUserVersion && leafCommand === rootCommand) {
153
- if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
154
- console.log(leafCommand.version ?? 'unknown');
233
+ if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
234
+ if (this.#hasFlag(optionTokens, 'version', 'V')) {
235
+ console.log(leafCommand.#version);
155
236
  return;
156
237
  }
157
238
  }
158
- const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
239
+ const resolveResult = this.#resolve(chain, optionTokens);
159
240
  const ctx = {
160
241
  cmd: leafCommand,
161
242
  envs,
162
- reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
243
+ reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
163
244
  argv,
164
245
  };
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 };
246
+ const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
247
+ const actionParams = {
248
+ ctx: parseResult.ctx,
249
+ opts: parseResult.opts,
250
+ args: parseResult.args,
251
+ rawArgs: parseResult.rawArgs,
252
+ };
170
253
  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
- }
254
+ await leafCommand.#runAction(actionParams);
183
255
  }
184
- else if (leafCommand.#subcommands.length > 0) {
256
+ else if (leafCommand.#subcommandsList.length > 0) {
185
257
  console.log(leafCommand.formatHelp());
186
258
  }
187
259
  else {
@@ -197,105 +269,33 @@ class Command {
197
269
  throw err;
198
270
  }
199
271
  }
200
- parse(argv) {
272
+ parse(params) {
273
+ const { argv, envs, reporter: reporter$1 } = params;
201
274
  const processedArgv = this.#processHelpSubcommand(argv);
202
- const { chain, remaining } = this.#routeChain(processedArgv);
275
+ const routeResult = this.#route(processedArgv);
276
+ const { chain, remaining } = routeResult;
203
277
  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 };
278
+ const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
279
+ const { optionTokens, restArgs } = tokenizeResult;
280
+ const resolveResult = this.#resolve(chain, optionTokens);
281
+ const ctx = {
282
+ cmd: leafCommand,
283
+ envs,
284
+ reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
285
+ argv,
286
+ };
287
+ return this.#parse(chain, resolveResult, ctx, restArgs);
288
288
  }
289
289
  formatHelp() {
290
290
  const lines = [];
291
291
  const allOptions = this.#getMergedOptions();
292
- lines.push(this.#description);
292
+ lines.push(this.#desc);
293
293
  lines.push('');
294
294
  const commandPath = this.#getCommandPath();
295
295
  let usage = `Usage: ${commandPath}`;
296
296
  if (allOptions.length > 0)
297
297
  usage += ' [options]';
298
- if (this.#subcommands.length > 0)
298
+ if (this.#subcommandsList.length > 0)
299
299
  usage += ' [command]';
300
300
  for (const arg of this.#arguments) {
301
301
  if (arg.kind === 'required') {
@@ -314,24 +314,24 @@ class Command {
314
314
  lines.push('Options:');
315
315
  const optLines = [];
316
316
  for (const opt of allOptions) {
317
+ const kebabLong = camelToKebabCase$1(opt.long);
317
318
  let sig = opt.short ? `-${opt.short}, ` : ' ';
318
- sig += `--${opt.long}`;
319
- const effectiveType = opt.type ?? 'string';
320
- if (effectiveType !== 'boolean') {
319
+ sig += `--${kebabLong}`;
320
+ if (opt.args !== 'none') {
321
321
  sig += ' <value>';
322
322
  }
323
- let desc = opt.description;
324
- if (opt.default !== undefined && effectiveType !== 'boolean') {
323
+ let desc = opt.desc;
324
+ if (opt.default !== undefined && opt.type !== 'boolean') {
325
325
  desc += ` (default: ${JSON.stringify(opt.default)})`;
326
326
  }
327
327
  if (opt.choices) {
328
328
  desc += ` [choices: ${opt.choices.join(', ')}]`;
329
329
  }
330
330
  optLines.push({ sig, desc });
331
- if (effectiveType === 'boolean') {
331
+ if (opt.type === 'boolean' && opt.args === 'none') {
332
332
  optLines.push({
333
- sig: ` --no-${opt.long}`,
334
- desc: `Negate --${opt.long}`,
333
+ sig: ` --no-${kebabLong}`,
334
+ desc: `Negate --${kebabLong}`,
335
335
  });
336
336
  }
337
337
  }
@@ -342,19 +342,19 @@ class Command {
342
342
  }
343
343
  lines.push('');
344
344
  }
345
- const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommands.length > 0;
346
- if (this.#subcommands.length > 0) {
345
+ const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
346
+ if (this.#subcommandsList.length > 0) {
347
347
  lines.push('Commands:');
348
348
  const cmdLines = [];
349
349
  if (showHelpSubcommand) {
350
350
  cmdLines.push({ name: 'help', desc: 'Show help for a command' });
351
351
  }
352
- for (const entry of this.#subcommands) {
352
+ for (const entry of this.#subcommandsList) {
353
353
  let name = entry.name;
354
354
  if (entry.aliases.length > 0) {
355
355
  name += `, ${entry.aliases.join(', ')}`;
356
356
  }
357
- cmdLines.push({ name, desc: entry.command.#description });
357
+ cmdLines.push({ name, desc: entry.command.#desc });
358
358
  }
359
359
  const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
360
360
  for (const { name, desc } of cmdLines) {
@@ -369,21 +369,20 @@ class Command {
369
369
  const allOptions = this.#getMergedOptions();
370
370
  const options = [];
371
371
  for (const opt of allOptions) {
372
- const effectiveType = opt.type ?? 'string';
373
372
  options.push({
374
373
  long: opt.long,
375
374
  short: opt.short,
376
- description: opt.description,
377
- takesValue: effectiveType !== 'boolean',
375
+ desc: opt.desc,
376
+ takesValue: opt.args !== 'none',
378
377
  choices: opt.choices,
379
378
  });
380
379
  }
381
380
  return {
382
381
  name: this.#name,
383
- description: this.#description,
382
+ desc: this.#desc,
384
383
  aliases: [],
385
384
  options,
386
- subcommands: this.#subcommands.map(entry => {
385
+ subcommands: this.#subcommandsList.map(entry => {
387
386
  const subMeta = entry.command.getCompletionMeta();
388
387
  return {
389
388
  ...subMeta,
@@ -398,17 +397,17 @@ class Command {
398
397
  return argv;
399
398
  if (argv.length < 1 || argv[0] !== 'help')
400
399
  return argv;
401
- if (argv.length === 1 || this.#subcommands.length === 0) {
400
+ if (argv.length === 1 || this.#subcommandsList.length === 0) {
402
401
  return ['--help'];
403
402
  }
404
403
  const subName = argv[1];
405
- const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
404
+ const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
406
405
  if (entry) {
407
406
  return [subName, '--help', ...argv.slice(2)];
408
407
  }
409
408
  return argv;
410
409
  }
411
- #routeChain(argv) {
410
+ #route(argv) {
412
411
  const chain = [this];
413
412
  let current = this;
414
413
  let idx = 0;
@@ -416,7 +415,7 @@ class Command {
416
415
  const token = argv[idx];
417
416
  if (token.startsWith('-'))
418
417
  break;
419
- const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
418
+ const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
420
419
  if (!entry)
421
420
  break;
422
421
  current = entry.command;
@@ -425,93 +424,299 @@ class Command {
425
424
  }
426
425
  return { chain, remaining: argv.slice(idx) };
427
426
  }
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();
427
+ #resolve(chain, tokens) {
428
+ const consumedTokens = new Map();
440
429
  let remaining = [...tokens];
441
- const rootCommand = chain[0];
442
430
  const shadowed = new Set();
443
431
  for (let i = chain.length - 1; i >= 0; i--) {
444
432
  const cmd = chain[i];
445
- const includeVersion = cmd === rootCommand && includeRootVersion;
446
- const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
447
- optsMap.set(cmd, result.opts);
433
+ const includeVersion = i === 0;
434
+ const result = cmd.#shift(remaining, shadowed, includeVersion);
435
+ consumedTokens.set(cmd, result.consumed);
448
436
  remaining = result.remaining;
449
437
  for (const opt of cmd.#options) {
450
438
  shadowed.add(opt.long);
451
439
  }
452
440
  }
453
- const positionalArgs = [];
441
+ const argTokens = [];
454
442
  for (const token of remaining) {
455
- if (token.startsWith('-')) {
443
+ if (token.type !== 'none') {
456
444
  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());
445
+ throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
446
+ }
447
+ argTokens.push(token);
448
+ }
449
+ return { consumedTokens, argTokens };
450
+ }
451
+ #shift(tokens, shadowed, includeVersion) {
452
+ const allOptions = this.#getMergedOptions(includeVersion);
453
+ const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
454
+ const optionByLong = new Map();
455
+ const optionByShort = new Map();
456
+ for (const opt of effectiveOptions) {
457
+ optionByLong.set(opt.long, opt);
458
+ if (opt.short) {
459
+ optionByShort.set(opt.short, opt);
460
+ }
461
+ }
462
+ const consumed = [];
463
+ const remaining = [];
464
+ let i = 0;
465
+ while (i < tokens.length) {
466
+ const token = tokens[i];
467
+ if (token.type === 'long') {
468
+ const opt = optionByLong.get(token.name);
469
+ if (opt) {
470
+ consumed.push(token);
471
+ if (opt.args === 'required') {
472
+ if (!token.resolved.includes('=') && i + 1 < tokens.length) {
473
+ i += 1;
474
+ consumed.push(tokens[i]);
475
+ }
476
+ }
477
+ else if (opt.args === 'variadic') {
478
+ if (!token.resolved.includes('=')) {
479
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
480
+ i += 1;
481
+ consumed.push(tokens[i]);
482
+ }
483
+ }
484
+ }
485
+ i += 1;
486
+ continue;
460
487
  }
461
- throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
488
+ remaining.push(token);
489
+ i += 1;
490
+ continue;
462
491
  }
463
- positionalArgs.push(token);
492
+ if (token.type === 'short') {
493
+ const opt = optionByShort.get(token.name);
494
+ if (opt) {
495
+ consumed.push(token);
496
+ if (opt.args === 'required') {
497
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
498
+ i += 1;
499
+ consumed.push(tokens[i]);
500
+ }
501
+ }
502
+ else if (opt.args === 'variadic') {
503
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
504
+ i += 1;
505
+ consumed.push(tokens[i]);
506
+ }
507
+ }
508
+ i += 1;
509
+ continue;
510
+ }
511
+ remaining.push(token);
512
+ i += 1;
513
+ continue;
514
+ }
515
+ remaining.push(token);
516
+ i += 1;
464
517
  }
465
- return { optsMap, positionalArgs };
518
+ return { consumed, remaining };
466
519
  }
467
- #applyChain(chain, optsMap, ctx) {
468
- for (const cmd of chain) {
469
- const opts = optsMap.get(cmd) ?? {};
470
- for (const opt of cmd.#getMergedOptions()) {
520
+ #parse(chain, resolveResult, ctx, restArgs) {
521
+ const { consumedTokens, argTokens } = resolveResult;
522
+ const leafCommand = chain[chain.length - 1];
523
+ this.#validateMergedShortOptions(chain);
524
+ const optsMap = new Map();
525
+ for (let i = 0; i < chain.length; i++) {
526
+ const cmd = chain[i];
527
+ const includeVersion = i === 0;
528
+ const tokens = consumedTokens.get(cmd) ?? [];
529
+ const opts = cmd.#parseOptions(tokens, includeVersion);
530
+ optsMap.set(cmd, opts);
531
+ for (const opt of cmd.#getMergedOptions(includeVersion)) {
471
532
  if (opt.apply && opts[opt.long] !== undefined) {
472
533
  opt.apply(opts[opt.long], ctx);
473
534
  }
474
535
  }
475
536
  }
476
- }
477
- #mergeOpts(chain, optsMap) {
478
- const merged = {};
537
+ const mergedOpts = {};
479
538
  for (const cmd of chain) {
480
- Object.assign(merged, optsMap.get(cmd) ?? {});
539
+ Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
481
540
  }
482
- return merged;
541
+ const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
542
+ const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
543
+ return { ctx, opts: mergedOpts, args, rawArgs };
483
544
  }
484
- #applyValue(opt, rawValue, opts) {
485
- const type = opt.type ?? 'string';
486
- let parsedValue = rawValue;
487
- if (opt.coerce) {
488
- parsedValue = opt.coerce(rawValue);
545
+ #parseOptions(tokens, includeVersion) {
546
+ const allOptions = this.#getMergedOptions(includeVersion);
547
+ const opts = {};
548
+ for (const opt of allOptions) {
549
+ if (opt.default !== undefined) {
550
+ opts[opt.long] = opt.default;
551
+ }
552
+ else if (opt.type === 'boolean' && opt.args === 'none') {
553
+ opts[opt.long] = false;
554
+ }
555
+ else if (opt.args === 'variadic') {
556
+ opts[opt.long] = [];
557
+ }
489
558
  }
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());
559
+ const optionByLong = new Map();
560
+ const optionByShort = new Map();
561
+ for (const opt of allOptions) {
562
+ optionByLong.set(opt.long, opt);
563
+ if (opt.short) {
564
+ optionByShort.set(opt.short, opt);
565
+ }
566
+ }
567
+ let i = 0;
568
+ while (i < tokens.length) {
569
+ const token = tokens[i];
570
+ const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
571
+ if (!opt) {
572
+ i += 1;
573
+ continue;
574
+ }
575
+ const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
576
+ if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
577
+ throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
578
+ }
579
+ if (opt.type === 'boolean' && opt.args === 'none') {
580
+ const eqIdx = token.resolved.indexOf('=');
581
+ if (eqIdx !== -1) {
582
+ const value = token.resolved.slice(eqIdx + 1);
583
+ if (value === 'true') {
584
+ opts[opt.long] = true;
501
585
  }
502
- parsedValue = num;
503
- break;
586
+ else if (value === 'false') {
587
+ opts[opt.long] = false;
588
+ }
589
+ else {
590
+ throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
591
+ }
592
+ }
593
+ else {
594
+ opts[opt.long] = true;
595
+ }
596
+ i += 1;
597
+ continue;
598
+ }
599
+ if (opt.args === 'required') {
600
+ const eqIdx = token.resolved.indexOf('=');
601
+ let rawValue;
602
+ if (eqIdx !== -1) {
603
+ rawValue = token.resolved.slice(eqIdx + 1);
604
+ }
605
+ else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
606
+ rawValue = tokens[i + 1].original;
607
+ i += 1;
504
608
  }
609
+ else {
610
+ throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
611
+ }
612
+ opts[opt.long] = this.#convertValue(opt, rawValue);
613
+ i += 1;
614
+ continue;
615
+ }
616
+ if (opt.args === 'variadic') {
617
+ const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
618
+ const eqIdx = token.resolved.indexOf('=');
619
+ if (eqIdx !== -1) {
620
+ values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
621
+ }
622
+ else {
623
+ while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
624
+ i += 1;
625
+ values.push(this.#convertValue(opt, tokens[i].original));
626
+ }
627
+ }
628
+ opts[opt.long] = values;
629
+ i += 1;
630
+ continue;
505
631
  }
632
+ i += 1;
506
633
  }
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];
634
+ for (const opt of allOptions) {
635
+ if (opt.required && opts[opt.long] === undefined) {
636
+ throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
637
+ }
511
638
  }
512
- else {
513
- opts[opt.long] = parsedValue;
639
+ for (const opt of allOptions) {
640
+ if (opt.choices && opts[opt.long] !== undefined) {
641
+ const value = opts[opt.long];
642
+ const values = Array.isArray(value) ? value : [value];
643
+ const choices = opt.choices;
644
+ for (const v of values) {
645
+ if (!choices.includes(v)) {
646
+ throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
647
+ }
648
+ }
649
+ }
650
+ }
651
+ return opts;
652
+ }
653
+ #convertValue(opt, rawValue) {
654
+ if (opt.coerce) {
655
+ return opt.coerce(rawValue);
514
656
  }
657
+ if (opt.type === 'number') {
658
+ const num = Number(rawValue);
659
+ if (Number.isNaN(num)) {
660
+ throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
661
+ }
662
+ return num;
663
+ }
664
+ return rawValue;
665
+ }
666
+ #parseArguments(rawArgs) {
667
+ const argumentDefs = this.#arguments;
668
+ const args = {};
669
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
670
+ if (rawArgs.length < requiredCount) {
671
+ const missing = argumentDefs
672
+ .filter(a => a.kind === 'required')
673
+ .slice(rawArgs.length)
674
+ .map(a => a.name);
675
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
676
+ }
677
+ let index = 0;
678
+ for (const def of argumentDefs) {
679
+ if (def.kind === 'variadic') {
680
+ const rest = rawArgs.slice(index);
681
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
682
+ index = rawArgs.length;
683
+ break;
684
+ }
685
+ const raw = rawArgs[index];
686
+ if (raw === undefined) {
687
+ if (def.kind === 'optional') {
688
+ args[def.name] = def.default ?? undefined;
689
+ continue;
690
+ }
691
+ }
692
+ else {
693
+ args[def.name] = this.#convertArgument(def, raw);
694
+ index += 1;
695
+ }
696
+ }
697
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
698
+ if (!hasVariadic && index < rawArgs.length) {
699
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
700
+ }
701
+ return { args, rawArgs };
702
+ }
703
+ #convertArgument(def, raw) {
704
+ if (def.coerce) {
705
+ try {
706
+ return def.coerce(raw);
707
+ }
708
+ catch {
709
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
710
+ }
711
+ }
712
+ if (def.type === 'number') {
713
+ const n = Number(raw);
714
+ if (Number.isNaN(n)) {
715
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
716
+ }
717
+ return n;
718
+ }
719
+ return raw;
515
720
  }
516
721
  #getMergedOptions(includeVersion = !this.#parent) {
517
722
  const optionMap = new Map();
@@ -528,11 +733,11 @@ class Command {
528
733
  }
529
734
  return Array.from(optionMap.values());
530
735
  }
531
- #validateMergedShortOptions(chain, includeRootVersion) {
736
+ #validateMergedShortOptions(chain) {
532
737
  const mergedByLong = new Map();
533
- const rootCommand = chain[0];
534
- for (const cmd of chain) {
535
- const includeVersion = cmd === rootCommand && includeRootVersion;
738
+ for (let i = 0; i < chain.length; i++) {
739
+ const cmd = chain[i];
740
+ const includeVersion = i === 0;
536
741
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
537
742
  mergedByLong.set(opt.long, opt);
538
743
  }
@@ -549,8 +754,17 @@ class Command {
549
754
  }
550
755
  }
551
756
  #validateOptionConfig(opt) {
552
- if (opt.long.startsWith('no-')) {
553
- throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
757
+ if (opt.type === 'boolean' && opt.args !== 'none') {
758
+ throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
759
+ }
760
+ if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
761
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
762
+ }
763
+ if (opt.long.startsWith('no')) {
764
+ throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
765
+ }
766
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
767
+ throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
554
768
  }
555
769
  if (opt.required && opt.default !== undefined) {
556
770
  throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
@@ -589,154 +803,33 @@ class Command {
589
803
  }
590
804
  }
591
805
  }
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());
806
+ async #runAction(params) {
807
+ if (!this.#action)
808
+ return;
809
+ try {
810
+ await this.#action(params);
602
811
  }
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
- }
812
+ catch (err) {
813
+ if (err instanceof Error) {
814
+ console.error(`Error: ${err.message}`);
617
815
  }
618
816
  else {
619
- args[def.name] = this.#convertArgument(def, raw);
620
- index += 1;
621
- }
622
- }
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
- }
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);
817
+ console.error('Error: action failed');
660
818
  }
819
+ process.exit(1);
661
820
  }
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
821
  }
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}`)) {
822
+ #hasFlag(tokens, longName, shortName) {
823
+ for (const token of tokens) {
824
+ if (token.type === 'long' && token.name === longName) {
679
825
  return true;
680
826
  }
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;
827
+ if (token.type === 'short' && token.name === shortName) {
828
+ return true;
705
829
  }
706
- const type = opt.type ?? 'string';
707
- return type !== 'boolean';
708
830
  }
709
831
  return false;
710
832
  }
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
833
  #getCommandPath() {
741
834
  const parts = [];
742
835
  let current = this;
@@ -748,130 +841,41 @@ class Command {
748
841
  }
749
842
  return parts.join(' ') || this.#name;
750
843
  }
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
844
  }
846
845
 
846
+ function camelToKebabCase(str) {
847
+ return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
848
+ }
847
849
  class CompletionCommand extends Command {
848
850
  constructor(root, config) {
849
851
  const paths = config.paths;
850
- const programName = config.programName ?? root.name;
851
- super({
852
- description: 'Generate shell completion script',
853
- });
852
+ const programName = config.programName ?? root.name ?? 'program';
853
+ super({ desc: 'Generate shell completion script' });
854
854
  this.option({
855
855
  long: 'bash',
856
856
  type: 'boolean',
857
- description: 'Generate Bash completion script',
857
+ args: 'none',
858
+ desc: 'Generate Bash completion script',
858
859
  })
859
860
  .option({
860
861
  long: 'fish',
861
862
  type: 'boolean',
862
- description: 'Generate Fish completion script',
863
+ args: 'none',
864
+ desc: 'Generate Fish completion script',
863
865
  })
864
866
  .option({
865
867
  long: 'pwsh',
866
868
  type: 'boolean',
867
- description: 'Generate PowerShell completion script',
869
+ args: 'none',
870
+ desc: 'Generate PowerShell completion script',
868
871
  })
869
872
  .option({
870
873
  long: 'write',
871
874
  short: 'w',
872
875
  type: 'string',
873
- description: 'Write to file (default path if no value given)',
874
- resolver: argv => resolveOptionalStringOption(argv, 'write', 'w'),
876
+ args: 'required',
877
+ desc: 'Write to file (use shell default path if empty)',
878
+ default: undefined,
875
879
  })
876
880
  .action(({ opts }) => {
877
881
  const meta = root.getCompletionMeta();
@@ -927,45 +931,6 @@ function expandHome(filepath) {
927
931
  }
928
932
  return filepath;
929
933
  }
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
934
  class BashCompletion {
970
935
  #meta;
971
936
  #programName;
@@ -998,11 +963,12 @@ class BashCompletion {
998
963
  const lines = [];
999
964
  const optParts = [];
1000
965
  for (const opt of cmd.options) {
966
+ const kebabLong = camelToKebabCase(opt.long);
1001
967
  if (opt.short)
1002
968
  optParts.push(`-${opt.short}`);
1003
- optParts.push(`--${opt.long}`);
969
+ optParts.push(`--${kebabLong}`);
1004
970
  if (!opt.takesValue) {
1005
- optParts.push(`--no-${opt.long}`);
971
+ optParts.push(`--no-${kebabLong}`);
1006
972
  }
1007
973
  }
1008
974
  const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
@@ -1051,13 +1017,14 @@ class FishCompletion {
1051
1017
  const isRoot = parentPath.length === 0;
1052
1018
  const condition = this.#buildCondition(parentPath);
1053
1019
  for (const opt of cmd.options) {
1020
+ const kebabLong = camelToKebabCase(opt.long);
1054
1021
  let line = `complete -c ${this.#programName}`;
1055
1022
  if (condition)
1056
1023
  line += ` -n '${condition}'`;
1057
1024
  if (opt.short)
1058
1025
  line += ` -s ${opt.short}`;
1059
- line += ` -l ${opt.long}`;
1060
- line += ` -d '${this.#escape(opt.description)}'`;
1026
+ line += ` -l ${kebabLong}`;
1027
+ line += ` -d '${this.#escape(opt.desc)}'`;
1061
1028
  if (opt.choices && opt.choices.length > 0) {
1062
1029
  line += ` -xa '${opt.choices.join(' ')}'`;
1063
1030
  }
@@ -1066,8 +1033,8 @@ class FishCompletion {
1066
1033
  let noLine = `complete -c ${this.#programName}`;
1067
1034
  if (condition)
1068
1035
  noLine += ` -n '${condition}'`;
1069
- noLine += ` -l no-${opt.long}`;
1070
- noLine += ` -d '${this.#escape(opt.description)}'`;
1036
+ noLine += ` -l no-${kebabLong}`;
1037
+ noLine += ` -d '${this.#escape(opt.desc)}'`;
1071
1038
  lines.push(noLine);
1072
1039
  }
1073
1040
  }
@@ -1080,7 +1047,7 @@ class FishCompletion {
1080
1047
  line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
1081
1048
  }
1082
1049
  line += ` -a ${sub.name}`;
1083
- line += ` -d '${this.#escape(sub.description)}'`;
1050
+ line += ` -d '${this.#escape(sub.desc)}'`;
1084
1051
  lines.push(line);
1085
1052
  for (const alias of sub.aliases) {
1086
1053
  let aliasLine = `complete -c ${this.#programName}`;
@@ -1153,7 +1120,7 @@ class PwshCompletion {
1153
1120
  ' "--$($opt.long)",',
1154
1121
  ' $opt.long,',
1155
1122
  ' "ParameterName",',
1156
- ' $opt.description',
1123
+ ' $opt.desc',
1157
1124
  ' )',
1158
1125
  ' }',
1159
1126
  ' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
@@ -1161,7 +1128,7 @@ class PwshCompletion {
1161
1128
  ' "--no-$($opt.long)",',
1162
1129
  ' "no-$($opt.long)",',
1163
1130
  ' "ParameterName",',
1164
- ' $opt.description',
1131
+ ' $opt.desc',
1165
1132
  ' )',
1166
1133
  ' }',
1167
1134
  ' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
@@ -1169,7 +1136,7 @@ class PwshCompletion {
1169
1136
  ' "-$($opt.short)",',
1170
1137
  ' $opt.short,',
1171
1138
  ' "ParameterName",',
1172
- ' $opt.description',
1139
+ ' $opt.desc',
1173
1140
  ' )',
1174
1141
  ' }',
1175
1142
  ' }',
@@ -1183,7 +1150,7 @@ class PwshCompletion {
1183
1150
  ' $sub,',
1184
1151
  ' $sub,',
1185
1152
  ' "Command",',
1186
- ' $cmd.subcommands[$sub].description',
1153
+ ' $cmd.subcommands[$sub].desc',
1187
1154
  ' )',
1188
1155
  ' }',
1189
1156
  ' }',
@@ -1197,14 +1164,15 @@ class PwshCompletion {
1197
1164
  }
1198
1165
  #generateCommandHash(cmd, indent) {
1199
1166
  const lines = [];
1200
- lines.push(`${indent}description = '${this.#escape(cmd.description)}'`);
1167
+ lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
1201
1168
  lines.push(`${indent}options = @(`);
1202
1169
  for (const opt of cmd.options) {
1170
+ const kebabLong = camelToKebabCase(opt.long);
1203
1171
  lines.push(`${indent} @{`);
1204
1172
  if (opt.short)
1205
1173
  lines.push(`${indent} short = '${opt.short}'`);
1206
- lines.push(`${indent} long = '${opt.long}'`);
1207
- lines.push(`${indent} description = '${this.#escape(opt.description)}'`);
1174
+ lines.push(`${indent} long = '${kebabLong}'`);
1175
+ lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
1208
1176
  lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
1209
1177
  if (opt.choices) {
1210
1178
  lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
@@ -1233,9 +1201,37 @@ class PwshCompletion {
1233
1201
  }
1234
1202
  }
1235
1203
 
1204
+ const logLevelOption = {
1205
+ long: 'logLevel',
1206
+ type: 'string',
1207
+ args: 'required',
1208
+ desc: 'Set log level',
1209
+ default: 'info',
1210
+ choices: reporter.LOG_LEVELS,
1211
+ coerce: (raw) => {
1212
+ const level = reporter.resolveLogLevel(raw);
1213
+ if (level === undefined) {
1214
+ throw new Error(`Invalid log level: ${raw}`);
1215
+ }
1216
+ return level;
1217
+ },
1218
+ apply: (value, ctx) => {
1219
+ ctx.reporter.setLevel(value);
1220
+ },
1221
+ };
1222
+ const silentOption = {
1223
+ long: 'silent',
1224
+ type: 'boolean',
1225
+ args: 'none',
1226
+ desc: 'Suppress non-error output',
1227
+ default: false,
1228
+ };
1229
+
1236
1230
  exports.BashCompletion = BashCompletion;
1237
1231
  exports.Command = Command;
1238
1232
  exports.CommanderError = CommanderError;
1239
1233
  exports.CompletionCommand = CompletionCommand;
1240
1234
  exports.FishCompletion = FishCompletion;
1241
1235
  exports.PwshCompletion = PwshCompletion;
1236
+ exports.logLevelOption = logLevelOption;
1237
+ exports.silentOption = silentOption;