toast-api 0.1.7

An unofficial CLI client and API server for Claude/Deepseek
Documentation
use std::fs;
use std::process::{Command, Stdio};
use std::time::Duration;
use std::thread;
use std::sync::mpsc;
use anyhow::Result;

/// Execute a shell command and capture its output with timeout
pub fn execute_command(command: &str) -> Result<String> {
    // Default timeout of 30 seconds for most commands
    let timeout_duration = Duration::from_secs(120);
    
    // Spawn the command
    let child = Command::new("sh")
        .arg("-c")
        .arg(command)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    
    // Create a channel to communicate between threads
    let (tx, rx) = mpsc::channel();
    
    // Spawn a thread to wait for the process
    thread::spawn(move || {
        let output = child.wait_with_output();
        let _ = tx.send(output);
    });
    
    // Wait for either completion or timeout
    let output = match rx.recv_timeout(timeout_duration) {
        Ok(Ok(output)) => output,
        Ok(Err(e)) => return Err(anyhow::anyhow!("Command failed: {}", e)),
        Err(_) => {
            return Err(anyhow::anyhow!("Command timed out after {} seconds. Consider using shorter commands or adding explicit timeouts.", timeout_duration.as_secs()));
        }
    };

    let mut msg = String::new();

    if !output.stdout.is_empty() {
        msg.push_str("=== STDOUT ===\n");
        msg.push_str(&String::from_utf8_lossy(&output.stdout));
        msg.push('\n');
    }

    if !output.stderr.is_empty() {
        msg.push_str("=== STDERR ===\n");
        msg.push_str(&String::from_utf8_lossy(&output.stderr));
        msg.push('\n');
    }

    msg.push_str(&format!(
        "Exit code: {}",
        output.status.code().unwrap_or(-1)
    ));

    Ok(msg)
}

/// Process file reads from a message
pub fn process_file_reads(files: &[String]) -> String {
    let mut result = String::new();
    
    for file_path in files {
        let trimmed_path = file_path.trim();
        match fs::read_to_string(trimmed_path) {
            Ok(content) => {
                result.push_str(&format!("=== {trimmed_path} ===\n{content}\n"));
            }
            Err(e) => {
                result.push_str(&format!("Error reading {file_path}: {e}\n"));
            }
        }
    }
    
    result
}

/// Check if a command is an exit command
pub fn is_exit_command(input: &str) -> bool {
    let lower = input.trim().to_lowercase();
    matches!(
        lower.as_str(),
        "exit" | "quit" | "bye" | "goodbye" | "/exit" | "/quit"
    )
}