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"),
));
}
let provider_kind =
crate::llm::provider::detect_provider(&config.api.model, &config.api.base_url);
checks.push(Check::pass(
"provider:detected",
&format!("Provider: {provider_kind:?}"),
));
if let Some(expected_env) = match provider_kind {
crate::llm::provider::ProviderKind::AzureOpenAi => Some("AZURE_OPENAI_API_KEY"),
crate::llm::provider::ProviderKind::Bedrock => Some("AWS_REGION"),
crate::llm::provider::ProviderKind::Vertex => Some("GOOGLE_CLOUD_PROJECT"),
_ => None,
} {
if std::env::var(expected_env).is_ok() {
checks.push(Check::pass(
"provider:env",
&format!("{expected_env} is set"),
));
} else {
checks.push(Check::warn(
"provider:env",
&format!("{expected_env} not set (may be needed for {provider_kind:?})"),
));
}
}
if config.api.api_key.is_some() {
let api_key = config.api.api_key.as_deref().unwrap_or("");
let url = format!("{}/models", config.api.base_url);
let client = reqwest::Client::new();
let mut request = client.get(&url).timeout(std::time::Duration::from_secs(5));
match provider_kind {
crate::llm::provider::ProviderKind::AzureOpenAi => {
request = request.header("api-key", api_key);
}
crate::llm::provider::ProviderKind::Anthropic
| crate::llm::provider::ProviderKind::Bedrock
| crate::llm::provider::ProviderKind::Vertex => {
request = request
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01");
}
_ => {
request = request.header("Authorization", format!("Bearer {api_key}"));
}
}
match request.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 ({:?} at {})",
provider_kind, config.api.base_url
),
));
} else if status.as_u16() == 401 || status.as_u16() == 403 {
checks.push(Check::fail(
"api:connectivity",
&format!(
"API key rejected by {:?} (HTTP {})",
provider_kind,
status.as_u16()
),
));
} else {
checks.push(Check::warn(
"api:connectivity",
&format!(
"{:?} responded with HTTP {}",
provider_kind,
status.as_u16()
),
));
}
}
Err(e) => {
let msg = if e.is_timeout() {
format!("{:?} unreachable (timeout after 5s)", provider_kind)
} else if e.is_connect() {
format!(
"Cannot connect to {:?} at {}",
provider_kind, config.api.base_url
)
} else {
format!("{:?} error: {e}", provider_kind)
};
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_includes_provider_check() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::config::Config::default();
config.api.base_url = "https://api.openai.com/v1".to_string();
config.api.model = "gpt-5.4".to_string();
let checks = run_all(dir.path(), &config).await;
let provider_check = checks.iter().find(|c| c.name == "provider:detected");
assert!(provider_check.is_some());
assert_eq!(provider_check.unwrap().status, CheckStatus::Pass);
assert!(provider_check.unwrap().detail.contains("OpenAi"));
}
#[tokio::test]
async fn test_run_all_azure_provider_env_check() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::config::Config::default();
config.api.base_url =
"https://myresource.openai.azure.com/openai/deployments/gpt-4".to_string();
let checks = run_all(dir.path(), &config).await;
let provider_check = checks.iter().find(|c| c.name == "provider:detected");
assert!(provider_check.is_some());
assert!(provider_check.unwrap().detail.contains("AzureOpenAi"));
let env_check = checks.iter().find(|c| c.name == "provider:env");
assert!(env_check.is_some());
}
#[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);
}
}