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:
2026-08-15 19:59:24 -04:00
parent 2be5f8e8b2
commit ba07f42d09
11 changed files with 1177 additions and 60 deletions
+2
View File
@@ -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),
}
+144
View 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()
);
}
}