|
| 1 | +/** |
| 2 | + * `bootcamp doctor` command. |
| 3 | + * |
| 4 | + * Runs environment diagnostics and prints either a colorized human report or a |
| 5 | + * stable JSON payload (`--json`). Exits with code 1 when a required check |
| 6 | + * fails so it can gate CI. The core logic lives in `src/doctor.ts`; this module |
| 7 | + * handles presentation and process wiring, with injectable dependencies so it |
| 8 | + * can be unit-tested without spawning real processes. |
| 9 | + */ |
| 10 | + |
| 11 | +import chalk from "chalk"; |
| 12 | + |
| 13 | +import { |
| 14 | + type DoctorCheck, |
| 15 | + type DoctorReport, |
| 16 | + type EnvironmentSnapshot, |
| 17 | + evaluateDoctor, |
| 18 | + formatDoctorReport, |
| 19 | + gatherEnvironment, |
| 20 | +} from "../doctor.js"; |
| 21 | + |
| 22 | +export interface DoctorCommandOptions { |
| 23 | + json?: boolean; |
| 24 | +} |
| 25 | + |
| 26 | +export interface DoctorCommandDeps { |
| 27 | + gather?: () => Promise<EnvironmentSnapshot>; |
| 28 | + log?: (message: string) => void; |
| 29 | + exit?: (code: number) => void; |
| 30 | +} |
| 31 | + |
| 32 | +interface DoctorJson { |
| 33 | + ok: boolean; |
| 34 | + hasWarnings: boolean; |
| 35 | + counts: DoctorReport["counts"]; |
| 36 | + checks: DoctorCheck[]; |
| 37 | + environment: EnvironmentSnapshot; |
| 38 | +} |
| 39 | + |
| 40 | +/** Build the machine-readable payload for `--json`. */ |
| 41 | +export function buildDoctorJson(report: DoctorReport, env: EnvironmentSnapshot): DoctorJson { |
| 42 | + return { |
| 43 | + ok: report.ok, |
| 44 | + hasWarnings: report.hasWarnings, |
| 45 | + counts: report.counts, |
| 46 | + checks: report.checks, |
| 47 | + environment: env, |
| 48 | + }; |
| 49 | +} |
| 50 | + |
| 51 | +const STATUS_COLOR: Record<DoctorCheck["status"], (text: string) => string> = { |
| 52 | + ok: chalk.green, |
| 53 | + warn: chalk.yellow, |
| 54 | + fail: chalk.red, |
| 55 | + info: chalk.gray, |
| 56 | +}; |
| 57 | + |
| 58 | +const STATUS_GLYPH: Record<DoctorCheck["status"], string> = { |
| 59 | + ok: "✓", |
| 60 | + warn: "!", |
| 61 | + fail: "✗", |
| 62 | + info: "·", |
| 63 | +}; |
| 64 | + |
| 65 | +/** Render a colorized human report for the terminal. */ |
| 66 | +export function colorizeReport(report: DoctorReport): string { |
| 67 | + const lines: string[] = []; |
| 68 | + lines.push(chalk.cyan.bold("repo-bootcamp environment check")); |
| 69 | + lines.push(""); |
| 70 | + for (const check of report.checks) { |
| 71 | + const color = STATUS_COLOR[check.status]; |
| 72 | + lines.push(` ${color(STATUS_GLYPH[check.status])} ${chalk.white(check.label)}: ${chalk.dim(check.detail)}`); |
| 73 | + if (check.remedy && (check.status === "fail" || check.status === "warn")) { |
| 74 | + lines.push(` ${chalk.dim("→ " + check.remedy)}`); |
| 75 | + } |
| 76 | + } |
| 77 | + lines.push(""); |
| 78 | + lines.push( |
| 79 | + chalk.dim( |
| 80 | + `Summary: ${report.counts.ok} ok, ${report.counts.warn} warning(s), ${report.counts.fail} failure(s)` |
| 81 | + ) |
| 82 | + ); |
| 83 | + lines.push( |
| 84 | + report.ok |
| 85 | + ? chalk.green("All required checks passed — you're ready to run bootcamp.") |
| 86 | + : chalk.red("One or more required checks failed — see remedies above.") |
| 87 | + ); |
| 88 | + return lines.join("\n"); |
| 89 | +} |
| 90 | + |
| 91 | +/** |
| 92 | + * Entry point used by the CLI. Returns the evaluated report (useful for tests); |
| 93 | + * triggers a non-zero exit when a required check fails. |
| 94 | + */ |
| 95 | +export async function runDoctor( |
| 96 | + options: DoctorCommandOptions = {}, |
| 97 | + deps: DoctorCommandDeps = {} |
| 98 | +): Promise<DoctorReport> { |
| 99 | + const gather = deps.gather ?? gatherEnvironment; |
| 100 | + const log = deps.log ?? ((msg: string) => console.log(msg)); |
| 101 | + const exit = deps.exit ?? ((code: number) => process.exit(code)); |
| 102 | + |
| 103 | + const env = await gather(); |
| 104 | + const report = evaluateDoctor(env); |
| 105 | + |
| 106 | + if (options.json) { |
| 107 | + log(JSON.stringify(buildDoctorJson(report, env), null, 2)); |
| 108 | + } else { |
| 109 | + log(colorizeReport(report)); |
| 110 | + } |
| 111 | + |
| 112 | + if (!report.ok) { |
| 113 | + exit(1); |
| 114 | + } |
| 115 | + |
| 116 | + return report; |
| 117 | +} |
| 118 | + |
| 119 | +export { formatDoctorReport }; |
0 commit comments