Skip to main content

lean_ctx/
proxy_setup.rs

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