feat: phase 2 — playbooks (am history --save + am playbook, rejeu pas à pas) (closes #51)
This commit is contained in:
@@ -42,6 +42,37 @@ fn rerun_entry(app: &App, n: usize) -> Result<i32> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter history entries; returns them most recent first.
|
||||
/// Parse "12..25" (inclusive bounds, 1-based) or a single "12".
|
||||
fn parse_range(r: &str) -> Result<(usize, usize)> {
|
||||
let r = r.trim();
|
||||
if r.is_empty() {
|
||||
anyhow::bail!("empty range — usage: am history <from>..<to> --save <file>");
|
||||
}
|
||||
if let Some((a, b)) = r.split_once("..") {
|
||||
let from = a
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.map_err(|_| anyhow!("invalid range '{r}' — expected <from>..<to>"))?;
|
||||
let to = b
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.map_err(|_| anyhow!("invalid range '{r}' — expected <from>..<to>"))?;
|
||||
if from == 0 || to == 0 {
|
||||
anyhow::bail!("history entries start at 1");
|
||||
}
|
||||
Ok((from, to))
|
||||
} else {
|
||||
let n = r
|
||||
.parse::<usize>()
|
||||
.map_err(|_| anyhow!("invalid range '{r}' — expected <from>..<to>"))?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("history entries start at 1");
|
||||
}
|
||||
Ok((n, n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter history entries; returns them most recent first.
|
||||
pub fn filter(
|
||||
app: &App,
|
||||
@@ -96,11 +127,25 @@ pub fn run(
|
||||
session: Option<&str>,
|
||||
limit: usize,
|
||||
rerun: Option<usize>,
|
||||
range: Option<&str>,
|
||||
save: Option<&Path>,
|
||||
) -> Result<i32> {
|
||||
history::migrate(app)?;
|
||||
if let Some(n) = rerun {
|
||||
return rerun_entry(app, n);
|
||||
}
|
||||
// Issue #51: 'am history 12..25 --save <file>' exports a playbook.
|
||||
if let Some(r) = range {
|
||||
let file = save.ok_or_else(|| anyhow!("--save <file> is required with a range"))?;
|
||||
let (from, to) = parse_range(r)?;
|
||||
let (n, path) = crate::playbook::export_range(app, from, to, file)?;
|
||||
app.log
|
||||
.success(&format!("playbook saved: {} ({n} step(s))", path.display()));
|
||||
return Ok(0);
|
||||
}
|
||||
if save.is_some() {
|
||||
anyhow::bail!("usage: am history <from>..<to> --save <file>");
|
||||
}
|
||||
let rows = filter(app, kind, cwd, search, failed, session, limit);
|
||||
if app.json() {
|
||||
let payload: Vec<serde_json::Value> = rows
|
||||
|
||||
@@ -37,6 +37,7 @@ pub mod stats_cmd;
|
||||
pub mod status_cmd;
|
||||
pub mod suggest_cmd;
|
||||
pub mod sync_cmd;
|
||||
pub mod playbook_cmd;
|
||||
pub mod theme_cmd;
|
||||
pub mod timeline_cmd;
|
||||
pub mod tip_cmd;
|
||||
@@ -179,6 +180,8 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
session,
|
||||
limit,
|
||||
rerun,
|
||||
range,
|
||||
save,
|
||||
} => history_cmd::run(
|
||||
app,
|
||||
kind.as_deref(),
|
||||
@@ -188,7 +191,10 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
session.as_deref(),
|
||||
*limit,
|
||||
*rerun,
|
||||
range.as_deref(),
|
||||
save.as_deref(),
|
||||
),
|
||||
Command::Playbook { path, var } => playbook_cmd::run(app, path, var),
|
||||
Command::Info { agent } => info_cmd::run(app, agent),
|
||||
Command::Init { force, template } => {
|
||||
init_cmd::run(app, *force, template.as_deref())
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//! playbook: replay a saved playbook step by step (issue #51).
|
||||
//! Playbooks are created with 'am history <from>..<to> --save <file>'.
|
||||
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn run(app: &App, path: &Path, vars: &[String]) -> Result<i32> {
|
||||
let mut values = BTreeMap::new();
|
||||
for v in vars {
|
||||
let Some((k, val)) = v.split_once('=') else {
|
||||
anyhow::bail!("--var expects name=value, got '{v}'");
|
||||
};
|
||||
values.insert(k.trim().to_string(), val.trim().to_string());
|
||||
}
|
||||
let resolved = crate::playbook::resolve_dest(app, path);
|
||||
let used = if resolved.exists() { resolved } else { path.to_path_buf() };
|
||||
crate::playbook::run_playbook(app, &used, &values)
|
||||
}
|
||||
@@ -254,6 +254,12 @@ pub static SECTIONS: &[TipSection] = &[
|
||||
options: &[("--export", "crée agent-manager-migrate.amx"), ("--output <f>", "chemin du bundle")],
|
||||
example: "migrate --export --output backup.amx",
|
||||
},
|
||||
TipEntry {
|
||||
usage: "history 12..25 --save deploy.yaml",
|
||||
about: "exporte une plage d'historique en playbook YAML (état_dir/playbooks/)",
|
||||
options: &[("--save <f>", "fichier playbook"), ("playbook <f>", "rejoue pas à pas avec confirmation")],
|
||||
example: "history 12..25 --save deploy.yaml && playbook deploy.yaml",
|
||||
},
|
||||
TipEntry {
|
||||
usage: "timeline",
|
||||
about: "une vue chronologique de toute l'activité",
|
||||
|
||||
Reference in New Issue
Block a user