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