v0.2.4 : prompt interactif style Hermes + passerelle shell systeme (shell <nom>, detection du shell par defaut) + desinstallation propre (self-uninstall)
This commit is contained in:
@@ -9,6 +9,7 @@ pub mod install_cmd;
|
||||
pub mod list_cmd;
|
||||
pub mod run_cmd;
|
||||
pub mod search_cmd;
|
||||
pub mod self_uninstall;
|
||||
pub mod self_update;
|
||||
pub mod status_cmd;
|
||||
pub mod uninstall_cmd;
|
||||
@@ -59,6 +60,7 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
Command::Run { agent, args } => run_cmd::run(app, agent, args),
|
||||
Command::Completion { shell } => completion_cmd::run(*shell),
|
||||
Command::SelfUpdate { check, to } => self_update::run(app, *check, to.as_deref()),
|
||||
Command::SelfUninstall => self_uninstall::run(app),
|
||||
Command::Export { output } => export_import::export_cmd(app, output),
|
||||
Command::Import { file } => export_import::import_cmd(app, file),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! self-uninstall: remove agent-manager and everything it created from this
|
||||
//! machine — background agents are stopped first, then the data, state and
|
||||
//! configuration directories are deleted, then the executable itself.
|
||||
|
||||
use crate::app::{self, App};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use colored::Colorize;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(windows)]
|
||||
const MOVEFILE_DELAY_UNTIL_REBOOT: u32 = 0x4;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[link(name = "Kernel32")]
|
||||
extern "system" {
|
||||
fn MoveFileExW(
|
||||
lpExistingFileName: *const u16,
|
||||
lpNewFileName: *const u16,
|
||||
dwFlags: u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
pub fn run(app: &App) -> Result<i32> {
|
||||
// 1. Stop every managed background agent.
|
||||
let state = app.state.load()?;
|
||||
for (name, entry) in &state.installed {
|
||||
if let Some(pid) = entry.pid {
|
||||
if crate::process::is_running(pid) {
|
||||
app.log.info(&format!("stopping agent '{name}' (pid {pid})"));
|
||||
crate::process::stop_pid(pid, false, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect the directories agent-manager owns.
|
||||
let data_root = app::data_dir();
|
||||
let state_root = app::state_dir();
|
||||
let mut roots: Vec<(&str, PathBuf)> = Vec::new();
|
||||
if data_root.is_dir() {
|
||||
roots.push(("data", data_root.clone()));
|
||||
}
|
||||
if state_root != data_root && state_root.is_dir() {
|
||||
roots.push(("state", state_root));
|
||||
}
|
||||
if let Some(dir) = crate::config::user_config_dir() {
|
||||
if dir.is_dir() {
|
||||
roots.push(("config", dir));
|
||||
}
|
||||
}
|
||||
|
||||
if roots.is_empty() {
|
||||
app.log
|
||||
.info("nothing to remove — agent-manager left no traces on this machine");
|
||||
} else {
|
||||
app.log.info("agent-manager will remove:");
|
||||
for (label, path) in &roots {
|
||||
println!(" {label}: {}", path.display());
|
||||
}
|
||||
if !app.confirm("Remove everything listed above?")? {
|
||||
app.log.info("cancelled — nothing was removed");
|
||||
return Ok(0);
|
||||
}
|
||||
if app.dry_run() {
|
||||
app.log.dry("nothing removed (dry run)");
|
||||
return Ok(0);
|
||||
}
|
||||
// Remove everything first; any message logged *after* deleting the
|
||||
// state directory would recreate the log folder, so the removal
|
||||
// itself is silent and the report below goes to plain stdout.
|
||||
for (_, path) in &roots {
|
||||
std::fs::remove_dir_all(path)
|
||||
.with_context(|| format!("cannot remove {}", path.display()))?;
|
||||
}
|
||||
for (_, path) in &roots {
|
||||
println!("{} {}", "removed:".green(), path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Delete the executable itself.
|
||||
let exe = std::env::current_exe()
|
||||
.with_context(|| "cannot locate the agent-manager executable")?;
|
||||
remove_self(&exe)?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Delete the running executable. On Unix the file can be unlinked while it
|
||||
/// runs; on Windows a detached cmd re-tries the deletion right after this
|
||||
/// process exits (the file is locked while we run).
|
||||
fn remove_self(exe: &Path) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::fs::remove_file(exe)
|
||||
.with_context(|| format!("cannot remove {}", exe.display()))?;
|
||||
println!(
|
||||
"{} {}",
|
||||
"agent-manager removed:".green(),
|
||||
exe.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
// Immediate deletion: a hidden cmd retries for a few seconds, because
|
||||
// the executable is locked while we are still running.
|
||||
let script = format!(
|
||||
"for /L %i in (1,1,20) do @(del /f /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit /b 0 & ping -n 2 127.0.0.1 >nul)",
|
||||
exe.display()
|
||||
);
|
||||
let spawned = std::process::Command::new("cmd")
|
||||
.args(["/C", &script])
|
||||
.spawn()
|
||||
.is_ok();
|
||||
// Belt and braces: if something keeps the file locked, the standard
|
||||
// uninstaller mechanism deletes it at the next reboot.
|
||||
let wide: Vec<u16> = exe
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let at_reboot = unsafe {
|
||||
MoveFileExW(wide.as_ptr(), std::ptr::null(), MOVEFILE_DELAY_UNTIL_REBOOT) != 0
|
||||
};
|
||||
if !spawned && !at_reboot {
|
||||
bail!(
|
||||
"cannot schedule deletion of {} — delete it manually",
|
||||
exe.display()
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"{} {}",
|
||||
"agent-manager removed; the executable will be deleted momentarily:".green(),
|
||||
exe.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
bail!(
|
||||
"automatic removal of the executable is not supported on this platform; delete {} manually",
|
||||
exe.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user