feat(cli): add --json output to doctor#320
feat(cli): add --json output to doctor#320Dylanwooo wants to merge 2 commits intoheygen-com:mainfrom
Conversation
`doctor` is one of the first commands users and agents run when something is off — remote bug reports (e.g. heygen-com#294, heygen-com#316, heygen-com#317) typically include a doctor screenshot, which is painful to parse programmatically. Every other CLI command that reports state already supports `--json` (info, lint, compositions, catalog, benchmark, validate, capture). This brings `doctor` in line with that convention so: - CI pipelines can gate on `hyperframes doctor --json` (exit 1 on failure) without scraping terminal output - AI agents consuming telemetry / diagnostic data get structured input - Bug report tooling can attach machine-readable doctor output Schema: { "ok": boolean, "platform": "darwin" | "linux" | "win32", "arch": "arm64" | "x64" | ..., "checks": [ { "name": "FFmpeg", "ok": true, "detail": "ffmpeg version 8.1 …" }, { "name": "Docker", "ok": false, "detail": "Not found", "hint": "https://docs.docker.com/get-docker/" } ], "_meta": { "version", "latestVersion", "updateAvailable" } } Uses the existing `withMeta()` helper so the `_meta` envelope matches other `--json` commands. Human output format is unchanged.
jrusso1020
left a comment
There was a problem hiding this comment.
Thanks for this — direction is great and the change is well-scoped. A couple of things I'd like to see addressed before this merges:
1. Drop process.exitCode = 1 on check failure
Exit code should reflect whether the command succeeded, not whether the environment it inspected is healthy. doctor ran all its checks and produced valid output — that's a successful execution. Whether the result is "ok" is data in the payload, not a property of the run.
Two concrete problems with gating the exit code on allOk:
checkVersionpoisons the CI-gate use case. It returnsok: falsewhenever a newer npm version is available, so any pipeline doinghyperframes doctor --json || failwill start failing the next time a new version is published, even if the environment is perfectly fine.- Asymmetry with human mode. Bare
doctorexits 0 on failure;doctor --jsonexits 1. Surprising for anyone scripting against it.
Proposal: always exit 0 on successful execution (reserve non-zero for real errors — bad flags, JSON serialization failure, an uncaught exception in a check). Consumers who want to gate can do:
hyperframes doctor --json | jq -e '.ok' > /dev/null || handle_failureJust as ergonomic, and no version-check footgun. Worth a one-liner in the doctor docs pointing at this pattern.
2. Lock the JSON schema with a test
The PR body publishes a schema that CI pipelines and AI agents are expected to parse. There's currently nothing preventing a future refactor from silently renaming a field, dropping hint, or reordering checks[]. A snapshot test — feed a known environment into the check runner and assert the JSON shape — would lock the contract cheaply. The current test plan is all manual.
Nice-to-haves (non-blocking)
- Privacy: paths in
detail(notably theChromecheck) typically include/Users/<name>/or/home/<name>/. Same was true in human mode, but JSON output is designed to be pasted into bug reports and agent contexts. Consider$HOMEredaction in--jsonmode. - Nit:
import { freemem, platform } from "node:os"on line 9 sits belowexport const examples. Grouping imports at the top is the repo-wide convention.
Security-wise this is clean — all execSync calls are hardcoded, no user input reflected into shell or JSON.
Follows up on jrusso1020's review in heygen-com#320. Exit code no longer gated on check health --------------------------------------- `doctor --json` previously set exitCode=1 when any check failed. Two problems: - `checkVersion` returns ok:false whenever a newer npm version is available, so any pipeline using `hyperframes doctor --json || fail` would start failing the next time a new CLI version was published. - Asymmetric with bare `doctor` which always exits 0. Exit code now strictly reflects whether the command executed, not whether the environment is healthy. Consumers who want to gate do: hyperframes doctor --json | jq -e '.ok' > /dev/null || handle_failure Documented that pattern in docs/packages/cli.mdx. Schema locked with a snapshot test ---------------------------------- Extracted `buildDoctorReport()` as a pure function and added `doctor.test.ts` covering: - top-level key set (any accidental rename/addition fails the test) - shape of each CheckOutcome entry - ok flag true/false semantics - check-order preservation - hint field: omitted when absent, preserved when present - redact option both on and off Any future refactor that silently breaks the documented JSON contract will now fail CI. $HOME redaction for JSON mode ----------------------------- JSON output is explicitly designed to be pasted into bug reports and agent contexts. Added `redactHome()` so the user's home directory is replaced with the literal `$HOME` in `detail`/`hint` when --json is set. Human mode is unchanged (shows real paths). Import grouping --------------- Moved `node:os` + `_examples` imports up with the rest so `export const examples` no longer sits between imports.
|
Thanks for the thorough review — all four points addressed in 3079e8c. 1. Exit code no longer gated on check health ✅ 2. Schema locked with snapshot test ✅ 3. $HOME redaction ✅ 4. Import grouping ✅ Re-run locally:
|
Summary
hyperframes doctoris one of the first commands users and agents run when something's off — remote bug reports (e.g. #294, #316, #317) typically include a doctor screenshot, which is painful to parse programmatically.Every other CLI command that reports state already supports
--json(info,lint,compositions,catalog,benchmark,validate,capture). This bringsdoctorin line so:hyperframes doctor --jsonwithout scraping terminal output (exits 1 when any check fails)Human output is unchanged. This is purely additive.
Output schema
{ "ok": false, "platform": "darwin", "arch": "arm64", "checks": [ { "name": "Version", "ok": true, "detail": "0.4.4 (latest)" }, { "name": "FFmpeg", "ok": true, "detail": "ffmpeg version 8.1 Copyright (c) 2000-2026 …" }, { "name": "Docker", "ok": false, "detail": "Not found", "hint": "https://docs.docker.com/get-docker/" }, { "name": "Docker running", "ok": false, "detail": "Not running", "hint": "Start Docker Desktop or run: sudo systemctl start docker" } ], "_meta": { "version": "0.4.4", "latestVersion": "0.4.4", "updateAvailable": false } }Uses the existing
withMeta()helper so the_metaenvelope matches other--jsoncommands.Design notes
outcomes[]is populated in the same order checks run, so JSON consumers see the same ordering as the human view (Linux-only/dev/shmslots into the middle).hintfield. Only emitted when a check has a hint, matching the human mode which only prints hints on failures.--jsonfailure.process.exitCode = 1when any check fails sohyperframes doctor --json || handle_failureworks as a CI gate. Human mode still exits 0 (unchanged behavior).withMeta()fromutils/updateCheck.js.Test plan
bunx oxlint packages/cli/src/commands/doctor.ts— cleanbunx oxfmt --check packages/cli/src/commands/doctor.ts— cleanbunx tsx packages/cli/src/cli.ts doctor— human output unchangedbunx tsx packages/cli/src/cli.ts doctor --json— valid JSON, correct schemabunx tsx packages/cli/src/cli.ts doctor --help—--jsonlisted, new example appearsFollow-ups (out of scope)
The
doctordocs page could be updated to mention--jsonalongside other--json-aware commands. Happy to do that in a follow-up PR if merged.