feat(cli): add doctor command for installation health checks (#334)

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>
This commit is contained in:
Sisyphus
2025-12-30 15:06:41 +09:00
committed by GitHub
parent b92cd6ab68
commit b8efd3c771
25 changed files with 2936 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import type { CheckResult, CheckDefinition, ConfigInfo } from "../types"
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
import { parseJsonc, detectConfigFile } from "../../../shared"
import { OhMyOpenCodeConfigSchema } from "../../../config"
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, `${PACKAGE_NAME}`)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
function findConfigPath(): { path: string; format: "json" | "jsonc" } | null {
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
if (projectDetected.format !== "none") {
return { path: projectDetected.path, format: projectDetected.format as "json" | "jsonc" }
}
const userDetected = detectConfigFile(USER_CONFIG_BASE)
if (userDetected.format !== "none") {
return { path: userDetected.path, format: userDetected.format as "json" | "jsonc" }
}
return null
}
export function validateConfig(configPath: string): { valid: boolean; errors: string[] } {
try {
const content = readFileSync(configPath, "utf-8")
const rawConfig = parseJsonc<Record<string, unknown>>(content)
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig)
if (!result.success) {
const errors = result.error.issues.map(
(i) => `${i.path.join(".")}: ${i.message}`
)
return { valid: false, errors }
}
return { valid: true, errors: [] }
} catch (err) {
return {
valid: false,
errors: [err instanceof Error ? err.message : "Failed to parse config"],
}
}
}
export function getConfigInfo(): ConfigInfo {
const configPath = findConfigPath()
if (!configPath) {
return {
exists: false,
path: null,
format: null,
valid: true,
errors: [],
}
}
if (!existsSync(configPath.path)) {
return {
exists: false,
path: configPath.path,
format: configPath.format,
valid: true,
errors: [],
}
}
const validation = validateConfig(configPath.path)
return {
exists: true,
path: configPath.path,
format: configPath.format,
valid: validation.valid,
errors: validation.errors,
}
}
export async function checkConfigValidity(): Promise<CheckResult> {
const info = getConfigInfo()
if (!info.exists) {
return {
name: CHECK_NAMES[CHECK_IDS.CONFIG_VALIDATION],
status: "pass",
message: "Using default configuration",
details: ["No custom config file found (optional)"],
}
}
if (!info.valid) {
return {
name: CHECK_NAMES[CHECK_IDS.CONFIG_VALIDATION],
status: "fail",
message: "Configuration has validation errors",
details: [
`Path: ${info.path}`,
...info.errors.map((e) => `Error: ${e}`),
],
}
}
return {
name: CHECK_NAMES[CHECK_IDS.CONFIG_VALIDATION],
status: "pass",
message: `Valid ${info.format?.toUpperCase()} config`,
details: [`Path: ${info.path}`],
}
}
export function getConfigCheckDefinition(): CheckDefinition {
return {
id: CHECK_IDS.CONFIG_VALIDATION,
name: CHECK_NAMES[CHECK_IDS.CONFIG_VALIDATION],
category: "configuration",
check: checkConfigValidity,
critical: false,
}
}