capsula_capture_command/
lib.rs1mod 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(CommandHook { 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}