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")]
155fn rewrap_launchagents_for_tcc() {
156 if crate::proxy_autostart::is_installed() {
157 crate::proxy_autostart::install(crate::core::proxy_endpoint::default_port(), true);
158 }
159 if crate::daemon_autostart::is_installed() {
160 crate::daemon_autostart::install(true);
161 }
162 if crate::core::update_scheduler::schedule_status().enabled {
163 let hours = crate::core::config::Config::load()
164 .updates
165 .check_interval_hours;
166 if let Err(e) = crate::core::update_scheduler::install_schedule(hours) {
167 tracing::warn!("#356 re-wrap of auto-update LaunchAgent failed: {e}");
168 }
169 }
170}
171
172fn wait_for_proxy_health(port: u16) {
173 let max_attempts = 20;
174 for i in 0..max_attempts {
175 if is_proxy_reachable(port) {
176 println!(" \x1b[32m✓\x1b[0m Proxy healthy on port {port}");
177 return;
178 }
179 if i == 0 {
180 print!(" \x1b[2mWaiting for proxy to become healthy");
181 }
182 print!(".");
183 use std::io::Write;
184 std::io::stdout().flush().ok();
185 std::thread::sleep(std::time::Duration::from_millis(500));
186 }
187 println!();
188 eprintln!(
189 " \x1b[33m⚠\x1b[0m Proxy did not respond within {}s — writing env vars anyway",
190 max_attempts / 2
191 );
192 eprintln!(" If Claude Code shows connection errors, run: lean-ctx proxy start");
193}
194
195fn restart_proxy_if_running() {
196 let port = crate::core::proxy_endpoint::default_port();
197
198 if restart_managed_proxy() {
199 return;
200 }
201
202 if is_proxy_reachable(port) {
203 println!(
204 " \x1b[33m⟳\x1b[0m Proxy running on port {port} — restart it to use the new binary:"
205 );
206 println!(" \x1b[1mlean-ctx proxy start --port={port}\x1b[0m");
207 }
208}
209
210fn restart_managed_proxy() -> bool {
213 #[cfg(target_os = "macos")]
214 {
215 let plist_path = dirs::home_dir()
216 .unwrap_or_default()
217 .join("Library/LaunchAgents/com.leanctx.proxy.plist");
218 if plist_path.exists() {
219 if crate::core::launchd::bootstrap("com.leanctx.proxy", &plist_path) {
220 println!(" \x1b[32m✓\x1b[0m Proxy restarted (LaunchAgent)");
221 } else {
222 println!(" \x1b[33m⚠\x1b[0m Could not restart proxy LaunchAgent");
223 }
224 return true;
225 }
226 }
227
228 #[cfg(target_os = "linux")]
229 {
230 let service_path = dirs::home_dir()
231 .unwrap_or_default()
232 .join(".config/systemd/user/lean-ctx-proxy.service");
233 if service_path.exists() {
234 let result = std::process::Command::new("systemctl")
235 .args(["--user", "restart", "lean-ctx-proxy"])
236 .output();
237 match result {
238 Ok(o) if o.status.success() => {
239 println!(" \x1b[32m✓\x1b[0m Proxy restarted (systemd)");
240 }
241 _ => {
242 println!(" \x1b[33m⚠\x1b[0m Could not restart proxy systemd service");
243 }
244 }
245 return true;
246 }
247 }
248
249 false
250}
251
252fn is_proxy_reachable(port: u16) -> bool {
253 ureq::get(&format!("http://127.0.0.1:{port}/health"))
254 .call()
255 .is_ok()
256}