1use anyhow::Result;
2use std::path::Path;
3use tracing::debug;
4
5use crate::types::Ecosystem;
6
7pub fn detect_ecosystems(root: &Path) -> Result<Vec<Ecosystem>> {
9 let mut detected = Vec::new();
10
11 let cargo_toml = root.join("Cargo.toml");
13 if cargo_toml.exists() {
14 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
15 if content.contains("[workspace]") {
16 debug!("Detected Cargo workspace at {}", cargo_toml.display());
17 detected.push(Ecosystem::Cargo);
18 }
19 }
20 }
21
22 let yarnrc = root.join(".yarnrc.yml");
24 if yarnrc.exists() {
25 debug!("Detected Yarn Berry project via .yarnrc.yml");
26 detected.push(Ecosystem::Yarn);
27 } else {
28 let pkg_json = root.join("package.json");
30 let pnpm_ws = root.join("pnpm-workspace.yaml");
31 if pnpm_ws.exists() {
32 debug!("Detected pnpm workspace via pnpm-workspace.yaml");
33 detected.push(Ecosystem::Npm);
34 } else if pkg_json.exists() {
35 if let Ok(content) = std::fs::read_to_string(&pkg_json) {
36 if content.contains("\"workspaces\"") {
37 debug!("Detected npm workspaces in package.json");
38 detected.push(Ecosystem::Npm);
39 }
40 }
41 }
42 }
43
44 if root.join("go.work").exists() || root.join("go.mod").exists() {
46 debug!("Detected Go project");
47 detected.push(Ecosystem::Go);
48 }
49
50 let root_pyproject = root.join("pyproject.toml");
52 if root_pyproject.exists() {
53 if let Ok(content) = std::fs::read_to_string(&root_pyproject) {
54 if content.contains("[tool.poetry]") {
55 debug!("Detected Poetry project via [tool.poetry] in pyproject.toml");
56 detected.push(Ecosystem::Python);
57 } else if content.contains("[tool.uv.workspace]") {
58 debug!("Detected uv workspace via [tool.uv.workspace] in pyproject.toml");
59 detected.push(Ecosystem::Python);
60 } else {
61 debug!("Detected generic Python project via pyproject.toml");
62 detected.push(Ecosystem::Python);
63 }
64 } else {
65 detected.push(Ecosystem::Python);
66 }
67 } else {
68 let pattern = root.join("*/pyproject.toml");
70 if let Ok(paths) = glob::glob(pattern.to_str().unwrap_or("")) {
71 let count = paths.filter_map(|p| p.ok()).count();
72 if count >= 2 {
73 debug!(
74 "Detected Python monorepo ({} pyproject.toml files found)",
75 count
76 );
77 detected.push(Ecosystem::Python);
78 }
79 }
80 }
81
82 let pom_xml = root.join("pom.xml");
84 if pom_xml.exists() {
85 if let Ok(content) = std::fs::read_to_string(&pom_xml) {
86 if content.contains("<modules>") {
87 debug!("Detected Maven multi-module project via pom.xml");
88 detected.push(Ecosystem::Maven);
89 }
90 }
91 }
92
93 if root.join("settings.gradle").exists() || root.join("settings.gradle.kts").exists() {
95 debug!("Detected Gradle project");
96 detected.push(Ecosystem::Gradle);
97 }
98
99 if detected.is_empty() {
100 anyhow::bail!(
101 "No supported project type found at {}.\n\
102 Looked for: Cargo.toml (workspace), package.json (workspaces), \
103 go.work/go.mod, pyproject.toml, pom.xml (modules), settings.gradle(.kts)",
104 root.display()
105 );
106 }
107
108 debug!("Detected ecosystems: {:?}", detected);
109 Ok(detected)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_detect_cargo_workspace() {
118 let dir = tempfile::tempdir().unwrap();
119 std::fs::write(
120 dir.path().join("Cargo.toml"),
121 "[workspace]\nmembers = [\"crates/*\"]\n",
122 )
123 .unwrap();
124
125 let ecosystems = detect_ecosystems(dir.path()).unwrap();
126 assert_eq!(ecosystems, vec![Ecosystem::Cargo]);
127 }
128
129 #[test]
130 fn test_detect_cargo_without_workspace_ignored() {
131 let dir = tempfile::tempdir().unwrap();
132 std::fs::write(
133 dir.path().join("Cargo.toml"),
134 "[package]\nname = \"solo\"\n",
135 )
136 .unwrap();
137
138 assert!(detect_ecosystems(dir.path()).is_err());
139 }
140
141 #[test]
142 fn test_detect_npm_workspaces() {
143 let dir = tempfile::tempdir().unwrap();
144 std::fs::write(
145 dir.path().join("package.json"),
146 r#"{"name": "root", "workspaces": ["packages/*"]}"#,
147 )
148 .unwrap();
149
150 let ecosystems = detect_ecosystems(dir.path()).unwrap();
151 assert_eq!(ecosystems, vec![Ecosystem::Npm]);
152 }
153
154 #[test]
155 fn test_detect_pnpm_workspace() {
156 let dir = tempfile::tempdir().unwrap();
157 std::fs::write(
158 dir.path().join("pnpm-workspace.yaml"),
159 "packages:\n - 'packages/*'\n",
160 )
161 .unwrap();
162
163 let ecosystems = detect_ecosystems(dir.path()).unwrap();
164 assert_eq!(ecosystems, vec![Ecosystem::Npm]);
165 }
166
167 #[test]
168 fn test_detect_yarn_workspace() {
169 let dir = tempfile::tempdir().unwrap();
170 std::fs::write(dir.path().join(".yarnrc.yml"), "nodeLinker: pnp\n").unwrap();
171
172 let ecosystems = detect_ecosystems(dir.path()).unwrap();
173 assert_eq!(ecosystems, vec![Ecosystem::Yarn]);
174 }
175
176 #[test]
177 fn test_detect_go_workspace() {
178 let dir = tempfile::tempdir().unwrap();
179 std::fs::write(dir.path().join("go.work"), "go 1.21\n").unwrap();
180
181 let ecosystems = detect_ecosystems(dir.path()).unwrap();
182 assert_eq!(ecosystems, vec![Ecosystem::Go]);
183 }
184
185 #[test]
186 fn test_detect_go_single_module() {
187 let dir = tempfile::tempdir().unwrap();
188 std::fs::write(dir.path().join("go.mod"), "module example.com/foo\n").unwrap();
189
190 let ecosystems = detect_ecosystems(dir.path()).unwrap();
191 assert_eq!(ecosystems, vec![Ecosystem::Go]);
192 }
193
194 #[test]
195 fn test_detect_python_root_pyproject() {
196 let dir = tempfile::tempdir().unwrap();
197 std::fs::write(
198 dir.path().join("pyproject.toml"),
199 "[project]\nname = \"myapp\"\n",
200 )
201 .unwrap();
202
203 let ecosystems = detect_ecosystems(dir.path()).unwrap();
204 assert_eq!(ecosystems, vec![Ecosystem::Python]);
205 }
206
207 #[test]
208 fn test_detect_multiple_ecosystems() {
209 let dir = tempfile::tempdir().unwrap();
210 std::fs::write(dir.path().join("Cargo.toml"), "[workspace]\nmembers = []\n").unwrap();
211 std::fs::write(dir.path().join("go.mod"), "module example.com/x\n").unwrap();
212
213 let ecosystems = detect_ecosystems(dir.path()).unwrap();
214 assert!(ecosystems.contains(&Ecosystem::Cargo));
215 assert!(ecosystems.contains(&Ecosystem::Go));
216 assert_eq!(ecosystems.len(), 2);
217 }
218
219 #[test]
220 fn test_detect_empty_directory_errors() {
221 let dir = tempfile::tempdir().unwrap();
222 assert!(detect_ecosystems(dir.path()).is_err());
223 }
224
225 #[test]
226 fn test_detect_npm_without_workspaces_ignored() {
227 let dir = tempfile::tempdir().unwrap();
228 std::fs::write(
229 dir.path().join("package.json"),
230 r#"{"name": "solo", "version": "1.0.0"}"#,
231 )
232 .unwrap();
233
234 assert!(detect_ecosystems(dir.path()).is_err());
235 }
236
237 #[test]
238 fn test_detect_maven_multi_module() {
239 let dir = tempfile::tempdir().unwrap();
240 std::fs::write(
241 dir.path().join("pom.xml"),
242 r#"<project><modules><module>core</module></modules></project>"#,
243 )
244 .unwrap();
245
246 let ecosystems = detect_ecosystems(dir.path()).unwrap();
247 assert_eq!(ecosystems, vec![Ecosystem::Maven]);
248 }
249
250 #[test]
251 fn test_detect_gradle_groovy() {
252 let dir = tempfile::tempdir().unwrap();
253 std::fs::write(
254 dir.path().join("settings.gradle"),
255 "include ':core', ':app'\n",
256 )
257 .unwrap();
258
259 let ecosystems = detect_ecosystems(dir.path()).unwrap();
260 assert_eq!(ecosystems, vec![Ecosystem::Gradle]);
261 }
262
263 #[test]
264 fn test_detect_gradle_kotlin() {
265 let dir = tempfile::tempdir().unwrap();
266 std::fs::write(
267 dir.path().join("settings.gradle.kts"),
268 "include(\":core\", \":app\")\n",
269 )
270 .unwrap();
271
272 let ecosystems = detect_ecosystems(dir.path()).unwrap();
273 assert_eq!(ecosystems, vec![Ecosystem::Gradle]);
274 }
275
276 #[test]
277 fn test_detect_poetry_project() {
278 let dir = tempfile::tempdir().unwrap();
279 std::fs::write(
280 dir.path().join("pyproject.toml"),
281 "[tool.poetry]\nname = \"myapp\"\n",
282 )
283 .unwrap();
284
285 let ecosystems = detect_ecosystems(dir.path()).unwrap();
286 assert_eq!(ecosystems, vec![Ecosystem::Python]);
287 }
288
289 #[test]
290 fn test_detect_uv_workspace() {
291 let dir = tempfile::tempdir().unwrap();
292 std::fs::write(
293 dir.path().join("pyproject.toml"),
294 "[tool.uv.workspace]\nmembers = [\"packages/*\"]\n",
295 )
296 .unwrap();
297
298 let ecosystems = detect_ecosystems(dir.path()).unwrap();
299 assert_eq!(ecosystems, vec![Ecosystem::Python]);
300 }
301}