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 if !quiet {
413 println!(
414 "{} Escalating to root to remove system package...",
415 "::".bold().blue()
416 );
417 }
418 let manifest_json = serde_json::to_string(&manifest)?;
419 let mut temp_file = tempfile::NamedTempFile::new()?;
420 use std::io::Write;
421 temp_file.write_all(manifest_json.as_bytes())?;
422 let temp_path = temp_file.path();
423
424 let mut cmd = std::process::Command::new("sudo");
425 cmd.arg(std::env::current_exe()?);
426 cmd.arg("helper").arg("elevate-uninstall");
427 cmd.arg("--manifest-json").arg(temp_path);
428 if yes {
429 cmd.arg("--yes");
430 }
431
432 let status = cmd
433 .status()
434 .map_err(|e| anyhow::anyhow!("Failed to spawn sudo: {}", e))?;
435 if !status.success() {
436 return Err(anyhow::anyhow!("Escalated uninstallation failed."));
437 }
438 } else {
439 if let Some(hooks) = &pkg.hooks
440 && let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PreRemove, scope)
441 {
442 return Err(anyhow::anyhow!("Pre-remove hook failed: {}", e));
443 }
444
445 let lua = Lua::new();
446 zoi_lua::functions::setup_lua_environment(
447 &lua,
448 &core_utils::get_platform()?,
449 Some(&manifest.version),
450 pkg_lua_path.to_str(),
451 None,
452 None,
453 None,
454 sub_package_to_uninstall.as_deref(),
455 Some(scope),
456 None,
457 true,
458 )
459 .map_err(|e| anyhow!(e.to_string()))?;
460 let lua_code = fs::read_to_string(pkg_lua_path)?;
461 lua.load(&lua_code)
462 .exec()
463 .map_err(|e| anyhow!(e.to_string()))?;
464
465 if let Ok(uninstall_fn) = lua.globals().get::<mlua::Function>("uninstall") {
466 if !quiet {
467 println!("Running uninstall() script...");
468 }
469 uninstall_fn
470 .call::<()>(())
471 .map_err(|e| anyhow!(e.to_string()))?;
472 }
473
474 if let Ok(uninstall_ops) = lua.globals().get::<mlua::Table>("__ZoiUninstallOperations") {
475 for op in uninstall_ops.sequence_values::<mlua::Table>() {
476 let op = op.map_err(|e| anyhow!(e.to_string()))?;
477 if let Ok(op_type) = op.get::<String>("op")
478 && op_type == "zrm"
479 {
480 let mut path_to_remove: String =
481 op.get("path").map_err(|e| anyhow!(e.to_string()))?;
482
483 path_to_remove =
484 path_to_remove.replace("${pkgstore}", &version_dir.to_string_lossy());
485
486 if let Some(home_dir) = core_utils::get_user_home() {
487 path_to_remove =
488 path_to_remove.replace("${usrhome}", &home_dir.to_string_lossy());
489 }
490 path_to_remove = path_to_remove.replace(
491 "${usrroot}",
492 &sysroot::apply_sysroot(PathBuf::from("/")).to_string_lossy(),
493 );
494
495 let path = std::path::PathBuf::from(path_to_remove);
496 if path.exists() {
497 if !quiet {
498 println!("Removing {}...", path.display());
499 }
500 if path.is_dir() {
501 fs::remove_dir_all(path)?;
502 } else {
503 fs::remove_file(path)?;
504 }
505 }
506 }
507 }
508 }
509
510 if let Some(backup_files) = &manifest.backup {
511 if !quiet {
512 println!("Saving configuration files...");
513 }
514 for backup_file_rel in backup_files {
515 let expanded_path = zoi_core::utils::expand_placeholders(
516 backup_file_rel,
517 &version_dir,
518 manifest.scope,
519 )?;
520 let backup_src = PathBuf::from(expanded_path);
521
522 if backup_src.exists() {
523 let backup_filename = backup_src
524 .file_name()
525 .ok_or_else(|| anyhow!("Invalid backup source name"))?
526 .to_string_lossy();
527 let backup_dest = version_dir
528 .parent()
529 .ok_or_else(|| anyhow!("version_dir should have a parent (package_dir)"))?
530 .join(format!("{}.zoisave", backup_filename));
531
532 if let Some(p) = backup_dest.parent()
533 && let Err(e) = fs::create_dir_all(p)
534 {
535 if !quiet {
536 eprintln!(
537 "Warning: could not create backup directory {}: {}",
538 p.display(),
539 e
540 );
541 }
542 continue;
543 }
544 if !quiet {
545 println!(
546 "Saving {} to {}",
547 backup_src.display(),
548 backup_dest.display()
549 );
550 }
551 if let Err(e) = fs::copy(&backup_src, &backup_dest) {
553 if !quiet {
554 eprintln!(
555 "Warning: failed to copy backup {}: {}",
556 backup_src.display(),
557 e
558 );
559 }
560 } else {
561 let _ = fs::remove_file(&backup_src);
562 }
563 }
564 }
565 }
566
567 if !quiet {
568 println!(
569 "Uninstalling '{}'...",
570 if let Some(sub) = &manifest.sub_package {
571 format!("{}:{}", pkg.name, sub)
572 } else {
573 pkg.name.clone()
574 }
575 .bold()
576 );
577 }
578
579 if let Some(bins) = &manifest.bins {
580 let bin_root = get_bin_root(scope)?;
581 for bin in bins {
582 let symlink_path = bin_root.join(bin);
583 if symlink_path.is_symlink() || symlink_path.exists() {
584 let other_providers = db::find_provides("local", bin)?;
585 let still_provided = other_providers.iter().any(|(p, _)| {
586 p.name != pkg.name || (p.sub_package != manifest.sub_package)
587 });
588
589 if !still_provided {
590 if !quiet {
591 println!(
592 "Removing shim for {} from {}...",
593 bin.cyan(),
594 symlink_path.display()
595 );
596 }
597 fs::remove_file(&symlink_path)?;
598 } else {
599 if !quiet {
600 println!(
601 "Keeping shim for {} as it is still provided by other packages.",
602 bin.cyan()
603 );
604 }
605 }
606 }
607 }
608 } else if manifest.sub_package.is_none() {
609 let bin = &pkg.name;
610 let symlink_path = get_bin_root(scope)?.join(bin);
611 if symlink_path.is_symlink() || symlink_path.exists() {
612 let other_providers = db::find_provides("local", bin)?;
613 let still_provided = other_providers
614 .iter()
615 .any(|(p, _)| p.name != pkg.name || (p.sub_package != manifest.sub_package));
616
617 if !still_provided {
618 if !quiet {
619 println!(
620 "Removing shim for {} from {}...",
621 bin.cyan(),
622 symlink_path.display()
623 );
624 }
625 fs::remove_file(symlink_path)?;
626 }
627 }
628 }
629
630 if let Some(completions) = &manifest.completions {
631 for completion in completions {
632 let completions_root = get_completions_root(scope, &completion.shell)?;
633 let pkg_dir = completions_root.join(&pkg.name);
634 let symlink_path = pkg_dir.join(&completion.filename);
635 if symlink_path.is_symlink() || symlink_path.exists() {
636 let other_providers = db::find_provides("local", &completion.filename)?;
637 let still_provided = other_providers.iter().any(|(p, _)| {
638 p.name != pkg.name || (p.sub_package != manifest.sub_package)
639 });
640
641 if !still_provided {
642 if !quiet {
643 println!(
644 "Removing {} completion for {} from {}...",
645 completion.shell.cyan(),
646 completion.filename.cyan(),
647 symlink_path.display()
648 );
649 }
650 fs::remove_file(&symlink_path)?;
651 } else if !quiet {
652 println!(
653 "Keeping {} completion for {} as it is still provided by other packages.",
654 completion.shell.cyan(),
655 completion.filename.cyan()
656 );
657 }
658 }
659 }
660
661 let shells: std::collections::HashSet<String> =
662 completions.iter().map(|c| c.shell.clone()).collect();
663 for shell_name in shells {
664 let pkg_dir = get_completions_root(scope, &shell_name)?.join(&pkg.name);
665 if pkg_dir.exists()
666 && fs::read_dir(&pkg_dir)
667 .map(|mut e| e.next().is_none())
668 .unwrap_or(false)
669 {
670 let _ = fs::remove_dir(&pkg_dir);
671 }
672 }
673 }
674
675 let pkg_id_opt = if let Ok(conn) = db::open_connection("local") {
676 db::get_package_id(
677 &conn,
678 &pkg.name,
679 manifest.sub_package.as_deref(),
680 &pkg.repo,
681 handle,
682 )
683 .ok()
684 } else {
685 None
686 };
687
688 for file_path_str in &manifest.installed_files {
689 let expanded = core_utils::expand_placeholders(file_path_str, &version_dir, scope)?;
690 let file_path = PathBuf::from(&expanded);
691
692 if let Some(pkg_id) = pkg_id_opt
693 && let Ok(conn) = db::open_connection("local")
694 && let Ok(true) = db::has_other_owners(&conn, file_path_str, pkg_id)
695 {
696 if !quiet {
697 println!(
698 "Keeping {} as it is still owned by other packages.",
699 file_path_str.dimmed()
700 );
701 }
702 continue;
703 }
704
705 if file_path.exists() {
706 if file_path.is_dir() {
707 if fs::read_dir(&file_path)
709 .map(|mut e| e.next().is_none())
710 .unwrap_or(false)
711 {
712 let _ = fs::remove_dir_all(&file_path);
713 }
714 } else {
715 let _ = fs::remove_file(&file_path);
716 }
717 }
718 }
719
720 let manifest_filename = if let Some(sub) = &manifest.sub_package {
721 format!("manifest-{}.yaml", sub)
722 } else {
723 "manifest.yaml".to_string()
724 };
725 let manifest_path = version_dir.join(manifest_filename);
726 if manifest_path.exists() {
727 fs::remove_file(manifest_path)?;
728 }
729
730 if version_dir.exists() {
731 let mut has_other_manifests = false;
732 if let Ok(entries) = fs::read_dir(&version_dir) {
733 for entry in entries.flatten() {
734 let name = entry.file_name().to_string_lossy().to_string();
735 if name.starts_with("manifest") && name.ends_with(".yaml") {
736 has_other_manifests = true;
737 break;
738 }
739 }
740 }
741 if !has_other_manifests {
742 if !quiet {
743 println!(
744 "Removing empty version directory: {}",
745 version_dir.display()
746 );
747 }
748 fs::remove_dir_all(&version_dir)?;
749 }
750 }
751
752 if package_dir.exists() {
753 let _ = cleanup_service(&pkg.name, scope);
754 let mut has_other_versions = false;
755 if let Ok(entries) = fs::read_dir(&package_dir) {
756 for entry in entries.flatten() {
757 let name = entry.file_name().to_string_lossy().to_string();
758 if name != "latest" && name != "dependents" {
759 has_other_versions = true;
760 break;
761 }
762 }
763 }
764 if !has_other_versions {
765 if !quiet {
766 println!("Removing package store: {}", package_dir.display());
767 }
768 fs::remove_dir_all(&package_dir)?;
769 }
770 }
771
772 let parent_id = format!(
773 "#{}@{}/{}@{}",
774 manifest.registry_handle, manifest.repo, manifest.name, manifest.version
775 );
776 for dep_str in &manifest.installed_dependencies {
777 if let Ok(dep) = dependencies::parse_dependency_string(dep_str)
778 && dep.manager == "zoi"
779 {
780 let dep_req = resolve::parse_source_string(dep.package)?;
781 let dep_matches = local::find_installed_manifests_matching(&dep_req, scope)?;
782 if dep_matches.len() == 1 {
783 let dep_manifest = &dep_matches[0];
784 match local::get_package_dir(
785 dep_manifest.scope,
786 &dep_manifest.registry_handle,
787 &dep_manifest.repo,
788 &dep_manifest.name,
789 ) {
790 Ok(dep_pkg_dir) => {
791 if let Err(e) = local::remove_dependent(&dep_pkg_dir, &parent_id)
792 && !quiet
793 {
794 eprintln!(
795 "Warning: failed to remove dependent link for {}: {}",
796 dep.package, e
797 );
798 }
799 }
800 Err(e) => {
801 if !quiet {
802 eprintln!(
803 "Warning: failed to get package dir for {}: {}",
804 dep.package, e
805 );
806 }
807 }
808 }
809 }
810 }
811 }
812
813 if let Some(hooks) = &pkg.hooks
814 && let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PostRemove, scope)
815 && !quiet
816 {
817 eprintln!("{} post-remove hook failed: {}", "Warning:".yellow(), e);
818 }
819 }
820
821 if let Err(e) = recorder::remove_package_from_record(&manifest)
822 && !quiet
823 {
824 eprintln!(
825 "{} Failed to remove package from lockfile: {}",
826 "Warning:".yellow(),
827 e
828 );
829 }
830
831 if let Ok(conn) = db::open_connection("local") {
832 let _ = db::delete_package(
833 &conn,
834 &pkg.name,
835 sub_package_to_uninstall.as_deref(),
836 &pkg.repo,
837 Some(scope),
838 );
839 }
840
841 if !quiet {
842 println!("Removed manifest for '{}'.", pkg.name);
843 }
844
845 if let Ok(true) = telemetry::posthog_capture_event(
846 "uninstall",
847 &pkg,
848 env!("CARGO_PKG_VERSION"),
849 &manifest.registry_handle,
850 None,
851 ) && !quiet
852 {
853 println!("{} telemetry sent", "Info:".green());
854 }
855
856 Ok(manifest)
857}