Skip to content

Instantly share code, notes, and snippets.

@realdimas
Last active April 20, 2025 03:38
Show Gist options
  • Save realdimas/e58723564cfada8efd93adab6efb747c to your computer and use it in GitHub Desktop.
Save realdimas/e58723564cfada8efd93adab6efb747c to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# set -e: exit on command failure
# set -u: error on using undefined variables
# set -o pipefail: fail a pipeline if any command within it fails
set -euo pipefail
# ---------------------------------------------------------------------------
# CLI script that updates or reads a value from a JSON file using jq.
# By default, it targets /Applications/Cursor.app/Contents/Resources/app/product.json,
# but you can override that via --file=... or -f.
#
# Inspired by:
# https://gist.github.com/joeblackwaslike/752b26ce92e3699084e1ecfc790f74b2
#
# Usage examples:
# ./cursor-fixup.sh
# ./cursor-fixup.sh -n
# ./cursor-fixup.sh '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion'
# ./cursor-fixup.sh '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion' 0.399.0
# ---------------------------------------------------------------------------
TARGET_FILE="/Applications/Cursor.app/Contents/Resources/app/product.json"
APP_PATH=""
DRY_RUN=false
# Store the script name for usage
SCRIPT_NAME=$(basename "$0")
# Helper function for consistent logging
log_message() {
local status="$1"
local message="$2"
echo "[${status}] ${message}"
}
usage() {
cat <<EOF
Usage: ${SCRIPT_NAME} [options] [command or json_path [args...]]
Options:
--file, -f <path> Override the default JSON file location (default: /Applications/Cursor.app/Contents/Resources/app/product.json)
The location of this file determines the default app path for maintenance commands.
(Ignored if target_file is given to update_gallery)
--dry-run, -n Show what would be changed, but do not actually write
--help, -h Show this help message
Commands:
update_gallery [target_file] Apply only the extensionsGallery updates.
If target_file is provided, it overrides --file.
check_and_resign [app_path] Check and re-sign the application. If app_path is omitted,
uses the path determined from TARGET_FILE (default: /Applications/Cursor.app).
Requires TARGET_FILE to be in .../App.app/Contents/Resources/app/product.json format.
remove_quarantine [app_path] Remove quarantine attributes. If app_path is omitted,
uses the path determined from TARGET_FILE (default: /Applications/Cursor.app).
Requires same format as check_and_resign.
JSON Operations:
<json_path> Get the value at the JSON path
<json_path> <new_value> Update the value at the JSON path
If no arguments given, the script applies a predefined set of changes
to the default file (/Applications/Cursor.app/Contents/Resources/app/product.json).
These include gallery and extension version updates.
Examples:
# Perform the default updates (gallery, extensions) on the standard file (/Applications/Cursor.app/...)
${SCRIPT_NAME}
# Dry-run only (no changes written)
${SCRIPT_NAME} -n
# Apply only gallery updates to the default file (/Applications/Cursor.app/...)
${SCRIPT_NAME} update_gallery
# Apply only gallery updates to a specific file (e.g., Windsurf.app) using positional arg
${SCRIPT_NAME} update_gallery /Applications/Windsurf.app/Contents/Resources/app/product.json
# Apply only gallery updates to a specific file (e.g., Windsurf.app) using --file, dry-run
${SCRIPT_NAME} -n -f /Applications/Windsurf.app/Contents/Resources/app/product.json update_gallery
# Print the current value from the default file (/Applications/Cursor.app/...)
${SCRIPT_NAME} '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion'
# Update that same key in the default file (/Applications/Cursor.app/...)
${SCRIPT_NAME} '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion' 0.399.0
# Check and re-sign application based on the default TARGET_FILE (/Applications/Cursor.app)
${SCRIPT_NAME} check_and_resign
# Check and re-sign a specific application (e.g., Windsurf.app, overriding the default)
${SCRIPT_NAME} check_and_resign /Applications/Windsurf.app
# Remove quarantine attribute from application based on default TARGET_FILE (/Applications/Cursor.app)
${SCRIPT_NAME} remove_quarantine
# Remove quarantine attribute for a specific application (e.g., Windsurf.app, overriding the default)
${SCRIPT_NAME} remove_quarantine /Applications/Windsurf.app
EOF
}
# Ensures the path starts with a leading dot if it doesn't already.
ensure_leading_dot() {
local path="$1"
if [ -z "$path" ]; then
echo "$path"
elif [[ "$path" == .* ]]; then
echo "$path"
else
echo ".$path"
fi
}
# Prints the current value of 'json_path' from 'file' using jq.
read_json_value() {
local file="$1"
local raw_path="$2"
local path_to_use
path_to_use="$(ensure_leading_dot "$raw_path")"
if ! jq -e "$path_to_use" "$file" &>/dev/null; then
log_message "NO_KEY" "${path_to_use}: key does not exist or cannot be retrieved."
return 0
fi
local val
val="$(jq -r "$path_to_use" "$file")"
log_message "READ" "${path_to_use} => ${val}"
}
# Updates 'json_path' in 'file' to 'new_value' using jq.
# Uses a herestring to feed the new JSON into the original file.
update_json_value() {
local file="$1"
local raw_path="$2"
local new_value="$3"
local path_to_use
path_to_use="$(ensure_leading_dot "$raw_path")"
local key_missing=false
if ! jq -e "$path_to_use" "$file" &>/dev/null; then
key_missing=true
fi
local current_value
current_value="$(jq -r "$path_to_use" "$file")"
if [ "$current_value" = "$new_value" ]; then
log_message "NO_CHANGE" "${path_to_use}: Already => ${new_value}"
return 0
fi
if [ "$DRY_RUN" = true ]; then
if [ "$key_missing" = true ]; then
log_message "WILL_CREATE" "${path_to_use}: Would create key and set to => ${new_value}"
else
log_message "WILL_UPDATE" "${path_to_use}: Would change from => ${current_value} to => ${new_value}"
fi
return 0
fi
# Generate new JSON content using jq (in a variable) so we can check if it failed
local jq_out
if ! jq_out="$(jq --arg val "$new_value" "$path_to_use |= \$val" "$file" 2>/dev/null)"; then
log_message "ERROR" "jq failed trying to update ${path_to_use}"
return 1
fi
if [ -z "$jq_out" ]; then
log_message "ERROR" "jq produced no output for updating ${path_to_use}"
return 1
fi
printf "%s\n" "$jq_out" >"$file"
if [ "$key_missing" = true ]; then
log_message "CREATED" "${path_to_use}: Set to => ${new_value}"
else
log_message "UPDATED" "${path_to_use}: Changed from => ${current_value} to => ${new_value}"
fi
}
check_and_resign() {
local APP_PATH="$1"
local STATUS=0
local ERROR_MSG
ERROR_MSG="$(codesign -v --deep --strict "${APP_PATH}" 2>&1)" || STATUS=$?
if [ "$STATUS" -eq 1 ] && [[ "$ERROR_MSG" == *"a sealed resource is missing or invalid"* ]]; then
if [ "$DRY_RUN" = true ]; then
log_message "WILL_RESIGN" "${APP_PATH}: Resource is missing or invalid, would re-sign"
return 0
fi
local codesign_output
codesign_output=$(codesign --force --deep --sign - "${APP_PATH}" 2>&1)
log_message "RESIGNED" "${APP_PATH}: ${codesign_output}"
else
log_message "NO_CHANGE" "${APP_PATH}: No code signature issues found"
fi
}
remove_quarantine() {
local APP_PATH="$1"
# Check for quarantine attribute. The presence of "com.apple.quarantine"
# in `xattr` output indicates it may be set.
if xattr -p com.apple.quarantine "${APP_PATH}" &>/dev/null; then
if [ "$DRY_RUN" = true ]; then
log_message "WILL_UPDATE" "${APP_PATH}: Would remove quarantine attributes"
return 0
fi
log_message "UPDATING" "${APP_PATH}: Removing quarantine attributes"
xattr -r -d com.apple.quarantine "${APP_PATH}"
log_message "UPDATED" "${APP_PATH}: Quarantine attributes removed"
else
log_message "NO_CHANGE" "${APP_PATH}: No quarantine attributes found"
fi
}
# Apply updates related to extensionsGallery
apply_gallery_updates() {
log_message "INFO" "Applying extensionsGallery updates to => $TARGET_FILE"
# Switchover to MS VSCode Marketplace endpoints
#
# galleryId needs to be unchanged to avoid the "Error while fetching extensions. Cannot read properties of undefined (reading 'identifier')" exception
update_json_value "$TARGET_FILE" .extensionsGallery.serviceUrl https://marketplace.visualstudio.com/_apis/public/gallery
update_json_value "$TARGET_FILE" .extensionsGallery.itemUrl https://marketplace.visualstudio.com/items
update_json_value "$TARGET_FILE" .extensionsGallery.resourceUrlTemplate https://{publisher}.vscode-unpkg.net/{publisher}/{name}/{version}/{path}
update_json_value "$TARGET_FILE" .extensionsGallery.controlUrl https://main.vscode-cdn.net/extensions/marketplace.json
update_json_value "$TARGET_FILE" .extensionsGallery.nlsBaseUrl https://www.vscode-unpkg.net/_lp/
update_json_value "$TARGET_FILE" .extensionsGallery.publisherUrl https://marketplace.visualstudio.com/publishers
}
# Apply updates related to extensionMaxVersions
apply_extension_version_updates() {
log_message "INFO" "Applying extensionMaxVersions updates to => $TARGET_FILE"
# Compatibility as of Cursor Version 0.49.2 (VSCode Version 1.96.2) and Windsurf Version 1.6.5 (VSCode Version 1.97.0) on macOS (arm64)
#
# ms-python.python
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-python.python
# VSIX download URL (darwin-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.5.2025041801/vspackage?targetPlatform=darwin-arm64
# VSIX download URL (linux-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.5.2025041801/vspackage?targetPlatform=linux-arm64
# VSIX download URL (linux-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.5.2025041801/vspackage?targetPlatform=linux-x64
# Last working (via .vsix) version: 2025.5.2025041801 (current)
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.python".maxVersion' 2025.5.2025041801
#
# ms-python.debugpy
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy
# VSIX download URL (darwin-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.6.0/vspackage?targetPlatform=darwin-arm64
# VSIX download URL (linux-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.6.0/vspackage?targetPlatform=linux-arm64
# VSIX download URL (linux-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.6.0/vspackage?targetPlatform=linux-x64
# Last working (via .vsix) version: 2025.6.0 (current)
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.debugpy".maxVersion' 2025.6.0
#
# ms-python.vscode-pylance
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance
# VSIX download URL: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/vscode-pylance/2025.4.100/vspackage
# Last working (via .vsix) version: 2025.4.100 (current)
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.vscode-pylance".maxVersion' 2025.4.100
#
# ms-vscode-remote.remote-containers
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers
# VSIX download URL: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-vscode-remote/vsextensions/remote-containers/0.399.0/vspackage
# Last working (via .vsix) version: 0.399.0 (versions up to and including 0.411.0 are not installable)
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion' 0.399.0
#
# anysphere.pyright
# Suppress installation by setting maxVersion to 0.0.0-0
# VSIX download URL: https://open-vsx.org/api/anysphere/pyright/1.1.327/file/anysphere.pyright-1.1.327.vsix
update_json_value "$TARGET_FILE" '.extensionMaxVersions."anysphere.pyright".maxVersion' 0.0.0-0
# codeium.windsurfPyright
# Suppress installation by setting maxVersion to 0.0.0-0
# VSIX download URL: https://open-vsx.org/api/codeium/windsurfPyright/1.28.0/file/codeium.windsurfPyright-1.28.0.vsix
update_json_value "$TARGET_FILE" '.extensionMaxVersions."codeium.windsurfPyright".maxVersion' 0.0.0-0
}
# Apply all predefined updates (default behavior)
apply_predefined_updates() {
log_message "INFO" "Using file => $TARGET_FILE"
log_message "INFO" "Applying all predefined updates..."
apply_gallery_updates
apply_extension_version_updates
log_message "INFO" "Successfully completed all updates."
}
# Parse flags and any remaining positional arguments
POSITIONAL=()
while [ $# -gt 0 ]; do
case "$1" in
--file | -f)
TARGET_FILE="$2"
shift 2
;;
--dry-run | -n)
DRY_RUN=true
shift
;;
--help | -h)
usage
exit 0
;;
*)
# Treat anything else as a positional argument (json_path or new_value)
POSITIONAL+=("$1")
shift
;;
esac
done
# Restore positional arguments
pos_count=0
if [ ${#POSITIONAL[@]:-0} -gt 0 ]; then
set -- "${POSITIONAL[@]}"
pos_count=$#
else
# Clear arguments when POSITIONAL is empty
set --
pos_count=0
fi
# Attempts to find the path ending in .app four levels up from product.json
determine_app_path_from_target() {
local target="$1"
local found_path=""
# Check if the path seems plausible before trying to determine
if [[ "$target" == *"/Contents/Resources/app/product.json" ]]; then
found_path=$(dirname "$(dirname "$(dirname "$(dirname "$target")")")")
# Basic check if the result looks like an app bundle path
if [[ "$found_path" == *.app ]]; then
APP_PATH="$found_path"
log_message "DEBUG" "Determined APP_PATH: $APP_PATH from TARGET_FILE: $target"
return 0 # Success
fi
fi
# If determination failed or path is unsuitable
log_message "DEBUG" "Could not determine .app path from TARGET_FILE: $target"
APP_PATH="" # Ensure it's empty on failure
return 1 # Failure
}
# Call function *after* TARGET_FILE might have been changed by args
determine_app_path_from_target "$TARGET_FILE"
# Ensure jq is installed
if ! command -v jq &>/dev/null; then
log_message "ERROR" "'jq' is required but not installed."
exit 1
fi
# Handle commands based on argument count
if [ "$pos_count" -gt 0 ]; then
# First check if the first argument is a known command
case "$1" in
"update_gallery")
if [ "$pos_count" -gt 2 ]; then
log_message "ERROR" "update_gallery takes at most one argument (target_file)"
usage
exit 1
fi
# If a second argument is provided, use it as the target file
if [ "$pos_count" -eq 2 ]; then
TARGET_FILE="$2"
log_message "INFO" "Overriding target file with argument => $TARGET_FILE"
fi
apply_gallery_updates
exit 0
;;
"check_and_resign")
if [ "$pos_count" -gt 2 ]; then
log_message "ERROR" "check_and_resign takes at most one argument (app path)"
usage
exit 1
fi
# Use the provided app path or the one determined from TARGET_FILE
app_to_check="${2:-$APP_PATH}"
if [ -z "$app_to_check" ]; then
log_message "ERROR" "No app path provided and could not determine one from TARGET_FILE ($TARGET_FILE)."
usage
exit 1
fi
check_and_resign "$app_to_check"
exit 0
;;
"remove_quarantine")
if [ "$pos_count" -gt 2 ]; then
log_message "ERROR" "remove_quarantine takes at most one argument (app path)"
usage
exit 1
fi
# Use the provided app path or the one determined from TARGET_FILE
app_to_quarantine="${2:-$APP_PATH}"
if [ -z "$app_to_quarantine" ]; then
log_message "ERROR" "No app path provided and could not determine one from TARGET_FILE ($TARGET_FILE)."
usage
exit 1
fi
remove_quarantine "$app_to_quarantine"
exit 0
;;
*)
# Handle as JSON operations
if [ "$pos_count" -eq 1 ]; then
# Treat as JSON path unless it's a command we missed?
# Could add more specific validation here if needed.
# "read" mode: show the existing JSON value
read_json_value "$TARGET_FILE" "$1"
exit 0
elif [ "$pos_count" -eq 2 ]; then
# "update" mode: update the JSON with the new value
update_json_value "$TARGET_FILE" "$1" "$2"
exit 0
else
# More than 2 positional args not matching a command is an error
log_message "ERROR" "Invalid arguments or command."
usage
exit 1
fi
;;
esac
fi
# If no commands were handled above, apply the predefined updates
apply_predefined_updates
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment