1use anyhow::Result;
2use check_updates_core::{UpdateSeverity, Version};
3use std::process::Command;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub enum GlobalSource {
9 Npm,
10}
11
12impl std::fmt::Display for GlobalSource {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 match self {
15 GlobalSource::Npm => write!(f, "npm"),
16 }
17 }
18}
19
20#[derive(Debug, Clone)]
22pub struct GlobalPackage {
23 pub name: String,
24 pub installed_version: Version,
25 pub source: GlobalSource,
26}
27
28#[derive(Debug, Clone)]
30pub struct GlobalCheck {
31 pub package: GlobalPackage,
32 pub latest: Version,
33 pub has_update: bool,
34}
35
36impl GlobalCheck {
37 pub fn update_severity(&self) -> Option<UpdateSeverity> {
39 if !self.has_update {
40 return None;
41 }
42 let current = &self.package.installed_version;
43 let target = &self.latest;
44
45 if target.major > current.major {
46 Some(UpdateSeverity::Major)
47 } else if target.minor > current.minor {
48 Some(UpdateSeverity::Minor)
49 } else if target.patch > current.patch {
50 Some(UpdateSeverity::Patch)
51 } else {
52 None
53 }
54 }
55}
56
57#[derive(Default)]
59pub struct GlobalPackageDiscovery;
60
61impl GlobalPackageDiscovery {
62 pub fn new() -> Self {
63 Self
64 }
65
66 pub fn discover(&self) -> Vec<GlobalPackage> {
68 self.discover_npm_packages().unwrap_or_default()
69 }
70
71 fn discover_npm_packages(&self) -> Result<Vec<GlobalPackage>> {
73 let output = Command::new("npm")
74 .args(["list", "-g", "--json", "--depth=0"])
75 .output();
76
77 match output {
78 Ok(output) if output.status.success() => {
79 self.parse_npm_global_json(&String::from_utf8_lossy(&output.stdout))
80 }
81 _ => Ok(Vec::new()),
82 }
83 }
84
85 fn parse_npm_global_json(&self, json_str: &str) -> Result<Vec<GlobalPackage>> {
88 let data: serde_json::Value = serde_json::from_str(json_str)?;
89 let mut packages = Vec::new();
90
91 if let Some(deps) = data.get("dependencies").and_then(|v| v.as_object()) {
92 for (name, dep_data) in deps {
93 if let Some(version_str) = dep_data.get("version").and_then(|v| v.as_str())
94 && let Ok(version) = Version::from_str(version_str)
95 {
96 packages.push(GlobalPackage {
97 name: name.clone(),
98 installed_version: version,
99 source: GlobalSource::Npm,
100 });
101 }
102 }
103 }
104
105 Ok(packages)
106 }
107}
108
109pub fn generate_upgrade_commands(checks: &[GlobalCheck]) -> Vec<String> {
111 let outdated: Vec<&GlobalCheck> = checks.iter().filter(|c| c.has_update).collect();
112
113 if outdated.is_empty() {
114 return Vec::new();
115 }
116
117 let package_names: Vec<&str> = outdated.iter().map(|c| c.package.name.as_str()).collect();
118 vec![format!("npm install -g {}", package_names.join(" "))]
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_parse_npm_global_json() {
127 let discovery = GlobalPackageDiscovery::new();
128 let json = r#"{
129 "version": "10.2.0",
130 "name": "lib",
131 "dependencies": {
132 "typescript": {
133 "version": "5.4.5"
134 },
135 "prettier": {
136 "version": "3.2.5"
137 },
138 "@angular/cli": {
139 "version": "17.3.8"
140 }
141 }
142 }"#;
143 let packages = discovery.parse_npm_global_json(json).expect("should parse");
144 assert_eq!(packages.len(), 3);
145
146 let ts = packages.iter().find(|p| p.name == "typescript").expect("should find typescript");
147 assert_eq!(ts.installed_version.to_string(), "5.4.5");
148 assert_eq!(ts.source, GlobalSource::Npm);
149
150 let angular = packages.iter().find(|p| p.name == "@angular/cli").expect("should find @angular/cli");
151 assert_eq!(angular.installed_version.to_string(), "17.3.8");
152 }
153
154 #[test]
155 fn test_parse_npm_global_json_empty() {
156 let discovery = GlobalPackageDiscovery::new();
157 let json = r#"{"version": "10.2.0", "name": "lib"}"#;
158 let packages = discovery.parse_npm_global_json(json).expect("should parse");
159 assert!(packages.is_empty());
160 }
161
162 #[test]
163 fn test_global_source_display() {
164 assert_eq!(GlobalSource::Npm.to_string(), "npm");
165 }
166
167 #[test]
168 fn test_update_severity() {
169 let pkg = GlobalPackage {
170 name: "test".to_string(),
171 installed_version: Version::from_str("1.0.0").expect("valid version"),
172 source: GlobalSource::Npm,
173 };
174
175 let check = GlobalCheck {
176 package: pkg.clone(),
177 latest: Version::from_str("2.0.0").expect("valid version"),
178 has_update: true,
179 };
180 assert_eq!(check.update_severity(), Some(UpdateSeverity::Major));
181
182 let check = GlobalCheck {
183 package: pkg.clone(),
184 latest: Version::from_str("1.1.0").expect("valid version"),
185 has_update: true,
186 };
187 assert_eq!(check.update_severity(), Some(UpdateSeverity::Minor));
188
189 let check = GlobalCheck {
190 package: pkg.clone(),
191 latest: Version::from_str("1.0.1").expect("valid version"),
192 has_update: true,
193 };
194 assert_eq!(check.update_severity(), Some(UpdateSeverity::Patch));
195
196 let check = GlobalCheck {
197 package: pkg,
198 latest: Version::from_str("1.0.0").expect("valid version"),
199 has_update: false,
200 };
201 assert_eq!(check.update_severity(), None);
202 }
203
204 #[test]
205 fn test_generate_upgrade_commands() {
206 let checks = vec![
207 GlobalCheck {
208 package: GlobalPackage {
209 name: "typescript".to_string(),
210 installed_version: Version::from_str("5.4.5").expect("valid version"),
211 source: GlobalSource::Npm,
212 },
213 latest: Version::from_str("5.6.3").expect("valid version"),
214 has_update: true,
215 },
216 GlobalCheck {
217 package: GlobalPackage {
218 name: "prettier".to_string(),
219 installed_version: Version::from_str("3.2.5").expect("valid version"),
220 source: GlobalSource::Npm,
221 },
222 latest: Version::from_str("3.2.5").expect("valid version"),
223 has_update: false,
224 },
225 ];
226
227 let commands = generate_upgrade_commands(&checks);
228 assert_eq!(commands.len(), 1);
229 assert_eq!(commands[0], "npm install -g typescript");
230 }
231
232 #[test]
233 fn test_generate_upgrade_commands_none_outdated() {
234 let checks = vec![GlobalCheck {
235 package: GlobalPackage {
236 name: "typescript".to_string(),
237 installed_version: Version::from_str("5.6.3").expect("valid version"),
238 source: GlobalSource::Npm,
239 },
240 latest: Version::from_str("5.6.3").expect("valid version"),
241 has_update: false,
242 }];
243
244 let commands = generate_upgrade_commands(&checks);
245 assert!(commands.is_empty());
246 }
247}