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