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