Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
337 lines
19 KiB
Bash
Executable File
337 lines
19 KiB
Bash
Executable File
#!/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 <username> open or create an encrypted 1:1 DM ║
|
|
║ ║
|
|
║ MLS GROUPS ║
|
|
║ /create-group <name> create a new MLS group (you are admin) ║
|
|
║ /cg <name> alias for /create-group ║
|
|
║ /invite <username> invite someone into the current group ║
|
|
║ /join accept a pending group Welcome message ║
|
|
║ /leave leave the currently active group ║
|
|
║ /remove <username> remove (kick) a member from the group ║
|
|
║ /kick <username> 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."
|