cli-forge 1.1.0 → 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.
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { makeComposableBuilder } from './composable-builder';
3
+ import cli from './public-api';
4
+ import { chain } from '@cli-forge/parser';
5
+
6
+ describe('makeComposableBuilder', () => {
7
+ describe('capture-and-replay', () => {
8
+ it('should produce stable middleware references across applications', () => {
9
+ const builder = makeComposableBuilder((cmd) =>
10
+ cmd
11
+ .option('verbose', { type: 'boolean' })
12
+ .middleware((args: any) => args)
13
+ );
14
+
15
+ const cli1 = builder(cli('test1'));
16
+ const cli2 = builder(cli('test2'));
17
+
18
+ const mw1 = [...(cli1 as any).registeredMiddleware];
19
+ const mw2 = [...(cli2 as any).registeredMiddleware];
20
+ expect(mw1.length).toBe(1);
21
+ expect(mw2.length).toBe(1);
22
+ expect(mw1[0]).toBe(mw2[0]);
23
+ });
24
+
25
+ it('should correctly replay option registrations', async () => {
26
+ const builder = makeComposableBuilder((cmd) =>
27
+ cmd.option('verbose', { type: 'boolean' })
28
+ );
29
+
30
+ let handlerArgs: any;
31
+ await chain(cli('test'), builder)
32
+ .command('$0', {
33
+ handler: (args) => {
34
+ handlerArgs = args;
35
+ },
36
+ })
37
+ .forge(['--verbose']);
38
+ expect(handlerArgs.verbose).toBe(true);
39
+ });
40
+
41
+ it('should deduplicate middleware when builder applied to parent and child', async () => {
42
+ let mwCallCount = 0;
43
+ const builder = makeComposableBuilder((cmd) =>
44
+ cmd.option('verbose', { type: 'boolean' }).middleware((args: any) => {
45
+ mwCallCount++;
46
+ return args;
47
+ })
48
+ );
49
+
50
+ await chain(cli('parent'), builder)
51
+ .command('child', {
52
+ builder: (cmd) =>
53
+ chain(cmd, builder).option('format', { type: 'string' }),
54
+ handler: () => { /* noop */ },
55
+ })
56
+ .forge(['child']);
57
+ expect(mwCallCount).toBe(1);
58
+ });
59
+
60
+ it('should replay commands registered by the builder', () => {
61
+ const builder = makeComposableBuilder((cmd) =>
62
+ cmd.command('sub', {
63
+ builder: (c) => c.option('flag', { type: 'boolean' }),
64
+ handler: () => { /* noop */ },
65
+ })
66
+ );
67
+
68
+ const myCli = builder(cli('test'));
69
+ const children = myCli.getChildren();
70
+ expect(children).toHaveProperty('sub');
71
+ });
72
+ });
73
+ });
@@ -1,4 +1,4 @@
1
- import { ParsedArgs } from '@cli-forge/parser';
1
+ import type { ParsedArgs } from '@cli-forge/parser';
2
2
  import { CLI } from './public-api';
3
3
 
4
4
  /**
@@ -30,6 +30,10 @@ export type ComposableBuilder<
30
30
  * Can be used to add options, commands, or any other CLI modifications.
31
31
  * Children added by the builder function are properly tracked in the type.
32
32
  *
33
+ * The builder function runs once at creation time against a recording Proxy.
34
+ * Subsequent applications replay the captured operations, ensuring inline
35
+ * middleware closures have stable references for Set-based deduplication.
36
+ *
33
37
  * @typeParam TArgs2 - The args type after the builder runs
34
38
  * @typeParam TChildren2 - The children type added by the builder
35
39
  */
@@ -43,15 +47,32 @@ export function makeComposableBuilder<
43
47
  init: CLI<ParsedArgs, any, {}, any>
44
48
  ) => CLI<TArgs2, any, TChildren2, any>
45
49
  ) {
50
+ // Run builder once against a recording proxy to capture operations.
51
+ // Replaying these ensures inline closures (e.g. middleware) keep stable
52
+ // references across applications, enabling Set-based deduplication.
53
+ const operations: { method: string; args: any[] }[] = [];
54
+ const proxy = new Proxy({} as CLI, {
55
+ get(_target, prop) {
56
+ return (...args: any[]) => {
57
+ operations.push({ method: prop as string, args });
58
+ return proxy;
59
+ };
60
+ },
61
+ });
62
+ fn(proxy);
63
+
46
64
  return <TInit extends ParsedArgs, THandlerReturn, TChildren, TParent>(
47
65
  init: CLI<TInit, THandlerReturn, TChildren, TParent>
48
- ) =>
49
- // eslint-disable-next-line @typescript-eslint/ban-types
50
- fn(init as unknown as CLI<ParsedArgs, any, {}, any>) as unknown as CLI<
66
+ ) => {
67
+ let current: any = init;
68
+ for (const op of operations) {
69
+ current = current[op.method](...op.args);
70
+ }
71
+ return current as unknown as CLI<
51
72
  TInit & TArgs2,
52
73
  THandlerReturn,
53
74
  TChildren & TChildren2,
54
75
  TParent
55
76
  >;
77
+ };
56
78
  }
57
-
@@ -58,6 +58,8 @@ export class InteractiveShell {
58
58
  process.emit('SIGINT');
59
59
  });
60
60
 
61
+ // Show the cursor (in case it was hidden)
62
+ process.stdout.write('\x1b[?25h');
61
63
  this.rl.prompt();
62
64
 
63
65
  this.registerLineListener(async (line) => {