mirror of
https://github.com/jimeh/.vscode.d.git
synced 2026-02-19 11:26:39 +00:00
This update reorganizes the command parsing logic for better clarity and maintains backward compatibility. The help output has been enhanced to provide clearer usage instructions, including support for specifying the editor in either argument position. Additionally, error handling has been improved for missing editor specifications and command requirements.
1509 lines
43 KiB
Bash
Executable File
1509 lines
43 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Ensure that any command in a pipeline that fails will cause the entire
|
|
# pipeline to fail. This is critical for API calls piped to jq.
|
|
set -o pipefail
|
|
# Require Bash 4+ (associative arrays used)
|
|
if [[ -z "${BASH_VERSINFO:-}" || "${BASH_VERSINFO[0]}" -lt 4 ]]; then
|
|
echo "ERROR: Bash 4+ required. On macOS: brew install bash and ensure it is first in PATH." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ==============================================================================
|
|
# Settings
|
|
# ==============================================================================
|
|
|
|
# Define base globals.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SETUP_EDITOR=""
|
|
declare -A STATIC_SYMLINKS=()
|
|
|
|
# When true, try downloading extensions from OpenVSX first, with a fallback to
|
|
# VS Marketplace. This is forced to true for the Kiro editor.
|
|
PREFER_OPENVSX="${PREFER_OPENVSX:-false}"
|
|
|
|
# Global cache to track where version information was found
|
|
# Key: extension name, Value: "openvsx" or "marketplace"
|
|
declare -A VERSION_SOURCE_CACHE=()
|
|
|
|
# Define settings as a function, allowing them to run after all other functions
|
|
# have been defined, using them as needed.
|
|
define_settings() {
|
|
# List of config files to symlink from current directory.
|
|
CONFIG_SOURCES=(
|
|
"keybindings.json"
|
|
"mcp.json"
|
|
"settings.json"
|
|
"snippets"
|
|
)
|
|
|
|
# Additional static symlinks to create (source => target).
|
|
STATIC_SYMLINKS["claude/CLAUDE.md"]="${HOME}/.claude/CLAUDE.md"
|
|
STATIC_SYMLINKS["cspell/vscode-user-dictionary.txt"]="${HOME}/.cspell/vscode-user-dictionary.txt"
|
|
STATIC_SYMLINKS["harper-ls/dictionary.txt"]="$(harper_config_dir)/dictionary.txt"
|
|
STATIC_SYMLINKS["harper-ls/file_dictionaries"]="$(harper_config_dir)/file_dictionaries"
|
|
STATIC_SYMLINKS["harper-ls/ignored_lints"]="$(harper_config_dir)/ignored_lints"
|
|
|
|
# Conditionally add `mcp.json` for Cursor.
|
|
if [[ "${SETUP_EDITOR}" == "cursor" ]]; then
|
|
STATIC_SYMLINKS["cursor/mcp.json"]="${HOME}/.cursor/mcp.json"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================
|
|
# Help
|
|
# ==============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Usage:
|
|
$(basename "$0") EDITOR COMMAND [OPTIONS]
|
|
$(basename "$0") COMMAND EDITOR [OPTIONS]
|
|
$(basename "$0") config
|
|
$(basename "$0") shared-extensions [--json] [EDITORS...]
|
|
|
|
Editors:
|
|
cursor, c Cursor editor
|
|
kiro, k Kiro editor (uses OpenVSX by default)
|
|
vscode, code, vsc, v Visual Studio Code
|
|
vscode-insiders, vsci, i Visual Studio Code Insiders
|
|
windsurf, surf, w Windsurf editor
|
|
|
|
Commands:
|
|
config, conf Create symlinks.
|
|
With editor: full editor + static config.
|
|
Without editor: static-only symlinks.
|
|
dump-extensions, dump Export installed extensions to lock file
|
|
for the specified editor.
|
|
extensions, ext Install extensions from lock file for the
|
|
specified editor.
|
|
install Install a specific extension id for the
|
|
specified editor (e.g. ms-python.python).
|
|
shared-extensions, shared
|
|
Print extensions present in all specified editors.
|
|
Defaults: cursor, vscode. Use --json for JSON.
|
|
|
|
Options:
|
|
--latest With 'extensions': install latest versions instead
|
|
of exact lockfile versions. With 'install': install
|
|
the latest version of the specified extension.
|
|
--force-latest Force latest behavior where applicable.
|
|
|
|
Notes:
|
|
- For 'dump', 'extensions', and 'install', the editor may be given as
|
|
arg 1 or arg 2; both orders are supported.
|
|
- Kiro prefers OpenVSX, falling back to VS Marketplace.
|
|
EOF
|
|
}
|
|
|
|
# ==============================================================================
|
|
# Functions
|
|
# ==============================================================================
|
|
|
|
info() {
|
|
echo "$@" >&2
|
|
}
|
|
|
|
debug() {
|
|
[[ -n "${DEBUG}" ]] && echo "DEBUG: $*" >&2
|
|
}
|
|
|
|
warn() {
|
|
echo "WARN: $*" >&2
|
|
}
|
|
|
|
error() {
|
|
echo "ERROR: $*" >&2
|
|
}
|
|
|
|
fatal() {
|
|
error "$@" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Check for required dependencies.
|
|
check_dependencies() {
|
|
if ! command -v jq > /dev/null 2>&1; then
|
|
fatal "jq is not installed. Please install it to continue."
|
|
fi
|
|
|
|
if ! command -v curl > /dev/null 2>&1; then
|
|
fatal "curl is not installed. Please install it to continue."
|
|
fi
|
|
}
|
|
|
|
# Get extensions lockfile path for current editor.
|
|
#
|
|
# Returns: Path to extensions lock file via `STDOUT`.
|
|
get_extensions_lock() {
|
|
echo "${SCRIPT_DIR}/extensions.${SETUP_EDITOR}.lock"
|
|
}
|
|
|
|
# Get extensions lockfile path for a specific editor name.
|
|
# Does not rely on SETUP_EDITOR.
|
|
extensions_lock_for_editor() {
|
|
local editor_name="$1"
|
|
echo "${SCRIPT_DIR}/extensions.${editor_name}.lock"
|
|
}
|
|
|
|
# Normalize editor name/synonyms to canonical editor id.
|
|
#
|
|
# Returns: canonical id via `STDOUT` or empty string if unknown.
|
|
normalize_editor_name() {
|
|
local name
|
|
name="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
|
case "${name}" in
|
|
cursor | c)
|
|
echo "cursor"
|
|
;;
|
|
vscode | code | vsc | v)
|
|
echo "vscode"
|
|
;;
|
|
vscode-insiders | code-insiders | insiders | vsci | i)
|
|
echo "vscode-insiders"
|
|
;;
|
|
windsurf | wind | surf | w)
|
|
echo "windsurf"
|
|
;;
|
|
kiro | k)
|
|
echo "kiro"
|
|
;;
|
|
*)
|
|
echo ""
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Require an editor to be specified for a command.
|
|
#
|
|
# Returns: If no editor is specified, exits program with error.
|
|
require_editor() {
|
|
local command="$1"
|
|
|
|
if [[ -z "${SETUP_EDITOR}" ]]; then
|
|
error "No editor specified for command '${command}'"
|
|
echo >&2
|
|
show_help
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Read a lockfile and output sorted unique extension ids without versions.
|
|
extensions_ids_from_lock() {
|
|
local lock_path="$1"
|
|
grep -Ev '^\s*(#|$)' "${lock_path}" |
|
|
cut -d@ -f1 |
|
|
sort -u
|
|
}
|
|
|
|
# Determine current platform for OpenVSX downloads.
|
|
#
|
|
# Returns: Platform string compatible with OpenVSX via `STDOUT`.
|
|
get_current_platform() {
|
|
local os_name
|
|
local arch_name
|
|
local platform=""
|
|
|
|
# Detect OS
|
|
case "$(uname -s)" in
|
|
"Darwin")
|
|
os_name="darwin"
|
|
;;
|
|
"Linux")
|
|
# Check if running on Alpine Linux
|
|
if [[ -f /etc/alpine-release ]]; then
|
|
os_name="alpine"
|
|
else
|
|
os_name="linux"
|
|
fi
|
|
;;
|
|
"CYGWIN"* | "MINGW"* | "MSYS"*)
|
|
os_name="win32"
|
|
;;
|
|
*)
|
|
fatal "Unsupported platform: $(uname -s)"
|
|
;;
|
|
esac
|
|
|
|
# Detect architecture
|
|
case "$(uname -m)" in
|
|
"x86_64" | "amd64")
|
|
arch_name="x64"
|
|
;;
|
|
"arm64" | "aarch64")
|
|
arch_name="arm64"
|
|
;;
|
|
"armv7l" | "armv6l")
|
|
arch_name="armhf"
|
|
;;
|
|
"i386" | "i686")
|
|
arch_name="x86"
|
|
;;
|
|
*)
|
|
fatal "Unsupported architecture: $(uname -m)"
|
|
;;
|
|
esac
|
|
|
|
platform="${os_name}-${arch_name}"
|
|
echo "${platform}"
|
|
}
|
|
|
|
# Determine editor config directory.
|
|
#
|
|
# Returns: Editor config directory path via `STDOUT`.
|
|
editor_config_dir() {
|
|
case "$(uname -s)" in
|
|
"Darwin")
|
|
case "${SETUP_EDITOR}" in
|
|
"cursor")
|
|
echo "${HOME}/Library/Application Support/Cursor/User"
|
|
;;
|
|
"vscode")
|
|
echo "${HOME}/Library/Application Support/Code/User"
|
|
;;
|
|
"vscode-insiders")
|
|
echo "${HOME}/Library/Application Support/Code - Insiders/User"
|
|
;;
|
|
"windsurf")
|
|
echo "${HOME}/Library/Application Support/Windsurf/User"
|
|
;;
|
|
"kiro")
|
|
echo "${HOME}/Library/Application Support/Kiro/User"
|
|
;;
|
|
*)
|
|
fatal "Invalid editor '${SETUP_EDITOR}' for macOS"
|
|
;;
|
|
esac
|
|
;;
|
|
"Linux")
|
|
case "${SETUP_EDITOR}" in
|
|
"cursor")
|
|
echo "${HOME}/.config/Cursor/User"
|
|
;;
|
|
"vscode")
|
|
echo "${HOME}/.config/Code/User"
|
|
;;
|
|
"vscode-insiders")
|
|
echo "${HOME}/.config/Code - Insiders/User"
|
|
;;
|
|
"windsurf")
|
|
echo "${HOME}/.config/Windsurf/User"
|
|
;;
|
|
"kiro")
|
|
echo "${HOME}/.config/Kiro/User"
|
|
;;
|
|
*)
|
|
fatal "Invalid editor '${SETUP_EDITOR}' for Linux"
|
|
;;
|
|
esac
|
|
;;
|
|
*)
|
|
fatal "Unsupported operating system"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Determine harper-ls config directory.
|
|
#
|
|
# Returns: Harper-ls config directory path via `STDOUT`.
|
|
harper_config_dir() {
|
|
case "$(uname -s)" in
|
|
"Darwin")
|
|
echo "${HOME}/Library/Application Support/harper-ls"
|
|
;;
|
|
"Linux")
|
|
if [[ -n "${XDG_CONFIG_HOME}" ]]; then
|
|
echo "${XDG_CONFIG_HOME}/harper-ls"
|
|
else
|
|
echo "${HOME}/.config/harper-ls"
|
|
fi
|
|
;;
|
|
*)
|
|
fatal "Unsupported operating system"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Cross-platform function to resolve symlinks.
|
|
#
|
|
# Returns: Resolved symlink path via `STDOUT`.
|
|
resolve_symlink() {
|
|
local path="$1"
|
|
if command -v realpath > /dev/null 2>&1; then
|
|
realpath "$path"
|
|
elif [[ "$(uname -s)" == "Darwin" ]]; then
|
|
if command -v python > /dev/null 2>&1; then
|
|
python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$path"
|
|
elif command -v python3 > /dev/null 2>&1; then
|
|
python3 -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$path"
|
|
elif command -v perl > /dev/null 2>&1; then
|
|
perl -MCwd=realpath -e 'print realpath(shift)' -- "$path"
|
|
else
|
|
readlink "$path"
|
|
fi
|
|
else
|
|
readlink -f "$path"
|
|
fi
|
|
}
|
|
|
|
# Strip ANSI escape sequences from stdin and write clean text to stdout.
|
|
# This removes CSI sequences like ESC[...A/K that UIs use for status lines.
|
|
strip_ansi_sequences() {
|
|
awk '{ gsub(/\033\[[0-9;?]*[ -\/]*[@-~]/, ""); print }'
|
|
}
|
|
|
|
# Backup and symlink.
|
|
backup_and_link() {
|
|
local source="$1"
|
|
local target="$2"
|
|
local real_target
|
|
local real_source
|
|
|
|
# Create target directory if it doesn't exist.
|
|
local target_dir
|
|
target_dir="$(dirname "${target}")"
|
|
mkdir -p "${target_dir}"
|
|
|
|
# Check if target already exists.
|
|
if [[ -e "${target}" ]]; then
|
|
# If it's a symlink, check if it points to the same location.
|
|
if [[ -L "${target}" ]]; then
|
|
real_target="$(resolve_symlink "$target")"
|
|
real_source="$(resolve_symlink "$source")"
|
|
if [[ "${real_target}" == "${real_source}" ]]; then
|
|
info "Skipping ${target} - already linked to ${source}"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
info "Backing up existing ${target} to ${target}.bak"
|
|
mv "${target}" "${target}.bak"
|
|
fi
|
|
|
|
# Create symlink.
|
|
info "Creating symlink for ${source} to ${target}"
|
|
ln -s "${source}" "${target}"
|
|
}
|
|
|
|
# Private function: Create editor-specific symlinks.
|
|
symlink_editor_config() {
|
|
# Create editor config directory if it doesn't exist.
|
|
local config_dir
|
|
config_dir="$(editor_config_dir)"
|
|
|
|
mkdir -p "${config_dir}"
|
|
local path
|
|
for path in "${CONFIG_SOURCES[@]}"; do
|
|
backup_and_link "${SCRIPT_DIR}/${path}" "${config_dir}/${path}"
|
|
done
|
|
}
|
|
|
|
# Private function: Create static symlinks.
|
|
symlink_static_config() {
|
|
local source
|
|
local target
|
|
|
|
# Create static symlinks to custom locations.
|
|
for source in "${!STATIC_SYMLINKS[@]}"; do
|
|
target="${STATIC_SYMLINKS[${source}]}"
|
|
backup_and_link "${SCRIPT_DIR}/${source}" "${target}"
|
|
done
|
|
}
|
|
|
|
# Find the editor CLI command.
|
|
#
|
|
# Returns: Editor command path via `STDOUT`.
|
|
# Helper to add editor paths for both command names and full paths.
|
|
_add_editor_paths() {
|
|
local editor_name="$1"
|
|
local command_name="$2"
|
|
local app_name="$3"
|
|
local paths=("${command_name}")
|
|
|
|
if [[ "$(uname -s)" == "Darwin" ]] && [[ -n "${app_name}" ]]; then
|
|
local app_locations=(
|
|
"/Applications"
|
|
"${HOME}/Applications"
|
|
"/System/Applications"
|
|
)
|
|
local loc
|
|
for loc in "${app_locations[@]}"; do
|
|
if [[ -n "${loc}" ]] && [[ -d "${loc}/${app_name}" ]]; then
|
|
paths+=("${loc}/${app_name}/Contents/Resources/app/bin/${command_name}")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
editor_paths["${editor_name}"]="${paths[*]}"
|
|
}
|
|
|
|
# Find the editor CLI command.
|
|
#
|
|
# Returns: Editor command path via `STDOUT`.
|
|
find_editor_cmd() {
|
|
local editor_cmd=""
|
|
local possible_commands=()
|
|
local -A editor_paths
|
|
|
|
# Define editor command names and their possible locations.
|
|
_add_editor_paths "cursor" "cursor" "Cursor.app"
|
|
_add_editor_paths "vscode" "code" "Visual Studio Code.app"
|
|
_add_editor_paths "vscode-insiders" "code-insiders" "Visual Studio Code - Insiders.app"
|
|
_add_editor_paths "windsurf" "windsurf" "Windsurf.app"
|
|
_add_editor_paths "kiro" "kiro" "Kiro.app"
|
|
|
|
if [[ -z "${editor_paths[${SETUP_EDITOR}]}" ]]; then
|
|
fatal "Invalid editor '${SETUP_EDITOR}'"
|
|
fi
|
|
|
|
# Convert string to array of possible commands/paths.
|
|
read -r -a possible_commands <<< "${editor_paths[${SETUP_EDITOR}]}"
|
|
|
|
# Check for the command in all possible locations.
|
|
local cmd
|
|
for cmd in "${possible_commands[@]}"; do
|
|
if command -v "${cmd}" > /dev/null 2>&1; then
|
|
editor_cmd="${cmd}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "${editor_cmd}" ]]; then
|
|
fatal "${SETUP_EDITOR} command not found"
|
|
fi
|
|
|
|
echo "${editor_cmd}"
|
|
}
|
|
|
|
# Validate extension line format.
|
|
validate_extension_line() {
|
|
local line="$1"
|
|
local extension=""
|
|
local version=""
|
|
|
|
# Check for exactly one `@` symbol.
|
|
local at_count
|
|
at_count=$(echo "${line}" | grep -o "@" | wc -l)
|
|
if [[ ${at_count} -ne 1 ]]; then
|
|
warn "Invalid format '${line}' - must contain exactly one '@'"
|
|
return 1
|
|
fi
|
|
|
|
# Extract extension and version parts.
|
|
extension="${line%@*}"
|
|
version="${line#*@}"
|
|
|
|
# Validate extension part (should be `<publisher>.<extension>`).
|
|
if [[ ! "${extension}" =~ ^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$ ]]; then
|
|
warn "Invalid extension format '${extension}' - must be" \
|
|
"'publisher.extension'"
|
|
return 1
|
|
fi
|
|
|
|
# Validate version is not empty and contains valid characters.
|
|
if [[ -z "${version}" ]]; then
|
|
warn "Empty version for extension '${extension}'"
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! "${version}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
|
warn "Invalid version format '${version}' for extension" \
|
|
"'${extension}'"
|
|
return 1
|
|
fi
|
|
|
|
# Check for leading/trailing whitespace.
|
|
if [[ "${line}" != "${line// /}" ]]; then
|
|
warn "Extension line contains spaces: '${line}'"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Global variable to cache installed extensions.
|
|
_INSTALLED_EXTENSIONS=""
|
|
|
|
# Get installed extensions with versions, using cache if available.
|
|
#
|
|
# Returns: List of installed extensions with versions via `STDOUT`.
|
|
installed_extensions() {
|
|
local editor_cmd="$1"
|
|
|
|
# Populate the cache if it's not already populated.
|
|
if [[ -z "${_INSTALLED_EXTENSIONS}" ]]; then
|
|
_INSTALLED_EXTENSIONS="$(
|
|
"${editor_cmd}" --list-extensions --show-versions 2> /dev/null
|
|
)"
|
|
fi
|
|
|
|
echo "${_INSTALLED_EXTENSIONS}"
|
|
}
|
|
|
|
# Get the currently installed version of an extension.
|
|
#
|
|
# Returns: Version string of installed extension via `STDOUT` (empty if not installed).
|
|
get_installed_version() {
|
|
local editor_cmd="$1"
|
|
local extension="$2"
|
|
local extension_pattern
|
|
extension_pattern="$(printf '%s' "${extension}" | sed 's/[[\.*^()$+?{|]/\\&/g')"
|
|
|
|
# Extract version from cached list.
|
|
installed_extensions "${editor_cmd}" |
|
|
grep "^${extension_pattern}@" |
|
|
sed 's/^[^@]*@//'
|
|
}
|
|
|
|
# Query latest version of an extension.
|
|
#
|
|
# Returns: Latest version string via `STDOUT` on success.
|
|
query_latest_version() {
|
|
local extension="$1"
|
|
local extension_source="marketplace"
|
|
debug "Querying latest version of ${extension}"
|
|
|
|
if [[ "${PREFER_OPENVSX}" == "true" ]]; then
|
|
extension_source="openvsx"
|
|
fi
|
|
|
|
case "${extension_source}" in
|
|
"openvsx")
|
|
query_openvsx_latest_version "${extension}" ||
|
|
query_marketplace_latest_version "${extension}" ||
|
|
return 1
|
|
;;
|
|
"marketplace")
|
|
query_marketplace_latest_version "${extension}" ||
|
|
query_openvsx_latest_version "${extension}" ||
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Query extension metadata from OpenVSX registry.
|
|
#
|
|
# Returns: JSON metadata via `STDOUT` on success.
|
|
query_openvsx_metadata() {
|
|
local extension="$1"
|
|
local version="$2"
|
|
local publisher_id="${extension%%.*}"
|
|
local extension_id="${extension#*.}"
|
|
local openvsx_api_url=""
|
|
|
|
if [[ "${version}" == "latest" ]]; then
|
|
openvsx_api_url="https://open-vsx.org/api/${publisher_id}/${extension_id}"
|
|
else
|
|
openvsx_api_url="https://open-vsx.org/api/${publisher_id}/${extension_id}/${version}"
|
|
fi
|
|
|
|
# Query OpenVSX API and return full JSON
|
|
debug "Querying OpenVSX API for ${extension}@${version}: ${openvsx_api_url}"
|
|
if curl --silent --compressed --fail "${openvsx_api_url}" 2> /dev/null; then
|
|
return 0
|
|
else
|
|
debug "OpenVSX API request failed for ${extension}@${version}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Query latest version from OpenVSX registry.
|
|
#
|
|
# Returns: Latest version string via `STDOUT` on success.
|
|
query_openvsx_latest_version() {
|
|
local extension="$1"
|
|
|
|
# Query OpenVSX metadata and extract latest version
|
|
if query_openvsx_metadata "${extension}" "latest" |
|
|
jq -r '.version // empty' 2> /dev/null; then
|
|
# Cache the successful source
|
|
VERSION_SOURCE_CACHE["${extension}"]="openvsx"
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Query extension metadata from VS Marketplace.
|
|
#
|
|
# Returns: JSON metadata via `STDOUT` on success.
|
|
query_marketplace_metadata() {
|
|
local extension="$1"
|
|
local metadata_url="https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"
|
|
|
|
# Use jq to properly construct JSON
|
|
local request_data
|
|
request_data=$(jq -n --arg ext "$extension" '{
|
|
filters: [{
|
|
criteria: [{ filterType: 7, value: $ext }]
|
|
}],
|
|
flags: 2
|
|
}')
|
|
|
|
# Query the marketplace and return full JSON
|
|
debug "Querying VS Marketplace API for ${extension}: ${metadata_url}"
|
|
if curl --silent --compressed --fail -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: application/json; api-version=7.2-preview.1" \
|
|
-d "${request_data}" "${metadata_url}" 2> /dev/null; then
|
|
return 0
|
|
else
|
|
debug "VS Marketplace API request failed for ${extension}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Query latest version from VS Marketplace.
|
|
#
|
|
# Returns: Latest version string via `STDOUT` on success.
|
|
query_marketplace_latest_version() {
|
|
local extension="$1"
|
|
|
|
# Query marketplace metadata and extract latest version
|
|
if query_marketplace_metadata "${extension}" |
|
|
jq -r '.results[0].extensions[0].versions[0].version // empty' 2> /dev/null; then
|
|
# Cache the successful source
|
|
VERSION_SOURCE_CACHE["${extension}"]="marketplace"
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Find platform-specific version from VS Marketplace metadata.
|
|
#
|
|
# Returns: Version and target platform via `STDOUT` as "version:platform".
|
|
|
|
# Helper to find the latest version for a specific platform.
|
|
_find_latest_platform_version() {
|
|
local metadata="$1"
|
|
local platform="$2"
|
|
echo "${metadata}" | jq -r \
|
|
--arg platform "$platform" \
|
|
'.results[0].extensions[0].versions[] | select(.targetPlatform == $platform) | .version' |
|
|
head -n 1
|
|
}
|
|
|
|
# Helper to find the latest universal version.
|
|
_find_latest_universal_version() {
|
|
local metadata="$1"
|
|
echo "${metadata}" | jq -r \
|
|
'.results[0].extensions[0].versions[] | select(.targetPlatform == null or .targetPlatform == "universal") | .version' |
|
|
head -n 1
|
|
}
|
|
|
|
# Helper to find the overall latest version.
|
|
_find_overall_latest_version() {
|
|
local metadata="$1"
|
|
local version
|
|
version=$(echo "${metadata}" | jq -r '.results[0].extensions[0].versions[0].version // empty')
|
|
local platform
|
|
platform=$(echo "${metadata}" | jq -r '.results[0].extensions[0].versions[0].targetPlatform // "universal"')
|
|
echo "${version}:${platform}"
|
|
}
|
|
|
|
# Helper to find a specific version for a given platform.
|
|
_find_specific_platform_version() {
|
|
local metadata="$1"
|
|
local version="$2"
|
|
local platform="$3"
|
|
echo "${metadata}" | jq -r \
|
|
--arg version "$version" --arg platform "$platform" \
|
|
'.results[0].extensions[0].versions[] | select(.version == $version and .targetPlatform == $platform)'
|
|
}
|
|
|
|
# Helper to find a specific universal version.
|
|
_find_specific_universal_version() {
|
|
local metadata="$1"
|
|
local version="$2"
|
|
echo "${metadata}" | jq -r \
|
|
--arg version "$version" \
|
|
'.results[0].extensions[0].versions[] | select(.version == $version and (.targetPlatform == null or .targetPlatform == "universal"))'
|
|
}
|
|
|
|
# Helper to find a specific version for any platform.
|
|
_find_specific_version_any_platform() {
|
|
local metadata="$1"
|
|
local version="$2"
|
|
echo "${metadata}" | jq -r \
|
|
--arg version "$version" \
|
|
'.results[0].extensions[0].versions[] | select(.version == $version)' |
|
|
head -n 1
|
|
}
|
|
|
|
# Find platform-specific version from VS Marketplace metadata.
|
|
#
|
|
# Returns: Version and target platform via `STDOUT` as "version:platform".
|
|
query_marketplace_platform_version() {
|
|
local extension="$1"
|
|
local version="$2"
|
|
local metadata="$3"
|
|
local current_platform
|
|
current_platform="$(get_current_platform)"
|
|
local install_version=""
|
|
local target_platform=""
|
|
|
|
# If version is "latest", find the latest version for our platform
|
|
if [[ "${version}" == "latest" ]]; then
|
|
install_version=$(_find_latest_platform_version "${metadata}" "${current_platform}")
|
|
target_platform="${current_platform}"
|
|
|
|
# If no platform-specific version, get the latest universal version
|
|
if [[ -z "${install_version}" ]]; then
|
|
install_version=$(_find_latest_universal_version "${metadata}")
|
|
target_platform="universal"
|
|
fi
|
|
|
|
# If still no version, get the very latest regardless of platform
|
|
if [[ -z "${install_version}" ]]; then
|
|
local latest_info
|
|
latest_info=$(_find_overall_latest_version "${metadata}")
|
|
install_version="${latest_info%:*}"
|
|
target_platform="${latest_info#*:}"
|
|
fi
|
|
else
|
|
# Find the specific version entry for our platform and version
|
|
local version_info
|
|
version_info=$(_find_specific_platform_version "${metadata}" "${version}" "${current_platform}")
|
|
|
|
# If no platform-specific version found, try universal
|
|
if [[ -z "${version_info}" || "${version_info}" == "null" ]]; then
|
|
version_info=$(_find_specific_universal_version "${metadata}" "${version}")
|
|
fi
|
|
|
|
# If still no specific version found, use the first version with that version number
|
|
if [[ -z "${version_info}" || "${version_info}" == "null" ]]; then
|
|
version_info=$(_find_specific_version_any_platform "${metadata}" "${version}")
|
|
fi
|
|
|
|
install_version="$version"
|
|
target_platform=$(echo "${version_info}" | jq -r '.targetPlatform // "universal"')
|
|
fi
|
|
|
|
if [[ -z "${install_version}" || "${install_version}" == "null" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
echo "${install_version}:${target_platform}"
|
|
}
|
|
|
|
# Install an extension directly using the marketplace.
|
|
install_extension_direct() {
|
|
local editor_cmd="$1"
|
|
local extension="$2"
|
|
local version="$3"
|
|
local force_install="$4"
|
|
local result=0
|
|
|
|
if [[ "${version}" == "latest" ]]; then
|
|
if ! "${editor_cmd}" --install-extension "${extension}" --force 2> /dev/null; then
|
|
warn "Direct install failed for ${extension}"
|
|
result=1
|
|
fi
|
|
else
|
|
local install_cmd=("${editor_cmd}" --install-extension "${extension}@${version}")
|
|
if [[ "${force_install}" == "true" ]]; then
|
|
install_cmd+=(--force)
|
|
fi
|
|
if ! "${install_cmd[@]}" 2> /dev/null; then
|
|
warn "Direct install failed for ${extension}@${version}"
|
|
result=1
|
|
fi
|
|
fi
|
|
|
|
return ${result}
|
|
}
|
|
|
|
# Download extension from OpenVSX registry.
|
|
#
|
|
# Returns: Path to downloaded `.vsix` file via `STDOUT` on success.
|
|
download_from_openvsx() {
|
|
local extension="$1"
|
|
local version="$2"
|
|
local extensions_cache_dir="$3"
|
|
local publisher_id="${extension%%.*}"
|
|
local extension_id="${extension#*.}"
|
|
local install_version="${version}"
|
|
local metadata=""
|
|
local download_url=""
|
|
local vsix_filename=""
|
|
local vsix_path=""
|
|
|
|
info "Downloading ${extension}@${version} from OpenVSX..."
|
|
|
|
# If version is "latest", query OpenVSX API for latest version
|
|
if [[ "${version}" == "latest" ]]; then
|
|
info "Querying OpenVSX for latest version of ${extension}..."
|
|
if install_version=$(query_openvsx_latest_version "${extension}"); then
|
|
if [[ -z "${install_version}" ]]; then
|
|
error "Could not determine latest version from OpenVSX for ${extension}"
|
|
return 1
|
|
fi
|
|
info "Latest version of ${extension} from OpenVSX is ${install_version}"
|
|
else
|
|
error "Failed to query OpenVSX API for ${extension}"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Query extension metadata to get download information
|
|
info "Querying OpenVSX metadata for ${extension}@${install_version}..."
|
|
if ! metadata=$(query_openvsx_metadata "${extension}" "${install_version}"); then
|
|
error "Failed to query OpenVSX metadata for ${extension}@${install_version}"
|
|
return 1
|
|
fi
|
|
|
|
# Check if extension has platform-specific downloads
|
|
local current_platform
|
|
current_platform="$(get_current_platform)"
|
|
|
|
# Try to get platform-specific download URL first
|
|
download_url=$(echo "${metadata}" | jq -r ".downloads[\"${current_platform}\"] // empty" 2> /dev/null)
|
|
|
|
# If no platform-specific version, try universal
|
|
if [[ -z "${download_url}" || "${download_url}" == "null" ]]; then
|
|
download_url=$(echo "${metadata}" | jq -r '.downloads.universal // .files.download // empty' 2> /dev/null)
|
|
fi
|
|
|
|
# Fallback to constructing the URL if no downloads object exists
|
|
if [[ -z "${download_url}" || "${download_url}" == "null" ]]; then
|
|
warn "No downloads information found, using fallback URL construction"
|
|
download_url="https://open-vsx.org/api/${publisher_id}/${extension_id}/${install_version}/file/${publisher_id}.${extension_id}-${install_version}.vsix"
|
|
fi
|
|
|
|
# Extract filename from URL or construct it
|
|
if [[ "${download_url}" =~ @([^/]+)\.vsix$ ]]; then
|
|
# Platform-specific filename (e.g., name-version@platform.vsix)
|
|
vsix_filename="${publisher_id}.${extension_id}-${install_version}@${BASH_REMATCH[1]}.vsix"
|
|
else
|
|
# Universal filename
|
|
vsix_filename="${publisher_id}.${extension_id}-${install_version}.vsix"
|
|
fi
|
|
|
|
vsix_path="${extensions_cache_dir}/${vsix_filename}"
|
|
|
|
info "Downloading ${extension}@${install_version} from OpenVSX..."
|
|
info " - Platform: ${current_platform}"
|
|
info " - OpenVSX URL: ${download_url}"
|
|
|
|
# Create extensions directory if it doesn't exist
|
|
mkdir -p "${extensions_cache_dir}"
|
|
|
|
if curl --compressed -L -o "${vsix_path}" --fail "${download_url}" 2> /dev/null; then
|
|
# Verify the download was successful by checking file size
|
|
if [[ -s "${vsix_path}" ]]; then
|
|
info "Successfully downloaded from OpenVSX"
|
|
echo "${vsix_path}"
|
|
return 0
|
|
else
|
|
error "OpenVSX download failed (empty file)"
|
|
rm -f "${vsix_path}"
|
|
return 1
|
|
fi
|
|
else
|
|
error "OpenVSX download failed"
|
|
rm -f "${vsix_path}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Download extension from VS Marketplace.
|
|
#
|
|
# Returns: Path to downloaded `.vsix` file via `STDOUT` on success.
|
|
download_from_marketplace() {
|
|
local extension="$1"
|
|
local version="$2"
|
|
local extensions_cache_dir="$3"
|
|
local publisher_id="${extension%%.*}"
|
|
local extension_id="${extension#*.}"
|
|
local metadata=""
|
|
local version_platform=""
|
|
local install_version=""
|
|
local target_platform=""
|
|
local download_url=""
|
|
local vsix_filename=""
|
|
local vsix_path=""
|
|
|
|
info "Downloading ${extension}@${version} from VS Marketplace..."
|
|
|
|
# Create extensions directory if it doesn't exist
|
|
mkdir -p "${extensions_cache_dir}"
|
|
|
|
# Query extension metadata
|
|
info "Querying VS Marketplace for ${extension}@${version}..."
|
|
if ! metadata=$(query_marketplace_metadata "${extension}"); then
|
|
error "Failed to query VS Marketplace API for ${extension}"
|
|
return 1
|
|
fi
|
|
|
|
# Find the appropriate version and platform
|
|
if ! version_platform=$(query_marketplace_platform_version "${extension}" "${version}" "${metadata}"); then
|
|
error "Could not determine version and platform for ${extension}@${version}"
|
|
return 1
|
|
fi
|
|
|
|
# Parse the version:platform result
|
|
install_version="${version_platform%:*}"
|
|
target_platform="${version_platform#*:}"
|
|
|
|
if [[ "${version}" == "latest" ]]; then
|
|
info "Latest version of ${extension} from VS Marketplace is ${install_version}"
|
|
fi
|
|
|
|
# Extract download URL directly from metadata
|
|
if [[ "${target_platform}" != "universal" && "${target_platform}" != "null" ]]; then
|
|
download_url=$(
|
|
echo "${metadata}" | jq -r --arg version "$install_version" --arg platform "$target_platform" \
|
|
'.results[0].extensions[0].versions[] | select(.version == $version and .targetPlatform == $platform) | .files[] | select(.assetType == "Microsoft.VisualStudio.Services.VSIXPackage") | .source' \
|
|
2> /dev/null
|
|
)
|
|
vsix_filename="${publisher_id}.${extension_id}-${install_version}@${target_platform}.vsix"
|
|
else
|
|
download_url=$(
|
|
echo "${metadata}" | jq -r --arg version "$install_version" \
|
|
'.results[0].extensions[0].versions[] | select(.version == $version and (.targetPlatform == null or .targetPlatform == "universal")) | .files[] | select(.assetType == "Microsoft.VisualStudio.Services.VSIXPackage") | .source' \
|
|
2> /dev/null
|
|
)
|
|
vsix_filename="${publisher_id}.${extension_id}-${install_version}.vsix"
|
|
fi
|
|
|
|
if [[ -z "${download_url}" || "${download_url}" == "null" ]]; then
|
|
error "No download URL found in metadata"
|
|
return 1
|
|
fi
|
|
|
|
vsix_path="${extensions_cache_dir}/${vsix_filename}"
|
|
|
|
info "Downloading ${extension}@${install_version} from VS Marketplace..."
|
|
info " - Platform: $(get_current_platform) (using ${target_platform})"
|
|
info " - Marketplace URL: ${download_url}"
|
|
|
|
if curl --compressed -L -o "${vsix_path}" --fail "${download_url}" 2> /dev/null; then
|
|
if [[ -s "${vsix_path}" ]]; then
|
|
info "Successfully downloaded from VS Marketplace"
|
|
echo "${vsix_path}"
|
|
return 0
|
|
else
|
|
error "VS Marketplace download failed (empty file)"
|
|
rm -f "${vsix_path}"
|
|
return 1
|
|
fi
|
|
else
|
|
error "VS Marketplace download failed"
|
|
rm -f "${vsix_path}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Try downloading extension from OpenVSX first, then fallback to official marketplace.
|
|
#
|
|
# Returns: Path to downloaded `.vsix` file via `STDOUT` on success.
|
|
download_extension_vsix() {
|
|
local extension="$1"
|
|
local version="$2"
|
|
local extensions_cache_dir="$3"
|
|
local extension_source="marketplace"
|
|
|
|
if [[ "${PREFER_OPENVSX}" == "true" ]]; then
|
|
extension_source="openvsx"
|
|
fi
|
|
|
|
# Check if we have cached source information for this extension
|
|
if [[ -n "${VERSION_SOURCE_CACHE[${extension}]:-}" ]]; then
|
|
extension_source="${VERSION_SOURCE_CACHE[${extension}]}"
|
|
debug "Using cached source '${extension_source}' for ${extension}"
|
|
fi
|
|
|
|
case "${extension_source}" in
|
|
"openvsx")
|
|
if download_from_openvsx "${extension}" "${version}" "${extensions_cache_dir}"; then
|
|
return 0
|
|
fi
|
|
# If cached source fails, fall back to marketplace
|
|
warn "OpenVSX source failed, trying VS Marketplace"
|
|
download_from_marketplace "${extension}" "${version}" "${extensions_cache_dir}"
|
|
return $?
|
|
;;
|
|
"marketplace")
|
|
if download_from_marketplace "${extension}" "${version}" "${extensions_cache_dir}"; then
|
|
return 0
|
|
fi
|
|
# If cached source fails, fall back to OpenVSX
|
|
warn "VS Marketplace source failed, trying OpenVSX"
|
|
download_from_openvsx "${extension}" "${version}" "${extensions_cache_dir}"
|
|
return $?
|
|
;;
|
|
*)
|
|
error "Invalid extension source '${extension_source}'"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Install an extension via downloading `*.vsix` file.
|
|
install_extension_via_vsix() {
|
|
local editor_cmd="$1"
|
|
local extension="$2"
|
|
local version="$3"
|
|
local extensions_cache_dir="$4"
|
|
local result=0
|
|
local vsix_path=""
|
|
|
|
vsix_path="$(
|
|
download_extension_vsix "${extension}" "${version}" "${extensions_cache_dir}"
|
|
)"
|
|
|
|
if [[ -n "${vsix_path}" ]]; then
|
|
# Install the extension from the downloaded `*.vsix` file.
|
|
# Note: Installing from `*.vsix` automatically overwrites existing versions.
|
|
info "Installing extension from ${vsix_path}"
|
|
if ! "${editor_cmd}" --install-extension "${vsix_path}" --force; then
|
|
warn "Failed to install ${extension} from '*.vsix'"
|
|
result=1
|
|
fi
|
|
|
|
# Clean up the `*.vsix` file after installation attempt, only if within cache dir.
|
|
if [[ "${vsix_path}" == "${extensions_cache_dir}/"* ]]; then
|
|
rm -f -- "${vsix_path}"
|
|
fi
|
|
else
|
|
warn "Failed to download ${extension}@${version}.vsix"
|
|
result=1
|
|
fi
|
|
|
|
return ${result}
|
|
}
|
|
|
|
# Install an extension.
|
|
install_extension() {
|
|
local editor_cmd="$1"
|
|
local extension="$2"
|
|
local version="$3"
|
|
local use_latest="$4"
|
|
local force_install="false"
|
|
|
|
local extensions_cache_dir
|
|
extensions_cache_dir="${SCRIPT_DIR}/cache/extensions"
|
|
|
|
# Check if already installed and get current version
|
|
local current_version
|
|
current_version="$(get_installed_version "${editor_cmd}" "${extension}")"
|
|
|
|
local target_version="${version}"
|
|
local latest_version
|
|
if [[ "${use_latest}" != "false" ]]; then
|
|
target_version="latest"
|
|
fi
|
|
|
|
if [[ -z "${current_version}" ]]; then
|
|
# Extension not installed.
|
|
info "Installing ${extension}@${target_version}"
|
|
elif [[ "${use_latest}" == "force" ]]; then
|
|
# Force install latest version.
|
|
info "Extension ${extension} is installed (current: ${current_version})," \
|
|
"attempting to force-reinstall latest version"
|
|
force_install="true"
|
|
elif [[ "${current_version}" == "${version}" ]]; then
|
|
# Exact version already installed.
|
|
info "Extension ${extension}@${version} is already installed, skipping"
|
|
return 0
|
|
elif [[ "${target_version}" != "latest" ]]; then
|
|
# Wrong version installed, need to force install.
|
|
info "Extension ${extension} has wrong version installed" \
|
|
"(current: ${current_version}, wanted: ${version})," \
|
|
"force-installing ${version}"
|
|
force_install="true"
|
|
else
|
|
info "Extension ${extension} is already installed" \
|
|
"(current: ${current_version}), skipping"
|
|
return 0
|
|
fi
|
|
|
|
# Try direct installation first; fall back to downloading `*.vsix` if needed.
|
|
if ! install_extension_direct "${editor_cmd}" "${extension}" \
|
|
"${target_version}" "${force_install}"; then
|
|
|
|
local latest_version
|
|
if [[ "${use_latest}" != "false" ]]; then
|
|
info "Checking latest version for ${extension}..."
|
|
latest_version=$(query_latest_version "${extension}")
|
|
if [[ -n "${latest_version}" ]]; then
|
|
info " - Latest available version: ${latest_version}"
|
|
version="${latest_version}"
|
|
else
|
|
error "Could not determine latest version for ${extension}"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "${current_version}" == "${version}" ]]; then
|
|
info "Extension ${extension} is already installed" \
|
|
"(current: ${current_version}), skipping"
|
|
return 0
|
|
fi
|
|
|
|
warn "Direct installation failed, trying .vsix download method..."
|
|
install_extension_via_vsix "${editor_cmd}" "${extension}" \
|
|
"${version}" "${extensions_cache_dir}"
|
|
fi
|
|
|
|
# Clean up extensions directory if empty
|
|
rmdir "${extensions_cache_dir}" 2> /dev/null || true
|
|
info "Extension ${extension} installed successfully!"
|
|
return 0
|
|
}
|
|
|
|
# ==============================================================================
|
|
# Command Functions
|
|
# ==============================================================================
|
|
|
|
# Dump installed extensions to `extensions.lock`.
|
|
do_dump_extensions() {
|
|
local editor_cmd
|
|
editor_cmd="$(find_editor_cmd)"
|
|
local current_date
|
|
current_date="$(date)"
|
|
local extensions_lock
|
|
extensions_lock="$(get_extensions_lock)"
|
|
|
|
{
|
|
echo "# ${SETUP_EDITOR} Extensions"
|
|
echo "# Generated on ${current_date}"
|
|
echo
|
|
"${editor_cmd}" --list-extensions --show-versions 2> /dev/null |
|
|
strip_ansi_sequences |
|
|
grep -E '^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+@[A-Za-z0-9._-]+$'
|
|
} > "${extensions_lock}"
|
|
|
|
info "Extensions list dumped to ${extensions_lock}"
|
|
}
|
|
|
|
# Public function: Create static symlinks only.
|
|
do_static_config() {
|
|
symlink_static_config
|
|
info "Static symlink setup complete!"
|
|
}
|
|
|
|
# Public function: Create all symlinks (editor-specific + static).
|
|
do_config() {
|
|
symlink_editor_config
|
|
symlink_static_config
|
|
info "Symlink setup complete!"
|
|
}
|
|
|
|
# Install extensions from `extensions.lock`.
|
|
do_install_extensions() {
|
|
local editor_cmd
|
|
editor_cmd="$(find_editor_cmd)"
|
|
local extensions_lock
|
|
extensions_lock="$(get_extensions_lock)"
|
|
local use_latest="${1:-false}"
|
|
|
|
if [[ ! -f "${extensions_lock}" ]]; then
|
|
fatal "${extensions_lock} not found"
|
|
fi
|
|
|
|
# Warm the installed extensions cache before we start processing the lockfile.
|
|
installed_extensions "${editor_cmd}" > /dev/null
|
|
|
|
# Process each extension.
|
|
local line
|
|
while IFS= read -r line; do
|
|
if [[ -n "${line}" && ! "${line}" =~ ^[[:space:]]*# ]]; then
|
|
# Validate extension line format.
|
|
if ! validate_extension_line "${line}"; then
|
|
continue
|
|
fi
|
|
|
|
local extension
|
|
local version
|
|
extension="${line%@*}"
|
|
version="${line#*@}"
|
|
|
|
install_extension "${editor_cmd}" "${extension}" "${version}" \
|
|
"${use_latest}"
|
|
fi
|
|
done < "${extensions_lock}"
|
|
}
|
|
|
|
# Install a specific extension by identifier.
|
|
do_install_extension() {
|
|
local extension_id="$1"
|
|
local use_latest="${2:-false}"
|
|
local editor_cmd
|
|
editor_cmd="$(find_editor_cmd)"
|
|
local extension=""
|
|
local version=""
|
|
|
|
if [[ -z "${extension_id}" ]]; then
|
|
error "Extension identifier required"
|
|
info "Usage:"
|
|
info " siren EDITOR install EXTENSION_ID"
|
|
info " siren install EDITOR EXTENSION_ID"
|
|
exit 1
|
|
fi
|
|
|
|
# Parse extension ID - can be just extension name or extension@version
|
|
if [[ "${extension_id}" =~ @ ]]; then
|
|
# Extension with specific version
|
|
if ! validate_extension_line "${extension_id}"; then
|
|
error "Invalid extension format '${extension_id}'"
|
|
info "Expected format: publisher.extension or publisher.extension@version"
|
|
exit 1
|
|
fi
|
|
extension="${extension_id%@*}"
|
|
version="${extension_id#*@}"
|
|
else
|
|
# Extension without version - install latest
|
|
if [[ ! "${extension_id}" =~ ^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$ ]]; then
|
|
error "Invalid extension format '${extension_id}'"
|
|
info "Expected format: publisher.extension or publisher.extension@version"
|
|
exit 1
|
|
fi
|
|
extension="${extension_id}"
|
|
version="latest"
|
|
fi
|
|
|
|
# Warm the installed extensions cache
|
|
installed_extensions "${editor_cmd}" > /dev/null
|
|
|
|
if [[ "${version}" == "latest" ]]; then
|
|
use_latest="force"
|
|
fi
|
|
|
|
install_extension "${editor_cmd}" "${extension}" "${version}" "${use_latest}"
|
|
return 0
|
|
}
|
|
|
|
# Compute and print the intersection of extensions across editors.
|
|
do_shared_extensions() {
|
|
local -a input_editors
|
|
local -a editors
|
|
local -A seen
|
|
local arg
|
|
local output_json="false"
|
|
|
|
# Parse optional flags (currently only --json)
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--json)
|
|
output_json="true"
|
|
shift
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
--*)
|
|
fatal "Unknown option for shared-extensions: '$1'"
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ $# -eq 0 ]]; then
|
|
input_editors=("cursor" "vscode")
|
|
else
|
|
input_editors=("$@")
|
|
fi
|
|
|
|
# Normalize and deduplicate editors.
|
|
for arg in "${input_editors[@]}"; do
|
|
local norm
|
|
norm="$(normalize_editor_name "${arg}")"
|
|
if [[ -z "${norm}" ]]; then
|
|
fatal "Unknown editor '${arg}'"
|
|
fi
|
|
if [[ -z "${seen[${norm}]:-}" ]]; then
|
|
editors+=("${norm}")
|
|
seen["${norm}"]=1
|
|
fi
|
|
done
|
|
|
|
if [[ ${#editors[@]} -eq 0 ]]; then
|
|
fatal "No valid editors specified"
|
|
fi
|
|
|
|
# Build intersection iteratively.
|
|
local result=""
|
|
local idx=0
|
|
for arg in "${editors[@]}"; do
|
|
local lock
|
|
lock="$(extensions_lock_for_editor "${arg}")"
|
|
if [[ ! -f "${lock}" ]]; then
|
|
fatal "Lock file not found for ${arg}: ${lock}"
|
|
fi
|
|
|
|
local current
|
|
current="$(extensions_ids_from_lock "${lock}")"
|
|
|
|
if [[ ${idx} -eq 0 ]]; then
|
|
result="${current}"
|
|
else
|
|
# 'comm' requires sorted input; our helper already sorts.
|
|
result="$(comm -12 <(printf '%s\n' "${result}") \
|
|
<(printf '%s\n' "${current}"))"
|
|
fi
|
|
idx=$((idx + 1))
|
|
done
|
|
|
|
# Print final result.
|
|
if [[ "${output_json}" == "true" ]]; then
|
|
# Convert newline-separated list to JSON array using jq safely
|
|
printf '%s\n' "${result}" | jq -R -s 'split("\n") | map(select(length>0))'
|
|
else
|
|
printf '%s\n' "${result}"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================
|
|
# Main
|
|
# ==============================================================================
|
|
|
|
main() {
|
|
check_dependencies
|
|
|
|
# Show help if no arguments are provided.
|
|
if [[ $# -lt 1 ]]; then
|
|
show_help
|
|
exit 1
|
|
fi
|
|
|
|
# Set command or SETUP_EDITOR based on first argument.
|
|
local command=""
|
|
if [[ $# -ge 1 ]]; then
|
|
SETUP_EDITOR="$(normalize_editor_name "$1")"
|
|
if [[ -z "${SETUP_EDITOR}" ]]; then
|
|
command="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
|
fi
|
|
shift 1
|
|
fi
|
|
|
|
# Set command or SETUP_EDITOR based on second argument.
|
|
if [[ $# -ge 1 ]]; then
|
|
if [[ -z "${command}" ]]; then
|
|
command="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
|
shift 1
|
|
else
|
|
SETUP_EDITOR="$(normalize_editor_name "$1")"
|
|
if [[ -n "${SETUP_EDITOR}" ]]; then
|
|
shift 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Handle help command.
|
|
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
|
|
command="help"
|
|
fi
|
|
|
|
# Handle help command early.
|
|
if [[ "${command}" == "help" ]]; then
|
|
show_help
|
|
exit 0
|
|
fi
|
|
|
|
# If still no command, exit with error.
|
|
if [[ -z "${command}" ]]; then
|
|
error "No command provided"
|
|
echo >&2
|
|
show_help
|
|
exit 1
|
|
fi
|
|
|
|
# Set PREFER_OPENVSX to true for Kiro.
|
|
if [[ "${SETUP_EDITOR}" == "kiro" ]]; then
|
|
PREFER_OPENVSX="true"
|
|
fi
|
|
|
|
define_settings
|
|
|
|
case "${command}" in
|
|
config | conf)
|
|
if [[ -n "${SETUP_EDITOR}" ]]; then
|
|
do_config
|
|
else
|
|
do_static_config
|
|
fi
|
|
exit 0
|
|
;;
|
|
dump-extensions | dump)
|
|
require_editor "dump"
|
|
do_dump_extensions
|
|
exit 0
|
|
;;
|
|
shared-extensions | shared)
|
|
do_shared_extensions "$@"
|
|
exit 0
|
|
;;
|
|
extensions | ext)
|
|
require_editor "extensions"
|
|
local use_latest="false"
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--latest)
|
|
use_latest="true"
|
|
shift
|
|
;;
|
|
--force-latest)
|
|
use_latest="force"
|
|
shift
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_install_extensions "${use_latest}"
|
|
exit 0
|
|
;;
|
|
install)
|
|
require_editor "install"
|
|
local use_latest_i="false"
|
|
local extension_arg=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--latest)
|
|
use_latest_i="true"
|
|
shift
|
|
;;
|
|
--force-latest)
|
|
use_latest_i="force"
|
|
shift
|
|
;;
|
|
*)
|
|
if [[ -z "${extension_arg}" ]]; then
|
|
extension_arg="$1"
|
|
shift
|
|
else
|
|
break
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
if [[ -z "${extension_arg}" ]]; then
|
|
error "The 'install' command requires exactly one argument: the extension ID."
|
|
show_help
|
|
exit 1
|
|
fi
|
|
do_install_extension "${extension_arg}" "${use_latest_i}"
|
|
exit 0
|
|
;;
|
|
"")
|
|
error "No command provided"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
*)
|
|
error "Unknown command '${command}'"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Run main function.
|
|
main "$@"
|