Files
oh-my-openagent/src/cli/doctor/checks/opencode.ts
YeonGyu-Kim d7807072e1 feat(doctor): detect OpenCode desktop GUI installations on all platforms (#1352)
* feat(doctor): detect OpenCode desktop GUI installations on all platforms

- Add getDesktopAppPaths() returning platform-specific desktop app paths
  - macOS: /Applications/OpenCode.app, ~/Applications/OpenCode.app
  - Windows: C:\Program Files\OpenCode, %LOCALAPPDATA%\Programs\OpenCode
  - Linux: /opt/opencode, /snap/bin, ~/.local/bin
- Add findDesktopBinary() for testable desktop path detection
- Modify findOpenCodeBinary() to check desktop paths as fallback

Fixes #1310

* fix: use verified installation paths from OpenCode source

Verified paths from sst/opencode Tauri config:

macOS:
- /Applications/OpenCode.app/Contents/MacOS/OpenCode (capital C)

Windows:
- C:\Program Files\OpenCode\OpenCode.exe
- %LOCALAPPDATA%\OpenCode\OpenCode.exe
- Removed hardcoded paths, use ProgramFiles env var
- Filter empty paths when env vars undefined

Linux:
- /usr/bin/opencode (deb symlink)
- /usr/lib/opencode/opencode (deb actual binary)
- ~/Applications/*.AppImage (user AppImage)
- Removed non-existent /opt/opencode and /snap/bin paths

* chore: remove unused imports from tests
2026-02-01 19:42:37 +09:00

228 lines
5.9 KiB
TypeScript

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<string | null> {
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<OpenCodeInfo> {
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<CheckResult> {
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,
}
}