lean_ctx/core/updater/
rewire.rs1fn migrate_shadow_mode_default() {
11 let Ok(state) = crate::core::paths::state_dir() else {
12 return;
13 };
14 let marker = state.join("shadow_mode_migrated");
15 if marker.exists() {
16 return;
17 }
18
19 let global = crate::core::config::Config::load_global();
21 let raw_toml = crate::core::config::Config::path()
22 .and_then(|p| std::fs::read_to_string(p).ok())
23 .unwrap_or_default();
24
25 let explicitly_set = raw_toml.lines().any(|line| {
26 let trimmed = line.trim();
27 trimmed.starts_with("shadow_mode") && trimmed.contains('=')
28 });
29
30 if !explicitly_set && !global.shadow_mode {
31 match crate::core::config::Config::update_global(|c| c.shadow_mode = true) {
34 Ok(_) => {
35 eprintln!(" \u{2139} shadow_mode enabled (recommended default since v3.9.9).");
36 eprintln!(" Disable: lean-ctx config set shadow_mode false");
37 }
38 Err(e) => tracing::warn!("could not enable shadow_mode during update: {e}"),
39 }
40 } else if explicitly_set && !global.shadow_mode {
41 eprintln!(" \u{2139} shadow_mode is false in your config. Since v3.9.9, true is the");
42 eprintln!(" recommended default (60%+ more token savings). Enable:");
43 eprintln!(" lean-ctx config set shadow_mode true");
44 }
45
46 if let Some(parent) = marker.parent() {
48 let _ = std::fs::create_dir_all(parent);
49 }
50 let _ = std::fs::write(&marker, "migrated");
51}
52
53pub fn migrate_shadow_mode_default_public() {
56 migrate_shadow_mode_default();
57}
58
59pub(super) fn post_update_rewire(skip_rules: bool, quiet: bool, setup: &dyn SetupRefresh) {
60 #[cfg(target_os = "macos")]
66 rewrap_launchagents_for_tcc();
67
68 if crate::core::config::Config::load_global()
72 .proxy_enabled
73 .is_none()
74 && crate::proxy_autostart::is_installed()
75 {
76 match crate::core::config::Config::update_global(|c| c.proxy_enabled = Some(true)) {
77 Ok(_) => {
78 eprintln!(" \u{2139} Proxy was already active \u{2014} keeping enabled.");
79 eprintln!(" Disable anytime: lean-ctx proxy disable");
80 }
81 Err(e) => tracing::warn!("could not persist proxy_enabled during update: {e}"),
82 }
83 }
84
85 migrate_shadow_mode_default();
88
89 let cfg = crate::core::config::Config::load();
91 let proxy_active = cfg.proxy_enabled == Some(true);
92
93 let effective_skip_rules = if skip_rules {
96 true
97 } else {
98 !cfg.setup.should_inject_rules()
99 };
100
101 if proxy_active {
103 restart_proxy_if_running();
104 wait_for_proxy_health(crate::core::proxy_endpoint::default_port());
105 }
106
107 if !cfg.shell_hook_disabled_effective() {
111 eprintln!(" \u{2139} Refreshing shell hooks in ~/.zshenv / ~/.bashenv.");
112 eprintln!(" Set shell_hook_disabled=true to prevent this in future updates.");
113 }
114 if let Err(e) = setup.refresh(proxy_active, effective_skip_rules) {
117 tracing::error!("Setup refresh error: {e}");
118 }
119 if !quiet {
120 setup.print_unconnected_tools_hint();
121 }
122
123 if crate::core::intelligence_runtime::cockpit::install_if_missing(true) && !quiet {
125 eprintln!(" \u{2713} LeanCTX Cockpit installed (open it with: lean-ctx cockpit).");
126 }
127}
128
129pub trait SetupRefresh {
132 fn refresh(&self, proxy_active: bool, skip_rules: bool) -> Result<(), String>;
135 fn print_plan(&self, proxy_active: bool, skip_rules: bool);
137 fn print_unconnected_tools_hint(&self);
139}
140
141pub(super) fn print_rewire_plan(skip_rules: bool, setup: &dyn SetupRefresh) {
143 let cfg = crate::core::config::Config::load();
144 let skip_rules = skip_rules || !cfg.setup.should_inject_rules();
145 setup.print_plan(cfg.proxy_enabled == Some(true), skip_rules);
146 setup.print_unconnected_tools_hint();
147}
148
149#[cfg(target_os = "macos")]
157fn rewrap_launchagents_for_tcc() {
158 if !crate::daemon_autostart::controls_user_services() {
159 return;
160 }
161 if crate::proxy_autostart::is_installed() {
162 crate::proxy_autostart::install(crate::core::proxy_endpoint::default_port(), true);
163 }
164 if crate::daemon_autostart::is_installed() {
165 crate::daemon_autostart::install(true);
166 }
167 if crate::core::update_scheduler::schedule_status().enabled {
168 let hours = crate::core::config::Config::load()
169 .updates
170 .check_interval_hours;
171 if let Err(e) = crate::core::update_scheduler::install_schedule(hours) {
172 tracing::warn!("#356 re-wrap of auto-update LaunchAgent failed: {e}");
173 }
174 }
175}
176
177fn wait_for_proxy_health(port: u16) {
178 let max_attempts = 20;
179 for i in 0..max_attempts {
180 if is_proxy_reachable(port) {
181 println!(" \x1b[32m✓\x1b[0m Proxy healthy on port {port}");
182 return;
183 }
184 if i == 0 {
185 print!(" \x1b[2mWaiting for proxy to become healthy");
186 }
187 print!(".");
188 use std::io::Write;
189 std::io::stdout().flush().ok();
190 std::thread::sleep(std::time::Duration::from_millis(500));
191 }
192 println!();
193 eprintln!(
194 " \x1b[33m⚠\x1b[0m Proxy did not respond within {}s — writing env vars anyway",
195 max_attempts / 2
196 );
197 eprintln!(" If Claude Code shows connection errors, run: lean-ctx proxy start");
198}
199
200fn restart_proxy_if_running() {
201 let port = crate::core::proxy_endpoint::default_port();
202
203 if restart_managed_proxy() {
204 return;
205 }
206
207 if is_proxy_reachable(port) {
208 println!(
209 " \x1b[33m⟳\x1b[0m Proxy running on port {port} — restart it to use the new binary:"
210 );
211 println!(" \x1b[1mlean-ctx proxy start --port={port}\x1b[0m");
212 }
213}
214
215fn restart_managed_proxy() -> bool {
220 if !crate::daemon_autostart::controls_user_services() {
221 return false;
222 }
223 #[cfg(target_os = "macos")]
224 {
225 let plist_path = dirs::home_dir()
226 .unwrap_or_default()
227 .join("Library/LaunchAgents/com.leanctx.proxy.plist");
228 if plist_path.exists() {
229 if crate::core::launchd::bootstrap("com.leanctx.proxy", &plist_path) {
230 println!(" \x1b[32m✓\x1b[0m Proxy restarted (LaunchAgent)");
231 } else {
232 println!(" \x1b[33m⚠\x1b[0m Could not restart proxy LaunchAgent");
233 }
234 return true;
235 }
236 }
237
238 #[cfg(target_os = "linux")]
239 {
240 let service_path = dirs::home_dir()
241 .unwrap_or_default()
242 .join(".config/systemd/user/lean-ctx-proxy.service");
243 if service_path.exists() {
244 let result = std::process::Command::new("systemctl")
245 .args(["--user", "restart", "lean-ctx-proxy"])
246 .output();
247 match result {
248 Ok(o) if o.status.success() => {
249 println!(" \x1b[32m✓\x1b[0m Proxy restarted (systemd)");
250 }
251 _ => {
252 println!(" \x1b[33m⚠\x1b[0m Could not restart proxy systemd service");
253 }
254 }
255 return true;
256 }
257 }
258
259 false
260}
261
262fn is_proxy_reachable(port: u16) -> bool {
263 ureq::get(&format!("http://127.0.0.1:{port}/health"))
264 .call()
265 .is_ok()
266}