#!/usr/bin/env bash # host-room.sh — host a hack-house room (the server) for others to join. # # Thin, friendly wrapper around `cmd_chat.py serve`: picks the project venv, # mints a room password if you don't supply one, prints the exact join command # (preferring your Tailscale IP), and runs the server in the foreground until # you Ctrl-C it. # # usage: # ./host-room.sh # 0.0.0.0:4173, --no-tls, random password # ./host-room.sh 4200 # custom port # ./host-room.sh --host 100.x.y.z # bind a specific address # PW=hunter2 ./host-room.sh # pin a known password (or --password) # ./host-room.sh --tls --cert c.pem --key k.pem # real TLS instead of --no-tls # ./host-room.sh -h # full usage # # The password lives in memory only (passed via $CMD_CHAT_PASSWORD, never on the # command line, so it won't show up in `ps`). Reveal/share it in-app with /pw. set -uo pipefail usage() { cat <&2; exit 2 ;; esac done # Mint a strong password if none was supplied — random per run, in memory only. if [[ -z "$PW" ]]; then if command -v openssl >/dev/null 2>&1; then PW="$(openssl rand -hex 12)" else PW="$(head -c 18 /dev/urandom | base64)"; fi fi # Resolve transport: --no-tls by default; --tls needs both cert and key. if [[ $USE_TLS -eq 1 ]]; then [[ -n "$CERT" && -n "$KEY" ]] || { echo "✖ --tls needs both --cert and --key" >&2; exit 2; } [[ -f "$CERT" ]] || { echo "✖ cert not found: $CERT" >&2; exit 2; } [[ -f "$KEY" ]] || { echo "✖ key not found: $KEY" >&2; exit 2; } SERVE_FLAGS=(--cert "$CERT" --key "$KEY") PROTO="https"; CLIENT_FLAG="--insecure" else SERVE_FLAGS=(--no-tls) PROTO="http"; CLIENT_FLAG="--no-tls" fi # Free the port if a stale listener is squatting on it (e.g. a server left over # from a previous run), so the bind can't fail with "address already in use". # We only target processes LISTENing on this TCP port, then escalate to -9. free_port() { local port="$1" pids pids="$(lsof -ti "tcp:$port" -sTCP:LISTEN 2>/dev/null || true)" [[ -z "$pids" ]] && pids="$(fuser "$port/tcp" 2>/dev/null | tr -s ' ' '\n' | grep -E '^[0-9]+$' || true)" [[ -z "$pids" ]] && return 0 echo "⚠ port $port already in use by PID(s): $(echo "$pids" | tr '\n' ' ')— killing" >&2 kill $pids 2>/dev/null || true sleep 0.5 pids="$(lsof -ti "tcp:$port" -sTCP:LISTEN 2>/dev/null || true)" [[ -n "$pids" ]] && { echo " still alive — SIGKILL" >&2; kill -9 $pids 2>/dev/null || true; } } free_port "$PORT" # Sanity-check deps so failures are an actionable hint, not a stack trace. if ! "$PY" -c "import sanic" >/dev/null 2>&1; then echo "✖ Python deps missing (sanic not importable with $PY)." >&2 echo " run ./bootstrap.sh to set up the .venv, then retry." >&2 exit 1 fi # Best IP to hand a joiner: Tailscale first (encrypted tunnel), else a global LAN IP. TS_IP="$(tailscale ip -4 2>/dev/null | head -1 || true)" LAN_IP="$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)" JOIN_IP="${TS_IP:-${LAN_IP:-$HOST}}" echo "═══════════════════════════════════════════════" echo " ⛧ hack-house room — $PROTO://$HOST:$PORT" echo "═══════════════════════════════════════════════" [[ -n "$TS_IP" ]] && echo " tailscale : $TS_IP (recommended — encrypted)" [[ -n "$LAN_IP" ]] && echo " lan : $LAN_IP" echo " password : $PW (share it; reveal in-app with /pw)" echo echo " others join with:" echo " python cmd_chat.py connect $JOIN_IP $PORT $CLIENT_FLAG" echo " or, with the TUI client:" echo " hh/target/debug/hack-house connect $JOIN_IP $PORT $CLIENT_FLAG" echo echo " Ctrl-C to stop the room." echo "═══════════════════════════════════════════════" # Hand the password to the server via env (resolve_password reads it), so it # never appears in the process list. Foreground exec: Ctrl-C stops the room. cd "$ROOT" exec env CMD_CHAT_PASSWORD="$PW" "$PY" cmd_chat.py serve "$HOST" "$PORT" "${SERVE_FLAGS[@]}"