dot_agent_core/channel/
channel_registry.rs1use std::fs;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{DotAgentError, Result};
11
12use super::types::{Channel, ChannelType};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ChannelRegistry {
17 pub channels: Vec<Channel>,
19}
20
21impl Default for ChannelRegistry {
22 fn default() -> Self {
23 Self::with_defaults()
24 }
25}
26
27impl ChannelRegistry {
28 const FILENAME: &'static str = "channels.toml";
29
30 pub fn with_defaults() -> Self {
32 Self {
33 channels: vec![Channel::github_global(), Channel::codex_skills()],
34 }
35 }
36
37 pub fn load(base_dir: &Path) -> Result<Self> {
39 let path = base_dir.join(Self::FILENAME);
40 if !path.exists() {
41 return Ok(Self::with_defaults());
42 }
43
44 let content = fs::read_to_string(&path)?;
45 let mut registry: Self =
46 toml::from_str(&content).map_err(|e| DotAgentError::ConfigParseSimple {
47 message: e.to_string(),
48 })?;
49
50 if !registry
52 .channels
53 .iter()
54 .any(|c| c.builtin && c.name == "github")
55 {
56 registry.channels.insert(0, Channel::github_global());
57 }
58 if !registry
59 .channels
60 .iter()
61 .any(|c| c.builtin && c.name == "codex")
62 {
63 registry.channels.push(Channel::codex_skills());
64 }
65
66 Ok(registry)
67 }
68
69 pub fn save(&self, base_dir: &Path) -> Result<()> {
71 let path = base_dir.join(Self::FILENAME);
72 fs::create_dir_all(base_dir)?;
73 let content =
74 toml::to_string_pretty(self).map_err(|e| DotAgentError::ConfigParseSimple {
75 message: e.to_string(),
76 })?;
77 fs::write(path, content)?;
78 Ok(())
79 }
80
81 pub fn add(&mut self, channel: Channel) -> Result<()> {
83 if self.channels.iter().any(|c| c.name == channel.name) {
84 return Err(DotAgentError::ChannelAlreadyExists { name: channel.name });
85 }
86 self.channels.push(channel);
87 Ok(())
88 }
89
90 pub fn remove(&mut self, name: &str) -> Result<Channel> {
92 let idx = self
93 .channels
94 .iter()
95 .position(|c| c.name == name)
96 .ok_or_else(|| DotAgentError::ChannelNotFound {
97 name: name.to_string(),
98 })?;
99
100 if self.channels[idx].builtin {
101 return Err(DotAgentError::CannotRemoveBuiltinChannel {
102 name: name.to_string(),
103 });
104 }
105
106 Ok(self.channels.remove(idx))
107 }
108
109 pub fn get(&self, name: &str) -> Option<&Channel> {
111 self.channels.iter().find(|c| c.name == name)
112 }
113
114 pub fn get_mut(&mut self, name: &str) -> Option<&mut Channel> {
116 self.channels.iter_mut().find(|c| c.name == name)
117 }
118
119 pub fn list(&self) -> &[Channel] {
121 &self.channels
122 }
123
124 pub fn list_enabled(&self) -> Vec<&Channel> {
126 self.channels.iter().filter(|c| c.enabled).collect()
127 }
128
129 pub fn list_searchable(&self) -> Vec<&Channel> {
131 self.channels
132 .iter()
133 .filter(|c| c.enabled && c.is_searchable())
134 .collect()
135 }
136
137 pub fn list_by_type(&self, channel_type: ChannelType) -> Vec<&Channel> {
139 self.channels
140 .iter()
141 .filter(|c| c.channel_type == channel_type)
142 .collect()
143 }
144
145 pub fn enable(&mut self, name: &str) -> Result<()> {
147 let channel = self
148 .get_mut(name)
149 .ok_or_else(|| DotAgentError::ChannelNotFound {
150 name: name.to_string(),
151 })?;
152 channel.enabled = true;
153 Ok(())
154 }
155
156 pub fn disable(&mut self, name: &str) -> Result<()> {
158 let channel = self
159 .get_mut(name)
160 .ok_or_else(|| DotAgentError::ChannelNotFound {
161 name: name.to_string(),
162 })?;
163 channel.enabled = false;
164 Ok(())
165 }
166
167 pub fn cache_dir(base_dir: &Path, channel_name: &str) -> PathBuf {
169 base_dir.join("cache").join("channels").join(channel_name)
170 }
171
172 pub fn contains(&self, name: &str) -> bool {
174 self.channels.iter().any(|c| c.name == name)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use tempfile::TempDir;
182
183 #[test]
184 fn registry_with_defaults() {
185 let registry = ChannelRegistry::with_defaults();
186 assert_eq!(registry.channels.len(), 2);
187 assert_eq!(registry.channels[0].name, "github");
188 assert!(registry.channels[0].builtin);
189 assert_eq!(registry.channels[1].name, "codex");
190 assert!(registry.channels[1].builtin);
191 }
192
193 #[test]
194 fn registry_add_remove() {
195 let mut registry = ChannelRegistry::with_defaults();
196 let initial_count = registry.channels.len(); let channel = Channel::from_url("test", "https://github.com/test/awesome");
199 registry.add(channel).unwrap();
200 assert_eq!(registry.channels.len(), initial_count + 1);
201
202 let channel2 = Channel::from_url("test", "https://github.com/other/awesome");
204 assert!(registry.add(channel2).is_err());
205
206 let removed = registry.remove("test").unwrap();
208 assert_eq!(removed.name, "test");
209 assert_eq!(registry.channels.len(), initial_count);
210
211 assert!(registry.remove("github").is_err());
213 assert!(registry.remove("codex").is_err());
214 }
215
216 #[test]
217 fn registry_enable_disable() {
218 let mut registry = ChannelRegistry::with_defaults();
219 registry
220 .add(Channel::from_url("test", "https://example.com"))
221 .unwrap();
222
223 assert!(registry.get("test").unwrap().enabled);
224
225 registry.disable("test").unwrap();
226 assert!(!registry.get("test").unwrap().enabled);
227
228 registry.enable("test").unwrap();
229 assert!(registry.get("test").unwrap().enabled);
230 }
231
232 #[test]
233 fn registry_save_load() {
234 let temp = TempDir::new().unwrap();
235 let base = temp.path();
236
237 let mut registry = ChannelRegistry::with_defaults();
238 let initial_count = registry.channels.len(); registry
240 .add(Channel::from_url("test", "https://github.com/test/awesome"))
241 .unwrap();
242 registry.save(base).unwrap();
243
244 let loaded = ChannelRegistry::load(base).unwrap();
245 assert_eq!(loaded.channels.len(), initial_count + 1);
246 assert!(loaded.get("github").is_some());
247 assert!(loaded.get("codex").is_some());
248 assert!(loaded.get("test").is_some());
249 }
250
251 #[test]
252 fn list_searchable() {
253 let mut registry = ChannelRegistry::with_defaults();
254 registry
255 .add(Channel::from_url(
256 "awesome1",
257 "https://github.com/test/awesome-list",
258 ))
259 .unwrap();
260 registry
261 .add(Channel::from_url(
262 "direct1",
263 "https://github.com/test/dotfiles",
264 ))
265 .unwrap();
266
267 let searchable = registry.list_searchable();
268 assert_eq!(searchable.len(), 3);
270 }
271
272 #[test]
273 fn list_by_type() {
274 let mut registry = ChannelRegistry::with_defaults();
275 registry
276 .add(Channel::from_url(
277 "awesome1",
278 "https://github.com/test/awesome-list",
279 ))
280 .unwrap();
281 registry
282 .add(Channel::from_hub("hub1", "official", "test"))
283 .unwrap();
284
285 let github = registry.list_by_type(ChannelType::GitHubGlobal);
286 assert_eq!(github.len(), 1);
287
288 let awesome = registry.list_by_type(ChannelType::AwesomeList);
289 assert_eq!(awesome.len(), 1);
290
291 let hub = registry.list_by_type(ChannelType::Hub);
292 assert_eq!(hub.len(), 1);
293 }
294}