cli_denoiser/hooks/
codex.rs1use std::path::PathBuf;
2
3use super::{InstallResult, InstallStatus};
4
5const HOOK_MARKER: &str = "cli-denoiser";
6
7fn config_path() -> PathBuf {
9 if let Some(config_dir) = dirs::config_dir() {
11 let xdg_path = config_dir.join("codex").join("config.json");
12 if xdg_path.exists() {
13 return xdg_path;
14 }
15 }
16
17 dirs::home_dir()
18 .unwrap_or_default()
19 .join(".codex")
20 .join("config.json")
21}
22
23#[must_use]
24pub fn install() -> InstallResult {
25 let path = config_path();
26 let agent = "Codex CLI".to_string();
27
28 if !path.exists() {
29 return InstallResult {
30 agent,
31 config_path: path,
32 status: InstallStatus::ConfigNotFound,
33 };
34 }
35
36 let content = match std::fs::read_to_string(&path) {
37 Ok(c) => c,
38 Err(e) => {
39 return InstallResult {
40 agent,
41 config_path: path,
42 status: InstallStatus::Failed(e.to_string()),
43 };
44 }
45 };
46
47 if content.contains(HOOK_MARKER) {
48 return InstallResult {
49 agent,
50 config_path: path,
51 status: InstallStatus::AlreadyInstalled,
52 };
53 }
54
55 let mut config: serde_json::Value = match serde_json::from_str(&content) {
57 Ok(v) => v,
58 Err(e) => {
59 return InstallResult {
60 agent,
61 config_path: path,
62 status: InstallStatus::Failed(format!("invalid JSON: {e}")),
63 };
64 }
65 };
66
67 let hooks = config.as_object_mut().and_then(|obj| {
68 obj.entry("hooks")
69 .or_insert_with(|| serde_json::json!({}))
70 .as_object_mut()
71 });
72
73 let Some(hooks) = hooks else {
74 return InstallResult {
75 agent,
76 config_path: path,
77 status: InstallStatus::Failed("could not access hooks object".to_string()),
78 };
79 };
80
81 let post_exec = hooks
82 .entry("post_exec")
83 .or_insert_with(|| serde_json::json!([]));
84
85 let hook_entry = serde_json::json!({
86 "name": "cli-denoiser",
87 "command": "cli-denoiser --hook-mode",
88 "on": ["shell"]
89 });
90
91 if let Some(arr) = post_exec.as_array_mut() {
92 arr.push(hook_entry);
93 }
94
95 match write_json(&path, &config) {
96 Ok(()) => InstallResult {
97 agent,
98 config_path: path,
99 status: InstallStatus::Installed,
100 },
101 Err(e) => InstallResult {
102 agent,
103 config_path: path,
104 status: InstallStatus::Failed(e),
105 },
106 }
107}
108
109#[must_use]
110pub fn uninstall() -> InstallResult {
111 let path = config_path();
112 let agent = "Codex CLI".to_string();
113
114 if !path.exists() {
115 return InstallResult {
116 agent,
117 config_path: path,
118 status: InstallStatus::ConfigNotFound,
119 };
120 }
121
122 let content = match std::fs::read_to_string(&path) {
123 Ok(c) => c,
124 Err(e) => {
125 return InstallResult {
126 agent,
127 config_path: path,
128 status: InstallStatus::Failed(e.to_string()),
129 };
130 }
131 };
132
133 if !content.contains(HOOK_MARKER) {
134 return InstallResult {
135 agent,
136 config_path: path,
137 status: InstallStatus::ConfigNotFound,
138 };
139 }
140
141 let mut config: serde_json::Value = match serde_json::from_str(&content) {
142 Ok(v) => v,
143 Err(e) => {
144 return InstallResult {
145 agent,
146 config_path: path,
147 status: InstallStatus::Failed(format!("invalid JSON: {e}")),
148 };
149 }
150 };
151
152 if let Some(hooks) = config.pointer_mut("/hooks/post_exec")
153 && let Some(arr) = hooks.as_array_mut()
154 {
155 arr.retain(|entry| {
156 let s = entry.to_string();
157 !s.contains(HOOK_MARKER)
158 });
159 }
160
161 match write_json(&path, &config) {
162 Ok(()) => InstallResult {
163 agent,
164 config_path: path,
165 status: InstallStatus::Installed,
166 },
167 Err(e) => InstallResult {
168 agent,
169 config_path: path,
170 status: InstallStatus::Failed(e),
171 },
172 }
173}
174
175fn write_json(path: &std::path::Path, value: &serde_json::Value) -> Result<(), String> {
176 let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
177 std::fs::write(path, json).map_err(|e| e.to_string())
178}