#!/usr/bin/env bash
# unoverse deploy — deploy to production VM from local

# ── Provisioning, spoken plainly ─────────────────────────────────────────────
#
# `terraform apply` shows ~200 lines of attributes and asks for a typed "yes". That is
# the right ceremony for an infrastructure engineer and the wrong first impression for a
# developer, who needs to know what will exist and what it costs. So: plan into a file,
# summarise it (tfsummary.mjs), take ONE clear answer, apply that exact plan.
#
# Nothing is softened. Destroys are shouted, the full technical plan is one keystroke
# away, and the SAVED plan is what runs, so what was approved is what happens. Identical
# for every cloud: the summary reads the plan JSON, which is provider-agnostic.
# Terraform is one static binary: install it from the official release rather than
# sending anyone to brew, whose compile chain can demand Xcode Command Line Tools
# updates for a tool that needs no compiling.
# Give this universe's droplet access to an ADOPTED database, additively.
#
# Terraform cannot do this safely: `digitalocean_database_firewall` replaces the whole
# trusted-sources list, so pointing it at a borrowed cluster deletes the operator's own
# IP, their other droplets and their apps. Read the rules, add ours if missing, write
# them back. Nothing else moves, and running it twice changes nothing.
# Add or remove THIS universe's droplet in an adopted cluster's trusted sources.
#   _adopted_db_access <cloud> grant|revoke [droplet_id]
# Symmetric on purpose. The grant existed and nothing revoked it, so a teardown left a
# stale rule behind — and worse, terraform's own destroy of an authoritative firewall
# resource PUT an EMPTY list and locked the operator out of a database it had borrowed.
# Both directions now touch exactly one rule and never the list.
# Keep the DEVELOPER'S OWN MACHINE able to reach an adopted database, across networks.
#
# A managed cluster's trusted sources are a list of IP addresses, and a laptop's address is
# not a stable thing: a different office, a hotspot, a train, and local `npm run dev` dies
# on "Connection terminated unexpectedly" ten seconds into boot, with nothing on screen
# connecting that to the network you joined this morning. The droplet's own firewall already
# follows the operator around (_ensure_ground_config re-checks admin_cidr every deploy);
# this is the same idea for the one rule that lets a laptop in.
#
# IT ONLY EVER REMOVES ITS OWN. The addresses it added are recorded in .unoverse/trusted-ips,
# and nothing absent from that file is touched — the operator's other machines, their
# colleagues, their CI, their other droplets and apps all survive untouched. That rule
# exists because the opposite mistake is what wiped this exact cluster's firewall once
# already: an authoritative write that assumed the whole list was ours to own.
_operator_db_access() {
  # `$2` is the cluster, when the CALLER already knows it. The monorepo has no ground to
  # read one from — it develops against a cluster named only in .env — so re-deriving it
  # from terraform.tfvars here meant this returned 0 and did nothing in the one place the
  # developer actually types `npm run dev`.
  local cloud="$1" dir="$ROOT/infra/$cloud" cluster="${2:-}"
  [ -n "$cluster" ] || cluster=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  [ -n "$cluster" ] || return 0
  [ -n "${DIGITALOCEAN_TOKEN:-}" ] || return 0
  mkdir -p "$ROOT/.unoverse"

  node - "$cluster" "$ROOT/.unoverse/trusted-ips" <<'NODE'
const [cluster, ledgerPath] = process.argv.slice(2);
const fs = require("fs");
const T = process.env.DIGITALOCEAN_TOKEN;
const H = { Authorization: `Bearer ${T}`, "Content-Type": "application/json" };
const api = (p, o = {}) => fetch(`https://api.digitalocean.com/v2${p}`, { headers: H, ...o });
const read = () => { try { return fs.readFileSync(ledgerPath, "utf8").split("\n").map(s => s.trim()).filter(Boolean); } catch { return []; } };

/**
 * A MACHINE IS A SET OF ADDRESSES, NOT ONE.
 *
 * This asked "what is my IP?" once and registered the answer, deleting whatever it had
 * registered before. That is correct for one stable address and actively harmful without
 * one: a connection that leaves through a different egress than the probe did arrives
 * from an address nobody trusted. Observed live 2026-08-10 on a laptop whose ISP
 * alternates two addresses roughly evenly — six probes returned three of each. Each run
 * registered whichever it drew and REMOVED the other, so running the command again
 * (exactly what a locked-out developer does) deleted the rule that was working, and the
 * database stayed unreachable through both.
 *
 * So probe several times and register the DISTINCT SET. Replacement is unchanged: the
 * previous set still goes, so rules never pile up. One address for a normal machine, two
 * for a flapping one, and the developer never has to know which they are.
 */
/**
 * EACH PROBE MUST BE ITS OWN CONNECTION, or the loop is theatre. `fetch` keeps the
 * connection alive, so six calls to one host ride ONE socket and one egress, and the
 * probe reports a single address on the very machine that has two. Measured: six
 * keep-alive probes returned the same address six times, while six separate `curl`
 * processes returned three of each. `Connection: close` retires the socket, and rotating
 * the endpoint keeps a single provider's own routing from deciding the answer.
 */
const ENDPOINTS = ["https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me/ip"];
const PROBES = 9;
const egress = async () => {
  const seen = new Set();
  for (let i = 0; i < PROBES; i++) {
    try {
      const res = await fetch(ENDPOINTS[i % ENDPOINTS.length], {
        headers: { connection: "close" },
        signal: AbortSignal.timeout(5000),
      });
      const v = (await res.text()).trim();
      if (/^\d+\.\d+\.\d+\.\d+$/.test(v)) seen.add(v);
    } catch {}
  }
  return [...seen];
};

(async () => {
  const ips = await egress();
  if (!ips.length) return;
  const isOurs = (v) => ips.includes(v);

  const list = await (await api("/databases")).json();
  const db = (list.databases || []).find((d) => d.name === cluster);
  if (!db) return;

  const fw = await (await api(`/databases/${db.id}/firewall`)).json();
  let rules = (fw.rules || []).map((r) => ({ type: r.type, value: r.value }));

  const ours = read();                                   // only these may be removed
  const stale = ours.filter((v) => !isOurs(v));
  const missing = ips.filter((v) => !rules.some((r) => r.type === "ip_addr" && r.value === v));
  const label = ips.join(", ");
  // SAY SO EVEN WHEN NOTHING CHANGES. Returning silently made the command look like it had
  // failed: the developer typed it because they could not connect, and got a blank line.
  if (!missing.length && !stale.length) {
    fs.writeFileSync(ledgerPath, ips.join("\n") + "\n");
    console.log(`  \x1b[32m✓\x1b[0m This machine (${label}) can already reach ${cluster} \x1b[2m(nothing changed)\x1b[0m`);
    return;
  }

  rules = rules.filter((r) => !(r.type === "ip_addr" && stale.includes(r.value)));
  for (const v of missing) rules.push({ type: "ip_addr", value: v });

  const res = await api(`/databases/${db.id}/firewall`, { method: "PUT", body: JSON.stringify({ rules }) });
  if (!res.ok) {
    console.log(`  \x1b[33m!\x1b[0m Could not update ${cluster}'s trusted sources — add ${label} by hand`);
    return;
  }
  fs.writeFileSync(ledgerPath, ips.join("\n") + "\n");
  const changed = ips.length > 1 ? `${ips.length} addresses for this machine` : "only ours changed";
  console.log(`  \x1b[32m✓\x1b[0m This machine (${label}) may reach ${cluster} \x1b[2m(${rules.length} trusted sources, ${changed})\x1b[0m`);
})().catch(() => {});
NODE
}

