use crate::core;
pub(super) fn cmd_stop() {
use crate::daemon;
use crate::ipc;
eprintln!("Stopping all lean-ctx processes…");
crate::proxy_autostart::stop();
crate::daemon_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);
}
}
pub(super) fn cmd_restart() {
use crate::daemon;
use crate::ipc;
eprintln!("Restarting lean-ctx…");
crate::proxy_autostart::stop();
crate::daemon_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_killable_pids("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();
if crate::daemon_autostart::is_installed() {
crate::daemon_autostart::start();
eprintln!(" ✓ Daemon restarted via autostart.");
} else {
match daemon::start_daemon(&[]) {
Ok(()) => eprintln!(" ✓ Daemon restarted."),
Err(e) => {
eprintln!(" ✗ Daemon start failed: {e}");
std::process::exit(1);
}
}
}
}
pub(super) 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();
crate::daemon_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));
}
if let Err(e) = atomic_install_binary(&built_binary, &install_path) {
eprintln!(" Error: {e}");
std::process::exit(1);
}
eprintln!(" ✓ Binary installed.");
#[cfg(target_os = "macos")]
if !crate::core::codesign::is_ready() {
eprintln!(
" ⚠ macOS: run `lean-ctx codesign-setup` once to stop the recurring\n \
\"lean-ctx wants to access your Documents\" prompt after updates (#356)."
);
}
reconcile_binary_drift(&install_path);
let mut verify = std::process::Command::new(&install_path);
verify.arg("--version");
let version = ipc::process::run_with_timeout(verify, std::time::Duration::from_secs(10))
.filter(|o| o.status.success())
.map_or_else(
|| "unknown (version check timed out)".to_string(),
|o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
);
eprintln!(" ✓ dev-install complete: {version}");
eprintln!(" Re-enabling autostart…");
if crate::proxy_autostart::is_installed() {
crate::proxy_autostart::install(crate::proxy_setup::default_port(), true);
}
if crate::daemon_autostart::is_installed() {
crate::daemon_autostart::install(true);
eprintln!(" ✓ Daemon restarted via autostart.");
} else {
eprintln!(" Starting daemon…");
match crate::daemon::start_daemon(&[]) {
Ok(()) => {}
Err(e) => eprintln!(" Warning: daemon start: {e} (will be started by editor)"),
}
}
}
#[cfg(target_os = "macos")]
pub(super) fn cmd_codesign_setup() {
use crate::core::codesign::{SetupOutcome, setup_identity, sign_binary};
eprintln!("Setting up a stable code-signing identity for lean-ctx (#356)…");
eprintln!(
" This stops the recurring macOS \"access to your Documents folder\" prompt.\n \
macOS will ask ONCE to authorize the trust setting — confirm with Touch ID\n \
or your login password.\n"
);
match setup_identity() {
Ok(SetupOutcome::AlreadyReady) => {
eprintln!(" ✓ Identity already set up and trusted. Nothing to do.");
}
Ok(SetupOutcome::Created) => {
eprintln!(" ✓ Signing identity created and trusted.");
if let Ok(exe) = std::env::current_exe()
&& sign_binary(&exe) == crate::core::codesign::SignKind::Stable
{
eprintln!(" ✓ Re-signed {} with the stable identity.", exe.display());
}
eprintln!(
"\n Done. `dev-install` and self-updates now reuse this identity.\n \
Click \"Allow\" on the next Documents prompt — it won't come back."
);
}
Err(e) => {
eprintln!(" ✗ Setup failed: {e}");
eprintln!(
" The binary still works (ad-hoc signed); the prompt may recur until\n \
setup succeeds. Re-run `lean-ctx codesign-setup` to retry."
);
std::process::exit(1);
}
}
}
#[cfg(not(target_os = "macos"))]
pub(super) fn cmd_codesign_setup() {
eprintln!("codesign-setup is only needed on macOS.");
}
fn atomic_install_binary(src: &std::path::Path, dst: &std::path::Path) -> Result<(), String> {
let staged = dst.with_extension("new");
let _ = std::fs::remove_file(&staged);
std::fs::copy(src, &staged).map_err(|e| format!("staging copy failed: {e}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("chmod failed: {e}"))?;
}
#[cfg(target_os = "macos")]
let _ = std::fs::remove_file(dst);
if let Err(e) = std::fs::rename(&staged, dst) {
let _ = std::fs::remove_file(&staged);
return Err(format!("atomic rename failed: {e}"));
}
#[cfg(target_os = "macos")]
{
let _ = crate::core::codesign::sign_binary(dst);
}
Ok(())
}
pub(super) 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;
}
}
}
pub(super) fn resolve_install_path() -> std::path::PathBuf {
if let Ok(exe) = std::env::current_exe()
&& 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")
}
#[cfg(unix)]
fn is_homebrew_cellar_link(target: &std::path::Path) -> bool {
let s = target.to_string_lossy();
s.contains("/Cellar/") || s.contains("/linuxbrew/")
}
fn reconcile_binary_drift(install_path: &std::path::Path) {
#[cfg(unix)]
{
let install_canon =
std::fs::canonicalize(install_path).unwrap_or_else(|_| install_path.to_path_buf());
for shim in [
"/opt/homebrew/bin/lean-ctx",
"/usr/local/bin/lean-ctx",
"/home/linuxbrew/.linuxbrew/bin/lean-ctx",
] {
let shim_path = std::path::Path::new(shim);
let Ok(target) = std::fs::read_link(shim_path) else {
continue;
};
if !is_homebrew_cellar_link(&target) {
continue;
}
if std::fs::canonicalize(shim_path).is_ok_and(|c| c == install_canon) {
continue;
}
let _ = std::fs::remove_file(shim_path);
match std::os::unix::fs::symlink(install_path, shim_path) {
Ok(()) => eprintln!(
" ✓ Repointed stale Homebrew shim {shim} → {} (#559 drift fix)",
install_path.display()
),
Err(e) => eprintln!(
" ⚠ Stale Homebrew shim {shim} → {} couldn't be repointed ({e}). \
Run: brew unlink lean-ctx",
target.display()
),
}
}
if let Ok(path_var) = std::env::var("PATH") {
for dir in std::env::split_paths(&path_var) {
let cand = dir.join("lean-ctx");
if !cand.exists() {
continue;
}
let cand_canon = std::fs::canonicalize(&cand).unwrap_or_else(|_| cand.clone());
if cand_canon == install_canon {
break; }
eprintln!(
" ⚠ PATH shadow: {} resolves before {} — plain `lean-ctx` may run an older build.",
cand.display(),
install_path.display()
);
break;
}
}
}
#[cfg(not(unix))]
{
let _ = install_path;
}
}
pub(super) fn spawn_proxy_if_needed() {
use std::net::TcpStream;
let cfg = core::config::Config::load();
if cfg.proxy_enabled != Some(true) {
return;
}
let port = crate::proxy_setup::default_port();
let already_running = {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
TcpStream::connect_timeout(&addr, crate::proxy_setup::proxy_timeout()).is_ok()
};
if already_running {
tracing::debug!("proxy already running on port {port}");
return;
}
let binary = core::portable_binary::resolve_portable_binary();
let mut cmd = std::process::Command::new(&binary);
cmd.args(["proxy", "start", &format!("--port={port}")])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
match crate::ipc::process::spawn_detached(&mut cmd) {
Ok(_) => tracing::info!("auto-started proxy on port {port}"),
Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
}
}
#[cfg(all(test, unix))]
mod tests {
use super::is_homebrew_cellar_link;
use std::path::Path;
#[test]
fn cellar_and_linuxbrew_links_are_detected() {
assert!(is_homebrew_cellar_link(Path::new(
"../Cellar/lean-ctx/3.7.1/bin/lean-ctx"
)));
assert!(is_homebrew_cellar_link(Path::new(
"/opt/homebrew/Cellar/lean-ctx/3.8.0/bin/lean-ctx"
)));
assert!(is_homebrew_cellar_link(Path::new(
"/home/linuxbrew/.linuxbrew/Cellar/lean-ctx/1.0/bin/lean-ctx"
)));
}
#[test]
fn non_brew_targets_are_left_alone() {
assert!(!is_homebrew_cellar_link(Path::new(
"/Users/me/.local/bin/lean-ctx"
)));
assert!(!is_homebrew_cellar_link(Path::new("/usr/local/bin/other")));
assert!(!is_homebrew_cellar_link(Path::new("lean-ctx")));
}
}