use super::WatchArgs;
use anyhow::Result;
use colored::*;
use std::path::PathBuf;
use std::time::{Duration, Instant};
pub fn run(args: &WatchArgs) -> Result<()> {
eprintln!("{}", "👁️ Forge Guard — Watch Mode".bold());
eprintln!(" Watching: {}", args.dirs);
eprintln!(" Debounce: {}ms", args.debounce_ms);
let watch_dirs: Vec<PathBuf> = args
.dirs
.split(',')
.map(|s| {
let p = PathBuf::from(s.trim());
if p.is_relative() {
args.shared.project.join(p)
} else {
p
}
})
.collect();
for dir in &watch_dirs {
if !dir.exists() {
anyhow::bail!("Directory does not exist: {}", dir.display());
}
}
eprintln!(
"\n{}",
" Watching for changes... (Ctrl+C to stop)".dimmed()
);
let debounce = Duration::from_millis(args.debounce_ms);
let poll_interval = Duration::from_millis(200);
let mut last_mod = get_last_modification(&watch_dirs)?;
let mut last_trigger = Instant::now();
loop {
std::thread::sleep(poll_interval);
let current_mod = get_last_modification(&watch_dirs)?;
if current_mod > last_mod && last_trigger.elapsed() >= debounce {
eprintln!("\n{}", "🔄 Change detected, re-auditing...".bold());
let audit_args = super::AuditArgs {
shared: super::SharedFlags {
chain: args.shared.chain.clone(),
project: args.shared.project.clone(),
json: args.shared.json,
markdown: args.shared.markdown,
html: args.shared.html,
strict: args.shared.strict,
offline: args.shared.offline,
production: args.shared.production,
report: args.shared.report,
parallelism: args.shared.parallelism,
},
full: args.full,
quick: false,
summary: false,
exploit: args.full,
gas: args.full,
all_chains: false,
max_parallel_chains: 4,
sources: args.dirs.clone(),
exclude: args.exclude.clone(),
ai: false,
ai_provider: "openai".into(),
ai_model: "gpt-4".into(),
ai_api_key: None,
ollama_endpoint: None,
ai_full: false,
template: None,
list_templates: false,
suppressions: None,
show_suppressed: false,
generate_suppressions: false,
notify: false,
};
if let Err(e) = super::audit::run(&audit_args) {
eprintln!("{} Audit error: {}", "⚠️".yellow(), e);
}
last_mod = current_mod;
last_trigger = Instant::now();
eprintln!(
"\n{}",
" Waiting for changes... (Ctrl+C to stop)".dimmed()
);
}
}
}
fn get_last_modification(dirs: &[PathBuf]) -> Result<std::time::SystemTime> {
let mut latest = std::time::UNIX_EPOCH;
for dir in dirs {
for entry in walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.file_type().is_file() {
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
if modified > latest {
latest = modified;
}
}
}
}
}
}
Ok(latest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watch_arg_defaults() {
let args = WatchArgs {
shared: super::super::SharedFlags {
chain: "ethereum".into(),
project: std::path::PathBuf::from("."),
json: false,
markdown: false,
html: false,
strict: false,
offline: false,
production: false,
report: false,
parallelism: 4,
},
dirs: "src".into(),
debounce_ms: 500,
exclude: None,
full: false,
};
assert_eq!(args.dirs, "src");
assert_eq!(args.debounce_ms, 500);
assert!(args.exclude.is_none());
assert!(!args.full);
}
#[test]
fn test_watch_with_custom_dirs() {
let args = WatchArgs {
shared: super::super::SharedFlags::default(),
dirs: "src,test-contracts,lib".into(),
debounce_ms: 1000,
exclude: Some("*.test.sol".into()),
full: true,
};
assert_eq!(args.dirs, "src,test-contracts,lib");
assert_eq!(args.debounce_ms, 1000);
assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
assert!(args.full);
}
#[test]
fn test_watch_dir_splitting() {
let dirs_str = "src, contracts, lib";
let dirs: Vec<PathBuf> = dirs_str
.split(',')
.map(|s| PathBuf::from(s.trim()))
.collect();
assert_eq!(dirs.len(), 3);
assert_eq!(dirs[0], PathBuf::from("src"));
assert_eq!(dirs[1], PathBuf::from("contracts"));
assert_eq!(dirs[2], PathBuf::from("lib"));
}
#[test]
fn test_watch_dir_relative_to_project() {
let project = PathBuf::from("/tmp/my-project");
let dir_str = "src";
let dir_relative = PathBuf::from(dir_str.trim());
let resolved = if dir_relative.is_relative() {
project.join(dir_relative)
} else {
dir_relative
};
assert_eq!(resolved, PathBuf::from("/tmp/my-project/src"));
}
#[test]
fn test_watch_dir_absolute_stays_absolute() {
let project = PathBuf::from("/tmp/my-project");
let dir_str = "/custom/src";
let dir_path = PathBuf::from(dir_str.trim());
let resolved = if dir_path.is_relative() {
project.join(dir_path)
} else {
dir_path
};
assert_eq!(resolved, PathBuf::from("/custom/src"));
}
#[test]
fn test_get_last_modification_nonexistent_dir() {
let dirs = vec![PathBuf::from("/nonexistent/path")];
let result = get_last_modification(&dirs);
assert!(result.is_ok());
let time = result.unwrap();
assert_eq!(time, std::time::UNIX_EPOCH);
}
#[test]
fn test_watch_debounce_duration() {
let debounce_ms: u64 = 500;
let debounce = Duration::from_millis(debounce_ms);
assert_eq!(debounce.as_millis(), 500);
}
#[test]
fn test_watch_poll_interval() {
let poll = Duration::from_millis(200);
assert_eq!(poll.as_millis(), 200);
}
#[test]
fn test_watch_audit_args_construction() {
let watch_args = WatchArgs {
shared: super::super::SharedFlags {
chain: "base".into(),
project: PathBuf::from("."),
json: false,
markdown: false,
html: false,
strict: false,
offline: false,
production: false,
report: false,
parallelism: 4,
},
dirs: "src".into(),
debounce_ms: 500,
exclude: None,
full: true,
};
let audit_args = super::super::AuditArgs {
shared: super::super::SharedFlags {
chain: watch_args.shared.chain.clone(),
project: watch_args.shared.project.clone(),
json: watch_args.shared.json,
markdown: watch_args.shared.markdown,
html: watch_args.shared.html,
strict: watch_args.shared.strict,
offline: watch_args.shared.offline,
production: watch_args.shared.production,
report: watch_args.shared.report,
parallelism: watch_args.shared.parallelism,
},
full: watch_args.full,
quick: false,
summary: false,
exploit: watch_args.full,
gas: watch_args.full,
all_chains: false,
max_parallel_chains: 4,
sources: watch_args.dirs.clone(),
exclude: watch_args.exclude.clone(),
ai: false,
ai_provider: "openai".into(),
ai_model: "gpt-4".into(),
ai_api_key: None,
ollama_endpoint: None,
ai_full: false,
template: None,
list_templates: false,
suppressions: None,
show_suppressed: false,
generate_suppressions: false,
notify: false,
};
assert_eq!(audit_args.shared.chain, "base");
assert!(audit_args.full);
assert!(audit_args.exploit);
assert!(audit_args.gas);
assert_eq!(audit_args.sources, "src");
}
}