1use super::{
2 BranchAction, CalendarAction, Cli, ClientAction, Commands, ConfigAction, EstimateAction,
3 GoalAction, IssueAction, ProjectAction, SessionAction, TagAction, TemplateAction,
4 WorkspaceAction,
5};
6use crate::cli::reports::ReportGenerator;
7use crate::db::advanced_queries::{
8 GitBranchQueries, GoalQueries, InsightQueries, TemplateQueries, TimeEstimateQueries,
9 WorkspaceQueries,
10};
11use crate::db::queries::{ProjectQueries, SessionEditQueries, SessionQueries, TagQueries};
12use crate::db::{get_connection, get_database_path, get_pool_stats, Database};
13use crate::models::{Goal, Project, ProjectTemplate, Tag, TimeEstimate, Workspace};
14use crate::utils::config::{load_config, save_config};
15use crate::utils::ipc::{get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse};
16use crate::utils::paths::{
17 canonicalize_path, detect_project_name, get_git_hash, is_git_repository, validate_project_path,
18};
19use crate::utils::validation::{validate_project_description, validate_project_name};
20use anyhow::{Context, Result};
21use chrono::{TimeZone, Utc};
22use serde::Deserialize;
23use std::env;
24use std::path::PathBuf;
25use std::process::{Command, Stdio};
26
27use crate::ui::dashboard::Dashboard;
28use crate::ui::history::SessionHistoryBrowser;
29use crate::ui::timer::InteractiveTimer;
30use crossterm::{
31 execute,
32 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
33};
34use ratatui::{backend::CrosstermBackend, Terminal};
35use std::io;
36use tokio::runtime::Handle;
37
38pub async fn handle_command(cli: Cli) -> Result<()> {
39 match cli.command {
40 Commands::Start => start_daemon().await,
41
42 Commands::Stop => stop_daemon().await,
43
44 Commands::Restart => restart_daemon().await,
45
46 Commands::Status => status_daemon().await,
47
48 Commands::Init {
49 name,
50 path,
51 description,
52 } => init_project(name, path, description).await,
53
54 Commands::List { all, tag } => list_projects(all, tag).await,
55
56 Commands::Report {
57 project,
58 from,
59 to,
60 format,
61 group,
62 } => generate_report(project, from, to, format, group).await,
63
64 Commands::Project { action } => handle_project_action(action).await,
65
66 Commands::Session { action } => handle_session_action(action).await,
67
68 Commands::Tag { action } => handle_tag_action(action).await,
69
70 Commands::Config { action } => handle_config_action(action).await,
71
72 Commands::Dashboard => launch_dashboard().await,
73
74 Commands::Tui => launch_dashboard().await,
75
76 Commands::Timer => launch_timer().await,
77
78 Commands::History => launch_history().await,
79
80 Commands::Goal { action } => handle_goal_action(action).await,
81
82 Commands::Insights { period, project } => show_insights(period, project).await,
83
84 Commands::Summary { period, from } => show_summary(period, from).await,
85
86 Commands::Compare { projects, from, to } => compare_projects(projects, from, to).await,
87
88 Commands::PoolStats => show_pool_stats().await,
89
90 Commands::Estimate { action } => handle_estimate_action(action).await,
91
92 Commands::Branch { action } => handle_branch_action(action).await,
93
94 Commands::Template { action } => handle_template_action(action).await,
95
96 Commands::Workspace { action } => handle_workspace_action(action).await,
97
98 Commands::Calendar { action } => handle_calendar_action(action).await,
99
100 Commands::Issue { action } => handle_issue_action(action).await,
101
102 Commands::Client { action } => handle_client_action(action).await,
103
104 Commands::Update {
105 check,
106 force,
107 verbose,
108 } => handle_update(check, force, verbose).await,
109
110 Commands::Completions { shell } => {
111 Cli::generate_completions(shell);
112 Ok(())
113 }
114 }
115}
116
117async fn handle_project_action(action: ProjectAction) -> Result<()> {
118 match action {
119 ProjectAction::Archive { project } => archive_project(project).await,
120
121 ProjectAction::Unarchive { project } => unarchive_project(project).await,
122
123 ProjectAction::UpdatePath { project, path } => update_project_path(project, path).await,
124
125 ProjectAction::AddTag { project, tag } => add_tag_to_project(project, tag).await,
126
127 ProjectAction::RemoveTag { project, tag } => remove_tag_from_project(project, tag).await,
128 }
129}
130
131async fn handle_session_action(action: SessionAction) -> Result<()> {
132 match action {
133 SessionAction::Start { project, context } => start_session(project, context).await,
134
135 SessionAction::Stop => stop_session().await,
136
137 SessionAction::Pause => pause_session().await,
138
139 SessionAction::Resume => resume_session().await,
140
141 SessionAction::Current => current_session().await,
142
143 SessionAction::List { limit, project } => list_sessions(limit, project).await,
144
145 SessionAction::Edit {
146 id,
147 start,
148 end,
149 project,
150 reason,
151 } => edit_session(id, start, end, project, reason).await,
152
153 SessionAction::Delete { id, force } => delete_session(id, force).await,
154
155 SessionAction::Merge {
156 session_ids,
157 project,
158 notes,
159 } => merge_sessions(session_ids, project, notes).await,
160
161 SessionAction::Split {
162 session_id,
163 split_times,
164 notes,
165 } => split_session(session_id, split_times, notes).await,
166 }
167}
168
169async fn handle_tag_action(action: TagAction) -> Result<()> {
170 match action {
171 TagAction::Create {
172 name,
173 color,
174 description,
175 } => create_tag(name, color, description).await,
176
177 TagAction::List => list_tags().await,
178
179 TagAction::Delete { name } => delete_tag(name).await,
180 }
181}
182
183async fn handle_config_action(action: ConfigAction) -> Result<()> {
184 match action {
185 ConfigAction::Show => show_config().await,
186
187 ConfigAction::Set { key, value } => set_config(key, value).await,
188
189 ConfigAction::Get { key } => get_config(key).await,
190
191 ConfigAction::Reset => reset_config().await,
192 }
193}
194
195async fn start_daemon() -> Result<()> {
197 if is_daemon_running() {
198 println!("Daemon is already running");
199 return Ok(());
200 }
201
202 println!("Starting tempo daemon...");
203
204 let current_exe = std::env::current_exe()?;
205 let daemon_exe = current_exe.with_file_name("tempo-daemon");
206
207 if !daemon_exe.exists() {
208 return Err(anyhow::anyhow!(
209 "tempo-daemon executable not found at {:?}",
210 daemon_exe
211 ));
212 }
213
214 let mut cmd = Command::new(&daemon_exe);
215 cmd.stdout(Stdio::null())
216 .stderr(Stdio::null())
217 .stdin(Stdio::null());
218
219 #[cfg(unix)]
220 {
221 use std::os::unix::process::CommandExt;
222 cmd.process_group(0);
223 }
224
225 let child = cmd.spawn()?;
226
227 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
229
230 if is_daemon_running() {
231 println!("Daemon started successfully (PID: {})", child.id());
232 Ok(())
233 } else {
234 Err(anyhow::anyhow!("Failed to start daemon"))
235 }
236}
237
238async fn stop_daemon() -> Result<()> {
239 if !is_daemon_running() {
240 println!("Daemon is not running");
241 return Ok(());
242 }
243
244 println!("Stopping tempo daemon...");
245
246 if let Ok(socket_path) = get_socket_path() {
248 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
249 match client.send_message(&IpcMessage::Shutdown).await {
250 Ok(_) => {
251 println!("Daemon stopped successfully");
252 return Ok(());
253 }
254 Err(e) => {
255 eprintln!("Failed to send shutdown message: {}", e);
256 }
257 }
258 }
259 }
260
261 if let Ok(Some(pid)) = crate::utils::ipc::read_pid_file() {
263 #[cfg(unix)]
264 {
265 let result = Command::new("kill").arg(pid.to_string()).output();
266
267 match result {
268 Ok(_) => println!("Daemon stopped via kill signal"),
269 Err(e) => eprintln!("Failed to kill daemon: {}", e),
270 }
271 }
272
273 #[cfg(windows)]
274 {
275 let result = Command::new("taskkill")
276 .args(&["/PID", &pid.to_string(), "/F"])
277 .output();
278
279 match result {
280 Ok(_) => println!("Daemon stopped via taskkill"),
281 Err(e) => eprintln!("Failed to kill daemon: {}", e),
282 }
283 }
284 }
285
286 Ok(())
287}
288
289async fn restart_daemon() -> Result<()> {
290 println!("Restarting tempo daemon...");
291 stop_daemon().await?;
292 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
293 start_daemon().await
294}
295
296async fn status_daemon() -> Result<()> {
297 if !is_daemon_running() {
298 print_daemon_not_running();
299 return Ok(());
300 }
301
302 if let Ok(socket_path) = get_socket_path() {
303 match IpcClient::connect(&socket_path).await {
304 Ok(mut client) => {
305 match client.send_message(&IpcMessage::GetStatus).await {
306 Ok(IpcResponse::Status {
307 daemon_running: _,
308 active_session,
309 uptime,
310 }) => {
311 print_daemon_status(uptime, active_session.as_ref());
312 }
313 Ok(IpcResponse::Pong) => {
314 print_daemon_status(0, None); }
316 Ok(other) => {
317 println!("Daemon is running (unexpected response: {:?})", other);
318 }
319 Err(e) => {
320 println!("Daemon is running but not responding: {}", e);
321 }
322 }
323 }
324 Err(e) => {
325 println!("Daemon appears to be running but cannot connect: {}", e);
326 }
327 }
328 } else {
329 println!("Cannot determine socket path");
330 }
331
332 Ok(())
333}
334
335async fn start_session(project: Option<String>, context: Option<String>) -> Result<()> {
337 if !is_daemon_running() {
338 println!("Daemon is not running. Start it with 'tempo start'");
339 return Ok(());
340 }
341
342 let project_path = if let Some(proj) = project {
343 PathBuf::from(proj)
344 } else {
345 env::current_dir()?
346 };
347
348 let context = context.unwrap_or_else(|| "manual".to_string());
349
350 let socket_path = get_socket_path()?;
351 let mut client = IpcClient::connect(&socket_path).await?;
352
353 let message = IpcMessage::StartSession {
354 project_path: Some(project_path.clone()),
355 context,
356 };
357
358 match client.send_message(&message).await {
359 Ok(IpcResponse::Ok) => {
360 println!("Session started for project at {:?}", project_path);
361 }
362 Ok(IpcResponse::Error(message)) => {
363 println!("Failed to start session: {}", message);
364 }
365 Ok(other) => {
366 println!("Unexpected response: {:?}", other);
367 }
368 Err(e) => {
369 println!("Failed to communicate with daemon: {}", e);
370 }
371 }
372
373 Ok(())
374}
375
376async fn stop_session() -> Result<()> {
377 if !is_daemon_running() {
378 println!("Daemon is not running");
379 return Ok(());
380 }
381
382 let socket_path = get_socket_path()?;
383 let mut client = IpcClient::connect(&socket_path).await?;
384
385 match client.send_message(&IpcMessage::StopSession).await {
386 Ok(IpcResponse::Ok) => {
387 println!("Session stopped");
388 }
389 Ok(IpcResponse::Error(message)) => {
390 println!("Failed to stop session: {}", message);
391 }
392 Ok(other) => {
393 println!("Unexpected response: {:?}", other);
394 }
395 Err(e) => {
396 println!("Failed to communicate with daemon: {}", e);
397 }
398 }
399
400 Ok(())
401}
402
403async fn pause_session() -> Result<()> {
404 if !is_daemon_running() {
405 println!("Daemon is not running");
406 return Ok(());
407 }
408
409 let socket_path = get_socket_path()?;
410 let mut client = IpcClient::connect(&socket_path).await?;
411
412 match client.send_message(&IpcMessage::PauseSession).await {
413 Ok(IpcResponse::Ok) => {
414 println!("Session paused");
415 }
416 Ok(IpcResponse::Error(message)) => {
417 println!("Failed to pause session: {}", message);
418 }
419 Ok(other) => {
420 println!("Unexpected response: {:?}", other);
421 }
422 Err(e) => {
423 println!("Failed to communicate with daemon: {}", e);
424 }
425 }
426
427 Ok(())
428}
429
430async fn resume_session() -> Result<()> {
431 if !is_daemon_running() {
432 println!("Daemon is not running");
433 return Ok(());
434 }
435
436 let socket_path = get_socket_path()?;
437 let mut client = IpcClient::connect(&socket_path).await?;
438
439 match client.send_message(&IpcMessage::ResumeSession).await {
440 Ok(IpcResponse::Ok) => {
441 println!("Session resumed");
442 }
443 Ok(IpcResponse::Error(message)) => {
444 println!("Failed to resume session: {}", message);
445 }
446 Ok(other) => {
447 println!("Unexpected response: {:?}", other);
448 }
449 Err(e) => {
450 println!("Failed to communicate with daemon: {}", e);
451 }
452 }
453
454 Ok(())
455}
456
457async fn current_session() -> Result<()> {
458 if !is_daemon_running() {
459 print_daemon_not_running();
460 return Ok(());
461 }
462
463 let socket_path = get_socket_path()?;
464 let mut client = IpcClient::connect(&socket_path).await?;
465
466 match client.send_message(&IpcMessage::GetActiveSession).await {
467 Ok(IpcResponse::SessionInfo(session)) => {
468 print_formatted_session(&session)?;
469 }
470 Ok(IpcResponse::Error(message)) => {
471 print_no_active_session(&message);
472 }
473 Ok(other) => {
474 println!("Unexpected response: {:?}", other);
475 }
476 Err(e) => {
477 println!("Failed to communicate with daemon: {}", e);
478 }
479 }
480
481 Ok(())
482}
483
484async fn generate_report(
486 project: Option<String>,
487 from: Option<String>,
488 to: Option<String>,
489 format: Option<String>,
490 group: Option<String>,
491) -> Result<()> {
492 println!("Generating time report...");
493
494 let generator = ReportGenerator::new()?;
495 let report = generator.generate_report(project, from, to, group)?;
496
497 match format.as_deref() {
498 Some("csv") => {
499 let output_path = PathBuf::from("tempo-report.csv");
500 generator.export_csv(&report, &output_path)?;
501 println!("Report exported to: {:?}", output_path);
502 }
503 Some("json") => {
504 let output_path = PathBuf::from("tempo-report.json");
505 generator.export_json(&report, &output_path)?;
506 println!("Report exported to: {:?}", output_path);
507 }
508 _ => {
509 print_formatted_report(&report)?;
511 }
512 }
513
514 Ok(())
515}
516
517fn print_formatted_session(session: &crate::utils::ipc::SessionInfo) -> Result<()> {
519 let context_color = match session.context.as_str() {
521 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", };
527
528 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
529 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
530 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
531 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m*\x1b[0m \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m");
532 println!(
533 "\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
534 truncate_string(&session.project_name, 25)
535 );
536 println!(
537 "\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
538 format_duration_fancy(session.duration)
539 );
540 println!(
541 "\x1b[36m│\x1b[0m Started: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
542 session.start_time.format("%H:%M:%S").to_string()
543 );
544 println!(
545 "\x1b[36m│\x1b[0m Context: {}{:<25}\x1b[0m \x1b[36m│\x1b[0m",
546 context_color,
547 truncate_string(&session.context, 25)
548 );
549 println!(
550 "\x1b[36m│\x1b[0m Path: \x1b[2;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
551 truncate_string(&session.project_path.to_string_lossy(), 25)
552 );
553 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
554 Ok(())
555}
556
557fn print_formatted_report(report: &crate::cli::reports::TimeReport) -> Result<()> {
558 let get_context_color = |context: &str| -> &str {
560 match context {
561 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", }
567 };
568
569 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
570 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Report\x1b[0m \x1b[36m│\x1b[0m");
571 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
572
573 for (project_name, project_summary) in &report.projects {
574 println!(
575 "\x1b[36m│\x1b[0m \x1b[1;33m{:<20}\x1b[0m \x1b[1;32m{:>15}\x1b[0m \x1b[36m│\x1b[0m",
576 truncate_string(project_name, 20),
577 format_duration_fancy(project_summary.total_duration)
578 );
579
580 for (context, duration) in &project_summary.contexts {
581 let context_color = get_context_color(context);
582 println!(
583 "\x1b[36m│\x1b[0m {}{:<15}\x1b[0m \x1b[32m{:>20}\x1b[0m \x1b[36m│\x1b[0m",
584 context_color,
585 truncate_string(context, 15),
586 format_duration_fancy(*duration)
587 );
588 }
589 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
590 }
591
592 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
593 println!(
594 "\x1b[36m│\x1b[0m \x1b[1;37mTotal Time:\x1b[0m \x1b[1;32m{:>26}\x1b[0m \x1b[36m│\x1b[0m",
595 format_duration_fancy(report.total_duration)
596 );
597 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
598 Ok(())
599}
600
601fn format_duration_fancy(seconds: i64) -> String {
602 let hours = seconds / 3600;
603 let minutes = (seconds % 3600) / 60;
604 let secs = seconds % 60;
605
606 if hours > 0 {
607 format!("{}h {}m {}s", hours, minutes, secs)
608 } else if minutes > 0 {
609 format!("{}m {}s", minutes, secs)
610 } else {
611 format!("{}s", secs)
612 }
613}
614
615fn truncate_string(s: &str, max_len: usize) -> String {
616 if s.len() <= max_len {
617 format!("{:<width$}", s, width = max_len)
618 } else {
619 format!("{:.width$}...", s, width = max_len.saturating_sub(3))
620 }
621}
622
623fn print_daemon_not_running() {
625 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
626 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
627 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
628 println!("\x1b[36m│\x1b[0m Status: \x1b[1;31m*\x1b[0m \x1b[31mOffline\x1b[0m \x1b[36m│\x1b[0m");
629 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
630 println!(
631 "\x1b[36m│\x1b[0m \x1b[33mDaemon is not running.\x1b[0m \x1b[36m│\x1b[0m"
632 );
633 println!("\x1b[36m│\x1b[0m \x1b[37mStart it with:\x1b[0m \x1b[96mtempo start\x1b[0m \x1b[36m│\x1b[0m");
634 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
635}
636
637fn print_no_active_session(message: &str) {
638 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
639 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
640 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
641 println!("\x1b[36m│\x1b[0m Status: \x1b[1;33m-\x1b[0m \x1b[33mIdle\x1b[0m \x1b[36m│\x1b[0m");
642 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
643 println!(
644 "\x1b[36m│\x1b[0m \x1b[90m{:<37}\x1b[0m \x1b[36m│\x1b[0m",
645 message
646 );
647 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
648 println!(
649 "\x1b[36m│\x1b[0m \x1b[37mStart tracking:\x1b[0m \x1b[36m│\x1b[0m"
650 );
651 println!(
652 "\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m"
653 );
654 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
655}
656
657fn print_daemon_status(uptime: u64, active_session: Option<&crate::utils::ipc::SessionInfo>) {
658 let uptime_formatted = format_duration_fancy(uptime as i64);
659
660 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
661 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
662 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
663 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m*\x1b[0m \x1b[32mOnline\x1b[0m \x1b[36m│\x1b[0m");
664 println!(
665 "\x1b[36m│\x1b[0m Uptime: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
666 uptime_formatted
667 );
668
669 if let Some(session) = active_session {
670 let context_color = match session.context.as_str() {
671 "terminal" => "\x1b[96m",
672 "ide" => "\x1b[95m",
673 "linked" => "\x1b[93m",
674 "manual" => "\x1b[94m",
675 _ => "\x1b[97m",
676 };
677
678 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
679 println!("\x1b[36m│\x1b[0m \x1b[1;37mActive Session:\x1b[0m \x1b[36m│\x1b[0m");
680 println!(
681 "\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
682 truncate_string(&session.project_name, 23)
683 );
684 println!(
685 "\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
686 format_duration_fancy(session.duration)
687 );
688 println!(
689 "\x1b[36m│\x1b[0m Context: {}{:<23}\x1b[0m \x1b[36m│\x1b[0m",
690 context_color, session.context
691 );
692 } else {
693 println!("\x1b[36m│\x1b[0m Session: \x1b[33mNo active session\x1b[0m \x1b[36m│\x1b[0m");
694 }
695
696 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
697}
698
699async fn init_project(
701 name: Option<String>,
702 path: Option<PathBuf>,
703 description: Option<String>,
704) -> Result<()> {
705 let validated_name = if let Some(n) = name.as_ref() {
707 Some(validate_project_name(n).with_context(|| format!("Invalid project name '{}'", n))?)
708 } else {
709 None
710 };
711
712 let validated_description = if let Some(d) = description.as_ref() {
713 Some(validate_project_description(d).with_context(|| "Invalid project description")?)
714 } else {
715 None
716 };
717
718 let project_path =
719 path.unwrap_or_else(|| env::current_dir().expect("Failed to get current directory"));
720
721 let canonical_path = validate_project_path(&project_path)
723 .with_context(|| format!("Invalid project path: {}", project_path.display()))?;
724
725 let project_name = validated_name.clone().unwrap_or_else(|| {
726 let detected = detect_project_name(&canonical_path);
727 validate_project_name(&detected).unwrap_or_else(|_| "project".to_string())
728 });
729
730 let conn = match get_connection().await {
732 Ok(conn) => conn,
733 Err(_) => {
734 let db_path = get_database_path()?;
736 let db = Database::new(&db_path)?;
737 return init_project_with_db(
738 validated_name,
739 Some(canonical_path),
740 validated_description,
741 &db.connection,
742 )
743 .await;
744 }
745 };
746
747 if let Some(existing) = ProjectQueries::find_by_path(conn.connection(), &canonical_path)? {
749 eprintln!(
750 "\x1b[33m! Warning:\x1b[0m A project named '{}' already exists at this path.",
751 existing.name
752 );
753 eprintln!("Use 'tempo list' to see all projects or choose a different location.");
754 return Ok(());
755 }
756
757 init_project_with_db(
759 Some(project_name.clone()),
760 Some(canonical_path.clone()),
761 validated_description,
762 conn.connection(),
763 )
764 .await?;
765
766 println!(
767 "\x1b[32m+ Success:\x1b[0m Project '{}' initialized at {}",
768 project_name,
769 canonical_path.display()
770 );
771 println!("Start tracking time with: \x1b[36mtempo start\x1b[0m");
772
773 Ok(())
774}
775
776async fn list_projects(include_archived: bool, tag_filter: Option<String>) -> Result<()> {
777 let db_path = get_database_path()?;
779 let db = Database::new(&db_path)?;
780
781 let projects = ProjectQueries::list_all(&db.connection, include_archived)?;
783
784 if projects.is_empty() {
785 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
786 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Projects\x1b[0m \x1b[36m│\x1b[0m");
787 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
788 println!("\x1b[36m│\x1b[0m No projects found. \x1b[36m│\x1b[0m");
789 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
790 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a project:\x1b[0m \x1b[36m│\x1b[0m");
791 println!("\x1b[36m│\x1b[0m \x1b[96mtempo init [project-name]\x1b[0m \x1b[36m│\x1b[0m");
792 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
793 return Ok(());
794 }
795
796 let filtered_projects = if let Some(_tag) = tag_filter {
798 projects
800 } else {
801 projects
802 };
803
804 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
805 println!("\x1b[36m│\x1b[0m \x1b[1;37mProjects\x1b[0m \x1b[36m│\x1b[0m");
806 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
807
808 for project in &filtered_projects {
809 let status_icon = if project.is_archived { "[A]" } else { "[P]" };
810 let status_color = if project.is_archived {
811 "\x1b[90m"
812 } else {
813 "\x1b[37m"
814 };
815 let git_indicator = if project.git_hash.is_some() {
816 " (git)"
817 } else {
818 ""
819 };
820
821 println!(
822 "\x1b[36m│\x1b[0m {} {}{:<25}\x1b[0m \x1b[36m│\x1b[0m",
823 status_icon,
824 status_color,
825 format!("{}{}", truncate_string(&project.name, 20), git_indicator)
826 );
827
828 if let Some(description) = &project.description {
829 println!(
830 "\x1b[36m│\x1b[0m \x1b[2;37m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
831 truncate_string(description, 35)
832 );
833 }
834
835 let path_display = project.path.to_string_lossy();
836 if path_display.len() > 35 {
837 let home_dir = dirs::home_dir();
838 let display_path = if let Some(home) = home_dir {
839 if let Ok(stripped) = project.path.strip_prefix(&home) {
840 format!("~/{}", stripped.display())
841 } else {
842 path_display.to_string()
843 }
844 } else {
845 path_display.to_string()
846 };
847 println!(
848 "\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
849 truncate_string(&display_path, 35)
850 );
851 } else {
852 println!(
853 "\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
854 truncate_string(&path_display, 35)
855 );
856 }
857
858 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
859 }
860
861 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
862 println!(
863 "\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
864 format!("{} projects", filtered_projects.len())
865 );
866 if include_archived {
867 let active_count = filtered_projects.iter().filter(|p| !p.is_archived).count();
868 let archived_count = filtered_projects.iter().filter(|p| p.is_archived).count();
869 println!("\x1b[36m│\x1b[0m \x1b[37mActive:\x1b[0m {:<15} \x1b[90mArchived:\x1b[0m {:<8} \x1b[36m│\x1b[0m",
870 active_count, archived_count
871 );
872 }
873 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
874
875 Ok(())
876}
877
878async fn create_tag(
880 name: String,
881 color: Option<String>,
882 description: Option<String>,
883) -> Result<()> {
884 let db_path = get_database_path()?;
886 let db = Database::new(&db_path)?;
887
888 let mut tag = Tag::new(name);
890 if let Some(c) = color {
891 tag = tag.with_color(c);
892 }
893 if let Some(d) = description {
894 tag = tag.with_description(d);
895 }
896
897 tag.validate()?;
899
900 let existing_tags = TagQueries::list_all(&db.connection)?;
902 if existing_tags.iter().any(|t| t.name == tag.name) {
903 println!("\x1b[33m⚠ Tag already exists:\x1b[0m {}", tag.name);
904 return Ok(());
905 }
906
907 let tag_id = TagQueries::create(&db.connection, &tag)?;
909
910 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
911 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Created\x1b[0m \x1b[36m│\x1b[0m");
912 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
913 println!(
914 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
915 truncate_string(&tag.name, 27)
916 );
917 if let Some(color_val) = &tag.color {
918 println!(
919 "\x1b[36m│\x1b[0m Color: \x1b[37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
920 truncate_string(color_val, 27)
921 );
922 }
923 if let Some(desc) = &tag.description {
924 println!(
925 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
926 truncate_string(desc, 27)
927 );
928 }
929 println!(
930 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
931 tag_id
932 );
933 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
934 println!(
935 "\x1b[36m│\x1b[0m \x1b[32m✓ Tag created successfully\x1b[0m \x1b[36m│\x1b[0m"
936 );
937 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
938
939 Ok(())
940}
941
942async fn list_tags() -> Result<()> {
943 let db_path = get_database_path()?;
945 let db = Database::new(&db_path)?;
946
947 let tags = TagQueries::list_all(&db.connection)?;
949
950 if tags.is_empty() {
951 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
952 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Tags\x1b[0m \x1b[36m│\x1b[0m");
953 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
954 println!("\x1b[36m│\x1b[0m No tags found. \x1b[36m│\x1b[0m");
955 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
956 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a tag:\x1b[0m \x1b[36m│\x1b[0m");
957 println!("\x1b[36m│\x1b[0m \x1b[96mtempo tag create <name>\x1b[0m \x1b[36m│\x1b[0m");
958 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
959 return Ok(());
960 }
961
962 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
963 println!("\x1b[36m│\x1b[0m \x1b[1;37mTags\x1b[0m \x1b[36m│\x1b[0m");
964 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
965
966 for tag in &tags {
967 let color_indicator = if let Some(color) = &tag.color {
968 format!(" ({})", color)
969 } else {
970 String::new()
971 };
972
973 println!(
974 "\x1b[36m│\x1b[0m 🏷️ \x1b[1;33m{:<30}\x1b[0m \x1b[36m│\x1b[0m",
975 format!("{}{}", truncate_string(&tag.name, 25), color_indicator)
976 );
977
978 if let Some(description) = &tag.description {
979 println!(
980 "\x1b[36m│\x1b[0m \x1b[2;37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
981 truncate_string(description, 33)
982 );
983 }
984
985 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
986 }
987
988 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
989 println!(
990 "\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
991 format!("{} tags", tags.len())
992 );
993 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
994
995 Ok(())
996}
997
998async fn delete_tag(name: String) -> Result<()> {
999 let db_path = get_database_path()?;
1001 let db = Database::new(&db_path)?;
1002
1003 if TagQueries::find_by_name(&db.connection, &name)?.is_none() {
1005 println!("\x1b[31m✗ Tag '{}' not found\x1b[0m", name);
1006 return Ok(());
1007 }
1008
1009 let deleted = TagQueries::delete_by_name(&db.connection, &name)?;
1011
1012 if deleted {
1013 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1014 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Deleted\x1b[0m \x1b[36m│\x1b[0m");
1015 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1016 println!(
1017 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1018 truncate_string(&name, 27)
1019 );
1020 println!(
1021 "\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m"
1022 );
1023 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1024 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag deleted successfully\x1b[0m \x1b[36m│\x1b[0m");
1025 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1026 } else {
1027 println!("\x1b[31m✗ Failed to delete tag '{}'\x1b[0m", name);
1028 }
1029
1030 Ok(())
1031}
1032
1033async fn show_config() -> Result<()> {
1035 let config = load_config()?;
1036
1037 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1038 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration\x1b[0m \x1b[36m│\x1b[0m");
1039 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1040 println!(
1041 "\x1b[36m│\x1b[0m idle_timeout_minutes: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1042 config.idle_timeout_minutes
1043 );
1044 println!(
1045 "\x1b[36m│\x1b[0m auto_pause_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1046 config.auto_pause_enabled
1047 );
1048 println!(
1049 "\x1b[36m│\x1b[0m default_context: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1050 config.default_context
1051 );
1052 println!(
1053 "\x1b[36m│\x1b[0m max_session_hours: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1054 config.max_session_hours
1055 );
1056 println!(
1057 "\x1b[36m│\x1b[0m backup_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1058 config.backup_enabled
1059 );
1060 println!(
1061 "\x1b[36m│\x1b[0m log_level: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1062 config.log_level
1063 );
1064
1065 if !config.custom_settings.is_empty() {
1066 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1067 println!("\x1b[36m│\x1b[0m \x1b[1;37mCustom Settings:\x1b[0m \x1b[36m│\x1b[0m");
1068 for (key, value) in &config.custom_settings {
1069 println!(
1070 "\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1071 truncate_string(key, 20),
1072 truncate_string(value, 16)
1073 );
1074 }
1075 }
1076
1077 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1078
1079 Ok(())
1080}
1081
1082async fn get_config(key: String) -> Result<()> {
1083 let config = load_config()?;
1084
1085 let value = match key.as_str() {
1086 "idle_timeout_minutes" => Some(config.idle_timeout_minutes.to_string()),
1087 "auto_pause_enabled" => Some(config.auto_pause_enabled.to_string()),
1088 "default_context" => Some(config.default_context),
1089 "max_session_hours" => Some(config.max_session_hours.to_string()),
1090 "backup_enabled" => Some(config.backup_enabled.to_string()),
1091 "log_level" => Some(config.log_level),
1092 _ => config.custom_settings.get(&key).cloned(),
1093 };
1094
1095 match value {
1096 Some(val) => {
1097 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1098 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Value\x1b[0m \x1b[36m│\x1b[0m");
1099 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1100 println!(
1101 "\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1102 truncate_string(&key, 20),
1103 truncate_string(&val, 16)
1104 );
1105 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1106 }
1107 None => {
1108 println!("\x1b[31m✗ Configuration key not found:\x1b[0m {}", key);
1109 }
1110 }
1111
1112 Ok(())
1113}
1114
1115async fn set_config(key: String, value: String) -> Result<()> {
1116 let mut config = load_config()?;
1117
1118 let display_value = value.clone(); match key.as_str() {
1121 "idle_timeout_minutes" => {
1122 config.idle_timeout_minutes = value.parse()?;
1123 }
1124 "auto_pause_enabled" => {
1125 config.auto_pause_enabled = value.parse()?;
1126 }
1127 "default_context" => {
1128 config.default_context = value;
1129 }
1130 "max_session_hours" => {
1131 config.max_session_hours = value.parse()?;
1132 }
1133 "backup_enabled" => {
1134 config.backup_enabled = value.parse()?;
1135 }
1136 "log_level" => {
1137 config.log_level = value;
1138 }
1139 _ => {
1140 config.set_custom(key.clone(), value);
1141 }
1142 }
1143
1144 config.validate()?;
1145 save_config(&config)?;
1146
1147 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1148 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Updated\x1b[0m \x1b[36m│\x1b[0m");
1149 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1150 println!(
1151 "\x1b[36m│\x1b[0m {:<20} \x1b[32m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1152 truncate_string(&key, 20),
1153 truncate_string(&display_value, 16)
1154 );
1155 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1156 println!(
1157 "\x1b[36m│\x1b[0m \x1b[32m✓ Configuration saved successfully\x1b[0m \x1b[36m│\x1b[0m"
1158 );
1159 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1160
1161 Ok(())
1162}
1163
1164async fn reset_config() -> Result<()> {
1165 let default_config = crate::models::Config::default();
1166 save_config(&default_config)?;
1167
1168 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1169 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Reset\x1b[0m \x1b[36m│\x1b[0m");
1170 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1171 println!(
1172 "\x1b[36m│\x1b[0m \x1b[32m✓ Configuration reset to defaults\x1b[0m \x1b[36m│\x1b[0m"
1173 );
1174 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1175 println!(
1176 "\x1b[36m│\x1b[0m \x1b[37mView current config:\x1b[0m \x1b[36m│\x1b[0m"
1177 );
1178 println!(
1179 "\x1b[36m│\x1b[0m \x1b[96mtempo config show\x1b[0m \x1b[36m│\x1b[0m"
1180 );
1181 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1182
1183 Ok(())
1184}
1185
1186async fn list_sessions(limit: Option<usize>, project_filter: Option<String>) -> Result<()> {
1188 let db_path = get_database_path()?;
1190 let db = Database::new(&db_path)?;
1191
1192 let session_limit = limit.unwrap_or(10);
1193
1194 let project_id = if let Some(project_name) = &project_filter {
1196 match ProjectQueries::find_by_name(&db.connection, project_name)? {
1197 Some(project) => Some(project.id.unwrap()),
1198 None => {
1199 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1200 return Ok(());
1201 }
1202 }
1203 } else {
1204 None
1205 };
1206
1207 let sessions = SessionQueries::list_with_filter(
1208 &db.connection,
1209 project_id,
1210 None,
1211 None,
1212 Some(session_limit),
1213 )?;
1214
1215 if sessions.is_empty() {
1216 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1217 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Sessions\x1b[0m \x1b[36m│\x1b[0m");
1218 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1219 println!("\x1b[36m│\x1b[0m No sessions found. \x1b[36m│\x1b[0m");
1220 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1221 println!("\x1b[36m│\x1b[0m \x1b[37mStart a session:\x1b[0m \x1b[36m│\x1b[0m");
1222 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
1223 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1224 return Ok(());
1225 }
1226
1227 let filtered_sessions = if let Some(_project) = project_filter {
1229 sessions
1231 } else {
1232 sessions
1233 };
1234
1235 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1236 println!("\x1b[36m│\x1b[0m \x1b[1;37mRecent Sessions\x1b[0m \x1b[36m│\x1b[0m");
1237 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1238
1239 for session in &filtered_sessions {
1240 let status_icon = if session.end_time.is_some() {
1241 "✅"
1242 } else {
1243 "🔄"
1244 };
1245 let duration = if let Some(end) = session.end_time {
1246 (end - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1247 } else {
1248 (Utc::now() - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1249 };
1250
1251 let context_color = match session.context {
1252 crate::models::SessionContext::Terminal => "\x1b[96m",
1253 crate::models::SessionContext::IDE => "\x1b[95m",
1254 crate::models::SessionContext::Linked => "\x1b[93m",
1255 crate::models::SessionContext::Manual => "\x1b[94m",
1256 };
1257
1258 println!(
1259 "\x1b[36m│\x1b[0m {} \x1b[1;37m{:<32}\x1b[0m \x1b[36m│\x1b[0m",
1260 status_icon,
1261 format!("Session {}", session.id.unwrap_or(0))
1262 );
1263 println!(
1264 "\x1b[36m│\x1b[0m Duration: \x1b[32m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1265 format_duration_fancy(duration)
1266 );
1267 println!(
1268 "\x1b[36m│\x1b[0m Context: {}{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1269 context_color, session.context
1270 );
1271 println!(
1272 "\x1b[36m│\x1b[0m Started: \x1b[37m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1273 session.start_time.format("%Y-%m-%d %H:%M:%S")
1274 );
1275 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1276 }
1277
1278 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1279 println!(
1280 "\x1b[36m│\x1b[0m \x1b[1;37mShowing:\x1b[0m {:<28} \x1b[36m│\x1b[0m",
1281 format!("{} recent sessions", filtered_sessions.len())
1282 );
1283 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1284
1285 Ok(())
1286}
1287
1288async fn edit_session(
1289 id: i64,
1290 start: Option<String>,
1291 end: Option<String>,
1292 project: Option<String>,
1293 reason: Option<String>,
1294) -> Result<()> {
1295 let db_path = get_database_path()?;
1297 let db = Database::new(&db_path)?;
1298
1299 let session = SessionQueries::find_by_id(&db.connection, id)?;
1301 let session = match session {
1302 Some(s) => s,
1303 None => {
1304 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1305 return Ok(());
1306 }
1307 };
1308
1309 let original_start = session.start_time;
1310 let original_end = session.end_time;
1311
1312 let mut new_start = original_start;
1314 let mut new_end = original_end;
1315 let mut new_project_id = session.project_id;
1316
1317 if let Some(start_str) = &start {
1319 new_start = match chrono::DateTime::parse_from_rfc3339(start_str) {
1320 Ok(dt) => dt.with_timezone(&chrono::Utc),
1321 Err(_) => match chrono::NaiveDateTime::parse_from_str(start_str, "%Y-%m-%d %H:%M:%S") {
1322 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1323 Err(_) => {
1324 return Err(anyhow::anyhow!(
1325 "Invalid start time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"
1326 ))
1327 }
1328 },
1329 };
1330 }
1331
1332 if let Some(end_str) = &end {
1334 if end_str.to_lowercase() == "null" || end_str.to_lowercase() == "none" {
1335 new_end = None;
1336 } else {
1337 new_end = Some(match chrono::DateTime::parse_from_rfc3339(end_str) {
1338 Ok(dt) => dt.with_timezone(&chrono::Utc),
1339 Err(_) => {
1340 match chrono::NaiveDateTime::parse_from_str(end_str, "%Y-%m-%d %H:%M:%S") {
1341 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1342 Err(_) => {
1343 return Err(anyhow::anyhow!(
1344 "Invalid end time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"
1345 ))
1346 }
1347 }
1348 }
1349 });
1350 }
1351 }
1352
1353 if let Some(project_name) = &project {
1355 if let Some(proj) = ProjectQueries::find_by_name(&db.connection, project_name)? {
1356 new_project_id = proj.id.unwrap();
1357 } else {
1358 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1359 return Ok(());
1360 }
1361 }
1362
1363 if new_start >= new_end.unwrap_or(chrono::Utc::now()) {
1365 println!("\x1b[31m✗ Start time must be before end time\x1b[0m");
1366 return Ok(());
1367 }
1368
1369 SessionEditQueries::create_edit_record(
1371 &db.connection,
1372 id,
1373 original_start,
1374 original_end,
1375 new_start,
1376 new_end,
1377 reason.clone(),
1378 )?;
1379
1380 SessionQueries::update_session(
1382 &db.connection,
1383 id,
1384 if start.is_some() {
1385 Some(new_start)
1386 } else {
1387 None
1388 },
1389 if end.is_some() { Some(new_end) } else { None },
1390 if project.is_some() {
1391 Some(new_project_id)
1392 } else {
1393 None
1394 },
1395 None,
1396 )?;
1397
1398 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1399 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Updated\x1b[0m \x1b[36m│\x1b[0m");
1400 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1401 println!(
1402 "\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1403 id
1404 );
1405
1406 if start.is_some() {
1407 println!(
1408 "\x1b[36m│\x1b[0m Start: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1409 truncate_string(&new_start.format("%Y-%m-%d %H:%M:%S").to_string(), 27)
1410 );
1411 }
1412
1413 if end.is_some() {
1414 let end_str = if let Some(e) = new_end {
1415 e.format("%Y-%m-%d %H:%M:%S").to_string()
1416 } else {
1417 "Ongoing".to_string()
1418 };
1419 println!(
1420 "\x1b[36m│\x1b[0m End: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1421 truncate_string(&end_str, 27)
1422 );
1423 }
1424
1425 if let Some(r) = &reason {
1426 println!(
1427 "\x1b[36m│\x1b[0m Reason: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1428 truncate_string(r, 27)
1429 );
1430 }
1431
1432 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1433 println!(
1434 "\x1b[36m│\x1b[0m \x1b[32m✓ Session updated with audit trail\x1b[0m \x1b[36m│\x1b[0m"
1435 );
1436 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1437
1438 Ok(())
1439}
1440
1441async fn delete_session(id: i64, force: bool) -> Result<()> {
1442 let db_path = get_database_path()?;
1444 let db = Database::new(&db_path)?;
1445
1446 let session = SessionQueries::find_by_id(&db.connection, id)?;
1448 let session = match session {
1449 Some(s) => s,
1450 None => {
1451 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1452 return Ok(());
1453 }
1454 };
1455
1456 if session.end_time.is_none() && !force {
1458 println!("\x1b[33m⚠ Cannot delete active session without --force flag\x1b[0m");
1459 println!(" Use: tempo session delete {} --force", id);
1460 return Ok(());
1461 }
1462
1463 SessionQueries::delete_session(&db.connection, id)?;
1465
1466 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1467 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Deleted\x1b[0m \x1b[36m│\x1b[0m");
1468 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1469 println!(
1470 "\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1471 id
1472 );
1473 println!(
1474 "\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m"
1475 );
1476
1477 if session.end_time.is_none() {
1478 println!("\x1b[36m│\x1b[0m Type: \x1b[33mActive session (forced)\x1b[0m \x1b[36m│\x1b[0m");
1479 } else {
1480 println!("\x1b[36m│\x1b[0m Type: \x1b[37mCompleted session\x1b[0m \x1b[36m│\x1b[0m");
1481 }
1482
1483 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1484 println!(
1485 "\x1b[36m│\x1b[0m \x1b[32m✓ Session and audit trail removed\x1b[0m \x1b[36m│\x1b[0m"
1486 );
1487 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1488
1489 Ok(())
1490}
1491
1492async fn archive_project(project_name: String) -> Result<()> {
1494 let db_path = get_database_path()?;
1495 let db = Database::new(&db_path)?;
1496
1497 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1498 Some(p) => p,
1499 None => {
1500 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1501 return Ok(());
1502 }
1503 };
1504
1505 if project.is_archived {
1506 println!(
1507 "\x1b[33m⚠ Project '{}' is already archived\x1b[0m",
1508 project_name
1509 );
1510 return Ok(());
1511 }
1512
1513 let success = ProjectQueries::archive_project(&db.connection, project.id.unwrap())?;
1514
1515 if success {
1516 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1517 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Archived\x1b[0m \x1b[36m│\x1b[0m");
1518 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1519 println!(
1520 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1521 truncate_string(&project_name, 27)
1522 );
1523 println!(
1524 "\x1b[36m│\x1b[0m Status: \x1b[90mArchived\x1b[0m \x1b[36m│\x1b[0m"
1525 );
1526 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1527 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project archived successfully\x1b[0m \x1b[36m│\x1b[0m");
1528 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1529 } else {
1530 println!(
1531 "\x1b[31m✗ Failed to archive project '{}'\x1b[0m",
1532 project_name
1533 );
1534 }
1535
1536 Ok(())
1537}
1538
1539async fn unarchive_project(project_name: String) -> Result<()> {
1540 let db_path = get_database_path()?;
1541 let db = Database::new(&db_path)?;
1542
1543 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1544 Some(p) => p,
1545 None => {
1546 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1547 return Ok(());
1548 }
1549 };
1550
1551 if !project.is_archived {
1552 println!(
1553 "\x1b[33m⚠ Project '{}' is not archived\x1b[0m",
1554 project_name
1555 );
1556 return Ok(());
1557 }
1558
1559 let success = ProjectQueries::unarchive_project(&db.connection, project.id.unwrap())?;
1560
1561 if success {
1562 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1563 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Unarchived\x1b[0m \x1b[36m│\x1b[0m");
1564 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1565 println!(
1566 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1567 truncate_string(&project_name, 27)
1568 );
1569 println!(
1570 "\x1b[36m│\x1b[0m Status: \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m"
1571 );
1572 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1573 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project unarchived successfully\x1b[0m \x1b[36m│\x1b[0m");
1574 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1575 } else {
1576 println!(
1577 "\x1b[31m✗ Failed to unarchive project '{}'\x1b[0m",
1578 project_name
1579 );
1580 }
1581
1582 Ok(())
1583}
1584
1585async fn update_project_path(project_name: String, new_path: PathBuf) -> Result<()> {
1586 let db_path = get_database_path()?;
1587 let db = Database::new(&db_path)?;
1588
1589 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1590 Some(p) => p,
1591 None => {
1592 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1593 return Ok(());
1594 }
1595 };
1596
1597 let canonical_path = canonicalize_path(&new_path)?;
1598 let success =
1599 ProjectQueries::update_project_path(&db.connection, project.id.unwrap(), &canonical_path)?;
1600
1601 if success {
1602 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1603 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Path Updated\x1b[0m \x1b[36m│\x1b[0m");
1604 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1605 println!(
1606 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1607 truncate_string(&project_name, 27)
1608 );
1609 println!(
1610 "\x1b[36m│\x1b[0m Old Path: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1611 truncate_string(&project.path.to_string_lossy(), 27)
1612 );
1613 println!(
1614 "\x1b[36m│\x1b[0m New Path: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1615 truncate_string(&canonical_path.to_string_lossy(), 27)
1616 );
1617 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1618 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Path updated successfully\x1b[0m \x1b[36m│\x1b[0m");
1619 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1620 } else {
1621 println!(
1622 "\x1b[31m✗ Failed to update path for project '{}'\x1b[0m",
1623 project_name
1624 );
1625 }
1626
1627 Ok(())
1628}
1629
1630async fn add_tag_to_project(project_name: String, tag_name: String) -> Result<()> {
1631 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1632 println!("Would add tag '{}' to project '{}'", tag_name, project_name);
1633 println!("This requires implementing project_tags table operations.");
1634 Ok(())
1635}
1636
1637async fn remove_tag_from_project(project_name: String, tag_name: String) -> Result<()> {
1638 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1639 println!(
1640 "Would remove tag '{}' from project '{}'",
1641 tag_name, project_name
1642 );
1643 println!("This requires implementing project_tags table operations.");
1644 Ok(())
1645}
1646
1647async fn bulk_update_sessions_project(
1649 session_ids: Vec<i64>,
1650 new_project_name: String,
1651) -> Result<()> {
1652 let db_path = get_database_path()?;
1653 let db = Database::new(&db_path)?;
1654
1655 let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1657 Some(p) => p,
1658 None => {
1659 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1660 return Ok(());
1661 }
1662 };
1663
1664 let updated =
1665 SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1666
1667 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1668 println!(
1669 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Update\x1b[0m \x1b[36m│\x1b[0m"
1670 );
1671 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1672 println!(
1673 "\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1674 updated
1675 );
1676 println!(
1677 "\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1678 truncate_string(&new_project_name, 27)
1679 );
1680 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1681 println!(
1682 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m",
1683 updated, ""
1684 );
1685 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1686
1687 Ok(())
1688}
1689
1690async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1691 let db_path = get_database_path()?;
1692 let db = Database::new(&db_path)?;
1693
1694 let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1695
1696 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1697 println!(
1698 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Delete\x1b[0m \x1b[36m│\x1b[0m"
1699 );
1700 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1701 println!(
1702 "\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1703 session_ids.len()
1704 );
1705 println!(
1706 "\x1b[36m│\x1b[0m Deleted: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1707 deleted
1708 );
1709 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1710 println!(
1711 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m",
1712 deleted, ""
1713 );
1714 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1715
1716 Ok(())
1717}
1718
1719async fn launch_dashboard() -> Result<()> {
1720 if !is_tty() {
1722 return show_dashboard_fallback().await;
1723 }
1724
1725 enable_raw_mode().context("Failed to enable raw mode - terminal may not support interactive features")?;
1727 let mut stdout = io::stdout();
1728
1729 execute!(stdout, EnterAlternateScreen)
1730 .context("Failed to enter alternate screen - terminal may not support full-screen mode")?;
1731
1732 let backend = CrosstermBackend::new(stdout);
1733 let mut terminal = Terminal::new(backend)
1734 .context("Failed to initialize terminal backend")?;
1735
1736 terminal.clear().context("Failed to clear terminal")?;
1738
1739 let result = async {
1741 let mut dashboard = Dashboard::new().await?;
1742 dashboard.run(&mut terminal).await
1743 };
1744
1745 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1746
1747 let cleanup_result = cleanup_terminal(&mut terminal);
1749
1750 if let Err(e) = cleanup_result {
1752 eprintln!("Warning: Failed to restore terminal: {}", e);
1753 }
1754
1755 result
1756}
1757
1758fn is_tty() -> bool {
1759 use std::os::unix::io::AsRawFd;
1760 unsafe { libc::isatty(std::io::stdin().as_raw_fd()) == 1 }
1761}
1762
1763async fn show_dashboard_fallback() -> Result<()> {
1764 println!("📊 Tempo Dashboard (Text Mode)");
1765 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1766 println!();
1767
1768 if is_daemon_running() {
1770 println!("🟢 Daemon Status: Running");
1771 } else {
1772 println!("🔴 Daemon Status: Offline");
1773 println!(" Start with: tempo start");
1774 println!();
1775 return Ok(());
1776 }
1777
1778 let socket_path = get_socket_path()?;
1780 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
1781 match client.send_message(&IpcMessage::GetActiveSession).await {
1782 Ok(IpcResponse::ActiveSession(Some(session))) => {
1783 println!("⏱️ Active Session:");
1784 println!(" Started: {}", session.start_time.format("%H:%M:%S"));
1785 println!(" Duration: {}", format_duration_simple((chrono::Utc::now().timestamp() - session.start_time.timestamp()) - session.paused_duration.num_seconds()));
1786 println!(" Context: {}", session.context);
1787 println!();
1788
1789 match client.send_message(&IpcMessage::GetProject(session.project_id)).await {
1791 Ok(IpcResponse::Project(Some(project))) => {
1792 println!("📁 Current Project: {}", project.name);
1793 println!(" Path: {}", project.path.display());
1794 println!();
1795 }
1796 _ => {
1797 println!("📁 Project: Unknown");
1798 println!();
1799 }
1800 }
1801 }
1802 _ => {
1803 println!("⏸️ No active session");
1804 println!(" Start tracking with: tempo session start");
1805 println!();
1806 }
1807 }
1808
1809 let today = chrono::Local::now().date_naive();
1811 match client.send_message(&IpcMessage::GetDailyStats(today)).await {
1812 Ok(IpcResponse::DailyStats { sessions_count, total_seconds, avg_seconds }) => {
1813 println!("📈 Today's Summary:");
1814 println!(" Sessions: {}", sessions_count);
1815 println!(" Total time: {}", format_duration_simple(total_seconds));
1816 if sessions_count > 0 {
1817 println!(" Average session: {}", format_duration_simple(avg_seconds));
1818 }
1819 let progress = (total_seconds as f64 / (8.0 * 3600.0)) * 100.0;
1820 println!(" Daily goal (8h): {:.1}%", progress);
1821 println!();
1822 }
1823 _ => {
1824 println!("📈 Today's Summary: No data available");
1825 println!();
1826 }
1827 }
1828 } else {
1829 println!("❌ Unable to connect to daemon");
1830 println!(" Try: tempo restart");
1831 println!();
1832 }
1833
1834 println!("💡 For interactive dashboard, run in a terminal:");
1835 println!(" • Terminal.app, iTerm2, or other terminal emulators");
1836 println!(" • SSH sessions with TTY allocation (ssh -t)");
1837 println!(" • Interactive shell environments");
1838
1839 Ok(())
1840}
1841
1842fn format_duration_simple(seconds: i64) -> String {
1843 let hours = seconds / 3600;
1844 let minutes = (seconds % 3600) / 60;
1845 let secs = seconds % 60;
1846
1847 if hours > 0 {
1848 format!("{}h {}m {}s", hours, minutes, secs)
1849 } else if minutes > 0 {
1850 format!("{}m {}s", minutes, secs)
1851 } else {
1852 format!("{}s", secs)
1853 }
1854}
1855
1856fn cleanup_terminal<B>(terminal: &mut Terminal<B>) -> Result<()>
1857where
1858 B: ratatui::backend::Backend + std::io::Write,
1859{
1860 disable_raw_mode().context("Failed to disable raw mode")?;
1862 execute!(terminal.backend_mut(), LeaveAlternateScreen)
1863 .context("Failed to leave alternate screen")?;
1864 terminal.show_cursor().context("Failed to show cursor")?;
1865 Ok(())
1866}
1867
1868async fn launch_timer() -> Result<()> {
1869 if !is_tty() {
1871 return Err(anyhow::anyhow!(
1872 "Interactive timer requires an interactive terminal (TTY).\n\
1873 \n\
1874 This command needs to run in a proper terminal environment.\n\
1875 Try running this command directly in your terminal application."
1876 ));
1877 }
1878
1879 enable_raw_mode().context("Failed to enable raw mode")?;
1881 let mut stdout = io::stdout();
1882 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
1883 let backend = CrosstermBackend::new(stdout);
1884 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
1885 terminal.clear().context("Failed to clear terminal")?;
1886
1887 let result = async {
1889 let mut timer = InteractiveTimer::new().await?;
1890 timer.run(&mut terminal).await
1891 };
1892
1893 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1894
1895 let cleanup_result = cleanup_terminal(&mut terminal);
1897 if let Err(e) = cleanup_result {
1898 eprintln!("Warning: Failed to restore terminal: {}", e);
1899 }
1900
1901 result
1902}
1903
1904async fn merge_sessions(
1905 session_ids_str: String,
1906 project_name: Option<String>,
1907 notes: Option<String>,
1908) -> Result<()> {
1909 let session_ids: Result<Vec<i64>, _> = session_ids_str
1911 .split(',')
1912 .map(|s| s.trim().parse::<i64>())
1913 .collect();
1914
1915 let session_ids = session_ids.map_err(|_| {
1916 anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'")
1917 })?;
1918
1919 if session_ids.len() < 2 {
1920 return Err(anyhow::anyhow!(
1921 "At least 2 sessions are required for merging"
1922 ));
1923 }
1924
1925 let mut target_project_id = None;
1927 if let Some(project) = project_name {
1928 let db_path = get_database_path()?;
1929 let db = Database::new(&db_path)?;
1930
1931 if let Ok(project_id) = project.parse::<i64>() {
1933 if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1934 target_project_id = Some(project_id);
1935 }
1936 } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1937 target_project_id = proj.id;
1938 }
1939
1940 if target_project_id.is_none() {
1941 return Err(anyhow::anyhow!("Project '{}' not found", project));
1942 }
1943 }
1944
1945 let db_path = get_database_path()?;
1947 let db = Database::new(&db_path)?;
1948
1949 let merged_id =
1950 SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1951
1952 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1953 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Merge Complete\x1b[0m \x1b[36m│\x1b[0m");
1954 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1955 println!(
1956 "\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1957 session_ids
1958 .iter()
1959 .map(|id| id.to_string())
1960 .collect::<Vec<_>>()
1961 .join(", ")
1962 );
1963 println!(
1964 "\x1b[36m│\x1b[0m New session ID: \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1965 merged_id
1966 );
1967 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1968 println!(
1969 "\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m \x1b[36m│\x1b[0m"
1970 );
1971 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1972
1973 Ok(())
1974}
1975
1976async fn split_session(
1977 session_id: i64,
1978 split_times_str: String,
1979 notes: Option<String>,
1980) -> Result<()> {
1981 let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
1983 let mut split_times = Vec::new();
1984
1985 for time_str in split_time_strings {
1986 let datetime = if time_str.contains(':') {
1988 let today = chrono::Local::now().date_naive();
1990 let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
1991 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
1992 .map_err(|_| {
1993 anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str)
1994 })?;
1995 today.and_time(time).and_utc()
1996 } else {
1997 chrono::DateTime::parse_from_rfc3339(time_str)
1999 .map_err(|_| {
2000 anyhow::anyhow!(
2001 "Invalid datetime format '{}'. Use HH:MM or RFC3339 format",
2002 time_str
2003 )
2004 })?
2005 .to_utc()
2006 };
2007
2008 split_times.push(datetime);
2009 }
2010
2011 if split_times.is_empty() {
2012 return Err(anyhow::anyhow!("No valid split times provided"));
2013 }
2014
2015 let notes_list = notes.map(|n| {
2017 n.split(',')
2018 .map(|s| s.trim().to_string())
2019 .collect::<Vec<String>>()
2020 });
2021
2022 let db_path = get_database_path()?;
2024 let db = Database::new(&db_path)?;
2025
2026 let new_session_ids =
2027 SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
2028
2029 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2030 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Split Complete\x1b[0m \x1b[36m│\x1b[0m");
2031 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2032 println!(
2033 "\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2034 session_id
2035 );
2036 println!(
2037 "\x1b[36m│\x1b[0m Split points: \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2038 split_times.len()
2039 );
2040 println!(
2041 "\x1b[36m│\x1b[0m New sessions: \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2042 new_session_ids
2043 .iter()
2044 .map(|id| id.to_string())
2045 .collect::<Vec<_>>()
2046 .join(", ")
2047 );
2048 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2049 println!(
2050 "\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m \x1b[36m│\x1b[0m"
2051 );
2052 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2053
2054 Ok(())
2055}
2056
2057async fn launch_history() -> Result<()> {
2058 if !is_tty() {
2060 return Err(anyhow::anyhow!(
2061 "Session history browser requires an interactive terminal (TTY).\n\
2062 \n\
2063 This command needs to run in a proper terminal environment.\n\
2064 Try running this command directly in your terminal application."
2065 ));
2066 }
2067
2068 enable_raw_mode().context("Failed to enable raw mode")?;
2070 let mut stdout = io::stdout();
2071 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
2072 let backend = CrosstermBackend::new(stdout);
2073 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
2074 terminal.clear().context("Failed to clear terminal")?;
2075
2076 let result = async {
2077 let mut browser = SessionHistoryBrowser::new().await?;
2078 browser.run(&mut terminal).await
2079 };
2080
2081 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
2082
2083 let cleanup_result = cleanup_terminal(&mut terminal);
2085 if let Err(e) = cleanup_result {
2086 eprintln!("Warning: Failed to restore terminal: {}", e);
2087 }
2088
2089 result
2090}
2091
2092async fn handle_goal_action(action: GoalAction) -> Result<()> {
2093 match action {
2094 GoalAction::Create {
2095 name,
2096 target_hours,
2097 project,
2098 description,
2099 start_date,
2100 end_date,
2101 } => {
2102 create_goal(
2103 name,
2104 target_hours,
2105 project,
2106 description,
2107 start_date,
2108 end_date,
2109 )
2110 .await
2111 }
2112 GoalAction::List { project } => list_goals(project).await,
2113 GoalAction::Update { id, hours } => update_goal_progress(id, hours).await,
2114 }
2115}
2116
2117async fn create_goal(
2118 name: String,
2119 target_hours: f64,
2120 project: Option<String>,
2121 description: Option<String>,
2122 start_date: Option<String>,
2123 end_date: Option<String>,
2124) -> Result<()> {
2125 let db_path = get_database_path()?;
2126 let db = Database::new(&db_path)?;
2127
2128 let project_id = if let Some(proj_name) = project {
2129 match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
2130 Some(p) => p.id,
2131 None => {
2132 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2133 return Ok(());
2134 }
2135 }
2136 } else {
2137 None
2138 };
2139
2140 let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2141 let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2142
2143 let mut goal = Goal::new(name.clone(), target_hours);
2144 if let Some(pid) = project_id {
2145 goal = goal.with_project(pid);
2146 }
2147 if let Some(desc) = description {
2148 goal = goal.with_description(desc);
2149 }
2150 goal = goal.with_dates(start, end);
2151
2152 goal.validate()?;
2153 let goal_id = GoalQueries::create(&db.connection, &goal)?;
2154
2155 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2156 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoal Created\x1b[0m \x1b[36m│\x1b[0m");
2157 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2158 println!(
2159 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2160 truncate_string(&name, 27)
2161 );
2162 println!(
2163 "\x1b[36m│\x1b[0m Target: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2164 format!("{} hours", target_hours)
2165 );
2166 println!(
2167 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2168 goal_id
2169 );
2170 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2171 println!(
2172 "\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m \x1b[36m│\x1b[0m"
2173 );
2174 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2175
2176 Ok(())
2177}
2178
2179async fn list_goals(project: Option<String>) -> Result<()> {
2180 let db_path = get_database_path()?;
2181 let db = Database::new(&db_path)?;
2182
2183 let project_id = if let Some(proj_name) = &project {
2184 match ProjectQueries::find_by_name(&db.connection, proj_name)? {
2185 Some(p) => p.id,
2186 None => {
2187 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2188 return Ok(());
2189 }
2190 }
2191 } else {
2192 None
2193 };
2194
2195 let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
2196
2197 if goals.is_empty() {
2198 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2199 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Goals\x1b[0m \x1b[36m│\x1b[0m");
2200 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2201 return Ok(());
2202 }
2203
2204 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2205 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoals\x1b[0m \x1b[36m│\x1b[0m");
2206 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2207
2208 for goal in &goals {
2209 let progress_pct = goal.progress_percentage();
2210 println!(
2211 "\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2212 truncate_string(&goal.name, 25)
2213 );
2214 println!("\x1b[36m│\x1b[0m Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h) \x1b[36m│\x1b[0m",
2215 progress_pct, goal.current_progress, goal.target_hours);
2216 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2217 }
2218
2219 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2220 Ok(())
2221}
2222
2223async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
2224 let db_path = get_database_path()?;
2225 let db = Database::new(&db_path)?;
2226
2227 GoalQueries::update_progress(&db.connection, id, hours)?;
2228 println!(
2229 "\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m",
2230 id, hours
2231 );
2232 Ok(())
2233}
2234
2235async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
2236 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2237 println!("\x1b[36m│\x1b[0m \x1b[1;37mProductivity Insights\x1b[0m \x1b[36m│\x1b[0m");
2238 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2239 println!(
2240 "\x1b[36m│\x1b[0m Period: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2241 period.as_deref().unwrap_or("all")
2242 );
2243 if let Some(proj) = project {
2244 println!(
2245 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2246 truncate_string(&proj, 27)
2247 );
2248 }
2249 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2250 println!(
2251 "\x1b[36m│\x1b[0m \x1b[33m⚠ Insights calculation in progress...\x1b[0m \x1b[36m│\x1b[0m"
2252 );
2253 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2254 Ok(())
2255}
2256
2257async fn show_summary(period: String, from: Option<String>) -> Result<()> {
2258 let db_path = get_database_path()?;
2259 let db = Database::new(&db_path)?;
2260
2261 let start_date = if let Some(from_str) = from {
2262 chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
2263 } else {
2264 match period.as_str() {
2265 "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
2266 "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
2267 _ => chrono::Local::now().date_naive(),
2268 }
2269 };
2270
2271 let insight_data = match period.as_str() {
2272 "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
2273 "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
2274 _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
2275 };
2276
2277 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2278 println!(
2279 "\x1b[36m│\x1b[0m \x1b[1;37m{} Summary\x1b[0m \x1b[36m│\x1b[0m",
2280 period
2281 );
2282 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2283 println!(
2284 "\x1b[36m│\x1b[0m Total Hours: \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2285 format!("{:.1}h", insight_data.total_hours)
2286 );
2287 println!(
2288 "\x1b[36m│\x1b[0m Sessions: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2289 insight_data.sessions_count
2290 );
2291 println!(
2292 "\x1b[36m│\x1b[0m Avg Session: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2293 format!("{:.1}h", insight_data.avg_session_duration)
2294 );
2295 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2296 Ok(())
2297}
2298
2299async fn compare_projects(
2300 projects: String,
2301 _from: Option<String>,
2302 _to: Option<String>,
2303) -> Result<()> {
2304 let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
2305
2306 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2307 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Comparison\x1b[0m \x1b[36m│\x1b[0m");
2308 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2309 println!(
2310 "\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2311 truncate_string(&projects, 27)
2312 );
2313 println!(
2314 "\x1b[36m│\x1b[0m \x1b[33m⚠ Comparison feature in development\x1b[0m \x1b[36m│\x1b[0m"
2315 );
2316 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2317 Ok(())
2318}
2319
2320async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
2321 match action {
2322 EstimateAction::Create {
2323 project,
2324 task,
2325 hours,
2326 due_date,
2327 } => create_estimate(project, task, hours, due_date).await,
2328 EstimateAction::Record { id, hours } => record_actual_time(id, hours).await,
2329 EstimateAction::List { project } => list_estimates(project).await,
2330 }
2331}
2332
2333async fn create_estimate(
2334 project: String,
2335 task: String,
2336 hours: f64,
2337 due_date: Option<String>,
2338) -> Result<()> {
2339 let db_path = get_database_path()?;
2340 let db = Database::new(&db_path)?;
2341
2342 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2343 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2344
2345 let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2346
2347 let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
2348 estimate.due_date = due;
2349
2350 let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
2351
2352 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2353 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimate Created\x1b[0m \x1b[36m│\x1b[0m");
2354 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2355 println!(
2356 "\x1b[36m│\x1b[0m Task: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2357 truncate_string(&task, 27)
2358 );
2359 println!(
2360 "\x1b[36m│\x1b[0m Estimate: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2361 format!("{} hours", hours)
2362 );
2363 println!(
2364 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2365 estimate_id
2366 );
2367 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2368 Ok(())
2369}
2370
2371async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
2372 let db_path = get_database_path()?;
2373 let db = Database::new(&db_path)?;
2374
2375 TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
2376 println!(
2377 "\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m",
2378 hours, id
2379 );
2380 Ok(())
2381}
2382
2383async fn list_estimates(project: String) -> Result<()> {
2384 let db_path = get_database_path()?;
2385 let db = Database::new(&db_path)?;
2386
2387 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2388 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2389
2390 let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2391
2392 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2393 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimates\x1b[0m \x1b[36m│\x1b[0m");
2394 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2395
2396 for est in &estimates {
2397 let variance = est.variance();
2398 let variance_str = if let Some(v) = variance {
2399 if v > 0.0 {
2400 format!("\x1b[31m+{:.1}h over\x1b[0m", v)
2401 } else {
2402 format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
2403 }
2404 } else {
2405 "N/A".to_string()
2406 };
2407
2408 println!(
2409 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2410 truncate_string(&est.task_name, 25)
2411 );
2412 let actual_str = est
2413 .actual_hours
2414 .map(|h| format!("{:.1}h", h))
2415 .unwrap_or_else(|| "N/A".to_string());
2416 println!(
2417 "\x1b[36m│\x1b[0m Est: {}h | Actual: {} | {} \x1b[36m│\x1b[0m",
2418 est.estimated_hours, actual_str, variance_str
2419 );
2420 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2421 }
2422
2423 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2424 Ok(())
2425}
2426
2427async fn handle_branch_action(action: BranchAction) -> Result<()> {
2428 match action {
2429 BranchAction::List { project } => list_branches(project).await,
2430 BranchAction::Stats { project, branch } => show_branch_stats(project, branch).await,
2431 }
2432}
2433
2434async fn list_branches(project: String) -> Result<()> {
2435 let db_path = get_database_path()?;
2436 let db = Database::new(&db_path)?;
2437
2438 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2439 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2440
2441 let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2442
2443 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2444 println!("\x1b[36m│\x1b[0m \x1b[1;37mGit Branches\x1b[0m \x1b[36m│\x1b[0m");
2445 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2446
2447 for branch in &branches {
2448 println!(
2449 "\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2450 truncate_string(&branch.branch_name, 25)
2451 );
2452 println!(
2453 "\x1b[36m│\x1b[0m Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2454 format!("{:.1}h", branch.total_hours())
2455 );
2456 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2457 }
2458
2459 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2460 Ok(())
2461}
2462
2463async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
2464 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2465 println!("\x1b[36m│\x1b[0m \x1b[1;37mBranch Statistics\x1b[0m \x1b[36m│\x1b[0m");
2466 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2467 println!(
2468 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2469 truncate_string(&project, 27)
2470 );
2471 if let Some(b) = branch {
2472 println!(
2473 "\x1b[36m│\x1b[0m Branch: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2474 truncate_string(&b, 27)
2475 );
2476 }
2477 println!(
2478 "\x1b[36m│\x1b[0m \x1b[33m⚠ Branch stats in development\x1b[0m \x1b[36m│\x1b[0m"
2479 );
2480 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2481 Ok(())
2482}
2483
2484async fn handle_template_action(action: TemplateAction) -> Result<()> {
2486 match action {
2487 TemplateAction::Create {
2488 name,
2489 description,
2490 tags,
2491 workspace_path,
2492 } => create_template(name, description, tags, workspace_path).await,
2493 TemplateAction::List => list_templates().await,
2494 TemplateAction::Delete { template } => delete_template(template).await,
2495 TemplateAction::Use {
2496 template,
2497 project_name,
2498 path,
2499 } => use_template(template, project_name, path).await,
2500 }
2501}
2502
2503async fn create_template(
2504 name: String,
2505 description: Option<String>,
2506 tags: Option<String>,
2507 workspace_path: Option<PathBuf>,
2508) -> Result<()> {
2509 let db_path = get_database_path()?;
2510 let db = Database::new(&db_path)?;
2511
2512 let default_tags = tags
2513 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2514 .unwrap_or_default();
2515
2516 let mut template = ProjectTemplate::new(name.clone()).with_tags(default_tags);
2517
2518 let desc_clone = description.clone();
2519 if let Some(desc) = description {
2520 template = template.with_description(desc);
2521 }
2522 if let Some(path) = workspace_path {
2523 template = template.with_workspace_path(path);
2524 }
2525
2526 let _template_id = TemplateQueries::create(&db.connection, &template)?;
2527
2528 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2529 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplate Created\x1b[0m \x1b[36m│\x1b[0m");
2530 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2531 println!(
2532 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2533 truncate_string(&name, 27)
2534 );
2535 if let Some(desc) = &desc_clone {
2536 println!(
2537 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2538 truncate_string(desc, 27)
2539 );
2540 }
2541 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2542 Ok(())
2543}
2544
2545async fn list_templates() -> Result<()> {
2546 let db_path = get_database_path()?;
2547 let db = Database::new(&db_path)?;
2548
2549 let templates = TemplateQueries::list_all(&db.connection)?;
2550
2551 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2552 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplates\x1b[0m \x1b[36m│\x1b[0m");
2553 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2554
2555 if templates.is_empty() {
2556 println!("\x1b[36m│\x1b[0m No templates found. \x1b[36m│\x1b[0m");
2557 } else {
2558 for template in &templates {
2559 println!(
2560 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2561 truncate_string(&template.name, 25)
2562 );
2563 if let Some(desc) = &template.description {
2564 println!(
2565 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2566 truncate_string(desc, 27)
2567 );
2568 }
2569 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2570 }
2571 }
2572
2573 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2574 Ok(())
2575}
2576
2577async fn delete_template(_template: String) -> Result<()> {
2578 println!("\x1b[33m⚠ Template deletion not yet implemented\x1b[0m");
2579 Ok(())
2580}
2581
2582async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2583 let db_path = get_database_path()?;
2584 let db = Database::new(&db_path)?;
2585
2586 let templates = TemplateQueries::list_all(&db.connection)?;
2587 let selected_template = templates
2588 .iter()
2589 .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2590 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2591
2592 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2594 let canonical_path = canonicalize_path(&project_path)?;
2595
2596 if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2598 return Err(anyhow::anyhow!("Project already exists at this path"));
2599 }
2600
2601 let git_hash = if is_git_repository(&canonical_path) {
2602 get_git_hash(&canonical_path)
2603 } else {
2604 None
2605 };
2606
2607 let template_desc = selected_template.description.clone();
2608 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2609 .with_git_hash(git_hash)
2610 .with_description(template_desc);
2611
2612 let project_id = ProjectQueries::create(&db.connection, &project)?;
2613 project.id = Some(project_id);
2614
2615 for goal_def in &selected_template.default_goals {
2620 let mut goal =
2621 Goal::new(goal_def.name.clone(), goal_def.target_hours).with_project(project_id);
2622 if let Some(desc) = &goal_def.description {
2623 goal = goal.with_description(desc.clone());
2624 }
2625 GoalQueries::create(&db.connection, &goal)?;
2626 }
2627
2628 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2629 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Created from Template\x1b[0m \x1b[36m│\x1b[0m");
2630 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2631 println!(
2632 "\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2633 truncate_string(&selected_template.name, 27)
2634 );
2635 println!(
2636 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2637 truncate_string(&project_name, 27)
2638 );
2639 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2640 Ok(())
2641}
2642
2643async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2645 match action {
2646 WorkspaceAction::Create {
2647 name,
2648 description,
2649 path,
2650 } => create_workspace(name, description, path).await,
2651 WorkspaceAction::List => list_workspaces().await,
2652 WorkspaceAction::AddProject { workspace, project } => {
2653 add_project_to_workspace(workspace, project).await
2654 }
2655 WorkspaceAction::RemoveProject { workspace, project } => {
2656 remove_project_from_workspace(workspace, project).await
2657 }
2658 WorkspaceAction::Projects { workspace } => list_workspace_projects(workspace).await,
2659 WorkspaceAction::Delete { workspace } => delete_workspace(workspace).await,
2660 }
2661}
2662
2663async fn create_workspace(
2664 name: String,
2665 description: Option<String>,
2666 path: Option<PathBuf>,
2667) -> Result<()> {
2668 let db_path = get_database_path()?;
2669 let db = Database::new(&db_path)?;
2670
2671 let mut workspace = Workspace::new(name.clone());
2672 let desc_clone = description.clone();
2673 if let Some(desc) = description {
2674 workspace = workspace.with_description(desc);
2675 }
2676 if let Some(p) = path {
2677 workspace = workspace.with_path(p);
2678 }
2679
2680 let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2681
2682 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2683 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Created\x1b[0m \x1b[36m│\x1b[0m");
2684 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2685 println!(
2686 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2687 truncate_string(&name, 27)
2688 );
2689 if let Some(desc) = &desc_clone {
2690 println!(
2691 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2692 truncate_string(desc, 27)
2693 );
2694 }
2695 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2696 Ok(())
2697}
2698
2699async fn list_workspaces() -> Result<()> {
2700 let db_path = get_database_path()?;
2701 let db = Database::new(&db_path)?;
2702
2703 let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2704
2705 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2706 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspaces\x1b[0m \x1b[36m│\x1b[0m");
2707 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2708
2709 if workspaces.is_empty() {
2710 println!("\x1b[36m│\x1b[0m No workspaces found. \x1b[36m│\x1b[0m");
2711 } else {
2712 for workspace in &workspaces {
2713 println!(
2714 "\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2715 truncate_string(&workspace.name, 25)
2716 );
2717 if let Some(desc) = &workspace.description {
2718 println!(
2719 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2720 truncate_string(desc, 27)
2721 );
2722 }
2723 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2724 }
2725 }
2726
2727 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2728 Ok(())
2729}
2730
2731async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2732 let db_path = get_database_path()?;
2733 let db = Database::new(&db_path)?;
2734
2735 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2737 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2738
2739 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2741 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2742
2743 let workspace_id = workspace_obj
2744 .id
2745 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2746 let project_id = project_obj
2747 .id
2748 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2749
2750 if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2751 println!(
2752 "\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'",
2753 project, workspace
2754 );
2755 } else {
2756 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2757 }
2758
2759 Ok(())
2760}
2761
2762async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2763 let db_path = get_database_path()?;
2764 let db = Database::new(&db_path)?;
2765
2766 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2768 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2769
2770 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2772 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2773
2774 let workspace_id = workspace_obj
2775 .id
2776 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2777 let project_id = project_obj
2778 .id
2779 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2780
2781 if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2782 println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2783 } else {
2784 println!(
2785 "\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'",
2786 project, workspace
2787 );
2788 }
2789
2790 Ok(())
2791}
2792
2793async fn list_workspace_projects(workspace: String) -> Result<()> {
2794 let db_path = get_database_path()?;
2795 let db = Database::new(&db_path)?;
2796
2797 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2799 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2800
2801 let workspace_id = workspace_obj
2802 .id
2803 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2804 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2805
2806 if projects.is_empty() {
2807 println!(
2808 "\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'",
2809 workspace
2810 );
2811 return Ok(());
2812 }
2813
2814 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2815 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Projects\x1b[0m \x1b[36m│\x1b[0m");
2816 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2817 println!(
2818 "\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2819 truncate_string(&workspace, 25)
2820 );
2821 println!(
2822 "\x1b[36m│\x1b[0m Projects: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2823 format!("{} projects", projects.len())
2824 );
2825 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2826
2827 for project in &projects {
2828 let status_indicator = if !project.is_archived {
2829 "\x1b[32m●\x1b[0m"
2830 } else {
2831 "\x1b[31m○\x1b[0m"
2832 };
2833 println!(
2834 "\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2835 status_indicator,
2836 truncate_string(&project.name, 33)
2837 );
2838 }
2839
2840 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2841 Ok(())
2842}
2843
2844async fn delete_workspace(workspace: String) -> Result<()> {
2845 let db_path = get_database_path()?;
2846 let db = Database::new(&db_path)?;
2847
2848 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2850 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2851
2852 let workspace_id = workspace_obj
2853 .id
2854 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2855
2856 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2858 if !projects.is_empty() {
2859 println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.",
2860 workspace, projects.len());
2861 return Ok(());
2862 }
2863
2864 if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2865 println!(
2866 "\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'",
2867 workspace
2868 );
2869 } else {
2870 println!(
2871 "\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'",
2872 workspace
2873 );
2874 }
2875
2876 Ok(())
2877}
2878
2879async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2881 match action {
2882 CalendarAction::Add {
2883 name,
2884 start,
2885 end,
2886 event_type,
2887 project,
2888 description,
2889 } => add_calendar_event(name, start, end, event_type, project, description).await,
2890 CalendarAction::List { from, to, project } => list_calendar_events(from, to, project).await,
2891 CalendarAction::Delete { id } => delete_calendar_event(id).await,
2892 }
2893}
2894
2895async fn add_calendar_event(
2896 _name: String,
2897 _start: String,
2898 _end: Option<String>,
2899 _event_type: Option<String>,
2900 _project: Option<String>,
2901 _description: Option<String>,
2902) -> Result<()> {
2903 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2904 Ok(())
2905}
2906
2907async fn list_calendar_events(
2908 _from: Option<String>,
2909 _to: Option<String>,
2910 _project: Option<String>,
2911) -> Result<()> {
2912 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2913 Ok(())
2914}
2915
2916async fn delete_calendar_event(_id: i64) -> Result<()> {
2917 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2918 Ok(())
2919}
2920
2921async fn handle_issue_action(action: IssueAction) -> Result<()> {
2923 match action {
2924 IssueAction::Sync {
2925 project,
2926 tracker_type,
2927 } => sync_issues(project, tracker_type).await,
2928 IssueAction::List { project, status } => list_issues(project, status).await,
2929 IssueAction::Link {
2930 session_id,
2931 issue_id,
2932 } => link_session_to_issue(session_id, issue_id).await,
2933 }
2934}
2935
2936async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2937 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2938 Ok(())
2939}
2940
2941async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2942 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2943 Ok(())
2944}
2945
2946async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2947 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2948 Ok(())
2949}
2950
2951async fn handle_client_action(action: ClientAction) -> Result<()> {
2953 match action {
2954 ClientAction::Generate {
2955 client,
2956 from,
2957 to,
2958 projects,
2959 format,
2960 } => generate_client_report(client, from, to, projects, format).await,
2961 ClientAction::List { client } => list_client_reports(client).await,
2962 ClientAction::View { id } => view_client_report(id).await,
2963 }
2964}
2965
2966async fn generate_client_report(
2967 _client: String,
2968 _from: String,
2969 _to: String,
2970 _projects: Option<String>,
2971 _format: Option<String>,
2972) -> Result<()> {
2973 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2974 Ok(())
2975}
2976
2977async fn list_client_reports(_client: Option<String>) -> Result<()> {
2978 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2979 Ok(())
2980}
2981
2982async fn view_client_report(_id: i64) -> Result<()> {
2983 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2984 Ok(())
2985}
2986
2987fn should_quit(event: crossterm::event::Event) -> bool {
2988 match event {
2989 crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
2990 matches!(
2991 key.code,
2992 crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc
2993 )
2994 }
2995 _ => false,
2996 }
2997}
2998
2999async fn init_project_with_db(
3001 name: Option<String>,
3002 canonical_path: Option<PathBuf>,
3003 description: Option<String>,
3004 conn: &rusqlite::Connection,
3005) -> Result<()> {
3006 let canonical_path =
3007 canonical_path.ok_or_else(|| anyhow::anyhow!("Canonical path required"))?;
3008 let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
3009
3010 if let Some(existing) = ProjectQueries::find_by_path(conn, &canonical_path)? {
3012 println!(
3013 "\x1b[33m⚠ Project already exists:\x1b[0m {}",
3014 existing.name
3015 );
3016 return Ok(());
3017 }
3018
3019 let git_hash = if is_git_repository(&canonical_path) {
3021 get_git_hash(&canonical_path)
3022 } else {
3023 None
3024 };
3025
3026 let mut project = Project::new(project_name.clone(), canonical_path.clone())
3028 .with_git_hash(git_hash.clone())
3029 .with_description(description.clone());
3030
3031 let project_id = ProjectQueries::create(conn, &project)?;
3033 project.id = Some(project_id);
3034
3035 let marker_path = canonical_path.join(".tempo");
3037 if !marker_path.exists() {
3038 std::fs::write(
3039 &marker_path,
3040 format!("# Tempo time tracking project\nname: {}\n", project_name),
3041 )?;
3042 }
3043
3044 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
3045 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Initialized\x1b[0m \x1b[36m│\x1b[0m");
3046 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
3047 println!(
3048 "\x1b[36m│\x1b[0m Name: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3049 truncate_string(&project_name, 25)
3050 );
3051 println!(
3052 "\x1b[36m│\x1b[0m Path: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3053 truncate_string(&canonical_path.display().to_string(), 25)
3054 );
3055
3056 if let Some(desc) = &description {
3057 println!(
3058 "\x1b[36m│\x1b[0m Description: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3059 truncate_string(desc, 25)
3060 );
3061 }
3062
3063 if is_git_repository(&canonical_path) {
3064 println!(
3065 "\x1b[36m│\x1b[0m Git: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3066 "Repository detected"
3067 );
3068 if let Some(hash) = &git_hash {
3069 println!(
3070 "\x1b[36m│\x1b[0m Git Hash: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3071 truncate_string(hash, 25)
3072 );
3073 }
3074 }
3075
3076 println!(
3077 "\x1b[36m│\x1b[0m ID: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3078 project_id
3079 );
3080 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
3081
3082 Ok(())
3083}
3084
3085async fn show_pool_stats() -> Result<()> {
3087 match get_pool_stats() {
3088 Ok(stats) => {
3089 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
3090 println!("\x1b[36m│\x1b[0m \x1b[1;37mDatabase Pool Statistics\x1b[0m \x1b[36m│\x1b[0m");
3091 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
3092 println!(
3093 "\x1b[36m│\x1b[0m Total Created: \x1b[32m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3094 stats.total_connections_created
3095 );
3096 println!(
3097 "\x1b[36m│\x1b[0m Active: \x1b[33m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3098 stats.active_connections
3099 );
3100 println!(
3101 "\x1b[36m│\x1b[0m Available in Pool:\x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3102 stats.connections_in_pool
3103 );
3104 println!(
3105 "\x1b[36m│\x1b[0m Total Requests: \x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3106 stats.connection_requests
3107 );
3108 println!(
3109 "\x1b[36m│\x1b[0m Timeouts: \x1b[31m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3110 stats.connection_timeouts
3111 );
3112 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
3113 }
3114 Err(_) => {
3115 println!("\x1b[33m⚠ Database pool not initialized or not available\x1b[0m");
3116 println!(" Using direct database connections as fallback");
3117 }
3118 }
3119 Ok(())
3120}
3121
3122#[derive(Deserialize)]
3123struct GitHubRelease {
3124 tag_name: String,
3125 name: String,
3126 body: String,
3127 published_at: String,
3128 prerelease: bool,
3129}
3130
3131async fn handle_update(check: bool, force: bool, verbose: bool) -> Result<()> {
3132 let current_version = env!("CARGO_PKG_VERSION");
3133
3134 if verbose {
3135 println!("🔍 Current version: v{}", current_version);
3136 println!("📡 Checking for updates...");
3137 } else {
3138 println!("🔍 Checking for updates...");
3139 }
3140
3141 let client = reqwest::Client::new();
3143 let response = client
3144 .get("https://api.github.com/repos/own-path/vibe/releases/latest")
3145 .header("User-Agent", format!("tempo-cli/{}", current_version))
3146 .send()
3147 .await
3148 .context("Failed to fetch release information")?;
3149
3150 if !response.status().is_success() {
3151 return Err(anyhow::anyhow!(
3152 "Failed to fetch release information: HTTP {}",
3153 response.status()
3154 ));
3155 }
3156
3157 let release: GitHubRelease = response
3158 .json()
3159 .await
3160 .context("Failed to parse release information")?;
3161
3162 let latest_version = release.tag_name.trim_start_matches('v');
3163
3164 if verbose {
3165 println!("📦 Latest version: v{}", latest_version);
3166 println!("📅 Released: {}", release.published_at);
3167 }
3168
3169 let current_semver = semver::Version::parse(current_version)
3171 .context("Failed to parse current version")?;
3172 let latest_semver = semver::Version::parse(latest_version)
3173 .context("Failed to parse latest version")?;
3174
3175 if current_semver >= latest_semver && !force {
3176 println!("✅ You're already running the latest version (v{})", current_version);
3177 if check {
3178 return Ok(());
3179 }
3180
3181 if !force {
3182 println!("💡 Use --force to reinstall the current version");
3183 return Ok(());
3184 }
3185 }
3186
3187 if check {
3188 if current_semver < latest_semver {
3189 println!("📦 Update available: v{} → v{}", current_version, latest_version);
3190 println!("🔗 Run `tempo update` to install the latest version");
3191
3192 if verbose && !release.body.is_empty() {
3193 println!("\n📝 Release Notes:");
3194 println!("{}", release.body);
3195 }
3196 }
3197 return Ok(());
3198 }
3199
3200 if current_semver < latest_semver || force {
3201 println!("⬇️ Updating tempo from v{} to v{}", current_version, latest_version);
3202
3203 if verbose {
3204 println!("🔧 Installing via cargo...");
3205 }
3206
3207 let mut cmd = Command::new("cargo");
3209 cmd.args(&["install", "tempo-cli", "--force"]);
3210
3211 if verbose {
3212 cmd.stdout(Stdio::inherit())
3213 .stderr(Stdio::inherit());
3214 } else {
3215 cmd.stdout(Stdio::null())
3216 .stderr(Stdio::null());
3217 }
3218
3219 let status = cmd.status()
3220 .context("Failed to run cargo install command")?;
3221
3222 if status.success() {
3223 println!("✅ Successfully updated tempo to v{}", latest_version);
3224 println!("🎉 You can now use the latest features!");
3225
3226 if !release.body.is_empty() && verbose {
3227 println!("\n📝 What's new in v{}:", latest_version);
3228 println!("{}", release.body);
3229 }
3230 } else {
3231 return Err(anyhow::anyhow!("Failed to install update. Try running manually: cargo install tempo-cli --force"));
3232 }
3233 }
3234
3235 Ok(())
3236}