1use anyhow::Result;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone)]
6pub struct DetectedFile {
7 pub path: PathBuf,
8}
9
10pub struct ProjectDetector {
12 project_path: PathBuf,
13}
14
15impl ProjectDetector {
16 pub fn new(project_path: PathBuf) -> Self {
17 Self { project_path }
18 }
19
20 pub fn detect(&self) -> Result<Vec<DetectedFile>> {
22 let mut detected = Vec::new();
23
24 let package_json = self.project_path.join("package.json");
25 if !package_json.exists() {
26 return Ok(detected);
27 }
28
29 detected.push(DetectedFile {
30 path: package_json.clone(),
31 });
32
33 if let Ok(content) = std::fs::read_to_string(&package_json)
35 && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content)
36 && let Some(workspaces) = self.get_workspaces(&parsed) {
37 for pattern in workspaces {
38 let member_jsons = self.expand_workspace_pattern(&pattern)?;
39 for path in member_jsons {
40 if path != package_json {
41 detected.push(DetectedFile { path });
42 }
43 }
44 }
45 }
46
47 Ok(detected)
48 }
49
50 fn get_workspaces(&self, parsed: &serde_json::Value) -> Option<Vec<String>> {
52 if let Some(workspaces) = parsed.get("workspaces") {
54 if let Some(arr) = workspaces.as_array() {
56 return Some(
57 arr.iter()
58 .filter_map(|v| v.as_str().map(String::from))
59 .collect(),
60 );
61 }
62 if let Some(packages) = workspaces.get("packages").and_then(|v| v.as_array()) {
64 return Some(
65 packages
66 .iter()
67 .filter_map(|v| v.as_str().map(String::from))
68 .collect(),
69 );
70 }
71 }
72 None
73 }
74
75 fn expand_workspace_pattern(&self, pattern: &str) -> Result<Vec<PathBuf>> {
77 let mut results = Vec::new();
78 let full_pattern = self.project_path.join(pattern).join("package.json");
79 let pattern_str = full_pattern.to_string_lossy();
80
81 if let Ok(paths) = glob::glob(&pattern_str) {
82 for entry in paths.flatten() {
83 if entry.exists() {
84 results.push(entry);
85 }
86 }
87 }
88
89 Ok(results)
90 }
91
92 pub fn detect_lockfile(&self) -> Option<LockfileType> {
94 if self.project_path.join("package-lock.json").exists() {
95 Some(LockfileType::Npm)
96 } else if self.project_path.join("pnpm-lock.yaml").exists() {
97 Some(LockfileType::Pnpm)
98 } else if self.project_path.join("yarn.lock").exists() {
99 Some(LockfileType::Yarn)
100 } else if self.project_path.join("bun.lockb").exists() {
101 Some(LockfileType::Bun)
102 } else {
103 None
104 }
105 }
106
107 pub fn lockfile_path(&self, lockfile_type: LockfileType) -> PathBuf {
108 match lockfile_type {
109 LockfileType::Npm => self.project_path.join("package-lock.json"),
110 LockfileType::Pnpm => self.project_path.join("pnpm-lock.yaml"),
111 LockfileType::Yarn => self.project_path.join("yarn.lock"),
112 LockfileType::Bun => self.project_path.join("bun.lockb"),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum LockfileType {
119 Npm,
120 Pnpm,
121 Yarn,
122 Bun,
123}