Files
agent-manager/src/commands/plugins_cmd.rs
T
bruno 4c7866a25a feat: phase 2 — am lab (benchmark N agents) + plugins d'événements (contrat JSON)
- #62: am lab --agents a,b --task <f> — séquentiel ou --parallel, capture
  durée/exit/sortie/coût estimé, rapport comparatif tableau + --json stable,
  tâches versionnables dans <state>/lab/ (--list), rejouables à l'identique
- #75: plugins/ sous state_dir, déclenchés sur on_install/on_start/on_stop/
  on_update (App::emit), contrat JSON stdin/stdout, timeout configurable,
  échec non bloquant, garde anti-récursion AM_PLUGINS_RUNNING, am plugins
  + am plugins --test <name> (CI), exemples notify + ci-webhook
- intégration REPL (parse, complétion, bannière, is_am_command), help, tips,
  man pages (am-lab.1, am-plugins.1), README, ROADMAP (26/27), v0.5.4
- 334 tests verts (309 + 25)

closes #62
closes #75
2026-08-18 14:17:30 -04:00

103 lines
3.5 KiB
Rust

//! plugins: list the event plugins and test one against a synthetic event
//! (issue #75). 'am plugins --test <name>' is the CI hook: it exercises the
//! JSON contract end to end and exits non-zero on failure.
use super::*;
use crate::cli::PluginsArgs;
use crate::plugins::{self, PluginStatus};
use anyhow::Result;
pub fn run(app: &App, args: &PluginsArgs) -> Result<i32> {
let dir = plugins::dir(app);
let all = plugins::discover(app, &dir);
if let Some(name) = &args.test {
let Some(plugin) = all.iter().find(|p| &p.name == name) else {
anyhow::bail!(
"unknown plugin '{name}' — 'am plugins' lists the plugins of {}",
dir.display()
);
};
// Synthetic event (a start, agent 'test') to exercise the contract.
let event = crate::events::Event::now(crate::events::EventKind::Start)
.with_agent("test")
.with_args(vec!["--probe".to_string()]);
let r = plugins::run_plugin(app, plugin, "on_start", &event);
if app.json() {
crate::output::print_json(&serde_json::json!({
"name": plugin.name,
"status": r.status.as_str(),
"exit_code": r.exit_code,
"duration_ms": r.duration_ms,
"response": r.response,
"stdout": r.stdout,
"stderr": r.stderr,
"error": r.error,
}));
} else {
app.log.info(&format!(
"plugin '{}' — {} in {}ms",
plugin.name,
r.status.as_str(),
r.duration_ms
));
if let Some(code) = r.exit_code {
app.log.info(&format!("exit code: {code}"));
}
if let Some(resp) = &r.response {
if let Some(msg) = &resp.message {
app.log.info(&format!("message: {msg}"));
}
if let Some(err) = &resp.error {
app.log.warn(&format!("error: {err}"));
}
}
if !r.stdout.trim().is_empty() {
app.log.info(&format!("stdout: {}", r.stdout.trim()));
}
if !r.stderr.trim().is_empty() {
app.log.warn(&format!("stderr: {}", r.stderr.trim()));
}
if let Some(e) = &r.error {
app.log.warn(&format!("{e}"));
}
}
return Ok(if r.status == PluginStatus::Ok { 0 } else { 1 });
}
if app.json() {
let items: Vec<serde_json::Value> = all
.iter()
.map(|p| {
serde_json::json!({
"name": p.name,
"events": p.events,
"timeout_secs": p.timeout.as_secs(),
"script": p.file.display().to_string(),
})
})
.collect();
crate::output::print_json(&items);
return Ok(0);
}
if all.is_empty() {
app.log.info(&format!(
"no plugins in {} — copy one from examples/plugins/ (notify, ci-webhook)",
dir.display()
));
return Ok(0);
}
let mut table = crate::output::Table::new(vec!["PLUGIN", "EVENTS", "TIMEOUT", "SCRIPT"]);
for p in &all {
table.row(vec![
p.name.clone(),
p.events.join(", "),
format!("{}s", p.timeout.as_secs()),
p.file.display().to_string(),
]);
}
print!("{}", table.render());
Ok(0)
}