/* eslint-disable max-lines -- Why: this module owns the daemon-side shell wrapper generation for zsh, bash, and PowerShell plus the launch-config plumbing; keeping them together lets the wrapper/marker contract be reviewed as a unit (mirrors src/main/providers/local-pty-shell-ready.ts). */ import { tmpdir } from 'os' import { basename, dirname, join, win32 as pathWin32 } from 'path' import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'fs' import { encodePowerShellCommand, getPowerShellOsc133Bootstrap, isPowerShellExecutableName } from '../powershell-osc133-bootstrap' import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper' import { getZshEnvTemplate, getZshFinalZdotdirRestoreBlock, getZshShellReadyMarkerRegistrationBlock, getZshStartupFileSourceBlock } from '../shell-templates' const ORCA_USER_DATA_PATH_ENV = 'ORCA_USER_DATA_PATH' const SHELL_READY_MARKER = '\\033]777;orca-shell-ready\\007' let didEnsureShellReadyWrappers = false function getShellReadyWrapperRoot(): string { const userDataPath = process.env[ORCA_USER_DATA_PATH_ENV] // Why: older/test launchers may not seed ORCA_USER_DATA_PATH. Keep a // fallback so daemon startup does not fail before the parent can be fixed. return join(userDataPath || tmpdir(), userDataPath ? 'shell-ready' : 'orca-shell-ready') } // Why: if our own process inherited ZDOTDIR from a parent shell that was // itself an Orca PTY (e.g. the user launched Orca from a terminal inside a // running Orca), that ZDOTDIR points at an Orca shell-ready wrapper dir. // Propagating it as the new PTY's ORCA_ORIG_ZDOTDIR makes the wrapper's // `source "$ORCA_ORIG_ZDOTDIR/.zshenv"` line source itself recursively — // zsh gives "job table full or recursion limit exceeded" and the shell // never reaches a usable prompt. // // Any path component ending in `/shell-ready/zsh` is an Orca wrapper dir // (regardless of whether it came from this daemon's userData, a packaged // Orca, or a different dev build). Treat it as if ZDOTDIR were unset so the // caller falls back to HOME for the user's real config root. function normalizeOriginalZdotdirCandidate(value: string | undefined): string | null { if (!value) { return null } // Why: tolerate trailing slashes — some shell startup scripts export // `ZDOTDIR="$dir/"`, and without normalization the suffix check would // miss the self-loop path and restore the recursion bug. Also collapses // a pathological `ZDOTDIR=/` to empty so we fall back to HOME rather than // sourcing `/.zshenv` (which is never the user's real config). const normalized = value.replace(/\/+$/, '') if (!normalized || normalized.endsWith('/shell-ready/zsh')) { return null } return value } function resolveOriginalZdotdir(): string { return ( normalizeOriginalZdotdirCandidate(process.env.ZDOTDIR) || normalizeOriginalZdotdirCandidate(process.env.ORCA_ORIG_ZDOTDIR) || process.env.HOME || '' ) } function resolveOriginalZshenvSourceDir(): string { return normalizeOriginalZdotdirCandidate(process.env.ZDOTDIR) || process.env.HOME || '' } function getRequiredShellReadyWrapperPaths(root = getShellReadyWrapperRoot()): string[] { return [ join(root, 'zsh', '.zshenv'), join(root, 'zsh', '.zprofile'), join(root, 'zsh', '.zshrc'), join(root, 'zsh', '.zlogin'), join(root, 'bash', 'rcfile') ] } function shellReadyWrappersExist(): boolean { return getRequiredShellReadyWrapperPaths().every((path) => existsSync(path)) } export function getDaemonBashShellReadyRcfileContent(): string { return `# Orca daemon bash shell-ready wrapper [[ -f /etc/profile ]] && source /etc/profile if [[ -f "$HOME/.bash_profile" ]]; then source "$HOME/.bash_profile" elif [[ -f "$HOME/.bash_login" ]]; then source "$HOME/.bash_login" elif [[ -f "$HOME/.profile" ]]; then source "$HOME/.profile" fi __orca_restore_attribution_path() { [[ -n "\${ORCA_ATTRIBUTION_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_ATTRIBUTION_SHIM_DIR}"|"\${ORCA_ATTRIBUTION_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_ATTRIBUTION_SHIM_DIR}:$PATH" } __orca_restore_attribution_path __orca_restore_agent_teams_path() { [[ -n "\${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_AGENT_TEAMS_SHIM_DIR}"|"\${ORCA_AGENT_TEAMS_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_AGENT_TEAMS_SHIM_DIR}:$PATH" } __orca_restore_agent_teams_path # Why: user startup files may set the default OpenCode config after Orca's # spawn env; restore the Orca-managed config dir before the first prompt. [[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" # Why: bare shells carry both Pi and OMP shadows so a later typed OMP can # switch on demand. Keep Pi as the shell default unless this PTY is OMP-only. [[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}" if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}" fi ${getPosixOmpShellWrapper()} # Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts. [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" # Why: emit OSC 133 C/D so terminal-command-lifecycle can drop stale agent # status when the foreground command exits — mirrors the zsh daemon wrapper. # Without this, bash users (default on most Linux distros) keep a stuck # 'working' spinner after the CLI exits without a Stop/SessionEnd hook. __orca_osc133_precmd() { local exit_code=$? __orca_in_prompt_command=1 if [[ -n "\${__orca_in_command:-}" ]]; then printf "\\033]133;D;%s\\007" "$exit_code" unset __orca_in_command fi printf "\\033]133;A\\007" } __orca_osc133_prompt_done() { unset __orca_in_prompt_command } __orca_run_user_debug_trap() { if [[ -n "\${__orca_user_debug_trap:-}" ]]; then eval "$__orca_user_debug_trap" || true fi } __orca_osc133_preexec() { __orca_run_user_debug_trap [[ -z "\${__orca_in_prompt_command:-}" ]] || return # Why: bash DEBUG fires for every simple command, including PROMPT_COMMAND # bodies. Skip our own prompt-time helpers so they don't mark the shell as # "in command" before the prompt has even drawn. case "$BASH_COMMAND" in *__orca_osc133_precmd*|*__orca_osc133_prompt_done*|*__orca_prompt_mark*) return ;; esac printf "\\033]133;C\\007" __orca_in_command=1 } # Why: prepend so we capture $? before the user's PROMPT_COMMAND chain mutates it. __orca_normalize_prompt_command() { local __orca_joined="" __orca_prompt_part if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then for __orca_prompt_part in "\${PROMPT_COMMAND[@]}"; do [[ -n "$__orca_prompt_part" ]] || continue if [[ -n "$__orca_joined" ]]; then __orca_joined="$__orca_joined;$__orca_prompt_part" else __orca_joined="$__orca_prompt_part" fi done PROMPT_COMMAND="$__orca_joined" fi } __orca_prepend_prompt_command() { __orca_normalize_prompt_command PROMPT_COMMAND="__orca_osc133_precmd\${PROMPT_COMMAND:+;\${PROMPT_COMMAND}}" } __orca_append_prompt_command() { local command="$1" __orca_normalize_prompt_command if [[ -n "\${PROMPT_COMMAND:-}" ]]; then PROMPT_COMMAND="\${PROMPT_COMMAND};$command" else PROMPT_COMMAND="$command" fi } __orca_prepend_prompt_command if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then __orca_prompt_mark() { printf "${SHELL_READY_MARKER}" } __orca_append_prompt_command "__orca_prompt_mark" fi __orca_append_prompt_command "__orca_osc133_prompt_done" __orca_debug_trap_spec="$(trap -p DEBUG)" if [[ -n "$__orca_debug_trap_spec" ]]; then __orca_debug_trap_command="\${__orca_debug_trap_spec#trap -- }" __orca_debug_trap_command="\${__orca_debug_trap_command% DEBUG}" eval "__orca_user_debug_trap=$__orca_debug_trap_command" fi unset __orca_debug_trap_spec __orca_debug_trap_command unset -f __orca_normalize_prompt_command __orca_prepend_prompt_command __orca_append_prompt_command # Why: arm DEBUG after wrapper setup; otherwise bash treats our own rcfile # commands as a foreground command and emits a fake C/D before the first prompt. trap '__orca_osc133_preexec' DEBUG ` } export function getDaemonZshShellReadyRcfileContent(): string { return `# Orca daemon zsh shell-ready wrapper ${getZshStartupFileSourceBlock({ fileName: '.zshrc', interactiveOnly: true, skipWhenHomeIsCurrentZdotdir: true })} __orca_restore_attribution_path() { [[ -n "\${ORCA_ATTRIBUTION_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_ATTRIBUTION_SHIM_DIR}"|"\${ORCA_ATTRIBUTION_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_ATTRIBUTION_SHIM_DIR}:$PATH" } [[ ! -o login ]] && __orca_restore_attribution_path __orca_restore_agent_teams_path() { [[ -n "\${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_AGENT_TEAMS_SHIM_DIR}"|"\${ORCA_AGENT_TEAMS_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_AGENT_TEAMS_SHIM_DIR}:$PATH" } [[ ! -o login ]] && __orca_restore_agent_teams_path if [[ ! -o login ]]; then # Why: ~/.zshrc can export the user's default OpenCode config after spawn. [[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" # Why: bare shells carry both Pi and OMP shadows; keep Pi as the default and # let the OMP wrapper switch to OMP only for that command. [[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}" if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}" fi ${getPosixOmpShellWrapper()} [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" fi __orca_osc133_precmd() { local exit_code=$? if [[ -n "\${__orca_in_command:-}" ]]; then printf "\\033]133;D;%s\\007" "$exit_code" unset __orca_in_command fi printf "\\033]133;A\\007" } __orca_osc133_preexec() { printf "\\033]133;C\\007" __orca_in_command=1 } # Why: prepend so Orca captures $? before user prompt hooks can overwrite it. precmd_functions=(__orca_osc133_precmd \${precmd_functions[@]}) preexec_functions=(__orca_osc133_preexec \${preexec_functions[@]}) if [[ ! -o login ]]; then ${getZshFinalZdotdirRestoreBlock()} fi ` } function ensureShellReadyWrappers(): void { if (process.platform === 'win32') { return } if (didEnsureShellReadyWrappers && shellReadyWrappersExist()) { return } didEnsureShellReadyWrappers = true const root = getShellReadyWrapperRoot() const zshDir = join(root, 'zsh') const bashDir = join(root, 'bash') const zshEnv = getZshEnvTemplate(zshDir, 'daemon') const zshProfile = `# Orca daemon zsh shell-ready wrapper ${getZshStartupFileSourceBlock({ fileName: '.zprofile' })} ` const zshRc = getDaemonZshShellReadyRcfileContent() const zshLogin = `# Orca daemon zsh shell-ready wrapper ${getZshStartupFileSourceBlock({ fileName: '.zlogin', interactiveOnly: true })} __orca_restore_attribution_path() { [[ -n "\${ORCA_ATTRIBUTION_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_ATTRIBUTION_SHIM_DIR}"|"\${ORCA_ATTRIBUTION_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_ATTRIBUTION_SHIM_DIR}:$PATH" } __orca_restore_attribution_path __orca_restore_agent_teams_path() { [[ -n "\${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0 case "$PATH" in "\${ORCA_AGENT_TEAMS_SHIM_DIR}"|"\${ORCA_AGENT_TEAMS_SHIM_DIR}:"*) return 0 ;; esac export PATH="\${ORCA_AGENT_TEAMS_SHIM_DIR}:$PATH" } __orca_restore_agent_teams_path # Why: .zlogin is the final login startup file before the prompt is shown. [[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" [[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}" if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}" fi ${getPosixOmpShellWrapper()} [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" ${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER)} ${getZshFinalZdotdirRestoreBlock()} ` const bashRc = getDaemonBashShellReadyRcfileContent() const files = [ [join(zshDir, '.zshenv'), zshEnv], [join(zshDir, '.zprofile'), zshProfile], [join(zshDir, '.zshrc'), zshRc], [join(zshDir, '.zlogin'), zshLogin], [join(bashDir, 'rcfile'), bashRc] ] as const try { for (const [path, content] of files) { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content, 'utf8') chmodSync(path, 0o644) } } catch (error) { // Why: wrapper file creation can fail due to read-only filesystems, permission // issues, or disk space. Rather than crashing, log the error and continue. // The shell will launch without the wrapper, which means no shell-ready marker // but at least the PTY is usable. const errorMessage = error instanceof Error ? `${error.message} (${(error as NodeJS.ErrnoException).code || 'unknown'})` : String(error) console.error(`[daemon/shell-ready] Failed to create wrapper files in ${root}: ${errorMessage}`) console.error('[daemon/shell-ready] Shell will launch without wrapper (no shell-ready marker)') // Reset the flag so next attempt will try again didEnsureShellReadyWrappers = false } } export function resolvePtyShellPath(env: Record): string { if (process.platform === 'win32') { return env.ORCA_TERMINAL_WINDOWS_SHELL || 'powershell.exe' } return env.SHELL || process.env.SHELL || '/bin/zsh' } export function supportsPtyStartupBarrier(env: Record): boolean { if (process.platform === 'win32') { return false } const resolvedShell = resolvePtyShellPath(env) const shellName = pathWin32.basename(basename(resolvedShell)).toLowerCase() return shellName === 'zsh' || shellName === 'bash' } type ShellLaunchConfig = { args: string[] | null env: Record supportsReadyMarker: boolean } function getWrappedShellLaunchConfig( shellPath: string, options: { emitReadyMarker: boolean } ): ShellLaunchConfig { const shellName = pathWin32.basename(basename(shellPath)).toLowerCase() if (shellName === 'zsh') { ensureShellReadyWrappers() const root = getShellReadyWrapperRoot() return { args: ['-l'], env: { ORCA_ORIG_ZDOTDIR: resolveOriginalZdotdir(), ORCA_ZSHENV_SOURCE_DIR: resolveOriginalZshenvSourceDir(), ZDOTDIR: join(root, 'zsh'), ORCA_SHELL_READY_MARKER: options.emitReadyMarker ? '1' : '0' }, supportsReadyMarker: options.emitReadyMarker } } if (shellName === 'bash') { ensureShellReadyWrappers() const root = getShellReadyWrapperRoot() return { args: ['--rcfile', join(root, 'bash', 'rcfile')], env: { ORCA_SHELL_READY_MARKER: options.emitReadyMarker ? '1' : '0' }, supportsReadyMarker: options.emitReadyMarker } } if (isPowerShellExecutableName(shellName)) { return { args: [ '-NoLogo', '-NoExit', '-EncodedCommand', encodePowerShellCommand(getPowerShellOsc133Bootstrap()) ], env: {}, supportsReadyMarker: false } } return { args: null, env: {}, supportsReadyMarker: false } } export function getShellReadyLaunchConfig(shellPath: string): ShellLaunchConfig { return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: true }) } export function getAttributionShellLaunchConfig(shellPath: string): ShellLaunchConfig { return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: false }) }