1mod 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#[derive(Debug)]
18pub struct SyncEngine {
19 source_of_truth: std::path::PathBuf,
21 editors: EditorConfig,
23 watcher: Option<FileWatcher>,
25}
26
27impl SyncEngine {
28 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 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 pub fn sync(&self, project_root: &Path) -> Result<SyncReport> {
65 let mut report = SyncReport::default();
66
67 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 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 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 pub fn stop_watching(&mut self) {
114 self.watcher = None;
115 }
116
117 pub fn is_watching(&self) -> bool {
119 self.watcher.is_some()
120 }
121}
122
123#[derive(Debug, Default)]
125pub struct SyncReport {
126 pub synced: Vec<SyncedEditor>,
128 pub errors: Vec<SyncError>,
130}
131
132impl SyncReport {
133 pub fn is_success(&self) -> bool {
135 self.errors.is_empty()
136 }
137
138 pub fn synced_count(&self) -> usize {
140 self.synced.len()
141 }
142}
143
144#[derive(Debug)]
146pub struct SyncedEditor {
147 pub editor: Editor,
149 pub path: std::path::PathBuf,
151}
152
153#[derive(Debug)]
155pub struct SyncError {
156 pub editor: Editor,
158 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}