@zokugun/exec-lite 0.0.0 → 0.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.
Files changed (67) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +306 -34
  3. package/lib/cjs/create-command.cjs +19 -0
  4. package/lib/cjs/create-command.d.cts +3 -0
  5. package/lib/cjs/exec-process.cjs +241 -0
  6. package/lib/cjs/exec-process.d.cts +41 -0
  7. package/lib/cjs/exec.cjs +9 -0
  8. package/lib/cjs/exec.d.cts +3 -0
  9. package/lib/cjs/index.cjs +9 -0
  10. package/lib/cjs/index.d.cts +4 -0
  11. package/lib/cjs/types.cjs +2 -0
  12. package/lib/cjs/types.d.cts +70 -0
  13. package/lib/cjs/utils/combineSignals.cjs +19 -0
  14. package/lib/cjs/utils/combineSignals.d.cts +1 -0
  15. package/lib/cjs/utils/command/detectShebang.cjs +36 -0
  16. package/lib/cjs/utils/command/detectShebang.d.cts +1 -0
  17. package/lib/cjs/utils/command/normalizeCommand.cjs +59 -0
  18. package/lib/cjs/utils/command/normalizeCommand.d.cts +6 -0
  19. package/lib/cjs/utils/command/resolveCommand.cjs +43 -0
  20. package/lib/cjs/utils/command/resolveCommand.d.cts +2 -0
  21. package/lib/cjs/utils/env/addNodeBinToPath.cjs +24 -0
  22. package/lib/cjs/utils/env/addNodeBinToPath.d.cts +2 -0
  23. package/lib/cjs/utils/env/computeEnv.cjs +17 -0
  24. package/lib/cjs/utils/env/computeEnv.d.cts +2 -0
  25. package/lib/cjs/utils/env/getPathFromEnv.cjs +22 -0
  26. package/lib/cjs/utils/env/getPathFromEnv.d.cts +2 -0
  27. package/lib/cjs/utils/env/types.cjs +2 -0
  28. package/lib/cjs/utils/env/types.d.cts +5 -0
  29. package/lib/cjs/utils/normalizeOptions.cjs +31 -0
  30. package/lib/cjs/utils/normalizeOptions.d.cts +2 -0
  31. package/lib/cjs/utils/stream/combineStreams.cjs +25 -0
  32. package/lib/cjs/utils/stream/combineStreams.d.cts +2 -0
  33. package/lib/cjs/utils/stream/readStream.cjs +16 -0
  34. package/lib/cjs/utils/stream/readStream.d.cts +5 -0
  35. package/lib/esm/create-command.d.mts +3 -0
  36. package/lib/esm/create-command.mjs +17 -0
  37. package/lib/esm/exec-process.d.mts +41 -0
  38. package/lib/esm/exec-process.mjs +235 -0
  39. package/lib/esm/exec.d.mts +3 -0
  40. package/lib/esm/exec.mjs +7 -0
  41. package/lib/esm/index.d.mts +4 -0
  42. package/lib/esm/index.mjs +3 -0
  43. package/lib/esm/types.d.mts +70 -0
  44. package/lib/esm/types.mjs +1 -0
  45. package/lib/esm/utils/combineSignals.d.mts +1 -0
  46. package/lib/esm/utils/combineSignals.mjs +16 -0
  47. package/lib/esm/utils/command/detectShebang.d.mts +1 -0
  48. package/lib/esm/utils/command/detectShebang.mjs +30 -0
  49. package/lib/esm/utils/command/normalizeCommand.d.mts +6 -0
  50. package/lib/esm/utils/command/normalizeCommand.mjs +53 -0
  51. package/lib/esm/utils/command/resolveCommand.d.mts +2 -0
  52. package/lib/esm/utils/command/resolveCommand.mjs +37 -0
  53. package/lib/esm/utils/env/addNodeBinToPath.d.mts +2 -0
  54. package/lib/esm/utils/env/addNodeBinToPath.mjs +18 -0
  55. package/lib/esm/utils/env/computeEnv.d.mts +2 -0
  56. package/lib/esm/utils/env/computeEnv.mjs +14 -0
  57. package/lib/esm/utils/env/getPathFromEnv.d.mts +2 -0
  58. package/lib/esm/utils/env/getPathFromEnv.mjs +19 -0
  59. package/lib/esm/utils/env/types.d.mts +5 -0
  60. package/lib/esm/utils/env/types.mjs +1 -0
  61. package/lib/esm/utils/normalizeOptions.d.mts +2 -0
  62. package/lib/esm/utils/normalizeOptions.mjs +25 -0
  63. package/lib/esm/utils/stream/combineStreams.d.mts +2 -0
  64. package/lib/esm/utils/stream/combineStreams.mjs +22 -0
  65. package/lib/esm/utils/stream/readStream.d.mts +5 -0
  66. package/lib/esm/utils/stream/readStream.mjs +13 -0
  67. package/package.json +145 -3