# unoverse db-allow — let THIS machine reach this universe's database.
#
# Typed, never automatic. It changes a live database's network ACL, and doing that as a
# side effect of `start` meant a coffee shop's shared address quietly joined a production
# cluster's trusted sources. Typing it is the consent.
#
# What makes it safe to hand a developer: the DigitalOcean token gates it, so nobody
# without your cloud credential can run it at all, and the database still demands its own
# password afterwards. This opens a door in the network layer; it does not open the
# database.
cmd_db_allow() {
  local cloud
  # DIGITALOCEAN ONLY, and it is not an oversight. This joins a MANAGED DATABASE's
  # trusted-source list through the DO API, and AWS has no equivalent to join: RDS
  # reachability is a VPC security group, owned by Terraform and changed by `deploy`, not by
  # a developer's laptop asking an API at runtime. Offering `db-allow aws` produced a
  # ground-picker prompt for a cloud that would then have done nothing.
  # A GROUND IS OPTIONAL HERE. This used to `return 1` the moment the picker found none,
  # which made the .env fallback below unreachable in the monorepo — the one checkout that
  # has no ground and the one the fallback was written for. Only ask the picker when there
  # is something to pick, so an ambiguous two-ground universe still stops, while a
  # groundless checkout proceeds to look the cluster up where it actually knows it.
  cloud=""
  if ls "$ROOT"/infra/*/terraform.tfvars >/dev/null 2>&1; then
    SELF_CMD="unoverse db-allow" \
    GROUNDS_ONLY="digitalocean" \
    GROUNDS_ONLY_WHY="On AWS the database is reached through its VPC security group, which the ground owns — change it in infra/aws and run ${BOLD}unoverse deploy aws${NC}." \
    cloud=$(_pick_ground "${1:-}") || return 1
  fi

  local cluster=""
  [ -n "$cloud" ] && cluster=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$ROOT/infra/$cloud/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')

  # THE MONOREPO NEEDS THIS TOO, and it has no ground. The platform's own checkout is
  # developed against a managed cluster named only in .env, so a ground-only lookup found
  # nothing and the one place the developer actually types `npm run dev` was the one place
  # this could not help. A cluster host is `<name>-do-user-...`, so the name is right there
  # in DATABASE_URL.
  if [ -z "$cluster" ]; then
    # `-n` + `p`: print ONLY when the substitution matched. The portable way to say "extract
    # or nothing". This was `s|…|\1|; t; d`, which is GNU: BSD sed reads everything after `t`
    # as a LABEL, so on macOS it died with `undefined label '; d'`, printed nothing, and the
    # command then reported "this database has no trusted-source list" — a real cluster
    # reported as an open one, on the platform's own machine.
    cluster=$(grep -E '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null | head -1 \
      | sed -nE 's|.*@([a-z0-9-]+)-do-user-[^.]*\..*|\1|p')
  fi

  if [ -z "$cluster" ]; then
    ok "This database has no trusted-source list to join"
    info "Nothing to do — you can already reach it"
    return 0
  fi

  _ground_credentials
  if [ -z "${DIGITALOCEAN_TOKEN:-}" ]; then
    fail "No DigitalOcean credential. Run ${BOLD}unoverse deploy${NC} once, or ${BOLD}doctl auth init${NC}"
    return 1
  fi

  echo ""
  # Hand the cluster over: we already resolved it, from a ground OR from .env, and the
  # helper cannot re-derive the .env case (there is no tfvars to read).
  _operator_db_access "$cloud" "$cluster"
  echo ""
  info "Run this again whenever you change network"
  echo ""
}

_adopted_db_access() {
  local cloud="$1" dir="$ROOT/infra/$cloud" cluster droplet_id
  local mode="$2"
  cluster=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  [ -n "$cluster" ] || return 0
  [ -n "${DIGITALOCEAN_TOKEN:-}" ] || return 0
  droplet_id="${3:-$(terraform -chdir="$dir" state show digitalocean_droplet.app 2>/dev/null | awk -F'"' '/^ *id *=/{print $2; exit}')}"
  [ -n "$droplet_id" ] || return 0

  node - "$cluster" "$droplet_id" "$mode" <<'NODE'
const [cluster, droplet, mode] = process.argv.slice(2);
const T = process.env.DIGITALOCEAN_TOKEN;
const H = { Authorization: `Bearer ${T}`, "Content-Type": "application/json" };
const api = (p, o = {}) => fetch(`https://api.digitalocean.com/v2${p}`, { headers: H, ...o });
(async () => {
  const list = await (await api("/databases")).json();
  const db = (list.databases || []).find((d) => d.name === cluster);
  if (!db) return;
  const fw = await (await api(`/databases/${db.id}/firewall`)).json();
  let rules = (fw.rules || []).map((r) => ({ type: r.type, value: r.value }));
  const mine = (r) => r.type === "droplet" && String(r.value) === String(droplet);
  const has = rules.some(mine);
  if (mode === "revoke") {
    if (!has) return;
    rules = rules.filter((r) => !mine(r));   // ours only; every other rule survives
  } else {
    if (has) return;
    rules.push({ type: "droplet", value: String(droplet) });
  }
  const res = await api(`/databases/${db.id}/firewall`, { method: "PUT", body: JSON.stringify({ rules }) });
  const what = mode === "revoke" ? "no longer reaches" : "may reach";
  console.log(res.ok
    ? `  \x1b[32m✓\x1b[0m This universe ${what} ${cluster} \x1b[2m(one rule changed, ${rules.length} left in place)\x1b[0m`
    : `  \x1b[33m!\x1b[0m Could not update ${cluster}'s trusted sources — adjust droplet ${droplet} by hand`);
})().catch(() => {});
NODE
}

# The cloud credential terraform needs, from where the CLI already put it.
#
# By design the DO token lives in doctl's config and never in a repo file (main.tf: an
# empty var falls through to DIGITALOCEAN_TOKEN). This used to sit INSIDE cmd_deploy, so
# `unoverse destroy` ran with no credential at all and every API call came back 401 —
# the same class of bug as the Postgres question living in one branch of two. Anything
# that talks to a ground calls this first.
# WHICH GROUND THIS COMMAND MEANS.
#   _pick_ground [name]   →  echoes the ground, or fails with a reason
#
# Every command used `for g in digitalocean aws; do ... break; done`: first match wins, and
# digitalocean is first in the list. With both grounds configured there was no way to reach
# the AWS one — and `unoverse destroy` would have torn down DigitalOcean while the operator
# meant AWS, silently, because the word "aws" had nowhere to go.
#
# So a ground can be named. With one configured, naming it is optional and the answer is
# obvious. With two, the name is REQUIRED rather than guessed: a wrong guess here destroys
# the wrong cloud.
_pick_ground() {
  local want="${1:-}" g found=""
  case "$want" in
    do|digitalocean) want="digitalocean" ;;
    aws|amazon)      want="aws" ;;
    "")              want="" ;;
    *) fail "Unknown ground '$want'. Use ${BOLD}digitalocean${NC} or ${BOLD}aws${NC}" >&2; return 1 ;;
  esac

  # A command may not apply to every cloud. `GROUNDS_ONLY` names the ones it does, so a
  # ground that cannot answer is never offered as a choice and never named in the
  # "say which" prompt. Unset means all of them, which is every other caller.
  local configured=()
  for g in digitalocean aws; do
    [ -f "$ROOT/infra/$g/terraform.tfvars" ] || continue
    if [ -n "${GROUNDS_ONLY:-}" ] && [[ " ${GROUNDS_ONLY} " != *" $g "* ]]; then continue; fi
    configured+=("$g")
  done

  # Asked for a ground this command cannot serve: say so plainly rather than "no such
  # ground here", which sends someone off to create one that would not have helped.
  if [ -n "$want" ] && [ -n "${GROUNDS_ONLY:-}" ] && [[ " ${GROUNDS_ONLY} " != *" $want "* ]]; then
    fail "${SELF_CMD:-This command} does not apply to $want. ${GROUNDS_ONLY_WHY:-}" >&2
    return 1
  fi

  if [ -n "$want" ]; then
    for g in "${configured[@]}"; do
      [ "$g" = "$want" ] && { printf '%s' "$want"; return 0; }
    done
    fail "No $want ground here. Run ${BOLD}unoverse deploy $want${NC} to create one" >&2
    return 1
  fi

  case "${#configured[@]}" in
    0) return 1 ;;                        # caller offers to create one
    1) printf '%s' "${configured[0]}"; return 0 ;;
    *)
      # Two grounds and no name. Refuse rather than pick: this decides which cloud a
      # destroy lands on.
      # Names the grounds THIS command can actually serve, not both clouds unconditionally:
      # offering a choice that would be refused is worse than not offering it.
      fail "Two grounds are configured. Say which: ${BOLD}${SELF_CMD:-unoverse deploy} ${configured[0]}${NC} or ${BOLD}${SELF_CMD:-unoverse deploy} ${configured[1]}${NC}" >&2
      return 1
      ;;
  esac
}

_ground_credentials() {
  [ -n "${DIGITALOCEAN_TOKEN:-}" ] && return 0
  local _docfg
  for _docfg in "$HOME/Library/Application Support/doctl/config.yaml" "$HOME/.config/doctl/config.yaml"; do
    [ -f "$_docfg" ] || continue
    DIGITALOCEAN_TOKEN=$(awk '/access-token:/{print $2; exit}' "$_docfg")
    if [ -n "$DIGITALOCEAN_TOKEN" ]; then export DIGITALOCEAN_TOKEN; return 0; fi
  done
  return 0
}

_ensure_terraform() {
  command -v terraform >/dev/null 2>&1 && return 0
  local REPLY tf_arch tf_os tf_v="1.9.8" tf_dir
  read -r -p "  Terraform is needed. Install it now (official binary, ~30 MB)? [Y/n] " REPLY
  [[ "$REPLY" =~ ^[Nn]$ ]] && { fail "terraform is needed to continue"; return 1; }
  tf_arch=$(uname -m)
  case "$tf_arch" in arm64|aarch64) tf_arch=arm64 ;; *) tf_arch=amd64 ;; esac
  [ "$(uname -s)" = "Darwin" ] && tf_os=darwin || tf_os=linux
  tf_dir=$(mktemp -d)
  curl -fsSL -o "$tf_dir/tf.zip" "https://releases.hashicorp.com/terraform/${tf_v}/terraform_${tf_v}_${tf_os}_${tf_arch}.zip" \
    && unzip -o -q "$tf_dir/tf.zip" -d "$tf_dir" || { fail "download failed"; rm -rf "$tf_dir"; return 1; }
  if [ -w /usr/local/bin ]; then mv "$tf_dir/terraform" /usr/local/bin/terraform
  else sudo mv "$tf_dir/terraform" /usr/local/bin/terraform || { fail "could not install to /usr/local/bin"; rm -rf "$tf_dir"; return 1; }
  fi
  rm -rf "$tf_dir"
  ok "terraform $(terraform version | head -1 | awk '{print $2}') installed"
}


# Everything that must be TRUE before a plan is worth looking at: every value filled,
# and the database decision made. It lives in a function because deploy has one flow now
# and this step belongs in it — it used to sit inside the "no .env.production yet" branch,
# so an already-initialised ground skipped straight to planning and the Postgres question
# was never asked. Two paths, and the configuration existed in only one of them.
_ensure_ground_config() {
  local cloud="$1"
  local tfv="$ROOT/infra/$cloud/terraform.tfvars"
    _tf_put() {  # key value — safe replacement via node (values may hold sed specials)
      node -e 'const fs=require("fs");const[f,k,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(new RegExp("^"+k+"(\\s*)=\\s*\"FILL_ME\"","m"),k+"$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$1" "$2"
    }
    _tf_fill() {  # key envname prompt
      local key="$1" envname="$2" prompt="$3" val
      grep -q "^${key}[[:space:]]*=[[:space:]]*\"FILL_ME\"" "$tfv" 2>/dev/null || return 0
      val=$(grep "^${envname}=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-)
      if [ -n "$val" ]; then
        _tf_put "$key" "$val"
        ok "$key ${DIM}from your .env${NC}"
      else
        read -r -p "  $prompt: " val
        [ -n "$val" ] && _tf_put "$key" "$val"
      fi
    }
    echo ""

  # YOUR IP MOVED. admin_cidr is captured once, when the ground is generated, and it is
  # the ONLY address the firewall lets near port 22. Home broadband, a phone hotspot or a
  # different office and the address is stale — at which point terraform applies happily,
  # every resource is correct, and the ship step dies on "connect to host ... port 22:
  # Operation timed out" with nothing on screen connecting the two. Check it every deploy
  # and just fix it: the firewall is ours, and a one-rule update is not a decision.
  local cur_ip cur_cidr have_cidr
  cur_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
  have_cidr=$(grep -E '^admin_cidr[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  if [ -n "$cur_ip" ] && [ -n "$have_cidr" ] && [ "$have_cidr" != "FILL_ME" ]; then
    cur_cidr="$cur_ip/32"
    if [ "$have_cidr" != "$cur_cidr" ]; then
      node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^admin_cidr(\s*)=\s*"[^"]*"/m,"admin_cidr$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$cur_cidr"
      ok "Your address changed ${DIM}($have_cidr → $cur_cidr). Admin access follows you${NC}"
    fi
  fi

  # PRODUCTION'S DATABASE IS NOT THE ONE IN .env, AND THE QUESTION MUST SAY SO.
  #
  # `.env` holds the DEVELOPMENT database — what `npm run dev` talks to on the laptop. The
  # deployed universe gets its own, so a developer cannot break production by experimenting
  # locally. That separation is right and is the default.
  #
  # What was wrong was the words. This asked "You already have a database — use it?", where
  # "it" meant the CLUSTER and "use" meant "create a new database inside it". A developer
  # who had typed a DATABASE_URL during setup read that as "use the database I gave you",
  # answered yes, and got an empty one — then could not find their 21 workflows and
  # reasonably concluded the deploy had lost them. Nothing was lost; the sentence was.
  #
  # So: name the cluster, say a NEW database goes in it, and say what happens to the one in
  # .env. Anyone wanting production to share the development database sets byo_postgres_url
  # deliberately, which is a different and much louder act.
  if [ "$cloud" = "digitalocean" ] \
     && ! grep -q '^existing_pg_cluster_name' "$tfv" 2>/dev/null \
     && ! grep -q '^byo_postgres_url' "$tfv" 2>/dev/null; then
    local found env_db db_name
    found=$(grep '^#[[:space:]]*existing_pg_cluster_name' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
    env_db=$(grep -E '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-)
    db_name=$(echo "$env_db" | sed -E 's|.*/([^/?]+)(\?.*)?$|\1|')

    echo ""
    echo -e "  ${CYAN}${BOLD}Which database should the DEPLOYED universe use?${NC}"
    echo ""
    echo -e "  ${DIM}Your .env is your local development database and does not change.${NC}"
    echo ""
    local opt_new_db="" opt_share=""
    [ -n "$found" ] && opt_new_db="Its own new database on ${found}   free"
    [ -n "$db_name" ] && opt_share="The same one you develop against   ${db_name}"

    # Order is the recommendation. Its own database first: production and development stay
    # independent, which is what almost everyone wants and what nobody regrets.
    if [ -n "$opt_new_db" ] && [ -n "$opt_share" ]; then
      pick_option "$opt_new_db" "$opt_share" "A new cluster of its own   ~\$15/month"
    elif [ -n "$opt_new_db" ]; then
      pick_option "$opt_new_db" "A new cluster of its own   ~\$15/month"
    elif [ -n "$opt_share" ]; then
      pick_option "$opt_share" "A new cluster of its own   ~\$15/month"
    else
      PICKED=99   # nothing to reuse: the ground provisions a cluster, no question worth asking
    fi

    local choice=""
    case "$PICKED" in
      0) [ -n "$opt_new_db" ] && choice="own" || choice="share" ;;
      1) [ -n "$opt_new_db" ] && [ -n "$opt_share" ] && choice="share" || choice="fresh" ;;
      *) choice="fresh" ;;
    esac

    case "$choice" in
      own)
        node -e 'const fs=require("fs");const[f]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^#\s*(existing_pg_cluster_name\s*=.*)$/m,"$1");fs.writeFileSync(f,s)' "$tfv"
        ok "Production gets its own database on $found ${DIM}(your development data is untouched)${NC}"
        ;;
      share)
        node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^#?\s*byo_postgres_url\s*=.*$/m,"byo_postgres_url = "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$env_db"
        ok "Production reads ${BOLD}$db_name${NC} ${DIM}(the same database you develop against — local changes are live)${NC}"
        ;;
      *) ok "A new cluster will be created for production" ;;
    esac
  fi

  # CAN THIS MACHINE ACTUALLY REACH WHAT IT IS ABOUT TO BUILD?
  #
  # An EC2 key pair the operator does not hold applies perfectly and then fails at the ship
  # step: eleven minutes of RDS and ElastiCache provisioning, then "Permission denied
  # (publickey)". Generating the ground correctly is not enough — tfvars is the developer's
  # file and `unoverse update` never rewrites it, so a ground made before this check keeps
  # its unusable key forever and fails the same way every time.
  #
  # Checked here, before the plan, because the cost of being wrong is money and eleven
  # minutes, and the fix is one line in a file this command already edits.
  if [ "$cloud" = "aws" ]; then
    local named_key pub_key
    named_key=$(grep -E '^ssh_key_name[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]*)".*/\1/')
    if [ -n "$named_key" ] && ! grep -qE '^operator_public_key[[:space:]]*=[[:space:]]*"ssh-' "$tfv" 2>/dev/null; then
      for pub_key in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub" ""; do
        [ -n "$pub_key" ] && [ -f "$pub_key" ] && break
      done
      if [ -n "$pub_key" ]; then
        node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");
          s=s.replace(/^ssh_key_name\s*=.*$/m,"ssh_key_name = \"\"  # empty = terraform uploads operator_public_key below\noperator_public_key = "+JSON.stringify(v)+"   # your key: the deploy ssh-es from this machine");
          fs.writeFileSync(f,s)' "$tfv" "$(cat "$pub_key")"
        ok "SSH key: your own ${DIM}($(basename "$pub_key") — replacing '"'"'$named_key'"'"', whose private half is not on this machine)${NC}"
      else
        warn "No ssh key on this machine. The deploy will build, then fail to reach the server"
        info "Make one with ${BOLD}ssh-keygen -t ed25519${NC} and run this again"
      fi
    fi
  fi

  _tf_fill docr_token     DOCR_TOKEN     "Registry access token (from your Unoverse admin)"
  _tf_fill openai_api_key OPENAI_API_KEY "OPENAI_API_KEY (powers the platform's AI)"
  if grep -q '^auth_issuer[[:space:]]*=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
    local env_auth
    env_auth=$(grep "^AUTH_ISSUER=" "$ROOT/.env" 2>/dev/null | cut -d= -f2-)
    if [ -z "$env_auth" ]; then
      echo ""
      info "A deployed universe requires a login. Local auth-off does not deploy"
      info "${DIM}(Auth0 or any OIDC provider; the issuer looks like https://your-tenant.auth0.com)${NC}"
    fi
  fi
  # AWS ONLY. Cognito needs a first administrator, and nothing else in the CLI collects an
  # email — so this was left FILL_ME and the deploy gave up. It is one question.
  if [ "$cloud" = "aws" ] && grep -q '^admin_email[[:space:]]*=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
    local admin_email=""
    echo ""
    echo -e "  ${CYAN}${BOLD}Who is the first administrator?${NC}"
    echo -e "  ${DIM}They get a Cognito account with every role, and an invitation by email.${NC}"
    echo ""
    while [ -z "$admin_email" ]; do
      read -r -p "  Email: " admin_email
      case "$admin_email" in
        *@*.*) : ;;
        *) [ -n "$admin_email" ] && warn "That does not look like an email address"; admin_email="" ;;
      esac
    done
    _tf_put admin_email "$admin_email"
    ok "First administrator: ${BOLD}$admin_email${NC}"
  fi

  _tf_fill auth_issuer    AUTH_ISSUER    "AUTH_ISSUER"
  _tf_fill auth_client_id AUTH_CLIENT_ID "AUTH_CLIENT_ID"

  # VALUES only: the file's own header comment says the word FILL_ME, and matching
  # it declared a complete file blank.
  if grep -qE '=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
    echo ""
    # NAME THE VALUES, AND NEVER HAND BACK TERRAFORM. This said "fill them, then cd
    # infra/<cloud> && terraform init && terraform apply" — the homework this CLI exists to
    # remove — and then named `unoverse deploy` without the ground, which on a universe with
    # two grounds is a command that refuses. Say which values, say where, say the command
    # that resumes.
    warn "These still need a value in ${BOLD}infra/$cloud/terraform.tfvars${NC}:"
    echo ""
    grep -nE '=[[:space:]]*"FILL_ME"' "$tfv" | sed -E 's/^([0-9]+):([a-z_]+).*/      line \1   \2/' | while read -r l; do echo -e "  ${DIM}$l${NC}"; done
    echo ""
    info "Fill them in, then: ${BOLD}unoverse deploy $cloud${NC}"
    echo ""
    exit 1
  fi
  ok "terraform.tfvars is complete"

  # SIZE IS NOT A ONE-WAY DOOR. small is the POC box, and the plan below is about to
  # quote its monthly cost — the moment someone wonders whether they are committing to
  # it. One line, here, rather than a discovery later.
  local cur_size
  cur_size=$(grep -E '^size[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  [ -n "$cur_size" ] && info "Size: ${BOLD}${cur_size}${NC} ${DIM}(small · medium · large — change it in terraform.tfvars and deploy again whenever you outgrow it)${NC}"

  # REGION MUST MATCH, or the platform cannot reach its own database. DigitalOcean's
  # private networking is per-region: the connection string uses the cluster's PRIVATE
  # host, which a droplet in another region cannot resolve. Everything provisions
  # perfectly and then the platform fails to start, which is the worst way to find out.
  local reuse_name ground_region db_region
  reuse_name=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  ground_region=$(grep -E '^region[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  if [ -n "$reuse_name" ] && [ -n "$ground_region" ] && command -v doctl >/dev/null 2>&1; then
    db_region=$(doctl databases list --format Name,Region --no-header 2>/dev/null | awk -v n="$reuse_name" '$1==n{print $2}')
    if [ -n "$db_region" ] && [ "$db_region" != "$ground_region" ]; then
      echo ""
      fail "Region mismatch: this universe is in ${BOLD}$ground_region${NC}, ${BOLD}$reuse_name${NC} is in ${BOLD}$db_region${NC}"
      info "Private networking does not cross regions, so the platform could not reach it."
      info "Either set ${BOLD}region = \"$db_region\"${NC} in infra/$cloud/terraform.tfvars,"
      info "or comment out existing_pg_cluster_name to create a database in $ground_region."
      echo ""
      return 1
    fi
  fi

  # THE DATABASE DECISION, STATED EVERY RUN. It is asked once and then recorded in
  # terraform.tfvars, so later deploys correctly do not re-ask — but silence reads
  # exactly like never having been asked. Say what is in force, the same way Size does.
  local pg_reuse pg_byo
  pg_reuse=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
  pg_byo=$(grep -cE '^byo_postgres_url[[:space:]]*=' "$tfv" 2>/dev/null)
  if [ -n "$pg_reuse" ]; then
    info "Database: ${BOLD}reusing $pg_reuse${NC} ${DIM}(no new cluster, nothing added to your bill)${NC}"
  elif [ "$pg_byo" = "1" ]; then
    info "Database: ${BOLD}your own URL${NC} ${DIM}(byo_postgres_url in terraform.tfvars)${NC}"
  else
    info "Database: ${BOLD}a new managed cluster${NC} ${DIM}(~\$15/month — comment in existing_pg_cluster_name to reuse one instead)${NC}"
  fi

  # STRAIGHT INTO TERRAFORM. Its plan plus its own typed "yes" IS the decision gate
  # for billable infrastructure; a [Y/n] in front of it was asking permission to ask
  # permission. Answering "no" is terraform's "no".
  echo ""

}

# A CREDENTIAL FAILURE IS A SIGN-IN PROBLEM, NOT A TERRAFORM PROBLEM.
#
# The most common way an AWS deploy fails is an expired SSO token: it happens to every
# operator, on a schedule, forever. Terraform reports it as a provider stack trace
# (InvalidGrantException, "failed to refresh cached credentials"), which reads as a
# broken setup and sends the operator debugging the wrong thing. Recognise the
# signatures and answer with the fix instead of the trace.
_plan_hit_credentials() {
  local cloud="$1" log="$2"
  if [ "$cloud" = "aws" ]; then
    grep -qE "No valid credential sources|refresh cached SSO token|InvalidGrantException|ExpiredToken|InvalidClientTokenId|security token included in the request is (expired|invalid)" "$log"
  else
    grep -qE "Unable to authenticate you|invalid or missing.*token|401.*Unauthorized" "$log"
  fi
}

_credentials_help() {
  local cloud="$1"
  echo ""
  if [ "$cloud" = "aws" ]; then
    fail "AWS did not accept your credentials, so nothing was changed."
    echo ""
    info "This is almost always an expired sign-in, not a broken setup:"
    echo ""
    echo -e "    Signed in with SSO?      ${BOLD}aws sso login${NC}${AWS_PROFILE:+ ${BOLD}--profile $AWS_PROFILE${NC}}  ${DIM}(tokens expire on a schedule; this is the usual fix)${NC}"
    echo -e "    Profile not selected?    ${BOLD}export AWS_PROFILE=<name>${NC}  ${DIM}(currently: ${AWS_PROFILE:-not set})${NC}"
    echo -e "    Using access keys?       ${BOLD}aws configure${NC}  ${DIM}(or export AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)${NC}"
    echo ""
    info "Confirm it works, then deploy again:"
    echo -e "    ${BOLD}aws sts get-caller-identity${NC}"
    echo ""
    echo -e "  ${DIM}Setting up AWS credentials for the first time:${NC}"
    echo -e "  ${DIM}https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html${NC}"
  else
    fail "DigitalOcean did not accept your token, so nothing was changed."
    echo ""
    info "The API token is expired, revoked, or not set:"
    echo ""
    echo -e "    Mint one (Write scope):  ${BOLD}https://cloud.digitalocean.com/account/api/tokens${NC}"
    echo -e "    Then provide it:         ${BOLD}export DIGITALOCEAN_TOKEN=<token>${NC}  ${DIM}(or do_token in terraform.tfvars)${NC}"
  fi
  echo ""
}

_ground_apply() {
  local cloud="$1" tmp planfile rc
  # SEPARATE STATEMENT. On macOS bash 3.2 a single `local a="$1" b="...$a"` expands $a
  # EMPTY, so dir became "$ROOT/infra/" and terraform answered "No configuration files".


  # Same trap that once made the release publish the repo root.
  local dir="$ROOT/infra/$cloud"
  tmp=$(mktemp -d); planfile="$tmp/plan"

  terraform -chdir="$dir" init -input=false >/dev/null 2>&1 || { fail "terraform init failed"; rm -rf "$tmp"; return 1; }

  info "Working out what needs to change ${DIM}($cloud)${NC}..."
  terraform -chdir="$dir" plan -input=false -detailed-exitcode -out="$planfile" >"$tmp/plan.log" 2>&1
  rc=$?
  case "$rc" in
    0) rm -rf "$tmp"; return 3 ;;
    2) : ;;
    *)
      if _plan_hit_credentials "$cloud" "$tmp/plan.log"; then
        _credentials_help "$cloud"
      else
        fail "Terraform could not plan the change:"; tail -20 "$tmp/plan.log" | sed 's/^/      /'
      fi
      rm -rf "$tmp"; return 1 ;;
  esac

  # STOPPING IS FOR LOSING SOMETHING, NOT FOR BUILDING IT. Typing `unoverse deploy` is
  # already the decision; a y/N gate straight afterwards asks the same question twice and
  # makes the developer's own instruction feel like a risk. The summary above still shows
  # exactly what appears and what it costs — it is a briefing, not a checkpoint. tfsummary
  # exits 3 when the plan destroys or replaces something, and THAT is worth interrupting.
  local summary_rc=0
  terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"
  summary_rc=${PIPESTATUS[1]}
  if [ "$summary_rc" = "2" ]; then
    # The plan could not be read, so nothing above is trustworthy. Show the real thing and
    # fall back to asking: silence plus an unreadable plan is not consent.
    terraform -chdir="$dir" show "$planfile"
    summary_rc=3
  fi

  local docpage
  [ "$cloud" = "aws" ] && docpage="aws" || docpage="digitalocean"
  echo -e "  ${DIM}How it fits together: https://github.com/unoverse-platform/docs/blob/main/architecture/$docpage.md${NC}"
  echo ""

  if [ "$summary_rc" = "3" ]; then
    local REPLY
    # ENTER GOES AHEAD, ESC STOPS. This asked for a typed `y` and made Enter mean "stop",
    # which is the opposite of every other prompt in this CLI — the developer who has just
    # read the plan and decided has to break the habit of the whole tool to act on it. One
    # meaning for Enter throughout, and the escape hatch is the key literally named for it.
    echo -e "  ${RED}This removes or rebuilds things that already exist.${NC}"
    echo ""
    echo -e "  ${DIM}Enter to go ahead   ·   Esc to stop${NC}"
    printf "  "
    local key=""
    if [ -t 0 ]; then
      IFS= read -r -s -n 1 key
    fi
    # \e is Escape; anything else typed (n, q, a stray letter) also stops, because only a
    # deliberate empty Enter should destroy something.
    if [ -n "$key" ]; then
      echo ""
      info "Nothing was changed. Run ${BOLD}unoverse deploy${NC} when you are ready"
      rm -rf "$tmp"; return 1
    fi
    echo ""
  fi

  echo ""
  info "Building. Managed databases take a few minutes on the provider's side"
  echo ""
  terraform -chdir="$dir" apply -input=false "$planfile" || { fail "The build did not finish. Nothing already built is lost — re-run: unoverse deploy $cloud"; rm -rf "$tmp"; return 1; }
  rm -rf "$tmp"
  return 0
}

