diff --git a/AGENTS.md b/AGENTS.md index 1b32338..ab1b8e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,13 +22,16 @@ re-brand of opencode-devbox's `pi-only` variant. by copying `/opt/pi-extensions/skill/` over the committed `rootfs/` snapshot (Option 1 over Option 2 — see `skills/VENDORED.md`). - `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`. -- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket - dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions - deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install, - optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), image-baked - skills symlink-in, skillset deploy. +- `entrypoint-user.sh` — per-container start: prints the `pi-devbox-version` + banner first (which build/commit is running, from the manifest below), + then SSH ControlMaster socket dir, LAN-access setup, MemPalace init, + pi-toolkit + pi-extensions deploy, mempalace-bridge symlink, fork/recall + + 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, - 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//SKILL.md` — image-baked agent skills (the repo-authored `pi-devbox-environment`, plus vendored fallback copies of `pi-extensions` and `mempalace` — see `skills/VENDORED.md`) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01463a5..45d9e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,22 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). ## 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-`. + ### Changed - **Bundled `pi-toolkit` settings template: `pi-fork` balanced tier bumped to diff --git a/Dockerfile.base b/Dockerfile.base index 6d3c554..2384e10 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -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/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/pi-devbox-version /usr/local/bin/pi-devbox-version COPY entrypoint.sh /usr/local/bin/entrypoint.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 \ /usr/local/bin/studio-expose \ /usr/local/bin/dot-watch \ + /usr/local/bin/pi-devbox-version \ /usr/local/lib/pi-devbox/*.sh 2>/dev/null || true # Start as root — entrypoint adjusts UID/GID then drops to developer diff --git a/README.md b/README.md index 0f902ae..b204b7f 100644 --- a/README.md +++ b/README.md @@ -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 ``` +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 ### Image grew unexpectedly diff --git a/entrypoint-user.sh b/entrypoint-user.sh index 60cb3fa..1301913 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash 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 ──────────────────────────────── # 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 diff --git a/rootfs/usr/local/bin/pi-devbox-version b/rootfs/usr/local/bin/pi-devbox-version new file mode 100755 index 0000000..1505527 --- /dev/null +++ b/rootfs/usr/local/bin/pi-devbox-version @@ -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" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 29ebe32..02a3642 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -19,6 +19,8 @@ # - mempalace bridge symlink present # - settings.json bootstrapped # - 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 # client bundle present + registered via `pi install` # - 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. run "manifest has no unresolved ('unknown') components" \ "! 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 # 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)