feat(scripts): all-in-one host-house.sh; free stale port; fail-fast join sync
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
- host-house.sh: one tmux session hosting a room (server window) plus your own GUI client window — builds, frees the port, waits for /health, then attaches. - host-room.sh: free_port() kills any stale LISTENer squatting on the port so the bind can't fail with "address already in use". - join.sh: cap each remote pull with `timeout` + GIT_TERMINAL_PROMPT=0 so an unreachable remote fails fast instead of hanging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+203
@@ -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 <<EOF
|
||||
host-house.sh — host a room + open your GUI seat, all in one tmux session
|
||||
|
||||
usage:
|
||||
./host-house.sh [NAME] [PORT] [--host ADDR] [--theme NAME]
|
||||
./host-house.sh --tls --cert CERT --key KEY [NAME] [PORT]
|
||||
./host-house.sh --kill
|
||||
./host-house.sh -h | --help
|
||||
|
||||
arguments:
|
||||
NAME your seat in the room (default: \$USER)
|
||||
PORT listen port (positional) (default: 4173)
|
||||
|
||||
flags:
|
||||
--host ADDR server bind address (default: 0.0.0.0 — all NICs)
|
||||
--port PORT listen port (default: 4173)
|
||||
--password PW room password (default: random; or PW=…)
|
||||
--theme NAME vestments for your client pane (church | neon | crypt)
|
||||
--tls serve real TLS (needs --cert and --key)
|
||||
--cert CERT TLS certificate path (implies --tls)
|
||||
--key KEY TLS private key path (implies --tls)
|
||||
--kill tear down the tmux session and the server we started
|
||||
-h, --help show this help and exit
|
||||
|
||||
environment (override any default):
|
||||
SESSION tmux session name (default: hh-house)
|
||||
HOST bind address (default: 0.0.0.0)
|
||||
PORT listen port (default: 4173)
|
||||
PW room password (default: random, openssl-generated)
|
||||
THEME theme name (church | neon | crypt)
|
||||
|
||||
notes:
|
||||
- Two windows in one session: 'server' (host-room.sh — join banner + logs) and
|
||||
your client window (the ratatui GUI). Ctrl-b n/p to switch, Ctrl-b d detach.
|
||||
- Default transport is --no-tls (the norm over a Tailscale tunnel); pass --tls
|
||||
with a cert/key for a public/untrusted network.
|
||||
|
||||
examples:
|
||||
./host-house.sh # host as \$USER on 0.0.0.0:4173
|
||||
./host-house.sh neo 4200 # seat "neo", port 4200
|
||||
./host-house.sh --kill # tear it all down
|
||||
EOF
|
||||
}
|
||||
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)" # .../hh
|
||||
ROOT="$(cd "$HERE/.." && pwd)" # repo root
|
||||
PY="$ROOT/.venv/bin/python"
|
||||
BIN="$HERE/target/debug/hack-house"
|
||||
HOST_ROOM="$HERE/scripts/host-room.sh"
|
||||
|
||||
SESSION="${SESSION:-hh-house}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
PORT="${PORT:-4173}"
|
||||
PW="${PW:-$(openssl rand -hex 12)}"
|
||||
THEMES_DIR="$HERE/themes"
|
||||
THEME="${THEME:-}"
|
||||
USE_TLS=0; CERT=""; KEY=""
|
||||
DO_KILL=0
|
||||
NAME=""
|
||||
|
||||
# Parse flags. A bare number is the port; the first bare word is your seat name.
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help|-help) usage; exit 0 ;;
|
||||
--kill) DO_KILL=1; shift ;;
|
||||
--host) HOST="$2"; shift 2 ;;
|
||||
--host=*) HOST="${1#--host=}"; shift ;;
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--port=*) PORT="${1#--port=}"; shift ;;
|
||||
--password) PW="$2"; shift 2 ;;
|
||||
--password=*) PW="${1#--password=}"; shift ;;
|
||||
--theme) THEME="$2"; shift 2 ;;
|
||||
--theme=*) THEME="${1#--theme=}"; shift ;;
|
||||
--tls) USE_TLS=1; shift ;;
|
||||
--cert) CERT="$2"; USE_TLS=1; shift 2 ;;
|
||||
--cert=*) CERT="${1#--cert=}"; USE_TLS=1; shift ;;
|
||||
--key) KEY="$2"; USE_TLS=1; shift 2 ;;
|
||||
--key=*) KEY="${1#--key=}"; USE_TLS=1; shift ;;
|
||||
[0-9]*) PORT="$1"; shift ;;
|
||||
-*) echo "✖ unknown flag: $1 (try --help)" >&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
|
||||
@@ -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
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user