Skip to main content

robonix_cli/
database.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// Package database for system-installed packages (~/.robonix/packages)
3
4use 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}