Skip to main content

scud/commands/
log.rs

1use anyhow::{Context, Result};
2use chrono::Local;
3use std::fs::{self, OpenOptions};
4use std::io::Write;
5use std::path::PathBuf;
6
7use crate::storage::Storage;
8
9/// Write a summary log entry for a task.
10/// Logs are stored in .scud/logs/<task-id>.log
11/// Each entry is timestamped and appended to the log file.
12pub fn run(
13    project_root: Option<PathBuf>,
14    task_id: &str,
15    summary: &str,
16    tag: Option<&str>,
17) -> Result<()> {
18    let storage = Storage::new(project_root);
19
20    if !storage.is_initialized() {
21        anyhow::bail!("SCUD not initialized. Run: scud init");
22    }
23
24    // Get active tag if not provided
25    let active_tag = match tag {
26        Some(t) => t.to_string(),
27        None => storage
28            .get_active_group()?
29            .ok_or_else(|| anyhow::anyhow!("No active tag. Use --tag or run: scud tags <tag>"))?,
30    };
31
32    // Verify task exists
33    let phase = storage.load_group(&active_tag)?;
34    if phase.get_task(task_id).is_none() {
35        anyhow::bail!("Task '{}' not found in tag '{}'", task_id, active_tag);
36    }
37
38    // Create logs directory if it doesn't exist
39    let logs_dir = storage.scud_dir().join("logs");
40    fs::create_dir_all(&logs_dir).context("Failed to create logs directory")?;
41
42    // Append to log file
43    let log_file = logs_dir.join(format!("{}.log", task_id));
44    let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
45
46    let mut file = OpenOptions::new()
47        .create(true)
48        .append(true)
49        .open(&log_file)
50        .context("Failed to open log file")?;
51
52    writeln!(file, "--- {} ---", timestamp)?;
53    writeln!(file, "{}", summary.trim())?;
54    writeln!(file)?;
55
56    println!("✓ Log entry added to {}", log_file.display());
57    Ok(())
58}
59
60/// Read the log file for a task
61pub fn show(project_root: Option<PathBuf>, task_id: &str) -> Result<()> {
62    let storage = Storage::new(project_root);
63
64    let logs_dir = storage.scud_dir().join("logs");
65    let log_file = logs_dir.join(format!("{}.log", task_id));
66
67    if !log_file.exists() {
68        println!("No log entries for task '{}'", task_id);
69        return Ok(());
70    }
71
72    let content = fs::read_to_string(&log_file).context("Failed to read log file")?;
73    print!("{}", content);
74    Ok(())
75}
76
77/// Show recent log entries from all tasks (for discovery sharing between agents)
78pub fn show_all(project_root: Option<PathBuf>, limit: usize, tag: Option<&str>) -> Result<()> {
79    let storage = Storage::new(project_root);
80    let logs_dir = storage.scud_dir().join("logs");
81
82    if !logs_dir.exists() {
83        println!("No logs directory found. Use 'scud log <task_id> <summary>' to create entries.");
84        return Ok(());
85    }
86
87    // Collect all log entries with timestamps
88    let mut entries: Vec<(String, String, String)> = Vec::new(); // (timestamp, task_id, content)
89
90    // If tag is specified, get task IDs from that tag to filter
91    let tag_task_ids: Option<std::collections::HashSet<String>> = if let Some(t) = tag {
92        let phase = storage.load_group(t)?;
93        Some(phase.tasks.iter().map(|task| task.id.clone()).collect())
94    } else {
95        None
96    };
97
98    // Read all .log files
99    for entry in fs::read_dir(&logs_dir).context("Failed to read logs directory")? {
100        let entry = entry?;
101        let path = entry.path();
102
103        if path.extension().map_or(false, |ext| ext == "log") {
104            let task_id = path
105                .file_stem()
106                .and_then(|s| s.to_str())
107                .unwrap_or("unknown")
108                .to_string();
109
110            // Filter by tag if specified
111            if let Some(ref ids) = tag_task_ids {
112                if !ids.contains(&task_id) {
113                    continue;
114                }
115            }
116
117            let content = fs::read_to_string(&path).unwrap_or_default();
118
119            // Parse entries from file
120            let mut current_timestamp = String::new();
121            let mut current_content = String::new();
122
123            for line in content.lines() {
124                if line.starts_with("--- ") && line.ends_with(" ---") {
125                    // Save previous entry if exists
126                    if !current_timestamp.is_empty() && !current_content.trim().is_empty() {
127                        entries.push((
128                            current_timestamp.clone(),
129                            task_id.clone(),
130                            current_content.trim().to_string(),
131                        ));
132                    }
133                    // Start new entry
134                    current_timestamp = line
135                        .trim_start_matches("--- ")
136                        .trim_end_matches(" ---")
137                        .to_string();
138                    current_content.clear();
139                } else if !line.is_empty() {
140                    current_content.push_str(line);
141                    current_content.push('\n');
142                }
143            }
144            // Don't forget the last entry
145            if !current_timestamp.is_empty() && !current_content.trim().is_empty() {
146                entries.push((current_timestamp, task_id, current_content.trim().to_string()));
147            }
148        }
149    }
150
151    if entries.is_empty() {
152        println!("No log entries found.");
153        return Ok(());
154    }
155
156    // Sort by timestamp descending (most recent first)
157    entries.sort_by(|a, b| b.0.cmp(&a.0));
158
159    // Take only the most recent entries
160    let entries: Vec<_> = entries.into_iter().take(limit).collect();
161
162    // Print header
163    println!("=== Recent Log Entries ({} shown) ===\n", entries.len());
164
165    for (timestamp, task_id, content) in entries {
166        println!("[{}] Task {}", timestamp, task_id);
167        for line in content.lines() {
168            println!("  {}", line);
169        }
170        println!();
171    }
172
173    Ok(())
174}