cli-forge 1.0.2 → 1.2.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.
Files changed (34) hide show
  1. package/dist/bin/cli.d.ts +16 -1
  2. package/dist/bin/commands/generate-documentation.d.ts +19 -2
  3. package/dist/bin/commands/generate-documentation.js +135 -0
  4. package/dist/bin/commands/generate-documentation.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/lib/composable-builder.d.ts +5 -1
  7. package/dist/lib/composable-builder.js +24 -3
  8. package/dist/lib/composable-builder.js.map +1 -1
  9. package/dist/lib/documentation.d.ts +6 -1
  10. package/dist/lib/documentation.js +35 -1
  11. package/dist/lib/documentation.js.map +1 -1
  12. package/dist/lib/format-help.js +20 -6
  13. package/dist/lib/format-help.js.map +1 -1
  14. package/dist/lib/interactive-shell.js +2 -0
  15. package/dist/lib/interactive-shell.js.map +1 -1
  16. package/dist/lib/internal-cli.d.ts +26 -5
  17. package/dist/lib/internal-cli.js +166 -38
  18. package/dist/lib/internal-cli.js.map +1 -1
  19. package/dist/lib/public-api.d.ts +59 -3
  20. package/dist/lib/public-api.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/bin/commands/generate-documentation.spec.ts +17 -0
  23. package/src/bin/commands/generate-documentation.ts +165 -2
  24. package/src/index.ts +1 -0
  25. package/src/lib/cli-localization.spec.ts +197 -0
  26. package/src/lib/composable-builder.spec.ts +73 -0
  27. package/src/lib/composable-builder.ts +26 -5
  28. package/src/lib/documentation.ts +49 -2
  29. package/src/lib/format-help.ts +24 -8
  30. package/src/lib/interactive-shell.ts +2 -0
  31. package/src/lib/internal-cli.spec.ts +720 -1
  32. package/src/lib/internal-cli.ts +223 -52
  33. package/src/lib/public-api.ts +80 -9
  34. package/tsconfig.lib.json.tsbuildinfo +1 -1
