use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Args;
use colored::Colorize;
use dialoguer::Confirm;
#[derive(Args)]
pub struct UninstallArgs {
#[arg(long, default_value = "~/.aegis")]
pub dir: String,
#[arg(long, short = 'y')]
pub yes: bool,
}
pub async fn run(args: UninstallArgs) -> Result<()> {
let dir = expand_tilde(Path::new(&args.dir));
if !dir.exists() {
println!(
" {} {} does not exist — nothing to uninstall.",
"ℹ".cyan(),
dir.display()
);
return Ok(());
}
println!();
println!("{}", "AEGIS Uninstall".bold().red());
println!();
println!(" This will:");
println!(
" 1. Run {} — stops all containers and removes their volumes",
"docker compose down --volumes".bold()
);
println!(
" 2. Permanently delete {}",
dir.display().to_string().bold()
);
println!();
if !args.yes {
let confirmed = Confirm::new()
.with_prompt(format!(
"Delete {} and all its contents? This cannot be undone",
dir.display()
))
.default(false)
.interact()?;
if !confirmed {
println!(" Aborted — nothing was changed.");
return Ok(());
}
}
let compose_file = dir.join("docker-compose.yml");
if compose_file.exists() {
println!();
println!("{}", "Stopping stack and removing volumes...".bold());
let status = std::process::Command::new("docker")
.arg("compose")
.args(["down", "--volumes"])
.current_dir(&dir)
.status();
match status {
Ok(s) if s.success() => {
println!(" {} Stack stopped and volumes removed.", "✓".green())
}
Ok(s) => println!(
" {} `docker compose down --volumes` exited {} — continuing with directory removal.",
"⚠".yellow(),
s.code().unwrap_or(-1)
),
Err(e) => println!(
" {} Could not run `docker compose down`: {} — continuing with directory removal.",
"⚠".yellow(),
e
),
}
} else {
println!(
" {} No docker-compose.yml found — skipping compose teardown.",
"ℹ".cyan()
);
}
println!();
println!("{}", format!("Removing {}...", dir.display()).bold());
match std::fs::remove_dir_all(&dir) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
println!(
" {} Permission denied while removing {}. Attempting automatic permission repair...",
"⚠".yellow(),
dir.display()
);
repair_permissions_for_removal(&dir)?;
std::fs::remove_dir_all(&dir).map_err(|retry_err| {
anyhow::anyhow!(
"Failed to remove {} after permission repair: {}",
dir.display(),
retry_err
)
})?;
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to remove {}: {}", dir.display(), e));
}
}
println!(" {} {} removed.", "✓".green(), dir.display());
println!();
println!("{}", "AEGIS has been uninstalled.".bold());
println!(" Run {} to set it up again.", "aegis init".cyan());
Ok(())
}
fn expand_tilde(path: &Path) -> PathBuf {
if let Ok(stripped) = path.strip_prefix("~") {
if let Some(home) = dirs_next::home_dir() {
return home.join(stripped);
}
}
path.to_path_buf()
}
fn repair_permissions_for_removal(dir: &Path) -> Result<()> {
let mount_arg = format!("{}:/target", dir.display());
let status = std::process::Command::new("docker")
.args([
"run",
"--rm",
"-v",
&mount_arg,
"alpine:3.20",
"sh",
"-c",
"chmod -R a+rwx /target",
])
.status();
match status {
Ok(s) if s.success() => {
println!(
" {} Repaired permissions in {}",
"✓".green(),
dir.display()
);
Ok(())
}
Ok(s) => Err(anyhow::anyhow!(
"Permission repair container exited {}",
s.code().unwrap_or(-1)
)),
Err(e) => Err(anyhow::anyhow!(
"Failed to run docker permission repair: {e}"
)),
}
}