vespe 0.1.2

Text as a Canvas for LLM Collaboration and Automation
Documentation
use anyhow::Context;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::thread;
use tracing::{debug, error};

pub fn shell_call<F>(
    command_template: &str,
    input: &str,
    mut on_output: F,
) -> anyhow::Result<String>
where
    F: FnMut(&str) + Send + 'static,
{
    let mut command_parts = command_template.split_whitespace();
    let program = command_parts
        .next()
        .context("Command template cannot be empty")?;
    let args: Vec<&str> = command_parts.collect();

    let mut command = {
        #[cfg(windows)]
        {
            let mut cmd = Command::new("cmd");
            cmd.arg("/C").arg(program).args(args.clone());
            cmd
        }
        #[cfg(not(windows))]
        {
            let mut cmd = Command::new("sh");
            cmd.arg("-c").arg(command_template);
            cmd
        }
    };

    debug!("Executing command: {:?} with args: {:?}", program, args);
    command
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let mut child = command.spawn().with_context(|| {
        format!(
            "Failed to spawn command: '{}'. Is it in your PATH?",
            command_template
        )
    })?;

    debug!("Writing query to stdin: '{}'.", input);
    child.stdin.as_mut().unwrap().write_all(input.as_bytes())?;

    let stdout = child.stdout.take().context("Failed to take stdout")?;
    let stderr = child.stderr.take().context("Failed to take stderr")?;

    #[allow(unused_assignments)]
    let mut full_stdout = Vec::new();
    #[allow(unused_assignments)]
    let mut full_stderr = Vec::new();

    let stdout_reader = BufReader::new(stdout);
    let stderr_reader = BufReader::new(stderr);

    let stdout_handle = thread::spawn(move || {
        let mut line = String::new();
        let mut reader = stdout_reader;
        let mut buffer = Vec::new();
        loop {
            line.clear();
            match reader.read_line(&mut line) {
                Ok(0) => break,
                Ok(_) => {
                    debug!("Child stdout: {}", line.trim_end());
                    on_output(&line);
                    buffer.extend_from_slice(line.as_bytes());
                }
                Err(e) => {
                    error!("Error reading child stdout: {:?}", e);
                    break;
                }
            }
        }
        buffer
    });

    let stderr_handle = thread::spawn(move || {
        let mut line = String::new();
        let mut reader = stderr_reader;
        let mut buffer = Vec::new();
        loop {
            line.clear();
            match reader.read_line(&mut line) {
                Ok(0) => break,
                Ok(_) => {
                    debug!("Child stderr: {}", line.trim_end());
                    buffer.extend_from_slice(line.as_bytes());
                }
                Err(e) => {
                    error!("Error reading child stderr: {:?}", e);
                    break;
                }
            }
        }
        buffer
    });

    let status = child.wait()?;

    full_stdout = stdout_handle.join().expect("Failed to join stdout thread");
    full_stderr = stderr_handle.join().expect("Failed to join stderr thread");

    debug!("Command finished with status: {:?}", status);
    if !status.success() {
        error!(
            "Command '{}' failed: {:?}",
            command_template,
            String::from_utf8_lossy(&full_stderr)
        );
        anyhow::bail!(
            "Command '{}' failed: {:?}",
            command_template,
            String::from_utf8_lossy(&full_stderr)
        );
    }

    debug!(
        "Command executed successfully. Output length: {}",
        full_stdout.len()
    );
    Ok(String::from_utf8_lossy(&full_stdout).to_string())
}