@@ -0,0 +1,235 @@
1
+ import { spawn } from 'node:child_process';
2
+ import readline from 'node:readline';
3
+ import { err, ok, stringifyError } from '@zokugun/xtry';
4
+ import { exec } from './exec.mjs';
5
+ import { normalizeCommand } from './utils/command/normalizeCommand.mjs';
6
+ import { normalizeOptions } from './utils/normalizeOptions.mjs';
7
+ import { combineStreams } from './utils/stream/combineStreams.mjs';
8
+ import { readStream } from './utils/stream/readStream.mjs';
9
+ class ExecProcess {
10
+ _aborted = false;
11
+ _args;
12
+ _command;
13
+ _cwd;
14
+ _failed = false;
15
+ _options;
16
+ _process;
17
+ _processClosed;
18
+ _resolveClose;
19
+ _signal;
20
+ _start;
21
+ _streamErr;
22
+ _streamOut;
23
+ _thrownError;
24
+ constructor(command, args, options) {
25
+ this._options = {
26
+ timeout: undefined,
27
+ windowsHide: true,
28
+ ...options,
29
+ };
30
+ this._command = command;
31
+ this._args = args ?? [];
32
+ this._processClosed = new Promise((resolve) => {
33
+ this._resolveClose = resolve;
34
+ });
35
+ } // }}}
36
+ get aborted() {
37
+ return this._aborted;
38
+ } // }}}
39
+ get error() {
40
+ return this._thrownError;
41
+ } // }}}
42
+ get exitCode() {
43
+ return this._process?.exitCode ?? undefined;
44
+ } // }}}
45
+ get failed() {
46
+ return this._failed;
47
+ } // }}}
48
+ get killed() {
49
+ return this._process?.killed === true;
50
+ } // }}}
51
+ get pid() {
52
+ return this._process?.pid;
53
+ } // }}}
54
+ get process() {
55
+ return this._process;
56
+ } // }}}
57
+ get signalCode() {
58
+ return this._process?.signalCode ?? undefined;
59
+ } // }}}
60
+ kill(signal) {
61
+ return this._process?.kill(signal) === true;
62
+ } // }}}
63
+ pipe(command, args, options) {
64
+ return exec(command, args, {
65
+ ...options,
66
+ stdin: this,
67
+ });
68
+ } // }}}
69
+ spawn() {
70
+ this._start = process.hrtime.bigint();
71
+ this.reset();
72
+ const normalizedOptions = normalizeOptions(this._options);
73
+ this._cwd = normalizedOptions.cwd;
74
+ if (normalizedOptions.signal) {
75
+ this._signal = normalizedOptions.signal;
76
+ }
77
+ const { command: spawnCommand, args: spawnArgs, options: spawnOptions } = normalizeCommand(this._command, this._args, normalizedOptions);
78
+ let task;
79
+ try {
80
+ task = spawn(spawnCommand, spawnArgs, spawnOptions);
81
+ }
82
+ catch (error) {
83
+ if (error instanceof Error) {
84
+ this._thrownError = error;
85
+ }
86
+ else {
87
+ this._thrownError = new Error(String(error));
88
+ }
89
+ return;
90
+ }
91
+ if (task.stderr) {
92
+ this._streamErr = task.stderr;
93
+ }
94
+ if (task.stdout) {
95
+ this._streamOut = task.stdout;
96
+ }
97
+ this._process = task;
98
+ task.once('error', this.onError.bind(this));
99
+ task.once('close', this.onClose.bind(this));
100
+ if (task.stdin) {
101
+ const { stdin } = this._options;
102
+ if (typeof stdin === 'string') {
103
+ task.stdin.end(stdin);
104
+ }
105
+ else if (typeof stdin === 'object' && stdin !== null && 'process' in stdin) {
106
+ stdin.process?.stdout?.pipe(task.stdin);
107
+ }
108
+ }
109
+ } // }}}
110
+ // eslint-disable-next-line ts/explicit-member-accessibility
111
+ async *[Symbol.asyncIterator]() {
112
+ const handle = this._process;
113
+ if (!handle) {
114
+ return;
115
+ }
116
+ const streams = [];
117
+ if (this._streamErr) {
118
+ streams.push(this._streamErr);
119
+ }
120
+ if (this._streamOut) {
121
+ streams.push(this._streamOut);
122
+ }
123
+ const streamCombined = combineStreams(streams);
124
+ const rl = readline.createInterface({ input: streamCombined });
125
+ for await (const chunk of rl) {
126
+ // eslint-disable-next-line ts/no-unnecessary-type-conversion
127
+ yield chunk.toString();
128
+ }
129
+ await this._processClosed;
130
+ handle.removeAllListeners();
131
+ if (this._thrownError || (this.exitCode !== 0 && this.exitCode !== undefined) || this.signalCode !== undefined) {
132
+ this._failed = true;
133
+ }
134
+ } // }}}
135
+ // eslint-disable-next-line ts/promise-function-async
136
+ then(onfulfilled, onrejected) {
137
+ return this.toResult().then((result) => {
138
+ if (onfulfilled) {
139
+ return onfulfilled(result);
140
+ }
141
+ return result;
142
+ }, onrejected);
143
+ } // }}}
144
+ onClose() {
145
+ if (this._resolveClose) {
146
+ this._resolveClose();
147
+ }
148
+ } // }}}
149
+ onError(error) {
150
+ if (error.name === 'AbortError' && (!(error.cause instanceof Error) || error.cause.name !== 'TimeoutError')) {
151
+ this._aborted = true;
152
+ return;
153
+ }
154
+ this._thrownError = error;
155
+ } // }}}
156
+ reset() {
157
+ this._aborted = false;
158
+ this._process = undefined;
159
+ this._processClosed = new Promise((resolve) => {
160
+ this._resolveClose = resolve;
161
+ });
162
+ this._signal = undefined;
163
+ this._streamErr = undefined;
164
+ this._streamOut = undefined;
165
+ this._thrownError = undefined;
166
+ } // }}}
167
+ toFailure(error) {
168
+ this._failed = true;
169
+ if (this._options.resultMode === 'merged') {
170
+ return stringifyError(error);
171
+ }
172
+ const result = {
173
+ args: this._args,
174
+ canceled: this._signal?.aborted ?? false,
175
+ command: this._command,
176
+ cwd: this._cwd,
177
+ durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
178
+ error,
179
+ exitCode: this.exitCode,
180
+ merged: '',
181
+ signalCode: this.signalCode,
182
+ };
183
+ return err(result);
184
+ } // }}}
185
+ async toResult() {
186
+ if (this._thrownError) {
187
+ return this.toFailure(this._thrownError);
188
+ }
189
+ const task = this._process;
190
+ if (!task) {
191
+ return this.toFailure(new Error('No process was started'));
192
+ }
193
+ const context = { merged: '' };
194
+ const [stdout, stderr] = await Promise.all([
195
+ this._streamOut ? readStream(this._streamOut, context) : '',
196
+ this._streamErr ? readStream(this._streamErr, context) : '',
197
+ ]);
198
+ await this._processClosed;
199
+ const { stdin } = this._options;
200
+ if (stdin && typeof stdin !== 'string') {
201
+ await stdin;
202
+ }
203
+ task.removeAllListeners();
204
+ if (this._thrownError) {
205
+ return this.toFailure(this._thrownError);
206
+ }
207
+ if (this._options.resultMode === 'merged') {
208
+ if ((this.exitCode !== 0 && this.exitCode !== undefined) || this.signalCode !== undefined) {
209
+ this._failed = true;
210
+ }
211
+ return context.merged;
212
+ }
213
+ const result = {
214
+ args: this._args,
215
+ command: this._command,
216
+ cwd: this._cwd,
217
+ durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
218
+ exitCode: this.exitCode,
219
+ merged: context.merged,
220
+ stderr,
221
+ stdout,
222
+ };
223
+ // if(this._options.stdin instanceof ExecProcess) {
224
+ // result.pipedFrom = this._options.stdin;
225
+ // }
226
+ if ((this.exitCode !== 0 && this.exitCode !== undefined) || this.signalCode !== undefined) {
227
+ this._failed = true;
228
+ const failure = result;
229
+ failure.signalCode = this.signalCode;
230
+ return err(failure);
231
+ }
232
+ return ok(result);
233
+ } // }}}
234
+ }
235
+ export { ExecProcess, };
@@ -0,0 +1,3 @@
1
+ import type { Options, ResultMode, Subprocess } from './types.mjs';
2
+ declare function exec<M extends ResultMode = 'result'>(command: string, args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
3
+ export { type Options, type ResultMode, type Subprocess, exec, };
@@ -0,0 +1,7 @@
1
+ import { ExecProcess } from './exec-process.mjs';
2
+ function exec(command, args, options) {
3
+ const subprocess = new ExecProcess(command, args, options);
4
+ subprocess.spawn();
5
+ return subprocess;
6
+ }
7
+ export { exec, };
@@ -0,0 +1,4 @@
1
+ export type { Command, ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, ResultMode, StdinOption, StdioOption, Subprocess, } from './types.mjs';
2
+ export { createCommand } from './create-command.mjs';
3
+ export { ExecProcess } from './exec-process.mjs';
4
+ export { exec } from './exec.mjs';
@@ -0,0 +1,3 @@
1
+ export { createCommand } from './create-command.mjs';
2
+ export { ExecProcess } from './exec-process.mjs';
3
+ export { exec } from './exec.mjs';
@@ -0,0 +1,70 @@
1
+ import type { ChildProcess, SpawnOptions as ProcessSpawnOptions } from 'node:child_process';
2
+ import type { Result } from '@zokugun/xtry';
3
+ export type Command<M extends ResultMode = 'result'> = {
4
+ (args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
5
+ toCwdFirst(): (cwd: string, ...args: string[]) => Subprocess<M>;
6
+ toFlatArgs(): (...args: string[]) => Subprocess<M>;
7
+ withOptions(options: Partial<Options<M>>): Command<M>;
8
+ };
9
+ export type ExecFailure = {
10
+ args: readonly string[];
11
+ canceled: boolean;
12
+ cwd?: string;
13
+ command: string;
14
+ durationMs: number;
15
+ error?: Error;
16
+ exitCode?: number;
17
+ merged?: string;
18
+ pipedFrom?: ExecResult;
19
+ signalCode?: string;
20
+ stderr?: string;
21
+ stdout?: string;
22
+ };
23
+ export type ResultMode = 'merged' | 'result';
24
+ export type ExecResult<M extends ResultMode = 'result'> = M extends 'merged' ? string : Result<ExecSuccess, ExecFailure>;
25
+ export type ExecSuccess = {
26
+ args: readonly string[];
27
+ command: string;
28
+ cwd: string;
29
+ durationMs: number;
30
+ error?: undefined;
31
+ exitCode: number;
32
+ merged: string;
33
+ pipedFrom?: ExecResult;
34
+ stderr: string;
35
+ stdout: string;
36
+ };
37
+ export type KillSignal = Parameters<ChildProcess['kill']>[0];
38
+ export type Options<M extends ResultMode = ResultMode> = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
39
+ cwd?: string;
40
+ env: Readonly<Partial<Record<string, string>>>;
41
+ localDir: string;
42
+ preferLocal: boolean;
43
+ resultMode: M;
44
+ signal: AbortSignal;
45
+ stderr: StdioOption;
46
+ stdin: ExecResult | StdinOption | Subprocess<ResultMode>;
47
+ stdio: readonly [StdinOption, ...readonly StdioOption[]] | SpawnOptions['stdio'];
48
+ stdout: StdioOption;
49
+ }>>;
50
+ export type SpawnOptions = {
51
+ cwd: string;
52
+ } & Omit<ProcessSpawnOptions, 'cwd'>;
53
+ export type StdinOption = StdioOption | {
54
+ readonly string?: string;
55
+ };
56
+ export type StdioOption = Readonly<Exclude<SpawnOptions['stdio'], undefined>[number]>;
57
+ export type Subprocess<M extends ResultMode = 'result'> = AsyncIterable<string> & PromiseLike<ExecResult<M>> & {
58
+ get aborted(): boolean;
59
+ get error(): Error | undefined;
60
+ get exitCode(): number | undefined;
61
+ get failed(): boolean;
62
+ kill: (signal?: KillSignal) => boolean;
63
+ get killed(): boolean;
64
+ get pid(): number | undefined;
65
+ pipe: <N extends ResultMode = 'result'>(command: string, args?: readonly string[], options?: Partial<Options> & {
66
+ resultMode?: N;
67
+ }) => Subprocess<N>;
68
+ get process(): ChildProcess | undefined;
69
+ get signalCode(): string | undefined;
70
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function combineSignals(signals: AbortSignal[]): AbortSignal;
@@ -0,0 +1,16 @@
1
+ export function combineSignals(signals) {
2
+ const controller = new AbortController();
3
+ for (const signal of signals) {
4
+ if (signal.aborted) {
5
+ controller.abort();
6
+ return signal;
7
+ }
8
+ const onAbort = () => {
9
+ controller.abort(signal.reason);
10
+ };
11
+ signal.addEventListener('abort', onAbort, {
12
+ signal: controller.signal,
13
+ });
14
+ }
15
+ return controller.signal;
16
+ }
@@ -0,0 +1 @@
1
+ export declare function detectShebang(file: string): string | null;
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ const SHEBANG_REGEX = /^#!\s*(.+)/;
4
+ const SIZE = 150;
5
+ export function detectShebang(file) {
6
+ const buffer = Buffer.alloc(SIZE);
7
+ let fd = null;
8
+ try {
9
+ fd = fs.openSync(file, 'r');
10
+ fs.readSync(fd, buffer, 0, SIZE, 0);
11
+ }
12
+ catch {
13
+ // do nothing, we'll just assume it's not a shebang
14
+ }
15
+ finally {
16
+ if (fd !== null) {
17
+ fs.closeSync(fd);
18
+ }
19
+ }
20
+ const match = SHEBANG_REGEX.exec(buffer.toString());
21
+ if (match === null) {
22
+ return null;
23
+ }
24
+ const line = match[1].trim();
25
+ const separatorIndex = line.indexOf(' ');
26
+ const path = separatorIndex !== -1 ? line.slice(0, separatorIndex) : line;
27
+ const argument = separatorIndex !== -1 ? line.slice(separatorIndex + 1) : '';
28
+ const binary = basename(path);
29
+ return binary === 'env' ? argument || null : binary;
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { SpawnOptions } from '../../types.mjs';
2
+ export declare function normalizeCommand(command: string, args: readonly string[], options: SpawnOptions): {
3
+ args: readonly string[];
4
+ command: string;
5
+ options: SpawnOptions;
6
+ };
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { detectShebang } from './detectShebang.mjs';
3
+ import { resolveCommand } from './resolveCommand.mjs';
4
+ const IS_BATCH_FILE_REGEX = /\.(?:bat|cmd)$/i;
5
+ const IS_EXECUTABLE_REGEX = /\.(?:com|exe)$/i;
6
+ const IS_WINDOWS = process.platform === 'win32';
7
+ const META_CHARS_REGEX = /([ !"%&()*,;<>?[\]^`|])/g;
8
+ export function normalizeCommand(command, args, options) {
9
+ if (['node', 'node.exe'].includes(command.toLowerCase())) {
10
+ command = process.execPath;
11
+ args = [...process.execArgv.filter((flag) => !flag.startsWith('--inspect')), ...args];
12
+ }
13
+ if (options.shell === true || !IS_WINDOWS) {
14
+ return {
15
+ command,
16
+ args,
17
+ options,
18
+ };
19
+ }
20
+ let file = resolveCommand(command, options);
21
+ if (file !== null) {
22
+ const shebang = detectShebang(file);
23
+ if (shebang !== null) {
24
+ args = [file, ...args];
25
+ command = shebang;
26
+ file = resolveCommand(command, options);
27
+ }
28
+ }
29
+ if (file !== null && IS_EXECUTABLE_REGEX.test(file)) {
30
+ return {
31
+ command,
32
+ args,
33
+ options,
34
+ };
35
+ }
36
+ const needsDoubleEscape = file !== null && IS_BATCH_FILE_REGEX.test(file);
37
+ const normalizedCommand = path.normalize(command).replace(META_CHARS_REGEX, '^$1');
38
+ const normalizedArgs = [];
39
+ for (let arg of args) {
40
+ arg = arg.replace(/(?=(\\+?))\1(?:"|$)/g, '$1$1\\"');
41
+ arg = `"${arg}"`;
42
+ arg = arg.replace(META_CHARS_REGEX, '^$1');
43
+ if (needsDoubleEscape) {
44
+ arg = arg.replace(META_CHARS_REGEX, '^$1');
45
+ }
46
+ normalizedArgs.push(arg);
47
+ }
48
+ return {
49
+ command: options.env?.comspec ?? 'cmd.exe',
50
+ args: ['/d', '/s', '/c', `"${[normalizedCommand, ...normalizedArgs].join(' ')}"`],
51
+ options: { ...options, windowsVerbatimArguments: true },
52
+ };
53
+ }
@@ -0,0 +1,2 @@
1
+ import type { SpawnOptions } from '../../types.mjs';
2
+ export declare function resolveCommand(command: string, options: SpawnOptions): string | null;
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getPathFromEnv } from '../env/getPathFromEnv.mjs';
4
+ const CACHE = Object.create(null);
5
+ const DEFAULT_PATH_EXT = ['.EXE', '.CMD', '.BAT', '.COM'];
6
+ export function resolveCommand(command, options) {
7
+ const cwd = options.cwd ?? process.cwd();
8
+ const env = options.env ?? process.env;
9
+ const pathEnv = getPathFromEnv(env).value;
10
+ const key = `${command}%%%%${pathEnv}`;
11
+ if (CACHE[key] !== undefined) {
12
+ return CACHE[key];
13
+ }
14
+ const parts = command.includes('/') || command.includes('\\') ? [''] : [cwd, ...pathEnv.split(path.delimiter)];
15
+ let pathExt = env.PATHEXT ? env.PATHEXT.split(path.delimiter) : DEFAULT_PATH_EXT;
16
+ if (command.includes('.') && pathExt[0] !== '') {
17
+ pathExt = ['', ...pathExt];
18
+ }
19
+ for (const part of parts) {
20
+ const unquoted = part.startsWith('"') && part.endsWith('"') && part.length > 1 ? part.slice(1, -1) : part;
21
+ const dest = path.resolve(cwd, unquoted, command);
22
+ for (const ext of [...pathExt, '']) {
23
+ const destWithExt = dest + ext;
24
+ try {
25
+ if (fs.statSync(destWithExt).isFile()) {
26
+ CACHE[key] = destWithExt;
27
+ return destWithExt;
28
+ }
29
+ }
30
+ catch {
31
+ // do nothing, it didn't exist
32
+ }
33
+ }
34
+ }
35
+ CACHE[key] = null;
36
+ return null;
37
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvPathInfo } from './types.mjs';
2
+ export declare function addNodeBinToPath(cwd: string, { value, key }: EnvPathInfo): EnvPathInfo;
@@ -0,0 +1,18 @@
1
+ import path from 'node:path';
2
+ export function addNodeBinToPath(cwd, { value, key }) {
3
+ const parts = value.split(path.delimiter);
4
+ const nodeBinPaths = [];
5
+ let currentPath = cwd;
6
+ let lastPath;
7
+ do {
8
+ nodeBinPaths.push(path.resolve(currentPath, 'node_modules', '.bin'));
9
+ lastPath = currentPath;
10
+ currentPath = path.dirname(currentPath);
11
+ } while (currentPath !== lastPath);
12
+ nodeBinPaths.push(path.dirname(process.execPath));
13
+ const newValue = nodeBinPaths.concat(parts).join(path.delimiter);
14
+ return {
15
+ key,
16
+ value: newValue,
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvLike } from './types.mjs';
2
+ export declare function computeEnv(env: Readonly<EnvLike> | undefined, preferLocal: boolean, localDir: string): Readonly<EnvLike>;
@@ -0,0 +1,14 @@
1
+ import { addNodeBinToPath } from './addNodeBinToPath.mjs';
2
+ import { getPathFromEnv } from './getPathFromEnv.mjs';
3
+ export function computeEnv(env, preferLocal, localDir) {
4
+ const computedEnv = {
5
+ ...process.env,
6
+ ...env,
7
+ };
8
+ if (!preferLocal) {
9
+ return computedEnv;
10
+ }
11
+ const pathEnvInfo = addNodeBinToPath(localDir, getPathFromEnv(computedEnv));
12
+ computedEnv[pathEnvInfo.key] = pathEnvInfo.value;
13
+ return computedEnv;
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvLike, EnvPathInfo } from './types.mjs';
2
+ export declare function getPathFromEnv(env: EnvLike): EnvPathInfo;
@@ -0,0 +1,19 @@
1
+ export function getPathFromEnv(env) {
2
+ for (const key in env) {
3
+ if (key.toLowerCase() !== 'path') {
4
+ continue;
5
+ }
6
+ const value = env[key];
7
+ if (!value) {
8
+ return {
9
+ key: 'PATH',
10
+ value: '',
11
+ };
12
+ }
13
+ return { key, value };
14
+ }
15
+ return {
16
+ key: 'PATH',
17
+ value: '',
18
+ };
19
+ }
@@ -0,0 +1,5 @@
1
+ export type EnvLike = (typeof process)['env'];
2
+ export type EnvPathInfo = {
3
+ key: string;
4
+ value: string;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Options, SpawnOptions } from '../types.mjs';
2
+ export declare function normalizeOptions(options: Options): SpawnOptions;
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { computeEnv } from './env/computeEnv.mjs';
3
+ import { combineSignals } from './combineSignals.mjs';
4
+ export function normalizeOptions(options) {
5
+ const { cwd, env, preferLocal, resultMode: _resultMode, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
6
+ const spawnOptions = restOptions;
7
+ spawnOptions.cwd = path.resolve(cwd ?? process.cwd());
8
+ spawnOptions.env = computeEnv(env, preferLocal ?? false, options.localDir ?? spawnOptions.cwd);
9
+ const signals = [];
10
+ if (options.timeout !== undefined) {
11
+ signals.push(AbortSignal.timeout(options.timeout));
12
+ }
13
+ if (signal !== undefined) {
14
+ signals.push(signal);
15
+ }
16
+ if (signals.length > 0) {
17
+ spawnOptions.signal = combineSignals(signals);
18
+ }
19
+ const stdio = stdioOption ?? [undefined, stdout, stderr];
20
+ if (Array.isArray(stdio) && !(typeof stdin === 'string' || (typeof stdin === 'object' && stdin !== null && 'process' in stdin))) {
21
+ stdio[0] = 'pipe';
22
+ }
23
+ spawnOptions.stdio = stdio;
24
+ return spawnOptions;
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { Readable } from 'node:stream';
2
+ export declare function combineStreams(streams: Readable[]): Readable;
@@ -0,0 +1,22 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ export function combineStreams(streams) {
4
+ let streamCount = streams.length;
5
+ const combined = new PassThrough();
6
+ if (streamCount === 0) {
7
+ combined.end();
8
+ return combined;
9
+ }
10
+ const maybeEmitEnd = () => {
11
+ streamCount -= 1;
12
+ if (streamCount === 0) {
13
+ combined.end();
14
+ }
15
+ };
16
+ for (const stream of streams) {
17
+ pipeline(stream, combined, { end: false })
18
+ .then(maybeEmitEnd)
19
+ .catch(maybeEmitEnd);
20
+ }
21
+ return combined;
22
+ }
@@ -0,0 +1,5 @@
1
+ import type { Readable } from 'node:stream';
2
+ export type StreamContext = {
3
+ merged: string;
4
+ };
5
+ export declare function readStream(stream: Readable, context: StreamContext): Promise<string>;
@@ -0,0 +1,13 @@
1
+ export async function readStream(stream, context) {
2
+ let output = '';
3
+ try {
4
+ for await (const chunk of stream) {
5
+ output += String(chunk);
6
+ context.merged += String(chunk);
7
+ }
8
+ }
9
+ catch {
10
+ // suppress errors, child process probably failed to spawn
11
+ }
12
+ return output;
13
+ }