Skip to main content

smc/cmd/
projects.rs

1/// smc projects — list projects with session counts, sizes, and date ranges.
2use std::collections::HashMap;
3use std::io::Write;
4
5use anyhow::Result;
6use serde::Serialize;
7
8use crate::models;
9use crate::output::Emitter;
10use crate::util::discover::SessionFile;
11
12// ── Opts ───────────────────────────────────────────────────────────────────
13
14pub struct ProjectsOpts {
15    pub max_tokens: usize,
16}
17
18// ── Records ────────────────────────────────────────────────────────────────
19
20#[derive(Serialize, Debug)]
21struct ProjectRecord {
22    #[serde(rename = "type")]
23    record_type: &'static str,
24    name: String,
25    sessions: usize,
26    size_bytes: u64,
27    size_human: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    earliest: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    latest: Option<String>,
32}
33
34// ── run ────────────────────────────────────────────────────────────────────
35
36pub fn run<W: Write>(_opts: &ProjectsOpts, files: &[SessionFile], em: &mut Emitter<W>) -> Result<()> {
37    struct Info {
38        sessions: usize,
39        total_size: u64,
40        earliest: Option<String>,
41        latest: Option<String>,
42    }
43
44    let mut projects: HashMap<String, Info> = HashMap::new();
45
46    for file in files {
47        let entry = projects.entry(file.project_name.clone()).or_insert(Info {
48            sessions: 0,
49            total_size: 0,
50            earliest: None,
51            latest: None,
52        });
53        entry.sessions += 1;
54        entry.total_size += file.size_bytes;
55
56        if let Ok(f) = std::fs::File::open(&file.path) {
57            use std::io::BufRead;
58            let reader = std::io::BufReader::new(f);
59            for line in reader.lines().take(5) {
60                let Ok(line) = line else { continue };
61                if let Ok(record) = serde_json::from_str::<models::Record>(&line) {
62                    if let Some(msg) = record.as_message() {
63                        if let Some(ts) = &msg.timestamp {
64                            let ts_date = ts.get(..10).unwrap_or(ts);
65                            if entry.earliest.as_deref().map_or(true, |e| ts_date < e) {
66                                entry.earliest = Some(ts_date.to_string());
67                            }
68                            if entry.latest.as_deref().map_or(true, |l| ts_date > l) {
69                                entry.latest = Some(ts_date.to_string());
70                            }
71                            break;
72                        }
73                    }
74                }
75            }
76        }
77    }
78
79    let mut sorted: Vec<_> = projects.into_iter().collect();
80    sorted.sort_by(|a, b| {
81        b.1.latest
82            .as_deref()
83            .unwrap_or("")
84            .cmp(a.1.latest.as_deref().unwrap_or(""))
85    });
86
87    for (name, info) in &sorted {
88        let rec = ProjectRecord {
89            record_type: "project",
90            name: name.clone(),
91            sessions: info.sessions,
92            size_bytes: info.total_size,
93            size_human: crate::cmd::stats::format_bytes(info.total_size),
94            earliest: info.earliest.clone(),
95            latest: info.latest.clone(),
96        };
97        if !em.emit(&rec)? {
98            break;
99        }
100    }
101
102    let summary = crate::output::SummaryRecord {
103        record_type: "summary",
104        count: sorted.len(),
105        files_scanned: None,
106        elapsed_ms: 0,
107    };
108    em.emit(&summary)?;
109
110    em.flush()?;
111    Ok(())
112}