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