Last active
April 13, 2025 08:35
-
-
Save realdimas/e58723564cfada8efd93adab6efb747c to your computer and use it in GitHub Desktop.
This file contains 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 | |
# --------------------------------------------------------------------------- | |
APP_PATH="/Applications/Cursor.app" | |
TARGET_FILE="${APP_PATH}/Contents/Resources/app/product.json" | |
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: $TARGET_FILE) | |
--dry-run, -n Show what would be changed, but do not actually write | |
--help, -h Show this help message | |
Commands: | |
check_and_resign [app_path] Check and re-sign the specified application if needed | |
(default: $APP_PATH) | |
remove_quarantine [app_path] Remove quarantine attributes from the specified application | |
(default: $APP_PATH) | |
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 (or a file you specify). | |
Examples: | |
# Perform the default updates on the standard file | |
${SCRIPT_NAME} | |
# Dry-run only (no changes written) | |
${SCRIPT_NAME} -n | |
# Print the current value | |
${SCRIPT_NAME} '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion' | |
# Update that same key | |
${SCRIPT_NAME} '.extensionMaxVersions."ms-vscode-remote.remote-containers".maxVersion' 0.399.0 | |
# Check and re-sign an application (using default path) | |
${SCRIPT_NAME} check_and_resign | |
# Check and re-sign a specific application | |
${SCRIPT_NAME} check_and_resign /Applications/Cursor.app | |
# Remove quarantine attribute (using default path) | |
${SCRIPT_NAME} remove_quarantine | |
# Remove quarantine attribute for a specific application | |
${SCRIPT_NAME} remove_quarantine /Applications/Cursor.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 predefined JSON updates | |
apply_predefined_updates() { | |
log_message "INFO" "Using file => $TARGET_FILE" | |
log_message "INFO" "Applying predefined JSON updates..." | |
# Switchover to MS VSCode Marketplace endpoints | |
# galleryId needs to be unchanged to avoid "Error while fetching extensions. Cannot read properties of undefined (reading 'identifier')" exception | |
# update_json_value "$TARGET_FILE" .extensionsGallery.galleryId cursor | |
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 | |
# Compatibility as of Cursor Version: 0.48.7 (VSCode Version: 1.96.2) 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.2025040401/vspackage?targetPlatform=darwin-arm64 | |
# VSIX download URL (linux-arm64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.5.2025040401/vspackage?targetPlatform=linux-arm64 | |
# VSIX download URL (linux-x64): https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.5.2025040401/vspackage?targetPlatform=linux-x64 | |
# Last working (via .vsix) version: 2025.5.2025040401 (current) | |
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.python".maxVersion' 2025.5.2025040401 | |
# | |
# 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.1/vspackage | |
# Last working (via .vsix) version: 2025.4.1 (current) | |
update_json_value "$TARGET_FILE" '.extensionMaxVersions."ms-python.vscode-pylance".maxVersion' 2025.4.1 | |
# | |
# 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.410.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 | |
# resign if needed (this would invalidate existing keychain access grants prompting for manual confirmation) | |
check_and_resign "${APP_PATH}" | |
# remove quarantine attribute if needed | |
remove_quarantine "${APP_PATH}" | |
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 | |
# 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 | |
"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 default | |
APP_PATH_ARG="${2:-$APP_PATH}" | |
check_and_resign "$APP_PATH_ARG" | |
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 default | |
APP_PATH_ARG="${2:-$APP_PATH}" | |
remove_quarantine "$APP_PATH_ARG" | |
exit 0 | |
;; | |
*) | |
# Handle as JSON operations | |
if [ "$pos_count" -eq 1 ]; then | |
# "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 | |
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