fix(shell): repair worktree HISTFILE in plain zsh panes via one positive feature channel

A plain zsh pane — no startup command, no agent overlay — was never wrapped, so
Orca's HISTFILE repair never ran in it. macOS `/etc/zshrc` assigns
`HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history` with no check-before-set and runs
before any file Orca controls, so per-worktree history was a silent no-op for
every ordinary pane on the primary platform.

Wrapping those panes needs a way to say which wrapper features a shell should
turn on. That channel is one exported variable, ORCA_SHELL_FEATURES, carrying a
comma-separated positive allowlist from a closed set (history, markers, ready,
identity, overlay). The wrapper .zshenv reads it into a plain, non-exported
array and unsets it in its first executable lines, before the user's own
.zshenv — so the selection survives .zshenv -> .zprofile -> .zshrc -> .zlogin in
this process but physically cannot reach a child. There is no negative or
suppression variable anywhere; an absent or inherited value can only ever mean
fewer features. ORCA_HISTFILE is consumed and destroyed the same way, which
removes the root cause of #11146 instead of patching it.

All order-sensitive wrapper work now lives in one `__orca_shell_epilogue`
defined in .zshenv and invoked exactly once, from .zshrc for a non-login shell
and .zlogin for a login shell, with each feature an independent guard.

Selection is a pure function of spawn env and launch intent, so a pane wrapped
only for history gets no OSC 133 and is observably identical to the unwrapped
pane it used to be.

Generation is now fail-closed: wrapper files are written to a temp name and
renamed, every required path is verified non-empty, and ZDOTDIR is only set when
that holds. Previously a failed write still pointed ZDOTDIR at an empty dir and
the user silently lost their entire zsh config.

