teen_process 3.0.6 → 4.0.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.
Files changed (53) hide show
  1. package/build/lib/exec.d.ts +32 -107
  2. package/build/lib/exec.d.ts.map +1 -1
  3. package/build/lib/exec.js +60 -103
  4. package/build/lib/exec.js.map +1 -1
  5. package/build/lib/helpers.d.ts +1 -12
  6. package/build/lib/helpers.d.ts.map +1 -1
  7. package/build/lib/helpers.js +17 -13
  8. package/build/lib/helpers.js.map +1 -1
  9. package/build/lib/index.d.ts +4 -6
  10. package/build/lib/index.d.ts.map +1 -1
  11. package/build/lib/index.js +7 -43
  12. package/build/lib/index.js.map +1 -1
  13. package/build/lib/subprocess.d.ts +106 -61
  14. package/build/lib/subprocess.d.ts.map +1 -1
  15. package/build/lib/subprocess.js +145 -143
  16. package/build/lib/subprocess.js.map +1 -1
  17. package/build/lib/types.d.ts +79 -0
  18. package/build/lib/types.d.ts.map +1 -0
  19. package/build/lib/types.js +3 -0
  20. package/build/lib/types.js.map +1 -0
  21. package/build/test/circular-buffer-specs.d.ts +2 -0
  22. package/build/test/circular-buffer-specs.d.ts.map +1 -0
  23. package/build/test/circular-buffer-specs.js +40 -0
  24. package/build/test/circular-buffer-specs.js.map +1 -0
  25. package/build/test/exec-specs.d.ts +2 -0
  26. package/build/test/exec-specs.d.ts.map +1 -0
  27. package/build/test/exec-specs.js +167 -0
  28. package/build/test/exec-specs.js.map +1 -0
  29. package/build/test/fixtures/bigbuffer.d.ts +3 -0
  30. package/build/test/fixtures/bigbuffer.d.ts.map +1 -0
  31. package/build/test/fixtures/bigbuffer.js +13 -0
  32. package/build/test/fixtures/bigbuffer.js.map +1 -0
  33. package/build/test/helpers.d.ts +3 -0
  34. package/build/test/helpers.d.ts.map +1 -0
  35. package/build/test/helpers.js +15 -0
  36. package/build/test/helpers.js.map +1 -0
  37. package/build/test/subproc-specs.d.ts +2 -0
  38. package/build/test/subproc-specs.d.ts.map +1 -0
  39. package/build/test/subproc-specs.js +414 -0
  40. package/build/test/subproc-specs.js.map +1 -0
  41. package/build/tsconfig.tsbuildinfo +1 -0
  42. package/lib/circular-buffer.ts +1 -1
  43. package/lib/exec.ts +158 -0
  44. package/lib/helpers.ts +44 -0
  45. package/lib/index.ts +8 -0
  46. package/lib/subprocess.ts +353 -0
  47. package/lib/types.ts +95 -0
  48. package/package.json +5 -5
  49. package/index.js +0 -1
  50. package/lib/exec.js +0 -191
  51. package/lib/helpers.js +0 -38
  52. package/lib/index.js +0 -9
  53. package/lib/subprocess.js +0 -329
