use crate::skills::skill_md::{SkillMdError, SkillMdFile};
use regex::Regex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProgressiveError {
#[error("SKILL.md error: {0}")]
SkillMdError(#[from] SkillMdError),
#[error("Referenced file not found: {0}")]
FileNotFound(PathBuf, #[source] std::io::Error),
#[error("Invalid reference format: {0}")]
InvalidReference(String),
}
#[derive(Debug, Clone)]
pub struct ProgressiveSkillLoader {
#[allow(dead_code)]
skill_dir: PathBuf,
main_content: String,
referenced_files: HashMap<String, PathBuf>,
available_scripts: Vec<PathBuf>,
}
impl ProgressiveSkillLoader {
pub fn load<P: AsRef<Path>>(skill_dir: P) -> Result<Self, ProgressiveError> {
let skill_dir = skill_dir.as_ref();
let skill_md = skill_dir.join("SKILL.md");
let skill_file = SkillMdFile::parse(&skill_md)?;
let referenced_files = Self::scan_references(&skill_file.content, skill_dir);
let available_scripts = Self::discover_scripts(skill_dir);
Ok(Self {
skill_dir: skill_dir.to_path_buf(),
main_content: skill_file.content,
referenced_files,
available_scripts,
})
}
pub fn get_main_content(&self) -> &str {
&self.main_content
}
pub fn load_reference(&self, filename: &str) -> Result<Option<String>, ProgressiveError> {
if let Some(path) = self.referenced_files.get(filename) {
std::fs::read_to_string(path)
.map(Some)
.map_err(|e| ProgressiveError::FileNotFound(path.clone(), e))
} else {
Ok(None)
}
}
pub fn load_all_references(&self) -> Result<HashMap<String, String>, ProgressiveError> {
let mut loaded = HashMap::new();
for (filename, path) in &self.referenced_files {
let content = std::fs::read_to_string(path)
.map_err(|e| ProgressiveError::FileNotFound(path.clone(), e))?;
loaded.insert(filename.clone(), content);
}
Ok(loaded)
}
pub fn list_references(&self) -> Vec<String> {
self.referenced_files.keys().cloned().collect()
}
pub fn get_reference_count(&self) -> usize {
self.referenced_files.len()
}
pub fn list_scripts(&self) -> &[PathBuf] {
&self.available_scripts
}
pub fn has_reference(&self, filename: &str) -> bool {
self.referenced_files.contains_key(filename)
}
fn scan_references(content: &str, base_dir: &Path) -> HashMap<String, PathBuf> {
let mut refs = HashMap::new();
let link_pattern = Regex::new(r"\[(?P<title>[^\]]+)\]\((?P<file>[^)]+\.md)\)").unwrap();
for cap in link_pattern.captures_iter(content) {
let file = cap.name("file").unwrap().as_str();
let full_path = base_dir.join(file);
if full_path.exists() && !refs.contains_key(file) {
refs.insert(file.to_string(), full_path);
}
}
let standard_files = ["reference.md", "examples.md", "forms.md"];
for standard in standard_files {
let path = base_dir.join(standard);
if path.exists() && !refs.contains_key(standard) {
refs.insert(standard.to_string(), path);
}
}
refs
}
fn discover_scripts(skill_dir: &Path) -> Vec<PathBuf> {
let scripts_dir = skill_dir.join("scripts");
if !scripts_dir.exists() {
return Vec::new();
}
std::fs::read_dir(&scripts_dir)
.ok()
.map(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file())
.collect()
})
.unwrap_or_default()
}
pub fn get_summary(&self) -> String {
format!(
"ProgressiveSkillLoader:\n\
- Main content: {} bytes\n\
- References: {} files\n\
- Scripts: {} files",
self.main_content.len(),
self.referenced_files.len(),
self.available_scripts.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use tempfile::TempDir;
fn create_test_skill(temp_dir: &Path) {
let skill_md = temp_dir.join("SKILL.md");
let content = r#"---
name: test-skill
description: A test skill for progressive disclosure
---
# Test Skill
Main content here.
See [reference.md](reference.md) for details.
See [examples.md](examples.md) for usage.
"#;
fs::write(&skill_md, content).unwrap();
let reference = temp_dir.join("reference.md");
fs::write(&reference, "Detailed reference documentation").unwrap();
let examples = temp_dir.join("examples.md");
fs::write(&examples, "Usage examples").unwrap();
let scripts_dir = temp_dir.join("scripts");
fs::create_dir_all(&scripts_dir).unwrap();
let script = scripts_dir.join("helper.sh");
File::create(&script).unwrap();
}
#[test]
fn test_progressive_loader_load() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
assert!(loader.get_main_content().contains("Main content here"));
assert_eq!(loader.get_reference_count(), 2);
}
#[test]
fn test_progressive_loader_load_reference() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
let reference = loader.load_reference("reference.md").unwrap();
assert!(reference.is_some());
assert!(reference.unwrap().contains("Detailed reference"));
let examples = loader.load_reference("examples.md").unwrap();
assert!(examples.is_some());
assert!(examples.unwrap().contains("Usage examples"));
let missing = loader.load_reference("nonexistent.md").unwrap();
assert!(missing.is_none());
}
#[test]
fn test_progressive_loader_list_references() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
let refs = loader.list_references();
assert_eq!(refs.len(), 2);
assert!(refs.contains(&"reference.md".to_string()));
assert!(refs.contains(&"examples.md".to_string()));
}
#[test]
fn test_progressive_loader_has_reference() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
assert!(loader.has_reference("reference.md"));
assert!(loader.has_reference("examples.md"));
assert!(!loader.has_reference("nonexistent.md"));
}
#[test]
fn test_progressive_loader_load_all_references() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
let all_refs = loader.load_all_references().unwrap();
assert_eq!(all_refs.len(), 2);
assert!(all_refs.get("reference.md").unwrap().contains("Detailed"));
assert!(all_refs.get("examples.md").unwrap().contains("Usage"));
}
#[test]
fn test_progressive_loader_scripts() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
let scripts = loader.list_scripts();
assert_eq!(scripts.len(), 1);
assert!(scripts[0].ends_with("helper.sh"));
}
#[test]
fn test_progressive_loader_summary() {
let temp_dir = TempDir::new().unwrap();
create_test_skill(temp_dir.path());
let loader = ProgressiveSkillLoader::load(temp_dir.path()).unwrap();
let summary = loader.get_summary();
assert!(summary.contains("ProgressiveSkillLoader"));
assert!(summary.contains("References: 2 files"));
assert!(summary.contains("Scripts: 1 files"));
}
}