use std::io::{IsTerminal, Write};
use crate::cli::commands::lifecycle;
use crate::cli::output::{OutputConfig, OutputFormat};
use crate::cli::UninstallArgs;
use crate::config;
use crate::error::{OlError, ERR_INVALID_CONFIG};
use crate::hooks;
pub fn run_uninstall(args: &UninstallArgs, output: &OutputConfig) -> Result<(), OlError> {
if !args.yes {
let is_tty = std::io::stdout().is_terminal();
if is_tty && output.format == OutputFormat::Human {
let purge_note = if args.purge {
" and DELETE all OpenLatch data"
} else {
""
};
eprint!("This will remove OpenLatch hooks{purge_note} and stop the daemon. Continue? [y/N] ");
let _ = std::io::stderr().flush();
let mut line = String::new();
let _ = std::io::stdin().read_line(&mut line);
if !line.trim().eq_ignore_ascii_case("y") {
output.print_info("Aborted.");
return Ok(());
}
}
}
match hooks::detect_agent() {
Ok(agent) => {
match hooks::remove_hooks(&agent) {
Ok(()) => {
let settings_path = match &agent {
hooks::DetectedAgent::ClaudeCode { settings_path, .. } => {
settings_path.display().to_string()
}
};
output.print_step(&format!("Hooks removed from {settings_path}"));
}
Err(e) => {
output.print_info(&format!(
"Warning: could not remove hooks: {} ({})",
e.message, e.code
));
}
}
}
Err(_) => {
output.print_info("Agent not detected — skipping hook removal");
}
}
if let Some(supervisor) = crate::supervision::select_supervisor() {
match supervisor.uninstall() {
Ok(()) => output.print_step("Supervision removed"),
Err(e) => output.print_info(&format!(
"Warning: could not remove supervision: {} ({})",
e.message, e.code
)),
}
}
let config_path = config::openlatch_dir().join("config.toml");
if config_path.exists() {
let _ = config::persist_supervision_state(
&config_path,
&crate::supervision::SupervisionMode::Disabled,
&crate::supervision::SupervisorKind::None,
Some("uninstalled"),
);
}
lifecycle::run_stop(output)?;
if args.purge {
let ol_dir = config::openlatch_dir();
if ol_dir.exists() {
let canonical = std::fs::canonicalize(&ol_dir).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot canonicalize openlatch directory: {e}"),
)
})?;
let dir_name = canonical.file_name().and_then(|n| n.to_str()).unwrap_or("");
if dir_name != "openlatch" && dir_name != ".openlatch" {
return Err(OlError::new(
ERR_INVALID_CONFIG,
format!(
"Unexpected openlatch directory name '{}' — refusing to delete for safety",
canonical.display()
),
)
.with_suggestion("If this is correct, manually delete the directory."));
}
std::fs::remove_dir_all(&canonical).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!(
"Cannot delete openlatch directory '{}': {e}",
canonical.display()
),
)
.with_suggestion("Check that you have write permission.")
})?;
output.print_step(&format!("Data directory removed: {}", canonical.display()));
} else {
output.print_info("Data directory does not exist — nothing to purge");
}
}
crate::telemetry::capture_global(crate::telemetry::Event::uninstalled(1));
if output.format == OutputFormat::Json {
let json = serde_json::json!({
"status": "ok",
"purged": args.purge,
});
output.print_json(&json);
} else if !output.quiet {
eprintln!();
eprintln!("OpenLatch uninstalled successfully.");
if args.purge {
eprintln!("All data removed.");
} else {
eprintln!(
"Data directory preserved at {}. Use --purge to remove it.",
config::openlatch_dir().display()
);
}
}
Ok(())
}