diff --git a/README.md b/README.md index 367d9fe..bc40b7e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ The one‑liner script will: 2. Download `lib.sh` to `~/.local/share/cursor-installer/lib.sh` 3. Make the script executable 4. Install a `cursor` shim at `~/.local/bin/cursor` (see [The `cursor` Shim](#the-cursor-shim)) -5. Download and install the latest version of Cursor +5. Install a managed shell startup hook for supported shells so `~/.local/bin` stays ahead of transient AppImage runtime paths +6. Download and install the latest version of Cursor **Note:** If you're installing via the piped bash method and don't have FUSE2 installed, the script will warn you but continue. You'll need to either: @@ -125,7 +126,8 @@ The uninstall script will: 1. Remove the `cursor-installer` script from `~/.local/bin/` 2. Remove the shared `lib.sh` from `~/.local/share/cursor-installer/` 3. Remove the Cursor AppImage -4. Ask if you want to remove the Cursor configuration files +4. Remove the managed shell PATH hook from supported shell startup files +5. Ask if you want to remove the Cursor configuration files **Note:** The `cursor` shim at `~/.local/bin/cursor` is not removed by the uninstall script. See [Removing the Shim](#removing-the-shim) for manual cleanup. @@ -211,12 +213,19 @@ The shim bridges that gap. It installs a lightweight script at `~/.local/bin/cur When you type `cursor`, the shim (`~/.local/bin/cursor`) follows a short resolution chain: -1. **Real Cursor binary found in PATH?** -- Forward all arguments to it (e.g. Cursor's official `cursor` CLI). -2. **`cursor agent` subcommand?** -- Delegate to `~/.local/bin/agent` if it exists. -3. **`cursor-installer` found?** -- Delegate to the installer CLI so commands like `cursor --update` still work. -4. **Nothing found** -- Print a helpful error with install instructions. +1. **`cursor agent` subcommand?** -- Delegate to `~/.local/bin/agent` if it exists. +2. **Installer-only flag?** -- Delegate to `cursor-installer` for commands like `cursor --update`, `cursor --check`, or `cursor --extract`. +3. **Stable Cursor binary found in PATH?** -- Forward all other arguments to it (e.g. Cursor's official `cursor` CLI). +4. **`cursor-installer` found?** -- Delegate to the installer CLI as a general fallback. +5. **Nothing found** -- Print a helpful error with install instructions. -The shim never hides a real Cursor binary; it only acts as a fallback. +The shim does not override a stable Cursor CLI, but it deliberately ignores transient AppImage mount paths under `/tmp/.mount_*` and normalizes duplicate path aliases so it cannot recurse back into itself. + +### AppImage Terminals + +When Cursor is launched from an AppImage, terminals opened inside Cursor may inherit a `PATH` where Cursor's transient runtime mount (`/tmp/.mount_*`) appears before `~/.local/bin`. That can bypass the shim entirely. + +To keep `cursor` resolving to the shim in supported shells, the installer manages a small startup hook that prepends `~/.local/bin` in interactive `bash` and `zsh` sessions. This keeps the shim available while still allowing it to delegate to a real Cursor CLI when appropriate. ### How It Works @@ -241,6 +250,7 @@ The shim is synced automatically during normal installer operations: - **`install.sh`** -- Copies `shim.sh` and `ensure-shim.sh` into `~/.local/share/cursor-installer/`, then runs `ensure-shim.sh`. - **`cursor-installer --update`** -- Re-downloads the latest shim assets from GitHub, then re-runs `ensure-shim.sh`. - **`cursor-installer` (install paths)** -- Runs `ensure-shim.sh` before each install to keep the shim current. +- **Shell PATH setup** -- Syncs `shell-path.sh` and `ensure-shell-path.sh`, then ensures supported shell startup files source the PATH helper. ### File Locations @@ -249,6 +259,8 @@ The shim is synced automatically during normal installer operations: | `~/.local/bin/cursor` | The shim (what you invoke). | | `~/.local/share/cursor-installer/shim.sh` | Cached copy of the shim source. | | `~/.local/share/cursor-installer/ensure-shim.sh` | Cached copy of the installer helper. | +| `~/.local/share/cursor-installer/shell-path.sh` | Shell snippet that prepends `~/.local/bin`. | +| `~/.local/share/cursor-installer/ensure-shell-path.sh` | Helper that updates supported shell startup files. | ### Removing the Shim @@ -264,7 +276,7 @@ If you only want to disable the shim without uninstalling the rest of the projec ## Note -If you encounter a warning that `~/.local/bin` is not in your PATH, you can add it by running: +If you encounter a warning that `~/.local/bin` is not in your PATH, or if `cursor` resolves to Cursor's transient AppImage runtime instead of the shim, prepend it by running: ```bash export PATH="$HOME/.local/bin:$PATH" diff --git a/cursor.sh b/cursor.sh index 3c7c12e..7b5e6dd 100755 --- a/cursor.sh +++ b/cursor.sh @@ -6,11 +6,32 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) LIB_DIR="$HOME/.local/share/cursor-installer" LIB_PATH="$SCRIPT_DIR/lib.sh" SHARED_LIB="$LIB_DIR/lib.sh" +INSTALLER_SOURCE_STATE="$LIB_DIR/source.env" +LOCAL_LIB_PATH="" +BOOTSTRAP_REPO_OWNER="${REPO_OWNER:-watzon}" +BOOTSTRAP_REPO_BRANCH="${REPO_BRANCH:-main}" +BOOTSTRAP_REPO_NAME="${REPO_NAME:-cursor-linux-installer}" -# Source shared helpers (local repo or installed lib) +if [ -f "$INSTALLER_SOURCE_STATE" ]; then + # shellcheck disable=SC1090 + source "$INSTALLER_SOURCE_STATE" + if [ -n "${INSTALLER_SOURCE_ROOT:-}" ] && [ -f "$INSTALLER_SOURCE_ROOT/lib.sh" ]; then + LOCAL_LIB_PATH="$INSTALLER_SOURCE_ROOT/lib.sh" + fi + BOOTSTRAP_REPO_OWNER="${INSTALLER_REPO_OWNER:-$BOOTSTRAP_REPO_OWNER}" + BOOTSTRAP_REPO_BRANCH="${INSTALLER_REPO_BRANCH:-$BOOTSTRAP_REPO_BRANCH}" + BOOTSTRAP_REPO_NAME="${INSTALLER_REPO_NAME:-$BOOTSTRAP_REPO_NAME}" +fi + +BOOTSTRAP_LIB_URL="https://raw.githubusercontent.com/${BOOTSTRAP_REPO_OWNER}/${BOOTSTRAP_REPO_NAME}/${BOOTSTRAP_REPO_BRANCH}/lib.sh" + +# Source shared helpers (local repo, persisted local source, or installed lib) if [ -f "$LIB_PATH" ]; then # shellcheck disable=SC1090 source "$LIB_PATH" +elif [ -n "$LOCAL_LIB_PATH" ]; then + # shellcheck disable=SC1090 + source "$LOCAL_LIB_PATH" elif [ -f "$SHARED_LIB" ]; then # shellcheck disable=SC1090 source "$SHARED_LIB" @@ -19,6 +40,49 @@ else exit 1 fi +function ensure_shell_path_lib_apis() { + if declare -F refresh_shell_path_assets >/dev/null 2>&1 && + declare -F run_ensure_shell_path >/dev/null 2>&1 && + declare -F run_remove_shell_path >/dev/null 2>&1 && + declare -F warn_if_cursor_shadowed_by_appimage_runtime >/dev/null 2>&1; then + return 0 + fi + + if [ -n "$LOCAL_LIB_PATH" ] && [ -f "$LOCAL_LIB_PATH" ]; then + # shellcheck disable=SC1090 + source "$LOCAL_LIB_PATH" + else + mkdir -p "$LIB_DIR" + if curl -fsSL "$BOOTSTRAP_LIB_URL" -o "$SHARED_LIB"; then + # shellcheck disable=SC1090 + source "$SHARED_LIB" + else + log_info "Latest shell PATH helper API unavailable; continuing in compatibility mode." + return 1 + fi + fi + + if declare -F refresh_shell_path_assets >/dev/null 2>&1 && + declare -F run_ensure_shell_path >/dev/null 2>&1 && + declare -F run_remove_shell_path >/dev/null 2>&1 && + declare -F warn_if_cursor_shadowed_by_appimage_runtime >/dev/null 2>&1; then + return 0 + fi + + log_info "Latest shell PATH helper API unavailable; continuing in compatibility mode." + return 1 +} + +function run_optional_helper() { + local helper="$1" + + if declare -F "$helper" >/dev/null 2>&1; then + "$helper" + fi +} + +ensure_shell_path_lib_apis || true + CLI_NAME="cursor-installer" CLI_BIN="$HOME/.local/bin/$CLI_NAME" @@ -289,7 +353,8 @@ EOF } function install_cursor_extracted() { - run_ensure_shim + run_optional_helper run_ensure_shim + run_optional_helper run_ensure_shell_path local install_dir="$1" local release_track=${2:-stable} local temp_file @@ -439,7 +504,8 @@ function install_cursor_extracted() { } function install_cursor() { - run_ensure_shim + run_optional_helper run_ensure_shim + run_optional_helper run_ensure_shell_path local install_dir="$1" local release_track=${2:-stable} # Default to stable if not specified @@ -665,7 +731,11 @@ EOF function update_cursor() { log_step "Updating Cursor..." - refresh_shim_assets + run_optional_helper refresh_shim_assets + run_optional_helper refresh_shell_path_assets + run_optional_helper run_ensure_shim + run_optional_helper run_ensure_shell_path + run_optional_helper warn_if_cursor_shadowed_by_appimage_runtime local current_appimage current_appimage=$(find_cursor_appimage || true) local install_dir diff --git a/install.sh b/install.sh index 707b5d0..e31f2b6 100755 --- a/install.sh +++ b/install.sh @@ -42,15 +42,14 @@ BASE_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REP LIB_URL="${BASE_RAW_URL}/lib.sh" CURSOR_SCRIPT_URL="${BASE_RAW_URL}/cursor.sh" -# Source shared helpers (local repo, installed lib, or download) +# Source shared helpers (local repo or freshly downloaded lib). +# Standalone installs must not rely on a potentially stale shared lib, because +# this script calls newer helper APIs later in the bootstrap flow. if [ -f "$LIB_PATH" ]; then # shellcheck disable=SC1090 source "$LIB_PATH" mkdir -p "$LIB_DIR" cp "$LIB_PATH" "$SHARED_LIB" -elif [ -f "$SHARED_LIB" ]; then - # shellcheck disable=SC1090 - source "$SHARED_LIB" else mkdir -p "$LIB_DIR" curl -fsSL "$LIB_URL" -o "$SHARED_LIB" || { @@ -94,8 +93,24 @@ chmod +x "$CLI_PATH" log_ok "Cursor installer script has been placed in $CLI_PATH" -log_step "Ensuring cursor shim..." -LOCAL_SHIM_PATH="$SCRIPT_DIR/shim.sh" LOCAL_SHIM_HELPER_PATH="$SCRIPT_DIR/scripts/ensure-shim.sh" sync_shim_assets && run_ensure_shim || log_warn "Shim update skipped or failed; continuing." +INSTALLER_SOURCE_ROOT="" +if [ -f "$LOCAL_CURSOR_SH" ] && [ -f "$LIB_PATH" ]; then + INSTALLER_SOURCE_ROOT="$SCRIPT_DIR" + if command -v git >/dev/null 2>&1; then + DETECTED_REPO_BRANCH=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || true) + if [ -n "$DETECTED_REPO_BRANCH" ]; then + REPO_BRANCH="$DETECTED_REPO_BRANCH" + fi + fi +fi +persist_installer_source_state "$INSTALLER_SOURCE_ROOT" + +log_step "Ensuring cursor shim and shell PATH setup..." +LOCAL_SHIM_PATH="$SCRIPT_DIR/shim.sh" +LOCAL_SHIM_HELPER_PATH="$SCRIPT_DIR/scripts/ensure-shim.sh" +LOCAL_SHELL_PATH_SCRIPT="$SCRIPT_DIR/shell-path.sh" +LOCAL_SHELL_PATH_HELPER_PATH="$SCRIPT_DIR/scripts/ensure-shell-path.sh" +sync_shim_assets && sync_shell_path_assets && run_ensure_shim && run_ensure_shell_path || log_warn "Shim or shell PATH setup skipped or failed; continuing." # Check if ~/.local/bin is in PATH if [[ ":$PATH:" != *":$LOCAL_BIN:"* ]]; then @@ -103,6 +118,7 @@ if [[ ":$PATH:" != *":$LOCAL_BIN:"* ]]; then log_info "To add it, run this or add it to your shell profile:" log_info "export PATH=\"\$HOME/.local/bin:\$PATH\"" fi +warn_if_cursor_shadowed_by_appimage_runtime # Run cursor --update to download and install Cursor log_step "Downloading and installing Cursor ($INSTALL_MODE mode) from ${REPO_OWNER}/${REPO_NAME}@${REPO_BRANCH}..." diff --git a/lib.sh b/lib.sh index 81aed43..454dacb 100644 --- a/lib.sh +++ b/lib.sh @@ -105,19 +105,132 @@ function find_cursor_appimage() { return 1 } +# --- Installer source metadata --- +INSTALLER_SOURCE_STATE="${LIB_DIR}/source.env" + +function load_installer_source_state() { + if [ ! -f "$INSTALLER_SOURCE_STATE" ]; then + return 0 + fi + + # shellcheck disable=SC1090 + source "$INSTALLER_SOURCE_STATE" +} + +function persist_installer_source_state() { + mkdir -p "$LIB_DIR" + + local source_root="${1:-${INSTALLER_SOURCE_ROOT:-}}" + local tmp_file + tmp_file=$(mktemp) + + { + printf 'INSTALLER_REPO_OWNER=%q\n' "${REPO_OWNER:-watzon}" + printf 'INSTALLER_REPO_BRANCH=%q\n' "${REPO_BRANCH:-main}" + printf 'INSTALLER_REPO_NAME=%q\n' "${REPO_NAME:-cursor-linux-installer}" + + if [ -n "$source_root" ] && [ -d "$source_root" ]; then + printf 'INSTALLER_SOURCE_ROOT=%q\n' "$source_root" + fi + } > "$tmp_file" + + mv "$tmp_file" "$INSTALLER_SOURCE_STATE" +} + +function apply_local_installer_source_overrides() { + local source_root="${INSTALLER_SOURCE_ROOT:-}" + + if [ -z "$source_root" ] || [ ! -d "$source_root" ]; then + return 0 + fi + + if [ -z "${LOCAL_CURSOR_SH:-}" ] && [ -f "$source_root/cursor.sh" ]; then + LOCAL_CURSOR_SH="$source_root/cursor.sh" + fi + if [ -z "${LOCAL_SHIM_PATH:-}" ] && [ -f "$source_root/shim.sh" ]; then + LOCAL_SHIM_PATH="$source_root/shim.sh" + fi + if [ -z "${LOCAL_SHIM_HELPER_PATH:-}" ] && [ -f "$source_root/scripts/ensure-shim.sh" ]; then + LOCAL_SHIM_HELPER_PATH="$source_root/scripts/ensure-shim.sh" + fi + if [ -z "${LOCAL_SHELL_PATH_SCRIPT:-}" ] && [ -f "$source_root/shell-path.sh" ]; then + LOCAL_SHELL_PATH_SCRIPT="$source_root/shell-path.sh" + fi + if [ -z "${LOCAL_SHELL_PATH_HELPER_PATH:-}" ] && [ -f "$source_root/scripts/ensure-shell-path.sh" ]; then + LOCAL_SHELL_PATH_HELPER_PATH="$source_root/scripts/ensure-shell-path.sh" + fi +} + +function get_managed_shell_files() { + local candidate + local files=() + + for candidate in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do + if [ -e "$candidate" ] || [ -L "$candidate" ]; then + files+=("$candidate") + fi + done + + if [ ${#files[@]} -eq 0 ]; then + case "$(basename "${SHELL:-}")" in + bash) + files+=("$HOME/.bashrc") + ;; + zsh) + files+=("$HOME/.zshrc") + ;; + sh|dash|ksh) + files+=("$HOME/.profile") + ;; + esac + fi + + if [ ${#files[@]} -eq 0 ]; then + return 1 + fi + + local old_ifs="$IFS" + IFS=: + printf '%s' "${files[*]}" + IFS="$old_ifs" +} + +function download_asset_atomically() { + local url="$1" + local destination="$2" + local description="$3" + local tmp_file + + tmp_file=$(mktemp) + if curl -fsSL "$url" -o "$tmp_file"; then + mv "$tmp_file" "$destination" + return 0 + fi + + rm -f "$tmp_file" + log_warn "Failed to download $description" + return 1 +} + # --- Shim (cursor in PATH): canonical paths and helpers --- # Requires LIB_DIR to be set by caller before sourcing lib. -REPO_OWNER="${REPO_OWNER:-watzon}" -REPO_BRANCH="${REPO_BRANCH:-main}" -REPO_NAME="${REPO_NAME:-cursor-linux-installer}" +load_installer_source_state +REPO_OWNER="${REPO_OWNER:-${INSTALLER_REPO_OWNER:-watzon}}" +REPO_BRANCH="${REPO_BRANCH:-${INSTALLER_REPO_BRANCH:-main}}" +REPO_NAME="${REPO_NAME:-${INSTALLER_REPO_NAME:-cursor-linux-installer}}" BASE_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}" SHIM_TARGET="${SHIM_TARGET:-$HOME/.local/bin/cursor}" SHARED_SHIM="${LIB_DIR}/shim.sh" SHIM_HELPER="${LIB_DIR}/ensure-shim.sh" +SHELL_PATH_SCRIPT="${LIB_DIR}/shell-path.sh" +SHELL_PATH_HELPER="${LIB_DIR}/ensure-shell-path.sh" SHIM_URL="${BASE_RAW_URL}/shim.sh" SHIM_HELPER_URL="${BASE_RAW_URL}/scripts/ensure-shim.sh" +SHELL_PATH_SCRIPT_URL="${BASE_RAW_URL}/shell-path.sh" +SHELL_PATH_HELPER_URL="${BASE_RAW_URL}/scripts/ensure-shell-path.sh" LIB_URL="${BASE_RAW_URL}/lib.sh" CURSOR_SCRIPT_URL="${BASE_RAW_URL}/cursor.sh" +apply_local_installer_source_overrides # Sync shim.sh and ensure-shim.sh into LIB_DIR (local copy or download). # Set LOCAL_SHIM_PATH and/or LOCAL_SHIM_HELPER_PATH to prefer repo files. @@ -126,37 +239,109 @@ function sync_shim_assets() { if [ -n "${LOCAL_SHIM_PATH:-}" ] && [ -f "$LOCAL_SHIM_PATH" ]; then cp "$LOCAL_SHIM_PATH" "$SHARED_SHIM" elif [ ! -f "$SHARED_SHIM" ]; then - curl -fsSL "$SHIM_URL" -o "$SHARED_SHIM" || { log_warn "Failed to download shim.sh"; return 1; } + download_asset_atomically "$SHIM_URL" "$SHARED_SHIM" "shim.sh" || return 1 fi if [ -n "${LOCAL_SHIM_HELPER_PATH:-}" ] && [ -f "$LOCAL_SHIM_HELPER_PATH" ]; then cp "$LOCAL_SHIM_HELPER_PATH" "$SHIM_HELPER" elif [ ! -f "$SHIM_HELPER" ]; then - curl -fsSL "$SHIM_HELPER_URL" -o "$SHIM_HELPER" || { log_warn "Failed to download ensure-shim.sh"; return 1; } + download_asset_atomically "$SHIM_HELPER_URL" "$SHIM_HELPER" "ensure-shim.sh" || return 1 fi chmod +x "$SHIM_HELPER" "$SHARED_SHIM" 2>/dev/null || true return 0 } +function sync_shell_path_assets() { + mkdir -p "$LIB_DIR" + if [ -n "${LOCAL_SHELL_PATH_SCRIPT:-}" ] && [ -f "$LOCAL_SHELL_PATH_SCRIPT" ]; then + cp "$LOCAL_SHELL_PATH_SCRIPT" "$SHELL_PATH_SCRIPT" + elif [ ! -f "$SHELL_PATH_SCRIPT" ]; then + download_asset_atomically "$SHELL_PATH_SCRIPT_URL" "$SHELL_PATH_SCRIPT" "shell-path.sh" || return 1 + fi + if [ -n "${LOCAL_SHELL_PATH_HELPER_PATH:-}" ] && [ -f "$LOCAL_SHELL_PATH_HELPER_PATH" ]; then + cp "$LOCAL_SHELL_PATH_HELPER_PATH" "$SHELL_PATH_HELPER" + elif [ ! -f "$SHELL_PATH_HELPER" ]; then + download_asset_atomically "$SHELL_PATH_HELPER_URL" "$SHELL_PATH_HELPER" "ensure-shell-path.sh" || return 1 + fi + chmod +x "$SHELL_PATH_HELPER" "$SHELL_PATH_SCRIPT" 2>/dev/null || true + return 0 +} + # Refresh shim assets from GitHub (used on cursor-installer --update). function refresh_shim_assets() { log_step "Refreshing cursor shim assets..." mkdir -p "$LIB_DIR" - if ! curl -fsSL "$SHIM_URL" -o "$SHARED_SHIM"; then - log_warn "Failed to download shim.sh; continuing." + if [ -n "${LOCAL_SHIM_PATH:-}" ] && [ -f "$LOCAL_SHIM_PATH" ]; then + cp "$LOCAL_SHIM_PATH" "$SHARED_SHIM" + elif ! download_asset_atomically "$SHIM_URL" "$SHARED_SHIM" "shim.sh"; then return 0 fi - if ! curl -fsSL "$SHIM_HELPER_URL" -o "$SHIM_HELPER"; then - log_warn "Failed to download ensure-shim.sh; continuing." + if [ -n "${LOCAL_SHIM_HELPER_PATH:-}" ] && [ -f "$LOCAL_SHIM_HELPER_PATH" ]; then + cp "$LOCAL_SHIM_HELPER_PATH" "$SHIM_HELPER" + elif ! download_asset_atomically "$SHIM_HELPER_URL" "$SHIM_HELPER" "ensure-shim.sh"; then return 0 fi chmod +x "$SHIM_HELPER" "$SHARED_SHIM" 2>/dev/null || true } +function refresh_shell_path_assets() { + log_step "Refreshing shell PATH assets..." + mkdir -p "$LIB_DIR" + if [ -n "${LOCAL_SHELL_PATH_SCRIPT:-}" ] && [ -f "$LOCAL_SHELL_PATH_SCRIPT" ]; then + cp "$LOCAL_SHELL_PATH_SCRIPT" "$SHELL_PATH_SCRIPT" + elif ! download_asset_atomically "$SHELL_PATH_SCRIPT_URL" "$SHELL_PATH_SCRIPT" "shell-path.sh"; then + return 0 + fi + if [ -n "${LOCAL_SHELL_PATH_HELPER_PATH:-}" ] && [ -f "$LOCAL_SHELL_PATH_HELPER_PATH" ]; then + cp "$LOCAL_SHELL_PATH_HELPER_PATH" "$SHELL_PATH_HELPER" + elif ! download_asset_atomically "$SHELL_PATH_HELPER_URL" "$SHELL_PATH_HELPER" "ensure-shell-path.sh"; then + return 0 + fi + chmod +x "$SHELL_PATH_HELPER" "$SHELL_PATH_SCRIPT" 2>/dev/null || true +} + # Run ensure-shim.sh with canonical SOURCE_SHIM and TARGET_SHIM. function run_ensure_shim() { - if [ ! -x "$SHIM_HELPER" ] && [ ! -f "$SHIM_HELPER" ]; then + if [ ! -f "$SHIM_HELPER" ]; then log_info "Shim helper not found; skipping shim update." return 0 fi - SOURCE_SHIM="$SHARED_SHIM" TARGET_SHIM="$SHIM_TARGET" "$SHIM_HELPER" || { log_warn "Shim update failed; continuing."; return 0; } + SOURCE_SHIM="$SHARED_SHIM" TARGET_SHIM="$SHIM_TARGET" sh "$SHIM_HELPER" || { log_warn "Shim update failed; continuing."; return 0; } +} + +function run_ensure_shell_path() { + if [ ! -f "$SHELL_PATH_HELPER" ] || [ ! -f "$SHELL_PATH_SCRIPT" ]; then + log_info "Shell PATH helper not found; skipping shell PATH setup." + return 0 + fi + local target_shell_files + target_shell_files="${TARGET_SHELL_FILES:-${MANAGED_SHELL_FILES:-$(get_managed_shell_files || true)}}" + if [ -z "$target_shell_files" ]; then + log_info "No managed shell rc files detected; skipping shell PATH setup." + return 0 + fi + TARGET_SHELL_FILES="$target_shell_files" SHELL_PATH_SCRIPT="$SHELL_PATH_SCRIPT" sh "$SHELL_PATH_HELPER" || { log_warn "Shell PATH setup failed; continuing."; return 0; } +} + +function run_remove_shell_path() { + if [ ! -f "$SHELL_PATH_HELPER" ]; then + return 0 + fi + local target_shell_files + target_shell_files="${TARGET_SHELL_FILES:-${MANAGED_SHELL_FILES:-$(get_managed_shell_files || true)}}" + if [ -z "$target_shell_files" ]; then + return 0 + fi + TARGET_SHELL_FILES="$target_shell_files" SHELL_PATH_SCRIPT="$SHELL_PATH_SCRIPT" sh "$SHELL_PATH_HELPER" --remove || { log_warn "Shell PATH cleanup failed; continuing."; return 0; } +} + +function warn_if_cursor_shadowed_by_appimage_runtime() { + local resolved_cursor + resolved_cursor=$(command -v cursor 2>/dev/null || true) + + case "$resolved_cursor" in + /tmp/.mount_*) + log_warn "The current shell resolves 'cursor' to Cursor's AppImage runtime path." + log_info "Open a new terminal or source your shell startup file so ~/.local/bin takes precedence." + ;; + esac } diff --git a/scripts/ensure-shell-path.sh b/scripts/ensure-shell-path.sh new file mode 100644 index 0000000..aacafba --- /dev/null +++ b/scripts/ensure-shell-path.sh @@ -0,0 +1,166 @@ +#!/bin/sh +# Ensure supported interactive shells source cursor-installer's PATH helper. +set -eu + +ACTION="${1:-ensure}" +LIB_DIR="${HOME}/.local/share/cursor-installer" +SHELL_PATH_SCRIPT="${SHELL_PATH_SCRIPT:-$LIB_DIR/shell-path.sh}" +START_MARKER="# >>> cursor-installer path >>>" +END_MARKER="# <<< cursor-installer path <<<" + +build_source_block() { + cat <&2 + return 1 + ;; + esac +} + +strip_managed_block() { + file="$1" + tmp="$2" + + if [ -f "$file" ]; then + awk -v start="$START_MARKER" -v end="$END_MARKER" ' + $0 == start { skip = 1; next } + skip && $0 == end { skip = 0; next } + !skip { print } + ' "$file" > "$tmp" + else + : > "$tmp" + fi +} + +trim_trailing_blank_lines() { + trim_file="$1" + trimmed_file=$(mktemp) + + awk ' + { lines[NR] = $0 } + END { + last = NR + while (last > 0 && lines[last] ~ /^[[:space:]]*$/) { + last-- + } + for (i = 1; i <= last; i++) { + print lines[i] + } + } + ' "$trim_file" > "$trimmed_file" + + mv "$trimmed_file" "$trim_file" +} + +write_updated_file() { + source_tmp="$1" + destination_file="$2" + + if [ -e "$destination_file" ] || [ -L "$destination_file" ]; then + cat "$source_tmp" > "$destination_file" + rm -f "$source_tmp" + return 0 + fi + + mv "$source_tmp" "$destination_file" +} + +ensure_block() { + file="$1" + tmp=$(mktemp) + mkdir -p "$(dirname "$file")" + strip_managed_block "$file" "$tmp" + trim_trailing_blank_lines "$tmp" + + if [ -s "$tmp" ]; then + printf '\n' >> "$tmp" + fi + + build_source_block >> "$tmp" + + if [ -f "$file" ] && cmp -s "$tmp" "$file"; then + rm -f "$tmp" + echo "Shell PATH setup already present in $file" + return 0 + fi + + write_updated_file "$tmp" "$file" + echo "Ensured shell PATH setup in $file" +} + +remove_block() { + file="$1" + [ -f "$file" ] || return 0 + + tmp=$(mktemp) + strip_managed_block "$file" "$tmp" + + if cmp -s "$tmp" "$file"; then + rm -f "$tmp" + return 0 + fi + + write_updated_file "$tmp" "$file" + echo "Removed shell PATH setup from $file" +} + +if [ "$ACTION" = "ensure" ] && [ ! -f "$SHELL_PATH_SCRIPT" ]; then + echo "Error: shell-path.sh source not found at $SHELL_PATH_SCRIPT" >&2 + exit 1 +fi + +if ! target_files=$(print_target_files); then + exit 0 +fi + +printf '%s\n' "$target_files" | while IFS= read -r file; do + [ -n "$file" ] || continue + + case "$ACTION" in + ensure) + ensure_block "$file" + ;; + --remove|remove) + remove_block "$file" + ;; + *) + echo "Unknown action: $ACTION" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/ensure-shim.sh b/scripts/ensure-shim.sh index 9d805f7..fc1ebf6 100644 --- a/scripts/ensure-shim.sh +++ b/scripts/ensure-shim.sh @@ -5,6 +5,7 @@ set -eu TARGET_SHIM="${TARGET_SHIM:-$HOME/.local/bin/cursor}" SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) LIB_DIR="${HOME}/.local/share/cursor-installer" +SHIM_MARKER="cursor-linux-installer-shim" SOURCE_SHIM="${SOURCE_SHIM:-}" if [ -z "$SOURCE_SHIM" ]; then @@ -36,12 +37,17 @@ is_shim() { return 1 ;; esac - if grep -q "Find cursor executable in PATH" "$file" 2>/dev/null; then + + if grep -Fq "$SHIM_MARKER" "$file" 2>/dev/null; then return 0 fi - if grep -q "cursor-installer" "$file" 2>/dev/null; then + + if grep -Fq "Find cursor executable in PATH" "$file" 2>/dev/null && + grep -Fq 'AGENT_BIN="$HOME/.local/bin/agent"' "$file" 2>/dev/null && + grep -Fq 'Install/update with: cursor-installer --update [stable|latest]' "$file" 2>/dev/null; then return 0 fi + return 1 } diff --git a/shell-path.sh b/shell-path.sh new file mode 100644 index 0000000..e8ef79c --- /dev/null +++ b/shell-path.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# cursor-linux-installer-path + +cursor_installer_local_bin="$HOME/.local/bin" + +if [ -d "$cursor_installer_local_bin" ]; then + cursor_installer_filtered_path=$( + printf '%s' "${PATH:-}" | + awk -v RS=: -v ORS=: -v skip="$cursor_installer_local_bin" '$0 != skip { print }' | + sed 's/:$//' + ) + + if [ -n "$cursor_installer_filtered_path" ]; then + PATH="$cursor_installer_local_bin:$cursor_installer_filtered_path" + else + PATH="$cursor_installer_local_bin" + fi + + export PATH +fi + +unset cursor_installer_local_bin +unset cursor_installer_filtered_path diff --git a/shim.sh b/shim.sh index 001bf49..f9c39b8 100644 --- a/shim.sh +++ b/shim.sh @@ -1,41 +1,118 @@ #!/bin/sh set -eu +# cursor-linux-installer-shim # Find cursor executable in PATH, excluding the current shim +canonicalize_path() { + path="$1" + + if command -v realpath >/dev/null 2>&1; then + realpath "$path" 2>/dev/null && return 0 + fi + + if command -v readlink >/dev/null 2>&1; then + readlink -f "$path" 2>/dev/null && return 0 + fi + + case "$path" in + */*) + dir_part=${path%/*} + base_part=${path##*/} + ;; + *) + dir_part=. + base_part=$path + ;; + esac + + old_pwd=$(pwd) + if cd "$dir_part" 2>/dev/null; then + resolved_dir=$(pwd -P) + cd "$old_pwd" || exit 1 + printf '%s/%s\n' "$resolved_dir" "$base_part" + return 0 + fi + + cd "$old_pwd" || exit 1 + printf '%s\n' "$path" +} + +same_path() { + left=$(canonicalize_path "$1" || printf '%s\n' "$1") + right=$(canonicalize_path "$2" || printf '%s\n' "$2") + [ "$left" = "$right" ] +} + +is_ignored_cursor_path() { + case "$1" in + # Ignore transient AppImage runtime mounts; they are not stable CLI installs + # and can shadow the shim inside terminals launched from Cursor itself. + /tmp/.mount_*) + return 0 + ;; + esac + + return 1 +} + +SHIM_PATH=$(canonicalize_path "$HOME/.local/bin/cursor" || printf '%s\n' "$HOME/.local/bin/cursor") +case "${0:-}" in + */*) + SHIM_PATH=$(canonicalize_path "$0" || printf '%s\n' "$0") + ;; +esac + find_cursor() { old_IFS="$IFS" IFS=: for dir in $PATH; do [ -n "$dir" ] || continue cursor_path="$dir/cursor" - if [ "$cursor_path" != "$HOME/.local/bin/cursor" ] && [ -x "$cursor_path" ]; then - IFS="$old_IFS" - echo "$cursor_path" - return 0 + [ -x "$cursor_path" ] || continue + + if is_ignored_cursor_path "$cursor_path"; then + continue fi + + if same_path "$cursor_path" "$SHIM_PATH"; then + continue + fi + + IFS="$old_IFS" + echo "$cursor_path" + return 0 done IFS="$old_IFS" return 1 } -OTHER_CURSOR=$(find_cursor || true) +first_arg="${1:-}" CURSOR_INSTALLER=$(command -v cursor-installer 2>/dev/null || true) AGENT_BIN="$HOME/.local/bin/agent" -if [ -n "${OTHER_CURSOR:-}" ]; then - exec "$OTHER_CURSOR" "$@" -fi - -first_arg="${1:-}" - if [ "$first_arg" = "agent" ]; then if [ -x "$AGENT_BIN" ]; then + shift exec "$AGENT_BIN" "$@" fi echo "Error: Cursor agent not found at $AGENT_BIN" 1>&2 exit 1 fi +case "$first_arg" in + --update|-u|--check|-c|--extract|--no-fuse|--reinstall-desktop) + if [ -n "${CURSOR_INSTALLER:-}" ]; then + exec "$CURSOR_INSTALLER" "$@" + fi + ;; +esac + +OTHER_CURSOR=$(find_cursor || true) + +if [ -n "${OTHER_CURSOR:-}" ]; then + exec "$OTHER_CURSOR" "$@" +fi + if [ -n "${CURSOR_INSTALLER:-}" ]; then exec "$CURSOR_INSTALLER" "$@" fi diff --git a/uninstall.sh b/uninstall.sh index 859c21b..3153651 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -11,11 +11,24 @@ LIB_DIR="$HOME/.local/share/cursor-installer" LIB_PATH="$SCRIPT_DIR/lib.sh" SHARED_LIB="$LIB_DIR/lib.sh" LIB_URL="$BASE_RAW_URL/lib.sh" +INSTALLER_SOURCE_STATE="$LIB_DIR/source.env" +LOCAL_LIB_PATH="" -# Source shared helpers (local repo, installed lib, or download) +if [ -f "$INSTALLER_SOURCE_STATE" ]; then + # shellcheck disable=SC1090 + source "$INSTALLER_SOURCE_STATE" + if [ -n "${INSTALLER_SOURCE_ROOT:-}" ] && [ -f "$INSTALLER_SOURCE_ROOT/lib.sh" ]; then + LOCAL_LIB_PATH="$INSTALLER_SOURCE_ROOT/lib.sh" + fi +fi + +# Source shared helpers (local repo, persisted local source, installed lib, or download) if [ -f "$LIB_PATH" ]; then # shellcheck disable=SC1090 source "$LIB_PATH" +elif [ -n "$LOCAL_LIB_PATH" ]; then + # shellcheck disable=SC1090 + source "$LOCAL_LIB_PATH" elif [ -f "$SHARED_LIB" ]; then # shellcheck disable=SC1090 source "$SHARED_LIB" @@ -29,6 +42,12 @@ else source "$SHARED_LIB" fi +SHARED_SHIM="${SHARED_SHIM:-$LIB_DIR/shim.sh}" +SHIM_HELPER="${SHIM_HELPER:-$LIB_DIR/ensure-shim.sh}" +SHELL_PATH_SCRIPT="${SHELL_PATH_SCRIPT:-$LIB_DIR/shell-path.sh}" +SHELL_PATH_HELPER="${SHELL_PATH_HELPER:-$LIB_DIR/ensure-shell-path.sh}" +INSTALLER_SOURCE_STATE="${INSTALLER_SOURCE_STATE:-$LIB_DIR/source.env}" + CLI_NAME="cursor-installer" CLI_PATH="$HOME/.local/bin/$CLI_NAME" LEGACY_CLI="$HOME/.local/bin/cursor" @@ -36,7 +55,7 @@ LEGACY_CLI="$HOME/.local/bin/cursor" log_step "Uninstalling Cursor..." # Remove the Cursor AppImage -cursor_appimage=$(find_cursor_appimage) +cursor_appimage=$(find_cursor_appimage || true) if [ -n "$cursor_appimage" ]; then log_step "Removing Cursor AppImage..." safe_remove "$cursor_appimage" "Cursor AppImage" @@ -48,7 +67,20 @@ fi log_step "Removing cursor-installer script..." safe_remove "$CLI_PATH" "cursor-installer script" -# Remove shared lib (installed by installer) +# Remove managed shell PATH setup before deleting helper assets +log_step "Removing managed shell PATH setup..." +if declare -F run_remove_shell_path >/dev/null 2>&1; then + run_remove_shell_path +else + log_info "Shell PATH cleanup helper unavailable; skipping managed shell PATH setup removal." +fi + +# Remove shared support assets (installed by installer) +safe_remove "$SHARED_SHIM" "cursor shim source" +safe_remove "$SHIM_HELPER" "cursor shim helper" +safe_remove "$SHELL_PATH_SCRIPT" "shell PATH helper script" +safe_remove "$SHELL_PATH_HELPER" "shell PATH helper" +safe_remove "$INSTALLER_SOURCE_STATE" "installer source metadata" safe_remove "$SHARED_LIB" "cursor-installer lib" if [ -d "$LIB_DIR" ] && [ -z "$(ls -A "$LIB_DIR")" ]; then rmdir "$LIB_DIR" 2>/dev/null || true