base: add pi-devbox-version command + startup banner
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 1m11s

Wraps /etc/pi-devbox/build-manifest.json (already written at docker-build
time in Dockerfile.variant) into a human-readable summary instead of
requiring users to know the manifest path and pipe it through jq
themselves.

- rootfs/usr/local/bin/pi-devbox-version: human (default) / --json /
  --quiet output modes. Also flags live drift — compares the baked
  pi_version against the actually-running `pi --version` and warns
  on mismatch rather than trusting the manifest blindly (same
  ground-truth philosophy as the manifest generation itself). Exits 1
  with a short stderr notice on images built before the manifest
  existed, instead of failing silently.
- entrypoint-user.sh: calls it as the very first line. CMD is
  `bash -l` with tty:true in compose, so this banner lands directly
  above the first prompt on container start — no separate motd/bashrc
  hook needed (deliberately not wired into .bash_aliases, which would
  reprint on every docker exec -it).
- Dockerfile.base: COPY + chmod, same pattern as dot-watch/studio-expose.
- scripts/smoke-test.sh: 4 new checks (binary present+executable, human
  output has release tag, --json round-trips the manifest, --quiet is
  a single line).
- README.md / AGENTS.md / CHANGELOG.md updated.
This commit is contained in:
2026-07-15 14:50:47 +02:00
parent 02be95ac1f
commit fb49828826
7 changed files with 158 additions and 6 deletions
+9 -6
View File
@@ -22,13 +22,16 @@ re-brand of opencode-devbox's `pi-only` variant.
by copying `/opt/pi-extensions/skill/` over the committed `rootfs/` snapshot by copying `/opt/pi-extensions/skill/` over the committed `rootfs/` snapshot
(Option 1 over Option 2 — see `skills/VENDORED.md`). (Option 1 over Option 2 — see `skills/VENDORED.md`).
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`. - `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket - `entrypoint-user.sh` — per-container start: prints the `pi-devbox-version`
dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions banner first (which build/commit is running, from the manifest below),
deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install, then SSH ControlMaster socket dir, LAN-access setup, MemPalace init,
optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), image-baked pi-toolkit + pi-extensions deploy, mempalace-bridge symlink, fork/recall +
skills symlink-in, skillset deploy. pi-studio pi-install, optional `studio-expose` bridge (when
`STUDIO_EXPOSE=1`), image-baked skills symlink-in, skillset deploy.
- `rootfs/` — files baked into the image (bash aliases, inputrc, - `rootfs/` — files baked into the image (bash aliases, inputrc,
setup-lan-access.sh, `studio-expose` helper). Also setup-lan-access.sh, `studio-expose` helper, `pi-devbox-version` — wraps
`/etc/pi-devbox/build-manifest.json` into a human-readable summary + live
drift check, see README “Build provenance”). Also
`usr/local/share/pi-devbox/skills/<name>/SKILL.md` — image-baked agent `usr/local/share/pi-devbox/skills/<name>/SKILL.md` — image-baked agent
skills (the repo-authored `pi-devbox-environment`, plus vendored fallback skills (the repo-authored `pi-devbox-environment`, plus vendored fallback
copies of `pi-extensions` and `mempalace` — see `skills/VENDORED.md`) copies of `pi-extensions` and `mempalace` — see `skills/VENDORED.md`)
+16
View File
@@ -13,6 +13,22 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased ## Unreleased
### Added
- **`pi-devbox-version` command.** Wraps `/etc/pi-devbox/build-manifest.json`
into a human-readable summary (release tag, build date, source revision,
baked `pi_version`, and short SHAs for every `/opt` component) instead of
requiring users to know the manifest path and pipe it through `jq`
themselves. Also flags **live drift** — if `pi --version` no longer matches
what was baked at build time, the `pi:` line calls that out rather than
silently trusting the manifest. `--json` dumps the raw manifest for
scripting; `--quiet` gives a one-line `release_tag (source_revision)` form.
Printed automatically once at container start (`entrypoint-user.sh`, before
the rest of the setup output), and stays available on demand for the rest
of the session. Exits 1 with a short notice — rather than failing silently
— on images built before this file existed. Base-affecting (new
`rootfs/usr/local/bin/pi-devbox-version`), rebuilds `base-<hash>`.
### Changed ### Changed
- **Bundled `pi-toolkit` settings template: `pi-fork` balanced tier bumped to - **Bundled `pi-toolkit` settings template: `pi-fork` balanced tier bumped to
+2
View File
@@ -604,11 +604,13 @@ COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
COPY rootfs/usr/local/share/pi-devbox/ /usr/local/share/pi-devbox/ COPY rootfs/usr/local/share/pi-devbox/ /usr/local/share/pi-devbox/
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
COPY rootfs/usr/local/bin/pi-devbox-version /usr/local/bin/pi-devbox-version
COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \ RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
/usr/local/bin/studio-expose \ /usr/local/bin/studio-expose \
/usr/local/bin/dot-watch \ /usr/local/bin/dot-watch \
/usr/local/bin/pi-devbox-version \
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true /usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
# Start as root — entrypoint adjusts UID/GID then drops to developer # Start as root — entrypoint adjusts UID/GID then drops to developer
+28
View File
@@ -693,6 +693,34 @@ truth** — the actual checked-out commit of each `/opt` clone and the live
docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json
``` ```
Inside a running container, `pi-devbox-version` wraps that manifest into a
human-readable summary — no need to remember the file path or pipe it
through `jq` yourself:
```console
$ pi-devbox-version
pi-devbox v1.5.0
built: 2026-07-13T17:53:16Z (source d68674d11e06)
pi: 0.80.6
components:
pi-toolkit: 9a8f6faeaa08
pi-extensions: 61c98e004e3d
pi-fork: 4a09af4ef527
pi-observational-memory: 27a5195eaf90
mempalace-toolkit: 96699f2a1781
pi-studio: 2ef38ef31cea
```
It also flags **live drift** — if `pi --version` no longer matches what was
baked at build time (e.g. something on a persisted volume shadowed the
image's binary), the `pi:` line calls that out instead of silently trusting
the manifest. `--json` dumps the raw manifest for scripting; `--quiet` gives
a one-line `release_tag (source_revision)` form. It also prints once,
automatically, at container start (from `entrypoint-user.sh`, before the
rest of the setup output) — so you see which build you're in without
asking. Exits 1 with a short notice on images built before this file
existed, rather than failing silently.
## Troubleshooting ## Troubleshooting
### Image grew unexpectedly ### Image grew unexpectedly
+8
View File
@@ -1,6 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ── Startup banner: which pi-devbox build is this? ─────────────────
# Printed FIRST, before the setup noise below, so it's the first thing
# visible when the container starts (CMD is `bash -l`, tty:true in compose,
# so this reaches the same stream as the interactive shell the user lands
# in). Reads the ground-truth manifest baked in Dockerfile.variant; a no-op
# with a short stderr notice on images built before it existed.
command -v pi-devbox-version >/dev/null 2>&1 && pi-devbox-version || true
# ── SSH ControlMaster socket dir ──────────────────────────────── # ── SSH ControlMaster socket dir ────────────────────────────────
# Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the # Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the
# base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this # base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# pi-devbox-version — show which pi-devbox image build is running.
#
# WHY THIS EXISTS
# The image bakes ground-truth build info into /etc/pi-devbox/build-manifest.json
# at `docker build` time (see Dockerfile.variant): the release tag, build date,
# source commit, live `pi --version` at build time, and the actual checked-out
# commit of every /opt component clone. That answers "what image am I running?"
# — but only if you know to go look for the file. This wraps it into one
# command, prints it human-first at container start (see entrypoint-user.sh),
# and stays available on demand for the rest of the session.
#
# USAGE
# pi-devbox-version human-readable summary (default)
# pi-devbox-version --json raw manifest JSON (for scripting)
# pi-devbox-version --quiet one-line "release_tag (source_revision)" form
#
# EXIT STATUS
# 0 on success. 1 if the manifest is missing (e.g. an image built before
# this file existed, or a non-pi-devbox base) — prints a short notice
# to stderr rather than failing silently.
set -euo pipefail
MANIFEST=/etc/pi-devbox/build-manifest.json
MODE="human"
case "${1:-}" in
--json) MODE="json" ;;
--quiet|-q) MODE="quiet" ;;
--help|-h)
sed -n '2,20p' "$0" | sed 's/^# \?//'
exit 0
;;
esac
if [ ! -f "$MANIFEST" ]; then
echo "pi-devbox-version: no build manifest at $MANIFEST" >&2
echo " (image predates the manifest, or this isn't a pi-devbox image)" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "pi-devbox-version: jq not found; dumping raw manifest instead" >&2
cat "$MANIFEST"
exit 0
fi
if [ "$MODE" = "json" ]; then
cat "$MANIFEST"
exit 0
fi
release_tag=$(jq -r '.release_tag' "$MANIFEST")
build_date=$(jq -r '.build_date' "$MANIFEST")
source_rev=$(jq -r '.source_revision' "$MANIFEST")
pi_version_baked=$(jq -r '.pi_version' "$MANIFEST")
if [ "$MODE" = "quiet" ]; then
printf '%s (%s)\n' "$release_tag" "${source_rev:0:7}"
exit 0
fi
# Live drift check: has `pi` been upgraded since this container was built?
# (image is immutable, but a volume-persisted ~/.pi could in theory shadow
# the baked binary — this stays honest rather than trusting the manifest
# blindly, same "ground truth over intent" spirit as how the manifest
# itself is generated in Dockerfile.variant.)
pi_version_live=""
if command -v pi >/dev/null 2>&1; then
pi_version_live=$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')
fi
printf 'pi-devbox %s\n' "$release_tag"
printf ' built: %s (source %s)\n' "$build_date" "${source_rev:0:12}"
if [ -n "$pi_version_live" ] && [ "$pi_version_live" != "$pi_version_baked" ]; then
printf ' pi: %s \033[33m(baked as %s — drift detected)\033[0m\n' "$pi_version_live" "$pi_version_baked"
else
printf ' pi: %s\n' "${pi_version_live:-$pi_version_baked}"
fi
printf ' components:\n'
jq -r '.components | to_entries[] | select(.value != null) | " \(.key): \(.value[0:12])"' "$MANIFEST"
+12
View File
@@ -19,6 +19,8 @@
# - mempalace bridge symlink present # - mempalace bridge symlink present
# - settings.json bootstrapped # - settings.json bootstrapped
# - pi-fork + pi-observational-memory registered via `pi install` # - pi-fork + pi-observational-memory registered via `pi install`
# - pi-devbox-version command present + wraps the build manifest correctly
# (human, --json, --quiet)
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt # - (studio variant only, auto-detected) pi-studio cloned + prebuilt
# client bundle present + registered via `pi install` # client bundle present + registered via `pi install`
# - image size within threshold # - image size within threshold
@@ -159,6 +161,16 @@ run_expect "manifest records pi_version" \
# non-studio variant) — 'unknown' means a clone silently failed to resolve. # non-studio variant) — 'unknown' means a clone silently failed to resolve.
run "manifest has no unresolved ('unknown') components" \ run "manifest has no unresolved ('unknown') components" \
"! grep -q '\"unknown\"' /etc/pi-devbox/build-manifest.json" "! grep -q '\"unknown\"' /etc/pi-devbox/build-manifest.json"
# pi-devbox-version wraps the manifest into a human-first command (this
# PR); verify the binary is present, executable, and both output modes work.
run "pi-devbox-version binary present + executable" \
"test -x /usr/local/bin/pi-devbox-version"
run_expect "pi-devbox-version human output shows release tag" \
"pi-devbox-version" "pi-devbox v"
run_expect "pi-devbox-version --json round-trips the manifest" \
"pi-devbox-version --json" '"release_tag"'
run_expect "pi-devbox-version --quiet is a compact one-liner" \
"pi-devbox-version --quiet | wc -l" "1"
# OCI labels live in the image config, not the container fs — inspect them # OCI labels live in the image config, not the container fs — inspect them
# from the host docker rather than via `docker run`. # from the host docker rather than via `docker run`.
LBL=$(docker inspect --format '{{ index .Config.Labels "se.jordbo.pi-devbox.pi-extensions-ref" }}' "$IMAGE" 2>/dev/null || true) LBL=$(docker inspect --format '{{ index .Config.Labels "se.jordbo.pi-devbox.pi-extensions-ref" }}' "$IMAGE" 2>/dev/null || true)