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
1647#[allow(dead_code)]
1649async fn bulk_update_sessions_project(
1650 session_ids: Vec<i64>,
1651 new_project_name: String,
1652) -> Result<()> {
1653 let db_path = get_database_path()?;
1654 let db = Database::new(&db_path)?;
1655
1656 let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1658 Some(p) => p,
1659 None => {
1660 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1661 return Ok(());
1662 }
1663 };
1664
1665 let updated =
1666 SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1667
1668 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1669 println!(
1670 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Update\x1b[0m \x1b[36m│\x1b[0m"
1671 );
1672 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1673 println!(
1674 "\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1675 updated
1676 );
1677 println!(
1678 "\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1679 truncate_string(&new_project_name, 27)
1680 );
1681 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1682 println!(
1683 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m",
1684 updated, ""
1685 );
1686 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1687
1688 Ok(())
1689}
1690
1691#[allow(dead_code)]
1692async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1693 let db_path = get_database_path()?;
1694 let db = Database::new(&db_path)?;
1695
1696 let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1697
1698 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1699 println!(
1700 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Delete\x1b[0m \x1b[36m│\x1b[0m"
1701 );
1702 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1703 println!(
1704 "\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1705 session_ids.len()
1706 );
1707 println!(
1708 "\x1b[36m│\x1b[0m Deleted: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1709 deleted
1710 );
1711 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1712 println!(
1713 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m",
1714 deleted, ""
1715 );
1716 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1717
1718 Ok(())
1719}
1720
1721async fn launch_dashboard() -> Result<()> {
1722 if !is_tty() {
1724 return show_dashboard_fallback().await;
1725 }
1726
1727 enable_raw_mode()
1729 .context("Failed to enable raw mode - terminal may not support interactive features")?;
1730 let mut stdout = io::stdout();
1731
1732 execute!(stdout, EnterAlternateScreen)
1733 .context("Failed to enter alternate screen - terminal may not support full-screen mode")?;
1734
1735 let backend = CrosstermBackend::new(stdout);
1736 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal backend")?;
1737
1738 terminal.clear().context("Failed to clear terminal")?;
1740
1741 let result = async {
1743 let mut dashboard = Dashboard::new().await?;
1744 dashboard.run(&mut terminal).await
1745 };
1746
1747 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1748
1749 let cleanup_result = cleanup_terminal(&mut terminal);
1751
1752 if let Err(e) = cleanup_result {
1754 eprintln!("Warning: Failed to restore terminal: {}", e);
1755 }
1756
1757 result
1758}
1759
1760fn is_tty() -> bool {
1761 use std::os::unix::io::AsRawFd;
1762 unsafe { libc::isatty(std::io::stdin().as_raw_fd()) == 1 }
1763}
1764
1765async fn show_dashboard_fallback() -> Result<()> {
1766 println!("📊 Tempo Dashboard (Text Mode)");
1767 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1768 println!();
1769
1770 if is_daemon_running() {
1772 println!("🟢 Daemon Status: Running");
1773 } else {
1774 println!("🔴 Daemon Status: Offline");
1775 println!(" Start with: tempo start");
1776 println!();
1777 return Ok(());
1778 }
1779
1780 let socket_path = get_socket_path()?;
1782 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
1783 match client.send_message(&IpcMessage::GetActiveSession).await {
1784 Ok(IpcResponse::ActiveSession(Some(session))) => {
1785 println!("⏱️ Active Session:");
1786 println!(" Started: {}", session.start_time.format("%H:%M:%S"));
1787 println!(
1788 " Duration: {}",
1789 format_duration_simple(
1790 (chrono::Utc::now().timestamp() - session.start_time.timestamp())
1791 - session.paused_duration.num_seconds()
1792 )
1793 );
1794 println!(" Context: {}", session.context);
1795 println!();
1796
1797 match client
1799 .send_message(&IpcMessage::GetProject(session.project_id))
1800 .await
1801 {
1802 Ok(IpcResponse::Project(Some(project))) => {
1803 println!("📁 Current Project: {}", project.name);
1804 println!(" Path: {}", project.path.display());
1805 println!();
1806 }
1807 _ => {
1808 println!("📁 Project: Unknown");
1809 println!();
1810 }
1811 }
1812 }
1813 _ => {
1814 println!("⏸️ No active session");
1815 println!(" Start tracking with: tempo session start");
1816 println!();
1817 }
1818 }
1819
1820 let today = chrono::Local::now().date_naive();
1822 match client.send_message(&IpcMessage::GetDailyStats(today)).await {
1823 Ok(IpcResponse::DailyStats {
1824 sessions_count,
1825 total_seconds,
1826 avg_seconds,
1827 }) => {
1828 println!("📈 Today's Summary:");
1829 println!(" Sessions: {}", sessions_count);
1830 println!(" Total time: {}", format_duration_simple(total_seconds));
1831 if sessions_count > 0 {
1832 println!(
1833 " Average session: {}",
1834 format_duration_simple(avg_seconds)
1835 );
1836 }
1837 let progress = (total_seconds as f64 / (8.0 * 3600.0)) * 100.0;
1838 println!(" Daily goal (8h): {:.1}%", progress);
1839 println!();
1840 }
1841 _ => {
1842 println!("📈 Today's Summary: No data available");
1843 println!();
1844 }
1845 }
1846 } else {
1847 println!("❌ Unable to connect to daemon");
1848 println!(" Try: tempo restart");
1849 println!();
1850 }
1851
1852 println!("💡 For interactive dashboard, run in a terminal:");
1853 println!(" • Terminal.app, iTerm2, or other terminal emulators");
1854 println!(" • SSH sessions with TTY allocation (ssh -t)");
1855 println!(" • Interactive shell environments");
1856
1857 Ok(())
1858}
1859
1860fn format_duration_simple(seconds: i64) -> String {
1861 let hours = seconds / 3600;
1862 let minutes = (seconds % 3600) / 60;
1863 let secs = seconds % 60;
1864
1865 if hours > 0 {
1866 format!("{}h {}m {}s", hours, minutes, secs)
1867 } else if minutes > 0 {
1868 format!("{}m {}s", minutes, secs)
1869 } else {
1870 format!("{}s", secs)
1871 }
1872}
1873
1874fn cleanup_terminal<B>(terminal: &mut Terminal<B>) -> Result<()>
1875where
1876 B: ratatui::backend::Backend + std::io::Write,
1877{
1878 disable_raw_mode().context("Failed to disable raw mode")?;
1880 execute!(terminal.backend_mut(), LeaveAlternateScreen)
1881 .context("Failed to leave alternate screen")?;
1882 terminal.show_cursor().context("Failed to show cursor")?;
1883 Ok(())
1884}
1885
1886async fn launch_timer() -> Result<()> {
1887 if !is_tty() {
1889 return Err(anyhow::anyhow!(
1890 "Interactive timer requires an interactive terminal (TTY).\n\
1891 \n\
1892 This command needs to run in a proper terminal environment.\n\
1893 Try running this command directly in your terminal application."
1894 ));
1895 }
1896
1897 enable_raw_mode().context("Failed to enable raw mode")?;
1899 let mut stdout = io::stdout();
1900 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
1901 let backend = CrosstermBackend::new(stdout);
1902 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
1903 terminal.clear().context("Failed to clear terminal")?;
1904
1905 let result = async {
1907 let mut timer = InteractiveTimer::new().await?;
1908 timer.run(&mut terminal).await
1909 };
1910
1911 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1912
1913 let cleanup_result = cleanup_terminal(&mut terminal);
1915 if let Err(e) = cleanup_result {
1916 eprintln!("Warning: Failed to restore terminal: {}", e);
1917 }
1918
1919 result
1920}
1921
1922async fn merge_sessions(
1923 session_ids_str: String,
1924 project_name: Option<String>,
1925 notes: Option<String>,
1926) -> Result<()> {
1927 let session_ids: Result<Vec<i64>, _> = session_ids_str
1929 .split(',')
1930 .map(|s| s.trim().parse::<i64>())
1931 .collect();
1932
1933 let session_ids = session_ids.map_err(|_| {
1934 anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'")
1935 })?;
1936
1937 if session_ids.len() < 2 {
1938 return Err(anyhow::anyhow!(
1939 "At least 2 sessions are required for merging"
1940 ));
1941 }
1942
1943 let mut target_project_id = None;
1945 if let Some(project) = project_name {
1946 let db_path = get_database_path()?;
1947 let db = Database::new(&db_path)?;
1948
1949 if let Ok(project_id) = project.parse::<i64>() {
1951 if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1952 target_project_id = Some(project_id);
1953 }
1954 } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1955 target_project_id = proj.id;
1956 }
1957
1958 if target_project_id.is_none() {
1959 return Err(anyhow::anyhow!("Project '{}' not found", project));
1960 }
1961 }
1962
1963 let db_path = get_database_path()?;
1965 let db = Database::new(&db_path)?;
1966
1967 let merged_id =
1968 SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1969
1970 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1971 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Merge Complete\x1b[0m \x1b[36m│\x1b[0m");
1972 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1973 println!(
1974 "\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1975 session_ids
1976 .iter()
1977 .map(|id| id.to_string())
1978 .collect::<Vec<_>>()
1979 .join(", ")
1980 );
1981 println!(
1982 "\x1b[36m│\x1b[0m New session ID: \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1983 merged_id
1984 );
1985 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1986 println!(
1987 "\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m \x1b[36m│\x1b[0m"
1988 );
1989 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1990
1991 Ok(())
1992}
1993
1994async fn split_session(
1995 session_id: i64,
1996 split_times_str: String,
1997 notes: Option<String>,
1998) -> Result<()> {
1999 let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
2001 let mut split_times = Vec::new();
2002
2003 for time_str in split_time_strings {
2004 let datetime = if time_str.contains(':') {
2006 let today = chrono::Local::now().date_naive();
2008 let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
2009 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
2010 .map_err(|_| {
2011 anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str)
2012 })?;
2013 today.and_time(time).and_utc()
2014 } else {
2015 chrono::DateTime::parse_from_rfc3339(time_str)
2017 .map_err(|_| {
2018 anyhow::anyhow!(
2019 "Invalid datetime format '{}'. Use HH:MM or RFC3339 format",
2020 time_str
2021 )
2022 })?
2023 .to_utc()
2024 };
2025
2026 split_times.push(datetime);
2027 }
2028
2029 if split_times.is_empty() {
2030 return Err(anyhow::anyhow!("No valid split times provided"));
2031 }
2032
2033 let notes_list = notes.map(|n| {
2035 n.split(',')
2036 .map(|s| s.trim().to_string())
2037 .collect::<Vec<String>>()
2038 });
2039
2040 let db_path = get_database_path()?;
2042 let db = Database::new(&db_path)?;
2043
2044 let new_session_ids =
2045 SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
2046
2047 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2048 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Split Complete\x1b[0m \x1b[36m│\x1b[0m");
2049 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2050 println!(
2051 "\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2052 session_id
2053 );
2054 println!(
2055 "\x1b[36m│\x1b[0m Split points: \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2056 split_times.len()
2057 );
2058 println!(
2059 "\x1b[36m│\x1b[0m New sessions: \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
2060 new_session_ids
2061 .iter()
2062 .map(|id| id.to_string())
2063 .collect::<Vec<_>>()
2064 .join(", ")
2065 );
2066 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2067 println!(
2068 "\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m \x1b[36m│\x1b[0m"
2069 );
2070 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2071
2072 Ok(())
2073}
2074
2075async fn launch_history() -> Result<()> {
2076 if !is_tty() {
2078 return Err(anyhow::anyhow!(
2079 "Session history browser requires an interactive terminal (TTY).\n\
2080 \n\
2081 This command needs to run in a proper terminal environment.\n\
2082 Try running this command directly in your terminal application."
2083 ));
2084 }
2085
2086 enable_raw_mode().context("Failed to enable raw mode")?;
2088 let mut stdout = io::stdout();
2089 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
2090 let backend = CrosstermBackend::new(stdout);
2091 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
2092 terminal.clear().context("Failed to clear terminal")?;
2093
2094 let result = async {
2095 let mut browser = SessionHistoryBrowser::new().await?;
2096 browser.run(&mut terminal).await
2097 };
2098
2099 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
2100
2101 let cleanup_result = cleanup_terminal(&mut terminal);
2103 if let Err(e) = cleanup_result {
2104 eprintln!("Warning: Failed to restore terminal: {}", e);
2105 }
2106
2107 result
2108}
2109
2110async fn handle_goal_action(action: GoalAction) -> Result<()> {
2111 match action {
2112 GoalAction::Create {
2113 name,
2114 target_hours,
2115 project,
2116 description,
2117 start_date,
2118 end_date,
2119 } => {
2120 create_goal(
2121 name,
2122 target_hours,
2123 project,
2124 description,
2125 start_date,
2126 end_date,
2127 )
2128 .await
2129 }
2130 GoalAction::List { project } => list_goals(project).await,
2131 GoalAction::Update { id, hours } => update_goal_progress(id, hours).await,
2132 }
2133}
2134
2135async fn create_goal(
2136 name: String,
2137 target_hours: f64,
2138 project: Option<String>,
2139 description: Option<String>,
2140 start_date: Option<String>,
2141 end_date: Option<String>,
2142) -> Result<()> {
2143 let db_path = get_database_path()?;
2144 let db = Database::new(&db_path)?;
2145
2146 let project_id = if let Some(proj_name) = project {
2147 match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
2148 Some(p) => p.id,
2149 None => {
2150 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2151 return Ok(());
2152 }
2153 }
2154 } else {
2155 None
2156 };
2157
2158 let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2159 let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2160
2161 let mut goal = Goal::new(name.clone(), target_hours);
2162 if let Some(pid) = project_id {
2163 goal = goal.with_project(pid);
2164 }
2165 if let Some(desc) = description {
2166 goal = goal.with_description(desc);
2167 }
2168 goal = goal.with_dates(start, end);
2169
2170 goal.validate()?;
2171 let goal_id = GoalQueries::create(&db.connection, &goal)?;
2172
2173 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2174 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoal Created\x1b[0m \x1b[36m│\x1b[0m");
2175 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2176 println!(
2177 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2178 truncate_string(&name, 27)
2179 );
2180 println!(
2181 "\x1b[36m│\x1b[0m Target: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2182 format!("{} hours", target_hours)
2183 );
2184 println!(
2185 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2186 goal_id
2187 );
2188 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2189 println!(
2190 "\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m \x1b[36m│\x1b[0m"
2191 );
2192 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2193
2194 Ok(())
2195}
2196
2197async fn list_goals(project: Option<String>) -> Result<()> {
2198 let db_path = get_database_path()?;
2199 let db = Database::new(&db_path)?;
2200
2201 let project_id = if let Some(proj_name) = &project {
2202 match ProjectQueries::find_by_name(&db.connection, proj_name)? {
2203 Some(p) => p.id,
2204 None => {
2205 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2206 return Ok(());
2207 }
2208 }
2209 } else {
2210 None
2211 };
2212
2213 let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
2214
2215 if goals.is_empty() {
2216 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2217 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Goals\x1b[0m \x1b[36m│\x1b[0m");
2218 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2219 return Ok(());
2220 }
2221
2222 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2223 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoals\x1b[0m \x1b[36m│\x1b[0m");
2224 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2225
2226 for goal in &goals {
2227 let progress_pct = goal.progress_percentage();
2228 println!(
2229 "\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2230 truncate_string(&goal.name, 25)
2231 );
2232 println!("\x1b[36m│\x1b[0m Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h) \x1b[36m│\x1b[0m",
2233 progress_pct, goal.current_progress, goal.target_hours);
2234 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2235 }
2236
2237 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2238 Ok(())
2239}
2240
2241async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
2242 let db_path = get_database_path()?;
2243 let db = Database::new(&db_path)?;
2244
2245 GoalQueries::update_progress(&db.connection, id, hours)?;
2246 println!(
2247 "\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m",
2248 id, hours
2249 );
2250 Ok(())
2251}
2252
2253async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
2254 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2255 println!("\x1b[36m│\x1b[0m \x1b[1;37mProductivity Insights\x1b[0m \x1b[36m│\x1b[0m");
2256 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2257 println!(
2258 "\x1b[36m│\x1b[0m Period: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2259 period.as_deref().unwrap_or("all")
2260 );
2261 if let Some(proj) = project {
2262 println!(
2263 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2264 truncate_string(&proj, 27)
2265 );
2266 }
2267 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2268 println!(
2269 "\x1b[36m│\x1b[0m \x1b[33m⚠ Insights calculation in progress...\x1b[0m \x1b[36m│\x1b[0m"
2270 );
2271 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2272 Ok(())
2273}
2274
2275async fn show_summary(period: String, from: Option<String>) -> Result<()> {
2276 let db_path = get_database_path()?;
2277 let db = Database::new(&db_path)?;
2278
2279 let start_date = if let Some(from_str) = from {
2280 chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
2281 } else {
2282 match period.as_str() {
2283 "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
2284 "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
2285 _ => chrono::Local::now().date_naive(),
2286 }
2287 };
2288
2289 let insight_data = match period.as_str() {
2290 "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
2291 "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
2292 _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
2293 };
2294
2295 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2296 println!(
2297 "\x1b[36m│\x1b[0m \x1b[1;37m{} Summary\x1b[0m \x1b[36m│\x1b[0m",
2298 period
2299 );
2300 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2301 println!(
2302 "\x1b[36m│\x1b[0m Total Hours: \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2303 format!("{:.1}h", insight_data.total_hours)
2304 );
2305 println!(
2306 "\x1b[36m│\x1b[0m Sessions: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2307 insight_data.sessions_count
2308 );
2309 println!(
2310 "\x1b[36m│\x1b[0m Avg Session: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2311 format!("{:.1}h", insight_data.avg_session_duration)
2312 );
2313 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2314 Ok(())
2315}
2316
2317async fn compare_projects(
2318 projects: String,
2319 _from: Option<String>,
2320 _to: Option<String>,
2321) -> Result<()> {
2322 let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
2323
2324 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2325 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Comparison\x1b[0m \x1b[36m│\x1b[0m");
2326 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2327 println!(
2328 "\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2329 truncate_string(&projects, 27)
2330 );
2331 println!(
2332 "\x1b[36m│\x1b[0m \x1b[33m⚠ Comparison feature in development\x1b[0m \x1b[36m│\x1b[0m"
2333 );
2334 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2335 Ok(())
2336}
2337
2338async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
2339 match action {
2340 EstimateAction::Create {
2341 project,
2342 task,
2343 hours,
2344 due_date,
2345 } => create_estimate(project, task, hours, due_date).await,
2346 EstimateAction::Record { id, hours } => record_actual_time(id, hours).await,
2347 EstimateAction::List { project } => list_estimates(project).await,
2348 }
2349}
2350
2351async fn create_estimate(
2352 project: String,
2353 task: String,
2354 hours: f64,
2355 due_date: Option<String>,
2356) -> Result<()> {
2357 let db_path = get_database_path()?;
2358 let db = Database::new(&db_path)?;
2359
2360 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2361 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2362
2363 let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2364
2365 let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
2366 estimate.due_date = due;
2367
2368 let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
2369
2370 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2371 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimate Created\x1b[0m \x1b[36m│\x1b[0m");
2372 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2373 println!(
2374 "\x1b[36m│\x1b[0m Task: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2375 truncate_string(&task, 27)
2376 );
2377 println!(
2378 "\x1b[36m│\x1b[0m Estimate: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2379 format!("{} hours", hours)
2380 );
2381 println!(
2382 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2383 estimate_id
2384 );
2385 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2386 Ok(())
2387}
2388
2389async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
2390 let db_path = get_database_path()?;
2391 let db = Database::new(&db_path)?;
2392
2393 TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
2394 println!(
2395 "\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m",
2396 hours, id
2397 );
2398 Ok(())
2399}
2400
2401async fn list_estimates(project: String) -> Result<()> {
2402 let db_path = get_database_path()?;
2403 let db = Database::new(&db_path)?;
2404
2405 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2406 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2407
2408 let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2409
2410 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2411 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimates\x1b[0m \x1b[36m│\x1b[0m");
2412 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2413
2414 for est in &estimates {
2415 let variance = est.variance();
2416 let variance_str = if let Some(v) = variance {
2417 if v > 0.0 {
2418 format!("\x1b[31m+{:.1}h over\x1b[0m", v)
2419 } else {
2420 format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
2421 }
2422 } else {
2423 "N/A".to_string()
2424 };
2425
2426 println!(
2427 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2428 truncate_string(&est.task_name, 25)
2429 );
2430 let actual_str = est
2431 .actual_hours
2432 .map(|h| format!("{:.1}h", h))
2433 .unwrap_or_else(|| "N/A".to_string());
2434 println!(
2435 "\x1b[36m│\x1b[0m Est: {}h | Actual: {} | {} \x1b[36m│\x1b[0m",
2436 est.estimated_hours, actual_str, variance_str
2437 );
2438 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2439 }
2440
2441 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2442 Ok(())
2443}
2444
2445async fn handle_branch_action(action: BranchAction) -> Result<()> {
2446 match action {
2447 BranchAction::List { project } => list_branches(project).await,
2448 BranchAction::Stats { project, branch } => show_branch_stats(project, branch).await,
2449 }
2450}
2451
2452async fn list_branches(project: String) -> Result<()> {
2453 let db_path = get_database_path()?;
2454 let db = Database::new(&db_path)?;
2455
2456 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2457 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2458
2459 let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2460
2461 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2462 println!("\x1b[36m│\x1b[0m \x1b[1;37mGit Branches\x1b[0m \x1b[36m│\x1b[0m");
2463 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2464
2465 for branch in &branches {
2466 println!(
2467 "\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2468 truncate_string(&branch.branch_name, 25)
2469 );
2470 println!(
2471 "\x1b[36m│\x1b[0m Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2472 format!("{:.1}h", branch.total_hours())
2473 );
2474 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2475 }
2476
2477 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2478 Ok(())
2479}
2480
2481async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
2482 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2483 println!("\x1b[36m│\x1b[0m \x1b[1;37mBranch Statistics\x1b[0m \x1b[36m│\x1b[0m");
2484 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2485 println!(
2486 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2487 truncate_string(&project, 27)
2488 );
2489 if let Some(b) = branch {
2490 println!(
2491 "\x1b[36m│\x1b[0m Branch: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2492 truncate_string(&b, 27)
2493 );
2494 }
2495 println!(
2496 "\x1b[36m│\x1b[0m \x1b[33m⚠ Branch stats in development\x1b[0m \x1b[36m│\x1b[0m"
2497 );
2498 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2499 Ok(())
2500}
2501
2502async fn handle_template_action(action: TemplateAction) -> Result<()> {
2504 match action {
2505 TemplateAction::Create {
2506 name,
2507 description,
2508 tags,
2509 workspace_path,
2510 } => create_template(name, description, tags, workspace_path).await,
2511 TemplateAction::List => list_templates().await,
2512 TemplateAction::Delete { template } => delete_template(template).await,
2513 TemplateAction::Use {
2514 template,
2515 project_name,
2516 path,
2517 } => use_template(template, project_name, path).await,
2518 }
2519}
2520
2521async fn create_template(
2522 name: String,
2523 description: Option<String>,
2524 tags: Option<String>,
2525 workspace_path: Option<PathBuf>,
2526) -> Result<()> {
2527 let db_path = get_database_path()?;
2528 let db = Database::new(&db_path)?;
2529
2530 let default_tags = tags
2531 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2532 .unwrap_or_default();
2533
2534 let mut template = ProjectTemplate::new(name.clone()).with_tags(default_tags);
2535
2536 let desc_clone = description.clone();
2537 if let Some(desc) = description {
2538 template = template.with_description(desc);
2539 }
2540 if let Some(path) = workspace_path {
2541 template = template.with_workspace_path(path);
2542 }
2543
2544 let _template_id = TemplateQueries::create(&db.connection, &template)?;
2545
2546 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2547 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplate Created\x1b[0m \x1b[36m│\x1b[0m");
2548 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2549 println!(
2550 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2551 truncate_string(&name, 27)
2552 );
2553 if let Some(desc) = &desc_clone {
2554 println!(
2555 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2556 truncate_string(desc, 27)
2557 );
2558 }
2559 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2560 Ok(())
2561}
2562
2563async fn list_templates() -> Result<()> {
2564 let db_path = get_database_path()?;
2565 let db = Database::new(&db_path)?;
2566
2567 let templates = TemplateQueries::list_all(&db.connection)?;
2568
2569 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2570 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplates\x1b[0m \x1b[36m│\x1b[0m");
2571 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2572
2573 if templates.is_empty() {
2574 println!("\x1b[36m│\x1b[0m No templates found. \x1b[36m│\x1b[0m");
2575 } else {
2576 for template in &templates {
2577 println!(
2578 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2579 truncate_string(&template.name, 25)
2580 );
2581 if let Some(desc) = &template.description {
2582 println!(
2583 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2584 truncate_string(desc, 27)
2585 );
2586 }
2587 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2588 }
2589 }
2590
2591 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2592 Ok(())
2593}
2594
2595async fn delete_template(_template: String) -> Result<()> {
2596 println!("\x1b[33m⚠ Template deletion not yet implemented\x1b[0m");
2597 Ok(())
2598}
2599
2600async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2601 let db_path = get_database_path()?;
2602 let db = Database::new(&db_path)?;
2603
2604 let templates = TemplateQueries::list_all(&db.connection)?;
2605 let selected_template = templates
2606 .iter()
2607 .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2608 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2609
2610 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2612 let canonical_path = canonicalize_path(&project_path)?;
2613
2614 if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2616 return Err(anyhow::anyhow!("Project already exists at this path"));
2617 }
2618
2619 let git_hash = if is_git_repository(&canonical_path) {
2620 get_git_hash(&canonical_path)
2621 } else {
2622 None
2623 };
2624
2625 let template_desc = selected_template.description.clone();
2626 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2627 .with_git_hash(git_hash)
2628 .with_description(template_desc);
2629
2630 let project_id = ProjectQueries::create(&db.connection, &project)?;
2631 project.id = Some(project_id);
2632
2633 for goal_def in &selected_template.default_goals {
2638 let mut goal =
2639 Goal::new(goal_def.name.clone(), goal_def.target_hours).with_project(project_id);
2640 if let Some(desc) = &goal_def.description {
2641 goal = goal.with_description(desc.clone());
2642 }
2643 GoalQueries::create(&db.connection, &goal)?;
2644 }
2645
2646 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2647 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Created from Template\x1b[0m \x1b[36m│\x1b[0m");
2648 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2649 println!(
2650 "\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2651 truncate_string(&selected_template.name, 27)
2652 );
2653 println!(
2654 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2655 truncate_string(&project_name, 27)
2656 );
2657 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2658 Ok(())
2659}
2660
2661async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2663 match action {
2664 WorkspaceAction::Create {
2665 name,
2666 description,
2667 path,
2668 } => create_workspace(name, description, path).await,
2669 WorkspaceAction::List => list_workspaces().await,
2670 WorkspaceAction::AddProject { workspace, project } => {
2671 add_project_to_workspace(workspace, project).await
2672 }
2673 WorkspaceAction::RemoveProject { workspace, project } => {
2674 remove_project_from_workspace(workspace, project).await
2675 }
2676 WorkspaceAction::Projects { workspace } => list_workspace_projects(workspace).await,
2677 WorkspaceAction::Delete { workspace } => delete_workspace(workspace).await,
2678 }
2679}
2680
2681async fn create_workspace(
2682 name: String,
2683 description: Option<String>,
2684 path: Option<PathBuf>,
2685) -> Result<()> {
2686 let db_path = get_database_path()?;
2687 let db = Database::new(&db_path)?;
2688
2689 let mut workspace = Workspace::new(name.clone());
2690 let desc_clone = description.clone();
2691 if let Some(desc) = description {
2692 workspace = workspace.with_description(desc);
2693 }
2694 if let Some(p) = path {
2695 workspace = workspace.with_path(p);
2696 }
2697
2698 let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2699
2700 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2701 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Created\x1b[0m \x1b[36m│\x1b[0m");
2702 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2703 println!(
2704 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2705 truncate_string(&name, 27)
2706 );
2707 if let Some(desc) = &desc_clone {
2708 println!(
2709 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2710 truncate_string(desc, 27)
2711 );
2712 }
2713 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2714 Ok(())
2715}
2716
2717async fn list_workspaces() -> Result<()> {
2718 let db_path = get_database_path()?;
2719 let db = Database::new(&db_path)?;
2720
2721 let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2722
2723 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2724 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspaces\x1b[0m \x1b[36m│\x1b[0m");
2725 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2726
2727 if workspaces.is_empty() {
2728 println!("\x1b[36m│\x1b[0m No workspaces found. \x1b[36m│\x1b[0m");
2729 } else {
2730 for workspace in &workspaces {
2731 println!(
2732 "\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2733 truncate_string(&workspace.name, 25)
2734 );
2735 if let Some(desc) = &workspace.description {
2736 println!(
2737 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2738 truncate_string(desc, 27)
2739 );
2740 }
2741 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2742 }
2743 }
2744
2745 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2746 Ok(())
2747}
2748
2749async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2750 let db_path = get_database_path()?;
2751 let db = Database::new(&db_path)?;
2752
2753 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2755 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2756
2757 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2759 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2760
2761 let workspace_id = workspace_obj
2762 .id
2763 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2764 let project_id = project_obj
2765 .id
2766 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2767
2768 if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2769 println!(
2770 "\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'",
2771 project, workspace
2772 );
2773 } else {
2774 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2775 }
2776
2777 Ok(())
2778}
2779
2780async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2781 let db_path = get_database_path()?;
2782 let db = Database::new(&db_path)?;
2783
2784 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2786 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2787
2788 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2790 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2791
2792 let workspace_id = workspace_obj
2793 .id
2794 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2795 let project_id = project_obj
2796 .id
2797 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2798
2799 if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2800 println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2801 } else {
2802 println!(
2803 "\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'",
2804 project, workspace
2805 );
2806 }
2807
2808 Ok(())
2809}
2810
2811async fn list_workspace_projects(workspace: String) -> Result<()> {
2812 let db_path = get_database_path()?;
2813 let db = Database::new(&db_path)?;
2814
2815 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2817 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2818
2819 let workspace_id = workspace_obj
2820 .id
2821 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2822 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2823
2824 if projects.is_empty() {
2825 println!(
2826 "\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'",
2827 workspace
2828 );
2829 return Ok(());
2830 }
2831
2832 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2833 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Projects\x1b[0m \x1b[36m│\x1b[0m");
2834 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2835 println!(
2836 "\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2837 truncate_string(&workspace, 25)
2838 );
2839 println!(
2840 "\x1b[36m│\x1b[0m Projects: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2841 format!("{} projects", projects.len())
2842 );
2843 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2844
2845 for project in &projects {
2846 let status_indicator = if !project.is_archived {
2847 "\x1b[32m●\x1b[0m"
2848 } else {
2849 "\x1b[31m○\x1b[0m"
2850 };
2851 println!(
2852 "\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2853 status_indicator,
2854 truncate_string(&project.name, 33)
2855 );
2856 }
2857
2858 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2859 Ok(())
2860}
2861
2862async fn delete_workspace(workspace: String) -> Result<()> {
2863 let db_path = get_database_path()?;
2864 let db = Database::new(&db_path)?;
2865
2866 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2868 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2869
2870 let workspace_id = workspace_obj
2871 .id
2872 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2873
2874 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2876 if !projects.is_empty() {
2877 println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.",
2878 workspace, projects.len());
2879 return Ok(());
2880 }
2881
2882 if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2883 println!(
2884 "\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'",
2885 workspace
2886 );
2887 } else {
2888 println!(
2889 "\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'",
2890 workspace
2891 );
2892 }
2893
2894 Ok(())
2895}
2896
2897async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2899 match action {
2900 CalendarAction::Add {
2901 name,
2902 start,
2903 end,
2904 event_type,
2905 project,
2906 description,
2907 } => add_calendar_event(name, start, end, event_type, project, description).await,
2908 CalendarAction::List { from, to, project } => list_calendar_events(from, to, project).await,
2909 CalendarAction::Delete { id } => delete_calendar_event(id).await,
2910 }
2911}
2912
2913async fn add_calendar_event(
2914 _name: String,
2915 _start: String,
2916 _end: Option<String>,
2917 _event_type: Option<String>,
2918 _project: Option<String>,
2919 _description: Option<String>,
2920) -> Result<()> {
2921 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2922 Ok(())
2923}
2924
2925async fn list_calendar_events(
2926 _from: Option<String>,
2927 _to: Option<String>,
2928 _project: Option<String>,
2929) -> Result<()> {
2930 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2931 Ok(())
2932}
2933
2934async fn delete_calendar_event(_id: i64) -> Result<()> {
2935 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2936 Ok(())
2937}
2938
2939async fn handle_issue_action(action: IssueAction) -> Result<()> {
2941 match action {
2942 IssueAction::Sync {
2943 project,
2944 tracker_type,
2945 } => sync_issues(project, tracker_type).await,
2946 IssueAction::List { project, status } => list_issues(project, status).await,
2947 IssueAction::Link {
2948 session_id,
2949 issue_id,
2950 } => link_session_to_issue(session_id, issue_id).await,
2951 }
2952}
2953
2954async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2955 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2956 Ok(())
2957}
2958
2959async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2960 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2961 Ok(())
2962}
2963
2964async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2965 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2966 Ok(())
2967}
2968
2969async fn handle_client_action(action: ClientAction) -> Result<()> {
2971 match action {
2972 ClientAction::Generate {
2973 client,
2974 from,
2975 to,
2976 projects,
2977 format,
2978 } => generate_client_report(client, from, to, projects, format).await,
2979 ClientAction::List { client } => list_client_reports(client).await,
2980 ClientAction::View { id } => view_client_report(id).await,
2981 }
2982}
2983
2984async fn generate_client_report(
2985 _client: String,
2986 _from: String,
2987 _to: String,
2988 _projects: Option<String>,
2989 _format: Option<String>,
2990) -> Result<()> {
2991 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2992 Ok(())
2993}
2994
2995async fn list_client_reports(_client: Option<String>) -> Result<()> {
2996 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2997 Ok(())
2998}
2999
3000async fn view_client_report(_id: i64) -> Result<()> {
3001 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
3002 Ok(())
3003}
3004
3005#[allow(dead_code)]
3006fn should_quit(event: crossterm::event::Event) -> bool {
3007 match event {
3008 crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
3009 matches!(
3010 key.code,
3011 crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc
3012 )
3013 }
3014 _ => false,
3015 }
3016}
3017
3018async fn init_project_with_db(
3020 name: Option<String>,
3021 canonical_path: Option<PathBuf>,
3022 description: Option<String>,
3023 conn: &rusqlite::Connection,
3024) -> Result<()> {
3025 let canonical_path =
3026 canonical_path.ok_or_else(|| anyhow::anyhow!("Canonical path required"))?;
3027 let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
3028
3029 if let Some(existing) = ProjectQueries::find_by_path(conn, &canonical_path)? {
3031 println!(
3032 "\x1b[33m⚠ Project already exists:\x1b[0m {}",
3033 existing.name
3034 );
3035 return Ok(());
3036 }
3037
3038 let git_hash = if is_git_repository(&canonical_path) {
3040 get_git_hash(&canonical_path)
3041 } else {
3042 None
3043 };
3044
3045 let mut project = Project::new(project_name.clone(), canonical_path.clone())
3047 .with_git_hash(git_hash.clone())
3048 .with_description(description.clone());
3049
3050 let project_id = ProjectQueries::create(conn, &project)?;
3052 project.id = Some(project_id);
3053
3054 let marker_path = canonical_path.join(".tempo");
3056 if !marker_path.exists() {
3057 std::fs::write(
3058 &marker_path,
3059 format!("# Tempo time tracking project\nname: {}\n", project_name),
3060 )?;
3061 }
3062
3063 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
3064 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Initialized\x1b[0m \x1b[36m│\x1b[0m");
3065 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
3066 println!(
3067 "\x1b[36m│\x1b[0m Name: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3068 truncate_string(&project_name, 25)
3069 );
3070 println!(
3071 "\x1b[36m│\x1b[0m Path: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3072 truncate_string(&canonical_path.display().to_string(), 25)
3073 );
3074
3075 if let Some(desc) = &description {
3076 println!(
3077 "\x1b[36m│\x1b[0m Description: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3078 truncate_string(desc, 25)
3079 );
3080 }
3081
3082 if is_git_repository(&canonical_path) {
3083 println!(
3084 "\x1b[36m│\x1b[0m Git: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3085 "Repository detected"
3086 );
3087 if let Some(hash) = &git_hash {
3088 println!(
3089 "\x1b[36m│\x1b[0m Git Hash: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3090 truncate_string(hash, 25)
3091 );
3092 }
3093 }
3094
3095 println!(
3096 "\x1b[36m│\x1b[0m ID: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
3097 project_id
3098 );
3099 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
3100
3101 Ok(())
3102}
3103
3104async fn show_pool_stats() -> Result<()> {
3106 match get_pool_stats() {
3107 Ok(stats) => {
3108 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
3109 println!("\x1b[36m│\x1b[0m \x1b[1;37mDatabase Pool Statistics\x1b[0m \x1b[36m│\x1b[0m");
3110 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
3111 println!(
3112 "\x1b[36m│\x1b[0m Total Created: \x1b[32m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3113 stats.total_connections_created
3114 );
3115 println!(
3116 "\x1b[36m│\x1b[0m Active: \x1b[33m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3117 stats.active_connections
3118 );
3119 println!(
3120 "\x1b[36m│\x1b[0m Available in Pool:\x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3121 stats.connections_in_pool
3122 );
3123 println!(
3124 "\x1b[36m│\x1b[0m Total Requests: \x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3125 stats.connection_requests
3126 );
3127 println!(
3128 "\x1b[36m│\x1b[0m Timeouts: \x1b[31m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3129 stats.connection_timeouts
3130 );
3131 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
3132 }
3133 Err(_) => {
3134 println!("\x1b[33m⚠ Database pool not initialized or not available\x1b[0m");
3135 println!(" Using direct database connections as fallback");
3136 }
3137 }
3138 Ok(())
3139}
3140
3141#[derive(Deserialize)]
3142#[allow(dead_code)]
3143struct GitHubRelease {
3144 tag_name: String,
3145 name: String,
3146 body: String,
3147 published_at: String,
3148 prerelease: bool,
3149}
3150
3151async fn handle_update(check: bool, force: bool, verbose: bool) -> Result<()> {
3152 let current_version = env!("CARGO_PKG_VERSION");
3153
3154 if verbose {
3155 println!("🔍 Current version: v{}", current_version);
3156 println!("📡 Checking for updates...");
3157 } else {
3158 println!("🔍 Checking for updates...");
3159 }
3160
3161 let client = reqwest::Client::new();
3163 let response = client
3164 .get("https://api.github.com/repos/own-path/vibe/releases/latest")
3165 .header("User-Agent", format!("tempo-cli/{}", current_version))
3166 .send()
3167 .await
3168 .context("Failed to fetch release information")?;
3169
3170 if !response.status().is_success() {
3171 return Err(anyhow::anyhow!(
3172 "Failed to fetch release information: HTTP {}",
3173 response.status()
3174 ));
3175 }
3176
3177 let release: GitHubRelease = response
3178 .json()
3179 .await
3180 .context("Failed to parse release information")?;
3181
3182 let latest_version = release.tag_name.trim_start_matches('v');
3183
3184 if verbose {
3185 println!("📦 Latest version: v{}", latest_version);
3186 println!("📅 Released: {}", release.published_at);
3187 }
3188
3189 let current_semver =
3191 semver::Version::parse(current_version).context("Failed to parse current version")?;
3192 let latest_semver =
3193 semver::Version::parse(latest_version).context("Failed to parse latest version")?;
3194
3195 if current_semver >= latest_semver && !force {
3196 println!(
3197 "✅ You're already running the latest version (v{})",
3198 current_version
3199 );
3200 if check {
3201 return Ok(());
3202 }
3203
3204 if !force {
3205 println!("💡 Use --force to reinstall the current version");
3206 return Ok(());
3207 }
3208 }
3209
3210 if check {
3211 if current_semver < latest_semver {
3212 println!(
3213 "📦 Update available: v{} → v{}",
3214 current_version, latest_version
3215 );
3216 println!("🔗 Run `tempo update` to install the latest version");
3217
3218 if verbose && !release.body.is_empty() {
3219 println!("\n📝 Release Notes:");
3220 println!("{}", release.body);
3221 }
3222 }
3223 return Ok(());
3224 }
3225
3226 if current_semver < latest_semver || force {
3227 println!(
3228 "⬇️ Updating tempo from v{} to v{}",
3229 current_version, latest_version
3230 );
3231
3232 if verbose {
3233 println!("🔧 Installing via cargo...");
3234 }
3235
3236 let mut cmd = Command::new("cargo");
3238 cmd.args(["install", "tempo-cli", "--force"]);
3239
3240 if verbose {
3241 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
3242 } else {
3243 cmd.stdout(Stdio::null()).stderr(Stdio::null());
3244 }
3245
3246 let status = cmd
3247 .status()
3248 .context("Failed to run cargo install command")?;
3249
3250 if status.success() {
3251 println!("✅ Successfully updated tempo to v{}", latest_version);
3252 println!("🎉 You can now use the latest features!");
3253
3254 if !release.body.is_empty() && verbose {
3255 println!("\n📝 What's new in v{}:", latest_version);
3256 println!("{}", release.body);
3257 }
3258 } else {
3259 return Err(anyhow::anyhow!(
3260 "Failed to install update. Try running manually: cargo install tempo-cli --force"
3261 ));
3262 }
3263 }
3264
3265 Ok(())
3266}