use anyhow::{Context, Result};
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use tokio::process::Command;
use tokio::time::{timeout, Duration};
#[derive(Debug, Clone)]
pub struct SkillResult {
pub success: bool,
pub stdout: Arc<str>,
pub stderr: Arc<str>,
pub exit_code: Option<i32>,
}
pub struct ShellRunner {
timeout_secs: u64,
}
impl ShellRunner {
pub fn new(timeout_secs: u64) -> Self {
Self { timeout_secs }
}
pub async fn run(&self, skill_path: &Path, args: &[String]) -> Result<SkillResult> {
if !skill_path.exists() {
anyhow::bail!("Skill file not found: {:?}", skill_path);
}
let mut cmd = Command::new("bash");
cmd.arg(skill_path);
cmd.args(args);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let child = cmd.spawn().context("Failed to spawn skill process")?;
let timeout_duration = Duration::from_secs(self.timeout_secs);
match timeout(timeout_duration, child.wait_with_output()).await {
Ok(Ok(output)) => {
let stdout = Arc::from(String::from_utf8_lossy(&output.stdout).as_ref());
let stderr = Arc::from(String::from_utf8_lossy(&output.stderr).as_ref());
Ok(SkillResult {
success: output.status.success(),
stdout,
stderr,
exit_code: output.status.code(),
})
}
Ok(Err(e)) => {
anyhow::bail!("Failed to execute skill: {}", e);
}
Err(_) => {
anyhow::bail!("Skill execution timed out after {} seconds", self.timeout_secs);
}
}
}
}
impl Default for ShellRunner {
fn default() -> Self {
Self::new(30)
}
}