diff --git a/hh/scripts/host-house.sh b/hh/scripts/host-house.sh new file mode 100755 index 0000000..22a8c49 --- /dev/null +++ b/hh/scripts/host-house.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# host-house.sh — host a hack-house room AND take your own GUI seat, all in tmux. +# +# All-in-one launcher: spins up the server (via host-room.sh) in a 'server' window +# and the ratatui client — already joined to that room — in its own window, both in +# a new, distinctly named tmux session (default: hh-house). Others on your +# Tailscale/LAN join with the command host-room.sh prints in the server window. +# +# usage: +# ./host-house.sh # host as $USER on 0.0.0.0:4173 (--no-tls) +# ./host-house.sh neo # host, take your seat as "neo" +# ./host-house.sh 4200 # custom port (also --port) +# PW=hunter2 ./host-house.sh # pin a known password (or --password) +# ./host-house.sh --theme neon # vestments for your client pane +# ./host-house.sh --tls --cert c.pem --key k.pem # real TLS instead of --no-tls +# ./host-house.sh --kill # tear down the session + the server we started +# ./host-house.sh -h # full usage +# +# The password lives in memory only; it's handed to the server via $CMD_CHAT_PASSWORD +# (host-room.sh), never on its command line. Reveal/share it in-app with /pw. +set -uo pipefail + +usage() { + cat <&2; exit 2 ;; + *) [[ -z "$NAME" ]] && NAME="$1"; shift ;; + esac +done +NAME="${NAME:-${USER:-$(id -un)}}" + +# Stop the server on $PORT (the one holding the port), used by --kill and before +# we (re)launch so only the room we start answers on this port. +stop_server() { + local pid + pid="$(ss -ltnpH "sport = :$PORT" 2>/dev/null | grep -oP 'pid=\K[0-9]+' | head -1)" + [[ -n "$pid" ]] && kill "$pid" 2>/dev/null && echo "stopped server holding :$PORT (pid $pid)" +} + +if [[ $DO_KILL -eq 1 ]]; then + tmux kill-session -t "$SESSION" 2>/dev/null && echo "killed tmux session $SESSION" + stop_server + exit 0 +fi + +command -v tmux >/dev/null 2>&1 || { echo "✖ tmux not found — install it first" >&2; exit 1; } + +# Resolve transport: --no-tls by default; --tls needs a 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; } + SCHEME="https"; CURLK="-k"; CLIENT_FLAG="--insecure" + TLS_ARGS=(--tls --cert "$CERT" --key "$KEY") +else + SCHEME="http"; CURLK=""; CLIENT_FLAG="--no-tls" + TLS_ARGS=() +fi + +# The local client connects over loopback when the server binds all interfaces. +case "$HOST" in + 0.0.0.0|::|"") CLIENT_HOST="127.0.0.1" ;; + *) CLIENT_HOST="$HOST" ;; +esac + +# Resolve a theme name → TOML path (empty = the client's built-in default). +THEME_PATH="" +if [[ -n "$THEME" ]]; then + THEME_PATH="$THEMES_DIR/$THEME.toml" + if [[ ! -f "$THEME_PATH" ]]; then + avail="$(cd "$THEMES_DIR" 2>/dev/null && ls -1 ./*.toml 2>/dev/null | sed 's#.*/##; s/\.toml$//' | paste -sd' ' -)" + echo "✖ no theme '$THEME' in $THEMES_DIR (available: ${avail:-none})" >&2 + exit 2 + fi +fi + +# Guard: don't rebuild the session we're sitting in (it would close on us). +if [[ -n "${TMUX:-}" && "$(tmux display-message -p '#S' 2>/dev/null)" == "$SESSION" ]]; then + echo "✖ you're inside the tmux session '$SESSION' — rebuilding it would close it." >&2 + echo " use a different name (e.g. SESSION=hh-host $0 $NAME) or run --kill from another window." >&2 + exit 2 +fi + +is_up() { curl -s $CURLK --max-time 2 "$SCHEME://$CLIENT_HOST:$PORT/health" 2>/dev/null | grep -q '"status":"ok"'; } + +# 1. build the client so the GUI pane has a binary to run. +echo "building client…" +( cd "$HERE" && cargo build --quiet ) || { echo "✖ build failed"; exit 1; } + +# 2. clear the port so only the room we start answers on it. +stop_server + +# 3. server pane: run host-room.sh (foreground, shows the join banner + logs). +# host-room reads HOST/PORT/PW from the env; --tls flags are passed through. +server_cmd="$(printf 'env HOST=%q PORT=%q PW=%q %q' "$HOST" "$PORT" "$PW" "$HOST_ROOM")" +for arg in "${TLS_ARGS[@]}"; do server_cmd+=" $(printf '%q' "$arg")"; done + +tmux kill-session -t "$SESSION" 2>/dev/null +tmux new-session -d -s "$SESSION" -n server -x 220 -y 50 -c "$HERE" "$server_cmd" + +# 4. wait for the room to answer before seating the client. +echo "waiting for the room on $CLIENT_HOST:$PORT…" +for _ in $(seq 1 30); do is_up && break; sleep 0.5; done +is_up || echo "⚠ server not up yet — the client pane will keep its own error on screen" >&2 + +# 5. client window: the ratatui GUI, joined to the room — a separate window in +# the SAME session (server stays on its own window). The trailing read keeps +# the window open if connect exits (e.g. auth error) instead of vanishing. +theme_arg="" +[[ -n "$THEME_PATH" ]] && theme_arg="$(printf -- '--theme %q' "$THEME_PATH")" +client_cmd="$(printf '%q connect %q %q %q --password %q %s %s; ec=$?; printf "\n%s left the house (exit %%s) — press enter to close\n" "$ec"; read _' \ + "$BIN" "$CLIENT_HOST" "$PORT" "$NAME" "$PW" "$CLIENT_FLAG" "$theme_arg" "$NAME")" +tmux new-window -t "$SESSION" -n "$NAME" -c "$HERE" "$client_cmd" +tmux select-window -t "$SESSION:$NAME" # land on the GUI; Ctrl-b p → server logs + +echo "house: $NAME · session: $SESSION · $SCHEME://$HOST:$PORT · vestments: ${THEME:-church (default)}" +echo "room password: $PW (share with joiners; reveal in-app with /pw)" +echo "tear down later with: $0 --kill" + +# 6. land in the house: attach from a plain shell, switch-client from inside tmux. +if [[ -z "${TMUX:-}" ]]; then + exec tmux attach -t "$SESSION" +else + echo "inside tmux — switching this client to '$SESSION' (detach with Ctrl-b d)" + tmux switch-client -t "$SESSION" +fi diff --git a/hh/scripts/host-room.sh b/hh/scripts/host-room.sh index b171bd1..f1a9de6 100755 --- a/hh/scripts/host-room.sh +++ b/hh/scripts/host-room.sh @@ -103,6 +103,22 @@ else 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 diff --git a/hh/scripts/join.sh b/hh/scripts/join.sh index b61a1cf..6805692 100755 --- a/hh/scripts/join.sh +++ b/hh/scripts/join.sh @@ -12,10 +12,14 @@ cd "$(dirname "$0")/.." # are best-effort and fast-forward only: an unreachable remote or diverged # history just warns and is skipped — it never blocks you from joining. BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)" +# Cap each pull so an unreachable/slow remote fails fast instead of hanging on +# the OS TCP timeout; GIT_TERMINAL_PROMPT=0 stops it blocking on a credential +# prompt. (No `timeout` on this box? fall back to a plain pull.) +TO=""; command -v timeout >/dev/null 2>&1 && TO="timeout 10" for remote in gitea origin; do if git remote get-url "$remote" >/dev/null 2>&1; then echo "⛧ syncing $BRANCH from $remote…" - git pull --ff-only "$remote" "$BRANCH" 2>&1 \ + GIT_TERMINAL_PROMPT=0 $TO git pull --ff-only "$remote" "$BRANCH" 2>&1 \ || echo " (skipped — couldn't fast-forward from $remote/$BRANCH)" fi done