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