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 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 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 if let Some(parent) = settings_path.parent() {
140 fs::create_dir_all(parent)?;
141 }
142
143 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 if let Some(hooks) = hooks_config.get("hooks") {
153 settings["hooks"] = hooks.clone();
154 }
155
156 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 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#[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}