use std::path::{Path, PathBuf};
use super::is_running;
use crate::global_lock::LockStatus;
use crate::paths::agents_dir;
use crate::routines::{agent_command_available_in, AgentCommand};
use crate::utils::time::format_local;
#[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, lock: &LockStatus) -> 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(lock_check(lock));
checks.push(Check::require(
"daemon",
daemon_up,
"not running; start it with: moadim",
));
checks
}
fn lock_check(lock: &LockStatus) -> Check {
Check {
label: "global lock".to_string(),
failure: lock.locked.then(|| lock_reason(lock)),
}
}
fn lock_reason(lock: &LockStatus) -> String {
let mut active = Vec::new();
if lock.shared {
active.push(".lock");
}
if lock.local {
active.push(".local.lock");
}
let since = [lock.shared_since, lock.local_since]
.into_iter()
.flatten()
.min()
.map_or_else(String::new, |ts| format!(" since {}", format_local(ts)));
format!(
"{}{since}; routines are halted, run: moadim unlock",
active.join(" and ")
)
}
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(),
&crate::global_lock::lock_status(),
);
for check in &checks {
println!("{}", check.render());
}
exit_code(&checks)
}
#[cfg(test)]
#[path = "doctor_tests.rs"]
mod doctor_tests;