- #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
103 lines
3.5 KiB
Rust
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)
|
|
}
|