use std::path::Path;
use klasp_agents_aider::AiderSurface;
use klasp_agents_claude::ClaudeCodeSurface;
use klasp_agents_codex::CodexSurface;
pub const ALL_AGENTS: &[&str] = &[
ClaudeCodeSurface::AGENT_ID,
CodexSurface::AGENT_ID,
AiderSurface::AGENT_ID,
];
pub fn detect_installed_agents(home_dir: Option<&Path>) -> (Vec<String>, bool) {
let Some(home) = home_dir else {
return (all_agents_fallback(), true);
};
let mut found = Vec::new();
if probe_claude_code(home) {
found.push(ClaudeCodeSurface::AGENT_ID.to_string());
}
if probe_codex(home) {
found.push(CodexSurface::AGENT_ID.to_string());
}
if probe_aider(home) {
found.push(AiderSurface::AGENT_ID.to_string());
}
if found.is_empty() {
(all_agents_fallback(), true)
} else {
(found, false)
}
}
fn probe_claude_code(home: &Path) -> bool {
home.join(".claude").is_dir()
}
fn probe_codex(home: &Path) -> bool {
home.join(".codex").is_dir()
}
fn probe_aider(home: &Path) -> bool {
[".aider", ".aider.conf.yml", ".aiderignore"]
.iter()
.any(|name| home.join(name).exists())
}
fn all_agents_fallback() -> Vec<String> {
ALL_AGENTS.iter().map(|s| s.to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_home_dir_returns_all_three_with_fallback_flag() {
let (agents, fell_back) = detect_installed_agents(None);
assert_eq!(agents, vec!["claude_code", "codex", "aider"]);
assert!(fell_back);
}
#[test]
fn empty_home_returns_all_three_fallback() {
let tmp = tempfile::tempdir().unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["claude_code", "codex", "aider"]);
assert!(fell_back);
}
#[test]
fn claude_only_home_returns_claude_code() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join(".claude")).unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["claude_code"]);
assert!(!fell_back);
}
#[test]
fn codex_only_home_returns_codex() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join(".codex")).unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["codex"]);
assert!(!fell_back);
}
#[test]
fn aider_conf_yml_detected() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".aider.conf.yml"), "commit: true\n").unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["aider"]);
assert!(!fell_back);
}
#[test]
fn all_three_detected_is_not_fallback() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join(".claude")).unwrap();
std::fs::create_dir(tmp.path().join(".codex")).unwrap();
std::fs::write(tmp.path().join(".aider.conf.yml"), "commit: true\n").unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["claude_code", "codex", "aider"]);
assert!(
!fell_back,
"user with all 3 agents present should NOT trigger fallback"
);
}
#[test]
fn claude_and_codex_detected() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join(".claude")).unwrap();
std::fs::create_dir(tmp.path().join(".codex")).unwrap();
let (agents, fell_back) = detect_installed_agents(Some(tmp.path()));
assert_eq!(agents, vec!["claude_code", "codex"]);
assert!(!fell_back);
}
}