Skip to main content

agent_procs/cli/
logs.rs

1use crate::paths;
2use crate::protocol::{Request, Response};
3use std::io::{BufRead, BufReader};
4use std::fs::File;
5
6#[allow(clippy::too_many_arguments)]
7pub async fn execute(
8    session: &str, target: Option<&str>, tail: usize,
9    follow: bool, stderr: bool, all: bool, timeout: Option<u64>, lines: Option<usize>,
10) -> i32 {
11    if follow {
12        return execute_follow(session, target, all, timeout, lines).await;
13    }
14
15    // Non-follow: read from disk (unchanged)
16    let log_dir = paths::log_dir(session);
17
18    if all || target.is_none() {
19        return show_all_logs(&log_dir, tail);
20    }
21
22    let target = target.unwrap();
23    let stream = if stderr { "stderr" } else { "stdout" };
24    let path = log_dir.join(format!("{}.{}", target, stream));
25
26    match tail_file(&path, tail) {
27        Ok(lines) => { for line in lines { println!("{}", line); } 0 }
28        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
29            eprintln!("error: no logs for process '{}' ({})", target, stream);
30            2
31        }
32        Err(e) => { eprintln!("error reading logs: {}", e); 1 }
33    }
34}
35
36async fn execute_follow(
37    session: &str, target: Option<&str>, all: bool, timeout: Option<u64>, lines: Option<usize>,
38) -> i32 {
39    let req = Request::Logs {
40        target: target.map(|t| t.to_string()),
41        tail: 0,
42        follow: true,
43        stderr: false,
44        all: all || target.is_none(),
45        timeout_secs: timeout.or(Some(30)), // CLI default; TUI passes None for infinite
46        lines,
47    };
48
49    let show_prefix = all || target.is_none();
50    match crate::cli::stream_responses(session, &req, false, |process, _stream, line| {
51        if show_prefix {
52            println!("[{}] {}", process, line);
53        } else {
54            println!("{}", line);
55        }
56    }).await {
57        Ok(Response::LogEnd) => 0,
58        Ok(Response::Error { code, message }) => { eprintln!("error: {}", message); code }
59        Ok(_) => 0,
60        Err(e) => { eprintln!("error: {}", e); 1 }
61    }
62}
63
64fn show_all_logs(log_dir: &std::path::Path, tail: usize) -> i32 {
65    let entries = match std::fs::read_dir(log_dir) {
66        Ok(e) => e,
67        Err(e) => { eprintln!("error: cannot read log dir: {}", e); return 1; }
68    };
69
70    let mut all_lines: Vec<(String, String)> = Vec::new();
71    for entry in entries.flatten() {
72        let name = entry.file_name().to_string_lossy().to_string();
73        if !name.ends_with(".stdout") { continue; }
74        let proc_name = name.trim_end_matches(".stdout").to_string();
75        if let Ok(lines) = tail_file(&entry.path(), tail) {
76            for line in lines {
77                all_lines.push((proc_name.to_string(), line));
78            }
79        }
80    }
81
82    for (proc_name, line) in &all_lines {
83        println!("[{}] {}", proc_name, line);
84    }
85    0
86}
87
88fn tail_file(path: &std::path::Path, n: usize) -> std::io::Result<Vec<String>> {
89    let file = File::open(path)?;
90    // Use a ring buffer to keep only the last N lines in memory
91    let mut ring: std::collections::VecDeque<String> = std::collections::VecDeque::with_capacity(n);
92    for line in BufReader::new(file).lines() {
93        let line = line?;
94        if ring.len() == n {
95            ring.pop_front();
96        }
97        ring.push_back(line);
98    }
99    Ok(ring.into_iter().collect())
100}