feat: implement uninstall functionality for managed artifacts and persist install metadata

This commit is contained in:
stoorps 2026-03-19 23:07:25 +00:00
parent 38f900ad50
commit 842c390260
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
11 changed files with 626 additions and 21 deletions

View file

@ -8,7 +8,7 @@ use aim_core::app::add::{
AddPlan, InstalledApp, build_add_plan, install_app, resolve_requested_scope,
};
use aim_core::app::list::{ListRow, build_list_rows};
use aim_core::app::remove::remove_registered_app;
use aim_core::app::remove::{RemovalResult, remove_registered_app};
use aim_core::app::update::build_update_plan;
use aim_core::domain::app::AppRecord;
use aim_core::domain::update::UpdatePlan;
@ -36,12 +36,13 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
return match command {
cli::args::Command::List => Ok(DispatchResult::List(build_list_rows(&apps))),
cli::args::Command::Remove { query } => {
let removal = remove_registered_app(&query, &apps)?;
let removal = remove_registered_app(&query, &apps, &install_home)?;
let remaining_apps = removal.remaining_apps.clone();
store.save(&Registry {
version: registry.version,
apps: removal.remaining_apps,
apps: remaining_apps,
})?;
Ok(DispatchResult::Removed(removal.removed.display_name))
Ok(DispatchResult::Removed(Box::new(removal)))
}
cli::args::Command::Update => Ok(DispatchResult::UpdatePlan(build_update_plan(&apps)?)),
};
@ -91,7 +92,7 @@ pub enum DispatchResult {
Added(Box<InstalledApp>),
List(Vec<ListRow>),
PendingAdd(Box<AddPlan>),
Removed(String),
Removed(Box<RemovalResult>),
UpdatePlan(UpdatePlan),
Noop,
}
@ -101,7 +102,7 @@ pub enum DispatchError {
AddPlan(aim_core::app::add::BuildAddPlanError),
AddInstall(aim_core::app::add::InstallAppError),
Prompt(ui::prompt::PromptError),
RemovePlan(aim_core::app::remove::ResolveRegisteredAppError),
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
Registry(aim_core::registry::store::RegistryStoreError),
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
}
@ -130,8 +131,8 @@ impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
}
}
impl From<aim_core::app::remove::ResolveRegisteredAppError> for DispatchError {
fn from(value: aim_core::app::remove::ResolveRegisteredAppError) -> Self {
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
Self::RemovePlan(value)
}
}

View file

@ -11,7 +11,7 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
DispatchResult::Added(added) => render_added_app(added),
DispatchResult::List(rows) => render_list(rows),
DispatchResult::PendingAdd(plan) => render_pending_add(plan),
DispatchResult::Removed(display_name) => format!("removed: {display_name}"),
DispatchResult::Removed(removed) => render_removed_app(removed),
DispatchResult::UpdatePlan(plan) => {
render_update_summary(plan.items.len(), plan.items.len(), 0)
}
@ -72,3 +72,19 @@ fn render_list(rows: &[aim_core::app::list::ListRow]) -> String {
}
output.trim_end().to_owned()
}
fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String {
let warning_lines = removed
.warnings
.iter()
.map(|warning| format!("warning: {warning}"))
.collect::<Vec<_>>()
.join("\n");
let summary = format!("removed: {}", removed.removed.display_name);
if warning_lines.is_empty() {
summary
} else {
format!("{summary}\n{warning_lines}")
}
}

View file

@ -55,6 +55,40 @@ fn remove_command_removes_registered_app_from_registry_file() {
assert!(!contents.contains("stable_id = \"bat\""));
}
#[test]
fn remove_command_uninstalls_managed_files() {
let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml");
let install_home = dir.path().join("install-home");
let payload_path = install_home.join(".local/lib/aim/appimages/sharkdp-bat.AppImage");
let desktop_path = install_home.join(".local/share/applications/aim-sharkdp-bat.desktop");
let icon_path = install_home.join(".local/share/icons/hicolor/256x256/apps/sharkdp-bat.png");
let mut add_cmd = Command::cargo_bin("aim").unwrap();
add_cmd
.arg("sharkdp/bat")
.env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1")
.assert()
.success();
assert!(payload_path.exists());
assert!(desktop_path.exists());
assert!(icon_path.exists());
let mut remove_cmd = Command::cargo_bin("aim").unwrap();
remove_cmd
.args(["remove", "sharkdp-bat"])
.env("AIM_REGISTRY_PATH", &registry_path)
.assert()
.success()
.stdout(contains("removed: bat"));
assert!(!payload_path.exists());
assert!(!desktop_path.exists());
assert!(!icon_path.exists());
}
#[test]
fn query_command_registers_unambiguous_app_in_registry_file() {
let dir = tempdir().unwrap();