0dab3116b7
- shell-env: detect Git Bash via MSYSTEM env var when SHELL is unset (#3366) On some Git Bash installations SHELL is not set but MSYSTEM (MINGW64/MSYS) is always present. Check MSYSTEM before PSModulePath to avoid emitting PowerShell syntax in bash shells. - session-state: resolve legacy agent names in resolveRegisteredAgentName (#3272) Historical sessions stored agent names like 'Sisyphus (Ultraworker)' which don't match the current registered format. Fall back to getAgentConfigKey for legacy/parenthesized name resolution before returning the raw name. - config-migration: skip backup when file content is unchanged (#3222) Compare serialized config with existing file content before creating a timestamped .bak file. Only create backup when the on-disk content actually differs from the migrated content.
176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
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<string, string>,
|
|
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
|
|
}
|