task_graph_mcp/config/
files.rs1use super::loader::{ConfigLoader, ConfigTier};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FileSource {
12 User,
14 Project,
16 ProjectDeprecated,
18 Embedded,
20}
21
22impl std::fmt::Display for FileSource {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 FileSource::User => write!(f, "user"),
26 FileSource::Project => write!(f, "project"),
27 FileSource::ProjectDeprecated => write!(f, "project (deprecated)"),
28 FileSource::Embedded => write!(f, "embedded"),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct ResolvedFile {
36 pub content: String,
38 pub path: Option<PathBuf>,
40 pub source: FileSource,
42}
43
44impl ResolvedFile {
45 pub fn from_disk(content: String, path: PathBuf, source: FileSource) -> Self {
47 Self {
48 content,
49 path: Some(path),
50 source,
51 }
52 }
53
54 pub fn from_embedded(content: &'static str) -> Self {
56 Self {
57 content: content.to_string(),
58 path: None,
59 source: FileSource::Embedded,
60 }
61 }
62}
63
64impl ConfigLoader {
65 pub fn find_file(&self, relative_path: &str) -> Option<ResolvedFile> {
69 if let Some(ref user_dir) = self.paths.user_dir {
71 let path = user_dir.join(relative_path);
72 if path.exists()
73 && let Ok(content) = std::fs::read_to_string(&path)
74 {
75 return Some(ResolvedFile::from_disk(content, path, FileSource::User));
76 }
77 }
78
79 if let Some(ref project_dir) = self.paths.project_dir
81 && project_dir.exists()
82 {
83 let path = project_dir.join(relative_path);
84 if path.exists()
85 && let Ok(content) = std::fs::read_to_string(&path)
86 {
87 return Some(ResolvedFile::from_disk(content, path, FileSource::Project));
88 }
89 }
90
91 if let Some(ref project_dir) = self.paths.project_dir_deprecated
93 && project_dir.exists()
94 {
95 let path = project_dir.join(relative_path);
96 if path.exists()
97 && let Ok(content) = std::fs::read_to_string(&path)
98 {
99 return Some(ResolvedFile::from_disk(
100 content,
101 path,
102 FileSource::ProjectDeprecated,
103 ));
104 }
105 }
106
107 None
109 }
110
111 pub fn find_file_path(&self, relative_path: &str) -> Option<PathBuf> {
113 if let Some(ref user_dir) = self.paths.user_dir {
115 let path = user_dir.join(relative_path);
116 if path.exists() {
117 return Some(path);
118 }
119 }
120
121 if let Some(ref project_dir) = self.paths.project_dir
123 && project_dir.exists()
124 {
125 let path = project_dir.join(relative_path);
126 if path.exists() {
127 return Some(path);
128 }
129 }
130
131 if let Some(ref project_dir) = self.paths.project_dir_deprecated
133 && project_dir.exists()
134 {
135 let path = project_dir.join(relative_path);
136 if path.exists() {
137 return Some(path);
138 }
139 }
140
141 None
142 }
143
144 pub fn file_exists(&self, relative_path: &str) -> bool {
146 self.find_file_path(relative_path).is_some()
147 }
148
149 pub fn file_tier(&self, relative_path: &str) -> Option<ConfigTier> {
151 if let Some(ref user_dir) = self.paths.user_dir
153 && user_dir.join(relative_path).exists()
154 {
155 return Some(ConfigTier::User);
156 }
157
158 if let Some(ref project_dir) = self.paths.project_dir
160 && project_dir.exists()
161 && project_dir.join(relative_path).exists()
162 {
163 return Some(ConfigTier::Project);
164 }
165
166 if let Some(ref project_dir) = self.paths.project_dir_deprecated
168 && project_dir.exists()
169 && project_dir.join(relative_path).exists()
170 {
171 return Some(ConfigTier::Project);
172 }
173
174 None
175 }
176
177 pub fn list_files(&self, relative_dir: &str) -> Vec<(String, FileSource)> {
181 use std::collections::HashMap;
182
183 let mut files: HashMap<String, FileSource> = HashMap::new();
184
185 if let Some(ref project_dir) = self.paths.project_dir_deprecated {
189 let dir = project_dir.join(relative_dir);
190 if dir.is_dir()
191 && let Ok(entries) = std::fs::read_dir(&dir)
192 {
193 for entry in entries.flatten() {
194 if let Some(name) = entry.file_name().to_str() {
195 files.insert(name.to_string(), FileSource::ProjectDeprecated);
196 }
197 }
198 }
199 }
200
201 if let Some(ref project_dir) = self.paths.project_dir {
203 let dir = project_dir.join(relative_dir);
204 if dir.is_dir()
205 && let Ok(entries) = std::fs::read_dir(&dir)
206 {
207 for entry in entries.flatten() {
208 if let Some(name) = entry.file_name().to_str() {
209 files.insert(name.to_string(), FileSource::Project);
210 }
211 }
212 }
213 }
214
215 if let Some(ref user_dir) = self.paths.user_dir {
217 let dir = user_dir.join(relative_dir);
218 if dir.is_dir()
219 && let Ok(entries) = std::fs::read_dir(&dir)
220 {
221 for entry in entries.flatten() {
222 if let Some(name) = entry.file_name().to_str() {
223 files.insert(name.to_string(), FileSource::User);
224 }
225 }
226 }
227 }
228
229 let mut result: Vec<_> = files.into_iter().collect();
230 result.sort_by(|a, b| a.0.cmp(&b.0));
231 result
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::config::ConfigPaths;
239 use tempfile::TempDir;
240
241 fn create_test_loader(temp: &TempDir) -> ConfigLoader {
242 let project_dir = temp.path().join("task-graph");
243 let user_dir = temp.path().join("user");
244 std::fs::create_dir_all(&project_dir).unwrap();
245 std::fs::create_dir_all(&user_dir).unwrap();
246
247 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
248 ConfigLoader::load_with_paths(paths).unwrap()
249 }
250
251 #[test]
252 fn test_find_file_user_priority() {
253 let temp = TempDir::new().unwrap();
254 let project_dir = temp.path().join("task-graph");
255 let user_dir = temp.path().join("user");
256 std::fs::create_dir_all(&project_dir).unwrap();
257 std::fs::create_dir_all(&user_dir).unwrap();
258
259 std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
261 std::fs::write(user_dir.join("test.txt"), "user content").unwrap();
262
263 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
264 let loader = ConfigLoader::load_with_paths(paths).unwrap();
265
266 let file = loader.find_file("test.txt").unwrap();
267 assert_eq!(file.content, "user content");
268 assert_eq!(file.source, FileSource::User);
269 }
270
271 #[test]
272 fn test_find_file_project_fallback() {
273 let temp = TempDir::new().unwrap();
274 let project_dir = temp.path().join("task-graph");
275 let user_dir = temp.path().join("user");
276 std::fs::create_dir_all(&project_dir).unwrap();
277 std::fs::create_dir_all(&user_dir).unwrap();
278
279 std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
281
282 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
283 let loader = ConfigLoader::load_with_paths(paths).unwrap();
284
285 let file = loader.find_file("test.txt").unwrap();
286 assert_eq!(file.content, "project content");
287 assert_eq!(file.source, FileSource::Project);
288 }
289
290 #[test]
291 fn test_find_file_not_found() {
292 let temp = TempDir::new().unwrap();
293 let loader = create_test_loader(&temp);
294
295 assert!(loader.find_file("nonexistent.txt").is_none());
296 }
297
298 #[test]
299 fn test_list_files_deduplication() {
300 let temp = TempDir::new().unwrap();
301 let project_dir = temp.path().join("task-graph");
302 let user_dir = temp.path().join("user");
303 let project_skills = project_dir.join("skills");
304 let user_skills = user_dir.join("skills");
305 std::fs::create_dir_all(&project_skills).unwrap();
306 std::fs::create_dir_all(&user_skills).unwrap();
307
308 std::fs::write(project_skills.join("shared.md"), "project").unwrap();
310 std::fs::write(project_skills.join("project-only.md"), "project").unwrap();
311 std::fs::write(user_skills.join("shared.md"), "user").unwrap();
312 std::fs::write(user_skills.join("user-only.md"), "user").unwrap();
313
314 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
315 let loader = ConfigLoader::load_with_paths(paths).unwrap();
316
317 let files = loader.list_files("skills");
318
319 assert_eq!(files.len(), 3);
321
322 let shared = files.iter().find(|(name, _)| name == "shared.md").unwrap();
324 assert_eq!(shared.1, FileSource::User);
325 }
326}