Ctrl K
ComunidadGuías…

install-ls.sh

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Install / update the Windsurf language server binary.
#
# Default path (manifest-driven, fail-closed):
#   1. Read the reviewed pins from ls-pins.json.
#   2. Fetch https://windsurf-stable.codeium.com/api/versions.
#   3. Pick the newest pinned stable release (currently 3.10.67) and require
#      the manifest's commit + archive sha256 to match the pin.
#   4. Download the desktop archive, verify its sha256, extract the LS
#      member, verify the inner binary sha256 against the pin, then
#      atomically rename it into place.
#
# There is no silent fallback: an unreachable manifest, a missing sha256
# tool, an unpinned version, or any hash mismatch exits non-zero before the
# target is touched. The old third-party fallback hop is gone.
#
# Usage:
#   ./install-ls.sh                          # newest pinned stable release
#   ./install-ls.sh --version 3.10.67        # exact pinned upstream version
#   ./install-ls.sh --dry-run                # print the plan, write nothing
#   ./install-ls.sh --rollback               # pinned 3.5.17 mirror rollback
#   ./install-ls.sh --version 3.5.17 --allow-mirror
#   ./install-ls.sh --file /path/to/bin      # install a local binary
#   ./install-ls.sh /path/to/bin             # same
#   ./install-ls.sh --url <https-url> --sha256 <hex> [--allowed-host <host>]...
#
# Environment overrides:
#   WINDSURFAPI_LS_VERSION=3.10.67           # same as --version
#   WINDSURFAPI_LS_CHANNEL=stable|next       # manifest channel (default stable)
#   WINDSURFAPI_LS_MANIFEST_URL=<url|path>   # manifest override
#   WINDSURFAPI_LS_PINS=/path/to/ls-pins.json
#   WINDSURFAPI_LS_ALLOW_MIRROR=1            # permit mirror pins (3.5.17)
#   WINDSURFAPI_LS_ALLOW_UNPINNED=1          # permit versions absent from pins
#   WINDSURFAPI_LS_PLATFORM=linux-x64|linux-arm64|darwin-arm64|darwin-x64
#   LS_INSTALL_PATH=/path/to/binary          # install target
#   LS_INSTALL_STAGE_PATH=/path/to/staging   # internal: stage before publish
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
LS_VERSION="${WINDSURFAPI_LS_VERSION:-}"
LS_CHANNEL="${WINDSURFAPI_LS_CHANNEL:-stable}"
LS_MANIFEST="${WINDSURFAPI_LS_MANIFEST_URL:-}"
LS_PINS="${WINDSURFAPI_LS_PINS:-$SCRIPT_DIR/ls-pins.json}"
ALLOW_MIRROR="${WINDSURFAPI_LS_ALLOW_MIRROR:-0}"
ALLOW_UNPINNED="${WINDSURFAPI_LS_ALLOW_UNPINNED:-0}"
PLATFORM_OVERRIDE="${WINDSURFAPI_LS_PLATFORM:-}"
MIRROR_BASE="https://github.com/dwgx/windsurf-ls-release/releases/download"
# Test/ops hook: a fake curl can be pointed at this path without changing
# PATH (Git Bash prepends its own mingw64/bin, so PATH prepending is not
# reliable on Windows). Defaults to the system curl.
CURL_BIN="${WINDSURFAPI_LS_CURL:-curl}"

DRY_RUN=0
ROLLBACK=0
INPUT_FILE=""
INPUT_URL=""
INPUT_SHA=""
# Space-separated host allowlist for download/redirect hops. Empty means
# "stay on the host of the initial URL" (strict same-host redirect policy).
ALLOWED_HOSTS=""

log() { echo -e "\033[1;34m==>\033[0m $*"; }
err() { echo -e "\033[1;31m!!\033[0m  $*" >&2; }

usage() {
  sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'
}

# Git Bash/MSYS runs POSIX paths, but the Windows curl/tar/unzip/gzip binaries
# need native paths. Mixed mode (C:/...) works for both. On Linux/macOS
# cygpath is absent and this is a no-op.
native_path() {
  if command -v cygpath >/dev/null 2>&1; then
    cygpath -m "$1"
  else
    printf '%s' "$1"
  fi
}

# GNU tar treats a C:/... argument as a remote host. Convert target paths
# back to POSIX form for MSYS tar/unzip/gzip on Git Bash.
posix_path() {
  if command -v cygpath >/dev/null 2>&1; then
    cygpath -u "$1"
  else
    printf '%s' "$1"
  fi
}

sha256_file() {
  # GNU coreutils prefixes the digest with a literal backslash when the file
  # name contains a backslash (Windows paths: C:\...). Strip it so callers
  # always compare 64 hex characters.
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}' | sed 's/^\\//'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}' | sed 's/^\\//'
  elif command -v node >/dev/null 2>&1; then
    node -e 'const c=require("crypto"),f=require("fs");const h=c.createHash("sha256");f.createReadStream(process.argv[1]).on("data",d=>h.update(d)).on("end",()=>process.stdout.write(h.digest("hex")+"\n")).on("error",e=>{console.error(e.message);process.exit(1)})' "$(native_path "$1")"
  else
    return 127
  fi
}

