Skip to main content

cgx_engine/
registry.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6/// Metadata for an indexed repository stored in the global registry.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RepoEntry {
9    /// Stable SHA-256–derived ID for the repo path.
10    pub id: String,
11    pub name: String,
12    pub path: PathBuf,
13    /// Path to the DuckDB database file (`~/.cgx/repos/<id>.db`).
14    pub db_path: PathBuf,
15    pub indexed_at: String,
16    pub node_count: u64,
17    pub edge_count: u64,
18    /// Fraction of nodes per language, e.g. `{"typescript": 0.72, "rust": 0.28}`.
19    #[serde(default)]
20    pub language_breakdown: HashMap<String, f64>,
21    /// RFC3339 timestamp of the last time this repo was queried or analyzed.
22    /// Falls back to `indexed_at` for entries created before LRU tracking existed.
23    #[serde(default)]
24    pub last_used_at: Option<String>,
25}
26
27/// Global registry of all repositories indexed by cgx, persisted at `~/.cgx/registry.json`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Registry {
30    #[serde(default = "default_version")]
31    pub version: u32,
32    #[serde(default)]
33    pub repos: Vec<RepoEntry>,
34}
35
36fn default_version() -> u32 {
37    1
38}
39
40impl Registry {
41    fn path() -> PathBuf {
42        dirs::home_dir()
43            .unwrap_or_else(|| PathBuf::from("."))
44            .join(".cgx")
45            .join("registry.json")
46    }
47
48    /// Load the registry from `~/.cgx/registry.json`, creating it if absent.
49    pub fn load() -> anyhow::Result<Self> {
50        let path = Self::path();
51        if path.exists() {
52            let content = std::fs::read_to_string(&path)?;
53            let registry: Registry = serde_json::from_str(&content)?;
54            Ok(registry)
55        } else {
56            if let Some(dir) = path.parent() {
57                std::fs::create_dir_all(dir)?;
58            }
59            Ok(Registry {
60                version: 1,
61                repos: Vec::new(),
62            })
63        }
64    }
65
66    /// Persist the registry to `~/.cgx/registry.json`.
67    pub fn save(&self) -> anyhow::Result<()> {
68        let path = Self::path();
69        if let Some(dir) = path.parent() {
70            std::fs::create_dir_all(dir)?;
71        }
72        let content = serde_json::to_string_pretty(self)?;
73        std::fs::write(&path, content)?;
74        Ok(())
75    }
76
77    /// Add or replace a repo entry (matched by `id`).
78    pub fn register(&mut self, mut entry: RepoEntry) {
79        if entry.last_used_at.is_none() {
80            entry.last_used_at = Some(entry.indexed_at.clone());
81        }
82        self.repos.retain(|r| r.id != entry.id);
83        self.repos.push(entry);
84    }
85
86    /// Bump `last_used_at` on the entry whose canonical path matches `path`.
87    /// Persists the registry on success. Silent no-op if the path isn't registered.
88    pub fn touch_path(path: &Path) -> anyhow::Result<()> {
89        let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
90        let mut reg = Self::load()?;
91        let now = chrono::Utc::now().to_rfc3339();
92        let mut changed = false;
93        for r in &mut reg.repos {
94            if r.path.canonicalize().ok().as_ref() == Some(&canonical) {
95                r.last_used_at = Some(now.clone());
96                changed = true;
97                break;
98            }
99        }
100        if changed {
101            reg.save()?;
102        }
103        Ok(())
104    }
105
106    /// Look up a repo by its canonical on-disk path.
107    pub fn find_by_path(&self, path: &Path) -> Option<&RepoEntry> {
108        let canonical = path.canonicalize().ok()?;
109        self.repos
110            .iter()
111            .find(|r| r.path.canonicalize().ok().as_ref() == Some(&canonical))
112    }
113
114    /// Look up a repo by its stable SHA-derived `id`.
115    pub fn find_by_id(&self, id: &str) -> Option<&RepoEntry> {
116        self.repos.iter().find(|r| r.id == id)
117    }
118}