From d2cefb6c0355fe2f961b7b3da19b0406b2f8091c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:24:01 -0700 Subject: [PATCH] fix(terminal): run the Codex launch preflight through the CLI the terminal names Packaged Linux names the userData shim in ORCA_CLI_COMMAND, while the preflight ran the bundled launcher behind it. The CLI saw a different launcher and handed the preflight off to the shim, booting Electron twice before every codex launch. --- .../pty-daemon-spawn-agent-home-env.test.ts | 33 ++++++++++- src/main/ipc/pty/host-env/assembly.ts | 59 ++++++++++--------- .../pty/codex-shell-launch-preflight.test.ts | 43 +++++++++++++- src/main/pty/codex-shell-launch-preflight.ts | 23 +++++--- 4 files changed, 118 insertions(+), 40 deletions(-) diff --git a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts index f6ffae641d3..920563608da 100644 --- a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts +++ b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { piBuildPtyEnvMock } from './pty-ipc-mock-registry' +import { piBuildPtyEnvMock, statSyncMock } from './pty-ipc-mock-registry' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { type DaemonSpawnCall, @@ -463,6 +463,37 @@ describe('registerPtyHandlers', () => { }) } }) + it('runs the Codex launch preflight through the CLI the packaged Linux terminal names', async () => { + // Why: the bundled launcher behind the shim is a different file, so running it directly + // made the CLI hand the preflight off to the shim and boot Electron twice per launch. + const originalPlatform = process.platform + Object.defineProperty(process, 'platform', { + configurable: true, + value: 'linux' + }) + const shimPath = join('/tmp/orca-user-data', 'linux-orca-cli-shim', 'orca') + statSyncMock.mockImplementation((target: string) => ({ + isDirectory: () => target !== shimPath, + isFile: () => target === shimPath, + mode: 0o755, + size: 1 + })) + try { + const env = await daemonSpawnAndGetEnv( + { PATH: ['/usr/local/bin', '/usr/bin'].join(delimiter) }, + () => '/tmp/orca-codex-home' + ) + expect(env.ORCA_CLI_COMMAND).toBe( + join('/tmp/orca-user-data', 'linux-orca-cli-shim', 'orca') + ) + expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBe(env.ORCA_CLI_COMMAND) + } finally { + Object.defineProperty(process, 'platform', { + configurable: true, + value: originalPlatform + }) + } + }) it('prepends the bundled CLI dir to PATH for packaged macOS spawns', async () => { const resourcesPathDescriptor = Object.getOwnPropertyDescriptor(process, 'resourcesPath') Object.defineProperty(process, 'resourcesPath', { diff --git a/src/main/ipc/pty/host-env/assembly.ts b/src/main/ipc/pty/host-env/assembly.ts index 80826e3f29c..6ff9dbfe3f3 100644 --- a/src/main/ipc/pty/host-env/assembly.ts +++ b/src/main/ipc/pty/host-env/assembly.ts @@ -242,35 +242,6 @@ export function buildPtyHostEnv( } } - // Why: keep the Codex home override PTY-scoped so dev/prod Orcas don't share hooks through ~/.codex. - if (opts.skipCodexHomeEnv) { - delete baseEnv.CODEX_HOME - delete baseEnv.ORCA_CODEX_HOME - delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT - } else if (opts.selectedCodexHomePath) { - baseEnv.CODEX_HOME = opts.selectedCodexHomePath - // Why: user startup files may re-export CODEX_HOME; shell-ready wrappers restore this runtime home before Codex launches. - baseEnv.ORCA_CODEX_HOME = opts.selectedCodexHomePath - const preflightCommand = resolveCodexShellLaunchPreflightCommand({ - hooksEnabled: opts.codexStatusHooksEnabled ?? opts.agentStatusHooksEnabled, - isPackaged: opts.isPackaged, - isWsl: opts.isWsl, - managedHomePath: opts.selectedCodexHomePath, - userDataPath: opts.userDataPath, - resourcesPath: opts.resourcesPath - }) - if (preflightCommand) { - baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT = preflightCommand - } else { - delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT - } - } else if (opts.stripInheritedOrcaCodexHome) { - stripInheritedOrcaCodexHomeOverride(baseEnv) - delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT - } else { - delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT - } - // Why: WSL shells need the managed userData root for shell-ready wrappers; dev-mode terminals need the same export so `orca` targets the live dev instance. if (opts.isWsl) { baseEnv.ORCA_USER_DATA_PATH = opts.userDataPath @@ -293,6 +264,36 @@ export function buildPtyHostEnv( delete baseEnv.ORCA_CLI_COMMAND } + // Why: keep the Codex home override PTY-scoped so dev/prod Orcas don't share hooks through ~/.codex. + if (opts.skipCodexHomeEnv) { + delete baseEnv.CODEX_HOME + delete baseEnv.ORCA_CODEX_HOME + delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT + } else if (opts.selectedCodexHomePath) { + baseEnv.CODEX_HOME = opts.selectedCodexHomePath + // Why: user startup files may re-export CODEX_HOME; shell-ready wrappers restore this runtime home before Codex launches. + baseEnv.ORCA_CODEX_HOME = opts.selectedCodexHomePath + const preflightCommand = resolveCodexShellLaunchPreflightCommand({ + hooksEnabled: opts.codexStatusHooksEnabled ?? opts.agentStatusHooksEnabled, + isPackaged: opts.isPackaged, + isWsl: opts.isWsl, + managedHomePath: opts.selectedCodexHomePath, + userDataPath: opts.userDataPath, + resourcesPath: opts.resourcesPath, + cliLauncher: launcher + }) + if (preflightCommand) { + baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT = preflightCommand + } else { + delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT + } + } else if (opts.stripInheritedOrcaCodexHome) { + stripInheritedOrcaCodexHomeOverride(baseEnv) + delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT + } else { + delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT + } + if ( opts.routeBrowserOpensToClient === true && baseEnv.BROWSER === undefined && diff --git a/src/main/pty/codex-shell-launch-preflight.test.ts b/src/main/pty/codex-shell-launch-preflight.test.ts index f0673fc9c95..e54d235fd07 100644 --- a/src/main/pty/codex-shell-launch-preflight.test.ts +++ b/src/main/pty/codex-shell-launch-preflight.test.ts @@ -10,7 +10,7 @@ import { writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { delimiter, isAbsolute, join } from 'node:path' +import { delimiter, dirname, isAbsolute, join } from 'node:path' import { execFileSync, spawnSync } from 'node:child_process' import { afterEach, describe, expect, it } from 'vitest' import { @@ -479,6 +479,47 @@ describe('Codex shell launch preflight command', () => { ).toBe(launcherPath) }) + it('runs the launcher the terminal names as its CLI, so the CLI never hands off to it', () => { + // Why: packaged Linux names the userData shim, not the bundled launcher behind it; running the + // launcher directly would re-run the preflight through the shim and boot Electron twice. + const { userDataPath, resourcesPath } = makeCliRoot() + writeExecutable(join(resourcesPath, 'bin', 'orca-ide'), '#!/bin/sh\nexit 0\n') + const shimPath = join(userDataPath, 'linux-orca-cli-shim', 'orca') + mkdirSync(dirname(shimPath), { recursive: true }) + writeExecutable(shimPath, '#!/bin/sh\nexit 0\n') + + expect( + resolveCodexShellLaunchPreflightCommand({ + hooksEnabled: true, + isPackaged: true, + managedHomePath: '/managed/home', + userDataPath, + resourcesPath, + cliLauncher: shimPath, + platform: 'linux' + }) + ).toBe(shimPath) + }) + + it('keeps the Windows launcher for WSL even when the host terminal names a CLI', () => { + const { userDataPath, resourcesPath } = makeCliRoot() + const launcherPath = join(resourcesPath, 'bin', 'orca.exe') + writeExecutable(launcherPath, '#!/bin/sh\nexit 0\n') + + expect( + resolveCodexShellLaunchPreflightCommand({ + hooksEnabled: true, + isPackaged: true, + isWsl: true, + managedHomePath: '/home/jin/.local/share/orca/codex-runtime-home/home', + userDataPath, + resourcesPath, + cliLauncher: join(userDataPath, 'elsewhere', 'orca.exe'), + platform: 'win32' + }) + ).toBe(launcherPath) + }) + it('never returns an unqualified command name that a profile-rewritten PATH could hijack', () => { const { userDataPath, resourcesPath } = makeCliRoot() writeExecutable(join(resourcesPath, 'bin', 'orca'), '#!/bin/sh\nexit 0\n') diff --git a/src/main/pty/codex-shell-launch-preflight.ts b/src/main/pty/codex-shell-launch-preflight.ts index 87e5df75827..84a90f6f90d 100644 --- a/src/main/pty/codex-shell-launch-preflight.ts +++ b/src/main/pty/codex-shell-launch-preflight.ts @@ -14,6 +14,8 @@ export type CodexShellLaunchPreflightCommandOptions = { userDataPath: string /** Packaged app resources root; the bundled launcher lives under it. */ resourcesPath?: string | null + /** The launcher this terminal names in `ORCA_CLI_COMMAND`; the preflight runs that same one so it never hands off to it (Linux names a shim in front of the bundled launcher). */ + cliLauncher?: string | null /** Test seam. */ platform?: NodeJS.Platform } @@ -33,15 +35,18 @@ export function resolveCodexShellLaunchPreflightCommand( return null } const platform = options.platform ?? process.platform - const candidate = options.isPackaged - ? options.resourcesPath - ? getBundledLauncherPath(platform, options.resourcesPath) - : null - : join( - options.userDataPath, - ...DEV_LAUNCHER_DIR, - platform === 'win32' ? `${DEV_COMMAND_NAME}.cmd` : DEV_COMMAND_NAME - ) + const candidate = + options.cliLauncher && !options.isWsl + ? options.cliLauncher + : options.isPackaged + ? options.resourcesPath + ? getBundledLauncherPath(platform, options.resourcesPath) + : null + : join( + options.userDataPath, + ...DEV_LAUNCHER_DIR, + platform === 'win32' ? `${DEV_COMMAND_NAME}.cmd` : DEV_COMMAND_NAME + ) if (!candidate || !isExecutableFileOnDisk(candidate, platform)) { return null }