2
0

refactor(watch): rename -f/config flag to -p/preset, default watch dir to src/

This commit is contained in:
2026-03-24 17:51:56 +09:00
parent b478846399
commit 4a47542f7a
2 changed files with 29 additions and 64 deletions

View File

@@ -115,9 +115,9 @@ fn main() {
} }
} }
Some("watch") => { Some("watch") => {
let dir = env::args().nth(2).unwrap_or_else(|| ".".to_string()); let dir = env::args().nth(2).unwrap_or_else(|| "src/".to_string());
let config = parse_flag("-f"); let preset = parse_flag("-p");
if let Err(e) = aishell::watch::run(&dir, config.as_deref()) { if let Err(e) = aishell::watch::run(&dir, preset.as_deref()) {
eprintln!("aishell: {}", e); eprintln!("aishell: {}", e);
std::process::exit(1); std::process::exit(1);
} }
@@ -158,7 +158,7 @@ fn print_help() {
println!(" aishell history [id] Show past sessions"); println!(" aishell history [id] Show past sessions");
println!(" aishell history cmd [grep] Command history (filterable)"); println!(" aishell history cmd [grep] Command history (filterable)");
println!(" aishell history clean Remove old sessions (keep 10)"); println!(" aishell history clean Remove old sessions (keep 10)");
println!(" aishell watch <dir> [-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 next Resume loop");
println!(" aishell stop Stop the loop"); println!(" aishell stop Stop the loop");
println!(" aishell help This help"); println!(" aishell help This help");

View File

@@ -3,65 +3,59 @@ use std::sync::mpsc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use notify::{Watcher, RecursiveMode, Event, EventKind}; use notify::{Watcher, RecursiveMode, Event, EventKind};
const STATE_DIR: &str = "/tmp/aishell";
const DEBOUNCE_MS: u64 = 2000; const DEBOUNCE_MS: u64 = 2000;
/// Watch a directory for file changes. /// Watch directory, run preset on change.
/// On change, write next=true to loop.json with changed file list. /// `aishell watch src/ -p review` → file change → review → auto-commit
pub fn run(dir: &str, config: Option<&str>) -> Result<(), String> { pub fn run(dir: &str, preset: Option<&str>) -> Result<(), String> {
let dir = crate::config::expand_tilde(dir); let dir = crate::config::expand_tilde(dir);
let dir_path = Path::new(&dir); let dir_path = Path::new(&dir);
if !dir_path.is_dir() { if !dir_path.is_dir() {
return Err(format!("not a directory: {dir}")); return Err(format!("not a directory: {dir}"));
} }
// Start headless run in background if config provided let preset = preset.unwrap_or("review");
if let Some(cfg) = config { eprintln!("watching: {dir} (preset: {preset})");
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}");
eprintln!("Ctrl+C to stop.\n"); eprintln!("Ctrl+C to stop.\n");
let (tx, rx) = mpsc::channel::<Event>(); let (tx, rx) = mpsc::channel::<Event>();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| { let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res { if let Ok(event) = res { let _ = tx.send(event); }
let _ = tx.send(event);
}
}).map_err(|e| format!("watcher error: {e}"))?; }).map_err(|e| format!("watcher error: {e}"))?;
watcher.watch(dir_path, RecursiveMode::Recursive) watcher.watch(dir_path, RecursiveMode::Recursive)
.map_err(|e| format!("watch error: {e}"))?; .map_err(|e| format!("watch error: {e}"))?;
let mut last_trigger = Instant::now(); let mut last_trigger = Instant::now();
let mut changed_files: Vec<String> = Vec::new(); let mut changed: Vec<String> = Vec::new();
loop { loop {
match rx.recv_timeout(Duration::from_millis(500)) { match rx.recv_timeout(Duration::from_millis(500)) {
Ok(event) => { Ok(event) => {
if !is_relevant(&event) { continue; } if !is_relevant(&event) { continue; }
for p in &event.paths {
for path in &event.paths { let s = p.display().to_string();
let p = path.display().to_string(); if !changed.contains(&s) { changed.push(s); }
if !changed_files.contains(&p) {
changed_files.push(p);
}
} }
last_trigger = Instant::now(); last_trigger = Instant::now();
} }
Err(mpsc::RecvTimeoutError::Timeout) => { Err(mpsc::RecvTimeoutError::Timeout) => {
// Debounce: if we have changes and enough time passed if !changed.is_empty() && last_trigger.elapsed() > Duration::from_millis(DEBOUNCE_MS) {
if !changed_files.is_empty() let short: Vec<&str> = changed.iter()
&& last_trigger.elapsed() > Duration::from_millis(DEBOUNCE_MS) .map(|f| f.rsplit('/').next().unwrap_or(f))
{ .take(5).collect();
trigger_cycle(&changed_files); eprintln!(" changed: {}", short.join(", "));
changed_files.clear();
// Run preset
if preset == "review" {
crate::headless::review();
} else {
let _ = crate::headless::run_preset(preset);
}
changed.clear();
eprintln!("\n watching...");
} }
} }
Err(_) => break, Err(_) => break,
@@ -76,35 +70,6 @@ fn is_relevant(event: &Event) -> bool {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
) && event.paths.iter().any(|p| { ) && event.paths.iter().any(|p| {
let s = p.display().to_string(); let s = p.display().to_string();
// Ignore hidden files, target/, /tmp/
!s.contains("/.") && !s.contains("/target/") && !s.contains("/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)");
}
}