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