# Authority (host[:port]) of an absolute URL, without userinfo/path/query.
url_authority() {
  local rest="${1#*://}"
  rest="${rest%%/*}"
  rest="${rest%%\?*}"
  rest="${rest%%#*}"
  printf '%s' "${rest##*@}"
}

# Lowercase hostname of an absolute URL, without port/brackets.
url_host() {
  local authority
  authority="$(url_authority "$1")"
  if [[ "$authority" == \[*\]* ]]; then
    authority="${authority#\[}"
    authority="${authority%%\]*}"
  else
    authority="${authority%%:*}"
  fi
  printf '%s' "$authority" | tr '[:upper:]' '[:lower:]'
}

host_in_list() {
  local host="$1"
  local list="$2"
  local candidate
  for candidate in $list; do
    [[ "$host" == "$candidate" ]] && return 0
  done
  return 1
}

# Resolve a Location header against the URL that produced it (absolute,
# protocol-relative, root-relative, query-only and path-relative forms).
resolve_redirect() {
  local base="$1"
  local location="$2"
  case "$location" in
    https://*) printf '%s' "$location" ;;
    //*) printf '%s:%s' "${base%%:*}" "$location" ;;
    /*) printf '%s://%s%s' "${base%%://*}" "$(url_authority "$base")" "$location" ;;
    \?*) printf '%s%s' "${base%%\?*}" "$location" ;;
    *)
      local origin="${base%%://*}://$(url_authority "$base")"
      local rest="${base#*://}"
      rest="/${rest#*/}"
      rest="${rest%%\?*}"
      local dir="${rest%/*}"
      [[ "$dir" == "/" ]] && dir=""
      printf '%s%s/%s' "$origin" "$dir" "$location"
      ;;
  esac
}

# Download to $dest. When allowed_hosts is non-empty every hop (initial URL
# and each redirect Location) must use one of those exact hosts. When empty,
# redirects may not leave the initial URL's host. curl is invoked with
# --max-redirs 0 and each 3xx is followed here, so a redirect is validated
# BEFORE any bytes from the next host are fetched. This closes the old
# `curl -L` behaviour where an allowlisted github.com URL could bounce to an
# arbitrary host and still be installed.
download() {
  local url="$1"
  local dest="$2"
  local allowed_hosts="${3:-$ALLOWED_HOSTS}"
  case "$url" in
    file://*)
      local p="${url#file://}"
      # Windows file URLs look like file:///C:/...; Git Bash wants /c/... or C:/...
      case "$p" in
        /[A-Za-z]:/*) p="${p#/}" ;;
      esac
      if [[ ! -f "$p" ]]; then
        err "Local archive not found: $p"
        return 1
      fi
      cp -f "$p" "$dest"
      ;;
    https://*)
      local current="$url"
      local initial_host
      initial_host="$(url_host "$url")"
      local hops=0
      local max_hops=5
      local header_file="$WORK_DIR/download-headers.$$"
      while :; do
        local host
        host="$(url_host "$current")"
        if [[ -z "$host" ]]; then
          err "Invalid download URL: $current"
          return 1
        fi
        if [[ -n "$allowed_hosts" ]]; then
          if ! host_in_list "$host" "$allowed_hosts"; then
            err "Download host not allowed: $host (permitted: $allowed_hosts)"
            return 1
          fi
        elif [[ "$host" != "$initial_host" ]]; then
          err "Refusing redirect to a different host: $host (initial: $initial_host)"
          return 1
        fi
        : > "$header_file"
        local code
        if ! code="$("$CURL_BIN" -S --progress-bar --proto '=https' --tlsv1.2 --retry 3 --retry-delay 2 \
          --max-time 900 --max-filesize 600000000 --max-redirs 0 \
          -D "$header_file" -o "$dest" -w '%{http_code}' "$current")"; then
          err "Download failed: $current"
          return 1
        fi
        case "$code" in
          2??)
            rm -f "$header_file"
            return 0
            ;;
          301|302|303|307|308)
            local location
            location="$(awk 'BEGIN{IGNORECASE=1} /^[Ll]ocation:/{sub(/^[^:]*:[[:space:]]*/,""); sub(/\r$/,""); print; exit}' "$header_file")"
            if [[ -z "$location" ]]; then
              err "Redirect without a Location header: $current"
              return 1
            fi
            hops=$((hops + 1))
            if [[ "$hops" -gt "$max_hops" ]]; then
              err "Too many redirects (limit $max_hops): $url"
              return 1
            fi
            current="$(resolve_redirect "$current" "$location")"
            ;;
          *)
            err "Download failed with HTTP $code: $current"
            return 1
            ;;
        esac
      done
      ;;
    *)
      err "Refusing non-https download URL: $url"
      return 1
      ;;
  esac
}

