dot_agent_core/channel/
hub_registry.rs

1//! Hub registry management
2//!
3//! Manages the list of registered Hubs in ~/.dot-agent/hubs.toml
4
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{DotAgentError, Result};
11
12use super::types::Hub;
13
14/// Hub registry configuration
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct HubRegistry {
17    /// Registered hubs
18    pub hubs: Vec<Hub>,
19}
20
21impl HubRegistry {
22    const FILENAME: &'static str = "hubs.toml";
23
24    /// Load hub registry from base directory
25    pub fn load(base_dir: &Path) -> Result<Self> {
26        let path = base_dir.join(Self::FILENAME);
27        if !path.exists() {
28            // Return default with official hub
29            return Ok(Self::with_official());
30        }
31
32        let content = fs::read_to_string(&path)?;
33        let mut registry: Self =
34            toml::from_str(&content).map_err(|e| DotAgentError::ConfigParseSimple {
35                message: e.to_string(),
36            })?;
37
38        // Ensure official hub is always present
39        if !registry.hubs.iter().any(|h| h.is_default) {
40            registry.hubs.insert(0, Hub::official());
41        }
42
43        Ok(registry)
44    }
45
46    /// Create registry with official hub
47    pub fn with_official() -> Self {
48        Self {
49            hubs: vec![Hub::official()],
50        }
51    }
52
53    /// Save hub registry to base directory
54    pub fn save(&self, base_dir: &Path) -> Result<()> {
55        let path = base_dir.join(Self::FILENAME);
56        fs::create_dir_all(base_dir)?;
57        let content =
58            toml::to_string_pretty(self).map_err(|e| DotAgentError::ConfigParseSimple {
59                message: e.to_string(),
60            })?;
61        fs::write(path, content)?;
62        Ok(())
63    }
64
65    /// Add a hub
66    pub fn add(&mut self, hub: Hub) -> Result<()> {
67        if self.hubs.iter().any(|h| h.name == hub.name) {
68            return Err(DotAgentError::HubAlreadyExists { name: hub.name });
69        }
70        self.hubs.push(hub);
71        Ok(())
72    }
73
74    /// Remove a hub by name (cannot remove default hub)
75    pub fn remove(&mut self, name: &str) -> Result<Hub> {
76        let idx = self
77            .hubs
78            .iter()
79            .position(|h| h.name == name)
80            .ok_or_else(|| DotAgentError::HubNotFound {
81                name: name.to_string(),
82            })?;
83
84        if self.hubs[idx].is_default {
85            return Err(DotAgentError::CannotRemoveDefaultHub);
86        }
87
88        Ok(self.hubs.remove(idx))
89    }
90
91    /// Get a hub by name
92    pub fn get(&self, name: &str) -> Option<&Hub> {
93        self.hubs.iter().find(|h| h.name == name)
94    }
95
96    /// Get the default hub
97    pub fn default_hub(&self) -> Option<&Hub> {
98        self.hubs.iter().find(|h| h.is_default)
99    }
100
101    /// List all hubs
102    pub fn list(&self) -> &[Hub] {
103        &self.hubs
104    }
105
106    /// Get cache directory for a hub
107    pub fn cache_dir(base_dir: &Path, hub_name: &str) -> PathBuf {
108        base_dir.join("cache").join("hubs").join(hub_name)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use tempfile::TempDir;
116
117    #[test]
118    fn registry_with_official() {
119        let registry = HubRegistry::with_official();
120        assert_eq!(registry.hubs.len(), 1);
121        assert!(registry.hubs[0].is_default);
122        assert_eq!(registry.hubs[0].name, "official");
123    }
124
125    #[test]
126    fn registry_add_remove() {
127        let mut registry = HubRegistry::with_official();
128
129        let hub = Hub::new("company", "https://github.com/company/hub");
130        registry.add(hub).unwrap();
131        assert_eq!(registry.hubs.len(), 2);
132
133        // Duplicate should fail
134        let hub2 = Hub::new("company", "https://github.com/other/hub");
135        assert!(registry.add(hub2).is_err());
136
137        // Remove
138        let removed = registry.remove("company").unwrap();
139        assert_eq!(removed.name, "company");
140        assert_eq!(registry.hubs.len(), 1);
141
142        // Cannot remove default
143        assert!(registry.remove("official").is_err());
144    }
145
146    #[test]
147    fn registry_save_load() {
148        let temp = TempDir::new().unwrap();
149        let base = temp.path();
150
151        let mut registry = HubRegistry::with_official();
152        registry
153            .add(Hub::new("test", "https://github.com/test/hub"))
154            .unwrap();
155        registry.save(base).unwrap();
156
157        let loaded = HubRegistry::load(base).unwrap();
158        assert_eq!(loaded.hubs.len(), 2);
159        assert!(loaded.get("official").is_some());
160        assert!(loaded.get("test").is_some());
161    }
162}