* 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
228 lines
5.9 KiB
TypeScript
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,
|
|
}
|
|
}
|