fn migrate_shadow_mode_default() {
let Ok(state) = crate::core::paths::state_dir() else {
return;
};
let marker = state.join("shadow_mode_migrated");
if marker.exists() {
return;
}
let global = crate::core::config::Config::load_global();
let raw_toml = crate::core::config::Config::path()
.and_then(|p| std::fs::read_to_string(p).ok())
.unwrap_or_default();
let explicitly_set = raw_toml.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("shadow_mode") && trimmed.contains('=')
});
if !explicitly_set && !global.shadow_mode {
match crate::core::config::Config::update_global(|c| c.shadow_mode = true) {
Ok(_) => {
eprintln!(" \u{2139} shadow_mode enabled (recommended default since v3.9.9).");
eprintln!(" Disable: lean-ctx config set shadow_mode false");
}
Err(e) => tracing::warn!("could not enable shadow_mode during update: {e}"),
}
} else if explicitly_set && !global.shadow_mode {
eprintln!(" \u{2139} shadow_mode is false in your config. Since v3.9.9, true is the");
eprintln!(" recommended default (60%+ more token savings). Enable:");
eprintln!(" lean-ctx config set shadow_mode true");
}
if let Some(parent) = marker.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&marker, "migrated");
}
pub fn migrate_shadow_mode_default_public() {
migrate_shadow_mode_default();
}
pub(super) fn post_update_rewire(skip_rules: bool, quiet: bool, setup: &dyn SetupRefresh) {
#[cfg(target_os = "macos")]
rewrap_launchagents_for_tcc();
if crate::core::config::Config::load_global()
.proxy_enabled
.is_none()
&& crate::proxy_autostart::is_installed()
{
match crate::core::config::Config::update_global(|c| c.proxy_enabled = Some(true)) {
Ok(_) => {
eprintln!(" \u{2139} Proxy was already active \u{2014} keeping enabled.");
eprintln!(" Disable anytime: lean-ctx proxy disable");
}
Err(e) => tracing::warn!("could not persist proxy_enabled during update: {e}"),
}
}
migrate_shadow_mode_default();
let cfg = crate::core::config::Config::load();
let proxy_active = cfg.proxy_enabled == Some(true);
let effective_skip_rules = if skip_rules {
true
} else {
!cfg.setup.should_inject_rules()
};
if proxy_active {
restart_proxy_if_running();
wait_for_proxy_health(crate::core::proxy_endpoint::default_port());
}
if !cfg.shell_hook_disabled_effective() {
eprintln!(" \u{2139} Refreshing shell hooks in ~/.zshenv / ~/.bashenv.");
eprintln!(" Set shell_hook_disabled=true to prevent this in future updates.");
}
if let Err(e) = setup.refresh(proxy_active, effective_skip_rules) {
tracing::error!("Setup refresh error: {e}");
}
if !quiet {
setup.print_unconnected_tools_hint();
}
if crate::core::intelligence_runtime::cockpit::install_if_missing(true) && !quiet {
eprintln!(" \u{2713} LeanCTX Cockpit installed (open it with: lean-ctx cockpit).");
}
}
pub trait SetupRefresh {
fn refresh(&self, proxy_active: bool, skip_rules: bool) -> Result<(), String>;
fn print_plan(&self, proxy_active: bool, skip_rules: bool);
fn print_unconnected_tools_hint(&self);
}
pub(super) fn print_rewire_plan(skip_rules: bool, setup: &dyn SetupRefresh) {
let cfg = crate::core::config::Config::load();
let skip_rules = skip_rules || !cfg.setup.should_inject_rules();
setup.print_plan(cfg.proxy_enabled == Some(true), skip_rules);
setup.print_unconnected_tools_hint();
}
#[cfg(target_os = "macos")]
fn rewrap_launchagents_for_tcc() {
if !crate::daemon_autostart::controls_user_services() {
return;
}
if crate::proxy_autostart::is_installed() {
crate::proxy_autostart::install(crate::core::proxy_endpoint::default_port(), true);
}
if crate::daemon_autostart::is_installed() {
crate::daemon_autostart::install(true);
}
if crate::core::update_scheduler::schedule_status().enabled {
let hours = crate::core::config::Config::load()
.updates
.check_interval_hours;
if let Err(e) = crate::core::update_scheduler::install_schedule(hours) {
tracing::warn!("#356 re-wrap of auto-update LaunchAgent failed: {e}");
}
}
}
fn wait_for_proxy_health(port: u16) {
let max_attempts = 20;
for i in 0..max_attempts {
if is_proxy_reachable(port) {
println!(" \x1b[32m✓\x1b[0m Proxy healthy on port {port}");
return;
}
if i == 0 {
print!(" \x1b[2mWaiting for proxy to become healthy");
}
print!(".");
use std::io::Write;
std::io::stdout().flush().ok();
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!();
eprintln!(
" \x1b[33m⚠\x1b[0m Proxy did not respond within {}s — writing env vars anyway",
max_attempts / 2
);
eprintln!(" If Claude Code shows connection errors, run: lean-ctx proxy start");
}
fn restart_proxy_if_running() {
let port = crate::core::proxy_endpoint::default_port();
if restart_managed_proxy() {
return;
}
if is_proxy_reachable(port) {
println!(
" \x1b[33m⟳\x1b[0m Proxy running on port {port} — restart it to use the new binary:"
);
println!(" \x1b[1mlean-ctx proxy start --port={port}\x1b[0m");
}
}
fn restart_managed_proxy() -> bool {
if !crate::daemon_autostart::controls_user_services() {
return false;
}
#[cfg(target_os = "macos")]
{
let plist_path = dirs::home_dir()
.unwrap_or_default()
.join("Library/LaunchAgents/com.leanctx.proxy.plist");
if plist_path.exists() {
if crate::core::launchd::bootstrap("com.leanctx.proxy", &plist_path) {
println!(" \x1b[32m✓\x1b[0m Proxy restarted (LaunchAgent)");
} else {
println!(" \x1b[33m⚠\x1b[0m Could not restart proxy LaunchAgent");
}
return true;
}
}
#[cfg(target_os = "linux")]
{
let service_path = dirs::home_dir()
.unwrap_or_default()
.join(".config/systemd/user/lean-ctx-proxy.service");
if service_path.exists() {
let result = std::process::Command::new("systemctl")
.args(["--user", "restart", "lean-ctx-proxy"])
.output();
match result {
Ok(o) if o.status.success() => {
println!(" \x1b[32m✓\x1b[0m Proxy restarted (systemd)");
}
_ => {
println!(" \x1b[33m⚠\x1b[0m Could not restart proxy systemd service");
}
}
return true;
}
}
false
}
fn is_proxy_reachable(port: u16) -> bool {
ureq::get(&format!("http://127.0.0.1:{port}/health"))
.call()
.is_ok()
}