diff --git a/src/main.rs b/src/main.rs
index 7b52d29..363fc9c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -115,9 +115,9 @@ fn main() {
}
}
Some("watch") => {
- let dir = env::args().nth(2).unwrap_or_else(|| ".".to_string());
- let config = parse_flag("-f");
- if let Err(e) = aishell::watch::run(&dir, config.as_deref()) {
+ let dir = env::args().nth(2).unwrap_or_else(|| "src/".to_string());
+ let preset = parse_flag("-p");
+ if let Err(e) = aishell::watch::run(&dir, preset.as_deref()) {
eprintln!("aishell: {}", e);
std::process::exit(1);
}
@@ -158,7 +158,7 @@ fn print_help() {
println!(" aishell history [id] Show past sessions");
println!(" aishell history cmd [grep] Command history (filterable)");
println!(" aishell history clean Remove old sessions (keep 10)");
- println!(" aishell watch
[-f cfg] Watch files, auto-trigger loop");
+ println!(" aishell watch [dir] [-p preset] Watch files → run preset on change");
println!(" aishell next Resume loop");
println!(" aishell stop Stop the loop");
println!(" aishell help This help");
diff --git a/src/watch.rs b/src/watch.rs
index e7fcc2a..641f671 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -3,65 +3,59 @@ use std::sync::mpsc;
use std::time::{Duration, Instant};
use notify::{Watcher, RecursiveMode, Event, EventKind};
-const STATE_DIR: &str = "/tmp/aishell";
const DEBOUNCE_MS: u64 = 2000;
-/// Watch a directory for file changes.
-/// On change, write next=true to loop.json with changed file list.
-pub fn run(dir: &str, config: Option<&str>) -> Result<(), String> {
+/// Watch directory, run preset on change.
+/// `aishell watch src/ -p review` → file change → review → auto-commit
+pub fn run(dir: &str, preset: Option<&str>) -> Result<(), String> {
let dir = crate::config::expand_tilde(dir);
let dir_path = Path::new(&dir);
if !dir_path.is_dir() {
return Err(format!("not a directory: {dir}"));
}
- // Start headless run in background if config provided
- if let Some(cfg) = config {
- let cfg = cfg.to_string();
- std::thread::spawn(move || {
- let _ = crate::headless::run(&cfg, None, None);
- });
- // Give it time to start
- std::thread::sleep(Duration::from_secs(2));
- }
-
- eprintln!("watching: {dir}");
+ let preset = preset.unwrap_or("review");
+ eprintln!("watching: {dir} (preset: {preset})");
eprintln!("Ctrl+C to stop.\n");
let (tx, rx) = mpsc::channel::();
let mut watcher = notify::recommended_watcher(move |res: Result| {
- if let Ok(event) = res {
- let _ = tx.send(event);
- }
+ if let Ok(event) = res { let _ = tx.send(event); }
}).map_err(|e| format!("watcher error: {e}"))?;
watcher.watch(dir_path, RecursiveMode::Recursive)
.map_err(|e| format!("watch error: {e}"))?;
let mut last_trigger = Instant::now();
- let mut changed_files: Vec = Vec::new();
+ let mut changed: Vec = Vec::new();
loop {
match rx.recv_timeout(Duration::from_millis(500)) {
Ok(event) => {
if !is_relevant(&event) { continue; }
-
- for path in &event.paths {
- let p = path.display().to_string();
- if !changed_files.contains(&p) {
- changed_files.push(p);
- }
+ for p in &event.paths {
+ let s = p.display().to_string();
+ if !changed.contains(&s) { changed.push(s); }
}
last_trigger = Instant::now();
}
Err(mpsc::RecvTimeoutError::Timeout) => {
- // Debounce: if we have changes and enough time passed
- if !changed_files.is_empty()
- && last_trigger.elapsed() > Duration::from_millis(DEBOUNCE_MS)
- {
- trigger_cycle(&changed_files);
- changed_files.clear();
+ if !changed.is_empty() && last_trigger.elapsed() > Duration::from_millis(DEBOUNCE_MS) {
+ let short: Vec<&str> = changed.iter()
+ .map(|f| f.rsplit('/').next().unwrap_or(f))
+ .take(5).collect();
+ eprintln!(" changed: {}", short.join(", "));
+
+ // Run preset
+ if preset == "review" {
+ crate::headless::review();
+ } else {
+ let _ = crate::headless::run_preset(preset);
+ }
+
+ changed.clear();
+ eprintln!("\n watching...");
}
}
Err(_) => break,
@@ -76,35 +70,6 @@ fn is_relevant(event: &Event) -> bool {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
) && event.paths.iter().any(|p| {
let s = p.display().to_string();
- // Ignore hidden files, target/, /tmp/
!s.contains("/.") && !s.contains("/target/") && !s.contains("/tmp/")
})
}
-
-fn trigger_cycle(files: &[String]) {
- let short: Vec<&str> = files.iter()
- .map(|f| f.rsplit('/').next().unwrap_or(f))
- .take(5)
- .collect();
-
- eprintln!(" changed: {}", short.join(", "));
-
- // Write to loop.json to signal the running headless loop
- let loop_path = format!("{STATE_DIR}/loop.json");
- if Path::new(&loop_path).exists() {
- let control = serde_json::json!({
- "ready": true,
- "next": true,
- "quit": false,
- "changed_files": files,
- });
- let json = serde_json::to_string_pretty(&control).unwrap_or_default();
- // atomic write
- let tmp = format!("{loop_path}.tmp");
- if std::fs::write(&tmp, &json).is_ok() {
- let _ = std::fs::rename(&tmp, &loop_path);
- }
- } else {
- eprintln!(" (no active loop to signal)");
- }
-}