claude_agent/common/
directory.rs1use std::future::Future;
2use std::path::{Path, PathBuf};
3
4pub async fn load_files<T, F, Fut>(
5 dir: &Path,
6 filter: impl Fn(&Path) -> bool,
7 loader: F,
8) -> crate::Result<Vec<T>>
9where
10 F: Fn(PathBuf) -> Fut,
11 Fut: Future<Output = crate::Result<T>>,
12{
13 let mut items = Vec::new();
14
15 if !dir.exists() {
16 return Ok(items);
17 }
18
19 let mut entries = tokio::fs::read_dir(dir).await.map_err(|e| {
20 crate::Error::Config(format!("Failed to read directory {}: {}", dir.display(), e))
21 })?;
22
23 while let Some(entry) = entries
24 .next_entry()
25 .await
26 .map_err(|e| crate::Error::Config(format!("Failed to read directory entry: {}", e)))?
27 {
28 let path = entry.path();
29 if filter(&path) {
30 match loader(path.clone()).await {
31 Ok(item) => items.push(item),
32 Err(e) => tracing::warn!("Failed to load {}: {}", path.display(), e),
33 }
34 }
35 }
36
37 Ok(items)
38}
39
40pub fn is_markdown(path: &Path) -> bool {
41 path.extension().is_some_and(|e| e == "md")
42}
43
44pub fn is_skill_file(path: &Path) -> bool {
45 path.file_name()
46 .and_then(|n| n.to_str())
47 .is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md") || name.ends_with(".skill.md"))
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use std::path::PathBuf;
54
55 #[test]
56 fn test_is_markdown() {
57 assert!(is_markdown(Path::new("file.md")));
58 assert!(is_markdown(Path::new("/path/to/file.md")));
59 assert!(!is_markdown(Path::new("file.txt")));
60 assert!(!is_markdown(Path::new("file")));
61 }
62
63 #[test]
64 fn test_is_skill_file() {
65 assert!(is_skill_file(Path::new("SKILL.md")));
66 assert!(is_skill_file(Path::new("skill.md"))); assert!(is_skill_file(Path::new("commit.skill.md")));
68 assert!(!is_skill_file(Path::new("README.md")));
69 assert!(!is_skill_file(Path::new("file.md")));
70 }
71
72 #[tokio::test]
73 async fn test_load_files_empty_dir() {
74 let result = load_files(
75 Path::new("/nonexistent/path"),
76 |_| true,
77 |_| async { Ok::<_, crate::Error>(()) },
78 )
79 .await
80 .unwrap();
81 assert!(result.is_empty());
82 }
83
84 #[tokio::test]
85 async fn test_load_files_from_temp() {
86 let temp = tempfile::tempdir().unwrap();
87 let file1 = temp.path().join("test.md");
88 let file2 = temp.path().join("test.txt");
89
90 tokio::fs::write(&file1, "content1").await.unwrap();
91 tokio::fs::write(&file2, "content2").await.unwrap();
92
93 let result: Vec<PathBuf> = load_files(temp.path(), is_markdown, |p| async move { Ok(p) })
94 .await
95 .unwrap();
96
97 assert_eq!(result.len(), 1);
98 assert!(result[0].ends_with("test.md"));
99 }
100}