cli-forge 0.10.1 → 0.12.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 (103) hide show
  1. package/.eslintrc.json +35 -0
  2. package/LICENSE.md +5 -0
  3. package/README.md +181 -5
  4. package/cli.js +9 -0
  5. package/dist/bin/cli.d.ts +23 -0
  6. package/{bin → dist/bin}/cli.js +2 -2
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/bin/commands/generate-documentation.d.ts +14 -0
  9. package/{bin → dist/bin}/commands/generate-documentation.js +58 -11
  10. package/dist/bin/commands/generate-documentation.js.map +1 -0
  11. package/{bin → dist/bin}/commands/init.d.ts +11 -11
  12. package/{bin → dist/bin}/commands/init.js +11 -6
  13. package/dist/bin/commands/init.js.map +1 -0
  14. package/dist/bin/utils/fs.js.map +1 -0
  15. package/{src → dist}/index.d.ts +2 -1
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/cli-option-groups.js.map +1 -0
  18. package/dist/lib/composable-builder.d.ts +24 -0
  19. package/dist/lib/composable-builder.js +17 -0
  20. package/dist/lib/composable-builder.js.map +1 -0
  21. package/dist/lib/configuration-providers.js.map +1 -0
  22. package/{src → dist}/lib/documentation.d.ts +3 -3
  23. package/dist/lib/documentation.js.map +1 -0
  24. package/dist/lib/format-help.js.map +1 -0
  25. package/{src → dist}/lib/interactive-shell.d.ts +1 -1
  26. package/{src → dist}/lib/interactive-shell.js +6 -3
  27. package/dist/lib/interactive-shell.js.map +1 -0
  28. package/{src → dist}/lib/internal-cli.d.ts +38 -21
  29. package/{src → dist}/lib/internal-cli.js +48 -3
  30. package/dist/lib/internal-cli.js.map +1 -0
  31. package/dist/lib/public-api.d.ts +332 -0
  32. package/dist/lib/public-api.js.map +1 -0
  33. package/dist/lib/test-harness.js.map +1 -0
  34. package/dist/lib/utils.js.map +1 -0
  35. package/dist/middleware/zod.d.ts +4 -0
  36. package/dist/middleware/zod.js +18 -0
  37. package/dist/middleware/zod.js.map +1 -0
  38. package/dist/middleware.d.ts +1 -0
  39. package/dist/middleware.js +5 -0
  40. package/dist/middleware.js.map +1 -0
  41. package/package.json +29 -10
  42. package/project.json +7 -0
  43. package/src/bin/cli.ts +17 -0
  44. package/src/bin/commands/generate-documentation.ts +403 -0
  45. package/src/bin/commands/init.ts +320 -0
  46. package/src/bin/utils/fs.ts +11 -0
  47. package/src/index.ts +12 -0
  48. package/src/lib/cli-option-groups.ts +69 -0
  49. package/src/lib/composable-builder.ts +57 -0
  50. package/src/lib/configuration-providers.ts +36 -0
  51. package/src/lib/documentation.spec.ts +156 -0
  52. package/src/lib/documentation.ts +107 -0
  53. package/src/lib/format-help.ts +149 -0
  54. package/src/lib/interactive-shell.ts +115 -0
  55. package/src/lib/internal-cli.spec.ts +345 -0
  56. package/src/lib/internal-cli.ts +689 -0
  57. package/src/lib/public-api.ts +943 -0
  58. package/src/lib/test-harness.spec.ts +29 -0
  59. package/src/lib/test-harness.ts +69 -0
  60. package/src/lib/utils.spec.ts +25 -0
  61. package/src/lib/utils.ts +144 -0
  62. package/src/middleware/zod.ts +21 -0
  63. package/src/middleware.ts +1 -0
  64. package/tsconfig.json +23 -0
  65. package/tsconfig.lib.json +20 -0
  66. package/tsconfig.lib.json.tsbuildinfo +1 -0
  67. package/tsconfig.spec.json +26 -0
  68. package/vitest.config.mts +18 -0
  69. package/bin/cli.d.ts +0 -6
  70. package/bin/cli.js.map +0 -1
  71. package/bin/commands/generate-documentation.d.ts +0 -14
  72. package/bin/commands/generate-documentation.js.map +0 -1
  73. package/bin/commands/init.js.map +0 -1
  74. package/bin/utils/fs.js.map +0 -1
  75. package/src/index.js.map +0 -1
  76. package/src/lib/cli-option-groups.js.map +0 -1
  77. package/src/lib/composable-builder.d.ts +0 -3
  78. package/src/lib/composable-builder.js +0 -7
  79. package/src/lib/composable-builder.js.map +0 -1
  80. package/src/lib/configuration-providers.js.map +0 -1
  81. package/src/lib/documentation.js.map +0 -1
  82. package/src/lib/format-help.js.map +0 -1
  83. package/src/lib/interactive-shell.js.map +0 -1
  84. package/src/lib/internal-cli.js.map +0 -1
  85. package/src/lib/public-api.d.ts +0 -215
  86. package/src/lib/public-api.js.map +0 -1
  87. package/src/lib/test-harness.js.map +0 -1
  88. package/src/lib/utils.js.map +0 -1
  89. /package/{bin → dist/bin}/utils/fs.d.ts +0 -0
  90. /package/{bin → dist/bin}/utils/fs.js +0 -0
  91. /package/{src → dist}/index.js +0 -0
  92. /package/{src → dist}/lib/cli-option-groups.d.ts +0 -0
  93. /package/{src → dist}/lib/cli-option-groups.js +0 -0
  94. /package/{src → dist}/lib/configuration-providers.d.ts +0 -0
  95. /package/{src → dist}/lib/configuration-providers.js +0 -0
  96. /package/{src → dist}/lib/documentation.js +0 -0
  97. /package/{src → dist}/lib/format-help.d.ts +0 -0
  98. /package/{src → dist}/lib/format-help.js +0 -0
  99. /package/{src → dist}/lib/public-api.js +0 -0
  100. /package/{src → dist}/lib/test-harness.d.ts +0 -0
  101. /package/{src → dist}/lib/test-harness.js +0 -0
  102. /package/{src → dist}/lib/utils.d.ts +0 -0
  103. /package/{src → dist}/lib/utils.js +0 -0
