watch : supervision + redémarrage automatique (issue #35) #84
+14
@@ -91,6 +91,20 @@ pub enum Command {
|
|||||||
/// Agent name or alias
|
/// Agent name or alias
|
||||||
agent: String,
|
agent: String,
|
||||||
},
|
},
|
||||||
|
/// Supervise an agent and optionally restart it on crash
|
||||||
|
Watch {
|
||||||
|
/// Agent name or alias
|
||||||
|
agent: String,
|
||||||
|
/// Restart the agent automatically when it dies (with backoff)
|
||||||
|
#[arg(long)]
|
||||||
|
restart: bool,
|
||||||
|
/// Desktop notification on every restart
|
||||||
|
#[arg(long)]
|
||||||
|
notify: bool,
|
||||||
|
/// Check interval in seconds (default: 5)
|
||||||
|
#[arg(long, value_name = "SECS", default_value = "5")]
|
||||||
|
interval: u64,
|
||||||
|
},
|
||||||
/// Manage command aliases (cc -> claude-code)
|
/// Manage command aliases (cc -> claude-code)
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Alias(AliasCmd),
|
Alias(AliasCmd),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub mod status_cmd;
|
|||||||
pub mod timeline_cmd;
|
pub mod timeline_cmd;
|
||||||
pub mod uninstall_cmd;
|
pub mod uninstall_cmd;
|
||||||
pub mod update_cmd;
|
pub mod update_cmd;
|
||||||
|
pub mod watch_cmd;
|
||||||
pub mod version_cmd;
|
pub mod version_cmd;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
@@ -57,6 +58,12 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
|||||||
follow,
|
follow,
|
||||||
} => logs_cmd::run(app, agent, *lines, *follow),
|
} => logs_cmd::run(app, agent, *lines, *follow),
|
||||||
Command::Open { agent } => open_cmd::run(app, agent),
|
Command::Open { agent } => open_cmd::run(app, agent),
|
||||||
|
Command::Watch {
|
||||||
|
agent,
|
||||||
|
restart,
|
||||||
|
notify,
|
||||||
|
interval,
|
||||||
|
} => watch_cmd::run(app, agent, *restart, *notify, *interval),
|
||||||
Command::Alias(sub) => alias_cmd::run(app, sub),
|
Command::Alias(sub) => alias_cmd::run(app, sub),
|
||||||
Command::Start(a) => run_cmd::start(app, a),
|
Command::Start(a) => run_cmd::start(app, a),
|
||||||
Command::Stop {
|
Command::Stop {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//! watch: supervise one agent and optionally restart it on crash.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::cli::StartArgs;
|
||||||
|
use crate::process;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// One supervision check. Returns whether the agent is running afterwards.
|
||||||
|
/// Restarts are background starts (event + session recorded as usual).
|
||||||
|
pub fn check_once(app: &App, agent_name: &str, restart: bool, notify: bool) -> bool {
|
||||||
|
let entry = app.state.get(agent_name).ok().flatten();
|
||||||
|
let running = entry
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|e| e.pid)
|
||||||
|
.map(process::is_running)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if running {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if !restart {
|
||||||
|
app.log.warn(&format!("'{agent_name}' is not running"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let agent = match app.catalog.resolve(agent_name) {
|
||||||
|
Some(a) => a,
|
||||||
|
None => {
|
||||||
|
app.log.error(&format!("unknown agent '{agent_name}'"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
app.log.warn(&format!("'{agent_name}' is not running — restarting"));
|
||||||
|
let opts = StartArgs {
|
||||||
|
agent: Some(agent_name.to_string()),
|
||||||
|
background: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match run_cmd::start(app, &opts) {
|
||||||
|
Ok(_) => {
|
||||||
|
if notify {
|
||||||
|
process::desktop_notify("agent-manager", &format!("{} restarted", agent.title()));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.log.error(&format!("restart failed: {e:#}"));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(app: &App, agent: &str, restart: bool, notify: bool, interval: u64) -> Result<i32> {
|
||||||
|
require_agent(app, agent)?;
|
||||||
|
let interval = interval.max(1);
|
||||||
|
app.log.info(&format!("watching '{agent}' every {interval}s (Ctrl-C to stop)"));
|
||||||
|
let mut backoff = 1u64;
|
||||||
|
loop {
|
||||||
|
let ok = check_once(app, agent, restart, notify);
|
||||||
|
if ok {
|
||||||
|
backoff = 1;
|
||||||
|
std::thread::sleep(Duration::from_secs(interval));
|
||||||
|
} else if restart {
|
||||||
|
app.log.warn(&format!("retrying in {backoff}s"));
|
||||||
|
std::thread::sleep(Duration::from_secs(backoff));
|
||||||
|
backoff = (backoff * 2).min(60);
|
||||||
|
} else {
|
||||||
|
std::thread::sleep(Duration::from_secs(interval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_once_reports_dead_agent_without_restart() {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let dir = guard.path().to_path_buf();
|
||||||
|
std::mem::forget(guard);
|
||||||
|
let cfg = dir.join("config.yaml");
|
||||||
|
std::fs::write(
|
||||||
|
&cfg,
|
||||||
|
concat!(
|
||||||
|
"version: \"1.0\"\n",
|
||||||
|
"settings:\n",
|
||||||
|
" auto_install_deps: false\n",
|
||||||
|
"agents: []\n",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||||
|
let mut app = App::from_cli(cli).unwrap();
|
||||||
|
let mut p = app.paths.clone();
|
||||||
|
p.install_dir = dir.join("agents");
|
||||||
|
p.bin_dir = dir.join("agents").join("bin");
|
||||||
|
p.log_dir = dir.join("logs");
|
||||||
|
p.state_file = dir.join("state.json");
|
||||||
|
p.probe_cache_file = dir.join("probe-cache.json");
|
||||||
|
p.config_dir = Some(dir.join("config"));
|
||||||
|
app.paths = p;
|
||||||
|
// Agent absent de l état : pas running, pas de restart -> false.
|
||||||
|
assert!(!check_once(&app, "claude-code", false, false));
|
||||||
|
// Agent inconnu avec restart : erreur loguée, false.
|
||||||
|
assert!(!check_once(&app, "inconnu", true, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -312,6 +312,26 @@ pub static HELP_SPECS: &[HelpSpec] = &[
|
|||||||
HelpExample { desc: "Open the install directory.", code: "open claude-code" },
|
HelpExample { desc: "Open the install directory.", code: "open claude-code" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
HelpSpec {
|
||||||
|
name: "watch",
|
||||||
|
category: "Commands",
|
||||||
|
usage: "watch {flags} <agent>",
|
||||||
|
about: "Supervise an agent and optionally restart it on crash.",
|
||||||
|
search_terms: &["monitor", "supervise", "restart"],
|
||||||
|
flags: &[
|
||||||
|
HelpFlag { short: "", long: "--restart", value: "", desc: "Restart the agent automatically when it dies (with backoff)" },
|
||||||
|
HelpFlag { short: "", long: "--notify", value: "", desc: "Desktop notification on every restart" },
|
||||||
|
HelpFlag { short: "", long: "--interval", value: "SECS", desc: "Check interval in seconds (default: 5)" },
|
||||||
|
],
|
||||||
|
subcommands: &[],
|
||||||
|
parameters: &[
|
||||||
|
HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" },
|
||||||
|
],
|
||||||
|
io: None,
|
||||||
|
examples: &[
|
||||||
|
HelpExample { desc: "Supervise with auto-restart.", code: "watch claude-code --restart --notify" },
|
||||||
|
],
|
||||||
|
},
|
||||||
HelpSpec {
|
HelpSpec {
|
||||||
name: "alias",
|
name: "alias",
|
||||||
category: "Commands",
|
category: "Commands",
|
||||||
|
|||||||
+9
-1
@@ -70,6 +70,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
|||||||
("init", "generate a project config"),
|
("init", "generate a project config"),
|
||||||
("alias", "manage aliases"),
|
("alias", "manage aliases"),
|
||||||
("open", "open the install dir"),
|
("open", "open the install dir"),
|
||||||
|
("watch", "supervise an agent"),
|
||||||
("search", "search the catalog"),
|
("search", "search the catalog"),
|
||||||
("info", "agent details"),
|
("info", "agent details"),
|
||||||
("install", "install an agent"),
|
("install", "install an agent"),
|
||||||
@@ -146,7 +147,7 @@ impl AmCompleter {
|
|||||||
Self {
|
Self {
|
||||||
theme: std::cell::Cell::new(crate::theme::default_theme()),
|
theme: std::cell::Cell::new(crate::theme::default_theme()),
|
||||||
commands: vec![
|
commands: vec![
|
||||||
"list", "status", "sessions", "stats", "projects", "timeline", "log", "logs", "history", "init", "alias", "open", "search", "info", "install", "uninstall", "update",
|
"list", "status", "sessions", "stats", "projects", "timeline", "log", "logs", "history", "init", "alias", "open", "watch", "search", "info", "install", "uninstall", "update",
|
||||||
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
||||||
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
||||||
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
||||||
@@ -1230,6 +1231,12 @@ fn handle_line(
|
|||||||
"open" => Command::Open {
|
"open" => Command::Open {
|
||||||
agent: need("agent name")?,
|
agent: need("agent name")?,
|
||||||
},
|
},
|
||||||
|
"watch" => Command::Watch {
|
||||||
|
agent: need("agent name")?,
|
||||||
|
restart: flag("--restart"),
|
||||||
|
notify: flag("--notify"),
|
||||||
|
interval: opt_value("--interval").and_then(|v| v.parse().ok()).unwrap_or(5),
|
||||||
|
},
|
||||||
"alias" => match rest.first().map(|s| s.as_str()) {
|
"alias" => match rest.first().map(|s| s.as_str()) {
|
||||||
Some("add") if rest.len() >= 3 => Command::Alias(crate::cli::AliasCmd::Add {
|
Some("add") if rest.len() >= 3 => Command::Alias(crate::cli::AliasCmd::Add {
|
||||||
name: rest[1].clone(),
|
name: rest[1].clone(),
|
||||||
@@ -1398,6 +1405,7 @@ fn is_am_command(word: &str) -> bool {
|
|||||||
| "init"
|
| "init"
|
||||||
| "alias"
|
| "alias"
|
||||||
| "open"
|
| "open"
|
||||||
|
| "watch"
|
||||||
| "search"
|
| "search"
|
||||||
| "info"
|
| "info"
|
||||||
| "install"
|
| "install"
|
||||||
|
|||||||
Reference in New Issue
Block a user