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 common;
33
34pub mod continue_dev;
36
37pub mod gemini;
39
40pub mod kilo_code;
42
43pub mod opencode;
45
46pub mod roo_code;
48
49pub mod vscode_extension;
51
52#[cfg(test)]
54pub mod test_common;
55
56#[derive(Debug, Clone)]
61pub struct WatcherInfo {
62 pub name: &'static str,
64
65 #[allow(dead_code)]
67 pub description: &'static str,
68
69 #[allow(dead_code)]
71 pub default_paths: Vec<PathBuf>,
72}
73
74pub trait Watcher: Send + Sync {
91 fn info(&self) -> WatcherInfo;
93
94 fn is_available(&self) -> bool;
99
100 fn find_sources(&self) -> Result<Vec<PathBuf>>;
105
106 fn parse_source(&self, path: &Path) -> Result<Vec<(Session, Vec<Message>)>>;
111
112 fn watch_paths(&self) -> Vec<PathBuf>;
116}
117
118pub struct WatcherRegistry {
123 watchers: Vec<Box<dyn Watcher>>,
124}
125
126impl Default for WatcherRegistry {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl WatcherRegistry {
133 pub fn new() -> Self {
135 Self {
136 watchers: Vec::new(),
137 }
138 }
139
140 pub fn register(&mut self, watcher: Box<dyn Watcher>) {
142 self.watchers.push(watcher);
143 }
144
145 pub fn all_watchers(&self) -> Vec<&dyn Watcher> {
147 self.watchers.iter().map(|w| w.as_ref()).collect()
148 }
149
150 pub fn available_watchers(&self) -> Vec<&dyn Watcher> {
155 self.watchers
156 .iter()
157 .filter(|w| w.is_available())
158 .map(|w| w.as_ref())
159 .collect()
160 }
161
162 pub fn enabled_watchers(&self, enabled_watchers: &[String]) -> Vec<&dyn Watcher> {
170 self.watchers
171 .iter()
172 .filter(|w| {
173 w.is_available() && enabled_watchers.iter().any(|name| name == w.info().name)
174 })
175 .map(|w| w.as_ref())
176 .collect()
177 }
178
179 #[allow(dead_code)]
183 pub fn get_watcher(&self, name: &str) -> Option<&dyn Watcher> {
184 self.watchers
185 .iter()
186 .find(|w| w.info().name == name)
187 .map(|w| w.as_ref())
188 }
189
190 pub fn all_watch_paths(&self) -> Vec<PathBuf> {
194 self.available_watchers()
195 .iter()
196 .flat_map(|w| w.watch_paths())
197 .collect()
198 }
199}
200
201pub fn default_registry() -> WatcherRegistry {
215 let mut registry = WatcherRegistry::new();
216 registry.register(Box::new(aider::AiderWatcher));
217 registry.register(Box::new(amp::AmpWatcher));
218 registry.register(Box::new(claude_code::ClaudeCodeWatcher));
219 registry.register(Box::new(cline::new_watcher()));
220 registry.register(Box::new(codex::CodexWatcher));
221 registry.register(Box::new(continue_dev::ContinueDevWatcher));
222 registry.register(Box::new(gemini::GeminiWatcher));
223 registry.register(Box::new(kilo_code::new_watcher()));
224 registry.register(Box::new(opencode::OpenCodeWatcher));
225 registry.register(Box::new(roo_code::new_watcher()));
226 registry
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 struct TestWatcher {
235 name: &'static str,
236 available: bool,
237 }
238
239 impl Watcher for TestWatcher {
240 fn info(&self) -> WatcherInfo {
241 WatcherInfo {
242 name: self.name,
243 description: "Test watcher",
244 default_paths: vec![PathBuf::from("/test")],
245 }
246 }
247
248 fn is_available(&self) -> bool {
249 self.available
250 }
251
252 fn find_sources(&self) -> Result<Vec<PathBuf>> {
253 Ok(vec![])
254 }
255
256 fn parse_source(&self, _path: &Path) -> Result<Vec<(Session, Vec<Message>)>> {
257 Ok(vec![])
258 }
259
260 fn watch_paths(&self) -> Vec<PathBuf> {
261 vec![PathBuf::from("/test")]
262 }
263 }
264
265 #[test]
266 fn test_registry_new_is_empty() {
267 let registry = WatcherRegistry::new();
268 assert!(registry.all_watchers().is_empty());
269 }
270
271 #[test]
272 fn test_registry_register_and_retrieve() {
273 let mut registry = WatcherRegistry::new();
274 registry.register(Box::new(TestWatcher {
275 name: "test-watcher",
276 available: true,
277 }));
278
279 assert_eq!(registry.all_watchers().len(), 1);
280 assert!(registry.get_watcher("test-watcher").is_some());
281 assert!(registry.get_watcher("nonexistent").is_none());
282 }
283
284 #[test]
285 fn test_registry_available_watchers_filters() {
286 let mut registry = WatcherRegistry::new();
287 registry.register(Box::new(TestWatcher {
288 name: "available",
289 available: true,
290 }));
291 registry.register(Box::new(TestWatcher {
292 name: "unavailable",
293 available: false,
294 }));
295
296 assert_eq!(registry.all_watchers().len(), 2);
297 assert_eq!(registry.available_watchers().len(), 1);
298 assert_eq!(registry.available_watchers()[0].info().name, "available");
299 }
300
301 #[test]
302 fn test_registry_all_watch_paths() {
303 let mut registry = WatcherRegistry::new();
304 registry.register(Box::new(TestWatcher {
305 name: "watcher1",
306 available: true,
307 }));
308 registry.register(Box::new(TestWatcher {
309 name: "watcher2",
310 available: true,
311 }));
312 registry.register(Box::new(TestWatcher {
313 name: "watcher3",
314 available: false,
315 }));
316
317 let paths = registry.all_watch_paths();
318 assert_eq!(paths.len(), 2);
320 }
321
322 #[test]
323 fn test_default_registry_contains_builtin_watchers() {
324 let registry = default_registry();
325 let watchers = registry.all_watchers();
326
327 assert!(watchers.len() >= 10);
329
330 assert!(registry.get_watcher("aider").is_some());
332 assert!(registry.get_watcher("amp").is_some());
333 assert!(registry.get_watcher("claude-code").is_some());
334 assert!(registry.get_watcher("cline").is_some());
335 assert!(registry.get_watcher("codex").is_some());
336 assert!(registry.get_watcher("continue").is_some());
337 assert!(registry.get_watcher("gemini").is_some());
338 assert!(registry.get_watcher("kilo-code").is_some());
339 assert!(registry.get_watcher("opencode").is_some());
340 assert!(registry.get_watcher("roo-code").is_some());
341 }
342
343 #[test]
344 fn test_watcher_info_fields() {
345 let watcher = TestWatcher {
346 name: "test",
347 available: true,
348 };
349 let info = watcher.info();
350
351 assert_eq!(info.name, "test");
352 assert_eq!(info.description, "Test watcher");
353 assert!(!info.default_paths.is_empty());
354 }
355
356 #[test]
357 fn test_registry_enabled_watchers() {
358 let mut registry = WatcherRegistry::new();
359 registry.register(Box::new(TestWatcher {
360 name: "watcher-a",
361 available: true,
362 }));
363 registry.register(Box::new(TestWatcher {
364 name: "watcher-b",
365 available: true,
366 }));
367 registry.register(Box::new(TestWatcher {
368 name: "watcher-c",
369 available: false,
370 }));
371
372 let enabled = vec!["watcher-a".to_string(), "watcher-c".to_string()];
374 let watchers = registry.enabled_watchers(&enabled);
375
376 assert_eq!(watchers.len(), 1);
380 assert_eq!(watchers[0].info().name, "watcher-a");
381 }
382
383 #[test]
384 fn test_registry_enabled_watchers_empty_list() {
385 let mut registry = WatcherRegistry::new();
386 registry.register(Box::new(TestWatcher {
387 name: "watcher",
388 available: true,
389 }));
390
391 let enabled: Vec<String> = vec![];
393 let watchers = registry.enabled_watchers(&enabled);
394 assert!(watchers.is_empty());
395 }
396}