use super::context::ExecutionContext;
use super::traits::{Executor, ExecutorError};
use crate::decider::{Action, ActionKind, ActionResult};
use std::process::Command;
use std::time::Instant;
pub struct CoreExecutor;
impl CoreExecutor {
pub fn new() -> Self {
Self
}
fn execute_read(
&self,
action: &Action,
ctx: &ExecutionContext,
) -> Result<ActionResult, ExecutorError> {
let start = Instant::now();
let path = action
.target
.as_ref()
.ok_or_else(|| ExecutorError::Other("Read requires a target path".to_string()))?;
let full_path = ctx.resolve_path(path);
let output = Command::new("cat")
.arg("-n")
.arg(&full_path)
.current_dir(&ctx.working_dir)
.output()
.map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let duration = start.elapsed();
if output.status.success() {
Ok(ActionResult::success(action.clone())
.with_output(truncate_lines(&stdout, ctx.max_output_lines))
.with_duration(duration.as_micros() as u64))
} else {
Ok(ActionResult::failure(action.clone(), stderr.to_string())
.with_duration(duration.as_micros() as u64))
}
}
fn execute_grep(
&self,
action: &Action,
ctx: &ExecutionContext,
) -> Result<ActionResult, ExecutorError> {
let start = Instant::now();
let pattern = action
.args
.get("pattern")
.ok_or_else(|| ExecutorError::Other("Grep requires a pattern".to_string()))?;
let path = action.target.as_deref().unwrap_or(".");
let mut cmd = Command::new(&ctx.rg_path);
cmd.arg("--color=never")
.arg("--line-number")
.arg("--no-heading")
.arg(pattern)
.arg(path)
.current_dir(&ctx.working_dir);
let output = cmd
.output()
.map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let duration = start.elapsed();
let success = matches!(output.status.code(), Some(0) | Some(1));
if success {
Ok(ActionResult::success(action.clone())
.with_output(truncate_lines(&stdout, ctx.max_output_lines))
.with_duration(duration.as_micros() as u64))
} else {
Ok(ActionResult::failure(action.clone(), stderr.to_string())
.with_duration(duration.as_micros() as u64))
}
}
fn execute_glob(
&self,
action: &Action,
ctx: &ExecutionContext,
) -> Result<ActionResult, ExecutorError> {
let start = Instant::now();
let pattern = action
.args
.get("pattern")
.ok_or_else(|| ExecutorError::Other("Glob requires a pattern".to_string()))?;
let path = action.target.as_deref().unwrap_or(".");
let output = if is_command_available(&ctx.fd_path) {
Command::new(&ctx.fd_path)
.arg("--type=f")
.arg("--glob")
.arg(pattern)
.arg(path)
.current_dir(&ctx.working_dir)
.output()
} else {
Command::new("find")
.arg(path)
.arg("-type")
.arg("f")
.arg("-name")
.arg(pattern)
.current_dir(&ctx.working_dir)
.output()
}
.map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let duration = start.elapsed();
if output.status.success() {
Ok(ActionResult::success(action.clone())
.with_output(truncate_lines(&stdout, ctx.max_output_lines))
.with_duration(duration.as_micros() as u64))
} else {
Ok(ActionResult::failure(action.clone(), stderr.to_string())
.with_duration(duration.as_micros() as u64))
}
}
fn execute_list(
&self,
action: &Action,
ctx: &ExecutionContext,
) -> Result<ActionResult, ExecutorError> {
let start = Instant::now();
let path = action.target.as_deref().unwrap_or(".");
let output = Command::new("ls")
.arg("-la")
.arg(path)
.current_dir(&ctx.working_dir)
.output()
.map_err(|e| ExecutorError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let duration = start.elapsed();
if output.status.success() {
Ok(ActionResult::success(action.clone())
.with_output(truncate_lines(&stdout, ctx.max_output_lines))
.with_duration(duration.as_micros() as u64))
} else {
Ok(ActionResult::failure(action.clone(), stderr.to_string())
.with_duration(duration.as_micros() as u64))
}
}
}
impl Default for CoreExecutor {
fn default() -> Self {
Self::new()
}
}
impl Executor for CoreExecutor {
fn execute(
&self,
action: &Action,
ctx: &ExecutionContext,
) -> Result<ActionResult, ExecutorError> {
match action.kind {
ActionKind::Read => self.execute_read(action, ctx),
ActionKind::Grep => self.execute_grep(action, ctx),
ActionKind::Glob => self.execute_glob(action, ctx),
ActionKind::List => self.execute_list(action, ctx),
ActionKind::Rest | ActionKind::Done => {
Ok(ActionResult::success(action.clone()).with_output("OK"))
}
kind => Err(ExecutorError::UnsupportedAction(kind)),
}
}
fn supported_kinds(&self) -> &[ActionKind] {
&[
ActionKind::Read,
ActionKind::Grep,
ActionKind::Glob,
ActionKind::List,
ActionKind::Rest,
ActionKind::Done,
]
}
fn name(&self) -> &'static str {
"CoreExecutor"
}
}
fn is_command_available(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn truncate_lines(s: &str, max_lines: usize) -> String {
let lines: Vec<&str> = s.lines().collect();
if lines.len() <= max_lines {
s.to_string()
} else {
let truncated: String = lines[..max_lines].join("\n");
format!(
"{}\n... ({} more lines)",
truncated,
lines.len() - max_lines
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_core_executor_supported_kinds() {
let executor = CoreExecutor::new();
assert!(executor.can_execute(&Action::read("test.rs")));
assert!(executor.can_execute(&Action::grep("pattern")));
assert!(executor.can_execute(&Action::glob("*.rs")));
assert!(!executor.can_execute(&Action::mutate("AddFn", "target")));
}
#[test]
fn test_truncate_lines() {
let s = "line1\nline2\nline3\nline4\nline5";
let result = truncate_lines(s, 10);
assert_eq!(result, s);
let result = truncate_lines(s, 3);
assert!(result.contains("line1"));
assert!(result.contains("line3"));
assert!(result.contains("2 more lines"));
}
}