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