lore_cli/capture/watchers/
mod.rs1use anyhow::Result;
12use std::path::{Path, PathBuf};
13
14use crate::storage::models::{Message, Session};
15
16pub mod aider;
18
19pub mod amp;
21
22pub mod claude_code;
24
25pub mod cline;
27
28pub mod codex;
30
31pub mod continue_dev;
33
34pub mod gemini;
36
37pub mod kilo_code;
39
40pub mod opencode;
42
43pub mod roo_code;
45
46#[derive(Debug, Clone)]
51pub struct WatcherInfo {
52 pub name: &'static str,
54
55 #[allow(dead_code)]
57 pub description: &'static str,
58
59 #[allow(dead_code)]
61 pub default_paths: Vec<PathBuf>,
62}
63
64pub trait Watcher: Send + Sync {
81 fn info(&self) -> WatcherInfo;
83
84 fn is_available(&self) -> bool;
89
90 fn find_sources(&self) -> Result<Vec<PathBuf>>;
95
96 fn parse_source(&self, path: &Path) -> Result<Vec<(Session, Vec<Message>)>>;
101
102 fn watch_paths(&self) -> Vec<PathBuf>;
106}
107
108pub struct WatcherRegistry {
113 watchers: Vec<Box<dyn Watcher>>,
114}
115
116impl Default for WatcherRegistry {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl WatcherRegistry {
123 pub fn new() -> Self {
125 Self {
126 watchers: Vec::new(),
127 }
128 }
129
130 pub fn register(&mut self, watcher: Box<dyn Watcher>) {
132 self.watchers.push(watcher);
133 }
134
135 pub fn all_watchers(&self) -> Vec<&dyn Watcher> {
137 self.watchers.iter().map(|w| w.as_ref()).collect()
138 }
139
140 pub fn available_watchers(&self) -> Vec<&dyn Watcher> {
145 self.watchers
146 .iter()
147 .filter(|w| w.is_available())
148 .map(|w| w.as_ref())
149 .collect()
150 }
151
152 pub fn enabled_watchers(&self, enabled_watchers: &[String]) -> Vec<&dyn Watcher> {
160 self.watchers
161 .iter()
162 .filter(|w| {
163 w.is_available() && enabled_watchers.iter().any(|name| name == w.info().name)
164 })
165 .map(|w| w.as_ref())
166 .collect()
167 }
168
169 #[allow(dead_code)]
173 pub fn get_watcher(&self, name: &str) -> Option<&dyn Watcher> {
174 self.watchers
175 .iter()
176 .find(|w| w.info().name == name)
177 .map(|w| w.as_ref())
178 }
179
180 pub fn all_watch_paths(&self) -> Vec<PathBuf> {
184 self.available_watchers()
185 .iter()
186 .flat_map(|w| w.watch_paths())
187 .collect()
188 }
189}
190
191pub fn default_registry() -> WatcherRegistry {
205 let mut registry = WatcherRegistry::new();
206 registry.register(Box::new(aider::AiderWatcher));
207 registry.register(Box::new(amp::AmpWatcher));
208 registry.register(Box::new(claude_code::ClaudeCodeWatcher));
209 registry.register(Box::new(cline::ClineWatcher));
210 registry.register(Box::new(codex::CodexWatcher));
211 registry.register(Box::new(continue_dev::ContinueDevWatcher));
212 registry.register(Box::new(gemini::GeminiWatcher));
213 registry.register(Box::new(kilo_code::KiloCodeWatcher));
214 registry.register(Box::new(opencode::OpenCodeWatcher));
215 registry.register(Box::new(roo_code::RooCodeWatcher));
216 registry
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 struct TestWatcher {
225 name: &'static str,
226 available: bool,
227 }
228
229 impl Watcher for TestWatcher {
230 fn info(&self) -> WatcherInfo {
231 WatcherInfo {
232 name: self.name,
233 description: "Test watcher",
234 default_paths: vec![PathBuf::from("/test")],
235 }
236 }
237
238 fn is_available(&self) -> bool {
239 self.available
240 }
241
242 fn find_sources(&self) -> Result<Vec<PathBuf>> {
243 Ok(vec![])
244 }
245
246 fn parse_source(&self, _path: &Path) -> Result<Vec<(Session, Vec<Message>)>> {
247 Ok(vec![])
248 }
249
250 fn watch_paths(&self) -> Vec<PathBuf> {
251 vec![PathBuf::from("/test")]
252 }
253 }
254
255 #[test]
256 fn test_registry_new_is_empty() {
257 let registry = WatcherRegistry::new();
258 assert!(registry.all_watchers().is_empty());
259 }
260
261 #[test]
262 fn test_registry_register_and_retrieve() {
263 let mut registry = WatcherRegistry::new();
264 registry.register(Box::new(TestWatcher {
265 name: "test-watcher",
266 available: true,
267 }));
268
269 assert_eq!(registry.all_watchers().len(), 1);
270 assert!(registry.get_watcher("test-watcher").is_some());
271 assert!(registry.get_watcher("nonexistent").is_none());
272 }
273
274 #[test]
275 fn test_registry_available_watchers_filters() {
276 let mut registry = WatcherRegistry::new();
277 registry.register(Box::new(TestWatcher {
278 name: "available",
279 available: true,
280 }));
281 registry.register(Box::new(TestWatcher {
282 name: "unavailable",
283 available: false,
284 }));
285
286 assert_eq!(registry.all_watchers().len(), 2);
287 assert_eq!(registry.available_watchers().len(), 1);
288 assert_eq!(registry.available_watchers()[0].info().name, "available");
289 }
290
291 #[test]
292 fn test_registry_all_watch_paths() {
293 let mut registry = WatcherRegistry::new();
294 registry.register(Box::new(TestWatcher {
295 name: "watcher1",
296 available: true,
297 }));
298 registry.register(Box::new(TestWatcher {
299 name: "watcher2",
300 available: true,
301 }));
302 registry.register(Box::new(TestWatcher {
303 name: "watcher3",
304 available: false,
305 }));
306
307 let paths = registry.all_watch_paths();
308 assert_eq!(paths.len(), 2);
310 }
311
312 #[test]
313 fn test_default_registry_contains_builtin_watchers() {
314 let registry = default_registry();
315 let watchers = registry.all_watchers();
316
317 assert!(watchers.len() >= 10);
319
320 assert!(registry.get_watcher("aider").is_some());
322 assert!(registry.get_watcher("amp").is_some());
323 assert!(registry.get_watcher("claude-code").is_some());
324 assert!(registry.get_watcher("cline").is_some());
325 assert!(registry.get_watcher("codex").is_some());
326 assert!(registry.get_watcher("continue").is_some());
327 assert!(registry.get_watcher("gemini").is_some());
328 assert!(registry.get_watcher("kilo-code").is_some());
329 assert!(registry.get_watcher("opencode").is_some());
330 assert!(registry.get_watcher("roo-code").is_some());
331 }
332
333 #[test]
334 fn test_watcher_info_fields() {
335 let watcher = TestWatcher {
336 name: "test",
337 available: true,
338 };
339 let info = watcher.info();
340
341 assert_eq!(info.name, "test");
342 assert_eq!(info.description, "Test watcher");
343 assert!(!info.default_paths.is_empty());
344 }
345
346 #[test]
347 fn test_registry_enabled_watchers() {
348 let mut registry = WatcherRegistry::new();
349 registry.register(Box::new(TestWatcher {
350 name: "watcher-a",
351 available: true,
352 }));
353 registry.register(Box::new(TestWatcher {
354 name: "watcher-b",
355 available: true,
356 }));
357 registry.register(Box::new(TestWatcher {
358 name: "watcher-c",
359 available: false,
360 }));
361
362 let enabled = vec!["watcher-a".to_string(), "watcher-c".to_string()];
364 let watchers = registry.enabled_watchers(&enabled);
365
366 assert_eq!(watchers.len(), 1);
370 assert_eq!(watchers[0].info().name, "watcher-a");
371 }
372
373 #[test]
374 fn test_registry_enabled_watchers_empty_list() {
375 let mut registry = WatcherRegistry::new();
376 registry.register(Box::new(TestWatcher {
377 name: "watcher",
378 available: true,
379 }));
380
381 let enabled: Vec<String> = vec![];
383 let watchers = registry.enabled_watchers(&enabled);
384 assert!(watchers.is_empty());
385 }
386}