agent-manager v0.2.0 : gestionnaire d'agents IA locaux
- CLI Rust (am) : list/install/uninstall/update/start/stop/restart/run/status/search/info/config/doctor/completion/export/import/self-update + shell interactif (Tab, historique) - 9 methodes d'installation (npm, pip, uv, cargo, go, bun, curl, binary, git) - catalogue de 72 agents, dependances, processus, securite (dry-run, checksums) - sondes de version paralleles + cache (am list ~25 ms a chaud) - installateurs one-liner : install.sh, install.ps1
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
//! The merged agent catalog: lookup, aliases, groups and search.
|
||||
|
||||
use crate::config::AgentDef;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Catalog {
|
||||
agents: Vec<AgentDef>,
|
||||
by_name: HashMap<String, usize>,
|
||||
aliases: BTreeMap<String, String>,
|
||||
groups: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
pub fn new(
|
||||
agents: Vec<AgentDef>,
|
||||
aliases: BTreeMap<String, String>,
|
||||
groups: BTreeMap<String, Vec<String>>,
|
||||
) -> Self {
|
||||
let mut by_name = HashMap::new();
|
||||
for (i, a) in agents.iter().enumerate() {
|
||||
by_name.insert(a.name.clone(), i);
|
||||
}
|
||||
Self {
|
||||
agents,
|
||||
by_name,
|
||||
aliases,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agents(&self) -> &[AgentDef] {
|
||||
&self.agents
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.agents.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.agents.is_empty()
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&AgentDef> {
|
||||
self.by_name.get(name).map(|&i| &self.agents[i])
|
||||
}
|
||||
|
||||
/// Resolve an agent name or an alias to the agent definition.
|
||||
pub fn resolve(&self, name_or_alias: &str) -> Option<&AgentDef> {
|
||||
if let Some(target) = self.aliases.get(name_or_alias) {
|
||||
return self.get(target);
|
||||
}
|
||||
self.get(name_or_alias)
|
||||
}
|
||||
|
||||
pub fn alias_target(&self, alias: &str) -> Option<&str> {
|
||||
self.aliases.get(alias).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Members of a group, in definition order (missing names are skipped).
|
||||
pub fn group_members(&self, group: &str) -> Vec<&AgentDef> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(names) = self.groups.get(group) {
|
||||
for n in names {
|
||||
if let Some(a) = self.get(n) {
|
||||
out.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn has_group(&self, group: &str) -> bool {
|
||||
self.groups.contains_key(group)
|
||||
}
|
||||
|
||||
/// Is this token a group selector ("group:<name>")?
|
||||
pub fn parse_group_selector(s: &str) -> Option<&str> {
|
||||
s.strip_prefix("group:")
|
||||
}
|
||||
|
||||
/// Case-insensitive search over name, display name, description,
|
||||
/// category, tags and note.
|
||||
pub fn search(&self, keyword: &str, category: Option<&str>) -> Vec<&AgentDef> {
|
||||
let kw = keyword.to_lowercase();
|
||||
self.agents
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
if a.hidden {
|
||||
return false;
|
||||
}
|
||||
if let Some(cat) = category {
|
||||
if a.category.as_deref() != Some(cat) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let hay = format!(
|
||||
"{} {} {} {} {} {}",
|
||||
a.name,
|
||||
a.title(),
|
||||
a.description.as_deref().unwrap_or(""),
|
||||
a.category.as_deref().unwrap_or(""),
|
||||
a.tags.join(" "),
|
||||
a.note.as_deref().unwrap_or("")
|
||||
)
|
||||
.to_lowercase();
|
||||
hay.contains(&kw)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sorted list of all categories present in the catalog.
|
||||
pub fn categories(&self) -> Vec<String> {
|
||||
let mut cats: Vec<String> = self
|
||||
.agents
|
||||
.iter()
|
||||
.filter_map(|a| a.category.clone())
|
||||
.collect();
|
||||
cats.sort();
|
||||
cats.dedup();
|
||||
cats
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
|
||||
fn catalog() -> Catalog {
|
||||
let cfg: Config = serde_yaml::from_str(crate::config::DEFAULT_CONFIG).unwrap();
|
||||
Catalog::new(cfg.agents, cfg.aliases, cfg.groups)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_aliases() {
|
||||
let cat = catalog();
|
||||
let via_alias = cat.resolve("cc").map(|a| a.name.clone());
|
||||
assert_eq!(via_alias.as_deref(), Some("claude-code"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_groups() {
|
||||
let cat = catalog();
|
||||
assert!(cat.has_group("dev"));
|
||||
let members = cat.group_members("dev");
|
||||
assert!(!members.is_empty());
|
||||
assert!(members.iter().any(|a| a.name == "claude-code"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn searches() {
|
||||
let cat = catalog();
|
||||
let hits = cat.search("anthropic", None);
|
||||
assert!(hits.iter().any(|a| a.name == "claude-code"));
|
||||
let cats = cat.categories();
|
||||
assert!(cats.contains(&"coding-agent".to_string()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user