package/lib/exec.ts ADDED
@@ -0,0 +1,158 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {quote} from 'shell-quote';
3
+ import B from 'bluebird';
4
+ import _ from 'lodash';
5
+ import {formatEnoent} from './helpers';
6
+ import {CircularBuffer, MAX_BUFFER_SIZE} from './circular-buffer';
7
+ import type {
8
+ TeenProcessExecOptions,
9
+ ExecResult,
10
+ BufferProp,
11
+ TeenProcessExecError,
12
+ StreamName
13
+ } from './types';
14
+
15
+
16
+ /**
17
+ * Spawns a child process and collects its output.
18
+ *
19
+ * This is a promisified version of Node's spawn that collects stdout and stderr,
20
+ * handles timeouts, and provides error context.
21
+ *
22
+ * @template T - The options type extending TeenProcessExecOptions
23
+ * @param cmd - The command to execute
24
+ * @param args - Array of arguments to pass to the command (default: [])
25
+ * @param originalOpts - Execution options including timeout, encoding, environment, etc.
26
+ * @returns Promise resolving to an object with stdout, stderr, and exit code
27
+ *
28
+ * @throws {TeenProcessExecError} When the process exits with non-zero code or times out
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Simple execution
33
+ * const {stdout, stderr, code} = await exec('ls', ['-la']);
34
+ *
35
+ * // With timeout and custom encoding
36
+ * const result = await exec('long-running-cmd', [], {
37
+ * timeout: 5000,
38
+ * encoding: 'utf8',
39
+ * cwd: '/custom/path'
40
+ * });
41
+ *
42
+ * // Return output as Buffer
43
+ * const {stdout} = await exec('cat', ['image.png'], {isBuffer: true});
44
+ * ```
45
+ */
46
+ export async function exec<T extends TeenProcessExecOptions = TeenProcessExecOptions>(
47
+ cmd: string,
48
+ args: string[] = [],
49
+ originalOpts: T = {} as T,
50
+ ): Promise<ExecResult<BufferProp<T>>> {
51
+ // get a quoted representation of the command for error strings
52
+ const rep = quote([cmd, ...args]);
53
+
54
+ const defaults: TeenProcessExecOptions = {
55
+ timeout: undefined,
56
+ encoding: 'utf8',
57
+ killSignal: 'SIGTERM',
58
+ cwd: undefined,
59
+ env: process.env,
60
+ ignoreOutput: false,
61
+ stdio: 'inherit',
62
+ isBuffer: false,
63
+ shell: undefined,
64
+ logger: undefined,
65
+ maxStdoutBufferSize: MAX_BUFFER_SIZE,
66
+ maxStderrBufferSize: MAX_BUFFER_SIZE,
67
+ };
68
+
69
+ const opts = _.defaults({}, originalOpts, defaults) as T;
70
+ const isBuffer = Boolean(opts.isBuffer);
71
+
72
+ return await new B<ExecResult<BufferProp<T>>>((resolve, reject) => {
73
+ const proc = spawn(cmd, args, {cwd: opts.cwd, env: opts.env, shell: opts.shell});
74
+ const stdoutBuffer = new CircularBuffer(opts.maxStdoutBufferSize);
75
+ const stderrBuffer = new CircularBuffer(opts.maxStderrBufferSize);
76
+ let timer: NodeJS.Timeout | null = null;
77
+
78
+ proc.on('error', async (err: NodeJS.ErrnoException) => {
79
+ let error = err;
80
+ if (error.code === 'ENOENT') {
81
+ error = await formatEnoent(error, cmd, opts.cwd?.toString());
82
+ }
83
+ reject(error);
84
+ });
85
+
86
+ if (proc.stdin) {
87
+ proc.stdin.on('error', (err: NodeJS.ErrnoException) => {
88
+ reject(new Error(`Standard input '${err.syscall}' error: ${err.stack}`));
89
+ });
90
+ }
91
+
92
+ const handleStream = (streamType: StreamName, buffer: CircularBuffer) => {
93
+ const stream = proc[streamType];
94
+ if (!stream) {
95
+ return;
96
+ }
97
+
98
+ stream.on('error', (err: NodeJS.ErrnoException) => {
99
+ reject(new Error(`${_.capitalize(streamType)} '${err.syscall}' error: ${err.stack}`));
100
+ });
101
+
102
+ if (opts.ignoreOutput) {
103
+ // https://github.com/nodejs/node/issues/4236
104
+ stream.on('data', () => {});
105
+ return;
106
+ }
107
+
108
+ stream.on('data', (chunk: Buffer) => {
109
+ buffer.add(chunk);
110
+ if (opts.logger?.debug && _.isFunction(opts.logger.debug)) {
111
+ opts.logger.debug(chunk.toString());
112
+ }
113
+ });
114
+ };
115
+
116
+ handleStream('stdout', stdoutBuffer);
117
+ handleStream('stderr', stderrBuffer);
118
+
119
+ function getStdio<U extends boolean>(
120
+ wantBuffer: U,
121
+ ): U extends true ? {stdout: Buffer; stderr: Buffer} : {stdout: string; stderr: string} {
122
+ const stdout = wantBuffer ? stdoutBuffer.value() : stdoutBuffer.value().toString(opts.encoding);
123
+ const stderr = wantBuffer ? stderrBuffer.value() : stderrBuffer.value().toString(opts.encoding);
124
+ return {stdout, stderr} as U extends true
125
+ ? {stdout: Buffer; stderr: Buffer}
126
+ : {stdout: string; stderr: string};
127
+ }
128
+
129
+ proc.on('close', (code: number | null) => {
130
+ if (timer) {
131
+ clearTimeout(timer);
132
+ }
133
+ const {stdout, stderr} = getStdio(isBuffer);
134
+ if (code === 0) {
135
+ resolve({stdout, stderr, code} as ExecResult<BufferProp<T>>);
136
+ } else {
137
+ const err = Object.assign(new Error(`Command '${rep}' exited with code ${code}`), {
138
+ stdout,
139
+ stderr,
140
+ code,
141
+ }) as TeenProcessExecError;
142
+ reject(err);
143
+ }
144
+ });
145
+
146
+ if (opts.timeout) {
147
+ timer = setTimeout(() => {
148
+ const {stdout, stderr} = getStdio(isBuffer);
149
+ const err = Object.assign(
150
+ new Error(`Command '${rep}' timed out after ${opts.timeout}ms`),
151
+ {stdout, stderr, code: null},
152
+ ) as TeenProcessExecError;
153
+ reject(err);
154
+ proc.kill(opts.killSignal ?? 'SIGTERM');
155
+ }, opts.timeout);
156
+ }
157
+ });
158
+ }
package/lib/helpers.ts ADDED
@@ -0,0 +1,44 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+
4
+ /**
5
+ * Enhances ENOENT errors from spawn with descriptive messages.
6
+ *
7
+ * This is an internal helper that mutates the error object to provide context about:
8
+ * - Invalid working directory paths
9
+ * - Missing executables in PATH
10
+ *
11
+ * @param error - The original ENOENT error from spawn
12
+ * @param cmd - The command that was attempted to execute
13
+ * @param cwd - The working directory used (if any)
14
+ * @returns The same error object with an enhanced message
15
+ *
16
+ * @internal
17
+ */
18
+ export async function formatEnoent(
19
+ error: NodeJS.ErrnoException,
20
+ cmd: string,
21
+ cwd?: string,
22
+ ): Promise<NodeJS.ErrnoException> {
23
+ if (cwd) {
24
+ try {
25
+ const stat = await fs.stat(cwd);
26
+ if (!stat.isDirectory()) {
27
+ error.message = `The working directory '${cwd}' of '${cmd}' is not a valid folder path`;
28
+ return error;
29
+ }
30
+ } catch (e) {
31
+ const err = e as NodeJS.ErrnoException;
32
+ if (err.code === 'ENOENT') {
33
+ error.message = `The working directory '${cwd}' of '${cmd}' does not exist`;
34
+ return error;
35
+ }
36
+ }
37
+ }
38
+
39
+ const curDir = path.resolve(cwd ?? process.cwd());
40
+ const pathMsg = process.env.PATH ?? 'which is not defined for the process';
41
+ error.message = `'${cmd}' executable is not found neither in the process working folder (${curDir}) ` +
42
+ `nor in any folders specified in the PATH environment variable (${pathMsg})`;
43
+ return error;
44
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {spawn} from 'node:child_process';
2
+ export {SubProcess} from './subprocess';
3
+ export {exec} from './exec';
4
+ export type {
5
+ TeenProcessExecOptions,
6
+ ExecResult,
7
+ SubProcessOptions
8
+ } from './types';
@@ -0,0 +1,353 @@
1
+ import {spawn} from 'node:child_process';
2
+ import type {ChildProcess} from 'node:child_process';
3
+ import {EventEmitter} from 'node:events';
4
+ import B from 'bluebird';
5
+ import {quote} from 'shell-quote';
6
+ import _ from 'lodash';
7
+ import {formatEnoent} from './helpers';
8
+ import {createInterface} from 'node:readline';
9
+ import type {Readable} from 'node:stream';
10
+ import type {
11
+ SubProcessOptions,
12
+ StartDetector,
13
+ TIsBufferOpts,
14
+ StreamName
15
+ } from './types';
16
+
17
+ /**
18
+ * A wrapper around Node's spawn that provides event-driven process management.
19
+ *
20
+ * Extends EventEmitter to provide real-time output streaming and lifecycle events.
21
+ *
22
+ * @template TSubProcessOptions - Options type extending SubProcessOptions
23
+ *
24
+ * @fires SubProcess#output - Emitted when stdout or stderr receives data
25
+ * @fires SubProcess#line-stdout - Emitted for each line of stdout
26
+ * @fires SubProcess#line-stderr - Emitted for each line of stderr
27
+ * @fires SubProcess#lines-stdout - Legacy event emitting stdout lines (deprecated)
28
+ * @fires SubProcess#lines-stderr - Legacy event emitting stderr lines (deprecated)
29
+ * @fires SubProcess#stream-line - Emitted for combined stdout/stderr lines
30
+ * @fires SubProcess#exit - Emitted when process exits
31
+ * @fires SubProcess#stop - Emitted when process is stopped intentionally
32
+ * @fires SubProcess#die - Emitted when process dies unexpectedly with non-zero code
33
+ * @fires SubProcess#end - Emitted when process ends normally with code 0
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const proc = new SubProcess('tail', ['-f', 'logfile.txt']);
38
+ *
39
+ * proc.on('output', (stdout, stderr) => {
40
+ * console.log('Output:', stdout);
41
+ * });
42
+ *
43
+ * proc.on('line-stdout', (line) => {
44
+ * console.log('Line:', line);
45
+ * });
46
+ *
47
+ * await proc.start();
48
+ * // ... later
49
+ * await proc.stop();
50
+ * ```
51
+ */
52
+ export class SubProcess<
53
+ TSubProcessOptions extends SubProcessOptions = SubProcessOptions,
54
+ > extends EventEmitter {
55
+ private proc: ChildProcess | null;
56
+ private args: string[];
57
+ private cmd: string;
58
+ private opts: TSubProcessOptions;
59
+ private expectingExit: boolean;
60
+ private readonly rep: string;
61
+
62
+ constructor(cmd: string, args: string[] = [], opts?: TSubProcessOptions) {
63
+ super();
64
+
65
+ if (!cmd) {
66
+ throw new Error('Command is required');
67
+ }
68
+
69
+ if (!_.isString(cmd)) {
70
+ throw new Error('Command must be a string');
71
+ }
72
+
73
+ if (!_.isArray(args)) {
74
+ throw new Error('Args must be an array');
75
+ }
76
+
77
+ this.cmd = cmd;
78
+ this.args = args;
79
+ this.proc = null;
80
+ this.opts = opts ?? ({} as TSubProcessOptions);
81
+ this.expectingExit = false;
82
+
83
+ this.rep = quote([cmd, ...args]);
84
+ }
85
+
86
+ get isRunning(): boolean {
87
+ return !!this.proc;
88
+ }
89
+
90
+ /**
91
+ * Starts the subprocess and waits for it to be ready.
92
+ *
93
+ * @param startDetector - Function to detect when process is ready, number for delay in ms,
94
+ * boolean true to detach immediately, or null for default behavior
95
+ * @param timeoutMs - Maximum time to wait for process to start (in ms), or boolean true to detach
96
+ * @param detach - Whether to detach the process (requires 'detached' option)
97
+ *
98
+ * @throws {Error} When process fails to start or times out
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Wait for any output
103
+ * await proc.start();
104
+ *
105
+ * // Wait 100ms then continue
106
+ * await proc.start(100);
107
+ *
108
+ * // Wait for specific output
109
+ * await proc.start((stdout) => stdout.includes('Server ready'));
110
+ *
111
+ * // With timeout
112
+ * await proc.start(null, 5000);
113
+ * ```
114
+ */
115
+ async start(
116
+ startDetector: StartDetector<TSubProcessOptions> | number | boolean | null = null,
117
+ timeoutMs: number | boolean | null = null,
118
+ detach: boolean = false,
119
+ ): Promise<void> {
120
+ let startDelay = 10;
121
+
122
+ const genericStartDetector: StartDetector<TSubProcessOptions> = (stdout, stderr) => stdout || stderr;
123
+ let detector: StartDetector<TSubProcessOptions> | null = null;
124
+
125
+ if (startDetector === null) {
126
+ detector = genericStartDetector;
127
+ }
128
+
129
+ if (_.isNumber(startDetector)) {
130
+ startDelay = startDetector;
131
+ detector = null;
132
+ } else if (_.isFunction(startDetector)) {
133
+ detector = startDetector;
134
+ }
135
+
136
+ if (_.isBoolean(startDetector) && startDetector) {
137
+ if (!this.opts.detached) {
138
+ throw new Error(`Unable to detach process that is not started with 'detached' option`);
139
+ }
140
+ detach = true;
141
+ detector = genericStartDetector;
142
+ } else if (_.isBoolean(timeoutMs) && timeoutMs) {
143
+ if (!this.opts.detached) {
144
+ throw new Error(`Unable to detach process that is not started with 'detached' option`);
145
+ }
146
+ detach = true;
147
+ timeoutMs = null;
148
+ }
149
+
150
+ return await new B<void>((resolve, reject) => {
151
+ this.proc = spawn(this.cmd, this.args, this.opts);
152
+
153
+ const handleOutput = (streams: {
154
+ stdout: TSubProcessOptions extends TIsBufferOpts ? Buffer : string;
155
+ stderr: TSubProcessOptions extends TIsBufferOpts ? Buffer : string;
156
+ }) => {
157
+ const {stdout, stderr} = streams;
158
+
159
+ try {
160
+ if (detector && detector(stdout, stderr)) {
161
+ detector = null;
162
+ resolve();
163
+ }
164
+ } catch (e) {
165
+ reject(e as Error);
166
+ }
167
+
168
+ this.emit('output', stdout, stderr);
169
+ };
170
+
171
+ this.proc.on('error', async (err: NodeJS.ErrnoException) => {
172
+ this.proc?.removeAllListeners('exit');
173
+ this.proc?.kill('SIGINT');
174
+
175
+ let error = err;
176
+ if (error.code === 'ENOENT') {
177
+ error = await formatEnoent(error, this.cmd, this.opts?.cwd?.toString());
178
+ }
179
+ reject(error);
180
+
181
+ this.proc?.unref();
182
+ this.proc = null;
183
+ });
184
+
185
+ const handleStreamLines = (streamName: StreamName, input: Readable) => {
186
+ const rl = createInterface({input});
187
+ rl.on('line', (line) => {
188
+ if (this.listenerCount(`lines-${streamName}`)) {
189
+ this.emit(`lines-${streamName}`, [line]);
190
+ }
191
+ this.emit(`line-${streamName}`, line);
192
+ if (this.listenerCount('stream-line')) {
193
+ this.emitLines(streamName, line);
194
+ }
195
+ });
196
+ };
197
+
198
+ const isBuffer = Boolean(this.opts.isBuffer);
199
+ const encoding = this.opts.encoding || 'utf8';
200
+
201
+ if (this.proc.stdout) {
202
+ this.proc.stdout.on('data', (chunk: Buffer) =>
203
+ handleOutput({
204
+ stdout: (isBuffer ? chunk : chunk.toString(encoding)) as TSubProcessOptions extends TIsBufferOpts
205
+ ? Buffer
206
+ : string,
207
+ stderr: (isBuffer ? Buffer.alloc(0) : '') as TSubProcessOptions extends TIsBufferOpts ? Buffer : string,
208
+ }),
209
+ );
210
+ handleStreamLines('stdout', this.proc.stdout);
211
+ }
212
+
213
+ if (this.proc.stderr) {
214
+ this.proc.stderr.on('data', (chunk: Buffer) =>
215
+ handleOutput({
216
+ stdout: (isBuffer ? Buffer.alloc(0) : '') as TSubProcessOptions extends TIsBufferOpts ? Buffer : string,
217
+ stderr: (isBuffer ? chunk : chunk.toString(encoding)) as TSubProcessOptions extends TIsBufferOpts
218
+ ? Buffer
219
+ : string,
220
+ }),
221
+ );
222
+ handleStreamLines('stderr', this.proc.stderr);
223
+ }
224
+
225
+ this.proc.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
226
+ this.emit('exit', code, signal);
227
+
228
+ let event: 'stop' | 'die' | 'end' = this.expectingExit ? 'stop' : 'die';
229
+ if (!this.expectingExit && code === 0) {
230
+ event = 'end';
231
+ }
232
+ this.emit(event, code, signal);
233
+
234
+ this.proc = null;
235
+ this.expectingExit = false;
236
+ });
237
+
238
+ if (!detector) {
239
+ setTimeout(() => resolve(), startDelay);
240
+ }
241
+
242
+ if (_.isNumber(timeoutMs)) {
243
+ setTimeout(() => {
244
+ reject(new Error(`The process did not start within ${timeoutMs}ms (cmd: '${this.rep}')`));
245
+ }, timeoutMs);
246
+ }
247
+ }).finally(() => {
248
+ if (detach && this.proc) {
249
+ this.proc.unref();
250
+ }
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Stops the running subprocess by sending a signal.
256
+ *
257
+ * @param signal - Signal to send to the process (default: 'SIGTERM')
258
+ * @param timeout - Maximum time to wait for process to exit in ms (default: 10000)
259
+ *
260
+ * @throws {Error} When process is not running or doesn't exit within timeout
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // Graceful stop with SIGTERM
265
+ * await proc.stop();
266
+ *
267
+ * // Force kill with SIGKILL
268
+ * await proc.stop('SIGKILL');
269
+ *
270
+ * // Custom timeout
271
+ * await proc.stop('SIGTERM', 5000);
272
+ * ```
273
+ */
274
+ async stop(signal: NodeJS.Signals = 'SIGTERM', timeout = 10000): Promise<void> {
275
+ if (!this.isRunning) {
276
+ throw new Error(`Can't stop process; it's not currently running (cmd: '${this.rep}')`);
277
+ }
278
+ return await new B<void>((resolve, reject) => {
279
+ this.proc?.on('close', () => resolve());
280
+ this.expectingExit = true;
281
+ this.proc?.kill(signal);
282
+ setTimeout(() => {
283
+ reject(new Error(`Process didn't end after ${timeout}ms (cmd: '${this.rep}')`));
284
+ }, timeout).unref();
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Waits for the process to exit and validates its exit code.
290
+ *
291
+ * @param allowedExitCodes - Array of acceptable exit codes (default: [0])
292
+ * @returns Promise resolving to the exit code
293
+ *
294
+ * @throws {Error} When process is not running or exits with disallowed code
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * // Wait for successful exit (code 0)
299
+ * const code = await proc.join();
300
+ *
301
+ * // Allow multiple exit codes
302
+ * const code = await proc.join([0, 1, 2]);
303
+ * ```
304
+ */
305
+ async join(allowedExitCodes: number[] = [0]): Promise<number | null> {
306
+ if (!this.isRunning) {
307
+ throw new Error(`Cannot join process; it is not currently running (cmd: '${this.rep}')`);
308
+ }
309
+
310
+ return await new B<number | null>((resolve, reject) => {
311
+ this.proc?.on('exit', (code: number | null) => {
312
+ if (code !== null && !allowedExitCodes.includes(code)) {
313
+ reject(new Error(`Process ended with exitcode ${code} (cmd: '${this.rep}')`));
314
+ } else {
315
+ resolve(code);
316
+ }
317
+ });
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Detaches the process so it continues running independently.
323
+ *
324
+ * The process must have been created with the 'detached' option.
325
+ * Once detached, the process will not be killed when the parent exits.
326
+ *
327
+ * @throws {Error} When process was not created with 'detached' option
328
+ */
329
+ detachProcess(): void {
330
+ if (!this.opts.detached) {
331
+ throw new Error(`Unable to detach process that is not started with 'detached' option`);
332
+ }
333
+ if (this.proc) {
334
+ this.proc.unref();
335
+ }
336
+ }
337
+
338
+ get pid(): number | null {
339
+ return this.proc?.pid ?? null;
340
+ }
341
+
342
+ private emitLines(streamName: StreamName, lines: Iterable<string> | string): void {
343
+ const doEmit = (line: string) => this.emit('stream-line', `[${streamName.toUpperCase()}] ${line}`);
344
+
345
+ if (_.isString(lines)) {
346
+ doEmit(lines);
347
+ } else {
348
+ for (const line of lines) {
349
+ doEmit(line);
350
+ }
351
+ }
352
+ }
353
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type {SpawnOptions, SpawnOptionsWithoutStdio} from 'node:child_process';
2
+
3
+ /**
4
+ * Minimal logger interface used by teen_process for debug output.
5
+ */
6
+ export type TeenProcessLogger = {
7
+ /** Called for each debug message emitted by exec/SubProcess. */
8
+ debug: (...args: any[]) => void;
9
+ };
10
+
11
+ /**
12
+ * Extra options supported by teen_process on top of Node's SpawnOptions.
13
+ */
14
+ export interface TeenProcessProps {
15
+ /** Ignore and discard all child stdout/stderr (reduces memory use). */
16
+ ignoreOutput?: boolean;
17
+ /** When true, exec/SubProcess emits Buffers; otherwise strings (default). */
18
+ isBuffer?: boolean;
19
+ /** Optional logger for streaming debug output. */
20
+ logger?: TeenProcessLogger;
21
+ /** Maximum bytes retained in the stdout circular buffer. */
22
+ maxStdoutBufferSize?: number;
23
+ /** Maximum bytes retained in the stderr circular buffer. */
24
+ maxStderrBufferSize?: number;
25
+ /** Encoding used when returning string output (default: 'utf8'). */
26
+ encoding?: BufferEncoding;
27
+ }
28
+
29
+ /** Options accepted by exec (SpawnOptions + teen_process props). */
30
+ export type TeenProcessExecOptions = SpawnOptions & TeenProcessProps;
31
+
32
+ /** exec result when isBuffer is false/undefined (string output). */
33
+ export type TeenProcessExecStringResult = {
34
+ stdout: string;
35
+ stderr: string;
36
+ code: number | null;
37
+ };
38
+
39
+ /** exec result when isBuffer is true (Buffer output). */
40
+ export type TeenProcessExecBufferResult = {
41
+ stdout: Buffer;
42
+ stderr: Buffer;
43
+ code: number | null;
44
+ };
45
+
46
+ /** Additional properties attached to exec errors. */
47
+ export type TeenProcessExecErrorProps = {
48
+ stdout: string;
49
+ stderr: string;
50
+ code: number | null;
51
+ };
52
+
53
+ /** Error thrown by exec on non-zero exit or timeout. */
54
+ export type TeenProcessExecError = Error & TeenProcessExecErrorProps;
55
+
56
+ /**
57
+ * Extracts the isBuffer property from options, normalizing undefined/false to false.
58
+ */
59
+ export type BufferProp<T extends {isBuffer?: boolean}> = T['isBuffer'] extends true ? true : false;
60
+
61
+ /**
62
+ * Resolves to the correct exec result shape based on buffer mode.
63
+ * Defaults to string output when isBuffer is false/undefined.
64
+ */
65
+ export type ExecResult<T extends boolean> = T extends true
66
+ ? TeenProcessExecBufferResult
67
+ : TeenProcessExecStringResult;
68
+
69
+ /** Supported stdio stream names. */
70
+ export type StreamName = 'stdout' | 'stderr';
71
+
72
+ /** Additional SubProcess-only options. */
73
+ export type SubProcessCustomOptions = {
74
+ /** When true, SubProcess emits Buffers instead of strings. */
75
+ isBuffer?: boolean;
76
+ /** Encoding used when isBuffer is false. */
77
+ encoding?: BufferEncoding;
78
+ };
79
+
80
+ /** Options accepted by SubProcess (extends spawn options). */
81
+ export type SubProcessOptions = SubProcessCustomOptions & SpawnOptionsWithoutStdio;
82
+
83
+ /** Helper type representing SubProcess buffer mode. */
84
+ export type TIsBufferOpts = {
85
+ isBuffer: true;
86
+ };
87
+
88
+ /**
89
+ * Function that detects when a subprocess has started.
90
+ * Receives stdout/stderr as Buffer when isBuffer is true, otherwise strings.
91
+ */
92
+ export type StartDetector<T extends SubProcessOptions> = (
93
+ stdout: T extends TIsBufferOpts ? Buffer : string,
94
+ stderr?: T extends TIsBufferOpts ? Buffer : string,
95
+ ) => unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teen_process",
3
- "version": "3.0.6",
3
+ "version": "4.0.1",
4
4
  "description": "A grown up version of Node's spawn/exec",
5
5
  "keywords": [
6
6
  "child_process",
@@ -20,15 +20,15 @@
20
20
  },
21
21
  "license": "Apache-2.0",
22
22
  "author": "Appium Contributors",
23
- "main": "index.js",
23
+ "main": "build/lib/index.js",
24
+ "types": "build/lib/index.d.ts",
24
25
  "bin": {},
25
26
  "directories": {
26
27
  "lib": "lib"
27
28
  },
28
29
  "files": [
29
- "index.js",
30
- "lib",
31
- "build/lib"
30
+ "build",
31
+ "lib"
32
32
  ],
33
33
  "scripts": {
34
34
  "build": "tsc -b",
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./build/lib');