mirror of
https://github.com/openai/codex.git
synced 2026-05-01 18:06:47 +00:00
655 lines
16 KiB
Bash
Executable File
655 lines
16 KiB
Bash
Executable File
#!/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"
|