# ─── Argument parsing ──────────────────────────────────
while [[ $# -gt 0 ]]; do
  case "$1" in
    --version)
      [[ -n "${2:-}" ]] || { err "--version needs a value"; exit 2; }
      LS_VERSION="$2"; shift 2 ;;
    --channel)
      [[ -n "${2:-}" ]] || { err "--channel needs a value"; exit 2; }
      LS_CHANNEL="$2"; shift 2 ;;
    --manifest)
      [[ -n "${2:-}" ]] || { err "--manifest needs a value"; exit 2; }
      LS_MANIFEST="$2"; shift 2 ;;
    --pins)
      [[ -n "${2:-}" ]] || { err "--pins needs a value"; exit 2; }
      LS_PINS="$2"; shift 2 ;;
    --platform)
      [[ -n "${2:-}" ]] || { err "--platform needs a value"; exit 2; }
      PLATFORM_OVERRIDE="$2"; shift 2 ;;
    --file)
      [[ -n "${2:-}" ]] || { err "--file needs a value"; exit 2; }
      INPUT_FILE="$2"; shift 2 ;;
    --url)
      [[ -n "${2:-}" ]] || { err "--url needs a value"; exit 2; }
      INPUT_URL="$2"; shift 2 ;;
    --sha256)
      [[ -n "${2:-}" ]] || { err "--sha256 needs a value"; exit 2; }
      INPUT_SHA="$2"; shift 2 ;;
    --allowed-host)
      [[ -n "${2:-}" ]] || { err "--allowed-host needs a value"; exit 2; }
      ALLOWED_HOSTS="${ALLOWED_HOSTS:+$ALLOWED_HOSTS }$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]')"; shift 2 ;;
    --dry-run) DRY_RUN=1; shift ;;
    --rollback) ROLLBACK=1; ALLOW_MIRROR=1; shift ;;
    --allow-mirror) ALLOW_MIRROR=1; shift ;;
    --allow-unpinned) ALLOW_UNPINNED=1; shift ;;
    -h|--help) usage; exit 0 ;;
    -*)
      err "Unknown option: $1"; usage >&2; exit 2 ;;
    *)
      if [[ -f "$1" ]]; then
        INPUT_FILE="$1"; shift
      else
        err "Unknown argument: $1"; usage >&2; exit 2
      fi
      ;;
  esac
done

if [[ -n "$INPUT_FILE" && -n "$INPUT_URL" ]]; then
  err "--file and --url are mutually exclusive"; exit 2
fi
if [[ "$ROLLBACK" == "1" && -n "$LS_VERSION" ]]; then
  err "--rollback and --version are mutually exclusive"; exit 2
fi

MODE="manifest"
if [[ -n "$INPUT_FILE" ]]; then
  MODE="file"
elif [[ -n "$INPUT_URL" ]]; then
  MODE="url"
fi

if [[ -n "$INPUT_SHA" ]]; then
  INPUT_SHA="$(printf '%s' "$INPUT_SHA" | tr 'A-F' 'a-f')"
  if [[ ! "$INPUT_SHA" =~ ^[0-9a-f]{64}$ ]]; then
    err "--sha256 must be 64 hex characters"; exit 2
  fi
fi

# ─── Platform detection ────────────────────────────────
if [[ -n "$PLATFORM_OVERRIDE" ]]; then
  TARGET_ID="$PLATFORM_OVERRIDE"
else
  os="$(uname -s)"
  arch="$(uname -m)"
  case "$os" in
    Linux)
      case "$arch" in
        x86_64|amd64)  TARGET_ID='linux-x64' ;;
        aarch64|arm64) TARGET_ID='linux-arm64' ;;
        *) err "Unsupported Linux arch: $arch"; exit 1 ;;
      esac
      ;;
    Darwin)
      case "$arch" in
        x86_64) TARGET_ID='darwin-x64' ;;
        arm64)  TARGET_ID='darwin-arm64' ;;
        *) err "Unsupported macOS arch: $arch"; exit 1 ;;
      esac
      ;;
    *)
      err "Unsupported OS: $os (only Linux and macOS are supported)"
      exit 1
      ;;
  esac
fi

case "$TARGET_ID" in
  linux-x64)
    ASSET='language_server_linux_x64'
    DEFAULT_PATH="/opt/windsurf/${ASSET}"
    MEMBER_PATH_DEFAULT='Devin/resources/app/extensions/windsurf/bin/language_server_linux_x64'
    COMPRESSION_DEFAULT='tar.gz'
    ;;
  linux-arm64)
    ASSET='language_server_linux_arm'
    DEFAULT_PATH="/opt/windsurf/${ASSET}"
    MEMBER_PATH_DEFAULT=''
    COMPRESSION_DEFAULT=''
    ;;
  darwin-arm64)
    ASSET='language_server_macos_arm'
    DEFAULT_PATH="$HOME/.windsurf/${ASSET}"
    MEMBER_PATH_DEFAULT='Devin.app/Contents/Resources/app/extensions/windsurf/bin/language_server_macos_arm'
    COMPRESSION_DEFAULT='zip'
    ;;
  darwin-x64)
    ASSET='language_server_macos_x64'
    DEFAULT_PATH="$HOME/.windsurf/${ASSET}"
    MEMBER_PATH_DEFAULT='Devin.app/Contents/Resources/app/extensions/windsurf/bin/language_server_macos_x64'
    COMPRESSION_DEFAULT='zip'
    ;;
  *)
    err "Unsupported target: $TARGET_ID"
    exit 1
    ;;
esac

