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