mirror of
https://github.com/openai/codex.git
synced 2026-05-02 10:26:45 +00:00
Add a safe editor extension installer for Cursor and VS Code
This commit is contained in:
654
scripts/install/install-editor-extension.sh
Executable file
654
scripts/install/install-editor-extension.sh
Executable file
@@ -0,0 +1,654 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
EXTENSION_ID="openai.chatgpt"
|
||||
CURSOR_BIN="${CODEX_CURSOR_BIN:-}"
|
||||
CODE_BIN="${CODEX_CODE_BIN:-}"
|
||||
CURSOR_DB="${CODEX_CURSOR_DB:-}"
|
||||
BACKUP_DIR="${CODEX_CURSOR_BACKUP_DIR:-}"
|
||||
PYTHON_BIN="${CODEX_PYTHON_BIN:-}"
|
||||
|
||||
HAD_ERROR=0
|
||||
UPDATED_CURSOR_STATE=0
|
||||
|
||||
step() {
|
||||
printf '==> %s\n' "$1"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
default_cursor_db_path() {
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
printf '%s\n' "${HOME}/Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
||||
;;
|
||||
Linux)
|
||||
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
|
||||
printf '%s\n' "${XDG_CONFIG_HOME}/Cursor/User/globalStorage/state.vscdb"
|
||||
else
|
||||
printf '%s\n' "${HOME}/.config/Cursor/User/globalStorage/state.vscdb"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_editor_bin() {
|
||||
local explicit_path="$1"
|
||||
local cli_name="$2"
|
||||
shift 2
|
||||
|
||||
if [ -n "$explicit_path" ]; then
|
||||
[ -x "$explicit_path" ] || die "Editor CLI is not executable: $explicit_path"
|
||||
printf '%s\n' "$explicit_path"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v "$cli_name" >/dev/null 2>&1; then
|
||||
command -v "$cli_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local candidate_path
|
||||
for candidate_path in "$@"; do
|
||||
if [ -n "$candidate_path" ] && [ -x "$candidate_path" ]; then
|
||||
printf '%s\n' "$candidate_path"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
find_python_bin() {
|
||||
local explicit_path="$1"
|
||||
shift
|
||||
|
||||
if [ -n "$explicit_path" ]; then
|
||||
[ -x "$explicit_path" ] || die "Python is not executable: $explicit_path"
|
||||
printf '%s\n' "$explicit_path"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
command -v python3
|
||||
return
|
||||
fi
|
||||
|
||||
local candidate_path
|
||||
for candidate_path in "$@"; do
|
||||
if [ -n "$candidate_path" ] && [ -x "$candidate_path" ]; then
|
||||
printf '%s\n' "$candidate_path"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
python_can_update_cursor_state() {
|
||||
local python_bin="$1"
|
||||
"$python_bin" -c 'import json, sqlite3' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_extension() {
|
||||
local editor_name="$1"
|
||||
local editor_bin="$2"
|
||||
|
||||
step "Installing ${EXTENSION_ID} into ${editor_name}"
|
||||
"$editor_bin" --install-extension "$EXTENSION_ID" --force
|
||||
}
|
||||
|
||||
process_running() {
|
||||
local process_name="$1"
|
||||
pgrep -ix "$process_name" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
darwin_app_running() {
|
||||
local app_name="$1"
|
||||
[ "$(uname -s)" = "Darwin" ] || return 1
|
||||
command -v osascript >/dev/null 2>&1 || return 1
|
||||
[ "$(osascript -e "application \"${app_name}\" is running" 2>/dev/null || printf 'false')" = "true" ]
|
||||
}
|
||||
|
||||
wait_for_shutdown() {
|
||||
local app_name="$1"
|
||||
shift
|
||||
local process_names=("$@")
|
||||
local timeout_seconds=20
|
||||
local second=0
|
||||
|
||||
while [ "$second" -lt "$timeout_seconds" ]; do
|
||||
local app_stopped=1
|
||||
local processes_stopped=1
|
||||
local process_name
|
||||
|
||||
if [ -n "$app_name" ] && darwin_app_running "$app_name"; then
|
||||
app_stopped=0
|
||||
fi
|
||||
|
||||
for process_name in "${process_names[@]}"; do
|
||||
if process_running "$process_name"; then
|
||||
processes_stopped=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$app_stopped" -eq 1 ] && [ "$processes_stopped" -eq 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
second=$((second + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
quit_app_if_running() {
|
||||
local editor_name="$1"
|
||||
shift
|
||||
|
||||
local app_name=""
|
||||
local process_names=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--app-name)
|
||||
app_name="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
process_names+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local was_running=0
|
||||
local process_name
|
||||
if [ -n "$app_name" ] && darwin_app_running "$app_name"; then
|
||||
was_running=1
|
||||
else
|
||||
for process_name in "${process_names[@]}"; do
|
||||
if process_running "$process_name"; then
|
||||
was_running=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$was_running" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
step "Closing ${editor_name}"
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ] && [ -n "$app_name" ] && command -v osascript >/dev/null 2>&1; then
|
||||
osascript -e "tell application \"${app_name}\" to quit" >/dev/null 2>&1 || true
|
||||
else
|
||||
printf 'Cannot safely close %s automatically on this platform; please quit it and rerun\n' "$editor_name" >&2
|
||||
HAD_ERROR=1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! wait_for_shutdown "$app_name" "${process_names[@]}"; then
|
||||
printf 'Failed to close %s safely: %s is still running\n' "$editor_name" "$app_name" >&2
|
||||
HAD_ERROR=1
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
backup_cursor_db() {
|
||||
if [ ! -f "$CURSOR_DB" ]; then
|
||||
printf '\n'
|
||||
return
|
||||
fi
|
||||
|
||||
local backup_root
|
||||
if [ -n "$BACKUP_DIR" ]; then
|
||||
backup_root="$BACKUP_DIR"
|
||||
else
|
||||
backup_root="$(dirname "$CURSOR_DB")"
|
||||
fi
|
||||
|
||||
mkdir -p "$backup_root"
|
||||
find "$backup_root" -maxdepth 1 -type f -name 'state.vscdb.backup.*' -delete >/dev/null 2>&1 || true
|
||||
local backup_path="${backup_root}/state.vscdb.backup.$(date +%Y%m%d%H%M%S)"
|
||||
cp "$CURSOR_DB" "$backup_path"
|
||||
printf '%s\n' "$backup_path"
|
||||
}
|
||||
|
||||
update_cursor_state() {
|
||||
local backup_path="$1"
|
||||
|
||||
step "Updating Cursor state in ${CURSOR_DB}"
|
||||
if ! "$PYTHON_BIN" - "$CURSOR_DB" <<'PY'
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path(sys.argv[1])
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
PIN_KEY = "sidebar2.sidebarData.memoized.v1"
|
||||
DEFAULT_HIDDEN_KEY = "workbench.view.extension.codexViewContainer.state.hidden"
|
||||
APP_KEY = "src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser"
|
||||
VIEWS_CUSTOMIZATIONS_KEY = "views.customizations"
|
||||
AUXILIARYBAR_PINNED_PANELS_KEY = "workbench.auxiliarybar.pinnedPanels"
|
||||
AUXILIARYBAR_PLACEHOLDER_PANELS_KEY = "workbench.auxiliarybar.placeholderPanels"
|
||||
|
||||
CONTAINER_ID = "workbench.view.extension.codexViewContainer"
|
||||
VIEW_ID = "chatgpt.sidebarView"
|
||||
EXTENSION_ID = "openai.chatgpt"
|
||||
AUXILIARYBAR_PREFIX = "workbench.views.service.auxiliarybar."
|
||||
|
||||
|
||||
def read_json(cur, key, default):
|
||||
row = cur.execute("SELECT value FROM ItemTable WHERE key = ?", (key,)).fetchone()
|
||||
if row is None or row[0] in (None, ""):
|
||||
return default
|
||||
return json.loads(row[0])
|
||||
|
||||
|
||||
def write_json(cur, key, value):
|
||||
payload = json.dumps(value, separators=(",", ":"))
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO ItemTable(key, value) VALUES(?, ?)",
|
||||
(key, payload),
|
||||
)
|
||||
|
||||
|
||||
def delete_key(cur, key):
|
||||
cur.execute("DELETE FROM ItemTable WHERE key = ?", (key,))
|
||||
|
||||
|
||||
def write_json_to_cursor_disk_kv(cur, key, value):
|
||||
payload = json.dumps(value, separators=(",", ":"))
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO cursorDiskKV(key, value) VALUES(?, ?)",
|
||||
(key, payload),
|
||||
)
|
||||
|
||||
|
||||
def candidate_icon_path():
|
||||
extensions_dir = Path.home() / ".cursor" / "extensions"
|
||||
if not extensions_dir.exists():
|
||||
return None
|
||||
|
||||
matches = sorted(
|
||||
extensions_dir.glob(f"{EXTENSION_ID}-*/resources/blossom-white.svg"),
|
||||
key=lambda path: path.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
if not matches:
|
||||
return None
|
||||
return str(matches[0])
|
||||
|
||||
|
||||
def is_auxiliarybar_id(value):
|
||||
return bool(value) and value.startswith(AUXILIARYBAR_PREFIX)
|
||||
|
||||
|
||||
def generated_auxiliarybar_id():
|
||||
stable_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, EXTENSION_ID)
|
||||
return f"{AUXILIARYBAR_PREFIX}{stable_uuid}"
|
||||
|
||||
|
||||
def panel_matches_codex(panel):
|
||||
panel_id = panel.get("id")
|
||||
if not is_auxiliarybar_id(panel_id):
|
||||
return False
|
||||
|
||||
if panel.get("name") == "Codex":
|
||||
return True
|
||||
|
||||
icon_url = panel.get("iconUrl") or {}
|
||||
icon_path = icon_url.get("path", "")
|
||||
if f"/{EXTENSION_ID}-" in icon_path:
|
||||
return True
|
||||
|
||||
for view in panel.get("views", []):
|
||||
if view.get("when") == "chatgpt.doesNotSupportSecondarySidebar":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"
|
||||
)
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS cursorDiskKV (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"
|
||||
)
|
||||
|
||||
sidebar_data = read_json(
|
||||
cur,
|
||||
PIN_KEY,
|
||||
{"pinnedViewContainerIDs": [], "viewContainerOrders": {}},
|
||||
)
|
||||
pinned_ids = sidebar_data.setdefault("pinnedViewContainerIDs", [])
|
||||
sidebar_data["pinnedViewContainerIDs"] = [
|
||||
item for item in pinned_ids if item != CONTAINER_ID
|
||||
]
|
||||
write_json(cur, PIN_KEY, sidebar_data)
|
||||
|
||||
default_hidden_data = read_json(cur, DEFAULT_HIDDEN_KEY, [])
|
||||
updated = False
|
||||
for item in default_hidden_data:
|
||||
if item.get("id") == VIEW_ID:
|
||||
item["isHidden"] = False
|
||||
updated = True
|
||||
break
|
||||
if not updated:
|
||||
default_hidden_data.append({"id": VIEW_ID, "isHidden": False})
|
||||
write_json(cur, DEFAULT_HIDDEN_KEY, default_hidden_data)
|
||||
|
||||
placeholder_panels = read_json(cur, AUXILIARYBAR_PLACEHOLDER_PANELS_KEY, [])
|
||||
pinned_panels = read_json(cur, AUXILIARYBAR_PINNED_PANELS_KEY, [])
|
||||
views_customizations = read_json(
|
||||
cur,
|
||||
VIEWS_CUSTOMIZATIONS_KEY,
|
||||
{
|
||||
"viewContainerLocations": {},
|
||||
"viewLocations": {},
|
||||
"viewContainerBadgeEnablementStates": {},
|
||||
},
|
||||
)
|
||||
view_locations = views_customizations.setdefault("viewLocations", {})
|
||||
view_container_locations = views_customizations.setdefault(
|
||||
"viewContainerLocations", {}
|
||||
)
|
||||
existing_container_id = view_locations.get(VIEW_ID)
|
||||
|
||||
codex_container_ids = {
|
||||
panel.get("id")
|
||||
for panel in placeholder_panels
|
||||
if panel_matches_codex(panel)
|
||||
}
|
||||
if is_auxiliarybar_id(existing_container_id):
|
||||
codex_container_ids.add(existing_container_id)
|
||||
|
||||
target_container_id = None
|
||||
if is_auxiliarybar_id(existing_container_id):
|
||||
target_container_id = existing_container_id
|
||||
else:
|
||||
for panel in placeholder_panels:
|
||||
if panel_matches_codex(panel):
|
||||
target_container_id = panel.get("id")
|
||||
break
|
||||
|
||||
if not target_container_id:
|
||||
target_container_id = generated_auxiliarybar_id()
|
||||
codex_container_ids.add(target_container_id)
|
||||
|
||||
view_locations[VIEW_ID] = target_container_id
|
||||
view_container_locations[target_container_id] = 2
|
||||
|
||||
for container_id in list(codex_container_ids):
|
||||
if container_id != target_container_id and container_id not in view_locations.values():
|
||||
view_container_locations.pop(container_id, None)
|
||||
|
||||
write_json(cur, VIEWS_CUSTOMIZATIONS_KEY, views_customizations)
|
||||
|
||||
icon_path = candidate_icon_path()
|
||||
|
||||
target_placeholder_panel = None
|
||||
filtered_placeholder_panels = []
|
||||
for panel in placeholder_panels:
|
||||
panel_id = panel.get("id")
|
||||
if panel_id == target_container_id:
|
||||
target_placeholder_panel = panel
|
||||
continue
|
||||
if panel_id in codex_container_ids:
|
||||
continue
|
||||
filtered_placeholder_panels.append(panel)
|
||||
|
||||
if target_placeholder_panel is None:
|
||||
target_placeholder_panel = {
|
||||
"id": target_container_id,
|
||||
"name": "Codex",
|
||||
"isBuiltin": True,
|
||||
"views": [{"when": "chatgpt.doesNotSupportSecondarySidebar"}],
|
||||
}
|
||||
|
||||
target_placeholder_panel["id"] = target_container_id
|
||||
target_placeholder_panel["name"] = "Codex"
|
||||
target_placeholder_panel["isBuiltin"] = True
|
||||
target_placeholder_panel["views"] = [{"when": "chatgpt.doesNotSupportSecondarySidebar"}]
|
||||
if icon_path is not None:
|
||||
target_placeholder_panel["iconUrl"] = {
|
||||
"$mid": 1,
|
||||
"path": icon_path,
|
||||
"scheme": "file",
|
||||
}
|
||||
|
||||
write_json(
|
||||
cur,
|
||||
AUXILIARYBAR_PLACEHOLDER_PANELS_KEY,
|
||||
[target_placeholder_panel, *filtered_placeholder_panels],
|
||||
)
|
||||
|
||||
target_pinned_panel = None
|
||||
filtered_pinned_panels = []
|
||||
for panel in pinned_panels:
|
||||
panel_id = panel.get("id")
|
||||
if panel_id == target_container_id:
|
||||
target_pinned_panel = panel
|
||||
continue
|
||||
if panel_id in codex_container_ids:
|
||||
continue
|
||||
filtered_pinned_panels.append(panel)
|
||||
|
||||
if target_pinned_panel is None:
|
||||
target_pinned_panel = {"id": target_container_id}
|
||||
|
||||
target_pinned_panel["id"] = target_container_id
|
||||
target_pinned_panel["pinned"] = True
|
||||
target_pinned_panel["visible"] = False
|
||||
|
||||
write_json(
|
||||
cur,
|
||||
AUXILIARYBAR_PINNED_PANELS_KEY,
|
||||
[target_pinned_panel, *filtered_pinned_panels],
|
||||
)
|
||||
|
||||
write_json(
|
||||
cur,
|
||||
f"{target_container_id}.state.hidden",
|
||||
[{"id": VIEW_ID, "isHidden": False}],
|
||||
)
|
||||
|
||||
for container_id in codex_container_ids:
|
||||
if container_id != target_container_id:
|
||||
delete_key(cur, f"{container_id}.state.hidden")
|
||||
|
||||
app_data = read_json(cur, APP_KEY, {})
|
||||
ai_settings = app_data.setdefault("aiSettings", {})
|
||||
model_config = ai_settings.setdefault("modelConfig", {})
|
||||
composer = model_config.setdefault("composer", {})
|
||||
composer["modelName"] = "gpt-5.4-medium"
|
||||
composer["maxMode"] = False
|
||||
write_json(cur, APP_KEY, app_data)
|
||||
|
||||
composer_rows = cur.execute(
|
||||
"SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'"
|
||||
).fetchall()
|
||||
for key, raw_value in composer_rows:
|
||||
if raw_value in (None, ""):
|
||||
continue
|
||||
|
||||
try:
|
||||
composer_data = json.loads(raw_value)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if not (
|
||||
composer_data.get("isAgentic") is True
|
||||
or composer_data.get("unifiedMode") == "agent"
|
||||
):
|
||||
continue
|
||||
|
||||
row_model_config = composer_data.setdefault("modelConfig", {})
|
||||
row_model_config["modelName"] = "gpt-5.4-medium"
|
||||
row_model_config["maxMode"] = False
|
||||
write_json_to_cursor_disk_kv(cur, key, composer_data)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
PY
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$backup_path" ]; then
|
||||
step "Cursor state backup saved to ${backup_path}"
|
||||
fi
|
||||
UPDATED_CURSOR_STATE=1
|
||||
}
|
||||
|
||||
apply_cursor_state_changes() {
|
||||
if [ -z "$CURSOR_DB" ]; then
|
||||
step "Skipping Cursor state changes: unsupported OS for automatic Cursor DB updates"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$CURSOR_DB")"
|
||||
|
||||
local backup_path=""
|
||||
backup_path="$(backup_cursor_db)"
|
||||
|
||||
if update_cursor_state "$backup_path"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$backup_path" ] && [ -f "$backup_path" ]; then
|
||||
printf 'Failed to update Cursor state; restoring backup from %s\n' "$backup_path" >&2
|
||||
cp "$backup_path" "$CURSOR_DB" >/dev/null 2>&1 || true
|
||||
else
|
||||
printf 'Failed to update Cursor state; removing incomplete database at %s\n' "$CURSOR_DB" >&2
|
||||
rm -f "$CURSOR_DB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
HAD_ERROR=1
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ "$#" -ne 0 ]; then
|
||||
die "This script takes no arguments."
|
||||
fi
|
||||
|
||||
if [ -z "$CURSOR_DB" ]; then
|
||||
CURSOR_DB="$(default_cursor_db_path || true)"
|
||||
fi
|
||||
|
||||
resolved_cursor_bin="$(find_editor_bin \
|
||||
"$CURSOR_BIN" \
|
||||
"cursor" \
|
||||
"/Applications/Cursor.app/Contents/Resources/app/bin/cursor" \
|
||||
"/opt/Cursor/resources/app/bin/cursor" \
|
||||
"/opt/cursor/resources/app/bin/cursor" \
|
||||
"/usr/bin/cursor" \
|
||||
|| true)"
|
||||
resolved_code_bin="$(find_editor_bin \
|
||||
"$CODE_BIN" \
|
||||
"code" \
|
||||
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \
|
||||
"/usr/bin/code" \
|
||||
"/snap/bin/code" \
|
||||
"/usr/share/code/bin/code" \
|
||||
"/opt/visual-studio-code/bin/code" \
|
||||
|| true)"
|
||||
resolved_python_bin="$(find_python_bin \
|
||||
"$PYTHON_BIN" \
|
||||
"/usr/bin/python3" \
|
||||
"/opt/homebrew/bin/python3" \
|
||||
"/usr/local/bin/python3" \
|
||||
|| true)"
|
||||
|
||||
install_cursor=0
|
||||
install_vscode=0
|
||||
cursor_ready=0
|
||||
vscode_ready=0
|
||||
|
||||
if [ -n "$resolved_cursor_bin" ]; then
|
||||
install_cursor=1
|
||||
else
|
||||
step "Skipping Cursor: editor is not installed"
|
||||
fi
|
||||
|
||||
if [ -n "$resolved_code_bin" ]; then
|
||||
install_vscode=1
|
||||
else
|
||||
step "Skipping VS Code: editor is not installed"
|
||||
fi
|
||||
|
||||
if [ "$install_cursor" -eq 0 ] && [ "$install_vscode" -eq 0 ]; then
|
||||
step "No supported editors were detected; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$install_cursor" -eq 1 ]; then
|
||||
if [ -n "$CURSOR_DB" ] && [ -z "$resolved_python_bin" ]; then
|
||||
printf 'Skipping Cursor: python3 is required to update Cursor state safely\n' >&2
|
||||
HAD_ERROR=1
|
||||
install_cursor=0
|
||||
elif [ -n "$CURSOR_DB" ] && ! python_can_update_cursor_state "$resolved_python_bin"; then
|
||||
printf 'Skipping Cursor: python3 cannot import the modules required to update Cursor state safely\n' >&2
|
||||
HAD_ERROR=1
|
||||
install_cursor=0
|
||||
else
|
||||
PYTHON_BIN="$resolved_python_bin"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$install_cursor" -eq 1 ]; then
|
||||
if quit_app_if_running "Cursor" --app-name "Cursor" Cursor cursor; then
|
||||
cursor_ready=1
|
||||
else
|
||||
step "Skipping Cursor: failed to close the running app safely"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$install_vscode" -eq 1 ]; then
|
||||
if quit_app_if_running "VS Code" --app-name "Visual Studio Code" "Visual Studio Code" Code code; then
|
||||
vscode_ready=1
|
||||
else
|
||||
step "Skipping VS Code: failed to close the running app safely"
|
||||
fi
|
||||
fi
|
||||
|
||||
cursor_installed=0
|
||||
if [ "$install_cursor" -eq 1 ] && [ "$cursor_ready" -eq 1 ]; then
|
||||
if install_extension "Cursor" "$resolved_cursor_bin"; then
|
||||
cursor_installed=1
|
||||
else
|
||||
printf 'Failed to install %s into Cursor\n' "$EXTENSION_ID" >&2
|
||||
HAD_ERROR=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$install_vscode" -eq 1 ] && [ "$vscode_ready" -eq 1 ]; then
|
||||
if ! install_extension "VS Code" "$resolved_code_bin"; then
|
||||
printf 'Failed to install %s into VS Code\n' "$EXTENSION_ID" >&2
|
||||
HAD_ERROR=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$cursor_installed" -eq 1 ]; then
|
||||
apply_cursor_state_changes || true
|
||||
fi
|
||||
|
||||
if [ "$UPDATED_CURSOR_STATE" -eq 1 ]; then
|
||||
step "Cursor state changes applied"
|
||||
fi
|
||||
|
||||
if [ "$HAD_ERROR" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "Done"
|
||||
Reference in New Issue
Block a user