Skip to main content

driven/sync/
mod.rs

1//! Multi-Editor Synchronization
2//!
3//! Keeps AI rules synchronized across multiple editors.
4
5mod differ;
6mod propagator;
7mod watcher;
8
9pub use differ::RuleDiffer;
10pub use propagator::ChangePropagator;
11pub use watcher::FileWatcher;
12
13use crate::{Editor, EditorConfig, Result, RuleSet};
14use std::path::Path;
15
16/// Engine for synchronizing rules across editors
17#[derive(Debug)]
18pub struct SyncEngine {
19    /// Source of truth path
20    source_of_truth: std::path::PathBuf,
21    /// Enabled editors
22    editors: EditorConfig,
23    /// File watcher
24    watcher: Option<FileWatcher>,
25}
26
27impl SyncEngine {
28    /// Create a new sync engine
29    pub fn new(source_of_truth: impl AsRef<Path>, editors: EditorConfig) -> Self {
30        Self {
31            source_of_truth: source_of_truth.as_ref().to_path_buf(),
32            editors,
33            watcher: None,
34        }
35    }
36
37    /// Get enabled editors
38    pub fn enabled_editors(&self) -> Vec<Editor> {
39        let mut editors = Vec::new();
40
41        if self.editors.cursor {
42            editors.push(Editor::Cursor);
43        }
44        if self.editors.copilot {
45            editors.push(Editor::Copilot);
46        }
47        if self.editors.windsurf {
48            editors.push(Editor::Windsurf);
49        }
50        if self.editors.claude {
51            editors.push(Editor::Claude);
52        }
53        if self.editors.aider {
54            editors.push(Editor::Aider);
55        }
56        if self.editors.cline {
57            editors.push(Editor::Cline);
58        }
59
60        editors
61    }
62
63    /// Sync rules to all enabled editors
64    pub fn sync(&self, project_root: &Path) -> Result<SyncReport> {
65        let mut report = SyncReport::default();
66
67        // Load source of truth
68        let source_path = project_root.join(&self.source_of_truth);
69        let rules = if source_path.exists() {
70            if source_path.extension().is_some_and(|ext| ext == "drv") {
71                RuleSet::load_binary(&source_path)?
72            } else {
73                RuleSet::load(&source_path)?
74            }
75        } else {
76            return Err(crate::DrivenError::Sync(format!(
77                "Source of truth not found: {}",
78                source_path.display()
79            )));
80        };
81
82        // Emit to each enabled editor
83        for editor in self.enabled_editors() {
84            let target_path = project_root.join(editor.rule_path());
85
86            match rules.emit(editor, &target_path) {
87                Ok(()) => {
88                    report.synced.push(SyncedEditor {
89                        editor,
90                        path: target_path,
91                    });
92                }
93                Err(e) => {
94                    report.errors.push(SyncError {
95                        editor,
96                        error: e.to_string(),
97                    });
98                }
99            }
100        }
101
102        Ok(report)
103    }
104
105    /// Start watching for changes
106    pub fn start_watching(&mut self, project_root: &Path) -> Result<()> {
107        let watcher = FileWatcher::new(project_root)?;
108        self.watcher = Some(watcher);
109        Ok(())
110    }
111
112    /// Stop watching for changes
113    pub fn stop_watching(&mut self) {
114        self.watcher = None;
115    }
116
117    /// Check if watching is active
118    pub fn is_watching(&self) -> bool {
119        self.watcher.is_some()
120    }
121}
122
123/// Report of sync operation
124#[derive(Debug, Default)]
125pub struct SyncReport {
126    /// Successfully synced editors
127    pub synced: Vec<SyncedEditor>,
128    /// Errors encountered
129    pub errors: Vec<SyncError>,
130}
131
132impl SyncReport {
133    /// Check if sync was fully successful
134    pub fn is_success(&self) -> bool {
135        self.errors.is_empty()
136    }
137
138    /// Get count of synced editors
139    pub fn synced_count(&self) -> usize {
140        self.synced.len()
141    }
142}
143
144/// Successfully synced editor
145#[derive(Debug)]
146pub struct SyncedEditor {
147    /// The editor
148    pub editor: Editor,
149    /// Path that was written
150    pub path: std::path::PathBuf,
151}
152
153/// Sync error for an editor
154#[derive(Debug)]
155pub struct SyncError {
156    /// The editor
157    pub editor: Editor,
158    /// Error message
159    pub error: String,
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_sync_engine_new() {
168        let engine = SyncEngine::new(".driven/rules.drv", EditorConfig::default());
169        assert!(!engine.is_watching());
170    }
171
172    #[test]
173    fn test_enabled_editors() {
174        let engine = SyncEngine::new(
175            ".driven/rules.drv",
176            EditorConfig {
177                cursor: true,
178                copilot: true,
179                windsurf: false,
180                claude: false,
181                aider: false,
182                cline: false,
183            },
184        );
185
186        let editors = engine.enabled_editors();
187        assert_eq!(editors.len(), 2);
188        assert!(editors.contains(&Editor::Cursor));
189        assert!(editors.contains(&Editor::Copilot));
190    }
191
192    #[test]
193    fn test_sync_report() {
194        let report = SyncReport::default();
195        assert!(report.is_success());
196        assert_eq!(report.synced_count(), 0);
197    }
198}