use atomcode_core::self_update::current_exe_path;
use atomcode_core::uninstall::{
actions::{PlatformSelfDelete, SelfDeleteStrategy},
paths::atomcode_dir,
scan::scan,
Decisions, ExecuteContext, Group, Outcome,
};
pub struct Args {
pub yes: bool,
pub purge: bool,
pub keep_data: bool,
pub dry_run: bool,
}
const EXIT_USER_DECLINED: u8 = 1;
const EXIT_BAD_ARGS: u8 = 2;
const EXIT_PARTIAL_FAIL: u8 = 3;
pub fn run(args: Args) -> anyhow::Result<()> {
use is_terminal::IsTerminal;
let tty = std::io::stdin().is_terminal();
if args.purge && args.keep_data {
eprintln!("atomcode uninstall: --purge conflicts with --keep-data");
std::process::exit(EXIT_BAD_ARGS as i32);
}
let mode = decision_mode(&args, tty);
let tty_mode = matches!(mode, DecisionMode::Tty);
let decisions = match mode {
DecisionMode::Tty => None,
DecisionMode::Flag(d) => Some(d),
DecisionMode::AbortNoTty => {
eprintln!(
"atomcode uninstall: refusing to run interactively without a TTY.\n\
Pass one of: --yes (use defaults), --purge (delete everything),\n\
--keep-data (binary only), --dry-run."
);
std::process::exit(EXIT_BAD_ARGS as i32);
}
};
let exe = current_exe_path()?;
let data_dir = atomcode_dir();
let plan = scan(&exe, &data_dir)?;
if args.dry_run {
print_plan(&plan, decisions.unwrap_or(Decisions::DEFAULTS));
return Ok(());
}
let final_decisions = match decisions {
Some(d) => d,
None => match prompt_user(&plan)? {
Some(d) => d,
None => std::process::exit(EXIT_USER_DECLINED as i32),
},
};
if !final_decisions.binary {
eprintln!("atomcode uninstall: cannot uninstall without removing binary; aborted.");
std::process::exit(EXIT_USER_DECLINED as i32);
}
if tty_mode {
if !confirm_and_kill_running_processes()? {
eprintln!("aborted: running processes were not terminated.");
std::process::exit(EXIT_USER_DECLINED as i32);
}
}
let ctx = build_context(&plan)?;
let strategy: Box<dyn SelfDeleteStrategy> = Box::new(PlatformSelfDelete);
let outcome =
atomcode_core::uninstall::execute(&plan, final_decisions, strategy.as_ref(), Some(ctx))?;
print_summary(&outcome);
if !outcome.failed.is_empty() {
std::process::exit(EXIT_PARTIAL_FAIL as i32);
}
Ok(())
}
enum DecisionMode {
Tty,
Flag(Decisions),
AbortNoTty,
}
fn decision_mode(args: &Args, tty: bool) -> DecisionMode {
if args.purge {
return DecisionMode::Flag(Decisions::PURGE);
}
if args.keep_data {
return DecisionMode::Flag(Decisions::KEEP_DATA);
}
if args.yes {
return DecisionMode::Flag(Decisions::DEFAULTS);
}
if args.dry_run {
return DecisionMode::Flag(Decisions::DEFAULTS);
}
if !tty {
return DecisionMode::AbortNoTty;
}
DecisionMode::Tty
}
fn confirm_and_kill_running_processes() -> anyhow::Result<bool> {
use atomcode_core::uninstall::actions::{kill_process, list_atomcode_processes};
use std::io::{BufRead, Write};
let procs = list_atomcode_processes();
if procs.is_empty() {
return Ok(true);
}
println!("\nFound {} running atomcode process(es):", procs.len());
for p in &procs {
println!(" pid {} {}", p.pid, p.name);
}
print!("Kill them and continue? [y/N]: ");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().lock().read_line(&mut line)?;
if !line.trim().eq_ignore_ascii_case("y") {
return Ok(false);
}
for p in procs {
if let Err(e) = kill_process(p.pid) {
#[cfg(windows)]
{
eprintln!("could not kill pid {}: {}", p.pid, e);
return Ok(false);
}
#[cfg(not(windows))]
{
eprintln!(
"warn: could not kill pid {}: {} (continuing — Unix unlink doesn't need it)",
p.pid, e
);
}
}
}
Ok(true)
}
fn print_plan(plan: &atomcode_core::uninstall::scan::Plan, decisions: Decisions) {
println!("DRY RUN — no changes will be made.\n");
print_group(
plan,
Group::Binary,
"[Group 1] Binary + PATH edit",
decisions.binary,
);
print_group(
plan,
Group::Credentials,
"[Group 2] Credentials and global config",
decisions.credentials,
);
print_group(
plan,
Group::State,
"[Group 3] Local state and extensions",
decisions.state,
);
}
fn print_group(
plan: &atomcode_core::uninstall::scan::Plan,
g: Group,
label: &str,
will_remove: bool,
) {
let items: Vec<_> = plan.items.iter().filter(|i| i.group == g).collect();
if items.is_empty() {
return;
}
println!(
"{label} [{}]",
if will_remove { "WILL REMOVE" } else { "KEEP" }
);
for it in &items {
let mark = if it.needs_privilege { " (sudo)" } else { "" };
println!(
" {} ({}){}",
it.path.display(),
human_size(it.size_bytes),
mark
);
}
println!();
}
fn human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut v = bytes as f64;
let mut u = 0;
while v >= 1024.0 && u < UNITS.len() - 1 {
v /= 1024.0;
u += 1;
}
if u == 0 {
format!("{} B", bytes)
} else {
format!("{:.1} {}", v, UNITS[u])
}
}
fn prompt_user(plan: &atomcode_core::uninstall::scan::Plan) -> anyhow::Result<Option<Decisions>> {
use std::io::{BufRead, Write};
println!("This will uninstall AtomCode from your system.\n");
let g1 = ask_group(
plan,
Group::Binary,
"[Group 1] Remove binary and PATH edit?",
true,
)?;
if !g1 {
eprintln!("Group 1 declined; aborting (cannot keep binary while removing data).");
return Ok(None);
}
let g2 = ask_group(
plan,
Group::Credentials,
"[Group 2] Remove credentials and global config?",
false,
)?;
let g3 = ask_group(
plan,
Group::State,
"[Group 3] Remove local state and extensions?",
true,
)?;
println!("\nSummary:");
summarize_decision(plan, Group::Binary, true);
summarize_decision(plan, Group::Credentials, g2);
summarize_decision(plan, Group::State, g3);
print!("\nContinue? [y/N]: ");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().lock().read_line(&mut line)?;
if !line.trim().eq_ignore_ascii_case("y") {
return Ok(None);
}
Ok(Some(Decisions {
binary: true,
credentials: g2,
state: g3,
}))
}
fn ask_group(
plan: &atomcode_core::uninstall::scan::Plan,
g: Group,
prompt: &str,
default_yes: bool,
) -> anyhow::Result<bool> {
use std::io::{BufRead, Write};
let items: Vec<_> = plan.items.iter().filter(|i| i.group == g).collect();
if items.is_empty() {
return Ok(default_yes);
}
println!("\n{prompt}");
for it in &items {
let mark = if it.needs_privilege { " (sudo)" } else { "" };
println!(
" {} ({}){}",
it.path.display(),
human_size(it.size_bytes),
mark
);
}
let prompt_suffix = if default_yes { "[Y/n]" } else { "[y/N]" };
print!("Proceed? {prompt_suffix}: ");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().lock().read_line(&mut line)?;
let answer = line.trim();
Ok(match answer {
"" => default_yes,
s if s.eq_ignore_ascii_case("y") || s.eq_ignore_ascii_case("yes") => true,
s if s.eq_ignore_ascii_case("n") || s.eq_ignore_ascii_case("no") => false,
_ => default_yes,
})
}
fn summarize_decision(plan: &atomcode_core::uninstall::scan::Plan, g: Group, will_remove: bool) {
let count = plan.items.iter().filter(|i| i.group == g).count();
if count == 0 {
return;
}
let action = if will_remove { "Remove" } else { "Keep" };
let label = match g {
Group::Binary => "binary + PATH",
Group::Credentials => "credentials",
Group::State => "local state",
};
println!(" {action}: {count} items ({label})");
}
fn print_summary(outcome: &Outcome) {
println!("\n──────────────────");
if !outcome.removed.is_empty() {
println!("Removed:");
for p in &outcome.removed {
println!(" {}", p.display());
}
}
if !outcome.kept.is_empty() {
println!("Kept (use --purge to remove later):");
for p in &outcome.kept {
println!(" {}", p.display());
}
}
if !outcome.failed.is_empty() {
println!("Failed:");
for (p, e) in &outcome.failed {
println!(" {} ({})", p.display(), e);
}
}
if !outcome.backups.is_empty() {
println!("Backups:");
for p in &outcome.backups {
println!(" {}", p.display());
}
}
}
fn build_context(plan: &atomcode_core::uninstall::scan::Plan) -> anyhow::Result<ExecuteContext> {
#[cfg_attr(not(unix), allow(unused_mut))]
let mut rc_files = Vec::new();
#[cfg(unix)]
{
let prefix = plan
.binary_path
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let rc = atomcode_core::uninstall::paths::unix_rc_paths();
for path in [rc.zshrc, rc.bashrc] {
if path.exists() {
rc_files.push((path, prefix.clone()));
}
}
}
let _ = plan;
let ctx = ExecuteContext {
rc_files,
#[cfg(windows)]
windows_install_dir_literal: plan
.binary_path
.parent()
.map(|p| p.to_string_lossy().into_owned()),
#[cfg(windows)]
windows_install_dir_expanded: plan
.binary_path
.parent()
.map(|p| p.to_string_lossy().into_owned()),
};
Ok(ctx)
}