From 23c508079c2de7b4567c5fffc38dbd8ea47d5987 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 25 Dec 2025 20:11:13 -0400 Subject: [PATCH] Update Cursor version resolution to avoid latest lagging behind newly published patch releases. --- README.md | 19 ++++++++++ update-cursor.sh | 93 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 66042b3..4ac0b07 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,25 @@ The `update-cursor.sh` script provides: - **x86_64 AppImage Download**: `https://api2.cursor.sh/updates/download/golden/linux-x64/cursor/{version}` - **aarch64 AppImage Download**: `https://api2.cursor.sh/updates/download/golden/linux-arm64/cursor/{version}` +### Version pointers / "channels" + +Cursor exposes a few redirect-style pointers that may not always move in lockstep: + +- **`.../cursor/latest`**: “promoted” latest build for that channel. This can lag a new release during phased rollout / promotion. +- **`.../cursor/{major.minor}` (e.g. `.../cursor/2.2`)**: latest patch within that major.minor line. This can point to a newer build earlier than `latest`. + +Concrete example (this has happened in practice): + +- `.../cursor/latest` redirects to **`Cursor-2.2.43-...AppImage`** +- `.../cursor/2.2` redirects to **`Cursor-2.2.44-...AppImage`** + +In that situation, **`latest` is behind**. This flake’s updater checks both and will choose **2.2.44**. + +This flake’s `update-cursor.sh` default (**auto**) strategy: + +- **Checks multiple pointers**: `latest`, the current installed major.minor (e.g. `2.2`), and (if different) `latest`’s own major.minor (e.g. `2.3`). +- **Picks the highest semver**: so it won’t stick to an old minor, but it also won’t miss patch releases when `latest` lags. + ## Testing ```bash diff --git a/update-cursor.sh b/update-cursor.sh index 32a79d5..cb9bb9f 100755 --- a/update-cursor.sh +++ b/update-cursor.sh @@ -3,7 +3,11 @@ set -euo pipefail # Script to manually update Cursor version/hashes used by the flake -# Usage: ./update-cursor.sh [version] +# Usage: +# ./update-cursor.sh # auto: pick highest of ("latest" pointer, current major.minor pointer) +# ./update-cursor.sh 2.2.44 # pin exact version +# ./update-cursor.sh latest # follow the "latest" pointer only +# ./update-cursor.sh 2.2 # follow a major.minor pointer only SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RELEASE_FILE="$SCRIPT_DIR/cursor-release.nix" @@ -11,26 +15,63 @@ RELEASE_FILE="$SCRIPT_DIR/cursor-release.nix" CHANNEL_FOR_SYSTEM_x86_64_linux="linux-x64" CHANNEL_FOR_SYSTEM_aarch64_linux="linux-arm64" -# Function to get latest version from API redirect +# Get a redirect URL for a given "track" pointer (e.g. latest, 2.2). +get_redirect_url() { + local track="${1:?track is required}" + local url="https://api2.cursor.sh/updates/download/golden/linux-x64/cursor/${track}" + + # Avoid stale CDN/proxy cache results for fast-moving pointers like "latest". + curl -sS -I \ + -H 'Cache-Control: no-cache' \ + -H 'Pragma: no-cache' \ + "$url" \ + | awk 'BEGIN{IGNORECASE=1} $1=="location:" {print $2; exit}' \ + | tr -d '\r\n' +} + +# Resolve a track pointer (e.g. latest, 2.2) into an actual semver (e.g. 2.2.44). get_latest_version() { - # Get the redirect URL and extract version from the AppImage filename + local track="${1:-latest}" + local redirect_url - redirect_url=$(curl -s -I "https://api2.cursor.sh/updates/download/golden/linux-x64/cursor/latest" | grep -i location | cut -d' ' -f2 | tr -d '\r\n') - + redirect_url="$(get_redirect_url "$track")" + if [[ -n "$redirect_url" ]]; then - # Extract version from URL like: .../Cursor-1.7.39-x86_64.AppImage - echo "$redirect_url" | grep -o 'Cursor-[0-9]\+\.[0-9]\+\.[0-9]\+' | sed 's/Cursor-//' + # Extract version from URL like: .../Cursor-2.2.44-x86_64.AppImage + echo "$redirect_url" | grep -Eo 'Cursor-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/Cursor-//' else - echo "Error: Could not get redirect URL" + echo "Error: Could not get redirect URL for track: $track" >&2 return 1 fi } +# True if arg looks like a full semver (e.g. 2.2.44). +is_full_semver() { + [[ "${1:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +# True if arg looks like a pointer we can resolve (e.g. latest, 2.2). +is_pointer() { + [[ "${1:-}" == "latest" || "${1:-}" =~ ^[0-9]+\.[0-9]+$ ]] +} + # Function to get current version from cursor-release.nix get_current_version() { grep -o 'version = "[^"]*"' "$RELEASE_FILE" | head -1 | cut -d'"' -f2 } +# Derive major.minor track from a full version (e.g. 2.2.43 -> 2.2). +get_track_from_version() { + local version="${1:?version is required}" + echo "$version" | awk -F. '{print $1 "." $2}' +} + +# Return the highest of the provided dotted versions using natural version sort. +semver_max() { + # Accepts N args; prints the max. + printf '%s\n' "$@" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 +} + # Function to compute sha256 for a given system+version prefetch_sha256() { local system="$1" @@ -126,11 +167,39 @@ main() { # Determine target version if [[ -n "$target_version" ]]; then - echo "Target version: $target_version" + if is_full_semver "$target_version"; then + echo "Target version (pinned): $target_version" + elif is_pointer "$target_version"; then + echo "Target pointer: $target_version" + target_version="$(get_latest_version "$target_version")" + echo "Resolved version: $target_version" + else + echo "Error: Invalid argument: $target_version" >&2 + echo "Expected: full version (e.g. 2.2.44) or pointer (latest or 2.2)" >&2 + exit 1 + fi else - echo "Fetching latest version..." - target_version=$(get_latest_version) - echo "Latest version: $target_version" + # Auto mode: + # - Check "latest" to allow moving to new major/minor (e.g. 2.3.x) + # - Check current major.minor line to avoid missing patches when "latest" lags (e.g. 2.2.44 vs latest=2.2.43) + # - Also check latest's own major.minor line (if latest=2.3.0 but 2.3 pointer is 2.3.1) + local current_track v_latest latest_track v_latest_track v_current_track + current_track="$(get_track_from_version "$current_version")" + + echo "Fetching pointers: latest, $current_track" + v_latest="$(get_latest_version latest)" + latest_track="$(get_track_from_version "$v_latest")" + v_current_track="$(get_latest_version "$current_track")" + + if [[ "$latest_track" != "$current_track" ]]; then + echo "Latest is on a different minor: $latest_track (also checking it)" + v_latest_track="$(get_latest_version "$latest_track")" + else + v_latest_track="$v_latest" + fi + + target_version="$(semver_max "$v_latest" "$v_current_track" "$v_latest_track")" + echo "Resolved: latest=$v_latest pointer($current_track)=$v_current_track pointer($latest_track)=$v_latest_track -> chosen=$target_version" fi # Check if update is needed