From c507ce7c56df3ef803b0a2ff3a8406d60af12911 Mon Sep 17 00:00:00 2001 From: GreenPi290 Date: Wed, 1 Apr 2026 15:52:47 +0900 Subject: [PATCH] fix(doctor): improve opencode binary detection for WSL - Add manual PATH directory search as fallback for WSL - Add isExecutable() to verify binary is actually executable - Reuse WINDOWS_EXECUTABLE_EXTS constant Fixes cubic-dev-ai review comments on PR #2991 --- src/cli/doctor/checks/system-binary.ts | 45 +++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/cli/doctor/checks/system-binary.ts b/src/cli/doctor/checks/system-binary.ts index 5a4d48126..372ee3562 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 { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide" @@ -7,6 +7,15 @@ import { OPENCODE_BINARIES } from "../constants" 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 @@ -94,15 +103,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(