cmd_deploy() {
  # A LEADING GROUND NAME IS NOT A SUBCOMMAND. `unoverse deploy aws` names the cloud. Take
  # it off the front when it is there, and leave everything else exactly as it was.
  local GROUND_ARG=""
  case "${1:-}" in
    do|digitalocean|aws|amazon) GROUND_ARG="$1"; shift ;;
  esac

  # .env.production IS NOT A FILE IN THE UNIVERSE. It is `terraform output -raw
  # env_production`: derived, complete, and regenerated in full on every deploy. Writing it
  # to the universe root put a second env file next to `.env`, which reads like something
  # to edit and is not — the next deploy overwrites it — and it outlived the server it
  # described, so a torn-down universe still looked deployed. Render it to a private temp
  # file, ship it, delete it. The ground stays the only source of truth.
  local env_prod
  env_prod=$(mktemp -t unoverse-env-prod) || exit 1
  chmod 600 "$env_prod"
  trap 'rm -f "$env_prod" 2>/dev/null' EXIT
  # Older universes have one on disk. It is stale by definition now, and leaving it means
  # the confusing second env file simply never goes away.
  rm -f "$ROOT/.env.production"

  _ground_credentials

  # ── ONE FLOW ────────────────────────────────────────────────────────────────
  # Which ground, is it configured, plan it, apply it, ship it. In that order, every
  # time, whether this is the first deploy or the fiftieth.
  # NAMING A GROUND THAT DOES NOT EXIST YET MEANS BUILD IT THERE. `unoverse deploy aws` on a
  # universe with only a DigitalOcean ground used to answer "No aws ground here. Run
  # unoverse deploy aws to create one" — the command just typed, refusing itself. Those
  # words already say where this should go, so the only thing left to do is set it up.
  #
  # A named ground with no tfvars falls through to the create flow below and skips the
  # "which cloud?" question, because it has been answered. An UNNAMED deploy still goes
  # through _pick_ground, which refuses to guess between two configured grounds.
  local cloud="" rc
  case "${GROUND_ARG:-}" in
    do|digitalocean) cloud="digitalocean" ;;
    aws|amazon)      cloud="aws" ;;
  esac

  if [ -n "$cloud" ]; then
    # Named. Configured already? Then use it; otherwise create it below.
    [ -f "$ROOT/infra/$cloud/terraform.tfvars" ] || { cmd_ground "$([ "$cloud" = "aws" ] && echo aws || echo do)" || {
        echo ""
        info "Once that is sorted, run ${BOLD}unoverse deploy $cloud${NC} again. It picks up right here."
        echo ""
        exit 1
      }; }
  elif [ -f "$ROOT/infra/digitalocean/terraform.tfvars" ] || [ -f "$ROOT/infra/aws/terraform.tfvars" ]; then
    cloud=$(SELF_CMD="unoverse deploy" _pick_ground "") || exit 1
  fi

  if [ -z "$cloud" ]; then
    echo ""
    echo -e "  ${CYAN}${BOLD}Where should this universe live?${NC}"
    echo ""
    pick_option "DigitalOcean" "AWS"
    [ "$PICKED" = "1" ] && cloud=aws || cloud=digitalocean
    echo ""
    cmd_ground "$([ "$cloud" = "aws" ] && echo aws || echo do)" || {
      echo ""
      info "Once that is sorted, run ${BOLD}unoverse deploy${NC} again. It picks up right here."
      echo ""
      exit 1
    }
  else
    # SAY WHERE THIS IS GOING, first and unmissably. An existing ground already names
    # the cloud, so re-asking every deploy would be noise — but a one-line mention
    # buried above other output reads as never having been asked at all.
    local pretty
    [ "$cloud" = "aws" ] && pretty="AWS" || pretty="DigitalOcean"
    echo ""
    echo -e "  ${CYAN}${BOLD}⬡ Deploying to $pretty${NC} ${DIM}(infra/$cloud — delete that folder's terraform.tfvars to choose again)${NC}"
    echo ""
  fi

  _ensure_ground_config "$cloud" || exit 1
  _ensure_terraform || exit 1

  _ground_apply "$cloud"; rc=$?
  [ "$rc" = "1" ] && exit 1
  terraform -chdir="$ROOT/infra/$cloud" output -raw env_production > "$env_prod" 2>/dev/null
  if [ ! -s "$env_prod" ]; then
    fail "Could not read this universe's settings from your $cloud ground. Has it been applied?"
    exit 1
  fi

  _adopted_db_access "$cloud" grant

  # The cluster admin connection, held in this shell and nowhere else. db-setup spends it
  # on a single GRANT so the universe user can create objects in its own database; it is
  # never written to the server, and an empty value (BYO database, or a ground without the
  # output) simply skips that step.
  local pg_admin_url
  pg_admin_url=$(terraform -chdir="$ROOT/infra/$cloud" output -raw pg_admin_url 2>/dev/null)


  # Read the deploy target from the rendered settings
  local deploy_host deploy_user
  deploy_host=$(grep '^DEPLOY_HOST=' "$env_prod" | cut -d= -f2- | tr -d '\r\n' | xargs)
  deploy_user=$(grep '^DEPLOY_USER=' "$env_prod" | cut -d= -f2- | tr -d '\r\n' | xargs)

  if [ -z "$deploy_host" ] || [ "$deploy_host" = "your-vm-ip" ]; then
    fail "This ground has no server address yet. Has it been applied?"
    exit 1
  fi
  deploy_user="${deploy_user:-root}"

  banner "Deploying to $deploy_host"
  echo ""
  timer_start

  # Check ansible is installed
  if ! command -v ansible-playbook &>/dev/null; then
    fail "Ansible is not installed"
    info "Install: pip install ansible"
    exit 1
  fi
  ok "Ansible available"

  # Generate a temporary inventory from the rendered settings
  local tmp_inventory
  tmp_inventory=$(mktemp).yml
  cat > "$tmp_inventory" << 'EOF'
