#!/bin/sh # install.sh — Install lean-ctx (download pre-built binary or build from source) # # Usage: # ./install.sh # download pre-built binary (no Rust needed) # ./install.sh --download # download pre-built binary (no Rust needed) # ./install.sh --cuda # download Linux x86_64 CUDA-enabled binary # ./install.sh --build-only # build only, don't install # ./install.sh --uninstall # fully remove lean-ctx (processes, configs, autostart, data, binary) # # One-liner (no Rust required): # curl -fsSL https://leanctx.com/install.sh | sh # curl -fsSL https://leanctx.com/install.sh | bash # # Uninstall one-liner: # curl -fsSL https://leanctx.com/install.sh | sh -s -- --uninstall set -eu REPO="yvgude/lean-ctx" INSTALL_DIR="${LEAN_CTX_INSTALL_DIR:-$HOME/.local/bin}" INSTALL_FLAVOR="${LEAN_CTX_INSTALL_FLAVOR:-cpu}" # Resolve the script's directory when invoked as a file. When piped via # `curl ... | sh`, $0 is "sh" (or similar) — the [ -f "$0" ] guard then # falls back to pwd, which is what the bottom-of-file dispatcher expects: # RUST_DIR check fails outside the repo, so we route to install_download. SCRIPT_DIR="$( src="$0" if [ -n "$src" ] && [ -f "$src" ]; then cd "$(dirname "$src")" 2>/dev/null && pwd else pwd fi )" RUST_DIR="$SCRIPT_DIR/rust" echo "lean-ctx installer" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" finish() { # --- Auto-fix PATH if needed --- case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) echo "" shell_name="$(basename "${SHELL:-bash}" 2>/dev/null || echo bash)" rc="$HOME/.bashrc" case "$shell_name" in zsh) rc="$HOME/.zshrc" ;; fish) rc="$HOME/.config/fish/config.fish" ;; esac if [ "${LEAN_CTX_NO_PATH_FIX:-}" = "1" ]; then echo "Warning: $INSTALL_DIR is not in your PATH." echo " Add it manually, then run: lean-ctx onboard" else echo "Adding $INSTALL_DIR to PATH..." if [ "$shell_name" = "fish" ]; then printf '\nfish_add_path %s\n' "$INSTALL_DIR" >> "$rc" 2>/dev/null || true else printf '\nexport PATH="%s:$PATH"\n' "$INSTALL_DIR" >> "$rc" 2>/dev/null || true if [ "$shell_name" = "bash" ] && [ "$(uname -s)" = "Darwin" ]; then grep -qs '.bashrc' "$HOME/.bash_profile" 2>/dev/null || \ printf '\n[ -f ~/.bashrc ] && . ~/.bashrc\n' >> "$HOME/.bash_profile" 2>/dev/null || true fi fi export PATH="$INSTALL_DIR:$PATH" echo " Done. PATH updated in $rc and current session." fi ;; esac echo "" echo "Done! lean-ctx $(\"$INSTALL_DIR/lean-ctx\" --version 2>/dev/null || echo 'installed')." # --- Auto-onboard unless opted out --- if [ "${LEAN_CTX_NO_ONBOARD:-}" = "1" ]; then echo "" echo "Next step: Run 'lean-ctx onboard' to connect your AI tools." return fi echo "" echo "Running onboard (connecting your AI tools)..." "$INSTALL_DIR/lean-ctx" onboard 2>&1 || true # --- Detect installed agents and suggest wrap --- echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Setup complete! Quick start:" echo "" echo " lean-ctx wrap cursor # one-command setup for Cursor" echo " lean-ctx wrap claude # one-command setup for Claude Code" echo " lean-ctx wrap codex # one-command setup for Codex CLI" echo "" echo " lean-ctx doctor # verify installation" echo " lean-ctx gain # see savings after first use" echo "" echo "Full control: lean-ctx setup (interactive wizard)" echo "Skip auto-onboard: curl ... | LEAN_CTX_NO_ONBOARD=1 sh" } detect_target() { os="$(uname -s | tr '[:upper:]' '[:lower:]')" arch="$(uname -m)" case "$arch" in x86_64) arch="x86_64" ;; arm64|aarch64) arch="aarch64" ;; *) echo "Error: unsupported architecture '$arch'" echo "Build from source instead: ./install.sh" exit 1 ;; esac case "$os" in linux) libc="musl" if command -v ldd >/dev/null 2>&1; then glibc_ver="$(ldd --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)" if [ -n "$glibc_ver" ]; then major="${glibc_ver%%.*}" minor="${glibc_ver##*.}" if [ "$major" -gt 2 ] || { [ "$major" -eq 2 ] && [ "$minor" -ge 35 ]; }; then libc="gnu" fi fi fi echo "${arch}-unknown-linux-${libc}" ;; darwin) echo "${arch}-apple-darwin" ;; *) echo "Error: unsupported OS '$os'" echo "Windows: download from https://github.com/${REPO}/releases/latest" exit 1 ;; esac } verify_checksum() { file="$1" expected="$2" if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$file" | cut -d' ' -f1)" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$file" | cut -d' ' -f1)" else echo "Warning: no sha256sum/shasum found, skipping checksum verification" return 0 fi if [ "$actual" != "$expected" ]; then echo "Error: checksum mismatch!" echo " Expected: $expected" echo " Got: $actual" exit 1 fi echo " Checksum verified ✓" } # Stop any running lean-ctx before swapping the binary. The proxy runs as a # LaunchAgent/systemd unit with KeepAlive, so without this it keeps a stale # binary alive (and may respawn mid-swap); `lean-ctx stop` boots it out so the # freshly installed binary is picked up cleanly on next use. No-op on a first # install (nothing on PATH yet). Best-effort — never fails the install. stop_running_instance() { if command -v lean-ctx >/dev/null 2>&1; then echo "Stopping running lean-ctx (if any)..." lean-ctx stop >/dev/null 2>&1 || true fi } install_download() { target="$(detect_target)" case "$INSTALL_FLAVOR" in cuda|gpu) if [ "$target" != "x86_64-unknown-linux-gnu" ]; then echo "Error: CUDA pre-built binary is currently published for x86_64 GNU/Linux only." echo "Detected: $target" exit 1 fi target="${target}-cuda" ;; cpu|"") ;; *) echo "Error: unknown install flavor '$INSTALL_FLAVOR' (expected cpu or cuda)" exit 1 ;; esac echo "Mode: download pre-built binary" echo "Platform: $target" echo "" echo "Fetching latest release..." latest="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' | head -1 | cut -d'"' -f4)" if [ -z "$latest" ]; then echo "Error: could not determine latest release." exit 1 fi echo "Latest: $latest" asset_url="https://github.com/${REPO}/releases/download/${latest}/lean-ctx-${target}.tar.gz" sums_url="https://github.com/${REPO}/releases/download/${latest}/SHA256SUMS" tmpdir="$(mktemp -d)" tmp_bin="" trap 'rm -rf "${tmpdir:-}"; [ -n "${tmp_bin:-}" ] && rm -f "${tmp_bin:-}" 2>/dev/null || true' EXIT echo "Downloading binary..." if ! curl -fsSL "$asset_url" -o "$tmpdir/lean-ctx.tar.gz"; then echo "Error: download failed. Check: https://github.com/${REPO}/releases" exit 1 fi echo "Downloading checksums..." if curl -fsSL "$sums_url" -o "$tmpdir/SHA256SUMS" 2>/dev/null; then expected="$(grep "lean-ctx-${target}.tar.gz" "$tmpdir/SHA256SUMS" | cut -d' ' -f1)" if [ -n "$expected" ]; then verify_checksum "$tmpdir/lean-ctx.tar.gz" "$expected" fi else echo " Warning: checksums not available, skipping verification" fi tar -xzf "$tmpdir/lean-ctx.tar.gz" -C "$tmpdir" mkdir -p "$INSTALL_DIR" tmp_bin="$INSTALL_DIR/.lean-ctx.new.$$" install -m755 "$tmpdir/lean-ctx" "$tmp_bin" if [ "$(uname -s)" = "Darwin" ]; then xattr -cr "$tmp_bin" 2>/dev/null || true codesign --force --sign - "$tmp_bin" 2>/dev/null || true fi stop_running_instance mv -f "$tmp_bin" "$INSTALL_DIR/lean-ctx" tmp_bin="" echo " Installed: $INSTALL_DIR/lean-ctx" finish } install_from_source() { if ! command -v cargo >/dev/null 2>&1; then echo "Error: cargo not found. Install Rust: https://rustup.rs" echo "Or download a pre-built binary: $0 --download" exit 1 fi build_only="${1:-}" echo "Mode: build from source" echo "" echo "Building lean-ctx (release)..." if [ -d "$RUST_DIR" ]; then (cd "$RUST_DIR" && cargo build --release) # Honour CARGO_TARGET_DIR / a config.toml [build] target-dir override # (GH #671): with a hardcoded ./target, a stale binary from an earlier # default-layout build gets linked instead of the one just built. # `|| true` keeps a failing `cargo metadata` on the fallback path. target_dir=$( (cd "$RUST_DIR" && cargo metadata --no-deps --format-version=1 2>/dev/null) \ | grep -o '"target_directory":"[^"]*"' \ | head -1 \ | sed -E 's/^"target_directory":"(.*)"$/\1/' \ | sed 's/\\\\/\//g' || true) binary="${target_dir:-$RUST_DIR/target}/release/lean-ctx" else cargo install lean-ctx echo "" echo "Installed via cargo install." return fi if [ ! -x "$binary" ]; then echo "Error: build failed — binary not found at $binary" echo "Hint: is CARGO_TARGET_DIR or ~/.cargo/config.toml [build] target-dir pointing elsewhere?" exit 1 fi echo "Built: $binary" if [ "$build_only" = "--build-only" ]; then echo "Done (build only)." return fi mkdir -p "$INSTALL_DIR" tmp_link="$INSTALL_DIR/.lean-ctx.link.$$" ln -sf "$binary" "$tmp_link" stop_running_instance mv -f "$tmp_link" "$INSTALL_DIR/lean-ctx" echo " Linked: $INSTALL_DIR/lean-ctx -> $binary" finish } uninstall() { echo "Mode: uninstall" echo "" # The binary's own `uninstall` does the thorough cleanup — stops every process, then # removes hooks, MCP configs, rules, autostart (LaunchAgent/systemd), data, and the # binary itself. Prefer it; forward any extra flags (e.g. --keep-config, --dry-run). if command -v lean-ctx >/dev/null 2>&1; then lean-ctx uninstall "$@" || true else echo "lean-ctx not on PATH — removing known artifacts directly." if [ "$(uname -s)" = "Darwin" ]; then for label in com.leanctx.proxy com.leanctx.daemon; do plist="$HOME/Library/LaunchAgents/$label.plist" [ -f "$plist" ] && launchctl unload "$plist" 2>/dev/null || true rm -f "$plist" 2>/dev/null || true done else for svc in lean-ctx-proxy lean-ctx-daemon; do systemctl --user disable --now "$svc" 2>/dev/null || true rm -f "$HOME/.config/systemd/user/$svc.service" 2>/dev/null || true done systemctl --user daemon-reload 2>/dev/null || true fi rm -rf "$HOME/.lean-ctx" "$HOME/.config/lean-ctx" 2>/dev/null || true echo " Removed autostart + data dir." echo " (Reinstall the binary and run 'lean-ctx uninstall' for full editor-config cleanup.)" fi # Belt-and-suspenders: ensure the binary + PATH symlinks install.sh created are gone, # even if the self-delete failed or the binary was never on PATH. for b in "$INSTALL_DIR/lean-ctx" "/usr/local/bin/lean-ctx"; do if [ -e "$b" ] || [ -L "$b" ]; then rm -f "$b" 2>/dev/null && echo " Removed $b" || true fi done echo "" echo "lean-ctx uninstalled. Restart your shell to drop stale aliases." echo "Verify with: command -v lean-ctx # should print nothing" } case "${1:-}" in --download) install_download ;; --cuda|--gpu) INSTALL_FLAVOR="cuda"; install_download ;; --build-only) install_from_source --build-only ;; --uninstall) shift; uninstall "$@" ;; --help|-h) echo "Usage: $0 [--download|--cuda|--build-only|--uninstall|--help]" echo "" echo " (no args) Download pre-built binary (builds from source if run inside the lean-ctx repo)" echo " --download Download pre-built binary (no Rust needed)" echo " --cuda Download Linux x86_64 CUDA-enabled binary" echo " --build-only Build only, don't install" echo " --uninstall Fully remove lean-ctx (processes, configs, autostart, data, binary)" echo "" echo "Uninstall one-liner:" echo " curl -fsSL https://leanctx.com/install.sh | sh -s -- --uninstall" echo "" echo "Environment:" echo " LEAN_CTX_INSTALL_DIR Custom install directory (default: ~/.local/bin)" echo " LEAN_CTX_INSTALL_FLAVOR Binary flavor: cpu or cuda (default: cpu)" ;; *) if [ -d "$RUST_DIR" ]; then install_from_source else install_download fi ;; esac