Files
itworx-knowledge-hub-Public/scripts/lib/exec.mjs
T

107 lines
3.4 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import process from "node:process";
export function commandExists(command) {
const locator = process.platform === "win32" ? "where" : "which";
const result = spawnSync(locator, [command], {
stdio: "ignore",
shell: false
});
return result.status === 0;
}
// On Windows, some tools (corepack, pnpm, npm, ...) are installed as `.cmd`/`.bat`
// shims. Those are not real PE executables, so libuv/CreateProcess cannot launch
// them directly: `child_process.spawn` fails with ENOENT even though the same
// command works in an interactive shell. Real executables (dotnet.exe, docker.exe,
// node.exe, python.exe) do not have this problem and must NOT be routed through a
// shell, because shell-quoting a path that contains spaces (e.g. "C:\Program
// Files\...") breaks it. Detect which case applies per command instead of
// guessing globally.
function requiresWindowsShell(command) {
if (process.platform !== "win32") {
return false;
}
if (/[\\/]/.test(command)) {
// Already a path; trust its extension.
return /\.(cmd|bat)$/i.test(command);
}
const located = spawnSync("where", [command], {
encoding: "utf8",
shell: false
});
if (located.status !== 0) {
return false;
}
// `where` can list several matches on Windows (e.g. an extension-less shebang
// launcher script alongside a `.cmd` shim for the same tool). Node can only
// ever run the `.cmd`/`.bat` variant through a shell, so treat the command as
// shell-requiring if *any* match needs one, not just the first line.
const matches = located.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
return matches.some((match) => /\.(cmd|bat)$/i.test(match));
}
function quoteForWindowsShell(value) {
if (value === "") {
return '""';
}
if (!/[\s"^&|<>()%]/.test(value)) {
return value;
}
return `"${value.replace(/"/g, '\\"')}"`;
}
export async function run(command, args = [], options = {}) {
const printable = [command, ...args].join(" ");
if (!options.quiet) {
process.stdout.write(`\n> ${printable}\n`);
}
const useShell = requiresWindowsShell(command);
const spawnCommand = useShell
? [command, ...args].map(quoteForWindowsShell).join(" ")
: command;
const spawnArgs = useShell ? [] : args;
return await new Promise((resolve, reject) => {
const child = spawn(spawnCommand, spawnArgs, {
cwd: options.cwd ?? process.cwd(),
env: { ...process.env, ...options.env },
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
shell: useShell
});
let stdout = "";
let stderr = "";
if (options.capture) {
child.stdout?.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
}
child.on("error", reject);
child.on("close", (code, signal) => {
if (code === 0) {
resolve({ code, stdout, stderr });
return;
}
const suffix = signal ? ` (signal ${signal})` : "";
const error = new Error(`Command failed (${code}${suffix}): ${printable}`);
error.code = code;
error.stdout = stdout;
error.stderr = stderr;
reject(error);
});
});
}
export async function capture(command, args = [], options = {}) {
const result = await run(command, args, { ...options, capture: true, quiet: true });
return result.stdout.trim();
}