use crate::{discovery, drift, manifest, output};
use anyhow::Result;
use colored::Colorize;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
pub fn run(
manifest_path: &Path,
root: &Path,
ignore: &[String],
team: Option<&str>,
depth: u32,
) -> Result<usize> {
let manifest_path = manifest_path.to_path_buf();
let root = root.to_path_buf();
let ignore = ignore.to_vec();
let team = team.map(str::to_owned);
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(&manifest_path, RecursiveMode::NonRecursive)?;
let initial_m = manifest::Manifest::load(&manifest_path)?;
let watch_paths = effective_watch_paths(&initial_m, &root);
for p in &watch_paths {
if p.exists() {
let _ = watcher.watch(p, RecursiveMode::Recursive);
}
}
let initial_errors = run_once(&manifest_path, &root, &ignore, team.as_deref(), depth);
eprintln!(
"\n{} Watching {} and service directories. Press Ctrl-C to stop.\n",
"●".cyan().bold(),
manifest_path.display()
);
let debounce = Duration::from_millis(500);
let mut last_trigger = Instant::now() - debounce * 2;
for res in rx {
match res {
Ok(event) => {
if !is_relevant(&event) {
continue;
}
let now = Instant::now();
if now.duration_since(last_trigger) < debounce {
continue;
}
last_trigger = now;
if let Ok(new_m) = manifest::Manifest::load(&manifest_path) {
for p in effective_watch_paths(&new_m, &root) {
if p.exists() {
let _ = watcher.watch(&p, RecursiveMode::Recursive);
}
}
}
run_once(&manifest_path, &root, &ignore, team.as_deref(), depth);
}
Err(e) => eprintln!("{} watcher error: {e}", "!".red()),
}
}
Ok(initial_errors)
}
fn run_once(manifest_path: &Path, root: &Path, ignore: &[String], team: Option<&str>, depth: u32) -> usize {
let ts = timestamp();
match manifest::Manifest::load(manifest_path) {
Err(e) => {
eprintln!("\n[{ts}] {} reloading manifest: {e:#}", "error".red());
0
}
Ok(mut m) => {
if let Some(t) = team {
m.services.retain(|s| {
s.team
.as_deref()
.map(|team_val| team_val.eq_ignore_ascii_case(t))
.unwrap_or(false)
});
}
let discovered = discovery::discover_services_with_opts(root, &m, ignore, depth);
let mut report = drift::analyze(&m, &discovered, root);
report.manifest = manifest_path.display().to_string();
eprintln!("\n[{ts}] change detected — re-running drift check");
output::terminal::render_check(&report, &[]);
report.error_count()
}
}
}
fn effective_watch_paths(m: &manifest::Manifest, root: &Path) -> Vec<PathBuf> {
m.effective_discovery_paths()
.iter()
.filter_map(|pat| {
let without_glob = pat
.split('*')
.next()
.unwrap_or("")
.trim_end_matches('/')
.to_string();
if without_glob.is_empty() {
None
} else {
Some(root.join(without_glob))
}
})
.collect()
}
fn is_relevant(event: ¬ify::Event) -> bool {
matches!(
event.kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
)
}
fn timestamp() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (h, m, s) = (secs % 86400 / 3600, secs % 3600 / 60, secs % 60);
format!("{h:02}:{m:02}:{s:02} UTC")
}