Skip to main content

volition_core/tools/
shell.rs

1// volition-agent-core/src/tools/shell.rs
2
3//! Core implementation for executing shell commands.
4
5use super::CommandOutput;
6use anyhow::{Context, Result};
7use std::path::Path;
8use std::process::{Command, Stdio};
9use tracing::{debug, warn};
10
11/// Executes an arbitrary shell command in a specified working directory.
12///
13/// This function uses the platform's default shell (`sh -c` on Unix, `cmd /C` on Windows).
14/// It captures stdout, stderr, and the exit status.
15///
16/// **Warning:** This function executes arbitrary commands as provided.
17/// It does **not** perform any sandboxing, validation, or user confirmation.
18/// Callers **must** ensure the command is safe to execute or implement appropriate
19/// safety measures (like user confirmation) before calling this function.
20/// Consider using more specific tool functions (e.g., `execute_git_command`)
21/// where possible.
22///
23/// # Arguments
24///
25/// * `command`: The command string to execute via the shell.
26/// * `working_dir`: The directory in which to execute the command.
27///
28/// # Returns
29///
30/// A `Result` containing a [`CommandOutput`] struct with the status, stdout, and stderr,
31/// or an error if the process failed to spawn.
32pub async fn execute_shell_command(command: &str, working_dir: &Path) -> Result<CommandOutput> {
33    debug!("Executing shell command: {} in {:?}", command, working_dir);
34
35    let shell_executable = if cfg!(target_os = "windows") {
36        "cmd"
37    } else {
38        "sh"
39    };
40    let shell_arg = if cfg!(target_os = "windows") {
41        "/C"
42    } else {
43        "-c"
44    };
45
46    let output_result = Command::new(shell_executable)
47        .current_dir(working_dir)
48        .arg(shell_arg)
49        .arg(command)
50        .stdout(Stdio::piped())
51        .stderr(Stdio::piped())
52        .output()
53        .with_context(|| format!("Failed to spawn shell process for command: {}", command));
54
55    let output = match output_result {
56        Ok(out) => out,
57        Err(e) => {
58            warn!(command = command, error = %e, "Failed to spawn command process");
59            return Err(e);
60        }
61    };
62
63    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
64    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
65    let status = output.status.code().unwrap_or(-1);
66
67    debug!(
68        "Shell command exit status: {}\nStdout preview (first 3 lines):\n{}\nStderr preview (first 3 lines):\n{}",
69        status,
70        stdout.lines().take(3).collect::<Vec<_>>().join("\n"),
71        stderr.lines().take(3).collect::<Vec<_>>().join("\n")
72    );
73
74    Ok(CommandOutput {
75        status,
76        stdout,
77        stderr,
78    })
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::path::PathBuf;
85    use tempfile::tempdir;
86    use tokio;
87
88    fn test_working_dir() -> PathBuf {
89        tempdir().map(|d| d.into_path()).unwrap_or_default()
90    }
91
92    #[tokio::test]
93    async fn test_execute_shell_echo() {
94        let command = "echo Hello Core Shell";
95        let working_dir = test_working_dir();
96        let result = execute_shell_command(command, &working_dir).await;
97        assert!(result.is_ok(), "Command failed: {:?}", result.err());
98        let output = result.unwrap();
99        println!("Output: {:?}", output);
100        assert_eq!(output.status, 0);
101        assert_eq!(output.stdout.trim(), "Hello Core Shell");
102        assert!(output.stderr.is_empty() || output.stderr == "<no output>");
103    }
104
105    #[tokio::test]
106    async fn test_execute_shell_nonexistent_command() {
107        let command = "this_command_does_not_exist_qwertyuiop";
108        let working_dir = test_working_dir();
109        let result = execute_shell_command(command, &working_dir).await;
110        assert!(result.is_ok());
111        let output = result.unwrap();
112        println!("Output: {:?}", output);
113        assert_ne!(output.status, 0);
114        assert!(output.stdout.is_empty() || output.stdout == "<no output>");
115        assert!(output.stderr.contains("not found") || output.stderr.contains("is not recognized"));
116    }
117}