use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use super::SkillRegistry;
use super::discovery::find_skills_in_dir;
impl SkillRegistry {
pub fn load_from_directory(&mut self, dir: &Path) -> Result<usize> {
if !dir.exists() {
anyhow::bail!("Directory does not exist: {}", dir.display());
}
let mut count = 0;
for discovered in find_skills_in_dir(dir, "internal", 3) {
match self.load_skill_file(&discovered.skill_file) {
Ok(_) => count += 1,
Err(e) => {
eprintln!(
"Warning: Failed to load skill from {}: {}",
discovered.skill_file.display(),
e
);
}
}
}
Ok(count)
}
pub fn load_with_precedence(&mut self, user_dir: &Path, repo_dir: &Path) -> Result<usize> {
if user_dir.exists() {
let _ = self.load_from_directory(user_dir);
}
if repo_dir.exists() {
let _ = self.load_from_directory(repo_dir)?;
}
Ok(self.skills.len())
}
fn load_skill_file(&mut self, path: &Path) -> Result<()> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read skill file: {}", path.display()))?;
let skill = super::parser::parse_skill_file(path, &content)
.with_context(|| format!("Failed to parse skill file: {}", path.display()))?;
self.register(skill);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_load_from_empty_directory() {
let temp = TempDir::new().unwrap();
let mut registry = SkillRegistry::new();
let count = registry.load_from_directory(temp.path()).unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_load_single_skill() {
let temp = TempDir::new().unwrap();
let skill_dir = temp.path().join("test-skill");
fs::create_dir(&skill_dir).unwrap();
let skill_content = r#"---
name: test-skill
triggers:
- "test"
---
# Test Skill
Content here.
"#;
fs::write(skill_dir.join("SKILL.md"), skill_content).unwrap();
let mut registry = SkillRegistry::new();
let count = registry.load_from_directory(temp.path()).unwrap();
assert_eq!(count, 1);
let skill = registry.get("test-skill").unwrap();
assert_eq!(skill.name, "test-skill");
assert_eq!(skill.triggers, vec!["test"]);
}
#[test]
fn test_load_multiple_skills_nested() {
let temp = TempDir::new().unwrap();
let rust_testing = temp.path().join("rust/testing");
fs::create_dir_all(&rust_testing).unwrap();
fs::write(
rust_testing.join("SKILL.md"),
r#"---
name: rust-testing
triggers:
- "cargo test"
---
Testing guide.
"#,
)
.unwrap();
let rust_errors = temp.path().join("rust/error-handling");
fs::create_dir_all(&rust_errors).unwrap();
fs::write(
rust_errors.join("SKILL.md"),
r#"---
name: rust-error-handling
triggers:
- "Result type"
---
Error handling guide.
"#,
)
.unwrap();
let mut registry = SkillRegistry::new();
let count = registry.load_from_directory(temp.path()).unwrap();
assert_eq!(count, 2);
assert!(registry.get("rust-testing").is_some());
assert!(registry.get("rust-error-handling").is_some());
}
#[test]
fn test_skip_invalid_skill_files() {
let temp = TempDir::new().unwrap();
let valid_dir = temp.path().join("valid");
fs::create_dir(&valid_dir).unwrap();
fs::write(
valid_dir.join("SKILL.md"),
r#"---
name: valid
triggers:
- "test"
---
Content.
"#,
)
.unwrap();
let invalid_dir = temp.path().join("invalid");
fs::create_dir(&invalid_dir).unwrap();
fs::write(invalid_dir.join("SKILL.md"), "Just content, no frontmatter").unwrap();
let mut registry = SkillRegistry::new();
let count = registry.load_from_directory(temp.path()).unwrap();
assert_eq!(count, 1);
assert!(registry.get("valid").is_some());
}
#[test]
fn test_nonexistent_directory() {
let mut registry = SkillRegistry::new();
let result = registry.load_from_directory(Path::new("/nonexistent/path"));
assert!(result.is_err());
}
#[test]
fn test_load_with_precedence_repo_overrides_user() {
let user_temp = TempDir::new().unwrap();
let repo_temp = TempDir::new().unwrap();
let user_skill = user_temp.path().join("shared-skill");
fs::create_dir(&user_skill).unwrap();
fs::write(
user_skill.join("SKILL.md"),
r#"---
name: shared-skill
triggers:
- "user trigger"
---
User version content.
"#,
)
.unwrap();
let repo_skill = repo_temp.path().join("shared-skill");
fs::create_dir(&repo_skill).unwrap();
fs::write(
repo_skill.join("SKILL.md"),
r#"---
name: shared-skill
triggers:
- "repo trigger"
---
Repo version content.
"#,
)
.unwrap();
let mut registry = SkillRegistry::new();
let count = registry
.load_with_precedence(user_temp.path(), repo_temp.path())
.unwrap();
assert_eq!(count, 1);
let skill = registry.get("shared-skill").unwrap();
assert_eq!(skill.triggers, vec!["repo trigger"]);
assert!(skill.content.contains("Repo version"));
}
#[test]
fn test_load_with_precedence_combines_unique_skills() {
let user_temp = TempDir::new().unwrap();
let repo_temp = TempDir::new().unwrap();
let user_skill = user_temp.path().join("user-skill");
fs::create_dir(&user_skill).unwrap();
fs::write(
user_skill.join("SKILL.md"),
r#"---
name: user-skill
triggers:
- "user"
---
User skill.
"#,
)
.unwrap();
let repo_skill = repo_temp.path().join("repo-skill");
fs::create_dir(&repo_skill).unwrap();
fs::write(
repo_skill.join("SKILL.md"),
r#"---
name: repo-skill
triggers:
- "repo"
---
Repo skill.
"#,
)
.unwrap();
let mut registry = SkillRegistry::new();
let count = registry
.load_with_precedence(user_temp.path(), repo_temp.path())
.unwrap();
assert_eq!(count, 2);
assert!(registry.get("user-skill").is_some());
assert!(registry.get("repo-skill").is_some());
}
#[test]
fn test_load_with_precedence_missing_user_dir() {
let repo_temp = TempDir::new().unwrap();
let repo_skill = repo_temp.path().join("skill");
fs::create_dir(&repo_skill).unwrap();
fs::write(
repo_skill.join("SKILL.md"),
r#"---
name: skill
triggers:
- "test"
---
Content.
"#,
)
.unwrap();
let mut registry = SkillRegistry::new();
let count = registry
.load_with_precedence(Path::new("/nonexistent/user"), repo_temp.path())
.unwrap();
assert_eq!(count, 1);
assert!(registry.get("skill").is_some());
}
}