1use anyhow::Result;
9use std::path::Path;
10use tracing::{error, info};
11
12use super::{
13 cert::generate_certificates, config::generate_local_config, install::LoreInstaller,
14 manager::ServerManager, version::verify_lore_installation,
15};
16
17pub struct NapDoctor {
19 nap_home: std::path::PathBuf,
20}
21
22impl NapDoctor {
23 pub fn new(nap_home: &Path) -> Self {
25 Self {
26 nap_home: nap_home.to_path_buf(),
27 }
28 }
29
30 pub async fn diagnose(&self) -> Result<DoctorReport> {
32 info!("Running NAP doctor diagnostics");
33
34 let mut checks = vec![];
35
36 checks.push(self.check_nap_configuration());
38
39 checks.push(self.check_lore_installation());
41
42 checks.push(self.check_lore_configuration());
44
45 checks.push(self.check_lore_certificates());
47
48 checks.push(self.check_lore_server_status().await);
50
51 checks.push(self.check_store_directories());
53
54 checks.push(self.check_provider_connectivity().await);
56
57 let report = DoctorReport {
58 checks,
59 nap_home: self.nap_home.clone(),
60 };
61
62 info!("Diagnostics complete: {} checks", report.checks.len());
63 Ok(report)
64 }
65
66 pub async fn repair(&self, report: &DoctorReport) -> Result<RepairReport> {
68 info!("Starting repair based on diagnostics");
69
70 let mut repairs = vec![];
71
72 for check in &report.checks {
73 if !check.passed {
74 match self.repair_check(check).await {
75 Ok(repair_result) => {
76 repairs.push(repair_result);
77 }
78 Err(e) => {
79 error!("Failed to repair {}: {}", check.name, e);
80 repairs.push(RepairResult {
81 check_name: check.name.clone(),
82 success: false,
83 message: format!("Repair failed: {}", e),
84 });
85 }
86 }
87 }
88 }
89
90 let repair_report = RepairReport { repairs };
91 info!(
92 "Repair complete: {} repairs attempted",
93 repair_report.repairs.len()
94 );
95 Ok(repair_report)
96 }
97
98 fn check_nap_configuration(&self) -> CheckResult {
100 let name = "NAP Configuration";
101
102 let config_exists = self.nap_home.exists();
103 let config_dir = self.nap_home.join("lore").join("config");
104 let lore_config_exists = config_dir.exists();
105
106 if config_exists && lore_config_exists {
107 CheckResult {
108 name: name.to_string(),
109 passed: true,
110 message: "NAP configuration exists".to_string(),
111 severity: CheckSeverity::Info,
112 }
113 } else {
114 CheckResult {
115 name: name.to_string(),
116 passed: false,
117 message: "NAP configuration missing".to_string(),
118 severity: CheckSeverity::Error,
119 }
120 }
121 }
122
123 fn check_lore_installation(&self) -> CheckResult {
125 let name = "Lore Installation";
126
127 match verify_lore_installation() {
128 Ok(status) => {
129 if status.is_fully_compatible() {
130 CheckResult {
131 name: name.to_string(),
132 passed: true,
133 message: format!("Lore {} installed and compatible", status.pinned_version),
134 severity: CheckSeverity::Info,
135 }
136 } else {
137 CheckResult {
138 name: name.to_string(),
139 passed: false,
140 message: status.status_message(),
141 severity: CheckSeverity::Error,
142 }
143 }
144 }
145 Err(e) => CheckResult {
146 name: name.to_string(),
147 passed: false,
148 message: format!("Failed to check Lore installation: {}", e),
149 severity: CheckSeverity::Error,
150 },
151 }
152 }
153
154 fn check_lore_configuration(&self) -> CheckResult {
156 let name = "Lore Configuration";
157
158 let config_path = self.nap_home.join("lore").join("config").join("local.toml");
159
160 if config_path.exists() {
161 CheckResult {
162 name: name.to_string(),
163 passed: true,
164 message: "Lore configuration exists".to_string(),
165 severity: CheckSeverity::Info,
166 }
167 } else {
168 CheckResult {
169 name: name.to_string(),
170 passed: false,
171 message: "Lore configuration missing".to_string(),
172 severity: CheckSeverity::Warning,
173 }
174 }
175 }
176
177 fn check_lore_certificates(&self) -> CheckResult {
179 let name = "Lore Certificates";
180
181 let cert_dir = self.nap_home.join("lore").join("certs");
182 let cert_path = cert_dir.join("cert.pem");
183 let key_path = cert_dir.join("key.pem");
184
185 if cert_path.exists() && key_path.exists() {
186 CheckResult {
187 name: name.to_string(),
188 passed: true,
189 message: "Lore certificates exist".to_string(),
190 severity: CheckSeverity::Info,
191 }
192 } else {
193 CheckResult {
194 name: name.to_string(),
195 passed: false,
196 message: "Lore certificates missing".to_string(),
197 severity: CheckSeverity::Warning,
198 }
199 }
200 }
201
202 async fn check_lore_server_status(&self) -> CheckResult {
204 let name = "Lore Server Status";
205
206 let server_manager = ServerManager::new(&self.nap_home);
207
208 match server_manager.status().await {
209 Ok(status) => {
210 if status.is_ready() {
211 CheckResult {
212 name: name.to_string(),
213 passed: true,
214 message: format!("Lore server running on port {}", status.http_port),
215 severity: CheckSeverity::Info,
216 }
217 } else {
218 CheckResult {
219 name: name.to_string(),
220 passed: false,
221 message: status.status_message(),
222 severity: CheckSeverity::Warning,
223 }
224 }
225 }
226 Err(e) => CheckResult {
227 name: name.to_string(),
228 passed: false,
229 message: format!("Failed to check server status: {}", e),
230 severity: CheckSeverity::Error,
231 },
232 }
233 }
234
235 fn check_store_directories(&self) -> CheckResult {
237 let name = "Store Directories";
238
239 let immutable_dir = self.nap_home.join("lore").join("store").join("immutable");
240 let mutable_dir = self.nap_home.join("lore").join("store").join("mutable");
241
242 if immutable_dir.exists() && mutable_dir.exists() {
243 CheckResult {
244 name: name.to_string(),
245 passed: true,
246 message: "Store directories exist".to_string(),
247 severity: CheckSeverity::Info,
248 }
249 } else {
250 CheckResult {
251 name: name.to_string(),
252 passed: false,
253 message: "Store directories missing".to_string(),
254 severity: CheckSeverity::Warning,
255 }
256 }
257 }
258
259 async fn check_provider_connectivity(&self) -> CheckResult {
261 let name = "Provider Connectivity";
262
263 match reqwest::get("http://127.0.0.1:41339/health_check").await {
265 Ok(resp) => {
266 if resp.status().is_success() {
267 CheckResult {
268 name: name.to_string(),
269 passed: true,
270 message: "Provider connectivity OK".to_string(),
271 severity: CheckSeverity::Info,
272 }
273 } else {
274 CheckResult {
275 name: name.to_string(),
276 passed: false,
277 message: format!("Provider returned status: {}", resp.status()),
278 severity: CheckSeverity::Warning,
279 }
280 }
281 }
282 Err(e) => CheckResult {
283 name: name.to_string(),
284 passed: false,
285 message: format!("Provider connectivity failed: {}", e),
286 severity: CheckSeverity::Warning,
287 },
288 }
289 }
290
291 async fn repair_check(&self, check: &CheckResult) -> Result<RepairResult> {
293 match check.name.as_str() {
294 "NAP Configuration" => {
295 std::fs::create_dir_all(&self.nap_home)?;
296 Ok(RepairResult {
297 check_name: check.name.clone(),
298 success: true,
299 message: "Created NAP home directory".to_string(),
300 })
301 }
302 "Lore Installation" => {
303 let install_dir = dirs::home_dir().unwrap().join(".local").join("bin");
304 let installer = LoreInstaller::new(&install_dir);
305 installer.install_all()?;
306 Ok(RepairResult {
307 check_name: check.name.clone(),
308 success: true,
309 message: "Installed Lore CLI and server".to_string(),
310 })
311 }
312 "Lore Configuration" => {
313 generate_local_config(&self.nap_home)?;
314 Ok(RepairResult {
315 check_name: check.name.clone(),
316 success: true,
317 message: "Generated Lore configuration".to_string(),
318 })
319 }
320 "Lore Certificates" => {
321 let cert_dir = self.nap_home.join("lore").join("certs");
322 generate_certificates(&cert_dir)?;
323 Ok(RepairResult {
324 check_name: check.name.clone(),
325 success: true,
326 message: "Generated Lore certificates".to_string(),
327 })
328 }
329 "Lore Server Status" => {
330 let server_manager = ServerManager::new(&self.nap_home);
331 server_manager.ensure_running().await?;
332 Ok(RepairResult {
333 check_name: check.name.clone(),
334 success: true,
335 message: "Started Lore server".to_string(),
336 })
337 }
338 "Store Directories" => {
339 let immutable_dir = self.nap_home.join("lore").join("store").join("immutable");
340 let mutable_dir = self.nap_home.join("lore").join("store").join("mutable");
341 std::fs::create_dir_all(&immutable_dir)?;
342 std::fs::create_dir_all(&mutable_dir)?;
343 Ok(RepairResult {
344 check_name: check.name.clone(),
345 success: true,
346 message: "Created store directories".to_string(),
347 })
348 }
349 _ => Ok(RepairResult {
350 check_name: check.name.clone(),
351 success: false,
352 message: "No repair available for this check".to_string(),
353 }),
354 }
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct CheckResult {
361 pub name: String,
362 pub passed: bool,
363 pub message: String,
364 pub severity: CheckSeverity,
365}
366
367#[derive(Debug, Clone, PartialEq)]
369pub enum CheckSeverity {
370 Info,
371 Warning,
372 Error,
373}
374
375#[derive(Debug, Clone)]
377pub struct DoctorReport {
378 pub checks: Vec<CheckResult>,
379 pub nap_home: std::path::PathBuf,
380}
381
382impl DoctorReport {
383 pub fn overall_health(&self) -> HealthStatus {
385 let has_errors = self
386 .checks
387 .iter()
388 .any(|c| c.severity == CheckSeverity::Error && !c.passed);
389 let has_warnings = self
390 .checks
391 .iter()
392 .any(|c| c.severity == CheckSeverity::Warning && !c.passed);
393
394 if has_errors {
395 HealthStatus::Unhealthy
396 } else if has_warnings {
397 HealthStatus::Degraded
398 } else {
399 HealthStatus::Healthy
400 }
401 }
402
403 pub fn summary(&self) -> String {
405 let passed = self.checks.iter().filter(|c| c.passed).count();
406 let total = self.checks.len();
407 format!("{} / {} checks passed", passed, total)
408 }
409}
410
411#[derive(Debug, Clone, PartialEq)]
413pub enum HealthStatus {
414 Healthy,
415 Degraded,
416 Unhealthy,
417}
418
419#[derive(Debug, Clone)]
421pub struct RepairResult {
422 pub check_name: String,
423 pub success: bool,
424 pub message: String,
425}
426
427#[derive(Debug, Clone)]
429pub struct RepairReport {
430 pub repairs: Vec<RepairResult>,
431}
432
433impl RepairReport {
434 pub fn successful_count(&self) -> usize {
436 self.repairs.iter().filter(|r| r.success).count()
437 }
438
439 pub fn failed_count(&self) -> usize {
441 self.repairs.iter().filter(|r| !r.success).count()
442 }
443
444 pub fn summary(&self) -> String {
446 format!(
447 "{} successful, {} failed",
448 self.successful_count(),
449 self.failed_count()
450 )
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use tempfile::TempDir;
458
459 #[test]
460 fn test_doctor_creation() {
461 let temp_dir = TempDir::new().unwrap();
462 let doctor = NapDoctor::new(temp_dir.path());
463 assert_eq!(doctor.nap_home, temp_dir.path());
464 }
465
466 #[test]
467 fn test_check_result() {
468 let check = CheckResult {
469 name: "Test Check".to_string(),
470 passed: true,
471 message: "Test passed".to_string(),
472 severity: CheckSeverity::Info,
473 };
474 assert!(check.passed);
475 assert_eq!(check.severity, CheckSeverity::Info);
476 }
477
478 #[test]
479 fn test_health_status() {
480 let report = DoctorReport {
481 checks: vec![
482 CheckResult {
483 name: "Check 1".to_string(),
484 passed: true,
485 message: "OK".to_string(),
486 severity: CheckSeverity::Info,
487 },
488 CheckResult {
489 name: "Check 2".to_string(),
490 passed: true,
491 message: "OK".to_string(),
492 severity: CheckSeverity::Info,
493 },
494 ],
495 nap_home: std::path::PathBuf::from("/tmp"),
496 };
497 assert_eq!(report.overall_health(), HealthStatus::Healthy);
498 }
499}