Skip to main content

ryo_executor/executor/
core.rs

1//! CoreExecutor: 基本ツール(Read, Grep, Glob, List)の実行
2
3use super::context::ExecutionContext;
4use super::traits::{Executor, ExecutorError};
5use crate::decider::{Action, ActionKind, ActionResult};
6use std::process::Command;
7use std::time::Instant;
8
9/// 基本ツールの Executor
10/// - Read: ファイル読み取り
11/// - Grep: パターン検索
12/// - Glob: ファイルパターン検索
13/// - List: ディレクトリ一覧
14pub struct CoreExecutor;
15
16impl CoreExecutor {
17    /// 新しい CoreExecutor を作成
18    pub fn new() -> Self {
19        Self
20    }
21
22    /// Read アクションを実行
23    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    /// Grep アクションを実行
60    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        // rg: exit 0=match, 1=no match, 2=error
91        // 0と1は成功として扱う
92        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    /// Glob アクションを実行
105    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        // fd があれば fd、なければ find
120        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    /// List アクションを実行
155    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            // Rest/Done は何もしない
204            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
227/// コマンドが利用可能かチェック
228fn 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
236/// 行数で切り詰め
237fn 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        // 切り詰めなし
269        let result = truncate_lines(s, 10);
270        assert_eq!(result, s);
271
272        // 切り詰めあり
273        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}