Fix Windows Claude CLI detection (#2145)

This commit is contained in:
Jinwoo Hong 2026-05-17 05:47:12 -04:00 committed by GitHub
parent 673e558f49
commit 8370d78e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 81 additions and 28 deletions

View File

@ -158,6 +158,16 @@ describe('resolveClaudeCommand', () => {
expect(resolveClaudeCommand({ platform: 'darwin', pathEnv: '', homePath: root })).toBe(bunPath)
})
it('finds native Windows claude.exe in user-local bin', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-claude-command-'))
const nativePath = join(root, '.local', 'bin', 'claude.exe')
makeExecutable(nativePath)
expect(resolveClaudeCommand({ platform: 'win32', pathEnv: '', homePath: root })).toBe(
nativePath
)
})
it('returns the bare command when no filesystem candidate exists', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-claude-command-'))
@ -195,4 +205,12 @@ describe('getVersionManagerBinPaths', () => {
expect(paths).toContain(join(root, '.local', 'share', 'pnpm'))
expect(paths).not.toContain(join(root, 'Library', 'pnpm'))
})
it('includes Windows user-local bin for native CLI installers', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-vm-paths-'))
const paths = getVersionManagerBinPaths({ platform: 'win32', pathEnv: '', homePath: root })
expect(paths).toContain(join(root, '.local', 'bin'))
expect(paths).toContain(join(root, 'AppData', 'Roaming', 'npm'))
})
})

View File

@ -98,6 +98,9 @@ function getVersionManagerDirectories(
}
if (platform === 'win32') {
// Why: Anthropic's native Windows installer places claude.exe here, and
// GUI-launched Orca may not inherit the user's PATH entry for it.
directories.push(join(homePath, '.local', 'bin'))
directories.push(join(homePath, 'AppData', 'Roaming', 'npm'))
directories.push(join(homePath, 'AppData', 'Local', 'pnpm'))
directories.push(join(homePath, 'AppData', 'Local', 'Yarn', 'bin'))

View File

@ -1,3 +1,4 @@
import { homedir } from 'os'
import { join } from 'path'
import { afterEach, describe, expect, it, vi } from 'vitest'
@ -70,9 +71,9 @@ describe('patchPackagedProcessPath', () => {
// fallback install locations for the opencode and Pi CLI install scripts.
// Without them on PATH, GUI-launched Orca reports both as "Not installed"
// even when `which` resolves them in the user's shell.
expect(segments).toContain('/Users/tester/.opencode/bin')
expect(segments).toContain('/Users/tester/.vite-plus/bin')
expect(segments).toContain('/Users/tester/bin')
expect(segments).toContain(join('/Users/tester', '.opencode/bin'))
expect(segments).toContain(join('/Users/tester', '.vite-plus/bin'))
expect(segments).toContain(join('/Users/tester', 'bin'))
})
it('leaves PATH untouched when the app is not packaged', async () => {
@ -88,6 +89,23 @@ describe('patchPackagedProcessPath', () => {
expect(process.env.PATH).toBe('/usr/bin:/bin')
})
it('prepends Windows user-local CLI dirs for packaged Start Menu launches', async () => {
const { app } = await import('electron')
const { patchPackagedProcessPath } = await import('./configure-process')
setPlatform('win32')
Object.defineProperty(app, 'isPackaged', { configurable: true, value: true })
const pathDelimiter = process.platform === 'win32' ? ';' : ':'
process.env.PATH = `C:\\Windows\\System32${pathDelimiter}C:\\Windows`
patchPackagedProcessPath()
const segments = (process.env.PATH ?? '').split(pathDelimiter)
const userLocalBin = join(homedir(), '.local', 'bin')
expect(segments).toContain(userLocalBin)
expect(segments.indexOf(userLocalBin)).toBeLessThan(segments.indexOf('C:\\Windows\\System32'))
})
})
describe('configureDevUserDataPath', () => {

View File

@ -5,6 +5,10 @@ import { getMainE2EConfig } from '../e2e-config'
const DEV_PARENT_SHUTDOWN_GRACE_MS = 3000
function getProcessPathDelimiter(): string {
return process.platform === 'win32' ? ';' : ':'
}
function requestDevParentShutdown(): void {
app.quit()
@ -36,35 +40,39 @@ export function installUncaughtPipeErrorGuard(): void {
}
export function patchPackagedProcessPath(): void {
if (!app.isPackaged || process.platform === 'win32') {
if (!app.isPackaged) {
return
}
const home = process.env.HOME ?? ''
const extraPaths = [
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
'/usr/local/sbin',
'/snap/bin',
'/home/linuxbrew/.linuxbrew/bin',
'/nix/var/nix/profiles/default/bin'
]
const extraPaths: string[] = []
if (home) {
if (process.platform !== 'win32') {
extraPaths.push(
join(home, 'bin'),
join(home, '.local/bin'),
join(home, '.nix-profile/bin'),
// Why: several agent CLIs ship install scripts that drop binaries into
// tool-specific ~/.<name>/bin directories (opencode's documented fallback,
// Pi's vite-plus installer). GUI-launched Electron inherits a minimal PATH
// without shell rc files, so these stay invisible to `which` probes — and
// the Agents settings page reports them as "Not installed" even when the
// user can run them from Terminal. See stablyai/orca#829.
join(home, '.opencode/bin'),
join(home, '.vite-plus/bin')
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
'/usr/local/sbin',
'/snap/bin',
'/home/linuxbrew/.linuxbrew/bin',
'/nix/var/nix/profiles/default/bin'
)
if (home) {
extraPaths.push(
join(home, 'bin'),
join(home, '.local/bin'),
join(home, '.nix-profile/bin'),
// Why: several agent CLIs ship install scripts that drop binaries into
// tool-specific ~/.<name>/bin directories (opencode's documented fallback,
// Pi's vite-plus installer). GUI-launched Electron inherits a minimal PATH
// without shell rc files, so these stay invisible to `which` probes — and
// the Agents settings page reports them as "Not installed" even when the
// user can run them from Terminal. See stablyai/orca#829.
join(home, '.opencode/bin'),
join(home, '.vite-plus/bin')
)
}
}
// Why: CLI tools installed via Node version managers (nvm, volta, asdf, fnm,
@ -72,14 +80,20 @@ export function patchPackagedProcessPath(): void {
// resolveCodexCommand() can locate the codex binary in these directories, but
// spawning it still fails if node itself isn't in PATH. Adding version manager
// bin paths here fixes all spawn sites (login, rate limits, usage tracking).
// On Windows this also seeds user-local installer dirs, since shell hydration
// is POSIX-only and Start Menu launches can miss user-level PATH updates.
extraPaths.push(...getVersionManagerBinPaths())
const currentPath = process.env.PATH ?? ''
const existing = new Set(currentPath.split(':'))
const pathKey = process.platform === 'win32' && process.env.Path !== undefined ? 'Path' : 'PATH'
const currentPath = process.env[pathKey] ?? ''
const pathDelimiter = getProcessPathDelimiter()
const existing = new Set(currentPath.split(pathDelimiter))
const missing = extraPaths.filter((path) => !existing.has(path))
if (missing.length > 0) {
process.env.PATH = [...missing, ...currentPath.split(':').filter(Boolean)].join(':')
process.env[pathKey] = [...missing, ...currentPath.split(pathDelimiter).filter(Boolean)].join(
pathDelimiter
)
}
}