1use anyhow::{anyhow, Context, Result};
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::fmt;
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Version {
17 pub major: u64,
18 pub minor: u64,
19 pub patch: u64,
20 pub pre_release: Option<String>,
21 pub build: Option<String>,
22}
23
24impl Version {
25 pub fn new(major: u64, minor: u64, patch: u64) -> Self {
27 Self {
28 major,
29 minor,
30 patch,
31 pre_release: None,
32 build: None,
33 }
34 }
35
36 pub fn is_compatible_with(&self, other: &Version) -> bool {
38 self.major == other.major && self.major > 0
39 }
40
41 pub fn satisfies(&self, req: &VersionReq) -> bool {
43 match req {
44 VersionReq::Exact(v) => self == v,
45 VersionReq::GreaterThan(v) => self > v,
46 VersionReq::GreaterOrEqual(v) => self >= v,
47 VersionReq::LessThan(v) => self < v,
48 VersionReq::LessOrEqual(v) => self <= v,
49 VersionReq::Compatible(v) => self.is_compatible_with(v) && self >= v,
50 VersionReq::Any => true,
51 }
52 }
53
54 pub fn is_prerelease(&self) -> bool {
56 self.pre_release.is_some()
57 }
58
59 pub fn is_stable(&self) -> bool {
61 self.major >= 1 && !self.is_prerelease()
62 }
63}
64
65impl fmt::Display for Version {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
68 if let Some(pre) = &self.pre_release {
69 write!(f, "-{}", pre)?;
70 }
71 if let Some(build) = &self.build {
72 write!(f, "+{}", build)?;
73 }
74 Ok(())
75 }
76}
77
78impl FromStr for Version {
79 type Err = anyhow::Error;
80
81 fn from_str(s: &str) -> Result<Self> {
82 let s = s.strip_prefix('v').unwrap_or(s);
84
85 let (version_pre, build) = match s.split_once('+') {
87 Some((v, b)) => (v, Some(b.to_string())),
88 None => (s, None),
89 };
90
91 let (version, pre_release) = match version_pre.split_once('-') {
93 Some((v, p)) => (v, Some(p.to_string())),
94 None => (version_pre, None),
95 };
96
97 let parts: Vec<&str> = version.split('.').collect();
99 if parts.len() != 3 {
100 return Err(anyhow!("Invalid version format: {}", s));
101 }
102
103 let major = parts[0]
104 .parse()
105 .context("Failed to parse major version")?;
106 let minor = parts[1]
107 .parse()
108 .context("Failed to parse minor version")?;
109 let patch = parts[2]
110 .parse()
111 .context("Failed to parse patch version")?;
112
113 Ok(Self {
114 major,
115 minor,
116 patch,
117 pre_release,
118 build,
119 })
120 }
121}
122
123impl PartialOrd for Version {
124 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
125 Some(self.cmp(other))
126 }
127}
128
129impl Ord for Version {
130 fn cmp(&self, other: &Self) -> Ordering {
131 match self.major.cmp(&other.major) {
133 Ordering::Equal => {}
134 ord => return ord,
135 }
136 match self.minor.cmp(&other.minor) {
137 Ordering::Equal => {}
138 ord => return ord,
139 }
140 match self.patch.cmp(&other.patch) {
141 Ordering::Equal => {}
142 ord => return ord,
143 }
144
145 match (&self.pre_release, &other.pre_release) {
147 (None, None) => Ordering::Equal,
148 (Some(_), None) => Ordering::Less,
149 (None, Some(_)) => Ordering::Greater,
150 (Some(a), Some(b)) => a.cmp(b),
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub enum VersionReq {
158 Exact(Version),
160 GreaterThan(Version),
162 GreaterOrEqual(Version),
164 LessThan(Version),
166 LessOrEqual(Version),
168 Compatible(Version),
170 Any,
172}
173
174impl fmt::Display for VersionReq {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 VersionReq::Exact(v) => write!(f, "={}", v),
178 VersionReq::GreaterThan(v) => write!(f, ">{}", v),
179 VersionReq::GreaterOrEqual(v) => write!(f, ">={}", v),
180 VersionReq::LessThan(v) => write!(f, "<{}", v),
181 VersionReq::LessOrEqual(v) => write!(f, "<={}", v),
182 VersionReq::Compatible(v) => write!(f, "^{}", v),
183 VersionReq::Any => write!(f, "*"),
184 }
185 }
186}
187
188impl FromStr for VersionReq {
189 type Err = anyhow::Error;
190
191 fn from_str(s: &str) -> Result<Self> {
192 let s = s.trim();
193
194 if s == "*" {
195 return Ok(VersionReq::Any);
196 }
197
198 if let Some(v) = s.strip_prefix(">=") {
199 return Ok(VersionReq::GreaterOrEqual(v.trim().parse()?));
200 }
201 if let Some(v) = s.strip_prefix('>') {
202 return Ok(VersionReq::GreaterThan(v.trim().parse()?));
203 }
204 if let Some(v) = s.strip_prefix("<=") {
205 return Ok(VersionReq::LessOrEqual(v.trim().parse()?));
206 }
207 if let Some(v) = s.strip_prefix('<') {
208 return Ok(VersionReq::LessThan(v.trim().parse()?));
209 }
210 if let Some(v) = s.strip_prefix('=') {
211 return Ok(VersionReq::Exact(v.trim().parse()?));
212 }
213 if let Some(v) = s.strip_prefix('^') {
214 return Ok(VersionReq::Compatible(v.trim().parse()?));
215 }
216
217 Ok(VersionReq::Compatible(s.parse()?))
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ToolInfo {
225 pub name: String,
226 pub version: Version,
227 pub installed_at: chrono::DateTime<chrono::Utc>,
228 pub source: ToolSource,
229 pub dependencies: HashMap<String, VersionReq>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum ToolSource {
235 Local(PathBuf),
237 Crate { version: String },
239 Git { url: String, rev: String },
241 R2 { bucket: String, key: String },
243}
244
245pub struct ToolRegistry {
249 registry_path: PathBuf,
250 tools: HashMap<String, ToolInfo>,
251}
252
253impl ToolRegistry {
254 pub fn new(forge_dir: &Path) -> Result<Self> {
256 let registry_path = forge_dir.join("tool_registry.json");
257
258 let tools = if registry_path.exists() {
259 let content = std::fs::read_to_string(®istry_path)
260 .context("Failed to read tool registry")?;
261 serde_json::from_str(&content).unwrap_or_default()
262 } else {
263 HashMap::new()
264 };
265
266 Ok(Self {
267 registry_path,
268 tools,
269 })
270 }
271
272 pub fn register(
274 &mut self,
275 name: String,
276 version: Version,
277 source: ToolSource,
278 dependencies: HashMap<String, VersionReq>,
279 ) -> Result<()> {
280 let info = ToolInfo {
281 name: name.clone(),
282 version,
283 installed_at: chrono::Utc::now(),
284 source,
285 dependencies,
286 };
287
288 self.tools.insert(name, info);
289 self.save()?;
290
291 Ok(())
292 }
293
294 pub fn get(&self, name: &str) -> Option<&ToolInfo> {
296 self.tools.get(name)
297 }
298
299 pub fn is_registered(&self, name: &str) -> bool {
301 self.tools.contains_key(name)
302 }
303
304 pub fn version(&self, name: &str) -> Option<&Version> {
306 self.tools.get(name).map(|info| &info.version)
307 }
308
309 pub fn check_dependencies(&self, tool_name: &str) -> Result<Vec<String>> {
311 let mut missing = Vec::new();
312
313 if let Some(info) = self.tools.get(tool_name) {
314 for (dep_name, req) in &info.dependencies {
315 match self.tools.get(dep_name) {
316 Some(dep_info) => {
317 if !dep_info.version.satisfies(req) {
318 missing.push(format!(
319 "{} requires {} {}, but {} is installed",
320 tool_name, dep_name, req, dep_info.version
321 ));
322 }
323 }
324 None => {
325 missing.push(format!("{} requires {} {}", tool_name, dep_name, req));
326 }
327 }
328 }
329 }
330
331 Ok(missing)
332 }
333
334 pub fn list(&self) -> Vec<&ToolInfo> {
336 self.tools.values().collect()
337 }
338
339 pub fn unregister(&mut self, name: &str) -> Result<()> {
341 self.tools.remove(name);
342 self.save()?;
343 Ok(())
344 }
345
346 pub fn needs_update(&self, name: &str, latest: &Version) -> bool {
348 if let Some(info) = self.tools.get(name) {
349 &info.version < latest
350 } else {
351 false
352 }
353 }
354
355 fn save(&self) -> Result<()> {
357 let content = serde_json::to_string_pretty(&self.tools)?;
358 std::fs::write(&self.registry_path, content)?;
359 Ok(())
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_version_parsing() {
369 let v = "1.2.3".parse::<Version>().unwrap();
370 assert_eq!(v.major, 1);
371 assert_eq!(v.minor, 2);
372 assert_eq!(v.patch, 3);
373
374 let v = "v2.0.0-beta.1".parse::<Version>().unwrap();
375 assert_eq!(v.major, 2);
376 assert_eq!(v.pre_release, Some("beta.1".to_string()));
377
378 let v = "1.0.0+build.123".parse::<Version>().unwrap();
379 assert_eq!(v.build, Some("build.123".to_string()));
380 }
381
382 #[test]
383 fn test_version_comparison() {
384 let v1 = Version::new(1, 2, 3);
385 let v2 = Version::new(1, 2, 4);
386 let v3 = Version::new(2, 0, 0);
387
388 assert!(v1 < v2);
389 assert!(v2 < v3);
390 assert!(v1 < v3);
391 }
392
393 #[test]
394 fn test_version_compatibility() {
395 let v1 = Version::new(1, 2, 3);
396 let v2 = Version::new(1, 5, 0);
397 let v3 = Version::new(2, 0, 0);
398
399 assert!(v1.is_compatible_with(&v2));
400 assert!(!v1.is_compatible_with(&v3));
401 }
402
403 #[test]
404 fn test_version_requirements() {
405 let v = Version::new(1, 2, 3);
406
407 let req = "^1.0.0".parse::<VersionReq>().unwrap();
408 assert!(v.satisfies(&req));
409
410 let req = ">=1.2.0".parse::<VersionReq>().unwrap();
411 assert!(v.satisfies(&req));
412
413 let req = ">2.0.0".parse::<VersionReq>().unwrap();
414 assert!(!v.satisfies(&req));
415 }
416
417 #[test]
418 fn test_version_req_parsing() {
419 assert!(matches!(
420 "^1.2.3".parse::<VersionReq>().unwrap(),
421 VersionReq::Compatible(_)
422 ));
423 assert!(matches!(
424 ">=1.2.3".parse::<VersionReq>().unwrap(),
425 VersionReq::GreaterOrEqual(_)
426 ));
427 assert!(matches!(
428 "*".parse::<VersionReq>().unwrap(),
429 VersionReq::Any
430 ));
431 }
432}