import { existsSync, readFileSync } from "node:fs" import { MIN_OPENCODE_VERSION, CHECK_IDS, CHECK_NAMES } from "../constants" import type { CheckResult, DoctorIssue, SystemInfo } from "../types" import { findOpenCodeBinary, getOpenCodeVersion, compareVersions } from "./system-binary" import { getPluginInfo } from "./system-plugin" import { getLatestPluginVersion, getLoadedPluginVersion, getSuggestedInstallTag } from "./system-loaded-version" import { parseJsonc } from "../../../shared" import { PUBLISHED_PACKAGE_NAME, PLUGIN_NAME, LEGACY_PLUGIN_NAME } from "../../../shared/plugin-identity" function isConfigValid(configPath: string | null): boolean { if (!configPath) return true if (!existsSync(configPath)) return false try { parseJsonc>(readFileSync(configPath, "utf-8")) return true } catch { return false } } function getResultStatus(issues: DoctorIssue[]): CheckResult["status"] { if (issues.some((issue) => issue.severity === "error")) return "fail" if (issues.some((issue) => issue.severity === "warning")) return "warn" return "pass" } function buildMessage(status: CheckResult["status"], issues: DoctorIssue[]): string { if (status === "pass") return "System checks passed" if (status === "fail") return `${issues.length} system issue(s) detected` return `${issues.length} system warning(s) detected` } export async function gatherSystemInfo(): Promise { const [binaryInfo, pluginInfo] = await Promise.all([findOpenCodeBinary(), Promise.resolve(getPluginInfo())]) const loadedInfo = getLoadedPluginVersion() const opencodeVersion = binaryInfo ? await getOpenCodeVersion(binaryInfo.path) : null const pluginVersion = pluginInfo.pinnedVersion ?? loadedInfo.expectedVersion ?? loadedInfo.loadedVersion return { opencodeVersion, opencodePath: binaryInfo?.path ?? null, pluginVersion, loadedVersion: loadedInfo.loadedVersion, bunVersion: Bun.version, configPath: pluginInfo.configPath, configValid: isConfigValid(pluginInfo.configPath), isLocalDev: pluginInfo.isLocalDev, } } export async function checkSystem(): Promise { const [systemInfo, pluginInfo] = await Promise.all([gatherSystemInfo(), Promise.resolve(getPluginInfo())]) const loadedInfo = getLoadedPluginVersion() const latestVersion = await getLatestPluginVersion(systemInfo.loadedVersion) const installTag = getSuggestedInstallTag(systemInfo.loadedVersion) const issues: DoctorIssue[] = [] if (!systemInfo.opencodePath) { issues.push({ title: "OpenCode binary not found", description: "Install OpenCode CLI or desktop and ensure the binary is available.", fix: "Install from https://opencode.ai/docs", severity: "error", affects: ["doctor", "run"], }) } if ( systemInfo.opencodeVersion && !compareVersions(systemInfo.opencodeVersion, MIN_OPENCODE_VERSION) ) { issues.push({ title: "OpenCode version below minimum", description: `Detected ${systemInfo.opencodeVersion}; required >= ${MIN_OPENCODE_VERSION}.`, fix: "Update OpenCode to the latest stable release", severity: "warning", affects: ["tooling", "doctor"], }) } if (!pluginInfo.registered) { issues.push({ title: `${PLUGIN_NAME} is not registered`, description: "Plugin entry is missing from OpenCode configuration.", fix: `Run: bunx ${PUBLISHED_PACKAGE_NAME} install`, severity: "error", affects: ["all agents"], }) } if (pluginInfo.entry && !pluginInfo.isLocalDev) { const isLegacyName = pluginInfo.entry === LEGACY_PLUGIN_NAME || pluginInfo.entry.startsWith(`${LEGACY_PLUGIN_NAME}@`) if (isLegacyName) { const suggestedEntry = pluginInfo.entry.replace(LEGACY_PLUGIN_NAME, PLUGIN_NAME) issues.push({ title: "Using legacy package name", description: `Your opencode.json references "${LEGACY_PLUGIN_NAME}" which has been renamed to "${PLUGIN_NAME}". The old name may stop working in a future release.`, fix: `Update your opencode.json plugin entry: "${pluginInfo.entry}" → "${suggestedEntry}"`, severity: "warning", affects: ["plugin loading"], }) } } if (loadedInfo.expectedVersion && loadedInfo.loadedVersion && loadedInfo.expectedVersion !== loadedInfo.loadedVersion) { issues.push({ title: "Loaded plugin version mismatch", description: `Cache expects ${loadedInfo.expectedVersion} but loaded ${loadedInfo.loadedVersion}.`, fix: `Reinstall: cd "${loadedInfo.cacheDir}" && bun install`, severity: "warning", affects: ["plugin loading"], }) } if ( systemInfo.loadedVersion && latestVersion && !compareVersions(systemInfo.loadedVersion, latestVersion) ) { issues.push({ title: "Loaded plugin is outdated", description: `Loaded ${systemInfo.loadedVersion}, latest ${latestVersion}.`, fix: `Update: cd "${loadedInfo.cacheDir}" && bun add ${PUBLISHED_PACKAGE_NAME}@${installTag}`, severity: "warning", affects: ["plugin features"], }) } const status = getResultStatus(issues) return { name: CHECK_NAMES[CHECK_IDS.SYSTEM], status, message: buildMessage(status, issues), details: [ systemInfo.opencodeVersion ? `OpenCode: ${systemInfo.opencodeVersion}` : "OpenCode: not detected", `Plugin expected: ${systemInfo.pluginVersion ?? "unknown"}`, `Plugin loaded: ${systemInfo.loadedVersion ?? "unknown"}`, `Bun: ${systemInfo.bunVersion ?? "unknown"}`, ], issues, } }