Skip to main content

git_warp/
hooks.rs

1use crate::error::Result;
2use serde_json::{Value, json};
3use std::path::PathBuf;
4use std::fs;
5
6pub struct HooksManager;
7
8impl HooksManager {
9    pub fn install_hooks(level: Option<&str>) -> Result<()> {
10        let hooks_config = Self::get_hooks_config();
11        
12        match level {
13            Some("console") | None => {
14                println!("Add this to your Claude Code settings:");
15                println!("{}", serde_json::to_string_pretty(&hooks_config)?);
16                Ok(())
17            }
18            Some("user") => {
19                let settings_path = Self::get_user_settings_path()?;
20                Self::merge_hooks_into_settings(settings_path, hooks_config)
21            }
22            Some("project") => {
23                let settings_path = Self::get_project_settings_path()?;
24                Self::merge_hooks_into_settings(settings_path, hooks_config)
25            }
26            _ => {
27                println!("Invalid level. Use: user, project, or console");
28                Ok(())
29            }
30        }
31    }
32    
33    pub fn remove_hooks(level: &str) -> Result<()> {
34        let settings_path = match level {
35            "user" => Self::get_user_settings_path()?,
36            "project" => Self::get_project_settings_path()?,
37            _ => {
38                println!("Invalid level. Use: user or project");
39                return Ok(());
40            }
41        };
42        
43        Self::remove_hooks_from_settings(settings_path)
44    }
45    
46    pub fn show_hooks_status() -> Result<()> {
47        println!("šŸ”§ Git-Warp Claude Code Integration Status");
48        println!("==========================================");
49        
50        // Check user level settings
51        match Self::get_user_settings_path() {
52            Ok(path) => {
53                if path.exists() {
54                    println!("āœ… User settings: {}", path.display());
55                    Self::show_hooks_for_path(&path)?;
56                } else {
57                    println!("āŒ User settings: Not found");
58                }
59            }
60            Err(_) => println!("āŒ User settings: Unable to locate"),
61        }
62        
63        // Check project level settings
64        match Self::get_project_settings_path() {
65            Ok(path) => {
66                if path.exists() {
67                    println!("āœ… Project settings: {}", path.display());
68                    Self::show_hooks_for_path(&path)?;
69                } else {
70                    println!("āŒ Project settings: Not found");
71                }
72            }
73            Err(_) => println!("āŒ Project settings: Unable to locate"),
74        }
75        
76        println!("\nšŸ“– Integration Guide:");
77        println!("   warp hooks-install user    # Install for all projects");
78        println!("   warp hooks-install project # Install for current project only");
79        println!("   warp hooks-install console # Show JSON to copy manually");
80        
81        Ok(())
82    }
83    
84    fn get_hooks_config() -> Value {
85        json!({
86            "hooks": {
87                "UserPromptSubmit": [{
88                    "hooks": [{
89                        "type": "command",
90                        "command": "ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && mkdir -p \"$ROOT/.claude/git-warp\" && echo \"{\\\"status\\\":\\\"processing\\\",\\\"last_activity\\\":\\\"$(date -Iseconds)\\\"}\" > \"$ROOT/.claude/git-warp/status\""
91                    }],
92                    "git_warp_hook_id": "agent_status_userpromptsubmit"
93                }],
94                "Stop": [{
95                    "hooks": [{
96                        "type": "command",
97                        "command": "ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && mkdir -p \"$ROOT/.claude/git-warp\" && echo \"{\\\"status\\\":\\\"waiting\\\",\\\"last_activity\\\":\\\"$(date -Iseconds)\\\"}\" > \"$ROOT/.claude/git-warp/status\""
98                    }],
99                    "git_warp_hook_id": "agent_status_stop"
100                }],
101                "PreToolUse": [{
102                    "hooks": [{
103                        "type": "command",
104                        "command": "ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && mkdir -p \"$ROOT/.claude/git-warp\" && echo \"{\\\"status\\\":\\\"working\\\",\\\"last_activity\\\":\\\"$(date -Iseconds)\\\"}\" > \"$ROOT/.claude/git-warp/status\""
105                    }],
106                    "git_warp_hook_id": "agent_status_pretooluse"
107                }],
108                "PostToolUse": [{
109                    "hooks": [{
110                        "type": "command",
111                        "command": "ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && mkdir -p \"$ROOT/.claude/git-warp\" && echo \"{\\\"status\\\":\\\"processing\\\",\\\"last_activity\\\":\\\"$(date -Iseconds)\\\"}\" > \"$ROOT/.claude/git-warp/status\""
112                    }],
113                    "git_warp_hook_id": "agent_status_posttooluse"
114                }],
115                "SubagentStop": [{
116                    "hooks": [{
117                        "type": "command",
118                        "command": "ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && mkdir -p \"$ROOT/.claude/git-warp\" && echo \"{\\\"status\\\":\\\"subagent_complete\\\",\\\"last_activity\\\":\\\"$(date -Iseconds)\\\"}\" > \"$ROOT/.claude/git-warp/status\""
119                    }],
120                    "git_warp_hook_id": "agent_status_subagent_stop"
121                }]
122            }
123        })
124    }
125    
126    fn get_user_settings_path() -> Result<PathBuf> {
127        let home = dirs::home_dir()
128            .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
129        Ok(home.join(".claude").join("settings.json"))
130    }
131    
132    fn get_project_settings_path() -> Result<PathBuf> {
133        let current_dir = std::env::current_dir()?;
134        Ok(current_dir.join(".claude").join("settings.json"))
135    }
136    
137    fn merge_hooks_into_settings(settings_path: PathBuf, hooks_config: Value) -> Result<()> {
138        // Create directory if it doesn't exist
139        if let Some(parent) = settings_path.parent() {
140            fs::create_dir_all(parent)?;
141        }
142        
143        // Load existing settings or create new
144        let mut settings: Value = if settings_path.exists() {
145            let content = fs::read_to_string(&settings_path)?;
146            serde_json::from_str(&content)?
147        } else {
148            json!({})
149        };
150        
151        // Merge hooks
152        if let Some(hooks) = hooks_config.get("hooks") {
153            settings["hooks"] = hooks.clone();
154        }
155        
156        // Write back
157        let content = serde_json::to_string_pretty(&settings)?;
158        fs::write(&settings_path, content)?;
159        
160        println!("Hooks installed to: {}", settings_path.display());
161        Ok(())
162    }
163    
164    fn remove_hooks_from_settings(settings_path: PathBuf) -> Result<()> {
165        if !settings_path.exists() {
166            println!("Settings file not found: {}", settings_path.display());
167            return Ok(());
168        }
169        
170        let content = fs::read_to_string(&settings_path)?;
171        let mut settings: Value = serde_json::from_str(&content)?;
172        
173        // Remove git-warp hooks
174        if let Some(hooks) = settings.get_mut("hooks") {
175            if let Some(hooks_obj) = hooks.as_object_mut() {
176                for (_, hook_array) in hooks_obj.iter_mut() {
177                    if let Some(array) = hook_array.as_array_mut() {
178                        array.retain(|hook| {
179                            !hook.get("git_warp_hook_id")
180                                .and_then(|id| id.as_str())
181                                .unwrap_or("")
182                                .starts_with("agent_status_")
183                        });
184                    }
185                }
186            }
187        }
188        
189        let content = serde_json::to_string_pretty(&settings)?;
190        fs::write(&settings_path, content)?;
191        
192        println!("Hooks removed from: {}", settings_path.display());
193        Ok(())
194    }
195    
196    fn show_hooks_for_path(path: &PathBuf) -> Result<()> {
197        if path.exists() {
198            let content = fs::read_to_string(path)?;
199            let settings: Value = serde_json::from_str(&content)?;
200            
201            let mut found_hooks = false;
202            if let Some(hooks) = settings.get("hooks") {
203                if let Some(hooks_obj) = hooks.as_object() {
204                    for (hook_type, hook_array) in hooks_obj {
205                        if let Some(array) = hook_array.as_array() {
206                            let git_warp_hooks: Vec<_> = array.iter()
207                                .filter(|hook| {
208                                    hook.get("git_warp_hook_id")
209                                        .and_then(|id| id.as_str())
210                                        .unwrap_or("")
211                                        .starts_with("agent_status_")
212                                })
213                                .collect();
214                            
215                            if !git_warp_hooks.is_empty() {
216                                if !found_hooks {
217                                    println!("  āœ“ Hooks installed:");
218                                    found_hooks = true;
219                                }
220                                println!("    {}: {} git-warp hook(s)", hook_type, git_warp_hooks.len());
221                            }
222                        }
223                    }
224                }
225            }
226            
227            if !found_hooks {
228                println!("  No git-warp hooks installed");
229            }
230        } else {
231            println!("  No settings file found");
232        }
233        Ok(())
234    }
235}
236
237// Add dirs crate dependency for home directory
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use tempfile::tempdir;
242    
243    #[test]
244    fn test_hooks_config_generation() {
245        let config = HooksManager::get_hooks_config();
246        assert!(config.get("hooks").is_some());
247        
248        let hooks = &config["hooks"];
249        assert!(hooks.get("UserPromptSubmit").is_some());
250        assert!(hooks.get("Stop").is_some());
251        assert!(hooks.get("PreToolUse").is_some());
252        assert!(hooks.get("PostToolUse").is_some());
253        assert!(hooks.get("SubagentStop").is_some());
254    }
255}