doot/crates/doot-core/src/hooks.rs
2026-02-05 23:18:33 -06:00

91 lines
2.1 KiB
Rust

//! Lifecycle hook execution.
use doot_lang::HookStage;
use std::process::Command;
use thiserror::Error;
/// Hook execution errors.
#[derive(Error, Debug)]
pub enum HookError {
#[error("hook failed: {command}: {message}")]
ExecutionFailed { command: String, message: String },
#[error("io error: {0}")]
IoError(#[from] std::io::Error),
}
/// A lifecycle hook.
#[derive(Debug, Clone)]
pub struct Hook {
pub stage: HookStage,
pub command: String,
pub working_dir: Option<std::path::PathBuf>,
}
/// Executes lifecycle hooks.
pub struct HookRunner {
hooks: Vec<Hook>,
dry_run: bool,
}
impl HookRunner {
/// Creates a new hook runner.
pub fn new() -> Self {
Self {
hooks: Vec::new(),
dry_run: false,
}
}
/// Sets dry run mode.
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
/// Registers a hook.
pub fn add_hook(&mut self, hook: Hook) {
self.hooks.push(hook);
}
/// Runs all hooks for a given stage.
#[tracing::instrument(skip(self))]
pub fn run_stage(&self, stage: HookStage) -> Result<(), HookError> {
for hook in self.hooks.iter().filter(|h| h.stage == stage) {
self.run_hook(hook)?;
}
Ok(())
}
#[tracing::instrument(skip(self), fields(command = %hook.command))]
fn run_hook(&self, hook: &Hook) -> Result<(), HookError> {
if self.dry_run {
println!("[dry-run] would run: {}", hook.command);
return Ok(());
}
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(&hook.command);
if let Some(ref dir) = hook.working_dir {
cmd.current_dir(dir);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(HookError::ExecutionFailed {
command: hook.command.clone(),
message: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
Ok(())
}
}
impl Default for HookRunner {
fn default() -> Self {
Self::new()
}
}