From 4a47542f7ac9e9206f78e67d4a50fdedb2217498 Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 24 Mar 2026 17:51:56 +0900 Subject: [PATCH] refactor(watch): rename -f/config flag to -p/preset, default watch dir to src/ --- src/main.rs | 8 ++--- src/watch.rs | 85 ++++++++++++++++------------------------------------ 2 files changed, 29 insertions(+), 64 deletions(-) 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)"); - } -}