diff --git a/src/cli/doctor/checks/system-binary.ts b/src/cli/doctor/checks/system-binary.ts index 9a92f7232..e26410dc9 100644 --- a/src/cli/doctor/checks/system-binary.ts +++ b/src/cli/doctor/checks/system-binary.ts @@ -1,4 +1,4 @@ -import { existsSync } from "node:fs" +import { existsSync, accessSync, constants } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { extractSemverFromOutput } from "../../../shared/extract-semver" @@ -10,6 +10,15 @@ export { extractSemverFromOutput } const WINDOWS_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".ps1"] +function isExecutable(path: string): boolean { + try { + accessSync(path, constants.X_OK) + return true + } catch { + return false + } +} + export interface OpenCodeBinaryInfo { binary: string path: string @@ -97,15 +106,43 @@ export function findDesktopBinary( return null } -export async function findOpenCodeBinary(): Promise { +export async function findOpenCodeBinary( + platform: NodeJS.Platform = process.platform, + checkExists: (path: string) => boolean = existsSync, +): Promise { + // 1) Try Bun.which first for (const binary of OPENCODE_BINARIES) { const path = Bun.which(binary) - if (path) { + if (path && checkExists(path)) { return { binary, path } } } - return findDesktopBinary() + // 2) Manually search through PATH directories (robust for WSL/mixed environments) + const pathEnv = process.env.PATH ?? "" + const delimiter = platform === "win32" ? ";" : ":" + const candidates = getCommandCandidates(platform) + + for (const entry of pathEnv.split(delimiter).filter(Boolean)) { + for (const command of candidates) { + const fullPath = join(entry, command) + if (checkExists(fullPath) && isExecutable(fullPath)) { + return { binary: command, path: fullPath } + } + } + } + + // 3) Fall back to desktop app paths + return findDesktopBinary(platform, checkExists) +} + +function getCommandCandidates(platform: NodeJS.Platform): string[] { + if (platform !== "win32") return [...OPENCODE_BINARIES] + + const WINDOWS_SUFFIXES = ["", ...WINDOWS_EXECUTABLE_EXTS] as const + return OPENCODE_BINARIES.flatMap((command) => + WINDOWS_SUFFIXES.map((suffix) => `${command}${suffix}`), + ) } export async function getOpenCodeVersion(