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) { function escapeArg(arg) {
// If argument contains spaces or quotes, wrap in double quotes and escape double quotes // 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) { function buildCommand(command, args) {
const escapedArgs = args.map(escapeArg); const escapedArgs = args.map(escapeArg);
return `${command} ${escapedArgs.join(" ")}`; return `${command} ${escapedArgs.join(" ")}`;
} }
export function safeSpawnSync(command, args, options = {}) { function resolveCommandPath(command) {
const fullCommand = buildCommand(command, args); // command will be "npm", "yarn", etc.
return spawnSync(fullCommand, { ...options, shell: true }); // 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 = {}) { export async function safeSpawn(command, args, options = {}) {
const fullCommand = buildCommand(command, args);
return new Promise((resolve, reject) => { 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 // When stdio is piped, we need to collect the output
let stdout = ""; let stdout = "";