1use anyhow::Result;
4use colored::Colorize;
5
6use crate::pkg;
7
8pub 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}