ralph/cli/
productivity.rs1use anyhow::Result;
16use clap::{Args, Subcommand, ValueEnum};
17
18use crate::config;
19use crate::productivity;
20
21fn load_done_queue_for_estimation(
22 resolved: &config::Resolved,
23) -> Result<crate::contracts::QueueFile> {
24 crate::queue::load_queue_or_default(&resolved.done_path)
25}
26
27#[derive(ValueEnum, Clone, Copy, Debug, Default)]
28#[clap(rename_all = "snake_case")]
29pub enum ProductivityFormat {
30 #[default]
31 Text,
32 Json,
33}
34
35#[derive(Args)]
36#[command(about = "View productivity stats (streaks, velocity, milestones)")]
37pub struct ProductivityArgs {
38 #[command(subcommand)]
39 pub command: ProductivityCommand,
40}
41
42#[derive(Subcommand)]
43pub enum ProductivityCommand {
44 #[command(
46 after_long_help = "Examples:\n ralph productivity summary\n ralph productivity summary --format json\n ralph productivity summary --recent 10"
47 )]
48 Summary(ProductivitySummaryArgs),
49
50 #[command(
52 after_long_help = "Examples:\n ralph productivity velocity\n ralph productivity velocity --format json\n ralph productivity velocity --days 14"
53 )]
54 Velocity(ProductivityVelocityArgs),
55
56 #[command(
58 after_long_help = "Examples:\n ralph productivity streak\n ralph productivity streak --format json"
59 )]
60 Streak(ProductivityStreakArgs),
61
62 #[command(
64 after_long_help = "Examples:\n ralph productivity estimation\n ralph productivity estimation --format json"
65 )]
66 Estimation(ProductivityEstimationArgs),
67}
68
69#[derive(Args)]
70pub struct ProductivitySummaryArgs {
71 #[arg(long, value_enum, default_value_t = ProductivityFormat::Text)]
73 pub format: ProductivityFormat,
74
75 #[arg(long, default_value = "5")]
77 pub recent: usize,
78}
79
80#[derive(Args)]
81pub struct ProductivityVelocityArgs {
82 #[arg(long, value_enum, default_value_t = ProductivityFormat::Text)]
84 pub format: ProductivityFormat,
85
86 #[arg(long, default_value = "7")]
88 pub days: u32,
89}
90
91#[derive(Args)]
92pub struct ProductivityStreakArgs {
93 #[arg(long, value_enum, default_value_t = ProductivityFormat::Text)]
95 pub format: ProductivityFormat,
96}
97
98#[derive(Args)]
99pub struct ProductivityEstimationArgs {
100 #[arg(long, value_enum, default_value_t = ProductivityFormat::Text)]
102 pub format: ProductivityFormat,
103}
104
105pub fn handle(args: ProductivityArgs) -> Result<()> {
106 let resolved = config::resolve_from_cwd()?;
107 let cache_dir = resolved.repo_root.join(".ralph/cache");
108 let stats = productivity::load_productivity_stats(&cache_dir)?;
109
110 match args.command {
111 ProductivityCommand::Summary(cmd) => {
112 let report = productivity::build_summary_report(&stats, cmd.recent);
113 match cmd.format {
114 ProductivityFormat::Json => {
115 print!("{}", serde_json::to_string_pretty(&report)?);
116 }
117 ProductivityFormat::Text => {
118 productivity::print_summary_report_text(&report);
119 }
120 }
121 }
122 ProductivityCommand::Velocity(cmd) => {
123 let report = productivity::build_velocity_report(&stats, cmd.days);
124 match cmd.format {
125 ProductivityFormat::Json => {
126 print!("{}", serde_json::to_string_pretty(&report)?);
127 }
128 ProductivityFormat::Text => {
129 productivity::print_velocity_report_text(&report);
130 }
131 }
132 }
133 ProductivityCommand::Streak(cmd) => {
134 let report = productivity::build_streak_report(&stats);
135 match cmd.format {
136 ProductivityFormat::Json => {
137 print!("{}", serde_json::to_string_pretty(&report)?);
138 }
139 ProductivityFormat::Text => {
140 productivity::print_streak_report_text(&report);
141 }
142 }
143 }
144 ProductivityCommand::Estimation(cmd) => {
145 let done_queue = load_done_queue_for_estimation(&resolved)?;
147 let report = productivity::build_estimation_report(&done_queue.tasks);
148 match cmd.format {
149 ProductivityFormat::Json => {
150 print!("{}", serde_json::to_string_pretty(&report)?);
151 }
152 ProductivityFormat::Text => {
153 productivity::print_estimation_report_text(&report);
154 }
155 }
156 }
157 }
158
159 Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::contracts::{Config, QueueFile, Task, TaskStatus};
166 use std::path::PathBuf;
167
168 #[test]
169 fn estimation_loads_done_tasks_from_resolved_done_path() -> Result<()> {
170 let temp = tempfile::tempdir()?;
171 let repo_root = temp.path().to_path_buf();
172
173 let default_done_path = repo_root.join(".ralph/done.json");
174 let custom_done_path = repo_root.join("archive/done.jsonc");
175 std::fs::create_dir_all(default_done_path.parent().expect("default parent"))?;
176 std::fs::create_dir_all(custom_done_path.parent().expect("custom parent"))?;
177
178 let default_done = QueueFile {
179 version: 1,
180 tasks: vec![Task {
181 id: "RQ-DEFAULT".to_string(),
182 status: TaskStatus::Done,
183 title: "default".to_string(),
184 estimated_minutes: Some(10),
185 actual_minutes: Some(10),
186 ..Task::default()
187 }],
188 };
189 crate::queue::save_queue(&default_done_path, &default_done)?;
190
191 let custom_done = QueueFile {
192 version: 1,
193 tasks: vec![Task {
194 id: "RQ-CUSTOM".to_string(),
195 status: TaskStatus::Done,
196 title: "custom".to_string(),
197 estimated_minutes: Some(20),
198 actual_minutes: Some(25),
199 ..Task::default()
200 }],
201 };
202 crate::queue::save_queue(&custom_done_path, &custom_done)?;
203
204 let resolved = config::Resolved {
205 config: Config::default(),
206 repo_root,
207 queue_path: PathBuf::from("unused-queue"),
208 done_path: custom_done_path,
209 id_prefix: "RQ".to_string(),
210 id_width: 4,
211 global_config_path: None,
212 project_config_path: None,
213 };
214
215 let loaded = load_done_queue_for_estimation(&resolved)?;
216 assert_eq!(loaded.tasks.len(), 1);
217 assert_eq!(loaded.tasks[0].id, "RQ-CUSTOM");
218 Ok(())
219 }
220}