mod builtin;
use std::collections::BTreeMap;
use crate::types::{AgentConfig, AgentId, UNIVERSAL_SKILLS_DIR};
#[derive(Debug)]
pub struct AgentRegistry {
agents: BTreeMap<AgentId, AgentConfig>,
}
impl Default for AgentRegistry {
fn default() -> Self {
Self::with_defaults()
}
}
impl AgentRegistry {
#[must_use]
pub fn with_defaults() -> Self {
let mut registry = Self::empty();
for config in builtin::builtin_agents() {
registry.register(config);
}
registry
}
#[must_use]
pub const fn empty() -> Self {
Self {
agents: BTreeMap::new(),
}
}
pub fn register(&mut self, config: AgentConfig) {
self.agents.insert(config.name.clone(), config);
}
#[must_use]
pub fn get(&self, id: &AgentId) -> Option<&AgentConfig> {
self.agents.get(id)
}
#[must_use]
pub fn all_ids(&self) -> Vec<AgentId> {
self.agents.keys().cloned().collect()
}
#[must_use]
pub fn all_configs(&self) -> Vec<&AgentConfig> {
self.agents.values().collect()
}
pub async fn detect_installed(&self) -> Vec<AgentId> {
let mut set: tokio::task::JoinSet<Option<AgentId>> = tokio::task::JoinSet::new();
for (id, config) in &self.agents {
let id = id.clone();
let paths = config.detect_paths.clone();
set.spawn(async move {
crate::installer::any_path_exists(&paths)
.await
.then_some(id)
});
}
let mut installed = Vec::with_capacity(set.len());
while let Some(result) = set.join_next().await {
if let Ok(Some(id)) = result {
installed.push(id);
}
}
installed.sort();
installed
}
#[must_use]
pub fn universal_agents(&self) -> Vec<AgentId> {
self.agents
.iter()
.filter(|(_, c)| c.skills_dir == UNIVERSAL_SKILLS_DIR && c.show_in_universal_list)
.map(|(id, _)| id.clone())
.collect()
}
#[must_use]
pub fn non_universal_agents(&self) -> Vec<AgentId> {
self.agents
.iter()
.filter(|(_, c)| c.skills_dir != UNIVERSAL_SKILLS_DIR)
.map(|(id, _)| id.clone())
.collect()
}
#[must_use]
pub fn is_universal(&self, id: &AgentId) -> bool {
self.agents
.get(id)
.is_some_and(|c| c.skills_dir == UNIVERSAL_SKILLS_DIR)
}
#[must_use]
pub fn len(&self) -> usize {
self.agents.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.agents.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_registry_has_no_agents() {
let r = AgentRegistry::empty();
assert!(r.is_empty());
assert_eq!(r.len(), 0);
}
#[test]
fn test_with_defaults_registers_all_builtin_agents() {
let r = AgentRegistry::with_defaults();
assert!(
r.len() >= 45,
"expected >= 45 built-in agents, got {}",
r.len()
);
}
#[test]
fn test_with_defaults_includes_recently_added_agents() {
let r = AgentRegistry::with_defaults();
for id in ["bob", "deepagents", "firebender", "warp"] {
assert!(
r.get(&AgentId::new(id)).is_some(),
"missing builtin agent: {id}"
);
}
}
#[test]
fn test_register_overwrites_existing_agent() {
let mut r = AgentRegistry::with_defaults();
let original = r.get(&AgentId::new("cursor")).cloned().unwrap();
let mut modified = original;
modified.display_name = "CustomCursor".to_owned();
r.register(modified);
assert_eq!(
r.get(&AgentId::new("cursor")).unwrap().display_name,
"CustomCursor"
);
}
#[test]
fn test_universal_agents_excludes_agent_specific_dirs() {
let r = AgentRegistry::with_defaults();
let universals = r.universal_agents();
assert!(universals.contains(&AgentId::new("cursor")));
assert!(!universals.contains(&AgentId::new("claude-code")));
}
#[test]
fn test_universal_agents_excludes_hidden_list_entries() {
let r = AgentRegistry::with_defaults();
let universals = r.universal_agents();
assert!(!universals.contains(&AgentId::new("replit")));
assert!(!universals.contains(&AgentId::new("universal")));
}
#[test]
fn test_is_universal_matches_skills_dir() {
let r = AgentRegistry::with_defaults();
assert!(r.is_universal(&AgentId::new("cursor")));
assert!(!r.is_universal(&AgentId::new("claude-code")));
}
#[test]
fn test_all_ids_sorted() {
let r = AgentRegistry::with_defaults();
let ids = r.all_ids();
for pair in ids.windows(2) {
let [a, b] = pair else { unreachable!() };
assert!(a <= b, "{pair:?} not sorted");
}
}
#[test]
fn test_antigravity_uses_plural_agents_skills_dir() {
let r = AgentRegistry::with_defaults();
let config = r.get(&AgentId::new("antigravity")).unwrap();
assert_eq!(config.skills_dir, ".agents/skills");
}
}