#!/bin/zsh
# Public binary installer. Source repositories remain private.
emulate -L zsh
set -euo pipefail
umask 022
base='https://gdmux.pages.dev/downloads'
version='preview-20261002.1'
prefix="$HOME/.local"
selection=all
with_skills=0
usage() {
  cat <<'HELP'
gdm tools for macOS — gdmux · gid · gdm

Usage: zsh install.sh [--tool all|gdmux|gid|gdm] [--prefix ABSOLUTE_PATH]
                     [--version VERSION] [--with-skills]

Default: all three tools in ~/.local/bin. No Rust, Bun or sudo required.
Run the same command to update. Existing sessions and settings are preserved.
--with-skills also installs GDM's Codex/Claude/Pi skills, Pi launcher and Codex
hooks using GDM's existing installer. It may add ~/.local/bin to your shell PATH.
Without this option, no shell startup files or agent configuration are changed.
HELP
}
die() { print -u2 -r -- "Install failed: $*"; exit 1; }
while (( $# )); do
  case "$1" in
    --tool) (( $# >= 2 )) || die 'Missing tool'; selection="$2"; shift 2 ;;
    --prefix) (( $# >= 2 )) || die 'Missing prefix'; prefix="$2"; shift 2 ;;
    --version) (( $# >= 2 )) || die 'Missing version'; version="$2"; shift 2 ;;
    --with-skills) with_skills=1; shift ;;
    --help|-h) usage; exit 0 ;;
    *) die "Unknown argument: $1" ;;
  esac
done
[[ "$selection" == (all|gdmux|gid|gdm) ]] || die 'Choose all, gdmux, gid or gdm'
[[ "$version" == [a-zA-Z0-9]* && "$version" != *[^a-zA-Z0-9._-]* && "$version" != *..* ]] || die 'Invalid version'
[[ "$prefix" == /* && "$prefix" != / ]] || die 'Prefix must be an absolute user-owned directory'
[[ "$(uname -s)" == Darwin ]] || die 'This preview supports macOS only'
[[ "$(uname -m)" == (arm64|x86_64) ]] || die 'Unsupported processor'
(( $(sw_vers -productVersion | cut -d. -f1) >= 13 )) || die 'macOS 13 or newer is required'
(( ! with_skills )) || [[ "$selection" == (all|gdm) ]] || die '--with-skills requires gdm or all'
prefix="${prefix:a}"
bin_dir="$prefix/bin"
tools_to_install=("$selection")
[[ "$selection" != all ]] || tools_to_install=(gdmux gid gdm)
binaries=()
for tool in "${tools_to_install[@]}"; do
  case "$tool" in
    gdmux) binaries+=(gdmux gdmuxd gdmuxctl gdmux-remote) ;;
    *) binaries+=("$tool") ;;
  esac
done
zmodload zsh/stat
check_path() {
  local target="$1" component current_path='' item
  local -A info
  for component in ${(s:/:)target}; do
    [[ -n "$component" ]] || continue
    current_path="$current_path/$component"
    [[ ! -L "$current_path" ]] || die "Symlink in destination: $current_path"
    [[ ! -e "$current_path" || -d "$current_path" ]] || die "Not a directory: $current_path"
  done
  for item in "$prefix" "$target"; do
    if [[ -e "$item" ]]; then
      [[ -O "$item" ]] || die "Not owned by you: $item"
      zstat -H info "$item"
      (( (info[mode] & 8#22) == 0 )) || die "Directory writable by other users: $item"
    fi
  done
}
check_path "$bin_dir"
for binary in "${binaries[@]}"; do
  [[ ! -L "$bin_dir/$binary" ]] || die "Refusing to replace symlink: $bin_dir/$binary"
  [[ ! -e "$bin_dir/$binary" || ( -f "$bin_dir/$binary" && -O "$bin_dir/$binary" ) ]] || die "Invalid existing file: $binary"
done
mkdir -p "$bin_dir"
lock="$prefix/.gdm-tools-install-lock"
mkdir "$lock" 2>/dev/null || die "Another install is in progress ($lock)"
stage="$(mktemp -d "$bin_dir/.gdm-tools.XXXXXXXX")"
chmod 700 "$stage"
committed=0
replaced=()
previous=()
cleanup() {
  local binary failed=0
  if (( ! committed )); then
    for binary in "${replaced[@]}"; do
      if (( ${previous[(Ie)$binary]} )); then
        mv -f "$stage/previous/$binary" "$bin_dir/$binary" || failed=1
      else
        rm -f "$bin_dir/$binary" || failed=1
      fi
    done
  fi
  if (( failed )); then
    print -u2 -r -- "Previous files preserved for recovery: $stage/previous"
  else
    rm -rf "$stage"
  fi
  rmdir "$lock" 2>/dev/null || true
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
mkdir "$stage/new" "$stage/previous" "$stage/packages"
url="$base/$version"
fetch() { curl --proto '=https' --tlsv1.2 --fail --show-error --location --retry 2 "$url/$1" -o "$2"; }
fetch SHA256SUMS "$stage/SHA256SUMS"
verify() {
  local name="$1" file="$2" expected actual
  expected="$(awk -v name="$name" '$2 == name { print $1 }' "$stage/SHA256SUMS")"
  [[ ${#expected} == 64 && "$expected" != *[^a-f0-9]* ]] || die "Missing/ambiguous checksum: $name"
  actual="$(shasum -a 256 "$file" | awk '{print $1}')"
  [[ "$actual" == "$expected" ]] || die "Checksum mismatch: $name"
}
for tool in "${tools_to_install[@]}"; do
  archive="$tool-macos-universal.tar.gz"
  print -r -- "Downloading $tool ($version)…"
  fetch "$archive" "$stage/$archive"
  verify "$archive" "$stage/$archive"
  # Reject traversal and non-regular payloads before extraction.
  tar -tzf "$stage/$archive" > "$stage/members"
  while IFS= read -r member; do
    [[ "$member" == "$tool/"* && "$member" != *../* && "$member" != /* ]] || die "Invalid archive member: $member"
  done < "$stage/members"
  tar -tvzf "$stage/$archive" | awk 'substr($0,1,1)!="-" && substr($0,1,1)!="d" { bad=1 } END { exit bad }' || die 'Archive contains links or special files'
  tar -xzf "$stage/$archive" -C "$stage/packages" --no-same-owner
done
for binary in "${binaries[@]}"; do
  tool="$binary"
  [[ "$binary" != gdmux* ]] || tool=gdmux
  source_file="$stage/packages/$tool/bin/$binary"
  [[ -f "$source_file" && ! -L "$source_file" ]] || die "Missing binary: $binary"
  codesign --verify --strict "$source_file" || die "Invalid signature: $binary"
  codesign -d -r- "$source_file" 2>&1 | grep -q '386867L72X' || die "Unexpected signing team: $binary"
  install -m 755 "$source_file" "$stage/new/$binary"
  "$stage/new/$binary" --version
  if [[ -e "$bin_dir/$binary" ]]; then
    cp -p "$bin_dir/$binary" "$stage/previous/$binary"
    previous+=("$binary")
  fi
done
check_path "$bin_dir"
docs="$prefix/share/gdm-tools/$version"
check_path "$docs"
mkdir -p "$docs"
for tool in "${tools_to_install[@]}"; do
  [[ ! -L "$docs/$tool" && ! -e "$docs/$tool" ]] || continue
  mkdir "$docs/$tool"
  cp "$stage/packages/$tool/README.md" "$stage/packages/$tool/LICENSE" "$stage/packages/$tool/THIRD_PARTY_NOTICES.txt" "$docs/$tool/"
done
for binary in "${binaries[@]}"; do
  replaced+=("$binary")
  mv -f "$stage/new/$binary" "$bin_dir/$binary"
done
committed=1
print -r -- "Installed: ${binaries[*]} → $bin_dir"
if (( with_skills )); then
  print -r -- 'Installing GDM skills and agent hooks…'
  fetch gdm-setup.sh "$stage/gdm-setup.sh"
  verify gdm-setup.sh "$stage/gdm-setup.sh"
  GDM_BASE_URL="$url" GDM_VERSION="$version" GDM_BIN_DIR="$bin_dir" bash "$stage/gdm-setup.sh" || die 'Binaries are installed, but GDM skills setup failed. Run --with-skills again.'
fi
case ":$PATH:" in
  *":$bin_dir:"*) ;;
  *) print -r -- "Add this to your shell configuration: export PATH=\"$bin_dir:\$PATH\"" ;;
esac
print -r -- 'Start: gdmux  |  gid  |  gdm --help'
print -r -- 'Updates use this same installer. Running gdmux sessions keep their existing daemon until you close them.'
