#!/usr/bin/env bash # # VS Code extension installer for macOS and Linux (Ubuntu). # # Usage: # ./install.sh # looks for .vscode/extensions.json # ./install.sh -f extensions.json # specify a local JSON file # ./install.sh -f https://link-to-your/ext.json # specify a remote URL directly # ./install.sh -p "MyProfile" -f # install into a named profile set -euo pipefail IFS=$'\n\t' EXTENSIONS=( "docker.docker" "github.github-vscode-theme" "GitHub.vscode-github-actions" "ms-azuretools.vscode-containers" "ms-vscode-remote.vscode-remote-extensionpack" "pkief.material-icon-theme" ) PROFILE="" SOURCE="" usage() { cat < Install into the named VS Code profile. -f, --file Path or URL to the JSON file containing recommendations. Defaults to .vscode/extensions.json if it exists. -h, --help Show this help and exit. EOF } while [[ $# -gt 0 ]]; do case "$1" in -p|--profile) if [[ $# -lt 2 || -z "${2:-}" ]]; then echo "Error: --profile requires a non-empty value." >&2 exit 2 fi PROFILE="$2" shift 2 ;; -f|--file) if [[ $# -lt 2 || -z "${2:-}" ]]; then echo "Error: --file requires a non-empty value." >&2 exit 2 fi SOURCE="$2" shift 2 ;; -h|--help) usage exit 0 ;; *) echo "Error: unknown argument '$1'." >&2 usage >&2 exit 2 ;; esac done OS="$(uname -s)" case "$OS" in Darwin|Linux) ;; *) echo "Error: unsupported OS '$OS'. Use install.ps1 on Windows." >&2 exit 1 ;; esac # 1. Check for 'code' command if ! command -v code >/dev/null 2>&1; then cat >&2 <<'EOF' Error: the 'code' command is not on your PATH. EOF exit 1 fi # 2. Check for 'jq' command if ! command -v jq >/dev/null 2>&1; then echo "Error: 'jq' is not installed. (Ubuntu: sudo apt-get install jq)" >&2 exit 1 fi # 3. Resolve JSON Source (Local File vs URL) JSON_FILE="" TEMP_JSON_FILE="" USE_HARDCODED=false if [[ -z "$SOURCE" ]]; then if [[ -f ".vscode/extensions.json" ]]; then JSON_FILE=".vscode/extensions.json" else echo "Info: No JSON file specified and .vscode/extensions.json not found. Using hardcoded extensions." >&2 USE_HARDCODED=true fi elif [[ "$SOURCE" =~ ^https?:// ]]; then if ! command -v curl >/dev/null 2>&1; then echo "Error: 'curl' is required to download remote JSON files." >&2 exit 1 fi echo "Downloading JSON from URL..." JSON_FILE="$(mktemp -t vscode-json.XXXXXX)" TEMP_JSON_FILE="$JSON_FILE" curl -sSL "$SOURCE" -o "$JSON_FILE" else JSON_FILE="$SOURCE" if [[ ! -f "$JSON_FILE" ]]; then echo "Error: File '$JSON_FILE' does not exist." >&2 exit 1 fi fi # 4. Extract extensions from JSON using jq (if not using hardcoded array) if [[ "$USE_HARDCODED" == false ]]; then mapfile -t EXTENSIONS < <(jq -r '.recommendations[]?' "$JSON_FILE") if [[ ${#EXTENSIONS[@]} -eq 0 ]]; then echo "Error: No extensions found under the 'recommendations' key." >&2 exit 1 fi fi declare -a CODE_ARGS=() if [[ -n "$PROFILE" ]]; then CODE_ARGS+=("--profile" "$PROFILE") fi # Trap to clean up logs and temporary downloaded JSON files LOG_FILE="$(mktemp -t vscode-install.XXXXXX)" trap 'rm -f "$LOG_FILE" $TEMP_JSON_FILE' EXIT header="Installing ${#EXTENSIONS[@]} extension(s)" [[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'" echo "$header..." echo installed=0 failed=0 declare -a FAILED_LIST=() for ext in "${EXTENSIONS[@]}"; do [[ -z "$ext" ]] && continue printf ' -> %-55s ' "$ext" if code "${CODE_ARGS[@]}" --install-extension "$ext" --force >"$LOG_FILE" 2>&1; then echo "ok" installed=$((installed + 1)) else echo "FAILED" failed=$((failed + 1)) FAILED_LIST+=("$ext") sed 's/^/ /' "$LOG_FILE" >&2 || true fi done echo echo "Done. Installed: ${installed}, Failed: ${failed}" if (( failed > 0 )); then echo "Failed extensions:" >&2 for f in "${FAILED_LIST[@]}"; do echo " - $f" >&2 done exit 1 fi