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 Some(lf) = lockfile::read_optional_lockfile(&lockfile::project_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 Some(lf) = lockfile::read_optional_lockfile(&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 hook in &target_entry.managed_hooks {
68 if lockfile::managed_hook_status(scope_root, hook) != "clean" {
69 failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
70 }
71 }
72
73 if let Some(managed_entry) = &target_entry.managed_mcp_entry
74 && lockfile::managed_mcp_entry_status(scope_root, id, managed_entry) != "clean"
75 {
76 failing_files.push(format!("{}#{}", managed_entry.config_path, id));
77 }
78
79 let status = if failing_files.is_empty() {
80 "ok"
81 } else {
82 "modified"
83 };
84
85 results.push(CheckResult {
86 id: id.clone(),
87 capability_type: entry.capability_type,
88 target: target_id.clone(),
89 status: status.to_string(),
90 files: failing_files,
91 });
92 }
93 }
94}
95
96fn home_dir() -> Option<std::path::PathBuf> {
97 std::env::var("HOME").ok().map(std::path::PathBuf::from)
98}