Skip to main content

claude_native/
watch.rs

1use std::path::Path;
2use std::time::{Duration, SystemTime};
3
4use anyhow::Result;
5use owo_colors::OwoColorize;
6
7use crate::detection;
8use crate::rules;
9use crate::scan;
10use crate::scoring;
11
12/// Watch for file changes and re-score automatically.
13/// Uses polling (checks every 2 seconds) to avoid extra dependencies.
14pub fn watch_and_score(path: &Path) -> Result<()> {
15    println!("{}", "Watching for changes... (Ctrl+C to stop)".dimmed());
16    println!();
17
18    let mut last_score = 0.0_f64;
19    let mut last_mod = get_latest_mtime(path);
20
21    loop {
22        let current_mod = get_latest_mtime(path);
23
24        if current_mod != last_mod || last_score == 0.0 {
25            last_mod = current_mod;
26
27            match run_score(path) {
28                Ok((score, grade, pt)) => {
29                    let delta = score - last_score;
30                    let delta_str = if last_score == 0.0 {
31                        String::new()
32                    } else if delta > 0.0 {
33                        format!(" ({})", format!("+{:.0}", delta).green())
34                    } else if delta < 0.0 {
35                        format!(" ({})", format!("{:.0}", delta).red())
36                    } else {
37                        String::new()
38                    };
39
40                    let now = chrono_now();
41                    let grade_colored = match grade.as_str() {
42                        "A+" | "A" => grade.green().bold().to_string(),
43                        "B" => grade.yellow().bold().to_string(),
44                        _ => grade.red().to_string(),
45                    };
46
47                    println!(
48                        "  {} {} {}/100 {}{} — {}",
49                        now.dimmed(),
50                        grade_colored,
51                        format!("{:.0}", score).bold(),
52                        pt.dimmed(),
53                        delta_str,
54                        if score >= 90.0 { "Claude Native".green().to_string() }
55                        else if score >= 70.0 { "Claude Friendly".yellow().to_string() }
56                        else { "Needs work".red().to_string() },
57                    );
58
59                    last_score = score;
60                }
61                Err(e) => {
62                    eprintln!("  {} {e}", "Error:".red());
63                }
64            }
65        }
66
67        std::thread::sleep(Duration::from_secs(2));
68    }
69}
70
71fn run_score(path: &Path) -> Result<(f64, String, String)> {
72    let mut ctx = scan::build_context(path)?;
73    let pt = detection::detect(&ctx);
74    ctx.project_type = Some(pt.clone());
75
76    let all = rules::all_rules();
77    let results: Vec<_> = all.iter()
78        .filter(|r| r.applies_to(&pt))
79        .map(|r| r.check(&ctx))
80        .collect();
81
82    let sc = scoring::calculate(results, &pt);
83    Ok((sc.total_score, format!("{}", sc.grade), format!("{}", pt)))
84}
85
86fn get_latest_mtime(path: &Path) -> u64 {
87    let mut latest = 0u64;
88    if let Ok(walker) = walkdir::WalkDir::new(path)
89        .max_depth(4)
90        .into_iter()
91        .collect::<Result<Vec<_>, _>>()
92    {
93        for entry in walker {
94            if let Ok(meta) = entry.metadata() {
95                if let Ok(modified) = meta.modified() {
96                    let secs = modified.duration_since(SystemTime::UNIX_EPOCH)
97                        .unwrap_or_default().as_secs();
98                    if secs > latest { latest = secs; }
99                }
100            }
101        }
102    }
103    latest
104}
105
106fn chrono_now() -> String {
107    // Simple timestamp without chrono dependency
108    let now = SystemTime::now()
109        .duration_since(SystemTime::UNIX_EPOCH)
110        .unwrap_or_default()
111        .as_secs();
112    let secs = now % 60;
113    let mins = (now / 60) % 60;
114    let hours = (now / 3600) % 24;
115    format!("{hours:02}:{mins:02}:{secs:02}")
116}