Skip to main content

oven_cli/cli/
look.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use tokio::io::{AsyncBufReadExt, BufReader};
5use tokio_util::sync::CancellationToken;
6
7use super::{GlobalOpts, LookArgs};
8use crate::db::{self, RunStatus};
9
10pub async fn run(args: LookArgs, _global: &GlobalOpts) -> Result<()> {
11    let project_dir = std::env::current_dir().context("getting current directory")?;
12    let db_path = project_dir.join(".oven").join("oven.db");
13    let conn = db::open(&db_path)?;
14
15    let run = if let Some(ref run_id) = args.run_id {
16        db::runs::get_run(&conn, run_id)?.with_context(|| format!("run {run_id} not found"))?
17    } else {
18        db::runs::get_latest_run(&conn)?.context("no runs found")?
19    };
20
21    let log_dir = project_dir.join(".oven").join("logs").join(&run.id);
22    let log_file = log_dir.join("pipeline.log");
23
24    if !log_file.exists() {
25        anyhow::bail!("no log file found for run {}", run.id);
26    }
27
28    let agent_tag = args.agent.as_deref().map(|a| format!("agent={a}"));
29    let is_active = !matches!(run.status, RunStatus::Complete | RunStatus::Failed);
30
31    if is_active {
32        tail_log(&log_file, args.agent.as_deref(), agent_tag.as_deref()).await?;
33    } else {
34        dump_log(&log_file, args.agent.as_deref(), agent_tag.as_deref()).await?;
35    }
36
37    Ok(())
38}
39
40async fn dump_log(path: &Path, agent_filter: Option<&str>, agent_tag: Option<&str>) -> Result<()> {
41    let file = tokio::fs::File::open(path).await.context("reading log file")?;
42    let reader = BufReader::new(file);
43    let mut lines = reader.lines();
44
45    while let Some(line) = lines.next_line().await.context("reading log line")? {
46        if should_show_line(&line, agent_filter, agent_tag) {
47            println!("{line}");
48        }
49    }
50
51    Ok(())
52}
53
54async fn tail_log(path: &Path, agent_filter: Option<&str>, agent_tag: Option<&str>) -> Result<()> {
55    let cancel = CancellationToken::new();
56    let cancel_for_signal = cancel.clone();
57
58    tokio::spawn(async move {
59        if tokio::signal::ctrl_c().await.is_ok() {
60            cancel_for_signal.cancel();
61        }
62    });
63
64    let file = tokio::fs::File::open(path).await.context("opening log file")?;
65    let mut reader = BufReader::new(file);
66    let mut line = String::new();
67
68    loop {
69        tokio::select! {
70            () = cancel.cancelled() => break,
71            result = reader.read_line(&mut line) => {
72                match result {
73                    Ok(0) => {
74                        // EOF, wait briefly for more content
75                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
76                    }
77                    Ok(_) => {
78                        let trimmed = line.trim_end();
79                        if should_show_line(trimmed, agent_filter, agent_tag) {
80                            println!("{trimmed}");
81                        }
82                        line.clear();
83                    }
84                    Err(e) => return Err(e).context("reading log file"),
85                }
86            }
87        }
88    }
89
90    Ok(())
91}
92
93fn should_show_line(line: &str, agent_filter: Option<&str>, agent_tag: Option<&str>) -> bool {
94    match (agent_filter, agent_tag) {
95        (Some(agent), Some(tag)) => line.contains(tag) || line.contains(agent),
96        _ => true,
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn filter_matches_agent_field() {
106        let tag = "agent=reviewer";
107        assert!(should_show_line(
108            r#"{"agent":"reviewer","msg":"ok"}"#,
109            Some("reviewer"),
110            Some(tag)
111        ));
112        assert!(!should_show_line(
113            r#"{"agent":"implementer","msg":"ok"}"#,
114            Some("reviewer"),
115            Some(tag)
116        ));
117    }
118
119    #[test]
120    fn no_filter_shows_all() {
121        assert!(should_show_line("any line at all", None, None));
122    }
123
124    #[test]
125    fn filter_matches_substring() {
126        assert!(should_show_line(
127            "agent=reviewer cycle=1",
128            Some("reviewer"),
129            Some("agent=reviewer")
130        ));
131    }
132}