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}")
119 .parse()
120 .expect("BUG: invalid hardcoded socket address"),
121 Duration::from_millis(200),
122 )
123 .is_ok()
124}
125
126fn install_codex_env(home: &Path, port: u16, quiet: bool) {
127 let base = format!("http://127.0.0.1:{port}");
128
129 if !is_proxy_reachable(port) {
130 if !quiet {
131 println!(" Skipping Codex CLI proxy env (proxy not running on port {port})");
132 }
133 return;
134 }
135
136 let config_dir = home.join(".codex");
137 let config_path = config_dir.join("config.toml");
138
139 let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
140
141 if existing.contains("OPENAI_BASE_URL") && existing.contains(&base) {
142 if !quiet {
143 println!(" Codex CLI proxy env already configured");
144 }
145 return;
146 }
147
148 if !config_dir.exists() {
149 return;
150 }
151
152 let mut content = existing;
153
154 if content.contains("[env]") {
155 if !content.contains("OPENAI_BASE_URL") {
156 content = content.replace("[env]", &format!("[env]\nOPENAI_BASE_URL = \"{base}\""));
157 }
158 } else {
159 if !content.is_empty() && !content.ends_with('\n') {
160 content.push('\n');
161 }
162 content.push_str(&format!("\n[env]\nOPENAI_BASE_URL = \"{base}\"\n"));
163 }
164
165 let _ = std::fs::write(&config_path, &content);
166 if !quiet {
167 println!(" Configured OPENAI_BASE_URL in Codex CLI config");
168 }
169}
170
171pub fn default_port() -> u16 {
172 DEFAULT_PROXY_PORT
173}