use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::storage::models::{Message, Session};
pub mod aider;
pub mod amp;
pub mod claude_code;
pub mod cline;
pub mod codex;
pub mod common;
pub mod continue_dev;
pub mod gemini;
pub mod kilo_code;
pub mod opencode;
pub mod roo_code;
pub mod vscode_extension;
#[cfg(test)]
pub mod test_common;
#[derive(Debug, Clone)]
pub struct WatcherInfo {
pub name: &'static str,
#[allow(dead_code)]
pub description: &'static str,
#[allow(dead_code)]
pub default_paths: Vec<PathBuf>,
}
pub trait Watcher: Send + Sync {
fn info(&self) -> WatcherInfo;
fn is_available(&self) -> bool;
fn find_sources(&self) -> Result<Vec<PathBuf>>;
fn parse_source(&self, path: &Path) -> Result<Vec<(Session, Vec<Message>)>>;
fn watch_paths(&self) -> Vec<PathBuf>;
}
pub struct WatcherRegistry {
watchers: Vec<Box<dyn Watcher>>,
}
impl Default for WatcherRegistry {
fn default() -> Self {
Self::new()
}
}
impl WatcherRegistry {
pub fn new() -> Self {
Self {
watchers: Vec::new(),
}
}
pub fn register(&mut self, watcher: Box<dyn Watcher>) {
self.watchers.push(watcher);
}
pub fn all_watchers(&self) -> Vec<&dyn Watcher> {
self.watchers.iter().map(|w| w.as_ref()).collect()
}
pub fn available_watchers(&self) -> Vec<&dyn Watcher> {
self.watchers
.iter()
.filter(|w| w.is_available())
.map(|w| w.as_ref())
.collect()
}
pub fn enabled_watchers(&self, enabled_watchers: &[String]) -> Vec<&dyn Watcher> {
self.watchers
.iter()
.filter(|w| {
w.is_available() && enabled_watchers.iter().any(|name| name == w.info().name)
})
.map(|w| w.as_ref())
.collect()
}
#[allow(dead_code)]
pub fn get_watcher(&self, name: &str) -> Option<&dyn Watcher> {
self.watchers
.iter()
.find(|w| w.info().name == name)
.map(|w| w.as_ref())
}
pub fn all_watch_paths(&self) -> Vec<PathBuf> {
self.available_watchers()
.iter()
.flat_map(|w| w.watch_paths())
.collect()
}
}
pub fn default_registry() -> WatcherRegistry {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(aider::AiderWatcher));
registry.register(Box::new(amp::AmpWatcher));
registry.register(Box::new(claude_code::ClaudeCodeWatcher));
registry.register(Box::new(cline::new_watcher()));
registry.register(Box::new(codex::CodexWatcher));
registry.register(Box::new(continue_dev::ContinueDevWatcher));
registry.register(Box::new(gemini::GeminiWatcher));
registry.register(Box::new(kilo_code::new_watcher()));
registry.register(Box::new(opencode::OpenCodeWatcher));
registry.register(Box::new(roo_code::new_watcher()));
registry
}
#[cfg(test)]
mod tests {
use super::*;
struct TestWatcher {
name: &'static str,
available: bool,
}
impl Watcher for TestWatcher {
fn info(&self) -> WatcherInfo {
WatcherInfo {
name: self.name,
description: "Test watcher",
default_paths: vec![PathBuf::from("/test")],
}
}
fn is_available(&self) -> bool {
self.available
}
fn find_sources(&self) -> Result<Vec<PathBuf>> {
Ok(vec![])
}
fn parse_source(&self, _path: &Path) -> Result<Vec<(Session, Vec<Message>)>> {
Ok(vec![])
}
fn watch_paths(&self) -> Vec<PathBuf> {
vec![PathBuf::from("/test")]
}
}
#[test]
fn test_registry_new_is_empty() {
let registry = WatcherRegistry::new();
assert!(registry.all_watchers().is_empty());
}
#[test]
fn test_registry_register_and_retrieve() {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(TestWatcher {
name: "test-watcher",
available: true,
}));
assert_eq!(registry.all_watchers().len(), 1);
assert!(registry.get_watcher("test-watcher").is_some());
assert!(registry.get_watcher("nonexistent").is_none());
}
#[test]
fn test_registry_available_watchers_filters() {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(TestWatcher {
name: "available",
available: true,
}));
registry.register(Box::new(TestWatcher {
name: "unavailable",
available: false,
}));
assert_eq!(registry.all_watchers().len(), 2);
assert_eq!(registry.available_watchers().len(), 1);
assert_eq!(registry.available_watchers()[0].info().name, "available");
}
#[test]
fn test_registry_all_watch_paths() {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(TestWatcher {
name: "watcher1",
available: true,
}));
registry.register(Box::new(TestWatcher {
name: "watcher2",
available: true,
}));
registry.register(Box::new(TestWatcher {
name: "watcher3",
available: false,
}));
let paths = registry.all_watch_paths();
assert_eq!(paths.len(), 2);
}
#[test]
fn test_default_registry_contains_builtin_watchers() {
let registry = default_registry();
let watchers = registry.all_watchers();
assert!(watchers.len() >= 10);
assert!(registry.get_watcher("aider").is_some());
assert!(registry.get_watcher("amp").is_some());
assert!(registry.get_watcher("claude-code").is_some());
assert!(registry.get_watcher("cline").is_some());
assert!(registry.get_watcher("codex").is_some());
assert!(registry.get_watcher("continue").is_some());
assert!(registry.get_watcher("gemini").is_some());
assert!(registry.get_watcher("kilo-code").is_some());
assert!(registry.get_watcher("opencode").is_some());
assert!(registry.get_watcher("roo-code").is_some());
}
#[test]
fn test_watcher_info_fields() {
let watcher = TestWatcher {
name: "test",
available: true,
};
let info = watcher.info();
assert_eq!(info.name, "test");
assert_eq!(info.description, "Test watcher");
assert!(!info.default_paths.is_empty());
}
#[test]
fn test_registry_enabled_watchers() {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(TestWatcher {
name: "watcher-a",
available: true,
}));
registry.register(Box::new(TestWatcher {
name: "watcher-b",
available: true,
}));
registry.register(Box::new(TestWatcher {
name: "watcher-c",
available: false,
}));
let enabled = vec!["watcher-a".to_string(), "watcher-c".to_string()];
let watchers = registry.enabled_watchers(&enabled);
assert_eq!(watchers.len(), 1);
assert_eq!(watchers[0].info().name, "watcher-a");
}
#[test]
fn test_registry_enabled_watchers_empty_list() {
let mut registry = WatcherRegistry::new();
registry.register(Box::new(TestWatcher {
name: "watcher",
available: true,
}));
let enabled: Vec<String> = vec![];
let watchers = registry.enabled_watchers(&enabled);
assert!(watchers.is_empty());
}
}