Skip to content

Instantly share code, notes, and snippets.

@realdimas
Last active July 30, 2025 15:33
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.
enable_settings_sync [target_file] Enable Settings Sync configuration. If target_file is omitted,
uses the file specified by --file. If target_file is provided,
it overrides --file.
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 extension version updates.
Examples:
# Perform the default updates (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
# Enable Settings Sync in the default file (/Applications/Cursor.app/...)
${SCRIPT_NAME} enable_settings_sync
# Enable Settings Sync in a specific product.json file
${SCRIPT_NAME} enable_settings_sync /Applications/Cursor.app/Contents/Resources/app/product.json
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 (Cursor 1.1.7 and earlier only)
#
# 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.extensionUrlTemplate 'https://www.vscode-unpkg.net/_gallery/{publisher}/{name}/latest'
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() {
# Compatibility as of Cursor Version 1.2.4 (VSCode Version 1.99.3) on macOS (arm64)
log_message "INFO" "Applying extensionMaxVersions updates to => $TARGET_FILE"
# ms-python.python
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-python.python
# Last working (via .vsix drag-and-drop) version: 2025.10.0 (current)
# VSIX download URL (darwin-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.10.0/vspackage?targetPlatform=darwin-arm64
# VSIX download URL (darwin-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.10.0/vspackage?targetPlatform=darwin-x64
# VSIX download URL (linux-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.10.0/vspackage?targetPlatform=linux-arm64
# VSIX download URL (linux-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.10.0/vspackage?targetPlatform=linux-x64
# Cursor supports up to 2026.6.1 which is recommending Cursor's Pyright, we try to get away from it by bumping the minVersion to 2025.6.2
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.python".minVersion' 2025.6.2
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.python".maxVersion' 2025.10.0
# ms-python.debugpy
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy
# Last working (via .vsix drag-and-drop) version: 2025.10.0 (current)
# VSIX download URL (darwin-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.10.0/vspackage?targetPlatform=darwin-arm64
# VSIX download URL (darwin-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.10.0/vspackage?targetPlatform=darwin-x64
# VSIX download URL (linux-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.10.0/vspackage?targetPlatform=linux-arm64
# VSIX download URL (linux-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.10.0/vspackage?targetPlatform=linux-x64
# Cursor supports up to 2025.6.0, we try to get away from it by bumping the minVersion to 2025.6.1
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.debugpy".minVersion' 2025.6.2
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.debugpy".maxVersion' 2025.10.0
# ms-python.vscode-pylance can be updated to 2025.6.2 (requires manual patching)
# See https://github.com/VSCodium/vscodium/discussions/1641#discussioncomment-13694853
# VSIX download URL: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/vscode-pylance/2025.6.2/vspackage
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.vscode-pylance".minVersion' 2025.4.1
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.vscode-pylance".maxVersion' 2025.6.2
# ms-vscode-remote.remote-containers is no longer supported in Cursor as of 1.2.0
# Note to self: how to download Cursor's anysphere.remote-containers .vsix -
# curl -L https://marketplace.cursorapi.com/downloads/production/extensions/94d8b289-9db0-4c3c-b4f3-a3294c789678/1.0.12/Microsoft.VisualStudio.Services.VSIXPackage -o anysphere.remote-containers-1.0.12.vsix
# ms-vsliveshare.vsliveshare
# Marketplace URL: https://marketplace.visualstudio.com/items?itemName=ms-vsliveshare.vsliveshare
# VSIX download URL: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-vsliveshare/vsextensions/vsliveshare/1.0.5936/vspackage
# Last working version: 1.0.5936
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-vsliveshare.vsliveshare".maxVersion' 1.0.5936
# anysphere.pyright
# Try to suppress installation by setting maxVersion to 0.0.0-0
update_json_value "$TARGET_FILE" '.extensionMaxVersions."anysphere.pyright".maxVersion' 0.0.0-0
# anysphere.cursorpyright
# Try to suppress installation by setting maxVersion to 0.0.0-0
update_json_value "$TARGET_FILE" '.extensionMaxVersions."anysphere.cursorpyright".maxVersion' 0.0.0-0
}
# Enable Settings Sync in the product.json file
enable_settings_sync() {
local target="$TARGET_FILE"
# If an argument is provided, use it as the target file
if [ -n "${1:-}" ]; then
target="$1"
log_message "INFO" "Using specified product.json => $target"
if [ ! -f "$target" ]; then
log_message "ERROR" "File not found at => $target"
return 1
fi
fi
log_message "INFO" "Enabling Settings Sync in => $target"
# Define the desired JSON structure for Settings Sync
local desired_sync_config='{
"configurationSync.store": {
"url": "https://vscode-sync.trafficmanager.net/",
"stableUrl": "https://vscode-sync.trafficmanager.net/",
"insidersUrl": "https://vscode-sync-insiders.trafficmanager.net/",
"canSwitch": false,
"authenticationProviders": {
"github": { "scopes": ["user:email"] },
"microsoft": { "scopes": ["openid", "profile", "email", "offline_access"] }
}
},
"editSessions.store": {
"url": "https://vscode-sync.trafficmanager.net/",
"authenticationProviders": {
"microsoft": { "scopes": ["openid", "profile", "email", "offline_access"] },
"github": { "scopes": ["user:email"] }
}
}
}'
# Use recursive merge (*=) to update/add keys without overwriting unrelated keys
local jq_filter=". *= ( \$sync_config | fromjson )"
# Generate the potential new content
local new_content
if ! new_content="$(jq --arg sync_config "$desired_sync_config" "$jq_filter" "$target" 2>/dev/null)"; then
log_message "ERROR" "jq failed trying to apply sync updates"
return 1
fi
if [ -z "$new_content" ]; then
log_message "ERROR" "jq produced no output for applying sync updates"
return 1
fi
# Compare with original content
local original_content
# Read file content without trailing newline for accurate comparison
original_content=$(<"$target")
if [ "$original_content" = "$new_content" ]; then
log_message "NO_CHANGE" "Sync settings: Already up-to-date"
return 0
fi
if [ "$DRY_RUN" = true ]; then
log_message "WILL_UPDATE" "Sync settings: Would apply updates."
return 0
fi
# Write the changes
printf "%s\n" "$new_content" >"$target"
log_message "UPDATED" "Settings Sync: Applied updates"
}
# Apply all predefined updates (default behavior)
apply_predefined_updates() {
log_message "INFO" "Using file => $TARGET_FILE"
log_message "INFO" "Applying all predefined 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[@]} -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
;;
"enable_settings_sync")
if [ "$pos_count" -gt 2 ]; then
log_message "ERROR" "enable_settings_sync takes at most one argument (target_file)"
usage
exit 1
fi
# If a second argument is provided, use it as the target file
target_file="${2:-}"
enable_settings_sync "$target_file"
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
@realdimas
Copy link
Author

