use std::path::PathBuf;
use directories::BaseDirs;
use serde::Serialize;
use crate::setup;
#[derive(Debug, Clone, Serialize)]
pub struct HarnessInfo {
pub name: String,
pub key: String,
pub detected: bool,
pub config_dir: Option<String>,
pub on_path: bool,
pub galdr_hook: Option<bool>,
pub skills_dir: Option<String>,
pub notes: String,
}
struct Known {
name: &'static str,
key: &'static str,
config: &'static str,
bin: &'static str,
skills_subdir: Option<&'static str>,
}
const KNOWN: &[Known] = &[
Known {
name: "Claude Code",
key: "claude",
config: ".claude",
bin: "claude",
skills_subdir: Some(".claude/skills"),
},
Known {
name: "Codex",
key: "codex",
config: ".codex",
bin: "codex",
skills_subdir: Some(".codex/skills"),
},
Known {
name: "Cursor",
key: "cursor",
config: ".cursor",
bin: "cursor",
skills_subdir: Some(".cursor/skills-cursor"),
},
Known {
name: "Gemini CLI",
key: "gemini",
config: ".gemini",
bin: "gemini",
skills_subdir: None,
},
Known {
name: "Aider",
key: "aider",
config: ".aider.conf.yml",
bin: "aider",
skills_subdir: None,
},
Known {
name: "Windsurf",
key: "windsurf",
config: ".windsurf",
bin: "windsurf",
skills_subdir: None,
},
];
pub fn detect() -> Vec<HarnessInfo> {
let home = BaseDirs::new().map(|b| b.home_dir().to_path_buf());
KNOWN.iter().map(|k| info_for(k, home.as_ref())).collect()
}
pub fn skills_dir(key: &str) -> Option<PathBuf> {
let home = BaseDirs::new()?.home_dir().to_path_buf();
let known = KNOWN.iter().find(|k| k.key == key)?;
known.skills_subdir.map(|sub| home.join(sub))
}
fn info_for(k: &Known, home: Option<&PathBuf>) -> HarnessInfo {
let config_dir = home
.map(|h| h.join(k.config))
.filter(|p| p.exists())
.map(|p| p.display().to_string());
let on_path = binary_on_path(k.bin);
let galdr_hook = match k.key {
"claude" => setup::claude_hook_configured(),
"codex" => setup::codex_hook_configured(),
_ => None,
};
let skills_dir = home
.zip(k.skills_subdir)
.map(|(h, sub)| h.join(sub))
.filter(|p| p.exists())
.map(|p| p.display().to_string());
let detected = config_dir.is_some() || on_path;
let notes = match galdr_hook {
Some(true) => "galdr sensor wired".to_string(),
Some(false) if detected => "galdr sensor not wired".to_string(),
_ => String::new(),
};
HarnessInfo {
name: k.name.to_string(),
key: k.key.to_string(),
detected,
config_dir,
on_path,
galdr_hook,
skills_dir,
notes,
}
}
fn binary_on_path(bin: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| dir.join(bin).is_file())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_returns_every_known_harness() {
let found = detect();
assert_eq!(found.len(), KNOWN.len());
assert!(found.iter().any(|h| h.key == "claude"));
let _ = found.iter().find(|h| h.key == "claude").unwrap().galdr_hook;
let _ = found.iter().find(|h| h.key == "codex").unwrap().galdr_hook;
let cursor = found.iter().find(|h| h.key == "cursor").unwrap();
assert!(cursor.galdr_hook.is_none());
}
#[test]
fn binary_on_path_finds_a_ubiquitous_binary() {
assert!(binary_on_path("sh"));
assert!(!binary_on_path("definitely-not-a-real-binary-xyzzy"));
}
}