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 help = r#"specify the exact package to uninstall:
56> lux uninstall '<name>@<version>'
57"#,
58 r#"
59multiple packages satisfying your version requirements were found:
60{:#?}
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 r#"cannot uninstall dependencies:
80{:#?}
81"#,
82 non_entrypoints,
83 ));
84 }
85
86 let (dependencies, entrypoints): (Vec<LocalPackageId>, Vec<LocalPackageId>) = packages
87 .iter()
88 .cloned()
89 .partition(|pkg_id| lockfile.is_dependency(pkg_id));
90
91 if dependencies.is_empty() {
92 operations::Uninstall::new()
93 .config(&config)
94 .packages(entrypoints)
95 .remove()
96 .await?;
97 } else {
98 let package_names = dependencies
99 .iter()
100 .map(|pkg_id| unsafe { lockfile.get_unchecked(pkg_id) }.name().to_string())
101 .collect_vec();
102 let prompt = if package_names.len() == 1 {
103 format!(
104 "
105 Package {} can be removed from the entrypoints, but it is also a dependency, so it will have to be reinstalled.
106Reinstall?
107 ",
108 package_names[0]
109 )
110 } else {
111 format!(
112 "
113 The following packages can be removed from the entrypoints, but are also dependencies:
114{package_names:#?}
115
116They will have to be reinstalled.
117Reinstall?
118 ",
119 )
120 };
121 if !config.no_prompt()
122 && Confirm::new(&prompt)
123 .with_default(false)
124 .prompt()
125 .into_diagnostic()
126 .map_err(|_| miette!("error prompting for reinstall"))?
127 {
128 operations::Uninstall::new()
129 .config(&config)
130 .packages(entrypoints)
131 .remove()
132 .await?;
133
134 let reinstall_specs = dependencies
135 .iter()
136 .map(|pkg_id| {
137 let package = unsafe { lockfile.get_unchecked(pkg_id) };
138 PackageInstallSpec::new(
139 package.clone().into_package_req(),
140 tree::EntryType::DependencyOnly,
141 )
142 .build_behaviour(BuildBehaviour::Force)
143 .pin(package.pinned())
144 .opt(package.opt())
145 .constraint(package.constraint())
146 .build()
147 })
148 .collect_vec();
149 operations::Uninstall::new()
150 .config(&config)
151 .packages(dependencies)
152 .remove()
153 .await?;
154 operations::Install::new(&config)
155 .packages(reinstall_specs)
156 .tree(tree)
157 .install()
158 .await?;
159 } else {
160 return Err(miette!("operation cancelled"));
161 }
162 };
163
164 let mut has_dangling_rocks = true;
165 while has_dangling_rocks {
166 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
167 let lockfile = tree.lockfile()?;
168 let dangling_rocks = lockfile
169 .rocks()
170 .keys()
171 .filter(|pkg_id| !lockfile.is_entrypoint(pkg_id) && !lockfile.is_dependency(pkg_id))
172 .cloned()
173 .collect_vec();
174 if dangling_rocks.is_empty() {
175 has_dangling_rocks = false
176 } else {
177 operations::Uninstall::new()
178 .config(&config)
179 .packages(dangling_rocks)
180 .remove()
181 .await?;
182 }
183 }
184
185 Ok(())
186}