use std::fs;
use std::path::{Path, PathBuf};
use super::shorten;
pub(super) fn stop_processes(dry_run: bool) {
if dry_run {
println!(" Would stop the daemon, proxy, and any running lean-ctx processes");
return;
}
println!(" Stopping lean-ctx processes…");
crate::proxy_autostart::stop();
crate::daemon_autostart::stop();
let _ = crate::daemon::stop_daemon();
crate::ipc::process::kill_all_by_name("lean-ctx");
std::thread::sleep(std::time::Duration::from_millis(500));
let remaining = crate::ipc::process::find_killable_pids("lean-ctx");
for &pid in &remaining {
let _ = crate::ipc::process::force_kill(pid);
}
if !remaining.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(300));
}
crate::daemon::cleanup_daemon_files();
println!(" ✓ Processes stopped");
}
enum Disposition {
Remove,
DevBuild,
Cargo,
Homebrew,
}
fn classify(path: &Path) -> Disposition {
let p = path.to_string_lossy();
if p.contains("/target/release/") || p.contains("/target/debug/") {
Disposition::DevBuild
} else if p.contains("/.cargo/") {
Disposition::Cargo
} else if p.contains("/Cellar/") || p.contains("homebrew") {
Disposition::Homebrew
} else {
Disposition::Remove
}
}
fn candidate_paths(home: &Path) -> Vec<PathBuf> {
let install_dir = std::env::var_os("LEAN_CTX_INSTALL_DIR")
.map_or_else(|| home.join(".local/bin"), PathBuf::from);
let mut out = vec![
install_dir.join("lean-ctx"),
PathBuf::from("/usr/local/bin/lean-ctx"),
PathBuf::from("/opt/homebrew/bin/lean-ctx"),
];
if let Ok(exe) = std::env::current_exe() {
out.push(exe);
}
let mut seen = std::collections::HashSet::new();
out.retain(|p| seen.insert(p.to_string_lossy().to_string()));
out
}
pub(super) fn remove_binaries(home: &Path, dry_run: bool, keep_binary: bool) -> bool {
if keep_binary {
println!(" · Skipped: binary (--keep-binary)");
return false;
}
let mut removed = false;
let mut cargo_hint = false;
let mut brew_hint = false;
for path in candidate_paths(home) {
let Ok(meta) = fs::symlink_metadata(&path) else {
continue;
};
match classify(&path) {
Disposition::DevBuild => {} Disposition::Cargo => cargo_hint = true,
Disposition::Homebrew => brew_hint = true,
Disposition::Remove => {
let short = shorten(&path, home);
if dry_run {
println!(" Would remove binary ({short})");
removed = true;
continue;
}
let res = if meta.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
};
match res {
Ok(()) => {
println!(" ✓ Binary removed ({short})");
removed = true;
}
Err(e) => {
if cfg!(windows) {
println!(
" · Could not remove the running binary ({short}). \
Delete it after this process exits."
);
} else {
tracing::warn!("Failed to remove binary {}: {e}", path.display());
}
}
}
}
}
}
if cargo_hint {
println!(" · Installed via cargo — finish with: cargo uninstall lean-ctx");
}
if brew_hint {
println!(" · Installed via Homebrew — finish with: brew uninstall lean-ctx");
}
if !removed && !cargo_hint && !brew_hint {
println!(" · No managed binary found on standard paths");
}
removed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dev_builds_are_never_removed() {
assert!(matches!(
classify(Path::new("/home/u/lean-ctx/rust/target/release/lean-ctx")),
Disposition::DevBuild
));
assert!(matches!(
classify(Path::new("/home/u/proj/target/debug/lean-ctx")),
Disposition::DevBuild
));
}
#[test]
fn package_managers_are_deferred() {
assert!(matches!(
classify(Path::new("/home/u/.cargo/bin/lean-ctx")),
Disposition::Cargo
));
assert!(matches!(
classify(Path::new("/opt/homebrew/bin/lean-ctx")),
Disposition::Homebrew
));
assert!(matches!(
classify(Path::new("/usr/local/Cellar/lean-ctx/3.7.0/bin/lean-ctx")),
Disposition::Homebrew
));
}
#[test]
fn managed_install_dirs_are_removable() {
assert!(matches!(
classify(Path::new("/home/u/.local/bin/lean-ctx")),
Disposition::Remove
));
assert!(matches!(
classify(Path::new("/usr/local/bin/lean-ctx")),
Disposition::Remove
));
}
#[test]
fn keep_binary_skips_removal() {
let home = std::env::temp_dir();
assert!(!remove_binaries(&home, true, true));
}
#[test]
fn candidate_paths_include_install_dir_and_dedup() {
let home = PathBuf::from("/home/tester");
let paths = candidate_paths(&home);
let mut seen = std::collections::HashSet::new();
for p in &paths {
assert!(
seen.insert(p.clone()),
"duplicate candidate: {}",
p.display()
);
}
}
}