1use crate::error::ExecutorError;
2use skill_core::{ResourceType, Skill, SkillResult};
3use std::path::PathBuf;
4use std::process::Stdio;
5use std::time::Instant;
6use tokio::process::Command;
7use tracing::{debug, info};
8
9pub mod context;
10pub mod error;
11
12pub use context::ExecutionContext;
13
14pub struct SkillExecutor {
15 skills_base_dir: PathBuf,
16 default_timeout_secs: u64,
17}
18
19impl SkillExecutor {
20 pub fn new(skills_base_dir: PathBuf) -> Self {
21 Self {
22 skills_base_dir,
23 default_timeout_secs: 300,
24 }
25 }
26
27 pub fn with_timeout(mut self, secs: u64) -> Self {
28 self.default_timeout_secs = secs;
29 self
30 }
31
32 pub async fn execute_skill(
33 &self,
34 skill: &Skill,
35 input: Option<&str>,
36 context: &ExecutionContext,
37 ) -> Result<SkillResult, ExecutorError> {
38 let start = Instant::now();
39
40 info!("Executing skill: {}", skill.name);
41 debug!("Skill resources: {:?}", skill.resources);
42
43 let mut output = String::new();
44 let mut errors = Vec::new();
45
46 for resource in &skill.resources {
47 if resource.resource_type == ResourceType::Script {
48 match self.run_script(&resource.path, input, context).await {
49 Ok(result) => {
50 output.push_str(&format!("\n--- {} ---\n", resource.name));
51 output.push_str(&result);
52 }
53 Err(e) => {
54 errors.push(format!("{}: {}", resource.name, e));
55 }
56 }
57 }
58 }
59
60 if !skill.instructions.is_empty() {
61 output.push_str("\n--- Instructions ---\n");
62 output.push_str(&skill.instructions);
63 }
64
65 let execution_time_ms = start.elapsed().as_millis() as u64;
66 let success = errors.is_empty();
67
68 let error_msg = if errors.is_empty() {
69 None
70 } else {
71 Some(errors.join("; "))
72 };
73
74 Ok(SkillResult {
75 skill_id: skill.id.clone(),
76 success,
77 output,
78 error: error_msg,
79 execution_time_ms,
80 })
81 }
82
83 async fn run_script(
84 &self,
85 script_path: &PathBuf,
86 input: Option<&str>,
87 context: &ExecutionContext,
88 ) -> Result<String, ExecutorError> {
89 debug!("Running script: {:?} with input: {:?}", script_path, input);
90
91 let extension = script_path
92 .extension()
93 .and_then(|e| e.to_str())
94 .unwrap_or("");
95
96 let (program, args) = match extension {
97 "py" => ("python3", vec!["-u"]),
98 "sh" => ("bash", vec![]),
99 "rb" => ("ruby", vec![]),
100 "js" => ("node", vec![]),
101 _ => ("bash", vec![]),
102 };
103
104 let mut cmd = Command::new(program);
105 for arg in &args {
106 cmd.arg(arg);
107 }
108 cmd.arg(script_path)
109 .stdout(Stdio::piped())
110 .stderr(Stdio::piped())
111 .current_dir(&context.working_dir);
112
113 for (key, value) in &context.env_vars {
114 cmd.env(key, value);
115 }
116
117 if let Some(inp) = input {
118 cmd.arg(inp);
119 }
120
121 let output = cmd
122 .output()
123 .await
124 .map_err(|e| ExecutorError::ExecutionError(e.to_string()))?;
125
126 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
127 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
128
129 if !output.status.success() {
130 return Err(ExecutorError::ScriptError(format!(
131 "Script failed: {}",
132 stderr
133 )));
134 }
135
136 Ok(stdout)
137 }
138
139 pub async fn get_skill_instructions(&self, skill: &Skill) -> Result<String, ExecutorError> {
140 Ok(skill.instructions.clone())
141 }
142
143 pub async fn get_resource_content(
144 &self,
145 resource_path: &PathBuf,
146 ) -> Result<String, ExecutorError> {
147 let content = tokio::fs::read_to_string(resource_path)
148 .await
149 .map_err(|e| ExecutorError::IoError(e.to_string()))?;
150 Ok(content)
151 }
152}