@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.
- package/LICENSE +22 -0
- package/README.md +306 -34
- package/lib/cjs/create-command.cjs +19 -0
- package/lib/cjs/create-command.d.cts +3 -0
- package/lib/cjs/exec-process.cjs +241 -0
- package/lib/cjs/exec-process.d.cts +41 -0
- package/lib/cjs/exec.cjs +9 -0
- package/lib/cjs/exec.d.cts +3 -0
- package/lib/cjs/index.cjs +9 -0
- package/lib/cjs/index.d.cts +4 -0
- package/lib/cjs/types.cjs +2 -0
- package/lib/cjs/types.d.cts +70 -0
- package/lib/cjs/utils/combineSignals.cjs +19 -0
- package/lib/cjs/utils/combineSignals.d.cts +1 -0
- package/lib/cjs/utils/command/detectShebang.cjs +36 -0
- package/lib/cjs/utils/command/detectShebang.d.cts +1 -0
- package/lib/cjs/utils/command/normalizeCommand.cjs +59 -0
- package/lib/cjs/utils/command/normalizeCommand.d.cts +6 -0
- package/lib/cjs/utils/command/resolveCommand.cjs +43 -0
- package/lib/cjs/utils/command/resolveCommand.d.cts +2 -0
- package/lib/cjs/utils/env/addNodeBinToPath.cjs +24 -0
- package/lib/cjs/utils/env/addNodeBinToPath.d.cts +2 -0
- package/lib/cjs/utils/env/computeEnv.cjs +17 -0
- package/lib/cjs/utils/env/computeEnv.d.cts +2 -0
- package/lib/cjs/utils/env/getPathFromEnv.cjs +22 -0
- package/lib/cjs/utils/env/getPathFromEnv.d.cts +2 -0
- package/lib/cjs/utils/env/types.cjs +2 -0
- package/lib/cjs/utils/env/types.d.cts +5 -0
- package/lib/cjs/utils/normalizeOptions.cjs +31 -0
- package/lib/cjs/utils/normalizeOptions.d.cts +2 -0
- package/lib/cjs/utils/stream/combineStreams.cjs +25 -0
- package/lib/cjs/utils/stream/combineStreams.d.cts +2 -0
- package/lib/cjs/utils/stream/readStream.cjs +16 -0
- package/lib/cjs/utils/stream/readStream.d.cts +5 -0
- package/lib/esm/create-command.d.mts +3 -0
- package/lib/esm/create-command.mjs +17 -0
- package/lib/esm/exec-process.d.mts +41 -0
- package/lib/esm/exec-process.mjs +235 -0
- package/lib/esm/exec.d.mts +3 -0
- package/lib/esm/exec.mjs +7 -0
- package/lib/esm/index.d.mts +4 -0
- package/lib/esm/index.mjs +3 -0
- package/lib/esm/types.d.mts +70 -0
- package/lib/esm/types.mjs +1 -0
- package/lib/esm/utils/combineSignals.d.mts +1 -0
- package/lib/esm/utils/combineSignals.mjs +16 -0
- package/lib/esm/utils/command/detectShebang.d.mts +1 -0
- package/lib/esm/utils/command/detectShebang.mjs +30 -0
- package/lib/esm/utils/command/normalizeCommand.d.mts +6 -0
- package/lib/esm/utils/command/normalizeCommand.mjs +53 -0
- package/lib/esm/utils/command/resolveCommand.d.mts +2 -0
- package/lib/esm/utils/command/resolveCommand.mjs +37 -0
- package/lib/esm/utils/env/addNodeBinToPath.d.mts +2 -0
- package/lib/esm/utils/env/addNodeBinToPath.mjs +18 -0
- package/lib/esm/utils/env/computeEnv.d.mts +2 -0
- package/lib/esm/utils/env/computeEnv.mjs +14 -0
- package/lib/esm/utils/env/getPathFromEnv.d.mts +2 -0
- package/lib/esm/utils/env/getPathFromEnv.mjs +19 -0
- package/lib/esm/utils/env/types.d.mts +5 -0
- package/lib/esm/utils/env/types.mjs +1 -0
- package/lib/esm/utils/normalizeOptions.d.mts +2 -0
- package/lib/esm/utils/normalizeOptions.mjs +25 -0
- package/lib/esm/utils/stream/combineStreams.d.mts +2 -0
- package/lib/esm/utils/stream/combineStreams.mjs +22 -0
- package/lib/esm/utils/stream/readStream.d.mts +5 -0
- package/lib/esm/utils/stream/readStream.mjs +13 -0
- 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, };
|
package/lib/esm/exec.mjs
ADDED
|
@@ -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,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,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,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,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,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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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,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
|
+
}
|