Aw, thanks! That was perhaps my final bash monstrosity.
Ever since I discovered inline script dependency metadata in Python (PEP 723), that has been my go-to for this level of complexity :-D

@joeblackwaslike
Copy link

Amen, thank dog for pep 723, uv, and ruff. I've developed in python for over 12+ years, and package management and distribution was hell. Then things started to get slightly better with these million third party alternatives that all fell apart or got worse as time went on like npm libraries.

I was already a big fan of astral by way of ruff which replaced a dozen separate tools and did the job better. When I found out they released uv and it was inspired by rust's cargo I was all in.

I have a feeling you would really appreciate my latest python cookiecutter template, it uses uv and is batteries included with the best of everything, great for bootstrapping a new project. I'd love to get your feedback on it, any suggestions, or ideas. It's open for anyone to contribute and make iterative improvements!

Repo is here

For actual usage you can just do (no deps required) and you have a new project. I alias it.

uvx cookiecutter gh:joeblackwaslike/cookiecutter-uv

Note

I've started to fork it to specialize on things like data science/jupyter notebooks, see cookiecutter-ds. It's still in alpha so very new, I just found data science projects have very specific needs that the og didn't fit. I am eventually going to do one for CLI apps, API servers, and generative AI/LLM/Agent stuff. I just need more hours in the day.

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