Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ The one‑liner script will:
- Install FUSE2 manually: `sudo apt-get install libfuse2` (Debian/Ubuntu), `sudo dnf install fuse` (Fedora), or `sudo pacman -S fuse2` (Arch)
- Use extracted mode: `curl -fsSL https://raw.githubusercontent.com/watzon/cursor-linux-installer/main/install.sh | bash -s -- stable --extract`

If automated downloads are blocked by Cloudflare in your region, you can use one of these overrides:

- `CURSOR_DOWNLOAD_URL=<direct-appimage-url> cursor-installer --update stable`
- `CURSOR_APPIMAGE_PATH=/path/to/Cursor-<version>.AppImage cursor-installer --update stable`

### Method 2: Local clone

```bash
Expand Down Expand Up @@ -130,6 +135,8 @@ The uninstall script will:

Note: The installer CLI is `cursor-installer` to avoid conflicts with Cursor's official `cursor` CLI.

During install, the script now removes only legacy installer-managed `~/.local/bin/cursor` scripts and leaves non-installer `cursor` executables untouched.

After installation, you can use the `cursor-installer` command to launch Cursor or update it:

- To launch Cursor: `cursor-installer`
Expand Down
67 changes: 65 additions & 2 deletions cursor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,69 @@ function get_download_info() {
return 0
}

function downloaded_file_looks_like_appimage() {
local file_path="$1"
if [ ! -s "$file_path" ]; then
return 1
fi

local file_info
file_info=$(file -b "$file_path" 2>/dev/null || true)
if echo "$file_info" | grep -qiE 'HTML|XML|ASCII text|Unicode text'; then
return 1
fi

local first_bytes
first_bytes=$(head -c 256 "$file_path" 2>/dev/null || true)
if echo "$first_bytes" | grep -qiE '<!doctype html|<html|<head|<body'; then
return 1
fi

return 0
}

function download_cursor_appimage() {
local default_url="$1"
local output_file="$2"
local source_file="${CURSOR_APPIMAGE_PATH:-}"
local download_url="${CURSOR_DOWNLOAD_URL:-$default_url}"

if [ -n "$source_file" ]; then
if [ ! -f "$source_file" ]; then
log_error "CURSOR_APPIMAGE_PATH is set, but file was not found: $source_file"
return 1
fi

log_step "Using local AppImage from CURSOR_APPIMAGE_PATH"
cp "$source_file" "$output_file"
return 0
fi

local browser_ua='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'

log_step "Downloading Cursor AppImage..."
if curl -fL --retry 3 --retry-delay 2 --retry-all-errors "$download_url" -o "$output_file" && downloaded_file_looks_like_appimage "$output_file"; then
return 0
fi

log_warn "Retrying download with browser-like headers (Cloudflare fallback)..."
if curl -fL --retry 5 --retry-delay 2 --retry-all-errors --compressed \
-A "$browser_ua" \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Referer: https://cursor.com/downloads' \
-H 'Cache-Control: no-cache' \
"$download_url" -o "$output_file" && downloaded_file_looks_like_appimage "$output_file"; then
return 0
fi

rm -f "$output_file"
log_error "Failed to download a valid Cursor AppImage from: $download_url"
log_info "If Cloudflare blocks automated download in your region, download it in a browser and run:"
log_info " CURSOR_APPIMAGE_PATH=/path/to/Cursor-<version>.AppImage $CLI_NAME --update stable"
log_info "You can also override the URL with CURSOR_DOWNLOAD_URL=<direct-appimage-url>."
return 1
}

function clean_broken_cursor_symlinks() {
local relative_path="$1"
local target_root="$2"
Expand Down Expand Up @@ -312,7 +375,7 @@ function install_cursor_extracted() {
version=$(echo "$download_info" | grep "VERSION=" | sed 's/^VERSION=//')

log_step "Downloading $version Cursor AppImage for extraction..."
if ! curl -L "$download_url" -o "$temp_file"; then
if ! download_cursor_appimage "$download_url" "$temp_file"; then
log_error "Failed to download Cursor AppImage"
rm -f "$temp_file"
return 1
Expand Down Expand Up @@ -476,7 +539,7 @@ function install_cursor() {
version=$(echo "$download_info" | grep "VERSION=" | sed 's/^VERSION=//')

log_step "Downloading $version Cursor AppImage..."
if ! curl -L "$download_url" -o "$temp_file"; then
if ! download_cursor_appimage "$download_url" "$temp_file"; then
log_error "Failed to download Cursor AppImage"
rm -f "$temp_file"
return 1
Expand Down
20 changes: 16 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,26 @@ CLI_NAME="cursor-installer"
CLI_PATH="$LOCAL_BIN/$CLI_NAME"
LEGACY_CLI="$LOCAL_BIN/cursor"

function is_legacy_installer_cli() {
local file_path="$1"
if [ ! -f "$file_path" ]; then
return 1
fi

grep -qE 'install_cursor|check_cursor_versions|cursor\.appimage|Cursor Linux Installer' "$file_path"
}

# Create ~/.local/bin if it doesn't exist
mkdir -p "$LOCAL_BIN"

# Remove legacy installer CLI if present to avoid conflicts
if [ -f "$LEGACY_CLI" ] && grep -q "install_cursor_extracted" "$LEGACY_CLI"; then
log_warn "Removing legacy 'cursor' installer CLI to avoid conflicts."
safe_remove "$LEGACY_CLI" "legacy cursor installer CLI"
if [ -f "$LEGACY_CLI" ]; then
if is_legacy_installer_cli "$LEGACY_CLI"; then
log_warn "Removing legacy 'cursor' installer CLI to avoid conflicts."
safe_remove "$LEGACY_CLI" "legacy cursor installer CLI"
else
log_info "Existing '$LEGACY_CLI' does not appear installer-managed; leaving it untouched."
fi
fi

# Place cursor-installer CLI into ~/.local/bin from local file or GitHub
Expand Down Expand Up @@ -112,4 +125,3 @@ else
fi

log_ok "Installation complete. You can now run '$CLI_NAME' to start Cursor."