#!/usr/bin/env bash # Bump PKGBUILD/.SRCINFO when Cursor publishes a newer linux-x64 Grok Bot. # Requires curl, sha256sum, python3. jq is optional (python3 parses the feed). # Fail loudly; do not leave a half-applied bump that looks like "up to date". set -euo pipefail cd "$(dirname "$0")/.." need_cmd() { command -v "$1" >/dev/null 2>&1; } missing=() need_cmd curl || missing+=(curl) need_cmd sha256sum || missing+=(sha256sum) need_cmd python3 || missing+=(python3) if ((${#missing[@]})); then echo "error: missing required tools: ${missing[*]}" >&2 exit 1 fi if ! need_cmd jq; then echo "note: jq not found; parsing update feed with python3" >&2 fi FEED='https://api2.cursor.sh/updates/api/update/linux-x64/sand/0.0.0/00000000-0000-0000-0000-000000000000/stable' echo "Fetching update feed..." if ! resp=$(curl -fsSL --retry 3 --retry-delay 2 "$FEED"); then echo "error: failed to fetch update feed: $FEED" >&2 exit 1 fi if [[ -z "$resp" ]]; then echo "error: empty update feed from $FEED" >&2 exit 1 fi feed_field() { local field="$1" if need_cmd jq; then jq -er --arg f "$field" '.[$f] | select(. != null and . != "")' <<<"$resp" else python3 -c ' import json, sys d = json.loads(sys.stdin.read()) v = d.get(sys.argv[1]) if not isinstance(v, (str, int, float)) or v == "": raise SystemExit(1) print(v) ' "$field" <<<"$resp" fi } if ! version=$(feed_field version); then echo "error: could not parse .version from update feed: $resp" >&2 exit 1 fi if ! url=$(feed_field url); then echo "error: could not parse .url from update feed: $resp" >&2 exit 1 fi commit=$(sed -E 's#.*/stable/([^/]+)/.*#\1#' <<<"$url") current=$(sed -n 's/^pkgver=//p' PKGBUILD | head -1) if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*$ ]]; then echo "error: feed version looks wrong: $version" >&2 exit 1 fi if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then echo "error: could not parse 40-char commit from feed url: $url" >&2 exit 1 fi if [[ -z "$current" ]]; then echo "error: could not read pkgver from PKGBUILD" >&2 exit 1 fi if [[ "$version" == "$current" ]]; then echo "Up to date at $version" exit 0 fi echo "Bump $current -> $version (commit $commit)" deb_url="https://downloads.cursor.com/grokbot/stable/${commit}/linux/x64/grok-bot_${version}_amd64.deb" tmp=$(mktemp) trap 'rm -f "$tmp"' EXIT echo "Downloading $deb_url" if ! curl -fL --retry 3 --retry-delay 2 -o "$tmp" "$deb_url"; then echo "error: failed to download deb: $deb_url" >&2 exit 1 fi sz=$(wc -c <"$tmp") if ((sz < 1000000)); then echo "error: downloaded deb too small (${sz} bytes): $deb_url" >&2 exit 1 fi magic=$(head -c 8 "$tmp" | tr -d '\0') if [[ "$magic" != '!'* ]]; then echo "error: download is not a .deb (missing ar magic): $deb_url" >&2 exit 1 fi deb_sha=$(sha256sum "$tmp" | cut -d' ' -f1) shim_sha=$(sha256sum grok-bot-launch.sh | cut -d' ' -f1) pkexec_sha=$(sha256sum pkexec-pacman-u | cut -d' ' -f1) policy_sha=$(sha256sum com.s1d3sw1ped.grok-bot-bin.policy | cut -d' ' -f1) for label in deb_sha:$deb_sha shim_sha:$shim_sha pkexec_sha:$pkexec_sha policy_sha:$policy_sha; do name=${label%%:*} val=${label#*:} if [[ ! "$val" =~ ^[0-9a-f]{64}$ ]]; then echo "error: sha256sum failed ($name=$val)" >&2 exit 1 fi done # Reset pkgrel on upstream version bumps. sed -i \ -e "s/^pkgver=.*/pkgver=${version}/" \ -e "s/^_commit=.*/_commit=${commit}/" \ -e "s/^pkgrel=.*/pkgrel=1/" \ PKGBUILD newver=$(sed -n 's/^pkgver=//p' PKGBUILD | head -1) newcommit=$(sed -n 's/^_commit=//p' PKGBUILD | head -1) newrel=$(sed -n 's/^pkgrel=//p' PKGBUILD | head -1) if [[ "$newver" != "$version" || "$newcommit" != "$commit" || "$newrel" != "1" ]]; then echo "error: PKGBUILD rewrite did not stick (pkgver=$newver _commit=$newcommit pkgrel=$newrel)" >&2 exit 1 fi python3 - "$deb_sha" "$shim_sha" "$pkexec_sha" "$policy_sha" <<'PY2' import pathlib, re, sys deb_sha, shim_sha, pkexec_sha, policy_sha = sys.argv[1:5] p = pathlib.Path("PKGBUILD") text = p.read_text() block = ( "sha256sums=(\n" f" '{deb_sha}'\n" f" '{shim_sha}'\n" f" '{pkexec_sha}'\n" f" '{policy_sha}'\n" ")" ) text2, n = re.subn(r"sha256sums=\([^\)]*\)", block, text, count=1, flags=re.S) if n != 1: raise SystemExit(f"sha256sums replace failed (n={n})") p.write_text(text2) PY2 python3 - "$version" "$commit" "$deb_sha" "$shim_sha" "$pkexec_sha" "$policy_sha" <<'PY2' import pathlib, sys version, commit, deb_sha, shim_sha, pkexec_sha, policy_sha = sys.argv[1:7] p = pathlib.Path(".SRCINFO") if not p.is_file(): raise SystemExit(".SRCINFO missing") shas = [deb_sha, shim_sha, pkexec_sha, policy_sha] lines = [] saw_sha = False saw_pkgver = saw_pkgrel = saw_source = saw_noextract = False for line in p.read_text().splitlines(True): if line.startswith("\tpkgver ="): lines.append(f"\tpkgver = {version}\n"); saw_pkgver = True; continue if line.startswith("\tpkgrel ="): lines.append("\tpkgrel = 1\n"); saw_pkgrel = True; continue if line.startswith("\tsource = https://downloads.cursor.com/grokbot/stable/"): lines.append( f"\tsource = https://downloads.cursor.com/grokbot/stable/{commit}/linux/x64/grok-bot_{version}_amd64.deb\n" ) saw_source = True continue if line.startswith("\tnoextract = grok-bot_"): lines.append(f"\tnoextract = grok-bot_{version}_amd64.deb\n") saw_noextract = True continue if line.startswith("\tsha256sums ="): if not saw_sha: for s in shas: lines.append(f"\tsha256sums = {s}\n") saw_sha = True continue lines.append(line) missing = [n for n, ok in ( ("pkgver", saw_pkgver), ("pkgrel", saw_pkgrel), ("source", saw_source), ("noextract", saw_noextract), ("sha256sums", saw_sha), ) if not ok] if missing: raise SystemExit(f".SRCINFO rewrite missed fields: {', '.join(missing)}") # Ensure four sha lines present sha_lines = [l for l in lines if l.startswith("\tsha256sums =")] if len(sha_lines) != 4: raise SystemExit(f".SRCINFO expected 4 sha256sums lines, got {len(sha_lines)}") p.write_text("".join(lines)) PY2 echo "Updated PKGBUILD and .SRCINFO to ${version} deb_sha=${deb_sha} shim_sha=${shim_sha} pkexec_sha=${pkexec_sha} policy_sha=${policy_sha}"