use super::executor::{CommandError, CommandExecutor, CommandResult, ExecutionContext};
use crate::cook::workflow::{CommandType, WorkflowStep};
use crate::subprocess::{ProcessCommandBuilder, ProcessRunner};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
pub struct ShellCommandExecutor {
runner: Arc<dyn ProcessRunner>,
}
impl ShellCommandExecutor {
pub fn new() -> Self {
Self {
runner: Arc::new(crate::subprocess::runner::TokioProcessRunner),
}
}
pub fn with_runner(runner: Arc<dyn ProcessRunner>) -> Self {
Self { runner }
}
fn build_command(
command_text: &str,
context: &ExecutionContext,
) -> crate::subprocess::ProcessCommand {
let mut builder = ProcessCommandBuilder::new("sh")
.args(["-c", command_text])
.current_dir(&context.worktree_path);
builder = builder
.env("PRODIGY_WORKTREE", &context.worktree_name)
.env("PRODIGY_ITEM_ID", &context.item_id)
.env("PRODIGY_AUTOMATION", "true");
builder.build()
}
fn build_result(output: crate::subprocess::ProcessOutput, start: Instant) -> CommandResult {
CommandResult {
output: Some(output.stdout.clone()),
exit_code: output.status.code().unwrap_or(-1),
variables: HashMap::new(),
duration: start.elapsed(),
success: output.status.success(),
stderr: output.stderr,
json_log_location: None, }
}
}
#[async_trait]
impl CommandExecutor for ShellCommandExecutor {
async fn execute(
&self,
step: &WorkflowStep,
context: &ExecutionContext,
) -> Result<CommandResult, CommandError> {
let start = Instant::now();
let command = step.shell.as_ref().ok_or_else(|| {
CommandError::InvalidConfiguration("No shell command in step".to_string())
})?;
let process_command = Self::build_command(command, context);
let output = if let Some(timeout_secs) = step.timeout {
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
let mut cmd_with_timeout = process_command.clone();
cmd_with_timeout.timeout = Some(timeout_duration);
self.runner
.run(cmd_with_timeout)
.await
.map_err(|e| CommandError::ExecutionFailed(e.to_string()))?
} else {
self.runner
.run(process_command)
.await
.map_err(|e| CommandError::ExecutionFailed(e.to_string()))?
};
Ok(Self::build_result(output, start))
}
fn supports(&self, command_type: &CommandType) -> bool {
matches!(command_type, CommandType::Shell(_) | CommandType::Test(_))
}
}
impl Default for ShellCommandExecutor {
fn default() -> Self {
Self::new()
}
}