From ca75b16b334b8002b0ee87c8818f4c67c17d66fb Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:37:16 -0400 Subject: [PATCH] fix: add version manager bin paths to startup PATH for GUI-launched apps (#702) --- src/main/codex-accounts/service.ts | 16 ++++++++-- src/main/codex-cli/command.test.ts | 42 ++++++++++++++++++++++++- src/main/codex-cli/command.ts | 18 ++++++++++- src/main/rate-limits/codex-fetcher.ts | 44 ++++++++++++++------------- src/main/startup/configure-process.ts | 8 +++++ 5 files changed, 102 insertions(+), 26 deletions(-) diff --git a/src/main/codex-accounts/service.ts b/src/main/codex-accounts/service.ts index a027adda5..16e798bf2 100644 --- a/src/main/codex-accounts/service.ts +++ b/src/main/codex-accounts/service.ts @@ -282,7 +282,8 @@ export class CodexAccountService { private async runCodexLogin(managedHomePath: string): Promise { await new Promise((resolvePromise, rejectPromise) => { - const child = spawn(resolveCodexCommand(), ['login'], { + const codexCommand = resolveCodexCommand() + const child = spawn(codexCommand, ['login'], { stdio: ['ignore', 'pipe', 'pipe'], // Why: on Windows, resolveCodexCommand() may return a .cmd/.bat file // (e.g. codex.cmd from npm). Node's child_process.spawn cannot execute @@ -325,8 +326,17 @@ export class CodexAccountService { child.on('error', (error) => { settle(() => { - const cause = (error as NodeJS.ErrnoException).code === 'ENOENT' - rejectPromise(new Error(cause ? 'Codex CLI not found.' : error.message)) + const isEnoent = (error as NodeJS.ErrnoException).code === 'ENOENT' + // Why: ENOENT can mean either the codex binary doesn't exist OR the + // script's shebang interpreter (node) isn't in PATH. When we resolved + // codex to a full path, ENOENT almost certainly means node is missing. + const isBareCommand = codexCommand === 'codex' + const message = isEnoent + ? isBareCommand + ? 'Codex CLI not found.' + : 'Codex CLI found but could not run — Node.js may not be in your PATH.' + : error.message + rejectPromise(new Error(message)) }) }) diff --git a/src/main/codex-cli/command.test.ts b/src/main/codex-cli/command.test.ts index eed114779..65e773d96 100644 --- a/src/main/codex-cli/command.test.ts +++ b/src/main/codex-cli/command.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { resolveClaudeCommand, resolveCodexCommand } from './command' +import { getVersionManagerBinPaths, resolveClaudeCommand, resolveCodexCommand } from './command' function makeExecutable(path: string): void { mkdirSync(dirname(path), { recursive: true }) @@ -92,6 +92,14 @@ describe('resolveCodexCommand', () => { expect(resolveCodexCommand({ platform: 'win32', pathEnv: '', homePath: root })).toBe(bunPath) }) + it('finds Codex in mise shims directory', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-codex-command-')) + const misePath = join(root, '.local', 'share', 'mise', 'shims', 'codex') + makeExecutable(misePath) + + expect(resolveCodexCommand({ platform: 'linux', pathEnv: '', homePath: root })).toBe(misePath) + }) + it('returns the bare command when no filesystem candidate exists', () => { const root = mkdtempSync(join(tmpdir(), 'orca-codex-command-')) @@ -156,3 +164,35 @@ describe('resolveClaudeCommand', () => { expect(resolveClaudeCommand({ platform: 'linux', pathEnv: '', homePath: root })).toBe('claude') }) }) + +describe('getVersionManagerBinPaths', () => { + it('includes volta, asdf, fnm, mise, pnpm, yarn, and bun directories', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-vm-paths-')) + const paths = getVersionManagerBinPaths({ platform: 'darwin', pathEnv: '', homePath: root }) + + expect(paths).toContain(join(root, '.volta', 'bin')) + expect(paths).toContain(join(root, '.asdf', 'shims')) + expect(paths).toContain(join(root, '.fnm', 'aliases', 'default', 'bin')) + expect(paths).toContain(join(root, '.local', 'share', 'mise', 'shims')) + expect(paths).toContain(join(root, 'Library', 'pnpm')) + expect(paths).toContain(join(root, '.yarn', 'bin')) + expect(paths).toContain(join(root, '.bun', 'bin')) + }) + + it('includes nvm bin dir when node versions exist', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-vm-paths-')) + const nodeBin = join(root, '.nvm', 'versions', 'node', 'v22.14.0', 'bin', 'node') + makeExecutable(nodeBin) + + const paths = getVersionManagerBinPaths({ platform: 'darwin', pathEnv: '', homePath: root }) + expect(paths).toContain(join(root, '.nvm', 'versions', 'node', 'v22.14.0', 'bin')) + }) + + it('uses platform-specific pnpm path on Linux', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-vm-paths-')) + const paths = getVersionManagerBinPaths({ platform: 'linux', pathEnv: '', homePath: root }) + + expect(paths).toContain(join(root, '.local', 'share', 'pnpm')) + expect(paths).not.toContain(join(root, 'Library', 'pnpm')) + }) +}) diff --git a/src/main/codex-cli/command.ts b/src/main/codex-cli/command.ts index d4e17f731..ccdb7981d 100644 --- a/src/main/codex-cli/command.ts +++ b/src/main/codex-cli/command.ts @@ -71,7 +71,11 @@ function getVersionManagerDirectories( const directories = [ join(homePath, '.volta', 'bin'), join(homePath, '.asdf', 'shims'), - join(homePath, '.fnm', 'aliases', 'default', 'bin') + join(homePath, '.fnm', 'aliases', 'default', 'bin'), + // Why: mise (formerly rtx) exposes managed tool binaries via a shims + // directory, similar to asdf. Without this, users who installed node + // or CLI tools through mise can't be found by the fallback probe. + join(homePath, '.local', 'share', 'mise', 'shims') ] // Why: GUI-launched Electron apps do not inherit shell init from version @@ -141,3 +145,15 @@ export function resolveCodexCommand(options: ResolveCommandOptions = {}): string export function resolveClaudeCommand(options: ResolveCommandOptions = {}): string { return resolveCommand('claude', options) } + +// Why: GUI-launched Electron apps inherit a minimal PATH that excludes Node +// version manager directories. CLI tools like codex/claude are Node scripts +// with #!/usr/bin/env node shebangs — they need `node` in PATH to execute, +// not just to be *found*. This function returns the version manager bin paths +// so the caller can augment process.env.PATH at startup. +export function getVersionManagerBinPaths(options: ResolveCommandOptions = {}): string[] { + const platform = options.platform ?? process.platform + const homePath = options.homePath ?? homedir() + const nodeNames = getExecutableNames(platform, 'node') + return getVersionManagerDirectories(platform, homePath, nodeNames) +} diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 15308c188..f41549a96 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -85,25 +85,22 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise { if (!resolved) { @@ -213,14 +210,19 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise !existing.has(path))