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
26pub fn run_checks(repo_root: &Path) -> Result<CheckOutcome> {
27 let mut results = Vec::new();
28
29 if let Ok(lf) = lockfile::require_lockfile(repo_root) {
31 check_lockfile(repo_root, &lf, &mut results);
32 }
33
34 if let Some(home) = home_dir() {
36 let lock_path = crate::paths::global_lockfile(&home);
37 if let Ok(lf) = lockfile::read_lockfile_at(&lock_path) {
38 check_lockfile(&home, &lf, &mut results);
39 }
40 }
41
42 let valid = results.iter().all(|r| r.status == "ok");
43 Ok(CheckOutcome { valid, results })
44}
45
46fn check_lockfile(scope_root: &Path, lf: &lockfile::Lockfile, results: &mut Vec<CheckResult>) {
47 for (id, entry) in lf.capabilities.iter() {
48 for (target_id, target_entry) in entry.targets.iter() {
49 let mut failing_files = Vec::new();
50
51 if target_entry.installed_path.is_empty() {
52 failing_files.push(id.clone());
53 } else {
54 let path = scope_root.join(&target_entry.installed_path);
55 match crate::cache::hash_tree(&path) {
56 Ok(hash) if hash == target_entry.sha256 => {}
57 Ok(_) | Err(_) => failing_files.push(target_entry.installed_path.clone()),
58 }
59 }
60
61 for emitted in &target_entry.emitted_files {
62 let file_path = scope_root.join(&emitted.path);
63
64 if !file_path.exists() {
65 failing_files.push(emitted.path.clone());
66 continue;
67 }
68
69 if let Ok(content) = std::fs::read(&file_path) {
70 let hash = lockfile::hash_bytes(&content);
71 if hash != emitted.hash {
72 failing_files.push(emitted.path.clone());
73 }
74 }
75 }
76
77 for hook in &target_entry.managed_hooks {
78 if lockfile::managed_hook_status(scope_root, hook) != "clean" {
79 failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
80 }
81 }
82
83 let status = if failing_files.is_empty() {
84 "ok"
85 } else {
86 "modified"
87 };
88
89 results.push(CheckResult {
90 id: id.clone(),
91 capability_type: entry.capability_type,
92 target: target_id.clone(),
93 status: status.to_string(),
94 files: failing_files,
95 });
96 }
97 }
98}
99
100fn home_dir() -> Option<std::path::PathBuf> {
101 std::env::var("HOME").ok().map(std::path::PathBuf::from)
102}