clap-ts 0.2.0 → 0.3.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/dist/runner.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Supports inferSubcommands, subcommandRequired, allowExternalSubcommands,
5
5
  * argsConflictsWithSubcommands, argRequiredElseHelp, and custom styles.
6
6
  */
7
- import { CliParseError, collectGlobalArgs, getRawArgs, mergeGlobalArgs, parseArgs, } from './parser.js';
7
+ import { CliParseError, coerceValue, collectGlobalArgs, getRawArgs, hasSubCommands, kebabToCamel, mergeGlobalArgs, parseArgs, subCommandsOf, } from './parser.js';
8
8
  import { validate } from './validation.js';
9
9
  import { showError, showHelp, showVersion } from './help.js';
10
10
  // ---- defineCommand ----
@@ -53,104 +53,199 @@ export function defineArgs(args) {
53
53
  export function defineArg(arg) {
54
54
  return arg;
55
55
  }
56
- // ---- Subcommand Resolution ----
56
+ // ---- Config Layer ----
57
57
  /**
58
- * Find a subcommand by prefix matching (inferSubcommands).
59
- * Returns the match if exactly one, 'ambiguous' if multiple, undefined if none.
58
+ * The config section that applies to a command, built by walking the path from
59
+ * the root. A nested object named for a subcommand scopes its contents to that
60
+ * command; scalar keys stay in scope all the way down.
60
61
  */
61
- function findSubcommandByPrefix(subCommands, token) {
62
- const matches = [];
63
- for (const [name, def] of Object.entries(subCommands)) {
64
- if (name.startsWith(token)) {
65
- matches.push({ name, def });
62
+ function configForPath(config, path) {
63
+ let scope = config;
64
+ const merged = {};
65
+ const takeScalars = (from) => {
66
+ for (const key of Object.keys(from)) {
67
+ const value = from[key];
68
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
69
+ continue;
70
+ }
71
+ merged[key] = value;
66
72
  }
67
- else if (def.meta.aliases?.some((a) => a.startsWith(token))) {
68
- matches.push({ name, def });
73
+ };
74
+ takeScalars(scope);
75
+ for (const segment of path) {
76
+ const next = scope[segment];
77
+ if (next === null || typeof next !== 'object' || Array.isArray(next)) {
78
+ break;
69
79
  }
80
+ scope = next;
81
+ takeScalars(scope);
70
82
  }
71
- if (matches.length === 1) {
72
- return matches[0];
73
- }
74
- if (matches.length > 1) {
75
- return 'ambiguous';
76
- }
77
- return undefined;
83
+ return merged;
78
84
  }
79
85
  /**
80
- * Resolve the command chain from the root command and raw args.
81
- * Supports inferSubcommands for prefix matching.
86
+ * Fill args the command line and environment left alone from the config.
87
+ * A default already applied during parsing loses to a config value, which is
88
+ * what makes the precedence CLI > env > config > default.
82
89
  */
83
- function resolveCommandChain(rootCommand, rawArgs) {
84
- let current = rootCommand;
85
- const parentNames = [];
86
- const remaining = [...rawArgs];
87
- while (remaining.length > 0 && (current.subCommands || current.meta.allowExternalSubcommands)) {
88
- const token = remaining[0];
89
- // Don't interpret flags as subcommands
90
- if (token.startsWith('-')) {
90
+ function applyConfig(result, command, source, path) {
91
+ const argsDef = command.args ?? {};
92
+ // Nothing to fill means nothing to load: with a thunk, a command line that
93
+ // answered every argument never touches the filesystem.
94
+ let anyOpen = false;
95
+ for (const key of Object.keys(argsDef)) {
96
+ const from = result.valueSources.get(key);
97
+ if (from !== 'cli' && from !== 'env') {
98
+ anyOpen = true;
91
99
  break;
92
100
  }
93
- // Direct match
94
- if (current.subCommands) {
95
- const subCmd = current.subCommands[token];
96
- if (subCmd) {
97
- parentNames.push(current.meta.name);
98
- current = subCmd;
99
- remaining.shift();
100
- continue;
101
- }
102
- // Alias match
103
- const aliasMatch = findSubcommandByAlias(current.subCommands, token);
104
- if (aliasMatch) {
105
- parentNames.push(current.meta.name);
106
- current = aliasMatch;
107
- remaining.shift();
108
- continue;
109
- }
110
- // inferSubcommands: try prefix matching
111
- if (current.meta.inferSubcommands) {
112
- const prefixMatch = findSubcommandByPrefix(current.subCommands, token);
113
- if (prefixMatch === 'ambiguous') {
114
- break;
115
- }
116
- if (prefixMatch) {
117
- parentNames.push(current.meta.name);
118
- current = prefixMatch.def;
119
- remaining.shift();
120
- continue;
121
- }
122
- }
101
+ }
102
+ if (!anyOpen) {
103
+ return result;
104
+ }
105
+ const config = typeof source === 'function' ? source() : source;
106
+ if (config === undefined) {
107
+ return result;
108
+ }
109
+ const section = configForPath(config, path);
110
+ const args = { ...result.args };
111
+ const valueSources = new Map(result.valueSources);
112
+ let changed = false;
113
+ for (const key of Object.keys(argsDef)) {
114
+ const raw = section[key];
115
+ if (raw === undefined) {
116
+ continue;
117
+ }
118
+ const source = valueSources.get(key);
119
+ if (source === 'cli' || source === 'env') {
120
+ continue;
123
121
  }
124
- // allowExternalSubcommands: accept unknown subcommand and stop
125
- if (current.meta.allowExternalSubcommands) {
126
- remaining.shift();
127
- return { command: current, remainingArgs: remaining, parentNames, externalSubcommand: token };
122
+ const def = argsDef[key];
123
+ const name = def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
124
+ const coerce = (value) => coerceValue(String(value), def, `config:${name}`);
125
+ args[key] = Array.isArray(raw) ? raw.map((v) => String(coerce(v))) : coerce(raw);
126
+ args[kebabToCamel(key)] = args[key];
127
+ valueSources.set(key, 'config');
128
+ changed = true;
129
+ }
130
+ return changed ? { ...result, args, valueSources } : result;
131
+ }
132
+ /** Required args that argv, the environment and the config all left empty. */
133
+ function missingRequired(command, result) {
134
+ const argsDef = command.args ?? {};
135
+ const missing = [];
136
+ for (const key of Object.keys(argsDef)) {
137
+ const def = argsDef[key];
138
+ if (def.required !== true || result.args[key] !== undefined) {
139
+ continue;
128
140
  }
129
- // Not a subcommand, stop resolution
141
+ // requiredUnlessPresent and friends may excuse it; leave those to validate.
142
+ if (def.requiredUnlessPresent !== undefined || def.requiredUnlessPresentAll !== undefined) {
143
+ continue;
144
+ }
145
+ missing.push({
146
+ key,
147
+ def,
148
+ label: def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`,
149
+ });
150
+ }
151
+ return missing;
152
+ }
153
+ /** Fold values supplied by a fillMissing hook into the result. */
154
+ function applyFilled(result, command, filled) {
155
+ const argsDef = command.args ?? {};
156
+ const args = { ...result.args };
157
+ const explicitlySet = new Set(result.explicitlySet);
158
+ const valueSources = new Map(result.valueSources);
159
+ for (const key of Object.keys(filled)) {
160
+ const def = argsDef[key];
161
+ const raw = filled[key];
162
+ if (def === undefined || raw === undefined) {
163
+ continue;
164
+ }
165
+ const name = def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
166
+ const coerce = (value) => typeof value === 'boolean' ? value : coerceValue(String(value), def, `prompt:${name}`);
167
+ args[key] = Array.isArray(raw) ? raw.map((v) => String(coerce(v))) : coerce(raw);
168
+ args[kebabToCamel(key)] = args[key];
169
+ explicitlySet.add(key);
170
+ valueSources.set(key, 'prompt');
171
+ }
172
+ return { ...result, args, explicitlySet, valueSources };
173
+ }
174
+ // ---- Global Args ----
175
+ /**
176
+ * Overlay inherited global args onto a command. The command's own args win, and
177
+ * a command with nothing to inherit is returned untouched so the parser's spec
178
+ * cache still hits.
179
+ */
180
+ function applyGlobals(command, globals, propagatedVersion) {
181
+ let hasGlobals = false;
182
+ for (const _key in globals) {
183
+ hasGlobals = true;
130
184
  break;
131
185
  }
132
- return { command: current, remainingArgs: remaining, parentNames };
186
+ const needsVersion = propagatedVersion !== undefined && command.meta.version === undefined;
187
+ if (!hasGlobals && !needsVersion) {
188
+ return command;
189
+ }
190
+ return {
191
+ ...command,
192
+ meta: needsVersion ? { ...command.meta, version: propagatedVersion } : command.meta,
193
+ args: hasGlobals ? mergeGlobalArgs(globals, command.args ?? {}) : command.args,
194
+ };
133
195
  }
134
- /** Find a subcommand definition by alias. */
135
- function findSubcommandByAlias(subCommands, token) {
136
- for (const def of Object.values(subCommands)) {
137
- if (def.meta.aliases?.includes(token)) {
138
- return def;
196
+ /**
197
+ * Walk a chain of subcommand names from a command, for `app help sub subsub`.
198
+ * Returns the deepest command reached and the names of its ancestors.
199
+ */
200
+ function resolveHelpTarget(root, rootParents, path) {
201
+ let current = root;
202
+ const parentNames = [...rootParents];
203
+ for (const name of path) {
204
+ const next = subCommandsOf(current)[name];
205
+ if (next === undefined) {
206
+ break;
207
+ }
208
+ parentNames.push(current.meta.name);
209
+ current = next;
210
+ }
211
+ return { command: current, parentNames };
212
+ }
213
+ /**
214
+ * Overlay global values captured at ancestor levels onto the resolved command's
215
+ * result. A value given again at this level always wins.
216
+ */
217
+ function applyInheritedGlobals(result, inherited) {
218
+ if (inherited.size === 0) {
219
+ return result;
220
+ }
221
+ const args = { ...result.args };
222
+ const explicitlySet = new Set(result.explicitlySet);
223
+ const valueSources = new Map(result.valueSources);
224
+ for (const [key, value] of inherited) {
225
+ if (explicitlySet.has(key)) {
226
+ continue;
139
227
  }
228
+ args[kebabToCamel(key)] = value;
229
+ args[key] = value;
230
+ explicitlySet.add(key);
231
+ valueSources.set(key, 'cli');
140
232
  }
141
- return undefined;
233
+ return { ...result, args, explicitlySet, valueSources };
142
234
  }
143
235
  // ---- runCommand ----
144
236
  /**
145
237
  * Run a specific command with pre-parsed arguments.
146
238
  * Executes the setup -> run -> cleanup lifecycle.
147
239
  */
148
- export async function runCommand(command, args, rawArgs = [], subCommand) {
240
+ export async function runCommand(command, args, rawArgs = [], subCommand, valueSources = new Map(), io) {
149
241
  const ctx = {
150
242
  rawArgs,
151
243
  args,
152
244
  cmd: command,
153
245
  subCommand,
246
+ valueSources,
247
+ stdout: io?.stdout ?? process.stdout,
248
+ stderr: io?.stderr ?? process.stderr,
154
249
  data: {},
155
250
  };
156
251
  let runError;
@@ -227,32 +322,44 @@ function simpleCharDistance(a, b, maxDist) {
227
322
  }
228
323
  // ---- runMain helpers ----
229
324
  /** Handle --help request with mode and style support. */
230
- function handleHelpRequest(command, parentNames, shouldExit, isShortHelp, styles) {
231
- showHelp(command, parentNames.length > 0 ? parentNames : undefined, isShortHelp, styles);
232
- if (shouldExit) {
233
- process.exit(0);
234
- }
325
+ function handleHelpRequest(command, parentNames, isShortHelp, io) {
326
+ showHelp(command, parentNames.length > 0 ? parentNames : undefined, isShortHelp, io.styles, io.stdout);
327
+ io.finish(0);
235
328
  }
236
329
  /** Handle --version request. */
237
- function handleVersionRequest(effectiveCommand, rootCommand, shouldExit) {
330
+ function handleVersionRequest(effectiveCommand, rootCommand, isShort, io) {
238
331
  const versionMeta = effectiveCommand.meta.version ? effectiveCommand.meta : rootCommand.meta;
239
- showVersion(versionMeta);
240
- if (shouldExit) {
241
- process.exit(0);
242
- }
332
+ showVersion(versionMeta, isShort, io.stdout);
333
+ io.finish(0);
243
334
  }
244
335
  /** Handle an unrecognized subcommand with typo suggestion. */
245
- function handleUnrecognizedSubcommand(unknownName, command, parentNames, shouldExit, styles) {
246
- const allNames = collectSubcommandNames(command.subCommands);
336
+ function handleUnrecognizedSubcommand(unknownName, command, parentNames, io) {
337
+ const allNames = collectSubcommandNames(subCommandsOf(command));
247
338
  let msg = `unrecognized subcommand '${unknownName}'`;
248
339
  const bestMatch = findClosestSubcommand(unknownName, allNames);
249
340
  if (bestMatch) {
250
341
  msg += `\n\n tip: a similar subcommand exists: '${bestMatch}'`;
251
342
  }
252
- showError(msg, command, parentNames.length > 0 ? parentNames : undefined, styles);
253
- if (shouldExit) {
254
- process.exit(2);
255
- }
343
+ fail(msg, command, parentNames, io);
344
+ }
345
+ function makeRunIO(opts) {
346
+ const shouldExit = opts?.exit !== false;
347
+ return {
348
+ stdout: opts?.stdout ?? process.stdout,
349
+ stderr: opts?.stderr ?? process.stderr,
350
+ styles: opts?.styles,
351
+ finish(code) {
352
+ opts?.onExit?.(code);
353
+ if (shouldExit) {
354
+ process.exit(code);
355
+ }
356
+ },
357
+ };
358
+ }
359
+ /** Print a usage error against the given command and settle on exit code 2. */
360
+ function fail(message, command, parentNames, io) {
361
+ showError(message, command, parentNames.length > 0 ? parentNames : undefined, io.styles, io.stderr);
362
+ io.finish(2);
256
363
  }
257
364
  // ---- runMain ----
258
365
  /**
@@ -265,105 +372,204 @@ function handleUnrecognizedSubcommand(unknownName, command, parentNames, shouldE
265
372
  * ```
266
373
  */
267
374
  export async function runMain(rootCommand, opts) {
375
+ const io = makeRunIO(opts);
268
376
  const shouldExit = opts?.exit !== false;
269
377
  const showHelpOnEmpty = opts?.showHelpOnEmpty !== false;
270
- const styles = opts?.styles;
378
+ // Tracked through the descent so an error names the command that failed,
379
+ // not the root.
380
+ let errorCommand = rootCommand;
381
+ let errorParents = [];
271
382
  try {
272
- const rawArgs = getRawArgs(opts?.argv);
273
- // Resolve subcommand chain (with inferSubcommands and allowExternalSubcommands)
274
- const { command, remainingArgs, parentNames, externalSubcommand } = resolveCommandChain(rootCommand, rawArgs);
275
- // Handle external subcommand: pass to parent command's run handler
276
- if (externalSubcommand) {
277
- if (command.run) {
278
- await runCommand(command, {}, remainingArgs, externalSubcommand);
383
+ let rawArgs = getRawArgs(opts?.argv, rootCommand.meta.noBinaryName === true);
384
+ // multicall: the name the binary was invoked under selects the subcommand.
385
+ if (rootCommand.meta.multicall && opts?.argv === undefined) {
386
+ const invoked = process.argv[1];
387
+ if (invoked !== undefined) {
388
+ const base = invoked.slice(invoked.lastIndexOf('/') + 1).replace(/\.[cm]?[jt]s$/, '');
389
+ rawArgs = [base, ...rawArgs];
390
+ }
391
+ }
392
+ let command = rootCommand;
393
+ let effectiveCommand = rootCommand;
394
+ let inheritedGlobals = {};
395
+ // Values of global args given at an ancestor level. clap carries these down
396
+ // to the command that finally runs, so `app --verbose serve` reaches serve.
397
+ const inheritedValues = new Map();
398
+ let propagatedVersion;
399
+ let argv = rawArgs;
400
+ const parentNames = [];
401
+ let parseResult = parseArgs([], rootCommand);
402
+ let externalSubcommand;
403
+ // Descend the subcommand chain one level at a time, parsing as we go: only
404
+ // the arg spec can say whether a bare token is a subcommand or the value of
405
+ // a preceding flag.
406
+ for (;;) {
407
+ inheritedGlobals = mergeGlobalArgs(inheritedGlobals, collectGlobalArgs(command));
408
+ if (command.meta.propagateVersion && command.meta.version !== undefined) {
409
+ propagatedVersion = command.meta.version;
410
+ }
411
+ effectiveCommand = applyGlobals(command, inheritedGlobals, propagatedVersion);
412
+ errorCommand = effectiveCommand;
413
+ errorParents = [...parentNames];
414
+ parseResult = parseArgs(argv, effectiveCommand);
415
+ for (const warning of parseResult.warnings) {
416
+ io.stderr.write(`warning: ${warning}\n`);
417
+ }
418
+ if (command.meta.deprecated !== undefined && command.meta.deprecated !== false) {
419
+ const reason = typeof command.meta.deprecated === 'string' ? `: ${command.meta.deprecated}` : '';
420
+ const instead = command.meta.replacedBy === undefined ? '' : `; use '${command.meta.replacedBy}' instead`;
421
+ io.stderr.write(`warning: '${command.meta.name}' is deprecated${reason}${instead}\n`);
279
422
  }
423
+ // The built-in `help` subcommand: `app help`, `app help sub sub`.
424
+ if (parseResult.subCommand === undefined &&
425
+ hasSubCommands(command) &&
426
+ command.meta.disableHelpSubcommand !== true &&
427
+ parseResult.positionals[0] === 'help') {
428
+ const target = resolveHelpTarget(effectiveCommand, parentNames, parseResult.positionals.slice(1));
429
+ handleHelpRequest(target.command, target.parentNames, false, io);
430
+ return;
431
+ }
432
+ for (const key of Object.keys(inheritedGlobals)) {
433
+ if (parseResult.explicitlySet.has(key)) {
434
+ const value = parseResult.args[key];
435
+ if (value !== undefined) {
436
+ inheritedValues.set(key, value);
437
+ }
438
+ }
439
+ }
440
+ if (parseResult.subCommand === undefined ||
441
+ parseResult.helpRequested ||
442
+ parseResult.versionRequested) {
443
+ break;
444
+ }
445
+ if (parseResult.subCommandIsExternal) {
446
+ externalSubcommand = parseResult.subCommand;
447
+ break;
448
+ }
449
+ if (command.meta.argsConflictsWithSubcommands && parseResult.explicitlySet.size > 0) {
450
+ fail('arguments cannot be used with subcommands', effectiveCommand, parentNames, io);
451
+ return;
452
+ }
453
+ const next = subCommandsOf(command)[parseResult.subCommand];
454
+ if (next === undefined) {
455
+ break;
456
+ }
457
+ // A parent's own constraints still apply to the flags given before the
458
+ // subcommand, unless it opted out with subcommandNegatesReqs.
459
+ if (!command.meta.subcommandNegatesReqs) {
460
+ validate(parseResult, effectiveCommand);
461
+ }
462
+ parentNames.push(command.meta.name);
463
+ command = next;
464
+ argv = parseResult.subCommandArgs;
465
+ }
466
+ // External subcommand: hand the name and the untouched remainder to the
467
+ // command that declared allowExternalSubcommands.
468
+ if (externalSubcommand !== undefined) {
469
+ if (effectiveCommand.run) {
470
+ const parser = effectiveCommand.externalSubcommandValueParser;
471
+ const externalArgs = parser === undefined
472
+ ? parseResult.subCommandArgs
473
+ : parseResult.subCommandArgs.map((value) => {
474
+ try {
475
+ return String(parser(value));
476
+ }
477
+ catch (error) {
478
+ const message = error instanceof Error ? error.message : String(error);
479
+ throw new CliParseError(`invalid value '${value}': ${message}`);
480
+ }
481
+ });
482
+ await runCommand(effectiveCommand, parseResult.args, externalArgs, externalSubcommand, parseResult.valueSources, io);
483
+ }
484
+ io.finish(0);
280
485
  return;
281
486
  }
282
- // Merge global args from parent into resolved command
283
- const globalArgs = collectGlobalArgs(rootCommand);
284
- const effectiveCommand = {
285
- ...command,
286
- args: command.args ? mergeGlobalArgs(globalArgs, command.args) : globalArgs,
287
- };
288
- // Parse remaining args against the resolved command
289
- const parseResult = parseArgs(remainingArgs, effectiveCommand);
290
487
  // Handle --help
291
488
  if (parseResult.helpRequested) {
292
- handleHelpRequest(effectiveCommand, parentNames, shouldExit, parseResult.helpIsShort, styles);
489
+ handleHelpRequest(effectiveCommand, parentNames, parseResult.helpIsShort, io);
293
490
  return;
294
491
  }
295
492
  // Handle --version
296
493
  if (parseResult.versionRequested) {
297
- handleVersionRequest(effectiveCommand, rootCommand, shouldExit);
494
+ handleVersionRequest(effectiveCommand, rootCommand, parseResult.versionIsShort, io);
298
495
  return;
299
496
  }
300
497
  // Show help if no args and command has subcommands
301
498
  if (showHelpOnEmpty &&
302
499
  rawArgs.length === 0 &&
303
- rootCommand.subCommands &&
304
- Object.keys(rootCommand.subCommands).length > 0) {
305
- handleHelpRequest(rootCommand, [], shouldExit, false, styles);
500
+ hasSubCommands(rootCommand)) {
501
+ handleHelpRequest(rootCommand, [], false, io);
306
502
  return;
307
503
  }
308
504
  // argRequiredElseHelp: show help if no args were explicitly provided
309
505
  if (command.meta.argRequiredElseHelp && parseResult.explicitlySet.size === 0) {
310
- handleHelpRequest(command, parentNames, shouldExit, false, styles);
506
+ handleHelpRequest(effectiveCommand, parentNames, false, io);
311
507
  return;
312
508
  }
313
509
  // If we resolved to a parent command that has subcommands but no run handler,
314
510
  // and the user didn't pass a valid subcommand, show help or error
315
- if (command.subCommands &&
316
- Object.keys(command.subCommands).length > 0 &&
511
+ if (hasSubCommands(command) &&
317
512
  !command.run &&
318
513
  !parseResult.subCommand) {
319
514
  // subcommandRequired: error if no subcommand
320
515
  if (command.meta.subcommandRequired) {
321
516
  if (parseResult.positionals.length > 0) {
322
- handleUnrecognizedSubcommand(parseResult.positionals[0], command, parentNames, shouldExit, styles);
517
+ handleUnrecognizedSubcommand(parseResult.positionals[0], effectiveCommand, parentNames, io);
323
518
  }
324
519
  else {
325
- showError("a subcommand is required but one was not provided", command, parentNames.length > 0 ? parentNames : undefined, styles);
326
- if (shouldExit) {
327
- process.exit(2);
328
- }
520
+ fail('a subcommand is required but one was not provided', effectiveCommand, parentNames, io);
329
521
  }
330
522
  return;
331
523
  }
332
524
  if (parseResult.positionals.length > 0) {
333
- handleUnrecognizedSubcommand(parseResult.positionals[0], command, parentNames, shouldExit, styles);
525
+ handleUnrecognizedSubcommand(parseResult.positionals[0], effectiveCommand, parentNames, io);
334
526
  return;
335
527
  }
336
- handleHelpRequest(command, parentNames, shouldExit, false, styles);
528
+ handleHelpRequest(effectiveCommand, parentNames, false, io);
337
529
  return;
338
530
  }
339
- // argsConflictsWithSubcommands: error if args + subcommand both present
340
- if (command.meta.argsConflictsWithSubcommands &&
341
- parseResult.subCommand &&
342
- parseResult.explicitlySet.size > 0) {
343
- showError("arguments cannot be used with subcommands", command, parentNames.length > 0 ? parentNames : undefined, styles);
344
- if (shouldExit) {
345
- process.exit(2);
531
+ // Fold ancestor-provided global values into the command that runs, then
532
+ // let the config fill whatever is still on its default.
533
+ let finalResult = applyInheritedGlobals(parseResult, inheritedValues);
534
+ if (opts?.config !== undefined) {
535
+ finalResult = applyConfig(finalResult, effectiveCommand, opts.config, [
536
+ ...parentNames.slice(1),
537
+ ...(parentNames.length > 0 ? [command.meta.name] : []),
538
+ ]);
539
+ }
540
+ // Last chance to supply what is still missing, before validation says no.
541
+ if (opts?.fillMissing !== undefined) {
542
+ const missing = missingRequired(effectiveCommand, finalResult);
543
+ if (missing.length > 0) {
544
+ const filled = await opts.fillMissing(missing, effectiveCommand);
545
+ if (filled !== undefined) {
546
+ finalResult = applyFilled(finalResult, effectiveCommand, filled);
547
+ }
346
548
  }
347
- return;
348
549
  }
349
550
  // Validate parsed args
350
- validate(parseResult, effectiveCommand);
551
+ validate(finalResult, effectiveCommand);
351
552
  // Run the command
352
- await runCommand(effectiveCommand, parseResult.args, rawArgs);
553
+ await runCommand(effectiveCommand, finalResult.args, rawArgs, undefined, finalResult.valueSources, io);
554
+ // If the command's run() didn't call process.exit() itself, exit cleanly.
555
+ // This prevents the process from hanging when the caller uses `void runMain()`
556
+ // instead of `await runMain()` — the unawaited Promise would otherwise keep
557
+ // the event loop alive due to pending NAPI handles or other resources.
558
+ io.finish(0);
353
559
  }
354
560
  catch (error) {
355
561
  if (error instanceof CliParseError) {
356
- showError(error.message, rootCommand, undefined, styles);
357
- if (shouldExit) {
358
- process.exit(2);
359
- }
562
+ fail(error.message, errorCommand, errorParents, io);
360
563
  return;
361
564
  }
362
565
  // Unexpected error
363
- if (shouldExit) {
566
+ if (shouldExit || opts?.onExit !== undefined) {
364
567
  const message = error instanceof Error ? error.message : String(error);
365
- process.stderr.write(`error: ${message}\n`);
366
- process.exit(1);
568
+ io.stderr.write(`error: ${message}\n`);
569
+ io.finish(1);
570
+ if (!shouldExit) {
571
+ return;
572
+ }
367
573
  }
368
574
  throw error;
369
575
  }