Skip to main content

hx_plugins/api/
shell.rs

1//! Shell command execution API for plugins.
2//!
3//! Provides: (hx/run), (hx/run-checked), (hx/run-silent)
4
5use crate::context::with_context;
6use crate::error::Result;
7use std::process::Command;
8use steel::SteelVal;
9use steel::steel_vm::engine::Engine;
10use steel::steel_vm::register_fn::RegisterFn;
11
12/// Register shell API functions.
13pub fn register(engine: &mut Engine) -> Result<()> {
14    engine.register_fn("hx/run", run_command);
15    engine.register_fn("hx/run-checked", run_checked);
16    engine.register_fn("hx/run-silent", run_silent);
17    Ok(())
18}
19
20/// Run a command and return an association list with exit-code, stdout, stderr.
21/// Returns: ((exit-code . N) (stdout . "...") (stderr . "..."))
22fn run_command(cmd: String, args: Vec<SteelVal>) -> SteelVal {
23    let args: Vec<String> = args
24        .into_iter()
25        .filter_map(|v| match v {
26            SteelVal::StringV(s) => Some(s.to_string()),
27            _ => None,
28        })
29        .collect();
30
31    // Get environment variables from context
32    let env_vars = with_context(|ctx| ctx.env_vars.clone()).unwrap_or_default();
33
34    let result = Command::new(&cmd).args(&args).envs(&env_vars).output();
35
36    match result {
37        Ok(output) => {
38            // Return as association list: ((exit-code . N) (stdout . "...") (stderr . "..."))
39            let exit_code = SteelVal::IntV(output.status.code().unwrap_or(-1) as isize);
40            let stdout =
41                SteelVal::StringV(String::from_utf8_lossy(&output.stdout).to_string().into());
42            let stderr =
43                SteelVal::StringV(String::from_utf8_lossy(&output.stderr).to_string().into());
44
45            SteelVal::ListV(
46                vec![
47                    SteelVal::ListV(vec![SteelVal::SymbolV("exit-code".into()), exit_code].into()),
48                    SteelVal::ListV(vec![SteelVal::SymbolV("stdout".into()), stdout].into()),
49                    SteelVal::ListV(vec![SteelVal::SymbolV("stderr".into()), stderr].into()),
50                ]
51                .into(),
52            )
53        }
54        Err(e) => SteelVal::ListV(
55            vec![
56                SteelVal::ListV(
57                    vec![SteelVal::SymbolV("exit-code".into()), SteelVal::IntV(-1)].into(),
58                ),
59                SteelVal::ListV(
60                    vec![
61                        SteelVal::SymbolV("stdout".into()),
62                        SteelVal::StringV("".into()),
63                    ]
64                    .into(),
65                ),
66                SteelVal::ListV(
67                    vec![
68                        SteelVal::SymbolV("stderr".into()),
69                        SteelVal::StringV(e.to_string().into()),
70                    ]
71                    .into(),
72                ),
73            ]
74            .into(),
75        ),
76    }
77}
78
79/// Run a command and return stdout, or raise an error on failure.
80fn run_checked(cmd: String, args: Vec<SteelVal>) -> std::result::Result<SteelVal, String> {
81    let args: Vec<String> = args
82        .into_iter()
83        .filter_map(|v| match v {
84            SteelVal::StringV(s) => Some(s.to_string()),
85            _ => None,
86        })
87        .collect();
88
89    let env_vars = with_context(|ctx| ctx.env_vars.clone()).unwrap_or_default();
90
91    let output = Command::new(&cmd)
92        .args(&args)
93        .envs(&env_vars)
94        .output()
95        .map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
96
97    if output.status.success() {
98        Ok(SteelVal::StringV(
99            String::from_utf8_lossy(&output.stdout).to_string().into(),
100        ))
101    } else {
102        let stderr = String::from_utf8_lossy(&output.stderr);
103        Err(format!(
104            "Command '{}' failed with exit code {:?}: {}",
105            cmd,
106            output.status.code(),
107            stderr
108        ))
109    }
110}
111
112/// Run a command silently and return just the exit code.
113fn run_silent(cmd: String, args: Vec<SteelVal>) -> SteelVal {
114    let args: Vec<String> = args
115        .into_iter()
116        .filter_map(|v| match v {
117            SteelVal::StringV(s) => Some(s.to_string()),
118            _ => None,
119        })
120        .collect();
121
122    let env_vars = with_context(|ctx| ctx.env_vars.clone()).unwrap_or_default();
123
124    let result = Command::new(&cmd).args(&args).envs(&env_vars).output();
125
126    match result {
127        Ok(output) => SteelVal::IntV(output.status.code().unwrap_or(-1) as isize),
128        Err(_) => SteelVal::IntV(-1),
129    }
130}