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