Skip to content

Instantly share code, notes, and snippets.

@zudsniper
Forked from tatosjb/install-cursor-sh
Last active April 16, 2025 22:49
Show Gist options
  • Save zudsniper/0117371b931d0026b03710d73c2ececf to your computer and use it in GitHub Desktop.
Save zudsniper/0117371b931d0026b03710d73c2ececf to your computer and use it in GitHub Desktop.
install_cursor.sh -- auto-download latest and version compare
#!/bin/bash
# Exit on error
set -e
# --- Configuration ---
# (Configuration remains the same)
INSTALL_DIR="/opt"
SYMLINK_NAME="cursor"
SYMLINK_PATH="${INSTALL_DIR}/${SYMLINK_NAME}"
ICON_NAME="cursor.png"
ICON_PATH="${INSTALL_DIR}/${ICON_NAME}"
ICON_URL="https://raw.githubusercontent.com/getcursor/cursor/main/apps/desktop/resources/images/icon.png"
DESKTOP_ENTRY_NAME="cursor.desktop"
DESKTOP_ENTRY_PATH="/usr/share/applications/${DESKTOP_ENTRY_NAME}"
API_URL="https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable"
TEMP_DIR="/tmp"
# --- End Configuration ---
# Function to check if a command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Function to compare versions (requires sort supporting -V)
# (version_ge function remains the same)
version_ge() {
if ! echo -e "1.10\n1.2" | sort -V >/dev/null 2>&1; then
echo "Warning: Your 'sort' command does not support -V (version sort)." >&2
echo "Version comparison might be inaccurate. Comparing lexicographically." >&2
[ "$1" = "$2" ] || [ "$1" \> "$2" ]
else
[ "$1" = "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -n1)" ]
fi
}
# Function to install required packages
install_packages() {
local missing_packages=()
# Loop through packages passed as arguments (e.g., curl, jq)
for pkg in "$@"; do
if ! command_exists "$pkg"; then
# If the COMMAND doesn't exist, assume the PACKAGE is missing
missing_packages+=("$pkg")
fi
done
if [ ${#missing_packages[@]} -gt 0 ]; then
echo "The following required packages appear missing: ${missing_packages[*]}"
echo "Attempting to install them..."
local manager_cmd=""
local update_cmd=""
local install_cmd=""
if command_exists apt-get; then
manager_cmd="apt-get"
update_cmd="sudo apt-get update"
install_cmd="sudo apt-get install -y"
elif command_exists dnf; then
manager_cmd="dnf"
# dnf usually doesn't require a separate update command before install
update_cmd="echo 'Using dnf, update not usually required before install.'" # Placeholder
install_cmd="sudo dnf install -y"
elif command_exists yum; then
manager_cmd="yum"
update_cmd="echo 'Using yum, update not usually required before install.'" # Placeholder
install_cmd="sudo yum install -y"
elif command_exists pacman; then
manager_cmd="pacman"
update_cmd="sudo pacman -Syu --noconfirm" # Update and install together
install_cmd="sudo pacman -S --noconfirm" # Install command for pacman
else
echo "Error: Cannot determine package manager. Please install manually: ${missing_packages[*]}"
exit 1
fi
# Run update command if defined and needed
if [[ "$manager_cmd" == "apt-get" || "$manager_cmd" == "pacman" ]]; then
$update_cmd
fi
# Run install command
$install_cmd "${missing_packages[@]}" || {
echo "Error: Package installation failed. Please install manually: ${missing_packages[*]}"
exit 1
}
# Verify installation *after* attempting install
echo "Verifying installation..."
local still_missing=()
for pkg in "${missing_packages[@]}"; do
if ! command_exists "$pkg"; then
# It's possible the package name doesn't match the command name exactly
# But for curl and jq, it usually does.
echo "Warning: Command '$pkg' still not found after installation attempt." >&2
echo "The script will continue, but might fail later if '$pkg' is truly needed." >&2
# Decide if you want to exit here or just warn. Warning is often okay.
# still_missing+=("$pkg") # Optionally track and exit if any are still missing
fi
done
# if [ ${#still_missing[@]} -gt 0 ]; then
# echo "Error: Failed to install or find commands for: ${still_missing[*]}"
# exit 1
# fi
echo "Required packages check complete."
else
echo "Dependencies satisfied."
fi
}
# Function to extract version from filename or URL path
# (extract_version function remains the same)
extract_version() {
local input_string="$1"
local version=$(echo "$input_string" | grep -oP '/\K([0-9]+\.[0-9]+\.[0-9]+)(?=/)' || true)
if [ -z "$version" ]; then
version=$(echo "$input_string" | grep -oP '(?<=-)([0-9]+\.[0-9]+\.[0-9]+)(?=-[a-zA-Z0-9]*)' || true)
fi
if [ -z "$version" ]; then
version=$(echo "$input_string" | grep -oP '(?<=-)([0-9]+\.[0-9]+\.[0-9]+)(?=\.AppImage)' || true)
fi
echo "$version"
}
installCursor() {
local CUSTOM_APPIMAGE_PATH="$1"
local TEMP_APPIMAGE=""
local REMOTE_VERSION=""
local REMOTE_APPIMAGE_URL=""
local REMOTE_FILENAME=""
local LOCAL_VERSION=""
local CURRENT_APPIMAGE_TARGET=""
echo "Starting Cursor AI IDE installation/update script..."
# 1. Check dependencies (ONLY check for curl and jq now)
echo "Checking dependencies (curl, jq)..."
install_packages curl jq # Removed coreutils from here
# (Rest of the installCursor function remains the same)
# 2. Handle custom AppImage path
if [ -n "$CUSTOM_APPIMAGE_PATH" ]; then
if [ -f "$CUSTOM_APPIMAGE_PATH" ] && [ -r "$CUSTOM_APPIMAGE_PATH" ]; then
echo "Using provided AppImage file: $CUSTOM_APPIMAGE_PATH"
REMOTE_FILENAME=$(basename "$CUSTOM_APPIMAGE_PATH")
TEMP_APPIMAGE="$CUSTOM_APPIMAGE_PATH"
REMOTE_VERSION=$(extract_version "$REMOTE_FILENAME")
if [ -z "$REMOTE_VERSION" ]; then
echo "Warning: Could not extract version from custom AppImage filename '$REMOTE_FILENAME'."
echo "Proceeding with installation without version check."
LOCAL_VERSION="0.0.0" # Force update if local exists
else
echo "Extracted version $REMOTE_VERSION from custom AppImage filename."
fi
else
echo "Error: The specified AppImage file does not exist or is not readable: $CUSTOM_APPIMAGE_PATH"
exit 1
fi
else
# 3. Fetch latest version info from API
echo "Fetching latest version information from Cursor API..."
local api_response
api_response=$(curl -fsSL "$API_URL") || { echo "Error: Failed to fetch from API: $API_URL"; exit 1; }
REMOTE_APPIMAGE_URL=$(echo "$api_response" | jq -r '.downloadUrl')
if [ -z "$REMOTE_APPIMAGE_URL" ] || [ "$REMOTE_APPIMAGE_URL" = "null" ]; then
echo "Error: Could not parse download URL from API response."
echo "Response: $api_response"
exit 1
fi
REMOTE_VERSION=$(extract_version "$REMOTE_APPIMAGE_URL")
if [ -z "$REMOTE_VERSION" ]; then
REMOTE_FILENAME=$(basename "$REMOTE_APPIMAGE_URL")
REMOTE_VERSION=$(extract_version "$REMOTE_FILENAME")
fi
if [ -z "$REMOTE_VERSION" ]; then
echo "Error: Could not extract version information from URL or filename: $REMOTE_APPIMAGE_URL"
exit 1
fi
REMOTE_FILENAME=$(basename "$REMOTE_APPIMAGE_URL")
echo "Latest version available: $REMOTE_VERSION"
fi
# 4. Check currently installed version (if exists)
echo "Checking for existing installation..."
if [ -L "$SYMLINK_PATH" ]; then
CURRENT_APPIMAGE_TARGET=$(readlink -f "$SYMLINK_PATH")
if [ -f "$CURRENT_APPIMAGE_TARGET" ]; then
LOCAL_VERSION=$(extract_version "$CURRENT_APPIMAGE_TARGET")
if [ -n "$LOCAL_VERSION" ]; then
echo "Currently installed version: $LOCAL_VERSION (at $CURRENT_APPIMAGE_TARGET)"
else
echo "Warning: Could not determine version of installed AppImage: $CURRENT_APPIMAGE_TARGET"
LOCAL_VERSION="0.0.0"
fi
else
echo "Symlink $SYMLINK_PATH points to a non-existent file: $CURRENT_APPIMAGE_TARGET"
echo "Treating as a fresh install."
LOCAL_VERSION=""
CURRENT_APPIMAGE_TARGET=""
fi
elif [ -f "$SYMLINK_PATH" ] && ! [ -L "$SYMLINK_PATH" ]; then # Check if it's a file but NOT a symlink
echo "Warning: Found a regular file instead of a symlink at $SYMLINK_PATH."
echo "Attempting to determine version, but installation structure is non-standard."
CURRENT_APPIMAGE_TARGET="$SYMLINK_PATH"
LOCAL_VERSION=$(extract_version "$CURRENT_APPIMAGE_TARGET")
if [ -n "$LOCAL_VERSION" ]; then
echo "Found version: $LOCAL_VERSION"
else
echo "Warning: Could not determine version of installed file: $CURRENT_APPIMAGE_TARGET"
LOCAL_VERSION="0.0.0"
fi
else
echo "No existing installation found at $SYMLINK_PATH."
LOCAL_VERSION=""
fi
# 5. Compare versions and decide whether to install/update
if [ -n "$LOCAL_VERSION" ] && [ -n "$REMOTE_VERSION" ]; then
if version_ge "$LOCAL_VERSION" "$REMOTE_VERSION"; then
echo "Current version ($LOCAL_VERSION) is the same or newer than the available version ($REMOTE_VERSION)."
echo "Skipping installation."
# Optionally ensure desktop entry/alias still point to SYMLINK_PATH? Usually not needed.
exit 0
else
echo "Newer version ($REMOTE_VERSION) available. Proceeding with update from $LOCAL_VERSION."
fi
elif [ -n "$CUSTOM_APPIMAGE_PATH" ]; then
echo "Installing provided custom AppImage..."
if [ -n "$LOCAL_VERSION" ]; then
echo "Replacing version $LOCAL_VERSION with custom AppImage."
fi
else
echo "Performing fresh installation of version $REMOTE_VERSION."
fi
# --- Installation/Update Steps ---
sudo mkdir -p "$INSTALL_DIR"
# 6. Download AppImage (if not using custom path)
if [ -z "$CUSTOM_APPIMAGE_PATH" ]; then
TEMP_APPIMAGE="${TEMP_DIR}/${REMOTE_FILENAME}"
echo "Downloading Cursor AppImage ($REMOTE_VERSION) to $TEMP_APPIMAGE..."
curl --progress-bar -L "$REMOTE_APPIMAGE_URL" -o "$TEMP_APPIMAGE" || { echo "Error: Failed to download AppImage."; rm -f "$TEMP_APPIMAGE"; exit 1; }
fi
# 7. Download Icon
echo "Downloading Cursor icon..."
curl -fsSL "$ICON_URL" -o "${TEMP_DIR}/${ICON_NAME}" || echo "Warning: Failed to download icon. Skipping icon installation."
# 8. Install files
echo "Installing Cursor files..."
local NEW_APPIMAGE_PATH="${INSTALL_DIR}/${REMOTE_FILENAME}"
# Ensure AppImage is executable before moving (if downloaded)
if [ -z "$CUSTOM_APPIMAGE_PATH" ]; then
chmod +x "$TEMP_APPIMAGE"
fi
# Move AppImage to final destination
# Use cp then rm for sudo permissions across filesystems /tmp might be different
sudo cp "$TEMP_APPIMAGE" "$NEW_APPIMAGE_PATH" || { echo "Error: Failed to copy AppImage to $NEW_APPIMAGE_PATH"; exit 1; }
sudo chmod +x "$NEW_APPIMAGE_PATH" # Ensure executable after copy
# If copy succeeded and we downloaded it (not custom path), remove temp file
if [ -z "$CUSTOM_APPIMAGE_PATH" ]; then
rm -f "$TEMP_APPIMAGE"
fi
# Move Icon
if [ -f "${TEMP_DIR}/${ICON_NAME}" ]; then
sudo mv "${TEMP_DIR}/${ICON_NAME}" "$ICON_PATH" || echo "Warning: Failed to move icon to $ICON_PATH"
fi
# 9. Create/Update Symlink
echo "Creating/Updating symlink: $SYMLINK_PATH -> $NEW_APPIMAGE_PATH"
sudo ln -sfn "$NEW_APPIMAGE_PATH" "$SYMLINK_PATH" || { echo "Error: Failed to create symlink."; exit 1; }
# 10. Create .desktop entry
echo "Creating/Updating .desktop entry..."
sudo mkdir -p "$(dirname "$DESKTOP_ENTRY_PATH")"
sudo bash -c "cat > $DESKTOP_ENTRY_PATH" <<EOL
[Desktop Entry]
Name=Cursor AI IDE
Comment=AI First Code Editor based on VSCode
Exec=$SYMLINK_PATH --no-sandbox %U
Icon=$ICON_PATH
Type=Application
Terminal=false
Categories=Development;IDE;TextEditor;
Keywords=vscode;ai;ide;editor;development;
StartupWMClass=cursor
MimeType=text/plain;inode/directory;application/x-cursor-workspace;
EOL
if command_exists update-desktop-database; then
echo "Updating desktop database..."
sudo update-desktop-database "$(dirname "$DESKTOP_ENTRY_PATH")" &> /dev/null || echo "Warning: Failed to update desktop database."
fi
# 11. Add alias (if needed)
local SHELL_NAME=$(basename "$SHELL")
local RC_FILE=""
case "$SHELL_NAME" in
bash) RC_FILE="$HOME/.bashrc" ;;
zsh) RC_FILE="$HOME/.zshrc" ;;
fish) RC_FILE="$HOME/.config/fish/config.fish" ;;
*) echo "Unsupported shell: $SHELL_NAME. Please add alias manually if desired." ;;
esac
if [ -n "$RC_FILE" ] && [ -f "$RC_FILE" ]; then
echo "Checking alias in $RC_FILE..."
local alias_exists=false
if grep -q "# Cursor alias (function)" "$RC_FILE"; then # Look for our comment marker
if [ "$SHELL_NAME" = "fish" ]; then
if grep -Eq "^\s*function cursor\s*$" "$RC_FILE"; then alias_exists=true; fi
else
if grep -Eq "^\s*function cursor\s*\(\s*\)\s*\{" "$RC_FILE"; then alias_exists=true; fi
fi
fi
if ! $alias_exists; then
echo "Adding cursor alias/function to $RC_FILE..."
if [ "$SHELL_NAME" = "fish" ]; then
# Fish shell syntax
printf '\n# Cursor alias (function)\nfunction cursor\n nohup %s --no-sandbox $argv > /dev/null 2>&1 & disown\nend\n' "$SYMLINK_PATH" >> "$RC_FILE"
else
# Bash/Zsh syntax
printf '\n# Cursor alias (function)\nfunction cursor() {\n nohup "%s" --no-sandbox "$@" > /dev/null 2>&1 & disown\n}\n' "$SYMLINK_PATH" >> "$RC_FILE"
fi
echo "Alias added. Please restart your terminal or run 'source $RC_FILE'"
else
echo "Cursor alias/function already seems to exist in $RC_FILE."
# Here you could add logic to check if the path inside the existing alias is correct ($SYMLINK_PATH),
# but since we use a symlink, it should always point to the latest version anyway.
fi
fi
# 12. Cleanup old AppImage
echo "Cleaning up..."
if [ -n "$CURRENT_APPIMAGE_TARGET" ] && [ "$CURRENT_APPIMAGE_TARGET" != "$NEW_APPIMAGE_PATH" ] && [ -f "$CURRENT_APPIMAGE_TARGET" ]; then
# Make sure we are not deleting the file we just installed or the symlink itself
if [[ "$(readlink -f "$CURRENT_APPIMAGE_TARGET")" != "$(readlink -f "$NEW_APPIMAGE_PATH")" ]] && \
[[ "$(readlink -f "$CURRENT_APPIMAGE_TARGET")" != "$(readlink -f "$SYMLINK_PATH")" ]]; then
echo "Removing old AppImage: $CURRENT_APPIMAGE_TARGET"
sudo rm -f "$CURRENT_APPIMAGE_TARGET" || echo "Warning: Failed to remove old AppImage."
fi
fi
echo "------------------------------------------------------------------"
echo "Cursor AI IDE installation/update complete!"
echo "Version: $REMOTE_VERSION"
echo "Installed to: $NEW_APPIMAGE_PATH"
echo "Executable link: $SYMLINK_PATH"
echo "Icon: $ICON_PATH"
echo "Desktop Entry: $DESKTOP_ENTRY_PATH"
echo "------------------------------------------------------------------"
if [ -n "$RC_FILE" ] && grep -q "# Cursor alias (function)" "$RC_FILE"; then
echo "NOTE: If your shell was already open, run 'source $RC_FILE' or restart it to use the 'cursor' command."
fi
echo "You should find 'Cursor AI IDE' in your application menu (might take a moment to appear)."
}
# Run the installation function
installCursor "$1"
@tatosjb
Copy link

tatosjb commented Apr 15, 2025

I updated the script to get the most recent one in the base script. Please take a look if you want to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment