use std::path::Path;
#[derive(Debug)]
pub struct Check {
pub name: String,
pub status: CheckStatus,
pub detail: String,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CheckStatus {
Pass,
Warn,
Fail,
}
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
status: CheckStatus::Pass,
detail: detail.to_string(),
}
}
fn warn(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
status: CheckStatus::Warn,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
status: CheckStatus::Fail,
detail: detail.to_string(),
}
}
pub fn symbol(&self) -> &str {
match self.status {
CheckStatus::Pass => "ok",
CheckStatus::Warn => "!?",
CheckStatus::Fail => "xx",
}
}
}
pub async fn run_all(cwd: &Path, config: &crate::config::Config) -> Vec<Check> {
let mut checks = Vec::new();
for (tool, purpose) in &[
("git", "version control"),
("rg", "content search (ripgrep)"),
("bash", "shell execution"),
] {
let available = tokio::process::Command::new("which")
.arg(tool)
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false);
if available {
checks.push(Check::pass(
&format!("tool:{tool}"),
&format!("{tool} found ({purpose})"),
));
} else {
checks.push(Check::fail(
&format!("tool:{tool}"),
&format!("{tool} not found — needed for {purpose}"),
));
}
}
for (tool, purpose) in &[
("node", "JavaScript execution"),
("python3", "Python execution"),
("cargo", "Rust toolchain"),
] {
let available = tokio::process::Command::new("which")
.arg(tool)
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false);
if available {
checks.push(Check::pass(
&format!("tool:{tool}"),
&format!("{tool} available ({purpose})"),
));
} else {
checks.push(Check::warn(
&format!("tool:{tool}"),
&format!("{tool} not found — optional, for {purpose}"),
));
}
}
if config.api.api_key.is_some() {
checks.push(Check::pass("config:api_key", "API key configured"));
} else {
checks.push(Check::fail(
"config:api_key",
"No API key set (AGENT_CODE_API_KEY or --api-key)",
));
}
checks.push(Check::pass(
"config:model",
&format!("Model: {}", config.api.model),
));
checks.push(Check::pass(
"config:base_url",
&format!("API endpoint: {}", config.api.base_url),
));
if crate::services::git::is_git_repo(cwd).await {
let branch = crate::services::git::current_branch(cwd)
.await
.unwrap_or_else(|| "(detached HEAD)".to_string());
checks.push(Check::pass(
"git:repo",
&format!("Git repository on branch '{branch}'"),
));
} else {
checks.push(Check::warn("git:repo", "Not inside a git repository"));
}
let user_config = dirs::config_dir().map(|d| d.join("agent-code").join("config.toml"));
if let Some(ref path) = user_config {
if path.exists() {
checks.push(Check::pass(
"config:user_file",
&format!("User config: {}", path.display()),
));
} else {
checks.push(Check::warn(
"config:user_file",
&format!("No user config at {}", path.display()),
));
}
}
let project_config = cwd.join(".agent").join("settings.toml");
if project_config.exists() {
checks.push(Check::pass(
"config:project_file",
&format!("Project config: {}", project_config.display()),
));
}
let mcp_count = config.mcp_servers.len();
if mcp_count > 0 {
checks.push(Check::pass(
"mcp:servers",
&format!("{mcp_count} MCP server(s) configured"),
));
}
if config.api.api_key.is_some() {
let url = format!("{}/models", config.api.base_url);
match reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(5))
.header(
"Authorization",
format!("Bearer {}", config.api.api_key.as_deref().unwrap_or("")),
)
.header("x-api-key", config.api.api_key.as_deref().unwrap_or(""))
.send()
.await
{
Ok(resp) => {
let status = resp.status();
if status.is_success() || status.as_u16() == 200 {
checks.push(Check::pass(
"api:connectivity",
&format!("API reachable ({})", config.api.base_url),
));
} else if status.as_u16() == 401 || status.as_u16() == 403 {
checks.push(Check::fail(
"api:connectivity",
&format!("API key rejected (HTTP {})", status.as_u16()),
));
} else {
checks.push(Check::warn(
"api:connectivity",
&format!("API responded with HTTP {}", status.as_u16()),
));
}
}
Err(e) => {
let msg = if e.is_timeout() {
"API unreachable (timeout after 5s)".to_string()
} else if e.is_connect() {
format!("Cannot connect to {}", config.api.base_url)
} else {
format!("API error: {e}")
};
checks.push(Check::fail("api:connectivity", &msg));
}
}
}
for (name, entry) in &config.mcp_servers {
if let Some(ref cmd) = entry.command {
let binary = cmd.split_whitespace().next().unwrap_or(cmd);
if let Ok(output) = tokio::process::Command::new("which")
.arg(binary)
.output()
.await
{
if output.status.success() {
checks.push(Check::pass(
&format!("mcp:{name}"),
&format!("MCP server '{name}' binary found: {binary}"),
));
} else {
checks.push(Check::fail(
&format!("mcp:{name}"),
&format!("MCP server '{name}' binary not found: {binary}"),
));
}
}
} else if let Some(ref url) = entry.url {
match reqwest::Client::new()
.get(url)
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(_) => {
checks.push(Check::pass(
&format!("mcp:{name}"),
&format!("MCP server '{name}' reachable at {url}"),
));
}
Err(_) => {
checks.push(Check::fail(
&format!("mcp:{name}"),
&format!("MCP server '{name}' unreachable at {url}"),
));
}
}
}
}
if let Ok(output) = tokio::process::Command::new("df")
.args(["-BG", "."])
.current_dir(cwd)
.output()
.await
{
let text = String::from_utf8_lossy(&output.stdout);
if let Some(line) = text.lines().nth(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(avail) = parts.get(3) {
let gb: f64 = avail.trim_end_matches('G').parse().unwrap_or(999.0);
if gb < 1.0 {
checks.push(Check::warn(
"disk:space",
&format!("Low disk space: {avail} available"),
));
}
}
}
}
checks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_constructors() {
let p = Check::pass("test", "ok");
assert_eq!(p.status, CheckStatus::Pass);
assert_eq!(p.symbol(), "ok");
let w = Check::warn("test", "warning");
assert_eq!(w.status, CheckStatus::Warn);
assert_eq!(w.symbol(), "!?");
let f = Check::fail("test", "failed");
assert_eq!(f.status, CheckStatus::Fail);
assert_eq!(f.symbol(), "xx");
}
#[test]
fn test_check_fields() {
let c = Check::pass("git:repo", "Git repository on branch 'main'");
assert_eq!(c.name, "git:repo");
assert!(c.detail.contains("main"));
}
#[tokio::test]
async fn test_run_all_returns_checks() {
let dir = tempfile::tempdir().unwrap();
let config = crate::config::Config::default();
let checks = run_all(dir.path(), &config).await;
assert!(checks.len() >= 3);
assert!(checks.iter().any(|c| c.name.starts_with("tool:")));
}
#[tokio::test]
async fn test_run_all_in_git_repo() {
let dir = tempfile::tempdir().unwrap();
tokio::process::Command::new("git")
.args(["init", "-q"])
.current_dir(dir.path())
.output()
.await
.unwrap();
let config = crate::config::Config::default();
let checks = run_all(dir.path(), &config).await;
let git_check = checks.iter().find(|c| c.name == "git:repo");
assert!(git_check.is_some());
assert_eq!(git_check.unwrap().status, CheckStatus::Pass);
}
#[tokio::test]
async fn test_run_all_no_api_key() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::config::Config::default();
config.api.api_key = None;
let checks = run_all(dir.path(), &config).await;
let api_check = checks.iter().find(|c| c.name == "config:api_key");
assert!(api_check.is_some());
assert_eq!(api_check.unwrap().status, CheckStatus::Fail);
}
#[tokio::test]
async fn test_run_all_with_api_key() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::config::Config::default();
config.api.api_key = Some("test-key".to_string());
let checks = run_all(dir.path(), &config).await;
let api_check = checks.iter().find(|c| c.name == "config:api_key");
assert!(api_check.is_some());
assert_eq!(api_check.unwrap().status, CheckStatus::Pass);
}
#[tokio::test]
async fn test_run_all_mcp_servers() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::config::Config::default();
config.mcp_servers.insert(
"test-server".to_string(),
crate::config::McpServerEntry {
command: Some("nonexistent-binary-xyz".to_string()),
args: vec![],
url: None,
env: std::collections::HashMap::new(),
},
);
let checks = run_all(dir.path(), &config).await;
let mcp_check = checks.iter().find(|c| c.name == "mcp:test-server");
assert!(mcp_check.is_some());
assert_eq!(mcp_check.unwrap().status, CheckStatus::Fail);
}
}