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