1use std::path::{Path, PathBuf};
11
12pub struct ContextManager {
13 project_path: PathBuf,
14}
15
16const DEFAULT_INIT_MD: &str = r#"# Project Guidelines
17
18## Core Rules
191. Annotate path at top of each code file: `# {relative_path}`
202. Prefer modifying existing files over creating new ones
213. All file operations must stay within the project directory
224. Dangerous commands require caution
23
24## Workflow
251. Analyze requirements first, then plan
262. Small steps, frequent testing
273. Write self-contained test functions
284. Check safety before using tools
29
30## Code Quality
31- Follow language best practices
32- Keep functions reasonably short
33- Add necessary docstrings
34- Handle errors gracefully
35------
36"#;
37
38impl ContextManager {
39 pub fn new(project_path: &Path) -> Self {
40 let cm = ContextManager {
41 project_path: project_path.to_path_buf(),
42 };
43 let _ = std::fs::create_dir_all(cm.context_dir());
44 cm
45 }
46
47 fn context_dir(&self) -> PathBuf {
48 self.project_path.join(".aacode").join("context")
49 }
50
51 pub fn load_init_instructions(&self) -> String {
53 let path = self.project_path.join("init.md");
54 if let Ok(s) = std::fs::read_to_string(&path) {
55 if !s.trim().is_empty() {
56 return s;
57 }
58 }
59 let _ = std::fs::write(&path, DEFAULT_INIT_MD);
61 DEFAULT_INIT_MD.to_string()
62 }
63
64 pub fn save_large_output(&self, content: &str, name_hint: &str) -> Option<String> {
66 let dir = self.context_dir();
67 std::fs::create_dir_all(&dir).ok()?;
68 let name = format!(
69 "{}_{}.txt",
70 sanitize(name_hint),
71 uuid::Uuid::new_v4().simple()
72 );
73 let path = dir.join(name);
74 std::fs::write(&path, content).ok()?;
75 Some(path.to_string_lossy().to_string())
76 }
77
78 pub fn analyze_project_structure(&self) -> String {
81 use std::collections::BTreeMap;
82 let mut ext_counts: BTreeMap<String, usize> = BTreeMap::new();
83 let mut top_entries: Vec<String> = Vec::new();
84 let exclude = [
85 ".git",
86 ".aacode",
87 "node_modules",
88 "target",
89 "__pycache__",
90 ".venv",
91 ];
92
93 if let Ok(rd) = std::fs::read_dir(&self.project_path) {
94 for e in rd.flatten() {
95 let name = e.file_name().to_string_lossy().to_string();
96 if exclude.contains(&name.as_str()) {
97 continue;
98 }
99 let is_dir = e.path().is_dir();
100 top_entries.push(if is_dir { format!("{name}/") } else { name });
101 }
102 }
103 top_entries.sort();
104
105 let mut stack = vec![self.project_path.clone()];
107 let mut visited = 0usize;
108 while let Some(dir) = stack.pop() {
109 if visited > 5000 {
110 break;
111 }
112 let rd = match std::fs::read_dir(&dir) {
113 Ok(r) => r,
114 Err(_) => continue,
115 };
116 for e in rd.flatten() {
117 let p = e.path();
118 let fname = e.file_name().to_string_lossy().to_string();
119 if exclude.contains(&fname.as_str()) {
120 continue;
121 }
122 if p.is_dir() {
123 stack.push(p);
124 } else {
125 visited += 1;
126 let ext = p
127 .extension()
128 .map(|s| s.to_string_lossy().to_string())
129 .unwrap_or_else(|| "(none)".to_string());
130 *ext_counts.entry(ext).or_insert(0) += 1;
131 }
132 }
133 }
134
135 let mut out = String::from("Project structure summary:\n\nTop-level entries:\n");
136 for e in top_entries.iter().take(50) {
137 out.push_str(&format!(" {e}\n"));
138 }
139 out.push_str("\nFile counts by extension:\n");
140 for (ext, n) in ext_counts.iter() {
141 out.push_str(&format!(" .{ext}: {n}\n"));
142 }
143 if out.len() > 1500 {
145 out.truncate(1500);
146 out.push_str("\n...(truncated)");
147 }
148 out
149 }
150}
151
152fn sanitize(s: &str) -> String {
153 s.chars()
154 .map(|c| {
155 if c.is_alphanumeric() || c == '_' {
156 c
157 } else {
158 '_'
159 }
160 })
161 .collect()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn tmp() -> PathBuf {
169 let d = std::env::temp_dir().join(format!(
170 "aacode_ctx_{}_{}",
171 std::process::id(),
172 uuid::Uuid::new_v4().simple()
173 ));
174 std::fs::create_dir_all(&d).unwrap();
175 d
176 }
177
178 #[test]
179 fn init_md_created_default() {
180 let d = tmp();
181 let cm = ContextManager::new(&d);
182 let content = cm.load_init_instructions();
183 assert!(content.contains("Project Guidelines"));
184 assert!(d.join("init.md").exists());
185 }
186
187 #[test]
188 fn init_md_existing_used() {
189 let d = tmp();
190 std::fs::write(d.join("init.md"), "# Custom\nrules").unwrap();
191 let cm = ContextManager::new(&d);
192 assert!(cm.load_init_instructions().contains("Custom"));
193 }
194
195 #[test]
196 fn large_output_archived() {
197 let d = tmp();
198 let cm = ContextManager::new(&d);
199 let path = cm
200 .save_large_output("big content", "run_shell_output")
201 .unwrap();
202 assert!(std::path::Path::new(&path).exists());
203 assert_eq!(std::fs::read_to_string(&path).unwrap(), "big content");
204 }
205
206 #[test]
207 fn structure_summary() {
208 let d = tmp();
209 std::fs::write(d.join("main.rs"), "fn main(){}").unwrap();
210 std::fs::write(d.join("lib.py"), "x=1").unwrap();
211 std::fs::create_dir_all(d.join("src")).unwrap();
212 let cm = ContextManager::new(&d);
213 let s = cm.analyze_project_structure();
214 assert!(s.contains("main.rs"));
215 assert!(s.contains(".rs:") || s.contains("rs:"));
216 }
217
218 #[test]
219 fn large_output_sanitizes_path_hint() {
220 let d = tmp();
221 let cm = ContextManager::new(&d);
222 let path = cm.save_large_output("payload", "../../etc/passwd").unwrap();
223 let p = std::path::Path::new(&path);
224 assert!(p.starts_with(cm.context_dir()));
226 let fname = p.file_name().unwrap().to_string_lossy();
227 assert!(!fname.contains('/') && !fname.contains(".."));
228 assert!(fname.starts_with("______etc_passwd_"));
229 }
230
231 #[test]
232 fn structure_excludes_heavy_dirs() {
233 let d = tmp();
234 std::fs::create_dir_all(d.join("node_modules")).unwrap();
235 std::fs::write(d.join("node_modules/evil.js"), "x").unwrap();
236 std::fs::create_dir_all(d.join(".git")).unwrap();
237 std::fs::write(d.join(".git/config"), "x").unwrap();
238 std::fs::create_dir_all(d.join("src")).unwrap();
239 std::fs::write(d.join("main.rs"), "fn main(){}").unwrap();
240 let cm = ContextManager::new(&d);
241 let s = cm.analyze_project_structure();
242 assert!(s.contains("main.rs"));
243 assert!(s.contains("src/"));
244 assert!(!s.contains("node_modules"));
245 assert!(!s.contains(".git"));
246 }
247}