aac4a1c323
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 18s
Publish Docker Image / resolve-versions (push) Successful in 9s
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / build-base (push) Successful in 41m49s
Publish Docker Image / smoke (push) Successful in 4m51s
Publish Docker Image / smoke-studio (push) Successful in 4m59s
Publish Docker Image / build-variant-studio (push) Successful in 16m58s
Publish Docker Image / build-variant (push) Successful in 28m35s
Publish Docker Image / update-description (push) Successful in 7s
Publish Docker Image / promote-base-latest (push) Successful in 17s
Two versions, two flags. `--expected-version` has only ever asserted
`pi --version`, but AGENTS.md step 4 spelled it `X.Y.Z` inside a checklist where
every other X.Y.Z is the pi-devbox tag. Run as documented for v1.8.8 the final
runtime gate of the release printed
✗ pi version mismatch: expected 1.8.8, got 0.84.3
and exited 1 — a red accusing the image of being the wrong version. Not one
reader's slip: the v1.8.8 release-readiness handoff from pi@emb-7kj4vr4g
propagated the same wrong spelling twice while correctly calling step 4 "not
ceremonial", so two independent readers converged on it. README.md had it right
all along, which means the two documents disagreed.
- new --expected-image-version asserts the pi-devbox release tag, read from
release_tag in /etc/pi-devbox/build-manifest.json (no checkout, no network);
leading `v` optional on either side
- both flags detect being handed the other one's value, and the test is exact
rather than heuristic: the value is compared against the other quantity the
image itself reports, so it can only fire on a real mix-up
- neither flag is required now. With none, live `pi --version` is asserted
against the manifest's pi_version — not a tautology, since a stale pi in the
~/.pi/npm-global volume can shadow the baked one, exactly as a stale
npm:pi-atelier can in packages[]
- the header note replaced was stale and load-bearing: it claimed pi is resolved
from 'latest' and cannot be self-derived, while Dockerfile.variant pins
ARG PI_VERSION=0.84.3 and docker-publish.yml reads that ARG as its source of
truth. The same withdrawn claim also sat in cli_utils' pi-devbox-sanity --help
- argument parsing: a missing value, or a value that is another flag, is a usage
error instead of silently consuming the next argument; --help works
All fourteen flag combinations exercised by execution, including the two
manifest-absent branches and the shadowing branch a healthy container cannot
reach — mutation-tested with a doctored manifest so each failure branch was
observed firing rather than assumed present.
CHANGELOG also names what no commit here causes: mempalace-toolkit main moved
e70bef2 -> 5b8d78f, so this tag ships the auto-delivered logstream mailbox
because base_tag folds the resolved toolkit SHA. It would have landed either
way; going unnamed is the 553d865 shape that already caused one cross-host
misattribution. Component audit found nothing else to bump — pi, mempalace,
pi-atelier all equal their upstream latest, and every other floating ref
resolves to the commit already baked.
482 lines
19 KiB
Bash
Executable File
482 lines
19 KiB
Bash
Executable File
#!/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 (both the pi version and — when asked —
|
|
# the pi-devbox image release tag; see the two version notes below)
|
|
# - Persisted named volumes survived (~/.pi config, shell history, zoxide,
|
|
# nvim data, uv cache, ssh-local)
|
|
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
|
|
# ≥4 extensions, the mempalace.ts bridge, settings.json, and the pi-fork /
|
|
# pi-observational-memory / (studio variant) pi-studio package
|
|
# registrations in settings.json packages[]
|
|
# - 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.
|
|
#
|
|
# TWO DIFFERENT VERSIONS, TWO DIFFERENT FLAGS. This distinction has already
|
|
# cost a release day, so it is spelled out here and in AGENTS.md step 4:
|
|
#
|
|
# --expected-version the PI CODING AGENT version, e.g. 0.84.3
|
|
# (`pi --version`; pinned as ARG PI_VERSION in
|
|
# Dockerfile.variant, which CI reads as the source
|
|
# of truth)
|
|
# --expected-image-version the PI-DEVBOX IMAGE release tag, e.g. 1.8.9 or
|
|
# v1.8.9 (the `release_tag` baked into
|
|
# /etc/pi-devbox/build-manifest.json)
|
|
#
|
|
# Passing a release tag to --expected-version used to report
|
|
# "pi version mismatch: expected 1.8.8, got 0.84.3" — an accusation aimed at
|
|
# the wrong component, on the last gate of a release. Both flags now detect
|
|
# being handed the other one's value and say so instead.
|
|
#
|
|
# Neither flag is required. Both values are derivable from the image's own
|
|
# build manifest, so by default the script asserts the LIVE pi version against
|
|
# the version recorded at build time — which is not a tautology: a stale
|
|
# `pi` in the ~/.pi/npm-global volume can shadow the baked one, exactly the
|
|
# way a stale npm:pi-atelier can (see the packages[] check below). Pass the
|
|
# flags when you want an assertion against a value you name yourself, which
|
|
# is what a release checklist wants.
|
|
#
|
|
# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z]
|
|
# [--expected-image-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=""
|
|
EXPECTED_IMAGE_VERSION=""
|
|
VARIANT=""
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
MANIFEST=/etc/pi-devbox/build-manifest.json
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage: recreate-sanity-check.sh [--expected-version X.Y.Z]
|
|
[--expected-image-version X.Y.Z]
|
|
[--variant studio|plain]
|
|
|
|
--expected-version pi coding agent version, e.g. 0.84.3 (`pi --version`)
|
|
--expected-image-version pi-devbox image release tag, e.g. 1.8.9 or v1.8.9
|
|
--variant studio|plain (auto-detected when omitted)
|
|
|
|
These are two different versions. Both are read from the image's own build
|
|
manifest when the corresponding flag is omitted.
|
|
EOF
|
|
}
|
|
|
|
# Parse arguments. Every flag takes a value, so reject a missing one rather
|
|
# than swallowing the next flag as if it were the value.
|
|
need_value() {
|
|
case "${2:-}" in
|
|
""|-*)
|
|
echo "$1 requires a value" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--expected-version)
|
|
need_value "$@"
|
|
EXPECTED_VERSION="$2"
|
|
shift 2
|
|
;;
|
|
--expected-image-version)
|
|
need_value "$@"
|
|
EXPECTED_IMAGE_VERSION="$2"
|
|
shift 2
|
|
;;
|
|
--variant)
|
|
need_value "$@"
|
|
VARIANT="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown option: $1" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
FAILED=0
|
|
pass() { echo " ✓ $1"; }
|
|
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
|
warn() { echo " ⚠ $1" >&2; }
|
|
|
|
# Read one top-level field from the build manifest, or print nothing. The
|
|
# manifest is the image's own ground truth (written at `docker build` time by
|
|
# Dockerfile.variant), so it needs no checkout and no network. Absent on an
|
|
# image built before it existed, hence every caller treats "" as unknown.
|
|
manifest_field() {
|
|
[ -f "$MANIFEST" ] || return 0
|
|
command -v jq >/dev/null 2>&1 || return 0
|
|
jq -r --arg k "$1" '.[$k] // empty' "$MANIFEST" 2>/dev/null || true
|
|
}
|
|
# Release tags are written with a leading v in the manifest and quoted without
|
|
# one in checklists; compare on the bare number so both spellings work.
|
|
strip_v() { printf '%s' "${1#v}"; }
|
|
|
|
# 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
|
|
|
|
MANIFEST_PI_VERSION=$(manifest_field pi_version)
|
|
MANIFEST_RELEASE_TAG=$(manifest_field release_tag)
|
|
|
|
echo "-- pi (coding agent) version --"
|
|
if ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then
|
|
if [ -n "$EXPECTED_VERSION" ]; then
|
|
if [ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$ACTUAL_VERSION")" ]; then
|
|
pass "pi version $ACTUAL_VERSION (matches --expected-version)"
|
|
elif [ -n "$MANIFEST_RELEASE_TAG" ] &&
|
|
[ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then
|
|
# Exact, not heuristic: the value handed over IS this image's release
|
|
# tag, so it cannot be a pi version anyone meant.
|
|
fail "--expected-version $EXPECTED_VERSION is the pi-devbox IMAGE version, not the pi version — use --expected-image-version $EXPECTED_VERSION (live pi is $ACTUAL_VERSION)"
|
|
else
|
|
fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION (this flag asserts the pi coding agent version; for the image release tag use --expected-image-version)"
|
|
fi
|
|
elif [ -n "$MANIFEST_PI_VERSION" ]; then
|
|
# Not a tautology: the manifest records what pi reported at BUILD time,
|
|
# while `pi --version` resolves through PATH, which a stale npm-global
|
|
# volume install can shadow.
|
|
if [ "$MANIFEST_PI_VERSION" = "$ACTUAL_VERSION" ]; then
|
|
pass "pi version $ACTUAL_VERSION (matches this image's build manifest)"
|
|
else
|
|
fail "live pi $ACTUAL_VERSION != $MANIFEST_PI_VERSION recorded in $MANIFEST — a stale pi in the ~/.pi/npm-global volume is shadowing the baked one"
|
|
fi
|
|
else
|
|
warn "pi version $ACTUAL_VERSION (no --expected-version and no build manifest to compare against — informational only)"
|
|
fi
|
|
else
|
|
fail "pi --version failed"
|
|
fi
|
|
|
|
echo
|
|
echo "-- pi-devbox image version --"
|
|
if [ -z "$MANIFEST_RELEASE_TAG" ]; then
|
|
if [ -n "$EXPECTED_IMAGE_VERSION" ]; then
|
|
fail "cannot verify --expected-image-version $EXPECTED_IMAGE_VERSION: no readable release_tag in $MANIFEST (image built before the manifest existed, or jq missing)"
|
|
else
|
|
warn "image release tag unknown (no readable $MANIFEST) — pi-devbox-version would say the same"
|
|
fi
|
|
elif [ -n "$EXPECTED_IMAGE_VERSION" ]; then
|
|
if [ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then
|
|
pass "image version $MANIFEST_RELEASE_TAG (matches --expected-image-version)"
|
|
elif [ -n "$MANIFEST_PI_VERSION" ] &&
|
|
[ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$MANIFEST_PI_VERSION" ]; then
|
|
fail "--expected-image-version $EXPECTED_IMAGE_VERSION is the pi version, not the image release tag — use --expected-version $EXPECTED_IMAGE_VERSION (this image is $MANIFEST_RELEASE_TAG)"
|
|
else
|
|
fail "image version mismatch: expected $EXPECTED_IMAGE_VERSION, got $MANIFEST_RELEASE_TAG — the recreate did not pick up the intended image"
|
|
fi
|
|
else
|
|
warn "image version $MANIFEST_RELEASE_TAG (no --expected-image-version given — informational only)"
|
|
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
|
|
|
|
# global AGENTS.md symlink (pi-toolkit) — global instructions loaded by pi at
|
|
# every start (directs the agent to read the pi-extensions skill at session start)
|
|
if [ -L "$HOME/.pi/agent/AGENTS.md" ]; then
|
|
pass "~/.pi/agent/AGENTS.md symlink (pi-toolkit)"
|
|
else
|
|
fail "~/.pi/agent/AGENTS.md 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
|
|
|
|
# settings.json merge: the entrypoint deep-merges new template keys into a
|
|
# preserved settings.json on every start, so config added in an image upgrade
|
|
# (e.g. the observational-memory / pi-fork blocks) reaches existing volumes.
|
|
# Assert those blocks are present and that the file is still valid JSON.
|
|
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then
|
|
if jq -e 'has("observational-memory") and has("pi-fork")' "$HOME/.pi/agent/settings.json" >/dev/null 2>&1; then
|
|
pass "settings.json has observational-memory + pi-fork blocks (template merge)"
|
|
else
|
|
fail "settings.json missing observational-memory and/or pi-fork blocks (template merge did not land)"
|
|
fi
|
|
fi
|
|
|
|
# pi package registrations (pi install <local-path> → recorded in settings.json).
|
|
# Check the `packages` ARRAY, not the whole file: the settings template ships a
|
|
# top-level "pi-fork" CONFIG block (asserted just above), so `grep -q pi-fork
|
|
# settings.json` is a guaranteed false green — which is how an un-registered
|
|
# fork tool went unnoticed from v1.0.0 through v1.6.3. Same array check the
|
|
# fixed entrypoint-user.sh guard uses.
|
|
_pkg_registered() {
|
|
_s="$HOME/.pi/agent/settings.json"
|
|
[ -f "$_s" ] || return 1
|
|
if command -v jq >/dev/null 2>&1; then
|
|
jq -e --arg n "$1" \
|
|
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
|
|
"$_s" >/dev/null 2>&1
|
|
else
|
|
grep -q "opt/$1\"" "$_s"
|
|
fi
|
|
}
|
|
|
|
# True when a literal `npm:pi-atelier` entry is still present — the
|
|
# volume-resident registration the entrypoint migrates away from.
|
|
_npm_atelier_present() {
|
|
_s="$HOME/.pi/agent/settings.json"
|
|
[ -f "$_s" ] || return 1
|
|
command -v jq >/dev/null 2>&1 || return 1
|
|
jq -e '(.packages // []) | any(. == "npm:pi-atelier")' "$_s" >/dev/null 2>&1
|
|
}
|
|
|
|
if [ -f "$HOME/.pi/agent/settings.json" ]; then
|
|
for pkg in pi-fork pi-observational-memory; do
|
|
if _pkg_registered "$pkg"; then
|
|
pass "$pkg registered in settings.json packages[]"
|
|
else
|
|
fail "$pkg NOT in settings.json packages[] (tool will not load)"
|
|
fi
|
|
done
|
|
|
|
if [ "$VARIANT" = "studio" ]; then
|
|
if _pkg_registered pi-studio; then
|
|
pass "pi-studio registered in settings.json packages[]"
|
|
else
|
|
fail "pi-studio NOT in settings.json packages[] (studio variant)"
|
|
fi
|
|
fi
|
|
|
|
# pi-atelier — vendored from v1.7.0 on. Absent on older images, and
|
|
# deliberately unregistered when DEVBOX_ATELIER=0; neither is a failure.
|
|
if [ -d /opt/pi-atelier ]; then
|
|
if [ "${DEVBOX_ATELIER:-1}" = "0" ]; then
|
|
if _pkg_registered pi-atelier; then
|
|
fail "pi-atelier still in packages[] despite DEVBOX_ATELIER=0"
|
|
else
|
|
pass "pi-atelier unregistered (DEVBOX_ATELIER=0, as requested)"
|
|
fi
|
|
elif _pkg_registered pi-atelier; then
|
|
pass "pi-atelier registered in settings.json packages[]"
|
|
else
|
|
fail "pi-atelier NOT in settings.json packages[] (sidebar will not load)"
|
|
fi
|
|
if _npm_atelier_present; then
|
|
fail "stale npm:pi-atelier still in packages[] — it resolves through the ~/.pi/npm-global VOLUME and shadows the pinned /opt copy (entrypoint migration did not run)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ── pi <-> pi-atelier compatibility floor ─────────────────────────────
|
|
# atelier < 0.7.1 wraps pi's private TUI renderer in a way that recurses under
|
|
# pi >= 0.84: pi hangs at startup burning CPU, with no error message. atelier's
|
|
# own peerDependencies (>=0.80.7) do not encode this. Assert it here too, not
|
|
# just in the build-time smoke test: this script runs after a real
|
|
# `--force-recreate` on a live box, where a volume-resident old copy is exactly
|
|
# what could bite.
|
|
if [ -d /opt/pi-atelier ] && command -v jq >/dev/null 2>&1; then
|
|
_ge() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$2" ]; }
|
|
_av=$(jq -r '.version // empty' /opt/pi-atelier/package.json 2>/dev/null || true)
|
|
_pv=$(pi --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)
|
|
if [ -n "$_av" ] && [ -n "$_pv" ]; then
|
|
if _ge "$_pv" 0.84.0 && ! _ge "$_av" 0.7.1; then
|
|
fail "pi $_pv with pi-atelier $_av — atelier < 0.7.1 hangs pi >= 0.84 at startup (bump PI_ATELIER_REF in Dockerfile.variant)"
|
|
else
|
|
pass "pi $_pv + pi-atelier $_av (compatibility floor OK)"
|
|
fi
|
|
else
|
|
warn "could not compare pi/pi-atelier versions (pi='$_pv' atelier='$_av')"
|
|
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
|
|
|
|
# History flush must survive shell nesting. The DEVBOX_HIST_SET guard must NOT
|
|
# be exported: if it leaks into child processes, nested shells (esp. tmux
|
|
# panes) skip installing `history -a` and lose in-memory history on abrupt
|
|
# termination. Assert a child login shell still wires up the per-prompt flush.
|
|
if bash -lic 'bash -lic "case \"\$PROMPT_COMMAND\" in *\"history -a\"*) exit 0;; *) exit 1;; esac"' </dev/null >/dev/null 2>&1; then
|
|
pass "nested shell installs 'history -a' (DEVBOX_HIST_SET not exported)"
|
|
else
|
|
fail "nested shell missing 'history -a' — DEVBOX_HIST_SET leaking to children?"
|
|
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 ==="
|