On Unix/macOS, pass args to spawn to avoid escaping issues

This commit is contained in:
Hans Ott 2025-10-09 16:33:40 +02:00
parent 662b26a2d5
commit 845336bf3a

View file

@ -1,4 +1,4 @@
import { spawnSync, spawn } from "child_process";
import { spawn, execSync } from "child_process";
function escapeArg(arg) {
// If argument contains spaces or quotes, wrap in double quotes and escape double quotes
@ -10,18 +10,39 @@ function escapeArg(arg) {
function buildCommand(command, args) {
const escapedArgs = args.map(escapeArg);
return `${command} ${escapedArgs.join(" ")}`;
}
export function safeSpawnSync(command, args, options = {}) {
const fullCommand = buildCommand(command, args);
return spawnSync(fullCommand, { ...options, shell: true });
function resolveCommandPath(command) {
// command will be "npm", "yarn", etc.
// Use 'command -v' to find the full path
const fullPath = execSync(`command -v ${command}`, {
encoding: "utf8",
shell: true,
}).trim();
if (!fullPath) {
throw new Error(`Command not found: ${command}`);
}
return fullPath;
}
export async function safeSpawn(command, args, options = {}) {
const fullCommand = buildCommand(command, args);
return new Promise((resolve, reject) => {
const child = spawn(fullCommand, { ...options, shell: true });
// Windows requires shell: true because .bat and .cmd files are not executable
// without a terminal. On Unix/macOS, we resolve the full path first, then use
// array args (safer, no escaping needed).
// See: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
let child;
if (process.platform === "win32") {
const fullCommand = buildCommand(command, args);
child = spawn(fullCommand, { ...options, shell: true });
} else {
const fullPath = resolveCommandPath(command);
child = spawn(fullPath, args, options);
}
// When stdio is piped, we need to collect the output
let stdout = "";