diff --git a/src/main/daemon/daemon-zsh-shell-ready-rcfile.ts b/src/main/daemon/daemon-zsh-shell-ready-rcfile.ts deleted file mode 100644 index 1b436bb1ea5..00000000000 --- a/src/main/daemon/daemon-zsh-shell-ready-rcfile.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper' -import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight' -import { - getZshFinalZdotdirRestoreBlock, - getZshStartupFileSourceBlock, - ZSH_HISTFILE_RESTORE_BLOCK -} from '../shell-templates' - -export function getDaemonZshShellReadyRcfileContent(): string { - return `# Orca daemon zsh shell-ready wrapper -${getZshStartupFileSourceBlock({ - fileName: '.zshrc', - interactiveOnly: true, - skipWhenHomeIsCurrentZdotdir: true -})} -__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}" - [[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" - ${getPosixOmpShellWrapper()} - [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" -${ZSH_HISTFILE_RESTORE_BLOCK} - ${getPosixCodexShellLaunchPreflight()} -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 -` -} diff --git a/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts b/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts new file mode 100644 index 00000000000..73f6dc21595 --- /dev/null +++ b/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts @@ -0,0 +1,25 @@ +import type { ZshStartupWrapperSpec } from '../zsh-startup-wrapper-builder' +import { SHELL_READY_MARKER } from './daemon-shell-ready-marker' + +/** The zsh wrapper the daemon (local fork and SSH host) launches shells with. */ +export function getDaemonZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec { + return { + headerLabel: 'Orca daemon zsh shell-ready wrapper', + zshDir, + zshenvStrategy: 'discover-user-zdotdir', + readyMarkerEscaped: SHELL_READY_MARKER, + osc133CommandMarkers: true, + skipUserZshrcWhenHomeIsWrapperDir: true, + interactiveRestoreComment: + "# Why: ~/.zshrc can export the user's default OpenCode config after spawn.", + loginRestoreComment: + '# Why: .zlogin is the final login startup file before the prompt is shown.', + restores: { + agentTeamsPath: true, + remoteCliBinDir: false, + codexHome: true, + codexLaunchPreflight: true + }, + readyMarkerOrder: 'before-zdotdir-restore' + } +} diff --git a/src/main/daemon/shell-ready.ts b/src/main/daemon/shell-ready.ts index 0f9b2d65885..bbcbdca6856 100644 --- a/src/main/daemon/shell-ready.ts +++ b/src/main/daemon/shell-ready.ts @@ -6,22 +6,12 @@ import { getPowerShellOsc133Bootstrap, isPowerShellExecutableName } from '../powershell-osc133-bootstrap' -import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper' -import { - getFishCodexShellLaunchPreflight, - getPosixCodexShellLaunchPreflight -} from '../pty/codex-shell-launch-preflight' -import { - getFishShellReadyInitCommand, - getZshEnvTemplate, - getZshFinalZdotdirRestoreBlock, - getZshShellReadyMarkerRegistrationBlock, - getZshStartupFileSourceBlock, - ZSH_HISTFILE_RESTORE_BLOCK -} from '../shell-templates' +import { getFishCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight' +import { getFishShellReadyInitCommand } from '../shell-templates' +import { buildZshStartupWrapperFiles } from '../zsh-startup-wrapper-builder' import { SHELL_READY_MARKER } from './daemon-shell-ready-marker' import { getDaemonBashShellReadyRcfileContent } from './daemon-bash-shell-ready-rcfile' -import { getDaemonZshShellReadyRcfileContent } from './daemon-zsh-shell-ready-rcfile' +import { getDaemonZshWrapperSpec } from './daemon-zsh-shell-ready-wrapper-spec' const ORCA_USER_DATA_PATH_ENV = 'ORCA_USER_DATA_PATH' @@ -102,38 +92,14 @@ function ensureShellReadyWrappers(): void { 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_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_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" -${getPosixOmpShellWrapper()} -[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" -${ZSH_HISTFILE_RESTORE_BLOCK} -${getPosixCodexShellLaunchPreflight()} -${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER)} -${getZshFinalZdotdirRestoreBlock()} -` + const zsh = buildZshStartupWrapperFiles(getDaemonZshWrapperSpec(zshDir)) const bashRc = getDaemonBashShellReadyRcfileContent() const files = [ - [join(zshDir, '.zshenv'), zshEnv], - [join(zshDir, '.zprofile'), zshProfile], - [join(zshDir, '.zshrc'), zshRc], - [join(zshDir, '.zlogin'), zshLogin], + [join(zshDir, '.zshenv'), zsh.zshenv], + [join(zshDir, '.zprofile'), zsh.zprofile], + [join(zshDir, '.zshrc'), zsh.zshrc], + [join(zshDir, '.zlogin'), zsh.zlogin], [join(bashDir, 'rcfile'), bashRc] ] as const diff --git a/src/main/providers/local-pty-shell-ready-wrapper-generation.ts b/src/main/providers/local-pty-shell-ready-wrapper-generation.ts index 7c32a283aa2..6dac269b7f4 100644 --- a/src/main/providers/local-pty-shell-ready-wrapper-generation.ts +++ b/src/main/providers/local-pty-shell-ready-wrapper-generation.ts @@ -5,15 +5,10 @@ * readiness scanner watches for before a startup command is written. */ import { mkdirSync, writeFileSync, chmodSync } from 'node:fs' -import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper' -import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight' import { - getZshEnvTemplate, - getZshFinalZdotdirRestoreBlock, - getZshShellReadyMarkerRegistrationBlock, - getZshStartupFileSourceBlock, - ZSH_HISTFILE_RESTORE_BLOCK -} from '../shell-templates' + buildZshStartupWrapperFiles, + type ZshStartupWrapperSpec +} from '../zsh-startup-wrapper-builder' import { getBashShellReadyRcfileContent } from './local-pty-shell-ready-bash-rcfile' import { getShellReadyWrapperRoot, @@ -23,50 +18,36 @@ import { let didEnsureShellReadyWrappers = false +function getLocalZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec { + return { + headerLabel: 'Orca zsh shell-ready wrapper', + zshDir, + zshenvStrategy: 'discover-user-zdotdir', + readyMarkerEscaped: SHELL_READY_MARKER_ESCAPED, + osc133CommandMarkers: true, + skipUserZshrcWhenHomeIsWrapperDir: true, + interactiveRestoreComment: + "# Why: ~/.zshrc can export the user's default OpenCode config after spawn.", + loginRestoreComment: + '# Why: .zlogin is the final login startup file before the prompt is shown.', + restores: { + agentTeamsPath: true, + remoteCliBinDir: false, + codexHome: true, + codexLaunchPreflight: true + }, + readyMarkerOrder: 'before-zdotdir-restore', + legacyFormatting: { + unindentedMimocodeRestore: true, + codexHomeRestoreComment: + "# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files." + } + } +} + export function getZshShellReadyRcfileContent(): string { - return `# Orca zsh shell-ready wrapper -${getZshStartupFileSourceBlock({ - fileName: '.zshrc', - interactiveOnly: true, - skipWhenHomeIsCurrentZdotdir: true -})} -__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}" -[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" - ${getPosixOmpShellWrapper()} - # Why: Codex must keep using Orca's runtime CODEX_HOME after rc files. - [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" -${ZSH_HISTFILE_RESTORE_BLOCK} - ${getPosixCodexShellLaunchPreflight()} -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 -` + return buildZshStartupWrapperFiles(getLocalZshWrapperSpec(`${getShellReadyWrapperRoot()}/zsh`)) + .zshrc } export function ensureShellReadyWrappersAt(root = getShellReadyWrapperRoot()): void { @@ -78,38 +59,14 @@ export function ensureShellReadyWrappersAt(root = getShellReadyWrapperRoot()): v const zshDir = `${root}/zsh` const bashDir = `${root}/bash` - const zshEnv = getZshEnvTemplate(zshDir) - const zshProfile = `# Orca zsh shell-ready wrapper -${getZshStartupFileSourceBlock({ fileName: '.zprofile' })} -` - const zshRc = getZshShellReadyRcfileContent() - const zshLogin = `# Orca zsh shell-ready wrapper -${getZshStartupFileSourceBlock({ fileName: '.zlogin', interactiveOnly: true })} -__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_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" -${getPosixOmpShellWrapper()} -[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" -${ZSH_HISTFILE_RESTORE_BLOCK} -${getPosixCodexShellLaunchPreflight()} -${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)} -${getZshFinalZdotdirRestoreBlock()} -` + const zsh = buildZshStartupWrapperFiles(getLocalZshWrapperSpec(zshDir)) const bashRc = getBashShellReadyRcfileContent() const files = [ - [`${zshDir}/.zshenv`, zshEnv], - [`${zshDir}/.zprofile`, zshProfile], - [`${zshDir}/.zshrc`, zshRc], - [`${zshDir}/.zlogin`, zshLogin], + [`${zshDir}/.zshenv`, zsh.zshenv], + [`${zshDir}/.zprofile`, zsh.zprofile], + [`${zshDir}/.zshrc`, zsh.zshrc], + [`${zshDir}/.zlogin`, zsh.zlogin], [`${bashDir}/rcfile`, bashRc] ] as const diff --git a/src/main/shell-templates.ts b/src/main/shell-templates.ts index 8748303c5c6..f9e5283dea1 100644 --- a/src/main/shell-templates.ts +++ b/src/main/shell-templates.ts @@ -12,11 +12,11 @@ fi` // Why: daemon, local, and relay wrappers must preserve one Bash prompt-hook contract. export { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from './bash-prompt-command-composition' -export function getZshEnvTemplate(zshDir: string, headerPrefix = ''): string { - const header = headerPrefix - ? `Orca ${headerPrefix} zsh shell-ready wrapper` - : 'Orca zsh shell-ready wrapper' - return `# ${header} +export function getZshEnvTemplate( + zshDir: string, + headerLabel = 'Orca zsh shell-ready wrapper' +): string { + return `# ${headerLabel} ${SHELL_STARTUP_IDENTITY_MARKER_BLOCK} # Why: capture the runtime wrapper dir before it is unset below. On WSL this # file is generated with a Windows path but sourced via /mnt/c, so the baked @@ -103,6 +103,30 @@ unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_ ` } +/** + * The relay variant of the .zshenv above: it trusts the ZDOTDIR the remote + * shell already inherited instead of re-deriving one, and republishes it as + * ORCA_USER_ZDOTDIR for the later wrapper files. + * + * Why separate: this diverged from the discovery template before unification + * and is preserved verbatim here — reconciling the two is a follow-up. + */ +export function getZshOverlayEnvTemplate(zshDir: string, headerLabel: string): string { + return `# ${headerLabel} +${SHELL_STARTUP_IDENTITY_MARKER_BLOCK} +export ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${ORCA_ORIG_ZDOTDIR%/}" in + */shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;; +esac +[[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv" +export ORCA_USER_ZDOTDIR="\${ZDOTDIR:-\${ORCA_ORIG_ZDOTDIR:-$HOME}}" +case "\${ORCA_USER_ZDOTDIR%/}" in + */shell-ready/zsh) export ORCA_USER_ZDOTDIR="$HOME" ;; +esac +export ZDOTDIR=${quotePosixSingle(zshDir)} +` +} + /** * Restores the worktree-scoped HISTFILE that macOS `/etc/zshrc` destroys. * diff --git a/src/main/zsh-startup-wrapper-builder.ts b/src/main/zsh-startup-wrapper-builder.ts new file mode 100644 index 00000000000..1d3edfacccf --- /dev/null +++ b/src/main/zsh-startup-wrapper-builder.ts @@ -0,0 +1,200 @@ +/** + * The single source of the zsh startup wrapper files (.zshenv/.zprofile/.zshrc/ + * .zlogin) Orca writes for every transport: local PTY, daemon/SSH, and relay. + * + * Why: the three generators were copies that drifted apart, so a fix landed in + * one transport and silently missed the other two. Everything they genuinely + * disagree on is a field on ZshStartupWrapperSpec, so the disagreements are + * visible in one place instead of spread across three template literals. + */ +import { getPosixOmpShellWrapper } from './pty/omp-shell-wrapper' +import { getPosixCodexShellLaunchPreflight } from './pty/codex-shell-launch-preflight' +import { + getZshEnvTemplate, + getZshFinalZdotdirRestoreBlock, + getZshOverlayEnvTemplate, + getZshShellReadyMarkerRegistrationBlock, + getZshStartupFileSourceBlock, + ZSH_HISTFILE_RESTORE_BLOCK +} from './shell-templates' + +/** Runtime values the wrapper re-exports after the user's own startup files ran. */ +export type ZshWrapperRestoreSpec = { + /** Orca's agent-teams shim dir back onto PATH. */ + agentTeamsPath: boolean + /** Remote CLI bin dir onto PATH — relay hosts only. */ + remoteCliBinDir: boolean + /** Orca's runtime CODEX_HOME. */ + codexHome: boolean + /** The `codex()` wrapper that runs Orca's launch preflight. */ + codexLaunchPreflight: boolean +} + +/** + * Pre-existing cosmetic drift in the .zshrc restore block. Carried as explicit + * fields so unifying the generators stays byte-identical; normalizing these + * away is a follow-up, not part of the unification. + */ +export type ZshWrapperLegacyFormatting = { + /** Local .zshrc lost the two-space indent on the MIMOCODE_HOME restore. */ + unindentedMimocodeRestore?: boolean + /** Only local .zshrc carries a comment above the CODEX_HOME restore. */ + codexHomeRestoreComment?: string +} + +export type ZshStartupWrapperSpec = { + /** First line of every generated file, e.g. `# Orca zsh shell-ready wrapper`. */ + headerLabel: string + /** Wrapper ZDOTDIR baked into .zshenv as the fallback literal. */ + zshDir: string + /** + * How .zshenv finds the user's real ZDOTDIR. `discover-user-zdotdir` sources + * the user .zshenv and reads what it exported; `overlay-user-zdotdir` trusts + * the inherited ZDOTDIR and republishes it as ORCA_USER_ZDOTDIR. + */ + zshenvStrategy: 'discover-user-zdotdir' | 'overlay-user-zdotdir' + /** zsh expression the wrapper resolves the user's startup-file dir from. */ + homeExpression?: string + readyMarkerEscaped: string + /** OSC 133 command-lifecycle hooks in .zshrc. */ + osc133CommandMarkers: boolean + /** Skip the user .zshrc when its dir is already the wrapper ZDOTDIR. */ + skipUserZshrcWhenHomeIsWrapperDir: boolean + /** Comment heading the non-login restore block in .zshrc. */ + interactiveRestoreComment: string + /** Comment heading the restore block in .zlogin. */ + loginRestoreComment: string + restores: ZshWrapperRestoreSpec + /** Where the ready-marker widget lands relative to the final ZDOTDIR restore. */ + readyMarkerOrder: 'before-zdotdir-restore' | 'after-zdotdir-restore' + legacyFormatting?: ZshWrapperLegacyFormatting +} + +export type ZshStartupWrapperFiles = { + zshenv: string + zprofile: string + zshrc: string + zlogin: string +} + +const AGENT_TEAMS_PATH_RESTORE_FUNCTION = `__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" +}` + +const OPENCODE_CONFIG_DIR_RESTORE = `[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"` +const MIMOCODE_HOME_RESTORE = `[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"` +const REMOTE_CLI_BIN_DIR_RESTORE = `[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac` +const CODEX_HOME_RESTORE = `[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"` + +const ZSH_OSC133_COMMAND_MARKER_BLOCK = `__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[@]})` + +/** + * Blocks already carrying a trailing newline (the omp wrapper, the codex + * preflight, the shared source/restore templates) keep it, so joining on a + * single newline reproduces the blank-line spacing of the originals. + */ +function joinBlocks(blocks: (string | null)[]): string { + return blocks.filter((block): block is string => block !== null).join('\n') +} + +/** The env/PATH restores that must outlast the user's own startup files. */ +function getRestoreLines( + spec: ZshStartupWrapperSpec, + indent: string, + legacyFormatting?: ZshWrapperLegacyFormatting +): (string | null)[] { + const mimocodeIndent = legacyFormatting?.unindentedMimocodeRestore ? '' : indent + const codexHomeComment = legacyFormatting?.codexHomeRestoreComment + return [ + `${indent}${OPENCODE_CONFIG_DIR_RESTORE}`, + `${mimocodeIndent}${MIMOCODE_HOME_RESTORE}`, + spec.restores.remoteCliBinDir ? `${indent}${REMOTE_CLI_BIN_DIR_RESTORE}` : null, + `${indent}${getPosixOmpShellWrapper()}`, + spec.restores.codexHome && codexHomeComment ? `${indent}${codexHomeComment}` : null, + spec.restores.codexHome ? `${indent}${CODEX_HOME_RESTORE}` : null, + ZSH_HISTFILE_RESTORE_BLOCK, + spec.restores.codexLaunchPreflight ? `${indent}${getPosixCodexShellLaunchPreflight()}` : null + ] +} + +function buildZshrc(spec: ZshStartupWrapperSpec): string { + return `${joinBlocks([ + `# ${spec.headerLabel}`, + getZshStartupFileSourceBlock({ + fileName: '.zshrc', + homeExpression: spec.homeExpression, + interactiveOnly: true, + skipWhenHomeIsCurrentZdotdir: spec.skipUserZshrcWhenHomeIsWrapperDir + }), + spec.restores.agentTeamsPath + ? `${AGENT_TEAMS_PATH_RESTORE_FUNCTION}\n[[ ! -o login ]] && __orca_restore_agent_teams_path` + : null, + joinBlocks([ + 'if [[ ! -o login ]]; then', + ` ${spec.interactiveRestoreComment}`, + ...getRestoreLines(spec, ' ', spec.legacyFormatting), + 'fi' + ]), + spec.osc133CommandMarkers ? ZSH_OSC133_COMMAND_MARKER_BLOCK : null, + `if [[ ! -o login ]]; then\n${getZshFinalZdotdirRestoreBlock(spec.homeExpression)}\nfi` + ])}\n` +} + +function buildZlogin(spec: ZshStartupWrapperSpec): string { + const finalZdotdirRestore = getZshFinalZdotdirRestoreBlock(spec.homeExpression) + const readyMarker = getZshShellReadyMarkerRegistrationBlock(spec.readyMarkerEscaped) + const tail = + spec.readyMarkerOrder === 'before-zdotdir-restore' + ? [readyMarker, finalZdotdirRestore] + : [finalZdotdirRestore, readyMarker] + + return `${joinBlocks([ + `# ${spec.headerLabel}`, + getZshStartupFileSourceBlock({ + fileName: '.zlogin', + homeExpression: spec.homeExpression, + interactiveOnly: true + }), + spec.restores.agentTeamsPath + ? `${AGENT_TEAMS_PATH_RESTORE_FUNCTION}\n__orca_restore_agent_teams_path` + : null, + joinBlocks([spec.loginRestoreComment, ...getRestoreLines(spec, '')]), + ...tail + ])}\n` +} + +export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshStartupWrapperFiles { + return { + zshenv: + spec.zshenvStrategy === 'discover-user-zdotdir' + ? getZshEnvTemplate(spec.zshDir, spec.headerLabel) + : getZshOverlayEnvTemplate(spec.zshDir, spec.headerLabel), + zprofile: `${joinBlocks([ + `# ${spec.headerLabel}`, + getZshStartupFileSourceBlock({ + fileName: '.zprofile', + homeExpression: spec.homeExpression + }) + ])}\n`, + zshrc: buildZshrc(spec), + zlogin: buildZlogin(spec) + } +} diff --git a/src/relay/pty-shell-overlay-wrappers.ts b/src/relay/pty-shell-overlay-wrappers.ts index 1fa2e9b821a..c62e5a0c123 100644 --- a/src/relay/pty-shell-overlay-wrappers.ts +++ b/src/relay/pty-shell-overlay-wrappers.ts @@ -3,12 +3,13 @@ import { dirname, join } from 'node:path' import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper' import { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK, - getZshFinalZdotdirRestoreBlock, - getZshShellReadyMarkerRegistrationBlock, SHELL_STARTUP_IDENTITY_MARKER_BLOCK, - ZSH_HISTFILE_RESTORE_BLOCK, - getZshStartupFileSourceBlock + ZSH_HISTFILE_RESTORE_BLOCK } from '../main/shell-templates' +import { + buildZshStartupWrapperFiles, + type ZshStartupWrapperSpec +} from '../main/zsh-startup-wrapper-builder' /** Writes the zsh/bash overlay wrapper files a relay-spawned shell sources. * Split from pty-shell-launch.ts so the launch-config decisions stay readable @@ -16,66 +17,37 @@ import { const SHELL_READY_MARKER_ESCAPED = '\\033]777;orca-shell-ready\\007' -function quotePosixSingle(value: string): string { - return `'${value.replace(/'/g, `'\\''`)}'` +// Why: the relay .zshenv republishes the inherited ZDOTDIR as ORCA_USER_ZDOTDIR, +// so later wrapper files prefer it over the spawn-time ORCA_ORIG_ZDOTDIR. +const RELAY_HOME_EXPRESSION = '"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"' + +function getRelayZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec { + return { + headerLabel: 'Orca relay zsh overlay wrapper', + zshDir, + zshenvStrategy: 'overlay-user-zdotdir', + homeExpression: RELAY_HOME_EXPRESSION, + readyMarkerEscaped: SHELL_READY_MARKER_ESCAPED, + osc133CommandMarkers: false, + skipUserZshrcWhenHomeIsWrapperDir: false, + interactiveRestoreComment: + '# Why: remote startup files can re-export user defaults after relay spawn.', + loginRestoreComment: '# Why: .zlogin is the final zsh login startup file before the prompt.', + restores: { + agentTeamsPath: false, + remoteCliBinDir: true, + codexHome: false, + codexLaunchPreflight: false + }, + readyMarkerOrder: 'after-zdotdir-restore' + } } export function ensureOverlayRestoreWrappers(root: string): void { const zshDir = join(root, 'zsh') const bashDir = join(root, 'bash') - const zshEnv = `# Orca relay zsh overlay wrapper -${SHELL_STARTUP_IDENTITY_MARKER_BLOCK} -export ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR:-$HOME}" -case "\${ORCA_ORIG_ZDOTDIR%/}" in - */shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;; -esac -[[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv" -export ORCA_USER_ZDOTDIR="\${ZDOTDIR:-\${ORCA_ORIG_ZDOTDIR:-$HOME}}" -case "\${ORCA_USER_ZDOTDIR%/}" in - */shell-ready/zsh) export ORCA_USER_ZDOTDIR="$HOME" ;; -esac -export ZDOTDIR=${quotePosixSingle(zshDir)} -` - const zshProfile = `# Orca relay zsh overlay wrapper -${getZshStartupFileSourceBlock({ - fileName: '.zprofile', - homeExpression: '"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"' -})} -` - const zshRc = `# Orca relay zsh overlay wrapper -${getZshStartupFileSourceBlock({ - fileName: '.zshrc', - homeExpression: '"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"', - interactiveOnly: true -})} -if [[ ! -o login ]]; then - # Why: remote startup files can re-export user defaults after relay spawn. - [[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" - [[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" - [[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac - ${getPosixOmpShellWrapper()} -${ZSH_HISTFILE_RESTORE_BLOCK} -fi -if [[ ! -o login ]]; then -${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')} -fi -` - const zshLogin = `# Orca relay zsh overlay wrapper -${getZshStartupFileSourceBlock({ - fileName: '.zlogin', - homeExpression: '"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"', - interactiveOnly: true -})} -# Why: .zlogin is the final zsh login startup file before the prompt. -[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}" -[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}" -[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac -${getPosixOmpShellWrapper()} -${ZSH_HISTFILE_RESTORE_BLOCK} -${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')} -${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)} -` + const zsh = buildZshStartupWrapperFiles(getRelayZshWrapperSpec(zshDir)) const bashRc = `# Orca relay bash overlay wrapper ${SHELL_STARTUP_IDENTITY_MARKER_BLOCK} [[ -f /etc/profile ]] && source /etc/profile @@ -181,10 +153,10 @@ unset __orca_initializing_wrapper ` const files = [ - [join(zshDir, '.zshenv'), zshEnv], - [join(zshDir, '.zprofile'), zshProfile], - [join(zshDir, '.zshrc'), zshRc], - [join(zshDir, '.zlogin'), zshLogin], + [join(zshDir, '.zshenv'), zsh.zshenv], + [join(zshDir, '.zprofile'), zsh.zprofile], + [join(zshDir, '.zshrc'), zsh.zshrc], + [join(zshDir, '.zlogin'), zsh.zlogin], [join(bashDir, 'rcfile'), bashRc] ] as const