scud/commands/spawn/
hooks.rs1use anyhow::{Context, Result};
8use serde_json::{json, Value};
9use std::fs;
10use std::path::Path;
11
12pub fn hooks_installed(project_root: &Path) -> bool {
14 let settings_path = project_root.join(".claude").join("settings.local.json");
15
16 if !settings_path.exists() {
17 return false;
18 }
19
20 match fs::read_to_string(&settings_path) {
21 Ok(content) => {
22 if let Ok(settings) = serde_json::from_str::<Value>(&content) {
23 settings
25 .get("hooks")
26 .and_then(|h| h.get("Stop"))
27 .and_then(|s| s.as_array())
28 .map(|arr| {
29 arr.iter().any(|hook| {
30 hook.get("hooks")
31 .and_then(|h| h.as_array())
32 .map(|cmds| {
33 cmds.iter().any(|cmd| {
34 cmd.get("command")
35 .and_then(|c| c.as_str())
36 .map(|s| s.contains("scud") && s.contains("set-status"))
37 .unwrap_or(false)
38 })
39 })
40 .unwrap_or(false)
41 })
42 })
43 .unwrap_or(false)
44 } else {
45 false
46 }
47 }
48 Err(_) => false,
49 }
50}
51
52pub fn install_hooks(project_root: &Path) -> Result<()> {
54 let claude_dir = project_root.join(".claude");
55 let settings_path = claude_dir.join("settings.local.json");
56
57 fs::create_dir_all(&claude_dir).context("Failed to create .claude directory")?;
59
60 let mut settings: Value = if settings_path.exists() {
62 let content = fs::read_to_string(&settings_path)?;
63 serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
64 } else {
65 json!({})
66 };
67
68 let stop_hook = json!([
73 {
74 "matcher": "",
75 "hooks": [
76 {
77 "type": "command",
78 "command": "bash -c 'if [ -n \"$SCUD_TASK_ID\" ]; then scud set-status \"$SCUD_TASK_ID\" done 2>/dev/null || true; fi'",
81 "timeout": 10
82 }
83 ]
84 }
85 ]);
86
87 let hooks = settings.get("hooks").cloned().unwrap_or_else(|| json!({}));
89
90 let mut hooks_obj = hooks.as_object().cloned().unwrap_or_default();
91 hooks_obj.insert("Stop".to_string(), stop_hook);
92
93 settings["hooks"] = json!(hooks_obj);
94
95 let content = serde_json::to_string_pretty(&settings)?;
97 fs::write(&settings_path, content)?;
98
99 Ok(())
100}
101
102pub fn uninstall_hooks(project_root: &Path) -> Result<()> {
104 let settings_path = project_root.join(".claude").join("settings.local.json");
105
106 if !settings_path.exists() {
107 return Ok(());
108 }
109
110 let content = fs::read_to_string(&settings_path)?;
111 let mut settings: Value = serde_json::from_str(&content)?;
112
113 if let Some(hooks) = settings.get_mut("hooks") {
115 if let Some(hooks_obj) = hooks.as_object_mut() {
116 if let Some(stop) = hooks_obj.get("Stop") {
118 let is_ours = stop
119 .as_array()
120 .map(|arr| {
121 arr.iter().any(|h| {
122 h.get("hooks")
123 .and_then(|cmds| cmds.as_array())
124 .map(|cmds| {
125 cmds.iter().any(|cmd| {
126 cmd.get("command")
127 .and_then(|c| c.as_str())
128 .map(|s| s.contains("SCUD_TASK_ID"))
129 .unwrap_or(false)
130 })
131 })
132 .unwrap_or(false)
133 })
134 })
135 .unwrap_or(false);
136
137 if is_ours {
138 hooks_obj.remove("Stop");
139 }
140 }
141 }
142 }
143
144 let content = serde_json::to_string_pretty(&settings)?;
145 fs::write(&settings_path, content)?;
146
147 Ok(())
148}
149
150pub fn agent_env_setup(task_id: &str) -> String {
153 format!("export SCUD_TASK_ID=\"{}\"", task_id)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use tempfile::TempDir;
160
161 #[test]
162 fn test_hooks_not_installed_missing_file() {
163 let tmp = TempDir::new().unwrap();
164 assert!(!hooks_installed(tmp.path()));
165 }
166
167 #[test]
168 fn test_install_hooks_creates_settings() {
169 let tmp = TempDir::new().unwrap();
170
171 install_hooks(tmp.path()).unwrap();
172
173 let settings_path = tmp.path().join(".claude").join("settings.local.json");
174 assert!(settings_path.exists());
175
176 let content = fs::read_to_string(&settings_path).unwrap();
177 assert!(content.contains("SCUD_TASK_ID"));
178 assert!(content.contains("scud"));
179 }
180
181 #[test]
182 fn test_hooks_installed_detects_our_hook() {
183 let tmp = TempDir::new().unwrap();
184
185 install_hooks(tmp.path()).unwrap();
186 assert!(hooks_installed(tmp.path()));
187 }
188
189 #[test]
190 fn test_uninstall_hooks() {
191 let tmp = TempDir::new().unwrap();
192
193 install_hooks(tmp.path()).unwrap();
194 assert!(hooks_installed(tmp.path()));
195
196 uninstall_hooks(tmp.path()).unwrap();
197 assert!(!hooks_installed(tmp.path()));
198 }
199
200 #[test]
201 fn test_agent_env_setup() {
202 let env = agent_env_setup("auth:5");
203 assert_eq!(env, "export SCUD_TASK_ID=\"auth:5\"");
204 }
205}