Implements a comprehensive 'doctor' command that diagnoses oh-my-opencode installation health with a beautiful TUI output. Checks performed: - OpenCode installation (version, path, binary) - Plugin registration in opencode.json - Configuration file validity (oh-my-opencode.json) - Auth providers (Anthropic, OpenAI, Google) - Dependencies (ast-grep CLI/NAPI, comment-checker) - LSP servers availability - MCP servers (builtin and user) - Version status and updates Features: - Beautiful TUI with symbols and colors - --verbose flag for detailed output - --json flag for machine-readable output - --category flag for running specific checks - Exit code 1 on failures for CI integration Closes #333 Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
128 lines
3.3 KiB
TypeScript
128 lines
3.3 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs"
|
|
import { homedir } from "node:os"
|
|
import { join } from "node:path"
|
|
import type { CheckResult, CheckDefinition, PluginInfo } from "../types"
|
|
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
|
|
import { parseJsonc } from "../../../shared"
|
|
|
|
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
|
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
|
|
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
|
|
|
function detectConfigPath(): { path: string; format: "json" | "jsonc" } | null {
|
|
if (existsSync(OPENCODE_JSONC)) {
|
|
return { path: OPENCODE_JSONC, format: "jsonc" }
|
|
}
|
|
if (existsSync(OPENCODE_JSON)) {
|
|
return { path: OPENCODE_JSON, format: "json" }
|
|
}
|
|
return null
|
|
}
|
|
|
|
function findPluginEntry(plugins: string[]): { entry: string; isPinned: boolean; version: string | null } | null {
|
|
for (const plugin of plugins) {
|
|
if (plugin === PACKAGE_NAME || plugin.startsWith(`${PACKAGE_NAME}@`)) {
|
|
const isPinned = plugin.includes("@")
|
|
const version = isPinned ? plugin.split("@")[1] : null
|
|
return { entry: plugin, isPinned, version }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function getPluginInfo(): PluginInfo {
|
|
const configInfo = detectConfigPath()
|
|
|
|
if (!configInfo) {
|
|
return {
|
|
registered: false,
|
|
configPath: null,
|
|
entry: null,
|
|
isPinned: false,
|
|
pinnedVersion: null,
|
|
}
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(configInfo.path, "utf-8")
|
|
const config = parseJsonc<{ plugin?: string[] }>(content)
|
|
const plugins = config.plugin ?? []
|
|
const pluginEntry = findPluginEntry(plugins)
|
|
|
|
if (!pluginEntry) {
|
|
return {
|
|
registered: false,
|
|
configPath: configInfo.path,
|
|
entry: null,
|
|
isPinned: false,
|
|
pinnedVersion: null,
|
|
}
|
|
}
|
|
|
|
return {
|
|
registered: true,
|
|
configPath: configInfo.path,
|
|
entry: pluginEntry.entry,
|
|
isPinned: pluginEntry.isPinned,
|
|
pinnedVersion: pluginEntry.version,
|
|
}
|
|
} catch {
|
|
return {
|
|
registered: false,
|
|
configPath: configInfo.path,
|
|
entry: null,
|
|
isPinned: false,
|
|
pinnedVersion: null,
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function checkPluginRegistration(): Promise<CheckResult> {
|
|
const info = getPluginInfo()
|
|
|
|
if (!info.configPath) {
|
|
return {
|
|
name: CHECK_NAMES[CHECK_IDS.PLUGIN_REGISTRATION],
|
|
status: "fail",
|
|
message: "OpenCode config file not found",
|
|
details: [
|
|
"Run: bunx oh-my-opencode install",
|
|
`Expected: ${OPENCODE_JSON} or ${OPENCODE_JSONC}`,
|
|
],
|
|
}
|
|
}
|
|
|
|
if (!info.registered) {
|
|
return {
|
|
name: CHECK_NAMES[CHECK_IDS.PLUGIN_REGISTRATION],
|
|
status: "fail",
|
|
message: "Plugin not registered in config",
|
|
details: [
|
|
"Run: bunx oh-my-opencode install",
|
|
`Config: ${info.configPath}`,
|
|
],
|
|
}
|
|
}
|
|
|
|
const message = info.isPinned
|
|
? `Registered (pinned: ${info.pinnedVersion})`
|
|
: "Registered"
|
|
|
|
return {
|
|
name: CHECK_NAMES[CHECK_IDS.PLUGIN_REGISTRATION],
|
|
status: "pass",
|
|
message,
|
|
details: [`Config: ${info.configPath}`],
|
|
}
|
|
}
|
|
|
|
export function getPluginCheckDefinition(): CheckDefinition {
|
|
return {
|
|
id: CHECK_IDS.PLUGIN_REGISTRATION,
|
|
name: CHECK_NAMES[CHECK_IDS.PLUGIN_REGISTRATION],
|
|
category: "installation",
|
|
check: checkPluginRegistration,
|
|
critical: true,
|
|
}
|
|
}
|