use crate::{
core, dashboard, doctor, heatmap, hook_handlers, mcp_stdio, report, setup, shell, status,
token_report, tools, tui, uninstall,
};
use anyhow::Result;
pub fn run() {
let args: Vec<String> = std::env::args().collect();
let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
if !enters_mcp {
crate::core::logging::init_logging();
}
if args.len() > 1 {
let rest = args[2..].to_vec();
match args[1].as_str() {
"-c" | "exec" => {
let raw = rest.first().is_some_and(|a| a == "--raw");
let cmd_args = if raw { &args[3..] } else { &args[2..] };
let command = if cmd_args.len() == 1 {
cmd_args[0].clone()
} else {
shell::join_command(cmd_args)
};
if std::env::var("LEAN_CTX_ACTIVE").is_ok()
|| std::env::var("LEAN_CTX_DISABLED").is_ok()
{
passthrough(&command);
}
if raw {
std::env::set_var("LEAN_CTX_RAW", "1");
} else {
std::env::set_var("LEAN_CTX_COMPRESS", "1");
}
let code = shell::exec(&command);
core::stats::flush();
core::heatmap::flush();
std::process::exit(code);
}
"-t" | "--track" => {
let cmd_args = &args[2..];
let code = if cmd_args.len() > 1 {
shell::exec_argv(cmd_args)
} else {
let command = cmd_args[0].clone();
if std::env::var("LEAN_CTX_ACTIVE").is_ok()
|| std::env::var("LEAN_CTX_DISABLED").is_ok()
{
passthrough(&command);
}
shell::exec(&command)
};
core::stats::flush();
core::heatmap::flush();
std::process::exit(code);
}
"shell" | "--shell" => {
shell::interactive();
return;
}
"gain" => {
if rest.iter().any(|a| a == "--reset") {
core::stats::reset_all();
println!("Stats reset. All token savings data cleared.");
return;
}
if rest.iter().any(|a| a == "--live" || a == "--watch") {
core::stats::gain_live();
return;
}
let model = rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--model=") {
return Some(v.to_string());
}
if a == "--model" {
return rest.get(i + 1).cloned();
}
None
});
let period = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--period=") {
return Some(v.to_string());
}
if a == "--period" {
return rest.get(i + 1).cloned();
}
None
})
.unwrap_or_else(|| "all".to_string());
let limit = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--limit=") {
return v.parse::<usize>().ok();
}
if a == "--limit" {
return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
}
None
})
.unwrap_or(10);
if rest.iter().any(|a| a == "--graph") {
println!("{}", core::stats::format_gain_graph());
} else if rest.iter().any(|a| a == "--daily") {
println!("{}", core::stats::format_gain_daily());
} else if rest.iter().any(|a| a == "--json") {
println!(
"{}",
tools::ctx_gain::handle(
"json",
Some(&period),
model.as_deref(),
Some(limit)
)
);
} else if rest.iter().any(|a| a == "--score") {
println!(
"{}",
tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
);
} else if rest.iter().any(|a| a == "--cost") {
println!(
"{}",
tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
);
} else if rest.iter().any(|a| a == "--tasks") {
println!(
"{}",
tools::ctx_gain::handle("tasks", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--agents") {
println!(
"{}",
tools::ctx_gain::handle("agents", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--heatmap") {
println!(
"{}",
tools::ctx_gain::handle("heatmap", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--wrapped") {
println!(
"{}",
tools::ctx_gain::handle(
"wrapped",
Some(&period),
model.as_deref(),
Some(limit)
)
);
} else if rest.iter().any(|a| a == "--pipeline") {
let stats_path = dirs::home_dir()
.unwrap_or_default()
.join(".lean-ctx")
.join("pipeline_stats.json");
if let Ok(data) = std::fs::read_to_string(&stats_path) {
if let Ok(stats) =
serde_json::from_str::<core::pipeline::PipelineStats>(&data)
{
println!("{}", stats.format_summary());
} else {
println!("No pipeline stats available yet (corrupt data).");
}
} else {
println!(
"No pipeline stats available yet. Use MCP tools to generate data."
);
}
} else if rest.iter().any(|a| a == "--deep") {
println!(
"{}\n{}\n{}\n{}\n{}",
tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
tools::ctx_gain::handle("tasks", None, None, Some(limit)),
tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
tools::ctx_gain::handle("agents", None, None, Some(limit)),
tools::ctx_gain::handle("heatmap", None, None, Some(limit))
);
} else {
println!("{}", core::stats::format_gain());
}
return;
}
"token-report" | "report-tokens" => {
let code = token_report::run_cli(&rest);
if code != 0 {
std::process::exit(code);
}
return;
}
"pack" => {
crate::cli::cmd_pack(&rest);
return;
}
"proof" => {
crate::cli::cmd_proof(&rest);
return;
}
"verify" => {
crate::cli::cmd_verify(&rest);
return;
}
"instructions" => {
crate::cli::cmd_instructions(&rest);
return;
}
"index" => {
crate::cli::cmd_index(&rest);
return;
}
"cep" => {
println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
return;
}
"dashboard" => {
if rest.iter().any(|a| a == "--help" || a == "-h") {
println!("Usage: lean-ctx dashboard [--port=N] [--host=H] [--project=PATH]");
println!("Examples:");
println!(" lean-ctx dashboard");
println!(" lean-ctx dashboard --port=3333");
println!(" lean-ctx dashboard --host=0.0.0.0");
return;
}
let port = rest
.iter()
.find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
.and_then(|p| p.parse().ok());
let host = rest
.iter()
.find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
.map(String::from);
let project = rest
.iter()
.find_map(|p| p.strip_prefix("--project="))
.map(String::from);
if let Some(ref p) = project {
std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
}
spawn_proxy_if_needed();
run_async(dashboard::start(port, host));
return;
}
"team" => {
let sub = rest.first().map_or("help", std::string::String::as_str);
match sub {
"serve" => {
#[cfg(feature = "team-server")]
{
let cfg_path = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--config=") {
return Some(v.to_string());
}
if a == "--config" {
return rest.get(i + 1).cloned();
}
None
})
.unwrap_or_default();
if cfg_path.trim().is_empty() {
eprintln!("Usage: lean-ctx team serve --config <path>");
std::process::exit(1);
}
let cfg = crate::http_server::team::TeamServerConfig::load(
std::path::Path::new(&cfg_path),
)
.unwrap_or_else(|e| {
eprintln!("Invalid team config: {e}");
std::process::exit(1);
});
if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
tracing::error!("Team server error: {e}");
std::process::exit(1);
}
return;
}
#[cfg(not(feature = "team-server"))]
{
eprintln!("lean-ctx team serve is not available in this build");
std::process::exit(1);
}
}
"token" => {
let action = rest.get(1).map_or("help", std::string::String::as_str);
if action == "create" {
#[cfg(feature = "team-server")]
{
let args = &rest[2..];
let cfg_path = args
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--config=") {
return Some(v.to_string());
}
if a == "--config" {
return args.get(i + 1).cloned();
}
None
})
.unwrap_or_default();
let token_id = args
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--id=") {
return Some(v.to_string());
}
if a == "--id" {
return args.get(i + 1).cloned();
}
None
})
.unwrap_or_default();
let scopes_csv = args
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--scopes=") {
return Some(v.to_string());
}
if let Some(v) = a.strip_prefix("--scope=") {
return Some(v.to_string());
}
if a == "--scopes" || a == "--scope" {
return args.get(i + 1).cloned();
}
None
})
.unwrap_or_default();
if cfg_path.trim().is_empty()
|| token_id.trim().is_empty()
|| scopes_csv.trim().is_empty()
{
eprintln!(
"Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
);
std::process::exit(1);
}
let cfg_p = std::path::PathBuf::from(&cfg_path);
let mut cfg = crate::http_server::team::TeamServerConfig::load(
cfg_p.as_path(),
)
.unwrap_or_else(|e| {
eprintln!("Invalid team config: {e}");
std::process::exit(1);
});
let mut scopes = Vec::new();
for part in scopes_csv.split(',') {
let p = part.trim().to_ascii_lowercase();
if p.is_empty() {
continue;
}
let scope = match p.as_str() {
"search" => crate::http_server::team::TeamScope::Search,
"graph" => crate::http_server::team::TeamScope::Graph,
"artifacts" => {
crate::http_server::team::TeamScope::Artifacts
}
"index" => crate::http_server::team::TeamScope::Index,
"events" => crate::http_server::team::TeamScope::Events,
"sessionmutations" | "session_mutations" => {
crate::http_server::team::TeamScope::SessionMutations
}
"knowledge" => {
crate::http_server::team::TeamScope::Knowledge
}
"audit" => crate::http_server::team::TeamScope::Audit,
_ => {
eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
std::process::exit(1);
}
};
if !scopes.contains(&scope) {
scopes.push(scope);
}
}
if scopes.is_empty() {
eprintln!("At least 1 scope is required");
std::process::exit(1);
}
let (token, hash) = crate::http_server::team::create_token()
.unwrap_or_else(|e| {
eprintln!("Token generation failed: {e}");
std::process::exit(1);
});
cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
id: token_id,
sha256_hex: hash,
scopes,
});
cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
eprintln!("Failed to write config: {e}");
std::process::exit(1);
});
println!("{token}");
return;
}
#[cfg(not(feature = "team-server"))]
{
eprintln!("lean-ctx team token is not available in this build");
std::process::exit(1);
}
}
eprintln!(
"Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
);
std::process::exit(1);
}
"sync" => {
#[cfg(feature = "team-server")]
{
let args = &rest[1..];
let cfg_path = args
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--config=") {
return Some(v.to_string());
}
if a == "--config" {
return args.get(i + 1).cloned();
}
None
})
.unwrap_or_default();
if cfg_path.trim().is_empty() {
eprintln!(
"Usage: lean-ctx team sync --config <path> [--workspace <id>]"
);
std::process::exit(1);
}
let only_ws = args.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--workspace=") {
return Some(v.to_string());
}
if let Some(v) = a.strip_prefix("--workspace-id=") {
return Some(v.to_string());
}
if a == "--workspace" || a == "--workspace-id" {
return args.get(i + 1).cloned();
}
None
});
let cfg = crate::http_server::team::TeamServerConfig::load(
std::path::Path::new(&cfg_path),
)
.unwrap_or_else(|e| {
eprintln!("Invalid team config: {e}");
std::process::exit(1);
});
for ws in &cfg.workspaces {
if let Some(ref only) = only_ws {
if ws.id != *only {
continue;
}
}
let git_dir = ws.root.join(".git");
if !git_dir.exists() {
eprintln!(
"workspace '{}' root is not a git repo: {}",
ws.id,
ws.root.display()
);
std::process::exit(1);
}
let status = std::process::Command::new("git")
.arg("-C")
.arg(&ws.root)
.args(["fetch", "--all", "--prune"])
.status()
.unwrap_or_else(|e| {
eprintln!(
"git fetch failed for workspace '{}': {e}",
ws.id
);
std::process::exit(1);
});
if !status.success() {
eprintln!(
"git fetch failed for workspace '{}' (exit={})",
ws.id,
status.code().unwrap_or(1)
);
std::process::exit(1);
}
}
return;
}
#[cfg(not(feature = "team-server"))]
{
eprintln!("lean-ctx team sync is not available in this build");
std::process::exit(1);
}
}
_ => {
eprintln!(
"Usage:\n lean-ctx team serve --config <path>\n lean-ctx team token create --config <path> --id <id> --scopes <csv>\n lean-ctx team sync --config <path> [--workspace <id>]"
);
std::process::exit(1);
}
}
}
"serve" => {
#[cfg(feature = "http-server")]
{
let mut cfg = crate::http_server::HttpServerConfig::default();
let mut daemon_mode = false;
let mut stop_mode = false;
let mut status_mode = false;
let mut foreground_daemon = false;
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--daemon" | "-d" => daemon_mode = true,
"--stop" => stop_mode = true,
"--status" => status_mode = true,
"--_foreground-daemon" => foreground_daemon = true,
"--host" | "-H" => {
i += 1;
if i < rest.len() {
cfg.host.clone_from(&rest[i]);
}
}
arg if arg.starts_with("--host=") => {
cfg.host = arg["--host=".len()..].to_string();
}
"--port" | "-p" => {
i += 1;
if i < rest.len() {
if let Ok(p) = rest[i].parse::<u16>() {
cfg.port = p;
}
}
}
arg if arg.starts_with("--port=") => {
if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
cfg.port = p;
}
}
"--project-root" => {
i += 1;
if i < rest.len() {
cfg.project_root = std::path::PathBuf::from(&rest[i]);
}
}
arg if arg.starts_with("--project-root=") => {
cfg.project_root =
std::path::PathBuf::from(&arg["--project-root=".len()..]);
}
"--auth-token" => {
i += 1;
if i < rest.len() {
cfg.auth_token = Some(rest[i].clone());
}
}
arg if arg.starts_with("--auth-token=") => {
cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
}
"--stateful" => cfg.stateful_mode = true,
"--stateless" => cfg.stateful_mode = false,
"--json" => cfg.json_response = true,
"--sse" => cfg.json_response = false,
"--disable-host-check" => cfg.disable_host_check = true,
"--allowed-host" => {
i += 1;
if i < rest.len() {
cfg.allowed_hosts.push(rest[i].clone());
}
}
arg if arg.starts_with("--allowed-host=") => {
cfg.allowed_hosts
.push(arg["--allowed-host=".len()..].to_string());
}
"--max-body-bytes" => {
i += 1;
if i < rest.len() {
if let Ok(n) = rest[i].parse::<usize>() {
cfg.max_body_bytes = n;
}
}
}
arg if arg.starts_with("--max-body-bytes=") => {
if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
cfg.max_body_bytes = n;
}
}
"--max-concurrency" => {
i += 1;
if i < rest.len() {
if let Ok(n) = rest[i].parse::<usize>() {
cfg.max_concurrency = n;
}
}
}
arg if arg.starts_with("--max-concurrency=") => {
if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
cfg.max_concurrency = n;
}
}
"--max-rps" => {
i += 1;
if i < rest.len() {
if let Ok(n) = rest[i].parse::<u32>() {
cfg.max_rps = n;
}
}
}
arg if arg.starts_with("--max-rps=") => {
if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
cfg.max_rps = n;
}
}
"--rate-burst" => {
i += 1;
if i < rest.len() {
if let Ok(n) = rest[i].parse::<u32>() {
cfg.rate_burst = n;
}
}
}
arg if arg.starts_with("--rate-burst=") => {
if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
cfg.rate_burst = n;
}
}
"--request-timeout-ms" => {
i += 1;
if i < rest.len() {
if let Ok(n) = rest[i].parse::<u64>() {
cfg.request_timeout_ms = n;
}
}
}
arg if arg.starts_with("--request-timeout-ms=") => {
if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
cfg.request_timeout_ms = n;
}
}
"--help" | "-h" => {
eprintln!(
"Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
\\n\\
Options:\\n\\
--daemon, -d Start as background daemon (UDS)\\n\\
--stop Stop running daemon\\n\\
--status Show daemon status\\n\\
--host, -H Bind host (default: 127.0.0.1)\\n\\
--port, -p Bind port (default: 8080)\\n\\
--project-root Resolve relative paths against this root (default: cwd)\\n\\
--auth-token Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
--stateful/--stateless Streamable HTTP session mode (default: stateless)\\n\\
--json/--sse Response framing in stateless mode (default: json)\\n\\
--max-body-bytes Max request body size in bytes (default: 2097152)\\n\\
--max-concurrency Max concurrent requests (default: 32)\\n\\
--max-rps Max requests/sec (global, default: 50)\\n\\
--rate-burst Rate limiter burst (global, default: 100)\\n\\
--request-timeout-ms REST tool-call timeout (default: 30000)\\n\\
--allowed-host Add allowed Host header (repeatable)\\n\\
--disable-host-check Disable Host header validation (unsafe)"
);
return;
}
_ => {}
}
i += 1;
}
if stop_mode {
if let Err(e) = crate::daemon::stop_daemon() {
eprintln!("Error: {e}");
std::process::exit(1);
}
return;
}
if status_mode {
println!("{}", crate::daemon::daemon_status());
return;
}
if daemon_mode {
if let Err(e) = crate::daemon::start_daemon(&rest) {
eprintln!("Error: {e}");
std::process::exit(1);
}
return;
}
if foreground_daemon {
if let Err(e) = crate::daemon::init_foreground_daemon() {
eprintln!("Error writing PID file: {e}");
std::process::exit(1);
}
let addr = crate::daemon::daemon_addr();
if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
{
tracing::error!("Daemon server error: {e}");
crate::daemon::cleanup_daemon_files();
std::process::exit(1);
}
crate::daemon::cleanup_daemon_files();
return;
}
if cfg.auth_token.is_none() {
if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
if !v.trim().is_empty() {
cfg.auth_token = Some(v);
}
}
}
if let Err(e) = run_async(crate::http_server::serve(cfg)) {
tracing::error!("HTTP server error: {e}");
std::process::exit(1);
}
return;
}
#[cfg(not(feature = "http-server"))]
{
eprintln!("lean-ctx serve is not available in this build");
std::process::exit(1);
}
}
"watch" => {
if rest.iter().any(|a| a == "--help" || a == "-h") {
println!("Usage: lean-ctx watch");
println!(" Live TUI dashboard (real-time event stream).");
return;
}
if let Err(e) = tui::run() {
tracing::error!("TUI error: {e}");
std::process::exit(1);
}
return;
}
"proxy" => {
#[cfg(feature = "http-server")]
{
let sub = rest.first().map_or("help", std::string::String::as_str);
match sub {
"start" => {
let port: u16 = rest
.iter()
.find_map(|p| {
p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
})
.and_then(|p| p.parse().ok())
.unwrap_or(4444);
let autostart = rest.iter().any(|a| a == "--autostart");
if autostart {
crate::proxy_autostart::install(port, false);
return;
}
if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
tracing::error!("Proxy error: {e}");
std::process::exit(1);
}
}
"stop" => {
let port: u16 = rest
.iter()
.find_map(|p| p.strip_prefix("--port="))
.and_then(|p| p.parse().ok())
.unwrap_or(4444);
let health_url = format!("http://127.0.0.1:{port}/health");
match ureq::get(&health_url).call() {
Ok(resp) => {
if let Ok(body) = resp.into_body().read_to_string() {
if let Some(pid_str) = body
.split("pid\":")
.nth(1)
.and_then(|s| s.split([',', '}']).next())
{
if let Ok(pid) = pid_str.trim().parse::<u32>() {
let _ =
crate::ipc::process::terminate_gracefully(pid);
std::thread::sleep(
std::time::Duration::from_millis(500),
);
if crate::ipc::process::is_alive(pid) {
let _ = crate::ipc::process::force_kill(pid);
}
println!(
"Proxy on port {port} stopped (PID {pid})."
);
return;
}
}
}
println!("Proxy on port {port} running but could not parse PID. Use `lean-ctx stop` to kill all.");
}
Err(_) => {
println!("No proxy running on port {port}.");
}
}
}
"status" => {
let port: u16 = rest
.iter()
.find_map(|p| p.strip_prefix("--port="))
.and_then(|p| p.parse().ok())
.unwrap_or(4444);
if let Ok(resp) =
ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
{
let body = resp.into_body().read_to_string().unwrap_or_default();
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
println!("lean-ctx proxy status:");
println!(" Requests: {}", v["requests_total"]);
println!(" Compressed: {}", v["requests_compressed"]);
println!(" Tokens saved: {}", v["tokens_saved"]);
println!(
" Compression: {}%",
v["compression_ratio_pct"].as_str().unwrap_or("0.0")
);
} else {
println!("{body}");
}
} else {
println!("No proxy running on port {port}.");
println!("Start with: lean-ctx proxy start");
}
}
_ => {
println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
}
}
return;
}
#[cfg(not(feature = "http-server"))]
{
eprintln!("lean-ctx proxy is not available in this build");
std::process::exit(1);
}
}
"init" => {
super::cmd_init(&rest);
return;
}
"setup" => {
let non_interactive = rest.iter().any(|a| a == "--non-interactive");
let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
let fix = rest.iter().any(|a| a == "--fix");
let json = rest.iter().any(|a| a == "--json");
let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
if non_interactive || fix || json || yes {
let opts = setup::SetupOptions {
non_interactive,
yes,
fix,
json,
no_auto_approve,
};
match setup::run_setup_with_options(opts) {
Ok(report) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&report)
.unwrap_or_else(|_| "{}".to_string())
);
}
if !report.success {
std::process::exit(1);
}
}
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
}
} else {
setup::run_setup();
}
return;
}
"install" => {
let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
let json = rest.iter().any(|a| a == "--json");
if !repair {
eprintln!("Usage: lean-ctx install --repair [--json]");
std::process::exit(1);
}
let opts = setup::SetupOptions {
non_interactive: true,
yes: true,
fix: true,
json,
..Default::default()
};
match setup::run_setup_with_options(opts) {
Ok(report) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&report)
.unwrap_or_else(|_| "{}".to_string())
);
}
if !report.success {
std::process::exit(1);
}
}
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
}
return;
}
"bootstrap" => {
let json = rest.iter().any(|a| a == "--json");
let opts = setup::SetupOptions {
non_interactive: true,
yes: true,
fix: true,
json,
..Default::default()
};
match setup::run_setup_with_options(opts) {
Ok(report) => {
if json {
println!(
"{}",
serde_json::to_string_pretty(&report)
.unwrap_or_else(|_| "{}".to_string())
);
}
if !report.success {
std::process::exit(1);
}
}
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
}
return;
}
"status" => {
let code = status::run_cli(&rest);
if code != 0 {
std::process::exit(code);
}
return;
}
"read" => {
super::cmd_read(&rest);
core::stats::flush();
return;
}
"diff" => {
super::cmd_diff(&rest);
core::stats::flush();
return;
}
"grep" => {
super::cmd_grep(&rest);
core::stats::flush();
return;
}
"find" => {
super::cmd_find(&rest);
core::stats::flush();
return;
}
"ls" => {
super::cmd_ls(&rest);
core::stats::flush();
return;
}
"deps" => {
super::cmd_deps(&rest);
core::stats::flush();
return;
}
"discover" => {
super::cmd_discover(&rest);
return;
}
"ghost" => {
super::cmd_ghost(&rest);
return;
}
"filter" => {
super::cmd_filter(&rest);
return;
}
"heatmap" => {
heatmap::cmd_heatmap(&rest);
return;
}
"graph" => {
let sub = rest.first().map_or("build", std::string::String::as_str);
match sub {
"build" => {
let root = rest.get(1).cloned().or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
});
let root = root.unwrap_or_else(|| ".".to_string());
let index = core::graph_index::load_or_build(&root);
println!(
"Graph built: {} files, {} edges",
index.files.len(),
index.edges.len()
);
}
"export-html" => {
let mut root: Option<String> = None;
let mut out: Option<String> = None;
let mut max_nodes: usize = 2500;
let args = &rest[1..];
let mut i = 0usize;
while i < args.len() {
let a = args[i].as_str();
if let Some(v) = a.strip_prefix("--root=") {
root = Some(v.to_string());
} else if a == "--root" {
root = args.get(i + 1).cloned();
i += 1;
} else if let Some(v) = a.strip_prefix("--out=") {
out = Some(v.to_string());
} else if a == "--out" {
out = args.get(i + 1).cloned();
i += 1;
} else if let Some(v) = a.strip_prefix("--max-nodes=") {
max_nodes = v.parse::<usize>().unwrap_or(0);
} else if a == "--max-nodes" {
let v = args.get(i + 1).map_or("", String::as_str);
max_nodes = v.parse::<usize>().unwrap_or(0);
i += 1;
}
i += 1;
}
let root = root
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
})
.unwrap_or_else(|| ".".to_string());
let Some(out) = out else {
eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
std::process::exit(1);
};
if max_nodes == 0 {
eprintln!("--max-nodes must be >= 1");
std::process::exit(1);
}
core::graph_export::export_graph_html(
&root,
std::path::Path::new(&out),
max_nodes,
)
.unwrap_or_else(|e| {
eprintln!("graph export failed: {e}");
std::process::exit(1);
});
println!("{out}");
}
_ => {
eprintln!(
"Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
);
std::process::exit(1);
}
}
return;
}
"smells" => {
let action = rest.first().map_or("summary", String::as_str);
let rule = rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--rule=") {
return Some(v.to_string());
}
if a == "--rule" {
return rest.get(i + 1).cloned();
}
None
});
let path = rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--path=") {
return Some(v.to_string());
}
if a == "--path" {
return rest.get(i + 1).cloned();
}
None
});
let root = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--root=") {
return Some(v.to_string());
}
if a == "--root" {
return rest.get(i + 1).cloned();
}
None
})
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
})
.unwrap_or_else(|| ".".to_string());
let fmt = if rest.iter().any(|a| a == "--json") {
Some("json")
} else {
None
};
println!(
"{}",
tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
);
return;
}
"session" => {
super::cmd_session_action(&rest);
return;
}
"control" | "context-control" => {
super::cmd_control(&rest);
return;
}
"plan" | "context-plan" => {
super::cmd_plan(&rest);
return;
}
"compile" | "context-compile" => {
super::cmd_compile(&rest);
return;
}
"knowledge" => {
super::cmd_knowledge(&rest);
return;
}
"overview" => {
super::cmd_overview(&rest);
return;
}
"compress" => {
super::cmd_compress(&rest);
return;
}
"wrapped" => {
super::cmd_wrapped(&rest);
return;
}
"sessions" => {
super::cmd_sessions(&rest);
return;
}
"benchmark" => {
super::cmd_benchmark(&rest);
return;
}
"profile" => {
super::cmd_profile(&rest);
return;
}
"config" => {
super::cmd_config(&rest);
return;
}
"stats" => {
super::cmd_stats(&rest);
return;
}
"cache" => {
super::cmd_cache(&rest);
return;
}
"theme" => {
super::cmd_theme(&rest);
return;
}
"tee" => {
super::cmd_tee(&rest);
return;
}
"terse" | "compression" => {
super::cmd_compression(&rest);
return;
}
"slow-log" => {
super::cmd_slow_log(&rest);
return;
}
"update" | "--self-update" => {
core::updater::run(&rest);
return;
}
"restart" => {
cmd_restart();
return;
}
"stop" => {
cmd_stop();
return;
}
"dev-install" => {
cmd_dev_install();
return;
}
"doctor" => {
let code = doctor::run_cli(&rest);
if code != 0 {
std::process::exit(code);
}
return;
}
"gotchas" | "bugs" => {
super::cloud::cmd_gotchas(&rest);
return;
}
"learn" => {
super::cmd_learn(&rest);
return;
}
"buddy" | "pet" => {
super::cloud::cmd_buddy(&rest);
return;
}
"hook" => {
hook_handlers::mark_hook_environment();
hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
let action = rest.first().map_or("help", std::string::String::as_str);
match action {
"rewrite" => hook_handlers::handle_rewrite(),
"redirect" => hook_handlers::handle_redirect(),
"observe" => hook_handlers::handle_observe(),
"copilot" => hook_handlers::handle_copilot(),
"codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
"codex-session-start" => hook_handlers::handle_codex_session_start(),
"rewrite-inline" => hook_handlers::handle_rewrite_inline(),
_ => {
eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
std::process::exit(1);
}
}
return;
}
"report-issue" | "report" => {
report::run(&rest);
return;
}
"uninstall" => {
let dry_run = rest.iter().any(|a| a == "--dry-run");
uninstall::run(dry_run);
return;
}
"bypass" => {
if rest.is_empty() {
eprintln!("Usage: lean-ctx bypass \"command\"");
eprintln!("Runs the command with zero compression (raw passthrough).");
std::process::exit(1);
}
let command = if rest.len() == 1 {
rest[0].clone()
} else {
shell::join_command(&args[2..])
};
std::env::set_var("LEAN_CTX_RAW", "1");
let code = shell::exec(&command);
std::process::exit(code);
}
"safety-levels" | "safety" => {
println!("{}", core::compression_safety::format_safety_table());
return;
}
"cheat" | "cheatsheet" | "cheat-sheet" => {
super::cmd_cheatsheet();
return;
}
"login" => {
super::cloud::cmd_login(&rest);
return;
}
"register" => {
super::cloud::cmd_register(&rest);
return;
}
"forgot-password" => {
super::cloud::cmd_forgot_password(&rest);
return;
}
"sync" => {
super::cloud::cmd_sync();
return;
}
"contribute" => {
super::cloud::cmd_contribute();
return;
}
"cloud" => {
super::cloud::cmd_cloud(&rest);
return;
}
"upgrade" => {
super::cloud::cmd_upgrade();
return;
}
"--version" | "-V" => {
println!("{}", core::integrity::origin_line());
return;
}
"--help" | "-h" => {
print_help();
return;
}
"mcp" => {}
_ => {
tracing::error!("lean-ctx: unknown command '{}'", args[1]);
print_help();
std::process::exit(1);
}
}
}
if let Err(e) = run_mcp_server() {
tracing::error!("lean-ctx: {e}");
std::process::exit(1);
}
}
fn passthrough(command: &str) -> ! {
let (shell, flag) = shell::shell_and_flag();
let status = std::process::Command::new(&shell)
.arg(&flag)
.arg(command)
.env("LEAN_CTX_ACTIVE", "1")
.status()
.map_or(127, |s| s.code().unwrap_or(1));
std::process::exit(status);
}
fn run_async<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Runtime::new()
.expect("failed to create async runtime")
.block_on(future)
}
fn run_mcp_server() -> Result<()> {
use rmcp::ServiceExt;
std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
crate::core::startup_guard::crash_loop_backoff("mcp-server");
let startup_lock = crate::core::startup_guard::try_acquire_lock(
"mcp-startup",
std::time::Duration::from_secs(3),
std::time::Duration::from_secs(30),
);
let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
let worker_threads = resolve_worker_threads(parallelism);
let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads)
.max_blocking_threads(max_blocking_threads)
.enable_all()
.build()?;
let server = tools::create_server();
drop(startup_lock);
spawn_proxy_if_needed();
rt.block_on(async {
core::logging::init_mcp_logging();
core::protocol::set_mcp_context(true);
tracing::info!(
"lean-ctx v{} MCP server starting",
env!("CARGO_PKG_VERSION")
);
let transport =
mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
let server_handle = server.clone();
let service = match server.serve(transport).await {
Ok(s) => s,
Err(e) => {
let msg = e.to_string();
if msg.contains("expect initialized")
|| msg.contains("context canceled")
|| msg.contains("broken pipe")
{
tracing::debug!("Client disconnected before init: {msg}");
return Ok(());
}
return Err(e.into());
}
};
match service.waiting().await {
Ok(reason) => {
tracing::info!("MCP server stopped: {reason:?}");
}
Err(e) => {
let msg = e.to_string();
if msg.contains("broken pipe")
|| msg.contains("connection reset")
|| msg.contains("context canceled")
{
tracing::info!("MCP server: transport closed ({msg})");
} else {
tracing::error!("MCP server error: {msg}");
}
}
}
server_handle.shutdown().await;
core::stats::flush();
core::heatmap::flush();
core::mode_predictor::ModePredictor::flush();
core::feedback::FeedbackStore::flush();
Ok(())
})
}
fn print_help() {
println!(
"lean-ctx {version} — Context Runtime for AI Agents
60+ compression patterns | 51 MCP tools | 10 read modes | Context Continuity Protocol
USAGE:
lean-ctx Start MCP server (stdio)
lean-ctx serve Start MCP server (Streamable HTTP)
lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
lean-ctx serve --stop Stop running daemon
lean-ctx serve --status Show daemon status
lean-ctx -t \"command\" Track command (full output + stats, no compression)
lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
lean-ctx -c --raw \"command\" Execute without compression (full output)
lean-ctx exec \"command\" Same as -c
lean-ctx shell Interactive shell with compression
COMMANDS:
gain Visual dashboard (colors, bars, sparklines, USD)
gain --live Live mode: auto-refreshes every 1s in-place
gain --graph 30-day savings chart
gain --daily Bordered day-by-day table with USD
gain --json Raw JSON export of all stats
token-report [--json] Token + memory report (project + session + CEP)
pack --pr PR Context Pack (changed files, impact, tests, artifacts)
index <status|build|build-full|watch> Codebase index utilities
cep CEP impact report (score trends, cache, modes)
watch Live TUI dashboard (real-time event stream)
dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
proxy start [--port=4444] API proxy: compress tool_results before LLM API
proxy status Show proxy statistics
cache [list|clear|stats] Show/manage file read cache
wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
benchmark run [path] [--json] Run real benchmark on project files
benchmark report [path] Generate shareable Markdown report
cheatsheet Command cheat sheet & workflow quick reference
setup One-command setup: shell + editor + verify
install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
bootstrap Non-interactive setup + fix (zero-config)
status [--json] Show setup + MCP + rules status
init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
init --agent <name> Configure MCP for specific editor/agent
read <file> [-m mode] Read file with compression
diff <file1> <file2> Compressed file diff
grep <pattern> [path] Search with compressed output
find <pattern> [path] Find files with compressed output
ls [path] Directory listing with compression
deps [path] Show project dependencies
discover Find uncompressed commands in shell history
ghost [--json] Ghost Token report: find hidden token waste
filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
session Show adoption statistics
session task <desc> Set current task
session finding <summary> Record a finding
session save Save current session
session load [id] Load session (latest if no ID)
knowledge remember <value> --category <c> --key <k> Store a fact
knowledge recall [query] [--category <c>] Retrieve facts
knowledge search <query> Cross-project knowledge search
knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
knowledge remove --category <c> --key <k> Remove a fact
knowledge status Knowledge base summary
overview [task] Project overview (task-contextualized if given)
compress [--signatures] Context compression checkpoint
config Show/edit configuration (~/.lean-ctx/config.toml)
profile [list|show|diff|create|set] Manage context profiles
theme [list|set|export|import] Customize terminal colors and themes
tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
update [--check] Self-update lean-ctx binary from GitHub Releases
stop Stop ALL lean-ctx processes (daemon, proxy, orphans)
restart Restart daemon (applies config.toml changes)
dev-install Build release + atomic install + restart (for development)
gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
doctor integrations [--json] Integration health checks (Cursor/Claude Code)
doctor [--fix] [--json] Run diagnostics (and optionally repair)
smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
Code smell detection (Property Graph, 8 rules)
control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
uninstall Remove shell hook, MCP configs, and data directory
SHELL HOOK PATTERNS (95+):
git status, log, diff, add, commit, push, pull, fetch, clone,
branch, checkout, switch, merge, stash, tag, reset, remote
docker build, ps, images, logs, compose, exec, network
npm/pnpm install, test, run, list, outdated, audit
cargo build, test, check, clippy
gh pr list/view/create, issue list/view, run list/view
kubectl get pods/services/deployments, logs, describe, apply
python pip install/list/outdated, ruff check/format, poetry, uv
linters eslint, biome, prettier, golangci-lint
builds tsc, next build, vite build
ruby rubocop, bundle install/update, rake test, rails test
tests jest, vitest, pytest, go test, playwright, rspec, minitest
iac terraform, make, maven, gradle, dotnet, flutter, dart
utils curl, grep/rg, find, ls, wget, env
data JSON schema extraction, log deduplication
READ MODES:
auto Auto-select optimal mode (default)
full Full content (cached re-reads = 13 tokens)
map Dependency graph + API signatures
signatures tree-sitter AST extraction (18 languages)
task Task-relevant filtering (requires ctx_session task)
reference One-line reference stub (cheap cache key)
aggressive Syntax-stripped content
entropy Shannon entropy filtered
diff Changed lines only
lines:N-M Specific line ranges (e.g. lines:10-50,80)
ENVIRONMENT:
LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
LEAN_CTX_RAW=1 Same as --raw for current command
LEAN_CTX_AUTONOMY=false Disable autonomous features
LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
OPTIONS:
--version, -V Show version
--help, -h Show this help
EXAMPLES:
lean-ctx -c \"git status\" Compressed git output
lean-ctx -c \"kubectl get pods\" Compressed k8s output
lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
lean-ctx gain Visual terminal dashboard
lean-ctx gain --live Live auto-updating terminal dashboard
lean-ctx gain --graph 30-day savings chart
lean-ctx gain --daily Day-by-day breakdown with USD
lean-ctx token-report --json Machine-readable token + memory report
lean-ctx dashboard Open web dashboard at localhost:3333
lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
lean-ctx gain --wrapped Wrapped report card (recommended)
lean-ctx gain --wrapped --period=month Monthly Wrapped report card
lean-ctx sessions list List all CCP sessions
lean-ctx sessions show Show latest session state
lean-ctx discover Find missed savings in shell history
lean-ctx setup One-command setup (shell + editors + verify)
lean-ctx install --repair Premium repair path (non-interactive, merge-based)
lean-ctx bootstrap Non-interactive setup + fix (zero-config)
lean-ctx bootstrap --json Machine-readable bootstrap report
lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
lean-ctx-on Enable shell aliases in track mode (full output + stats)
lean-ctx-off Disable all shell aliases
lean-ctx-mode track Track mode: full output, stats recorded (default)
lean-ctx-mode compress Compress mode: all output compressed (power users)
lean-ctx-mode off Same as lean-ctx-off
lean-ctx-status Show whether compression is active
lean-ctx init --agent pi Install Pi Coding Agent extension
lean-ctx doctor Check PATH, config, MCP, and dashboard port
lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
lean-ctx doctor --fix --json Repair + machine-readable report
lean-ctx status --json Machine-readable current status
lean-ctx session task \"implement auth\"
lean-ctx session finding \"auth.rs:42 — missing validation\"
lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
lean-ctx knowledge recall \"authentication\"
lean-ctx knowledge search \"database migration\"
lean-ctx overview \"refactor auth module\"
lean-ctx compress --signatures
lean-ctx read src/main.rs -m map
lean-ctx grep \"pub fn\" src/
lean-ctx deps .
CLOUD:
cloud status Show cloud connection status
login <email> Log into existing LeanCTX Cloud account
register <email> Create a new LeanCTX Cloud account
forgot-password <email> Send password reset email
sync Upload local stats to cloud dashboard
contribute Share anonymized compression data
TROUBLESHOOTING:
Commands broken? lean-ctx-off (fixes current session)
Permanent fix? lean-ctx uninstall (removes all hooks)
Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
Binary missing? Aliases auto-fallback to original commands (safe)
Preview init? lean-ctx init --global --dry-run
WEBSITE: https://leanctx.com
GITHUB: https://github.com/yvgude/lean-ctx
",
version = env!("CARGO_PKG_VERSION"),
);
}
fn cmd_stop() {
use crate::daemon;
use crate::ipc;
eprintln!("Stopping all lean-ctx processes…");
crate::proxy_autostart::stop();
eprintln!(" Unloaded autostart (LaunchAgent/systemd).");
if let Err(e) = daemon::stop_daemon() {
eprintln!(" Warning: daemon stop: {e}");
}
let killed = ipc::process::kill_all_by_name("lean-ctx");
if killed > 0 {
eprintln!(" Sent SIGTERM to {killed} process(es).");
}
std::thread::sleep(std::time::Duration::from_millis(500));
let remaining = ipc::process::find_killable_pids("lean-ctx");
if !remaining.is_empty() {
eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
for &pid in &remaining {
let _ = ipc::process::force_kill(pid);
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
daemon::cleanup_daemon_files();
let final_check = ipc::process::find_killable_pids("lean-ctx");
if final_check.is_empty() {
eprintln!(" ✓ All lean-ctx processes stopped.");
} else {
eprintln!(
" ✗ {} process(es) could not be killed: {:?}",
final_check.len(),
final_check
);
eprintln!(
" Try: sudo kill -9 {}",
final_check
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
);
std::process::exit(1);
}
}
fn cmd_restart() {
use crate::daemon;
use crate::ipc;
eprintln!("Restarting lean-ctx…");
crate::proxy_autostart::stop();
if let Err(e) = daemon::stop_daemon() {
eprintln!(" Warning: daemon stop: {e}");
}
let orphans = ipc::process::kill_all_by_name("lean-ctx");
if orphans > 0 {
eprintln!(" Terminated {orphans} orphan process(es).");
}
std::thread::sleep(std::time::Duration::from_millis(500));
let remaining = ipc::process::find_pids_by_name("lean-ctx");
if !remaining.is_empty() {
eprintln!(
" Force-killing {} stubborn process(es): {:?}",
remaining.len(),
remaining
);
for &pid in &remaining {
let _ = ipc::process::force_kill(pid);
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
daemon::cleanup_daemon_files();
crate::proxy_autostart::start();
match daemon::start_daemon(&[]) {
Ok(()) => eprintln!(" ✓ Daemon restarted. Config changes are now active."),
Err(e) => {
eprintln!(" ✗ Daemon start failed: {e}");
std::process::exit(1);
}
}
}
fn cmd_dev_install() {
use crate::ipc;
let cargo_root = find_cargo_project_root();
let Some(cargo_root) = cargo_root else {
eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
std::process::exit(1);
};
eprintln!("Building release binary…");
let build = std::process::Command::new("cargo")
.args(["build", "--release"])
.current_dir(&cargo_root)
.status();
match build {
Ok(s) if s.success() => {}
Ok(s) => {
eprintln!(" Build failed with exit code {}", s.code().unwrap_or(-1));
std::process::exit(1);
}
Err(e) => {
eprintln!(" Build failed: {e}");
std::process::exit(1);
}
}
let built_binary = cargo_root.join("target/release/lean-ctx");
if !built_binary.exists() {
eprintln!(
" Error: Built binary not found at {}",
built_binary.display()
);
std::process::exit(1);
}
let install_path = resolve_install_path();
eprintln!("Installing to {}…", install_path.display());
eprintln!(" Stopping all lean-ctx processes…");
crate::proxy_autostart::stop();
let _ = crate::daemon::stop_daemon();
ipc::process::kill_all_by_name("lean-ctx");
std::thread::sleep(std::time::Duration::from_millis(500));
let remaining = ipc::process::find_pids_by_name("lean-ctx");
if !remaining.is_empty() {
eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
for &pid in &remaining {
let _ = ipc::process::force_kill(pid);
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let old_path = install_path.with_extension("old");
if install_path.exists() {
if let Err(e) = std::fs::rename(&install_path, &old_path) {
eprintln!(" Warning: rename existing binary: {e}");
}
}
match std::fs::copy(&built_binary, &install_path) {
Ok(_) => {
let _ = std::fs::remove_file(&old_path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ =
std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
}
eprintln!(" ✓ Binary installed.");
}
Err(e) => {
eprintln!(" Error: copy failed: {e}");
if old_path.exists() {
let _ = std::fs::rename(&old_path, &install_path);
eprintln!(" Rolled back to previous binary.");
}
std::process::exit(1);
}
}
let version = std::process::Command::new(&install_path)
.arg("--version")
.output()
.map_or_else(
|_| "unknown".to_string(),
|o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
);
eprintln!(" ✓ dev-install complete: {version}");
eprintln!(" Re-enabling autostart…");
crate::proxy_autostart::start();
eprintln!(" Starting daemon…");
match crate::daemon::start_daemon(&[]) {
Ok(()) => {}
Err(e) => eprintln!(" Warning: daemon start: {e} (will be started by editor)"),
}
}
fn find_cargo_project_root() -> Option<std::path::PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
if dir.join("Cargo.toml").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
fn resolve_install_path() -> std::path::PathBuf {
if let Ok(exe) = std::env::current_exe() {
if let Ok(canonical) = exe.canonicalize() {
let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
if !is_in_cargo_target && canonical.exists() {
return canonical;
}
}
}
if let Ok(home) = std::env::var("HOME") {
let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
if local_bin.parent().is_some_and(std::path::Path::exists) {
return local_bin;
}
}
std::path::PathBuf::from("/usr/local/bin/lean-ctx")
}
fn spawn_proxy_if_needed() {
use std::net::TcpStream;
use std::time::Duration;
let port = crate::proxy_setup::default_port();
let already_running = TcpStream::connect_timeout(
&format!("127.0.0.1:{port}").parse().unwrap(),
Duration::from_millis(200),
)
.is_ok();
if already_running {
tracing::debug!("proxy already running on port {port}");
return;
}
let binary = std::env::current_exe().map_or_else(
|_| "lean-ctx".to_string(),
|p| p.to_string_lossy().to_string(),
);
match std::process::Command::new(&binary)
.args(["proxy", "start", &format!("--port={port}")])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(_) => tracing::info!("auto-started proxy on port {port}"),
Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
}
}
fn resolve_worker_threads(parallelism: usize) -> usize {
std::env::var("LEAN_CTX_WORKER_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or_else(|| parallelism.clamp(1, 4))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn worker_threads_default_clamps_low() {
std::env::remove_var("LEAN_CTX_WORKER_THREADS");
assert_eq!(resolve_worker_threads(1), 1);
}
#[test]
fn worker_threads_default_clamps_high() {
std::env::remove_var("LEAN_CTX_WORKER_THREADS");
assert_eq!(resolve_worker_threads(32), 4);
}
#[test]
fn worker_threads_default_passthrough() {
std::env::remove_var("LEAN_CTX_WORKER_THREADS");
assert_eq!(resolve_worker_threads(3), 3);
}
#[test]
fn worker_threads_env_override() {
std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
assert_eq!(resolve_worker_threads(2), 12);
std::env::remove_var("LEAN_CTX_WORKER_THREADS");
}
#[test]
fn worker_threads_env_invalid_falls_back() {
std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
assert_eq!(resolve_worker_threads(3), 3);
std::env::remove_var("LEAN_CTX_WORKER_THREADS");
}
}