use super::{Skill, SkillError, SkillMetadata};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use tracing::{debug, warn};
#[derive(Debug, Clone)]
pub struct SkillRegistry {
skills: BTreeMap<String, Arc<Skill>>,
}
impl SkillRegistry {
pub fn new() -> Self {
Self {
skills: BTreeMap::new(),
}
}
pub async fn discover(search_paths: Vec<PathBuf>) -> Result<Self, SkillError> {
let mut registry = Self::new();
for search_path in search_paths {
if !search_path.exists() {
debug!("Skill search path does not exist: {:?}", search_path);
continue;
}
match discover_in_directory(search_path.clone()).await {
Ok(skills) => {
debug!("Discovered {} skills in {:?}", skills.len(), search_path);
for skill in skills {
registry.register(skill);
}
}
Err(e) => {
warn!("Failed to discover skills in {:?}: {}", search_path, e);
}
}
}
Ok(registry)
}
pub fn register(&mut self, skill: Skill) {
let name = skill.name().to_string();
self.skills.insert(name, Arc::new(skill));
}
pub fn all_metadata(&self) -> Vec<SkillMetadata> {
self.skills
.values()
.map(|skill| skill.metadata().clone())
.collect()
}
pub fn get(&self, name: &str) -> Option<Arc<Skill>> {
self.skills.get(name).cloned()
}
pub fn find_matching(&self, request: &str) -> Vec<Arc<Skill>> {
self.skills
.values()
.filter(|skill| skill.matches(request))
.cloned()
.collect()
}
pub fn len(&self) -> usize {
self.skills.len()
}
pub fn is_empty(&self) -> bool {
self.skills.is_empty()
}
pub fn skill_names(&self) -> Vec<String> {
self.skills.keys().cloned().collect()
}
}
impl Default for SkillRegistry {
fn default() -> Self {
Self::new()
}
}
fn discover_in_directory(
path: PathBuf,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Skill>, SkillError>> + Send>> {
Box::pin(async move {
let mut skills = Vec::new();
if !path.is_dir() {
return Ok(skills);
}
let mut entries = fs::read_dir(&path)
.await
.map_err(|e| SkillError::read_error(path.clone(), e))?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| SkillError::read_error(path.clone(), e))?
{
let entry_path = entry.path();
if entry_path.is_dir() {
let skill_file = entry_path.join("SKILL.md");
if skill_file.exists() {
match Skill::from_path(entry_path.clone()).await {
Ok(skill) => {
debug!("Discovered skill '{}' at {:?}", skill.name(), entry_path);
skills.push(skill);
}
Err(e) => {
warn!("Failed to load skill from {:?}: {}", entry_path, e);
}
}
} else {
match discover_in_directory(entry_path.clone()).await {
Ok(subdir_skills) => {
skills.extend(subdir_skills);
}
Err(e) => {
warn!("Failed to search {:?}: {}", entry_path, e);
}
}
}
}
}
Ok(skills)
})
}
pub fn default_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(current_dir) = std::env::current_dir() {
paths.push(current_dir.join(".sac").join("skills"));
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(home_dir.join(".sac").join("skills"));
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(home_dir.join(".sombra").join("skills"));
}
paths
}
mod dirs {
use std::path::PathBuf;
pub fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.and_then(|h| if h.is_empty() { None } else { Some(h) })
.map(PathBuf::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_new() {
let registry = SkillRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_default_search_paths() {
let paths = default_search_paths();
assert!(!paths.is_empty());
assert!(paths.iter().any(|p| p.ends_with(".sac/skills")));
}
}