workhelix_cli_common/
doctor.rs1use crate::types::{DoctorCheck, RepoInfo};
7
8pub trait DoctorChecks {
12 fn repo_info() -> RepoInfo;
14
15 fn current_version() -> &'static str;
17
18 fn tool_checks(&self) -> Vec<DoctorCheck> {
22 Vec::new()
23 }
24}
25
26pub fn run_doctor<T: DoctorChecks>(tool: &T) -> i32 {
33 let tool_name = T::repo_info().name;
34 println!("🏥 {tool_name} health check");
35 println!("{}", "=".repeat(tool_name.len() + 14));
36 println!();
37
38 let mut has_errors = false;
39 let mut has_warnings = false;
40
41 let tool_checks = tool.tool_checks();
43 if !tool_checks.is_empty() {
44 println!("Configuration:");
45 for check in tool_checks {
46 if check.passed {
47 println!(" ✅ {}", check.name);
48 } else {
49 println!(" ❌ {}", check.name);
50 if let Some(msg) = check.message {
51 println!(" {msg}");
52 }
53 has_errors = true;
54 }
55 }
56 println!();
57 }
58
59 println!("Updates:");
61 match check_for_updates(&T::repo_info(), T::current_version()) {
62 Ok(Some(latest)) => {
63 let current = T::current_version();
64 println!(" ⚠️ Update available: v{latest} (current: v{current})");
65 println!(" 💡 Run '{tool_name} update' to install the latest version");
66 has_warnings = true;
67 }
68 Ok(None) => {
69 println!(" ✅ Running latest version (v{})", T::current_version());
70 }
71 Err(e) => {
72 println!(" ⚠️ Failed to check for updates: {e}");
73 has_warnings = true;
74 }
75 }
76
77 println!();
78
79 if has_errors {
81 println!("❌ Issues found - see above for details");
82 1
83 } else if has_warnings {
84 println!("⚠️ Warnings found");
85 0 } else {
87 println!("✨ Everything looks healthy!");
88 0
89 }
90}
91
92pub fn check_for_updates(
100 repo_info: &RepoInfo,
101 current_version: &str,
102) -> Result<Option<String>, String> {
103 let client = reqwest::blocking::Client::builder()
104 .user_agent(format!("{}-doctor", repo_info.name))
105 .timeout(std::time::Duration::from_secs(5))
106 .build()
107 .map_err(|e| e.to_string())?;
108
109 let response: serde_json::Value = client
110 .get(repo_info.latest_release_url())
111 .send()
112 .map_err(|e| e.to_string())?
113 .json()
114 .map_err(|e| e.to_string())?;
115
116 let tag_name = response["tag_name"]
117 .as_str()
118 .ok_or_else(|| "No tag_name in response".to_string())?;
119
120 let latest = tag_name
121 .trim_start_matches(repo_info.tag_prefix)
122 .trim_start_matches('v');
123
124 if latest == current_version {
125 Ok(None)
126 } else {
127 Ok(Some(latest.to_string()))
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 struct TestTool;
136
137 impl DoctorChecks for TestTool {
138 fn repo_info() -> RepoInfo {
139 RepoInfo::new("workhelix", "test-tool", "test-tool-v")
140 }
141
142 fn current_version() -> &'static str {
143 "1.0.0"
144 }
145
146 fn tool_checks(&self) -> Vec<DoctorCheck> {
147 vec![
148 DoctorCheck::pass("Test check 1"),
149 DoctorCheck::fail("Test check 2", "This is a failure"),
150 ]
151 }
152 }
153
154 #[test]
155 fn test_run_doctor() {
156 let tool = TestTool;
157 let exit_code = run_doctor(&tool);
158 assert_eq!(exit_code, 1);
160 }
161
162 #[test]
163 fn test_check_for_updates_handles_errors() {
164 let repo = RepoInfo::new("nonexistent", "repo", "v");
165 let result = check_for_updates(&repo, "1.0.0");
166 assert!(result.is_ok() || result.is_err());
168 }
169}