Files
orca/src/main/pty/codex-no-daemon-launch-command.ts
T
Neil 1912c90141 Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging.

Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan.

Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews.

Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
2026-10-09 18:32:28 -07:00

100 lines
3.6 KiB
TypeScript

import { isAbsolute, win32 as pathWin32 } from 'node:path'
import { runProcess } from '@orca/process-host'
import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell'
import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell'
import { resolveCommandOnLocalPath } from '../ipc/command-path-resolver'
import { CODEX_SHARED_SERVER_ARGS } from '../../shared/codex-shell-function'
const CODEX_EXECUTABLE = /^codex(\.(exe|cmd|bat|ps1))?$/i
const SHARED_SERVER_ARGS: ReadonlySet<string> = new Set(CODEX_SHARED_SERVER_ARGS)
// Why bounded: a hung binary must not hold the pane; a slow probe only costs the flag.
const HELP_PROBE_TIMEOUT_MS = 5_000
export type LocalCodexLaunch = {
command: string | undefined
/** False for SSH and WSL spawns: their shell's codex function probes on that host. */
executesOnThisHost: boolean
/** Shell the provider will launch; undefined means the platform default. */
shellOverride: string | undefined
/** Env the PTY gets on top of this process's own. */
env: Record<string, string | undefined> | undefined
/** Keys the provider removes from that merged env. */
envToDelete?: readonly string[]
cwd: string | undefined
}
/**
* The launch command with `--no-daemon` after the Codex executable, applying the
* shell codex function's rule (src/shared/codex-shell-function.ts) where that
* function never runs: cmd.exe defines none, and a path-named binary bypasses it.
* Everywhere else the function probes the binary the shell itself resolves after
* the user's startup files, which main cannot see. Null (synchronously) when
* nothing applies: an extra await tick would reorder the pane-spawn reservation
* races the spawn handlers arbitrate right after this.
*/
export function planCodexNoDaemonLaunch(launch: LocalCodexLaunch): Promise<string> | null {
const { command } = launch
if (!command || !launch.executesOnThisHost) {
return null
}
const shell =
resolveLocalWindowsAgentStartupShell({
platform: process.platform,
isRemote: false,
terminalWindowsShell: launch.shellOverride
}) ?? 'posix'
const parsed = tokenizeStartupCommand(command, shell)
const executableSpan = parsed.ok ? parsed.spans[0] : undefined
if (!parsed.ok || !executableSpan || executableSpan.divergesFromShell) {
return null
}
const [executable, ...args] = parsed.tokens
if (
!CODEX_EXECUTABLE.test(pathWin32.basename(executable)) ||
(shell !== 'cmd' && !isAbsolute(executable))
) {
return null
}
const env = { ...process.env, ...launch.env }
for (const key of launch.envToDelete ?? []) {
delete env[key]
}
if (
env.ORCA_CODEX_ISOLATE === '0' ||
args.some((arg) => SHARED_SERVER_ARGS.has(arg) || arg.startsWith('--remote='))
) {
return null
}
return supportsNoDaemon(executable, env, launch.cwd).then((supported) =>
supported
? `${command.slice(0, executableSpan.end)} --no-daemon${command.slice(executableSpan.end)}`
: command
)
}
// Why probe every launch: a cached answer goes stale across an upgrade, and 0.155 and older exit 2 on the flag.
async function supportsNoDaemon(
executable: string,
env: NodeJS.ProcessEnv,
cwd: string | undefined
): Promise<boolean> {
const program = isAbsolute(executable)
? executable
: await resolveCommandOnLocalPath(executable, { env, cwd })
if (!program) {
return false
}
try {
const help = await runProcess({
program,
args: ['--help'],
cwd,
env,
timeoutMs: HELP_PROBE_TIMEOUT_MS
})
return help.stdout.includes('--no-daemon')
} catch {
return false
}
}