1use std::path::{Path, PathBuf};
7
8use serde_json::{json, Value};
9
10use crate::detection;
11use crate::{AdapterError, Result, ToolAdapter};
12
13const HOOKS_REL: &str = ".cursor/hooks.json";
14
15const EVENTS: &[&str] = &[
16 "stop",
17 "beforeSubmitPrompt",
18 "preToolUse",
19 "postToolUse",
20 "subagentStart",
21 "subagentStop",
22];
23
24pub struct CursorAdapter;
25
26impl CursorAdapter {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn hooks_path(&self) -> Option<PathBuf> {
32 detection::home_path(HOOKS_REL)
33 }
34}
35
36impl ToolAdapter for CursorAdapter {
37 fn name(&self) -> &str {
38 "cursor"
39 }
40
41 fn display_name(&self) -> &str {
42 "Cursor"
43 }
44
45 fn is_installed(&self) -> bool {
46 detection::home_dir_exists(".cursor") || detection::command_exists("cursor")
47 }
48
49 fn hooks_registered(&self) -> bool {
50 let Some(path) = self.hooks_path() else {
51 return false;
52 };
53 let Ok(content) = std::fs::read_to_string(&path) else {
54 return false;
55 };
56 content.contains("hook_event_bridge")
57 }
58
59 fn register_hooks(&self, bridge_script: &Path) -> Result<()> {
60 let path = self.hooks_path().ok_or(AdapterError::NoHomeDir)?;
61 detection::ensure_parent_dir(&path)?;
62
63 let mut root: Value = if path.exists() {
64 let content = std::fs::read_to_string(&path)?;
65 serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
66 } else {
67 json!({})
68 };
69
70 let obj = root
71 .as_object_mut()
72 .ok_or_else(|| AdapterError::Config("Invalid hooks.json".into()))?;
73
74 let cmd = bridge_script.to_string_lossy().to_string();
75
76 for event in EVENTS {
77 let arr = obj.entry(*event).or_insert_with(|| json!([]));
78 if !arr.is_array() {
79 *arr = json!([]);
80 }
81 let hooks = arr.as_array_mut().unwrap();
82
83 let already = hooks.iter().any(|h| {
84 h.get("command")
85 .and_then(|c| c.as_str())
86 .is_some_and(|c| c == cmd)
87 });
88
89 if !already {
90 hooks.push(json!({ "command": cmd }));
91 }
92 }
93
94 detection::backup_file(&path);
95 let output = serde_json::to_string_pretty(&root)?;
96 std::fs::write(&path, output)?;
97 Ok(())
98 }
99
100 fn unregister_hooks(&self) -> Result<()> {
101 let path = self.hooks_path().ok_or(AdapterError::NoHomeDir)?;
102 if !path.exists() {
103 return Ok(());
104 }
105
106 let content = std::fs::read_to_string(&path)?;
107 let mut root: Value = serde_json::from_str(&content)?;
108
109 let Some(obj) = root.as_object_mut() else {
110 return Ok(());
111 };
112
113 let mut modified = false;
114 for event in EVENTS {
115 if let Some(arr) = obj.get_mut(*event).and_then(|v| v.as_array_mut()) {
116 let before = arr.len();
117 arr.retain(|h| {
118 !h.get("command")
119 .and_then(|c| c.as_str())
120 .is_some_and(|c| c.contains("hook_event_bridge"))
121 });
122 if arr.len() != before {
123 modified = true;
124 }
125 }
126 }
127
128 if modified {
129 detection::backup_file(&path);
130 let output = serde_json::to_string_pretty(&root)?;
131 std::fs::write(&path, output)?;
132 }
133 Ok(())
134 }
135
136 fn config_path(&self) -> Option<PathBuf> {
137 self.hooks_path()
138 }
139
140 fn supported_events(&self) -> &[&str] {
141 EVENTS
142 }
143}