cove_cli/commands/
start.rs1use std::io::{self, Write};
2use std::path::PathBuf;
3
4use crate::colors::*;
5use crate::commands::init;
6use crate::sidebar::state;
7use crate::tmux;
8
9fn resolve_sidebar_bin() -> String {
12 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 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
28fn 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
72pub 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 check_hooks();
86
87 if tmux::has_session() {
88 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 let _ = tmux::set_window_option(name, "@cove_base", base);
101
102 if let Ok(pane_id) = tmux::get_claude_pane_id(name) {
104 state::purge_events_for_pane(&pane_id);
105 }
106
107 if !tmux::is_inside_tmux() {
109 tmux::attach()?;
110 }
111 } else {
112 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 let _ = tmux::set_window_option(name, "@cove_base", base);
124
125 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}