import { cli_locale } from "std/cli/localization"
import { cli_message } from "std/cli/localization/doctor"
import { safe_bool, safe_dict, safe_int, safe_list, safe_string } from "std/cli/render"
/**
* `harn doctor` rendering layer ported to .harn — see harn#2312 (W12).
*
* **Pragmatic partial port.** The actual probes (toolchain, providers,
* MCP, file permissions, manifest health, capability matrix, hardware
* snapshot, ollama, target probes) stay in Rust because each one
* reaches into a different VM/host facility (subprocess execution,
* `harn_vm::llm`, `notify::recommended_watcher`, `runtime_paths`,
* trigger registry snapshots, …) and is a multi-file refactor on its
* own. The Rust shim runs every check, assembles the structured
* `DoctorReport`, serialises it to JSON, and hands it across the
* dispatch wedge for formatting.
*
* What this script owns: the human-readable report layout (sections,
* pad widths, status uppercasing, suggested-fix indenting) and the
* pass-through of the JSON envelope. That's the surface a user reads
* or pipes into another tool, so it's the surface we want under .harn.
*
* Inputs (from the dispatch shim in
* crates/harn-cli/src/commands/doctor.rs):
* HARN_DOCTOR_REPORT_JSON — JSON-serialised `DoctorReport`
* (see the struct in doctor.rs for the canonical shape).
* HARN_DOCTOR_REPORT_ENVELOPE_JSON — JSON-serialised
* `JsonEnvelope<DoctorReport>` — used verbatim for `--json` so the
* byte stream preserves serde's declaration-order field layout
* and `null` for absent fields. Harn's `json_stringify_pretty`
* would alphabetise the keys, which would break downstream JSON
* consumers that expect the declared shape.
* HARN_OUTPUT_JSON — "1" for the JSON envelope, else
* human-readable text.
*/
/**
* Uppercase an ASCII status string (`ok` → `OK`, `warn` → `WARN`,
* etc). Harn's stdlib doesn't expose `to_uppercase` as a free
* builtin, so do the four cases by hand — there are only ever four
* status values.
*/
fn __status_upper(__cli_locale: string, status: string) -> string {
if status == "ok" {
return cli_message(__cli_locale, "OK", [])
}
if status == "warn" {
return cli_message(__cli_locale, "WARN", [])
}
if status == "fail" {
return cli_message(__cli_locale, "FAIL", [])
}
if status == "skip" {
return cli_message(__cli_locale, "SKIP", [])
}
return status
}
/**
* Render the `checks:` section. Layout matches the host formatter:
* `{status:>4} {label:<24} {detail}`
* where status is uppercased, padded to width 4 with a leading space,
* and label is left-justified to width 24. Failing/warning checks
* also emit indented `fix:`, `docs:`, and `blocks:` lines beneath
* their primary row.
*/
fn __render_checks_section(__cli_locale: string, checks: list) -> string {
let out = ""
for raw_check in checks {
const check = safe_dict(raw_check)
const status = safe_string(check["status"], "")
const label = safe_string(check["label"], "")
const detail = safe_string(check["detail"], "")
const status_upper = __status_upper(__cli_locale, status)
const status_padded = str_pad(status_upper, 4, " ", "left")
const label_padded = str_pad(label, 24, " ", "right")
out = out + status_padded + " " + label_padded + " " + detail + "\n"
if status != "ok" && status != "skip" {
const fix = check["fix_command"]
if type_of(fix) == "string" && fix != "" {
out = cli_message(__cli_locale, "{0} fix: {1}\n", [to_string(out), to_string(fix)])
}
const docs = check["docs_url"]
if type_of(docs) == "string" && docs != "" {
out = cli_message(__cli_locale, "{0} docs: {1}\n", [to_string(out), to_string(docs)])
}
const blocks = safe_list(check["blocks"])
if len(blocks) > 0 {
out = cli_message(
__cli_locale,
"{0} blocks: {1}\n",
[to_string(out), to_string(join(blocks, ", "))],
)
}
}
}
return out
}
fn __render_targets_section(__cli_locale: string, targets: list) -> string {
let out = cli_message(__cli_locale, "--- Targets ---\n", [])
for raw_target in targets {
const target = safe_dict(raw_target)
const triple = safe_string(target["triple"], "")
const triple_padded = str_pad(triple, 32, " ", "right")
const installed = if safe_bool(target["installed"], false) {
cli_message(__cli_locale, "installed", [])
} else {
cli_message(__cli_locale, "missing", [])
}
const buildable_raw = target["buildable"]
const buildable = if buildable_raw == nil {
cli_message(__cli_locale, "not probed", [])
} else if safe_bool(buildable_raw, false) {
cli_message(__cli_locale, "buildable", [])
} else {
cli_message(__cli_locale, "not buildable", [])
}
out = out + " " + triple_padded + " " + installed + ", " + buildable + "\n"
const reasons = safe_list(target["reasons"])
for reason in reasons {
if type_of(reason) == "string" {
out = out + " " + reason + "\n"
}
}
}
return out
}
fn __render_providers_section(__cli_locale: string, providers: list) -> string {
let out = cli_message(__cli_locale, "--- Providers ---\n", [])
for raw_provider in providers {
const provider = safe_dict(raw_provider)
const name = safe_string(provider["name"], "")
const name_padded = str_pad(name, 24, " ", "right")
const configured = if safe_bool(provider["configured"], false) {
cli_message(__cli_locale, "configured", [])
} else {
cli_message(__cli_locale, "no credentials", [])
}
const probed = safe_bool(provider["probed"], false)
const reachable_raw = provider["reachable"]
const latency_raw = provider["latency_ms"]
const reachable = if !probed {
cli_message(__cli_locale, "not probed", [])
} else if reachable_raw == nil {
cli_message(__cli_locale, "probed", [])
} else if safe_bool(reachable_raw, false) {
if latency_raw == nil {
cli_message(__cli_locale, "reachable", [])
} else {
cli_message(__cli_locale, "reachable ({0}ms)", [to_string(to_string(latency_raw))])
}
} else {
cli_message(__cli_locale, "unreachable", [])
}
out = out + " " + name_padded + " " + configured + ", " + reachable + "\n"
const errors = safe_list(provider["errors"])
for error in errors {
if type_of(error) == "string" {
out = out + " " + error + "\n"
}
}
}
return out
}
fn __render_capabilities_section(__cli_locale: string, capabilities: list) -> string {
let out = cli_message(__cli_locale, "--- Stdlib capabilities ---\n", [])
for raw_capability in capabilities {
const capability = safe_dict(raw_capability)
const name = safe_string(capability["name"], "")
const name_padded = str_pad(name, 24, " ", "right")
const profiles = safe_list(capability["available_in_sandbox_profile"])
out = cli_message(
__cli_locale,
"{0} {1} sandbox: {2}\n",
[to_string(out), to_string(name_padded), to_string(join(profiles, ", "))],
)
}
return out
}
fn __render_summary_section(__cli_locale: string, summary: dict) -> string {
let out = cli_message(__cli_locale, "--- Summary ---\n", [])
const ok_count = safe_int(summary["ok"], 0)
const warn_count = safe_int(summary["warning"], 0)
const fail_count = safe_int(summary["blocking"], 0)
const skip_count = safe_int(summary["skip"], 0)
out = cli_message(
__cli_locale,
"{0}OK={1} WARN={2} FAIL={3} SKIP={4}\n",
[
to_string(out),
to_string(to_string(ok_count)),
to_string(to_string(warn_count)),
to_string(to_string(fail_count)),
to_string(to_string(skip_count)),
],
)
const blocked = safe_list(summary["blocked_flows"])
if len(blocked) > 0 {
out = cli_message(
__cli_locale,
"{0}blocked: {1}\n",
[to_string(out), to_string(join(blocked, ", "))],
)
}
return out
}
/**
* Render the full human-readable report. Layout follows the host
* formatter:
* `Harn doctor\n`
* `\n`
* <checks>...
* `\n`
* `--- Targets ---\n` ...
* `\n`
* `--- Providers ---\n` ...
* `\n`
* `--- Stdlib capabilities ---\n` ...
* `\n`
* `--- Summary ---\n` ...
* `\n`
* `--- Next step ---\n`
* <next_step>
*/
fn __render_human(__cli_locale: string, report: dict) -> string {
let out = cli_message(__cli_locale, "Harn doctor\n\n", [])
out = out + __render_checks_section(__cli_locale, safe_list(report["checks"]))
out = out + "\n"
out = out + __render_targets_section(__cli_locale, safe_list(report["targets"]))
out = out + "\n"
out = out + __render_providers_section(__cli_locale, safe_list(report["providers"]))
out = out + "\n"
out = out + __render_capabilities_section(__cli_locale, safe_list(report["capabilities"]))
out = out + "\n"
out = out + __render_summary_section(__cli_locale, safe_dict(report["summary"]))
out = out + "\n"
out = cli_message(__cli_locale, "{0}--- Next step ---\n", [to_string(out)])
out = out + safe_string(report["next_step"], "")
return out
}
fn main(harness: Harness) -> int {
const __cli_locale = cli_locale(harness.env)
const raw = harness.env.get_or("HARN_DOCTOR_REPORT_JSON", "")
if raw == "" {
harness.stdio.eprintln("internal error: HARN_DOCTOR_REPORT_JSON not set by dispatch shim")
return 70
}
const report = try {
json_parse(raw)
} catch (e) {
harness.stdio.eprintln("internal error: failed to parse doctor report: " + e.message)
return 70
}
if type_of(report) != "dict" {
harness.stdio.eprintln("internal error: doctor report must be a JSON object")
return 70
}
const json_mode = harness.env.get_or("HARN_OUTPUT_JSON", "0") == "1"
if json_mode {
// Use the pre-serialised envelope from the Rust shim verbatim so
// the byte stream keeps serde's declaration-order field layout.
// Re-serialising via
// `json_stringify_pretty` here would alphabetise the keys.
const envelope_raw = harness.env.get_or("HARN_DOCTOR_REPORT_ENVELOPE_JSON", "")
if envelope_raw == "" {
harness.stdio.eprintln(
"internal error: HARN_DOCTOR_REPORT_ENVELOPE_JSON not set by dispatch shim",
)
return 70
}
harness.stdio.println(envelope_raw)
} else {
// The human renderer should end with one trailing newline.
// `__render_human` does not add that newline itself; let
// `harness.stdio.println` add exactly one.
harness.stdio.println(__render_human(__cli_locale, report))
}
const summary = safe_dict(report["summary"])
const blocking = safe_int(summary["blocking"], 0)
if blocking > 0 {
return 1
}
return 0
}