# LS_INSTALL_STAGE_PATH lets the dashboard download/verify a custom binary in
# a sibling staging file; the route publishes it with rename after its own
# digest check. Normal installs have no stage path and write TARGET directly.
TARGET="${LS_INSTALL_STAGE_PATH:-${LS_INSTALL_PATH:-$DEFAULT_PATH}}"
log "Platform: $TARGET_ID → asset=$ASSET"
log "Target:   $TARGET"

if [[ "$MODE" == "manifest" && -z "$MEMBER_PATH_DEFAULT" ]]; then
  err "Upstream does not ship a language server for $TARGET_ID."
  err "Supported upstream targets: linux-x64, darwin-arm64, darwin-x64."
  err "Copy the binary from a desktop install and use: bash install-ls.sh --file <path>"
  exit 1
fi

# ─── Temp files ────────────────────────────────────────
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/windsurfapi-ls.XXXXXX")"
TMP_TARGET="${TARGET}.new.$$"
SIDECAR="${TARGET}.version.json"
TMP_SIDECAR="${SIDECAR}.new.$$"
# Keep the 345 MB archive on the target's filesystem, not in a possibly small
# /tmp. The final binary is written to a sibling temp path so the mv is an
# atomic rename; a running LS keeps its old inode and the next exec gets the
# new binary without ETXTBSY ("Text file busy").
TMP_ARCHIVE="${TARGET}.archive.$$"
TMP_MANIFEST="$WORK_DIR/manifest.json"
cleanup() {
  rm -rf "$WORK_DIR"
  rm -f "$TMP_TARGET" "$TMP_SIDECAR" "$TMP_ARCHIVE"
}
trap cleanup EXIT

INSTALLED_SHA=''

# ─── Manual modes ──────────────────────────────────────
if [[ "$MODE" == "file" ]]; then
  if [[ ! -f "$INPUT_FILE" ]]; then
    err "File not found: $INPUT_FILE"; exit 1
  fi
  actual="$(sha256_file "$INPUT_FILE")" || { err "No sha256 tool available (sha256sum, shasum, or node is required)"; exit 1; }
  if [[ "$DRY_RUN" == "1" ]]; then
    echo "source=file"
    echo "file=$INPUT_FILE"
    echo "sha256=$actual"
    echo "install_path=$TARGET"
    exit 0
  fi
  mkdir -p "$(dirname "$TARGET")"
  log "Installing from local file: $INPUT_FILE"
  cp -f "$INPUT_FILE" "$TMP_TARGET"
  if [[ -n "$INPUT_SHA" && "$actual" != "$INPUT_SHA" ]]; then
    err "Checksum mismatch for $INPUT_FILE"
    err "Expected: $INPUT_SHA"
    err "Actual:   $actual"
    exit 1
  fi
  INSTALLED_SHA="$actual"
elif [[ "$MODE" == "url" ]]; then
  if [[ "$INPUT_URL" != https://* ]]; then
    err "Only https URLs are supported: $INPUT_URL"; exit 1
  fi
  # A05: a custom URL has no pin/manifest trust anchor, so the operator must
  # supply the expected digest. Refuse before any network I/O or filesystem
  # write. The old flow printed a warning and installed unverified bytes.
  if [[ -z "$INPUT_SHA" ]]; then
    err "--url requires --sha256 <64 hex characters>; refusing an unverified download"
    exit 2
  fi
  # Standalone GitHub release URLs legitimately redirect to GitHub's asset
  # CDNs. Keep that convenience without weakening the per-hop check: only
  # these fixed GitHub hosts are auto-allowed, and every other URL keeps the
  # strict same-host redirect policy.
  if [[ -z "$ALLOWED_HOSTS" ]]; then
    case "$(url_host "$INPUT_URL")" in
      github.com) ALLOWED_HOSTS="github.com objects.githubusercontent.com release-assets.githubusercontent.com api.github.com" ;;
    esac
  fi
  if [[ -n "$ALLOWED_HOSTS" ]]; then
    input_host="$(url_host "$INPUT_URL")"
    if ! host_in_list "$input_host" "$ALLOWED_HOSTS"; then
      err "Download host not allowed: $input_host (permitted: $ALLOWED_HOSTS)"
      exit 2
    fi
  fi
  if [[ "$DRY_RUN" == "1" ]]; then
    echo "source=url"
    echo "url=$INPUT_URL"
    echo "sha256=$INPUT_SHA"
    echo "install_path=$TARGET"
    exit 0
  fi
  mkdir -p "$(dirname "$TARGET")"
  log "Downloading from: $INPUT_URL"
  download "$INPUT_URL" "$TMP_TARGET"
  actual="$(sha256_file "$TMP_TARGET")" || { err "No sha256 tool available (sha256sum, shasum, or node is required)"; exit 1; }
  if [[ "$actual" != "$INPUT_SHA" ]]; then
    err "Checksum mismatch for $INPUT_URL"
    err "Expected: $INPUT_SHA"
    err "Actual:   $actual"
    exit 1
  fi
  log "Binary sha256 verified: $actual"
  INSTALLED_SHA="$actual"
else
  # ─── Manifest mode ───────────────────────────────────
  if [[ -z "$LS_MANIFEST" ]]; then
    case "$LS_CHANNEL" in
      stable) LS_MANIFEST='https://windsurf-stable.codeium.com/api/versions' ;;
      next)   LS_MANIFEST='https://windsurf-next.codeium.com/api/versions' ;;
      *) err "Unsupported channel: $LS_CHANNEL (stable|next)"; exit 1 ;;
    esac
  fi

  command -v node >/dev/null 2>&1 || {
    err "Node.js is required to read the release manifest and pins"; exit 1
  }

  MANIFEST_IS_LOCAL=0
  MIRROR_PIN=0
  if [[ "$ROLLBACK" == "1" ]]; then
    MIRROR_PIN=1
  elif [[ -n "$LS_VERSION" && -f "$LS_PINS" ]]; then
    MIRROR_PIN="$(node -e '
