use std::process::ExitCode;
use std::time::Duration;
use anyhow::Result;
use astrid_core::dirs::AstridHome;
use astrid_core::kernel_api::{KernelRequest, KernelResponse};
use clap::Args;
use colored::Colorize;
use crate::theme::Theme;
#[derive(Args, Debug, Clone)]
pub(crate) struct DoctorArgs {
#[arg(long = "no-daemon")]
pub no_daemon: bool,
}
pub(crate) async fn run(args: DoctorArgs) -> Result<ExitCode> {
println!("{}", "Astrid health check".bold());
let mut all_passed = true;
let home_check = match AstridHome::resolve() {
Ok(home) => {
check_pass(
"ASTRID_HOME",
&format!("resolved to {}", home.root().display()),
);
Some(home)
},
Err(e) => {
all_passed = false;
check_fail("ASTRID_HOME", &format!("{e}"));
None
},
};
if let Some(home) = home_check.as_ref() {
let runtime_key = home.runtime_key_path();
if runtime_key.exists() {
check_pass(
"Runtime signing key",
&format!("present at {}", runtime_key.display()),
);
} else {
check_warn(
"Runtime signing key",
&format!(
"missing at {}; will be generated on first daemon boot",
runtime_key.display()
),
);
}
let socket = home.socket_path();
if socket.exists() {
check_pass("Daemon socket", &format!("present at {}", socket.display()));
} else {
check_warn(
"Daemon socket",
&format!("missing at {} — run `astrid start`", socket.display()),
);
}
}
if !args.no_daemon
&& let Some(home) = home_check.as_ref()
&& home.socket_path().exists()
{
match daemon_roundtrip().await {
Ok(()) => check_pass("Daemon roundtrip", "GetStatus succeeded"),
Err(e) => {
all_passed = false;
check_fail("Daemon roundtrip", &e.to_string());
},
}
match agent_readiness().await {
Ok(report) => {
if report.ready {
check_pass(
"Agent loop readiness",
&format!("ready ({} capsule(s) loaded)", report.loaded_capsules.len()),
);
} else {
all_passed = false;
check_fail("Agent loop readiness", &readiness_detail(&report));
}
},
Err(e) => check_warn("Agent loop readiness", &format!("could not probe: {e}")),
}
}
println!();
if all_passed {
println!("{}", Theme::success("All checks passed."));
Ok(ExitCode::SUCCESS)
} else {
println!("{}", Theme::error("One or more checks failed."));
Ok(ExitCode::from(1))
}
}
fn check_pass(name: &str, detail: &str) {
println!(" [{}] {} — {}", "OK".green().bold(), name.bold(), detail);
}
fn check_warn(name: &str, detail: &str) {
println!(
" [{}] {} — {}",
"WARN".yellow().bold(),
name.bold(),
detail
);
}
fn check_fail(name: &str, detail: &str) {
println!(" [{}] {} — {}", "FAIL".red().bold(), name.bold(), detail);
}
async fn daemon_roundtrip() -> Result<()> {
let mut client = tokio::time::timeout(
Duration::from_secs(5),
crate::socket_client::connect_kernel_for_workspace(None),
)
.await
.map_err(|_| anyhow::anyhow!("connection timed out after 5s"))??;
match tokio::time::timeout(
Duration::from_secs(5),
client.request(KernelRequest::GetStatus),
)
.await
.map_err(|_| anyhow::anyhow!("daemon response timed out after 5s"))??
{
KernelResponse::Status(_) => Ok(()),
KernelResponse::Error(message) => {
Err(anyhow::anyhow!("daemon rejected status request: {message}"))
},
_ => Err(anyhow::anyhow!(
"daemon returned an unexpected status response"
)),
}
}
async fn agent_readiness() -> Result<astrid_core::kernel_api::AgentLoopReadiness> {
let mut client = tokio::time::timeout(
Duration::from_secs(5),
crate::socket_client::connect_kernel_for_workspace(None),
)
.await
.map_err(|_| anyhow::anyhow!("connection timed out after 5s"))??;
match tokio::time::timeout(
Duration::from_secs(5),
client.request(KernelRequest::GetAgentReadiness),
)
.await
.map_err(|_| anyhow::anyhow!("daemon response timed out after 5s"))??
{
KernelResponse::AgentReadiness(readiness) => Ok(readiness),
KernelResponse::Error(msg) => {
Err(anyhow::anyhow!("daemon rejected readiness query: {msg}"))
},
_ => Err(anyhow::anyhow!(
"daemon did not return an agent-readiness response"
)),
}
}
fn readiness_detail(report: &astrid_core::kernel_api::AgentLoopReadiness) -> String {
let mut parts: Vec<String> = Vec::new();
if report.prompt_subscribers.is_empty() {
parts.push(format!(
"no capsule subscribes {}",
astrid_capsule::readiness::AGENT_PROMPT_TOPIC
));
}
if report.response_publishers.is_empty() {
parts.push(format!(
"no capsule publishes {}",
astrid_capsule::readiness::AGENT_RESPONSE_TOPIC
));
}
if !report.unsatisfied_required_imports.is_empty() {
let ifaces: Vec<String> = report
.unsatisfied_required_imports
.iter()
.map(|m| format!("{}:{} ({})", m.namespace, m.interface, m.requirement))
.collect();
parts.push(format!("unsatisfied interfaces: {}", ifaces.join(", ")));
}
if parts.is_empty() {
"not ready".to_string()
} else {
parts.join("; ")
}
}