Skip to main content

lean_ctx/proxy_setup/
mod.rs

1//! Install / uninstall LLM-client proxy environment wiring.
2//!
3//! Submodules hold per-client setup (Claude, Codex, Grok, Pi, shell). This
4//! facade keeps the public API stable as `crate::proxy_setup::*`.
5
6mod claude;
7mod codex;
8mod commandcode;
9mod grok;
10mod pi;
11mod shell;
12mod util;
13
14#[cfg(test)]
15#[path = "tests.rs"]
16mod tests;
17
18use std::path::Path;
19
20use crate::marked_block;
21
22use claude::{install_claude_env, install_claude_env_inner, uninstall_claude_env};
23use codex::{codex_config_has_local_proxy_entry, strip_codex_proxy_entries, uninstall_codex_env};
24use commandcode::{install_commandcode_env, uninstall_commandcode_env};
25use grok::{
26    grok_config_has_local_proxy_entry, install_grok_env, strip_grok_proxy_entries,
27    uninstall_grok_env,
28};
29use pi::{install_pi_env, uninstall_pi_env};
30use shell::install_shell_exports;
31use util::{PROXY_ENV_END, PROXY_ENV_START};
32
33pub use claude::anthropic_api_key_available;
34pub(crate) use codex::install_codex_env;
35pub(crate) use commandcode::COMMANDCODE_MCP_INSTRUCTIONS;
36pub use grok::{grok_session_auth_available, xai_api_key_available};
37pub(crate) use util::is_proxy_reachable;
38pub use util::{default_port, is_local_lean_ctx_url, proxy_timeout};
39
40pub fn install_proxy_env(home: &Path, port: u16, quiet: bool) {
41    let cfg = crate::core::config::Config::load();
42    if cfg.proxy_enabled != Some(true) {
43        if !quiet {
44            println!("  Proxy env skipped (not enabled in config)");
45        }
46        return;
47    }
48    install_shell_exports(home, port, quiet, false);
49    install_claude_env(home, port, quiet);
50    install_codex_env(home, port, quiet);
51    install_pi_env(home, port, quiet, false);
52    install_grok_env(home, port, quiet, false);
53    install_commandcode_env(home, port, quiet, false);
54}
55
56/// Install proxy env without config guard (used by `lean-ctx proxy enable` which has already set the flag).
57/// `force_endpoint`: if true, overrides even non-local custom endpoints.
58pub fn install_proxy_env_unchecked(home: &Path, port: u16, quiet: bool, force_endpoint: bool) {
59    // Shell exports and Grok install share `force_endpoint` so force+no-auth
60    // seeds grok-chat and still emits GROK_CLI_CHAT_PROXY_BASE_URL.
61    install_shell_exports(home, port, quiet, force_endpoint);
62    if force_endpoint {
63        install_claude_env_inner(home, port, quiet, true);
64    } else {
65        install_claude_env(home, port, quiet);
66    }
67    install_codex_env(home, port, quiet);
68    install_pi_env(home, port, quiet, force_endpoint);
69    install_grok_env(home, port, quiet, force_endpoint);
70    install_commandcode_env(home, port, quiet, force_endpoint);
71}
72
73pub fn preview_proxy_cleanup(home: &Path) {
74    let settings_dir = crate::core::editor_registry::claude_state_dir(home);
75    let settings_path = settings_dir.join("settings.json");
76    if let Ok(content) = std::fs::read_to_string(&settings_path)
77        && content.contains("ANTHROPIC_BASE_URL")
78    {
79        let cfg = crate::core::config::Config::load();
80        if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
81            println!("  Would restore ANTHROPIC_BASE_URL → {upstream} in Claude Code settings");
82        } else {
83            println!("  Would remove ANTHROPIC_BASE_URL from Claude Code settings");
84        }
85    }
86
87    let codex_path = crate::core::home::resolve_codex_config_path()
88        .unwrap_or_else(|| home.join(".codex/config.toml"));
89    if let Ok(content) = std::fs::read_to_string(codex_path)
90        && codex_config_has_local_proxy_entry(&content)
91    {
92        println!("  Would remove Codex proxy URL from config.toml");
93    }
94
95    let grok_path = home.join(".grok/config.toml");
96    if let Ok(content) = std::fs::read_to_string(grok_path)
97        && grok_config_has_local_proxy_entry(&content)
98    {
99        println!("  Would remove Grok proxy models_base_url from config.toml");
100    }
101
102    let cc_mcp = home.join(".commandcode/mcp.json");
103    if let Ok(content) = std::fs::read_to_string(cc_mcp)
104        && content.contains("lean-ctx")
105    {
106        println!("  Would remove lean-ctx from Command Code MCP (~/.commandcode/mcp.json)");
107    }
108}
109
110/// Removes stale proxy URLs from Claude Code / Codex settings when the proxy is not enabled.
111/// Returns the number of stale URLs cleaned up.
112pub fn cleanup_stale_proxy_env(home: &Path) -> usize {
113    let cfg = crate::core::config::Config::load();
114    if cfg.proxy_enabled == Some(true) {
115        return 0;
116    }
117
118    let mut cleaned = 0;
119
120    let settings_dir = crate::core::editor_registry::claude_state_dir(home);
121    let settings_path = settings_dir.join("settings.json");
122    if let Ok(content) = std::fs::read_to_string(&settings_path)
123        && let Ok(mut doc) = crate::core::jsonc::parse_jsonc(&content)
124        && let Some(base_url) = doc
125            .get("env")
126            .and_then(|e| e.get("ANTHROPIC_BASE_URL"))
127            .and_then(|v| v.as_str())
128            .map(String::from)
129        && is_local_lean_ctx_url(&base_url)
130        && let Some(env_obj) = doc.get_mut("env").and_then(|e| e.as_object_mut())
131    {
132        if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
133            env_obj.insert(
134                "ANTHROPIC_BASE_URL".to_string(),
135                serde_json::Value::String(upstream.clone()),
136            );
137            println!("  ✓ Restored ANTHROPIC_BASE_URL → {upstream} in Claude Code settings");
138        } else {
139            env_obj.remove("ANTHROPIC_BASE_URL");
140            if env_obj.is_empty() {
141                doc.as_object_mut().map(|o| o.remove("env"));
142            }
143            println!("  ✓ Removed stale ANTHROPIC_BASE_URL from Claude Code settings");
144        }
145        let out = serde_json::to_string_pretty(&doc).unwrap_or_default();
146        let _ = std::fs::write(&settings_path, out + "\n");
147        cleaned += 1;
148    }
149
150    let codex_path = crate::core::home::resolve_codex_config_path()
151        .unwrap_or_else(|| home.join(".codex/config.toml"));
152    if let Ok(content) = std::fs::read_to_string(&codex_path)
153        && codex_config_has_local_proxy_entry(&content)
154    {
155        let filtered = strip_codex_proxy_entries(&content);
156        let _ = std::fs::write(&codex_path, &filtered);
157        println!("  ✓ Removed stale Codex proxy URL from config.toml");
158        cleaned += 1;
159    }
160
161    let grok_path = home.join(".grok/config.toml");
162    if let Ok(content) = std::fs::read_to_string(&grok_path)
163        && grok_config_has_local_proxy_entry(&content)
164    {
165        let cleaned_toml = strip_grok_proxy_entries(&content);
166        let _ = std::fs::write(&grok_path, &cleaned_toml);
167        println!("  ✓ Removed stale Grok proxy models_base_url from config.toml");
168        cleaned += 1;
169    }
170
171    cleaned
172}
173
174pub fn has_stale_proxy_url(home: &Path) -> bool {
175    let cfg = crate::core::config::Config::load();
176    if cfg.proxy_enabled == Some(true) {
177        return false;
178    }
179
180    let settings_dir = crate::core::editor_registry::claude_state_dir(home);
181    let settings_path = settings_dir.join("settings.json");
182    let Ok(content) = std::fs::read_to_string(&settings_path) else {
183        return false;
184    };
185    let Ok(doc) = crate::core::jsonc::parse_jsonc(&content) else {
186        return false;
187    };
188
189    let base_url = doc
190        .get("env")
191        .and_then(|e| e.get("ANTHROPIC_BASE_URL"))
192        .and_then(|v| v.as_str())
193        .unwrap_or("");
194
195    is_local_lean_ctx_url(base_url)
196}
197
198pub fn uninstall_proxy_env(home: &Path, quiet: bool) {
199    for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
200        let label = format!(
201            "proxy env from ~/{}",
202            rc.file_name().unwrap_or_default().to_string_lossy()
203        );
204        marked_block::remove_from_file(rc, PROXY_ENV_START, PROXY_ENV_END, quiet, &label);
205    }
206
207    let fish_config = home.join(".config/fish/config.fish");
208    if fish_config.exists() {
209        marked_block::remove_from_file(
210            &fish_config,
211            PROXY_ENV_START,
212            PROXY_ENV_END,
213            quiet,
214            "proxy env from ~/.config/fish/config.fish",
215        );
216    }
217
218    let ps_profile =
219        dirs::home_dir().map(|h| crate::shell::platform::resolve_powershell_profile_path(&h));
220    if let Some(ref ps) = ps_profile
221        && ps.exists()
222    {
223        marked_block::remove_from_file(
224            ps,
225            PROXY_ENV_START,
226            PROXY_ENV_END,
227            quiet,
228            "proxy env from PowerShell profile",
229        );
230    }
231
232    uninstall_claude_env(home, quiet);
233    uninstall_codex_env(home, quiet);
234    uninstall_pi_env(home, quiet);
235    uninstall_grok_env(home, quiet);
236    uninstall_commandcode_env(home, quiet);
237}