use anyhow::{Context, Result};
#[cfg(any(not(feature = "powershell-process"), feature = "pure-rust"))]
use anyhow::{anyhow, bail};
#[cfg(feature = "pure-rust")]
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "serde-errors")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "pure-rust")]
use std::fs;
#[cfg(feature = "dry-run")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "exec-events")]
use std::sync::{
Mutex as StdMutex,
atomic::{AtomicU64, Ordering},
};
#[cfg(feature = "exec-events")]
use vtcode_exec_events::{
CommandExecutionItem, CommandExecutionStatus, EventEmitter, ItemCompletedEvent,
ItemStartedEvent, ThreadEvent, ThreadItem, ThreadItemDetails,
};
#[cfg_attr(feature = "serde-errors", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandCategory {
ChangeDirectory,
ListDirectory,
PrintDirectory,
CreateDirectory,
Remove,
Copy,
Move,
Search,
}
#[cfg_attr(feature = "serde-errors", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ShellKind {
Unix,
Windows,
}
#[cfg_attr(feature = "serde-errors", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CommandInvocation {
pub shell: ShellKind,
pub command: String,
pub category: CommandCategory,
pub working_dir: PathBuf,
pub touched_paths: Vec<PathBuf>,
}
impl CommandInvocation {
pub fn new(
shell: ShellKind,
command: String,
category: CommandCategory,
working_dir: PathBuf,
) -> Self {
Self {
shell,
command,
category,
working_dir,
touched_paths: Vec::new(),
}
}
pub fn with_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.touched_paths = paths;
self
}
}
#[cfg_attr(feature = "serde-errors", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandStatus {
success: bool,
code: Option<i32>,
}
impl CommandStatus {
pub fn new(success: bool, code: Option<i32>) -> Self {
Self { success, code }
}
pub fn success(&self) -> bool {
self.success
}
pub fn code(&self) -> Option<i32> {
self.code
}
pub fn failure(code: Option<i32>) -> Self {
Self {
success: false,
code,
}
}
}
impl From<std::process::ExitStatus> for CommandStatus {
fn from(status: std::process::ExitStatus) -> Self {
let code = status.code();
Self {
success: status.success(),
code,
}
}
}
#[cfg_attr(feature = "serde-errors", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub status: CommandStatus,
pub stdout: String,
pub stderr: String,
}
impl CommandOutput {
pub fn success(stdout: impl Into<String>) -> Self {
Self {
status: CommandStatus::new(true, Some(0)),
stdout: stdout.into(),
stderr: String::new(),
}
}
pub fn failure(
code: Option<i32>,
stdout: impl Into<String>,
stderr: impl Into<String>,
) -> Self {
Self {
status: CommandStatus::failure(code),
stdout: stdout.into(),
stderr: stderr.into(),
}
}
}
pub trait CommandExecutor: Send + Sync {
fn execute(&self, invocation: &CommandInvocation) -> Result<CommandOutput>;
}
#[cfg(feature = "std-process")]
pub struct ProcessCommandExecutor;
#[cfg(feature = "std-process")]
impl ProcessCommandExecutor {
pub fn new() -> Self {
Self
}
}
#[cfg(feature = "std-process")]
impl Default for ProcessCommandExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "std-process")]
impl CommandExecutor for ProcessCommandExecutor {
fn execute(&self, invocation: &CommandInvocation) -> Result<CommandOutput> {
use std::process::Command;
let mut cmd = match invocation.shell {
ShellKind::Unix => {
let mut command = Command::new("sh");
command.arg("-c").arg(&invocation.command);
command
}
ShellKind::Windows => {
#[cfg(not(feature = "powershell-process"))]
{
bail!(
"powershell-process feature disabled; enable it to execute Windows commands"
);
}
#[cfg(feature = "powershell-process")]
let mut command = Command::new("powershell");
command
.arg("-NoProfile")
.arg("-NonInteractive")
.arg("-Command")
.arg(&invocation.command);
#[cfg(feature = "powershell-process")]
{
command
}
}
};
cmd.current_dir(&invocation.working_dir);
let output = cmd
.output()
.with_context(|| format!("failed to execute command: {}", invocation.command))?;
Ok(CommandOutput {
status: CommandStatus::from(output.status),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
})
}
}
#[cfg(feature = "dry-run")]
#[derive(Clone, Default)]
pub struct DryRunCommandExecutor {
log: Arc<Mutex<Vec<CommandInvocation>>>,
}
#[cfg(feature = "dry-run")]
impl DryRunCommandExecutor {
pub fn new() -> Self {
Self::default()
}
pub fn logged_invocations(&self) -> Vec<CommandInvocation> {
match self.log.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
}
#[cfg(feature = "dry-run")]
impl CommandExecutor for DryRunCommandExecutor {
fn execute(&self, invocation: &CommandInvocation) -> Result<CommandOutput> {
let mut guard = match self.log.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.push(invocation.clone());
Ok(match invocation.category {
CommandCategory::ListDirectory => CommandOutput::success("(dry-run listing)"),
_ => CommandOutput::success(String::new()),
})
}
}
#[cfg(feature = "pure-rust")]
#[derive(Debug, Default, Clone, Copy)]
pub struct PureRustCommandExecutor;
#[cfg(feature = "pure-rust")]
impl PureRustCommandExecutor {
fn resolve_primary_path(invocation: &CommandInvocation) -> Result<&PathBuf> {
invocation
.touched_paths
.first()
.ok_or_else(|| anyhow!("invocation missing target path"))
}
fn should_include_hidden(command: &str) -> bool {
command.contains("-a") || command.contains("-Force")
}
fn mkdir(path: &Path, command: &str) -> Result<()> {
if command.contains("-p") || command.contains("-Force") {
fs::create_dir_all(path)
.with_context(|| format!("failed to create directory `{}`", path.display()))?
} else {
fs::create_dir(path)
.with_context(|| format!("failed to create directory `{}`", path.display()))?
}
Ok(())
}
fn rm(path: &Path, command: &str) -> Result<()> {
if path.is_dir() {
if command.contains("-r") || command.contains("-Recurse") {
fs::remove_dir_all(path)
.with_context(|| format!("failed to remove directory `{}`", path.display()))?
} else {
fs::remove_dir(path)
.with_context(|| format!("failed to remove directory `{}`", path.display()))?
}
} else if path.exists() {
fs::remove_file(path)
.with_context(|| format!("failed to remove file `{}`", path.display()))?
}
Ok(())
}
fn copy_recursive(source: &Path, dest: &Path, recursive: bool) -> Result<()> {
if source.is_dir() {
if !recursive {
bail!(
"copying directory `{}` requires recursive flag",
source.display()
);
}
fs::create_dir_all(dest)
.with_context(|| format!("failed to create directory `{}`", dest.display()))?;
for entry in fs::read_dir(source)
.with_context(|| format!("failed to read directory `{}`", source.display()))?
{
let entry = entry?;
let entry_path = entry.path();
let dest_path = dest.join(entry.file_name());
if entry_path.is_dir() {
Self::copy_recursive(&entry_path, &dest_path, true)?;
} else {
Self::copy_file(&entry_path, &dest_path)?;
}
}
} else {
Self::copy_file(source, dest)?;
}
Ok(())
}
fn copy_file(source: &Path, dest: &Path) -> Result<()> {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).with_context(|| {
format!(
"failed to prepare destination directory `{}`",
parent.display()
)
})?;
}
fs::copy(source, dest).with_context(|| {
format!(
"failed to copy `{}` to `{}`",
source.display(),
dest.display()
)
})?;
Ok(())
}
fn move_path(source: &Path, dest: &Path) -> Result<()> {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).with_context(|| {
format!(
"failed to prepare destination directory `{}`",
parent.display()
)
})?;
}
if let Err(rename_err) = fs::rename(source, dest) {
Self::copy_recursive(source, dest, true)
.and_then(|_| Self::rm(source, "-r -f"))
.with_context(|| {
format!(
"failed to move `{}` to `{}` via rename: {rename_err}",
source.display(),
dest.display()
)
})?;
}
Ok(())
}
}
#[cfg(feature = "pure-rust")]
impl CommandExecutor for PureRustCommandExecutor {
fn execute(&self, invocation: &CommandInvocation) -> Result<CommandOutput> {
match invocation.category {
CommandCategory::ListDirectory => {
let path = Self::resolve_primary_path(invocation)?;
let mut entries = Vec::new();
for entry in fs::read_dir(path)
.with_context(|| format!("failed to read directory `{}`", path.display()))?
{
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
if !Self::should_include_hidden(&invocation.command) && name.starts_with('.') {
continue;
}
entries.push(name.to_string());
}
entries.sort();
Ok(CommandOutput::success(entries.join("\n")))
}
CommandCategory::CreateDirectory => {
let path = Self::resolve_primary_path(invocation)?;
Self::mkdir(path, &invocation.command)?;
Ok(CommandOutput::success(String::new()))
}
CommandCategory::Remove => {
let path = Self::resolve_primary_path(invocation)?;
Self::rm(path, &invocation.command)?;
Ok(CommandOutput::success(String::new()))
}
CommandCategory::Copy => {
let source = invocation
.touched_paths
.first()
.ok_or_else(|| anyhow!("copy missing source path"))?;
let dest = invocation
.touched_paths
.get(1)
.ok_or_else(|| anyhow!("copy missing destination path"))?;
let recursive =
invocation.command.contains("-r") || invocation.command.contains("-Recurse");
Self::copy_recursive(source.as_path(), dest.as_path(), recursive)?;
Ok(CommandOutput::success(String::new()))
}
CommandCategory::Move => {
let source = invocation
.touched_paths
.first()
.ok_or_else(|| anyhow!("move missing source path"))?;
let dest = invocation
.touched_paths
.get(1)
.ok_or_else(|| anyhow!("move missing destination path"))?;
Self::move_path(source.as_path(), dest.as_path())?;
Ok(CommandOutput::success(String::new()))
}
CommandCategory::Search => bail!(
"pure-rust executor does not implement search; enable std-process or provide a custom executor"
),
CommandCategory::ChangeDirectory | CommandCategory::PrintDirectory => {
Ok(CommandOutput::success(String::new()))
}
}
}
}
#[cfg(feature = "exec-events")]
#[derive(Debug)]
pub struct EventfulExecutor<E, T> {
inner: E,
emitter: StdMutex<T>,
counter: AtomicU64,
id_prefix: String,
}
#[cfg(feature = "exec-events")]
impl<E, T> EventfulExecutor<E, T>
where
T: EventEmitter,
{
pub fn new(inner: E, emitter: T) -> Self {
Self {
inner,
emitter: StdMutex::new(emitter),
counter: AtomicU64::new(0),
id_prefix: "cmd-".to_string(),
}
}
pub fn with_id_prefix(inner: E, emitter: T, prefix: impl Into<String>) -> Self {
let mut executor = Self::new(inner, emitter);
executor.id_prefix = prefix.into();
executor
}
fn next_id(&self) -> String {
let value = self.counter.fetch_add(1, Ordering::Relaxed) + 1;
format!("{}{}", self.id_prefix, value)
}
fn emit_event(&self, event: ThreadEvent) {
if let Ok(mut emitter) = self.emitter.lock() {
EventEmitter::emit(&mut *emitter, &event);
}
}
fn command_details(
&self,
invocation: &CommandInvocation,
status: CommandExecutionStatus,
output: Option<&CommandOutput>,
error: Option<&anyhow::Error>,
) -> CommandExecutionItem {
let aggregated_output = if let Some(output) = output {
aggregate_output(output)
} else if let Some(err) = error {
err.to_string()
} else {
String::new()
};
CommandExecutionItem {
command: invocation.command.clone(),
arguments: None,
aggregated_output,
exit_code: output.and_then(|out| out.status.code()),
status,
}
}
}
#[cfg(feature = "exec-events")]
impl<E, T> CommandExecutor for EventfulExecutor<E, T>
where
E: CommandExecutor,
T: EventEmitter + Send,
{
fn execute(&self, invocation: &CommandInvocation) -> Result<CommandOutput> {
let item_id = self.next_id();
let starting_item = ThreadItem {
id: item_id.clone(),
details: ThreadItemDetails::CommandExecution(Box::new(self.command_details(
invocation,
CommandExecutionStatus::InProgress,
None,
None,
))),
};
self.emit_event(ThreadEvent::ItemStarted(ItemStartedEvent {
item: starting_item,
}));
match self.inner.execute(invocation) {
Ok(output) => {
let status = if output.status.success() {
CommandExecutionStatus::Completed
} else {
CommandExecutionStatus::Failed
};
let completed_item =
ThreadItem {
id: item_id,
details: ThreadItemDetails::CommandExecution(Box::new(
self.command_details(invocation, status, Some(&output), None),
)),
};
self.emit_event(ThreadEvent::ItemCompleted(ItemCompletedEvent {
item: completed_item,
}));
Ok(output)
}
Err(err) => {
let failure = ThreadItem {
id: item_id,
details: ThreadItemDetails::CommandExecution(Box::new(self.command_details(
invocation,
CommandExecutionStatus::Failed,
None,
Some(&err),
))),
};
self.emit_event(ThreadEvent::ItemCompleted(ItemCompletedEvent {
item: failure,
}));
Err(err)
}
}
}
}
#[cfg(feature = "exec-events")]
fn aggregate_output(output: &CommandOutput) -> String {
let mut combined = String::new();
if !output.stdout.trim().is_empty() {
combined.push_str(output.stdout.trim());
}
if !output.stderr.trim().is_empty() {
if !combined.is_empty() {
combined.push('\n');
}
combined.push_str(output.stderr.trim());
}
combined
}