Files
codex/scripts/install/install-editor-extension.sh

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"