use super::*;
pub(crate) fn exit_code(report: &DoctorReport) -> u8 {
let any_fail = report
.observability
.iter()
.chain(report.completions.iter())
.any(|c| matches!(c.status, Status::Fail))
|| report
.agents
.iter()
.any(|agent| matches!(agent.status, Status::Fail))
|| report.host_plugins.iter().any(|plugin| !plugin.ok())
|| matches!(report.configuration.workspace.status, Status::Fail)
|| matches!(report.configuration.global.status, Status::Fail)
|| matches!(report.configuration.system.status, Status::Fail)
|| matches!(report.configuration.plugin_resolution.status, Status::Fail)
|| matches!(report.configuration.resolution.status, Status::Fail);
u8::from(any_fail)
}
pub(super) fn report_has_warn(report: &DoctorReport) -> bool {
report
.observability
.iter()
.chain(report.completions.iter())
.any(|c| matches!(c.status, Status::Warn))
|| report
.agents
.iter()
.any(|agent| matches!(agent.status, Status::Warn))
|| report.host_plugins.iter().any(|plugin| !plugin.ok())
|| matches!(report.configuration.workspace.status, Status::Warn)
|| matches!(report.configuration.global.status, Status::Warn)
|| matches!(report.configuration.system.status, Status::Warn)
|| matches!(report.configuration.plugin_resolution.status, Status::Warn)
|| matches!(report.configuration.resolution.status, Status::Warn)
}
pub(crate) fn format_human(report: &DoctorReport) -> String {
let mut out = String::new();
format_human_header(&mut out, report);
format_human_environment(&mut out, report);
format_human_configuration(&mut out, report);
format_human_plugin_configuration(&mut out, report);
format_human_agents(&mut out, report);
format_human_host_plugins(&mut out, report);
format_human_checks(&mut out, "Observability", &report.observability);
format_human_completion_checks(&mut out, &report.completions);
format_human_conclusion(&mut out, report);
out
}
pub(super) fn format_human_header(out: &mut String, report: &DoctorReport) {
out.push_str(&format!("\n NeMo Relay {}\n", report.binary_version));
out.push_str(" ─────────────────────────────────────────────\n");
if let Some(agent) = &report.target_agent {
out.push_str(&format!(" Target agent {agent}\n\n"));
}
}
pub(super) fn format_human_environment(out: &mut String, report: &DoctorReport) {
out.push_str(" Environment\n");
out.push_str(&format!(
" OS {}\n",
report.environment.os.trim()
));
out.push_str(&format!(" Arch {}\n", report.environment.arch));
if let Some(shell) = &report.environment.shell {
out.push_str(&format!(" Shell {shell}\n"));
}
out.push('\n');
}
pub(super) fn format_human_configuration(out: &mut String, report: &DoctorReport) {
out.push_str(" Configuration\n");
out.push_str(&format!(
" Workspace {}\n",
format_layer(&report.configuration.workspace)
));
out.push_str(&format!(
" Global {}\n",
format_layer(&report.configuration.global)
));
out.push_str(&format!(
" System {}\n",
format_layer(&report.configuration.system)
));
if !matches!(report.configuration.resolution.status, Status::Pass) {
out.push_str(&format!(
" Resolution {} {}\n",
format_status(report.configuration.resolution.status),
report.configuration.resolution.details
));
}
if !report.configuration.configured_agents.is_empty() {
out.push_str(&format!(
" Agents {}\n",
report.configuration.configured_agents.join(", ")
));
}
out.push('\n');
}
pub(super) fn format_human_plugin_configuration(out: &mut String, report: &DoctorReport) {
out.push_str(" Plugin configuration\n");
for plugin in &report.configuration.dynamic_plugins {
let config_suffix = if matches!(
plugin.host_config_status,
DynamicPluginHostConfigStatus::Present
) {
"; host config"
} else {
""
};
out.push_str(&format!(
" Dynamic {} ({}){}\n",
plugin.plugin_id, plugin.manifest_ref, config_suffix
));
}
if !report.configuration.plugin_configs.is_empty() {
for (index, layer) in report.configuration.plugin_configs.iter().enumerate() {
let label = if index == 0 { "Plugin files" } else { "" };
out.push_str(&format!(" {label:<13}{}\n", format_layer(layer)));
}
}
out.push_str(&format!(
" Plugins {} {}\n",
format_status(report.configuration.plugin_resolution.status),
report.configuration.plugin_resolution.details
));
for plugin in &report.configuration.dynamic_plugins {
for check in [
dynamic_plugin_reference_check(plugin),
dynamic_plugin_host_config_check(plugin),
] {
out.push_str(&format!(
" Dynamic {} {}\n",
format_status(check.status),
check.details
));
}
}
out.push('\n');
}
pub(super) fn format_human_agents(out: &mut String, report: &DoctorReport) {
out.push_str(" Agents detected\n");
for agent in &report.agents {
let status = format_status(agent.status);
match &agent.path {
Some(path) => {
let version = agent.version.as_deref().unwrap_or("(unknown version)");
out.push_str(&format!(
" {} {:<8} {}\n command {}\n path {}\n {}\n",
status,
agent.name,
version,
agent.command,
path.display(),
agent.annotation
));
}
None => {
out.push_str(&format!(
" {} {:<8} not on $PATH\n command {}\n {}\n",
status, agent.name, agent.command, agent.annotation
));
}
}
}
out.push('\n');
}
pub(super) fn format_human_host_plugins(out: &mut String, report: &DoctorReport) {
out.push_str(" Persistent integrations\n");
if report.host_plugins.is_empty() {
out.push_str(" · none installed; run `nemo-relay install <host>` to enable one\n");
} else {
for plugin in &report.host_plugins {
out.push_str(&format!(
" {} {}\n",
if plugin.ok() { "✓" } else { "✗" },
plugin.host
));
for check in &plugin.checks {
out.push_str(&format!(
" {} {}: {}\n",
if check.ok { "✓" } else { "✗" },
check.name,
check.details
));
}
if !plugin.ok() {
out.push_str(&format!(" repair: {}\n", plugin.remediation));
}
}
}
out.push('\n');
}
pub(super) fn format_human_checks(out: &mut String, title: &str, checks: &[Check]) {
out.push_str(&format!(" {title}\n"));
for check in checks {
out.push_str(&format!(
" {} {:<22} {}\n",
format_status(check.status),
check.name,
check.details
));
}
out.push('\n');
}
pub(super) fn format_human_completion_checks(out: &mut String, checks: &[Check]) {
out.push_str(" Completions\n");
for check in checks {
out.push_str(&format!(" {}\n", check.details));
}
out.push('\n');
}
pub(super) fn format_human_conclusion(out: &mut String, report: &DoctorReport) {
if exit_code(report) == 0 {
if report_has_warn(report) {
out.push_str(" All checks passed, but some issued warnings; see details above.\n");
} else {
out.push_str(" All checks passed.\n");
}
} else {
out.push_str(" Some checks FAILED; see details above.\n");
}
}
pub(super) fn format_layer(layer: &ConfigLayer) -> String {
let active = if layer.active { " (loaded)" } else { "" };
format!("{} {}{}", layer.path.display(), layer.details, active)
}
pub(super) fn format_status(status: Status) -> &'static str {
match status {
Status::Pass => "✓",
Status::Warn => "!",
Status::Fail => "✗",
Status::Info => "·",
}
}
pub(crate) fn format_json(report: &DoctorReport) -> Result<String, CliError> {
serde_json::to_string_pretty(report)
.map_err(|err| CliError::Config(format!("could not serialize doctor report: {err}")))
}
pub(crate) async fn agents_report() -> Result<Vec<AgentInfo>, CliError> {
let resolved = resolve_server_config(&GatewayOverrides::default())?;
Ok(collect_agents(None, &resolved).await)
}
pub(crate) fn format_agents_human(agents: &[AgentInfo]) -> String {
let mut out = String::new();
out.push_str("\n Supported\n");
for agent in agents {
out.push_str(&format!(" {}\n", agent.name));
}
out.push('\n');
out.push_str(" Detected on this machine\n");
let detected: Vec<&AgentInfo> = agents.iter().filter(|a| a.path.is_some()).collect();
if detected.is_empty() {
out.push_str(" (none)\n");
} else {
for agent in detected {
let version = agent.version.as_deref().unwrap_or("(unknown version)");
let path = agent
.path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
out.push_str(&format!(
" {} {:<8} {}\n {}\n {}\n",
format_status(agent.status),
agent.name,
version,
path,
agent.annotation
));
}
}
out.push('\n');
out
}