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 uninstall_proxy_env(home: &Path, quiet: bool) {
17    for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
18        let label = format!(
19            "proxy env from ~/{}",
20            rc.file_name().unwrap_or_default().to_string_lossy()
21        );
22        marked_block::remove_from_file(rc, PROXY_ENV_START, PROXY_ENV_END, quiet, &label);
23    }
24}
25
26fn install_shell_exports(home: &Path, port: u16, quiet: bool) {
27    if !is_proxy_reachable(port) {
28        if !quiet {
29            println!("  Skipping shell proxy exports (proxy not running on port {port})");
30        }
31        return;
32    }
33
34    let base = format!("http://127.0.0.1:{port}");
35
36    let block = format!(
37        r#"{PROXY_ENV_START}
38export GEMINI_API_BASE_URL="{base}"
39{PROXY_ENV_END}"#
40    );
41
42    for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
43        if rc.exists() {
44            let label = format!(
45                "proxy env in ~/{}",
46                rc.file_name().unwrap_or_default().to_string_lossy()
47            );
48            marked_block::upsert(rc, PROXY_ENV_START, PROXY_ENV_END, &block, quiet, &label);
49        }
50    }
51}
52
53fn install_claude_env(home: &Path, port: u16, quiet: bool) {
54    let base = format!("http://127.0.0.1:{port}");
55
56    if !is_proxy_reachable(port) {
57        if !quiet {
58            println!("  Skipping Claude Code proxy env (proxy not running on port {port})");
59        }
60        return;
61    }
62
63    let settings_dir = crate::core::editor_registry::claude_state_dir(home);
64    let settings_path = settings_dir.join("settings.json");
65
66    let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
67
68    let mut doc: serde_json::Value = if existing.trim().is_empty() {
69        serde_json::json!({})
70    } else {
71        match serde_json::from_str(&existing) {
72            Ok(v) => v,
73            Err(_) => return,
74        }
75    };
76
77    let env = doc
78        .as_object_mut()
79        .and_then(|o| {
80            o.entry("env")
81                .or_insert(serde_json::json!({}))
82                .as_object_mut()
83                .map(|_| ())
84        })
85        .is_some();
86
87    if env {
88        if let Some(env_obj) = doc.get_mut("env").and_then(|e| e.as_object_mut()) {
89            let current = env_obj
90                .get("ANTHROPIC_BASE_URL")
91                .and_then(|v| v.as_str())
92                .unwrap_or("");
93            if current == base {
94                if !quiet {
95                    println!("  Claude Code proxy env already configured");
96                }
97                return;
98            }
99            env_obj.insert(
100                "ANTHROPIC_BASE_URL".to_string(),
101                serde_json::Value::String(base),
102            );
103        }
104    }
105
106    let _ = std::fs::create_dir_all(&settings_dir);
107    let content = serde_json::to_string_pretty(&doc).unwrap_or_default();
108    let _ = std::fs::write(&settings_path, content + "\n");
109    if !quiet {
110        println!("  Configured ANTHROPIC_BASE_URL in Claude Code settings");
111    }
112}
113
114fn is_proxy_reachable(port: u16) -> bool {
115    use std::net::TcpStream;
116    use std::time::Duration;
117    TcpStream::connect_timeout(
118        &format!("127.0.0.1:{port}").parse().unwrap(),
119        Duration::from_millis(200),
120    )
121    .is_ok()
122}
123
124fn install_codex_env(home: &Path, port: u16, quiet: bool) {
125    let base = format!("http://127.0.0.1:{port}");
126
127    if !is_proxy_reachable(port) {
128        if !quiet {
129            println!("  Skipping Codex CLI proxy env (proxy not running on port {port})");
130        }
131        return;
132    }
133
134    let config_dir = home.join(".codex");
135    let config_path = config_dir.join("config.toml");
136
137    let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
138
139    if existing.contains("OPENAI_BASE_URL") && existing.contains(&base) {
140        if !quiet {
141            println!("  Codex CLI proxy env already configured");
142        }
143        return;
144    }
145
146    if !config_dir.exists() {
147        return;
148    }
149
150    let mut content = existing;
151
152    if content.contains("[env]") {
153        if !content.contains("OPENAI_BASE_URL") {
154            content = content.replace("[env]", &format!("[env]\nOPENAI_BASE_URL = \"{base}\""));
155        }
156    } else {
157        if !content.is_empty() && !content.ends_with('\n') {
158            content.push('\n');
159        }
160        content.push_str(&format!("\n[env]\nOPENAI_BASE_URL = \"{base}\"\n"));
161    }
162
163    let _ = std::fs::write(&config_path, &content);
164    if !quiet {
165        println!("  Configured OPENAI_BASE_URL in Codex CLI config");
166    }
167}
168
169pub fn default_port() -> u16 {
170    DEFAULT_PROXY_PORT
171}