chore: ROADMAP Phase 8, parallel AI team script, docker and infra updates
- ROADMAP.md: add Phase 8 — Freifunk / Community Mesh Networking with F0-F8 checkboxes; F0-F2 marked complete - scripts/ai_team.py: rewrite to support asyncio.gather parallel agent runs; add --sprint flag with predefined work packages (audit, phase1-hardening, phase2-tests, phase1-infra, status); add --parallel for ad-hoc concurrent agent invocations; output written to logs/ai_team/<sprint>_<timestamp>/<agent>.md - scripts/dev-shell.sh: convenience development shell helper - docker: update Dockerfiles for quicproquo rename and new server flags - .gitignore: add qpq-state artifacts (*.bin, *.session, *.pending.ks, *.convdb*)
This commit is contained in:
336
scripts/dev-shell.sh
Executable file
336
scripts/dev-shell.sh
Executable file
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── qpq Dev Shell ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Builds qpq (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 qpq-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 qpq-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="qpq-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/qpq-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/qpq"
|
||||
QPQS="$BIN_DIR/qpq-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 qpq-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 qpq-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
|
||||
|
||||
QPQ_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 ${QPQ_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'
|
||||
╔══════════════════════════════════════════════════════════════════════════╗
|
||||
║ qpq 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 qpq-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}qpq 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 qpq-dev ${GRN}║${NC}\n"
|
||||
printf "${GRN}╚══════════════════════════════════════════════════════╝${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# ── Attach ─────────────────────────────────────────────────────────────────────
|
||||
tmux attach-session -t "$SESSION"
|
||||
|
||||
step "Dev shell exited."
|
||||
Reference in New Issue
Block a user