forge-guard 0.1.2

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge watch` — watch files for changes and re-audit.

use super::WatchArgs;
use anyhow::Result;
use colored::*;
use std::path::PathBuf;
use std::time::{Duration, Instant};

/// Watch source files for changes and automatically re-audit.
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();

    // Verify directories exist
    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()
    );

    // Simple polling watch loop
    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,
                    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,
                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,
            };

            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)
}