2026-02-13 17:29:38 +09:00
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"
2026-03-03 00:31:00 +09:00
import { getLatestPluginVersion , getLoadedPluginVersion , getSuggestedInstallTag } from "./system-loaded-version"
2026-02-13 17:29:38 +09:00
import { parseJsonc } from "../../../shared"
2026-03-26 17:01:42 +09:00
import { PLUGIN_NAME , LEGACY_PLUGIN_NAME } from "../../../shared/plugin-identity"
2026-02-13 17:29:38 +09:00
function isConfigValid ( configPath : string | null ) : boolean {
if ( ! configPath ) return true
if ( ! existsSync ( configPath ) ) return false
try {
parseJsonc < Record < string , unknown > > ( 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 < SystemInfo > {
const [ binaryInfo , pluginInfo ] = await Promise . all ( [ findOpenCodeBinary ( ) , Promise . resolve ( getPluginInfo ( ) ) ] )
const loadedInfo = getLoadedPluginVersion ( )
const opencodeVersion = binaryInfo ? await getOpenCodeVersion ( binaryInfo . path ) : null
2026-03-16 11:00:05 +09:00
const pluginVersion = pluginInfo . pinnedVersion ? ? loadedInfo . expectedVersion ? ? loadedInfo . loadedVersion
2026-02-13 17:29:38 +09:00
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 < CheckResult > {
const [ systemInfo , pluginInfo ] = await Promise . all ( [ gatherSystemInfo ( ) , Promise . resolve ( getPluginInfo ( ) ) ] )
const loadedInfo = getLoadedPluginVersion ( )
const latestVersion = await getLatestPluginVersion ( systemInfo . loadedVersion )
2026-03-03 00:31:00 +09:00
const installTag = getSuggestedInstallTag ( systemInfo . loadedVersion )
2026-02-13 17:29:38 +09:00
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 : "oh-my-opencode is not registered" ,
description : "Plugin entry is missing from OpenCode configuration." ,
fix : "Run: bunx oh-my-opencode install" ,
severity : "error" ,
affects : [ "all agents" ] ,
} )
}
2026-03-26 17:01:42 +09:00
// Detect legacy package name in plugin config
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" ] ,
} )
}
}
2026-02-13 17:29:38 +09:00
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 } . ` ,
2026-03-03 00:31:00 +09:00
fix : ` Reinstall: cd " ${ loadedInfo . cacheDir } " && bun install ` ,
2026-02-13 17:29:38 +09:00
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 } . ` ,
2026-03-03 00:31:00 +09:00
fix : ` Update: cd " ${ loadedInfo . cacheDir } " && bun add oh-my-opencode@ ${ installTag } ` ,
2026-02-13 17:29:38 +09:00
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 ,
}
}