use std::path::{Path, PathBuf};
use super::is_running;
use crate::paths::agents_dir;
use crate::routines::{agent_command_available_in, AgentCommand};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Check {
pub label: String,
pub failure: Option<String>,
}
impl Check {
fn fail(label: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
label: label.into(),
failure: Some(reason.into()),
}
}
fn require(label: impl Into<String>, found: bool, reason: &str) -> Self {
let label = label.into();
if found {
Self {
label,
failure: None,
}
} else {
Self::fail(label, reason)
}
}
pub const fn passed(&self) -> bool {
self.failure.is_none()
}
pub fn render(&self) -> String {
match &self.failure {
None => format!("ok {}", self.label),
Some(reason) => format!("FAIL {} — {reason}", self.label),
}
}
}
pub fn run_checks(path: &str, agents_dir: &Path, daemon_up: bool) -> Vec<Check> {
let mut checks = vec![
Check::require(
"tmux",
agent_command_available_in(path, "tmux"),
"not found on PATH (routine runs cannot launch)",
),
Check::require(
"python3",
agent_command_available_in(path, "python3"),
"not found on PATH (agent setup steps cannot run)",
),
];
checks.extend(agent_checks(path, agents_dir));
checks.push(Check::require(
"daemon",
daemon_up,
"not running; start it with: moadim",
));
checks
}
fn agent_checks(path: &str, dir: &Path) -> Vec<Check> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut files: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|file| file.extension().is_some_and(|ext| ext == "toml"))
.collect();
files.sort();
files.iter().map(|file| agent_check(path, file)).collect()
}
fn agent_check(path: &str, file: &Path) -> Check {
let name = file.file_stem().and_then(|stem| stem.to_str());
let label = format!("agent {}", name.unwrap_or_default());
let loaded = std::fs::read_to_string(file)
.map_err(|err| err.to_string())
.and_then(|text| toml::from_str::<AgentCommand>(&text).map_err(|err| err.to_string()));
match loaded {
Ok(agent) => Check::require(
label,
agent_command_available_in(path, &agent.command),
&format!("`{}` not found on PATH", agent.command),
),
Err(err) => {
let first_line = err.lines().next().unwrap_or_default();
Check::fail(label, first_line)
}
}
}
pub fn exit_code(checks: &[Check]) -> i32 {
if checks.iter().all(Check::passed) {
0
} else {
1
}
}
pub fn run() -> i32 {
let path = std::env::var("PATH").unwrap_or_default();
let checks = run_checks(&path, &agents_dir(), is_running());
for check in &checks {
println!("{}", check.render());
}
exit_code(&checks)
}
#[cfg(test)]
#[path = "doctor_tests.rs"]
mod doctor_tests;