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(repo_info: &RepoInfo, current_version: &str) -> Result<Option<String>, String> {
100 let client = reqwest::blocking::Client::builder()
101 .user_agent(format!("{}-doctor", repo_info.name))
102 .timeout(std::time::Duration::from_secs(5))
103 .build()
104 .map_err(|e| e.to_string())?;
105
106 let response: serde_json::Value = client
107 .get(repo_info.latest_release_url())
108 .send()
109 .map_err(|e| e.to_string())?
110 .json()
111 .map_err(|e| e.to_string())?;
112
113 let tag_name = response["tag_name"]
114 .as_str()
115 .ok_or_else(|| "No tag_name in response".to_string())?;
116
117 let latest = tag_name
118 .trim_start_matches(repo_info.tag_prefix)
119 .trim_start_matches('v');
120
121 if latest == current_version {
122 Ok(None)
123 } else {
124 Ok(Some(latest.to_string()))
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 struct TestTool;
133
134 impl DoctorChecks for TestTool {
135 fn repo_info() -> RepoInfo {
136 RepoInfo::new("workhelix", "test-tool", "test-tool-v")
137 }
138
139 fn current_version() -> &'static str {
140 "1.0.0"
141 }
142
143 fn tool_checks(&self) -> Vec<DoctorCheck> {
144 vec![
145 DoctorCheck::pass("Test check 1"),
146 DoctorCheck::fail("Test check 2", "This is a failure"),
147 ]
148 }
149 }
150
151 #[test]
152 fn test_run_doctor() {
153 let tool = TestTool;
154 let exit_code = run_doctor(&tool);
155 assert_eq!(exit_code, 1);
157 }
158
159 #[test]
160 fn test_check_for_updates_handles_errors() {
161 let repo = RepoInfo::new("nonexistent", "repo", "v");
162 let result = check_for_updates(&repo, "1.0.0");
163 assert!(result.is_ok() || result.is_err());
165 }
166}