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