const fs = require("fs");
const [pinsFile, target, version] = process.argv.slice(1);
let pins;
try { pins = JSON.parse(fs.readFileSync(pinsFile, "utf8")); } catch { process.stdout.write("0"); process.exit(0); }
const releases = (pins && Array.isArray(pins.releases)) ? pins.releases : [];
const hit = releases.find(r => r && r.version === version && r.source === "mirror" && r.targets && r.targets[target]);
process.stdout.write(hit ? "1" : "0");
' "$(native_path "$LS_PINS")" "$TARGET_ID" "$LS_VERSION")" || MIRROR_PIN=0
  fi

  if [[ "$MIRROR_PIN" == "1" ]]; then
    log "Rollback pin selected; skipping the upstream manifest"
  elif [[ "$LS_MANIFEST" == https://* ]]; then
    log "Fetching manifest: $LS_MANIFEST"
    if ! curl -fsSL --proto '=https' --tlsv1.2 --max-time 60 --max-filesize 8388608 \
      -o "$TMP_MANIFEST" "$LS_MANIFEST"; then
      err "Could not fetch the release manifest: $LS_MANIFEST"
      err "Refusing to fall back to the stale mirror. Use --rollback for the pinned 3.5.17 mirror."
      exit 1
    fi
  elif [[ "$LS_MANIFEST" == http://* ]]; then
    err "Refusing plain http manifest URL: $LS_MANIFEST"; exit 1
  elif [[ -f "$LS_MANIFEST" ]]; then
    cp -f "$LS_MANIFEST" "$TMP_MANIFEST"
    MANIFEST_IS_LOCAL=1
  else
    err "Manifest not found: $LS_MANIFEST"; exit 1
  fi

  plan="$(node - "$(native_path "$TMP_MANIFEST")" "$(native_path "$LS_PINS")" "$TARGET_ID" "$LS_VERSION" "$LS_CHANNEL" \
    "$ROLLBACK" "$ALLOW_UNPINNED" "$ALLOW_MIRROR" "$MANIFEST_IS_LOCAL" "$MIRROR_BASE" <<'NODE'
const fs = require('fs');
const path = require('path');

const [
  manifestFile, pinsFile, target, requested, channel, rollbackStr,
  allowUnpinnedStr, allowMirrorStr, manifestIsLocalStr, mirrorBase,
] = process.argv.slice(2);

const rollback = rollbackStr === '1';
const allowUnpinned = allowUnpinnedStr === '1';
const allowMirror = allowMirrorStr === '1';
const manifestIsLocal = manifestIsLocalStr === '1';

const fail = (msg) => { process.stderr.write('install-ls: ' + msg + '\n'); process.exit(1); };
const HEX64 = /^[0-9a-f]{64}$/;
const TARGETS = {
  'linux-x64': { member: 'Devin/resources/app/extensions/windsurf/bin/language_server_linux_x64', compression: 'tar.gz' },
  'darwin-arm64': { member: 'Devin.app/Contents/Resources/app/extensions/windsurf/bin/language_server_macos_arm', compression: 'zip' },
  'darwin-x64': { member: 'Devin.app/Contents/Resources/app/extensions/windsurf/bin/language_server_macos_x64', compression: 'zip' },
};
if (!TARGETS[target]) fail('unsupported target: ' + target);
const spec = TARGETS[target];

let pins = { schemaVersion: 1, releases: [] };
if (fs.existsSync(pinsFile)) {
  let raw;
  try { raw = fs.readFileSync(pinsFile, 'utf8'); } catch (e) { fail('cannot read pins ' + pinsFile + ': ' + e.message); }
  try { pins = JSON.parse(raw); } catch (e) { fail('cannot parse pins ' + pinsFile + ': ' + e.message); }
  if (!pins || pins.schemaVersion !== 1 || !Array.isArray(pins.releases)) fail('invalid pins file: ' + pinsFile);
} else if (!allowUnpinned) {
  fail('pins file not found: ' + pinsFile + ' (pass --allow-unpinned to bypass)');
}
const releases = pins.releases || [];
const pinFor = (v) => releases.find(r => r && r.version === v && r.targets && r.targets[target]);

function loadManifest() {
  let raw;
  try { raw = fs.readFileSync(manifestFile, 'utf8'); } catch (e) { fail('cannot read manifest: ' + e.message); }
  let arr;
  try { arr = JSON.parse(raw); } catch (e) { fail('manifest is not valid JSON: ' + e.message); }
  if (!Array.isArray(arr) || arr.length === 0) fail('manifest is not a non-empty array');
  return arr;
}

const entryFor = (x) => (x && x.manifest && x.manifest[target] && typeof x.manifest[target] === 'object') ? x.manifest[target] : null;
const verOf = (x) => {
  const m = entryFor(x);
  return String((x && x.version) || (m && m.windsurfVersion) || '');
};
const score = (x) => Math.max(0, ...Object.values((x && x.manifest) || {}).map(m => Number(m && m.timestamp) || 0));

function checkUrl(url, pinned) {
  let parsed;
  try { parsed = new URL(url); } catch { fail('invalid download URL: ' + url); }
  if (parsed.protocol === 'file:') {
    if (!manifestIsLocal) fail('file:// archive URLs are only allowed with a local manifest');
    return;
  }
  if (parsed.protocol !== 'https:') fail('refusing non-https download URL: ' + url);
  if (!pinned) {
    const host = parsed.hostname;
    if (host !== 'windsurf-stable.codeiumdata.com' && host !== 'windsurf-next.codeiumdata.com') {
      fail('unpinned archive host not allowed: ' + host);
    }
  }
}

function emit(source, version, commit, url, archiveSha, member, binarySha, compression, archiveName, releaseTag, pinned, ch) {
  const fields = [source, version, commit, url, archiveSha, member, binarySha, compression, archiveName, releaseTag, pinned ? '1' : '0', ch];
  for (const value of fields) {
    if (/[|\r\n]/.test(String(value))) fail('resolved field contains a forbidden delimiter');
  }
  process.stdout.write(fields.join('|') + '\n');
}

let chosenPin = null;
let chosenEntry = null;
let source = 'upstream';

if (rollback) {
  const mirrors = releases.filter(r => r && r.source === 'mirror' && r.targets && r.targets[target]);
  mirrors.sort((a, b) => String(b.releasedAt || '').localeCompare(String(a.releasedAt || '')));
  if (!mirrors.length) fail('no mirror rollback pin for ' + target + ' in ' + pinsFile);
  chosenPin = mirrors[0];
  source = 'mirror';
} else if (requested) {
  if (!/^[0-9A-Za-z][0-9A-Za-z.+_-]*$/.test(requested)) fail('invalid version: ' + requested);
  const p = pinFor(requested);
  if (p) {
    chosenPin = p;
    source = p.source === 'mirror' ? 'mirror' : 'upstream';
  } else if (allowUnpinned) {
    source = 'upstream';
  } else {
    fail('version ' + requested + ' is not pinned in ' + pinsFile + ' (pass --allow-unpinned to bypass)');
  }
}

if (source === 'mirror') {
  if (!allowMirror) fail('version ' + chosenPin.version + ' is a mirror rollback pin; re-run with --allow-mirror (or --rollback)');
  const t = chosenPin.targets[target];
  if (!t || typeof t !== 'object') fail('mirror pin has no ' + target + ' target');
  if (!HEX64.test(t.archiveSha256 || '')) fail('mirror pin ' + chosenPin.version + ' has no archive sha256 for ' + target);
  if (!HEX64.test(t.binarySha256 || '')) fail('mirror pin ' + chosenPin.version + ' has no binary sha256 for ' + target);
  if (typeof t.archive !== 'string' || !t.archive) fail('mirror pin ' + chosenPin.version + ' has no archive name for ' + target);
  if (typeof chosenPin.releaseTag !== 'string' || !/^v[0-9A-Za-z._+-]+$/.test(chosenPin.releaseTag)) fail('mirror pin ' + chosenPin.version + ' has no valid releaseTag');
  const url = mirrorBase + '/' + chosenPin.releaseTag + '/' + t.archive;
  emit('mirror', chosenPin.version, chosenPin.commit || '', url, t.archiveSha256, '', t.binarySha256, t.compression || 'gzip', t.archive, chosenPin.releaseTag, true, chosenPin.channel || channel);
  process.exit(0);
}

const manifest = loadManifest();
const candidates = manifest.filter(x => entryFor(x));
if (!candidates.length) fail('manifest has no ' + target + ' entries');

if (requested && chosenPin) {
  const x = candidates.find(c => verOf(c) === requested);
  if (!x) fail('upstream manifest no longer lists ' + requested + ' for ' + target);
  const t = chosenPin.targets[target];
  const m = entryFor(x);
  if (!HEX64.test(t.archiveSha256 || '')) fail('pin ' + requested + ' has no archive sha256 for ' + target);
  if (!HEX64.test(t.binarySha256 || '')) fail('pin ' + requested + ' has no binary sha256 for ' + target);
  if (m.sha256hash !== t.archiveSha256) fail('manifest archive sha256 for ' + requested + '/' + target + ' does not match the pin');
  if (chosenPin.commit && m.version !== chosenPin.commit) fail('manifest commit for ' + requested + '/' + target + ' does not match the pin');
  if (t.member && t.member !== spec.member) fail('pin member path for ' + requested + '/' + target + ' does not match the expected path');
  chosenEntry = x;
} else if (requested) {
  const x = candidates.find(c => verOf(c) === requested);
  if (!x) fail('upstream manifest has no ' + requested + ' entry for ' + target);
  chosenEntry = x;
} else {
  const pinnedUpstream = releases.filter(r => r && r.source !== 'mirror' && (r.channel || 'stable') === channel && r.targets && r.targets[target]);
  const confirmed = [];
  let mismatch = '';
  for (const r of pinnedUpstream) {
    const x = candidates.find(c => verOf(c) === r.version);
    if (!x) continue;
    const t = r.targets[target];
    const m = entryFor(x);
    if (!HEX64.test(t.archiveSha256 || '') || !HEX64.test(t.binarySha256 || '')) {
      mismatch = 'pin ' + r.version + ' is incomplete for ' + target;
      continue;
    }
    if (m.sha256hash !== t.archiveSha256) {
      mismatch = 'manifest archive sha256 for ' + r.version + '/' + target + ' does not match the pin';
      continue;
    }
    if (r.commit && m.version !== r.commit) {
      mismatch = 'manifest commit for ' + r.version + '/' + target + ' does not match the pin';
      continue;
    }
    confirmed.push({ r, x });
  }
  if (confirmed.length) {
    confirmed.sort((a, b) => score(b.x) - score(a.x));
    chosenPin = confirmed[0].r;
    chosenEntry = confirmed[0].x;
  } else if (allowUnpinned) {
    // Explicit escape hatch: no pinned release matched, so take the newest
    // manifest entry. Archive verification still happens in bash.
    chosenEntry = candidates.slice().sort((a, b) => score(b) - score(a))[0];
  } else if (mismatch) {
    fail(mismatch);
  } else if (!pinnedUpstream.length) {
    fail('no pinned upstream release for ' + target + ' in ' + pinsFile);
  } else {
    fail('no pinned release for ' + target + ' matches the upstream manifest (pins: ' + pinnedUpstream.map(r => r.version).join(', ') + ')');
  }
}

const m = entryFor(chosenEntry);
const version = verOf(chosenEntry);
const commit = String(m.version || '');
const url = String(m.url || '');
const archiveSha = String(m.sha256hash || '');
if (!HEX64.test(archiveSha)) fail('manifest entry ' + version + ' has an invalid sha256hash');
const t = chosenPin ? (chosenPin.targets[target] || {}) : {};
if (chosenPin) {
  if (t.archiveSha256 !== archiveSha) fail('pin/manifest archive sha256 mismatch for ' + version + '/' + target);
  if (!HEX64.test(t.binarySha256 || '')) fail('pin ' + version + ' has no binary sha256 for ' + target);
  if (t.member && t.member !== spec.member) fail('pin member path for ' + version + '/' + target + ' does not match the expected path');
}
const pinned = !!chosenPin;
if (!pinned && !allowUnpinned) fail('refusing unpinned release ' + version);
checkUrl(url, pinned);
const archiveName = (t.archive && String(t.archive)) || path.basename(new URL(url).pathname) || 'archive';
emit('upstream', version, commit, url, archiveSha, spec.member, pinned ? t.binarySha256 : '', spec.compression, archiveName, '', pinned, chosenPin && chosenPin.channel ? chosenPin.channel : channel);
NODE
)" || { err "Could not resolve a language server release"; exit 1; }

  IFS='|' read -r SOURCE VERSION COMMIT URL ARCHIVE_SHA MEMBER_PATH BINARY_SHA COMPRESSION ARCHIVE_NAME RELEASE_TAG PINNED CHANNEL <<< "$plan"

  if [[ -z "$VERSION" || -z "$URL" || -z "$ARCHIVE_SHA" ]]; then
    err "Resolver returned an incomplete plan"; exit 1
  fi
  if [[ ! "$ARCHIVE_SHA" =~ ^[0-9a-f]{64}$ ]]; then
    err "Resolver returned an invalid archive sha256"; exit 1
  fi
  if [[ -z "$COMPRESSION" ]]; then
    err "Resolver returned no archive type for $TARGET_ID"; exit 1
  fi
  if [[ "$COMPRESSION" != "gzip" && -z "$MEMBER_PATH" ]]; then
    err "Resolver returned no archive member for $TARGET_ID"; exit 1
  fi
  if [[ "$PINNED" == "1" && ! "$BINARY_SHA" =~ ^[0-9a-f]{64}$ ]]; then
    err "Resolver returned an invalid binary sha256"; exit 1
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    echo "version=$VERSION"
    echo "commit=$COMMIT"
    echo "channel=$CHANNEL"
    echo "target=$TARGET_ID"
    echo "source=$SOURCE"
    echo "url=$URL"
    echo "archive=$ARCHIVE_NAME"
    echo "archive_sha256=$ARCHIVE_SHA"
    echo "member=$MEMBER_PATH"
    if [[ -n "$BINARY_SHA" ]]; then
      echo "binary_sha256=$BINARY_SHA"
    else
      echo "binary_sha256=unpinned"
    fi
    echo "pinned=$PINNED"
    echo "install_path=$TARGET"
    exit 0
  fi

  # Manifest mode is already hash-pinned, but redirect hops still get the
  # same host validation as custom URLs. GitHub release downloads legitimately
  # bounce to its asset CDNs; everything else must stay on the initial host.
  manifest_hosts="$(url_host "$URL")"
  case "$manifest_hosts" in
    github.com) manifest_hosts="$manifest_hosts objects.githubusercontent.com release-assets.githubusercontent.com" ;;
  esac
  ALLOWED_HOSTS="$manifest_hosts"
  mkdir -p "$(dirname "$TARGET")"
  log "Installing Windsurf LS $VERSION ($TARGET_ID, $SOURCE)"
  log "Downloading: $URL"
  download "$URL" "$TMP_ARCHIVE"

  log "Verifying archive sha256..."
  actual_archive_sha="$(sha256_file "$TMP_ARCHIVE")" || {
    err "No sha256 tool available (sha256sum, shasum, or node is required)"; exit 1
  }
  if [[ "$actual_archive_sha" != "$ARCHIVE_SHA" ]]; then
    err "Archive checksum mismatch for $ARCHIVE_NAME"
    err "Expected: $ARCHIVE_SHA"
    err "Actual:   $actual_archive_sha"
    exit 1
  fi
  log "Archive sha256 verified: $actual_archive_sha"

  if [[ -n "$MEMBER_PATH" ]]; then
    log "Extracting: $MEMBER_PATH"
  else
    log "Decompressing: $ARCHIVE_NAME"
  fi
  case "$COMPRESSION" in
    tar.gz)
      command -v tar >/dev/null 2>&1 || { err "tar is required to extract $ARCHIVE_NAME"; exit 1; }
      if ! tar -xOf "$(posix_path "$TMP_ARCHIVE")" "$MEMBER_PATH" > "$TMP_TARGET"; then
        err "Could not extract $MEMBER_PATH from $ARCHIVE_NAME"; exit 1
      fi
      ;;
    zip)
      command -v unzip >/dev/null 2>&1 || { err "unzip is required to extract $ARCHIVE_NAME"; exit 1; }
      if ! unzip -p "$(posix_path "$TMP_ARCHIVE")" "$MEMBER_PATH" > "$TMP_TARGET"; then
        err "Could not extract $MEMBER_PATH from $ARCHIVE_NAME"; exit 1
      fi
      ;;
    gzip)
      command -v gzip >/dev/null 2>&1 || { err "gzip is required to decompress $ARCHIVE_NAME"; exit 1; }
      if ! gzip -dc "$(posix_path "$TMP_ARCHIVE")" > "$TMP_TARGET"; then
        err "Could not decompress $ARCHIVE_NAME"; exit 1
      fi
      ;;
    *)
      err "Unsupported archive type: $COMPRESSION"; exit 1 ;;
  esac

  if [[ ! -s "$TMP_TARGET" ]]; then
    err "Extracted binary is empty (member not found: $MEMBER_PATH)"; exit 1
  fi

  actual_bin_sha="$(sha256_file "$TMP_TARGET")" || {
    err "No sha256 tool available (sha256sum, shasum, or node is required)"; exit 1
  }
  if [[ -n "$BINARY_SHA" ]]; then
    if [[ "$actual_bin_sha" != "$BINARY_SHA" ]]; then
      err "Binary checksum mismatch for $ASSET"
      err "Expected: $BINARY_SHA"
      err "Actual:   $actual_bin_sha"
      exit 1
    fi
    log "Binary sha256 verified: $actual_bin_sha"
  else
    log "WARNING: no pinned binary sha256 for $VERSION/$TARGET_ID; computed $actual_bin_sha"
  fi
  INSTALLED_SHA="$actual_bin_sha"
  rm -f "$TMP_ARCHIVE"
