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