helix_core/compiler/workflow/
watch.rs1#![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 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}