capsula_capture_command/
lib.rs

1mod config;
2mod error;
3
4use crate::config::{CommandHookConfig, CommandHookFactory};
5use crate::error::CommandHookError;
6use capsula_core::captured::Captured;
7use capsula_core::error::CapsulaResult;
8use capsula_core::hook::{Hook, HookFactory, RuntimeParams};
9
10pub const KEY: &str = "capture-command";
11
12#[derive(Debug)]
13pub struct CommandHook {
14    pub config: CommandHookConfig,
15    pub command: Vec<String>,
16    pub abort_on_failure: bool,
17}
18
19#[derive(Debug)]
20pub struct CommandCaptured {
21    pub command: Vec<String>,
22    pub stdout: String,
23    pub stderr: String,
24    pub status: i32,
25    pub abort_requested: bool,
26}
27
28impl Hook for CommandHook {
29    type Config = CommandHookConfig;
30    type Output = CommandCaptured;
31
32    fn id(&self) -> String {
33        KEY.to_string()
34    }
35
36    fn config(&self) -> &Self::Config {
37        &self.config
38    }
39
40    fn run(&self, _params: &RuntimeParams) -> CapsulaResult<Self::Output> {
41        use std::process::Command;
42
43        if self.command.is_empty() {
44            return Err(CommandHookError::EmptyCommand.into());
45        }
46
47        let mut cmd = Command::new(&self.command[0]);
48        if self.command.len() > 1 {
49            cmd.args(&self.command[1..]);
50        }
51
52        let output = cmd
53            .output()
54            .map_err(|source| CommandHookError::ExecutionFailed {
55                command: self.command.join(" "),
56                source,
57            })?;
58
59        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
60        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
61        let status = output.status.code().unwrap_or(-1);
62
63        Ok(CommandCaptured {
64            command: self.command.clone(),
65            stdout,
66            stderr,
67            status,
68            abort_requested: self.abort_on_failure && status != 0,
69        })
70    }
71}
72
73impl Captured for CommandCaptured {
74    fn to_json(&self) -> serde_json::Value {
75        serde_json::json!({
76            "stdout": self.stdout,
77            "stderr": self.stderr,
78            "status": self.status,
79            "abort_requested": self.abort_requested,
80        })
81    }
82
83    fn abort_requested(&self) -> bool {
84        self.abort_requested
85    }
86}
87
88pub fn create_factory() -> Box<dyn HookFactory> {
89    Box::new(CommandHookFactory)
90}