Skip to main content

agent_procs/cli/
logs.rs

1use crate::paths;
2use crate::protocol::{Request, Response};
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5
6#[allow(clippy::too_many_arguments)]
7pub async fn execute(
8    session: &str,
9    target: Option<&str>,
10    tail: usize,
11    follow: bool,
12    stderr: bool,
13    all: bool,
14    timeout: Option<u64>,
15    lines: Option<usize>,
16) -> i32 {
17    if follow {
18        return execute_follow(session, target, tail, stderr, all, timeout, lines).await;
19    }
20
21    // Non-follow: read from disk (unchanged)
22    let log_dir = paths::log_dir(session);
23
24    if all || target.is_none() {
25        return show_all_logs(&log_dir, tail, stderr);
26    }
27
28    let target = target.unwrap();
29    let stream = if stderr { "stderr" } else { "stdout" };
30    let path = log_dir.join(format!("{}.{}", target, stream));
31
32    match tail_file(&path, tail) {
33        Ok(lines) => {
34            for line in lines {
35                println!("{}", line);
36            }
37            0
38        }
39        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
40            eprintln!("error: no logs for process '{}' ({})", target, stream);
41            2
42        }
43        Err(e) => {
44            eprintln!("error reading logs: {}", e);
45            1
46        }
47    }
48}
49
50async fn execute_follow(
51    session: &str,
52    target: Option<&str>,
53    tail: usize,
54    stderr: bool,
55    all: bool,
56    timeout: Option<u64>,
57    lines: Option<usize>,
58) -> i32 {
59    if let Some(code) = replay_follow_tail(session, target, tail, stderr, all) {
60        return code;
61    }
62
63    let req = Request::Logs {
64        target: target.map(std::string::ToString::to_string),
65        tail: 0,
66        follow: true,
67        stderr,
68        all: all || target.is_none(),
69        timeout_secs: timeout.or(Some(30)), // CLI default; TUI passes None for infinite
70        lines,
71    };
72
73    let show_prefix = all || target.is_none();
74    match crate::cli::stream_responses(session, &req, false, |process, _stream, line| {
75        if show_prefix {
76            println!("[{}] {}", process, line);
77        } else {
78            println!("{}", line);
79        }
80    })
81    .await
82    {
83        Ok(Response::Error { code, message }) => {
84            eprintln!("error: {}", message);
85            code.exit_code()
86        }
87        Ok(_) => 0,
88        Err(e) => {
89            eprintln!("error: {}", e);
90            1
91        }
92    }
93}
94
95fn replay_follow_tail(
96    session: &str,
97    target: Option<&str>,
98    tail: usize,
99    stderr: bool,
100    all: bool,
101) -> Option<i32> {
102    if tail == 0 {
103        return None;
104    }
105
106    let log_dir = paths::log_dir(session);
107    if all || target.is_none() {
108        let code = show_all_logs(&log_dir, tail, stderr);
109        return if code == 0 { None } else { Some(code) };
110    }
111
112    let target = target.expect("target should exist when not following all logs");
113    let stream = if stderr { "stderr" } else { "stdout" };
114    let path = log_dir.join(format!("{}.{}", target, stream));
115
116    match tail_file(&path, tail) {
117        Ok(lines) => {
118            for line in lines {
119                println!("{}", line);
120            }
121            None
122        }
123        Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
124        Err(e) => {
125            eprintln!("error reading logs: {}", e);
126            Some(1)
127        }
128    }
129}
130
131fn show_all_logs(log_dir: &std::path::Path, tail: usize, stderr: bool) -> i32 {
132    let entries = match std::fs::read_dir(log_dir) {
133        Ok(e) => e,
134        Err(e) => {
135            eprintln!("error: cannot read log dir: {}", e);
136            return 1;
137        }
138    };
139
140    let suffix = if stderr { ".stderr" } else { ".stdout" };
141    let mut all_lines: Vec<(String, String)> = Vec::new();
142    for entry in entries.flatten() {
143        let name = entry.file_name().to_string_lossy().to_string();
144        if !name.ends_with(suffix) {
145            continue;
146        }
147        let proc_name = name.trim_end_matches(suffix).to_string();
148        if let Ok(lines) = tail_file(&entry.path(), tail) {
149            for line in lines {
150                all_lines.push((proc_name.clone(), line));
151            }
152        }
153    }
154
155    for (proc_name, line) in &all_lines {
156        println!("[{}] {}", proc_name, line);
157    }
158    0
159}
160
161pub(crate) fn tail_file(path: &std::path::Path, n: usize) -> std::io::Result<Vec<String>> {
162    if n == 0 {
163        return Ok(Vec::new());
164    }
165
166    let file = File::open(path)?;
167    // Use a ring buffer to keep only the last N lines in memory
168    let mut ring: std::collections::VecDeque<String> = std::collections::VecDeque::with_capacity(n);
169    for line in BufReader::new(file).lines() {
170        let line = line?;
171        if ring.len() == n {
172            ring.pop_front();
173        }
174        ring.push_back(line);
175    }
176    Ok(ring.into_iter().collect())
177}