fi

# ─── Atomic install ────────────────────────────────────
if [[ "$MODE" == "manifest" ]]; then
  installed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  sidecar_json="$(node -e '
const [version, commit, archiveSha, binarySha, url, archive, source, pinned, installedAt] = process.argv.slice(1);
process.stdout.write(JSON.stringify({windsurfVersion: version, commit, archiveSha256: archiveSha, binarySha256: binarySha, downloadUrl: url, archive, resolver: source, pinned: pinned === "1", installedAt}) + "\n");
' "$VERSION" "$COMMIT" "$ARCHIVE_SHA" "$INSTALLED_SHA" "$URL" "$ARCHIVE_NAME" "$SOURCE" "$PINNED" "$installed_at")"
  printf '%s' "$sidecar_json" > "$TMP_SIDECAR"
fi

chmod +x "$TMP_TARGET"
mv -f "$TMP_TARGET" "$TARGET"

if [[ "$MODE" == "manifest" ]]; then
  mv -f "$TMP_SIDECAR" "$SIDECAR"
  log "Wrote metadata: $SIDECAR"
else
  # A local/custom binary has no manifest metadata; do not leave a stale
  # sidecar describing the previous manifest install.
  rm -f "$SIDECAR"
fi

rm -rf "$WORK_DIR"
trap - EXIT

size="$(du -h "$TARGET" | cut -f1)"
sha_short="$(printf '%s' "$INSTALLED_SHA" | cut -c1-16)"
log "Installed: $TARGET ($size, sha256:${sha_short}...)"

if [[ "$TARGET_ID" == darwin-* ]]; then
  log ""
  log "macOS users: set this in your .env:"
  log "  LS_BINARY_PATH=$TARGET"
fi