@@ -0,0 +1,689 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
2
+ import {
3
+ ArgvParser,
4
+ EnvOptionConfig,
5
+ OptionConfig,
6
+ ParsedArgs,
7
+ ValidationFailedError,
8
+ fromCamelOrDashedCaseToConstCase,
9
+ hideBin,
10
+ type ConfigurationFiles,
11
+ } from '@cli-forge/parser';
12
+ import { getCallingFile, getParentPackageJson } from './utils';
13
+ import { INTERACTIVE_SHELL, InteractiveShell } from './interactive-shell';
14
+ import {
15
+ CLI,
16
+ CLICommandOptions,
17
+ CLIHandlerContext,
18
+ Command,
19
+ ErrorHandler,
20
+ } from './public-api';
21
+ import { readOptionGroupsForCLI } from './cli-option-groups';
22
+ import { formatHelp } from './format-help';
23
+
24
+ /**
25
+ * The base class for a CLI application. This class is used to define the structure of the CLI.
26
+ *
27
+ * {@link cli} is provided as a small helper function to create a new CLI instance.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { cli } from 'cli-forge';
32
+ *
33
+ * cli('basic-cli').command('hello', {
34
+ * builder: (args) =>
35
+ * args.option('name', {
36
+ * type: 'string',
37
+ * }),
38
+ * handler: (args) => {
39
+ * console.log(`Hello, ${args.name}!`);
40
+ * }).forge();
41
+ * ```
42
+ */
43
+ export class InternalCLI<
44
+ TArgs extends ParsedArgs = ParsedArgs,
45
+ THandlerReturn = void,
46
+ // eslint-disable-next-line @typescript-eslint/ban-types
47
+ TChildren = {},
48
+ TParent = undefined
49
+ > implements CLI<TArgs, THandlerReturn, TChildren, TParent>
50
+ {
51
+ /**
52
+ * For internal use only. Stick to properties available on {@link CLI}.
53
+ */
54
+ registeredCommands: Record<string, InternalCLI<any, any, any, any>> = {};
55
+
56
+ /**
57
+ * For internal use only. Stick to properties available on {@link CLI}.
58
+ */
59
+ commandChain: string[] = [];
60
+
61
+ /**
62
+ * Reference to the parent CLI instance, if this command was registered as a subcommand.
63
+ * For internal use only. Use `getParent()` instead.
64
+ */
65
+ private _parent?: InternalCLI<any, any, any, any>;
66
+
67
+ private requiresCommand: 'IMPLICIT' | 'EXPLICIT' | false = 'IMPLICIT';
68
+
69
+ private _configuration?: CLICommandOptions<any, any>;
70
+
71
+ private _versionOverride?: string;
72
+
73
+ private registeredErrorHandlers: Array<ErrorHandler> = [
74
+ (e: unknown, actions) => {
75
+ if (e instanceof ValidationFailedError) {
76
+ this.printHelp();
77
+ console.log();
78
+ console.log(e.message);
79
+ console.log(e.errors.map((e) => ` - ${e.message}`).join('\n'));
80
+ actions.exit(1);
81
+ }
82
+ },
83
+ ];
84
+
85
+ private registeredMiddleware: Array<(args: TArgs) => void> = [];
86
+
87
+ /**
88
+ * A list of option groups that have been registered with the CLI. Grouped Options are displayed together in the help text.
89
+ *
90
+ * For internal use only. Stick to properties available on {@link CLI}.
91
+ */
92
+ registeredOptionGroups: Array<{
93
+ label: string;
94
+ sortOrder: number;
95
+ keys: Array<keyof TArgs>;
96
+ }> = [];
97
+
98
+ getGroupedOptions() {
99
+ return readOptionGroupsForCLI(this);
100
+ }
101
+
102
+ get configuration() {
103
+ return this._configuration;
104
+ }
105
+
106
+ private set configuration(value: CLICommandOptions<any, any> | undefined) {
107
+ this._configuration = value;
108
+ }
109
+
110
+ /**
111
+ * The parser used to parse the arguments for the current command.
112
+ *
113
+ * Meant for internal use only. Stick to properties available on {@link CLI}.
114
+ *
115
+ * If you need this kind of info, please open an issue on the GitHub repo with
116
+ * your use case.
117
+ */
118
+ parser = new ArgvParser<TArgs>({
119
+ unmatchedParser: (arg) => {
120
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
121
+ let currentCommand: InternalCLI<any, any, any, any> = this;
122
+ for (const command of this.commandChain) {
123
+ currentCommand = currentCommand.registeredCommands[command];
124
+ }
125
+ const command = currentCommand.registeredCommands[arg];
126
+ if (command && command.configuration) {
127
+ command.parser = this.parser;
128
+ command.configuration.builder?.(command as any);
129
+ this.commandChain.push(arg);
130
+ return true;
131
+ }
132
+ return false;
133
+ },
134
+ })
135
+ .option('help', {
136
+ type: 'boolean',
137
+ alias: ['h'],
138
+ description: 'Show help for the current command',
139
+ })
140
+ .option('version', {
141
+ type: 'boolean',
142
+ description: 'Show the version number for the CLI',
143
+ });
144
+
145
+ /**
146
+ * @param name What should the name of the cli command be?
147
+ * @param configuration Configuration for the current CLI command.
148
+ */
149
+ constructor(
150
+ public name: string,
151
+ rootCommandConfiguration?: CLICommandOptions<
152
+ TArgs,
153
+ any,
154
+ THandlerReturn,
155
+ TChildren
156
+ >
157
+ ) {
158
+ if (rootCommandConfiguration) {
159
+ this.withRootCommandConfiguration(rootCommandConfiguration as any);
160
+ } else {
161
+ this.requiresCommand = 'IMPLICIT';
162
+ }
163
+ }
164
+
165
+ withRootCommandConfiguration<TRootCommandArgs extends TArgs>(
166
+ configuration: CLICommandOptions<TArgs, TRootCommandArgs>
167
+ ): InternalCLI<TArgs, THandlerReturn, TChildren, TParent> {
168
+ this.configuration = configuration;
169
+ this.requiresCommand = false;
170
+ return this;
171
+ }
172
+
173
+ command<TCommandArgs extends TArgs>(
174
+ cmd: Command<TArgs, TCommandArgs>
175
+ ): CLI<
176
+ TArgs,
177
+ THandlerReturn,
178
+ TChildren &
179
+ (typeof cmd extends Command<TArgs, infer TCmdArgs, infer TCmdName>
180
+ ? {
181
+ [key in TCmdName]: CLI<
182
+ TCmdArgs,
183
+ void,
184
+ {},
185
+ CLI<TArgs, THandlerReturn, TChildren, TParent>
186
+ >;
187
+ }
188
+ : {}),
189
+ TParent
190
+ >;
191
+ command<
192
+ TCommandArgs extends TArgs,
193
+ TChildHandlerReturn = void,
194
+ TCommandName extends string = string
195
+ >(
196
+ key: TCommandName,
197
+ options: CLICommandOptions<TArgs, TCommandArgs, TChildHandlerReturn>
198
+ ): CLI<
199
+ TArgs,
200
+ THandlerReturn,
201
+ TChildren & {
202
+ [key in TCommandName]: CLI<
203
+ TCommandArgs,
204
+ TChildHandlerReturn,
205
+ {},
206
+ CLI<TArgs, THandlerReturn, TChildren, TParent>
207
+ >;
208
+ },
209
+ TParent
210
+ >;
211
+ command<
212
+ TCommandArgs extends TArgs,
213
+ TChildHandlerReturn = void,
214
+ TCommandName extends string = string
215
+ >(
216
+ keyOrCommand: TCommandName | Command<TArgs, TCommandArgs>,
217
+ options?: CLICommandOptions<TArgs, TCommandArgs, TChildHandlerReturn>
218
+ ): CLI<
219
+ TArgs,
220
+ THandlerReturn,
221
+ TChildren &
222
+ (typeof keyOrCommand extends string
223
+ ? {
224
+ [key in typeof keyOrCommand]: CLI<
225
+ TCommandArgs,
226
+ TChildHandlerReturn,
227
+ {},
228
+ CLI<TArgs, THandlerReturn, TChildren, TParent>
229
+ >;
230
+ }
231
+ : typeof keyOrCommand extends Command<
232
+ TArgs,
233
+ infer TCmdArgs,
234
+ infer TCmdName
235
+ >
236
+ ? {
237
+ [key in TCmdName]: CLI<
238
+ TCmdArgs,
239
+ void,
240
+ {},
241
+ CLI<TArgs, THandlerReturn, TChildren, TParent>
242
+ >;
243
+ }
244
+ : // eslint-disable-next-line @typescript-eslint/ban-types
245
+ {}),
246
+ TParent
247
+ > {
248
+ if (typeof keyOrCommand === 'string') {
249
+ const key = keyOrCommand;
250
+ if (!options) {
251
+ throw new Error(
252
+ 'options must be provided when calling `command` with a string'
253
+ );
254
+ }
255
+ if (key === '$0' || options.alias?.includes('$0')) {
256
+ this.withRootCommandConfiguration({
257
+ ...this._configuration,
258
+ builder: options.builder as any,
259
+ handler: options.handler as any,
260
+ description: options.description,
261
+ });
262
+ }
263
+ const cmd = new InternalCLI<TArgs, TChildHandlerReturn>(
264
+ key
265
+ ).withRootCommandConfiguration(options as any);
266
+ cmd._parent = this;
267
+ this.registeredCommands[key] = cmd;
268
+ if (options.alias) {
269
+ for (const alias of options.alias) {
270
+ this.registeredCommands[alias] = cmd;
271
+ }
272
+ }
273
+ } else if (keyOrCommand instanceof InternalCLI) {
274
+ const cmd = keyOrCommand;
275
+ cmd._parent = this;
276
+ this.registeredCommands[cmd.name] = cmd;
277
+ if (cmd.configuration?.alias) {
278
+ for (const alias of cmd.configuration.alias) {
279
+ this.registeredCommands[alias] = cmd;
280
+ }
281
+ }
282
+ } else {
283
+ const { name, ...configuration } = keyOrCommand as {
284
+ name: string;
285
+ } & CLICommandOptions<TArgs, TCommandArgs>;
286
+ this.command<TCommandArgs>(name, configuration);
287
+ }
288
+ return this as any;
289
+ }
290
+
291
+ commands(...a0: Command[] | Command[][]): any {
292
+ const commands = a0.flat();
293
+ for (const val of commands) {
294
+ if (val instanceof InternalCLI) {
295
+ val._parent = this;
296
+ this.registeredCommands[val.name] = val;
297
+ // Include any options that were defined via cli(...).option() instead of via builder
298
+ this.parser.augment(val.parser);
299
+ } else {
300
+ const { name, ...configuration } = val as {
301
+ name: string;
302
+ } & CLICommandOptions<any, any>;
303
+ this.command(name, configuration);
304
+ }
305
+ }
306
+ return this;
307
+ }
308
+
309
+ option<
310
+ TOption extends string,
311
+ const TOptionConfig extends OptionConfig<any, any, any, any>
312
+ >(name: TOption, config: TOptionConfig) {
313
+ this.parser.option(name, config);
314
+ // Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
315
+ return this as any;
316
+ }
317
+
318
+ positional<
319
+ TOption extends string,
320
+ const TOptionConfig extends OptionConfig<any, any, any, any>
321
+ >(name: TOption, config: TOptionConfig) {
322
+ this.parser.positional(name, config);
323
+ // Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
324
+ return this as any;
325
+ }
326
+
327
+ conflicts(
328
+ ...args: [string, string, ...string[]]
329
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
330
+ this.parser.conflicts(...args);
331
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
332
+ }
333
+
334
+ implies(
335
+ option: string,
336
+ ...impliedOptions: string[]
337
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
338
+ this.parser.implies(option, ...impliedOptions);
339
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
340
+ }
341
+
342
+ env(
343
+ a0: string | EnvOptionConfig | undefined = fromCamelOrDashedCaseToConstCase(
344
+ this.name
345
+ )
346
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
347
+ if (typeof a0 === 'string') {
348
+ this.parser.env(a0);
349
+ } else {
350
+ a0.prefix ??= fromCamelOrDashedCaseToConstCase(this.name);
351
+ this.parser.env(a0);
352
+ }
353
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
354
+ }
355
+
356
+ demandCommand(): CLI<TArgs, THandlerReturn, TChildren, TParent> {
357
+ this.requiresCommand = 'EXPLICIT';
358
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
359
+ }
360
+
361
+ usage(usageText: string): CLI<TArgs, THandlerReturn, TChildren, TParent> {
362
+ this.configuration ??= {};
363
+ this.configuration.usage = usageText;
364
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
365
+ }
366
+
367
+ examples(
368
+ ...examples: string[]
369
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
370
+ this.configuration ??= {};
371
+ this.configuration.examples ??= [];
372
+ this.configuration.examples.push(...examples);
373
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
374
+ }
375
+
376
+ version(version?: string): CLI<TArgs, THandlerReturn, TChildren, TParent> {
377
+ this._versionOverride = version;
378
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
379
+ }
380
+
381
+ /**
382
+ * Gets help text for the current command as a string.
383
+ * @returns Help text for the current command.
384
+ */
385
+ formatHelp() {
386
+ return formatHelp(this);
387
+ }
388
+
389
+ /**
390
+ * Prints help text for the current command to the console.
391
+ */
392
+ printHelp() {
393
+ console.log(this.formatHelp());
394
+ }
395
+
396
+ middleware<TArgs2>(
397
+ callback: (args: TArgs) => TArgs2 | Promise<TArgs2>
398
+ ): CLI<
399
+ TArgs2 extends void ? TArgs : TArgs & TArgs2,
400
+ THandlerReturn,
401
+ TChildren,
402
+ TParent
403
+ > {
404
+ this.registeredMiddleware.push(callback);
405
+ // If middleware returns void, TArgs doesn't change...
406
+ // If it returns something, we need to merge it into TArgs...
407
+ // that's not here though, its where we apply the middleware results.
408
+ return this as any;
409
+ }
410
+
411
+ /**
412
+ * Runs the current command.
413
+ * @param cmd The command to run.
414
+ * @param args The arguments to pass to the command.
415
+ */
416
+ async runCommand<T extends ParsedArgs>(args: T, originalArgV: string[]) {
417
+ const middlewares: Array<(args: any) => void> = [
418
+ ...this.registeredMiddleware,
419
+ ];
420
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
421
+ let cmd: InternalCLI<any, any, any, any> = this;
422
+ for (const command of this.commandChain) {
423
+ cmd = cmd.registeredCommands[command];
424
+ middlewares.push(...cmd.registeredMiddleware);
425
+ }
426
+ try {
427
+ if (cmd.requiresCommand) {
428
+ throw new Error(
429
+ `${[this.name, ...this.commandChain].join(' ')} requires a command`
430
+ );
431
+ }
432
+ if (cmd.configuration?.handler) {
433
+ for (const middleware of middlewares) {
434
+ const middlewareResult = await middleware(args);
435
+ if (
436
+ middlewareResult !== void 0 &&
437
+ typeof middlewareResult === 'object'
438
+ ) {
439
+ args = middlewareResult as T;
440
+ }
441
+ }
442
+ return cmd.configuration.handler(args, {
443
+ command: cmd as any,
444
+ });
445
+ } else {
446
+ // We can treat a command as a subshell if it has subcommands
447
+ if (Object.keys(cmd.registeredCommands).length > 0) {
448
+ if (!process.stdout.isTTY) {
449
+ // If we're not in a TTY, we can't run an interactive shell...
450
+ // Maybe we should warn here?
451
+ } else if (!INTERACTIVE_SHELL) {
452
+ const tui = new InteractiveShell(
453
+ this as unknown as InternalCLI<any>,
454
+ {
455
+ prependArgs: originalArgV,
456
+ }
457
+ );
458
+ await new Promise<void>((res) => {
459
+ ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((s) =>
460
+ process.on(s, () => {
461
+ tui.close();
462
+ res();
463
+ })
464
+ );
465
+ });
466
+ }
467
+ }
468
+ // No subcommands so subshell doesn't make sense
469
+ // No handler, so nothing to run
470
+ else {
471
+ throw new Error(
472
+ `${[this.name, ...this.commandChain].join(' ')} is not implemented.`
473
+ );
474
+ }
475
+ }
476
+ } catch (e) {
477
+ process.exitCode = 1;
478
+ console.error(e);
479
+ this.printHelp();
480
+ }
481
+ }
482
+
483
+ getChildren(): TChildren {
484
+ // Return a copy of registered commands, excluding aliases (same command registered under different keys)
485
+ const children: Record<string, InternalCLI<any, any, any, any>> = {};
486
+ const seen = new Set<InternalCLI<any, any, any, any>>();
487
+ for (const [key, cmd] of Object.entries(this.registeredCommands)) {
488
+ if (!seen.has(cmd)) {
489
+ seen.add(cmd);
490
+ children[key] = cmd;
491
+ }
492
+ }
493
+ return children as TChildren;
494
+ }
495
+
496
+ getParent(): TParent {
497
+ return this._parent as TParent;
498
+ }
499
+
500
+ getBuilder<T extends ParsedArgs = { unmatched: string[]; '--'?: string[] }>(
501
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- This is used to help TS infer T correctly
502
+ _?: CLI<T, any, any> | undefined
503
+ ):
504
+ | ((parser: CLI<T, any, any>) => CLI<TArgs, THandlerReturn, TChildren>)
505
+ | undefined {
506
+ return this.configuration?.builder as any;
507
+ }
508
+
509
+ getHandler():
510
+ | ((args: Omit<TArgs, keyof ParsedArgs>) => THandlerReturn)
511
+ | undefined {
512
+ const context: CLIHandlerContext<TChildren, TParent> = {
513
+ command: this as unknown as CLI<any, any, TChildren, TParent>,
514
+ };
515
+ const handler = this._configuration?.handler;
516
+ if (!handler) {
517
+ return undefined;
518
+ }
519
+ return (args: Omit<TArgs, keyof ParsedArgs>) =>
520
+ handler(
521
+ args as TArgs,
522
+ context as CLIHandlerContext<any, any>
523
+ ) as THandlerReturn;
524
+ }
525
+
526
+ enableInteractiveShell(): CLI<TArgs, THandlerReturn, TChildren, TParent> {
527
+ if (this.requiresCommand === 'EXPLICIT') {
528
+ throw new Error(
529
+ 'Interactive shell is not supported for commands that require a command.'
530
+ );
531
+ } else if (process.stdout.isTTY) {
532
+ this.requiresCommand = false;
533
+ }
534
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
535
+ }
536
+
537
+ private versionHandler() {
538
+ if (this._versionOverride) {
539
+ console.log(this._versionOverride);
540
+ return;
541
+ }
542
+ let mainFile = require?.main?.filename;
543
+ mainFile ??= getCallingFile();
544
+ if (!mainFile) {
545
+ console.log('unknown');
546
+ return;
547
+ }
548
+ const packageJson = getParentPackageJson(mainFile);
549
+ console.log(packageJson.version ?? 'unknown');
550
+ }
551
+
552
+ private async withErrorHandlers<T>(cb: () => T): Promise<Awaited<T>> {
553
+ try {
554
+ return await cb();
555
+ } catch (e) {
556
+ for (const handler of this.registeredErrorHandlers) {
557
+ try {
558
+ handler(e, {
559
+ exit: (c) => {
560
+ process.exit(c);
561
+ },
562
+ });
563
+ // Error was handled, no need to continue
564
+ break;
565
+ } catch {
566
+ // Error was not handled, continue to the next handler
567
+ }
568
+ }
569
+ throw e;
570
+ }
571
+ }
572
+
573
+ errorHandler(
574
+ handler: ErrorHandler
575
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
576
+ this.registeredErrorHandlers.unshift(handler);
577
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
578
+ }
579
+
580
+ group(
581
+ labelOrConfigObject:
582
+ | string
583
+ | { label: string; keys: (keyof TArgs)[]; sortOrder: number },
584
+ keys?: (keyof TArgs)[]
585
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
586
+ const config =
587
+ typeof labelOrConfigObject === 'object'
588
+ ? labelOrConfigObject
589
+ : {
590
+ label: labelOrConfigObject,
591
+ keys: keys as (keyof TArgs)[],
592
+ sortOrder: Object.keys(this.registeredOptionGroups).length,
593
+ };
594
+
595
+ if (!config.keys) {
596
+ throw new Error('keys must be provided when calling `group`.');
597
+ }
598
+
599
+ this.registeredOptionGroups.push(config);
600
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
601
+ }
602
+
603
+ config(
604
+ provider: ConfigurationFiles.ConfigurationProvider<TArgs>
605
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
606
+ this.parser.config(
607
+ provider as ConfigurationFiles.ConfigurationProvider<any>
608
+ );
609
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
610
+ }
611
+
612
+ /**
613
+ * Parses argv and executes the CLI
614
+ * @param args argv. Defaults to process.argv.slice(2)
615
+ * @returns Promise that resolves when the handler completes.
616
+ */
617
+ forge = (args: string[] = hideBin(process.argv)) =>
618
+ this.withErrorHandlers(async () => {
619
+ // Parsing the args does two things:
620
+ // - builds argv to pass to handler
621
+ // - fills the command chain + registers commands
622
+ let argv: TArgs & { help?: boolean; version?: boolean };
623
+ let validationFailedError: ValidationFailedError<TArgs> | undefined;
624
+ try {
625
+ argv = this.parser.parse(args);
626
+ } catch (e) {
627
+ if (e instanceof ValidationFailedError) {
628
+ argv = e.partialArgV as TArgs;
629
+ validationFailedError = e;
630
+ } else {
631
+ throw e;
632
+ }
633
+ }
634
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
635
+ let currentCommand: InternalCLI<any, any, any, any> = this;
636
+ for (const command of this.commandChain) {
637
+ currentCommand = currentCommand.registeredCommands[command];
638
+ }
639
+
640
+ if (argv.version) {
641
+ this.versionHandler();
642
+ return argv;
643
+ }
644
+
645
+ if (argv.help) {
646
+ this.printHelp();
647
+ return argv;
648
+ } else if (validationFailedError) {
649
+ throw validationFailedError;
650
+ }
651
+
652
+ const finalArgV =
653
+ this.commandChain.length === 0 && this.configuration?.builder
654
+ ? (
655
+ this.configuration.builder?.(
656
+ this as any
657
+ ) as unknown as InternalCLI<TArgs, any, any, any>
658
+ ).parser.parse(args)
659
+ : argv;
660
+
661
+ await this.runCommand(finalArgV, args);
662
+ return finalArgV as TArgs;
663
+ });
664
+
665
+ getParser() {
666
+ return this.parser.asReadonly();
667
+ }
668
+
669
+ getSubcommands() {
670
+ return this.registeredCommands as Readonly<Record<string, InternalCLI>>;
671
+ }
672
+
673
+ clone() {
674
+ const clone = new InternalCLI<TArgs, THandlerReturn, TChildren, TParent>(
675
+ this.name
676
+ );
677
+ clone.parser = this.parser.clone(clone.parser.options) as any;
678
+ if (this.configuration) {
679
+ clone.withRootCommandConfiguration(this.configuration);
680
+ }
681
+ clone.registeredCommands = {};
682
+ for (const command in this.registeredCommands ?? {}) {
683
+ clone.command(this.registeredCommands[command].clone() as any);
684
+ }
685
+ clone.commandChain = [...this.commandChain];
686
+ clone.requiresCommand = this.requiresCommand;
687
+ return clone;
688
+ }
689
+ }