1use crate::config;
2
3pub async fn stats(all: bool, since_hours: u32) -> anyhow::Result<()> {
7 let store = crate::cli::init_store().await?;
8 let window_label = crate::cli::hours_to_label(since_hours);
9
10 if all {
11 let summary = store.get_metrics_summary(since_hours).await?;
12 let cost = store.get_cost_summary().await.ok();
13 println!();
14 print_summary_table("All Projects", &window_label, &summary, cost.as_ref());
15 } else {
16 let repos = get_configured_repos();
17 if repos.is_empty() {
18 let summary = store.get_metrics_summary(since_hours).await?;
20 let cost = store.get_cost_summary().await.ok();
21 println!();
22 print_summary_table("All Projects", &window_label, &summary, cost.as_ref());
23 } else {
24 println!();
25 for repo in &repos {
26 let summary = store.get_metrics_summary_by_repo(repo, since_hours).await?;
27 let cost = store.get_cost_summary_by_repo(repo, since_hours).await.ok();
28 print_summary_table(repo, &window_label, &summary, cost.as_ref());
29 }
30 }
31 }
32
33 Ok(())
34}
35
36fn print_summary_table(
37 title: &str,
38 window_label: &str,
39 summary: &crate::store::MetricsSummary,
40 cost: Option<&crate::store::CostSummary>,
41) {
42 println!("── {title} ──────────────────────────────────");
43
44 let total = summary.tasks_completed_24h + summary.tasks_failed_24h;
45 #[expect(
46 clippy::cast_precision_loss,
47 reason = "a display percentage of task counts far below 2^52"
48 )]
49 let success_rate = if total > 0 {
50 summary.tasks_completed_24h as f64 / total as f64 * 100.0
51 } else {
52 0.0
53 };
54
55 println!(
56 " Tasks ({}): {} completed, {} failed",
57 window_label, summary.tasks_completed_24h, summary.tasks_failed_24h
58 );
59 println!(" Success rate: {success_rate:.1}%");
60
61 let mut durations = Vec::new();
63 if let Some(d) = summary.avg_duration_simple {
64 durations.push(format!("simple {:.1}m", d / 60.0));
65 }
66 if let Some(d) = summary.avg_duration_medium {
67 durations.push(format!("medium {:.1}m", d / 60.0));
68 }
69 if let Some(d) = summary.avg_duration_complex {
70 durations.push(format!("complex {:.1}m", d / 60.0));
71 }
72 if !durations.is_empty() {
73 println!(" Avg duration: {}", durations.join(" | "));
74 }
75
76 if !summary.agent_stats.is_empty() {
78 let agents: Vec<String> = summary
79 .agent_stats
80 .iter()
81 .map(|a| format!("{}: {} ({:.0}%)", a.agent, a.total_runs, a.success_rate))
82 .collect();
83 println!(" Agents: {}", agents.join(" | "));
84 }
85
86 if let Some(c) = cost {
88 let total_cost: f64 = c.periods.iter().map(|p| p.total_cost_usd).sum();
89 if total_cost > 0.0 {
90 if let Some(period) = c.periods.iter().find(|p| p.label == window_label) {
91 println!(" Cost ({}): ${:.2}", window_label, period.total_cost_usd);
92 } else if let Some(period_24h) = c.periods.iter().find(|p| p.label == "24h")
93 && period_24h.total_cost_usd > 0.0
94 {
95 println!(" Cost (24h): ${:.2}", period_24h.total_cost_usd);
96 }
97 }
98 }
99
100 if summary.rate_limits_24h > 0 {
101 println!(" Rate limits: {}", summary.rate_limits_24h);
102 }
103
104 println!();
105}
106
107fn get_configured_repos() -> Vec<String> {
109 let mut repos = Vec::new();
110
111 if let Ok(repo) = config::get_current_repo()
113 && !repo.is_empty()
114 {
115 repos.push(repo);
116 }
117
118 if let Ok(paths) = config::get_project_paths() {
120 for path_str in &paths {
121 let candidate = std::path::PathBuf::from(path_str).join(".orch.yml");
122 if candidate.exists()
123 && let Ok(content) = std::fs::read_to_string(&candidate)
124 && let Ok(val) = serde_norway::from_str::<serde_norway::Value>(&content)
125 && let Some(repo) = val
126 .get("gh")
127 .and_then(|g| g.get("repo"))
128 .and_then(|r| r.as_str())
129 && !repos.contains(&repo.to_string())
130 {
131 repos.push(repo.to_string());
132 }
133 }
134 }
135
136 repos
137}