Last active
April 20, 2025 03:38
-
-
Save realdimas/e58723564cfada8efd93adab6efb747c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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