← back to Claude Webdev Accelerator

scripts/preview-tunnel.sh

59 lines

#!/bin/bash
# Stand up <slug>.agentabrams.com as a preview of a local app on <port>, through the
# shared dw-followup cloudflared tunnel. Reversible local work runs automatically;
# the DNS write (Cloudflare proxied CNAME) only runs with --dns (Steve-gated step).
#
# Usage: scripts/preview-tunnel.sh <slug> <port> [--dns]
set -euo pipefail
SLUG="${1:?usage: preview-tunnel.sh <slug> <port> [--dns]}"
PORT="${2:?need a local port}"
DO_DNS=""; [ "${3:-}" = "--dns" ] && DO_DNS=1
HOST="${SLUG}.agentabrams.com"
CFG="$HOME/.cloudflared/followup.yml"
TUNNEL_ID="0e1edf5a-f5b4-48c9-af62-639645519983"          # dw-followup
ZONE_NAME="agentabrams.com"

[ -f "$CFG" ] || { echo "no tunnel config at $CFG"; exit 1; }

# 1. Idempotently add the ingress rule before the 404 catch-all.
if grep -q "hostname: ${HOST}" "$CFG"; then
  echo "ingress for ${HOST} already present"
else
  # insert two lines before the first "- service: http_status:404"
  awk -v host="$HOST" -v port="$PORT" '
    /- service: http_status:404/ && !done {
      print "  - hostname: " host; print "    service: http://127.0.0.1:" port; done=1 }
    { print }' "$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
  echo "added ingress ${HOST} -> 127.0.0.1:${PORT}"
fi

# 2. Restart the tunnel to pick up the new ingress.
launchctl kickstart -k "gui/$(id -u)/com.steve.followup-tunnel" 2>/dev/null && echo "tunnel restarted" || echo "WARN: could not kickstart tunnel (start it manually)"

# 3. DNS (gated). Repoint/point the CF record to a proxied CNAME on the tunnel.
if [ -n "$DO_DNS" ]; then
  # Read ONLY the token (never `source` the whole .env — one malformed value aborts it under set -e).
  CLOUDFLARE_API_TOKEN=$(grep -E '^CLOUDFLARE_API_TOKEN=' "$HOME/Projects/secrets-manager/.env" | head -1 | cut -d= -f2- | tr -d "\"'\r")
  [ -n "$CLOUDFLARE_API_TOKEN" ] || { echo "CLOUDFLARE_API_TOKEN not found in secrets .env"; exit 1; }
  ZONE=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    "https://api.cloudflare.com/client/v4/zones?name=${ZONE_NAME}" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])")
  REC=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    "https://api.cloudflare.com/client/v4/zones/${ZONE}/dns_records?name=${HOST}" | python3 -c "import sys,json;r=json.load(sys.stdin)['result'];print(r[0]['id'] if r else '')")
  BODY="{\"type\":\"CNAME\",\"name\":\"${HOST}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"proxied\":true,\"comment\":\"preview via dw-followup tunnel\"}"
  if [ -n "$REC" ]; then
    curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE}/dns_records/${REC}" \
      -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" -d "$BODY" >/dev/null
    echo "updated existing DNS record → proxied CNAME to tunnel"
  else
    curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE}/dns_records" \
      -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" -d "$BODY" >/dev/null
    echo "created DNS record → proxied CNAME to tunnel"
  fi
  echo "live (after CF propagation): https://${HOST}"
else
  echo "DNS NOT changed (gated). To publish: re-run with --dns, or add a proxied CNAME"
  echo "  ${HOST} -> ${TUNNEL_ID}.cfargotunnel.com  in Cloudflare (zone ${ZONE_NAME})."
fi

echo "REMINDER: gate the app — run it with BASIC_AUTH=\"admin:DW2024!\" since previews are public."