#!/bin/sh
set -eu
calling_path=$PATH
usage() {
  cat <<'HELP'
Trellis setup

On a Mac: install an isolated Trellis toolchain and connect your development VM. Setup adds ~/.local/bin to your login shell PATH for the trellis command; your local checkouts stay yours.
On the development VM: prepare your VM checkout at ~/trellis and install its repository tools.
Install the worktree collector on both hosts. The browser bridge runs only on the Mac.
Explicit service choices persist across setup. Run trellis update to install a newer release.

Usage:
  curl -fsSL https://setup.trellistech.com | sh
  trellis disable worktrees-cleanup
  trellis update
  trellis uninstall

Options:
  --vm-checkout PATH   select and remember an absolute VM checkout path (default ~/trellis)
  --non-interactive    never prompt or open a browser; exit 2 when sign-in is required
  --verbose  stream tool diagnostics as they run (diagnostics are saved in the setup log)
  --help     show this help and exit
HELP
}
verbose=0
non_interactive=0
vm_checkout=
while [ "$#" -gt 0 ]; do
  arg=$1
  case "$arg" in
    --vm-checkout)
      if [ "$#" -lt 2 ]; then echo "--vm-checkout requires an absolute path." >&2; exit 2; fi
      vm_checkout=$2
      case "$vm_checkout" in /*) ;; *) echo "--vm-checkout requires an absolute path." >&2; exit 2 ;; esac
      shift ;;
    --verbose) verbose=1 ;;
    --non-interactive) non_interactive=1 ;;
    --help) usage; exit 0 ;;
    *) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;;
  esac
  shift
done
umask 077
companion_root="${TRELLIS_COMPANION_HOME:-$HOME/.local/share/trellis/mac-companion}"
companion_origin="${TRELLIS_COMPANION_ORIGIN:-https://setup.trellistech.com}"
case "$companion_root" in /*) ;; *) echo 'TRELLIS_COMPANION_HOME must be an absolute path.' >&2; exit 1 ;; esac
case "$companion_origin" in https://*) ;; *) echo 'The Trellis download origin must use HTTPS.' >&2; exit 1 ;; esac
host_os=$(uname -s)
case "$host_os" in
  Darwin) host_kind=Mac ;;
  Linux)
    # Only the Google Cloud instance designated as the development VM is a target.
    if [ -n "${TRELLIS_REMOTE_DEV:-}" ]; then designated=$TRELLIS_REMOTE_DEV
    else designated=$(curl --noproxy '*' --fail --silent --max-time 2 --header 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/trellis-remote-dev 2>/dev/null || true); fi
    if [ "$designated" != 1 ]; then echo 'This Linux machine is not the designated development VM (trellis-remote-dev metadata). Run the installer on your Mac or on the VM.' >&2; exit 1; fi
    host_kind=VM ;;
  *) echo 'This installer supports macOS and the development VM. Run it on your Mac or on the VM.' >&2; exit 1 ;;
esac
mkdir -p "$companion_root/logs"
started_at=$(date +%s)
installer_log="$companion_root/logs/install-$(date -u +%Y%m%dT%H%M%SZ)-$$.log"
touch "$installer_log"
display_log=$installer_log
case "$installer_log" in "$HOME"/*) display_log="~${installer_log#"$HOME"}" ;; esac
installer_tmp=$(mktemp -d "$companion_root/.install.XXXXXX")
exec 3>&1 4>&2
ui_mode=${TRELLIS_UI_MODE:-auto}
if [ "$non_interactive" -eq 1 ]; then
  export TRELLIS_NON_INTERACTIVE=1
  ui_mode=plain
fi
if [ "$ui_mode" = auto ]; then
  ui_mode=plain
  if [ -t 3 ] && [ "${TERM:-dumb}" != dumb ] && [ -z "${CI:-}" ]; then ui_mode=rich; fi
fi
unicode=${TRELLIS_UNICODE:-auto}
if [ "$unicode" = auto ]; then
  case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in *UTF-8*|*utf-8*|*utf8*|*UTF8*) unicode=1 ;; *) unicode=0 ;; esac
fi
color=0
if [ "$ui_mode" = rich ] && [ "${TERM:-dumb}" != dumb ] && [ "${NO_COLOR+x}" != x ] && [ "${CLICOLOR:-1}" != 0 ]; then color=1; fi
# Canonical Trellis green (#007A68); theme-aware neutral, warning and danger colors.
color_scheme=${TRELLIS_COLOR_SCHEME:-dark}
terminal_background=${COLORFGBG:-}
if [ -z "${TRELLIS_COLOR_SCHEME:-}" ] && [ "${terminal_background##*;}" = 15 ]; then color_scheme=light; fi
palette=basic
case "${TERM:-}" in *256color*) palette=indexed ;; esac
case "${COLORTERM:-}:${TERM:-}" in truecolor:*|24bit:*|*direct*) palette=truecolor ;; esac
export TRELLIS_COLOR_SCHEME="$color_scheme"
export TRELLIS_UI_MODE="$ui_mode" TRELLIS_UNICODE="$unicode"
export TRELLIS_SETUP_LOG="$installer_log" TRELLIS_SETUP_VERBOSE="$verbose"
rail='|'; section_symbol='*'; ready_symbol=o; error_symbol=x; close_symbol='`'
if [ "$unicode" = 1 ]; then rail='│'; section_symbol='◆'; ready_symbol='◇'; error_symbol='×'; close_symbol='└'; fi
log() { printf '%s\n' "$*" >>"$installer_log"; }
paint() {
  shade=$1
  case "$palette:$color_scheme:$1" in
    truecolor:light:36|truecolor:light:32) shade='38;2;0;122;104' ;;
    truecolor:dark:36|truecolor:dark:32) shade='38;2;0;122;104' ;;
    truecolor:light:2) shade='38;2;115;115;115' ;;
    truecolor:dark:2) shade='38;2;163;163;163' ;;
    truecolor:light:33) shade='38;2;180;83;9' ;;
    truecolor:dark:33) shade='38;2;217;130;43' ;;
    truecolor:light:31) shade='38;2;198;40;40' ;;
    truecolor:dark:31) shade='38;2;231;114;114' ;;
    indexed:light:36|indexed:light:32) shade='38;5;29' ;;
    indexed:dark:36|indexed:dark:32) shade='38;5;29' ;;
    indexed:light:2) shade='38;5;243' ;;
    indexed:dark:2) shade='38;5;247' ;;
    indexed:light:33) shade='38;5;130' ;;
    indexed:dark:33) shade='38;5;172' ;;
    indexed:light:31) shade='38;5;160' ;;
    indexed:dark:31) shade='38;5;210' ;;
    basic:*:36) shade=32 ;;
  esac
  if [ "$color" -eq 1 ]; then printf '\033[%sm%s\033[0m' "$shade" "$2" >&3; else printf '%s' "$2" >&3; fi
}
detail() {
  if [ "$ui_mode" = rich ]; then printf ' %s  ' "$rail" >&3; paint 2 "$*"; printf '\n' >&3
  else printf '[info] %s\n' "$*" >&3; fi
}
say() { detail "$*"; log "$*"; }
section() {
  if [ "$ui_mode" = rich ]; then
    printf ' %s\n ' "$rail" >&3; paint 36 "$section_symbol"; printf '  %s\n %s\n' "$*" "$rail" >&3
  else printf '== %s\n' "$*" >&3; fi
  log "$*"
}
ready() {
  if [ "$ui_mode" = rich ]; then printf ' ' >&3; paint 32 "$ready_symbol"; printf '  %s\n' "$*" >&3
  else printf '[ok] %s\n' "$*" >&3; fi
  log "ready: $*"
}
fail() {
  if [ "$ui_mode" = rich ]; then printf ' ' >&3; paint 31 "$error_symbol"; printf '  %s\n' "$*" >&3
  else printf '[error] %s\n' "$*" >&3; fi
  log "failed: $*"
}
header() {
  if [ "$ui_mode" = rich ]; then
    if [ "$unicode" = 1 ] && [ "${COLUMNS:-80}" -ge 44 ]; then
      header_width=42
      logo_row=0
      for line in '▗▄▄▄▖▗▄▄▖ ▗▄▄▄▖▗▖   ▗▖   ▗▄▄▄▖ ▗▄▄▖' '  █  ▐▌ ▐▌▐▌   ▐▌   ▐▌     █  ▐▌   ' '  █  ▐▛▀▚▖▐▛▀▀▘▐▌   ▐▌     █   ▝▀▚▖' '  █  ▐▌ ▐▌▐▙▄▄▖▐▙▄▄▖▐▙▄▄▖▗▄█▄▖▗▄▄▞▘'; do
        printf ' ' >&3; paint 36 "$line"
        logo_row=$((logo_row + 1))
        if [ "$logo_row" -eq 4 ]; then paint 2 '  setup'; fi
        printf '\n' >&3
      done
    else header_width=14; paint 36 ' TRELLIS'; paint 2 '  setup'; printf '\n' >&3; fi
    edge='-'; corner='+'
    if [ "$unicode" = 1 ]; then edge='─'; corner='┌'; fi
    printf ' %s' "$corner" >&3
    while [ "$header_width" -gt 0 ]; do printf '%s' "$edge" >&3; header_width=$((header_width - 1)); done
    printf '\n' >&3
    detail "$header_detail"
  else printf 'trellis setup: %s\n' "$header_detail" >&3; fi
}
cleanup() {
  status=$?
  trap - EXIT HUP INT TERM
  # Reap background jobs before printing the final UI. Bash on macOS otherwise
  # reports terminated jobs after the footer, and the spinner can redraw it.
  {
    if [ -n "${command_pid:-}" ]; then
      kill "$command_pid" 2>/dev/null || true
      wait "$command_pid" || true
    fi
    if [ -n "${progress_pid:-}" ]; then
      wait "$progress_pid" || true
    fi
    if [ -n "${output_pid:-}" ]; then
      kill "$output_pid" 2>/dev/null || true
      wait "$output_pid" || true
    fi
  } 2>>"$installer_log"
  if [ -n "${pending_output:-}" ] && [ -f "$pending_output" ]; then
    cat "$pending_output" >>"$installer_log"
  fi
  if [ "$status" -ne 0 ] && [ "${release_pending:-0}" -eq 1 ]; then
    if [ -f "${release_backup:-}" ]; then
      mv "$release_backup" "$companion_root/RELEASE"
    else
      rm -f "$companion_root/RELEASE"
    fi
  fi
  if [ "$color" -eq 1 ]; then printf '\r\033[2K' >&3; fi
  if [ "$status" -ne 0 ]; then
    fail "Setup stopped after $(( $(date +%s) - started_at ))s · exit $status"
    say "Log: $display_log"
    if [ "$non_interactive" -eq 1 ]; then say 'Retry: curl -fsSL https://setup.trellistech.com | sh -s -- --non-interactive'
    else say 'Retry: curl -fsSL https://setup.trellistech.com | sh'; fi
  fi
  rm -rf "$installer_tmp"
  exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' HUP TERM
command_progress_summary() {
  tail -c 4096 "$1" 2>/dev/null | tr '\r' '\n' | awk '
    {
      text = tolower($0)
      stage = ""
      if (text ~ /(select|resolv)/) stage = "Selecting release"
      else if (text ~ /(download|fetch|receiv|transfer)/) stage = "Downloading"
      else if (text ~ /(verif|checksum)/) stage = "Verifying"
      else if (text ~ /(extract|unpack)/) stage = "Extracting"
      else if (text ~ /install/) stage = "Installing"
      else if (text ~ /(build|compil)/) stage = "Preparing"
      percent = ""
      if (match($0, /[0-9]+([.][0-9]+)?%/)) percent = substr($0, RSTART, RLENGTH)
      if (stage != "") summary = stage
      if (percent != "") {
        if (stage == "") stage = "Downloading"
        summary = stage " · " percent
      }
    }
    END { if (summary != "") print summary }
  '
}
run_logged() {
  quiet_step=0
  if [ "$1" = --quiet ]; then quiet_step=1; shift; fi
  label=$1; shift
  log "start: $label"
  if [ "$color" -eq 0 ] && [ "$quiet_step" -eq 0 ]; then say "$label"; fi
  output="$installer_tmp/command-output"
  : >"$output"
  pending_output=$output
  command_output=$output
  if [ "$verbose" -eq 1 ]; then
    command_output="$installer_tmp/command-output.fifo"
    mkfifo "$command_output"
    tee "$output" < "$command_output" | while IFS= read -r line || [ -n "$line" ]; do detail "$line"; done &
    output_pid=$!
  fi
  set +e
  # Preserve terminal input for tools that genuinely need a human response.
  if [ "$non_interactive" -eq 0 ] && ( : < /dev/tty ) 2>/dev/null; then
    NO_COLOR=1 TERM=dumb MISE_HIDE_PROGRESS=1 "$@" < /dev/tty >"$command_output" 2>&1 &
  else
    NO_COLOR=1 TERM=dumb MISE_HIDE_PROGRESS=1 "$@" < /dev/null >"$command_output" 2>&1 &
  fi
  command_pid=$!
  command_started_at=$(date +%s)
  (
    # Only the parent owns installer cleanup.
    trap - EXIT
    sleep_pid=
    trap 'if [ -n "$sleep_pid" ]; then kill "$sleep_pid" 2>/dev/null || true; fi; exit 0' TERM INT HUP
    frame=0
    progress_hint=
    while kill -0 "$command_pid" 2>/dev/null; do
      command_elapsed=$(( $(date +%s) - command_started_at ))
      if [ $((frame % 4)) -eq 0 ]; then progress_hint=$(command_progress_summary "$output"); fi
      if [ "$color" -eq 1 ] && [ "$verbose" -eq 0 ]; then
        if [ "$unicode" = 1 ]; then
          case $((frame % 10)) in 0) glyph='⠋';; 1) glyph='⠙';; 2) glyph='⠹';; 3) glyph='⠸';; 4) glyph='⠼';; 5) glyph='⠴';; 6) glyph='⠦';; 7) glyph='⠧';; 8) glyph='⠇';; 9) glyph='⠏';; esac
        else case $((frame % 4)) in 0) glyph='-';; 1) glyph='\';; 2) glyph='|';; 3) glyph='/';; esac; fi
        printf '\r\033[2K ' >&3; paint 36 "$glyph"; printf '  %s… ' "$label" >&3; paint 2 "${command_elapsed}s"
        if [ -n "$progress_hint" ]; then printf ' · ' >&3; paint 2 "$progress_hint"; fi
      elif [ "$command_elapsed" -gt 0 ] && [ $((frame % 120)) -eq 0 ]; then
        progress_message="$label · ${command_elapsed}s elapsed"
        if [ -n "$progress_hint" ]; then progress_message="$progress_message · $progress_hint"; fi
        detail "$progress_message"
      fi
      frame=$((frame + 1))
      sleep 0.12 &
      sleep_pid=$!
      wait "$sleep_pid"
    done
  ) &
  progress_pid=$!
  wait "$command_pid"
  status=$?
  # Let the spinner observe command completion. Killing a just-forked Bash
  # subshell can run the parent's EXIT trap before its own traps are installed.
  wait "$progress_pid" 2>/dev/null || true
  progress_pid=; command_pid=
  if [ -n "${output_pid:-}" ]; then wait "$output_pid"; output_pid=; rm -f "$command_output"; fi
  set -e
  cat "$output" >>"$installer_log"
  pending_output=
  if [ "$color" -eq 1 ]; then printf '\r\033[2K' >&3; fi
  if [ "$status" -ne 0 ]; then
    failure_detail=$(tail -n 6 "$output")
    case "$failure_detail" in
      *'Checksum verification failed'*) fail "$label · download verification failed. Rerun setup to download a fresh copy." ;;
      *'Could not resolve'*|*'Failed to connect'*|*'timed out'*) fail "$label · could not reach the service. Check your internet connection or VPN, then rerun setup." ;;
      *'Permission denied'*) fail "$label · permission denied. Check access to the install directory, then rerun setup." ;;
      *) fail "$label could not complete. See the setup log for the cause, then rerun setup." ;;
    esac
    return "$status"
  fi
  if [ "$quiet_step" -eq 0 ]; then ready "$label"; fi
}
header_detail='Connect this Mac to your development VM.'
if [ "$host_kind" = VM ]; then header_detail='Set up background services on this development VM.'; fi
header
log "Install directory: $companion_root"
log "Download origin: $companion_origin"
say "Log: $display_log"
section 'Trellis command'
# Download diagnostics stay in the log; show one step for the release.
run_logged 'Download and verify trellis command' sh -c '
  set -eu
  cd "$1"
  for artifact in host-tools.toml setup.mjs worker.mjs bridge-peer.mjs device-host.mjs worktree-collector.mjs messaging-client.mjs RELEASE SHA256SUMS; do
    curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 --connect-timeout 15 --max-time 180 --retry 2 "${2%/}/companion/$artifact" --output "$artifact"
  done
  for artifact in host-tools.toml setup.mjs worker.mjs bridge-peer.mjs device-host.mjs worktree-collector.mjs messaging-client.mjs RELEASE; do
    expected=$(awk -v file="$artifact" '\''$2 == file { print $1 }'\'' SHA256SUMS)
    if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$artifact" | awk '\''{ print $1 }'\'')
    else actual=$(shasum -a 256 "$artifact" | awk '\''{ print $1 }'\''); fi
    [ -n "$expected" ] && [ "$expected" = "$actual" ] || { echo "Checksum verification failed for $artifact" >&2; exit 1; }
  done
' sh "$installer_tmp" "$companion_origin"
section "Tools on this $host_kind"
export MISE_DATA_DIR="$companion_root/mise/data"
export MISE_CACHE_DIR="$companion_root/mise/cache"
export MISE_STATE_DIR="$companion_root/mise/state"
export MISE_CONFIG_DIR="$companion_root/mise/config"
export MISE_GLOBAL_CONFIG_FILE="$companion_root/mise/config/config.toml"
export MISE_SYSTEM_CONFIG_DIR="$companion_root/mise/system"
mkdir -p "$MISE_DATA_DIR" "$MISE_CACHE_DIR" "$MISE_STATE_DIR" "$MISE_CONFIG_DIR" "$MISE_SYSTEM_CONFIG_DIR"
# mise is always the official standalone binary. Do not adopt a package-manager
# installation whose update lifecycle Trellis cannot configure.
mise_path="$HOME/.local/bin/mise"
mkdir -p "$(dirname "$mise_path")"
mise_curl=$(command -v curl)
mise_network_bin="$installer_tmp/mise-network"
mkdir -p "$mise_network_bin"
cat >"$mise_network_bin/curl" <<'CURL_WRAPPER'
#!/bin/sh
exec "$TRELLIS_MISE_CURL" --connect-timeout 10 --max-time 60 --retry 2 --retry-max-time 60 "$@"
CURL_WRAPPER
chmod 755 "$mise_network_bin/curl"
mise_backup="$installer_tmp/mise-backup"
if [ -e "$mise_path" ] || [ -L "$mise_path" ]; then
  mv "$mise_path" "$mise_backup"
fi
if ! run_logged 'Install mise' sh -c '
  TRELLIS_MISE_CURL=$1
  PATH="$2:$PATH"
  MISE_INSTALL_PATH=$3
  export TRELLIS_MISE_CURL PATH MISE_INSTALL_PATH
  curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 https://mise.run | sh
' sh "$mise_curl" "$mise_network_bin" "$mise_path"; then
  if [ -e "$mise_backup" ] || [ -L "$mise_backup" ]; then mv "$mise_backup" "$mise_path"; fi
  exit 1
fi
if ! "$mise_path" --version >>"$installer_log" 2>&1; then
  rm -f "$mise_path"
  if [ -e "$mise_backup" ] || [ -L "$mise_backup" ]; then mv "$mise_backup" "$mise_path"; fi
  fail 'The standalone mise executable could not run.'
  exit 1
fi
rm -f "$mise_backup"
# These are user-global settings. Keep the companion's private configuration
# out of this command so interactive repository commands inherit the settings.
run_logged --quiet 'Configure mise automatic updates' sh -c '
  unset MISE_CONFIG_DIR MISE_GLOBAL_CONFIG_FILE MISE_SYSTEM_CONFIG_DIR
  "$1" settings set auto_update true
  "$1" settings set auto_update_check_duration 1d
' sh "$mise_path"
ready 'mise · standalone with automatic updates'
# Retire the previous private mise configuration. The companion defaults are
# generated from the repository manifest and contain only its bootstrap tools.
for variable in MISE_CONFIG_DIR MISE_GLOBAL_CONFIG_FILE MISE_SYSTEM_CONFIG_DIR; do
  case "$variable" in
    MISE_CONFIG_DIR) value=${MISE_CONFIG_DIR:-} ;;
    MISE_GLOBAL_CONFIG_FILE) value=${MISE_GLOBAL_CONFIG_FILE:-} ;;
    MISE_SYSTEM_CONFIG_DIR) value=${MISE_SYSTEM_CONFIG_DIR:-} ;;
  esac
  case "$value" in "$companion_root/toolchain"*) unset "$variable" ;; esac
done
unset MISE_NODE_VERSION MISE_PYTHON_VERSION MISE_GCLOUD_VERSION MISE_GH_VERSION
# The manifest is checksummed above and is never sourced as executable shell.
tool_version() {
  awk -v tool="$1" '
    /^\[tools\]$/ { tools=1; next }
    /^\[/ { tools=0 }
    tools && $1 == tool && $2 == "=" { gsub(/"/, "", $3); print $3 }
  ' "$installer_tmp/host-tools.toml"
}
node_version=$(tool_version node)
python_version=$(tool_version python)
gcloud_version=$(tool_version gcloud)
gh_version=$(tool_version gh)
ffmpeg_version=$(tool_version ffmpeg)
for version in "$node_version" "$python_version" "$gcloud_version" "$gh_version" "$ffmpeg_version"; do
  case "$version" in ''|*[!0-9.]*|.*) fail 'Invalid host tool version in the published manifest.'; exit 1 ;; esac
done
# Avoid inheriting the caller's checkout configuration while selecting host defaults.
run_mise() { "$mise_path" -C "$installer_tmp" "$@"; }
find_tool() {
  tool=$1; version=$2
  candidate=$(run_mise which "$tool" --tool="$tool@$version" 2>/dev/null || true)
  if tool_works "$candidate" "$tool"; then printf '%s\n' "$candidate"; fi
}
tool_works() {
  [ -n "$1" ] && [ -x "$1" ] || return 1
  if [ "$2" = node ]; then
    "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1
  else
    "$1" --version >/dev/null 2>&1
  fi
}
python_works() {
  [ -n "$1" ] && [ -x "$1" ] || return 1
  "$1" -c 'import sys; sys.exit(not ((3, 10) <= sys.version_info[:2] < (3, 16)))' >/dev/null 2>&1
}
find_python() {
  candidate=$(run_mise which python3 --tool="python@$python_version" 2>/dev/null || true)
  if python_works "$candidate"; then printf '%s\n' "$candidate"; fi
}
node_path=$(find_tool node "$node_version")
if [ -n "$node_path" ]; then ready 'Node.js · reused'
else
  run_logged --quiet 'Node.js' "$mise_path" -C "$installer_tmp" install "node@$node_version"
  node_path=$(run_mise which node --tool="node@$node_version")
  ready 'Node.js · installed'
fi
python_path=$(find_python)
if [ -n "$python_path" ]; then ready 'Python · reused'
else
  run_logged --quiet 'Python' "$mise_path" -C "$installer_tmp" install "python@$python_version"
  python_path=$(run_mise which python3 --tool="python@$python_version")
  ready 'Python · installed'
fi
if ! python_works "$python_path"; then fail 'Google CLI requires a working Python 3.10–3.15 interpreter.'; exit 1; fi
export CLOUDSDK_PYTHON="$python_path"
# The gcloud plugin probes versioned Python command names. Exclude inherited
# Mise shims while its parent Mise process holds the install lock, otherwise a
# probe such as python3.14 can re-enter Mise and wait on its own lock forever.
gcloud_install_path="$(dirname "$python_path"):/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
gcloud_path=$(find_tool gcloud "$gcloud_version")
gcloud_result=reused
if [ -z "$gcloud_path" ]; then
  # Refresh the backend before installing: old versions requested sudo for Python.
  log 'Prepare Google Cloud CLI installer'
  PATH="$gcloud_install_path" run_logged --quiet 'Google CLI' "$mise_path" -C "$installer_tmp" plugins install gcloud
  log 'Update Google Cloud CLI installer'
  PATH="$gcloud_install_path" run_logged --quiet 'Google CLI' "$mise_path" -C "$installer_tmp" plugins update gcloud
  PATH="$gcloud_install_path" run_logged --quiet 'Google CLI' "$mise_path" -C "$installer_tmp" install "gcloud@$gcloud_version"
  gcloud_path=$(run_mise which gcloud --tool="gcloud@$gcloud_version")
  gcloud_result=installed
fi
# Keep tunnel acceleration out of system Python and other mise Python projects.
# -I excludes inherited PYTHONPATH, user packages and the current directory.
gcloud_python="$companion_root/gcloud-python/bin/python"
if ! "$gcloud_python" -I -c 'import numpy; assert numpy.__version__ == "2.5.3"' >/dev/null 2>&1; then
  run_logged --quiet 'Google CLI' "$python_path" -I -m venv "$companion_root/gcloud-python"
  run_logged --quiet 'Google CLI' "$gcloud_python" -I -m pip --isolated install --disable-pip-version-check --only-binary=:all: numpy==2.5.3
fi
run_logged --quiet 'Google CLI' "$gcloud_python" -I -c 'import numpy; assert numpy.__version__ == "2.5.3"'
managed_bin="$companion_root/bin"
mkdir -p "$managed_bin"
shell_quote() { printf "'"; printf '%s' "$1" | sed "s/'/'\\\\''/g"; printf "'"; }
# Every consumer, including launchd/systemd and SSH ProxyCommand, uses this
# wrapper so it does not depend on the environment of the installing shell.
{
  printf '#!/bin/sh\n'
  printf 'unset PYTHONPATH PYTHONHOME VIRTUAL_ENV CLOUDSDK_PYTHON_ARGS\n'
  printf 'export CLOUDSDK_PYTHON=%s\n' "$(shell_quote "$gcloud_python")"
  printf 'export CLOUDSDK_PYTHON_SITEPACKAGES=1 PYTHONNOUSERSITE=1 CLOUDSDK_PYTHON_ARGS=-I\n'
  printf 'exec %s "$@"\n' "$(shell_quote "$gcloud_path")"
} >"$managed_bin/gcloud"
chmod 755 "$managed_bin/gcloud"
gcloud_path="$managed_bin/gcloud"
run_logged --quiet 'Google CLI' "$gcloud_path" --version
ready "Google CLI · $gcloud_result"
gh_path=$(find_tool gh "$gh_version")
if [ -n "$gh_path" ]; then ready 'GitHub CLI · reused'
else
  run_logged --quiet 'GitHub CLI' "$mise_path" -C "$installer_tmp" install "gh@$gh_version"
  gh_path=$(run_mise which gh --tool="gh@$gh_version")
  ready 'GitHub CLI · installed'
fi
# Device-hub video capture runs outside repository shells. Install its encoder
# with mise and expose an owned launcher on the host PATH, on both Mac and VM.
ffmpeg_path=$(run_mise which ffmpeg --tool="ffmpeg@$ffmpeg_version" 2>/dev/null || true)
if [ -z "$ffmpeg_path" ] || ! "$ffmpeg_path" -version >/dev/null 2>&1; then
  run_logged --quiet 'Device video encoder' "$mise_path" -C "$installer_tmp" install "ffmpeg@$ffmpeg_version"
  ffmpeg_path=$(run_mise which ffmpeg --tool="ffmpeg@$ffmpeg_version")
fi
for command in ffmpeg ffprobe; do
  target=$(run_mise which "$command" --tool="ffmpeg@$ffmpeg_version")
  device_launcher="$HOME/.local/bin/$command"
  if [ -e "$device_launcher" ] && ! grep -q 'Trellis device tool launcher' "$device_launcher"; then
    continue
  fi
  escaped_target=$(printf '%s' "$target" | sed "s/'/'\\\\''/g")
  cat >"$installer_tmp/$command" <<DEVICE_TOOL
#!/bin/sh
# Trellis device tool launcher
exec '$escaped_target' "\$@"
DEVICE_TOOL
  chmod 755 "$installer_tmp/$command"
  mv "$installer_tmp/$command" "$device_launcher"
done
ready 'Device video encoder · ready'
# Preserve old shim targets while replacing our obsolete mise wrapper with the native binary.
if [ -e "$managed_bin/mise-executable" ]; then
  ln -sf "$managed_bin/mise-executable" "$managed_bin/mise"
fi
# Retain the gcloud launcher for installed SSH and service configurations.
for command in node npm npx python python3 gh trellis; do
  if [ -L "$managed_bin/$command" ]; then rm "$managed_bin/$command"; fi
done
if [ "$host_kind" = VM ]; then
  PATH="$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
  unset NODE_PATH NODE_OPTIONS PYTHONPATH PYTHONHOME VIRTUAL_ENV
else PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"; fi
export PATH
log 'Install verified companion programs'
mv "$installer_tmp/setup.mjs" "$companion_root/setup.mjs"
mv "$installer_tmp/worker.mjs" "$companion_root/worker.mjs"
mv "$installer_tmp/bridge-peer.mjs" "$companion_root/bridge-peer.mjs"
mv "$installer_tmp/device-host.mjs" "$companion_root/device-host.mjs"
mv "$installer_tmp/worktree-collector.mjs" "$companion_root/worktree-collector.mjs"
mv "$installer_tmp/messaging-client.mjs" "$companion_root/messaging-client.mjs"
release_backup="$installer_tmp/previous-RELEASE"
if [ -f "$companion_root/RELEASE" ]; then cp "$companion_root/RELEASE" "$release_backup"; fi
release_pending=1
mv "$installer_tmp/RELEASE" "$companion_root/RELEASE"
mkdir -p "$companion_root/toolchain"
mv "$installer_tmp/host-tools.toml" "$companion_root/toolchain/mise.toml"
mv "$installer_tmp/SHA256SUMS" "$companion_root/SHA256SUMS"
launcher_dir="$HOME/.local/bin"
launcher="$launcher_dir/trellis"
mkdir -p "$launcher_dir"
if [ -e "$launcher" ] && ! grep -q 'Trellis Mac companion launcher' "$launcher"; then
  say "The path $launcher belongs to another command. Preserve it and choose a different PATH before retrying."
  exit 1
fi
launcher_tmp="$installer_tmp/trellis"
cat >"$launcher_tmp" <<LAUNCHER
#!/bin/sh
# Trellis Mac companion launcher
export PATH="$PATH"
export CLOUDSDK_PYTHON="$gcloud_python" CLOUDSDK_PYTHON_SITEPACKAGES=1 CLOUDSDK_PYTHON_ARGS=-I
if [ "\$#" -eq 0 ]; then set -- help; fi
if [ "\${1:-}" = uninstall ]; then
  shift
  set -- --uninstall "\$@"
fi
exec "$node_path" "$companion_root/setup.mjs" "\$@" --gcloud "$gcloud_path" --gh "$gh_path" --node "$node_path" --worker "$companion_root/worker.mjs"
LAUNCHER
chmod 755 "$launcher_tmp"
mv "$launcher_tmp" "$launcher"
legacy_launcher="$launcher_dir/trellis-companion"
if [ -f "$legacy_launcher" ] && grep -q 'Trellis Mac companion launcher' "$legacy_launcher"; then rm "$legacy_launcher"; fi
ready "trellis command · installed"
log "Command installed: trellis (from $launcher)"
"$node_path" "$companion_root/setup.mjs" --install-completions
if [ "$host_kind" = VM ]; then section 'VM checkout'; else section 'Remote Development'; fi
set -- --install
if [ -n "$vm_checkout" ]; then set -- "$@" --vm-checkout "$vm_checkout"; fi
if [ "$verbose" -eq 1 ]; then set -- "$@" --verbose; fi
if [ "$non_interactive" -eq 1 ]; then set -- "$@" --non-interactive; fi
set +e
if [ "$non_interactive" -eq 0 ] && ( : < /dev/tty ) 2>/dev/null; then
  "$node_path" "$companion_root/setup.mjs" "$@" --gcloud "$gcloud_path" --gh "$gh_path" --node "$node_path" --worker "$companion_root/worker.mjs" < /dev/tty
else
  "$node_path" "$companion_root/setup.mjs" "$@" --gcloud "$gcloud_path" --gh "$gh_path" --node "$node_path" --worker "$companion_root/worker.mjs" < /dev/null
fi
node_status=$?
set -e
if [ "$node_status" -ne 0 ]; then
  exit "$node_status"
fi
release_pending=0
elapsed=$(( $(date +%s) - started_at ))
if [ "$host_kind" = VM ]; then section 'Service details'; else section 'Connection details'; fi
command_name=$launcher
case ":$calling_path:" in *":$HOME/.local/bin:"*) command_name=trellis ;; esac
if [ "$host_kind" = Mac ]; then say 'SSH command  ssh trellis-remote-dev'; fi
say "Status       $command_name status"
say "Update       $command_name update"
say "Repair       $command_name repair"
say "Completions  $HOME/.local/share/trellis/completions"
say "Log          $display_log"
ready_message='Ready. Your Mac is connected.'
if [ "$host_kind" = VM ]; then ready_message='Ready. Services are running on this VM.'; fi
if [ "$ui_mode" = rich ]; then
  printf ' %s\n ' "$rail" >&3; printf '%s' "$close_symbol" >&3; printf '  %s\n    ' "$ready_message" >&3; paint 2 "${elapsed}s"; printf '\n' >&3
else printf '[ok] %s (%ss)\n' "$ready_message" "$elapsed" >&3; fi
