Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions cursor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -666,6 +668,10 @@ EOF
function update_cursor() {
log_step "Updating Cursor..."
refresh_shim_assets
refresh_shell_path_assets

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate shell-path refresh calls on lib API availability

update_cursor now calls refresh_shell_path_assets unconditionally, but installed environments can still source an older ~/.local/share/cursor-installer/lib.sh that predates this API. In that mixed-version state, cursor-installer --update exits with command not found before any update logic runs, so users are left with a broken updater until lib assets are manually repaired. Add a compatibility guard (or force-refresh lib.sh) before invoking new helper functions.

Useful? React with 👍 / 👎.

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
Expand Down
9 changes: 7 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,20 @@ 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
log_warn "$LOCAL_BIN is not in your PATH."
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bundle or refresh lib before invoking shell-path helpers

This new call is unconditional, but install.sh still prefers an existing ~/.local/share/cursor-installer/lib.sh when run standalone (the common curl ... | bash path). On systems upgrading from pre-change installs, that older lib does not define the new shell-path functions, so the installer exits with command not found here after already replacing ~/.local/bin/cursor-installer, leaving users in a partially upgraded/broken state. Ensure the script refreshes/copies the latest lib.sh before using new helper APIs, or defensively gate the call when the function is missing.

Useful? React with 👍 / 👎.


# Run cursor --update to download and install Cursor
log_step "Downloading and installing Cursor ($INSTALL_MODE mode) from ${REPO_OWNER}/${REPO_NAME}@${REPO_BRANCH}..."
Expand Down
65 changes: 63 additions & 2 deletions lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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..."
Expand All @@ -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
}
166 changes: 166 additions & 0 deletions scripts/ensure-shell-path.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
$START_MARKER
if [ -f "$SHELL_PATH_SCRIPT" ]; then
. "$SHELL_PATH_SCRIPT"
fi
$END_MARKER
EOF
}

print_target_files() {
if [ -n "${TARGET_SHELL_FILES:-}" ]; then
old_IFS="$IFS"
IFS=:
for file in $TARGET_SHELL_FILES; do
[ -n "$file" ] && printf '%s\n' "$file"
done
IFS="$old_IFS"
return 0
fi

if [ -n "${TARGET_SHELL_RC:-}" ]; then
printf '%s\n' "$TARGET_SHELL_RC"
return 0
fi

shell_name=$(basename "${SHELL:-}")
case "$shell_name" in
bash)
printf '%s\n' "$HOME/.bashrc"
Comment on lines +37 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove PATH hook from all managed shell rc files

print_target_files derives exactly one target rc file from the current $SHELL, and uninstall reuses this default selection. If the PATH hook was added from a different shell earlier (for example, install from bash but uninstall from zsh), cleanup skips the original file and leaves the managed block behind, so uninstall does not fully remove the feature it installed.

Useful? React with 👍 / 👎.

;;
zsh)
printf '%s\n' "$HOME/.zshrc"
;;
sh|dash|ksh)
printf '%s\n' "$HOME/.profile"
;;
*)
echo "Skipping shell PATH setup; unsupported shell: ${SHELL:-unknown}" >&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
Loading
Loading