1use anyhow::{Context, Result};
5use robonix_scribe::{info, warn};
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PackageInfo {
12 pub name: String,
13 pub version: String,
14 pub path: PathBuf,
15 pub manifest_path: PathBuf,
16 #[serde(default)]
17 pub capabilities: Vec<String>,
18 #[serde(default)]
19 pub depends: Vec<String>,
20 pub installed_at: String,
21 pub source: PackageSource,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum PackageSource {
26 Local {
27 path: PathBuf,
28 },
29 GitHub {
30 repo: String,
31 branch: Option<String>,
32 commit: String,
33 },
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PackageDatabase {
38 packages: HashMap<String, PackageInfo>,
39}
40
41impl PackageDatabase {
42 pub fn db_path(storage_path: &Path) -> PathBuf {
43 storage_path.join("db.json")
44 }
45
46 pub fn load(storage_path: &Path) -> Result<Self> {
47 let db_path = Self::db_path(storage_path);
48 if !db_path.exists() {
49 return Ok(Self {
50 packages: HashMap::new(),
51 });
52 }
53 let content = std::fs::read_to_string(&db_path)
54 .with_context(|| format!("Failed to read database: {}", db_path.display()))?;
55 let db: PackageDatabase = serde_json::from_str(&content)
56 .with_context(|| format!("Failed to parse database: {}", db_path.display()))?;
57 Ok(db)
58 }
59
60 pub fn save(&self, storage_path: &Path) -> Result<()> {
61 let db_path = Self::db_path(storage_path);
62 let content = serde_json::to_string_pretty(self).context("Failed to serialize database")?;
63 std::fs::write(&db_path, content)
64 .with_context(|| format!("Failed to write database: {}", db_path.display()))?;
65 Ok(())
66 }
67
68 pub fn add_package(&mut self, info: PackageInfo) {
69 self.packages.insert(info.name.clone(), info);
70 }
71
72 pub fn remove_package(&mut self, name: &str) -> Option<PackageInfo> {
73 self.packages.remove(name)
74 }
75
76 pub fn get_package(&self, name: &str) -> Option<&PackageInfo> {
77 self.packages.get(name)
78 }
79
80 pub fn list_packages(&self) -> Vec<&PackageInfo> {
81 let mut packages: Vec<&PackageInfo> = self.packages.values().collect();
82 packages.sort_by(|a, b| a.name.cmp(&b.name));
83 packages
84 }
85
86 pub fn find_by_name(&self, name: &str) -> Option<&PackageInfo> {
87 self.packages.get(name)
88 }
89
90 pub fn sync(storage_path: &Path) -> Result<()> {
91 use crate::install::PackageInstaller;
92
93 let mut db = Self::load(storage_path)?;
94 let mut found_packages = HashSet::new();
95
96 if storage_path.exists() {
97 for entry in std::fs::read_dir(storage_path)
98 .with_context(|| format!("Failed to read storage: {}", storage_path.display()))?
99 {
100 let entry = entry?;
101 let path = entry.path();
102 if path.file_name().and_then(|n| n.to_str()) == Some("db.json") {
103 continue;
104 }
105 if !path.is_dir() {
106 continue;
107 }
108
109 let manifest_path = match crate::manifest::detect_manifest_path(&path, None) {
110 Ok(p) => p,
111 Err(_) => continue,
112 };
113 if !manifest_path.exists() {
114 continue;
115 }
116
117 let package_name = match PackageInstaller::parse_manifest_name(&manifest_path) {
118 Ok(n) => n,
119 Err(e) => {
120 warn!(
121 "Failed to parse manifest at {}: {}",
122 manifest_path.display(),
123 e
124 );
125 continue;
126 }
127 };
128 found_packages.insert(package_name.clone());
129
130 let source = db
131 .get_package(&package_name)
132 .filter(|e| e.path == path)
133 .map(|e| e.source.clone())
134 .unwrap_or(PackageSource::Local { path: path.clone() });
135
136 let summary = match crate::manifest::load_from_path(&manifest_path)
137 .and_then(|m| m.validate_and_summarize())
138 {
139 Ok(s) => s,
140 Err(e) => {
141 warn!("Failed to parse manifest at {}: {}", path.display(), e);
142 continue;
143 }
144 };
145
146 match PackageInstaller::create_package_info(&path, &manifest_path, &summary, source)
147 {
148 Ok(info) => db.add_package(info),
149 Err(e) => {
150 warn!("Failed to create package info at {}: {}", path.display(), e)
151 }
152 }
153 }
154 }
155
156 for name in db.packages.keys().cloned().collect::<Vec<_>>() {
157 if !found_packages.contains(&name)
158 && let Some(removed) = db.remove_package(&name)
159 {
160 info!(
161 "Removed '{}' from database (not found: {})",
162 name,
163 removed.path.display()
164 );
165 }
166 }
167
168 db.save(storage_path)?;
169 Ok(())
170 }
171}