Skip to main content

cove_cli/commands/
start.rs

1use std::io::{self, Write};
2use std::path::PathBuf;
3
4use crate::colors::*;
5use crate::commands::init;
6use crate::sidebar::state;
7use crate::tmux;
8
9// ── Helpers ──
10
11fn resolve_sidebar_bin() -> String {
12    // Try to find the binary we're running from (works after `cargo install` or symlink)
13    if let Ok(exe) = std::env::current_exe()
14        && let Ok(canonical) = std::fs::canonicalize(exe)
15    {
16        return canonical.to_string_lossy().to_string();
17    }
18    // Fallback to the expected install location
19    let home = std::env::var("HOME").unwrap_or_default();
20    format!("{home}/.local/bin/cove")
21}
22
23fn settings_path() -> PathBuf {
24    let home = std::env::var("HOME").unwrap_or_default();
25    PathBuf::from(home).join(".claude").join("settings.json")
26}
27
28/// Prompt user to install or update hooks if needed.
29fn check_hooks() {
30    let path = settings_path();
31    if init::hooks_installed(&path) {
32        return;
33    }
34
35    let bin = resolve_sidebar_bin();
36    let stale = init::has_stale_hooks(&path, &bin);
37
38    if stale {
39        println!(
40            "{ANSI_PEACH}Warning:{ANSI_RESET} Cove hooks point to an old binary path.\n\
41             Status indicators (spinner, waiting) won't work until hooks are updated.\n"
42        );
43        print!("Update hook paths? [Y/n] ");
44    } else {
45        println!(
46            "Cove needs Claude Code hooks to show session status (Working/Idle/Asking).\n\
47             This adds async hooks to ~/.claude/settings.json:\n\
48             {ANSI_PEACH}  UserPromptSubmit{ANSI_RESET}  detects when you send a message\n\
49             {ANSI_PEACH}  Stop{ANSI_RESET}              detects when Claude finishes responding\n"
50        );
51        print!("Add Cove hooks? [Y/n] ");
52    }
53    let _ = io::stdout().flush();
54
55    let mut input = String::new();
56    if io::stdin().read_line(&mut input).is_err() {
57        return;
58    }
59
60    let answer = input.trim().to_lowercase();
61    if answer.is_empty() || answer == "y" || answer == "yes" {
62        match init::install_hooks(&path) {
63            Ok(()) if stale => println!("Hooks updated.\n"),
64            Ok(()) => println!("Hooks installed.\n"),
65            Err(e) => eprintln!("Failed to install hooks: {e}\n"),
66        }
67    } else {
68        println!("Skipped. Run `cove init` later to enable status indicators.\n");
69    }
70}
71
72// ── Public API ──
73
74pub fn run(name: &str, base: &str, dir: Option<&str>) -> Result<(), String> {
75    let dir = dir.unwrap_or(".");
76    let dir = std::fs::canonicalize(dir)
77        .map_err(|e| format!("invalid directory '{dir}': {e}"))?
78        .to_string_lossy()
79        .to_string();
80
81    let sidebar_bin = resolve_sidebar_bin();
82    let sidebar_cmd = format!("{sidebar_bin} sidebar");
83
84    // First-run: prompt to install hooks if needed
85    check_hooks();
86
87    if tmux::has_session() {
88        // Reject duplicate window names
89        let names = tmux::list_window_names()?;
90        if names.iter().any(|n| n == name) {
91            return Err(format!(
92                "Session '{ANSI_PEACH}{name}{ANSI_RESET}' already exists. Pick a different name."
93            ));
94        }
95
96        tmux::new_window(name, &dir)?;
97        tmux::setup_layout(name, &dir, &sidebar_cmd)?;
98
99        // Store base name so hooks can recompute the window name on branch changes
100        let _ = tmux::set_window_option(name, "@cove_base", base);
101
102        // Purge stale event files that match this pane's recycled ID
103        if let Ok(pane_id) = tmux::get_claude_pane_id(name) {
104            state::purge_events_for_pane(&pane_id);
105        }
106
107        // If outside tmux, attach so the user sees it
108        if !tmux::is_inside_tmux() {
109            tmux::attach()?;
110        }
111    } else {
112        // No session — create from scratch. Must run outside tmux for proper dimensions.
113        if tmux::is_inside_tmux() {
114            return Err(format!(
115                "No cove session exists. Run from outside tmux first:\n  \
116                 {ANSI_PEACH}cove{ANSI_RESET} {name} {dir}"
117            ));
118        }
119
120        tmux::new_session(name, &dir, &sidebar_cmd)?;
121
122        // Store base name so hooks can recompute the window name on branch changes
123        let _ = tmux::set_window_option(name, "@cove_base", base);
124
125        // Purge stale event files that match this pane's recycled ID.
126        // new_session creates detached (-d), so this runs before the user sees anything.
127        if let Ok(pane_id) = tmux::get_claude_pane_id(name) {
128            state::purge_events_for_pane(&pane_id);
129        }
130
131        tmux::attach()?;
132    }
133
134    Ok(())
135}