spawn-rx 5.1.2 → 6.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/src/index.ts CHANGED
@@ -1,18 +1,58 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import * as path from "path";
3
- import * as net from "net";
4
- import * as sfs from "fs";
1
+ import { type SpawnOptions, spawn as spawnOg } from "node:child_process";
2
+ import * as sfs from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
5
 
6
- import type { Observer, Subject } from "rxjs";
7
- import { Observable, Subscription, AsyncSubject, of, merge } from "rxjs";
8
- import { map, reduce } from "rxjs/operators";
9
- import { spawn as spawnOg, SpawnOptions } from "child_process";
10
6
  import Debug from "debug";
7
+ import { LRUCache } from "lru-cache";
8
+ import type { Observer, Subject } from "rxjs";
9
+ import { AsyncSubject, merge, Observable, of, Subscription, timer } from "rxjs";
10
+ import { map, reduce, retry as rxRetry } from "rxjs/operators";
11
11
 
12
12
  const isWindows = process.platform === "win32";
13
13
 
14
14
  const d = Debug("spawn-rx"); // tslint:disable-line:no-var-requires
15
15
 
16
+ /**
17
+ * Custom error class for spawn operations with additional metadata
18
+ */
19
+ export class SpawnError extends Error {
20
+ public readonly exitCode: number;
21
+ public readonly code: number;
22
+ public readonly stdout?: string;
23
+ public readonly stderr?: string;
24
+ public readonly command: string;
25
+ public readonly args: string[];
26
+
27
+ constructor(message: string, exitCode: number, command: string, args: string[], stdout?: string, stderr?: string) {
28
+ super(message);
29
+ this.name = "SpawnError";
30
+ this.exitCode = exitCode;
31
+ this.code = exitCode;
32
+ this.stdout = stdout;
33
+ this.stderr = stderr;
34
+ this.command = command;
35
+ this.args = args;
36
+
37
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ if ((Error as any).captureStackTrace) {
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ (Error as any).captureStackTrace(this, SpawnError);
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Process metadata tracked during execution
48
+ */
49
+ export interface ProcessMetadata {
50
+ pid: number;
51
+ startTime: number;
52
+ command: string;
53
+ args: string[];
54
+ }
55
+
16
56
  /**
17
57
  * stat a file but don't throw if it doesn't exist
18
58
  *
@@ -21,7 +61,7 @@ const d = Debug("spawn-rx"); // tslint:disable-line:no-var-requires
21
61
  *
22
62
  * @private
23
63
  */
24
- function statSyncNoException(file: string): sfs.Stats | null {
64
+ export function statSyncNoException(file: string): sfs.Stats | null {
25
65
  try {
26
66
  return sfs.statSync(file);
27
67
  } catch {
@@ -29,6 +69,23 @@ function statSyncNoException(file: string): sfs.Stats | null {
29
69
  }
30
70
  }
31
71
 
72
+ /**
73
+ * stat a file but don't throw if it doesn't exist
74
+ *
75
+ * @param {string} file The path to a file
76
+ * @return {Stats} The stats structure
77
+ *
78
+ * @private
79
+ */
80
+ export function statNoException(file: string): Promise<sfs.Stats | null> {
81
+ return fs.stat(file).catch(() => null);
82
+ }
83
+
84
+ /**
85
+ * Cache for resolved executable paths
86
+ */
87
+ const pathCache = new LRUCache<string, string>({ max: 512 });
88
+
32
89
  /**
33
90
  * Search PATH to see if a file exists in any of the path folders.
34
91
  *
@@ -39,38 +96,56 @@ function statSyncNoException(file: string): sfs.Stats | null {
39
96
  * @private
40
97
  */
41
98
  function runDownPath(exe: string): string {
99
+ // Check cache first
100
+ const cached = pathCache.get(exe);
101
+ if (cached !== undefined) {
102
+ d(`Cache hit for executable: ${exe} -> ${cached}`);
103
+ return cached;
104
+ }
105
+
42
106
  // NB: Windows won't search PATH looking for executables in spawn like
43
107
  // Posix does
44
108
 
45
109
  // Files with any directory path don't get this applied
46
110
  if (exe.match(/[\\/]/)) {
47
111
  d("Path has slash in directory, bailing");
112
+ pathCache.set(exe, exe);
48
113
  return exe;
49
114
  }
50
115
 
51
116
  const target = path.join(".", exe);
52
117
  if (statSyncNoException(target)) {
53
- d(`Found executable in currect directory: ${target}`);
118
+ d(`Found executable in current directory: ${target}`);
54
119
 
55
120
  // XXX: Some very Odd programs decide to use args[0] as a parameter
56
121
  // to determine what to do, and also symlink themselves, so we can't
57
122
  // use realpathSync here like we used to
123
+ pathCache.set(exe, target);
58
124
  return target;
59
125
  }
60
126
 
61
- const haystack = process.env.PATH!.split(isWindows ? ";" : ":");
62
- for (const p of haystack) {
63
- const needle = path.join(p, exe);
64
- if (statSyncNoException(needle)) {
65
- // NB: Same deal as above
66
- return needle;
127
+ const haystack = process.env.PATH?.split(isWindows ? ";" : ":");
128
+ if (haystack) {
129
+ for (const p of haystack) {
130
+ const needle = path.join(p, exe);
131
+ if (statSyncNoException(needle)) {
132
+ // NB: Same deal as above
133
+ pathCache.set(exe, needle);
134
+ return needle;
135
+ }
67
136
  }
68
137
  }
69
138
 
70
139
  d("Failed to find executable anywhere in path");
140
+ pathCache.set(exe, exe);
71
141
  return exe;
72
142
  }
73
143
 
144
+ export type CmdWithArgs = {
145
+ cmd: string;
146
+ args: string[];
147
+ };
148
+
74
149
  /**
75
150
  * Finds the actual executable and parameters to run on Windows. This method
76
151
  * mimics the POSIX behavior of being able to run scripts as executables by
@@ -87,13 +162,7 @@ function runDownPath(exe: string): string {
87
162
  * @property {string} cmd The command to pass to spawn
88
163
  * @property {string[]} args The arguments to pass to spawn
89
164
  */
90
- export function findActualExecutable(
91
- exe: string,
92
- args: string[],
93
- ): {
94
- cmd: string;
95
- args: string[];
96
- } {
165
+ export function findActualExecutable(exe: string, args: string[]): CmdWithArgs {
97
166
  // POSIX can just execute scripts directly, no need for silly goosery
98
167
  if (process.platform !== "win32") {
99
168
  return { cmd: runDownPath(exe), args: args };
@@ -114,21 +183,8 @@ export function findActualExecutable(
114
183
  }
115
184
 
116
185
  if (exe.match(/\.ps1$/i)) {
117
- const cmd = path.join(
118
- process.env.SYSTEMROOT!,
119
- "System32",
120
- "WindowsPowerShell",
121
- "v1.0",
122
- "PowerShell.exe",
123
- );
124
- const psargs = [
125
- "-ExecutionPolicy",
126
- "Unrestricted",
127
- "-NoLogo",
128
- "-NonInteractive",
129
- "-File",
130
- exe,
131
- ];
186
+ const cmd = path.join(process.env.SYSTEMROOT!, "System32", "WindowsPowerShell", "v1.0", "PowerShell.exe");
187
+ const psargs = ["-ExecutionPolicy", "Unrestricted", "-NoLogo", "-NonInteractive", "-File", exe];
132
188
 
133
189
  return { cmd: cmd, args: psargs.concat(args) };
134
190
  }
@@ -155,8 +211,21 @@ export type SpawnRxExtras = {
155
211
  stdin?: Observable<string>;
156
212
  echoOutput?: boolean;
157
213
  split?: boolean;
158
- jobber?: boolean;
159
214
  encoding?: BufferEncoding;
215
+ /**
216
+ * Timeout in milliseconds. If the process doesn't complete within this time,
217
+ * it will be killed and the observable will error with a TimeoutError.
218
+ */
219
+ timeout?: number;
220
+ /**
221
+ * Number of retry attempts if the process fails (non-zero exit code).
222
+ * Defaults to 0 (no retries).
223
+ */
224
+ retries?: number;
225
+ /**
226
+ * Delay in milliseconds between retry attempts. Defaults to 1000ms.
227
+ */
228
+ retryDelay?: number;
160
229
  };
161
230
 
162
231
  export type OutputLine = {
@@ -165,102 +234,20 @@ export type OutputLine = {
165
234
  };
166
235
 
167
236
  /**
168
- * Spawns a process but detached from the current process. The process is put
169
- * into its own Process Group that can be killed by unsubscribing from the
170
- * return Observable.
171
- *
172
- * @param {string} exe The executable to run
173
- * @param {string[]} params The parameters to pass to the child
174
- * @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
175
- *
176
- * @return {Observable<OutputLine>} Returns an Observable that when subscribed
177
- * to, will create a detached process. The
178
- * process output will be streamed to this
179
- * Observable, and if unsubscribed from, the
180
- * process will be terminated early. If the
181
- * process terminates with a non-zero value,
182
- * the Observable will terminate with onError.
237
+ * Utility type to extract the return type based on split option
183
238
  */
184
- export function spawnDetached(
185
- exe: string,
186
- params: string[],
187
- opts: SpawnOptions & SpawnRxExtras & { split: true },
188
- ): Observable<OutputLine>;
189
-
190
- /**
191
- * Spawns a process but detached from the current process. The process is put
192
- * into its own Process Group that can be killed by unsubscribing from the
193
- * return Observable.
194
- *
195
- * @param {string} exe The executable to run
196
- * @param {string[]} params The parameters to pass to the child
197
- * @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
198
- *
199
- * @return {Observable<string>} Returns an Observable that when subscribed
200
- * to, will create a detached process. The
201
- * process output will be streamed to this
202
- * Observable, and if unsubscribed from, the
203
- * process will be terminated early. If the
204
- * process terminates with a non-zero value,
205
- * the Observable will terminate with onError.
206
- */
207
- export function spawnDetached(
208
- exe: string,
209
- params: string[],
210
- opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
211
- ): Observable<string>;
239
+ export type SpawnResult<T extends SpawnRxExtras> = T extends { split: true }
240
+ ? Observable<OutputLine>
241
+ : Observable<string>;
212
242
 
213
243
  /**
214
- * Spawns a process but detached from the current process. The process is put
215
- * into its own Process Group that can be killed by unsubscribing from the
216
- * return Observable.
217
- *
218
- * @param {string} exe The executable to run
219
- * @param {string[]} params The parameters to pass to the child
220
- * @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
221
- *
222
- * @return {Observable<string>} Returns an Observable that when subscribed
223
- * to, will create a detached process. The
224
- * process output will be streamed to this
225
- * Observable, and if unsubscribed from, the
226
- * process will be terminated early. If the
227
- * process terminates with a non-zero value,
228
- * the Observable will terminate with onError.
244
+ * Utility type to extract the promise return type based on split option
229
245
  */
230
- export function spawnDetached(
231
- exe: string,
232
- params: string[],
233
- opts?: SpawnOptions & SpawnRxExtras,
234
- ): Observable<string> | Observable<OutputLine> {
235
- const { cmd, args } = findActualExecutable(exe, params ?? []);
236
-
237
- if (!isWindows) {
238
- return spawn(
239
- cmd,
240
- args,
241
- Object.assign({}, opts || {}, { detached: true }) as any,
242
- );
243
- }
244
-
245
- const newParams = [cmd].concat(args);
246
-
247
- const target = path.join(
248
- __dirname,
249
- "..",
250
- "..",
251
- "vendor",
252
- "jobber",
253
- "Jobber.exe",
254
- );
255
- const options = {
256
- ...(opts ?? {}),
257
- detached: true,
258
- jobber: true,
259
- };
260
-
261
- d(`spawnDetached: ${target}, ${newParams}`);
262
- return spawn(target, newParams, options as any);
246
+ export type SpawnPromiseResult<T extends SpawnRxExtras> = T extends {
247
+ split: true;
263
248
  }
249
+ ? Promise<[string, string]>
250
+ : Promise<string>;
264
251
 
265
252
  /**
266
253
  * Spawns a process attached as a child of the current process.
@@ -325,133 +312,167 @@ export function spawn(
325
312
  opts?: SpawnOptions & SpawnRxExtras,
326
313
  ): Observable<string> | Observable<OutputLine> {
327
314
  opts = opts ?? {};
328
- const spawnObs: Observable<OutputLine> = new Observable(
329
- (subj: Observer<OutputLine>) => {
330
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
331
- const { stdin, jobber, split, encoding, ...spawnOpts } = opts;
332
- const { cmd, args } = findActualExecutable(exe, params);
333
- d(
334
- `spawning process: ${cmd} ${args.join()}, ${JSON.stringify(spawnOpts)}`,
335
- );
336
-
337
- const proc = spawnOg(cmd, args, spawnOpts);
338
-
339
- const bufHandler =
340
- (source: "stdout" | "stderr") => (b: string | Buffer) => {
341
- if (b.length < 1) {
342
- return;
343
- }
344
-
345
- if (opts.echoOutput) {
346
- (source === "stdout" ? process.stdout : process.stderr).write(b);
347
- }
348
-
349
- let chunk = "<< String sent back was too long >>";
350
- try {
351
- if (typeof b === "string") {
352
- chunk = b.toString();
353
- } else {
354
- chunk = b.toString(encoding || "utf8");
355
- }
356
- } catch {
357
- chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
358
- }
315
+ const spawnObs: Observable<OutputLine> = new Observable((subj: Observer<OutputLine>) => {
316
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
317
+ const { encoding, timeout, ...spawnOpts } = opts;
318
+ const { cmd, args } = findActualExecutable(exe, params);
319
+ d(`spawning process: ${cmd} ${args.join()}, ${JSON.stringify(spawnOpts)}`);
320
+
321
+ const proc = spawnOg(cmd, args, spawnOpts);
322
+ // Process metadata is tracked but not currently exposed
323
+ // Could be added to SpawnError or returned in a future enhancement
324
+ // const _processMetadata: ProcessMetadata = {
325
+ // pid: proc.pid ?? 0,
326
+ // startTime: Date.now(),
327
+ // command: cmd,
328
+ // args: args,
329
+ // };
330
+
331
+ // Set up timeout if specified
332
+ let timeoutHandle: NodeJS.Timeout | null = null;
333
+ if (timeout && timeout > 0) {
334
+ timeoutHandle = setTimeout(() => {
335
+ d(`Process timeout reached: ${cmd} ${args.join()}`);
336
+ if (!proc.killed) {
337
+ proc.kill();
338
+ }
339
+ const error = new SpawnError(`Process timed out after ${timeout}ms`, -1, cmd, args);
340
+ subj.error(error);
341
+ }, timeout);
342
+ }
359
343
 
360
- subj.next({ source: source, text: chunk });
361
- };
344
+ const bufHandler = (source: "stdout" | "stderr") => (b: string | Buffer) => {
345
+ if (b.length < 1) {
346
+ return;
347
+ }
362
348
 
363
- const ret = new Subscription();
349
+ if (opts.echoOutput) {
350
+ (source === "stdout" ? process.stdout : process.stderr).write(b);
351
+ }
364
352
 
365
- if (opts.stdin) {
366
- if (proc.stdin) {
367
- ret.add(
368
- opts.stdin.subscribe({
369
- next: (x: any) => proc.stdin.write(x),
370
- error: subj.error.bind(subj),
371
- complete: () => proc.stdin.end(),
372
- }),
373
- );
353
+ let chunk = "<< String sent back was too long >>";
354
+ try {
355
+ if (typeof b === "string") {
356
+ chunk = b.toString();
374
357
  } else {
375
- subj.error(
376
- new Error(
377
- `opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`,
378
- ),
379
- );
358
+ chunk = b.toString(encoding || "utf8");
380
359
  }
360
+ } catch {
361
+ chunk = `<< Lost chunk of process output for ${exe} - length was ${b.length}>>`;
381
362
  }
382
363
 
383
- let stderrCompleted: Subject<boolean> | Observable<boolean> | null = null;
384
- let stdoutCompleted: Subject<boolean> | Observable<boolean> | null = null;
385
- let noClose = false;
364
+ subj.next({ source: source, text: chunk });
365
+ };
386
366
 
387
- if (proc.stdout) {
388
- stdoutCompleted = new AsyncSubject<boolean>();
389
- proc.stdout.on("data", bufHandler("stdout"));
390
- proc.stdout.on("close", () => {
391
- (stdoutCompleted! as Subject<boolean>).next(true);
392
- (stdoutCompleted! as Subject<boolean>).complete();
393
- });
394
- } else {
395
- stdoutCompleted = of(true);
396
- }
367
+ const ret = new Subscription();
397
368
 
398
- if (proc.stderr) {
399
- stderrCompleted = new AsyncSubject<boolean>();
400
- proc.stderr.on("data", bufHandler("stderr"));
401
- proc.stderr.on("close", () => {
402
- (stderrCompleted! as Subject<boolean>).next(true);
403
- (stderrCompleted! as Subject<boolean>).complete();
404
- });
369
+ if (opts.stdin) {
370
+ if (proc.stdin) {
371
+ const stdin = proc.stdin;
372
+ ret.add(
373
+ opts.stdin.subscribe({
374
+ next: (x) => stdin.write(x),
375
+ error: subj.error.bind(subj),
376
+ complete: () => stdin.end(),
377
+ }),
378
+ );
405
379
  } else {
406
- stderrCompleted = of(true);
380
+ subj.error(new Error(`opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required`));
407
381
  }
382
+ }
383
+
384
+ let stderrCompleted: Subject<boolean> | Observable<boolean> | null = null;
385
+ let stdoutCompleted: Subject<boolean> | Observable<boolean> | null = null;
386
+ let noClose = false;
408
387
 
409
- proc.on("error", (e: Error) => {
410
- noClose = true;
411
- subj.error(e);
388
+ if (proc.stdout) {
389
+ stdoutCompleted = new AsyncSubject<boolean>();
390
+ proc.stdout.on("data", bufHandler("stdout"));
391
+ proc.stdout.on("close", () => {
392
+ (stdoutCompleted as Subject<boolean>).next(true);
393
+ (stdoutCompleted as Subject<boolean>).complete();
412
394
  });
395
+ } else {
396
+ stdoutCompleted = of(true);
397
+ }
413
398
 
414
- proc.on("close", (code: number) => {
415
- noClose = true;
416
- const pipesClosed = merge(stdoutCompleted!, stderrCompleted!).pipe(
417
- reduce((acc) => acc, true),
418
- );
399
+ if (proc.stderr) {
400
+ stderrCompleted = new AsyncSubject<boolean>();
401
+ proc.stderr.on("data", bufHandler("stderr"));
402
+ proc.stderr.on("close", () => {
403
+ (stderrCompleted as Subject<boolean>).next(true);
404
+ (stderrCompleted as Subject<boolean>).complete();
405
+ });
406
+ } else {
407
+ stderrCompleted = of(true);
408
+ }
419
409
 
420
- if (code === 0) {
421
- pipesClosed.subscribe(() => subj.complete());
422
- } else {
423
- pipesClosed.subscribe(() => {
424
- const e: any = new Error(`Failed with exit code: ${code}`);
425
- e.exitCode = code;
426
- e.code = code;
410
+ proc.on("error", (e: Error) => {
411
+ noClose = true;
412
+ if (timeoutHandle) {
413
+ clearTimeout(timeoutHandle);
414
+ }
415
+ subj.error(e);
416
+ });
417
+
418
+ proc.on("close", (code: number) => {
419
+ noClose = true;
420
+ if (timeoutHandle) {
421
+ clearTimeout(timeoutHandle);
422
+ }
423
+ const pipesClosed = merge(stdoutCompleted, stderrCompleted).pipe(reduce((_acc: boolean) => true, true));
424
+
425
+ if (code === 0) {
426
+ pipesClosed.subscribe(() => subj.complete());
427
+ } else {
428
+ pipesClosed.subscribe(() => {
429
+ const error = new SpawnError(`Process failed with exit code: ${code}`, code, cmd, args);
430
+ subj.error(error);
431
+ });
432
+ }
433
+ });
427
434
 
428
- subj.error(e);
429
- });
435
+ ret.add(
436
+ new Subscription(() => {
437
+ if (noClose) {
438
+ return;
430
439
  }
431
- });
432
440
 
433
- ret.add(
434
- new Subscription(() => {
435
- if (noClose) {
436
- return;
437
- }
441
+ if (timeoutHandle) {
442
+ clearTimeout(timeoutHandle);
443
+ }
438
444
 
439
- d(`Killing process: ${cmd} ${args.join()}`);
440
- if (opts.jobber) {
441
- // NB: Connecting to Jobber's named pipe will kill it
442
- net.connect(`\\\\.\\pipe\\jobber-${proc.pid}`);
443
- setTimeout(() => proc.kill(), 5 * 1000);
444
- } else {
445
- proc.kill();
446
- }
447
- }),
448
- );
445
+ d(`Killing process: ${cmd} ${args.join()}`);
446
+ proc.kill();
447
+ }),
448
+ );
449
449
 
450
- return ret;
451
- },
452
- );
450
+ return ret;
451
+ });
452
+
453
+ let resultObs: Observable<OutputLine> = spawnObs;
454
+
455
+ // Apply retry logic if specified
456
+ if (opts.retries && opts.retries > 0) {
457
+ const retryCount = opts.retries;
458
+ const delay = opts.retryDelay ?? 1000;
459
+ resultObs = resultObs.pipe(
460
+ rxRetry({
461
+ count: retryCount,
462
+ delay: (error: unknown, retryIndex: number) => {
463
+ // Only retry on SpawnError with non-zero exit codes
464
+ if (error instanceof SpawnError && error.exitCode !== 0) {
465
+ d(`Retrying process (attempt ${retryIndex + 1}/${retryCount}): ${exe}`);
466
+ return timer(delay);
467
+ }
468
+ // Don't retry on other errors
469
+ throw error;
470
+ },
471
+ }),
472
+ );
473
+ }
453
474
 
454
- return opts.split ? spawnObs : spawnObs.pipe(map((x: any) => x?.text));
475
+ return opts.split ? resultObs : resultObs.pipe(map((x: OutputLine) => x?.text));
455
476
  }
456
477
 
457
478
  function wrapObservableInPromise(obs: Observable<string>) {
@@ -459,14 +480,17 @@ function wrapObservableInPromise(obs: Observable<string>) {
459
480
  let out = "";
460
481
 
461
482
  obs.subscribe({
462
- next: (x) => (out += x),
463
- error: (e) => {
464
- const err: any = new Error(`${out}\n${e.message}`);
465
- if ("exitCode" in e) {
466
- err.exitCode = e.exitCode;
467
- err.code = e.exitCode;
483
+ next: (x: string) => {
484
+ out += x;
485
+ },
486
+ error: (e: unknown) => {
487
+ if (e instanceof SpawnError) {
488
+ const err = new SpawnError(`${out}\n${e.message}`, e.exitCode, e.command, e.args, out, e.stderr);
489
+ rej(err);
490
+ } else {
491
+ const err = new Error(`${out}\n${e instanceof Error ? e.message : String(e)}`);
492
+ rej(err);
468
493
  }
469
- rej(err);
470
494
  },
471
495
  complete: () => res(out),
472
496
  });
@@ -479,93 +503,27 @@ function wrapObservableInSplitPromise(obs: Observable<OutputLine>) {
479
503
  let err = "";
480
504
 
481
505
  obs.subscribe({
482
- next: (x) => (x.source === "stdout" ? (out += x.text) : (err += x.text)),
483
- error: (e) => {
484
- const error: any = new Error(`${out}\n${e.message}`);
485
-
486
- if ("exitCode" in e) {
487
- error.exitCode = e.exitCode;
488
- error.code = e.exitCode;
489
- error.stdout = out;
490
- error.stderr = err;
506
+ next: (x: OutputLine) => {
507
+ if (x.source === "stdout") {
508
+ out += x.text;
509
+ } else {
510
+ err += x.text;
511
+ }
512
+ },
513
+ error: (e: unknown) => {
514
+ if (e instanceof SpawnError) {
515
+ const error = new SpawnError(`${out}\n${e.message}`, e.exitCode, e.command, e.args, out, err);
516
+ rej(error);
517
+ } else {
518
+ const error = new Error(`${out}\n${e instanceof Error ? e.message : String(e)}`);
519
+ rej(error);
491
520
  }
492
- rej(error);
493
521
  },
494
522
  complete: () => res([out, err]),
495
523
  });
496
524
  });
497
525
  }
498
526
 
499
- /**
500
- * Spawns a process but detached from the current process. The process is put
501
- * into its own Process Group.
502
- *
503
- * @param {string} exe The executable to run
504
- * @param {string[]} params The parameters to pass to the child
505
- * @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
506
- *
507
- * @return {Promise<[string, string]>} Returns an Promise that represents a detached
508
- * process. The value returned is the process
509
- * output. If the process terminates with a
510
- * non-zero value, the Promise will resolve with
511
- * an Error.
512
- */
513
- export function spawnDetachedPromise(
514
- exe: string,
515
- params: string[],
516
- opts: SpawnOptions & SpawnRxExtras & { split: true },
517
- ): Promise<[string, string]>;
518
-
519
- /**
520
- * Spawns a process but detached from the current process. The process is put
521
- * into its own Process Group.
522
- *
523
- * @param {string} exe The executable to run
524
- * @param {string[]} params The parameters to pass to the child
525
- * @param {SpawnOptions & SpawnRxExtras} opts Options to pass to spawn.
526
- *
527
- * @return {Promise<string>} Returns an Promise that represents a detached
528
- * process. The value returned is the process
529
- * output. If the process terminates with a
530
- * non-zero value, the Promise will resolve with
531
- * an Error.
532
- */
533
- export function spawnDetachedPromise(
534
- exe: string,
535
- params: string[],
536
- opts?: SpawnOptions & SpawnRxExtras & { split: false | undefined },
537
- ): Promise<string>;
538
-
539
- /**
540
- * Spawns a process but detached from the current process. The process is put
541
- * into its own Process Group.
542
- *
543
- * @param {string} exe The executable to run
544
- * @param {string[]} params The parameters to pass to the child
545
- * @param {Object} opts Options to pass to spawn.
546
- *
547
- * @return {Promise<string>} Returns an Promise that represents a detached
548
- * process. The value returned is the process
549
- * output. If the process terminates with a
550
- * non-zero value, the Promise will resolve with
551
- * an Error.
552
- */
553
- export function spawnDetachedPromise(
554
- exe: string,
555
- params: string[],
556
- opts?: SpawnOptions & SpawnRxExtras,
557
- ): Promise<string> | Promise<[string, string]> {
558
- if (opts?.split) {
559
- return wrapObservableInSplitPromise(
560
- spawnDetached(exe, params, { ...(opts ?? {}), split: true }),
561
- );
562
- } else {
563
- return wrapObservableInPromise(
564
- spawnDetached(exe, params, { ...(opts ?? {}), split: false }),
565
- );
566
- }
567
- }
568
-
569
527
  /**
570
528
  * Spawns a process as a child process.
571
529
  *
@@ -598,11 +556,7 @@ export function spawnPromise(
598
556
  * non-zero value, the Promise will resolve with
599
557
  * an Error.
600
558
  */
601
- export function spawnPromise(
602
- exe: string,
603
- params: string[],
604
- opts?: SpawnOptions & SpawnRxExtras,
605
- ): Promise<string>;
559
+ export function spawnPromise(exe: string, params: string[], opts?: SpawnOptions & SpawnRxExtras): Promise<string>;
606
560
 
607
561
  /**
608
562
  * Spawns a process as a child process.
@@ -623,12 +577,7 @@ export function spawnPromise(
623
577
  opts?: SpawnOptions & SpawnRxExtras,
624
578
  ): Promise<string> | Promise<[string, string]> {
625
579
  if (opts?.split) {
626
- return wrapObservableInSplitPromise(
627
- spawn(exe, params, { ...(opts ?? {}), split: true }),
628
- );
629
- } else {
630
- return wrapObservableInPromise(
631
- spawn(exe, params, { ...(opts ?? {}), split: false }),
632
- );
580
+ return wrapObservableInSplitPromise(spawn(exe, params, { ...(opts ?? {}), split: true }));
633
581
  }
582
+ return wrapObservableInPromise(spawn(exe, params, { ...(opts ?? {}), split: false }));
634
583
  }