agent_runtime/doctor/
project.rs1use 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}