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..8ffdac9 100755 --- a/cursor.sh +++ b/cursor.sh @@ -290,6 +290,7 @@ EOF function install_cursor_extracted() { run_ensure_shim + run_ensure_shell_path local install_dir="$1" local release_track=${2:-stable} local temp_file @@ -440,6 +441,7 @@ function install_cursor_extracted() { function install_cursor() { run_ensure_shim + run_ensure_shell_path local install_dir="$1" local release_track=${2:-stable} # Default to stable if not specified @@ -666,6 +668,10 @@ EOF function update_cursor() { log_step "Updating Cursor..." refresh_shim_assets + refresh_shell_path_assets + run_ensure_shim + run_ensure_shell_path + 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..3513cd1 100755 --- a/install.sh +++ b/install.sh @@ -94,8 +94,12 @@ 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." +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 +107,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..55abee4 100644 --- a/lib.sh +++ b/lib.sh @@ -114,8 +114,12 @@ BASE_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REP 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" @@ -137,6 +141,22 @@ function sync_shim_assets() { 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 + curl -fsSL "$SHELL_PATH_SCRIPT_URL" -o "$SHELL_PATH_SCRIPT" || { log_warn "Failed to download 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 + curl -fsSL "$SHELL_PATH_HELPER_URL" -o "$SHELL_PATH_HELPER" || { log_warn "Failed to download 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..." @@ -152,11 +172,52 @@ function refresh_shim_assets() { 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 ! curl -fsSL "$SHELL_PATH_SCRIPT_URL" -o "$SHELL_PATH_SCRIPT"; then + log_warn "Failed to download shell-path.sh; continuing." + return 0 + fi + if ! curl -fsSL "$SHELL_PATH_HELPER_URL" -o "$SHELL_PATH_HELPER"; then + log_warn "Failed to download ensure-shell-path.sh; continuing." + 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 + 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 + 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..9213a73 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -48,7 +48,15 @@ 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..." +run_remove_shell_path + +# 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 "$SHARED_LIB" "cursor-installer lib" if [ -d "$LIB_DIR" ] && [ -z "$(ls -A "$LIB_DIR")" ]; then rmdir "$LIB_DIR" 2>/dev/null || true