@guanghechen/commander 3.2.0 → 4.0.0

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