use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
pub const SKILL_SEARCH_DIRS: &[&str] =
&[".skills", "skills", ".agents/skills", ".claude/skills", "."];
pub fn discover_skills_in_workspace(
workspace: &Path,
search_dirs: Option<&[&str]>,
) -> Vec<PathBuf> {
let dirs = search_dirs.unwrap_or(SKILL_SEARCH_DIRS);
let mut candidates: Vec<PathBuf> = Vec::new();
let mut seen = HashSet::new();
if workspace.join("SKILL.md").exists() {
if let Ok(real) = workspace.canonicalize() {
if seen.insert(real) {
candidates.push(workspace.to_path_buf());
}
}
}
for search_dir in dirs {
let search_path = workspace.join(search_dir);
if !search_path.is_dir() {
continue;
}
let is_root = search_dir == &".";
if !is_root && search_path.join("SKILL.md").exists() {
if let Ok(real) = search_path.canonicalize() {
if seen.insert(real) {
candidates.push(search_path.clone());
}
}
}
let Ok(entries) = fs::read_dir(&search_path) else {
continue;
};
let mut children: Vec<_> = entries.flatten().collect();
children.sort_by_key(|e| e.file_name());
for entry in children {
let p = entry.path();
if p.is_dir() && p.join("SKILL.md").exists() {
if let Ok(real) = p.canonicalize() {
if seen.insert(real) {
candidates.push(p);
}
}
}
}
}
candidates.sort();
candidates
}
pub fn discover_skill_dirs_for_loading(
workspace: &Path,
search_dirs: Option<&[&str]>,
) -> Vec<String> {
let dirs = search_dirs.unwrap_or(SKILL_SEARCH_DIRS);
let mut result = Vec::new();
for d in dirs {
let p = workspace.join(d);
if p.is_dir() {
result.push(p.to_string_lossy().to_string());
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_discover_skills_in_workspace_empty() {
let tmp = tempfile::tempdir().unwrap();
let found = discover_skills_in_workspace(tmp.path(), Some(&[".skills", "skills"]));
assert!(found.is_empty());
}
#[test]
fn test_discover_skills_in_workspace_finds_skills() {
let tmp = tempfile::tempdir().unwrap();
let skills_dir = tmp.path().join(".skills");
fs::create_dir_all(&skills_dir).unwrap();
let skill_a = skills_dir.join("skill-a");
fs::create_dir_all(&skill_a).unwrap();
fs::write(skill_a.join("SKILL.md"), "name: skill-a\n").unwrap();
let skill_b = skills_dir.join("skill-b");
fs::create_dir_all(&skill_b).unwrap();
fs::write(skill_b.join("SKILL.md"), "name: skill-b\n").unwrap();
let found = discover_skills_in_workspace(tmp.path(), Some(&[".skills", "skills"]));
assert_eq!(found.len(), 2);
assert!(found.iter().any(|p| p.ends_with("skill-a")));
assert!(found.iter().any(|p| p.ends_with("skill-b")));
}
#[test]
fn test_discover_skill_dirs_for_loading_fallback() {
let tmp = tempfile::tempdir().unwrap();
let skills_dir = tmp.path().join(".skills");
fs::create_dir_all(&skills_dir).unwrap();
let found = discover_skill_dirs_for_loading(tmp.path(), Some(&[".skills", "skills"]));
assert_eq!(found.len(), 1);
assert!(found[0].ends_with(".skills"));
}
}