Skip to main content

tuff_core/
check.rs

1use std::path::Path;
2
3use serde::Serialize;
4
5use crate::error::Result;
6use crate::lockfile;
7use crate::manifest::CapabilityType;
8
9#[derive(Debug, Serialize)]
10pub struct CheckResult {
11    pub id: String,
12    #[serde(rename = "type")]
13    pub capability_type: CapabilityType,
14    pub target: String,
15    pub status: String,
16    #[serde(skip_serializing_if = "Vec::is_empty")]
17    pub files: Vec<String>,
18}
19
20#[derive(Debug, Serialize)]
21pub struct CheckOutcome {
22    pub valid: bool,
23    pub results: Vec<CheckResult>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum CheckScope {
28    ProjectAndGlobal,
29    Global,
30}
31
32pub fn run_checks(repo_root: &Path, scope: CheckScope) -> Result<CheckOutcome> {
33    let mut results = Vec::new();
34
35    if scope == CheckScope::ProjectAndGlobal
36        && let Ok(lf) = lockfile::require_lockfile(repo_root)
37    {
38        check_lockfile(repo_root, &lf, &mut results);
39    }
40
41    if let Some(home) = home_dir() {
42        let lock_path = crate::paths::global_lockfile(&home);
43        if let Ok(lf) = lockfile::read_lockfile_at(&lock_path) {
44            check_lockfile(&home, &lf, &mut results);
45        }
46    }
47
48    let valid = results.iter().all(|r| r.status == "ok");
49    Ok(CheckOutcome { valid, results })
50}
51
52fn check_lockfile(scope_root: &Path, lf: &lockfile::Lockfile, results: &mut Vec<CheckResult>) {
53    for (id, entry) in lf.capabilities.iter() {
54        for (target_id, target_entry) in entry.targets.iter() {
55            let mut failing_files = Vec::new();
56
57            if target_entry.installed_path.is_empty() {
58                failing_files.push(id.clone());
59            } else {
60                let path = scope_root.join(&target_entry.installed_path);
61                match crate::cache::hash_tree(&path) {
62                    Ok(hash) if hash == target_entry.sha256 => {}
63                    Ok(_) | Err(_) => failing_files.push(target_entry.installed_path.clone()),
64                }
65            }
66
67            for emitted in &target_entry.emitted_files {
68                let file_path = scope_root.join(&emitted.path);
69
70                if !file_path.exists() {
71                    failing_files.push(emitted.path.clone());
72                    continue;
73                }
74
75                if let Ok(content) = std::fs::read(&file_path) {
76                    let hash = lockfile::hash_bytes(&content);
77                    if hash != emitted.hash {
78                        failing_files.push(emitted.path.clone());
79                    }
80                }
81            }
82
83            for hook in &target_entry.managed_hooks {
84                if lockfile::managed_hook_status(scope_root, hook) != "clean" {
85                    failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
86                }
87            }
88
89            if let Some(managed_entry) = &target_entry.managed_mcp_entry
90                && lockfile::managed_mcp_entry_status(scope_root, id, managed_entry) != "clean"
91            {
92                failing_files.push(format!("{}#{}", managed_entry.config_path, id));
93            }
94
95            let status = if failing_files.is_empty() {
96                "ok"
97            } else {
98                "modified"
99            };
100
101            results.push(CheckResult {
102                id: id.clone(),
103                capability_type: entry.capability_type,
104                target: target_id.clone(),
105                status: status.to_string(),
106                files: failing_files,
107            });
108        }
109    }
110}
111
112fn home_dir() -> Option<std::path::PathBuf> {
113    std::env::var("HOME").ok().map(std::path::PathBuf::from)
114}