Skip to main content

orch/cli/
stats.rs

1use crate::config;
2
3/// Show task metrics and statistics, per-project by default.
4///
5/// `since_hours` controls the time window (default 24; pass 168 for 7 days, etc.).
6pub 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            // Fallback to global stats if no projects configured
19            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    let success_rate = if total > 0 {
46        summary.tasks_completed_24h as f64 / total as f64 * 100.0
47    } else {
48        0.0
49    };
50
51    println!(
52        "  Tasks ({}):  {} completed, {} failed",
53        window_label, summary.tasks_completed_24h, summary.tasks_failed_24h
54    );
55    println!("  Success rate: {:.1}%", success_rate);
56
57    // Duration by complexity
58    let mut durations = Vec::new();
59    if let Some(d) = summary.avg_duration_simple {
60        durations.push(format!("simple {:.1}m", d / 60.0));
61    }
62    if let Some(d) = summary.avg_duration_medium {
63        durations.push(format!("medium {:.1}m", d / 60.0));
64    }
65    if let Some(d) = summary.avg_duration_complex {
66        durations.push(format!("complex {:.1}m", d / 60.0));
67    }
68    if !durations.is_empty() {
69        println!("  Avg duration: {}", durations.join(" | "));
70    }
71
72    // Agent stats
73    if !summary.agent_stats.is_empty() {
74        let agents: Vec<String> = summary
75            .agent_stats
76            .iter()
77            .map(|a| format!("{}: {} ({:.0}%)", a.agent, a.total_runs, a.success_rate))
78            .collect();
79        println!("  Agents:       {}", agents.join(" | "));
80    }
81
82    // Cost: show total across all recorded periods
83    if let Some(c) = cost {
84        let total_cost: f64 = c.periods.iter().map(|p| p.total_cost_usd).sum();
85        if total_cost > 0.0 {
86            if let Some(period) = c.periods.iter().find(|p| p.label == window_label) {
87                println!("  Cost ({}): ${:.2}", window_label, period.total_cost_usd);
88            } else if let Some(period_24h) = c.periods.iter().find(|p| p.label == "24h") {
89                if period_24h.total_cost_usd > 0.0 {
90                    println!("  Cost (24h):   ${:.2}", period_24h.total_cost_usd);
91                }
92            }
93        }
94    }
95
96    if summary.rate_limits_24h > 0 {
97        println!("  Rate limits:  {}", summary.rate_limits_24h);
98    }
99
100    println!();
101}
102
103/// Get all configured project repos from global config + .orch.yml files.
104fn get_configured_repos() -> Vec<String> {
105    let mut repos = Vec::new();
106
107    // Try current project
108    if let Ok(repo) = config::get_current_repo() {
109        if !repo.is_empty() {
110            repos.push(repo);
111        }
112    }
113
114    // Try projects from global config
115    if let Ok(paths) = config::get_project_paths() {
116        for path_str in &paths {
117            let candidate = std::path::PathBuf::from(path_str).join(".orch.yml");
118            if candidate.exists() {
119                if let Ok(content) = std::fs::read_to_string(&candidate) {
120                    if let Ok(val) = serde_norway::from_str::<serde_norway::Value>(&content) {
121                        if let Some(repo) = val
122                            .get("gh")
123                            .and_then(|g| g.get("repo"))
124                            .and_then(|r| r.as_str())
125                        {
126                            if !repos.contains(&repo.to_string()) {
127                                repos.push(repo.to_string());
128                            }
129                        }
130                    }
131                }
132            }
133        }
134    }
135
136    repos
137}