#!/usr/bin/env bash # Runtime post-recreate verification for pi-devbox. # # Verifies that after `docker compose up -d --force-recreate`: # - The new image is actually live (pi version matches, when an expected # version is supplied — see the version note below) # - Persisted named volumes survived (~/.pi config, shell history, zoxide, # nvim data, uv cache, ssh-local) # - pi runtime wiring is intact: keybindings symlink, ≥4 extensions, the # mempalace.ts bridge, settings.json, and the pi-fork / # pi-observational-memory / (studio variant) pi-studio package registrations # - Shell defaults re-seeded from /etc/skel-devbox # - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir) # - /opt toolkits intact # - Known expected-absences don't regress # # This is repo/maintainer tooling — the runtime peer of smoke-test.sh. # smoke-test.sh runs at BUILD time with `--entrypoint=""`, so it can never see # a recreated container's persisted volumes or the entrypoint's runtime # deploy. This script is its runtime counterpart: it inspects what is actually # live in the container you are sitting in after a recreate. # # It is NOT baked into the published Docker Hub image; run it from a checkout of # the pi-devbox repo (which a maintainer already has for CI builds). A plain # `docker pull` consumer is not the audience and will not have this file. # # Version note: pi's version is resolved from `latest` at CI build time and is # NOT pinned to a concrete value in Dockerfile.variant (ARG PI_VERSION=latest). # So unlike opencode-devbox, this script cannot self-derive an expected version # from the Dockerfile. Pass --expected-version to assert a match; without it the # live pi version is reported as an informational WARN, not a failure. # # Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] [--variant studio|plain] # # Exit codes: # 0 all checks passed # 1 one or more checks failed # 2 usage error set -euo pipefail EXPECTED_VERSION="" VARIANT="" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --expected-version) EXPECTED_VERSION="$2" shift 2 ;; --variant) VARIANT="$2" shift 2 ;; *) echo "usage: $0 [--expected-version X.Y.Z] [--variant studio|plain]" >&2 exit 2 ;; esac done FAILED=0 pass() { echo " ✓ $1"; } fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); } warn() { echo " ⚠ $1" >&2; } # Auto-detect variant if not provided. The studio variant vendors pi-studio to # /opt/pi-studio; the plain variant does not. if [ -z "$VARIANT" ]; then if [ -d /opt/pi-studio ]; then VARIANT="studio" else VARIANT="plain" fi fi # Print header with git context echo "=== Recreate sanity check (variant: $VARIANT) ===" if GIT_TAG=$(git -C "$REPO_DIR" describe --tags 2>/dev/null); then echo " Repo HEAD: $GIT_TAG (version-match only meaningful when image tag matches)" else echo " Repo HEAD: (not a git repo or no tags)" fi echo echo "-- pi version --" if ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then if [ -n "$EXPECTED_VERSION" ]; then if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then pass "pi version $ACTUAL_VERSION" else fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION" fi else warn "pi version $ACTUAL_VERSION (no --expected-version given; pi is built from 'latest', cannot self-derive — informational only)" fi else fail "pi --version failed" fi echo echo "-- Persisted named volumes (must survive --force-recreate) --" # ~/.pi config volume (devbox-pi-config) — holds agent settings, extensions, # keybindings symlink. Must exist and be non-empty after recreate. if [ -d "$HOME/.pi/agent" ] && [ -n "$(ls -A "$HOME/.pi/agent" 2>/dev/null)" ]; then pass "~/.pi/agent exists and is non-empty" else fail "~/.pi/agent missing or empty" fi # shell history volume (devbox-shell-history). An empty .bash_history right # after recreate is NORMAL — only the mount point must exist. if [ -d "$HOME/.cache/bash" ]; then pass "~/.cache/bash exists as directory" else fail "~/.cache/bash missing or not a directory" fi # remaining persisted volumes — mount points must exist for vol_path in \ "$HOME/.local/share/zoxide" \ "$HOME/.local/share/nvim" \ "$HOME/.local/share/uv" \ "$HOME/.ssh-local"; do if [ -d "$vol_path" ]; then pass "$vol_path exists" else fail "$vol_path missing or not a directory" fi done # mempalace palace — CONDITIONAL. In this repo's docker-compose.yml the # devbox-palace named volume is commented out; the palace is reached via the # shared /workspace (virtiofs) path instead. So absence of a local palace dir # is NOT a recreate regression here. if [ -f "$HOME/.mempalace/palace/chroma.sqlite3" ]; then SIZE=$(du -h "$HOME/.mempalace/palace/chroma.sqlite3" | cut -f1) if [ -s "$HOME/.mempalace/palace/chroma.sqlite3" ]; then pass "~/.mempalace/palace/chroma.sqlite3 exists ($SIZE)" else fail "~/.mempalace/palace/chroma.sqlite3 exists but is empty" fi else warn "~/.mempalace/palace/chroma.sqlite3 absent — expected unless devbox-palace volume is enabled (palace is shared via /workspace by default)" fi echo echo "-- pi runtime wiring (deployed by entrypoint-user.sh) --" # keybindings symlink (pi-toolkit) if [ -L "$HOME/.pi/agent/keybindings.json" ]; then pass "~/.pi/agent/keybindings.json symlink (pi-toolkit)" else fail "~/.pi/agent/keybindings.json missing or not a symlink" fi # extensions deployed (pi-extensions) — expect ≥4 *.ts EXT_COUNT=$(ls -1 "$HOME"/.pi/agent/extensions/*.ts 2>/dev/null | wc -l | tr -d ' ') if [ "$EXT_COUNT" -ge 4 ]; then pass "$EXT_COUNT extensions deployed (≥4, pi-extensions)" else fail "only $EXT_COUNT extensions deployed (expected ≥4)" fi # mempalace.ts bridge symlink if [ -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then pass "~/.pi/agent/extensions/mempalace.ts bridge symlink" else fail "~/.pi/agent/extensions/mempalace.ts missing or not a symlink" fi # settings.json bootstrapped if [ -f "$HOME/.pi/agent/settings.json" ]; then pass "~/.pi/agent/settings.json bootstrapped" else fail "~/.pi/agent/settings.json missing" fi # pi package registrations (pi install → recorded in settings.json) if [ -f "$HOME/.pi/agent/settings.json" ]; then for pkg in pi-fork pi-observational-memory; do if grep -q "$pkg" "$HOME/.pi/agent/settings.json" 2>/dev/null; then pass "$pkg registered in settings.json" else fail "$pkg not registered in settings.json" fi done if [ "$VARIANT" = "studio" ]; then if grep -q "pi-studio" "$HOME/.pi/agent/settings.json" 2>/dev/null; then pass "pi-studio registered in settings.json" else fail "pi-studio not registered in settings.json (studio variant)" fi fi fi echo echo "-- ssh ControlMaster dir --" if [ -d /tmp/sshcm ] && [ "$(stat -c %a /tmp/sshcm 2>/dev/null)" = "700" ]; then pass "/tmp/sshcm exists with mode 700" else fail "/tmp/sshcm missing or not mode 700" fi echo echo "-- Shell defaults re-seeded from /etc/skel-devbox --" if [ -f "$HOME/.bash_aliases" ]; then pass "~/.bash_aliases exists" else fail "~/.bash_aliases missing" fi if [ -f "$HOME/.inputrc" ]; then pass "~/.inputrc exists" else fail "~/.inputrc missing" fi echo echo "-- cli_utils bind-mount --" if [ -d /workspace/cli_utils ] && [ -d /workspace/cli_utils/.git ]; then pass "/workspace/cli_utils exists with .git subdir" else warn "/workspace/cli_utils missing or .git subdir absent — expected only if cli_utils is bind-mounted" fi echo echo "-- Baked /opt toolkits --" for opt_path in /opt/pi-toolkit /opt/pi-extensions /opt/pi-fork /opt/pi-observational-memory /opt/mempalace-toolkit; do if [ -d "$opt_path" ]; then pass "$opt_path exists" else fail "$opt_path missing" fi done if [ "$VARIANT" = "studio" ]; then if [ -d /opt/pi-studio ] && [ -f /opt/pi-studio/client/studio-client.js ]; then pass "/opt/pi-studio exists with prebuilt client bundle" else fail "/opt/pi-studio missing or prebuilt client bundle absent (studio variant)" fi fi # mempalace MCP entrypoint on PATH if command -v mempalace-mcp >/dev/null 2>&1; then pass "mempalace-mcp on PATH" else fail "mempalace-mcp not on PATH" fi echo echo "-- Known expected-absences (regressions vs by-design) --" if ! command -v go >/dev/null 2>&1; then warn "go absent — expected unless image built with INSTALL_GO=true" else pass "go is on PATH" fi if [ "$VARIANT" = "plain" ] && [ ! -d /opt/pi-studio ]; then warn "/opt/pi-studio absent — expected on the plain (non-studio) variant" fi echo if [ "$FAILED" -gt 0 ]; then echo "=== FAILED: $FAILED check(s) ===" >&2 exit 1 fi echo "=== PASSED ==="