winx_code_agent/utils/
repo.rs1use crate::errors::Result;
2use std::fmt::Write as FmtWrite;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7const MAX_DEPTH: usize = 10;
8const MAX_CONTEXT_FILES: usize = 160;
9const MAX_RECENT_FILES: usize = 30;
10
11const IMPORTANT_NAMES: &[&str] = &[
12 "Cargo.toml",
13 "README.md",
14 "AGENTS.md",
15 "package.json",
16 "pnpm-workspace.yaml",
17 "pyproject.toml",
18 "go.mod",
19 "Dockerfile",
20 "docker-compose.yml",
21 ".github/workflows/ci.yml",
22];
23
24const SKIP_DIRS: &[&str] =
25 &[".git", "target", "node_modules", ".next", "dist", "build", ".venv", "__pycache__"];
26
27#[derive(Debug, Clone)]
28pub struct RepoContext {
29 pub root: PathBuf,
30 pub is_git_repo: bool,
31 pub project_summary: String,
32 pub recent_files: Vec<String>,
33 pub important_files: Vec<String>,
34 pub project_files: Vec<String>,
35}
36
37pub struct RepoContextAnalyzer;
38
39impl RepoContextAnalyzer {
40 pub fn analyze(path: &Path) -> Result<RepoContext> {
41 let root = workspace_root(path);
42 let is_git_repo = root.join(".git").exists();
43 let mut project_files = collect_project_files(&root)?;
44 project_files.sort_by_key(|path| (path_score(path), path.clone()));
45 project_files.truncate(MAX_CONTEXT_FILES);
46
47 let recent_files = if is_git_repo { recent_git_files(&root) } else { Vec::new() };
48 let important_files = important_files(&project_files);
49 let project_summary = project_summary(&root, is_git_repo, project_files.len());
50
51 Ok(RepoContext {
52 root,
53 is_git_repo,
54 project_summary,
55 recent_files,
56 important_files,
57 project_files,
58 })
59 }
60}
61
62pub fn get_repo_context(path: &Path) -> Result<(String, Vec<String>)> {
63 let context = RepoContextAnalyzer::analyze(path)?;
64 let mut output = String::new();
65
66 let _ = writeln!(output, "Project root: {}", context.root.display());
67 let _ = writeln!(output, "Git repository: {}", if context.is_git_repo { "yes" } else { "no" });
68 let _ = writeln!(output, "{}", context.project_summary);
69
70 if !context.important_files.is_empty() {
71 output.push_str("\nImportant files:\n");
72 for file in &context.important_files {
73 let _ = writeln!(output, "- {file}");
74 }
75 }
76
77 if !context.recent_files.is_empty() {
78 output.push_str("\nRecent git files:\n");
79 for file in &context.recent_files {
80 let _ = writeln!(output, "- {file}");
81 }
82 }
83
84 output.push_str("\nWorkspace files:\n");
85 for file in &context.project_files {
86 let _ = writeln!(output, "- {file}");
87 }
88
89 Ok((output, context.project_files))
90}
91
92fn workspace_root(path: &Path) -> PathBuf {
93 if path.is_file() {
94 path.parent().unwrap_or(path).to_path_buf()
95 } else {
96 path.to_path_buf()
97 }
98}
99
100fn collect_project_files(root: &Path) -> Result<Vec<String>> {
101 let mut files = Vec::new();
102 collect_files(root, root, 0, &mut files)?;
103 Ok(files)
104}
105
106fn collect_files(root: &Path, current: &Path, depth: usize, files: &mut Vec<String>) -> Result<()> {
107 if depth > MAX_DEPTH {
108 return Ok(());
109 }
110
111 let mut entries = fs::read_dir(current)?.collect::<std::result::Result<Vec<_>, _>>()?;
112 entries.sort_by_key(std::fs::DirEntry::path);
113
114 for entry in entries {
115 let path = entry.path();
116 let name = entry.file_name();
117 let name = name.to_string_lossy();
118 if path.is_dir() {
119 if !SKIP_DIRS.contains(&name.as_ref()) {
120 collect_files(root, &path, depth + 1, files)?;
121 }
122 } else if path.is_file() {
123 if let Ok(relative) = path.strip_prefix(root) {
124 files.push(relative.to_string_lossy().to_string());
125 }
126 }
127 }
128
129 Ok(())
130}
131
132fn important_files(files: &[String]) -> Vec<String> {
133 files.iter().filter(|file| IMPORTANT_NAMES.contains(&file.as_str())).cloned().collect()
134}
135
136fn recent_git_files(root: &Path) -> Vec<String> {
137 let output = Command::new("git")
138 .args(["-C"])
139 .arg(root)
140 .args(["log", "--name-only", "--pretty=format:", "-n", "50"])
141 .output();
142
143 let Ok(output) = output else {
144 return Vec::new();
145 };
146 if !output.status.success() {
147 return Vec::new();
148 }
149
150 let mut recent = Vec::new();
151 for line in String::from_utf8_lossy(&output.stdout).lines().map(str::trim) {
152 if !line.is_empty() && !recent.iter().any(|existing| existing == line) {
153 recent.push(line.to_string());
154 }
155 if recent.len() >= MAX_RECENT_FILES {
156 break;
157 }
158 }
159 recent
160}
161
162fn project_summary(root: &Path, is_git_repo: bool, file_count: usize) -> String {
163 let manifest = if root.join("Cargo.toml").exists() {
164 "Rust/Cargo"
165 } else if root.join("package.json").exists() {
166 "Node.js"
167 } else if root.join("pyproject.toml").exists() {
168 "Python"
169 } else {
170 "generic"
171 };
172 format!("Detected {manifest} workspace with {file_count} indexed files; git={is_git_repo}.")
173}
174
175fn path_score(path: &str) -> usize {
176 let important = usize::from(!IMPORTANT_NAMES.contains(&path));
177 let depth = path.matches('/').count();
178 let test_penalty = usize::from(path.contains("test") || path.contains("spec"));
179 important * 10 + depth + test_penalty
180}
181
182#[cfg(test)]
183mod tests {
184 use super::get_repo_context;
185 use crate::errors::Result;
186 use tempfile::TempDir;
187
188 #[test]
189 fn builds_repo_context_from_files() -> Result<()> {
190 let temp_dir = TempDir::new()?;
191 std::fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname='x'\n")?;
192 std::fs::create_dir(temp_dir.path().join("src"))?;
193 std::fs::write(temp_dir.path().join("src/lib.rs"), "pub fn x() {}\n")?;
194
195 let (context, files) = get_repo_context(temp_dir.path())?;
196 assert!(context.contains("Cargo.toml"));
197 assert!(files.iter().any(|file| file == "src/lib.rs"));
198 Ok(())
199 }
200}