all:
  hosts:
    gravity-prod:
      ansible_host: DEPLOY_HOST_PLACEHOLDER
      ansible_user: DEPLOY_USER_PLACEHOLDER
      ansible_python_interpreter: /usr/bin/python3
EOF

  # Replace placeholders with actual values
  if [[ "$OSTYPE" == "darwin"* ]]; then
    sed -i '' "s/DEPLOY_HOST_PLACEHOLDER/$deploy_host/g" "$tmp_inventory"
    sed -i '' "s/DEPLOY_USER_PLACEHOLDER/$deploy_user/g" "$tmp_inventory"
  else
    sed -i "s/DEPLOY_HOST_PLACEHOLDER/$deploy_host/g" "$tmp_inventory"
    sed -i "s/DEPLOY_USER_PLACEHOLDER/$deploy_user/g" "$tmp_inventory"
  fi

  # Debug: show what's in the inventory
  echo ""
  info "Generated inventory file:"
  cat "$tmp_inventory" | sed 's/^/  /'
  echo ""

  # Vendored next to the operator lib in the published CLI; at the repo root in the
  # monorepo. The playbooks are tooling, not universe content — a universe carries none.
  local ansible_dir="$GRAVITY_LIB/../ansible"
  [ -d "$ansible_dir" ] || ansible_dir="$ROOT/ansible"
  # ansible only reads ansible.cfg from CWD/env — we run from the repo root, so
  # point it at ours explicitly (inventory defaults, deprecation-noise silencing).
  export ANSIBLE_CONFIG="$ansible_dir/ansible.cfg"
  local subcommand="${1:-}"

  # A FIRST DEPLOY TO A NEW SERVER IS A PROVISION, NOT AN IMAGE PULL. Bare `deploy` meant
  # only "pull the latest images and restart", which on a freshly built droplet fails on
  # "Destination directory /opt/gravity does not exist" — /opt/gravity being the directory
  # install.yml creates. Deploy owns the whole journey, so it asks the server what it is and
  # picks the right playbook itself rather than expecting anyone to know a second command.
  # THE MARKER IS "SETUP FINISHED", NOT "A DIRECTORY EXISTS". Testing for /opt/gravity
  # looked right and was not: install.yml creates that directory as its seventh task, so a
  # first-time setup that then FAILED at the database step still left it behind. The next
  # deploy saw it, decided the server was ready, shipped images onto a database that had
  # never been migrated, and reported success. The stamp is written only after install,
  # database and verify have all passed, so an interrupted setup resumes as a setup.
  if [ -z "$subcommand" ]; then
    # "COULD NOT ASK" IS NOT "NOT SET UP". ssh exits 255 on a connection failure and
    # with the remote command's status otherwise — and a transient timeout here once
    # reclassified a healthy, deployed server as first-time, pointing a full install at
    # it. Retry the connect, and if the host still cannot be reached, stop and say so
    # rather than guessing what it is.
    local stamp_probe=0
    ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
        -o ConnectionAttempts=5 \
        "$deploy_user@$deploy_host" 'test -f /opt/gravity/.setup-complete' >/dev/null 2>&1 || stamp_probe=$?
    if [ "$stamp_probe" -eq 255 ]; then
      rm -f "$tmp_inventory"
      fail "Cannot reach $deploy_user@$deploy_host over SSH (tried 5 times). Check the network / security-group admin rule, then: unoverse deploy $cloud"
      exit 1
    elif [ "$stamp_probe" -ne 0 ]; then
      echo ""
      info "This server's setup has not finished. Running it ${DIM}(install, database, verify)${NC}"
      subcommand="first-time"
    fi
  fi

  case "$subcommand" in
    ""|deploy)
      # THE deploy: the server takes the latest platform images (pull + restart).
      # Content does NOT ride deploys — it arrives from the marketplace as database
      # ROWS (`POST /marketplace/install`) or from `unoverse deploy studio`.
      #
      # "self-healing at boot" was the npm install lane (`plugins/startup.ts`),
      # deleted 2026-08-30 — nothing is fetched from a registry (MARKETPLACE.md §10 q3).
      # The one fetch that is coming is the node runtime, `@unoverse-platform/base`,
      # installed here within a range the image declares (§5a, NOT YET BUILT).
      info "Deploying platform images..."
      echo ""
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/deploy-images.yml" \
        -e "universe_root=$ROOT" \
        -e "env_file=$env_prod"
      ;;
    first-time)
      # INTERNAL, never typed. Deploy reaches this by finding no .setup-complete stamp on
      # the server: install → database → verify, end to end. Hardening stays a deliberate
      # follow-up (POCs get verified first; harden when you decide to keep the box).
      info "First-time setup: install → database → verify"
      echo ""
      info "[1/3] Provisioning (Docker, services, mounts)..."
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/install.yml" \
        -e "universe_root=$ROOT" \
        -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "install failed. Fix the error above, then: unoverse deploy $cloud"; exit 1; }
      echo ""
      info "[2/3] Database setup..."
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/db-setup.yml" \
        -e "universe_root=$ROOT" \
        -e "pg_admin_url=$pg_admin_url" \
        -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "database setup failed. Fix the error above, then: unoverse deploy $cloud"; exit 1; }
      echo ""
      info "[3/3] Verifying..."
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/test-connectivity.yml" \
        -e "universe_root=$ROOT" \
        -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "verification failed. Look at the output above, then: unoverse deploy $cloud"; exit 1; }
      # Only now. Every step passed, so the next deploy can safely be an image push.
      ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
        "$deploy_user@$deploy_host" 'touch /opt/gravity/.setup-complete' >/dev/null 2>&1
      echo ""
      ok "Your universe is up. From now on, deploys are just: unoverse deploy"
      info "Keeping it? Harden the VM when you're ready: unoverse deploy harden"
      ;;
    db)
      info "Running database setup..."
      echo ""
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/db-setup.yml" \
        -e "universe_root=$ROOT" \
        -e "pg_admin_url=$pg_admin_url" \
        -e "env_file=$env_prod"
      ;;
    test|check)
      info "Running connectivity test..."
      echo ""
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/test-connectivity.yml" \
        -e "universe_root=$ROOT" \
        -e "env_file=$env_prod"
      ;;
    harden)
      info "Hardening VM (SSH, firewall, updates)..."
      echo ""
      ansible-playbook \
        -i "$tmp_inventory" \
        "$ansible_dir/playbooks/harden.yml"
      ;;
    *)
      echo "Usage: unoverse deploy [command]"
      echo ""
      echo "  (none)       Deploy: pull latest platform images + restart"
      echo "  init         First-time setup, end to end: install + db + verify"
      echo ""
      echo "More:"
      echo "  db           Re-run database setup"
      echo "  harden       Security hardening (SSH, fail2ban, auto-updates) — run it when a universe graduates from POC"
      echo "  test         Re-run the connectivity test"
      rm -f "$tmp_inventory"
      exit 1
      ;;
  esac

  rm -f "$tmp_inventory"

  # ── POST-DEPLOY SMOKE ───────────────────────────────────────────────────────
  # The deployed stack, probed from the outside. SELF-CONTAINED (curl only): this script
  # is vendored into the operator CLI and runs in a universe folder, so it can lean on no
  # repo test suite. Every check is an outage that shipped and was found by hand: the
  # blank canvas (uncompressed/uncached bundle, 2026-08-15), the no-login boot (empty
  # auth config, 2026-08-14), and the engine→Postgres 500 behind a healthy-looking stack
  # (Trusted Sources, 2026-08-13). The repo twin with more detail lives at
  # apps/canvas/tests/smoke/deployed.test.ts.
  # Runs on the plain deploy only — init/db/harden are not "the site is live" moments.
  if [ -z "$subcommand" ] || [ "$subcommand" = "deploy" ]; then
    local smoke_canvas smoke_api smoke_fail=0
    smoke_canvas=$(terraform -chdir="$ROOT/infra/$cloud" output -raw canvas_url 2>/dev/null)
    smoke_api=$(terraform -chdir="$ROOT/infra/$cloud" output -raw api_url 2>/dev/null)
    if [ -n "$smoke_api" ]; then
      echo ""
      info "Smoke-testing the deployed stack ${DIM}($smoke_api)${NC}"
      # WAIT FOR STEADY STATE FIRST. A restart is ~36s of boot plus however many
      # health-check intervals the load balancer needs to re-admit the droplet, and
      # during that window EVERY api-side probe answers 503. The smoke test measures
      # the stack's steady state, not the race — so it starts at the first 200 from
      # /health THROUGH the balancer (which proves both the app and re-admission), and
      # only a stack that never gets there fails. Observed live 2026-08-16: a healthy
      # v1.13.79 deploy reported four FAILs and "fix forward or roll back", all of it
      # re-admission lag. A false stop after the irreversible half is the worst kind.
      local smoke_wait=0
      until [ "$(curl -s -m 5 -o /dev/null -w '%{http_code}' "$smoke_api/health")" = 200 ]; do
        if [ "$smoke_wait" -ge 180 ]; then
          echo -e "    ${RED}FAIL${NC} stack never became healthy (/health ≠ 200 after ${smoke_wait}s)"
          smoke_fail=1
          break
        fi
        [ "$smoke_wait" = 0 ] && echo -e "    ${DIM}waiting for /health through the balancer (boot + LB re-admission)...${NC}"
        sleep 5; smoke_wait=$((smoke_wait + 5))
      done
      [ "$smoke_fail" = 0 ] && [ "$smoke_wait" -gt 0 ] && echo -e "    ${DIM}healthy after ${smoke_wait}s${NC}"
      _smoke() { # name, then the test itself as "$@"
        local name="$1"; shift
        if "$@" >/dev/null 2>&1; then echo -e "    ${GREEN}ok${NC} $name"
        else echo -e "    ${RED}FAIL${NC} $name"; smoke_fail=1; fi
      }
      # Universe discovery answers (the connector's first question).
      _smoke "universe discovery" curl -sf -m 10 "$smoke_api/.well-known/unoverse-universe" -o /dev/null
      # Embed doorway is served AND compressed (identity-encoded JS was a live outage).
      _smoke "embed.js gzipped" sh -c "curl -sf -m 10 -H 'accept-encoding: gzip' -D - '$smoke_api/embed.js' -o /dev/null | grep -qi '^content-encoding: gzip'"
      # MCP lane accepts an initialize (every external connector dies here first).
      # 200 (auth off) or 401 (auth on) prove the lane is ALIVE, the same judgement the
      # database canary below makes. This used `curl -sf`, which fails on any error status,
      # so a universe that boots "JWT enforced on /mcp" — every Cognito/AWS deployment —
      # reported FAIL on a correctly protected endpoint and turned a healthy deploy red.
      _smoke "MCP initialize" sh -c "code=\$(curl -s -m 10 -o /dev/null -w '%{http_code}' -X POST '$smoke_api/mcp' -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"smoke\",\"version\":\"0\"}}}'); [ \"\$code\" = 200 ] || [ \"\$code\" = 401 ]"
      # THE MARKETPLACE LANE ANSWERS. 200 (auth off), or 401/404 (auth on) — all three
      # prove the listener is up and routing; a 5xx is a stack that is live and broken.
      #
      # RENAMED FROM "engine reaches database" (2026-08-31), because it stopped being able
      # to prove that and a check that overclaims is worse than one that does less. The
      # capability gate (server/src/security/gate.ts) refuses an unauthenticated request
      # with 404 BEFORE the handler runs — deliberately, so a 403 cannot tell an attacker
      # which routes exist — so on any auth-enabled universe this request never reaches
      # Postgres at all. It also, until this change, failed every deploy: the check expected
      # 401, the gate returned 404, and a healthy stack reported FAIL.
      #
      # WHAT STILL COVERS THE 2026-08-13 INCIDENT (an engine→Postgres 500 hiding behind a
      # healthy-looking stack): the wait above. `/health` answers 200 only when isReady(),
      # and readiness comes after the catalogue has loaded FROM THE DATABASE, so a universe
      # that cannot reach Postgres never becomes healthy and never gets here. That is
      # boot-time proof, not live proof. Proving it LIVE needs a probe that reaches a
      # handler — an authenticated call, or a public deep-health route — and neither exists
      # yet. Left as a known gap rather than papered over with a check that cannot see it.
      _smoke "marketplace lane answers" sh -c "code=\$(curl -s -m 10 -o /dev/null -w '%{http_code}' '$smoke_api/marketplace'); [ \"\$code\" = 200 ] || [ \"\$code\" = 401 ] || [ \"\$code\" = 404 ]"
      if [ -n "$smoke_canvas" ]; then
        # The canvas page and the hashed bundle it names: present, compressed, cacheable.
        _smoke "canvas page + compressed bundle" sh -c "asset=\$(curl -sf -m 10 '$smoke_canvas/' | grep -o '/assets/index-[^\"]*\.js' | head -1); [ -n \"\$asset\" ] && curl -sf -m 20 -H 'accept-encoding: gzip' -D - \"$smoke_canvas\$asset\" -o /dev/null | grep -qi '^content-encoding: gzip'"
        # Empty auth values in config.js = the no-login boot where every call 401s.
        _smoke "canvas auth config present" sh -c "! curl -sf -m 10 '$smoke_canvas/config.js' | grep -qE '(issuer|clientId): *\"\"'"
      fi
      if [ "$smoke_fail" = 1 ]; then
        fail "Deploy finished but the smoke test FAILED — the stack above is live and misbehaving. Fix forward or roll back."
        exit 1
      fi
    fi
  fi

  echo ""
  echo -e "  ${GREEN}${BOLD}Done${NC} ${DIM}in $(timer_elapsed)${NC}"
  echo ""
}
