ts-repo-utils 5.2.0 → 5.3.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,17 +1,26 @@
1
- import { type ExecException } from 'node:child_process';
1
+ import { type ExecException, type ExecOptions as ExecOptions_ } from 'node:child_process';
2
2
  import { Result } from 'ts-data-forge';
3
+ type ExecOptionsCustom = Readonly<{
4
+ silent?: boolean;
5
+ }>;
6
+ type ExecOptions = DeepReadonly<ExecOptions_ & ExecOptionsCustom>;
7
+ type ExecResult<T extends string | Buffer> = Result<Readonly<{
8
+ stdout: T;
9
+ stderr: T;
10
+ }>, ExecException>;
3
11
  /**
4
12
  * Executes a shell command asynchronously.
5
13
  *
6
- * @param cmd - The command to execute.
14
+ * @param command - The command to execute.
7
15
  * @param options - Optional configuration for command execution.
8
16
  * @returns A promise that resolves with the command result.
9
17
  */
10
- export declare const $: (cmd: string, options?: Readonly<{
11
- silent?: boolean;
12
- timeout?: number;
13
- }>) => Promise<Result<Readonly<{
14
- stdout: string;
15
- stderr: string;
16
- }>, ExecException>>;
18
+ export declare function $(command: string, options?: ExecOptionsCustom): Promise<ExecResult<string>>;
19
+ export declare function $(command: string, options: Readonly<{
20
+ encoding: 'buffer' | null;
21
+ } & ExecOptions>): Promise<ExecResult<Buffer>>;
22
+ export declare function $(command: string, options: Readonly<{
23
+ encoding: BufferEncoding;
24
+ } & ExecOptions>): Promise<ExecResult<string>>;
25
+ export {};
17
26
  //# sourceMappingURL=exec-async.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec-async.d.mts","sourceRoot":"","sources":["../../src/functions/exec-async.mts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,CAAC,GACZ,KAAK,MAAM,EACX,UAAS,QAAQ,CAAC;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,KAC7D,OAAO,CACR,MAAM,CAAC,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,aAAa,CAAC,CA6BpE,CAAC"}
