import { existsSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import type { CheckResult, CheckDefinition, OpenCodeInfo } from "../types" import { CHECK_IDS, CHECK_NAMES, MIN_OPENCODE_VERSION, OPENCODE_BINARIES } from "../constants" const WINDOWS_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".ps1"] export function getDesktopAppPaths(platform: NodeJS.Platform): string[] { const home = homedir() switch (platform) { case "darwin": return [ "/Applications/OpenCode.app/Contents/MacOS/OpenCode", join(home, "Applications", "OpenCode.app", "Contents", "MacOS", "OpenCode"), ] case "win32": { const programFiles = process.env.ProgramFiles const localAppData = process.env.LOCALAPPDATA const paths: string[] = [] if (programFiles) { paths.push(join(programFiles, "OpenCode", "OpenCode.exe")) } if (localAppData) { paths.push(join(localAppData, "OpenCode", "OpenCode.exe")) } return paths } case "linux": return [ "/usr/bin/opencode", "/usr/lib/opencode/opencode", join(home, "Applications", "opencode-desktop-linux-x86_64.AppImage"), join(home, "Applications", "opencode-desktop-linux-aarch64.AppImage"), ] default: return [] } } export function getBinaryLookupCommand(platform: NodeJS.Platform): "which" | "where" { return platform === "win32" ? "where" : "which" } export function parseBinaryPaths(output: string): string[] { return output .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0) } export function selectBinaryPath( paths: string[], platform: NodeJS.Platform ): string | null { if (paths.length === 0) return null if (platform !== "win32") return paths[0] const normalized = paths.map((path) => path.toLowerCase()) for (const ext of WINDOWS_EXECUTABLE_EXTS) { const index = normalized.findIndex((path) => path.endsWith(ext)) if (index !== -1) return paths[index] } return paths[0] } export function buildVersionCommand( binaryPath: string, platform: NodeJS.Platform ): string[] { if ( platform === "win32" && binaryPath.toLowerCase().endsWith(".ps1") ) { return [ "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, "--version", ] } return [binaryPath, "--version"] } export function findDesktopBinary( platform: NodeJS.Platform = process.platform, checkExists: (path: string) => boolean = existsSync ): { binary: string; path: string } | null { const desktopPaths = getDesktopAppPaths(platform) for (const desktopPath of desktopPaths) { if (checkExists(desktopPath)) { return { binary: "opencode", path: desktopPath } } } return null } export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> { for (const binary of OPENCODE_BINARIES) { try { const path = Bun.which(binary) if (path) { return { binary, path } } } catch { continue } } const desktopResult = findDesktopBinary() if (desktopResult) { return desktopResult } return null } export async function getOpenCodeVersion( binaryPath: string, platform: NodeJS.Platform = process.platform ): Promise { try { const command = buildVersionCommand(binaryPath, platform) const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" }) const output = await new Response(proc.stdout).text() await proc.exited if (proc.exitCode === 0) { return output.trim() } } catch { return null } return null } export function compareVersions(current: string, minimum: string): boolean { const parseVersion = (v: string): number[] => { const cleaned = v.replace(/^v/, "").split("-")[0] return cleaned.split(".").map((n) => parseInt(n, 10) || 0) } const curr = parseVersion(current) const min = parseVersion(minimum) for (let i = 0; i < Math.max(curr.length, min.length); i++) { const c = curr[i] ?? 0 const m = min[i] ?? 0 if (c > m) return true if (c < m) return false } return true } export async function getOpenCodeInfo(): Promise { const binaryInfo = await findOpenCodeBinary() if (!binaryInfo) { return { installed: false, version: null, path: null, binary: null, } } const version = await getOpenCodeVersion(binaryInfo.path ?? binaryInfo.binary) return { installed: true, version, path: binaryInfo.path, binary: binaryInfo.binary as "opencode" | "opencode-desktop", } } export async function checkOpenCodeInstallation(): Promise { const info = await getOpenCodeInfo() if (!info.installed) { return { name: CHECK_NAMES[CHECK_IDS.OPENCODE_INSTALLATION], status: "fail", message: "OpenCode is not installed", details: [ "Visit: https://opencode.ai/docs for installation instructions", "Run: npm install -g opencode", ], } } if (info.version && !compareVersions(info.version, MIN_OPENCODE_VERSION)) { return { name: CHECK_NAMES[CHECK_IDS.OPENCODE_INSTALLATION], status: "warn", message: `Version ${info.version} is below minimum ${MIN_OPENCODE_VERSION}`, details: [ `Current: ${info.version}`, `Required: >= ${MIN_OPENCODE_VERSION}`, "Run: npm update -g opencode", ], } } return { name: CHECK_NAMES[CHECK_IDS.OPENCODE_INSTALLATION], status: "pass", message: info.version ?? "installed", details: info.path ? [`Path: ${info.path}`] : undefined, } } export function getOpenCodeCheckDefinition(): CheckDefinition { return { id: CHECK_IDS.OPENCODE_INSTALLATION, name: CHECK_NAMES[CHECK_IDS.OPENCODE_INSTALLATION], category: "installation", check: checkOpenCodeInstallation, critical: true, } }