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)
59 .context("Failed to create .claude directory")?;
60
61 let mut settings: Value = if settings_path.exists() {
63 let content = fs::read_to_string(&settings_path)?;
64 serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
65 } else {
66 json!({})
67 };
68
69 let stop_hook = json!([
74 {
75 "matcher": "",
76 "hooks": [
77 {
78 "type": "command",
79 "command": "bash -c 'if [ -n \"$SCUD_TASK_ID\" ]; then scud set-status \"$SCUD_TASK_ID\" done 2>/dev/null || true; fi'",
82 "timeout": 10
83 }
84 ]
85 }
86 ]);
87
88 let hooks = settings
90 .get("hooks")
91 .cloned()
92 .unwrap_or_else(|| json!({}));
93
94 let mut hooks_obj = hooks.as_object().cloned().unwrap_or_default();
95 hooks_obj.insert("Stop".to_string(), stop_hook);
96
97 settings["hooks"] = json!(hooks_obj);
98
99 let content = serde_json::to_string_pretty(&settings)?;
101 fs::write(&settings_path, content)?;
102
103 Ok(())
104}
105
106pub fn uninstall_hooks(project_root: &Path) -> Result<()> {
108 let settings_path = project_root.join(".claude").join("settings.local.json");
109
110 if !settings_path.exists() {
111 return Ok(());
112 }
113
114 let content = fs::read_to_string(&settings_path)?;
115 let mut settings: Value = serde_json::from_str(&content)?;
116
117 if let Some(hooks) = settings.get_mut("hooks") {
119 if let Some(hooks_obj) = hooks.as_object_mut() {
120 if let Some(stop) = hooks_obj.get("Stop") {
122 let is_ours = stop
123 .as_array()
124 .map(|arr| {
125 arr.iter().any(|h| {
126 h.get("hooks")
127 .and_then(|cmds| cmds.as_array())
128 .map(|cmds| {
129 cmds.iter().any(|cmd| {
130 cmd.get("command")
131 .and_then(|c| c.as_str())
132 .map(|s| s.contains("SCUD_TASK_ID"))
133 .unwrap_or(false)
134 })
135 })
136 .unwrap_or(false)
137 })
138 })
139 .unwrap_or(false);
140
141 if is_ours {
142 hooks_obj.remove("Stop");
143 }
144 }
145 }
146 }
147
148 let content = serde_json::to_string_pretty(&settings)?;
149 fs::write(&settings_path, content)?;
150
151 Ok(())
152}
153
154pub fn agent_env_setup(task_id: &str) -> String {
157 format!("export SCUD_TASK_ID=\"{}\"", task_id)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use tempfile::TempDir;
164
165 #[test]
166 fn test_hooks_not_installed_missing_file() {
167 let tmp = TempDir::new().unwrap();
168 assert!(!hooks_installed(tmp.path()));
169 }
170
171 #[test]
172 fn test_install_hooks_creates_settings() {
173 let tmp = TempDir::new().unwrap();
174
175 install_hooks(tmp.path()).unwrap();
176
177 let settings_path = tmp.path().join(".claude").join("settings.local.json");
178 assert!(settings_path.exists());
179
180 let content = fs::read_to_string(&settings_path).unwrap();
181 assert!(content.contains("SCUD_TASK_ID"));
182 assert!(content.contains("scud"));
183 }
184
185 #[test]
186 fn test_hooks_installed_detects_our_hook() {
187 let tmp = TempDir::new().unwrap();
188
189 install_hooks(tmp.path()).unwrap();
190 assert!(hooks_installed(tmp.path()));
191 }
192
193 #[test]
194 fn test_uninstall_hooks() {
195 let tmp = TempDir::new().unwrap();
196
197 install_hooks(tmp.path()).unwrap();
198 assert!(hooks_installed(tmp.path()));
199
200 uninstall_hooks(tmp.path()).unwrap();
201 assert!(!hooks_installed(tmp.path()));
202 }
203
204 #[test]
205 fn test_agent_env_setup() {
206 let env = agent_env_setup("auth:5");
207 assert_eq!(env, "export SCUD_TASK_ID=\"auth:5\"");
208 }
209}