Skip to main content

zoi_cli/cmd/
doctor.rs

1use crate::pkg;
2use anyhow::Result;
3use colored::*;
4
5pub fn run() -> Result<()> {
6    println!("{} Running Zoi doctor...", "::".bold().blue());
7    println!("Checking your system for potential issues...");
8
9    let mut issues_found = 0;
10
11    println!("\n{} Checking for broken symlinks...", "->".bold().cyan());
12    match pkg::doctor::check_broken_symlinks() {
13        Ok(broken_links) => {
14            if broken_links.is_empty() {
15                println!("{}", "No broken symlinks found.".green());
16            } else {
17                issues_found += broken_links.len();
18                println!(
19                    "{}: Found {} broken symlinks:",
20                    "Warning".yellow(),
21                    broken_links.len()
22                );
23                for link in broken_links {
24                    println!("  - {}", link.display());
25                }
26                println!(
27                    "\nConsider running 'zoi uninstall <package>' and reinstalling it for the affected packages."
28                );
29            }
30        }
31        Err(e) => {
32            eprintln!(
33                "{}: Failed to check for broken symlinks: {}",
34                "Error".red(),
35                e
36            );
37            issues_found += 1;
38        }
39    }
40
41    println!("\n{} Checking PATH configuration...", "->".bold().cyan());
42    match pkg::doctor::check_path_configuration() {
43        Ok(Some(warning)) => {
44            issues_found += 1;
45            println!("{}: {}", "Warning".yellow(), warning);
46            println!("Please run 'zoi shell <shell>' to add Zoi's binary directory to your PATH.");
47        }
48        Ok(None) => {
49            println!("{}", "PATH configuration looks good.".green());
50        }
51        Err(e) => {
52            eprintln!(
53                "{}: Failed to check PATH configuration: {}",
54                "Error".red(),
55                e
56            );
57            issues_found += 1;
58        }
59    }
60
61    println!(
62        "\n{} Checking for outdated repositories...",
63        "->".bold().cyan()
64    );
65    match pkg::doctor::check_outdated_repos() {
66        Ok(Some(warning)) => {
67            issues_found += 1;
68            println!("{}: {}", "Warning".yellow(), warning);
69            println!("Consider running 'zoi sync' to update your local package database.");
70        }
71        Ok(None) => {
72            println!("{}", "Repositories look up to date.".green());
73        }
74        Err(e) => {
75            eprintln!("{}: Failed to check repositories: {}", "Error".red(), e);
76            issues_found += 1;
77        }
78    }
79
80    println!(
81        "\n{} Checking for duplicate package IDs...",
82        "->".bold().cyan()
83    );
84    match pkg::doctor::check_duplicate_packages() {
85        Ok(duplicates) => {
86            if duplicates.is_empty() {
87                println!("{}", "No duplicate package IDs found.".green());
88            } else {
89                issues_found += duplicates.len();
90                println!(
91                    "{}: Found {} duplicate package IDs across registries:",
92                    "Warning".yellow(),
93                    duplicates.len()
94                );
95                for (pkg_id, registries) in duplicates {
96                    println!(
97                        "  - {} (found in: {})",
98                        pkg_id.cyan(),
99                        registries.join(", ")
100                    );
101                }
102                println!(
103                    "\nThis may cause ambiguity during installation. Consider specifying the registry handle (e.g. #registry@repo/name)."
104                );
105            }
106        }
107        Err(e) => {
108            eprintln!("{}: Failed to check for duplicates: {}", "Error".red(), e);
109            issues_found += 1;
110        }
111    }
112
113    println!("\n{} Checking PGP configurations...", "->".bold().cyan());
114    match pkg::doctor::check_pgp_configuration() {
115        Ok(missing_keys) => {
116            if missing_keys.is_empty() {
117                println!("{}", "PGP configuration looks valid.".green());
118            } else {
119                issues_found += missing_keys.len();
120                println!(
121                    "{}: The following trusted PGP keys are missing from your keyring:",
122                    "Warning".yellow()
123                );
124                for key in missing_keys {
125                    println!("  - {}", key.red());
126                }
127                println!("\nRun 'zoi pgp add --name <name> --url <url>' to add missing keys.");
128            }
129        }
130        Err(e) => {
131            eprintln!(
132                "{}: Failed to check PGP configuration: {}",
133                "Error".red(),
134                e
135            );
136            issues_found += 1;
137        }
138    }
139
140    println!("\n{} Validating zoi.lock integrity...", "->".bold().cyan());
141    match pkg::doctor::validate_lockfile_integrity() {
142        Ok(missing_packages) => {
143            if missing_packages.is_empty() {
144                println!("{}", "zoi.lock integrity is good.".green());
145            } else {
146                issues_found += missing_packages.len();
147                println!(
148                    "{}: The following packages are recorded but missing from the store:",
149                    "Warning".yellow()
150                );
151                for pkg in missing_packages {
152                    println!("  - {}", pkg.red());
153                }
154                println!(
155                    "\nYour package record file is out of sync with the actual installation store."
156                );
157            }
158        }
159        Err(e) => {
160            eprintln!("{}: Failed to validate zoi.lock: {}", "Error".red(), e);
161            issues_found += 1;
162        }
163    }
164
165    println!("\n{} Checking for orphaned packages...", "->".bold().cyan());
166    match pkg::doctor::check_orphaned_packages() {
167        Ok(orphaned) => {
168            if orphaned.is_empty() {
169                println!("{}", "No orphaned packages found.".green());
170            } else {
171                issues_found += orphaned.len();
172                println!(
173                    "{}: Found {} orphaned packages (unused dependencies):",
174                    "Warning".yellow(),
175                    orphaned.len()
176                );
177                for pkg in orphaned {
178                    println!("  - {}", pkg.cyan());
179                }
180                println!("\nConsider running 'zoi autoremove' to clean up these packages.");
181            }
182        }
183        Err(e) => {
184            eprintln!(
185                "{}: Failed to check for orphaned packages: {}",
186                "Error".red(),
187                e
188            );
189            issues_found += 1;
190        }
191    }
192
193    println!("\n{} Checking for ghost dependents...", "->".bold().cyan());
194    match pkg::doctor::check_ghost_dependents() {
195        Ok(ghost_links) => {
196            if ghost_links.is_empty() {
197                println!("{}", "No ghost dependents found.".green());
198            } else {
199                issues_found += ghost_links.len();
200                println!(
201                    "{}: Found {} broken dependent links (ghost parents):",
202                    "Warning".yellow(),
203                    ghost_links.len()
204                );
205                for (_, parent_id) in &ghost_links {
206                    println!("  - parent missing: {}", parent_id.cyan());
207                }
208
209                if crate::utils::ask_for_confirmation(
210                    "\nDo you want to prune these broken links?",
211                    false,
212                ) {
213                    pkg::doctor::prune_ghost_dependents(&ghost_links)?;
214                    println!("{}", "Successfully pruned broken links.".green());
215                    issues_found -= ghost_links.len();
216                } else {
217                    println!("Broken links were NOT pruned.");
218                }
219            }
220        }
221        Err(e) => {
222            eprintln!(
223                "{}: Failed to check for ghost dependents: {}",
224                "Error".red(),
225                e
226            );
227            issues_found += 1;
228        }
229    }
230
231    println!("\n{} Checking for external tools...", "->".bold().cyan());
232    let tool_results = pkg::doctor::check_external_tools();
233    if tool_results.essential_missing.is_empty() && tool_results.recommended_missing.is_empty() {
234        println!(
235            "{}",
236            "All essential and recommended tools are installed.".green()
237        );
238    } else {
239        if !tool_results.essential_missing.is_empty() {
240            issues_found += tool_results.essential_missing.len();
241            println!("{}: Essential tools are missing:", "Error".red().bold());
242            for tool in tool_results.essential_missing {
243                println!("  - {}", tool.red());
244            }
245            println!(
246                "Please install these tools as they are required for Zoi to function correctly."
247            );
248        }
249        if !tool_results.recommended_missing.is_empty() {
250            println!("{}: Recommended tools are missing:", "Note".yellow().bold());
251            for tool in tool_results.recommended_missing {
252                println!("  - {}", tool.yellow());
253            }
254            println!("Zoi will work without these, but some features may be limited.");
255        }
256    }
257
258    if issues_found == 0 {
259        println!(
260            "\n{}",
261            "Zoi is looking healthy! No issues found.".green().bold()
262        );
263    } else {
264        println!("\nFound {} potential issues.", issues_found);
265    }
266
267    Ok(())
268}