@@ -2,6 +2,8 @@
2
2
  import {
3
3
  ArgvParser,
4
4
  EnvOptionConfig,
5
+ LocalizationDictionary,
6
+ LocalizationFunction,
5
7
  OptionConfig,
6
8
  ParsedArgs,
7
9
  ValidationFailedError,
@@ -9,7 +11,8 @@ import {
9
11
  hideBin,
10
12
  type ConfigurationFiles,
11
13
  } from '@cli-forge/parser';
12
- import { getCallingFile, getParentPackageJson } from './utils';
14
+ import { readOptionGroupsForCLI } from './cli-option-groups';
15
+ import { formatHelp } from './format-help';
13
16
  import { INTERACTIVE_SHELL, InteractiveShell } from './interactive-shell';
14
17
  import {
15
18
  CLI,
@@ -19,8 +22,7 @@ import {
19
22
  ErrorHandler,
20
23
  SDKCommand,
21
24
  } from './public-api';
22
- import { readOptionGroupsForCLI } from './cli-option-groups';
23
- import { formatHelp } from './format-help';
25
+ import { getCallingFile, getParentPackageJson } from './utils';
24
26
 
25
27
  /**
26
28
  * The base class for a CLI application. This class is used to define the structure of the CLI.
@@ -83,7 +85,13 @@ export class InternalCLI<
83
85
  },
84
86
  ];
85
87
 
86
- private registeredMiddleware: Array<(args: TArgs) => void> = [];
88
+ private registeredMiddleware = new Set<
89
+ (args: TArgs) => void | unknown | Promise<void> | Promise<unknown>
90
+ >();
91
+
92
+ private registeredInitHooks: Array<
93
+ (cli: any, args: TArgs) => Promise<void> | void
94
+ > = [];
87
95
 
88
96
  /**
89
97
  * A list of option groups that have been registered with the CLI. Grouped Options are displayed together in the help text.
@@ -167,7 +175,7 @@ export class InternalCLI<
167
175
  configuration: CLICommandOptions<TArgs, TRootCommandArgs>
168
176
  ): InternalCLI<TArgs, THandlerReturn, TChildren, TParent> {
169
177
  this.configuration = configuration;
170
- this.requiresCommand = false;
178
+ this.requiresCommand = configuration.handler ? false : 'IMPLICIT';
171
179
  return this;
172
180
  }
173
181
 
@@ -266,7 +274,18 @@ export class InternalCLI<
266
274
  key
267
275
  ).withRootCommandConfiguration(options as any);
268
276
  cmd._parent = this;
277
+
278
+ // Get localized command name
279
+ const localizedKey = this.getLocalizedCommandName(key);
280
+
281
+ // Register under the default key
269
282
  this.registeredCommands[key] = cmd;
283
+
284
+ // If localized name is different, also register under localized name as an alias
285
+ if (localizedKey !== key) {
286
+ this.registeredCommands[localizedKey] = cmd;
287
+ }
288
+
270
289
  if (options.alias) {
271
290
  for (const alias of options.alias) {
272
291
  this.registeredCommands[alias] = cmd;
@@ -274,6 +293,10 @@ export class InternalCLI<
274
293
  }
275
294
  } else if (keyOrCommand instanceof InternalCLI) {
276
295
  const cmd = keyOrCommand;
296
+ if (cmd.name === '$0') {
297
+ this.withRootCommandConfiguration(cmd.configuration as any);
298
+ return this as any;
299
+ }
277
300
  cmd._parent = this;
278
301
  this.registeredCommands[cmd.name] = cmd;
279
302
  if (cmd.configuration?.alias) {
@@ -293,17 +316,7 @@ export class InternalCLI<
293
316
  commands(...a0: Command[] | Command[][]): any {
294
317
  const commands = a0.flat();
295
318
  for (const val of commands) {
296
- if (val instanceof InternalCLI) {
297
- val._parent = this;
298
- this.registeredCommands[val.name] = val;
299
- // Include any options that were defined via cli(...).option() instead of via builder
300
- this.parser.augment(val.parser);
301
- } else {
302
- const { name, ...configuration } = val as {
303
- name: string;
304
- } & CLICommandOptions<any, any>;
305
- this.command(name, configuration);
306
- }
319
+ this.command(val);
307
320
  }
308
321
  return this;
309
322
  }
@@ -355,11 +368,37 @@ export class InternalCLI<
355
368
  return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
356
369
  }
357
370
 
371
+ localize(
372
+ dictionaryOrFn: LocalizationDictionary | LocalizationFunction,
373
+ locale?: string
374
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
375
+ if (typeof dictionaryOrFn === 'function') {
376
+ this.parser.localize(dictionaryOrFn);
377
+ } else {
378
+ this.parser.localize(dictionaryOrFn, locale);
379
+ }
380
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
381
+ }
382
+
383
+ /**
384
+ * Gets the localized display name for a command key.
385
+ * @param key The command key
386
+ * @returns The localized command name, or the original key if not localized
387
+ */
388
+ getLocalizedCommandName(key: string): string {
389
+ return this.parser.getDisplayKey(key);
390
+ }
391
+
358
392
  demandCommand(): CLI<TArgs, THandlerReturn, TChildren, TParent> {
359
393
  this.requiresCommand = 'EXPLICIT';
360
394
  return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
361
395
  }
362
396
 
397
+ strict(enable = true): CLI<TArgs, THandlerReturn, TChildren, TParent> {
398
+ this.parser.options.strict = enable;
399
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
400
+ }
401
+
363
402
  usage(usageText: string): CLI<TArgs, THandlerReturn, TChildren, TParent> {
364
403
  this.configuration ??= {};
365
404
  this.configuration.usage = usageText;
@@ -396,34 +435,48 @@ export class InternalCLI<
396
435
  }
397
436
 
398
437
  middleware<TArgs2>(
399
- callback: (args: TArgs) => TArgs2 | Promise<TArgs2>
438
+ callback: (args: TArgs) => TArgs2 | Promise<TArgs2> | void | Promise<void>
400
439
  ): CLI<
401
440
  TArgs2 extends void ? TArgs : TArgs & TArgs2,
402
441
  THandlerReturn,
403
442
  TChildren,
404
443
  TParent
405
444
  > {
406
- this.registeredMiddleware.push(callback);
445
+ this.registeredMiddleware.add(callback);
407
446
  // If middleware returns void, TArgs doesn't change...
408
447
  // If it returns something, we need to merge it into TArgs...
409
448
  // that's not here though, its where we apply the middleware results.
410
449
  return this as any;
411
450
  }
412
451
 
452
+ init(
453
+ callback: (
454
+ cli: CLI<TArgs, THandlerReturn, TChildren, TParent>,
455
+ args: TArgs
456
+ ) => Promise<void> | void
457
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
458
+ this.registeredInitHooks.push(callback);
459
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
460
+ }
461
+
413
462
  /**
414
463
  * Runs the current command.
415
464
  * @param cmd The command to run.
416
465
  * @param args The arguments to pass to the command.
417
466
  */
418
- async runCommand<T extends ParsedArgs>(args: T, originalArgV: string[]) {
419
- const middlewares: Array<(args: any) => void> = [
420
- ...this.registeredMiddleware,
421
- ];
467
+ async runCommand<T extends ParsedArgs>(
468
+ args: T,
469
+ originalArgV: string[],
470
+ executedMiddleware?: Set<(args: any) => void>
471
+ ): Promise<T> {
472
+ const middlewares = new Set<(args: any) => void>(this.registeredMiddleware);
422
473
  // eslint-disable-next-line @typescript-eslint/no-this-alias
423
474
  let cmd: InternalCLI<any, any, any, any> = this;
424
475
  for (const command of this.commandChain) {
425
476
  cmd = cmd.registeredCommands[command];
426
- middlewares.push(...cmd.registeredMiddleware);
477
+ for (const mw of cmd.registeredMiddleware) {
478
+ middlewares.add(mw);
479
+ }
427
480
  }
428
481
  try {
429
482
  if (cmd.requiresCommand) {
@@ -433,7 +486,8 @@ export class InternalCLI<
433
486
  }
434
487
  if (cmd.configuration?.handler) {
435
488
  for (const middleware of middlewares) {
436
- const middlewareResult = await middleware(args);
489
+ if (executedMiddleware?.has(middleware)) continue;
490
+ const middlewareResult = await middleware(args as any);
437
491
  if (
438
492
  middlewareResult !== void 0 &&
439
493
  typeof middlewareResult === 'object'
@@ -441,15 +495,25 @@ export class InternalCLI<
441
495
  args = middlewareResult as T;
442
496
  }
443
497
  }
444
- return cmd.configuration.handler(args, {
498
+ await cmd.configuration.handler(args, {
445
499
  command: cmd as any,
446
500
  });
501
+ return args;
447
502
  } else {
448
503
  // We can treat a command as a subshell if it has subcommands
449
504
  if (Object.keys(cmd.registeredCommands).length > 0) {
450
505
  if (!process.stdout.isTTY) {
451
506
  // If we're not in a TTY, we can't run an interactive shell...
452
507
  // Maybe we should warn here?
508
+ } else if (args.unmatched.length > 0) {
509
+ // If there are unmatched args, we don't run an interactive shell...
510
+ // this could represent a user misspelling a subcommand so it gets rather confusing.
511
+ console.warn(
512
+ `Warning: Unrecognized command or arguments: ${args.unmatched.join(
513
+ ' '
514
+ )}`
515
+ );
516
+ cmd.printHelp();
453
517
  } else if (!INTERACTIVE_SHELL) {
454
518
  const tui = new InteractiveShell(
455
519
  this as unknown as InternalCLI<any>,
@@ -480,6 +544,7 @@ export class InternalCLI<
480
544
  console.error(e);
481
545
  this.printHelp();
482
546
  }
547
+ return args;
483
548
  }
484
549
 
485
550
  getChildren(): TChildren {
@@ -500,9 +565,19 @@ export class InternalCLI<
500
565
  }
501
566
 
502
567
  getBuilder():
503
- | (<TInit extends ParsedArgs, TInitHandlerReturn, TInitChildren, TInitParent>(
568
+ | (<
569
+ TInit extends ParsedArgs,
570
+ TInitHandlerReturn,
571
+ TInitChildren,
572
+ TInitParent
573
+ >(
504
574
  parser: CLI<TInit, TInitHandlerReturn, TInitChildren, TInitParent>
505
- ) => CLI<TInit & TArgs, TInitHandlerReturn, TInitChildren & TChildren, TInitParent>)
575
+ ) => CLI<
576
+ TInit & TArgs,
577
+ TInitHandlerReturn,
578
+ TInitChildren & TChildren,
579
+ TInitParent
580
+ >)
506
581
  | undefined {
507
582
  const builder = this.configuration?.builder;
508
583
  if (!builder) return undefined;
@@ -643,7 +718,13 @@ export class InternalCLI<
643
718
  chain.unshift(current);
644
719
  current = current._parent;
645
720
  }
646
- return chain.flatMap((c) => c.registeredMiddleware);
721
+ const seen = new Set<(args: any) => unknown | Promise<unknown>>();
722
+ for (const c of chain) {
723
+ for (const mw of c.registeredMiddleware) {
724
+ seen.add(mw);
725
+ }
726
+ }
727
+ return [...seen];
647
728
  }
648
729
 
649
730
  enableInteractiveShell(): CLI<TArgs, THandlerReturn, TChildren, TParent> {
@@ -703,7 +784,7 @@ export class InternalCLI<
703
784
  group(
704
785
  labelOrConfigObject:
705
786
  | string
706
- | { label: string; keys: (keyof TArgs)[]; sortOrder: number },
787
+ | { label: string; keys: (keyof TArgs)[]; sortOrder?: number },
707
788
  keys?: (keyof TArgs)[]
708
789
  ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
709
790
  const config =
@@ -712,14 +793,17 @@ export class InternalCLI<
712
793
  : {
713
794
  label: labelOrConfigObject,
714
795
  keys: keys as (keyof TArgs)[],
715
- sortOrder: Object.keys(this.registeredOptionGroups).length,
716
796
  };
717
797
 
718
798
  if (!config.keys) {
719
799
  throw new Error('keys must be provided when calling `group`.');
720
800
  }
721
801
 
722
- this.registeredOptionGroups.push(config);
802
+ this.registeredOptionGroups.push({
803
+ ...config,
804
+ sortOrder:
805
+ config.sortOrder ?? Object.keys(this.registeredOptionGroups).length,
806
+ });
723
807
  return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
724
808
  }
725
809
 
@@ -733,32 +817,128 @@ export class InternalCLI<
733
817
  }
734
818
 
735
819
  /**
736
- * Parses argv and executes the CLI
820
+ * Parses argv and executes the CLI.
821
+ *
822
+ * Execution proceeds in two phases:
823
+ *
824
+ * **Discovery loop** (per command level): builder → parse (non-strict,
825
+ * no validation) → merge → middleware → init hooks → find next
826
+ * subcommand in unmatched tokens → repeat.
827
+ *
828
+ * **Final parse + execution**: parse (with validation, seeded with
829
+ * accumulated args) → help/version check → handler. Middleware that
830
+ * already ran during discovery is skipped.
831
+ *
737
832
  * @param args argv. Defaults to process.argv.slice(2)
738
833
  * @returns Promise that resolves when the handler completes.
739
834
  */
740
835
  forge = (args: string[] = hideBin(process.argv)) =>
741
836
  this.withErrorHandlers(async () => {
742
- // Parsing the args does two things:
743
- // - builds argv to pass to handler
744
- // - fills the command chain + registers commands
745
837
  let argv: TArgs & { help?: boolean; version?: boolean };
746
838
  let validationFailedError: ValidationFailedError<TArgs> | undefined;
839
+
840
+ // Run root builder (may register options, init hooks, commands)
841
+ this.configuration?.builder?.(this as any);
842
+
843
+ // Merge helper: accumulate defined values without overwriting
844
+ const mergeNew = (target: any, source: any) => {
845
+ for (const [key, value] of Object.entries(source)) {
846
+ if (key !== 'unmatched' && value !== undefined && target[key] === undefined) {
847
+ target[key] = value;
848
+ }
849
+ }
850
+ };
851
+
852
+ // Iterative command discovery with init hooks.
853
+ // Each level: non-strict parse filtered args → run middleware
854
+ // → run init hooks → find next command in unmatched tokens
855
+ // → filter down. Builders stay lazy.
856
+ let currentArgs = [...args];
857
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
858
+ let currentCmd: InternalCLI<any, any, any, any> = this;
859
+ const mergedArgs: any = {};
860
+ const executedMiddleware = new Set<(args: any) => void>();
861
+
862
+ // eslint-disable-next-line no-constant-condition
863
+ while (true) {
864
+ // Non-strict parse to get current arg values for init hooks.
865
+ // Seeded with mergedArgs so required options parsed at earlier
866
+ // levels satisfy validation.
867
+ const parsed = this.parser
868
+ .clone({
869
+ ...this.parser.options,
870
+ unmatchedParser: () => false,
871
+ strict: false,
872
+ validate: false,
873
+ })
874
+ .parse(currentArgs, mergedArgs) as any;
875
+
876
+ mergeNew(mergedArgs, parsed);
877
+
878
+ // Run middleware for this command level before init hooks,
879
+ // so init hooks can see middleware-transformed args.
880
+ for (const mw of currentCmd.registeredMiddleware) {
881
+ if (!executedMiddleware.has(mw)) {
882
+ executedMiddleware.add(mw);
883
+ const result = await mw(mergedArgs);
884
+ if (result !== void 0 && typeof result === 'object') {
885
+ Object.assign(mergedArgs, result);
886
+ }
887
+ }
888
+ }
889
+
890
+ // Run init hooks with the full accumulated args
891
+ for (const hook of currentCmd.registeredInitHooks) {
892
+ await hook(currentCmd as any, mergedArgs);
893
+ }
894
+
895
+ // Find the next command in unmatched tokens
896
+ const unmatched: string[] = parsed.unmatched ?? [];
897
+ let nextCmd: InternalCLI<any, any, any, any> | null = null;
898
+ const remainingArgs: string[] = [];
899
+
900
+ for (const token of unmatched) {
901
+ if (!nextCmd && !token.startsWith('-')) {
902
+ const cmd = currentCmd.registeredCommands[token];
903
+ if (cmd && cmd.configuration) {
904
+ cmd.parser = this.parser;
905
+ cmd.configuration.builder?.(cmd as any);
906
+ this.commandChain.push(token);
907
+ nextCmd = cmd;
908
+ continue;
909
+ }
910
+ }
911
+ remainingArgs.push(token);
912
+ }
913
+
914
+ if (!nextCmd) {
915
+ currentArgs = unmatched;
916
+ break;
917
+ }
918
+ currentArgs = remainingArgs;
919
+ currentCmd = nextCmd;
920
+ }
921
+
922
+ // All builders and init hooks have run. The parser now has
923
+ // all options registered. Parse the remaining unmatched tokens
924
+ // seeded with the accumulated values from the discovery loop.
925
+ // The alreadyParsed values ensure proper required-option
926
+ // validation and prevent positional re-consumption.
747
927
  try {
748
- argv = this.parser.parse(args);
928
+ argv = this.parser
929
+ .clone({
930
+ ...this.parser.options,
931
+ unmatchedParser: () => false,
932
+ })
933
+ .parse(currentArgs, mergedArgs) as any;
749
934
  } catch (e) {
750
935
  if (e instanceof ValidationFailedError) {
751
- argv = e.partialArgV as TArgs;
936
+ argv = e.partialArgV as any;
752
937
  validationFailedError = e;
753
938
  } else {
754
939
  throw e;
755
940
  }
756
941
  }
757
- // eslint-disable-next-line @typescript-eslint/no-this-alias
758
- let currentCommand: InternalCLI<any, any, any, any> = this;
759
- for (const command of this.commandChain) {
760
- currentCommand = currentCommand.registeredCommands[command];
761
- }
762
942
 
763
943
  if (argv.version) {
764
944
  this.versionHandler();
@@ -772,16 +952,7 @@ export class InternalCLI<
772
952
  throw validationFailedError;
773
953
  }
774
954
 
775
- const finalArgV =
776
- this.commandChain.length === 0 && this.configuration?.builder
777
- ? (
778
- this.configuration.builder?.(
779
- this as any
780
- ) as unknown as InternalCLI<TArgs, any, any, any>
781
- ).parser.parse(args)
782
- : argv;
783
-
784
- await this.runCommand(finalArgV, args);
955
+ const finalArgV = await this.runCommand(argv, args, executedMiddleware);
785
956
  return finalArgV as TArgs;
786
957
  });
787
958
 
@@ -1,18 +1,20 @@
1
1
  /* eslint-disable @typescript-eslint/ban-types */
2
2
  import {
3
+ ArrayOptionConfig,
4
+ BooleanOptionConfig,
3
5
  type ConfigurationFiles,
6
+ EnvOptionConfig,
7
+ LocalizationDictionary,
8
+ LocalizationFunction,
9
+ MakeUndefinedPropertiesOptional,
10
+ NumberOptionConfig,
11
+ ObjectOptionConfig,
4
12
  OptionConfig,
5
13
  OptionConfigToType,
6
14
  ParsedArgs,
7
- EnvOptionConfig,
8
- ObjectOptionConfig,
9
- StringOptionConfig,
10
- NumberOptionConfig,
11
- BooleanOptionConfig,
12
- ArrayOptionConfig,
13
15
  ResolveProperties,
16
+ StringOptionConfig,
14
17
  WithOptional,
15
- MakeUndefinedPropertiesOptional,
16
18
  } from '@cli-forge/parser';
17
19
 
18
20
  import { InternalCLI } from './internal-cli';
@@ -651,6 +653,51 @@ export interface CLI<
651
653
 
652
654
  env(options: EnvOptionConfig): CLI<TArgs, THandlerReturn, TChildren, TParent>;
653
655
 
656
+ /**
657
+ * Sets up localization for option keys and other text.
658
+ * When localization is enabled, option keys will be displayed in the specified locale in help text and documentation,
659
+ * and both the default and localized keys will be accepted when parsing arguments.
660
+ *
661
+ * @param dictionary The localization dictionary mapping keys to their translations
662
+ * @param locale The target locale (defaults to system locale if not provided)
663
+ * @returns Updated CLI instance for chaining
664
+ *
665
+ * @example
666
+ * ```ts
667
+ * cli('myapp')
668
+ * .localize({
669
+ * name: { default: 'name', 'es-ES': 'nombre' },
670
+ * port: { default: 'port', 'es-ES': 'puerto' }
671
+ * }, 'es-ES')
672
+ * .option('name', { type: 'string' })
673
+ * .option('port', { type: 'number' });
674
+ * ```
675
+ */
676
+ localize(
677
+ dictionary: LocalizationDictionary,
678
+ locale?: string
679
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent>;
680
+ /**
681
+ * Sets up localization using a custom function for translating keys.
682
+ * This allows integration with existing localization libraries like i18next.
683
+ *
684
+ * @param fn A function that takes a key and returns its localized value
685
+ * @returns Updated CLI instance for chaining
686
+ *
687
+ * @example
688
+ * ```ts
689
+ * import i18next from 'i18next';
690
+ *
691
+ * cli('myapp')
692
+ * .localize((key) => i18next.t(key))
693
+ * .option('name', { type: 'string' })
694
+ * .option('port', { type: 'number' });
695
+ * ```
696
+ */
697
+ localize(
698
+ fn: LocalizationFunction
699
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent>;
700
+
654
701
  /**
655
702
  * Sets a group of options as mutually exclusive. If more than one option is provided, there will be a validation error.
656
703
  * @param options The options that should be mutually exclusive.
@@ -676,6 +723,15 @@ export interface CLI<
676
723
  */
677
724
  demandCommand(): CLI<TArgs, THandlerReturn, TChildren, TParent>;
678
725
 
726
+ /**
727
+ * Enables or disables strict mode. When strict mode is enabled, the parser throws a validation error
728
+ * when unmatched arguments are encountered. Unmatched arguments are those that don't match any
729
+ * configured option or positional argument.
730
+ * @param enable Whether to enable strict mode. Defaults to true.
731
+ * @returns Updated CLI instance.
732
+ */
733
+ strict(enable?: boolean): CLI<TArgs, THandlerReturn, TChildren, TParent>;
734
+
679
735
  /**
680
736
  * Sets the usage text for the CLI. This text will be displayed in place of the default usage text
681
737
  * @param usageText Text displayed in place of the default usage text for `--help` and in generated docs.
@@ -709,7 +765,7 @@ export interface CLI<
709
765
  }: {
710
766
  label: string;
711
767
  keys: (keyof TArgs)[];
712
- sortOrder: number;
768
+ sortOrder?: number;
713
769
  }): CLI<TArgs, THandlerReturn, TChildren, TParent>;
714
770
  group(
715
771
  label: string,
@@ -725,6 +781,21 @@ export interface CLI<
725
781
  TParent
726
782
  >;
727
783
 
784
+ /**
785
+ * Registers an init hook that runs before command resolution.
786
+ * Init hooks receive partially-parsed args (from currently-registered options)
787
+ * and can modify the CLI (register commands, options, middleware) before the
788
+ * full parse runs. This enables plugin loading from config files.
789
+ *
790
+ * @param callback Async function receiving (args, cli). Mutate cli to add commands/options.
791
+ */
792
+ init(
793
+ callback: (
794
+ cli: CLI<TArgs, THandlerReturn, TChildren, TParent>,
795
+ args: TArgs
796
+ ) => Promise<void> | void
797
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent>;
798
+
728
799
  /**
729
800
  * Parses argv and executes the CLI
730
801
  * @param args argv. Defaults to process.argv.slice(2)
@@ -853,7 +924,7 @@ export interface CLICommandOptions<
853
924
  * The type of the arguments that are registered after `builder` is invoked, and the type that is passed to the handler.
854
925
  */
855
926
  TArgs extends TInitial = TInitial,
856
- THandlerReturn = void,
927
+ THandlerReturn = void | Promise<void>,
857
928
  /**
858
929
  * The children commands that exist before the builder runs.
859
930
  */