1use clap::Args;
2use inquire::Confirm;
3use itertools::Itertools;
4use lux_lib::{
5 build::BuildBehaviour,
6 config::Config,
7 lockfile::LocalPackageId,
8 lua_version::LuaVersion,
9 operations::{self, PackageInstallSpec},
10 package::PackageReq,
11 tree::{self, InstallTree, RockMatches, TreeError},
12};
13
14use miette::{miette, IntoDiagnostic, Result};
15
16#[derive(Args)]
17pub struct Uninstall {
18 packages: Vec<PackageReq>,
20}
21
22pub async fn uninstall(uninstall_args: Uninstall, config: Config) -> Result<()> {
24 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
25
26 let package_matches = uninstall_args
27 .packages
28 .iter()
29 .map(|package_req| tree.match_rocks(package_req))
30 .try_collect::<_, Vec<_>, TreeError>()?;
31
32 let (packages, nonexistent_packages, duplicate_packages) = package_matches.into_iter().fold(
33 (Vec::new(), Vec::new(), Vec::new()),
34 |(mut p, mut n, mut d), rock_match| {
35 match rock_match {
36 RockMatches::NotFound(req) => n.push(req),
37 RockMatches::Single(package) => p.push(package),
38 RockMatches::Many(packages) => d.extend(packages),
39 };
40
41 (p, n, d)
42 },
43 );
44
45 if !nonexistent_packages.is_empty() {
46 return Err(miette!(
48 "The following packages were not found: {:#?}",
49 nonexistent_packages
50 ));
51 }
52
53 if !duplicate_packages.is_empty() {
54 return Err(miette!(
55 "
56Multiple packages satisfying your version requirements were found:
57{:#?}
58
59Please specify the exact package to uninstall:
60> lux uninstall '<name>@<version>'
61",
62 duplicate_packages,
63 ));
64 }
65
66 let lockfile = tree.lockfile()?;
67 let non_entrypoints = packages
68 .iter()
69 .filter_map(|pkg_id| {
70 if lockfile.is_entrypoint(pkg_id) {
71 None
72 } else {
73 Some(unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
74 }
75 })
76 .collect_vec();
77 if !non_entrypoints.is_empty() {
78 return Err(miette!(
79 "
80Cannot uninstall dependencies:
81{:#?}
82",
83 non_entrypoints,
84 ));
85 }
86
87 let (dependencies, entrypoints): (Vec<LocalPackageId>, Vec<LocalPackageId>) = packages
88 .iter()
89 .cloned()
90 .partition(|pkg_id| lockfile.is_dependency(pkg_id));
91
92 if dependencies.is_empty() {
93 operations::Uninstall::new()
94 .config(&config)
95 .packages(entrypoints)
96 .remove()
97 .await?;
98 } else {
99 let package_names = dependencies
100 .iter()
101 .map(|pkg_id| unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
102 .collect_vec();
103 let prompt = if package_names.len() == 1 {
104 format!(
105 "
106 Package {} can be removed from the entrypoints, but it is also a dependency, so it will have to be reinstalled.
107Reinstall?
108 ",
109 package_names[0]
110 )
111 } else {
112 format!(
113 "
114 The following packages can be removed from the entrypoints, but are also dependencies:
115{package_names:#?}
116
117They will have to be reinstalled.
118Reinstall?
119 ",
120 )
121 };
122 if !config.no_prompt()
123 && Confirm::new(&prompt)
124 .with_default(false)
125 .prompt()
126 .into_diagnostic()
127 .map_err(|_| miette!("Error prompting for reinstall"))?
128 {
129 operations::Uninstall::new()
130 .config(&config)
131 .packages(entrypoints)
132 .remove()
133 .await?;
134
135 let reinstall_specs = dependencies
136 .iter()
137 .map(|pkg_id| {
138 let package = unsafe { lockfile.get_unchecked(pkg_id) };
139 PackageInstallSpec::new(
140 package.clone().into_package_req(),
141 tree::EntryType::DependencyOnly,
142 )
143 .build_behaviour(BuildBehaviour::Force)
144 .pin(package.pinned())
145 .opt(package.opt())
146 .constraint(package.constraint())
147 .build()
148 })
149 .collect_vec();
150 operations::Uninstall::new()
151 .config(&config)
152 .packages(dependencies)
153 .remove()
154 .await?;
155 operations::Install::new(&config)
156 .packages(reinstall_specs)
157 .tree(tree)
158 .install()
159 .await?;
160 } else {
161 return Err(miette!("Operation cancelled."));
162 }
163 };
164
165 let mut has_dangling_rocks = true;
166 while has_dangling_rocks {
167 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
168 let lockfile = tree.lockfile()?;
169 let dangling_rocks = lockfile
170 .rocks()
171 .keys()
172 .filter(|pkg_id| !lockfile.is_entrypoint(pkg_id) && !lockfile.is_dependency(pkg_id))
173 .cloned()
174 .collect_vec();
175 if dangling_rocks.is_empty() {
176 has_dangling_rocks = false
177 } else {
178 operations::Uninstall::new()
179 .config(&config)
180 .packages(dangling_rocks)
181 .remove()
182 .await?;
183 }
184 }
185
186 Ok(())
187}