Skip to main content

grex_cli/cli/verbs/
run.rs

1//! `grex run` — execute a single named action across one pack.
2//!
3//! v1.4.0 ships the single-pack form: load the manifest at the
4//! resolved pack root, filter `actions:` to entries whose action
5//! kind equals `<action>` (per [`grex_core::Action::name`]), execute
6//! them through the standard executor with the registered plugins.
7//! Recursive walks over child packs are deferred to v1.5.0.
8
9use crate::cli::args::{GlobalFlags, RunArgs};
10use anyhow::Result;
11use grex_core::execute::{ActionExecutor, ExecCtx, ExecStep, Platform};
12use grex_core::tree::{FsPackLoader, PackLoader};
13use grex_core::vars::VarEnv;
14use grex_core::{register_builtins, FsExecutor, PlanExecutor, Registry};
15use std::sync::Arc;
16use tokio_util::sync::CancellationToken;
17
18pub fn run(args: RunArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
19    let Some(pack_root) = super::resolve_pack_root_or_cwd(args.pack_root.as_deref()) else {
20        emit_error(
21            global.json,
22            "usage",
23            "`<pack_root>` required (directory with `.grex/pack.yaml`)",
24        );
25        std::process::exit(2);
26    };
27    let manifest = match FsPackLoader::new().load(&pack_root) {
28        Ok(m) => m,
29        Err(err) => {
30            emit_error(global.json, "load_manifest", &err.to_string());
31            std::process::exit(3);
32        }
33    };
34    let target = args.action.as_str();
35    let matched: Vec<(usize, &grex_core::Action)> =
36        manifest.actions.iter().enumerate().filter(|(_, a)| a.name() == target).collect();
37    if matched.is_empty() {
38        emit_no_match(global.json, target);
39        return Ok(());
40    }
41    let (steps, had_err) = dispatch_actions(&matched, &pack_root, global.dry_run, global.json);
42    emit_report(global.json, target, &pack_root, &steps, global.dry_run);
43    if had_err {
44        std::process::exit(2);
45    }
46    Ok(())
47}
48
49/// Execute each filtered action in order through `PlanExecutor`
50/// (dry-run) or `FsExecutor` (wet-run). Stops on the first failure
51/// so partial state is bounded by the first-failure index. Returns
52/// the accumulated steps + a flag indicating whether execution
53/// halted on an error.
54fn dispatch_actions(
55    matched: &[(usize, &grex_core::Action)],
56    pack_root: &std::path::Path,
57    dry_run: bool,
58    json: bool,
59) -> (Vec<(usize, ExecStep)>, bool) {
60    let mut registry = Registry::new();
61    register_builtins(&mut registry);
62    let registry = Arc::new(registry);
63    let vars = VarEnv::new();
64    let plan = PlanExecutor::with_registry(registry.clone());
65    let fs = FsExecutor::with_registry(registry);
66    let mut steps: Vec<(usize, ExecStep)> = Vec::new();
67    for (idx, action) in matched {
68        let ctx = ExecCtx::new(&vars, pack_root, pack_root).with_platform(Platform::current());
69        let result = if dry_run { plan.execute(action, &ctx) } else { fs.execute(action, &ctx) };
70        match result {
71            Ok(step) => steps.push((*idx, step)),
72            Err(err) => {
73                emit_error(json, "exec", &format!("action[{idx}] failed: {err}"));
74                return (steps, true);
75            }
76        }
77    }
78    (steps, false)
79}
80
81fn emit_no_match(json: bool, action: &str) {
82    if json {
83        let doc = serde_json::json!({
84            "verb": "run",
85            "action": action,
86            "matched_packs": 0,
87            "steps": [],
88        });
89        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
90    } else {
91        println!("grex run: no packs declare action `{action}`");
92    }
93}
94
95fn emit_report(
96    json: bool,
97    action: &str,
98    pack_root: &std::path::Path,
99    steps: &[(usize, ExecStep)],
100    dry_run: bool,
101) {
102    if json {
103        let step_docs: Vec<serde_json::Value> = steps
104            .iter()
105            .map(|(idx, _s)| {
106                serde_json::json!({
107                    "action_idx": idx,
108                    "action": action,
109                })
110            })
111            .collect();
112        let doc = serde_json::json!({
113            "verb": "run",
114            "action": action,
115            "pack_root": pack_root.display().to_string(),
116            "dry_run": dry_run,
117            "matched_packs": if steps.is_empty() { 0 } else { 1 },
118            "steps": step_docs,
119        });
120        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
121    } else {
122        let prefix = if dry_run { "DRY-RUN: would run" } else { "ran" };
123        for (idx, _s) in steps {
124            println!("{prefix} action[{idx}] `{action}` in {}", pack_root.display());
125        }
126    }
127}
128
129fn emit_error(json: bool, kind: &str, msg: &str) {
130    if json {
131        let doc = serde_json::json!({
132            "verb": "run",
133            "error": { "kind": kind, "message": msg },
134        });
135        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
136    } else {
137        eprintln!("grex run: {msg}");
138    }
139}