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 if let Ok(content) = std::fs::read_to_string(&path) {
74 return Some(ResolvedFile::from_disk(content, path, FileSource::User));
75 }
76 }
77 }
78
79 if let Some(ref project_dir) = self.paths.project_dir {
81 if project_dir.exists() {
82 let path = project_dir.join(relative_path);
83 if path.exists() {
84 if let Ok(content) = std::fs::read_to_string(&path) {
85 return Some(ResolvedFile::from_disk(content, path, FileSource::Project));
86 }
87 }
88 }
89 }
90
91 if let Some(ref project_dir) = self.paths.project_dir_deprecated {
93 if project_dir.exists() {
94 let path = project_dir.join(relative_path);
95 if path.exists() {
96 if let Ok(content) = std::fs::read_to_string(&path) {
97 return Some(ResolvedFile::from_disk(
98 content,
99 path,
100 FileSource::ProjectDeprecated,
101 ));
102 }
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 if project_dir.exists() {
124 let path = project_dir.join(relative_path);
125 if path.exists() {
126 return Some(path);
127 }
128 }
129 }
130
131 if let Some(ref project_dir) = self.paths.project_dir_deprecated {
133 if project_dir.exists() {
134 let path = project_dir.join(relative_path);
135 if path.exists() {
136 return Some(path);
137 }
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 if user_dir.join(relative_path).exists() {
154 return Some(ConfigTier::User);
155 }
156 }
157
158 if let Some(ref project_dir) = self.paths.project_dir {
160 if project_dir.exists() && project_dir.join(relative_path).exists() {
161 return Some(ConfigTier::Project);
162 }
163 }
164
165 if let Some(ref project_dir) = self.paths.project_dir_deprecated {
167 if project_dir.exists() && project_dir.join(relative_path).exists() {
168 return Some(ConfigTier::Project);
169 }
170 }
171
172 None
173 }
174
175 pub fn list_files(&self, relative_dir: &str) -> Vec<(String, FileSource)> {
179 use std::collections::HashMap;
180
181 let mut files: HashMap<String, FileSource> = HashMap::new();
182
183 if let Some(ref project_dir) = self.paths.project_dir_deprecated {
187 let dir = project_dir.join(relative_dir);
188 if dir.is_dir() {
189 if let Ok(entries) = std::fs::read_dir(&dir) {
190 for entry in entries.flatten() {
191 if let Some(name) = entry.file_name().to_str() {
192 files.insert(name.to_string(), FileSource::ProjectDeprecated);
193 }
194 }
195 }
196 }
197 }
198
199 if let Some(ref project_dir) = self.paths.project_dir {
201 let dir = project_dir.join(relative_dir);
202 if dir.is_dir() {
203 if let Ok(entries) = std::fs::read_dir(&dir) {
204 for entry in entries.flatten() {
205 if let Some(name) = entry.file_name().to_str() {
206 files.insert(name.to_string(), FileSource::Project);
207 }
208 }
209 }
210 }
211 }
212
213 if let Some(ref user_dir) = self.paths.user_dir {
215 let dir = user_dir.join(relative_dir);
216 if dir.is_dir() {
217 if let Ok(entries) = std::fs::read_dir(&dir) {
218 for entry in entries.flatten() {
219 if let Some(name) = entry.file_name().to_str() {
220 files.insert(name.to_string(), FileSource::User);
221 }
222 }
223 }
224 }
225 }
226
227 let mut result: Vec<_> = files.into_iter().collect();
228 result.sort_by(|a, b| a.0.cmp(&b.0));
229 result
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::config::ConfigPaths;
237 use tempfile::TempDir;
238
239 fn create_test_loader(temp: &TempDir) -> ConfigLoader {
240 let project_dir = temp.path().join("task-graph");
241 let user_dir = temp.path().join("user");
242 std::fs::create_dir_all(&project_dir).unwrap();
243 std::fs::create_dir_all(&user_dir).unwrap();
244
245 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
246 ConfigLoader::load_with_paths(paths).unwrap()
247 }
248
249 #[test]
250 fn test_find_file_user_priority() {
251 let temp = TempDir::new().unwrap();
252 let project_dir = temp.path().join("task-graph");
253 let user_dir = temp.path().join("user");
254 std::fs::create_dir_all(&project_dir).unwrap();
255 std::fs::create_dir_all(&user_dir).unwrap();
256
257 std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
259 std::fs::write(user_dir.join("test.txt"), "user content").unwrap();
260
261 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
262 let loader = ConfigLoader::load_with_paths(paths).unwrap();
263
264 let file = loader.find_file("test.txt").unwrap();
265 assert_eq!(file.content, "user content");
266 assert_eq!(file.source, FileSource::User);
267 }
268
269 #[test]
270 fn test_find_file_project_fallback() {
271 let temp = TempDir::new().unwrap();
272 let project_dir = temp.path().join("task-graph");
273 let user_dir = temp.path().join("user");
274 std::fs::create_dir_all(&project_dir).unwrap();
275 std::fs::create_dir_all(&user_dir).unwrap();
276
277 std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
279
280 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
281 let loader = ConfigLoader::load_with_paths(paths).unwrap();
282
283 let file = loader.find_file("test.txt").unwrap();
284 assert_eq!(file.content, "project content");
285 assert_eq!(file.source, FileSource::Project);
286 }
287
288 #[test]
289 fn test_find_file_not_found() {
290 let temp = TempDir::new().unwrap();
291 let loader = create_test_loader(&temp);
292
293 assert!(loader.find_file("nonexistent.txt").is_none());
294 }
295
296 #[test]
297 fn test_list_files_deduplication() {
298 let temp = TempDir::new().unwrap();
299 let project_dir = temp.path().join("task-graph");
300 let user_dir = temp.path().join("user");
301 let project_skills = project_dir.join("skills");
302 let user_skills = user_dir.join("skills");
303 std::fs::create_dir_all(&project_skills).unwrap();
304 std::fs::create_dir_all(&user_skills).unwrap();
305
306 std::fs::write(project_skills.join("shared.md"), "project").unwrap();
308 std::fs::write(project_skills.join("project-only.md"), "project").unwrap();
309 std::fs::write(user_skills.join("shared.md"), "user").unwrap();
310 std::fs::write(user_skills.join("user-only.md"), "user").unwrap();
311
312 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
313 let loader = ConfigLoader::load_with_paths(paths).unwrap();
314
315 let files = loader.list_files("skills");
316
317 assert_eq!(files.len(), 3);
319
320 let shared = files.iter().find(|(name, _)| name == "shared.md").unwrap();
322 assert_eq!(shared.1, FileSource::User);
323 }
324}