Skip to main content

capsula_capture_command/
lib.rs

1mod error;
2
3use crate::error::CommandHookError;
4use capsula_core::captured::Captured;
5use capsula_core::error::CapsulaResult;
6use capsula_core::hook::{Hook, PhaseMarker, RuntimeParams};
7use capsula_core::run::PreparedRun;
8use serde::{Deserialize, Serialize};
9use tracing::debug;
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct CommandHookConfig {
13    command: Vec<String>,
14    #[serde(default)]
15    abort_on_failure: bool,
16}
17
18#[derive(Debug)]
19pub struct CommandHook {
20    config: CommandHookConfig,
21}
22
23#[derive(Debug, Serialize)]
24pub struct CommandCaptured {
25    stdout: String,
26    stderr: String,
27    status: i32,
28    #[serde(skip)]
29    abort_requested: bool,
30}
31
32impl<P> Hook<P> for CommandHook
33where
34    P: PhaseMarker,
35{
36    const ID: &'static str = "capture-command";
37
38    type Config = CommandHookConfig;
39    type Output = CommandCaptured;
40
41    fn from_config(
42        config: &serde_json::Value,
43        _project_root: &std::path::Path,
44    ) -> CapsulaResult<Self> {
45        let config: CommandHookConfig = serde_json::from_value(config.clone())?;
46        Ok(Self { config })
47    }
48
49    fn config(&self) -> &Self::Config {
50        &self.config
51    }
52
53    fn run(
54        &self,
55        _metadata: &PreparedRun,
56        _params: &RuntimeParams<P>,
57    ) -> CapsulaResult<Self::Output> {
58        use std::process::Command;
59
60        if self.config.command.is_empty() {
61            return Err(CommandHookError::EmptyCommand.into());
62        }
63
64        debug!("CommandHook: Executing command: {:?}", self.config.command);
65        let mut cmd = Command::new(&self.config.command[0]);
66        if self.config.command.len() > 1 {
67            cmd.args(&self.config.command[1..]);
68        }
69
70        let output = cmd
71            .output()
72            .map_err(|source| CommandHookError::ExecutionFailed {
73                command: self.config.command.join(" "),
74                source,
75            })?;
76
77        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
78        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
79        let status = output.status.code().unwrap_or(-1);
80
81        debug!(
82            "CommandHook: Command completed with exit code {}, {} bytes stdout, {} bytes stderr",
83            status,
84            stdout.len(),
85            stderr.len()
86        );
87
88        let abort_requested = self.config.abort_on_failure && status != 0;
89        if abort_requested {
90            debug!("CommandHook: Requesting abort due to command failure");
91        }
92
93        Ok(CommandCaptured {
94            stdout,
95            stderr,
96            status,
97            abort_requested,
98        })
99    }
100}
101
102impl Captured for CommandCaptured {
103    fn serialize_json(&self) -> Result<serde_json::Value, serde_json::Error> {
104        serde_json::to_value(self)
105    }
106
107    fn abort_requested(&self) -> bool {
108        self.abort_requested
109    }
110}