1use std::path::Path;
2
3use crate::marked_block;
4
5const PROXY_ENV_START: &str = "# >>> lean-ctx proxy env >>>";
6const PROXY_ENV_END: &str = "# <<< lean-ctx proxy env <<<";
7
8const DEFAULT_PROXY_PORT: u16 = 4444;
9
10pub fn install_proxy_env(home: &Path, port: u16, quiet: bool) {
11 install_shell_exports(home, port, quiet);
12 install_claude_env(home, port, quiet);
13 install_codex_env(home, port, quiet);
14}
15
16pub fn preview_proxy_cleanup(home: &Path) {
17 let settings_dir = crate::core::editor_registry::claude_state_dir(home);
18 let settings_path = settings_dir.join("settings.json");
19 if let Ok(content) = std::fs::read_to_string(&settings_path) {
20 if content.contains("ANTHROPIC_BASE_URL") {
21 let cfg = crate::core::config::Config::load();
22 if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
23 println!(" Would restore ANTHROPIC_BASE_URL → {upstream} in Claude Code settings");
24 } else {
25 println!(" Would remove ANTHROPIC_BASE_URL from Claude Code settings");
26 }
27 }
28 }
29
30 let codex_dir = crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
31 let codex_path = codex_dir.join("config.toml");
32 if let Ok(content) = std::fs::read_to_string(codex_path) {
33 if content.contains("OPENAI_BASE_URL") {
34 println!(" Would remove OPENAI_BASE_URL from Codex CLI config");
35 }
36 }
37}
38
39pub fn uninstall_proxy_env(home: &Path, quiet: bool) {
40 for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
41 let label = format!(
42 "proxy env from ~/{}",
43 rc.file_name().unwrap_or_default().to_string_lossy()
44 );
45 marked_block::remove_from_file(rc, PROXY_ENV_START, PROXY_ENV_END, quiet, &label);
46 }
47 uninstall_claude_env(home, quiet);
48 uninstall_codex_env(home, quiet);
49}
50
51fn install_shell_exports(home: &Path, port: u16, quiet: bool) {
52 if !is_proxy_reachable(port) {
53 if !quiet {
54 println!(" Skipping shell proxy exports (proxy not running on port {port})");
55 }
56 return;
57 }
58
59 let base = format!("http://127.0.0.1:{port}");
60
61 let block = format!(
62 r#"{PROXY_ENV_START}
63export GEMINI_API_BASE_URL="{base}"
64{PROXY_ENV_END}"#
65 );
66
67 for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
68 if rc.exists() {
69 let label = format!(
70 "proxy env in ~/{}",
71 rc.file_name().unwrap_or_default().to_string_lossy()
72 );
73 marked_block::upsert(rc, PROXY_ENV_START, PROXY_ENV_END, &block, quiet, &label);
74 }
75 }
76}
77
78fn uninstall_claude_env(home: &Path, quiet: bool) {
79 use crate::core::config::Config;
80
81 let settings_dir = crate::core::editor_registry::claude_state_dir(home);
82 let settings_path = settings_dir.join("settings.json");
83 let existing = match std::fs::read_to_string(&settings_path) {
84 Ok(s) if !s.trim().is_empty() => s,
85 _ => return,
86 };
87 let mut doc: serde_json::Value = match serde_json::from_str(&existing) {
88 Ok(v) => v,
89 Err(_) => return,
90 };
91
92 let Some(env_obj) = doc.get_mut("env").and_then(|e| e.as_object_mut()) else {
93 return;
94 };
95
96 if !env_obj.contains_key("ANTHROPIC_BASE_URL") {
97 return;
98 }
99
100 let cfg = Config::load();
101 if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
102 env_obj.insert(
103 "ANTHROPIC_BASE_URL".to_string(),
104 serde_json::Value::String(upstream.clone()),
105 );
106 if !quiet {
107 println!(" ✓ Restored ANTHROPIC_BASE_URL → {upstream} in Claude Code settings");
108 }
109 } else {
110 env_obj.remove("ANTHROPIC_BASE_URL");
111 if env_obj.is_empty() {
112 doc.as_object_mut().map(|o| o.remove("env"));
113 }
114 if !quiet {
115 println!(" ✓ Removed ANTHROPIC_BASE_URL from Claude Code settings");
116 }
117 }
118
119 let content = serde_json::to_string_pretty(&doc).unwrap_or_default();
120 let _ = std::fs::write(&settings_path, content + "\n");
121}
122
123fn uninstall_codex_env(home: &Path, quiet: bool) {
124 let codex_dir = crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
125 let config_path = codex_dir.join("config.toml");
126 let existing = match std::fs::read_to_string(&config_path) {
127 Ok(s) if !s.trim().is_empty() => s,
128 _ => return,
129 };
130
131 if !existing.contains("OPENAI_BASE_URL") {
132 return;
133 }
134
135 let cleaned: String = existing
136 .lines()
137 .filter(|line| {
138 let trimmed = line.trim();
139 !trimmed.starts_with("OPENAI_BASE_URL")
140 })
141 .collect::<Vec<_>>()
142 .join("\n");
143
144 let cleaned = cleaned
145 .replace("\n[env]\n\n", "\n")
146 .replace("[env]\n\n", "");
147 let cleaned = if cleaned.trim() == "[env]" {
148 String::new()
149 } else {
150 cleaned
151 };
152
153 let _ = std::fs::write(&config_path, &cleaned);
154 if !quiet {
155 println!(" ✓ Removed OPENAI_BASE_URL from Codex CLI config");
156 }
157}
158
159fn install_claude_env(home: &Path, port: u16, quiet: bool) {
160 use crate::core::config::{is_local_proxy_url, normalize_url_opt, Config};
161
162 let base = format!("http://127.0.0.1:{port}");
163
164 let settings_dir = crate::core::editor_registry::claude_state_dir(home);
165 let settings_path = settings_dir.join("settings.json");
166 let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
167 let mut doc: serde_json::Value = if existing.trim().is_empty() {
168 serde_json::json!({})
169 } else {
170 match serde_json::from_str(&existing) {
171 Ok(v) => v,
172 Err(_) => return,
173 }
174 };
175
176 let current_url = doc
177 .get("env")
178 .and_then(|e| e.get("ANTHROPIC_BASE_URL"))
179 .and_then(|v| v.as_str())
180 .unwrap_or("");
181
182 if current_url == base {
183 if !quiet {
184 println!(" Claude Code proxy env already configured");
185 }
186 return;
187 }
188
189 if let Some(upstream) = normalize_url_opt(current_url) {
190 if !is_local_proxy_url(&upstream) {
191 let mut cfg = Config::load();
192 if cfg.proxy.anthropic_upstream.is_none() {
193 cfg.proxy.anthropic_upstream = Some(upstream.clone());
194 let _ = cfg.save();
195 if !quiet {
196 println!(" Preserved Claude Code upstream: {upstream}");
197 println!(" → saved as proxy.anthropic_upstream in config");
198 }
199 }
200 }
201 }
202
203 if !is_proxy_reachable(port) {
204 if !quiet {
205 println!(" Skipping Claude Code proxy env (proxy not running on port {port})");
206 }
207 return;
208 }
209
210 if let Some(env_obj) = doc.as_object_mut().and_then(|o| {
211 o.entry("env")
212 .or_insert(serde_json::json!({}))
213 .as_object_mut()
214 }) {
215 env_obj.insert(
216 "ANTHROPIC_BASE_URL".to_string(),
217 serde_json::Value::String(base),
218 );
219 }
220
221 let _ = std::fs::create_dir_all(&settings_dir);
222 let content = serde_json::to_string_pretty(&doc).unwrap_or_default();
223 let _ = std::fs::write(&settings_path, content + "\n");
224 if !quiet {
225 println!(" Configured ANTHROPIC_BASE_URL in Claude Code settings");
226 }
227}
228
229fn is_proxy_reachable(port: u16) -> bool {
230 use std::net::TcpStream;
231 use std::time::Duration;
232 TcpStream::connect_timeout(
233 &format!("127.0.0.1:{port}")
234 .parse()
235 .expect("BUG: invalid hardcoded socket address"),
236 Duration::from_millis(200),
237 )
238 .is_ok()
239}
240
241fn install_codex_env(home: &Path, port: u16, quiet: bool) {
242 let base = format!("http://127.0.0.1:{port}");
243
244 if !is_proxy_reachable(port) {
245 if !quiet {
246 println!(" Skipping Codex CLI proxy env (proxy not running on port {port})");
247 }
248 return;
249 }
250
251 let config_dir = crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
252 let config_path = config_dir.join("config.toml");
253
254 let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
255
256 if existing.contains("OPENAI_BASE_URL") && existing.contains(&base) {
257 if !quiet {
258 println!(" Codex CLI proxy env already configured");
259 }
260 return;
261 }
262
263 if !config_dir.exists() {
264 return;
265 }
266
267 let mut content = existing;
268
269 if content.contains("[env]") {
270 if !content.contains("OPENAI_BASE_URL") {
271 content = content.replace("[env]", &format!("[env]\nOPENAI_BASE_URL = \"{base}\""));
272 }
273 } else {
274 if !content.is_empty() && !content.ends_with('\n') {
275 content.push('\n');
276 }
277 content.push_str(&format!("\n[env]\nOPENAI_BASE_URL = \"{base}\"\n"));
278 }
279
280 let _ = std::fs::write(&config_path, &content);
281 if !quiet {
282 println!(" Configured OPENAI_BASE_URL in Codex CLI config");
283 }
284}
285
286pub fn default_port() -> u16 {
287 DEFAULT_PROXY_PORT
288}