import * as p from "@clack/prompts"; import pc from "picocolors"; import type { PaperclipConfig } from "../config/schema.js"; import { readConfig, resolveConfigPath } from "../config/store.js"; import { agentJwtSecretCheck, configCheck, databaseCheck, deploymentAuthCheck, llmCheck, logCheck, portCheck, secretsCheck, storageCheck, type CheckResult, } from "../checks/index.js"; const STATUS_ICON = { pass: pc.green("✓"), warn: pc.yellow("!"), fail: pc.red("✗"), } as const; export async function doctor(opts: { config?: string; repair?: boolean; yes?: boolean; }): Promise<{ passed: number; warned: number; failed: number }> { p.intro(pc.bgCyan(pc.black(" paperclip doctor "))); const configPath = resolveConfigPath(opts.config); const results: CheckResult[] = []; // 1. Config check (must pass before others) const cfgResult = configCheck(opts.config); results.push(cfgResult); printResult(cfgResult); if (cfgResult.status === "fail") { return printSummary(results); } let config: PaperclipConfig; try { config = readConfig(opts.config)!; } catch (err) { const readResult: CheckResult = { name: "Config file", status: "fail", message: `Could not read config: ${err instanceof Error ? err.message : String(err)}`, canRepair: false, repairHint: "Run `paperclip configure --section database` or `paperclip onboard`", }; results.push(readResult); printResult(readResult); return printSummary(results); } // 2. Deployment/auth mode check const deploymentAuthResult = deploymentAuthCheck(config); results.push(deploymentAuthResult); printResult(deploymentAuthResult); // 3. Agent JWT check const jwtResult = agentJwtSecretCheck(); results.push(jwtResult); printResult(jwtResult); await maybeRepair(jwtResult, opts); // 4. Secrets adapter check const secretsResult = secretsCheck(config, configPath); results.push(secretsResult); printResult(secretsResult); await maybeRepair(secretsResult, opts); // 5. Storage check const storageResult = storageCheck(config, configPath); results.push(storageResult); printResult(storageResult); await maybeRepair(storageResult, opts); // 6. Database check const dbResult = await databaseCheck(config, configPath); results.push(dbResult); printResult(dbResult); await maybeRepair(dbResult, opts); // 7. LLM check const llmResult = await llmCheck(config); results.push(llmResult); printResult(llmResult); // 8. Log directory check const logResult = logCheck(config, configPath); results.push(logResult); printResult(logResult); await maybeRepair(logResult, opts); // 9. Port check const portResult = await portCheck(config); results.push(portResult); printResult(portResult); // Summary return printSummary(results); } function printResult(result: CheckResult): void { const icon = STATUS_ICON[result.status]; p.log.message(`${icon} ${pc.bold(result.name)}: ${result.message}`); if (result.status !== "pass" && result.repairHint) { p.log.message(` ${pc.dim(result.repairHint)}`); } } async function maybeRepair( result: CheckResult, opts: { repair?: boolean; yes?: boolean }, ): Promise { if (result.status === "pass" || !result.canRepair || !result.repair) return; if (!opts.repair) return; let shouldRepair = opts.yes; if (!shouldRepair) { const answer = await p.confirm({ message: `Repair "${result.name}"?`, initialValue: true, }); if (p.isCancel(answer)) return; shouldRepair = answer; } if (shouldRepair) { try { await result.repair(); p.log.success(`Repaired: ${result.name}`); } catch (err) { p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`); } } } function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } { const passed = results.filter((r) => r.status === "pass").length; const warned = results.filter((r) => r.status === "warn").length; const failed = results.filter((r) => r.status === "fail").length; const parts: string[] = []; parts.push(pc.green(`${passed} passed`)); if (warned) parts.push(pc.yellow(`${warned} warnings`)); if (failed) parts.push(pc.red(`${failed} failed`)); p.note(parts.join(", "), "Summary"); if (failed > 0) { p.outro(pc.red("Some checks failed. Fix the issues above and re-run doctor.")); } else if (warned > 0) { p.outro(pc.yellow("All critical checks passed with some warnings.")); } else { p.outro(pc.green("All checks passed!")); } return { passed, warned, failed }; }