Skip to main content

zoi_cli/cmd/
doctor.rs

1//! Implementation of the `doctor` command for diagnosing system issues.
2
3use anyhow::Result;
4use colored::Colorize;
5
6use crate::pkg;
7
8/// Runs the `doctor` command to diagnose system issues.
9///
10/// # Errors
11///
12/// Returns an error if any system check fails or if pruning ghost dependents
13/// fails.
14pub fn run() -> Result<()> {
15    println!("{} Running Zoi doctor...", "::".bold().blue());
16    println!("Checking your system for potential issues...");
17
18    let mut issues_found = 0;
19
20    println!("\n{} Checking for broken symlinks...", "->".bold().cyan());
21    match pkg::doctor::check_broken_symlinks() {
22        Ok(broken_links) => {
23            if broken_links.is_empty() {
24                println!("{}", "No broken symlinks found.".green());
25            } else {
26                issues_found += broken_links.len();
27                println!(
28                    "{}: Found {} broken symlinks:",
29                    "Warning".yellow(),
30                    broken_links.len()
31                );
32                for link in broken_links {
33                    println!("  - {}", link.display());
34                }
35                println!(
36                    "\nConsider running 'zoi uninstall <package>' and \
37                     reinstalling it for the affected packages."
38                );
39            }
40        }
41        Err(e) => {
42            eprintln!(
43                "{}: Failed to check for broken symlinks: {}",
44                "Error".red(),
45                e
46            );
47            issues_found += 1;
48        }
49    }
50
51    println!("\n{} Checking PATH configuration...", "->".bold().cyan());
52    match pkg::doctor::check_path_configuration() {
53        Ok(Some(warning)) => {
54            issues_found += 1;
55            println!("{}: {}", "Warning".yellow(), warning);
56            println!(
57                "Please run 'zoi shell <shell>' to add Zoi's binary directory \
58                 to your PATH."
59            );
60        }
61        Ok(None) => {
62            println!("{}", "PATH configuration looks good.".green());
63        }
64        Err(e) => {
65            eprintln!(
66                "{}: Failed to check PATH configuration: {}",
67                "Error".red(),
68                e
69            );
70            issues_found += 1;
71        }
72    }
73
74    println!(
75        "\n{} Checking for outdated repositories...",
76        "->".bold().cyan()
77    );
78    match pkg::doctor::check_outdated_repos() {
79        Ok(Some(warning)) => {
80            issues_found += 1;
81            println!("{}: {}", "Warning".yellow(), warning);
82            println!(
83                "Consider running 'zoi sync' to update your local package \
84                 database."
85            );
86        }
87        Ok(None) => {
88            println!("{}", "Repositories look up to date.".green());
89        }
90        Err(e) => {
91            eprintln!("{}: Failed to check repositories: {}", "Error".red(), e);
92            issues_found += 1;
93        }
94    }
95
96    println!(
97        "\n{} Checking for duplicate package IDs...",
98        "->".bold().cyan()
99    );
100    match pkg::doctor::check_duplicate_packages() {
101        Ok(duplicates) => {
102            if duplicates.is_empty() {
103                println!("{}", "No duplicate package IDs found.".green());
104            } else {
105                issues_found += duplicates.len();
106                println!(
107                    "{}: Found {} duplicate package IDs across registries:",
108                    "Warning".yellow(),
109                    duplicates.len()
110                );
111                for (pkg_id, registries) in duplicates {
112                    println!(
113                        "  - {} (found in: {})",
114                        pkg_id.cyan(),
115                        registries.join(", ")
116                    );
117                }
118                println!(
119                    "\nThis may cause ambiguity during installation. Consider \
120                     specifying the registry handle (e.g. \
121                     #registry@repo/name)."
122                );
123            }
124        }
125        Err(e) => {
126            eprintln!(
127                "{}: Failed to check for duplicates: {}",
128                "Error".red(),
129                e
130            );
131            issues_found += 1;
132        }
133    }
134
135    println!("\n{} Checking PGP configurations...", "->".bold().cyan());
136    match pkg::doctor::check_pgp_configuration() {
137        Ok(missing_keys) => {
138            if missing_keys.is_empty() {
139                println!("{}", "PGP configuration looks valid.".green());
140            } else {
141                issues_found += missing_keys.len();
142                println!(
143                    "{}: The following trusted PGP keys are missing from your \
144                     keyring:",
145                    "Warning".yellow()
146                );
147                for key in missing_keys {
148                    println!("  - {}", key.red());
149                }
150                println!(
151                    "\nRun 'zoi pgp add --name <name> --url <url>' to add \
152                     missing keys."
153                );
154            }
155        }
156        Err(e) => {
157            eprintln!(
158                "{}: Failed to check PGP configuration: {}",
159                "Error".red(),
160                e
161            );
162            issues_found += 1;
163        }
164    }
165
166    println!("\n{} Validating zoi.lock integrity...", "->".bold().cyan());
167    match pkg::doctor::validate_lockfile_integrity() {
168        Ok(missing_packages) => {
169            if missing_packages.is_empty() {
170                println!("{}", "zoi.lock integrity is good.".green());
171            } else {
172                issues_found += missing_packages.len();
173                println!(
174                    "{}: The following packages are recorded but missing from \
175                     the store:",
176                    "Warning".yellow()
177                );
178                for pkg in missing_packages {
179                    println!("  - {}", pkg.red());
180                }
181                println!(
182                    "\nYour package record file is out of sync with the \
183                     actual installation store."
184                );
185            }
186        }
187        Err(e) => {
188            eprintln!("{}: Failed to validate zoi.lock: {}", "Error".red(), e);
189            issues_found += 1;
190        }
191    }
192
193    println!("\n{} Checking for orphaned packages...", "->".bold().cyan());
194    match pkg::doctor::check_orphaned_packages() {
195        Ok(orphaned) => {
196            if orphaned.is_empty() {
197                println!("{}", "No orphaned packages found.".green());
198            } else {
199                issues_found += orphaned.len();
200                println!(
201                    "{}: Found {} orphaned packages (unused dependencies):",
202                    "Warning".yellow(),
203                    orphaned.len()
204                );
205                for pkg in orphaned {
206                    println!("  - {}", pkg.cyan());
207                }
208                println!(
209                    "\nConsider running 'zoi autoremove' to clean up these \
210                     packages."
211                );
212            }
213        }
214        Err(e) => {
215            eprintln!(
216                "{}: Failed to check for orphaned packages: {}",
217                "Error".red(),
218                e
219            );
220            issues_found += 1;
221        }
222    }
223
224    println!("\n{} Checking for ghost dependents...", "->".bold().cyan());
225    match pkg::doctor::check_ghost_dependents() {
226        Ok(ghost_links) => {
227            if ghost_links.is_empty() {
228                println!("{}", "No ghost dependents found.".green());
229            } else {
230                issues_found += ghost_links.len();
231                println!(
232                    "{}: Found {} broken dependent links (ghost parents):",
233                    "Warning".yellow(),
234                    ghost_links.len()
235                );
236                for (_, parent_id) in &ghost_links {
237                    println!("  - parent missing: {}", parent_id.cyan());
238                }
239
240                if crate::utils::ask_for_confirmation(
241                    "\nDo you want to prune these broken links?",
242                    false
243                ) {
244                    pkg::doctor::prune_ghost_dependents(&ghost_links)?;
245                    println!("{}", "Successfully pruned broken links.".green());
246                    issues_found -= ghost_links.len();
247                } else {
248                    println!("Broken links were NOT pruned.");
249                }
250            }
251        }
252        Err(e) => {
253            eprintln!(
254                "{}: Failed to check for ghost dependents: {}",
255                "Error".red(),
256                e
257            );
258            issues_found += 1;
259        }
260    }
261
262    println!("\n{} Checking for external tools...", "->".bold().cyan());
263    let tool_results = pkg::doctor::check_external_tools();
264    if tool_results.essential_missing.is_empty()
265        && tool_results.recommended_missing.is_empty()
266    {
267        println!(
268            "{}",
269            "All essential and recommended tools are installed.".green()
270        );
271    } else {
272        if !tool_results.essential_missing.is_empty() {
273            issues_found += tool_results.essential_missing.len();
274            println!("{}: Essential tools are missing:", "Error".red().bold());
275            for tool in tool_results.essential_missing {
276                println!("  - {}", tool.red());
277            }
278            println!(
279                "Please install these tools as they are required for Zoi to \
280                 function correctly."
281            );
282        }
283        if !tool_results.recommended_missing.is_empty() {
284            println!(
285                "{}: Recommended tools are missing:",
286                "Note".yellow().bold()
287            );
288            for tool in tool_results.recommended_missing {
289                println!("  - {}", tool.yellow());
290            }
291            println!(
292                "Zoi will work without these, but some features may be \
293                 limited."
294            );
295        }
296    }
297
298    println!("\n{} Checking for registry drift...", "->".bold().cyan());
299    match pkg::doctor::check_registry_drift() {
300        Ok(drifted) => {
301            if drifted.is_empty() {
302                println!("{}", "No registry drift detected.".green());
303            } else {
304                issues_found += drifted.len();
305                println!(
306                    "{}: Local .pkg.lua files differ from database hash:",
307                    "Warning".yellow()
308                );
309                for d in drifted {
310                    println!("  - {d}");
311                }
312                println!(
313                    "This usually means the package registry was synced but \
314                     the package wasn't updated."
315                );
316            }
317        }
318        Err(e) => {
319            eprintln!("{}: Failed to check registry drift: {e}", "Error".red());
320            issues_found += 1;
321        }
322    }
323
324    println!(
325        "\n{} Checking for lockfile mismatches...",
326        "->".bold().cyan()
327    );
328    match pkg::doctor::check_lockfile_mismatch() {
329        Ok(Some(warning)) => {
330            issues_found += 1;
331            println!("{}: {warning}", "Warning".yellow());
332            println!(
333                "This lockfile was generated for another system and may lead \
334                 to resolution failures."
335            );
336        }
337        Ok(None) => {
338            println!("{}", "No lockfile mismatches found.".green());
339        }
340        Err(e) => {
341            eprintln!(
342                "{}: Failed to check lockfile mismatch: {e}",
343                "Error".red()
344            );
345            issues_found += 1;
346        }
347    }
348
349    if issues_found == 0 {
350        println!(
351            "\n{}",
352            "Zoi is looking healthy! No issues found.".green().bold()
353        );
354    } else {
355        println!("\nFound {issues_found} potential issues.");
356    }
357
358    Ok(())
359}