72 lines
1.8 KiB
TypeScript
72 lines
1.8 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
|
|
interface ShellOptions {
|
|
cwd?: string;
|
|
encoding?:
|
|
| "utf-8"
|
|
| "utf8"
|
|
| "ascii"
|
|
| "base64"
|
|
| "base64url"
|
|
| "hex"
|
|
| "latin1"
|
|
| "ucs-2"
|
|
| "ucs2"
|
|
| "utf16le";
|
|
log?: boolean;
|
|
onError?: (result: { status: number; stdout: string; stderr: string }) => void;
|
|
}
|
|
|
|
/**
|
|
* Execute a shell command safely using spawnSync with argument arrays.
|
|
* Prevents shell injection by avoiding string interpolation in shell commands.
|
|
*
|
|
* @param cmd - The command to execute
|
|
* @param args - Array of arguments to pass to the command
|
|
* @param options - Optional configuration (cwd, encoding, onError)
|
|
* @returns The trimmed stdout output
|
|
* @throws Error if command fails and no onError handler is provided
|
|
*/
|
|
export function $(cmd: string, args: string[], options?: ShellOptions): string {
|
|
const encoding = options?.encoding ?? "utf-8";
|
|
const result = spawnSync(cmd, args, {
|
|
stdio: ["inherit", "pipe", "pipe"],
|
|
encoding,
|
|
cwd: options?.cwd,
|
|
});
|
|
|
|
const stdout = result.stdout ?? "";
|
|
const stderr = result.stderr ?? "";
|
|
|
|
// Write output to process streams so it behaves like stdio: "inherit"
|
|
// Only log if log option is not explicitly set to false
|
|
if (options?.log !== false) {
|
|
if (stdout) {
|
|
process.stdout.write(stdout);
|
|
}
|
|
if (stderr) {
|
|
process.stderr.write(stderr);
|
|
}
|
|
}
|
|
|
|
// Handle errors
|
|
if (result.status !== 0) {
|
|
const errorResult = {
|
|
status: result.status ?? -1,
|
|
stdout,
|
|
stderr,
|
|
};
|
|
|
|
if (options?.onError) {
|
|
options.onError(errorResult);
|
|
return stdout.trim();
|
|
}
|
|
|
|
throw new Error(
|
|
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
);
|
|
}
|
|
|
|
return stdout.trim();
|
|
}
|