1pub mod autoremove;
2
3use anyhow::anyhow;
4use colored::*;
5use mlua::Lua;
6use std::fs;
7use std::path::PathBuf;
8use zoi_core::{recorder, sysroot, types, utils as core_utils};
9use zoi_db as db;
10use zoi_deps as dependencies;
11use zoi_hooks as hooks;
12use zoi_resolver::{local, resolve};
13use zoi_telemetry as telemetry;
14
15fn get_bin_root(scope: types::Scope) -> anyhow::Result<PathBuf> {
16 match scope {
17 types::Scope::User => {
18 let home_dir = core_utils::get_user_home()
19 .ok_or_else(|| anyhow!("Could not find home directory."))?;
20 Ok(sysroot::apply_sysroot(home_dir.join(".zoi/pkgs/bin")))
21 }
22 types::Scope::System => {
23 if cfg!(target_os = "windows") {
24 Ok(sysroot::apply_sysroot(PathBuf::from(
25 "C:\\ProgramData\\zoi\\pkgs\\bin",
26 )))
27 } else {
28 Ok(sysroot::apply_sysroot(PathBuf::from("/usr/local/bin")))
29 }
30 }
31 types::Scope::Project => {
32 let current_dir = std::env::current_dir()?;
33 Ok(current_dir.join(".zoi").join("pkgs").join("bin"))
34 }
35 }
36}
37
38fn get_completions_root(scope: types::Scope, shell: &str) -> anyhow::Result<PathBuf> {
39 match scope {
40 types::Scope::User => {
41 let home_dir = core_utils::get_user_home()
42 .ok_or_else(|| anyhow!("Could not find home directory."))?;
43 Ok(sysroot::apply_sysroot(
44 home_dir.join(".zoi/pkgs/shell").join(shell),
45 ))
46 }
47 types::Scope::System => {
48 if cfg!(target_os = "windows") {
49 Ok(sysroot::apply_sysroot(PathBuf::from(format!(
50 "C:\\ProgramData\\zoi\\pkgs\\shell\\{}",
51 shell
52 ))))
53 } else {
54 let base = match shell {
55 "bash" => "/usr/share/bash-completion/completions",
56 "zsh" => "/usr/share/zsh/site-functions",
57 "fish" => "/usr/share/fish/vendor_completions.d",
58 "elvish" => "/usr/share/elvish/lib",
59 _ => "/usr/local/share/zoi/completions",
60 };
61 Ok(sysroot::apply_sysroot(PathBuf::from(base)))
62 }
63 }
64 types::Scope::Project => {
65 let current_dir = std::env::current_dir()?;
66 Ok(current_dir
67 .join(".zoi")
68 .join("pkgs")
69 .join("shell")
70 .join(shell))
71 }
72 }
73}
74
75fn cleanup_service(package_name: &str, scope: types::Scope) -> anyhow::Result<()> {
76 let service_name = format!("zoi-{}", package_name);
77 let is_user = scope != types::Scope::System;
78
79 match std::env::consts::OS {
80 "linux" => {
81 let unit_path = if is_user {
82 let home = core_utils::get_user_home()
83 .ok_or_else(|| anyhow!("Could not find home directory"))?;
84 sysroot::apply_sysroot(
85 home.join(".config/systemd/user")
86 .join(format!("{}.service", service_name)),
87 )
88 } else {
89 sysroot::apply_sysroot(PathBuf::from(format!(
90 "/etc/systemd/system/{}.service",
91 service_name
92 )))
93 };
94 if unit_path.exists() {
95 println!("Removing service unit file: {}", unit_path.display());
96 fs::remove_file(&unit_path).map_err(|e| {
97 anyhow!("Failed to remove unit file: {}: {}", unit_path.display(), e)
98 })?;
99 if std::env::var("ZOI_TEST_SKIP_SERVICE_COMMANDS").is_err() {
100 let mut cmd = std::process::Command::new("systemctl");
101 if is_user {
102 cmd.arg("--user");
103 }
104 cmd.arg("daemon-reload")
105 .status()
106 .map_err(|e| anyhow!("Failed to run systemctl daemon-reload: {}", e))?;
107 }
108 }
109 }
110 "macos" => {
111 let plist_path = if is_user {
112 let home = core_utils::get_user_home()
113 .ok_or_else(|| anyhow!("Could not find home directory"))?;
114 sysroot::apply_sysroot(
115 home.join("Library/LaunchAgents")
116 .join(format!("{}.plist", service_name)),
117 )
118 } else {
119 sysroot::apply_sysroot(PathBuf::from(format!(
120 "/Library/LaunchDaemons/{}.plist",
121 service_name
122 )))
123 };
124 if plist_path.exists() {
125 println!("Removing service plist file: {}", plist_path.display());
126 fs::remove_file(&plist_path).map_err(|e| {
127 anyhow!(
128 "Failed to remove plist file: {}: {}",
129 plist_path.display(),
130 e
131 )
132 })?;
133 }
134 }
135 "windows" => {
136 let exists = {
137 let output = std::process::Command::new("sc")
138 .arg("query")
139 .arg(&service_name)
140 .output()
141 .map_err(|e| anyhow!("Failed to run sc query: {}", e))?;
142 output.status.success()
143 };
144 if std::env::var("ZOI_TEST_SKIP_SERVICE_COMMANDS").is_err() && exists {
145 println!("Removing Windows service: {}", service_name);
146 std::process::Command::new("sc")
147 .arg("delete")
148 .arg(&service_name)
149 .status()
150 .map_err(|e| anyhow!("Failed to run sc delete: {}", e))?;
151 }
152 }
153 _ => {}
154 }
155
156 Ok(())
157}
158
159fn uninstall_collection(
160 pkg: &types::Package,
161 manifest: &types::InstallManifest,
162 scope: types::Scope,
163 registry_handle: Option<String>,
164 yes: bool,
165 quiet: bool,
166 dry_run: bool,
167) -> anyhow::Result<types::InstallManifest> {
168 if !quiet {
169 println!("Uninstalling collection '{}'...", pkg.name.bold());
170 }
171
172 if dry_run {
173 return Ok(manifest.clone());
174 }
175
176 let dependencies_to_uninstall = &manifest.installed_dependencies;
177
178 if dependencies_to_uninstall.is_empty() {
179 if !quiet {
180 println!("Collection has no dependencies to uninstall.");
181 }
182 } else {
183 if !quiet {
184 println!("Uninstalling dependencies of the collection...");
185 }
186 for dep_str in dependencies_to_uninstall {
187 let dep = dependencies::parse_dependency_string(dep_str)?;
188
189 if dep.manager != "zoi" {
190 let prompt = format!(
191 "Uninstall native dependency '{}' ({})?",
192 dep.package.cyan(),
193 dep.manager.yellow()
194 );
195 let warning = "Warning: Zoi cannot track if other non-Zoi applications depend on this package.";
196
197 if yes {
198 if !quiet {
199 println!(
200 "\n{} Uninstalling native dependency: {}...",
201 "::".bold().blue(),
202 dep_str.bold()
203 );
204 println!("{}: {}", "Note".yellow(), warning);
205 }
206 } else if core_utils::ask_for_confirmation(
207 &format!("{}\n {}", prompt, warning.dimmed()),
208 false,
209 ) {
210 if !quiet {
211 println!(
212 "\n{} Uninstalling dependency: {}...",
213 "::".bold().blue(),
214 dep_str.bold()
215 );
216 }
217 } else {
218 if !quiet {
219 println!(
220 "Skipping uninstallation of native dependency: {}",
221 dep.package.yellow()
222 );
223 }
224 continue;
225 }
226 } else {
227 if !quiet {
228 println!(
229 "\n{} Uninstalling zoi dependency: {}...",
230 "::".bold().blue(),
231 dep_str.bold()
232 );
233 }
234 }
235
236 if let Err(e) = dependencies::uninstall_dependency(dep_str, &move |name| {
237 run(name, Some(scope), yes, quiet, dry_run).map(|_| ())
238 }) && !quiet
239 {
240 eprintln!(
241 "Warning: Could not uninstall dependency '{}': {}",
242 dep_str, e
243 );
244 }
245 }
246 }
247
248 let handle = registry_handle.as_deref().unwrap_or("local");
249 let package_dir = local::get_package_dir(scope, handle, &pkg.repo, &pkg.name)?;
250 if package_dir.exists() {
251 let _ = cleanup_service(&pkg.name, scope);
252 fs::remove_dir_all(&package_dir)?;
253 }
254 if let Err(e) = recorder::remove_package_from_record(manifest)
255 && !quiet
256 {
257 eprintln!(
258 "{} Failed to remove package from lockfile: {}",
259 "Warning:".yellow(),
260 e
261 );
262 }
263
264 if let Ok(conn) = db::open_connection("local") {
265 let _ = db::delete_package(&conn, &pkg.name, None, &pkg.repo, Some(scope));
266 }
267
268 if let Ok(true) = telemetry::posthog_capture_event(
269 "uninstall",
270 pkg,
271 env!("CARGO_PKG_VERSION"),
272 registry_handle.as_deref().unwrap_or("local"),
273 None,
274 ) && !quiet
275 {
276 println!("{} telemetry sent", "Info:".green());
277 }
278
279 Ok(manifest.clone())
280}
281
282fn find_installed_manifest(
283 request: &resolve::PackageRequest,
284 scope_override: Option<types::Scope>,
285) -> anyhow::Result<(types::InstallManifest, types::Scope)> {
286 let scopes = if let Some(scope) = scope_override {
287 vec![scope]
288 } else {
289 vec![
290 types::Scope::Project,
291 types::Scope::User,
292 types::Scope::System,
293 ]
294 };
295
296 for scope in scopes {
297 let mut matches = local::find_installed_manifests_matching(request, scope)?;
298 match matches.len() {
299 0 => continue,
300 1 => return Ok((matches.remove(0), scope)),
301 _ => {
302 return Err(anyhow!(
303 "Package '{}' is ambiguous in {:?} scope. Use an explicit source like '#handle@repo/name[:sub]@version'.",
304 request.name,
305 scope
306 ));
307 }
308 }
309 }
310
311 if scope_override.is_some() {
312 Err(anyhow!(
313 "Package '{}' is not installed in the specified scope.",
314 request.name
315 ))
316 } else {
317 Err(anyhow!(
318 "Package '{}' is not installed by Zoi.",
319 request.name
320 ))
321 }
322}
323
324fn load_installed_package(
325 manifest: &types::InstallManifest,
326 yes: bool,
327) -> anyhow::Result<(types::Package, PathBuf)> {
328 let installed_source_path = local::get_package_source_path(manifest)?;
329 if installed_source_path.exists() {
330 let path = installed_source_path
331 .to_str()
332 .ok_or_else(|| anyhow!("Stored package source path contains invalid UTF-8"))?;
333 let mut pkg = zoi_lua::parser::parse_lua_package(
334 path,
335 Some(&manifest.version),
336 Some(manifest.scope),
337 true,
338 )?;
339 pkg.repo = manifest.repo.clone();
340 pkg.scope = manifest.scope;
341 pkg.registry_handle = Some(manifest.registry_handle.clone());
342 pkg.sub_package = manifest.sub_package.clone();
343 return Ok((pkg, installed_source_path));
344 }
345
346 let source = local::installed_manifest_source(manifest);
347 let (mut pkg, _, _, pkg_lua_path, _, _, _) =
348 resolve::resolve_package_and_version(&source, Some(manifest.scope), true, yes)?;
349 pkg.scope = manifest.scope;
350 pkg.sub_package = manifest.sub_package.clone();
351 Ok((pkg, pkg_lua_path))
352}
353
354pub fn run(
368 package_name: &str,
369 scope_override: Option<types::Scope>,
370 yes: bool,
371 quiet: bool,
372 dry_run: bool,
373) -> anyhow::Result<types::InstallManifest> {
374 let request = resolve::parse_source_string(package_name)?;
375 let (manifest, scope) = find_installed_manifest(&request, scope_override)?;
376 let sub_package_to_uninstall = manifest.sub_package.clone();
377 let registry_handle = Some(manifest.registry_handle.clone());
378 let (pkg, pkg_lua_path) = load_installed_package(&manifest, yes)?;
379
380 if pkg.package_type == types::PackageType::Collection {
381 return uninstall_collection(
382 &pkg,
383 &manifest,
384 scope,
385 registry_handle.clone(),
386 yes,
387 quiet,
388 dry_run,
389 );
390 }
391
392 if dry_run {
393 return Ok(manifest);
394 }
395
396 let handle = manifest.registry_handle.as_str();
397 let package_dir = local::get_package_dir(scope, handle, &pkg.repo, &pkg.name)?;
398 let version_dir = package_dir.join(&manifest.version);
399
400 let dependents = local::get_dependents(&package_dir)?;
401 if !dependents.is_empty() {
402 return Err(anyhow::anyhow!(
403 "Cannot uninstall '{}' because other packages depend on it:\n -{}\n\nPlease uninstall these packages first.",
404 pkg.name,
405 dependents.join("\n - ")
406 ));
407 }
408
409 let needs_escalation = scope == types::Scope::System && !core_utils::is_admin();
410
411 if needs_escalation {
412 let escalator = core_utils::get_privilege_escalator()
413 .ok_or_else(|| anyhow!("Root privileges required to remove system package, but neither 'sudo' nor 'doas' was found."))?;
414
415 if !quiet {
416 println!(
417 "{} Escalating to root via {} to remove system package...",
418 "::".bold().blue(),
419 escalator
420 );
421 }
422 let manifest_json = serde_json::to_string(&manifest)?;
423 let mut temp_file = tempfile::NamedTempFile::new()?;
424 use std::io::Write;
425 temp_file.write_all(manifest_json.as_bytes())?;
426 let temp_path = temp_file.path();
427
428 let mut cmd = std::process::Command::new(escalator);
429 cmd.arg(std::env::current_exe()?);
430 cmd.arg("helper").arg("elevate-uninstall");
431 cmd.arg("--manifest-json").arg(temp_path);
432 if yes {
433 cmd.arg("--yes");
434 }
435
436 let status = cmd
437 .status()
438 .map_err(|e| anyhow::anyhow!("Failed to spawn privilege escalator: {}", e))?;
439 if !status.success() {
440 return Err(anyhow::anyhow!("Escalated uninstallation failed."));
441 }
442 } else {
443 if let Some(hooks) = &pkg.hooks
444 && let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PreRemove, scope)
445 {
446 return Err(anyhow::anyhow!("Pre-remove hook failed: {}", e));
447 }
448
449 let lua = Lua::new();
450 zoi_lua::functions::setup_lua_environment(
451 &lua,
452 &core_utils::get_platform()?,
453 Some(&manifest.version),
454 pkg_lua_path.to_str(),
455 None,
456 None,
457 None,
458 sub_package_to_uninstall.as_deref(),
459 Some(scope),
460 None,
461 true,
462 )
463 .map_err(|e| anyhow!(e.to_string()))?;
464 let lua_code = fs::read_to_string(pkg_lua_path)?;
465 lua.load(&lua_code)
466 .exec()
467 .map_err(|e| anyhow!(e.to_string()))?;
468
469 if let Ok(uninstall_fn) = lua.globals().get::<mlua::Function>("uninstall") {
470 if !quiet {
471 println!("Running uninstall() script...");
472 }
473 uninstall_fn
474 .call::<()>(())
475 .map_err(|e| anyhow!(e.to_string()))?;
476 }
477
478 if let Ok(uninstall_ops) = lua.globals().get::<mlua::Table>("__ZoiUninstallOperations") {
479 for op in uninstall_ops.sequence_values::<mlua::Table>() {
480 let op = op.map_err(|e| anyhow!(e.to_string()))?;
481 if let Ok(op_type) = op.get::<String>("op")
482 && op_type == "zrm"
483 {
484 let mut path_to_remove: String =
485 op.get("path").map_err(|e| anyhow!(e.to_string()))?;
486
487 path_to_remove =
488 path_to_remove.replace("${pkgstore}", &version_dir.to_string_lossy());
489
490 if let Some(home_dir) = core_utils::get_user_home() {
491 path_to_remove =
492 path_to_remove.replace("${usrhome}", &home_dir.to_string_lossy());
493 }
494 path_to_remove = path_to_remove.replace(
495 "${usrroot}",
496 &sysroot::apply_sysroot(PathBuf::from("/")).to_string_lossy(),
497 );
498
499 let path = std::path::PathBuf::from(path_to_remove);
500 if path.exists() {
501 if !quiet {
502 println!("Removing {}...", path.display());
503 }
504 if path.is_dir() {
505 fs::remove_dir_all(path)?;
506 } else {
507 fs::remove_file(path)?;
508 }
509 }
510 }
511 }
512 }
513
514 if let Some(backup_files) = &manifest.backup {
515 if !quiet {
516 println!("Saving configuration files...");
517 }
518 for backup_file_rel in backup_files {
519 let expanded_path = zoi_core::utils::expand_placeholders(
520 backup_file_rel,
521 &version_dir,
522 manifest.scope,
523 )?;
524 let backup_src = PathBuf::from(expanded_path);
525
526 if backup_src.exists() {
527 let backup_filename = backup_src
528 .file_name()
529 .ok_or_else(|| anyhow!("Invalid backup source name"))?
530 .to_string_lossy();
531 let backup_dest = version_dir
532 .parent()
533 .ok_or_else(|| anyhow!("version_dir should have a parent (package_dir)"))?
534 .join(format!("{}.zoisave", backup_filename));
535
536 if let Some(p) = backup_dest.parent()
537 && let Err(e) = fs::create_dir_all(p)
538 {
539 if !quiet {
540 eprintln!(
541 "Warning: could not create backup directory {}: {}",
542 p.display(),
543 e
544 );
545 }
546 continue;
547 }
548 if !quiet {
549 println!(
550 "Saving {} to {}",
551 backup_src.display(),
552 backup_dest.display()
553 );
554 }
555 if let Err(e) = fs::copy(&backup_src, &backup_dest) {
557 if !quiet {
558 eprintln!(
559 "Warning: failed to copy backup {}: {}",
560 backup_src.display(),
561 e
562 );
563 }
564 } else {
565 let _ = fs::remove_file(&backup_src);
566 }
567 }
568 }
569 }
570
571 if !quiet {
572 println!(
573 "Uninstalling '{}'...",
574 if let Some(sub) = &manifest.sub_package {
575 format!("{}:{}", pkg.name, sub)
576 } else {
577 pkg.name.clone()
578 }
579 .bold()
580 );
581 }
582
583 if let Some(bins) = &manifest.bins {
584 let bin_root = get_bin_root(scope)?;
585 for bin in bins {
586 let symlink_path = bin_root.join(bin);
587 if symlink_path.is_symlink() || symlink_path.exists() {
588 let other_providers = db::find_provides("local", bin)?;
589 let still_provided = other_providers.iter().any(|(p, _)| {
590 p.name != pkg.name || (p.sub_package != manifest.sub_package)
591 });
592
593 if !still_provided {
594 if !quiet {
595 println!(
596 "Removing shim for {} from {}...",
597 bin.cyan(),
598 symlink_path.display()
599 );
600 }
601 fs::remove_file(&symlink_path)?;
602 } else {
603 if !quiet {
604 println!(
605 "Keeping shim for {} as it is still provided by other packages.",
606 bin.cyan()
607 );
608 }
609 }
610 }
611 }
612 } else if manifest.sub_package.is_none() {
613 let bin = &pkg.name;
614 let symlink_path = get_bin_root(scope)?.join(bin);
615 if symlink_path.is_symlink() || symlink_path.exists() {
616 let other_providers = db::find_provides("local", bin)?;
617 let still_provided = other_providers
618 .iter()
619 .any(|(p, _)| p.name != pkg.name || (p.sub_package != manifest.sub_package));
620
621 if !still_provided {
622 if !quiet {
623 println!(
624 "Removing shim for {} from {}...",
625 bin.cyan(),
626 symlink_path.display()
627 );
628 }
629 fs::remove_file(symlink_path)?;
630 }
631 }
632 }
633
634 if let Some(completions) = &manifest.completions {
635 for completion in completions {
636 let completions_root = get_completions_root(scope, &completion.shell)?;
637 let pkg_dir = completions_root.join(&pkg.name);
638 let symlink_path = pkg_dir.join(&completion.filename);
639 if symlink_path.is_symlink() || symlink_path.exists() {
640 let other_providers = db::find_provides("local", &completion.filename)?;
641 let still_provided = other_providers.iter().any(|(p, _)| {
642 p.name != pkg.name || (p.sub_package != manifest.sub_package)
643 });
644
645 if !still_provided {
646 if !quiet {
647 println!(
648 "Removing {} completion for {} from {}...",
649 completion.shell.cyan(),
650 completion.filename.cyan(),
651 symlink_path.display()
652 );
653 }
654 fs::remove_file(&symlink_path)?;
655 } else if !quiet {
656 println!(
657 "Keeping {} completion for {} as it is still provided by other packages.",
658 completion.shell.cyan(),
659 completion.filename.cyan()
660 );
661 }
662 }
663 }
664
665 let shells: std::collections::HashSet<String> =
666 completions.iter().map(|c| c.shell.clone()).collect();
667 for shell_name in shells {
668 let pkg_dir = get_completions_root(scope, &shell_name)?.join(&pkg.name);
669 if pkg_dir.exists()
670 && fs::read_dir(&pkg_dir)
671 .map(|mut e| e.next().is_none())
672 .unwrap_or(false)
673 {
674 let _ = fs::remove_dir(&pkg_dir);
675 }
676 }
677 }
678
679 let pkg_id_opt = if let Ok(conn) = db::open_connection("local") {
680 db::get_package_id(
681 &conn,
682 &pkg.name,
683 manifest.sub_package.as_deref(),
684 &pkg.repo,
685 handle,
686 )
687 .ok()
688 } else {
689 None
690 };
691
692 for file_path_str in &manifest.installed_files {
693 let expanded = core_utils::expand_placeholders(file_path_str, &version_dir, scope)?;
694 let file_path = PathBuf::from(&expanded);
695
696 if let Some(pkg_id) = pkg_id_opt
697 && let Ok(conn) = db::open_connection("local")
698 && let Ok(true) = db::has_other_owners(&conn, file_path_str, pkg_id)
699 {
700 if !quiet {
701 println!(
702 "Keeping {} as it is still owned by other packages.",
703 file_path_str.dimmed()
704 );
705 }
706 continue;
707 }
708
709 if file_path.exists() {
710 if file_path.is_dir() {
711 if fs::read_dir(&file_path)
713 .map(|mut e| e.next().is_none())
714 .unwrap_or(false)
715 {
716 let _ = fs::remove_dir_all(&file_path);
717 }
718 } else {
719 let _ = fs::remove_file(&file_path);
720 }
721 }
722 }
723
724 let manifest_filename = if let Some(sub) = &manifest.sub_package {
725 format!("manifest-{}.yaml", sub)
726 } else {
727 "manifest.yaml".to_string()
728 };
729 let manifest_path = version_dir.join(manifest_filename);
730 if manifest_path.exists() {
731 fs::remove_file(manifest_path)?;
732 }
733
734 if version_dir.exists() {
735 let mut has_other_manifests = false;
736 if let Ok(entries) = fs::read_dir(&version_dir) {
737 for entry in entries.flatten() {
738 let name = entry.file_name().to_string_lossy().to_string();
739 if name.starts_with("manifest") && name.ends_with(".yaml") {
740 has_other_manifests = true;
741 break;
742 }
743 }
744 }
745 if !has_other_manifests {
746 if !quiet {
747 println!(
748 "Removing empty version directory: {}",
749 version_dir.display()
750 );
751 }
752 fs::remove_dir_all(&version_dir)?;
753 }
754 }
755
756 if package_dir.exists() {
757 let _ = cleanup_service(&pkg.name, scope);
758 let mut has_other_versions = false;
759 if let Ok(entries) = fs::read_dir(&package_dir) {
760 for entry in entries.flatten() {
761 let name = entry.file_name().to_string_lossy().to_string();
762 if name != "latest" && name != "dependents" {
763 has_other_versions = true;
764 break;
765 }
766 }
767 }
768 if !has_other_versions {
769 if !quiet {
770 println!("Removing package store: {}", package_dir.display());
771 }
772 fs::remove_dir_all(&package_dir)?;
773 }
774 }
775
776 let parent_id = format!(
777 "#{}@{}/{}@{}",
778 manifest.registry_handle, manifest.repo, manifest.name, manifest.version
779 );
780 for dep_str in &manifest.installed_dependencies {
781 if let Ok(dep) = dependencies::parse_dependency_string(dep_str)
782 && dep.manager == "zoi"
783 {
784 let dep_req = resolve::parse_source_string(dep.package)?;
785 let dep_matches = local::find_installed_manifests_matching(&dep_req, scope)?;
786 if dep_matches.len() == 1 {
787 let dep_manifest = &dep_matches[0];
788 match local::get_package_dir(
789 dep_manifest.scope,
790 &dep_manifest.registry_handle,
791 &dep_manifest.repo,
792 &dep_manifest.name,
793 ) {
794 Ok(dep_pkg_dir) => {
795 if let Err(e) = local::remove_dependent(&dep_pkg_dir, &parent_id)
796 && !quiet
797 {
798 eprintln!(
799 "Warning: failed to remove dependent link for {}: {}",
800 dep.package, e
801 );
802 }
803 }
804 Err(e) => {
805 if !quiet {
806 eprintln!(
807 "Warning: failed to get package dir for {}: {}",
808 dep.package, e
809 );
810 }
811 }
812 }
813 }
814 }
815 }
816
817 if let Some(hooks) = &pkg.hooks
818 && let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PostRemove, scope)
819 && !quiet
820 {
821 eprintln!("{} post-remove hook failed: {}", "Warning:".yellow(), e);
822 }
823 }
824
825 if let Err(e) = recorder::remove_package_from_record(&manifest)
826 && !quiet
827 {
828 eprintln!(
829 "{} Failed to remove package from lockfile: {}",
830 "Warning:".yellow(),
831 e
832 );
833 }
834
835 if let Ok(conn) = db::open_connection("local") {
836 let _ = db::delete_package(
837 &conn,
838 &pkg.name,
839 sub_package_to_uninstall.as_deref(),
840 &pkg.repo,
841 Some(scope),
842 );
843 }
844
845 if !quiet {
846 println!("Removed manifest for '{}'.", pkg.name);
847 }
848
849 if let Ok(true) = telemetry::posthog_capture_event(
850 "uninstall",
851 &pkg,
852 env!("CARGO_PKG_VERSION"),
853 &manifest.registry_handle,
854 None,
855 ) && !quiet
856 {
857 println!("{} telemetry sent", "Info:".green());
858 }
859
860 Ok(manifest)
861}