#!/usr/bin/env bash # ── qpc Dev Shell ───────────────────────────────────────────────────────────── # # Builds qpc (if needed), starts a local server, registers Alice + Bob, then # opens a tmux session with two side-by-side REPL panes and a server-log strip. # # Layout (window 0 — "chat"): # # ┌──────────[ ALICE user=alice pass=alice ]──┬──[ BOB user=bob pass=bob ]──┐ # │ │ │ # │ /dm bob ← start a DM here │ reply here │ # │ /create-group g ← or create a group │ /join ← to accept invite │ # │ │ │ # ├──────────[ SERVER LOG ]────────────────────┴──────────────────────────────┤ # │ live qpc-server stdout / stderr │ # └───────────────────────────────────────────────────────────────────────────┘ # # Window 1 — "ref": full slash-command cheatsheet (read-only) # # Usage: # ./scripts/dev-shell.sh build if needed, fresh session # ./scripts/dev-shell.sh --rebuild force cargo build first # ./scripts/dev-shell.sh --resume reuse existing state files + server data # ./scripts/dev-shell.sh --help show this message # # Stop: Ctrl-C here OR tmux kill-session -t qpc-dev # Ref: Ctrl-B 1 (inside tmux → cheatsheet window) # Nav: Ctrl-B ←/→ (switch Alice ↔ Bob pane) # Ctrl-B z (zoom current pane) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BIN_DIR="$PROJECT_ROOT/target/debug" SESSION="qpc-dev" SERVER_PORT=7000 SERVER_ADDR="127.0.0.1:$SERVER_PORT" SERVER_NAME="localhost" # All runtime state lives in /tmp so the project tree stays clean RUN_DIR="/tmp/qpc-devshell" DATA_DIR="$RUN_DIR/server-data" # server stores TLS cert + OPAQUE data here CA_CERT="$DATA_DIR/server-cert.der" LOG_FILE="$RUN_DIR/server.log" QPQ="$BIN_DIR/qpc" QPQS="$BIN_DIR/qpc-server" # ── Colours ──────────────────────────────────────────────────────────────────── GRN='\033[0;32m'; CYN='\033[0;36m'; YLW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' step() { printf "\n${GRN}▶ %s${NC}\n" "$*"; } info() { printf " ${CYN}%s${NC}\n" "$*"; } warn() { printf " ${YLW}⚠ %s${NC}\n" "$*"; } die() { printf "${RED}✗ %s${NC}\n" "$*" >&2; exit 1; } # ── Parse flags ──────────────────────────────────────────────────────────────── REBUILD=false RESUME=false for arg in "$@"; do case "$arg" in -r|--rebuild) REBUILD=true ;; --resume) RESUME=true ;; -h|--help) sed -n '2,/^[^#]/{ /^#/p }' "$0" | sed 's/^# \?//' exit 0 ;; *) die "Unknown argument: $arg" ;; esac done # ── Preflight ────────────────────────────────────────────────────────────────── step "Checking requirements..." for cmd in cargo tmux; do command -v "$cmd" &>/dev/null || die "'$cmd' is required but not installed." done info "cargo $(cargo --version 2>&1 | head -1)" info "tmux $(tmux -V)" # ── Decide whether to clean state ───────────────────────────────────────────── # By default we start fresh (server is always restarted, state must match). # Pass --resume to reuse an existing consistent state from a previous run. if $RESUME; then info "Resume mode — keeping existing state in $RUN_DIR" [[ -d "$RUN_DIR" ]] || die "--resume requires a previous dev-shell run (no $RUN_DIR)" else step "Cleaning previous run state..." rm -rf "$RUN_DIR" info "Cleared $RUN_DIR" fi mkdir -p "$RUN_DIR" "$DATA_DIR" # ── Build ────────────────────────────────────────────────────────────────────── if $REBUILD || [[ ! -x "$QPQ" ]] || [[ ! -x "$QPQS" ]]; then step "Building workspace (cargo build)..." cd "$PROJECT_ROOT" cargo build info "Build complete." else info "Using cached binaries in $BIN_DIR" info "(pass --rebuild to recompile)" fi [[ -x "$QPQ" ]] || die "Client binary not found: $QPQ" [[ -x "$QPQS" ]] || die "Server binary not found: $QPQS" # ── Free the port ────────────────────────────────────────────────────────────── step "Ensuring port $SERVER_PORT is free..." SERVER_PID="" free_port() { if command -v fuser &>/dev/null; then fuser -k "${SERVER_PORT}/tcp" 2>/dev/null || true elif command -v lsof &>/dev/null; then local pids pids=$(lsof -ti "tcp:${SERVER_PORT}" 2>/dev/null || true) [[ -n "$pids" ]] && kill $pids 2>/dev/null || true fi } free_port sleep 0.3 # ── Cleanup on exit ──────────────────────────────────────────────────────────── cleanup() { printf "\n" step "Shutting down..." tmux kill-session -t "$SESSION" 2>/dev/null || true if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then info "Stopping qpc-server (PID $SERVER_PID)..." kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true fi free_port info "Done." } trap cleanup EXIT INT TERM # ── Start server ─────────────────────────────────────────────────────────────── step "Starting qpc-server on $SERVER_ADDR..." "$QPQS" \ --listen "$SERVER_ADDR" \ --data-dir "$DATA_DIR" \ --tls-cert "$DATA_DIR/server-cert.der" \ --tls-key "$DATA_DIR/server-key.der" \ --allow-insecure-auth \ >"$LOG_FILE" 2>&1 & SERVER_PID=$! info "PID $SERVER_PID log → $LOG_FILE" # ── Wait for TLS cert (written by server on first boot) ──────────────────────── step "Waiting for server to initialise..." for i in $(seq 1 20); do if [[ -f "$CA_CERT" ]]; then info "Server ready after ${i}s (cert present)." break fi if ! kill -0 "$SERVER_PID" 2>/dev/null; then warn "Server exited early. Last log output:" tail -30 "$LOG_FILE" >&2 die "Server failed to start." fi sleep 1 if [[ $i -eq 20 ]]; then tail -30 "$LOG_FILE" >&2 die "Server did not produce TLS cert within 20s." fi done sleep 0.5 # brief pause for the QUIC listener to open QPC_GLOBAL=(--ca-cert "$CA_CERT" --server-name "$SERVER_NAME") # ── Build REPL command strings ───────────────────────────────────────────────── # Registration is handled automatically by the REPL on first launch: # 1. load_or_init_state creates state file + identity key (if missing) # 2. opaque_register sends the identity key → server binds username→identity_key # 3. opaque_login returns a session token → cached for future runs # Pre-registering here (without an identity key) would break /dm by preventing # the identity key from ever being bound on a subsequent REPL launch. repl_cmd() { local user="$1" pass="$2" echo "$QPQ ${QPC_GLOBAL[*]} repl \ --state $RUN_DIR/${user}.bin \ --server $SERVER_ADDR \ --username $user \ --password $pass" } ALICE_CMD=$(repl_cmd alice alice) BOB_CMD=$(repl_cmd bob bob) # ── Build tmux session ───────────────────────────────────────────────────────── step "Creating tmux session '$SESSION'..." tmux kill-session -t "$SESSION" 2>/dev/null || true # ─ Window 0 "chat" layout: # top-left → Alice REPL # top-right → Bob REPL # bottom → server log (full width, 30% height) # # tmux 3.x renumbers pane indices after each split, so we capture pane IDs # with -P -F '#{pane_id}' instead of relying on 0.0 / 0.1 / 0.2 arithmetic. tmux new-session -d -s "$SESSION" -n "chat" -x 220 -y 55 # The initial pane is always %0 / pane 0 — that's Alice. PANE_ALICE="${SESSION}:0.0" # Split bottom strip for server log; capture the new pane's stable ID. PANE_LOG=$(tmux split-window -v -t "$PANE_ALICE" -p 30 -P -F '#{pane_id}') # Split top-right for Bob from Alice's pane; capture ID. tmux select-pane -t "$PANE_ALICE" PANE_BOB=$(tmux split-window -h -t "$PANE_ALICE" -P -F '#{pane_id}') # Send commands to each pane by stable ID — immune to index renumbering. tmux send-keys -t "$PANE_LOG" \ "printf '\\033[0;36m[server log]\\033[0m\\n' && tail -F '$LOG_FILE'" \ Enter tmux send-keys -t "$PANE_BOB" \ "sleep 1.5 && $BOB_CMD" \ Enter tmux send-keys -t "$PANE_ALICE" \ "sleep 0.8 && $ALICE_CMD" \ Enter # Pane border labels (tmux ≥ 2.6) tmux select-pane -t "$PANE_ALICE" -T " ✉ ALICE │ user=alice pass=alice " tmux select-pane -t "$PANE_LOG" -T " ⚙ SERVER LOG " tmux select-pane -t "$PANE_BOB" -T " ✉ BOB │ user=bob pass=bob " tmux set-option -t "$SESSION" pane-border-status top 2>/dev/null || true tmux set-option -t "$SESSION" \ pane-border-format \ "#{?pane_active,#[bold fg=colour226],#[fg=colour244]} #{pane_title} " \ 2>/dev/null || true # Focus Alice to start tmux select-pane -t "$PANE_ALICE" # ─ Window 1 "ref" — slash-command cheatsheet ────────────────────────────────── tmux new-window -t "${SESSION}:1" -n "ref" tmux send-keys -t "${SESSION}:1" "clear" Enter # Heredoc piped through cat so it renders immediately and stays visible tmux send-keys -t "${SESSION}:1" "cat << 'CHEAT' ╔══════════════════════════════════════════════════════════════════════════╗ ║ qpc REPL ─ Slash Command Cheatsheet ║ ╠══════════════════════════════════════════════════════════════════════════╣ ║ GENERAL ║ ║ /help show all commands in the REPL ║ ║ /whoami identity key + hybrid key fingerprint ║ ║ /quit /q /exit exit the REPL ║ ║ ║ ║ CONVERSATIONS ║ ║ /list /ls list all open conversations ║ ║ /switch @username make a DM the active conversation ║ ║ /switch #groupname make a group the active conversation ║ ║ /history [N] print last N messages (default: 20) ║ ║ /members list all members of the current conv. ║ ║ ║ ║ DIRECT MESSAGES ║ ║ /dm open or create an encrypted 1:1 DM ║ ║ ║ ║ MLS GROUPS ║ ║ /create-group create a new MLS group (you are admin) ║ ║ /cg alias for /create-group ║ ║ /invite invite someone into the current group ║ ║ /join accept a pending group Welcome message ║ ║ /leave leave the currently active group ║ ║ /remove remove (kick) a member from the group ║ ║ /kick alias for /remove ║ ║ ║ ╠══════════════════════════════════════════════════════════════════════════╣ ║ QUICK START — 1:1 DM TEST ║ ║ ║ ║ [Alice] /dm bob creates encrypted DM channel ║ ║ [Bob] (Welcome arrives) background poller picks it up auto ║ ║ [Alice] Hello Bob! send your first message ║ ║ [Bob] Hey Alice! reply ║ ║ [Alice] /history verify messages are stored ║ ║ [Alice] /whoami check identity + hybrid key status ║ ║ ║ ║ QUICK START — GROUP CHAT TEST ║ ║ ║ ║ [Alice] /create-group devtest create an MLS group ║ ║ [Alice] /invite bob send a Welcome to Bob ║ ║ [Bob] /join accept the Welcome ║ ║ [Alice] Hello everyone! send to group ║ ║ [Bob] Hi Alice! reply in group ║ ║ [Alice] /members verify both Alice + Bob listed ║ ║ [Alice] /history 50 dump full message log ║ ║ [Alice] /remove bob kick Bob (test admin ops) ║ ║ [Bob] (removed from group) ║ ║ ║ ╠══════════════════════════════════════════════════════════════════════════╣ ║ TMUX NAVIGATION ║ ║ Ctrl-B 0 window 0 — chat panes (Alice / Bob / log) ║ ║ Ctrl-B 1 window 1 — this cheatsheet ║ ║ Ctrl-B ← → move between panes in the chat window ║ ║ Ctrl-B z zoom current pane to fullscreen (toggle) ║ ║ Ctrl-B [ scroll mode — use arrows / PgUp/PgDn (q exits) ║ ║ Ctrl-B d detach (session stays alive in background) ║ ║ ║ ║ EXIT / STOP ║ ║ Ctrl-B :kill-session Enter kill tmux + triggers script cleanup ║ ║ tmux kill-session -t qpc-dev from any other terminal ║ ║ /quit (in Alice or Bob pane) exit that REPL only ║ ╚══════════════════════════════════════════════════════════════════════════╝ CHEAT" Enter # Return focus to the chat window, Alice pane tmux select-window -t "${SESSION}:0" tmux select-pane -t "${SESSION}:0.0" # ── Print startup summary ────────────────────────────────────────────────────── printf "\n" printf "${GRN}╔══════════════════════════════════════════════════════╗${NC}\n" printf "${GRN}║${NC} ${GRN}qpc dev shell — ready${NC} ${GRN}║${NC}\n" printf "${GRN}╠══════════════════════════════════════════════════════╣${NC}\n" printf "${GRN}║${NC} Session ${CYN}%s${NC}\n" "$SESSION ${GRN}║${NC}" printf "${GRN}║${NC} Server ${CYN}%s${NC}\n" "$SERVER_ADDR (log → $LOG_FILE) ${GRN}║${NC}" printf "${GRN}║${NC} Alice user=${CYN}alice${NC} pass=${CYN}alice${NC} ${GRN}║${NC}\n" printf "${GRN}║${NC} Bob user=${CYN}bob${NC} pass=${CYN}bob${NC} ${GRN}║${NC}\n" printf "${GRN}║${NC} ${GRN}║${NC}\n" printf "${GRN}║${NC} Quick DM: ${CYN}[Alice pane]${NC} type: /dm bob ${GRN}║${NC}\n" printf "${GRN}║${NC} Cheatsheet: Ctrl-B 1 (inside tmux) ${GRN}║${NC}\n" printf "${GRN}║${NC} Exit: Ctrl-B :kill-session Enter ${GRN}║${NC}\n" printf "${GRN}║${NC} or from another terminal: ${GRN}║${NC}\n" printf "${GRN}║${NC} tmux kill-session -t qpc-dev ${GRN}║${NC}\n" printf "${GRN}╚══════════════════════════════════════════════════════╝${NC}\n" printf "\n" # ── Attach ───────────────────────────────────────────────────────────────────── tmux attach-session -t "$SESSION" step "Dev shell exited."