agent-procs 0.6.3

Concurrent process runner for AI agents
Documentation
use crate::paths;
use crate::protocol::{Request, Response};
use std::fs::File;
use std::io::{BufRead, BufReader};

#[allow(clippy::too_many_arguments)]
pub async fn execute(
    session: &str,
    target: Option<&str>,
    tail: usize,
    follow: bool,
    stderr: bool,
    all: bool,
    timeout: Option<u64>,
    lines: Option<usize>,
) -> i32 {
    if follow {
        return execute_follow(session, target, tail, stderr, all, timeout, lines).await;
    }

    // Non-follow: read from disk (unchanged)
    let log_dir = paths::log_dir(session);

    if all || target.is_none() {
        return show_all_logs(&log_dir, tail, stderr);
    }

    let target = target.unwrap();
    let stream = if stderr { "stderr" } else { "stdout" };
    let path = log_dir.join(format!("{}.{}", target, stream));

    match tail_file(&path, tail) {
        Ok(lines) => {
            for line in lines {
                println!("{}", line);
            }
            0
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            eprintln!("error: no logs for process '{}' ({})", target, stream);
            2
        }
        Err(e) => {
            eprintln!("error reading logs: {}", e);
            1
        }
    }
}

async fn execute_follow(
    session: &str,
    target: Option<&str>,
    tail: usize,
    stderr: bool,
    all: bool,
    timeout: Option<u64>,
    lines: Option<usize>,
) -> i32 {
    if let Some(code) = replay_follow_tail(session, target, tail, stderr, all) {
        return code;
    }

    let req = Request::Logs {
        target: target.map(std::string::ToString::to_string),
        tail: 0,
        follow: true,
        stderr,
        all: all || target.is_none(),
        timeout_secs: timeout.or(Some(30)), // CLI default; TUI passes None for infinite
        lines,
    };

    let show_prefix = all || target.is_none();
    match crate::cli::stream_responses(session, &req, false, |process, _stream, line| {
        if show_prefix {
            println!("[{}] {}", process, line);
        } else {
            println!("{}", line);
        }
    })
    .await
    {
        Ok(Response::Error { code, message }) => {
            eprintln!("error: {}", message);
            code.exit_code()
        }
        Ok(_) => 0,
        Err(e) => {
            eprintln!("error: {}", e);
            1
        }
    }
}

fn replay_follow_tail(
    session: &str,
    target: Option<&str>,
    tail: usize,
    stderr: bool,
    all: bool,
) -> Option<i32> {
    if tail == 0 {
        return None;
    }

    let log_dir = paths::log_dir(session);
    if all || target.is_none() {
        let code = show_all_logs(&log_dir, tail, stderr);
        return if code == 0 { None } else { Some(code) };
    }

    let target = target.expect("target should exist when not following all logs");
    let stream = if stderr { "stderr" } else { "stdout" };
    let path = log_dir.join(format!("{}.{}", target, stream));

    match tail_file(&path, tail) {
        Ok(lines) => {
            for line in lines {
                println!("{}", line);
            }
            None
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
        Err(e) => {
            eprintln!("error reading logs: {}", e);
            Some(1)
        }
    }
}

fn show_all_logs(log_dir: &std::path::Path, tail: usize, stderr: bool) -> i32 {
    let entries = match std::fs::read_dir(log_dir) {
        Ok(e) => e,
        Err(e) => {
            eprintln!("error: cannot read log dir: {}", e);
            return 1;
        }
    };

    let suffix = if stderr { ".stderr" } else { ".stdout" };
    let mut all_lines: Vec<(String, String)> = Vec::new();
    for entry in entries.flatten() {
        let name = entry.file_name().to_string_lossy().to_string();
        if !name.ends_with(suffix) {
            continue;
        }
        let proc_name = name.trim_end_matches(suffix).to_string();
        if let Ok(lines) = tail_file(&entry.path(), tail) {
            for line in lines {
                all_lines.push((proc_name.clone(), line));
            }
        }
    }

    for (proc_name, line) in &all_lines {
        println!("[{}] {}", proc_name, line);
    }
    0
}

pub(crate) fn tail_file(path: &std::path::Path, n: usize) -> std::io::Result<Vec<String>> {
    if n == 0 {
        return Ok(Vec::new());
    }

    let file = File::open(path)?;
    // Use a ring buffer to keep only the last N lines in memory
    let mut ring: std::collections::VecDeque<String> = std::collections::VecDeque::with_capacity(n);
    for line in BufReader::new(file).lines() {
        let line = line?;
        if ring.len() == n {
            ring.pop_front();
        }
        ring.push_back(line);
    }
    Ok(ring.into_iter().collect())
}