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.
- package/dist/bin/cli.d.ts +16 -1
- package/dist/bin/commands/generate-documentation.d.ts +19 -2
- package/dist/bin/commands/generate-documentation.js +135 -0
- package/dist/bin/commands/generate-documentation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/lib/composable-builder.d.ts +5 -1
- package/dist/lib/composable-builder.js +24 -3
- package/dist/lib/composable-builder.js.map +1 -1
- package/dist/lib/documentation.d.ts +6 -1
- package/dist/lib/documentation.js +35 -1
- package/dist/lib/documentation.js.map +1 -1
- package/dist/lib/format-help.js +20 -6
- package/dist/lib/format-help.js.map +1 -1
- package/dist/lib/interactive-shell.js +2 -0
- package/dist/lib/interactive-shell.js.map +1 -1
- package/dist/lib/internal-cli.d.ts +26 -5
- package/dist/lib/internal-cli.js +166 -38
- package/dist/lib/internal-cli.js.map +1 -1
- package/dist/lib/public-api.d.ts +59 -3
- package/dist/lib/public-api.js.map +1 -1
- package/package.json +2 -2
- package/src/bin/commands/generate-documentation.spec.ts +17 -0
- package/src/bin/commands/generate-documentation.ts +165 -2
- package/src/index.ts +1 -0
- package/src/lib/cli-localization.spec.ts +197 -0
- package/src/lib/composable-builder.spec.ts +73 -0
- package/src/lib/composable-builder.ts +26 -5
- package/src/lib/documentation.ts +49 -2
- package/src/lib/format-help.ts +24 -8
- package/src/lib/interactive-shell.ts +2 -0
- package/src/lib/internal-cli.spec.ts +720 -1
- package/src/lib/internal-cli.ts +223 -52
- package/src/lib/public-api.ts +80 -9
- package/tsconfig.lib.json.tsbuildinfo +1 -1
package/src/lib/internal-cli.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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>(
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| (<
|
|
568
|
+
| (<
|
|
569
|
+
TInit extends ParsedArgs,
|
|
570
|
+
TInitHandlerReturn,
|
|
571
|
+
TInitChildren,
|
|
572
|
+
TInitParent
|
|
573
|
+
>(
|
|
504
574
|
parser: CLI<TInit, TInitHandlerReturn, TInitChildren, TInitParent>
|
|
505
|
-
) => CLI<
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
package/src/lib/public-api.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|