cli-forge 0.12.0 → 1.0.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/.eslintrc.json +1 -0
- package/dist/lib/internal-cli.d.ts +7 -7
- package/dist/lib/internal-cli.js +101 -4
- package/dist/lib/internal-cli.js.map +1 -1
- package/dist/lib/public-api.d.ts +90 -9
- package/dist/lib/public-api.js.map +1 -1
- package/package.json +7 -3
- package/src/lib/documentation.spec.ts +1 -0
- package/src/lib/internal-cli.spec.ts +6 -5
- package/src/lib/internal-cli.ts +130 -8
- package/src/lib/public-api.ts +120 -28
- package/src/lib/sdk.spec.ts +285 -0
- package/src/lib/test-harness.spec.ts +1 -0
- package/src/lib/utils.spec.ts +1 -0
- package/tsconfig.lib.json.tsbuildinfo +1 -1
package/src/lib/public-api.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
ResolveProperties,
|
|
14
14
|
WithOptional,
|
|
15
15
|
MakeUndefinedPropertiesOptional,
|
|
16
|
-
WithAdditionalProperties,
|
|
17
16
|
} from '@cli-forge/parser';
|
|
18
17
|
|
|
19
18
|
import { InternalCLI } from './internal-cli';
|
|
@@ -127,7 +126,7 @@ export interface CLI<
|
|
|
127
126
|
TArgs,
|
|
128
127
|
TCommandArgs,
|
|
129
128
|
TChildHandlerReturn,
|
|
130
|
-
|
|
129
|
+
TChildren,
|
|
131
130
|
CLI<TArgs, THandlerReturn, TChildren, TParent>,
|
|
132
131
|
TChildChildren
|
|
133
132
|
>
|
|
@@ -436,22 +435,16 @@ export interface CLI<
|
|
|
436
435
|
option<
|
|
437
436
|
TOption extends string,
|
|
438
437
|
TCoerce,
|
|
439
|
-
const TProps extends Record<string, { type: string }
|
|
440
|
-
TAdditionalProps extends false | 'string' | 'number' | 'boolean' = false
|
|
438
|
+
const TProps extends Record<string, { type: string }>
|
|
441
439
|
>(
|
|
442
440
|
name: TOption,
|
|
443
|
-
config: ObjectOptionConfig<TCoerce, TProps
|
|
441
|
+
config: ObjectOptionConfig<TCoerce, TProps>
|
|
444
442
|
): CLI<
|
|
445
443
|
TArgs &
|
|
446
444
|
MakeUndefinedPropertiesOptional<{
|
|
447
445
|
[key in TOption]: WithOptional<
|
|
448
|
-
unknown extends TCoerce
|
|
449
|
-
|
|
450
|
-
ResolveProperties<TProps>,
|
|
451
|
-
TAdditionalProps
|
|
452
|
-
>
|
|
453
|
-
: TCoerce,
|
|
454
|
-
ObjectOptionConfig<TCoerce, TProps, TAdditionalProps>
|
|
446
|
+
unknown extends TCoerce ? ResolveProperties<TProps> : TCoerce,
|
|
447
|
+
ObjectOptionConfig<TCoerce, TProps>
|
|
455
448
|
>;
|
|
456
449
|
}>,
|
|
457
450
|
THandlerReturn,
|
|
@@ -525,7 +518,7 @@ export interface CLI<
|
|
|
525
518
|
// Generic fallback overload
|
|
526
519
|
option<
|
|
527
520
|
TOption extends string,
|
|
528
|
-
const TOptionConfig extends OptionConfig<any, any, any
|
|
521
|
+
const TOptionConfig extends OptionConfig<any, any, any>
|
|
529
522
|
>(
|
|
530
523
|
name: TOption,
|
|
531
524
|
config: TOptionConfig
|
|
@@ -552,22 +545,16 @@ export interface CLI<
|
|
|
552
545
|
positional<
|
|
553
546
|
TOption extends string,
|
|
554
547
|
TCoerce,
|
|
555
|
-
const TProps extends Record<string, { type: string }
|
|
556
|
-
TAdditionalProps extends false | 'string' | 'number' | 'boolean' = false
|
|
548
|
+
const TProps extends Record<string, { type: string }>
|
|
557
549
|
>(
|
|
558
550
|
name: TOption,
|
|
559
|
-
config: ObjectOptionConfig<TCoerce, TProps
|
|
551
|
+
config: ObjectOptionConfig<TCoerce, TProps>
|
|
560
552
|
): CLI<
|
|
561
553
|
TArgs &
|
|
562
554
|
MakeUndefinedPropertiesOptional<{
|
|
563
555
|
[key in TOption]: WithOptional<
|
|
564
|
-
unknown extends TCoerce
|
|
565
|
-
|
|
566
|
-
ResolveProperties<TProps>,
|
|
567
|
-
TAdditionalProps
|
|
568
|
-
>
|
|
569
|
-
: TCoerce,
|
|
570
|
-
ObjectOptionConfig<TCoerce, TProps, TAdditionalProps>
|
|
556
|
+
unknown extends TCoerce ? ResolveProperties<TProps> : TCoerce,
|
|
557
|
+
ObjectOptionConfig<TCoerce, TProps>
|
|
571
558
|
>;
|
|
572
559
|
}>,
|
|
573
560
|
THandlerReturn,
|
|
@@ -641,7 +628,7 @@ export interface CLI<
|
|
|
641
628
|
// Generic fallback overload
|
|
642
629
|
positional<
|
|
643
630
|
TOption extends string,
|
|
644
|
-
const TOptionConfig extends OptionConfig<any, any, any
|
|
631
|
+
const TOptionConfig extends OptionConfig<any, any, any>
|
|
645
632
|
>(
|
|
646
633
|
name: TOption,
|
|
647
634
|
config: TOptionConfig
|
|
@@ -778,10 +765,54 @@ export interface CLI<
|
|
|
778
765
|
*/
|
|
779
766
|
getParent(): TParent;
|
|
780
767
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
768
|
+
/**
|
|
769
|
+
* Returns a programmatic SDK for invoking this CLI and its subcommands.
|
|
770
|
+
* The SDK provides typed function calls instead of argv parsing.
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* ```ts
|
|
774
|
+
* const myCLI = cli('my-app')
|
|
775
|
+
* .option('verbose', { type: 'boolean' })
|
|
776
|
+
* .command('build', {
|
|
777
|
+
* builder: (cmd) => cmd.option('watch', { type: 'boolean' }),
|
|
778
|
+
* handler: (args) => ({ success: true, files: ['a.js'] })
|
|
779
|
+
* });
|
|
780
|
+
*
|
|
781
|
+
* const sdk = myCLI.sdk();
|
|
782
|
+
*
|
|
783
|
+
* // Invoke root command (if it has a handler)
|
|
784
|
+
* await sdk({ verbose: true });
|
|
785
|
+
*
|
|
786
|
+
* // Invoke subcommand with typed args
|
|
787
|
+
* const result = await sdk.build({ watch: true });
|
|
788
|
+
* console.log(result.files); // ['a.js']
|
|
789
|
+
* console.log(result.$args.watch); // true
|
|
790
|
+
*
|
|
791
|
+
* // Use CLI-style args for -- support
|
|
792
|
+
* await sdk.build(['--watch', '--', 'extra-arg']);
|
|
793
|
+
* ```
|
|
794
|
+
*
|
|
795
|
+
* @returns An SDK object that is callable (if this command has a handler)
|
|
796
|
+
* and has properties for each subcommand.
|
|
797
|
+
*/
|
|
798
|
+
sdk(): SDKCommand<TArgs, THandlerReturn, TChildren>;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Returns the builder function for this command as a composable builder.
|
|
802
|
+
* The returned function can be used with `chain` to compose multiple builders.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```ts
|
|
806
|
+
* const siblings = args.getParent().getChildren();
|
|
807
|
+
* const withBuildArgs = siblings.build.getBuilder()!;
|
|
808
|
+
* const withServeArgs = siblings.serve.getBuilder()!;
|
|
809
|
+
* return chain(args, withBuildArgs, withServeArgs);
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
getBuilder():
|
|
813
|
+
| (<TInit extends ParsedArgs, TInitHandlerReturn, TInitChildren, TInitParent>(
|
|
814
|
+
parser: CLI<TInit, TInitHandlerReturn, TInitChildren, TInitParent>
|
|
815
|
+
) => CLI<TInit & TArgs, TInitHandlerReturn, TInitChildren & TChildren, TInitParent>)
|
|
785
816
|
| undefined;
|
|
786
817
|
getHandler():
|
|
787
818
|
| ((args: Omit<TArgs, keyof ParsedArgs>) => THandlerReturn)
|
|
@@ -910,6 +941,67 @@ export type MiddlewareFunction<TArgs extends ParsedArgs, TArgs2> = (
|
|
|
910
941
|
args: TArgs
|
|
911
942
|
) => TArgs2 | Promise<TArgs2>;
|
|
912
943
|
|
|
944
|
+
// ============================================================================
|
|
945
|
+
// SDK Types
|
|
946
|
+
// ============================================================================
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Result type that conditionally includes $args.
|
|
950
|
+
* Only attaches $args when result is an object type.
|
|
951
|
+
* Uses Awaited<T> to handle async handlers that return Promise<U>.
|
|
952
|
+
*/
|
|
953
|
+
export type SDKResult<TArgs, THandlerReturn> =
|
|
954
|
+
Awaited<THandlerReturn> extends object
|
|
955
|
+
? Awaited<THandlerReturn> & { $args: TArgs }
|
|
956
|
+
: Awaited<THandlerReturn>;
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* The callable signature for a command with a handler.
|
|
960
|
+
* Supports both object-style args (typed, skips validation) and
|
|
961
|
+
* string array args (CLI-style, full validation pipeline).
|
|
962
|
+
*/
|
|
963
|
+
export type SDKInvokable<TArgs, THandlerReturn> = {
|
|
964
|
+
/**
|
|
965
|
+
* Invoke the command with typed object args.
|
|
966
|
+
* Skips validation (TypeScript handles it), applies defaults, runs middleware.
|
|
967
|
+
*/
|
|
968
|
+
(args?: Partial<Omit<TArgs, 'unmatched' | '--'>>): Promise<
|
|
969
|
+
SDKResult<TArgs, THandlerReturn>
|
|
970
|
+
>;
|
|
971
|
+
/**
|
|
972
|
+
* Invoke the command with CLI-style string args.
|
|
973
|
+
* Runs full pipeline: parse → validate → middleware → handler.
|
|
974
|
+
* Use this when you need to pass `--` extra args.
|
|
975
|
+
*/
|
|
976
|
+
(args: string[]): Promise<SDKResult<TArgs, THandlerReturn>>;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Recursively builds SDK type from TChildren.
|
|
981
|
+
* Each child command becomes a property on the SDK object.
|
|
982
|
+
*/
|
|
983
|
+
export type SDKChildren<TChildren> = {
|
|
984
|
+
[K in keyof TChildren]: TChildren[K] extends CLI<
|
|
985
|
+
infer A,
|
|
986
|
+
infer R,
|
|
987
|
+
infer C,
|
|
988
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
989
|
+
infer _P
|
|
990
|
+
>
|
|
991
|
+
? SDKCommand<A, R, C>
|
|
992
|
+
: never;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* A single SDK command - callable if it has a handler, with nested children as properties.
|
|
997
|
+
* Container commands (no handler) are not callable but still provide access to children.
|
|
998
|
+
*/
|
|
999
|
+
export type SDKCommand<TArgs, THandlerReturn, TChildren> =
|
|
1000
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
1001
|
+
// THandlerReturn extends void | undefined
|
|
1002
|
+
// ? SDKChildren<TChildren> // No handler = just children (not callable)
|
|
1003
|
+
SDKInvokable<TArgs, THandlerReturn> & SDKChildren<TChildren>;
|
|
1004
|
+
|
|
913
1005
|
/**
|
|
914
1006
|
* Constructs a CLI instance. See {@link CLI} for more information.
|
|
915
1007
|
* @param name Name for the top level CLI
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { cli } from './public-api';
|
|
2
|
+
|
|
3
|
+
describe('CLI.sdk()', () => {
|
|
4
|
+
describe('basic invocation', () => {
|
|
5
|
+
it('should invoke root command with object args', async () => {
|
|
6
|
+
let receivedArgs: any;
|
|
7
|
+
const myCLI = cli('test', {
|
|
8
|
+
builder: (cmd) => cmd.option('name', { type: 'string' }),
|
|
9
|
+
handler: (args) => {
|
|
10
|
+
receivedArgs = args;
|
|
11
|
+
return { success: true };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const sdk = myCLI.sdk();
|
|
16
|
+
const result = await sdk({ name: 'world' });
|
|
17
|
+
|
|
18
|
+
expect(result.success).toBe(true);
|
|
19
|
+
expect(receivedArgs.name).toBe('world');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should invoke root command with string array args', async () => {
|
|
23
|
+
let receivedArgs: any;
|
|
24
|
+
const myCLI = cli('test', {
|
|
25
|
+
builder: (cmd) => cmd.option('name', { type: 'string' }),
|
|
26
|
+
handler: (args) => {
|
|
27
|
+
receivedArgs = args;
|
|
28
|
+
return { success: true };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const sdk = myCLI.sdk();
|
|
33
|
+
const result = await sdk(['--name', 'world']);
|
|
34
|
+
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(receivedArgs.name).toBe('world');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should attach $args to object results', async () => {
|
|
40
|
+
const myCLI = cli('test', {
|
|
41
|
+
builder: (cmd) =>
|
|
42
|
+
cmd.option('count', { type: 'number', required: true }),
|
|
43
|
+
handler: (args) => ({ value: args.count * 2 }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const sdk = myCLI.sdk();
|
|
47
|
+
const result = await sdk({ count: 5 });
|
|
48
|
+
|
|
49
|
+
expect(result.value).toBe(10);
|
|
50
|
+
expect(result.$args.count).toBe(5);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return primitives without $args', async () => {
|
|
54
|
+
const myCLI = cli('test', {
|
|
55
|
+
handler: () => 42,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const sdk = myCLI.sdk();
|
|
59
|
+
const result = await sdk();
|
|
60
|
+
|
|
61
|
+
expect(result).toBe(42);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('subcommands', () => {
|
|
66
|
+
it('should invoke subcommand via property access', async () => {
|
|
67
|
+
let buildCalled = false;
|
|
68
|
+
const myCLI = cli('test').command('build', {
|
|
69
|
+
builder: (cmd) => cmd.option('watch', { type: 'boolean' }),
|
|
70
|
+
handler: (args) => {
|
|
71
|
+
buildCalled = true;
|
|
72
|
+
return { watching: args.watch };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const sdk = myCLI.sdk();
|
|
77
|
+
const result = await sdk.build({ watch: true });
|
|
78
|
+
|
|
79
|
+
expect(buildCalled).toBe(true);
|
|
80
|
+
expect(result.watching).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should invoke nested subcommands', async () => {
|
|
84
|
+
let migrateCalled = false;
|
|
85
|
+
const myCLI = cli('test').command('db', {
|
|
86
|
+
builder: (cmd) =>
|
|
87
|
+
cmd.command('migrate', {
|
|
88
|
+
builder: (c) => c.option('dry', { type: 'boolean' }),
|
|
89
|
+
handler: (args) => {
|
|
90
|
+
migrateCalled = true;
|
|
91
|
+
return { dryRun: args.dry };
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sdk = myCLI.sdk();
|
|
97
|
+
const result = await sdk.db.migrate({ dry: true });
|
|
98
|
+
|
|
99
|
+
expect(migrateCalled).toBe(true);
|
|
100
|
+
expect(result.dryRun).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw when invoking command without handler', async () => {
|
|
104
|
+
const myCLI = cli('test').command('db', {
|
|
105
|
+
// No handler - container command
|
|
106
|
+
builder: (cmd) =>
|
|
107
|
+
cmd.command('migrate', {
|
|
108
|
+
handler: () => 'done',
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const sdk = myCLI.sdk();
|
|
113
|
+
|
|
114
|
+
await expect(sdk.db()).rejects.toThrow("Command 'db' has no handler");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('defaults', () => {
|
|
119
|
+
it('should apply default values for object args', async () => {
|
|
120
|
+
let receivedArgs: any;
|
|
121
|
+
const myCLI = cli('test', {
|
|
122
|
+
builder: (cmd) =>
|
|
123
|
+
cmd
|
|
124
|
+
.option('host', { type: 'string', default: 'localhost' })
|
|
125
|
+
.option('port', { type: 'number', default: 3000 }),
|
|
126
|
+
handler: (args) => {
|
|
127
|
+
receivedArgs = args;
|
|
128
|
+
return 'ok';
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const sdk = myCLI.sdk();
|
|
133
|
+
await sdk({ port: 8080 });
|
|
134
|
+
|
|
135
|
+
expect(receivedArgs.host).toBe('localhost');
|
|
136
|
+
expect(receivedArgs.port).toBe(8080);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should allow overriding all defaults', async () => {
|
|
140
|
+
let receivedArgs: any;
|
|
141
|
+
const myCLI = cli('test', {
|
|
142
|
+
builder: (cmd) =>
|
|
143
|
+
cmd.option('verbose', { type: 'boolean', default: false }),
|
|
144
|
+
handler: (args) => {
|
|
145
|
+
receivedArgs = args;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const sdk = myCLI.sdk();
|
|
150
|
+
await sdk({ verbose: true });
|
|
151
|
+
|
|
152
|
+
expect(receivedArgs.verbose).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('middleware', () => {
|
|
157
|
+
it('should run middleware for object args', async () => {
|
|
158
|
+
let receivedArgs: any;
|
|
159
|
+
const myCLI = cli('test', {
|
|
160
|
+
builder: (cmd) => cmd.option('name', { type: 'string' }),
|
|
161
|
+
handler: (args) => {
|
|
162
|
+
receivedArgs = args;
|
|
163
|
+
},
|
|
164
|
+
}).middleware((args) => ({ ...args, timestamp: 12345 }));
|
|
165
|
+
|
|
166
|
+
const sdk = myCLI.sdk();
|
|
167
|
+
await sdk({ name: 'test' });
|
|
168
|
+
|
|
169
|
+
expect(receivedArgs.timestamp).toBe(12345);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should run parent middleware for subcommands', async () => {
|
|
173
|
+
let receivedArgs: any;
|
|
174
|
+
const myCLI = cli('test')
|
|
175
|
+
.middleware((args) => ({ ...args, fromRoot: true }))
|
|
176
|
+
.command('child', {
|
|
177
|
+
handler: (args) => {
|
|
178
|
+
receivedArgs = args;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const sdk = myCLI.sdk();
|
|
183
|
+
await sdk.child({});
|
|
184
|
+
|
|
185
|
+
expect(receivedArgs.fromRoot).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('validation', () => {
|
|
190
|
+
it('should validate string array args', async () => {
|
|
191
|
+
const myCLI = cli('test', {
|
|
192
|
+
builder: (cmd) =>
|
|
193
|
+
cmd.option('count', { type: 'number', required: true }),
|
|
194
|
+
handler: () => 'ok',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const sdk = myCLI.sdk();
|
|
198
|
+
|
|
199
|
+
// String array args go through full validation
|
|
200
|
+
await expect(sdk([])).rejects.toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should skip validation for object args (trust TypeScript)', async () => {
|
|
204
|
+
let receivedArgs: any;
|
|
205
|
+
const myCLI = cli('test', {
|
|
206
|
+
builder: (cmd) =>
|
|
207
|
+
cmd.option('count', { type: 'number', required: true }),
|
|
208
|
+
handler: (args) => {
|
|
209
|
+
receivedArgs = args;
|
|
210
|
+
return 'ok';
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const sdk = myCLI.sdk();
|
|
215
|
+
|
|
216
|
+
// Object args skip validation - TypeScript should catch this
|
|
217
|
+
// but at runtime we allow it through
|
|
218
|
+
const result = await sdk({});
|
|
219
|
+
expect(result).toBe('ok');
|
|
220
|
+
expect(receivedArgs.count).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('error handling', () => {
|
|
225
|
+
it('should throw handler errors directly', async () => {
|
|
226
|
+
const myCLI = cli('test', {
|
|
227
|
+
handler: () => {
|
|
228
|
+
throw new Error('Handler failed');
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const sdk = myCLI.sdk();
|
|
233
|
+
|
|
234
|
+
await expect(sdk()).rejects.toThrow('Handler failed');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw for non-existent subcommands', async () => {
|
|
238
|
+
const myCLI = cli('test').command('build', { handler: () => 'ok' });
|
|
239
|
+
|
|
240
|
+
const sdk = myCLI.sdk();
|
|
241
|
+
|
|
242
|
+
// Access non-existent command returns undefined
|
|
243
|
+
expect((sdk as any).nonexistent).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('async handlers', () => {
|
|
248
|
+
it('should handle async handlers', async () => {
|
|
249
|
+
const myCLI = cli('test', {
|
|
250
|
+
handler: async () => {
|
|
251
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
252
|
+
return { async: true };
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const sdk = myCLI.sdk();
|
|
257
|
+
const result = await sdk();
|
|
258
|
+
|
|
259
|
+
expect(result.async).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('type safety', () => {
|
|
264
|
+
it('should preserve handler return type', async () => {
|
|
265
|
+
interface BuildResult {
|
|
266
|
+
files: string[];
|
|
267
|
+
success: boolean;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const myCLI = cli('test', {
|
|
271
|
+
handler: (): BuildResult => ({
|
|
272
|
+
files: ['a.js', 'b.js'],
|
|
273
|
+
success: true,
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const sdk = myCLI.sdk();
|
|
278
|
+
const result = await sdk();
|
|
279
|
+
|
|
280
|
+
// Type should be BuildResult & { $args: ... }
|
|
281
|
+
expect(result.files).toEqual(['a.js', 'b.js']);
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
package/src/lib/utils.spec.ts
CHANGED