commit 7b634605b79a852b83bf0be777d551eebbfa1e07 Author: Joakim Persson Date: Thu May 14 19:57:17 2026 +0200 Initial commit: pi-devbox v0.74.0 pi coding-agent container built on opencode-devbox:base-latest. Includes Dockerfile, docker-compose, CI workflow, smoke-test, README, CHANGELOG, AGENTS.md. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c0e5591 --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# pi-devbox environment configuration +# Copy this file to .env and fill in your values: +# cp .env.example .env + +# ── Workspace ──────────────────────────────────────────────────────── +# Path on host to mount as /workspace in the container +WORKSPACE_PATH=~/projects + +# Path to SSH keys on host +SSH_KEY_PATH=~/.ssh + +# ── Git Configuration ──────────────────────────────────────────────── +GIT_USER_NAME= +GIT_USER_EMAIL= + +# ── Gitea (for gitea-mcp MCP server) ──────────────────────────────── +# GITEA_ACCESS_TOKEN= +# GITEA_HOST=https://gitea.example.com + +# ── GitHub (optional, for GitHub MCP / git operations) ─────────────── +# GITHUB_PERSONAL_ACCESS_TOKEN= + +# ── AWS (optional, for AWS CLI / Bedrock) ──────────────────────────── +# AWS_REGION=eu-west-1 +# AWS_PROFILE=default +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= + +# ── Skillset (agent skills and instructions) ───────────────────────── +# If you have a skillset repo, the entrypoint auto-deploys skills and +# instructions on container start using relative symlinks. +# Detection is automatic if the skillset lives at WORKSPACE_PATH/skillset. +# SKILLSET_CONTAINER_PATH= + +# ── Locale ─────────────────────────────────────────────────────────── +# LANG=sv_SE.UTF-8 +# LANGUAGE=sv_SE:sv +# LC_ALL=sv_SE.UTF-8 diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml new file mode 100644 index 0000000..b37541c --- /dev/null +++ b/.gitea/workflows/docker-publish.yml @@ -0,0 +1,111 @@ +name: Publish Docker Image + +on: + push: + tags: + - 'v*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + BUILDKIT_PROGRESS: plain + IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox + +jobs: + smoke: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v4 + - run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf + - run: | + rm -rf /opt/hostedtoolcache /opt/microsoft /opt/az /opt/ghc \ + /usr/local/.ghcup /usr/share/dotnet /usr/share/swift \ + /usr/local/lib/android /usr/local/share/powershell \ + /usr/local/share/chromium /usr/local/share/boost \ + /usr/lib/jvm 2>/dev/null || true + docker system prune -af --volumes || true + docker builder prune -af || true + + - uses: docker/setup-buildx-action@v4 + with: {driver-opts: network=host} + + - name: Build (amd64, load to local daemon) + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: pi-devbox:smoke + + - name: Smoke test + run: bash scripts/smoke-test.sh pi-devbox:smoke + + publish: + needs: smoke + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v4 + - run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf + - run: | + rm -rf /opt/hostedtoolcache /opt/microsoft /opt/az /opt/ghc \ + /usr/local/.ghcup /usr/share/dotnet /usr/share/swift \ + /usr/local/lib/android /usr/local/share/powershell \ + /usr/local/share/chromium /usr/local/share/boost \ + /usr/lib/jvm 2>/dev/null || true + docker system prune -af --volumes || true + docker builder prune -af || true + + - uses: docker/setup-qemu-action@v3 + with: {platforms: arm64} + - uses: docker/setup-buildx-action@v4 + with: {driver-opts: network=host} + - uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Compute tags + id: tags + run: | + VERSION="${{ github.ref_name }}" + { echo "tags<> "$GITHUB_OUTPUT" + + - name: Build and push (amd64 + arm64) + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max + + update-description: + needs: publish + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v4 + - name: Update Docker Hub description + run: | + PAYLOAD=$(jq -n --rawfile desc DOCKER_HUB.md '{"full_description": $desc}') + TOKEN=$(curl -s -X POST "https://hub.docker.com/v2/auth/token" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${{ vars.DOCKERHUB_USERNAME }}\",\"password\":\"${{ secrets.DOCKERHUB_TOKEN }}\"}" \ + | jq -r '.token') + curl -s -X PATCH "https://hub.docker.com/v2/repositories/${{ vars.DOCKERHUB_USERNAME }}/pi-devbox/" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "${PAYLOAD}" | jq -r '.full_description | if . then "✅ description updated (\(. | length) chars)" else "❌ update failed" end' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b78152a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md — pi-devbox + +Container image that adds pi coding-agent on top of the opencode-devbox base image. + +## Repository layout + +- `Dockerfile` — single-stage build, `FROM opencode-devbox:base-latest`, installs pi + companion repos +- `docker-compose.yml` — compose file for local use +- `.env.example` — environment variable template +- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Docker Hub +- `.gitea/workflows/docker-publish.yml` — CI pipeline: smoke amd64 → multi-arch push → update Hub description + +## Versioning scheme + +- Tags follow the pi npm version: `v{pi_version}[letter]` +- Bump `PI_VERSION` build-arg default in `Dockerfile` when cutting a new release +- Docker Hub: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest` + +## Release-day checklist + +1. Bump `PI_VERSION` in `Dockerfile` (or leave as `latest` to pick up current) +2. Update `CHANGELOG.md`: promote `Unreleased` → `vX.Y.Z — YYYY-MM-DD` +3. Add fresh `## Unreleased` section +4. Commit, tag `vX.Y.Z`, push tag → CI fires automatically + +## Key facts + +- **Base image**: `joakimp/opencode-devbox:base-latest` — rebuilt whenever opencode-devbox cuts a new base +- **pi binary**: baked at `/usr/bin/pi` (system npm prefix); `NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global` at runtime so user-installed pi/packages land on the named volume +- **Companion repos**: pi-toolkit and pi-extensions cloned to `/opt/` at build time; `entrypoint-user.sh` (inherited from base) deploys symlinks to `~/.pi/agent/` on container start +- **MemPalace**: fully operational — inherited from base image; bridge extension deployed by entrypoint + +## Conventions + +- Do NOT call `mempalace-toolkit/install.sh` in the Dockerfile — the base entrypoint handles it +- `NPM_CONFIG_PREFIX=/usr` must be set per-RUN for any build-time `npm install -g` to keep baked binaries off the volume-shadowed path +- The smoke test threshold is 2200 MB — update if the image legitimately grows past it diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bd8e433 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to the pi-devbox container image. + +Tags follow the pi npm version: `v{pi_version}[letter]` — bare tag for the first build on a new pi release, letter suffix (`b`, `c`, …) for container-level rebuilds on the same version. + +--- + +## Unreleased + +## v0.74.0 — 2026-05-14 + +Initial release. + +- pi `@earendil-works/pi-coding-agent@0.74.0` baked at `/usr/bin/pi` +- pi-toolkit and pi-extensions cloned at build time; deployed to `~/.pi/agent/` by entrypoint on container start +- mempalace bridge (`mempalace.ts`) symlinked from `/opt/mempalace-toolkit/` +- Built on `joakimp/opencode-devbox:base-latest` diff --git a/DOCKER_HUB.md b/DOCKER_HUB.md new file mode 100644 index 0000000..8a09039 --- /dev/null +++ b/DOCKER_HUB.md @@ -0,0 +1 @@ +pi coding-agent container — built on opencode-devbox base. Includes pi, pi-toolkit, pi-extensions, mempalace, AWS CLI, neovim, and full dev toolchain. See https://gitea.jordbo.se/joakimp/pi-devbox for full docs. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..79d090d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# pi-devbox — pi coding-agent container +# +# Builds on top of the opencode-devbox base image, which provides: +# Debian trixie, Node.js, AWS CLI, mempalace + MCP server, gitea-mcp, +# dev tools (neovim, tmux, bat, eza, fzf, zoxide, ripgrep, uv, rustup), +# user setup (developer/gosu), entrypoints, chromadb prewarm. +# +# This image adds only pi itself and its companion repos. +# +# Build args: +# BASE_IMAGE — base image to build from (default: base-latest) +# PI_VERSION — pi npm version: "latest" or a pinned version e.g. "0.74.0" +# PI_TOOLKIT_REF — git ref for pi-toolkit (default: main) +# PI_EXTENSIONS_REF — git ref for pi-extensions (default: main) + +ARG BASE_IMAGE=joakimp/opencode-devbox:base-latest +FROM ${BASE_IMAGE} + +ARG PI_VERSION=latest +ARG PI_TOOLKIT_REF=main +ARG PI_EXTENSIONS_REF=main + +# Install pi and clone companion repos. +# NPM_CONFIG_PREFIX is overridden to /usr so the baked binary lands at the +# system prefix — same pattern as opencode-devbox's variant Dockerfile. +# At runtime, NPM_CONFIG_PREFIX is reset to /home/developer/.pi/npm-global +# (inherited from base ENV) so user-installed packages land on the named +# volume and survive container recreate. +RUN 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 clone --depth 1 --branch "${PI_TOOLKIT_REF}" \ + https://gitea.jordbo.se/joakimp/pi-toolkit.git /opt/pi-toolkit && \ + git clone --depth 1 --branch "${PI_EXTENSIONS_REF}" \ + https://gitea.jordbo.se/joakimp/pi-extensions.git /opt/pi-extensions && \ + 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)" + +# WORKDIR / ENTRYPOINT / CMD inherited from base. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd0cd4c --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# pi-devbox + +A Docker container image with [pi coding-agent](https://github.com/earendil-works/pi) pre-installed, built on the [opencode-devbox](https://gitea.jordbo.se/joakimp/opencode-devbox) base image. + +## What's inside + +Built on `opencode-devbox:base-latest`, which provides: + +- **Debian trixie** (stable base) +- **Node.js** (LTS), **uv** (Python), **rustup** (Rust on-demand) +- **AWS CLI** v2 +- **MemPalace** + MCP server (persistent agent memory across sessions) +- **Gitea MCP** server +- **Dev tools**: neovim (LazyVim), tmux, bat, eza, fzf, zoxide, ripgrep, git-lfs, make +- **Shell**: bash with history tuning, prefix-search, fzf/zoxide integration + +This image adds: + +- **pi** (`@earendil-works/pi-coding-agent`) — baked at `/usr/bin/pi` +- **pi-toolkit** — keybindings, env loader, settings template (cloned to `/opt/pi-toolkit`) +- **pi-extensions** — ext-toggle, todo, ssh-controlmaster, notify, git-checkpoint, mcp-loader, confirm-destructive (cloned to `/opt/pi-extensions`) +- **mempalace bridge** — `mempalace.ts` extension symlinked from `/opt/mempalace-toolkit` + +## Quick start + +```bash +cp .env.example .env +# edit .env — set WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL +docker compose run --rm devbox +# inside the container: +pi +``` + +## Versioning + +Tags follow the pi npm version: `v0.74.0`, `v0.75.0`, etc. +`latest` always points at the most recent release. + +## Persistence + +| Volume | What it holds | +|--------|---------------| +| `devbox-pi-config` | pi settings, extensions toggle state, sessions (`~/.pi/`) | +| `devbox-shell-history` | bash history | +| `devbox-zoxide` | zoxide directory jump history | +| `devbox-nvim-data` | neovim plugins, Mason packages | +| `devbox-uv` | uv Python installs and tool cache | + +## User-installed pi packages + +`NPM_CONFIG_PREFIX` is set to `/home/developer/.pi/npm-global`, so any `pi install npm:...` or `npm install -g` as the `developer` user lands on the `devbox-pi-config` volume and survives container recreation and image rebuilds. A user-installed pi wins over the baked binary via `PATH` order. + +## Source + +- [pi-devbox](https://gitea.jordbo.se/joakimp/pi-devbox) — this repo +- [opencode-devbox](https://gitea.jordbo.se/joakimp/opencode-devbox) — base image source +- [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) +- [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1fd237f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +# pi-devbox docker-compose +# +# Usage: +# cp .env.example .env # configure your keys +# docker compose up -d +# docker compose exec -u developer devbox pi +# +# Or for interactive one-shot: +# docker compose run --rm devbox + +name: pi-devbox + +services: + devbox: + image: joakimp/pi-devbox:latest + # To build from source instead of pulling from Docker Hub: + # build: + # context: . + # args: + # PI_VERSION: "latest" + container_name: pi-devbox + stdin_open: true + tty: true + env_file: + - .env + environment: + - TERM=xterm-256color + - GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-} + - GITEA_HOST=${GITEA_HOST:-} + - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-} + volumes: + # Host workspace — mount your project here + - ${WORKSPACE_PATH:-.}:/workspace + + # SSH keys (read-only) — for git push/pull + - ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro + + # Optional: mount skillset repo for automatic skill/instruction deployment. + # - ${SKILLSET_PATH}:/home/developer/skillset + + # Persist pi config (settings.json, extensions, sessions, auth) + - devbox-pi-config:/home/developer/.pi + + # Persist bash history across container recreations + - devbox-shell-history:/home/developer/.cache/bash + + # Persist zoxide directory history + - devbox-zoxide:/home/developer/.local/share/zoxide + + # Persist neovim plugin/Mason data + - devbox-nvim-data:/home/developer/.local/share/nvim + + # Persist uv data (Python installs, tool installs) + - devbox-uv:/home/developer/.local/share/uv + + # Optional: persist MemPalace data (conversation memory, knowledge graph) + # - devbox-palace:/home/developer/.mempalace + + # Optional: persist ChromaDB embedding model cache (~79 MB) + # - devbox-chroma-cache:/home/developer/.cache/chroma + + # Optional: AWS credentials/SSO config + # - ~/.aws:/home/developer/.aws + +volumes: + devbox-pi-config: + devbox-shell-history: + devbox-zoxide: + devbox-nvim-data: + devbox-uv: + # devbox-palace: + # devbox-chroma-cache: diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..d7e8dad --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# smoke-test.sh — basic sanity checks for the pi-devbox image +# +# Usage: ./scripts/smoke-test.sh +# +# Verifies: +# - pi binary present and returns a version +# - pi-toolkit cloned at /opt/pi-toolkit +# - pi-extensions cloned at /opt/pi-extensions +# - entrypoint deploys pi-toolkit keybindings symlink +# - entrypoint deploys ≥4 extensions +# - mempalace bridge symlink present +# - settings.json bootstrapped +# - image size within threshold + +set -euo pipefail + +IMAGE="${1:?usage: $0 }" +PASS=0; FAIL=0 +SIZE_THRESHOLD_MB=2200 + +run() { + local label="$1"; local cmd="$2" + if docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" >/dev/null 2>&1; then + printf " ✅ %s\n" "$label"; ((PASS++)) + else + printf " ❌ %s\n" "$label"; ((FAIL++)) + fi +} + +echo "=== pi-devbox smoke test: $IMAGE ===" +echo "" + +# ── Basic binary checks ─────────────────────────────────────────────── +echo "── Binaries ──" +run "pi" "pi --version" +run "node" "node --version" +run "git" "git --version" +run "aws" "aws --version" +run "uv" "uv --version" +run "nvim" "nvim --version" +run "mempalace-mcp" "mempalace-mcp --help" + +# ── Repo clones ─────────────────────────────────────────────────────── +echo "" +echo "── Repo clones ──" +run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD" +run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD" + +# ── Runtime deployment (needs entrypoint to run) ────────────────────── +echo "" +echo "── Runtime deployment ──" +CID=$(docker run -d --entrypoint="" "$IMAGE" sleep 60) +cleanup() { docker rm -f "$CID" >/dev/null 2>&1 || true; } +trap cleanup EXIT + +# Wait for entrypoint-user.sh to finish deploying pi-toolkit + extensions +for i in $(seq 1 30); do + if docker exec "$CID" test -L /home/developer/.pi/agent/keybindings.json 2>/dev/null; then + break + fi + sleep 1 +done + +exec_test() { + local label="$1"; local cmd="$2" + if docker exec "$CID" sh -c "$cmd" >/dev/null 2>&1; then + printf " ✅ %s\n" "$label"; ((PASS++)) + else + printf " ❌ %s\n" "$label"; ((FAIL++)) + fi +} + +exec_test "keybindings.json (pi-toolkit)" 'test -L $HOME/.pi/agent/keybindings.json && echo ok' +exec_test "extensions ≥ 4 (pi-extensions)" 'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"' +exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok' +exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok' + +# ── Image size ──────────────────────────────────────────────────────── +echo "" +echo "── Image size ──" +SIZE_MB=$(docker image inspect "$IMAGE" --format='{{.Size}}' | awk '{printf "%d", $1/1048576}') +if [ "$SIZE_MB" -le "$SIZE_THRESHOLD_MB" ]; then + printf " ✅ size: %d MB (threshold %d MB)\n" "$SIZE_MB" "$SIZE_THRESHOLD_MB"; ((PASS++)) +else + printf " ❌ size: %d MB exceeds threshold %d MB\n" "$SIZE_MB" "$SIZE_THRESHOLD_MB"; ((FAIL++)) +fi + +# ── Summary ─────────────────────────────────────────────────────────── +echo "" +echo "=== Results: ${PASS} passed, ${FAIL} failed ===" +[ "$FAIL" -eq 0 ]