Skip to main content

systemprompt_cli/shared/
disk_logs.rs

1//! On-disk log-file discovery and tailing shared by the agent and MCP `logs`
2//! commands.
3//!
4//! Log files follow the `<prefix>-<name>.log` convention; lookup also accepts
5//! bare `<name>.log` paths and substring matches over the discovered set.
6
7use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result, anyhow};
10
11/// Lists `.log` files in `logs_dir` whose names start with `prefix`, sorted.
12pub fn list_log_files(logs_dir: &Path, prefix: &str) -> Result<Vec<String>> {
13    let mut files = std::fs::read_dir(logs_dir)
14        .context("Failed to read logs directory")?
15        .filter_map(Result::ok)
16        .filter_map(|entry| {
17            let path = entry.path();
18            path.file_name()
19                .and_then(|n| n.to_str())
20                .filter(|name| {
21                    name.starts_with(prefix)
22                        && path
23                            .extension()
24                            .is_some_and(|ext| ext.eq_ignore_ascii_case("log"))
25                })
26                .map(String::from)
27        })
28        .collect::<Vec<_>>();
29
30    files.sort();
31    Ok(files)
32}
33
34/// Resolves a log file for `name`: exact `<name>.log`, then
35/// `<prefix><name>.log`, then the first prefixed file containing `name`.
36pub fn find_log_file(logs_dir: &Path, prefix: &str, name: &str) -> Result<PathBuf> {
37    let exact_path = logs_dir.join(format!("{name}.log"));
38    if exact_path.exists() {
39        return Ok(exact_path);
40    }
41
42    let prefixed_path = logs_dir.join(format!("{prefix}{name}.log"));
43    if prefixed_path.exists() {
44        return Ok(prefixed_path);
45    }
46
47    let log_files = list_log_files(logs_dir, prefix)?;
48    log_files
49        .iter()
50        .find(|file| file.contains(name))
51        .map(|file| logs_dir.join(file))
52        .ok_or_else(|| {
53            anyhow!(
54                "Log file not found for '{}'. Available: {:?}",
55                name,
56                log_files
57            )
58        })
59}
60
61/// Reads the last `lines` lines of `log_file` that satisfy `filter`.
62pub fn read_log_lines(
63    log_file: &Path,
64    lines: usize,
65    filter: impl Fn(&str) -> bool,
66) -> Result<Vec<String>> {
67    use std::io::{BufRead, BufReader};
68
69    let file = std::fs::File::open(log_file)
70        .with_context(|| format!("Failed to open log file: {}", log_file.display()))?;
71
72    let reader = BufReader::new(file);
73    let filtered: Vec<String> = reader
74        .lines()
75        .collect::<Result<Vec<_>, _>>()
76        .context("Failed to read log lines")?
77        .into_iter()
78        .filter(|line| filter(line))
79        .collect();
80
81    let start = filtered.len().saturating_sub(lines);
82    Ok(filtered[start..].to_vec())
83}
84
85/// Strips `prefix` and the `.log` suffix from each file name, yielding the
86/// bare service/agent names offered in interactive selection.
87#[must_use]
88pub fn display_names(log_files: &[String], prefix: &str) -> Vec<String> {
89    log_files
90        .iter()
91        .map(|f| {
92            f.strip_prefix(prefix)
93                .unwrap_or(f)
94                .strip_suffix(".log")
95                .unwrap_or(f)
96                .to_owned()
97        })
98        .collect()
99}