"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectPlatform = detectPlatform;
exports.resolvePwshPath = resolvePwshPath;
exports.resolveCwd = resolveCwd;
exports.execCommand = execCommand;
exports.runProcess = runProcess;
exports.execCommandStream = execCommandStream;
exports.withRetry = withRetry;
exports.normalizeLineEndings = normalizeLineEndings;
exports.formatBytes = formatBytes;
const child_process_1 = require("child_process");
const config_1 = require("./config");
const securityEnhanced_1 = require("./securityEnhanced");
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
// ── Constants ──
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
const MAX_RETRIES = 2;
const RETRY_DELAY_MS = 500;
const RETRYABLE_CODES = ["ETIMEDOUT", "ECONNRESET", "EPIPE"];
const NON_RETRYABLE_CODES = ["ENOENT", "EACCES", "EEXIST", "EPERM", "EISDIR", "ELOOP", "E2BIG", "EINVAL", "EILSEQ"];
// ── Platform Detection ──
function detectPlatform() {
const p = os.platform();
return p === "win32" ? "windows" : p === "darwin" ? "macos" : "linux";
}
function resolvePwshPath() {
const platform = detectPlatform();
if (platform === "windows") {
const candidates = [
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe",
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
];
for (const c of candidates) {
try {
const resolved = path.resolve((0, securityEnhanced_1.expandPath)(c));
if (fs.existsSync(resolved))
return resolved;
}
catch { /* skip */ }
}
return "powershell";
}
const candidates = ["/usr/bin/pwsh", "/usr/bin/powershell", "/opt/microsoft/powershell/7/pwsh"];
for (const c of candidates) {
try {
const resolved = path.resolve((0, securityEnhanced_1.expandPath)(c));
fs.accessSync(resolved, fs.constants.X_OK);
return resolved;
}
catch { /* skip */ }
}
return "pwsh";
}
// ── Shell Resolution ──
function resolveShell(shellPath, windowsShell) {
const platform = detectPlatform();
if (shellPath) {
const expanded = (0, securityEnhanced_1.expandPath)(shellPath);
return { name: path.basename(expanded), path: expanded, platform };
}
if (platform === "windows") {
const shell = windowsShell || process.env.WINDOWS_SHELL || "powershell";
const resolved = shell === "cmd" ? "cmd.exe" : resolvePwshPath();
return { name: path.basename(resolved), path: resolved, platform };
}
return { name: "bash", path: "/bin/bash", platform };
}
// ── Working Directory Resolution ──
function resolveCwd(cwd) {
if (!cwd)
return process.cwd();
const expanded = (0, securityEnhanced_1.expandPath)(cwd);
const resolved = path.resolve(expanded);
try {
if (!fs.statSync(resolved).isDirectory())
throw new Error();
return resolved;
}
catch {
throw new Error(`Invalid working directory: ${resolved}`);
}
}
// ── Result Builders ──
function createExecResult(stdout, stderr, exitCode, timedOut, shell, platform, command, durationMs) {
return { stdout, stderr, exitCode, timedOut, shell, platform, command, durationMs };
}
function createRunResult(stdout, stderr, exitCode, timedOut, durationMs, signal) {
return { stdout, stderr, exitCode, timedOut, durationMs, signal, platform: detectPlatform() };
}
// ── Buffer Collector — DRY helper for stdout/stderr accumulation ──
function createBufferCollector(maxBytes) {
let stdoutBuf = Buffer.alloc(0);
let stderrBuf = Buffer.alloc(0);
const append = (current, data) => {
if (current.length >= maxBytes)
return current;
const remaining = maxBytes - current.length;
return Buffer.concat([current, data.subarray(0, Math.max(0, remaining))]);
};
return {
onStdoutData: (d) => {
stdoutBuf = append(stdoutBuf, d);
},
onStderrData: (d) => {
stderrBuf = append(stderrBuf, d);
},
getStdout: () => stdoutBuf.toString("utf8"),
getStderr: () => stderrBuf.toString("utf8"),
};
}
/**
* Detect if a command uses cmd.exe-style syntax that PowerShell's -Command
* would misinterpret or fail to execute correctly.
*/
function isCmdStyleCommand(command) {
const trimmed = command.trim();
// Known cmd.exe builtins
const cmdBuiltins = /^(dir|copy|xcopy|del|erase|ren|rename|move|cls|type|more|find|findstr|assoc|ftype|fc|format|tree|chkdsk|diskpart|attrib|subst|path|set|if|for|call|shift|goto|start|pause|break|ver|vol|title|prompt|color|help|exit|rem|echo|pushd|popd|cd|chdir|md|mkdir|rd|rmdir|sort|comp|compact|cipher|cacls|icacls|takeown|robocopy|where|endlocal|defined|exist)\s/i;
if (cmdBuiltins.test(trimmed))
return true;
// Bare / flags (cmd switches, not PowerShell --style)
if (/\b\/[a-zA-Z]+\b/.test(trimmed))
return true;
// cmd-style chaining: ||, &&, | (pipe), &
if (/\|\||&&|\|[^|]|&[^&]/.test(trimmed))
return true;
// cmd-style redirection: >, >>, <
if (/>[^>]|<[^>]/.test(trimmed))
return true;
return false;
}
/**
* Detect if a command starts with an absolute file path (executable path).
* PowerShell's -Command parser mangles paths with spaces and special chars,
* so we route these to cmd /c for reliable execution.
*/
function isAbsolutePathCommand(command) {
const trimmed = command.trim();
if (/^[a-zA-Z]:[\\/]/.test(trimmed))
return true;
if (/^\//.test(trimmed))
return true;
return false;
}
function buildExecInvocation(command, options, shellInfo) {
if (options.args && options.args.length > 0) {
return { command, args: options.args };
}
if (shellInfo.platform === "windows") {
const forceCmd = options.windowsShell === "cmd" || isCmdStyleCommand(command) || isAbsolutePathCommand(command);
if (forceCmd) {
return { command: "cmd.exe", args: ["/d", "/s", "/c", command] };
}
return { command: shellInfo.path, args: ["-NoLogo", "-NoProfile", "-Command", command] };
}
return { command: shellInfo.path, args: ["-lc", command] };
}
// ── Exec Command (with shell, retry-capable) ──
async function execCommand(command, options = {}) {
const config = loadConfig();
const validation = (0, securityEnhanced_1.validateExecutionParams)(options);
if (!validation.valid)
throw new Error(`Invalid execution params: ${validation.error}`);
const validated = validation.validated;
const sanitized = (0, securityEnhanced_1.sanitizeCommand)(command);
if (!sanitized.valid) {
throw new Error(`Command rejected: ${sanitized.error}`);
}
const shellInfo = resolveShell(options.shellPath, options.windowsShell);
const cwd = resolveCwd(validated.cwd);
const timeoutMs = validated.timeoutMs ?? config.defaultTimeoutMs;
const maxOutput = validated.maxOutputBytes ?? config.maxOutputBytes;
let lastError;
const startTime = Date.now();
const maxRetries = options.retryCount ?? config.maxRetries ?? MAX_RETRIES;
const runAttempt = () => {
return new Promise((resolve, reject) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
let timedOut = false;
controller.signal.addEventListener("abort", () => { timedOut = true; }, { once: true });
const spawnOpts = {
cwd,
shell: false,
env: validated.env,
stdio: ["pipe", "pipe", "pipe"],
signal: controller.signal,
};
const invocation = buildExecInvocation(command, options, shellInfo);
const child = (0, child_process_1.spawn)(invocation.command, invocation.args, spawnOpts);
const collector = createBufferCollector(maxOutput);
child.stdout?.on("data", collector.onStdoutData);
child.stderr?.on("data", collector.onStderrData);
child.on("error", (err) => {
clearTimeout(timeoutId);
reject(err);
});
let stdoutEnded = false;
let stderrEnded = false;
let exitCode = null;
const tryResolve = () => {
if (stdoutEnded && stderrEnded) {
clearTimeout(timeoutId);
resolve(createExecResult(collector.getStdout(), collector.getStderr(), exitCode, timedOut, shellInfo.name, shellInfo.platform, command, Date.now() - startTime));
}
};
child.stdout?.on("end", () => { stdoutEnded = true; tryResolve(); });
child.stderr?.on("end", () => { stderrEnded = true; tryResolve(); });
child.on("close", (code) => {
exitCode = code;
});
if (validated.stdin) {
child.stdin?.write(validated.stdin);
child.stdin?.end();
}
});
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (attempt > 0) {
await sleep(options.retryDelayMs ?? config.retryDelayMs ?? RETRY_DELAY_MS);
}
try {
return await runAttempt();
}
catch (err) {
lastError = err;
const code = err.code;
if (NON_RETRYABLE_CODES.includes(code || ""))
throw err;
if (!RETRYABLE_CODES.includes(code || ""))
throw err;
}
}
throw lastError ?? new Error("execCommand failed with unknown error");
}
// ── Run Process (direct executable, no shell) ──
async function runProcess(command, args = [], options = {}) {
const config = loadConfig();
const validation = (0, securityEnhanced_1.validateExecutionParams)(options);
if (!validation.valid)
throw new Error(`Invalid execution params: ${validation.error}`);
const validated = validation.validated;
const cwd = resolveCwd(validated.cwd);
const timeoutMs = validated.timeoutMs ?? config.defaultTimeoutMs;
const maxOutput = validated.maxOutputBytes ?? config.maxOutputBytes;
const startTime = Date.now();
const controller = new AbortController();
let timedOut = false;
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
const spawnOpts = {
cwd,
shell: false,
env: validated.env,
stdio: ["pipe", "pipe", "pipe"],
signal: controller.signal,
};
const child = (0, child_process_1.spawn)(command, args, spawnOpts);
const collector = createBufferCollector(maxOutput);
child.stdout?.on("data", collector.onStdoutData);
child.stderr?.on("data", collector.onStderrData);
child.on("error", (err) => {
clearTimeout(timeoutId);
reject(err);
});
let stdoutEnded = false;
let stderrEnded = false;
let exitCode = null;
let signal = null;
const tryResolve = () => {
if (stdoutEnded && stderrEnded) {
clearTimeout(timeoutId);
const normalizedExitCode = exitCode !== null ? exitCode : (signal !== null ? -1 : 0);
resolve(createRunResult(collector.getStdout(), collector.getStderr(), normalizedExitCode, timedOut, Date.now() - startTime, signal ?? undefined));
}
};
child.stdout?.on("end", () => { stdoutEnded = true; tryResolve(); });
child.stderr?.on("end", () => { stderrEnded = true; tryResolve(); });
child.on("close", (code, sig) => {
exitCode = code;
signal = sig;
});
if (validated.stdin) {
child.stdin?.write(validated.stdin);
child.stdin?.end();
}
});
}
// ── Exec Command Stream ──
async function execCommandStream(command, options = {}) {
const config = loadConfig();
const validation = (0, securityEnhanced_1.validateExecutionParams)(options);
if (!validation.valid)
throw new Error(`Invalid execution params: ${validation.error}`);
const validated = validation.validated;
const shellInfo = resolveShell(options.shellPath, options.windowsShell);
const cwd = resolveCwd(validated.cwd);
const maxOutput = validated.maxOutputBytes ?? config.maxOutputBytes;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const invocation = buildExecInvocation(command, options, shellInfo);
const spawnOpts = {
cwd,
shell: false,
env: validated.env,
stdio: ["pipe", "pipe", "pipe"],
};
const child = (0, child_process_1.spawn)(invocation.command, invocation.args, spawnOpts);
const collector = createBufferCollector(maxOutput);
child.stdout?.on("data", collector.onStdoutData);
child.stderr?.on("data", collector.onStderrData);
child.on("error", (err) => reject(err));
let stdoutEnded = false;
let stderrEnded = false;
let exitCode = null;
const tryResolve = () => {
if (stdoutEnded && stderrEnded) {
resolve(createExecResult(collector.getStdout(), collector.getStderr(), exitCode, false, shellInfo.name, shellInfo.platform, command, Date.now() - startTime));
}
};
child.stdout?.on("end", () => { stdoutEnded = true; tryResolve(); });
child.stderr?.on("end", () => { stderrEnded = true; tryResolve(); });
child.on("close", (code) => {
exitCode = code;
});
});
}
// ── Retry Helper ──
async function withRetry(fn, options = {}) {
const maxRetries = options.maxRetries ?? MAX_RETRIES;
const retryDelay = options.retryDelayMs ?? RETRY_DELAY_MS;
const retryOnCodes = options.retryOnCodes ?? RETRYABLE_CODES;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
}
catch (err) {
lastError = err;
const code = err.code;
if (!code || !retryOnCodes.includes(code))
throw err;
if (attempt < maxRetries)
await sleep(retryDelay);
}
}
throw lastError;
}
// ── Helpers ──
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let _cachedConfig = null;
function loadConfig() {
if (!_cachedConfig) {
const configPath = (0, config_1.resolveConfigPath)();
try {
const raw = fs.readFileSync(configPath, "utf8");
_cachedConfig = { ...config_1.DEFAULT_CONFIG, ...JSON.parse(raw) };
}
catch {
_cachedConfig = config_1.DEFAULT_CONFIG;
}
}
return _cachedConfig;
}
// ── Normalize Line Endings ──
function normalizeLineEndings(input) {
return input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
// ── Format Bytes ──
function formatBytes(bytes) {
if (bytes === 0)
return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
}
//# sourceMappingURL=executor.js.map