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>
124 lines
3.3 KiB
TypeScript
124 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, 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,
|
|
}
|
|
}
|