Orca also recognised only its own `*/shell-ready/zsh` dir shape when deciding
what the user's ZDOTDIR was, so being launched from any other terminal that had
hijacked ZDOTDIR captured that as the user's config dir. Ownership is now
established positively — a stamped marker file, or Orca's own dir shape for
wrappers written by older builds — and an inherited ZDOTDIR holding no zsh
startup file is ignored. No vendor is detected by name.
This commit is contained in:
Neil
2026-08-17 23:52:12 -07:00
parent 545b3fba08
commit 2326cbbb5d
66 changed files with 2039 additions and 1372 deletions
@@ -1,12 +1,19 @@
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from '../bash-prompt-command-composition'
import { SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
import { BASH_FEATURE_CHANNEL_BLOCK, SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
import { SHELL_READY_MARKER } from './daemon-shell-ready-marker'
export function getDaemonBashShellReadyRcfileContent(): string {
return `# Orca daemon bash shell-ready wrapper
${BASH_FEATURE_CHANNEL_BLOCK}
${SHELL_STARTUP_IDENTITY_MARKER_BLOCK}
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -52,7 +59,7 @@ __orca_osc133_precmd() {
# Why: emit the shell-ready marker here (not a trailing PROMPT_COMMAND entry)
# so a framework that must be last in PROMPT_COMMAND — bash-preexec — is not
# displaced by one of Orca's own hooks.
[[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]] && printf "${SHELL_READY_MARKER}"
[[ -n "$__orca_ready_marker" ]] && printf "${SHELL_READY_MARKER}"
return "$exit_code"
}
__orca_osc133_preexec() {
@@ -10,16 +10,13 @@ export function getDaemonZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec {
readyMarkerEscaped: SHELL_READY_MARKER,
osc133CommandMarkers: true,
skipUserZshrcWhenHomeIsWrapperDir: true,
interactiveRestoreComment:
overlayRestoreComment:
"# 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'
}
}
}
@@ -112,7 +112,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell wrapper when OpenCode config must survive shell startup', async () => {
@@ -141,7 +141,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell wrapper when MiMo home must survive shell startup', async () => {
@@ -170,7 +170,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell wrapper when typed OMP commands need the status extension', async () => {
@@ -198,7 +198,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell wrapper when Codex home must survive shell startup', async () => {
@@ -227,7 +227,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell wrapper when Agent Teams shim path must survive shell startup', async () => {
@@ -257,7 +257,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('keeps plain Codex startup commands on the no-marker wrapper', async () => {
@@ -284,7 +284,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('uses shell-ready wrapper for delivery-hinted Codex startup commands', async () => {
@@ -312,7 +312,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).toContain('ready')
})
it('uses shell-ready wrapper for Codex native prefill flags', async () => {
@@ -339,7 +339,7 @@ describe('createPtySubprocess', () => {
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(lastCall[2].env.ORCA_SHELL_FEATURES).toContain('ready')
})
it('deletes requested env keys after merging daemon process env', async () => {
+29 -31
View File
@@ -5,11 +5,8 @@ import { release } from 'node:os'
import { delimiter, win32 as pathWin32 } from 'node:path'
import type { SubprocessHandle } from './session-subprocess-handle'
import { DaemonProtocolError } from './types'
import {
getMarkerlessShellLaunchConfig,
getShellReadyLaunchConfig,
resolvePtyShellPath
} from './shell-ready'
import { getShellLaunchConfig, resolvePtyShellPath } from './shell-ready'
import { selectShellStartupFeatures } from '../shell-startup-features'
import { isValidPtySize, normalizePtySize } from './daemon-pty-size'
import {
ensureNodePtySpawnHelperExecutable,
@@ -824,33 +821,34 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise<S
`[daemon/pty] Preferred shell "${preferredShellPath}" is unavailable, fell back to "${shellPath}"`
)
}
// Why: OpenCode/Codex path restoration and OMP's typed-command status wrapper need shell-ready code after user startup files run.
let shellLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
if (opts.command && isCodexStartupCommand) {
const shouldWaitForShellReady = shouldUseShellReadyStartupDelivery({
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery
// Why: OpenCode/Codex path restoration, OMP's typed-command status wrapper,
// and the worktree HISTFILE repair all need shell-ready code that runs after
// the user's own startup files.
// Why: payload-bearing Codex startup text can be dropped by rc-file noise; plain Codex stays markerless for the startup-speed path.
const waitsForShellReady =
Boolean(opts.command) &&
(!isCodexStartupCommand ||
shouldUseShellReadyStartupDelivery({
command: opts.command as string,
startupCommandDelivery: opts.startupCommandDelivery
}))
// Why delete: ORCA_SHELL_FEATURES is Orca-owned, and only the launch config
// below may name features for this shell.
delete env.ORCA_SHELL_FEATURES
const shellLaunch = getShellLaunchConfig(
shellPath,
selectShellStartupFeatures({
shellPath,
env,
hasStartupCommand: Boolean(opts.command),
waitsForShellReady,
// Why identical: the identity marker exists so the readiness handshake
// can bind output to the right shell PID.
emitsStartupIdentity: waitsForShellReady
})
// Why: payload-bearing Codex startup text can be dropped by rc-file noise; plain Codex stays markerless for the startup-speed path.
shellLaunch = shouldWaitForShellReady
? getShellReadyLaunchConfig(shellPath)
: getMarkerlessShellLaunchConfig(shellPath)
} else if (opts.command) {
shellLaunch = getShellReadyLaunchConfig(shellPath)
} else {
shellLaunch =
env.ORCA_OPENCODE_CONFIG_DIR ||
env.ORCA_MIMOCODE_HOME ||
env.ORCA_OMP_STATUS_EXTENSION ||
env.ORCA_CODEX_HOME ||
env.ORCA_AGENT_TEAMS_SHIM_DIR
? getMarkerlessShellLaunchConfig(shellPath)
: null
}
if (shellLaunch) {
Object.assign(env, shellLaunch.env)
}
shellArgs = shellLaunch?.args ?? ['-l']
)
Object.assign(env, shellLaunch.env)
shellArgs = shellLaunch.args ?? ['-l']
}
seedPowerlevel10kWizardEnv(env, { envToDelete: opts.envToDelete })
if (
+57 -36
View File
@@ -3,8 +3,12 @@ import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import type * as ShellReadyModule from './shell-ready'
import type * as DaemonBashRcfileModule from './daemon-bash-shell-ready-rcfile'
import {
OVERLAY_ONLY_FEATURES,
STARTUP_COMMAND_FEATURES
} from '../shell-startup-launch-intent-fixtures'
import { makeUserZdotdir } from '../zsh-user-config-dir-fixture'
import { getZshShellReadyMarkerRegistrationBlock } from '../shell-templates'
import { fishRequirementViolation, resolveFishBinary } from '../../shared/fish-binary-requirement'
import {
@@ -14,9 +18,16 @@ import {
} from '../shell-startup-output-scanner'
import { HeadlessEmulator } from './headless-emulator'
async function importFreshShellReady(): Promise<typeof ShellReadyModule> {
async function importFreshShellReady() {
vi.resetModules()
return import('./shell-ready')
const module = await import('./shell-ready')
return {
...module,
getShellReadyLaunchConfig: (shell: string) =>
module.getShellLaunchConfig(shell, STARTUP_COMMAND_FEATURES),
getMarkerlessShellLaunchConfig: (shell: string) =>
module.getShellLaunchConfig(shell, OVERLAY_ONLY_FEATURES)
}
}
async function importFreshDaemonBashRcfile(): Promise<typeof DaemonBashRcfileModule> {
@@ -68,7 +79,7 @@ async function runInteractiveZshLogin(args: {
ZDOTDIR: args.wrapperZdotdir,
ORCA_ORIG_ZDOTDIR: args.tempHome,
ORCA_ZSHENV_SOURCE_DIR: args.tempHome,
ORCA_SHELL_READY_MARKER: '1'
ORCA_SHELL_FEATURES: 'ready'
}
})
let output = ''
@@ -106,7 +117,7 @@ async function runInteractiveZshRc(args: {
HOME: args.zdotdir,
TERM: 'xterm-256color',
ZDOTDIR: args.zdotdir,
ORCA_SHELL_READY_MARKER: '1'
ORCA_SHELL_FEATURES: 'ready'
}
})
let output = ''
@@ -140,7 +151,7 @@ function runInteractiveBashRcfile(rcfileContent: string, tempDir: string): strin
env: {
...process.env,
HOME: tempDir,
ORCA_SHELL_READY_MARKER: '1',
ORCA_SHELL_FEATURES: 'ready',
TERM: process.env.TERM || 'xterm'
},
timeout: 5000
@@ -175,7 +186,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
function expectFinalZdotdirRestoreContext(content: string) {
expect(content).toContain("after Orca's last wrapper file has loaded")
expect(content).toContain('export ZDOTDIR="$_orca_home"')
expect(content).toContain('export ZDOTDIR="$REPLY"')
}
describePosix('daemon shell-ready launch config', () => {
@@ -259,7 +270,9 @@ describePosix('daemon shell-ready launch config', () => {
const config = getShellReadyLaunchConfig('/opt/homebrew/bin/fish')
expect(config.supportsReadyMarker).toBe(true)
expect(config.env).toEqual({ ORCA_SHELL_READY_MARKER: '1' })
// Why empty: fish's selection is baked into the init command text, so it
// needs no exported feature variable at all.
expect(config.env).toEqual({})
expect(config.args?.slice(0, 2)).toEqual(['-l', '-C'])
const init = config.args?.[2] ?? ''
expect(init).toContain('--on-event fish_prompt')
@@ -415,14 +428,15 @@ describePosix('daemon shell-ready launch config', () => {
const previousZdotdir = process.env.ZDOTDIR
const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR
const previousHome = process.env.HOME
const userZdotdir = makeUserZdotdir(userDataPath, '.config', 'zsh')
process.env.ZDOTDIR = '/some/other/orca/shell-ready/zsh'
process.env.ORCA_ORIG_ZDOTDIR = '/Users/alice/.config/zsh'
process.env.HOME = '/Users/alice'
process.env.ORCA_ORIG_ZDOTDIR = userZdotdir
process.env.HOME = userDataPath
try {
const { getShellReadyLaunchConfig } = await importFreshShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe('/Users/alice')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe(userDataPath)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -482,15 +496,17 @@ describePosix('daemon shell-ready launch config', () => {
const zprofile = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zprofile'), 'utf8')
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshrc'), 'utf8')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
expect(zshenv).toContain('_orca_user_zdotdir="${_orca_spawn_orig_zdotdir:-$HOME}"')
expect(zshenv).toContain('__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"')
expect(zshenv).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
expect(zshenv).toContain('*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;')
expect(zshenv).toContain('""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;')
expect(zshenv).toContain('"$REPLY" == */shell-ready/zsh ]]; then')
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$REPLY"')
expectZdotdirSourceContext(zprofile, '.zprofile')
expectZdotdirSourceContext(zshrc, '.zshrc')
expectZdotdirSourceContext(zlogin, '.zlogin')
expectFinalZdotdirRestoreContext(zshrc)
expectFinalZdotdirRestoreContext(zlogin)
// Why .zshenv: the final restore is the last step of the single epilogue,
// which .zshrc (non-login) and .zlogin (login) each invoke once.
expectFinalZdotdirRestoreContext(zshenv)
expect(zshrc).toContain('[[ -o login ]] || __orca_shell_epilogue')
})
it('owns zle-line-init for the shell-ready marker instead of an azhw hook', async () => {
@@ -498,14 +514,16 @@ describePosix('daemon shell-ready launch config', () => {
getShellReadyLaunchConfig('/bin/zsh')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
expect(zlogin).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zlogin).toContain('__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"')
expect(zlogin).toContain('printf "\\033]777;orca-shell-ready\\007"')
// Why .zshenv: the widget registration lives in the epilogue, which .zlogin
// (login) and .zshrc (non-login) both call exactly once.
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
expect(zshenv).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zshenv).toContain('__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"')
expect(zshenv).toContain('printf "\\033]777;orca-shell-ready\\007"')
// Why: add-zle-hook-widget aborts its chain when an earlier hook exits non-zero, so don't register the marker through it.
expect(zlogin).not.toContain('add-zle-hook-widget line-init')
expect(zshenv).not.toContain('add-zle-hook-widget line-init')
// Why: re-source guard — skip re-capturing when already the bound widget so the prior chain survives a second source.
expect(zlogin).toContain('== "user:__orca_prompt_mark"')
expect(zshenv).toContain('== "user:__orca_prompt_mark"')
})
// Why: oh-my-zsh vi-mode's zle-line-init returns non-zero; add-zle-hook-widget then aborts the chain and the marker never fires.
@@ -621,8 +639,10 @@ describePosix('daemon shell-ready launch config', () => {
getShellReadyLaunchConfig('/bin/zsh')
getShellReadyLaunchConfig('/bin/bash')
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshrc'), 'utf8')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
// Why one file: every restore now lives in the single epilogue in .zshenv,
// which .zshrc and .zlogin each invoke on their own startup path.
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
const zlogin = zshrc
const bashRc = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
@@ -668,7 +688,8 @@ describePosix('daemon shell-ready launch config', () => {
getShellReadyLaunchConfig('/bin/zsh')
getShellReadyLaunchConfig('/bin/bash')
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshrc'), 'utf8')
// Why .zshenv: the zsh markers live in the epilogue, behind `markers`.
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
const bashRc = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
expect(bashRc).toContain('printf "\\033]133;D;%s\\007"')
@@ -800,12 +821,13 @@ describePosix('daemon shell-ready launch config', () => {
it('preserves a real inherited ZDOTDIR as ORCA_ORIG_ZDOTDIR', async () => {
// Why: only the wrapper self-loop should be rejected; a real user ZDOTDIR must round-trip so their configs load.
const previousZdotdir = process.env.ZDOTDIR
process.env.ZDOTDIR = '/Users/alice/.config/zsh'
const userZdotdir = makeUserZdotdir(userDataPath, '.config', 'zsh')
process.env.ZDOTDIR = userZdotdir
try {
const { getShellReadyLaunchConfig } = await importFreshShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe(userZdotdir)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -866,11 +888,12 @@ describePosix('daemon shell-ready launch config', () => {
it('preserves ZDOTDIR that contains /shell-ready/zsh as a substring but does not end with it', async () => {
// Why: guard must match suffix not substring — `/Users/alice/shell-ready/zsh-custom` must round-trip unchanged.
const previousZdotdir = process.env.ZDOTDIR
process.env.ZDOTDIR = '/Users/alice/shell-ready/zsh-custom'
const userZdotdir = makeUserZdotdir(userDataPath, 'shell-ready', 'zsh-custom')
process.env.ZDOTDIR = userZdotdir
try {
const { getShellReadyLaunchConfig } = await importFreshShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/shell-ready/zsh-custom')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -889,12 +912,10 @@ describePosix('daemon shell-ready launch config', () => {
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
expect(zshenv).toContain('unset ZDOTDIR')
expect(zshenv).toContain('_orca_zshenv_source_dir="${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"')
expect(zshenv).toContain('__orca_resolve_inherited_config_dir "${ORCA_ZSHENV')
expect(zshenv).toContain('source "${_orca_zshenv_path}"')
expect(zshenv).toContain('_orca_discovered_zdotdir="${ZDOTDIR:-}"')
expect(zshenv).toContain(
'export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"'
)
expect(zshenv).toContain('${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}')
expect(zshenv).toContain('export ZDOTDIR=')
})
@@ -907,7 +928,7 @@ describePosix('daemon shell-ready launch config', () => {
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
// Save spawn-env value before sourcing user .zshenv
expect(zshenv).toContain('_orca_spawn_orig_zdotdir="${ORCA_ORIG_ZDOTDIR:-}"')
expect(zshenv).toContain('_orca_user_zdotdir="$REPLY"')
// Fallback chain: discovered → normalized spawn-env path → HOME
expect(zshenv).toContain('${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}')
+83 -110
View File
@@ -1,13 +1,27 @@
import { tmpdir } from 'node:os'
import { basename, dirname, join, win32 as pathWin32 } from 'node:path'
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { basename, join, win32 as pathWin32 } from 'node:path'
import { statSync } from 'node:fs'
import {
encodePowerShellCommand,
getPowerShellOsc133Bootstrap,
isPowerShellExecutableName
} from '../powershell-osc133-bootstrap'
import { getFishCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getFishShellReadyInitCommand } from '../shell-templates'
import {
getFishShellReadyInitCommand,
ZSH_WRAPPER_DIR_MARKER_CONTENT,
ZSH_WRAPPER_DIR_MARKER_FILE
} from '../shell-templates'
import {
encodeShellStartupFeatures,
SHELL_STARTUP_FEATURE_ENV,
type ShellStartupFeature
} from '../shell-startup-features'
import { writeShellWrapperFiles } from '../shell-wrapper-file-writer'
import {
resolveInheritedZdotdir,
resolveInheritedZshenvSourceDir
} from '../zsh-wrapper-dir-ownership'
import { buildZshStartupWrapperFiles } from '../zsh-startup-wrapper-builder'
import { SHELL_READY_MARKER } from './daemon-shell-ready-marker'
import { getDaemonBashShellReadyRcfileContent } from './daemon-bash-shell-ready-rcfile'
@@ -24,105 +38,60 @@ function getShellReadyWrapperRoot(): string {
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, 'zsh', ZSH_WRAPPER_DIR_MARKER_FILE),
join(root, 'bash', 'rcfile')
]
}
// Why non-empty and not just present: a partial write leaves a zero-byte
// .zshenv, and pointing ZDOTDIR at that dir makes zsh skip the user's config.
function shellReadyWrappersExist(): boolean {
return getRequiredShellReadyWrapperPaths().every((path) => existsSync(path))
return getRequiredShellReadyWrapperPaths().every((path) => {
try {
return statSync(path).size > 0
} catch {
return false
}
})
}
function ensureShellReadyWrappers(): void {
/** True when every wrapper file is present and non-empty afterwards. */
function ensureShellReadyWrappers(): boolean {
if (process.platform === 'win32') {
return
return false
}
if (didEnsureShellReadyWrappers && shellReadyWrappersExist()) {
return
return true
}
didEnsureShellReadyWrappers = true
const root = getShellReadyWrapperRoot()
const zshDir = join(root, 'zsh')
const bashDir = join(root, 'bash')
const zsh = buildZshStartupWrapperFiles(getDaemonZshWrapperSpec(zshDir))
const bashRc = getDaemonBashShellReadyRcfileContent()
const files = [
[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
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
const written = writeShellWrapperFiles(
[
[join(zshDir, '.zshenv'), zsh.zshenv],
[join(zshDir, '.zprofile'), zsh.zprofile],
[join(zshDir, '.zshrc'), zsh.zshrc],
[join(zshDir, '.zlogin'), zsh.zlogin],
[join(zshDir, ZSH_WRAPPER_DIR_MARKER_FILE), ZSH_WRAPPER_DIR_MARKER_CONTENT],
[join(root, 'bash', 'rcfile'), getDaemonBashShellReadyRcfileContent()]
],
'[daemon/shell-ready]'
)
if (!written || !shellReadyWrappersExist()) {
// Why reset: the next launch retries instead of trusting a half-written tree.
didEnsureShellReadyWrappers = false
return false
}
return true
}
export function resolvePtyShellPath(env: Record<string, string>): string {
@@ -146,44 +115,59 @@ export function supportsPtyStartupBarrier(env: Record<string, string>): boolean
return shellPathSupportsPtyStartupBarrier(resolvePtyShellPath(env))
}
type ShellLaunchConfig = {
export type ShellLaunchConfig = {
args: string[] | null
env: Record<string, string>
supportsReadyMarker: boolean
}
function getWrappedShellLaunchConfig(
const UNWRAPPED: ShellLaunchConfig = {
args: null,
env: {},
supportsReadyMarker: false
}
/**
* The one launch-config entry point: args + env for a shell that should start
* with exactly `features` enabled. An empty selection is never wrapped.
*/
export function getShellLaunchConfig(
shellPath: string,
options: { emitReadyMarker: boolean }
features: readonly ShellStartupFeature[]
): ShellLaunchConfig {
const shellName = pathWin32.basename(basename(shellPath)).toLowerCase()
if (shellName === 'zsh') {
ensureShellReadyWrappers()
const root = getShellReadyWrapperRoot()
if (features.length === 0) {
return UNWRAPPED
}
if (!ensureShellReadyWrappers()) {
// Why plain login zsh: ZDOTDIR pointed at an incomplete wrapper dir makes
// zsh skip the user's whole config. Losing Orca's features is recoverable.
return { args: ['-l'], env: {}, supportsReadyMarker: false }
}
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',
ORCA_SHELL_STARTUP_IDENTITY: options.emitReadyMarker ? '1' : '0'
ORCA_ORIG_ZDOTDIR: resolveInheritedZdotdir(process.env),
ORCA_ZSHENV_SOURCE_DIR: resolveInheritedZshenvSourceDir(process.env),
ZDOTDIR: join(getShellReadyWrapperRoot(), 'zsh'),
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
},
supportsReadyMarker: options.emitReadyMarker
supportsReadyMarker: features.includes('ready')
}
}
if (shellName === 'bash') {
ensureShellReadyWrappers()
const root = getShellReadyWrapperRoot()
if (features.length === 0 || !ensureShellReadyWrappers()) {
return UNWRAPPED
}
return {
args: ['--rcfile', join(root, 'bash', 'rcfile')],
args: ['--rcfile', join(getShellReadyWrapperRoot(), 'bash', 'rcfile')],
env: {
ORCA_SHELL_READY_MARKER: options.emitReadyMarker ? '1' : '0',
ORCA_SHELL_STARTUP_IDENTITY: options.emitReadyMarker ? '1' : '0'
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
},
supportsReadyMarker: options.emitReadyMarker
supportsReadyMarker: features.includes('ready')
}
}
@@ -200,30 +184,19 @@ function getWrappedShellLaunchConfig(
}
}
// Why: mirrors local-pty-shell-ready.ts; markerless fish stays unwrapped.
if (shellName === 'fish' && options.emitReadyMarker) {
// Why: mirrors local-pty-shell-ready.ts; markerless fish stays unwrapped. The
// selection is baked into the init command, so fish needs no feature env var.
if (shellName === 'fish' && features.includes('ready')) {
return {
args: [
'-l',
'-C',
`${getFishShellReadyInitCommand(SHELL_READY_MARKER)}\n${getFishCodexShellLaunchPreflight()}`
],
env: { ORCA_SHELL_READY_MARKER: '1' },
env: {},
supportsReadyMarker: true
}
}
return {
args: null,
env: {},
supportsReadyMarker: false
}
}
export function getShellReadyLaunchConfig(shellPath: string): ShellLaunchConfig {
return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: true })
}
export function getMarkerlessShellLaunchConfig(shellPath: string): ShellLaunchConfig {
return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: false })
return UNWRAPPED
}
+4
View File
@@ -17,6 +17,8 @@ export const mkdirSyncMock: Mock = vi.fn()
export const readFileSyncMock: Mock = vi.fn()
export const writeFileSyncMock: Mock = vi.fn()
export const chmodSyncMock: Mock = vi.fn()
export const renameSyncMock: Mock = vi.fn()
export const rmSyncMock: Mock = vi.fn()
export const getPathMock: Mock = vi.fn()
export const loginPreflightExecFileMock: Mock = vi.fn()
export const spawnMock: Mock = vi.fn()
@@ -85,6 +87,8 @@ export const fsModuleMock = () => ({
readFileSync: readFileSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: chmodSyncMock,
renameSync: renameSyncMock,
rmSync: rmSyncMock,
constants: {
X_OK: 1,
R_OK: 4
+2 -1
View File
@@ -122,7 +122,8 @@ export function createPtyIpcSpawnDrivers(ctx: {
statSyncMock.mockImplementation((target: string) => ({
isDirectory: () => target !== BUNDLED_CLI_PATH,
isFile: () => target === BUNDLED_CLI_PATH,
mode: 0o755
mode: 0o755,
size: 1
}))
if (!launcherExecutable) {
accessSyncMock.mockImplementation((target: string) => {
+2 -1
View File
@@ -164,7 +164,8 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment {
// Why: wrapper roots resolve from ORCA_USER_DATA_PATH; mirror the mocked userData so ZDOTDIR/wrapper assertions match.
process.env.ORCA_USER_DATA_PATH = '/tmp/orca-user-data'
existsSyncMock.mockReturnValue(true)
statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755 })
// size: the shell wrapper writer verifies each generated file is non-empty.
statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755, size: 1 })
readFileSyncMock.mockReturnValue('')
openCodeBuildPtyEnvMock.mockImplementation((_ptyId: string, existingConfigDir?: string) => ({
ORCA_OPENCODE_HOOK_PORT: '4567',
@@ -86,7 +86,7 @@ describe('registerPtyHandlers', () => {
env: expect.objectContaining({
SHELL: '/bin/zsh',
ORCA_OPENCODE_CONFIG_DIR: '/tmp/orca-opencode-config',
ORCA_SHELL_READY_MARKER: '0',
ORCA_SHELL_FEATURES: 'overlay,history,markers',
ZDOTDIR: '/tmp/orca-user-data/shell-ready/zsh'
})
})
@@ -122,7 +122,7 @@ describe('registerPtyHandlers', () => {
expect(options.env.OPENCODE_CONFIG_DIR).toBe('/tmp/orca-opencode-config')
expect(options.env.ORCA_OPENCODE_CONFIG_DIR).toBe('/tmp/orca-opencode-config')
expect(options.env.ZDOTDIR).toBe('/tmp/orca-user-data/shell-ready/zsh')
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(options.env.ORCA_SHELL_FEATURES).not.toContain('ready')
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,
@@ -163,7 +163,7 @@ describe('registerPtyHandlers', () => {
expect(options.env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
expect(options.env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/user-pi-agent')
expect(options.env.ZDOTDIR).toBe('/tmp/orca-user-data/shell-ready/zsh')
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(options.env.ORCA_SHELL_FEATURES).not.toContain('ready')
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,
@@ -306,7 +306,7 @@ describe('registerPtyHandlers', () => {
})
const [, , options] = spawnMock.mock.calls[0]!
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(options.env.ORCA_SHELL_FEATURES).not.toContain('ready')
await Promise.resolve()
vi.advanceTimersByTime(49)
@@ -338,7 +338,7 @@ describe('registerPtyHandlers', () => {
})
const [, , options] = spawnMock.mock.calls[0]!
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(options.env.ORCA_SHELL_FEATURES).toContain('ready')
expect(mockProc.proc.write).not.toHaveBeenCalled()
mockProc.emitData('last login: today\r\n')
@@ -405,7 +405,7 @@ describe('registerPtyHandlers', () => {
})
const [, , options] = spawnMock.mock.calls[0]!
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(options.env.ORCA_SHELL_FEATURES).toContain('ready')
expect(mockProc.proc.write).not.toHaveBeenCalled()
mockProc.emitData('\x1b]777;orca-shell-ready\x07')
+3 -3
View File
@@ -166,7 +166,7 @@ describe('registerPtyHandlers', () => {
if (target === missingCwd) {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
return { isDirectory: () => true, mode: 0o755, size: 1 }
})
try {
@@ -229,7 +229,7 @@ describe('registerPtyHandlers', () => {
if (target === '/repo/app/deleted-folder') {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
return { isDirectory: () => true, mode: 0o755, size: 1 }
})
// Why: without the renderer opt-in the provider surfaces its normal missing-directory error — API/runtime callers keep exact cwd semantics.
@@ -266,7 +266,7 @@ describe('registerPtyHandlers', () => {
if (target === '/repo/app/deleted-folder') {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
return { isDirectory: () => true, mode: 0o755, size: 1 }
})
// Why: a reattach must keep the session's exact cwd; remapping would silently detach the restored terminal from its recorded state.
@@ -2,7 +2,8 @@ import { tmpdir } from 'node:os'
import { join, basename } from 'node:path'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { getShellReadyLaunchConfig } from '../../local-pty-shell-ready'
import { getShellLaunchConfig } from '../../local-pty-shell-ready'
import { selectShellStartupFeatures } from '../../../shell-startup-features'
import { escapeRegex } from '../../../../shared/string-utils'
const RUN_MARKER = /^[ \t]*#[ \t]*Run:.*$/m
@@ -58,12 +59,25 @@ export async function shellScriptTest(
const runScript = hasRunMarker ? parts[1].trim() : script.trim()
const wrapperShell = detectShellFromCommand(runScript, options.shell || '/bin/zsh')
const config = getShellReadyLaunchConfig(wrapperShell)
const config = getShellLaunchConfig(
wrapperShell,
selectShellStartupFeatures({
shellPath: wrapperShell,
env: {},
hasStartupCommand: true,
waitsForShellReady: true,
emitsStartupIdentity: true
})
)
const env: Record<string, string> = {
...config.env,
// Why: these examples inspect startup-file discovery, not the PTY-owner protocol stream.
ORCA_SHELL_STARTUP_IDENTITY: '0',
// Why: these examples inspect startup-file discovery, not the PTY-owner
// protocol stream, so drop the tokens that write to stdout.
ORCA_SHELL_FEATURES: (config.env.ORCA_SHELL_FEATURES ?? '')
.split(',')
.filter((feature) => feature !== 'identity' && feature !== 'markers')
.join(','),
// Why: the framework creates user startup files under testHome after
// computing the wrapper config; route wrapper discovery to that fixture.
HOME: testHome,
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -40,6 +40,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -421,7 +423,7 @@ describe('LocalPtyProvider', () => {
const spawnCall = spawnMock.mock.calls.at(-1)!
expect(spawnCall[1]).toEqual(['-l'])
expect(spawnCall[2].env.ZDOTDIR).toMatch(/shell-ready[\\/]zsh/)
expect(spawnCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(spawnCall[2].env.ORCA_SHELL_FEATURES).not.toContain('ready')
})
it('promotes the agent-teams shim onto the Windows `Path` spelling', async () => {
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
@@ -74,7 +74,8 @@ export function installLocalPtyProviderEnvSandbox(): void {
export function applyLocalPtyProviderMockDefaults(mocks: LocalPtyProviderMocks): void {
mocks.existsSyncMock.mockReturnValue(true)
mocks.statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755 })
// size: the wrapper writer verifies each generated file is non-empty.
mocks.statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755, size: 1 })
mocks.accessSyncMock.mockReturnValue(undefined)
mocks.mkdirSyncMock.mockReset()
mocks.writeFileSyncMock.mockReset()
@@ -38,6 +38,8 @@ vi.mock('fs', () => ({
mkdirSync: mkdirSyncMock,
writeFileSync: writeFileSyncMock,
chmodSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
constants: { X_OK: 1 }
}))
+39 -41
View File
@@ -32,7 +32,8 @@ import {
spawnShellWithFallback
} from './local-pty-utils'
import { prepareMacosTccLoginShell } from './macos-tcc-login-shell'
import { getMarkerlessShellLaunchConfig, getShellReadyLaunchConfig } from './local-pty-shell-ready'
import { getShellLaunchConfig } from './local-pty-shell-ready'
import { selectShellStartupFeatures } from '../shell-startup-features'
import {
writeStartupCommandWhenShellReady,
STARTUP_COMMAND_READY_MAX_WAIT_MS
@@ -596,9 +597,9 @@ export class LocalPtyProvider implements IPtyProvider {
let validationCwd: string
let startupCommandDeliveredInShellArgs = false
let windowsFallbackAttempts: ReturnType<typeof buildWindowsPowerShellSpawnAttempts> = []
let shellReadyLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
let shellReadyLaunch: ReturnType<typeof getShellLaunchConfig> | null = null
let getFallbackShellReadyConfig:
| ((shell: string) => ReturnType<typeof getShellReadyLaunchConfig>)
| ((shell: string) => ReturnType<typeof getShellLaunchConfig>)
| undefined
if (wslInfo) {
shellPath = 'wsl.exe'
@@ -815,44 +816,6 @@ export class LocalPtyProvider implements IPtyProvider {
) {
addWslEnvKeys(finalEnv, [POWERLEVEL10K_WIZARD_DISABLE_ENV])
}
if (!wslInfo && process.platform !== 'win32') {
// Why: OpenCode/Codex PATH restoration and OMP's status wrapper need shell-ready code after user startup files run.
const needsNoMarkerWrapper =
finalEnv.ORCA_OPENCODE_CONFIG_DIR ||
finalEnv.ORCA_MIMOCODE_HOME ||
finalEnv.ORCA_OMP_STATUS_EXTENSION ||
finalEnv.ORCA_CODEX_HOME ||
finalEnv.ORCA_AGENT_TEAMS_SHIM_DIR
const isCodexStartupCommand = startupAgentRecognition?.agent === 'codex'
let shellLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
if (args.command && isCodexStartupCommand) {
const shouldWaitForShellReady = shouldUseShellReadyStartupDelivery({
command: args.command,
startupCommandDelivery: args.startupCommandDelivery
})
// Why: payload-bearing Codex startup can be lost to rc-file noise; plain Codex stays markerless for startup speed.
getFallbackShellReadyConfig = (shell) =>
shouldWaitForShellReady
? getShellReadyLaunchConfig(shell)
: getMarkerlessShellLaunchConfig(shell)
shellLaunch = shouldWaitForShellReady
? getShellReadyLaunchConfig(shellPath)
: getMarkerlessShellLaunchConfig(shellPath)
} else if (args.command) {
getFallbackShellReadyConfig = (shell) => getShellReadyLaunchConfig(shell)
shellLaunch = getShellReadyLaunchConfig(shellPath)
} else if (needsNoMarkerWrapper) {
getFallbackShellReadyConfig = (shell) => getMarkerlessShellLaunchConfig(shell)
shellLaunch = getMarkerlessShellLaunchConfig(shellPath)
} else {
getFallbackShellReadyConfig = undefined
}
if (shellLaunch) {
Object.assign(finalEnv, shellLaunch.env)
shellArgs = shellLaunch.args ?? shellArgs
shellReadyLaunch = args.command ? shellLaunch : null
}
}
const requestedEnv = args.env
expandWindowsPathEnvironmentVariables(finalEnv)
promoteAgentTeamsShimPath(
@@ -889,6 +852,41 @@ export class LocalPtyProvider implements IPtyProvider {
dropInheritedOrcaFishHistory(finalEnv)
}
if (!wslInfo && process.platform !== 'win32') {
// Why after history injection: the wrapper is what repairs a worktree
// HISTFILE that the system zshrc clobbers, so the decision to wrap has to
// see whether this spawn actually injected one.
const isCodexStartupCommand = startupAgentRecognition?.agent === 'codex'
// Why: payload-bearing Codex startup can be lost to rc-file noise; plain Codex stays markerless for startup speed.
const waitsForShellReady =
Boolean(args.command) &&
(!isCodexStartupCommand ||
shouldUseShellReadyStartupDelivery({
command: args.command as string,
startupCommandDelivery: args.startupCommandDelivery
}))
// Why delete: ORCA_SHELL_FEATURES is Orca-owned, and only the launch
// config below may name features for this shell.
delete finalEnv.ORCA_SHELL_FEATURES
getFallbackShellReadyConfig = (shell) =>
getShellLaunchConfig(
shell,
selectShellStartupFeatures({
shellPath: shell,
env: finalEnv,
hasStartupCommand: Boolean(args.command),
waitsForShellReady,
// Why identical: the identity marker exists so the readiness
// handshake can bind output to the right shell PID.
emitsStartupIdentity: waitsForShellReady
})
)
const shellLaunch = getFallbackShellReadyConfig(shellPath)
Object.assign(finalEnv, shellLaunch.env)
shellArgs = shellLaunch.args ?? shellArgs
shellReadyLaunch = args.command ? shellLaunch : null
}
await prepareLocalPtySpawn(id)
if (args.signal?.aborted) {
throw new Error('client_disconnected')
@@ -7,12 +7,19 @@
import { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from '../bash-prompt-command-composition'
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
import { BASH_FEATURE_CHANNEL_BLOCK, SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
import { SHELL_READY_MARKER_ESCAPED } from './local-pty-shell-ready-wrapper-root'
export function getBashShellReadyRcfileContent(): string {
return `# Orca bash shell-ready wrapper
${BASH_FEATURE_CHANNEL_BLOCK}
${SHELL_STARTUP_IDENTITY_MARKER_BLOCK}
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -107,7 +114,7 @@ ${BASH_PROMPT_COMMAND_COMPOSITION_BLOCK}
__orca_prepend_prompt_command "__orca_osc133_precmd"
# Why: append the marker through PROMPT_COMMAND so it fires after the login
# startup files have rebuilt the prompt, without re-running user rc files.
if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
if [[ -n "$__orca_ready_marker" ]]; then
__orca_prompt_mark() {
printf "${SHELL_READY_MARKER_ESCAPED}"
}
@@ -1,6 +1,9 @@
import { afterEach, describe, vi } from 'vitest'
import { spawnSync } from 'node:child_process'
import type * as LocalPtyShellReadyModule from './local-pty-shell-ready'
import {
OVERLAY_ONLY_FEATURES,
STARTUP_COMMAND_FEATURES
} from '../shell-startup-launch-intent-fixtures'
// Why: can't import electron (bundled into the plain-node daemon-entry fork), so tests set the wrapper root via ORCA_USER_DATA_PATH instead of mocking app.
export function setTestUserDataPath(path: string): void {
@@ -18,9 +21,18 @@ export function restoreUserDataPathAfterEach(): void {
})
}
export async function importFreshLocalPtyShellReady(): Promise<typeof LocalPtyShellReadyModule> {
export async function importFreshLocalPtyShellReady() {
vi.resetModules()
return import('./local-pty-shell-ready')
const module = await import('./local-pty-shell-ready')
// Why the two adapters: the module exposes one feature-driven entry point, and
// these are the two launch intents the call sites select between.
return {
...module,
getShellReadyLaunchConfig: (shell: string) =>
module.getShellLaunchConfig(shell, STARTUP_COMMAND_FEATURES),
getMarkerlessShellLaunchConfig: (shell: string) =>
module.getShellLaunchConfig(shell, OVERLAY_ONLY_FEATURES)
}
}
/** Plain suite call only; annotated because the inferred `describe.skip` type names vitest-runner internals. */
@@ -11,7 +11,8 @@ import {
} from './local-pty-shell-ready-test-harness'
// Why: rcfile content is pure, so a static import is equivalent to the fresh-module import used for wrapper writing.
import { getBashShellReadyRcfileContent } from './local-pty-shell-ready-bash-rcfile'
import { getZshShellReadyRcfileContent } from './local-pty-shell-ready-wrapper-generation'
import { getZshShellReadyWrapperFiles } from './local-pty-shell-ready-wrapper-generation'
import { makeUserZdotdir } from '../zsh-user-config-dir-fixture'
restoreUserDataPathAfterEach()
@@ -95,7 +96,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
function expectFinalZdotdirRestoreContext(content: string) {
expect(content).toContain("after Orca's last wrapper file has loaded")
expect(content).toContain('export ZDOTDIR="$_orca_home"')
expect(content).toContain('export ZDOTDIR="$REPLY"')
}
describePosix('local PTY shell-ready launch config', () => {
@@ -127,7 +128,9 @@ describePosix('local PTY shell-ready launch config', () => {
const config = getShellReadyLaunchConfig('/opt/homebrew/bin/fish')
expect(config.supportsReadyMarker).toBe(true)
expect(config.env).toEqual({ ORCA_SHELL_READY_MARKER: '1' })
// Why empty: fish's selection is baked into the init command text, so it
// needs no exported feature variable at all.
expect(config.env).toEqual({})
expect(config.args?.slice(0, 2)).toEqual(['-l', '-C'])
const init = config.args?.[2] ?? ''
expect(init).toContain('--on-event fish_prompt')
@@ -173,14 +176,15 @@ describePosix('local PTY shell-ready launch config', () => {
const previousZdotdir = process.env.ZDOTDIR
const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR
const previousHome = process.env.HOME
const userZdotdir = makeUserZdotdir(userDataPath, '.config', 'zsh')
process.env.ZDOTDIR = '/some/other/orca/shell-ready/zsh'
process.env.ORCA_ORIG_ZDOTDIR = '/Users/alice/.config/zsh'
process.env.HOME = '/Users/alice'
process.env.ORCA_ORIG_ZDOTDIR = userZdotdir
process.env.HOME = userDataPath
try {
const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe('/Users/alice')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe(userDataPath)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -240,15 +244,18 @@ describePosix('local PTY shell-ready launch config', () => {
const zprofile = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zprofile'), 'utf8')
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshrc'), 'utf8')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
expect(zshenv).toContain('_orca_user_zdotdir="${_orca_spawn_orig_zdotdir:-$HOME}"')
expect(zshenv).toContain('__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"')
expect(zshenv).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
expect(zshenv).toContain('*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;')
expect(zshenv).toContain('""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;')
expect(zshenv).toContain('"$REPLY" == */shell-ready/zsh ]]; then')
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$REPLY"')
expectZdotdirSourceContext(zprofile, '.zprofile')
expectZdotdirSourceContext(zshrc, '.zshrc')
expectZdotdirSourceContext(zlogin, '.zlogin')
expectFinalZdotdirRestoreContext(zshrc)
expectFinalZdotdirRestoreContext(zlogin)
// Why .zshenv: the final restore is the last step of the single epilogue,
// which .zshrc (non-login) and .zlogin (login) each invoke once.
expectFinalZdotdirRestoreContext(zshenv)
expect(zshrc).toContain('[[ -o login ]] || __orca_shell_epilogue')
expect(zlogin).toContain('__orca_shell_epilogue')
})
it('owns zle-line-init for the shell-ready marker instead of an azhw hook', async () => {
@@ -256,14 +263,19 @@ describePosix('local PTY shell-ready launch config', () => {
getShellReadyLaunchConfig('/bin/zsh')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
expect(zlogin).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zlogin).toContain('__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"')
expect(zlogin).toContain('printf "\\033]777;orca-shell-ready\\007"')
// Why .zshenv: the widget registration lives in the epilogue, which .zlogin
// (login) and .zshrc (non-login) both call exactly once.
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
expect(zshenv).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zshenv).toContain('__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"')
expect(zshenv).toContain('printf "\\033]777;orca-shell-ready\\007"')
// Why: add-zle-hook-widget aborts its chain on a non-zero earlier hook (e.g. oh-my-zsh vi-mode); don't register the marker through it.
expect(zlogin).not.toContain('add-zle-hook-widget line-init')
expect(zshenv).not.toContain('add-zle-hook-widget line-init')
// Why: re-source guard — skip re-capturing when already the bound widget so the prior chain survives a second source.
expect(zlogin).toContain('== "user:__orca_prompt_mark"')
expect(zshenv).toContain('== "user:__orca_prompt_mark"')
expect(readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')).toContain(
'__orca_shell_epilogue'
)
})
it('writes wrappers without restoring Pi/OMP homes after user startup files', async () => {
@@ -271,8 +283,10 @@ describePosix('local PTY shell-ready launch config', () => {
getShellReadyLaunchConfig('/bin/zsh')
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshrc'), 'utf8')
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
// Why one file: every restore now lives in the single epilogue in .zshenv,
// which .zshrc and .zlogin each invoke on their own startup path.
const zshrc = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
const zlogin = zshrc
const bashRc = getBashShellReadyRcfileContent()
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
@@ -314,7 +328,8 @@ describePosix('local PTY shell-ready launch config', () => {
// Why: issue #2422 — without OSC 133 C/D markers, bash sessions kept the worktree spinner "working" ~30min after the agent exited.
it('emits OSC 133 C/D markers in the bash wrapper so agent exit cleanup fires', async () => {
const bashRc = getBashShellReadyRcfileContent()
const zshRc = getZshShellReadyRcfileContent()
// Why .zshenv: the zsh markers live in the epilogue, behind `markers`.
const zshRc = getZshShellReadyWrapperFiles().zshenv
// The exact escape sequences terminal-command-lifecycle parses (133;D = finished, 133;C = start).
expect(bashRc).toContain('printf "\\033]133;D;%s\\007"')
@@ -323,9 +338,10 @@ describePosix('local PTY shell-ready launch config', () => {
expect(bashRc).toContain('__orca_prepend_prompt_command "__orca_osc133_precmd"')
// DEBUG is armed after setup; lastIndexOf skips the dispatcher's conditional re-arm.
expect(bashRc.lastIndexOf("trap '__orca_osc133_preexec' DEBUG")).toBeGreaterThan(
bashRc.indexOf('if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then')
bashRc.indexOf('if [[ -n "$__orca_ready_marker" ]]; then')
)
// Sanity: zsh wrapper emits the same markers — both branches must stay in sync.
// Sanity: zsh wrapper emits the same markers — both branches must stay in
// sync. They live in the .zshenv epilogue, behind the `markers` feature.
expect(zshRc).toContain('printf "\\033]133;D;%s\\007"')
expect(zshRc).toContain('printf "\\033]133;C\\007"')
})
@@ -411,12 +427,13 @@ describePosix('local PTY shell-ready launch config', () => {
it('preserves a real inherited ZDOTDIR as ORCA_ORIG_ZDOTDIR', async () => {
const previousZdotdir = process.env.ZDOTDIR
process.env.ZDOTDIR = '/Users/alice/.config/zsh'
const userZdotdir = makeUserZdotdir(userDataPath, '.config', 'zsh')
process.env.ZDOTDIR = userZdotdir
try {
const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe('/Users/alice/.config/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
expect(config.env.ORCA_ZSHENV_SOURCE_DIR).toBe(userZdotdir)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -474,11 +491,12 @@ describePosix('local PTY shell-ready launch config', () => {
it('preserves ZDOTDIR that contains /shell-ready/zsh as a substring but does not end with it', async () => {
const previousZdotdir = process.env.ZDOTDIR
process.env.ZDOTDIR = '/Users/alice/shell-ready/zsh-custom'
const userZdotdir = makeUserZdotdir(userDataPath, 'shell-ready', 'zsh-custom')
process.env.ZDOTDIR = userZdotdir
try {
const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady()
const config = getShellReadyLaunchConfig('/bin/zsh')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/shell-ready/zsh-custom')
expect(config.env.ORCA_ORIG_ZDOTDIR).toBe(userZdotdir)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
@@ -497,11 +515,13 @@ describePosix('local PTY shell-ready launch config', () => {
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
expect(zshenv).toContain('unset ZDOTDIR')
expect(zshenv).toContain('_orca_zshenv_source_dir="${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"')
expect(zshenv).toContain(
'__orca_resolve_inherited_config_dir "${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"'
)
expect(zshenv).toContain('source "${_orca_zshenv_path}"')
expect(zshenv).toContain('_orca_discovered_zdotdir="${ZDOTDIR:-}"')
expect(zshenv).toContain(
'export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"'
'__orca_resolve_user_config_dir "${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"'
)
expect(zshenv).toContain('export ZDOTDIR=')
})
@@ -515,7 +535,9 @@ describePosix('local PTY shell-ready launch config', () => {
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
// Save spawn-env value before sourcing user .zshenv
expect(zshenv).toContain('_orca_spawn_orig_zdotdir="${ORCA_ORIG_ZDOTDIR:-}"')
expect(zshenv.indexOf('_orca_user_zdotdir="$REPLY"')).toBeLessThan(
zshenv.indexOf('source "${_orca_zshenv_path}"')
)
// Fallback chain: discovered → normalized spawn-env path → HOME
expect(zshenv).toContain('${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}')
@@ -4,11 +4,13 @@
* Why: the wrappers emit an OSC 777 marker after startup files finish, which the
* readiness scanner watches for before a startup command is written.
*/
import { mkdirSync, writeFileSync, chmodSync } from 'node:fs'
import {
buildZshStartupWrapperFiles,
type ZshStartupWrapperFiles,
type ZshStartupWrapperSpec
} from '../zsh-startup-wrapper-builder'
import { writeShellWrapperFiles } from '../shell-wrapper-file-writer'
import { ZSH_WRAPPER_DIR_MARKER_CONTENT, ZSH_WRAPPER_DIR_MARKER_FILE } from '../shell-templates'
import { getBashShellReadyRcfileContent } from './local-pty-shell-ready-bash-rcfile'
import {
getShellReadyWrapperRoot,
@@ -26,73 +28,53 @@ function getLocalZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec {
readyMarkerEscaped: SHELL_READY_MARKER_ESCAPED,
osc133CommandMarkers: true,
skipUserZshrcWhenHomeIsWrapperDir: true,
interactiveRestoreComment:
overlayRestoreComment:
"# 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 {
export function getZshShellReadyWrapperFiles(): ZshStartupWrapperFiles {
return buildZshStartupWrapperFiles(getLocalZshWrapperSpec(`${getShellReadyWrapperRoot()}/zsh`))
.zshrc
}
export function ensureShellReadyWrappersAt(root = getShellReadyWrapperRoot()): void {
/** True when every wrapper file is present and non-empty afterwards. */
export function ensureShellReadyWrappersAt(root = getShellReadyWrapperRoot()): boolean {
if (didEnsureShellReadyWrappers && shellReadyWrappersExist(root)) {
return
return true
}
didEnsureShellReadyWrappers = true
const zshDir = `${root}/zsh`
const bashDir = `${root}/bash`
const zsh = buildZshStartupWrapperFiles(getLocalZshWrapperSpec(zshDir))
const bashRc = getBashShellReadyRcfileContent()
const files = [
[`${zshDir}/.zshenv`, zsh.zshenv],
[`${zshDir}/.zprofile`, zsh.zprofile],
[`${zshDir}/.zshrc`, zsh.zshrc],
[`${zshDir}/.zlogin`, zsh.zlogin],
[`${bashDir}/rcfile`, bashRc]
] as const
try {
for (const [path, content] of files) {
const dir = path.slice(0, path.lastIndexOf('/'))
mkdirSync(dir, { recursive: true })
writeFileSync(path, content, 'utf8')
chmodSync(path, 0o644)
}
} catch (error) {
// Why: degrade gracefully — a failed wrapper (read-only FS, perms, disk) just means no ready marker, PTY stays usable.
const errorMessage =
error instanceof Error
? `${error.message} (${(error as NodeJS.ErrnoException).code || 'unknown'})`
: String(error)
console.error(`[shell-ready] Failed to create wrapper files in ${root}: ${errorMessage}`)
console.error('[shell-ready] Shell will launch without wrapper (no shell-ready marker)')
// Reset the flag so next attempt will try again
const written = writeShellWrapperFiles(
[
[`${zshDir}/.zshenv`, zsh.zshenv],
[`${zshDir}/.zprofile`, zsh.zprofile],
[`${zshDir}/.zshrc`, zsh.zshrc],
[`${zshDir}/.zlogin`, zsh.zlogin],
[`${zshDir}/${ZSH_WRAPPER_DIR_MARKER_FILE}`, ZSH_WRAPPER_DIR_MARKER_CONTENT],
[`${root}/bash/rcfile`, getBashShellReadyRcfileContent()]
],
'[shell-ready]'
)
if (!written || !shellReadyWrappersExist(root)) {
// Why reset: the next launch retries instead of trusting a half-written tree.
didEnsureShellReadyWrappers = false
return false
}
return true
}
export function ensureShellReadyWrappers(): void {
export function ensureShellReadyWrappers(): boolean {
if (process.platform === 'win32') {
return
return false
}
ensureShellReadyWrappersAt()
return ensureShellReadyWrappersAt()
}
@@ -3,7 +3,8 @@
* wrappers emit — the shared contract between wrapper generation and shell launch.
*/
import { tmpdir } from 'node:os'
import { existsSync } from 'node:fs'
import { statSync } from 'node:fs'
import { ZSH_WRAPPER_DIR_MARKER_FILE } from '../shell-templates'
export const SHELL_READY_MARKER_ESCAPED = '\\033]777;orca-shell-ready\\007'
@@ -19,10 +20,24 @@ export function getRequiredShellReadyWrapperPaths(root = getShellReadyWrapperRoo
`${root}/zsh/.zprofile`,
`${root}/zsh/.zshrc`,
`${root}/zsh/.zlogin`,
`${root}/zsh/${ZSH_WRAPPER_DIR_MARKER_FILE}`,
`${root}/bash/rcfile`
]
}
/**
* Every wrapper file exists and has content.
*
* Why non-empty and not just present: a partial write (full disk, killed mid
* write) leaves a zero-byte .zshenv, and pointing ZDOTDIR at that directory
* makes zsh skip the user's entire configuration silently.
*/
export function shellReadyWrappersExist(root = getShellReadyWrapperRoot()): boolean {
return getRequiredShellReadyWrapperPaths(root).every((path) => existsSync(path))
return getRequiredShellReadyWrapperPaths(root).every((path) => {
try {
return statSync(path).size > 0
} catch {
return false
}
})
}
@@ -86,7 +86,11 @@ describePosix('live zsh subprocess tests', () => {
})
it('handles sudo -E where HOME and ZDOTDIR mismatch', async () => {
const userZdotdir = join('/home', 'alice', '.config', 'zsh')
// Why a real dir: an inherited ZDOTDIR only counts as the user's config
// root when it actually holds a zsh startup file.
const userZdotdir = join(testHome, '.config', 'zsh')
mkdirSync(userZdotdir, { recursive: true })
writeFileSync(join(userZdotdir, '.zshrc'), '')
const previousZdotdir = process.env.ZDOTDIR
const previousHome = process.env.HOME
+62 -54
View File
@@ -12,9 +12,19 @@ import {
} from '../powershell-osc133-bootstrap'
import { getFishCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getFishShellReadyInitCommand } from '../shell-templates'
import {
encodeShellStartupFeatures,
SHELL_STARTUP_FEATURE_ENV,
type ShellStartupFeature
} from '../shell-startup-features'
import {
resolveInheritedZdotdir,
resolveInheritedZshenvSourceDir
} from '../zsh-wrapper-dir-ownership'
import { ensureShellReadyWrappers } from './local-pty-shell-ready-wrapper-generation'
import {
getShellReadyWrapperRoot,
shellReadyWrappersExist,
SHELL_READY_MARKER_ESCAPED
} from './local-pty-shell-ready-wrapper-root'
export {
@@ -25,68 +35,77 @@ export {
} from '../shell-ready-marker-scanner'
export type { ShellReadyScanResult, ShellReadyScanState } from '../shell-ready-marker-scanner'
// Why: an Orca wrapper ZDOTDIR recurses; treat it as unset and fall back to HOME.
function normalizeOriginalZdotdirCandidate(value: string | undefined): string | null {
if (!value) {
return null
}
// Why: strip trailing slashes so wrapper paths match; `/` falls back to HOME.
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 || ''
}
export type ShellReadyLaunchConfig = {
args: string[] | null
env: Record<string, string>
supportsReadyMarker: boolean
}
function getWrappedShellLaunchConfig(
const UNWRAPPED: ShellReadyLaunchConfig = {
args: null,
env: {},
supportsReadyMarker: false
}
/** True when the wrapper tree is complete on disk right now. */
function wrapperTreeUsable(): boolean {
const ensured = ensureShellReadyWrappers()
return ensured && shellReadyWrappersExist()
}
/** Args that point bash at Orca's rcfile, or null when it is not usable. */
export function getBashWrapperLaunchArgs(): string[] | null {
return shellReadyWrappersExist()
? ['--rcfile', `${getShellReadyWrapperRoot()}/bash/rcfile`]
: null
}
/**
* The one launch-config entry point: args + env for a shell that should start
* with exactly `features` enabled. An empty selection is never wrapped.
*/
export function getShellLaunchConfig(
shellPath: string,
options: { emitReadyMarker: boolean }
features: readonly ShellStartupFeature[]
): ShellReadyLaunchConfig {
const shellName = pathWin32.basename(basename(shellPath)).toLowerCase()
if (shellName === 'zsh') {
ensureShellReadyWrappers()
if (features.length === 0) {
return UNWRAPPED
}
if (!wrapperTreeUsable()) {
// Why plain login zsh: ZDOTDIR pointed at an incomplete wrapper dir makes
// zsh skip the user's whole config. Losing Orca's features is recoverable.
return { args: ['-l'], env: {}, supportsReadyMarker: false }
}
return {
args: ['-l'],
env: {
ORCA_ORIG_ZDOTDIR: resolveOriginalZdotdir(),
ORCA_ZSHENV_SOURCE_DIR: resolveOriginalZshenvSourceDir(),
ORCA_ORIG_ZDOTDIR: resolveInheritedZdotdir(process.env),
ORCA_ZSHENV_SOURCE_DIR: resolveInheritedZshenvSourceDir(process.env),
ZDOTDIR: `${getShellReadyWrapperRoot()}/zsh`,
ORCA_SHELL_READY_MARKER: options.emitReadyMarker ? '1' : '0',
ORCA_SHELL_STARTUP_IDENTITY: options.emitReadyMarker ? '1' : '0'
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
},
supportsReadyMarker: options.emitReadyMarker
supportsReadyMarker: features.includes('ready')
}
}
if (shellName === 'bash') {
if (features.length === 0) {
return UNWRAPPED
}
ensureShellReadyWrappers()
const args = getBashWrapperLaunchArgs()
if (!args) {
return UNWRAPPED
}
return {
args: ['--rcfile', `${getShellReadyWrapperRoot()}/bash/rcfile`],
args,
env: {
ORCA_SHELL_READY_MARKER: options.emitReadyMarker ? '1' : '0',
ORCA_SHELL_STARTUP_IDENTITY: options.emitReadyMarker ? '1' : '0'
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
},
supportsReadyMarker: options.emitReadyMarker
supportsReadyMarker: features.includes('ready')
}
}
@@ -103,30 +122,19 @@ function getWrappedShellLaunchConfig(
}
}
// Why: mirrors daemon/shell-ready.ts; markerless fish stays unwrapped.
if (shellName === 'fish' && options.emitReadyMarker) {
// Why: mirrors daemon/shell-ready.ts; markerless fish stays unwrapped. The
// selection is baked into the init command, so fish needs no feature env var.
if (shellName === 'fish' && features.includes('ready')) {
return {
args: [
'-l',
'-C',
`${getFishShellReadyInitCommand(SHELL_READY_MARKER_ESCAPED)}\n${getFishCodexShellLaunchPreflight()}`
],
env: { ORCA_SHELL_READY_MARKER: '1' },
env: {},
supportsReadyMarker: true
}
}
return {
args: null,
env: {},
supportsReadyMarker: false
}
}
export function getShellReadyLaunchConfig(shellPath: string): ShellReadyLaunchConfig {
return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: true })
}
export function getMarkerlessShellLaunchConfig(shellPath: string): ShellReadyLaunchConfig {
return getWrappedShellLaunchConfig(shellPath, { emitReadyMarker: false })
return UNWRAPPED
}
@@ -384,8 +384,9 @@ describe('resolveWindowsShellLaunchArgs', () => {
// Why: typed OMP keeps its existing shell integration, while typed Prime
// commands must reach the user's binary without Orca rewriting argv.
const bashRcfile = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
const zshLogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
for (const wrapperFile of [bashRcfile, zshLogin]) {
// Why .zshenv: the omp wrapper is part of the epilogue defined there.
const zshEnv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
for (const wrapperFile of [bashRcfile, zshEnv]) {
expect(wrapperFile).toContain('command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"')
expect(wrapperFile).toContain('omp() { __orca_omp "$@"; }')
expect(wrapperFile).not.toContain('prime-agent()')
+2 -2
View File
@@ -6,7 +6,7 @@ import {
escapeWslShCommandForWindows,
quotePosixShell
} from '../../shared/wsl-login-shell-command'
import { getMarkerlessShellLaunchConfig } from './local-pty-shell-ready'
import { getBashWrapperLaunchArgs } from './local-pty-shell-ready'
import { ensureShellReadyWrappersAt } from './local-pty-shell-ready-wrapper-generation'
import {
encodePowerShellCommand,
@@ -34,7 +34,7 @@ function getGitBashLaunchCommand(codexLaunchPreflightCommand?: string): string {
}
ensureShellReadyWrappersAt()
const wrapperArgs = getMarkerlessShellLaunchConfig('bash').args
const wrapperArgs = getBashWrapperLaunchArgs()
if (!wrapperArgs) {
return GIT_BASH_UTF8_LOGIN_COMMAND
}
@@ -0,0 +1,312 @@
/**
* The feature channel's two load-bearing guarantees:
*
* 1. Leak-proof — the selection is a pure function of spawn env + launch intent,
* and the variable that carries it is destroyed by the wrapper before the
* user's own config runs. Neither an inherited value nor a stale one in
* `process.env` can turn a feature on or off for a shell or its children.
* (#15197 shipped exported switches twice and re-broke #11146 and agent
* status both times.)
* 2. Behaviour parity — a pane wrapped only because Orca injected a worktree
* HISTFILE runs exactly what an unwrapped pane runs, with no OSC 133.
*/
import { execFileSync, spawnSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { selectShellStartupFeatures } from './shell-startup-features'
import { ZSH_WRAPPER_DIR_MARKER_FILE } from './shell-templates'
import {
importFreshLocalPtyShellReady,
restoreUserDataPathAfterEach,
setTestUserDataPath
} from './providers/local-pty-shell-ready-test-harness'
restoreUserDataPathAfterEach()
const hasZsh = process.platform !== 'win32' && spawnSync('zsh', ['--version']).status === 0
const ZSH_PATH = hasZsh
? (spawnSync('sh', ['-c', 'command -v zsh'], { encoding: 'utf8' }).stdout || '').trim()
: ''
const itWithZsh = hasZsh ? it : it.skip
const describePosix = process.platform === 'win32' ? describe.skip : describe
const PLAIN_PANE = {
hasStartupCommand: false,
waitsForShellReady: false,
emitsStartupIdentity: false
}
describe('shell startup feature selection', () => {
it('ignores an inherited ORCA_SHELL_FEATURES in the spawn env', () => {
// Why: the value a parent shell exported is not an input. If it were, a pane
// opened from another pane would inherit that pane's feature set.
expect(
selectShellStartupFeatures({
shellPath: '/bin/zsh',
env: { ORCA_SHELL_FEATURES: 'overlay,markers,ready,identity,history' },
...PLAIN_PANE
})
).toEqual([])
})
it('cannot be turned off for a pane that needs a feature', () => {
// Why: the allowlist is positive only — there is no value of the variable
// that suppresses anything, so an inherited one can never mean "less".
expect(
selectShellStartupFeatures({
shellPath: '/bin/zsh',
env: { ORCA_SHELL_FEATURES: '', ORCA_HISTFILE: '/tmp/wt/zsh_history' },
...PLAIN_PANE
})
).toEqual(['history'])
})
it('gives a history-only pane no command markers', () => {
// Why: markers would be new output on a pane that emitted none before.
expect(
selectShellStartupFeatures({
shellPath: '/bin/zsh',
env: { ORCA_HISTFILE: '/tmp/wt/zsh_history' },
...PLAIN_PANE
})
).not.toContain('markers')
})
it('keeps markers on a pane that already had them', () => {
expect(
selectShellStartupFeatures({
shellPath: '/bin/zsh',
env: { ORCA_CODEX_HOME: '/tmp/codex' },
...PLAIN_PANE
})
).toContain('markers')
})
it('does not wrap bash for history alone', () => {
// Why: bash has no system rc that clobbers HISTFILE, and `--rcfile` would
// replace its login startup-file chain with Orca's approximation.
expect(
selectShellStartupFeatures({
shellPath: '/bin/bash',
env: { ORCA_HISTFILE: '/tmp/wt/bash_history' },
...PLAIN_PANE
})
).toEqual([])
})
})
describePosix('zsh launch config', () => {
let userDataPath = ''
let previousFeatures: string | undefined
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-feature-channel-'))
setTestUserDataPath(userDataPath)
previousFeatures = process.env.ORCA_SHELL_FEATURES
})
afterEach(() => {
if (previousFeatures === undefined) {
delete process.env.ORCA_SHELL_FEATURES
} else {
process.env.ORCA_SHELL_FEATURES = previousFeatures
}
chmodSync(userDataPath, 0o755)
rmSync(userDataPath, { recursive: true, force: true })
})
it('publishes exactly the selected features, whatever process.env holds', async () => {
process.env.ORCA_SHELL_FEATURES = 'overlay,markers,ready,identity'
const { getShellLaunchConfig } = await importFreshLocalPtyShellReady()
const config = getShellLaunchConfig('/bin/zsh', ['history'])
expect(config.env.ORCA_SHELL_FEATURES).toBe('history')
})
it('falls back to plain login zsh when the wrapper tree cannot be written', async () => {
// Why: pointing ZDOTDIR at an unwritten wrapper dir makes zsh skip the
// user's entire configuration — silently, and for every future pane.
chmodSync(userDataPath, 0o500)
if (
spawnSync('sh', ['-c', `touch ${JSON.stringify(join(userDataPath, 'probe'))}`]).status === 0
) {
return // running with write access regardless of mode (e.g. root)
}
const { getShellLaunchConfig } = await importFreshLocalPtyShellReady()
const config = getShellLaunchConfig('/bin/zsh', ['history'])
expect(config).toEqual({
args: ['-l'],
env: {},
supportsReadyMarker: false
})
})
it('does not treat another terminal’s hijacked ZDOTDIR as the user config dir', async () => {
// Why: Orca can be launched from a terminal that already owns ZDOTDIR. Only
// a dir Orca can prove is its own, or one holding no zsh startup file at
// all, may be rejected — never a vendor guessed at by name.
const home = mkdtempSync(join(tmpdir(), 'orca-stacked-zdotdir-'))
const foreignWrapper = join(home, 'other-terminal', 'zsh')
mkdirSync(foreignWrapper, { recursive: true })
const previousZdotdir = process.env.ZDOTDIR
const previousHome = process.env.HOME
process.env.ZDOTDIR = foreignWrapper
process.env.HOME = home
try {
const { getShellLaunchConfig } = await importFreshLocalPtyShellReady()
// Empty dir: not a config root whoever wrote it.
expect(getShellLaunchConfig('/bin/zsh', ['history']).env.ORCA_ORIG_ZDOTDIR).toBe(home)
// Stamped as Orca-owned: rejected by positive identification, even though
// the path shape is not one of Orca's.
writeFileSync(join(foreignWrapper, '.zshrc'), '')
writeFileSync(join(foreignWrapper, ZSH_WRAPPER_DIR_MARKER_FILE), '')
const stamped = await importFreshLocalPtyShellReady()
expect(stamped.getShellLaunchConfig('/bin/zsh', ['history']).env.ORCA_ORIG_ZDOTDIR).toBe(home)
// A real user config dir still round-trips.
rmSync(join(foreignWrapper, ZSH_WRAPPER_DIR_MARKER_FILE))
const real = await importFreshLocalPtyShellReady()
expect(real.getShellLaunchConfig('/bin/zsh', ['history']).env.ORCA_ORIG_ZDOTDIR).toBe(
foreignWrapper
)
} finally {
if (previousZdotdir === undefined) {
delete process.env.ZDOTDIR
} else {
process.env.ZDOTDIR = previousZdotdir
}
if (previousHome === undefined) {
delete process.env.HOME
} else {
process.env.HOME = previousHome
}
rmSync(home, { recursive: true, force: true })
}
})
})
/** A temp HOME whose four zsh startup files each announce that they ran. */
function makeUserHome(): string {
const home = mkdtempSync(join(tmpdir(), 'orca-feature-home-'))
for (const file of ['.zshenv', '.zprofile', '.zshrc', '.zlogin']) {
writeFileSync(join(home, file), `print -r -- "RAN=${file}"\n`)
}
return home
}
describePosix('history-only pane in a real zsh', () => {
let userDataPath = ''
let home = ''
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-feature-channel-live-'))
setTestUserDataPath(userDataPath)
home = makeUserHome()
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
rmSync(home, { recursive: true, force: true })
})
const OBSERVABLES =
'print -r -- "PRECMD=${precmd_functions[*]}"; ' +
'print -r -- "PREEXEC=${preexec_functions[*]}"; ' +
'print -r -- "LINEINIT=${widgets[zle-line-init]:-none}"; ' +
'print -r -- "ZDOTDIR=$ZDOTDIR"; ' +
'print -r -- "FEATURES=[${ORCA_SHELL_FEATURES:-}]"; ' +
'print -r -- "ORCA_HISTFILE=[${ORCA_HISTFILE:-}]"'
async function launchHistoryOnly(): Promise<{
args: string[]
env: Record<string, string>
zdotdir: string
}> {
const scoped = join(home, 'orca-history', 'zsh_history')
const spawnEnv: Record<string, string> = {
HOME: home,
HISTFILE: scoped,
ORCA_HISTFILE: scoped
}
const features = selectShellStartupFeatures({
shellPath: ZSH_PATH,
env: spawnEnv,
...PLAIN_PANE
})
expect(features).toEqual(['history'])
const { getShellLaunchConfig } = await importFreshLocalPtyShellReady()
const launch = getShellLaunchConfig(ZSH_PATH, features)
return {
args: launch.args ?? ['-l'],
env: {
PATH: '/usr/bin:/bin',
...spawnEnv,
...launch.env,
// Why override: the launch config reads the real process env, and this
// run must resolve the user's config against the sandbox home.
ORCA_ORIG_ZDOTDIR: home,
ORCA_ZSHENV_SOURCE_DIR: home
},
zdotdir: launch.env.ZDOTDIR
}
}
itWithZsh('unsets the feature channel for the shell and for a grandchild', async () => {
const { args, env } = await launchHistoryOnly()
const output = execFileSync(
ZSH_PATH,
[
...args,
'-i',
'-c',
`print -r -- "SELF=[\${ORCA_SHELL_FEATURES:-}]"; zsh -c 'printenv ORCA_SHELL_FEATURES; print -r -- CHILD_CLEAN'`
],
{ encoding: 'utf8', timeout: 20_000, env }
)
expect(output).toContain('SELF=[]')
expect(output).toContain('CHILD_CLEAN')
// A grandchild that saw the variable would have printed its value first.
expect(output).not.toContain('history')
})
itWithZsh('emits no OSC 133 and matches an unwrapped pane', async () => {
const { args, env } = await launchHistoryOnly()
const wrapped = execFileSync(ZSH_PATH, [...args, '-i', '-c', OBSERVABLES], {
encoding: 'utf8',
timeout: 20_000,
env
})
const unwrapped = execFileSync(ZSH_PATH, ['-l', '-i', '-c', OBSERVABLES], {
encoding: 'utf8',
timeout: 20_000,
env: { PATH: '/usr/bin:/bin', HOME: home }
})
// eslint-disable-next-line no-control-regex
expect(wrapped).not.toMatch(/]133;/)
expect(wrapped).toContain('PRECMD=\n')
expect(wrapped).toContain('LINEINIT=none')
// Startup files run in the same order with the same hooks, so the pane is
// observably what it was before it got wrapped.
const comparable = (output: string): string[] =>
output
.split('\n')
.filter((line) => !line.startsWith('ORCA_HISTFILE=') && !line.startsWith('ZDOTDIR='))
expect(comparable(wrapped)).toEqual(comparable(unwrapped))
// The one carried-over difference, unchanged from every pane Orca already
// wrapped: ZDOTDIR ends up explicitly set to the user's config dir, which
// is the value zsh itself defaults to when it is unset.
expect(wrapped).toContain(`ZDOTDIR=${home}`)
expect(unwrapped).toContain('ZDOTDIR=\n')
})
})
+95
View File
@@ -0,0 +1,95 @@
/**
* The single env variable Orca uses to tell a launched shell which startup
* features its wrapper should turn on, plus the pure selection that fills it.
*
* Why a positive allowlist the wrapper destroys before anything else runs:
* every earlier switch was a negative, exported one (`ORCA_SHELL_READY_MARKER=0`,
* `ORCA_SHELL_COMMAND_MARKERS=0`). Those live in the pane's PTY env, so every
* child inherits them — a pane launched with a feature suppressed suppressed it
* for an Orca started from that pane too. With an allowlist, an inherited or
* stale value can only ever mean *fewer* features, never more, and the wrapper
* unsets it before the user's own config (or anything it spawns) can see it.
*/
export const SHELL_STARTUP_FEATURE_ENV = 'ORCA_SHELL_FEATURES'
export const SHELL_STARTUP_FEATURES = [
'overlay',
'history',
'markers',
'ready',
'identity'
] as const
export type ShellStartupFeature = (typeof SHELL_STARTUP_FEATURES)[number]
/** Spawn-env keys that mean this pane carries an Orca overlay the wrapper must re-apply. */
const OVERLAY_ENV_KEYS = [
'ORCA_OPENCODE_CONFIG_DIR',
'ORCA_MIMOCODE_HOME',
'ORCA_OMP_STATUS_EXTENSION',
'ORCA_CODEX_HOME',
'ORCA_AGENT_TEAMS_SHIM_DIR',
'ORCA_REMOTE_CLI_BIN_DIR'
] as const
export type ShellStartupFeatureInput = {
/** Path (or bare name) of the shell being launched. */
shellPath: string
/** The env this spawn will hand the shell — never `process.env`. */
env: Record<string, string | undefined>
/** True when Orca will deliver a startup command into this pane. */
hasStartupCommand: boolean
/** True when that delivery waits for the wrapper's OSC 777 readiness marker. */
waitsForShellReady: boolean
/** True when Orca needs the shell to announce its PID at startup. */
emitsStartupIdentity: boolean
}
function shellName(shellPath: string): string {
return shellPath.replace(/\\/g, '/').split('/').pop()?.toLowerCase() ?? ''
}
/**
* Pure function of spawn env + launch intent. Nothing here reads
* `ORCA_SHELL_FEATURES`, so a value inherited from a parent shell cannot
* enable or disable anything for the shell Orca is about to launch.
*/
export function selectShellStartupFeatures(input: ShellStartupFeatureInput): ShellStartupFeature[] {
const overlay = OVERLAY_ENV_KEYS.some((key) => Boolean(input.env[key]))
// Exactly the panes Orca wrapped before history widened wrapping.
const wrappedBefore = overlay || input.hasStartupCommand
const ready = input.waitsForShellReady
// Why zsh only: the unguarded HISTFILE assignment lives in the *system zshrc*.
// bash has no equivalent, and wrapping bash for history alone would swap its
// login startup-file chain for Orca's approximation of one.
// Why also when Orca injected nothing: any wrapped pane has Orca's ZDOTDIR in
// place while the system zshrc runs, so the clobbered value it derives lands
// inside Orca's wrapper dir and has to be repaired the same way.
const history =
shellName(input.shellPath) === 'zsh' && (Boolean(input.env.ORCA_HISTFILE) || wrappedBefore)
const features: ShellStartupFeature[] = []
if (overlay) {
features.push('overlay')
}
if (history) {
features.push('history')
}
// Why gated on wrappedBefore: a pane wrapped only for history must stay
// observably identical to the unwrapped pane it was before this change.
if (wrappedBefore) {
features.push('markers')
}
if (ready) {
features.push('ready')
}
if (input.emitsStartupIdentity) {
features.push('identity')
}
return features
}
export function encodeShellStartupFeatures(features: readonly ShellStartupFeature[]): string {
return features.join(',')
}
@@ -0,0 +1,25 @@
/**
* The two launch intents Orca's call sites choose between, resolved through the
* real selector so tests cannot drift from the decision production makes.
*
* Test support only; nothing under src/main imports this at runtime.
*/
import { selectShellStartupFeatures } from './shell-startup-features'
/** A pane Orca will write a startup command into. */
export const STARTUP_COMMAND_FEATURES = selectShellStartupFeatures({
shellPath: 'zsh',
env: {},
hasStartupCommand: true,
waitsForShellReady: true,
emitsStartupIdentity: true
})
/** A pane carrying an Orca overlay but no startup command. */
export const OVERLAY_ONLY_FEATURES = selectShellStartupFeatures({
shellPath: 'zsh',
env: { ORCA_CODEX_HOME: '/tmp/orca-codex-home' },
hasStartupCommand: false,
waitsForShellReady: false,
emitsStartupIdentity: false
})
+117 -91
View File
@@ -1,24 +1,77 @@
// Why: local PTYs and the daemon/SSH path must use identical ZDOTDIR discovery;
// small drift here breaks different terminal transports in different ways.
import { SHELL_STARTUP_FEATURE_ENV } from './shell-startup-features'
function quotePosixSingle(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `if [[ "\${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\\033]777;orca-shell-start:%s\\007" "$$"
fi`
/** Basename of the file every Orca-generated zsh wrapper dir is stamped with. */
export const ZSH_WRAPPER_DIR_MARKER_FILE = '.orca-shell-wrapper'
export const ZSH_WRAPPER_DIR_MARKER_CONTENT = `# Orca-generated zsh startup wrapper directory.
# Its presence is how Orca recognises its own wrapper dir instead of treating it
# as the user's ZDOTDIR. Do not edit; the whole directory is regenerated.
`
/**
* The first executable lines of every zsh wrapper: read the feature allowlist,
* then destroy the variable.
*
* Why destroy it here: `_orca_shell_features` is a plain (non-exported) shell
* variable, so it survives .zshenv -> .zprofile -> .zshrc -> .zlogin in this
* process but physically cannot reach a child. Unsetting before the user's own
* .zshenv is sourced means nothing the user's config spawns can see or inherit
* Orca's feature selection.
*/
export const ZSH_FEATURE_CHANNEL_BLOCK = `typeset -ga _orca_shell_features
_orca_shell_features=(\${(s:,:)\${${SHELL_STARTUP_FEATURE_ENV}:-}})
builtin unset ${SHELL_STARTUP_FEATURE_ENV}
__orca_has_feature() { (( \${_orca_shell_features[(Ie)$1]} )) }`
/** The bash rcfile equivalent of ZSH_FEATURE_CHANNEL_BLOCK. */
export const BASH_FEATURE_CHANNEL_BLOCK = `_orca_shell_features=",\${${SHELL_STARTUP_FEATURE_ENV}:-},"
builtin unset ${SHELL_STARTUP_FEATURE_ENV}
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }`
// Why one line usable by both languages: __orca_has_feature is defined with the
// same name and semantics in the zsh and bash channel blocks above.
export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `__orca_has_feature identity && printf "\\033]777;orca-shell-start:%s\\007" "$$"`
/**
* Resolves the directory Orca should treat as the user's zsh config root.
*
* Why positive identification: Orca may only reject a config dir it can prove is
* its own. A stamped marker file (or Orca's own wrapper path shape, for
* wrappers written by older builds) is that proof. Guessing at other
* terminals' wrapper dirs by name never can be, so this never tries.
*/
export const ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK = `__orca_resolve_user_config_dir() {
REPLY="\${1:-}"
while [[ "$REPLY" == */ ]]; do REPLY="\${REPLY%/}"; done
if [[ -z "$REPLY" || -f "$REPLY/${ZSH_WRAPPER_DIR_MARKER_FILE}" || "$REPLY" == */shell-ready/zsh ]]; then
REPLY="$HOME"
fi
}
# Why stricter for an inherited value: Orca can be launched from a terminal that
# already pointed ZDOTDIR at its own wrapper dir, and a directory holding no zsh
# startup file at all is not the user's config root whoever wrote it.
__orca_resolve_inherited_config_dir() {
__orca_resolve_user_config_dir "\${1:-}"
[[ "$REPLY" == "$HOME" ]] && return 0
local _orca_startup_file
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
done
REPLY="$HOME"
}`
// 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,
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
/** The ZDOTDIR-discovery body of the wrapper .zshenv (no header, no epilogue). */
export function getZshEnvDiscoveryBody(zshDir: string): string {
return `# 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
# literal is unusable there and ZDOTDIR must be restored from this value.
# Derive it from the file being sourced (%x, zsh's internal script name) rather
@@ -35,26 +88,15 @@ fi
while [[ "\${_orca_wrapper_zdotdir_self:-}" == */ ]]; do
_orca_wrapper_zdotdir_self="\${_orca_wrapper_zdotdir_self%/}"
done
_orca_spawn_orig_zdotdir="\${ORCA_ORIG_ZDOTDIR:-}"
_orca_user_zdotdir="\${_orca_spawn_orig_zdotdir:-$HOME}"
_orca_zshenv_source_dir="\${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_path=""
unset ORCA_ZSHENV_SOURCE_DIR
# Normalize fallback and source roots before reading user .zshenv so nested
# Orca PTYs never source another Orca wrapper recursively.
while [[ "\${_orca_user_zdotdir}" == */ ]]; do
_orca_user_zdotdir="\${_orca_user_zdotdir%/}"
done
case "\${_orca_user_zdotdir}" in
""|*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;
esac
while [[ "\${_orca_zshenv_source_dir}" == */ ]]; do
_orca_zshenv_source_dir="\${_orca_zshenv_source_dir%/}"
done
case "\${_orca_zshenv_source_dir}" in
""|*/shell-ready/zsh) _orca_zshenv_source_dir="$HOME" ;;
esac
__orca_resolve_inherited_config_dir "\${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_user_zdotdir="$REPLY"
__orca_resolve_inherited_config_dir "\${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_source_dir="$REPLY"
unset ORCA_ZSHENV_SOURCE_DIR
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
# exports, functions, path/fpath typesets, and zsh options keep normal scope.
@@ -82,15 +124,10 @@ if [[ -n "\${_orca_discovered_zdotdir}" && ! -d "\${_orca_discovered_zdotdir}" ]
_orca_discovered_zdotdir=""
fi
export ORCA_ORIG_ZDOTDIR="\${_orca_discovered_zdotdir:-\${_orca_user_zdotdir:-$HOME}}"
while [[ "\${ORCA_ORIG_ZDOTDIR}" == */ ]]; do
ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR%/}"
done
case "\${ORCA_ORIG_ZDOTDIR}" in
""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
# exported is the user's by construction, whatever it happens to contain.
__orca_resolve_user_config_dir "\${_orca_discovered_zdotdir:-\${_orca_user_zdotdir:-$HOME}}"
export ORCA_ORIG_ZDOTDIR="$REPLY"
# Why: use :- after user .zshenv — a pathological unset under set -u must not
# abort the wrapper; empty falls through to the baked-literal branch.
@@ -99,30 +136,24 @@ if [[ -n "\${_orca_wrapper_zdotdir_self:-}" && -f "\${_orca_wrapper_zdotdir_self
else
export ZDOTDIR=${quotePosixSingle(zshDir)}
fi
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
`
}
/**
* The relay variant of the .zshenv above: it trusts the ZDOTDIR the remote
* The relay variant of the discovery body: 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.
* and is preserved 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
export function getZshOverlayEnvBody(zshDir: string): string {
return `__orca_resolve_inherited_config_dir "\${ORCA_ORIG_ZDOTDIR:-$HOME}"
export ORCA_ORIG_ZDOTDIR="$REPLY"
[[ -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
__orca_resolve_user_config_dir "\${ZDOTDIR:-\${ORCA_ORIG_ZDOTDIR:-$HOME}}"
export ORCA_USER_ZDOTDIR="$REPLY"
export ZDOTDIR=${quotePosixSingle(zshDir)}
`
}
@@ -136,11 +167,13 @@ export ZDOTDIR=${quotePosixSingle(zshDir)}
* wrapper dir the replacement lands INSIDE it. Per-worktree history was a
* silent no-op on the primary platform as a result (#11044).
*
* Emitted after the user's own startup file has been sourced, so it is the last
* word, exactly like the CODEX_HOME/OPENCODE_CONFIG_DIR restores beside it.
* `builtin unset ORCA_HISTFILE` is the root-cause fix for #11146: the variable
* cannot be inherited by anything the shell later spawns if it no longer exists
* once it has been consumed. HISTFILE itself stays exported.
*/
export const ZSH_HISTFILE_RESTORE_BLOCK = `if [[ -n "\${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
builtin unset ORCA_HISTFILE
elif [[ "\${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
@@ -163,10 +196,8 @@ export function getZshStartupFileSourceBlock(options: {
`-f "$_orca_home/${options.fileName}"`
].filter(Boolean)
return `_orca_home=${homeExpression}
case "\${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
return `__orca_resolve_user_config_dir ${homeExpression}
_orca_home="$REPLY"
if [[ ${checks.join(' && ')} ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -188,31 +219,28 @@ fi
// startup command on the pre-ready timeout. Instead, own zle-line-init: emit
// the marker first, then chain to whatever widget was installed before.
export function getZshShellReadyMarkerRegistrationBlock(escapedMarker: string): string {
return `if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "\${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( \${+widgets[zle-line-init]} )) && [[ "\${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="\${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "${escapedMarker}"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "\${__orca_prev_line_init_fn:-}" ]]; then
"\${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
return `# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "\${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( \${+widgets[zle-line-init]} )) && [[ "\${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="\${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
`
__orca_prompt_mark() {
printf "${escapedMarker}"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "\${__orca_prev_line_init_fn:-}" ]]; then
"\${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark`
}
// Why: fish has no ZDOTDIR-style wrapper dir, so the marker rides `--init-command`,
@@ -222,23 +250,21 @@ fi
// just *before* fish arms `?2004h`, which PostReadyFlushGate absorbs. `builtin
// printf` so a user-defined printf can't silently swallow the marker and send
// every launch to the ready timeout.
//
// Why no feature variable: the init command is composed per pane, so the
// selection is already baked into the text and nothing needs to be exported.
export function getFishShellReadyInitCommand(escapedMarker: string): string {
return `if test "$ORCA_SHELL_READY_MARKER" = 1
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "${escapedMarker}"
functions -e __orca_shell_ready_marker
end
return `function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "${escapedMarker}"
functions -e __orca_shell_ready_marker
end`
}
export function getZshFinalZdotdirRestoreBlock(homeExpression = '"${ORCA_ORIG_ZDOTDIR:-$HOME}"') {
return `_orca_home=${homeExpression}
case "\${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
return `__orca_resolve_user_config_dir ${homeExpression}
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
export ZDOTDIR="$REPLY"
unset _orca_home REPLY
`
}
+44
View File
@@ -0,0 +1,44 @@
/**
* Writes a generated shell wrapper tree atomically, and says whether it worked.
*
* Why the boolean matters: the launch config points ZDOTDIR at the wrapper dir.
* If a write failed — read-only FS, full disk, EACCES — the old code still
* pointed ZDOTDIR at that dir, so zsh found no .zshrc there and the user
* silently lost their entire configuration. Callers use the result to fall back
* to an unwrapped shell instead.
*/
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
export type ShellWrapperFile = readonly [path: string, content: string]
export function writeShellWrapperFiles(
files: readonly ShellWrapperFile[],
logPrefix: string
): boolean {
try {
for (const [path, content] of files) {
mkdirSync(dirname(path), { recursive: true })
// Why temp+rename: a shell reading the wrapper concurrently sees either
// the old file or the new one, never a truncated prefix of the new one.
const temporaryPath = `${path}.orca-tmp-${process.pid}`
try {
writeFileSync(temporaryPath, content, 'utf8')
chmodSync(temporaryPath, 0o644)
renameSync(temporaryPath, path)
} catch (error) {
rmSync(temporaryPath, { force: true })
throw error
}
}
return true
} catch (error) {
const errorMessage =
error instanceof Error
? `${error.message} (${(error as NodeJS.ErrnoException).code || 'unknown'})`
: String(error)
console.error(`${logPrefix} Failed to write shell wrapper files: ${errorMessage}`)
console.error(`${logPrefix} Shell will launch unwrapped`)
return false
}
}
@@ -10,9 +10,20 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { ensureShellReadyWrappersAt } from './providers/local-pty-shell-ready-wrapper-generation'
import { getShellReadyLaunchConfig as getDaemonShellReadyLaunchConfig } from './daemon/shell-ready'
import { getShellLaunchConfig as getDaemonShellLaunchConfig } from './daemon/shell-ready'
import { ensureOverlayRestoreWrappers } from '../relay/pty-shell-overlay-wrappers'
import { getShellReadyLaunchConfig as getLocalShellReadyLaunchConfig } from './providers/local-pty-shell-ready'
import { getShellLaunchConfig as getLocalShellLaunchConfig } from './providers/local-pty-shell-ready'
import { selectShellStartupFeatures } from './shell-startup-features'
// Why the real selector: the snapshots then pin what a startup-command pane
// actually launches with, not a hand-written feature list.
const STARTUP_COMMAND_FEATURES = selectShellStartupFeatures({
shellPath: 'zsh',
env: {},
hasStartupCommand: true,
waitsForShellReady: true,
emitsStartupIdentity: true
})
const WRAPPER_FILES = [
['zsh-zshenv', join('zsh', '.zshenv')],
@@ -70,7 +81,7 @@ describePosix('generated shell wrapper files', () => {
it('daemon wrappers', async () => {
process.env.ORCA_USER_DATA_PATH = root
getDaemonShellReadyLaunchConfig('/bin/zsh')
getDaemonShellLaunchConfig('/bin/zsh', STARTUP_COMMAND_FEATURES)
await expectWrapperFiles('daemon', join(root, 'shell-ready'))
})
@@ -81,8 +92,8 @@ describePosix('generated shell wrapper files', () => {
it('fish shell-ready init commands', async () => {
process.env.ORCA_USER_DATA_PATH = root
const local = getLocalShellReadyLaunchConfig('/usr/bin/fish')
const daemon = getDaemonShellReadyLaunchConfig('/usr/bin/fish')
const local = getLocalShellLaunchConfig('/usr/bin/fish', STARTUP_COMMAND_FEATURES)
const daemon = getDaemonShellLaunchConfig('/usr/bin/fish', STARTUP_COMMAND_FEATURES)
await expect(local.args?.[2]).toMatchFileSnapshot(snapshotPath('local', 'fish-init'))
await expect(daemon.args?.[2]).toMatchFileSnapshot(snapshotPath('daemon', 'fish-init'))
})
@@ -1,8 +1,14 @@
# Orca daemon bash shell-ready wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -84,7 +90,7 @@ __orca_osc133_precmd() {
# Why: emit the shell-ready marker here (not a trailing PROMPT_COMMAND entry)
# so a framework that must be last in PROMPT_COMMAND — bash-preexec — is not
# displaced by one of Orca's own hooks.
[[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]] && printf "\033]777;orca-shell-ready\007"
[[ -n "$__orca_ready_marker" ]] && printf "\033]777;orca-shell-ready\007"
return "$exit_code"
}
__orca_osc133_preexec() {
@@ -1,8 +1,6 @@
if test "$ORCA_SHELL_READY_MARKER" = 1
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
if test -n "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
function codex
@@ -1,8 +1,6 @@
# Orca daemon zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,97 +11,4 @@ if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
unset _orca_wrapper_zdotdir
fi
__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}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
__orca_shell_epilogue
@@ -1,8 +1,6 @@
# Orca daemon zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ -f "$_orca_home/.zprofile" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -1,8 +1,28 @@
# Orca daemon zsh shell-ready wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
typeset -ga _orca_shell_features
_orca_shell_features=(${(s:,:)${ORCA_SHELL_FEATURES:-}})
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
__orca_resolve_user_config_dir() {
REPLY="${1:-}"
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
REPLY="$HOME"
fi
}
# Why stricter for an inherited value: Orca can be launched from a terminal that
# already pointed ZDOTDIR at its own wrapper dir, and a directory holding no zsh
# startup file at all is not the user's config root whoever wrote it.
__orca_resolve_inherited_config_dir() {
__orca_resolve_user_config_dir "${1:-}"
[[ "$REPLY" == "$HOME" ]] && return 0
local _orca_startup_file
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
done
REPLY="$HOME"
}
# 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
# literal is unusable there and ZDOTDIR must be restored from this value.
@@ -20,26 +40,15 @@ fi
while [[ "${_orca_wrapper_zdotdir_self:-}" == */ ]]; do
_orca_wrapper_zdotdir_self="${_orca_wrapper_zdotdir_self%/}"
done
_orca_spawn_orig_zdotdir="${ORCA_ORIG_ZDOTDIR:-}"
_orca_user_zdotdir="${_orca_spawn_orig_zdotdir:-$HOME}"
_orca_zshenv_source_dir="${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_path=""
unset ORCA_ZSHENV_SOURCE_DIR
# Normalize fallback and source roots before reading user .zshenv so nested
# Orca PTYs never source another Orca wrapper recursively.
while [[ "${_orca_user_zdotdir}" == */ ]]; do
_orca_user_zdotdir="${_orca_user_zdotdir%/}"
done
case "${_orca_user_zdotdir}" in
""|*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;
esac
while [[ "${_orca_zshenv_source_dir}" == */ ]]; do
_orca_zshenv_source_dir="${_orca_zshenv_source_dir%/}"
done
case "${_orca_zshenv_source_dir}" in
""|*/shell-ready/zsh) _orca_zshenv_source_dir="$HOME" ;;
esac
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_user_zdotdir="$REPLY"
__orca_resolve_inherited_config_dir "${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_source_dir="$REPLY"
unset ORCA_ZSHENV_SOURCE_DIR
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
# exports, functions, path/fpath typesets, and zsh options keep normal scope.
@@ -67,15 +76,10 @@ if [[ -n "${_orca_discovered_zdotdir}" && ! -d "${_orca_discovered_zdotdir}" ]];
_orca_discovered_zdotdir=""
fi
export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
while [[ "${ORCA_ORIG_ZDOTDIR}" == */ ]]; do
ORCA_ORIG_ZDOTDIR="${ORCA_ORIG_ZDOTDIR%/}"
done
case "${ORCA_ORIG_ZDOTDIR}" in
""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
# exported is the user's by construction, whatever it happens to contain.
__orca_resolve_user_config_dir "${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
export ORCA_ORIG_ZDOTDIR="$REPLY"
# Why: use :- after user .zshenv — a pathological unset under set -u must not
# abort the wrapper; empty falls through to the baked-literal branch.
@@ -84,4 +88,122 @@ if [[ -n "${_orca_wrapper_zdotdir_self:-}" && -f "${_orca_wrapper_zdotdir_self:-
else
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
fi
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
__orca_shell_epilogue() {
(( $+_orca_epilogue_done )) && return 0
typeset -g _orca_epilogue_done=1
if __orca_has_feature overlay; then
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
__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
[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
fi
if __orca_has_feature history; then
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
builtin unset ORCA_HISTFILE
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
fi
if __orca_has_feature markers; then
__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[@]})
fi
if __orca_has_feature ready; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$REPLY"
unset _orca_home REPLY
unset _orca_shell_features
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
}
@@ -1,8 +1,6 @@
# Orca daemon zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,91 +11,5 @@ if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]
unset _orca_wrapper_zdotdir
fi
__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}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
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
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
fi
# Why: a login shell still has .zlogin to load; it runs the epilogue instead.
[[ -o login ]] || __orca_shell_epilogue
@@ -1,8 +1,14 @@
# Orca bash shell-ready wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -311,7 +317,7 @@ __orca_append_prompt_command() {
__orca_prepend_prompt_command "__orca_osc133_precmd"
# Why: append the marker through PROMPT_COMMAND so it fires after the login
# startup files have rebuilt the prompt, without re-running user rc files.
if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
if [[ -n "$__orca_ready_marker" ]]; then
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
}
@@ -1,8 +1,6 @@
if test "$ORCA_SHELL_READY_MARKER" = 1
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
if test -n "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
function codex
@@ -1,8 +1,6 @@
# Orca zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,97 +11,4 @@ if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
unset _orca_wrapper_zdotdir
fi
__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}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
__orca_shell_epilogue
@@ -1,8 +1,6 @@
# Orca zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ -f "$_orca_home/.zprofile" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -1,8 +1,28 @@
# Orca zsh shell-ready wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
typeset -ga _orca_shell_features
_orca_shell_features=(${(s:,:)${ORCA_SHELL_FEATURES:-}})
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
__orca_resolve_user_config_dir() {
REPLY="${1:-}"
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
REPLY="$HOME"
fi
}
# Why stricter for an inherited value: Orca can be launched from a terminal that
# already pointed ZDOTDIR at its own wrapper dir, and a directory holding no zsh
# startup file at all is not the user's config root whoever wrote it.
__orca_resolve_inherited_config_dir() {
__orca_resolve_user_config_dir "${1:-}"
[[ "$REPLY" == "$HOME" ]] && return 0
local _orca_startup_file
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
done
REPLY="$HOME"
}
# 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
# literal is unusable there and ZDOTDIR must be restored from this value.
@@ -20,26 +40,15 @@ fi
while [[ "${_orca_wrapper_zdotdir_self:-}" == */ ]]; do
_orca_wrapper_zdotdir_self="${_orca_wrapper_zdotdir_self%/}"
done
_orca_spawn_orig_zdotdir="${ORCA_ORIG_ZDOTDIR:-}"
_orca_user_zdotdir="${_orca_spawn_orig_zdotdir:-$HOME}"
_orca_zshenv_source_dir="${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_path=""
unset ORCA_ZSHENV_SOURCE_DIR
# Normalize fallback and source roots before reading user .zshenv so nested
# Orca PTYs never source another Orca wrapper recursively.
while [[ "${_orca_user_zdotdir}" == */ ]]; do
_orca_user_zdotdir="${_orca_user_zdotdir%/}"
done
case "${_orca_user_zdotdir}" in
""|*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;
esac
while [[ "${_orca_zshenv_source_dir}" == */ ]]; do
_orca_zshenv_source_dir="${_orca_zshenv_source_dir%/}"
done
case "${_orca_zshenv_source_dir}" in
""|*/shell-ready/zsh) _orca_zshenv_source_dir="$HOME" ;;
esac
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_user_zdotdir="$REPLY"
__orca_resolve_inherited_config_dir "${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_source_dir="$REPLY"
unset ORCA_ZSHENV_SOURCE_DIR
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
# exports, functions, path/fpath typesets, and zsh options keep normal scope.
@@ -67,15 +76,10 @@ if [[ -n "${_orca_discovered_zdotdir}" && ! -d "${_orca_discovered_zdotdir}" ]];
_orca_discovered_zdotdir=""
fi
export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
while [[ "${ORCA_ORIG_ZDOTDIR}" == */ ]]; do
ORCA_ORIG_ZDOTDIR="${ORCA_ORIG_ZDOTDIR%/}"
done
case "${ORCA_ORIG_ZDOTDIR}" in
""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
# exported is the user's by construction, whatever it happens to contain.
__orca_resolve_user_config_dir "${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
export ORCA_ORIG_ZDOTDIR="$REPLY"
# Why: use :- after user .zshenv — a pathological unset under set -u must not
# abort the wrapper; empty falls through to the baked-literal branch.
@@ -84,4 +88,122 @@ if [[ -n "${_orca_wrapper_zdotdir_self:-}" && -f "${_orca_wrapper_zdotdir_self:-
else
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
fi
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
__orca_shell_epilogue() {
(( $+_orca_epilogue_done )) && return 0
typeset -g _orca_epilogue_done=1
if __orca_has_feature overlay; then
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
__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
[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
fi
if __orca_has_feature history; then
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
builtin unset ORCA_HISTFILE
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
fi
if __orca_has_feature markers; then
__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[@]})
fi
if __orca_has_feature ready; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$REPLY"
unset _orca_home REPLY
unset _orca_shell_features
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
}
@@ -1,8 +1,6 @@
# Orca zsh shell-ready wrapper
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
_orca_home="$REPLY"
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,92 +11,5 @@ if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]
unset _orca_wrapper_zdotdir
fi
__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}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
# Why: a typed alias expands inside the shell, after pane launch prep.
__orca_codex_binary="$(command -v codex 2>/dev/null || :)"
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
codex() {
"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
command codex "$@"
}
fi
unset __orca_codex_binary
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
_orca_home="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
fi
# Why: a login shell still has .zlogin to load; it runs the epilogue instead.
[[ -o login ]] || __orca_shell_epilogue
@@ -1,8 +1,14 @@
# Orca relay bash overlay wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
_orca_shell_features=",${ORCA_SHELL_FEATURES:-},"
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -50,6 +56,7 @@ fi
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
builtin unset ORCA_HISTFILE
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
@@ -291,7 +298,7 @@ __orca_append_prompt_command() {
__orca_prepend_prompt_command "__orca_osc133_precmd"
# Why: SSH startup commands are renderer-delivered; emit the same internal
# readiness marker as local shells only when that delivery mode asks for it.
if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
if [[ -n "$__orca_ready_marker" ]]; then
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
}
@@ -1,8 +1,6 @@
# Orca relay zsh overlay wrapper
_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
_orca_home="$REPLY"
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,79 +11,4 @@ if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
unset _orca_wrapper_zdotdir
fi
# 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
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
if [[ "${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
__orca_shell_epilogue
@@ -1,8 +1,6 @@
# Orca relay zsh overlay wrapper
_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
_orca_home="$REPLY"
if [[ -f "$_orca_home/.zprofile" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -1,15 +1,113 @@
# Orca relay zsh overlay wrapper
if [[ "${ORCA_SHELL_STARTUP_IDENTITY:-0}" == "1" ]]; then
unset ORCA_SHELL_STARTUP_IDENTITY
printf "\033]777;orca-shell-start:%s\007" "$$"
fi
export ORCA_ORIG_ZDOTDIR="${ORCA_ORIG_ZDOTDIR:-$HOME}"
case "${ORCA_ORIG_ZDOTDIR%/}" in
*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
typeset -ga _orca_shell_features
_orca_shell_features=(${(s:,:)${ORCA_SHELL_FEATURES:-}})
builtin unset ORCA_SHELL_FEATURES
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
__orca_resolve_user_config_dir() {
REPLY="${1:-}"
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
REPLY="$HOME"
fi
}
# Why stricter for an inherited value: Orca can be launched from a terminal that
# already pointed ZDOTDIR at its own wrapper dir, and a directory holding no zsh
# startup file at all is not the user's config root whoever wrote it.
__orca_resolve_inherited_config_dir() {
__orca_resolve_user_config_dir "${1:-}"
[[ "$REPLY" == "$HOME" ]] && return 0
local _orca_startup_file
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
done
REPLY="$HOME"
}
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
export ORCA_ORIG_ZDOTDIR="$REPLY"
[[ -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
__orca_resolve_user_config_dir "${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
export ORCA_USER_ZDOTDIR="$REPLY"
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
__orca_shell_epilogue() {
(( $+_orca_epilogue_done )) && return 0
typeset -g _orca_epilogue_done=1
if __orca_has_feature overlay; 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
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
fi
if __orca_has_feature history; then
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
builtin unset ORCA_HISTFILE
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
fi
if __orca_has_feature ready; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( ${+widgets[zle-line-init]} )) && [[ "${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "\033]777;orca-shell-ready\007"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "${__orca_prev_line_init_fn:-}" ]]; then
"${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$REPLY"
unset _orca_home REPLY
unset _orca_shell_features
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
}
@@ -1,8 +1,6 @@
# Orca relay zsh overlay wrapper
_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
_orca_home="$REPLY"
if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
@@ -13,58 +11,5 @@ if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then
unset _orca_wrapper_zdotdir
fi
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
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
__orca_omp_should_skip_extension() {
case "${1:-}" in
help|--help|-h|--version|-v) return 0 ;;
__complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;;
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
command omp launch --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
else
command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"
fi
else
command omp "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
omp() { __orca_omp "$@"; }
fi
if [[ -n "${ORCA_HISTFILE:-}" ]]; then
HISTFILE="$ORCA_HISTFILE"
elif [[ "${HISTFILE:-}" == "$ZDOTDIR/.zsh_history" ]]; then
# Why also when Orca injected nothing: /etc/zshrc derived this from Orca's
# wrapper ZDOTDIR, so history would accumulate INSIDE the wrapper dir and the
# user's real history would be invisible — the plain #11044 bug, with no
# per-worktree scoping involved. Matching the exact clobbered value means a
# HISTFILE the user set deliberately is never touched.
HISTFILE="${ORCA_ORIG_ZDOTDIR:-$HOME}/.zsh_history"
fi
fi
if [[ ! -o login ]]; then
_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
case "${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
fi
# Why: a login shell still has .zlogin to load; it runs the epilogue instead.
[[ -o login ]] || __orca_shell_epilogue
+141 -43
View File
@@ -11,14 +11,18 @@
* Only a real zsh can show this: the string the wrapper emits looks correct
* either way, and the whole bug lives in what /etc/zshrc does between the spawn
* env and the first prompt.
*
* These tests drive the REAL launch decision (`selectShellStartupFeatures` +
* `getShellLaunchConfig`) rather than an inline copy of the gate, so a pane that
* Orca would not wrap cannot pass here by construction.
*/
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { getZshShellReadyRcfileContent } from './providers/local-pty-shell-ready-wrapper-generation'
import { getZshEnvTemplate, ZSH_HISTFILE_RESTORE_BLOCK } from './shell-templates'
import { getShellLaunchConfig } from './providers/local-pty-shell-ready'
import { selectShellStartupFeatures } from './shell-startup-features'
// Why probe and execute the same binary: guarding on `zsh` from PATH but then
// running a hardcoded `/bin/zsh` lets the guard pass on a host that installs zsh
@@ -31,55 +35,142 @@ const ZSH_PATH = hasZsh
: ''
const itWithZsh = hasZsh ? it : it.skip
function runLoginZsh(home: string, zdotdir: string, env: Record<string, string>): string {
function runZsh(args: string[], env: Record<string, string>): string {
// -o noglobalrcs is deliberately NOT passed: /etc/zshrc is the thing under test.
return execFileSync(ZSH_PATH, ['-li', '-c', 'echo "RESULT=$HISTFILE"'], {
return execFileSync(ZSH_PATH, [...args, '-i', '-c', 'echo "RESULT=$HISTFILE"'], {
encoding: 'utf8',
timeout: 20_000,
env: {
PATH: '/usr/bin:/bin',
HOME: home,
ZDOTDIR: zdotdir,
ORCA_ORIG_ZDOTDIR: home,
ORCA_ZSHENV_SOURCE_DIR: home,
...env
}
env: { PATH: '/usr/bin:/bin', ...env }
})
}
describe('worktree-scoped HISTFILE survives zsh startup', () => {
const withWrapper = (run: (home: string, zdotdir: string) => void): void => {
const home = mkdtempSync(join(tmpdir(), 'orca-scoped-histfile-'))
const zdotdir = join(home, 'shell-ready', 'zsh')
mkdirSync(zdotdir, { recursive: true })
writeFileSync(join(zdotdir, '.zshenv'), getZshEnvTemplate(zdotdir))
writeFileSync(join(zdotdir, '.zshrc'), getZshShellReadyRcfileContent())
writeFileSync(join(zdotdir, '.zlogin'), `${ZSH_HISTFILE_RESTORE_BLOCK}\n`)
try {
run(home, zdotdir)
} finally {
rmSync(home, { recursive: true, force: true })
}
/**
* True when this host's system zshrc is the thing that destroys HISTFILE.
*
* Why probed and not keyed off `process.platform`: macOS ships the clobber in
* /etc/zshrc, stock Ubuntu ships no such assignment, and a hardened corporate
* image can go either way. Assertions that only mean something under a clobber
* are skipped where there is none rather than asserting a tautology.
*/
const systemZshrcClobbersHistfile = (() => {
if (!hasZsh) {
return false
}
const home = mkdtempSync(join(tmpdir(), 'orca-histfile-probe-'))
try {
const output = execFileSync(ZSH_PATH, ['-l', '-i', '-c', 'echo "RESULT=$HISTFILE"'], {
encoding: 'utf8',
timeout: 20_000,
env: {
PATH: '/usr/bin:/bin',
HOME: home,
HISTFILE: join(home, 'injected-history')
}
})
return !output.includes(join(home, 'injected-history'))
} catch {
return false
} finally {
rmSync(home, { recursive: true, force: true })
}
})()
itWithZsh('keeps the injected path that a system zshrc would otherwise clobber', () => {
withWrapper((home, zdotdir) => {
const scoped = join(home, 'orca-history', 'zsh_history')
const itWithClobber = systemZshrcClobbersHistfile ? itWithZsh : it.skip
const output = runLoginZsh(home, zdotdir, { HISTFILE: scoped, ORCA_HISTFILE: scoped })
/** The launch config Orca produces for a plain pane whose only Orca-owned
* concern is a worktree-scoped HISTFILE. */
function launchPlainHistoryPane(home: string, scopedHistfile: string) {
const env: Record<string, string> = {
HOME: home,
HISTFILE: scopedHistfile,
ORCA_HISTFILE: scopedHistfile
}
const features = selectShellStartupFeatures({
shellPath: ZSH_PATH,
env,
hasStartupCommand: false,
waitsForShellReady: false,
emitsStartupIdentity: false
})
const launch = getShellLaunchConfig(ZSH_PATH, features)
return {
features,
launch,
env: { ...env, ...launch.env, ...sandboxConfigDir(home) }
}
}
expect(output).toContain(`RESULT=${scoped}`)
// Why override: the launch config resolves the user's config dir from the real
// process env, and these runs must resolve against the sandbox home instead.
function sandboxConfigDir(home: string): Record<string, string> {
return { ORCA_ORIG_ZDOTDIR: home, ORCA_ZSHENV_SOURCE_DIR: home }
}
function withTempHome(run: (home: string) => void): void {
const home = mkdtempSync(join(tmpdir(), 'orca-scoped-histfile-'))
try {
run(home)
} finally {
rmSync(home, { recursive: true, force: true })
}
}
const histfileOf = (output: string): string =>
/^RESULT=(.*)$/m.exec(output)?.[1]?.trim() ?? '<unmatched>'
describe('worktree-scoped HISTFILE survives zsh startup', () => {
itWithZsh('wraps a plain pane once Orca injected a worktree HISTFILE', () => {
withTempHome((home) => {
const { features, launch } = launchPlainHistoryPane(home, join(home, 'zsh_history'))
// The whole bug: this pane has no overlay env and no startup command, so
// before this change nothing pointed it at a wrapper at all.
expect(features).toEqual(['history'])
expect(launch.env.ZDOTDIR).toBeTruthy()
expect(launch.env.ORCA_SHELL_FEATURES).toBe('history')
})
})
itWithZsh('never leaves history inside Orca’s own wrapper directory', () => {
withWrapper((home, zdotdir) => {
itWithClobber('keeps the injected path that the system zshrc would otherwise clobber', () => {
withTempHome((home) => {
const scoped = join(home, 'orca-history', 'zsh_history')
const { launch, env } = launchPlainHistoryPane(home, scoped)
const output = runLoginZsh(home, zdotdir, { HISTFILE: scoped, ORCA_HISTFILE: scoped })
const output = runZsh(launch.args ?? ['-l'], env)
expect(histfileOf(output)).toBe(scoped)
})
})
itWithClobber('never leaves history inside Orca’s own wrapper directory', () => {
withTempHome((home) => {
const scoped = join(home, 'orca-history', 'zsh_history')
const { launch, env } = launchPlainHistoryPane(home, scoped)
const output = runZsh(launch.args ?? ['-l'], env)
// The exact failure mode of #11044: history written into shell-ready/zsh.
expect(output).not.toContain(zdotdir)
expect(output).not.toContain(launch.env.ZDOTDIR)
})
})
itWithZsh('consumes ORCA_HISTFILE so nothing the shell spawns can inherit it', () => {
withTempHome((home) => {
const scoped = join(home, 'orca-history', 'zsh_history')
const { launch, env } = launchPlainHistoryPane(home, scoped)
const output = execFileSync(
ZSH_PATH,
[...(launch.args ?? ['-l']), '-i', '-c', 'echo "LEAK=[${ORCA_HISTFILE:-}]"'],
{
encoding: 'utf8',
timeout: 20_000,
env: { PATH: '/usr/bin:/bin', ...env }
}
)
// Root-cause fix for #11146: the variable no longer exists after use.
expect(output).toContain('LEAK=[]')
})
})
@@ -89,16 +180,23 @@ describe('worktree-scoped HISTFILE survives zsh startup', () => {
// so it is always set there; a stock Ubuntu zsh has no such file and leaves
// it EMPTY. The contract is that Orca's wrapper does not change it either
// way, which is the same assertion on both.
withWrapper((home, zdotdir) => {
const wrapped = runLoginZsh(home, zdotdir, {})
const unwrapped = execFileSync(ZSH_PATH, ['-li', '-c', 'echo "RESULT=$HISTFILE"'], {
encoding: 'utf8',
timeout: 20_000,
env: { PATH: '/usr/bin:/bin', HOME: home }
withTempHome((home) => {
const features = selectShellStartupFeatures({
shellPath: ZSH_PATH,
env: { HOME: home, ORCA_CODEX_HOME: join(home, 'codex') },
hasStartupCommand: false,
waitsForShellReady: false,
emitsStartupIdentity: false
})
const launch = getShellLaunchConfig(ZSH_PATH, features)
const wrapped = runZsh(launch.args ?? ['-l'], {
HOME: home,
ORCA_CODEX_HOME: join(home, 'codex'),
...launch.env,
...sandboxConfigDir(home)
})
const unwrapped = runZsh(['-l'], { HOME: home })
const histfileOf = (output: string): string =>
/^RESULT=(.*)$/m.exec(output)?.[1]?.trim() ?? '<unmatched>'
expect(histfileOf(wrapped)).toBe(histfileOf(unwrapped))
expect(wrapped).not.toContain('ORCA_HISTFILE')
})
+96 -86
View File
@@ -6,16 +6,25 @@
* 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.
*
* All order-sensitive Orca work lives in ONE `__orca_shell_epilogue` defined in
* .zshenv and invoked exactly once — from .zshrc for a non-login shell, from
* .zlogin for a login shell. Each feature inside it is an independent guard on
* the allowlist snapshotted (and destroyed) at the top of .zshenv, so a pane
* wrapped only for one feature runs only that feature's code.
*/
import { getPosixOmpShellWrapper } from './pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from './pty/codex-shell-launch-preflight'
import {
getZshEnvTemplate,
getZshEnvDiscoveryBody,
getZshFinalZdotdirRestoreBlock,
getZshOverlayEnvTemplate,
getZshOverlayEnvBody,
getZshShellReadyMarkerRegistrationBlock,
getZshStartupFileSourceBlock,
ZSH_HISTFILE_RESTORE_BLOCK
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
ZSH_FEATURE_CHANNEL_BLOCK,
ZSH_HISTFILE_RESTORE_BLOCK,
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK
} from './shell-templates'
/** Runtime values the wrapper re-exports after the user's own startup files ran. */
@@ -30,18 +39,6 @@ export type ZshWrapperRestoreSpec = {
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
@@ -56,18 +53,13 @@ export type ZshStartupWrapperSpec = {
/** zsh expression the wrapper resolves the user's startup-file dir from. */
homeExpression?: string
readyMarkerEscaped: string
/** OSC 133 command-lifecycle hooks in .zshrc. */
/** OSC 133 command-lifecycle hooks (behind the `markers` feature). */
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
/** Comment heading the overlay restores inside the epilogue. */
overlayRestoreComment: 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 = {
@@ -77,18 +69,20 @@ export type ZshStartupWrapperFiles = {
zlogin: string
}
const AGENT_TEAMS_PATH_RESTORE_FUNCTION = `__orca_restore_agent_teams_path() {
const AGENT_TEAMS_PATH_RESTORE_BLOCK = `__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`
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 CODEX_HOME_RESTORE = `# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"`
const ZSH_OSC133_COMMAND_MARKER_BLOCK = `__orca_osc133_precmd() {
local exit_code=$?
@@ -115,78 +109,76 @@ function joinBlocks(blocks: (string | null)[]): string {
return blocks.filter((block): block is string => block !== null).join('\n')
}
function indentBlock(block: string, indent: string): string {
return block
.split('\n')
.map((line) => (line.length > 0 ? `${indent}${line}` : line))
.join('\n')
}
/** One epilogue feature: `if __orca_has_feature <name>; then ... fi`. */
function featureGuard(name: string, body: (string | null)[]): string | null {
const blocks = body.filter((block): block is string => block !== null)
if (blocks.length === 0) {
return null
}
return ` if __orca_has_feature ${name}; then\n${indentBlock(joinBlocks(blocks).replace(/\n$/, ''), ' ')}\n fi`
}
/** 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
function getOverlayRestoreBlocks(spec: ZshStartupWrapperSpec): (string | null)[] {
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
spec.overlayRestoreComment,
spec.restores.agentTeamsPath ? AGENT_TEAMS_PATH_RESTORE_BLOCK : null,
OPENCODE_CONFIG_DIR_RESTORE,
MIMOCODE_HOME_RESTORE,
spec.restores.remoteCliBinDir ? REMOTE_CLI_BIN_DIR_RESTORE : null,
getPosixOmpShellWrapper(),
spec.restores.codexHome ? CODEX_HOME_RESTORE : null,
spec.restores.codexLaunchPreflight ? 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`
/**
* Every Orca-owned, order-sensitive step, in one function called once.
*
* Why a function in .zshenv rather than inline code in .zshrc/.zlogin: login and
* non-login shells load a different last file, and duplicating the body in both
* is how the two copies drifted before. The once-flag makes a double invocation
* (re-sourced rc files) a no-op rather than a double-registered prompt hook.
*/
function buildEpilogue(spec: ZshStartupWrapperSpec): string {
return `__orca_shell_epilogue() {
(( $+_orca_epilogue_done )) && return 0
typeset -g _orca_epilogue_done=1
${joinBlocks([
featureGuard('overlay', getOverlayRestoreBlocks(spec)),
featureGuard('history', [ZSH_HISTFILE_RESTORE_BLOCK]),
featureGuard('markers', [spec.osc133CommandMarkers ? ZSH_OSC133_COMMAND_MARKER_BLOCK : null]),
featureGuard('ready', [getZshShellReadyMarkerRegistrationBlock(spec.readyMarkerEscaped)])
])}
${indentBlock(getZshFinalZdotdirRestoreBlock(spec.homeExpression).replace(/\n$/, ''), ' ')}
unset _orca_shell_features
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
}`
}
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]
function buildZshenv(spec: ZshStartupWrapperSpec): string {
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
ZSH_FEATURE_CHANNEL_BLOCK,
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK,
spec.zshenvStrategy === 'discover-user-zdotdir'
? getZshEnvDiscoveryBody(spec.zshDir)
: getZshOverlayEnvBody(spec.zshDir),
buildEpilogue(spec)
])}\n`
}
export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshStartupWrapperFiles {
return {
zshenv:
spec.zshenvStrategy === 'discover-user-zdotdir'
? getZshEnvTemplate(spec.zshDir, spec.headerLabel)
: getZshOverlayEnvTemplate(spec.zshDir, spec.headerLabel),
zshenv: buildZshenv(spec),
zprofile: `${joinBlocks([
`# ${spec.headerLabel}`,
getZshStartupFileSourceBlock({
@@ -194,7 +186,25 @@ export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshSta
homeExpression: spec.homeExpression
})
])}\n`,
zshrc: buildZshrc(spec),
zlogin: buildZlogin(spec)
zshrc: `${joinBlocks([
`# ${spec.headerLabel}`,
getZshStartupFileSourceBlock({
fileName: '.zshrc',
homeExpression: spec.homeExpression,
interactiveOnly: true,
skipWhenHomeIsCurrentZdotdir: spec.skipUserZshrcWhenHomeIsWrapperDir
}),
'# Why: a login shell still has .zlogin to load; it runs the epilogue instead.',
'[[ -o login ]] || __orca_shell_epilogue'
])}\n`,
zlogin: `${joinBlocks([
`# ${spec.headerLabel}`,
getZshStartupFileSourceBlock({
fileName: '.zlogin',
homeExpression: spec.homeExpression,
interactiveOnly: true
}),
'__orca_shell_epilogue'
])}\n`
}
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Builds a directory that counts as a user's real zsh config root.
*
* Why a fixture: the inherited-ZDOTDIR guard only accepts a directory that
* actually holds a zsh startup file, so tests can no longer use a made-up path.
*
* Test support only; nothing under src/main imports this at runtime.
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
export function makeUserZdotdir(parent: string, ...segments: string[]): string {
const dir = join(parent, ...segments)
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, '.zshrc'), '')
return dir
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Decides which inherited ZDOTDIR Orca may treat as the user's real zsh config
* dir, on the Node side of the launch.
*
* Why: Orca used to recognise only its OWN wrapper path shape (a path ending in
* shell-ready/zsh). Launched from any other terminal that had already
* hijacked ZDOTDIR, Orca captured that dir as "the user's config" and re-sourced
* it. Ownership is now established positively — a stamped marker file, or Orca's
* own dir shape for wrappers written by older builds — and a dir holding no zsh
* startup file at all is not a config dir whoever wrote it. No vendor is
* detected by name, because that can never be complete.
*/
import { existsSync } from 'node:fs'
import { ZSH_WRAPPER_DIR_MARKER_FILE } from './shell-templates'
type EnvLike = Record<string, string | undefined>
const ZSH_STARTUP_FILES = ['.zshenv', '.zshrc', '.zprofile', '.zlogin'] as const
export function isOrcaOwnedZshWrapperDir(dir: string): boolean {
const normalized = dir.replace(/\/+$/, '')
if (!normalized) {
return false
}
return (
existsSync(`${normalized}/${ZSH_WRAPPER_DIR_MARKER_FILE}`) ||
normalized.endsWith('/shell-ready/zsh')
)
}
/** The inherited value if it is usable as the user's config dir, else null. */
function usableInheritedZdotdir(value: string | undefined): string | null {
if (!value) {
return null
}
const normalized = value.replace(/\/+$/, '')
if (!normalized || isOrcaOwnedZshWrapperDir(normalized)) {
return null
}
if (!ZSH_STARTUP_FILES.some((file) => existsSync(`${normalized}/${file}`))) {
return null
}
return value
}
export function resolveInheritedZdotdir(env: EnvLike, homeFallback = ''): string {
return (
usableInheritedZdotdir(env.ZDOTDIR) ??
usableInheritedZdotdir(env.ORCA_ORIG_ZDOTDIR) ??
env.HOME ??
homeFallback
)
}
export function resolveInheritedZshenvSourceDir(env: EnvLike, homeFallback = ''): string {
return usableInheritedZdotdir(env.ZDOTDIR) ?? env.HOME ?? homeFallback
}
+9 -7
View File
@@ -79,15 +79,17 @@ describe('PtyHandler', () => {
ORCA_AGENT_HOOK_TOKEN: 'abc-uuid'
}))
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true)
const oldStartupIdentity = process.env.ORCA_SHELL_STARTUP_IDENTITY
process.env.ORCA_SHELL_STARTUP_IDENTITY = '1'
// Why seeded: a revived pane must not inherit a feature selection from the
// relay process's own environment.
const oldShellFeatures = process.env.ORCA_SHELL_FEATURES
process.env.ORCA_SHELL_FEATURES = 'ready,identity,markers,overlay'
try {
await dispatcher.callRequest('pty.revive', { state })
} finally {
if (oldStartupIdentity === undefined) {
delete process.env.ORCA_SHELL_STARTUP_IDENTITY
if (oldShellFeatures === undefined) {
delete process.env.ORCA_SHELL_FEATURES
} else {
process.env.ORCA_SHELL_STARTUP_IDENTITY = oldStartupIdentity
process.env.ORCA_SHELL_FEATURES = oldShellFeatures
}
killSpy.mockRestore()
}
@@ -101,8 +103,8 @@ describe('PtyHandler', () => {
expect(callArgs.env.ORCA_AGENT_HOOK_TOKEN).toBe('abc-uuid')
expect(callArgs.env.TERM).toBe('xterm-256color')
expect(callArgs.env.TERM_PROGRAM).toBe('Orca')
expect(callArgs.env.ORCA_SHELL_READY_MARKER).toBe('0')
expect(callArgs.env.ORCA_SHELL_STARTUP_IDENTITY).toBe('0')
expect(callArgs.env.ORCA_SHELL_FEATURES).not.toContain('ready')
expect(callArgs.env.ORCA_SHELL_FEATURES).not.toContain('identity')
})
it('fences both revived worktree identity and cwd with rollback', async () => {
@@ -122,7 +122,7 @@ describe('PtyHandler', () => {
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
expect(spawnOptions?.env?.ORCA_SHELL_FEATURES).toContain('ready')
expect(handler.retainedStartupCommandCount).toBe(1)
expect(handler.retainedStartupCommandBytes).toBe(0)
vi.advanceTimersByTime(15_000)
@@ -161,7 +161,7 @@ describe('PtyHandler', () => {
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
expect(spawnOptions?.env?.ORCA_SHELL_FEATURES).toContain('ready')
expect(handler.retainedStartupCommandCount).toBe(1)
}
)
@@ -199,7 +199,7 @@ describe('PtyHandler', () => {
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
expect(spawnOptions?.env?.ORCA_SHELL_FEATURES).toContain('ready')
}
)
@@ -238,7 +238,7 @@ describe('PtyHandler', () => {
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
expect(spawnOptions?.env?.ORCA_SHELL_FEATURES).toContain('ready')
}
)
@@ -512,7 +512,7 @@ describe('PtyHandler', () => {
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('0')
expect(spawnOptions?.env?.ORCA_SHELL_FEATURES).toBe('')
expect(handler.retainedStartupCommandCount).toBe(0)
}
)
+10 -11
View File
@@ -18,6 +18,7 @@ import {
} from './pty-shell-utils'
import { getRelayShellLaunchConfig, isRelayWslShell } from './pty-shell-launch'
import { addWslEnvKeys } from '../shared/wsl-env'
import { SHELL_STARTUP_FEATURE_ENV } from '../main/shell-startup-features'
import { DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../shared/ssh-types'
import { shouldUseShellReadyStartupDelivery } from '../shared/codex-startup-delivery'
import { buildStartupCommandSubmission } from '../shared/startup-command-submission'
@@ -1590,7 +1591,7 @@ export class PtyHandler {
emitStartupIdentity: shouldEmitShellReadyMarker
})
const rendererShellReadySupported =
!shouldProviderDeliverCommand && shellLaunch.env.ORCA_SHELL_READY_MARKER === '1'
!shouldProviderDeliverCommand && shellLaunch.supportsReadyMarker
if (context?.signal?.aborted || context?.isStale()) {
// Why: cancellation remains side-effect-free until the exact native spawn seam.
@@ -1610,11 +1611,11 @@ export class PtyHandler {
cols,
rows,
cwd,
// Why: relay shells inherit process.env; don't let an ambient Orca marker enable shell-ready unless requested.
// Why the empty default: relay shells inherit process.env, and the launch
// config is the only thing allowed to name features for this shell.
env: {
...spawnEnv,
ORCA_SHELL_READY_MARKER: '0',
ORCA_SHELL_STARTUP_IDENTITY: '0',
[SHELL_STARTUP_FEATURE_ENV]: '',
...shellLaunch.env
}
})
@@ -1671,11 +1672,10 @@ export class PtyHandler {
command: shouldProviderDeliverCommand ? managedStartupCommand : null,
providerDelivery: shouldProviderDeliverCommand,
delivered: false,
waitForShellReady: shellLaunch.env.ORCA_SHELL_READY_MARKER === '1',
outputScanState:
shellLaunch.env.ORCA_SHELL_READY_MARKER === '1'
? createShellStartupOutputScanState()
: null,
waitForShellReady: shellLaunch.supportsReadyMarker,
outputScanState: shellLaunch.supportsReadyMarker
? createShellStartupOutputScanState()
: null,
shellPid: null,
promptProbe: null,
timer: null
@@ -2189,8 +2189,7 @@ export class PtyHandler {
// Why: no provider-delivered command is waiting for a ready marker.
env: {
...spawnEnv,
ORCA_SHELL_READY_MARKER: '0',
ORCA_SHELL_STARTUP_IDENTITY: '0',
[SHELL_STARTUP_FEATURE_ENV]: '',
...shellLaunch.env
}
})
+36 -32
View File
@@ -66,7 +66,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
function expectFinalZdotdirRestoreContext(content: string) {
expect(content).toContain("after Orca's last wrapper file has loaded")
expect(content).toContain('export ZDOTDIR="$_orca_home"')
expect(content).toContain('export ZDOTDIR="$REPLY"')
}
describe('isRelayWslShell', () => {
@@ -109,22 +109,21 @@ describe('getRelayShellLaunchConfig', () => {
expect(config.args).toEqual(['-l'])
expect(config.env.ZDOTDIR).toBe(zshRoot)
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
'export ORCA_USER_ZDOTDIR="${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
)
expect(readFileSync(join(zshRoot, '.zprofile'), 'utf8')).toContain(
'_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
)
const zshenv = readFileSync(join(zshRoot, '.zshenv'), 'utf8')
const userZdotdirResolution =
'__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
expect(zshenv).toContain('export ORCA_USER_ZDOTDIR="$REPLY"')
const zprofile = readFileSync(join(zshRoot, '.zprofile'), 'utf8')
const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
expect(zshrc).toContain('_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')
expect(zlogin).toContain('_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')
expect(zprofile).toContain(userZdotdirResolution)
expect(zshrc).toContain(userZdotdirResolution)
expect(zlogin).toContain(userZdotdirResolution)
expectZdotdirSourceContext(zprofile, '.zprofile')
expectZdotdirSourceContext(zshrc, '.zshrc')
expectZdotdirSourceContext(zlogin, '.zlogin')
expectFinalZdotdirRestoreContext(zshrc)
expectFinalZdotdirRestoreContext(zlogin)
// Why .zshenv: the final restore is the last step of the one epilogue.
expectFinalZdotdirRestoreContext(zshenv)
}
)
@@ -133,7 +132,8 @@ describe('getRelayShellLaunchConfig', () => {
getRelayShellLaunchConfig('C:\\Windows\\System32\\cmd.exe', { HOME: homeDir }, 'win32')
).toEqual({
args: [],
env: {}
env: {},
supportsReadyMarker: false
})
expect(
getRelayShellLaunchConfig(
@@ -143,14 +143,16 @@ describe('getRelayShellLaunchConfig', () => {
)
).toEqual({
args: ['-NoLogo'],
env: {}
env: {},
supportsReadyMarker: false
})
})
it('keeps PowerShell Core on POSIX remotes as a login shell', () => {
expect(getRelayShellLaunchConfig('/usr/bin/pwsh', { HOME: homeDir }, 'linux')).toEqual({
args: ['-l'],
env: {}
env: {},
supportsReadyMarker: false
})
})
@@ -165,7 +167,7 @@ describe('getRelayShellLaunchConfig', () => {
})
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
'export ORCA_USER_ZDOTDIR="${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
'export ORCA_USER_ZDOTDIR="$REPLY"'
)
})
@@ -177,11 +179,13 @@ describe('getRelayShellLaunchConfig', () => {
ORCA_MIMOCODE_HOME: '/tmp/orca-mimocode-overlay'
})
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
// Why .zshenv: the overlay restores live in the one epilogue defined there.
const zshenv = readFileSync(join(zshRoot, '.zshenv'), 'utf8')
expect(config.args).toEqual(['-l'])
expect(config.env.ZDOTDIR).toBe(zshRoot)
expect(zshrc).toContain(
expect(config.env.ORCA_SHELL_FEATURES).toBe('overlay,history,markers')
expect(zshenv).toContain(
'[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
)
}
@@ -195,7 +199,9 @@ describe('getRelayShellLaunchConfig', () => {
const bashRc = readFileSync(rcfile, 'utf8')
expect(config.args).toEqual(['--rcfile', rcfile])
expect(config.env).toEqual({})
// Why the empty allowlist: bash keeps its unconditional OSC 133 hooks, and
// an explicit empty value also overrides anything the relay inherited.
expect(config.env).toEqual({ ORCA_SHELL_FEATURES: '' })
expect(bashRc).toContain('printf "\\033]133;D;%s\\007"')
expect(bashRc).toContain('printf "\\033]133;C\\007"')
}
@@ -208,19 +214,17 @@ describe('getRelayShellLaunchConfig', () => {
emitReadyMarker: true
})
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
const zshenv = readFileSync(join(zshRoot, '.zshenv'), 'utf8')
expect(config.args).toEqual(['-l'])
expect(config.env.ZDOTDIR).toBe(zshRoot)
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
'printf "\\033]777;orca-shell-start:%s\\007" "$$"'
)
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
'unset ORCA_SHELL_STARTUP_IDENTITY'
)
expect(zlogin).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zlogin).toContain('printf "\\033]777;orca-shell-ready\\007"')
expect(config.supportsReadyMarker).toBe(true)
expect(config.env.ORCA_SHELL_FEATURES).toContain('ready')
expect(zshenv).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
// Why: the channel is destroyed before the user's own config is sourced.
expect(zshenv).toContain('builtin unset ORCA_SHELL_FEATURES')
expect(zshenv).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zshenv).toContain('printf "\\033]777;orca-shell-ready\\007"')
}
)
@@ -232,9 +236,10 @@ describe('getRelayShellLaunchConfig', () => {
})
const bashRc = readFileSync(config.args[1] as string, 'utf8')
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(config.supportsReadyMarker).toBe(true)
expect(config.env.ORCA_SHELL_FEATURES).toContain('ready')
expect(bashRc).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
expect(bashRc).toContain('unset ORCA_SHELL_STARTUP_IDENTITY')
expect(bashRc).toContain('builtin unset ORCA_SHELL_FEATURES')
expect(bashRc).toContain('__orca_append_prompt_command "__orca_prompt_mark"')
expect(bashRc).toContain('printf "\\033]777;orca-shell-ready\\007"')
}
@@ -248,8 +253,7 @@ describe('getRelayShellLaunchConfig', () => {
})
expect(config.env.ZDOTDIR).toBe(join(homeDir, '.orca-relay', 'shell-ready', 'zsh'))
expect(config.env.ORCA_SHELL_STARTUP_IDENTITY).toBe('1')
expect(config.env.ORCA_SHELL_READY_MARKER).toBeUndefined()
expect(config.env.ORCA_SHELL_FEATURES).toContain('identity')
}
)
+59 -47
View File
@@ -1,5 +1,12 @@
import { homedir } from 'node:os'
import { join } from 'node:path'
import {
encodeShellStartupFeatures,
selectShellStartupFeatures,
SHELL_STARTUP_FEATURE_ENV,
type ShellStartupFeature
} from '../main/shell-startup-features'
import { resolveInheritedZdotdir } from '../main/zsh-wrapper-dir-ownership'
import { ensureOverlayRestoreWrappers } from './pty-shell-overlay-wrappers'
const RELAY_SHELL_READY_DIR = '.orca-relay/shell-ready'
const POSIX_LOGIN_ARGS = ['-l']
@@ -7,6 +14,7 @@ const POSIX_LOGIN_ARGS = ['-l']
export type RelayShellLaunchConfig = {
args: string[]
env: Record<string, string>
supportsReadyMarker: boolean
}
function shellBasename(shellPath: string): string {
@@ -46,40 +54,10 @@ function windowsShellArgs(
return null
}
function hasOverlayRestoreEnv(env: Record<string, string>): boolean {
return Boolean(
env.ORCA_OPENCODE_CONFIG_DIR ||
env.ORCA_MIMOCODE_HOME ||
env.ORCA_REMOTE_CLI_BIN_DIR ||
env.ORCA_OMP_STATUS_EXTENSION
)
}
function getWrapperRoot(env: Record<string, string>): string {
return join(env.HOME || process.env.HOME || homedir(), RELAY_SHELL_READY_DIR)
}
function normalizeOriginalZdotdirCandidate(value: string | undefined): string | null {
if (!value) {
return null
}
const normalized = value.replace(/\/+$/, '')
if (!normalized || normalized.endsWith('/shell-ready/zsh')) {
return null
}
return value
}
function resolveOriginalZdotdir(env: Record<string, string>): string {
return (
normalizeOriginalZdotdirCandidate(env.ZDOTDIR) ||
normalizeOriginalZdotdirCandidate(env.ORCA_ORIG_ZDOTDIR) ||
env.HOME ||
process.env.HOME ||
''
)
}
export function getRelayShellLaunchConfig(
shellPath: string,
env: Record<string, string>,
@@ -91,8 +69,11 @@ export function getRelayShellLaunchConfig(
} = {}
): RelayShellLaunchConfig {
const shellName = shellBasename(shellPath)
const emitReadyMarker = options.emitReadyMarker === true
const emitStartupIdentity = options.emitStartupIdentity === true
const unwrapped: RelayShellLaunchConfig = {
args: POSIX_LOGIN_ARGS,
env: {},
supportsReadyMarker: false
}
if (platform === 'win32') {
// Why: pwsh also exists on POSIX remotes; Windows-specific shell args must
// only apply when the relay itself is running on native Windows.
@@ -101,39 +82,70 @@ export function getRelayShellLaunchConfig(
windowsShellArgs(shellName, {
terminalWindowsWslDistro: options.terminalWindowsWslDistro
}) ?? [],
env: {}
env: {},
supportsReadyMarker: false
}
}
if (shellName !== 'zsh' && shellName !== 'bash') {
return { args: POSIX_LOGIN_ARGS, env: {} }
return unwrapped
}
// Why: preserve plain zsh startup fast path unless markers or overlay restoration are requested.
const requiresZshWrapper = hasOverlayRestoreEnv(env) || emitReadyMarker || emitStartupIdentity
if (shellName === 'zsh' && !requiresZshWrapper) {
return { args: POSIX_LOGIN_ARGS, env: {} }
// Why both map to the same flag: the relay only wraps for a startup command
// when that command's delivery asked for the readiness handshake.
const startupCommandRequested =
options.emitReadyMarker === true || options.emitStartupIdentity === true
const features = selectShellStartupFeatures({
shellPath: shellName,
env,
hasStartupCommand: startupCommandRequested,
waitsForShellReady: options.emitReadyMarker === true,
emitsStartupIdentity: options.emitStartupIdentity === true
})
// Why bash is always wrapped: its rcfile carries the OSC 133 command-lifecycle
// hooks unconditionally today, and dropping them would strand agent rows on
// "working". zsh keeps the plain startup fast path when nothing needs it.
if (features.length === 0 && shellName !== 'bash') {
return unwrapped
}
const root = getWrapperRoot(env)
ensureOverlayRestoreWrappers(root)
let wrappersReady = false
try {
wrappersReady = ensureOverlayRestoreWrappers(root)
} catch {
// Why swallow: a remote HOME can be read-only or root-owned (EACCES), and
// that must not stop the pane from opening at all.
wrappersReady = false
}
if (!wrappersReady) {
// Why plain login shell: ZDOTDIR pointed at an incomplete wrapper dir makes
// zsh skip the user's whole config. Losing Orca's features is recoverable.
return unwrapped
}
const featureEnv = {
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
}
const supportsReadyMarker = features.includes('ready')
if (shellName === 'zsh') {
return {
args: POSIX_LOGIN_ARGS,
env: {
ORCA_ORIG_ZDOTDIR: resolveOriginalZdotdir(env),
ORCA_ORIG_ZDOTDIR: resolveInheritedZdotdir(env, process.env.HOME ?? ''),
ZDOTDIR: join(root, 'zsh'),
...(emitReadyMarker ? { ORCA_SHELL_READY_MARKER: '1' } : {}),
...(emitStartupIdentity ? { ORCA_SHELL_STARTUP_IDENTITY: '1' } : {})
}
...featureEnv
},
supportsReadyMarker
}
}
return {
args: ['--rcfile', join(root, 'bash', 'rcfile')],
env: {
...(emitReadyMarker ? { ORCA_SHELL_READY_MARKER: '1' } : {}),
...(emitStartupIdentity ? { ORCA_SHELL_STARTUP_IDENTITY: '1' } : {})
}
env: featureEnv,
supportsReadyMarker
}
}
export type { ShellStartupFeature }
+43 -24
View File
@@ -1,11 +1,15 @@
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper'
import {
BASH_FEATURE_CHANNEL_BLOCK,
BASH_PROMPT_COMMAND_COMPOSITION_BLOCK,
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
ZSH_HISTFILE_RESTORE_BLOCK
ZSH_HISTFILE_RESTORE_BLOCK,
ZSH_WRAPPER_DIR_MARKER_CONTENT,
ZSH_WRAPPER_DIR_MARKER_FILE
} from '../main/shell-templates'
import { writeShellWrapperFiles } from '../main/shell-wrapper-file-writer'
import {
buildZshStartupWrapperFiles,
type ZshStartupWrapperSpec
@@ -30,26 +34,32 @@ function getRelayZshWrapperSpec(zshDir: string): ZshStartupWrapperSpec {
readyMarkerEscaped: SHELL_READY_MARKER_ESCAPED,
osc133CommandMarkers: false,
skipUserZshrcWhenHomeIsWrapperDir: false,
interactiveRestoreComment:
overlayRestoreComment:
'# 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 {
/** True when every overlay wrapper file is present and non-empty afterwards. */
export function ensureOverlayRestoreWrappers(root: string): boolean {
const zshDir = join(root, 'zsh')
const bashDir = join(root, 'bash')
const zsh = buildZshStartupWrapperFiles(getRelayZshWrapperSpec(zshDir))
const bashRc = `# Orca relay bash overlay wrapper
${BASH_FEATURE_CHANNEL_BLOCK}
${SHELL_STARTUP_IDENTITY_MARKER_BLOCK}
# Why a plain variable: the channel is consumed and destroyed in these first
# lines, so nothing this shell later spawns can see or inherit the selection.
__orca_ready_marker=""
__orca_has_feature ready && __orca_ready_marker=1
unset _orca_shell_features
unset -f __orca_has_feature
[[ -f /etc/profile ]] && source /etc/profile
if [[ -f "$HOME/.bash_profile" ]]; then
source "$HOME/.bash_profile"
@@ -124,7 +134,7 @@ ${BASH_PROMPT_COMMAND_COMPOSITION_BLOCK}
__orca_prepend_prompt_command "__orca_osc133_precmd"
# Why: SSH startup commands are renderer-delivered; emit the same internal
# readiness marker as local shells only when that delivery mode asks for it.
if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
if [[ -n "$__orca_ready_marker" ]]; then
__orca_prompt_mark() {
printf "${SHELL_READY_MARKER_ESCAPED}"
}
@@ -157,23 +167,32 @@ unset __orca_initializing_wrapper
[join(zshDir, '.zprofile'), zsh.zprofile],
[join(zshDir, '.zshrc'), zsh.zshrc],
[join(zshDir, '.zlogin'), zsh.zlogin],
[join(zshDir, ZSH_WRAPPER_DIR_MARKER_FILE), ZSH_WRAPPER_DIR_MARKER_CONTENT],
[join(bashDir, 'rcfile'), bashRc]
] as const
for (const [path, content] of files) {
mkdirSync(dirname(path), { recursive: true })
let existing: string | null = null
try {
existing = readFileSync(path, 'utf8')
} catch {
existing = null
}
// Why: relay wrapper files persist under ~/.orca-relay across app
// upgrades. Existence alone is not enough; stale wrappers would miss
// later fixes such as preserving post-.zshenv ZDOTDIR.
if (existing !== content) {
writeFileSync(path, content, 'utf8')
}
chmodSync(path, 0o644)
// Why: relay wrapper files persist under ~/.orca-relay across app upgrades.
// Existence alone is not enough; stale wrappers would miss later fixes such
// as preserving post-.zshenv ZDOTDIR.
const stale = files.filter(([path, content]) => readFileOrNull(path) !== content)
if (stale.length > 0 && !writeShellWrapperFiles(stale, '[relay/shell-overlay]')) {
return false
}
return files.every(([path]) => isNonEmptyFile(path))
}
function readFileOrNull(path: string): string | null {
try {
return readFileSync(path, 'utf8')
} catch {
return null
}
}
function isNonEmptyFile(path: string): boolean {
try {
return statSync(path).size > 0
} catch {
return false
}
}