1use crate::core::find_repo_root;
2use crate::response::{WatchEvent, WatchResponse};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6use std::time::SystemTime;
7use walkdir::WalkDir;
8
9pub fn execute(
10 debounce_ms: u64,
11 filter: Option<String>,
12) -> Result<WatchResponse, crate::errors::LitError> {
13 let repo_root = find_repo_root()?;
14
15 let mut last_state = scan_files(&repo_root, filter.as_deref())?;
17
18 let debounce = std::time::Duration::from_millis(debounce_ms);
19
20 eprintln!(
21 "Watching {} for changes (Ctrl+C to stop)...",
22 repo_root.display()
23 );
24
25 let start_event = WatchEvent {
27 event_type: "start".to_string(),
28 path: repo_root.to_string_lossy().to_string(),
29 timestamp: chrono::Utc::now().timestamp(),
30 };
31 println!(
32 "{}",
33 serde_json::to_string(&start_event).unwrap_or_default()
34 );
35
36 loop {
37 std::thread::sleep(debounce);
38
39 let current_state = scan_files(&repo_root, filter.as_deref())?;
40 let mut events = Vec::new();
41
42 for (path, mtime) in ¤t_state {
44 match last_state.get(path) {
45 Some(old_mtime) if old_mtime != mtime => {
46 events.push(WatchEvent {
47 event_type: "modified".to_string(),
48 path: path.clone(),
49 timestamp: chrono::Utc::now().timestamp(),
50 });
51 }
52 None => {
53 events.push(WatchEvent {
54 event_type: "created".to_string(),
55 path: path.clone(),
56 timestamp: chrono::Utc::now().timestamp(),
57 });
58 }
59 _ => {}
60 }
61 }
62
63 for path in last_state.keys() {
65 if !current_state.contains_key(path) {
66 events.push(WatchEvent {
67 event_type: "deleted".to_string(),
68 path: path.clone(),
69 timestamp: chrono::Utc::now().timestamp(),
70 });
71 }
72 }
73
74 for event in &events {
76 println!("{}", serde_json::to_string(event).unwrap_or_default());
77 }
78
79 last_state = current_state;
80 }
81}
82
83fn scan_files(repo_root: &Path, filter: Option<&str>) -> Result<HashMap<String, u64>, String> {
85 let mut files = HashMap::new();
86
87 for entry in WalkDir::new(repo_root).into_iter().filter_entry(|e| {
88 let name = e.file_name().to_string_lossy();
89 !name.starts_with('.') && name != "target" && name != "node_modules"
90 }) {
91 let entry = entry.map_err(|e| format!("Walk error: {}", e))?;
92 if !entry.file_type().is_file() {
93 continue;
94 }
95 let path = entry.path();
96 if path.starts_with(repo_root.join(".lit")) {
97 continue;
98 }
99
100 let rel_path = path
101 .strip_prefix(repo_root)
102 .unwrap_or(path)
103 .to_string_lossy()
104 .replace('\\', "/");
105
106 if let Some(f) = filter {
108 if !rel_path.contains(f) {
109 continue;
110 }
111 }
112
113 let mtime = fs::metadata(path)
114 .and_then(|m| m.modified())
115 .unwrap_or(SystemTime::UNIX_EPOCH)
116 .duration_since(SystemTime::UNIX_EPOCH)
117 .unwrap_or_default()
118 .as_secs();
119
120 files.insert(rel_path, mtime);
121 }
122
123 Ok(files)
124}