Skip to main content

grex_cli/cli/verbs/
exec.rs

1//! `grex exec` — run an arbitrary command inside a pack root.
2//!
3//! Resolves the pack root via the shared cwd-default helper (same as
4//! `sync` / `teardown`). The first positional element is the program
5//! name; subsequent elements are passed verbatim. Stdio is inherited
6//! in human mode and captured under `--json`. The child's exit code
7//! is propagated, clamped at 125 so it never collides with the sync
8//! exit-code band (1/2/3).
9
10use crate::cli::args::{ExecArgs, GlobalFlags};
11use anyhow::Result;
12use std::path::PathBuf;
13use std::process::Command;
14use tokio_util::sync::CancellationToken;
15
16pub fn run(args: ExecArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
17    let json = global.json;
18    let cwd = match resolve_cwd(args.pack) {
19        Some(p) => p,
20        None => {
21            emit_error(json, "usage", "no pack root: cwd lacks .grex/pack.yaml");
22            std::process::exit(2);
23        }
24    };
25
26    let (program, rest) = match args.cmd.split_first() {
27        Some((p, r)) => (p.clone(), r.to_vec()),
28        None => {
29            // clap enforces required=true, but defend the boundary anyway.
30            emit_error(json, "usage", "command is required");
31            std::process::exit(2);
32        }
33    };
34
35    let mut cmd = Command::new(&program);
36    cmd.args(&rest).current_dir(&cwd);
37
38    let exit_code =
39        if json { run_capture(&mut cmd, &cwd, &program) } else { run_inherit(&mut cmd, &program) };
40    std::process::exit(exit_code.min(125));
41}
42
43fn resolve_cwd(explicit: Option<PathBuf>) -> Option<PathBuf> {
44    if let Some(p) = explicit {
45        return Some(p);
46    }
47    let cwd = std::env::current_dir().ok()?;
48    if cwd.join(".grex").join("pack.yaml").is_file() {
49        Some(cwd)
50    } else {
51        None
52    }
53}
54
55fn run_inherit(cmd: &mut Command, program: &str) -> i32 {
56    match cmd.status() {
57        Ok(status) => status.code().unwrap_or(125),
58        Err(err) => {
59            eprintln!("grex exec: spawn `{program}` failed: {err}");
60            127
61        }
62    }
63}
64
65fn run_capture(cmd: &mut Command, cwd: &std::path::Path, program: &str) -> i32 {
66    match cmd.output() {
67        Ok(out) => {
68            let code = out.status.code().unwrap_or(125);
69            let doc = serde_json::json!({
70                "verb": "exec",
71                "cwd": cwd.display().to_string(),
72                "exit_code": code,
73                "stdout": String::from_utf8_lossy(&out.stdout).into_owned(),
74                "stderr": String::from_utf8_lossy(&out.stderr).into_owned(),
75            });
76            println!("{}", serde_json::to_string(&doc).unwrap_or_default());
77            code
78        }
79        Err(err) => {
80            let doc = serde_json::json!({
81                "verb": "exec",
82                "error": { "kind": "spawn_failed", "program": program, "message": err.to_string() },
83            });
84            println!("{}", serde_json::to_string(&doc).unwrap_or_default());
85            127
86        }
87    }
88}
89
90fn emit_error(json: bool, kind: &str, msg: &str) {
91    if json {
92        let doc = serde_json::json!({
93            "verb": "exec",
94            "error": { "kind": kind, "message": msg },
95        });
96        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
97    } else {
98        eprintln!("grex exec: {msg}");
99    }
100}