1use 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
12pub struct ProjectsOpts {
15 pub max_tokens: usize,
16}
17
18#[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
34pub 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}