cli-forge 0.11.0 → 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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
1
2
  import {
2
3
  ArgvParser,
3
4
  EnvOptionConfig,
@@ -10,7 +11,13 @@ import {
10
11
  } from '@cli-forge/parser';
11
12
  import { getCallingFile, getParentPackageJson } from './utils';
12
13
  import { INTERACTIVE_SHELL, InteractiveShell } from './interactive-shell';
13
- import { CLI, CLICommandOptions, Command, ErrorHandler } from './public-api';
14
+ import {
15
+ CLI,
16
+ CLICommandOptions,
17
+ CLIHandlerContext,
18
+ Command,
19
+ ErrorHandler,
20
+ } from './public-api';
14
21
  import { readOptionGroupsForCLI } from './cli-option-groups';
15
22
  import { formatHelp } from './format-help';
16
23
 
@@ -33,19 +40,30 @@ import { formatHelp } from './format-help';
33
40
  * }).forge();
34
41
  * ```
35
42
  */
36
- export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
37
- implements CLI<TArgs>
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>
38
50
  {
39
51
  /**
40
52
  * For internal use only. Stick to properties available on {@link CLI}.
41
53
  */
42
- registeredCommands: Record<string, InternalCLI<any>> = {};
54
+ registeredCommands: Record<string, InternalCLI<any, any, any, any>> = {};
43
55
 
44
56
  /**
45
57
  * For internal use only. Stick to properties available on {@link CLI}.
46
58
  */
47
59
  commandChain: string[] = [];
48
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
+
49
67
  private requiresCommand: 'IMPLICIT' | 'EXPLICIT' | false = 'IMPLICIT';
50
68
 
51
69
  private _configuration?: CLICommandOptions<any, any>;
@@ -100,14 +118,14 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
100
118
  parser = new ArgvParser<TArgs>({
101
119
  unmatchedParser: (arg) => {
102
120
  // eslint-disable-next-line @typescript-eslint/no-this-alias
103
- let currentCommand: InternalCLI<any> = this;
121
+ let currentCommand: InternalCLI<any, any, any, any> = this;
104
122
  for (const command of this.commandChain) {
105
123
  currentCommand = currentCommand.registeredCommands[command];
106
124
  }
107
125
  const command = currentCommand.registeredCommands[arg];
108
126
  if (command && command.configuration) {
109
127
  command.parser = this.parser;
110
- command.configuration.builder?.(command);
128
+ command.configuration.builder?.(command as any);
111
129
  this.commandChain.push(arg);
112
130
  return true;
113
131
  }
@@ -130,10 +148,15 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
130
148
  */
131
149
  constructor(
132
150
  public name: string,
133
- rootCommandConfiguration?: CLICommandOptions<TArgs>
151
+ rootCommandConfiguration?: CLICommandOptions<
152
+ TArgs,
153
+ any,
154
+ THandlerReturn,
155
+ TChildren
156
+ >
134
157
  ) {
135
158
  if (rootCommandConfiguration) {
136
- this.withRootCommandConfiguration(rootCommandConfiguration);
159
+ this.withRootCommandConfiguration(rootCommandConfiguration as any);
137
160
  } else {
138
161
  this.requiresCommand = 'IMPLICIT';
139
162
  }
@@ -141,16 +164,87 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
141
164
 
142
165
  withRootCommandConfiguration<TRootCommandArgs extends TArgs>(
143
166
  configuration: CLICommandOptions<TArgs, TRootCommandArgs>
144
- ): InternalCLI<TArgs> {
167
+ ): InternalCLI<TArgs, THandlerReturn, TChildren, TParent> {
145
168
  this.configuration = configuration;
146
169
  this.requiresCommand = false;
147
170
  return this;
148
171
  }
149
172
 
150
173
  command<TCommandArgs extends TArgs>(
151
- keyOrCommand: string | Command<TArgs, TCommandArgs>,
152
- options?: CLICommandOptions<TArgs, TCommandArgs>
153
- ): CLI<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
+ > {
154
248
  if (typeof keyOrCommand === 'string') {
155
249
  const key = keyOrCommand;
156
250
  if (!options) {
@@ -166,9 +260,10 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
166
260
  description: options.description,
167
261
  });
168
262
  }
169
- const cmd = new InternalCLI<TArgs>(key).withRootCommandConfiguration(
170
- options
171
- );
263
+ const cmd = new InternalCLI<TArgs, TChildHandlerReturn>(
264
+ key
265
+ ).withRootCommandConfiguration(options as any);
266
+ cmd._parent = this;
172
267
  this.registeredCommands[key] = cmd;
173
268
  if (options.alias) {
174
269
  for (const alias of options.alias) {
@@ -177,6 +272,7 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
177
272
  }
178
273
  } else if (keyOrCommand instanceof InternalCLI) {
179
274
  const cmd = keyOrCommand;
275
+ cmd._parent = this;
180
276
  this.registeredCommands[cmd.name] = cmd;
181
277
  if (cmd.configuration?.alias) {
182
278
  for (const alias of cmd.configuration.alias) {
@@ -189,13 +285,14 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
189
285
  } & CLICommandOptions<TArgs, TCommandArgs>;
190
286
  this.command<TCommandArgs>(name, configuration);
191
287
  }
192
- return this;
288
+ return this as any;
193
289
  }
194
290
 
195
- commands(...a0: Command[] | Command[][]): CLI<TArgs> {
291
+ commands(...a0: Command[] | Command[][]): any {
196
292
  const commands = a0.flat();
197
293
  for (const val of commands) {
198
294
  if (val instanceof InternalCLI) {
295
+ val._parent = this;
199
296
  this.registeredCommands[val.name] = val;
200
297
  // Include any options that were defined via cli(...).option() instead of via builder
201
298
  this.parser.augment(val.parser);
@@ -227,51 +324,58 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
227
324
  return this as any;
228
325
  }
229
326
 
230
- conflicts(...args: [string, string, ...string[]]): CLI<TArgs> {
327
+ conflicts(
328
+ ...args: [string, string, ...string[]]
329
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
231
330
  this.parser.conflicts(...args);
232
- return this;
331
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
233
332
  }
234
333
 
235
- implies(option: string, ...impliedOptions: string[]): CLI<TArgs> {
334
+ implies(
335
+ option: string,
336
+ ...impliedOptions: string[]
337
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
236
338
  this.parser.implies(option, ...impliedOptions);
237
- return this;
339
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
238
340
  }
239
341
 
240
342
  env(
241
343
  a0: string | EnvOptionConfig | undefined = fromCamelOrDashedCaseToConstCase(
242
344
  this.name
243
345
  )
244
- ) {
346
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
245
347
  if (typeof a0 === 'string') {
246
348
  this.parser.env(a0);
247
349
  } else {
248
350
  a0.prefix ??= fromCamelOrDashedCaseToConstCase(this.name);
249
351
  this.parser.env(a0);
250
352
  }
251
- return this;
353
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
252
354
  }
253
355
 
254
- demandCommand() {
356
+ demandCommand(): CLI<TArgs, THandlerReturn, TChildren, TParent> {
255
357
  this.requiresCommand = 'EXPLICIT';
256
- return this;
358
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
257
359
  }
258
360
 
259
- usage(usageText: string) {
361
+ usage(usageText: string): CLI<TArgs, THandlerReturn, TChildren, TParent> {
260
362
  this.configuration ??= {};
261
363
  this.configuration.usage = usageText;
262
- return this;
364
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
263
365
  }
264
366
 
265
- examples(...examples: string[]) {
367
+ examples(
368
+ ...examples: string[]
369
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
266
370
  this.configuration ??= {};
267
371
  this.configuration.examples ??= [];
268
372
  this.configuration.examples.push(...examples);
269
- return this;
373
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
270
374
  }
271
375
 
272
- version(version?: string) {
376
+ version(version?: string): CLI<TArgs, THandlerReturn, TChildren, TParent> {
273
377
  this._versionOverride = version;
274
- return this;
378
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
275
379
  }
276
380
 
277
381
  /**
@@ -291,7 +395,12 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
291
395
 
292
396
  middleware<TArgs2>(
293
397
  callback: (args: TArgs) => TArgs2 | Promise<TArgs2>
294
- ): CLI<TArgs2 extends void ? TArgs : TArgs & TArgs2> {
398
+ ): CLI<
399
+ TArgs2 extends void ? TArgs : TArgs & TArgs2,
400
+ THandlerReturn,
401
+ TChildren,
402
+ TParent
403
+ > {
295
404
  this.registeredMiddleware.push(callback);
296
405
  // If middleware returns void, TArgs doesn't change...
297
406
  // If it returns something, we need to merge it into TArgs...
@@ -309,7 +418,7 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
309
418
  ...this.registeredMiddleware,
310
419
  ];
311
420
  // eslint-disable-next-line @typescript-eslint/no-this-alias
312
- let cmd: InternalCLI<any> = this;
421
+ let cmd: InternalCLI<any, any, any, any> = this;
313
422
  for (const command of this.commandChain) {
314
423
  cmd = cmd.registeredCommands[command];
315
424
  middlewares.push(...cmd.registeredMiddleware);
@@ -330,8 +439,8 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
330
439
  args = middlewareResult as T;
331
440
  }
332
441
  }
333
- await cmd.configuration.handler(args, {
334
- command: cmd,
442
+ return cmd.configuration.handler(args, {
443
+ command: cmd as any,
335
444
  });
336
445
  } else {
337
446
  // We can treat a command as a subshell if it has subcommands
@@ -340,9 +449,12 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
340
449
  // If we're not in a TTY, we can't run an interactive shell...
341
450
  // Maybe we should warn here?
342
451
  } else if (!INTERACTIVE_SHELL) {
343
- const tui = new InteractiveShell(this, {
344
- prependArgs: originalArgV,
345
- });
452
+ const tui = new InteractiveShell(
453
+ this as unknown as InternalCLI<any>,
454
+ {
455
+ prependArgs: originalArgV,
456
+ }
457
+ );
346
458
  await new Promise<void>((res) => {
347
459
  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((s) =>
348
460
  process.on(s, () => {
@@ -368,7 +480,50 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
368
480
  }
369
481
  }
370
482
 
371
- enableInteractiveShell() {
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> {
372
527
  if (this.requiresCommand === 'EXPLICIT') {
373
528
  throw new Error(
374
529
  'Interactive shell is not supported for commands that require a command.'
@@ -376,7 +531,7 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
376
531
  } else if (process.stdout.isTTY) {
377
532
  this.requiresCommand = false;
378
533
  }
379
- return this;
534
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
380
535
  }
381
536
 
382
537
  private versionHandler() {
@@ -415,9 +570,11 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
415
570
  }
416
571
  }
417
572
 
418
- errorHandler(handler: ErrorHandler) {
573
+ errorHandler(
574
+ handler: ErrorHandler
575
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
419
576
  this.registeredErrorHandlers.unshift(handler);
420
- return this;
577
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
421
578
  }
422
579
 
423
580
  group(
@@ -425,7 +582,7 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
425
582
  | string
426
583
  | { label: string; keys: (keyof TArgs)[]; sortOrder: number },
427
584
  keys?: (keyof TArgs)[]
428
- ): CLI<TArgs> {
585
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
429
586
  const config =
430
587
  typeof labelOrConfigObject === 'object'
431
588
  ? labelOrConfigObject
@@ -440,16 +597,16 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
440
597
  }
441
598
 
442
599
  this.registeredOptionGroups.push(config);
443
- return this;
600
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
444
601
  }
445
602
 
446
603
  config(
447
604
  provider: ConfigurationFiles.ConfigurationProvider<TArgs>
448
- ): CLI<TArgs> {
605
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
449
606
  this.parser.config(
450
607
  provider as ConfigurationFiles.ConfigurationProvider<any>
451
608
  );
452
- return this;
609
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
453
610
  }
454
611
 
455
612
  /**
@@ -475,7 +632,7 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
475
632
  }
476
633
  }
477
634
  // eslint-disable-next-line @typescript-eslint/no-this-alias
478
- let currentCommand: InternalCLI<any> = this;
635
+ let currentCommand: InternalCLI<any, any, any, any> = this;
479
636
  for (const command of this.commandChain) {
480
637
  currentCommand = currentCommand.registeredCommands[command];
481
638
  }
@@ -495,7 +652,9 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
495
652
  const finalArgV =
496
653
  this.commandChain.length === 0 && this.configuration?.builder
497
654
  ? (
498
- this.configuration.builder?.(this as any) as InternalCLI<TArgs>
655
+ this.configuration.builder?.(
656
+ this as any
657
+ ) as unknown as InternalCLI<TArgs, any, any, any>
499
658
  ).parser.parse(args)
500
659
  : argv;
501
660
 
@@ -512,15 +671,16 @@ export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
512
671
  }
513
672
 
514
673
  clone() {
515
- const clone = new InternalCLI<TArgs>(this.name);
674
+ const clone = new InternalCLI<TArgs, THandlerReturn, TChildren, TParent>(
675
+ this.name
676
+ );
516
677
  clone.parser = this.parser.clone(clone.parser.options) as any;
517
678
  if (this.configuration) {
518
679
  clone.withRootCommandConfiguration(this.configuration);
519
680
  }
520
681
  clone.registeredCommands = {};
521
682
  for (const command in this.registeredCommands ?? {}) {
522
- clone.command(this.registeredCommands[command].clone());
523
- // this.registeredCommands[command].clone();
683
+ clone.command(this.registeredCommands[command].clone() as any);
524
684
  }
525
685
  clone.commandChain = [...this.commandChain];
526
686
  clone.requiresCommand = this.requiresCommand;