export type ShellType = "unix" | "powershell" | "cmd" | "csh" /** * Detect the current shell type based on environment variables. * * Detection priority: * 1. SHELL env var → Unix shell (explicit user choice takes precedence) * 2. PSModulePath → PowerShell * 3. Platform fallback → win32: cmd, others: unix * * Note: SHELL is checked before PSModulePath because on Windows, PSModulePath * is always set by the system even when the active shell is Git Bash or WSL. * An explicit SHELL variable indicates the user's chosen shell overrides that. */ export function detectShellType(): ShellType { if (process.env.SHELL) { const shell = process.env.SHELL if (shell.includes("csh") || shell.includes("tcsh")) { return "csh" } return "unix" } // Git Bash on Windows sets MSYSTEM (e.g. "MINGW64", "MINGW32", "MSYS") // even when SHELL is not set. Detect this before PSModulePath which is // always present on Windows regardless of the active shell. if (process.env.MSYSTEM) { return "unix" } if (process.env.PSModulePath) { return "powershell" } return process.platform === "win32" ? "cmd" : "unix" } /** * Shell-escape a value for use in environment variable assignment. * * @param value - The value to escape * @param shellType - The target shell type * @returns Escaped value appropriate for the shell */ export function shellEscape(value: string, shellType: ShellType): string { if (value === "") { return shellType === "cmd" ? '""' : "''" } switch (shellType) { case "unix": case "csh": if (/[^a-zA-Z0-9_\-.:\/]/.test(value)) { return `'${value.replace(/'/g, "'\\''")}'` } return value case "powershell": return `'${value.replace(/'/g, "''")}'` case "cmd": // Escape % first (for environment variable expansion), then " (for quoting) return `"${value.replace(/%/g, '%%').replace(/"/g, '""')}"` default: return value } } /** * Build environment variable prefix command for the target shell. * * @param env - Record of environment variables to set * @param shellType - The target shell type * @returns Command prefix string to prepend to the actual command * * @example * ```ts * // Unix: "export VAR1=val1 VAR2=val2; command" * buildEnvPrefix({ VAR1: "val1", VAR2: "val2" }, "unix") * // => "export VAR1=val1 VAR2=val2;" * * // PowerShell: "$env:VAR1='val1'; $env:VAR2='val2'; command" * buildEnvPrefix({ VAR1: "val1", VAR2: "val2" }, "powershell") * // => "$env:VAR1='val1'; $env:VAR2='val2';" * * // cmd.exe: "set VAR1=val1 && set VAR2=val2 && command" * buildEnvPrefix({ VAR1: "val1", VAR2: "val2" }, "cmd") * // => "set VAR1=\"val1\" && set VAR2=\"val2\" &&" * ``` */ export function buildEnvPrefix( env: Record, shellType: ShellType ): string { const entries = Object.entries(env) if (entries.length === 0) { return "" } switch (shellType) { case "unix": { const assignments = entries .map(([key, value]) => `${key}=${shellEscape(value, shellType)}`) .join(" ") return `export ${assignments};` } case "csh": { const assignments = entries .map(([key, value]) => `setenv ${key} ${shellEscape(value, shellType)}`) .join("; ") return `${assignments};` } case "powershell": { const assignments = entries .map(([key, value]) => `$env:${key}=${shellEscape(value, shellType)}`) .join("; ") return `${assignments};` } case "cmd": { const assignments = entries .map(([key, value]) => `set ${key}=${shellEscape(value, shellType)}`) .join(" && ") return `${assignments} &&` } default: return "" } } /** * Escape a value for use in a double-quoted shell -c command argument. * * In shell -c "..." strings, these characters have special meaning and must be escaped: * - $ - variable expansion, command substitution $(...) * - ` - command substitution `...` * - \\ - escape character * - " - end quote * - ; | & - command separators * - # - comment * - () - grouping operators * * @param value - The value to escape * @returns Escaped value safe for double-quoted shell -c argument * * @example * ```ts * // For malicious input * const url = "http://localhost:3000'; cat /etc/passwd; echo '" * const escaped = shellEscapeForDoubleQuotedCommand(url) * // => "http://localhost:3000'\''; cat /etc/passwd; echo '" * * // Usage in command: * const cmd = `/bin/sh -c "opencode attach ${escaped} --session ${sessionId}"` * ``` */ export function shellEscapeForDoubleQuotedCommand(value: string): string { // Order matters: escape backslash FIRST, then other characters return value .replace(/\\/g, "\\\\") // escape backslash first .replace(/\$/g, "\\$") // escape dollar sign .replace(/`/g, "\\`") // escape backticks .replace(/"/g, "\\\"") // escape double quotes .replace(/;/g, "\\;") // escape semicolon (command separator) .replace(/\|/g, "\\|") // escape pipe (command separator) .replace(/&/g, "\\&") // escape ampersand (command separator) .replace(/#/g, "\\#") // escape hash (comment) .replace(/\(/g, "\\(") // escape parentheses .replace(/\)/g, "\\)") // escape parentheses }