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.
This commit is contained in:
Brennan Benson
2026-09-25 19:24:01 -07:00
parent 177ff7d9ac
commit d2cefb6c03
4 changed files with 118 additions and 40 deletions
@@ -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', {
+30 -29
View File
@@ -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 &&
@@ -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')
+14 -9
View File
@@ -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
}