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