Skip to main content

agent_runtime/doctor/
project.rs

1//! Project-local overlay coverage for `agent-runtime doctor --check-project`.
2
3use super::{DoctorFinding, DoctorSeverity};
4use std::path::{Path, PathBuf};
5
6const PROJECT_OVERLAY_SCRIPTS: &[&str] =
7    &["bench", "bootstrap", "demo", "deploy", "pre-pr", "release"];
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ProjectOverlayStatus {
11    Wired,
12    Missing,
13}
14
15impl ProjectOverlayStatus {
16    pub fn as_str(self) -> &'static str {
17        match self {
18            ProjectOverlayStatus::Wired => "wired",
19            ProjectOverlayStatus::Missing => "missing",
20        }
21    }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ProjectOverlayFinding {
26    pub script: String,
27    pub status: ProjectOverlayStatus,
28    pub severity: DoctorSeverity,
29    pub path: PathBuf,
30    pub message: String,
31}
32
33impl ProjectOverlayFinding {
34    pub fn to_doctor_finding(&self, product: &str) -> DoctorFinding {
35        let message = format!("status={}: {}", self.status.as_str(), self.message);
36        match self.severity {
37            DoctorSeverity::Ok => DoctorFinding {
38                product: product.to_string(),
39                check: "project-overlay",
40                severity: DoctorSeverity::Ok,
41                entry_id: Some(self.script.clone()),
42                path: Some(self.path.clone()),
43                message,
44            },
45            DoctorSeverity::Warn => DoctorFinding::warn(
46                product,
47                "project-overlay",
48                Some(self.script.clone()),
49                Some(self.path.clone()),
50                message,
51            ),
52            DoctorSeverity::Block => DoctorFinding::block(
53                product,
54                "project-overlay",
55                Some(self.script.clone()),
56                Some(self.path.clone()),
57                message,
58            ),
59        }
60    }
61}
62
63pub fn probe_project(project_root: &Path) -> Vec<ProjectOverlayFinding> {
64    PROJECT_OVERLAY_SCRIPTS
65        .iter()
66        .map(|script| probe_script(project_root, script))
67        .collect()
68}
69
70fn probe_script(project_root: &Path, script: &str) -> ProjectOverlayFinding {
71    let path = project_root
72        .join(".agents")
73        .join("scripts")
74        .join(format!("{script}.sh"));
75    if is_executable_file(&path) {
76        ProjectOverlayFinding {
77            script: script.to_string(),
78            status: ProjectOverlayStatus::Wired,
79            severity: DoctorSeverity::Ok,
80            path,
81            message: "project-local script exists and is executable".to_string(),
82        }
83    } else {
84        let message = if path.exists() {
85            "project-local script exists but is not executable"
86        } else {
87            "project-local script is missing"
88        };
89        ProjectOverlayFinding {
90            script: script.to_string(),
91            status: ProjectOverlayStatus::Missing,
92            severity: DoctorSeverity::Warn,
93            path,
94            message: message.to_string(),
95        }
96    }
97}
98
99fn is_executable_file(path: &Path) -> bool {
100    let Ok(meta) = std::fs::metadata(path) else {
101        return false;
102    };
103    if !meta.is_file() {
104        return false;
105    }
106    #[cfg(unix)]
107    {
108        use std::os::unix::fs::PermissionsExt;
109        meta.permissions().mode() & 0o111 != 0
110    }
111    #[cfg(not(unix))]
112    {
113        true
114    }
115}