ryo_executor/executor/
core.rs1use super::context::ExecutionContext;
4use super::traits::{Executor, ExecutorError};
5use crate::decider::{Action, ActionKind, ActionResult};
6use std::process::Command;
7use std::time::Instant;
8
9pub struct CoreExecutor;
15
16impl CoreExecutor {
17 pub fn new() -> Self {
19 Self
20 }
21
22 fn execute_read(
24 &self,
25 action: &Action,
26 ctx: &ExecutionContext,
27 ) -> Result<ActionResult, ExecutorError> {
28 let start = Instant::now();
29
30 let path = action
31 .target
32 .as_ref()
33 .ok_or_else(|| ExecutorError::Other("Read requires a target path".to_string()))?;
34
35 let full_path = ctx.resolve_path(path);
36
37 let output = Command::new("cat")
38 .arg("-n")
39 .arg(&full_path)
40 .current_dir(&ctx.working_dir)
41 .output()
42 .map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
43
44 let stdout = String::from_utf8_lossy(&output.stdout);
45 let stderr = String::from_utf8_lossy(&output.stderr);
46
47 let duration = start.elapsed();
48
49 if output.status.success() {
50 Ok(ActionResult::success(action.clone())
51 .with_output(truncate_lines(&stdout, ctx.max_output_lines))
52 .with_duration(duration.as_micros() as u64))
53 } else {
54 Ok(ActionResult::failure(action.clone(), stderr.to_string())
55 .with_duration(duration.as_micros() as u64))
56 }
57 }
58
59 fn execute_grep(
61 &self,
62 action: &Action,
63 ctx: &ExecutionContext,
64 ) -> Result<ActionResult, ExecutorError> {
65 let start = Instant::now();
66
67 let pattern = action
68 .args
69 .get("pattern")
70 .ok_or_else(|| ExecutorError::Other("Grep requires a pattern".to_string()))?;
71
72 let path = action.target.as_deref().unwrap_or(".");
73
74 let mut cmd = Command::new(&ctx.rg_path);
75 cmd.arg("--color=never")
76 .arg("--line-number")
77 .arg("--no-heading")
78 .arg(pattern)
79 .arg(path)
80 .current_dir(&ctx.working_dir);
81
82 let output = cmd
83 .output()
84 .map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
85
86 let stdout = String::from_utf8_lossy(&output.stdout);
87 let stderr = String::from_utf8_lossy(&output.stderr);
88 let duration = start.elapsed();
89
90 let success = matches!(output.status.code(), Some(0) | Some(1));
93
94 if success {
95 Ok(ActionResult::success(action.clone())
96 .with_output(truncate_lines(&stdout, ctx.max_output_lines))
97 .with_duration(duration.as_micros() as u64))
98 } else {
99 Ok(ActionResult::failure(action.clone(), stderr.to_string())
100 .with_duration(duration.as_micros() as u64))
101 }
102 }
103
104 fn execute_glob(
106 &self,
107 action: &Action,
108 ctx: &ExecutionContext,
109 ) -> Result<ActionResult, ExecutorError> {
110 let start = Instant::now();
111
112 let pattern = action
113 .args
114 .get("pattern")
115 .ok_or_else(|| ExecutorError::Other("Glob requires a pattern".to_string()))?;
116
117 let path = action.target.as_deref().unwrap_or(".");
118
119 let output = if is_command_available(&ctx.fd_path) {
121 Command::new(&ctx.fd_path)
122 .arg("--type=f")
123 .arg("--glob")
124 .arg(pattern)
125 .arg(path)
126 .current_dir(&ctx.working_dir)
127 .output()
128 } else {
129 Command::new("find")
130 .arg(path)
131 .arg("-type")
132 .arg("f")
133 .arg("-name")
134 .arg(pattern)
135 .current_dir(&ctx.working_dir)
136 .output()
137 }
138 .map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
139
140 let stdout = String::from_utf8_lossy(&output.stdout);
141 let stderr = String::from_utf8_lossy(&output.stderr);
142 let duration = start.elapsed();
143
144 if output.status.success() {
145 Ok(ActionResult::success(action.clone())
146 .with_output(truncate_lines(&stdout, ctx.max_output_lines))
147 .with_duration(duration.as_micros() as u64))
148 } else {
149 Ok(ActionResult::failure(action.clone(), stderr.to_string())
150 .with_duration(duration.as_micros() as u64))
151 }
152 }
153
154 fn execute_list(
156 &self,
157 action: &Action,
158 ctx: &ExecutionContext,
159 ) -> Result<ActionResult, ExecutorError> {
160 let start = Instant::now();
161
162 let path = action.target.as_deref().unwrap_or(".");
163
164 let output = Command::new("ls")
165 .arg("-la")
166 .arg(path)
167 .current_dir(&ctx.working_dir)
168 .output()
169 .map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
170
171 let stdout = String::from_utf8_lossy(&output.stdout);
172 let stderr = String::from_utf8_lossy(&output.stderr);
173 let duration = start.elapsed();
174
175 if output.status.success() {
176 Ok(ActionResult::success(action.clone())
177 .with_output(truncate_lines(&stdout, ctx.max_output_lines))
178 .with_duration(duration.as_micros() as u64))
179 } else {
180 Ok(ActionResult::failure(action.clone(), stderr.to_string())
181 .with_duration(duration.as_micros() as u64))
182 }
183 }
184}
185
186impl Default for CoreExecutor {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192impl Executor for CoreExecutor {
193 fn execute(
194 &self,
195 action: &Action,
196 ctx: &ExecutionContext,
197 ) -> Result<ActionResult, ExecutorError> {
198 match action.kind {
199 ActionKind::Read => self.execute_read(action, ctx),
200 ActionKind::Grep => self.execute_grep(action, ctx),
201 ActionKind::Glob => self.execute_glob(action, ctx),
202 ActionKind::List => self.execute_list(action, ctx),
203 ActionKind::Rest | ActionKind::Done => {
205 Ok(ActionResult::success(action.clone()).with_output("OK"))
206 }
207 kind => Err(ExecutorError::UnsupportedAction(kind)),
208 }
209 }
210
211 fn supported_kinds(&self) -> &[ActionKind] {
212 &[
213 ActionKind::Read,
214 ActionKind::Grep,
215 ActionKind::Glob,
216 ActionKind::List,
217 ActionKind::Rest,
218 ActionKind::Done,
219 ]
220 }
221
222 fn name(&self) -> &'static str {
223 "CoreExecutor"
224 }
225}
226
227fn is_command_available(cmd: &str) -> bool {
229 Command::new("which")
230 .arg(cmd)
231 .output()
232 .map(|o| o.status.success())
233 .unwrap_or(false)
234}
235
236fn truncate_lines(s: &str, max_lines: usize) -> String {
238 let lines: Vec<&str> = s.lines().collect();
239 if lines.len() <= max_lines {
240 s.to_string()
241 } else {
242 let truncated: String = lines[..max_lines].join("\n");
243 format!(
244 "{}\n... ({} more lines)",
245 truncated,
246 lines.len() - max_lines
247 )
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_core_executor_supported_kinds() {
257 let executor = CoreExecutor::new();
258 assert!(executor.can_execute(&Action::read("test.rs")));
259 assert!(executor.can_execute(&Action::grep("pattern")));
260 assert!(executor.can_execute(&Action::glob("*.rs")));
261 assert!(!executor.can_execute(&Action::mutate("AddFn", "target")));
262 }
263
264 #[test]
265 fn test_truncate_lines() {
266 let s = "line1\nline2\nline3\nline4\nline5";
267
268 let result = truncate_lines(s, 10);
270 assert_eq!(result, s);
271
272 let result = truncate_lines(s, 3);
274 assert!(result.contains("line1"));
275 assert!(result.contains("line3"));
276 assert!(result.contains("2 more lines"));
277 }
278}