helix_core/compiler/workflow/
watch.rs

1#![cfg(feature = "cli")]
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5use std::collections::HashMap;
6use notify::{Watcher, RecursiveMode, Event, EventKind, Config};
7use anyhow::{Result, Context};
8pub type ChangeCallback = Box<dyn Fn(&Path) -> Result<()> + Send + Sync>;
9pub struct HelixWatcher {
10    watcher: notify::RecommendedWatcher,
11    callbacks: Arc<Mutex<HashMap<PathBuf, Vec<ChangeCallback>>>>,
12    compile_on_change: bool,
13    debounce_duration: Duration,
14}
15impl HelixWatcher {
16    pub fn new() -> Result<Self> {
17        let callbacks = Arc::new(Mutex::new(HashMap::new()));
18        let callbacks_clone = callbacks.clone();
19        let watcher = notify::recommended_watcher(move |
20            res: Result<Event, notify::Error>|
21        {
22            if let Ok(event) = res {
23                Self::handle_event(event, &callbacks_clone);
24            }
25        })?;
26        Ok(Self {
27            watcher,
28            callbacks,
29            compile_on_change: true,
30            debounce_duration: Duration::from_millis(500),
31        })
32    }
33    pub fn with_config(_config: Config) -> Result<Self> {
34        let callbacks = Arc::new(Mutex::new(HashMap::new()));
35        let callbacks_clone = callbacks.clone();
36        let watcher = notify::recommended_watcher(move |
37            res: Result<Event, notify::Error>|
38        {
39            if let Ok(event) = res {
40                Self::handle_event(event, &callbacks_clone);
41            }
42        })?;
43        Ok(Self {
44            watcher,
45            callbacks,
46            compile_on_change: true,
47            debounce_duration: Duration::from_millis(500),
48        })
49    }
50    pub fn watch_file<P: AsRef<Path>>(
51        &mut self,
52        path: P,
53        callback: ChangeCallback,
54    ) -> Result<()> {
55        let path = path.as_ref().to_path_buf();
56        let mut callbacks = self.callbacks.lock().unwrap();
57        callbacks.entry(path.clone()).or_insert_with(Vec::new).push(callback);
58        self.watcher
59            .watch(&path, RecursiveMode::NonRecursive)
60            .context("Failed to watch file")?;
61        Ok(())
62    }
63    pub fn watch_directory<P: AsRef<Path>>(
64        &mut self,
65        path: P,
66        callback: ChangeCallback,
67    ) -> Result<()> {
68        let path = path.as_ref().to_path_buf();
69        let mut callbacks = self.callbacks.lock().unwrap();
70        callbacks.entry(path.clone()).or_insert_with(Vec::new).push(callback);
71        self.watcher
72            .watch(&path, RecursiveMode::Recursive)
73            .context("Failed to watch directory")?;
74        Ok(())
75    }
76    pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
77        let path = path.as_ref();
78        let mut callbacks = self.callbacks.lock().unwrap();
79        callbacks.remove(path);
80        self.watcher.unwatch(path).context("Failed to unwatch path")?;
81        Ok(())
82    }
83    fn handle_event(
84        event: Event,
85        callbacks: &Arc<Mutex<HashMap<PathBuf, Vec<ChangeCallback>>>>,
86    ) {
87        match event.kind {
88            EventKind::Modify(_) | EventKind::Create(_) => {
89                for path in event.paths {
90                    if path.extension().and_then(|s| s.to_str()) != Some("hlx") {
91                        continue;
92                    }
93                    let callbacks = callbacks.lock().unwrap();
94                    if let Some(cbs) = callbacks.get(&path) {
95                        for callback in cbs {
96                            if let Err(e) = callback(&path) {
97                                eprintln!("Callback error for {:?}: {}", path, e);
98                            }
99                        }
100                    }
101                    if let Some(parent) = path.parent() {
102                        if let Some(cbs) = callbacks.get(parent) {
103                            for callback in cbs {
104                                if let Err(e) = callback(&path) {
105                                    eprintln!("Callback error for {:?}: {}", path, e);
106                                }
107                            }
108                        }
109                    }
110                }
111            }
112            _ => {}
113        }
114    }
115    pub fn set_compile_on_change(&mut self, enable: bool) {
116        self.compile_on_change = enable;
117    }
118    pub fn set_debounce(&mut self, duration: Duration) {
119        self.debounce_duration = duration;
120    }
121}
122pub struct CompileWatcher {
123    watcher: HelixWatcher,
124    compiler: crate::compiler::Compiler,
125    output_dir: Option<PathBuf>,
126}
127impl CompileWatcher {
128    pub fn new(optimization_level: crate::compiler::OptimizationLevel) -> Result<Self> {
129        Ok(Self {
130            watcher: HelixWatcher::new()?,
131            compiler: crate::compiler::Compiler::new(optimization_level),
132            output_dir: None,
133        })
134    }
135    pub fn output_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
136        self.output_dir = Some(dir.as_ref().to_path_buf());
137        self
138    }
139    pub fn watch<P: AsRef<Path>>(&mut self, dir: P) -> Result<()> {
140        let dir = dir.as_ref().to_path_buf();
141        let compiler = self.compiler.clone();
142        let output_dir = self.output_dir.clone();
143        println!("👀 Watching directory: {}", dir.display());
144        println!("   Press Ctrl+C to stop");
145        self.watcher
146            .watch_directory(
147                dir,
148                Box::new(move |path| {
149                    println!("📝 File changed: {}", path.display());
150                    match compiler.compile_file(path) {
151                        Ok(binary) => {
152                            let output_path = if let Some(ref out_dir) = output_dir {
153                                let file_name = path
154                                    .file_stem()
155                                    .unwrap_or_default()
156                                    .to_string_lossy();
157                                out_dir.join(format!("{}.hlxb", file_name))
158                            } else {
159                                let mut p = path.to_path_buf();
160                                p.set_extension("hlxb");
161                                p
162                            };
163                            let serializer = crate::compiler::BinarySerializer::new(
164                                true,
165                            );
166                            if let Err(e) = serializer
167                                .write_to_file(&binary, &output_path)
168                            {
169                                eprintln!("   ❌ Failed to write binary: {}", e);
170                            } else {
171                                println!("   ✅ Compiled to: {}", output_path.display());
172                            }
173                        }
174                        Err(e) => {
175                            eprintln!("   ❌ Compilation failed: {}", e);
176                        }
177                    }
178                    Ok(())
179                }),
180            )?;
181        Ok(())
182    }
183    pub fn run(self) -> Result<()> {
184        loop {
185            std::thread::sleep(Duration::from_secs(1));
186        }
187    }
188}
189pub struct HotReloadManager {
190    configs: Arc<Mutex<HashMap<PathBuf, crate::types::HelixConfig>>>,
191    watcher: HelixWatcher,
192    callbacks: Vec<Box<dyn Fn(&Path, &crate::types::HelixConfig) + Send + Sync>>,
193}
194impl HotReloadManager {
195    pub fn new() -> Result<Self> {
196        Ok(Self {
197            configs: Arc::new(Mutex::new(HashMap::new())),
198            watcher: HelixWatcher::new()?,
199            callbacks: Vec::new(),
200        })
201    }
202    pub fn add_config<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
203        let path = path.as_ref().to_path_buf();
204        let config = crate::load_file(&path)
205            .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
206        {
207            let mut configs = self.configs.lock().unwrap();
208            configs.insert(path.clone(), config);
209        }
210        let configs_clone = self.configs.clone();
211        let path_clone = path.clone();
212        self.watcher
213            .watch_file(
214                path,
215                Box::new(move |changed_path| {
216                    match crate::load_file(changed_path) {
217                        Ok(new_config) => {
218                            let mut configs = configs_clone.lock().unwrap();
219                            configs.insert(path_clone.clone(), new_config);
220                            println!("🔄 Reloaded config: {}", changed_path.display());
221                        }
222                        Err(e) => {
223                            eprintln!("❌ Failed to reload config: {}", e);
224                        }
225                    }
226                    Ok(())
227                }),
228            )?;
229        Ok(())
230    }
231    pub fn get_config<P: AsRef<Path>>(
232        &self,
233        path: P,
234    ) -> Option<crate::types::HelixConfig> {
235        let configs = self.configs.lock().unwrap();
236        configs.get(path.as_ref()).cloned()
237    }
238    pub fn on_change<F>(&mut self, callback: F)
239    where
240        F: Fn(&Path, &crate::types::HelixConfig) + Send + Sync + 'static,
241    {
242        self.callbacks.push(Box::new(callback));
243    }
244    /// Get all managed configurations
245    pub fn get_all_configs(&self) -> HashMap<PathBuf, crate::types::HelixConfig> {
246        let configs = self.configs.lock().unwrap();
247        configs.clone()
248    }
249}
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use tempfile::TempDir;
254    use std::fs;
255    #[test]
256    fn test_watcher_creation() {
257        let watcher = HelixWatcher::new();
258        assert!(watcher.is_ok());
259    }
260    #[test]
261    fn test_compile_watcher() {
262        let watcher = CompileWatcher::new(crate::compiler::OptimizationLevel::Two);
263        assert!(watcher.is_ok());
264    }
265    #[test]
266    fn test_hot_reload_manager() {
267        let manager = HotReloadManager::new();
268        assert!(manager.is_ok());
269    }
270    #[test]
271    fn test_watch_file() -> Result<()> {
272        let temp_dir = TempDir::new()?;
273        let file_path = temp_dir.path().join("test.hlx");
274        fs::write(&file_path, "agent \"test\" { model = \"gpt-4\" }")?;
275        let mut watcher = HelixWatcher::new()?;
276        let called = Arc::new(Mutex::new(false));
277        let called_clone = called.clone();
278        watcher
279            .watch_file(
280                &file_path,
281                Box::new(move |_path| {
282                    let mut c = called_clone.lock().unwrap();
283                    *c = true;
284                    Ok(())
285                }),
286            )?;
287        fs::write(&file_path, "agent \"test\" { model = \"gpt-4o\" }")?;
288        std::thread::sleep(Duration::from_millis(100));
289        Ok(())
290    }
291}