tempo_cli/cli/
commands.rs

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
195// Daemon control functions
196async 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    // Wait a moment for daemon to start
228    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    // Try to connect and send shutdown message
247    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    // Fallback: kill via PID file
262    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); // Minimal response
315                    }
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
335// Session control functions
336async 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
484// Report generation function
485async 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 to console with formatted output
510            print_formatted_report(&report)?;
511        }
512    }
513
514    Ok(())
515}
516
517// Formatted output functions
518fn print_formatted_session(session: &crate::utils::ipc::SessionInfo) -> Result<()> {
519    // Color scheme definitions
520    let context_color = match session.context.as_str() {
521        "terminal" => "\x1b[96m", // Bright cyan
522        "ide" => "\x1b[95m",      // Bright magenta
523        "linked" => "\x1b[93m",   // Bright yellow
524        "manual" => "\x1b[94m",   // Bright blue
525        _ => "\x1b[97m",          // Bright white (default)
526    };
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    // Helper function to get context color
559    let get_context_color = |context: &str| -> &str {
560        match context {
561            "terminal" => "\x1b[96m", // Bright cyan
562            "ide" => "\x1b[95m",      // Bright magenta
563            "linked" => "\x1b[93m",   // Bright yellow
564            "manual" => "\x1b[94m",   // Bright blue
565            _ => "\x1b[97m",          // Bright white (default)
566        }
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
623// Helper functions for consistent messaging
624fn 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
699// Project management functions
700async fn init_project(
701    name: Option<String>,
702    path: Option<PathBuf>,
703    description: Option<String>,
704) -> Result<()> {
705    // Validate inputs early
706    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    // Use secure path validation
722    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    // Get database connection from pool
731    let conn = match get_connection().await {
732        Ok(conn) => conn,
733        Err(_) => {
734            // Fallback to direct connection
735            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    // Check if project already exists
748    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    // Use the pooled connection to complete initialization
758    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    // Initialize database
778    let db_path = get_database_path()?;
779    let db = Database::new(&db_path)?;
780
781    // Get projects
782    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    // Filter by tag if specified
797    let filtered_projects = if let Some(_tag) = tag_filter {
798        // TODO: Implement tag filtering when project-tag associations are implemented
799        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
878// Tag management functions
879async fn create_tag(
880    name: String,
881    color: Option<String>,
882    description: Option<String>,
883) -> Result<()> {
884    // Initialize database
885    let db_path = get_database_path()?;
886    let db = Database::new(&db_path)?;
887
888    // Create tag - use builder pattern to avoid cloning
889    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    // Validate tag
898    tag.validate()?;
899
900    // Check if tag already exists
901    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    // Save to database
908    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    // Initialize database
944    let db_path = get_database_path()?;
945    let db = Database::new(&db_path)?;
946
947    // Get tags
948    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    // Initialize database
1000    let db_path = get_database_path()?;
1001    let db = Database::new(&db_path)?;
1002
1003    // Check if tag exists
1004    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    // Delete the tag
1010    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
1033// Configuration management functions
1034async 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(); // Clone for display purposes
1119
1120    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
1186// Session management functions
1187async fn list_sessions(limit: Option<usize>, project_filter: Option<String>) -> Result<()> {
1188    // Initialize database
1189    let db_path = get_database_path()?;
1190    let db = Database::new(&db_path)?;
1191
1192    let session_limit = limit.unwrap_or(10);
1193
1194    // Handle project filtering
1195    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    // Filter by project if specified
1228    let filtered_sessions = if let Some(_project) = project_filter {
1229        // TODO: Implement project filtering when we have project relationships
1230        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    // Initialize database
1296    let db_path = get_database_path()?;
1297    let db = Database::new(&db_path)?;
1298
1299    // Find the session
1300    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    // Parse new values
1313    let mut new_start = original_start;
1314    let mut new_end = original_end;
1315    let mut new_project_id = session.project_id;
1316
1317    // Parse start time if provided
1318    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    // Parse end time if provided
1333    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    // Find project by name if provided
1354    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    // Validate the edit
1364    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    // Create audit trail record
1370    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    // Update the session
1381    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    // Initialize database
1443    let db_path = get_database_path()?;
1444    let db = Database::new(&db_path)?;
1445
1446    // Check if session exists
1447    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    // Check if it's an active session and require force flag
1457    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    // Delete the session
1464    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
1492// Project management functions
1493async 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// Bulk session operations
1648async fn bulk_update_sessions_project(
1649    session_ids: Vec<i64>,
1650    new_project_name: String,
1651) -> Result<()> {
1652    let db_path = get_database_path()?;
1653    let db = Database::new(&db_path)?;
1654
1655    // Find the target project
1656    let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1657        Some(p) => p,
1658        None => {
1659            println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1660            return Ok(());
1661        }
1662    };
1663
1664    let updated =
1665        SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1666
1667    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1668    println!(
1669        "\x1b[36m│\x1b[0m      \x1b[1;37mBulk Session Update\x1b[0m               \x1b[36m│\x1b[0m"
1670    );
1671    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1672    println!(
1673        "\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1674        updated
1675    );
1676    println!(
1677        "\x1b[36m│\x1b[0m Project:  \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1678        truncate_string(&new_project_name, 27)
1679    );
1680    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1681    println!(
1682        "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m",
1683        updated, ""
1684    );
1685    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1686
1687    Ok(())
1688}
1689
1690async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1691    let db_path = get_database_path()?;
1692    let db = Database::new(&db_path)?;
1693
1694    let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1695
1696    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1697    println!(
1698        "\x1b[36m│\x1b[0m      \x1b[1;37mBulk Session Delete\x1b[0m               \x1b[36m│\x1b[0m"
1699    );
1700    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1701    println!(
1702        "\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1703        session_ids.len()
1704    );
1705    println!(
1706        "\x1b[36m│\x1b[0m Deleted:   \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1707        deleted
1708    );
1709    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1710    println!(
1711        "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m",
1712        deleted, ""
1713    );
1714    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1715
1716    Ok(())
1717}
1718
1719async fn launch_dashboard() -> Result<()> {
1720    // Setup terminal
1721    enable_raw_mode()?;
1722    let mut stdout = io::stdout();
1723    execute!(stdout, EnterAlternateScreen)?;
1724    let backend = CrosstermBackend::new(stdout);
1725    let mut terminal = Terminal::new(backend)?;
1726
1727    // Create dashboard instance and run it
1728    let result = async {
1729        let mut dashboard = Dashboard::new().await?;
1730        dashboard.run(&mut terminal).await
1731    };
1732
1733    let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1734
1735    // Restore terminal
1736    disable_raw_mode()?;
1737    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1738    terminal.show_cursor()?;
1739
1740    result
1741}
1742
1743async fn launch_timer() -> Result<()> {
1744    // Setup terminal
1745    enable_raw_mode()?;
1746    let mut stdout = io::stdout();
1747    execute!(stdout, EnterAlternateScreen)?;
1748    let backend = CrosstermBackend::new(stdout);
1749    let mut terminal = Terminal::new(backend)?;
1750
1751    // Create timer instance and run it
1752    let result = async {
1753        let mut timer = InteractiveTimer::new().await?;
1754        timer.run(&mut terminal).await
1755    };
1756
1757    let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1758
1759    // Restore terminal
1760    disable_raw_mode()?;
1761    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1762    terminal.show_cursor()?;
1763
1764    result
1765}
1766
1767async fn merge_sessions(
1768    session_ids_str: String,
1769    project_name: Option<String>,
1770    notes: Option<String>,
1771) -> Result<()> {
1772    // Parse session IDs
1773    let session_ids: Result<Vec<i64>, _> = session_ids_str
1774        .split(',')
1775        .map(|s| s.trim().parse::<i64>())
1776        .collect();
1777
1778    let session_ids = session_ids.map_err(|_| {
1779        anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'")
1780    })?;
1781
1782    if session_ids.len() < 2 {
1783        return Err(anyhow::anyhow!(
1784            "At least 2 sessions are required for merging"
1785        ));
1786    }
1787
1788    // Get target project ID if specified
1789    let mut target_project_id = None;
1790    if let Some(project) = project_name {
1791        let db_path = get_database_path()?;
1792        let db = Database::new(&db_path)?;
1793
1794        // Try to find project by name first, then by ID
1795        if let Ok(project_id) = project.parse::<i64>() {
1796            if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1797                target_project_id = Some(project_id);
1798            }
1799        } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1800            target_project_id = proj.id;
1801        }
1802
1803        if target_project_id.is_none() {
1804            return Err(anyhow::anyhow!("Project '{}' not found", project));
1805        }
1806    }
1807
1808    // Perform the merge
1809    let db_path = get_database_path()?;
1810    let db = Database::new(&db_path)?;
1811
1812    let merged_id =
1813        SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1814
1815    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1816    println!("\x1b[36m│\x1b[0m        \x1b[1;37mSession Merge Complete\x1b[0m            \x1b[36m│\x1b[0m");
1817    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1818    println!(
1819        "\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1820        session_ids
1821            .iter()
1822            .map(|id| id.to_string())
1823            .collect::<Vec<_>>()
1824            .join(", ")
1825    );
1826    println!(
1827        "\x1b[36m│\x1b[0m New session ID:  \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1828        merged_id
1829    );
1830    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1831    println!(
1832        "\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m        \x1b[36m│\x1b[0m"
1833    );
1834    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1835
1836    Ok(())
1837}
1838
1839async fn split_session(
1840    session_id: i64,
1841    split_times_str: String,
1842    notes: Option<String>,
1843) -> Result<()> {
1844    // Parse split times
1845    let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
1846    let mut split_times = Vec::new();
1847
1848    for time_str in split_time_strings {
1849        // Try to parse as time (HH:MM or HH:MM:SS)
1850        let datetime = if time_str.contains(':') {
1851            // Parse as time and combine with today's date
1852            let today = chrono::Local::now().date_naive();
1853            let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
1854                .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
1855                .map_err(|_| {
1856                    anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str)
1857                })?;
1858            today.and_time(time).and_utc()
1859        } else {
1860            // Try to parse as full datetime
1861            chrono::DateTime::parse_from_rfc3339(time_str)
1862                .map_err(|_| {
1863                    anyhow::anyhow!(
1864                        "Invalid datetime format '{}'. Use HH:MM or RFC3339 format",
1865                        time_str
1866                    )
1867                })?
1868                .to_utc()
1869        };
1870
1871        split_times.push(datetime);
1872    }
1873
1874    if split_times.is_empty() {
1875        return Err(anyhow::anyhow!("No valid split times provided"));
1876    }
1877
1878    // Parse notes if provided
1879    let notes_list = notes.map(|n| {
1880        n.split(',')
1881            .map(|s| s.trim().to_string())
1882            .collect::<Vec<String>>()
1883    });
1884
1885    // Perform the split
1886    let db_path = get_database_path()?;
1887    let db = Database::new(&db_path)?;
1888
1889    let new_session_ids =
1890        SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
1891
1892    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1893    println!("\x1b[36m│\x1b[0m        \x1b[1;37mSession Split Complete\x1b[0m            \x1b[36m│\x1b[0m");
1894    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1895    println!(
1896        "\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1897        session_id
1898    );
1899    println!(
1900        "\x1b[36m│\x1b[0m Split points:     \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1901        split_times.len()
1902    );
1903    println!(
1904        "\x1b[36m│\x1b[0m New sessions:     \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1905        new_session_ids
1906            .iter()
1907            .map(|id| id.to_string())
1908            .collect::<Vec<_>>()
1909            .join(", ")
1910    );
1911    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1912    println!(
1913        "\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m          \x1b[36m│\x1b[0m"
1914    );
1915    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1916
1917    Ok(())
1918}
1919
1920async fn launch_history() -> Result<()> {
1921    enable_raw_mode()?;
1922    let mut stdout = io::stdout();
1923    execute!(stdout, EnterAlternateScreen)?;
1924    let backend = CrosstermBackend::new(stdout);
1925    let mut terminal = Terminal::new(backend)?;
1926
1927    let result = async {
1928        let mut browser = SessionHistoryBrowser::new().await?;
1929        browser.run(&mut terminal).await
1930    };
1931
1932    let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1933
1934    disable_raw_mode()?;
1935    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1936    terminal.show_cursor()?;
1937
1938    result
1939}
1940
1941async fn handle_goal_action(action: GoalAction) -> Result<()> {
1942    match action {
1943        GoalAction::Create {
1944            name,
1945            target_hours,
1946            project,
1947            description,
1948            start_date,
1949            end_date,
1950        } => {
1951            create_goal(
1952                name,
1953                target_hours,
1954                project,
1955                description,
1956                start_date,
1957                end_date,
1958            )
1959            .await
1960        }
1961        GoalAction::List { project } => list_goals(project).await,
1962        GoalAction::Update { id, hours } => update_goal_progress(id, hours).await,
1963    }
1964}
1965
1966async fn create_goal(
1967    name: String,
1968    target_hours: f64,
1969    project: Option<String>,
1970    description: Option<String>,
1971    start_date: Option<String>,
1972    end_date: Option<String>,
1973) -> Result<()> {
1974    let db_path = get_database_path()?;
1975    let db = Database::new(&db_path)?;
1976
1977    let project_id = if let Some(proj_name) = project {
1978        match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
1979            Some(p) => p.id,
1980            None => {
1981                println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
1982                return Ok(());
1983            }
1984        }
1985    } else {
1986        None
1987    };
1988
1989    let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1990    let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1991
1992    let mut goal = Goal::new(name.clone(), target_hours);
1993    if let Some(pid) = project_id {
1994        goal = goal.with_project(pid);
1995    }
1996    if let Some(desc) = description {
1997        goal = goal.with_description(desc);
1998    }
1999    goal = goal.with_dates(start, end);
2000
2001    goal.validate()?;
2002    let goal_id = GoalQueries::create(&db.connection, &goal)?;
2003
2004    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2005    println!("\x1b[36m│\x1b[0m           \x1b[1;37mGoal Created\x1b[0m                   \x1b[36m│\x1b[0m");
2006    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2007    println!(
2008        "\x1b[36m│\x1b[0m Name:     \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2009        truncate_string(&name, 27)
2010    );
2011    println!(
2012        "\x1b[36m│\x1b[0m Target:   \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2013        format!("{} hours", target_hours)
2014    );
2015    println!(
2016        "\x1b[36m│\x1b[0m ID:       \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2017        goal_id
2018    );
2019    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2020    println!(
2021        "\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m             \x1b[36m│\x1b[0m"
2022    );
2023    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2024
2025    Ok(())
2026}
2027
2028async fn list_goals(project: Option<String>) -> Result<()> {
2029    let db_path = get_database_path()?;
2030    let db = Database::new(&db_path)?;
2031
2032    let project_id = if let Some(proj_name) = &project {
2033        match ProjectQueries::find_by_name(&db.connection, proj_name)? {
2034            Some(p) => p.id,
2035            None => {
2036                println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2037                return Ok(());
2038            }
2039        }
2040    } else {
2041        None
2042    };
2043
2044    let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
2045
2046    if goals.is_empty() {
2047        println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2048        println!("\x1b[36m│\x1b[0m              \x1b[1;37mNo Goals\x1b[0m                    \x1b[36m│\x1b[0m");
2049        println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2050        return Ok(());
2051    }
2052
2053    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2054    println!("\x1b[36m│\x1b[0m                \x1b[1;37mGoals\x1b[0m                      \x1b[36m│\x1b[0m");
2055    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2056
2057    for goal in &goals {
2058        let progress_pct = goal.progress_percentage();
2059        println!(
2060            "\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2061            truncate_string(&goal.name, 25)
2062        );
2063        println!("\x1b[36m│\x1b[0m    Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h)     \x1b[36m│\x1b[0m", 
2064            progress_pct, goal.current_progress, goal.target_hours);
2065        println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2066    }
2067
2068    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2069    Ok(())
2070}
2071
2072async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
2073    let db_path = get_database_path()?;
2074    let db = Database::new(&db_path)?;
2075
2076    GoalQueries::update_progress(&db.connection, id, hours)?;
2077    println!(
2078        "\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m",
2079        id, hours
2080    );
2081    Ok(())
2082}
2083
2084async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
2085    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2086    println!("\x1b[36m│\x1b[0m        \x1b[1;37mProductivity Insights\x1b[0m              \x1b[36m│\x1b[0m");
2087    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2088    println!(
2089        "\x1b[36m│\x1b[0m Period:   \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2090        period.as_deref().unwrap_or("all")
2091    );
2092    if let Some(proj) = project {
2093        println!(
2094            "\x1b[36m│\x1b[0m Project:  \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2095            truncate_string(&proj, 27)
2096        );
2097    }
2098    println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2099    println!(
2100        "\x1b[36m│\x1b[0m \x1b[33m⚠  Insights calculation in progress...\x1b[0m  \x1b[36m│\x1b[0m"
2101    );
2102    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2103    Ok(())
2104}
2105
2106async fn show_summary(period: String, from: Option<String>) -> Result<()> {
2107    let db_path = get_database_path()?;
2108    let db = Database::new(&db_path)?;
2109
2110    let start_date = if let Some(from_str) = from {
2111        chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
2112    } else {
2113        match period.as_str() {
2114            "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
2115            "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
2116            _ => chrono::Local::now().date_naive(),
2117        }
2118    };
2119
2120    let insight_data = match period.as_str() {
2121        "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
2122        "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
2123        _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
2124    };
2125
2126    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2127    println!(
2128        "\x1b[36m│\x1b[0m         \x1b[1;37m{} Summary\x1b[0m                  \x1b[36m│\x1b[0m",
2129        period
2130    );
2131    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2132    println!(
2133        "\x1b[36m│\x1b[0m Total Hours:  \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2134        format!("{:.1}h", insight_data.total_hours)
2135    );
2136    println!(
2137        "\x1b[36m│\x1b[0m Sessions:     \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2138        insight_data.sessions_count
2139    );
2140    println!(
2141        "\x1b[36m│\x1b[0m Avg Session:  \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2142        format!("{:.1}h", insight_data.avg_session_duration)
2143    );
2144    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2145    Ok(())
2146}
2147
2148async fn compare_projects(
2149    projects: String,
2150    _from: Option<String>,
2151    _to: Option<String>,
2152) -> Result<()> {
2153    let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
2154
2155    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2156    println!("\x1b[36m│\x1b[0m        \x1b[1;37mProject Comparison\x1b[0m                \x1b[36m│\x1b[0m");
2157    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2158    println!(
2159        "\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2160        truncate_string(&projects, 27)
2161    );
2162    println!(
2163        "\x1b[36m│\x1b[0m \x1b[33m⚠  Comparison feature in development\x1b[0m    \x1b[36m│\x1b[0m"
2164    );
2165    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2166    Ok(())
2167}
2168
2169async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
2170    match action {
2171        EstimateAction::Create {
2172            project,
2173            task,
2174            hours,
2175            due_date,
2176        } => create_estimate(project, task, hours, due_date).await,
2177        EstimateAction::Record { id, hours } => record_actual_time(id, hours).await,
2178        EstimateAction::List { project } => list_estimates(project).await,
2179    }
2180}
2181
2182async fn create_estimate(
2183    project: String,
2184    task: String,
2185    hours: f64,
2186    due_date: Option<String>,
2187) -> Result<()> {
2188    let db_path = get_database_path()?;
2189    let db = Database::new(&db_path)?;
2190
2191    let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2192        .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2193
2194    let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2195
2196    let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
2197    estimate.due_date = due;
2198
2199    let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
2200
2201    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2202    println!("\x1b[36m│\x1b[0m      \x1b[1;37mTime Estimate Created\x1b[0m              \x1b[36m│\x1b[0m");
2203    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2204    println!(
2205        "\x1b[36m│\x1b[0m Task:      \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2206        truncate_string(&task, 27)
2207    );
2208    println!(
2209        "\x1b[36m│\x1b[0m Estimate:  \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2210        format!("{} hours", hours)
2211    );
2212    println!(
2213        "\x1b[36m│\x1b[0m ID:        \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2214        estimate_id
2215    );
2216    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2217    Ok(())
2218}
2219
2220async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
2221    let db_path = get_database_path()?;
2222    let db = Database::new(&db_path)?;
2223
2224    TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
2225    println!(
2226        "\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m",
2227        hours, id
2228    );
2229    Ok(())
2230}
2231
2232async fn list_estimates(project: String) -> Result<()> {
2233    let db_path = get_database_path()?;
2234    let db = Database::new(&db_path)?;
2235
2236    let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2237        .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2238
2239    let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2240
2241    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2242    println!("\x1b[36m│\x1b[0m          \x1b[1;37mTime Estimates\x1b[0m                  \x1b[36m│\x1b[0m");
2243    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2244
2245    for est in &estimates {
2246        let variance = est.variance();
2247        let variance_str = if let Some(v) = variance {
2248            if v > 0.0 {
2249                format!("\x1b[31m+{:.1}h over\x1b[0m", v)
2250            } else {
2251                format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
2252            }
2253        } else {
2254            "N/A".to_string()
2255        };
2256
2257        println!(
2258            "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2259            truncate_string(&est.task_name, 25)
2260        );
2261        let actual_str = est
2262            .actual_hours
2263            .map(|h| format!("{:.1}h", h))
2264            .unwrap_or_else(|| "N/A".to_string());
2265        println!(
2266            "\x1b[36m│\x1b[0m    Est: {}h | Actual: {} | {}  \x1b[36m│\x1b[0m",
2267            est.estimated_hours, actual_str, variance_str
2268        );
2269        println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2270    }
2271
2272    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2273    Ok(())
2274}
2275
2276async fn handle_branch_action(action: BranchAction) -> Result<()> {
2277    match action {
2278        BranchAction::List { project } => list_branches(project).await,
2279        BranchAction::Stats { project, branch } => show_branch_stats(project, branch).await,
2280    }
2281}
2282
2283async fn list_branches(project: String) -> Result<()> {
2284    let db_path = get_database_path()?;
2285    let db = Database::new(&db_path)?;
2286
2287    let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2288        .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2289
2290    let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2291
2292    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2293    println!("\x1b[36m│\x1b[0m          \x1b[1;37mGit Branches\x1b[0m                   \x1b[36m│\x1b[0m");
2294    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2295
2296    for branch in &branches {
2297        println!(
2298            "\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2299            truncate_string(&branch.branch_name, 25)
2300        );
2301        println!(
2302            "\x1b[36m│\x1b[0m    Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2303            format!("{:.1}h", branch.total_hours())
2304        );
2305        println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2306    }
2307
2308    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2309    Ok(())
2310}
2311
2312async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
2313    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2314    println!("\x1b[36m│\x1b[0m        \x1b[1;37mBranch Statistics\x1b[0m                \x1b[36m│\x1b[0m");
2315    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2316    println!(
2317        "\x1b[36m│\x1b[0m Project:  \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2318        truncate_string(&project, 27)
2319    );
2320    if let Some(b) = branch {
2321        println!(
2322            "\x1b[36m│\x1b[0m Branch:   \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2323            truncate_string(&b, 27)
2324        );
2325    }
2326    println!(
2327        "\x1b[36m│\x1b[0m \x1b[33m⚠  Branch stats in development\x1b[0m         \x1b[36m│\x1b[0m"
2328    );
2329    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2330    Ok(())
2331}
2332
2333// Template management functions
2334async fn handle_template_action(action: TemplateAction) -> Result<()> {
2335    match action {
2336        TemplateAction::Create {
2337            name,
2338            description,
2339            tags,
2340            workspace_path,
2341        } => create_template(name, description, tags, workspace_path).await,
2342        TemplateAction::List => list_templates().await,
2343        TemplateAction::Delete { template } => delete_template(template).await,
2344        TemplateAction::Use {
2345            template,
2346            project_name,
2347            path,
2348        } => use_template(template, project_name, path).await,
2349    }
2350}
2351
2352async fn create_template(
2353    name: String,
2354    description: Option<String>,
2355    tags: Option<String>,
2356    workspace_path: Option<PathBuf>,
2357) -> Result<()> {
2358    let db_path = get_database_path()?;
2359    let db = Database::new(&db_path)?;
2360
2361    let default_tags = tags
2362        .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2363        .unwrap_or_default();
2364
2365    let mut template = ProjectTemplate::new(name.clone()).with_tags(default_tags);
2366
2367    let desc_clone = description.clone();
2368    if let Some(desc) = description {
2369        template = template.with_description(desc);
2370    }
2371    if let Some(path) = workspace_path {
2372        template = template.with_workspace_path(path);
2373    }
2374
2375    let _template_id = TemplateQueries::create(&db.connection, &template)?;
2376
2377    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2378    println!("\x1b[36m│\x1b[0m         \x1b[1;37mTemplate Created\x1b[0m                  \x1b[36m│\x1b[0m");
2379    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2380    println!(
2381        "\x1b[36m│\x1b[0m Name:     \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2382        truncate_string(&name, 27)
2383    );
2384    if let Some(desc) = &desc_clone {
2385        println!(
2386            "\x1b[36m│\x1b[0m Desc:     \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2387            truncate_string(desc, 27)
2388        );
2389    }
2390    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2391    Ok(())
2392}
2393
2394async fn list_templates() -> Result<()> {
2395    let db_path = get_database_path()?;
2396    let db = Database::new(&db_path)?;
2397
2398    let templates = TemplateQueries::list_all(&db.connection)?;
2399
2400    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2401    println!("\x1b[36m│\x1b[0m          \x1b[1;37mTemplates\x1b[0m                      \x1b[36m│\x1b[0m");
2402    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2403
2404    if templates.is_empty() {
2405        println!("\x1b[36m│\x1b[0m No templates found.                      \x1b[36m│\x1b[0m");
2406    } else {
2407        for template in &templates {
2408            println!(
2409                "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2410                truncate_string(&template.name, 25)
2411            );
2412            if let Some(desc) = &template.description {
2413                println!(
2414                    "\x1b[36m│\x1b[0m    \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2415                    truncate_string(desc, 27)
2416                );
2417            }
2418            println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2419        }
2420    }
2421
2422    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2423    Ok(())
2424}
2425
2426async fn delete_template(_template: String) -> Result<()> {
2427    println!("\x1b[33m⚠  Template deletion not yet implemented\x1b[0m");
2428    Ok(())
2429}
2430
2431async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2432    let db_path = get_database_path()?;
2433    let db = Database::new(&db_path)?;
2434
2435    let templates = TemplateQueries::list_all(&db.connection)?;
2436    let selected_template = templates
2437        .iter()
2438        .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2439        .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2440
2441    // Initialize project with template
2442    let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2443    let canonical_path = canonicalize_path(&project_path)?;
2444
2445    // Check if project already exists
2446    if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2447        return Err(anyhow::anyhow!("Project already exists at this path"));
2448    }
2449
2450    let git_hash = if is_git_repository(&canonical_path) {
2451        get_git_hash(&canonical_path)
2452    } else {
2453        None
2454    };
2455
2456    let template_desc = selected_template.description.clone();
2457    let mut project = Project::new(project_name.clone(), canonical_path.clone())
2458        .with_git_hash(git_hash)
2459        .with_description(template_desc);
2460
2461    let project_id = ProjectQueries::create(&db.connection, &project)?;
2462    project.id = Some(project_id);
2463
2464    // Apply template tags (project-tag associations not yet implemented)
2465    // TODO: Implement project_tags table operations
2466
2467    // Apply template goals
2468    for goal_def in &selected_template.default_goals {
2469        let mut goal =
2470            Goal::new(goal_def.name.clone(), goal_def.target_hours).with_project(project_id);
2471        if let Some(desc) = &goal_def.description {
2472            goal = goal.with_description(desc.clone());
2473        }
2474        GoalQueries::create(&db.connection, &goal)?;
2475    }
2476
2477    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2478    println!("\x1b[36m│\x1b[0m    \x1b[1;37mProject Created from Template\x1b[0m          \x1b[36m│\x1b[0m");
2479    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2480    println!(
2481        "\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2482        truncate_string(&selected_template.name, 27)
2483    );
2484    println!(
2485        "\x1b[36m│\x1b[0m Project:   \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2486        truncate_string(&project_name, 27)
2487    );
2488    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2489    Ok(())
2490}
2491
2492// Workspace management functions
2493async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2494    match action {
2495        WorkspaceAction::Create {
2496            name,
2497            description,
2498            path,
2499        } => create_workspace(name, description, path).await,
2500        WorkspaceAction::List => list_workspaces().await,
2501        WorkspaceAction::AddProject { workspace, project } => {
2502            add_project_to_workspace(workspace, project).await
2503        }
2504        WorkspaceAction::RemoveProject { workspace, project } => {
2505            remove_project_from_workspace(workspace, project).await
2506        }
2507        WorkspaceAction::Projects { workspace } => list_workspace_projects(workspace).await,
2508        WorkspaceAction::Delete { workspace } => delete_workspace(workspace).await,
2509    }
2510}
2511
2512async fn create_workspace(
2513    name: String,
2514    description: Option<String>,
2515    path: Option<PathBuf>,
2516) -> Result<()> {
2517    let db_path = get_database_path()?;
2518    let db = Database::new(&db_path)?;
2519
2520    let mut workspace = Workspace::new(name.clone());
2521    let desc_clone = description.clone();
2522    if let Some(desc) = description {
2523        workspace = workspace.with_description(desc);
2524    }
2525    if let Some(p) = path {
2526        workspace = workspace.with_path(p);
2527    }
2528
2529    let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2530
2531    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2532    println!("\x1b[36m│\x1b[0m        \x1b[1;37mWorkspace Created\x1b[0m                  \x1b[36m│\x1b[0m");
2533    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2534    println!(
2535        "\x1b[36m│\x1b[0m Name:     \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2536        truncate_string(&name, 27)
2537    );
2538    if let Some(desc) = &desc_clone {
2539        println!(
2540            "\x1b[36m│\x1b[0m Desc:     \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2541            truncate_string(desc, 27)
2542        );
2543    }
2544    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2545    Ok(())
2546}
2547
2548async fn list_workspaces() -> Result<()> {
2549    let db_path = get_database_path()?;
2550    let db = Database::new(&db_path)?;
2551
2552    let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2553
2554    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2555    println!("\x1b[36m│\x1b[0m          \x1b[1;37mWorkspaces\x1b[0m                      \x1b[36m│\x1b[0m");
2556    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2557
2558    if workspaces.is_empty() {
2559        println!("\x1b[36m│\x1b[0m No workspaces found.                     \x1b[36m│\x1b[0m");
2560    } else {
2561        for workspace in &workspaces {
2562            println!(
2563                "\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2564                truncate_string(&workspace.name, 25)
2565            );
2566            if let Some(desc) = &workspace.description {
2567                println!(
2568                    "\x1b[36m│\x1b[0m    \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2569                    truncate_string(desc, 27)
2570                );
2571            }
2572            println!("\x1b[36m│\x1b[0m                                         \x1b[36m│\x1b[0m");
2573        }
2574    }
2575
2576    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2577    Ok(())
2578}
2579
2580async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2581    let db_path = get_database_path()?;
2582    let db = Database::new(&db_path)?;
2583
2584    // Find workspace by name
2585    let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2586        .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2587
2588    // Find project by name
2589    let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2590        .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2591
2592    let workspace_id = workspace_obj
2593        .id
2594        .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2595    let project_id = project_obj
2596        .id
2597        .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2598
2599    if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2600        println!(
2601            "\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'",
2602            project, workspace
2603        );
2604    } else {
2605        println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2606    }
2607
2608    Ok(())
2609}
2610
2611async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2612    let db_path = get_database_path()?;
2613    let db = Database::new(&db_path)?;
2614
2615    // Find workspace by name
2616    let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2617        .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2618
2619    // Find project by name
2620    let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2621        .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2622
2623    let workspace_id = workspace_obj
2624        .id
2625        .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2626    let project_id = project_obj
2627        .id
2628        .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2629
2630    if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2631        println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2632    } else {
2633        println!(
2634            "\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'",
2635            project, workspace
2636        );
2637    }
2638
2639    Ok(())
2640}
2641
2642async fn list_workspace_projects(workspace: String) -> Result<()> {
2643    let db_path = get_database_path()?;
2644    let db = Database::new(&db_path)?;
2645
2646    // Find workspace by name
2647    let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2648        .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2649
2650    let workspace_id = workspace_obj
2651        .id
2652        .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2653    let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2654
2655    if projects.is_empty() {
2656        println!(
2657            "\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'",
2658            workspace
2659        );
2660        return Ok(());
2661    }
2662
2663    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2664    println!("\x1b[36m│\x1b[0m        \x1b[1;37mWorkspace Projects\x1b[0m               \x1b[36m│\x1b[0m");
2665    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2666    println!(
2667        "\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2668        truncate_string(&workspace, 25)
2669    );
2670    println!(
2671        "\x1b[36m│\x1b[0m Projects:  \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2672        format!("{} projects", projects.len())
2673    );
2674    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2675
2676    for project in &projects {
2677        let status_indicator = if !project.is_archived {
2678            "\x1b[32m●\x1b[0m"
2679        } else {
2680            "\x1b[31m○\x1b[0m"
2681        };
2682        println!(
2683            "\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2684            status_indicator,
2685            truncate_string(&project.name, 33)
2686        );
2687    }
2688
2689    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2690    Ok(())
2691}
2692
2693async fn delete_workspace(workspace: String) -> Result<()> {
2694    let db_path = get_database_path()?;
2695    let db = Database::new(&db_path)?;
2696
2697    // Find workspace by name
2698    let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2699        .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2700
2701    let workspace_id = workspace_obj
2702        .id
2703        .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2704
2705    // Check if workspace has projects
2706    let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2707    if !projects.is_empty() {
2708        println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.", 
2709                workspace, projects.len());
2710        return Ok(());
2711    }
2712
2713    if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2714        println!(
2715            "\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'",
2716            workspace
2717        );
2718    } else {
2719        println!(
2720            "\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'",
2721            workspace
2722        );
2723    }
2724
2725    Ok(())
2726}
2727
2728// Calendar integration functions
2729async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2730    match action {
2731        CalendarAction::Add {
2732            name,
2733            start,
2734            end,
2735            event_type,
2736            project,
2737            description,
2738        } => add_calendar_event(name, start, end, event_type, project, description).await,
2739        CalendarAction::List { from, to, project } => list_calendar_events(from, to, project).await,
2740        CalendarAction::Delete { id } => delete_calendar_event(id).await,
2741    }
2742}
2743
2744async fn add_calendar_event(
2745    _name: String,
2746    _start: String,
2747    _end: Option<String>,
2748    _event_type: Option<String>,
2749    _project: Option<String>,
2750    _description: Option<String>,
2751) -> Result<()> {
2752    println!("\x1b[33m⚠  Calendar integration in development\x1b[0m");
2753    Ok(())
2754}
2755
2756async fn list_calendar_events(
2757    _from: Option<String>,
2758    _to: Option<String>,
2759    _project: Option<String>,
2760) -> Result<()> {
2761    println!("\x1b[33m⚠  Calendar integration in development\x1b[0m");
2762    Ok(())
2763}
2764
2765async fn delete_calendar_event(_id: i64) -> Result<()> {
2766    println!("\x1b[33m⚠  Calendar integration in development\x1b[0m");
2767    Ok(())
2768}
2769
2770// Issue tracker integration functions
2771async fn handle_issue_action(action: IssueAction) -> Result<()> {
2772    match action {
2773        IssueAction::Sync {
2774            project,
2775            tracker_type,
2776        } => sync_issues(project, tracker_type).await,
2777        IssueAction::List { project, status } => list_issues(project, status).await,
2778        IssueAction::Link {
2779            session_id,
2780            issue_id,
2781        } => link_session_to_issue(session_id, issue_id).await,
2782    }
2783}
2784
2785async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2786    println!("\x1b[33m⚠  Issue tracker integration in development\x1b[0m");
2787    Ok(())
2788}
2789
2790async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2791    println!("\x1b[33m⚠  Issue tracker integration in development\x1b[0m");
2792    Ok(())
2793}
2794
2795async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2796    println!("\x1b[33m⚠  Issue tracker integration in development\x1b[0m");
2797    Ok(())
2798}
2799
2800// Client reporting functions
2801async fn handle_client_action(action: ClientAction) -> Result<()> {
2802    match action {
2803        ClientAction::Generate {
2804            client,
2805            from,
2806            to,
2807            projects,
2808            format,
2809        } => generate_client_report(client, from, to, projects, format).await,
2810        ClientAction::List { client } => list_client_reports(client).await,
2811        ClientAction::View { id } => view_client_report(id).await,
2812    }
2813}
2814
2815async fn generate_client_report(
2816    _client: String,
2817    _from: String,
2818    _to: String,
2819    _projects: Option<String>,
2820    _format: Option<String>,
2821) -> Result<()> {
2822    println!("\x1b[33m⚠  Client reporting in development\x1b[0m");
2823    Ok(())
2824}
2825
2826async fn list_client_reports(_client: Option<String>) -> Result<()> {
2827    println!("\x1b[33m⚠  Client reporting in development\x1b[0m");
2828    Ok(())
2829}
2830
2831async fn view_client_report(_id: i64) -> Result<()> {
2832    println!("\x1b[33m⚠  Client reporting in development\x1b[0m");
2833    Ok(())
2834}
2835
2836fn should_quit(event: crossterm::event::Event) -> bool {
2837    match event {
2838        crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
2839            matches!(
2840                key.code,
2841                crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc
2842            )
2843        }
2844        _ => false,
2845    }
2846}
2847
2848// Helper function for init_project with database connection
2849async fn init_project_with_db(
2850    name: Option<String>,
2851    canonical_path: Option<PathBuf>,
2852    description: Option<String>,
2853    conn: &rusqlite::Connection,
2854) -> Result<()> {
2855    let canonical_path =
2856        canonical_path.ok_or_else(|| anyhow::anyhow!("Canonical path required"))?;
2857    let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
2858
2859    // Check if project already exists
2860    if let Some(existing) = ProjectQueries::find_by_path(conn, &canonical_path)? {
2861        println!(
2862            "\x1b[33m⚠  Project already exists:\x1b[0m {}",
2863            existing.name
2864        );
2865        return Ok(());
2866    }
2867
2868    // Get git hash if it's a git repository
2869    let git_hash = if is_git_repository(&canonical_path) {
2870        get_git_hash(&canonical_path)
2871    } else {
2872        None
2873    };
2874
2875    // Create project
2876    let mut project = Project::new(project_name.clone(), canonical_path.clone())
2877        .with_git_hash(git_hash.clone())
2878        .with_description(description.clone());
2879
2880    // Save to database
2881    let project_id = ProjectQueries::create(conn, &project)?;
2882    project.id = Some(project_id);
2883
2884    // Create .tempo marker file
2885    let marker_path = canonical_path.join(".tempo");
2886    if !marker_path.exists() {
2887        std::fs::write(
2888            &marker_path,
2889            format!("# Tempo time tracking project\nname: {}\n", project_name),
2890        )?;
2891    }
2892
2893    println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2894    println!("\x1b[36m│\x1b[0m         \x1b[1;37mProject Initialized\x1b[0m               \x1b[36m│\x1b[0m");
2895    println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2896    println!(
2897        "\x1b[36m│\x1b[0m Name:        \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2898        truncate_string(&project_name, 25)
2899    );
2900    println!(
2901        "\x1b[36m│\x1b[0m Path:        \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2902        truncate_string(&canonical_path.display().to_string(), 25)
2903    );
2904
2905    if let Some(desc) = &description {
2906        println!(
2907            "\x1b[36m│\x1b[0m Description: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2908            truncate_string(desc, 25)
2909        );
2910    }
2911
2912    if is_git_repository(&canonical_path) {
2913        println!(
2914            "\x1b[36m│\x1b[0m Git:         \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2915            "Repository detected"
2916        );
2917        if let Some(hash) = &git_hash {
2918            println!(
2919                "\x1b[36m│\x1b[0m Git Hash:    \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2920                truncate_string(hash, 25)
2921            );
2922        }
2923    }
2924
2925    println!(
2926        "\x1b[36m│\x1b[0m ID:          \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2927        project_id
2928    );
2929    println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2930
2931    Ok(())
2932}
2933
2934// Show database connection pool statistics
2935async fn show_pool_stats() -> Result<()> {
2936    match get_pool_stats() {
2937        Ok(stats) => {
2938            println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2939            println!("\x1b[36m│\x1b[0m        \x1b[1;37mDatabase Pool Statistics\x1b[0m          \x1b[36m│\x1b[0m");
2940            println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2941            println!(
2942                "\x1b[36m│\x1b[0m Total Created:    \x1b[32m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
2943                stats.total_connections_created
2944            );
2945            println!(
2946                "\x1b[36m│\x1b[0m Active:           \x1b[33m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
2947                stats.active_connections
2948            );
2949            println!(
2950                "\x1b[36m│\x1b[0m Available in Pool:\x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
2951                stats.connections_in_pool
2952            );
2953            println!(
2954                "\x1b[36m│\x1b[0m Total Requests:   \x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
2955                stats.connection_requests
2956            );
2957            println!(
2958                "\x1b[36m│\x1b[0m Timeouts:         \x1b[31m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
2959                stats.connection_timeouts
2960            );
2961            println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2962        }
2963        Err(_) => {
2964            println!("\x1b[33m⚠  Database pool not initialized or not available\x1b[0m");
2965            println!("   Using direct database connections as fallback");
2966        }
2967    }
2968    Ok(())
2969}
2970
2971#[derive(Deserialize)]
2972struct GitHubRelease {
2973    tag_name: String,
2974    name: String,
2975    body: String,
2976    published_at: String,
2977    prerelease: bool,
2978}
2979
2980async fn handle_update(check: bool, force: bool, verbose: bool) -> Result<()> {
2981    let current_version = env!("CARGO_PKG_VERSION");
2982    
2983    if verbose {
2984        println!("🔍 Current version: v{}", current_version);
2985        println!("📡 Checking for updates...");
2986    } else {
2987        println!("🔍 Checking for updates...");
2988    }
2989
2990    // Fetch latest release information from GitHub
2991    let client = reqwest::Client::new();
2992    let response = client
2993        .get("https://api.github.com/repos/own-path/vibe/releases/latest")
2994        .header("User-Agent", format!("tempo-cli/{}", current_version))
2995        .send()
2996        .await
2997        .context("Failed to fetch release information")?;
2998
2999    if !response.status().is_success() {
3000        return Err(anyhow::anyhow!(
3001            "Failed to fetch release information: HTTP {}",
3002            response.status()
3003        ));
3004    }
3005
3006    let release: GitHubRelease = response
3007        .json()
3008        .await
3009        .context("Failed to parse release information")?;
3010
3011    let latest_version = release.tag_name.trim_start_matches('v');
3012    
3013    if verbose {
3014        println!("📦 Latest version: v{}", latest_version);
3015        println!("📅 Released: {}", release.published_at);
3016    }
3017
3018    // Compare versions
3019    let current_semver = semver::Version::parse(current_version)
3020        .context("Failed to parse current version")?;
3021    let latest_semver = semver::Version::parse(latest_version)
3022        .context("Failed to parse latest version")?;
3023
3024    if current_semver >= latest_semver && !force {
3025        println!("✅ You're already running the latest version (v{})", current_version);
3026        if check {
3027            return Ok(());
3028        }
3029        
3030        if !force {
3031            println!("💡 Use --force to reinstall the current version");
3032            return Ok(());
3033        }
3034    }
3035
3036    if check {
3037        if current_semver < latest_semver {
3038            println!("📦 Update available: v{} → v{}", current_version, latest_version);
3039            println!("🔗 Run `tempo update` to install the latest version");
3040            
3041            if verbose && !release.body.is_empty() {
3042                println!("\n📝 Release Notes:");
3043                println!("{}", release.body);
3044            }
3045        }
3046        return Ok(());
3047    }
3048
3049    if current_semver < latest_semver || force {
3050        println!("⬇️  Updating tempo from v{} to v{}", current_version, latest_version);
3051        
3052        if verbose {
3053            println!("🔧 Installing via cargo...");
3054        }
3055        
3056        // Update using cargo install
3057        let mut cmd = Command::new("cargo");
3058        cmd.args(&["install", "tempo-cli", "--force"]);
3059        
3060        if verbose {
3061            cmd.stdout(Stdio::inherit())
3062               .stderr(Stdio::inherit());
3063        } else {
3064            cmd.stdout(Stdio::null())
3065               .stderr(Stdio::null());
3066        }
3067
3068        let status = cmd.status()
3069            .context("Failed to run cargo install command")?;
3070
3071        if status.success() {
3072            println!("✅ Successfully updated tempo to v{}", latest_version);
3073            println!("🎉 You can now use the latest features!");
3074            
3075            if !release.body.is_empty() && verbose {
3076                println!("\n📝 What's new in v{}:", latest_version);
3077                println!("{}", release.body);
3078            }
3079        } else {
3080            return Err(anyhow::anyhow!("Failed to install update. Try running manually: cargo install tempo-cli --force"));
3081        }
3082    }
3083
3084    Ok(())
3085}