1
+ {"version":3,"file":"exec-async.d.mts","sourceRoot":"","sources":["../../src/functions/exec-async.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,WAAW,IAAI,YAAY,EACjC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,KAAK,iBAAiB,GAAG,QAAQ,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH,KAAK,WAAW,GAAG,YAAY,CAAC,YAAY,GAAG,iBAAiB,CAAC,CAAC;AAElE,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,MAAM,CACjD,QAAQ,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,EAClC,aAAa,CACd,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,CAAC,CACf,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAE/B,wBAAgB,CAAC,CACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,QAAQ,CAAC;IAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;CAAE,GAAG,WAAW,CAAC,GAC7D,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAE/B,wBAAgB,CAAC,CACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,QAAQ,CAAC;IAAE,QAAQ,EAAE,cAAc,CAAA;CAAE,GAAG,WAAW,CAAC,GAC5D,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC"}
@@ -1,22 +1,16 @@
1
1
  import { exec } from 'node:child_process';
2
2
  import { Result } from 'ts-data-forge';
3
3
 
4
- /**
5
- * Executes a shell command asynchronously.
6
- *
7
- * @param cmd - The command to execute.
8
- * @param options - Optional configuration for command execution.
9
- * @returns A promise that resolves with the command result.
10
- */
11
- const $ = (cmd, options = {}) => {
12
- const { silent = false, timeout = 30000 } = options;
4
+ function $(command, options) {
5
+ const { silent = false, ...restOptions } = options ?? {};
13
6
  if (!silent) {
14
- echo(`$ ${cmd}`);
7
+ echo(`$ ${command}`);
15
8
  }
16
9
  return new Promise((resolve) => {
17
- const execOptions = { timeout };
18
10
  // eslint-disable-next-line security/detect-child-process
19
- exec(cmd, execOptions, (error, stdout, stderr) => {
11
+ exec(command,
12
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
13
+ restOptions, (error, stdout, stderr) => {
20
14
  if (!silent) {
21
15
  if (stdout !== '') {
22
16
  echo(stdout);
@@ -33,7 +27,7 @@ const $ = (cmd, options = {}) => {
33
27
  }
34
28
  });
35
29
  });
36
- };
30
+ }
37
31
 
38
32
  export { $ };
39
33
  //# sourceMappingURL=exec-async.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec-async.mjs","sources":["../../src/functions/exec-async.mts"],"sourcesContent":[null],"names":[],"mappings":";;;AAGA;;;;;;AAMG;AACI,MAAM,CAAC,GAAG,CACf,GAAW,EACX,OAAA,GAA4D,EAAE,KAG5D;IACF,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,OAAO;IAEnD,IAAI,CAAC,MAAM,EAAE;AACX,QAAA,IAAI,CAAC,CAAA,EAAA,EAAK,GAAG,CAAA,CAAE,CAAC;IAClB;AAEA,IAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAI;AAC7B,QAAA,MAAM,WAAW,GAAG,EAAE,OAAO,EAAE;;AAG/B,QAAA,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,KAAI;YAC/C,IAAI,CAAC,MAAM,EAAE;AACX,gBAAA,IAAI,MAAM,KAAK,EAAE,EAAE;oBACjB,IAAI,CAAC,MAAM,CAAC;gBACd;AACA,gBAAA,IAAI,MAAM,KAAK,EAAE,EAAE;AACjB,oBAAA,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;gBACvB;YACF;AAEA,YAAA,IAAI,KAAK,KAAK,IAAI,EAAE;gBAClB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC5B;iBAAO;AACL,gBAAA,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACxC;AACF,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,CAAC;AACJ;;;;"}
1
+ {"version":3,"file":"exec-async.mjs","sources":["../../src/functions/exec-async.mts"],"sourcesContent":[null],"names":[],"mappings":";;;AAwCM,SAAU,CAAC,CACf,OAAe,EACf,OAEC,EAAA;AAED,IAAA,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,IAAI,EAAE;IAExD,IAAI,CAAC,MAAM,EAAE;AACX,QAAA,IAAI,CAAC,CAAA,EAAA,EAAK,OAAO,CAAA,CAAE,CAAC;IACtB;AAEA,IAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAI;;AAE7B,QAAA,IAAI,CACF,OAAO;;QAEP,WAEgB,EAChB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,KAAI;YACxB,IAAI,CAAC,MAAM,EAAE;AACX,gBAAA,IAAI,MAAM,KAAK,EAAE,EAAE;oBACjB,IAAI,CAAC,MAAM,CAAC;gBACd;AACA,gBAAA,IAAI,MAAM,KAAK,EAAE,EAAE;AACjB,oBAAA,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;gBACvB;YACF;AAEA,YAAA,IAAI,KAAK,KAAK,IAAI,EAAE;gBAClB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC5B;iBAAO;AACL,gBAAA,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACxC;AACF,QAAA,CAAC,CACF;AACH,IAAA,CAAC,CAAC;AACJ;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-repo-utils",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -57,7 +57,7 @@
57
57
  "fast-glob": "^3.3.3",
58
58
  "micromatch": "^4.0.8",
59
59
  "prettier": "^3.6.2",
60
- "ts-data-forge": "^3.0.4"
60
+ "ts-data-forge": "^3.0.5"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^9.31.0",
@@ -91,13 +91,13 @@
91
91
  "prettier-plugin-jsdoc": "^1.3.3",
92
92
  "prettier-plugin-organize-imports": "^4.2.0",
93
93
  "prettier-plugin-packagejson": "^2.5.19",
94
- "rollup": "^4.45.1",
94
+ "rollup": "^4.46.2",
95
95
  "semantic-release": "^24.2.7",
96
96
  "ts-type-forge": "^2.1.1",
97
97
  "tslib": "^2.8.1",
98
98
  "tsx": "^4.20.3",
99
99
  "typedoc": "^0.28.7",
100
- "typedoc-plugin-markdown": "^4.7.1",
100
+ "typedoc-plugin-markdown": "^4.8.0",
101
101
  "typescript": "^5.8.3",
102
102
  "typescript-eslint": "^8.38.0",
103
103
  "vitest": "^3.2.4"
@@ -1,44 +1,79 @@
1
- import { exec, type ExecException } from 'node:child_process';
1
+ import {
2
+ exec,
3
+ type ExecException,
4
+ type ExecOptions as ExecOptions_,
5
+ } from 'node:child_process';
2
6
  import { Result } from 'ts-data-forge';
3
7
 
8
+ type ExecOptionsCustom = Readonly<{
9
+ silent?: boolean;
10
+ }>;
11
+
12
+ type ExecOptions = DeepReadonly<ExecOptions_ & ExecOptionsCustom>;
13
+
14
+ type ExecResult<T extends string | Buffer> = Result<
15
+ Readonly<{ stdout: T; stderr: T }>,
16
+ ExecException
17
+ >;
18
+
4
19
  /**
5
20
  * Executes a shell command asynchronously.
6
21
  *
7
- * @param cmd - The command to execute.
22
+ * @param command - The command to execute.
8
23
  * @param options - Optional configuration for command execution.
9
24
  * @returns A promise that resolves with the command result.
10
25
  */
11
- export const $ = (
12
- cmd: string,
13
- options: Readonly<{ silent?: boolean; timeout?: number }> = {},
14
- ): Promise<
15
- Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
16
- > => {
17
- const { silent = false, timeout = 30000 } = options;
26
+ export function $(
27
+ command: string,
28
+ options?: ExecOptionsCustom,
29
+ ): Promise<ExecResult<string>>;
30
+
31
+ export function $(
32
+ command: string,
33
+ options: Readonly<{ encoding: 'buffer' | null } & ExecOptions>,
34
+ ): Promise<ExecResult<Buffer>>;
35
+
36
+ export function $(
37
+ command: string,
38
+ options: Readonly<{ encoding: BufferEncoding } & ExecOptions>,
39
+ ): Promise<ExecResult<string>>;
40
+
41
+ export function $(
42
+ command: string,
43
+ options?: Readonly<
44
+ { encoding?: BufferEncoding | 'buffer' | null } & ExecOptions
45
+ >,
46
+ ): Promise<ExecResult<string | Buffer>> {
47
+ const { silent = false, ...restOptions } = options ?? {};
18
48
 
19
49
  if (!silent) {
20
- echo(`$ ${cmd}`);
50
+ echo(`$ ${command}`);
21
51
  }
22
52
 
23
53
  return new Promise((resolve) => {
24
- const execOptions = { timeout };
25
-
26
54
  // eslint-disable-next-line security/detect-child-process
27
- exec(cmd, execOptions, (error, stdout, stderr) => {
28
- if (!silent) {
29
- if (stdout !== '') {
30
- echo(stdout);
55
+ exec(
56
+ command,
57
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
58
+ restOptions as {
59
+ encoding?: 'buffer' | null | BufferEncoding;
60
+ } & ExecOptions_,
61
+ (error, stdout, stderr) => {
62
+ if (!silent) {
63
+ if (stdout !== '') {
64
+ echo(stdout);
65
+ }
66
+ if (stderr !== '') {
67
+ console.error(stderr);
68
+ }
31
69
  }
32
- if (stderr !== '') {
33
- console.error(stderr);
70
+
71
+ if (error !== null) {
72
+ resolve(Result.err(error));
73
+ } else {
74
+ resolve(Result.ok({ stdout, stderr }));
34
75
  }
35
- }
36
-
37
- if (error !== null) {
38
- resolve(Result.err(error));
39
- } else {
40
- resolve(Result.ok({ stdout, stderr }));
41
- }
42
- });
76
+ },
77
+ );
43
78
  });
44
- };
79
+ }
@@ -0,0 +1,501 @@
1
+ import { exec, type ExecException } from 'node:child_process';
2
+ import { expectType, Result } from 'ts-data-forge';
3
+ import '../node-global.mjs';
4
+ import { $ } from './exec-async.mjs';
5
+
6
+ describe('exec-async', () => {
7
+ // Helper to suppress echo output during tests
8
+ const withSilentEcho = async <T,>(fn: () => Promise<T>): Promise<T> => {
9
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
10
+ const originalEcho = (globalThis as any).echo;
11
+ // eslint-disable-next-line functional/immutable-data, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
12
+ (globalThis as any).echo = () => {
13
+ // Silent implementation - no output
14
+ };
15
+ try {
16
+ return await fn();
17
+ } finally {
18
+ // eslint-disable-next-line functional/immutable-data, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
19
+ (globalThis as any).echo = originalEcho;
20
+ }
21
+ };
22
+ describe('basic command execution', () => {
23
+ test('should execute simple command successfully', async () => {
24
+ const result = await $('echo "hello world"', { silent: true });
25
+
26
+ expect(Result.isOk(result)).toBe(true);
27
+ if (Result.isOk(result)) {
28
+ expect(result.value.stdout.trim()).toBe('hello world');
29
+ expect(result.value.stderr).toBe('');
30
+ }
31
+ });
32
+
33
+ test('should execute command with multiple lines of output', async () => {
34
+ const result = await $('echo "line1\nline2\nline3"', { silent: true });
35
+
36
+ expect(Result.isOk(result)).toBe(true);
37
+ if (Result.isOk(result)) {
38
+ expect(result.value.stdout.trim()).toBe('line1\nline2\nline3');
39
+ expect(result.value.stderr).toBe('');
40
+ }
41
+ });
42
+
43
+ test('should handle empty output', async () => {
44
+ const result = await $('true', { silent: true });
45
+
46
+ expect(Result.isOk(result)).toBe(true);
47
+ if (Result.isOk(result)) {
48
+ expect(result.value.stdout).toBe('');
49
+ expect(result.value.stderr).toBe('');
50
+ }
51
+ });
52
+ });
53
+
54
+ describe('error handling', () => {
55
+ test('should handle command not found error', async () => {
56
+ const result = await $('nonexistent_command_xyz', { silent: true });
57
+
58
+ expect(Result.isErr(result)).toBe(true);
59
+ if (Result.isErr(result)) {
60
+ expect(result.value).toBeDefined();
61
+ expect(result.value.code).toBeDefined();
62
+ }
63
+ });
64
+
65
+ test('should handle exit code error', async () => {
66
+ const result = await $('exit 1', { silent: true });
67
+
68
+ expect(Result.isErr(result)).toBe(true);
69
+ if (Result.isErr(result)) {
70
+ expect(result.value).toBeDefined();
71
+ expect(result.value.code).toBe(1);
72
+ }
73
+ });
74
+
75
+ test('should capture stderr on error', async () => {
76
+ const result = await $('>&2 echo "error message" && exit 1', {
77
+ silent: true,
78
+ });
79
+
80
+ expect(Result.isErr(result)).toBe(true);
81
+ if (Result.isErr(result)) {
82
+ expect(result.value).toBeDefined();
83
+ }
84
+ });
85
+ });
86
+
87
+ describe('silent option', () => {
88
+ test('should not output when silent is true', async () => {
89
+ // Silent mode should not produce any output
90
+ await $('echo "test"', { silent: true });
91
+ // If no error is thrown, the test passes
92
+ });
93
+
94
+ test('should output when silent is false', async () => {
95
+ // Non-silent mode should produce output (suppressed for clean test output)
96
+ await withSilentEcho(async () => {
97
+ await $('echo "test"', { silent: false });
98
+ });
99
+ // If no error is thrown, the test passes
100
+ });
101
+
102
+ test('should default to not silent', async () => {
103
+ // Default behavior should produce output (suppressed for clean test output)
104
+ await withSilentEcho(async () => {
105
+ await $('echo "test"');
106
+ });
107
+ // If no error is thrown, the test passes
108
+ });
109
+ });
110
+
111
+ describe('encoding options', () => {
112
+ test('should return string with default encoding', async () => {
113
+ const result = await $('echo "test"', { silent: true });
114
+
115
+ expect(Result.isOk(result)).toBe(true);
116
+ if (Result.isOk(result)) {
117
+ expect(typeof result.value.stdout).toBe('string');
118
+ }
119
+ });
120
+
121
+ test('should return Buffer with buffer encoding', async () => {
122
+ const result = await $('echo "test"', {
123
+ silent: true,
124
+ encoding: 'buffer',
125
+ });
126
+
127
+ expect(Result.isOk(result)).toBe(true);
128
+ if (Result.isOk(result)) {
129
+ expect(Buffer.isBuffer(result.value.stdout)).toBe(true);
130
+ expect(Buffer.isBuffer(result.value.stderr)).toBe(true);
131
+ }
132
+ });
133
+
134
+ test('should return Buffer with null encoding', async () => {
135
+ const result = await $('echo "test"', { silent: true, encoding: null });
136
+
137
+ expect(Result.isOk(result)).toBe(true);
138
+ if (Result.isOk(result)) {
139
+ expect(Buffer.isBuffer(result.value.stdout)).toBe(true);
140
+ expect(Buffer.isBuffer(result.value.stderr)).toBe(true);
141
+ }
142
+ });
143
+
144
+ test('should handle utf8 encoding', async () => {
145
+ const result = await $('echo "test 日本語"', {
146
+ silent: true,
147
+ encoding: 'utf8',
148
+ });
149
+
150
+ expect(Result.isOk(result)).toBe(true);
151
+ if (Result.isOk(result)) {
152
+ expect(typeof result.value.stdout).toBe('string');
153
+ expect(result.value.stdout.trim()).toBe('test 日本語');
154
+ }
155
+ });
156
+ });
157
+
158
+ describe('complex commands', () => {
159
+ test('should handle pipes', async () => {
160
+ const result = await $('echo "hello world" | grep "world"', {
161
+ silent: true,
162
+ });
163
+
164
+ expect(Result.isOk(result)).toBe(true);
165
+ if (Result.isOk(result)) {
166
+ expect(result.value.stdout.trim()).toBe('hello world');
167
+ }
168
+ });
169
+
170
+ test('should handle command chaining with &&', async () => {
171
+ const result = await $('echo "first" && echo "second"', { silent: true });
172
+
173
+ expect(Result.isOk(result)).toBe(true);
174
+ if (Result.isOk(result)) {
175
+ expect(result.value.stdout.trim()).toBe('first\nsecond');
176
+ }
177
+ });
178
+
179
+ test('should handle command chaining with ;', async () => {
180
+ const result = await $('echo "first"; echo "second"', { silent: true });
181
+
182
+ expect(Result.isOk(result)).toBe(true);
183
+ if (Result.isOk(result)) {
184
+ expect(result.value.stdout.trim()).toBe('first\nsecond');
185
+ }
186
+ });
187
+
188
+ test('should stop on first error with &&', async () => {
189
+ const result = await $('false && echo "should not print"', {
190
+ silent: true,
191
+ });
192
+
193
+ expect(Result.isErr(result)).toBe(true);
194
+ });
195
+ });
196
+
197
+ describe('type inference (compile-time checks)', () => {
198
+ test('should infer string result type with no encoding option', async () => {
199
+ const result = await $('echo "test"', { silent: true });
200
+
201
+ // Type assertion: result should be string type
202
+ assertType<
203
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
204
+ >(result);
205
+
206
+ if (Result.isOk(result)) {
207
+ // These assignments will fail at compile time if types don't match
208
+ assertType<string>(result.value.stdout);
209
+ assertType<string>(result.value.stderr);
210
+ }
211
+ });
212
+
213
+ test('should infer string result type with default options', async () => {
214
+ const _result = await withSilentEcho(async () => $('echo "test"'));
215
+
216
+ expectType<
217
+ typeof _result,
218
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
219
+ >('=');
220
+ });
221
+
222
+ test('should infer Buffer result type with buffer encoding', async () => {
223
+ const result = await $('echo "test"', {
224
+ encoding: 'buffer',
225
+ silent: true,
226
+ });
227
+
228
+ expectType<
229
+ typeof result,
230
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
231
+ >('=');
232
+
233
+ if (Result.isOk(result)) {
234
+ assertType<Buffer>(result.value.stdout);
235
+ assertType<Buffer>(result.value.stderr);
236
+ }
237
+ });
238
+
239
+ test('should infer Buffer result type with null encoding', async () => {
240
+ const result = await $('echo "test"', { encoding: null, silent: true });
241
+
242
+ expectType<
243
+ typeof result,
244
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
245
+ >('=');
246
+
247
+ if (Result.isOk(result)) {
248
+ assertType<Buffer>(result.value.stdout);
249
+ assertType<Buffer>(result.value.stderr);
250
+ }
251
+ });
252
+
253
+ test('should infer string result type with specific BufferEncoding', async () => {
254
+ const _resultUtf8 = await $('echo "test"', {
255
+ encoding: 'utf8',
256
+ silent: true,
257
+ });
258
+ const _resultAscii = await $('echo "test"', {
259
+ encoding: 'ascii',
260
+ silent: true,
261
+ });
262
+ const _resultBase64 = await $('echo "test"', {
263
+ encoding: 'base64',
264
+ silent: true,
265
+ });
266
+
267
+ expectType<
268
+ typeof _resultUtf8,
269
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
270
+ >('=');
271
+
272
+ expectType<
273
+ typeof _resultAscii,
274
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
275
+ >('=');
276
+
277
+ expectType<
278
+ typeof _resultBase64,
279
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
280
+ >('=');
281
+ });
282
+
283
+ test('should maintain type safety with const encoding values', async () => {
284
+ const bufferEncoding = 'buffer';
285
+ const nullEncoding = null;
286
+ const utf8Encoding = 'utf8';
287
+
288
+ const _resultBuffer = await $('echo "test"', {
289
+ encoding: bufferEncoding,
290
+ silent: true,
291
+ });
292
+ const _resultNull = await $('echo "test"', {
293
+ encoding: nullEncoding,
294
+ silent: true,
295
+ });
296
+ const _resultUtf8 = await $('echo "test"', {
297
+ encoding: utf8Encoding,
298
+ silent: true,
299
+ });
300
+
301
+ expectType<
302
+ typeof _resultBuffer,
303
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
304
+ >('=');
305
+
306
+ expectType<
307
+ typeof _resultNull,
308
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
309
+ >('=');
310
+
311
+ expectType<
312
+ typeof _resultUtf8,
313
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
314
+ >('=');
315
+ });
316
+
317
+ test('should handle union types when encoding is not const', async () => {
318
+ // Test that when we don't know the encoding at compile time,
319
+ // we need to check the type at runtime
320
+ const randomEncoding = Math.random() > 0.5 ? 'utf8' : 'buffer';
321
+
322
+ if (randomEncoding === 'buffer') {
323
+ const _result = await $('echo "test"', {
324
+ encoding: randomEncoding,
325
+ silent: true,
326
+ });
327
+
328
+ expectType<
329
+ typeof _result,
330
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
331
+ >('=');
332
+ } else {
333
+ const _result = await $('echo "test"', {
334
+ encoding: randomEncoding,
335
+ silent: true,
336
+ });
337
+
338
+ expectType<
339
+ typeof _result,
340
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
341
+ >('=');
342
+ }
343
+ });
344
+ });
345
+
346
+ describe('type correspondence with native exec', () => {
347
+ test('should match exec callback types for default encoding', () => {
348
+ // Type check for native exec with default options
349
+ exec('echo "test"', (error, stdout, stderr) => {
350
+ expectType<ExecException | null, typeof error>('=');
351
+ expectType<string, typeof stdout>('=');
352
+ expectType<string, typeof stderr>('=');
353
+ });
354
+
355
+ // The $ function should produce the same types wrapped in Result (suppressed for clean output)
356
+ const _resultPromise = withSilentEcho(async () => $('echo "test"'));
357
+ expectType<
358
+ typeof _resultPromise,
359
+ Promise<
360
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
361
+ >
362
+ >('=');
363
+ });
364
+
365
+ test('should match exec callback types for buffer encoding', () => {
366
+ // Type check for native exec with buffer encoding
367
+ exec('echo "test"', { encoding: 'buffer' }, (error, stdout, stderr) => {
368
+ expectType<ExecException | null, typeof error>('=');
369
+ expectType<Buffer, typeof stdout>('=');
370
+ expectType<Buffer, typeof stderr>('=');
371
+ });
372
+
373
+ // The $ function should produce the same types wrapped in Result (suppressed for clean output)
374
+ const _resultPromise = withSilentEcho(async () =>
375
+ $('echo "test"', { encoding: 'buffer' }),
376
+ );
377
+ expectType<
378
+ typeof _resultPromise,
379
+ Promise<
380
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
381
+ >
382
+ >('=');
383
+ });
384
+
385
+ test('should match exec callback types for null encoding', () => {
386
+ // Type check for native exec with null encoding
387
+ exec('echo "test"', { encoding: null }, (error, stdout, stderr) => {
388
+ expectType<ExecException | null, typeof error>('=');
389
+ expectType<Buffer, typeof stdout>('=');
390
+ expectType<Buffer, typeof stderr>('=');
391
+ });
392
+
393
+ // The $ function should produce the same types wrapped in Result (suppressed for clean output)
394
+ const _resultPromise = withSilentEcho(async () =>
395
+ $('echo "test"', { encoding: null }),
396
+ );
397
+ expectType<
398
+ typeof _resultPromise,
399
+ Promise<
400
+ Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
401
+ >
402
+ >('=');
403
+ });
404
+
405
+ test('should match exec callback types for specific BufferEncoding', () => {
406
+ // Type check for native exec with utf8 encoding
407
+ exec('echo "test"', { encoding: 'utf8' }, (error, stdout, stderr) => {
408
+ expectType<ExecException | null, typeof error>('=');
409
+ expectType<string, typeof stdout>('=');
410
+ expectType<string, typeof stderr>('=');
411
+ });
412
+
413
+ // The $ function should produce the same types wrapped in Result (suppressed for clean output)
414
+ const _resultPromise = withSilentEcho(async () =>
415
+ $('echo "test"', { encoding: 'utf8' }),
416
+ );
417
+ expectType<
418
+ typeof _resultPromise,
419
+ Promise<
420
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
421
+ >
422
+ >('=');
423
+ });
424
+
425
+ test('should match exec callback types with custom options', () => {
426
+ // Type check for native exec with custom options
427
+ exec(
428
+ 'echo "test"',
429
+ { encoding: 'utf8', timeout: 5000 },
430
+ (error, stdout, stderr) => {
431
+ expectType<ExecException | null, typeof error>('=');
432
+ expectType<string, typeof stdout>('=');
433
+ expectType<string, typeof stderr>('=');
434
+ },
435
+ );
436
+
437
+ // The $ function with silent option should produce the same stdout/stderr types
438
+ const _resultPromise = $('echo "test"', {
439
+ encoding: 'utf8',
440
+ silent: true,
441
+ });
442
+ expectType<
443
+ typeof _resultPromise,
444
+ Promise<
445
+ Result<Readonly<{ stdout: string; stderr: string }>, ExecException>
446
+ >
447
+ >('=');
448
+ });
449
+
450
+ test('should demonstrate type equivalence with runtime comparison', async () => {
451
+ // Create a type that represents what exec callback receives
452
+ type ExecCallbackParams<T extends string | Buffer> = {
453
+ error: ExecException | null;
454
+ stdout: T;
455
+ stderr: T;
456
+ };
457
+
458
+ // Helper to capture exec callback types
459
+ const captureExecTypes = <T extends string | Buffer>(
460
+ _encoding?: BufferEncoding | 'buffer' | null,
461
+ ): ExecCallbackParams<T> => {
462
+ const emptyParams: ExecCallbackParams<T> = {
463
+ error: null,
464
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
465
+ stdout: undefined as unknown as T,
466
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
467
+ stderr: undefined as unknown as T,
468
+ };
469
+ return emptyParams;
470
+ };
471
+
472
+ // Default encoding comparison
473
+ const _execDefault = captureExecTypes<string>();
474
+ const $Default = await $('echo "test"', { silent: true });
475
+ if (Result.isOk($Default)) {
476
+ expectType<typeof _execDefault.stdout, typeof $Default.value.stdout>(
477
+ '=',
478
+ );
479
+ expectType<typeof _execDefault.stderr, typeof $Default.value.stderr>(
480
+ '=',
481
+ );
482
+ }
483
+
484
+ // Buffer encoding comparison
485
+ const _execBuffer = captureExecTypes<Buffer>('buffer');
486
+ const $Buffer = await $('echo "test"', {
487
+ encoding: 'buffer',
488
+ silent: true,
489
+ });
490
+ if (Result.isOk($Buffer)) {
491
+ expectType<typeof _execBuffer.stdout, typeof $Buffer.value.stdout>('=');
492
+ expectType<typeof _execBuffer.stderr, typeof $Buffer.value.stderr>('=');
493
+ }
494
+
495
+ // Error type comparison
496
+ if (Result.isErr($Default)) {
497
+ expectType<ExecException, typeof $Default.value>('=');
498
+ }
499
+ });
500
+ });
501
+ });