Merge pull request 'hooks : on_install/on_start/on_stop/on_update (issue #34)' (#83) from feat/hooks into main
This commit was merged in pull request #83.
This commit is contained in:
@@ -143,6 +143,11 @@ pub fn run(app: &App, agent_name: &str, method_sel: Option<&str>, force: bool) -
|
|||||||
.with_agent(agent.name.clone())
|
.with_agent(agent.name.clone())
|
||||||
.with_args(vec![format!("method={}", method.kind)]),
|
.with_args(vec![format!("method={}", method.kind)]),
|
||||||
);
|
);
|
||||||
|
crate::hooks::run_hooks(
|
||||||
|
app,
|
||||||
|
"on_install",
|
||||||
|
&std::env::current_dir().unwrap_or_default(),
|
||||||
|
);
|
||||||
app.log.success(&format!(
|
app.log.success(&format!(
|
||||||
"installed {} (v{version}) in {}",
|
"installed {} (v{version}) in {}",
|
||||||
agent.title(),
|
agent.title(),
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ fn start_one(
|
|||||||
let bin = PathBuf::from(&exec.program);
|
let bin = PathBuf::from(&exec.program);
|
||||||
let is_managed = app.state.get(&agent.name).ok().flatten().is_some();
|
let is_managed = app.state.get(&agent.name).ok().flatten().is_some();
|
||||||
let ctx = crate::context::detect(&std::env::current_dir().unwrap_or_default());
|
let ctx = crate::context::detect(&std::env::current_dir().unwrap_or_default());
|
||||||
|
let current = std::env::current_dir().unwrap_or_default();
|
||||||
|
crate::hooks::run_hooks(app, "on_start", cwd.unwrap_or(¤t));
|
||||||
if background {
|
if background {
|
||||||
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env, cwd)?;
|
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env, cwd)?;
|
||||||
if app.dry_run() {
|
if app.dry_run() {
|
||||||
@@ -194,6 +196,8 @@ fn stop_one(app: &App, agent: &AgentDef, force: bool, timeout: u64) -> Result<i3
|
|||||||
);
|
);
|
||||||
let _ = sessions::finish_agent(app, &agent.name, Some(pid), 0, true);
|
let _ = sessions::finish_agent(app, &agent.name, Some(pid), 0, true);
|
||||||
app.state.update_pid(&agent.name, None, None)?;
|
app.state.update_pid(&agent.name, None, None)?;
|
||||||
|
let current = std::env::current_dir().unwrap_or_default();
|
||||||
|
crate::hooks::run_hooks(app, "on_stop", ¤t);
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
app.log.info(&format!("stopping '{}' (pid {pid})", agent.name));
|
app.log.info(&format!("stopping '{}' (pid {pid})", agent.name));
|
||||||
@@ -210,6 +214,8 @@ fn stop_one(app: &App, agent: &AgentDef, force: bool, timeout: u64) -> Result<i3
|
|||||||
app.emit(&ev);
|
app.emit(&ev);
|
||||||
let _ = sessions::finish_agent(app, &agent.name, Some(pid), if stopped { 0 } else { 1 }, stopped);
|
let _ = sessions::finish_agent(app, &agent.name, Some(pid), if stopped { 0 } else { 1 }, stopped);
|
||||||
app.state.update_pid(&agent.name, None, None)?;
|
app.state.update_pid(&agent.name, None, None)?;
|
||||||
|
let current = std::env::current_dir().unwrap_or_default();
|
||||||
|
crate::hooks::run_hooks(app, "on_stop", ¤t);
|
||||||
if stopped {
|
if stopped {
|
||||||
app.log.success(&format!("{} stopped", agent.title()));
|
app.log.success(&format!("{} stopped", agent.title()));
|
||||||
Ok(0)
|
Ok(0)
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ fn update_one(app: &App, runner: &dyn Runner, name: &str) -> Result<i32> {
|
|||||||
.with_agent(agent.name.clone())
|
.with_agent(agent.name.clone())
|
||||||
.with_args(vec![format!("method={}", method.kind)]),
|
.with_args(vec![format!("method={}", method.kind)]),
|
||||||
);
|
);
|
||||||
|
crate::hooks::run_hooks(
|
||||||
|
app,
|
||||||
|
"on_update",
|
||||||
|
&std::env::current_dir().unwrap_or_default(),
|
||||||
|
);
|
||||||
app.log.success(&format!(
|
app.log.success(&format!(
|
||||||
"updated {} to v{version}",
|
"updated {} to v{version}",
|
||||||
agent.title()
|
agent.title()
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ pub struct Settings {
|
|||||||
/// --theme flag overrides it).
|
/// --theme flag overrides it).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub theme: Option<String>,
|
pub theme: Option<String>,
|
||||||
|
/// Lifecycle hooks: on_install / on_start / on_stop / on_update command
|
||||||
|
/// lines (global; project profiles can add their own).
|
||||||
|
#[serde(default)]
|
||||||
|
pub hooks: BTreeMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A project profile: everything am needs to know about one project.
|
/// A project profile: everything am needs to know about one project.
|
||||||
@@ -497,6 +501,9 @@ pub fn merge(base: &mut Config, overlay: Config) {
|
|||||||
if o.self_update_repo.is_some() {
|
if o.self_update_repo.is_some() {
|
||||||
s.self_update_repo = o.self_update_repo;
|
s.self_update_repo = o.self_update_repo;
|
||||||
}
|
}
|
||||||
|
for (k, v) in o.hooks {
|
||||||
|
s.hooks.insert(k, v);
|
||||||
|
}
|
||||||
for (k, v) in overlay.aliases {
|
for (k, v) in overlay.aliases {
|
||||||
base.aliases.insert(k, v);
|
base.aliases.insert(k, v);
|
||||||
}
|
}
|
||||||
|
|||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
//! Hooks: user-defined commands run around agent lifecycle events
|
||||||
|
//! (on_install, on_start, on_stop, on_update). Global settings.hooks run
|
||||||
|
//! first, then the hooks of the project profile containing the cwd.
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::runner::SystemRunner;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Run every hook registered for an event, in order. Failures are warnings
|
||||||
|
/// and never break the main command; --dry-run announces them without
|
||||||
|
/// executing anything.
|
||||||
|
pub fn run_hooks(app: &App, hook: &str, cwd: &Path) {
|
||||||
|
let mut cmds: Vec<String> = Vec::new();
|
||||||
|
if let Some(global) = app.config.settings.hooks.get(hook) {
|
||||||
|
cmds.extend(global.iter().cloned());
|
||||||
|
}
|
||||||
|
if let Some((_, profile)) = crate::projects::profile_for(app, cwd) {
|
||||||
|
if let Some(list) = profile.hooks.get(hook) {
|
||||||
|
cmds.extend(list.iter().cloned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmds.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let runner = SystemRunner::new(app.dry_run(), app.cli.verbose, &app.log);
|
||||||
|
for cmd in &cmds {
|
||||||
|
let tokens = shell_words::split(cmd).unwrap_or_else(|_| vec![cmd.clone()]);
|
||||||
|
if tokens.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prog = tokens[0].clone();
|
||||||
|
let args: Vec<String> = tokens[1..].to_vec();
|
||||||
|
app.log.info(&format!("hook {hook}: {cmd}"));
|
||||||
|
if let Err(e) = crate::installers::sh(&runner, &prog, &args, &BTreeMap::new(), Some(cwd)) {
|
||||||
|
app.log.warn(&format!("hook {hook} failed: {e:#}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
fn app_with(dir: &std::path::Path) -> App {
|
||||||
|
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;
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hook_runs_and_writes_marker() {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let dir = guard.path().to_path_buf();
|
||||||
|
std::mem::forget(guard);
|
||||||
|
let marker = dir.join("marker.txt");
|
||||||
|
let mut app = app_with(&dir);
|
||||||
|
let cmd = format!("sh -c \"echo ran > {}\"", marker.display().to_string().replace("\\", "/"));
|
||||||
|
app.config.settings.hooks.insert("on_start".to_string(), vec![cmd]);
|
||||||
|
run_hooks(&app, "on_start", &dir);
|
||||||
|
assert!(marker.exists(), "hook a ecrit le marqueur");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failing_hook_warns_without_panicking() {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let dir = guard.path().to_path_buf();
|
||||||
|
std::mem::forget(guard);
|
||||||
|
let mut app = app_with(&dir);
|
||||||
|
app.config.settings.hooks.insert("on_start".to_string(), vec!["sh -c exit 3".to_string()]);
|
||||||
|
run_hooks(&app, "on_start", &dir); // ne panique pas
|
||||||
|
run_hooks(&app, "on_update", &dir); // aucun hook : no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dry_run_announces_without_executing() {
|
||||||
|
let guard = tempfile::tempdir().unwrap();
|
||||||
|
let dir = guard.path().to_path_buf();
|
||||||
|
std::mem::forget(guard);
|
||||||
|
let marker = dir.join("marker2.txt");
|
||||||
|
let mut app = app_with(&dir);
|
||||||
|
let cmd = format!("sh -c \"echo ran > {}\"", marker.display().to_string().replace("\\", "/"));
|
||||||
|
app.config.settings.hooks.insert("on_start".to_string(), vec![cmd]);
|
||||||
|
app.cli.dry_run = true;
|
||||||
|
run_hooks(&app, "on_start", &dir);
|
||||||
|
assert!(!marker.exists(), "dry-run ne doit rien executer");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_hooks_parse_from_yaml() {
|
||||||
|
let cfg: crate::config::Config = serde_yaml::from_str(
|
||||||
|
"version: \"1.0\"\nsettings:\n hooks:\n on_start: [\"sh -c echo hi\"]\nagents: []\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cfg.settings.hooks["on_start"],
|
||||||
|
vec!["sh -c echo hi".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub mod download;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
|
pub mod hooks;
|
||||||
pub mod installers;
|
pub mod installers;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
|||||||
Reference in New Issue
Block a user