# pi-devbox — variant image # # FROMs a base- image produced by Dockerfile.base and adds only # the variant-specific tools — currently just the pi install. Kept as a # separate file (rather than collapsed into Dockerfile.base) so future # variants (e.g. studio, studio-tex) can FROM the variant or extend # this Dockerfile with additional build args without rebuilding the # base on every pi version bump. # # Pass `--build-arg BASE_IMAGE=:base-` to select the base. # CI computes the base hash from Dockerfile.base + rootfs/ + # entrypoint*.sh and feeds it in. # # IMPORTANT: the base image sets NPM_CONFIG_PREFIX to # /home/developer/.pi/npm-global so runtime `pi install npm:...` and # `npm install -g` by the developer user lands on the named volume. # At BUILD time we want the baked binaries on /usr so they survive the # volume mount. Each `npm install -g` below therefore prefixes the # command with `NPM_CONFIG_PREFIX=/usr`. ARG BASE_IMAGE FROM ${BASE_IMAGE} ARG TARGETARCH ARG USER_NAME=developer # ── pi coding-agent + companions ───────────────────────────────────── # pi-toolkit and pi-extensions are cloned into /opt/. entrypoint-user.sh # runs each repo's install.sh on container start so symlinks land under # ~/.pi/agent/ on the named volume. # # PI_VERSION should be passed explicitly by CI as a concrete version # (resolved from `npm view @earendil-works/pi-coding-agent version`). # The default `latest` is for local dev convenience only — it has a # known cache-hit footgun in registry-cached CI builds: the resulting # build-arg string is byte-identical across builds, the layer-hash is # identical, and the registry buildcache silently reuses the layer # from whatever pi version was current when the cache was first # populated. CI MUST pass a resolved concrete version. See pi-devbox # v0.75.5b 2026-05-23 for the discovery + canonical fix. ARG PI_VERSION=latest ARG PI_TOOLKIT_REF=main ARG PI_EXTENSIONS_REF=main # pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub # under elpapi42. CI resolves these to commit SHAs to defeat the same # cache-hit footgun that affects PI_VERSION. ARG PI_FORK_REPO=https://github.com/elpapi42/pi-fork.git ARG PI_FORK_REF=master ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git ARG PI_OBSMEM_REF=master RUN set -e && \ # git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name # OR a commit SHA as $ref. Uses `git fetch + checkout FETCH_HEAD` # which (a) works with both name and SHA forms uniformly, and (b) defeats # the registry-buildcache footgun when CI passes a resolved SHA. The # earlier helper `git_clone_retry` (using `git clone --branch`) only # worked with branch names — a SHA-resolved build-arg made `git clone # --branch <40-char-SHA>` fail with "Remote branch not found". Surfaced # in pi-devbox v1.0.0-rerun (run 374) 2026-06-10 and fixed by switching # all four clones to git_fetch_ref. Both Gitea and GitHub allow fetching # arbitrary commits by default (uploadpack.allowReachableSHA1InWant). git_fetch_ref() { \ url="$1"; ref="$2"; dest="$3"; \ rm -rf "$dest"; mkdir -p "$dest"; \ git -C "$dest" init -q && git -C "$dest" remote add origin "$url" && \ for i in 1 2 3 4 5; do \ if git -C "$dest" fetch --depth 1 origin "$ref" && git -C "$dest" checkout -q FETCH_HEAD; then return 0; fi; \ echo "git fetch $url@$ref failed (attempt $i/5), retrying in $((i*5))s..."; \ sleep $((i*5)); \ done; \ return 1; \ } && \ if [ "${PI_VERSION}" = "latest" ]; then \ NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \ else \ NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \ fi && \ pi --version && \ git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-toolkit.git" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \ git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-extensions.git" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \ git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \ git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \ (cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \ (cd /opt/pi-observational-memory && npm install --omit=dev --no-audit --no-fund) && \ echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \ echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" && \ echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \ echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" # ── Optional: pi-studio (:latest-studio variant) ───────────────────── # pi-studio (omaclaren/pi-studio) is a pi-package + theme providing a # two-pane browser workspace: prompt/response editor, KaTeX/Mermaid live # preview, and tmux-backed literate REPLs. Off by default; the studio # variant sets INSTALL_STUDIO=true. # # Vendored to /opt/pi-studio and registered at container start by # entrypoint-user.sh via `pi install /opt/pi-studio` — the SAME pattern # as pi-fork / pi-observational-memory above. We deliberately do NOT run # `pi install ` at build time: that writes into ~/.pi/agent, # which is a named volume, so a build-time install collides with / is # shadowed by the volume on first run. Vendoring to /opt (an image layer) # + a runtime local-path install keeps it on the image and idempotent. # # No build step is needed: pi-studio ships its browser bundle prebuilt in # git (client/studio-client.js) and pi loads index.ts directly; its # package.json scripts are only test/typecheck. So we just fetch + install # the 3 prod deps (@earendil-works/pi-ai, @sinclair/typebox, ws). # # PI_STUDIO_REF is CI-resolved to a commit SHA to defeat the registry- # buildcache cache-hit footgun (see the PI_VERSION note above). ARG INSTALL_STUDIO=false ARG PI_STUDIO_REPO=https://github.com/omaclaren/pi-studio.git ARG PI_STUDIO_REF=main RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \ set -e; \ rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \ git -C /opt/pi-studio init -q && \ git -C /opt/pi-studio remote add origin "${PI_STUDIO_REPO}" && \ ok=0; for i in 1 2 3 4 5; do \ if git -C /opt/pi-studio fetch --depth 1 origin "${PI_STUDIO_REF}" && \ git -C /opt/pi-studio checkout -q FETCH_HEAD; then ok=1; break; fi; \ echo "git fetch pi-studio@${PI_STUDIO_REF} failed (attempt $i/5), retrying in $((i*5))s..."; \ sleep $((i*5)); \ done; \ [ "$ok" = "1" ] && \ (cd /opt/pi-studio && npm install --omit=dev --no-audit --no-fund) && \ echo "pi-studio at $(cd /opt/pi-studio && git rev-parse --short HEAD)"; \ fi # STUDIO_PORT: advisory default consumed by docker-compose port publishing # and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch. # Harmless in the non-studio variant. NOTE: pi-studio hard-binds the server # to 127.0.0.1 inside the container (index.ts: .listen(port,"127.0.0.1")), # so reaching it from a browser needs a loopback bridge or host networking — # see the "Using pi-studio" section in README.md. ENV STUDIO_PORT=8765 # ── Optional: Go toolchain ─────────────────────────────────────────── # Off by default; opt in for users who run Go tools inside the devbox. ARG INSTALL_GO=false ARG GO_VERSION=latest RUN if [ "${INSTALL_GO}" = "true" ]; then \ GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${GO_VERSION}" && \ if [ "$V" = "latest" ]; then \ V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \ awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \ fi && \ [ -n "$V" ] && \ echo "Installing Go ${V}" && \ curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \ ln -s /usr/local/go/bin/go /usr/local/bin/go && \ ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \ fi # WORKDIR / ENTRYPOINT / CMD inherited from base.