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