cli-forge 1.1.0 → 1.2.1

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.
@@ -48,7 +48,7 @@ describe('CLI localization', () => {
48
48
  .option('name', { type: 'string' })
49
49
  .option('port', { type: 'number' })
50
50
  .command('$0', {
51
- handler: () => {},
51
+ handler: () => { /* noop */ },
52
52
  });
53
53
 
54
54
  const harness = new TestHarness(testCli);
@@ -64,7 +64,7 @@ describe('CLI localization', () => {
64
64
  .option('name', { type: 'string' })
65
65
  .option('port', { type: 'number' })
66
66
  .command('$0', {
67
- handler: () => {},
67
+ handler: () => { /* noop */ },
68
68
  });
69
69
 
70
70
  const harness = new TestHarness(testCli);
@@ -100,7 +100,7 @@ describe('CLI localization', () => {
100
100
  .localize(dictionary, 'es-ES')
101
101
  .command('serve', {
102
102
  builder: (cmd) => cmd,
103
- handler: () => {},
103
+ handler: () => { /* noop */ },
104
104
  description: 'Start the server',
105
105
  })
106
106
  .forge(['--help']);
@@ -121,7 +121,7 @@ describe('CLI localization', () => {
121
121
  cmd
122
122
  .option('port', { type: 'number' })
123
123
  .option('name', { type: 'string' }),
124
- handler: () => {},
124
+ handler: () => { /* noop */ },
125
125
  });
126
126
 
127
127
  const harness = new TestHarness(testCli);
@@ -137,7 +137,7 @@ describe('CLI localization', () => {
137
137
  .option('name', { type: 'string' })
138
138
  .option('port', { type: 'number' })
139
139
  .command('$0', {
140
- handler: () => {},
140
+ handler: () => { /* noop */ },
141
141
  });
142
142
 
143
143
  const harness = new TestHarness(testCli);
@@ -154,7 +154,7 @@ describe('CLI localization', () => {
154
154
  .option('port', { type: 'number' })
155
155
  .env('TEST')
156
156
  .command('$0', {
157
- handler: () => {},
157
+ handler: () => { /* noop */ },
158
158
  });
159
159
 
160
160
  const harness = new TestHarness(testCli);
@@ -180,7 +180,7 @@ describe('CLI localization', () => {
180
180
  .option('port', { type: 'number' })
181
181
  .command('serve', {
182
182
  builder: (cmd) => cmd,
183
- handler: () => {},
183
+ handler: () => { /* noop */ },
184
184
  });
185
185
 
186
186
  const harness = new TestHarness(testCli);
@@ -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) => {