feat(studio): add :latest-studio variant (PR-3)
Bundle pi-studio (omaclaren/pi-studio) as a new -studio image variant: browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs, /studio command + studio_* agent tools. - Dockerfile.variant: INSTALL_STUDIO + PI_STUDIO_REPO/REF args; vendor pi-studio to /opt/pi-studio (no build step — prebuilt client in git; npm install --omit=dev for 3 prod deps). STUDIO_PORT=8765 advisory. - entrypoint-user.sh: register /opt/pi-studio via the existing pi install local-path loop (auto-skips in non-studio variant). - smoke-test.sh: auto-detected studio assertions (clone + prebuilt client + pi install registration). - CI: resolve PI_STUDIO_REF to a SHA; independent smoke-studio + build-variant-studio jobs that gate ONLY the -studio tags, so a studio failure never blocks the core :latest release. - README: 'Using pi-studio' section documenting the container access reality — pi-studio hard-binds 127.0.0.1 (index.ts .listen(port, '127.0.0.1'), no --host flag), so -p publish alone can't reach it. Documents host-networking and loopback-bridge paths, the remote ssh -L forward, and the mosh caveat (no port forwarding; run parallel ssh -L). - CHANGELOG/AGENTS/DOCKER_HUB updated. Will tag as v1.1.0 (minor). No tag created — stopping for review.
This commit is contained in:
@@ -116,6 +116,7 @@ jobs:
|
|||||||
obsmem_ref: ${{ steps.resolve.outputs.obsmem_ref }}
|
obsmem_ref: ${{ steps.resolve.outputs.obsmem_ref }}
|
||||||
toolkit_ref: ${{ steps.resolve.outputs.toolkit_ref }}
|
toolkit_ref: ${{ steps.resolve.outputs.toolkit_ref }}
|
||||||
extensions_ref: ${{ steps.resolve.outputs.extensions_ref }}
|
extensions_ref: ${{ steps.resolve.outputs.extensions_ref }}
|
||||||
|
studio_ref: ${{ steps.resolve.outputs.studio_ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve pi version + companion refs
|
- name: Resolve pi version + companion refs
|
||||||
id: resolve
|
id: resolve
|
||||||
@@ -150,9 +151,16 @@ jobs:
|
|||||||
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
|
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
|
||||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
# Resolve pi-studio (omaclaren/pi-studio) main HEAD to a SHA for
|
||||||
|
# the :latest-studio variant — same cache-busting rationale.
|
||||||
|
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
|
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || echo "main")
|
||||||
|
[ -n "$STUDIO_REF" ] || STUDIO_REF=main
|
||||||
|
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Resolved PI_VERSION=${PI_VERSION}"
|
echo "Resolved PI_VERSION=${PI_VERSION}"
|
||||||
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
||||||
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
||||||
|
echo "Resolved PI_STUDIO_REF=${STUDIO_REF}"
|
||||||
|
|
||||||
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
||||||
build-base:
|
build-base:
|
||||||
@@ -277,6 +285,62 @@ jobs:
|
|||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
run: bash scripts/smoke-test.sh pi-devbox:smoke
|
run: bash scripts/smoke-test.sh pi-devbox:smoke
|
||||||
|
|
||||||
|
# ── Phase 3b: amd64 smoke for the studio variant ────────────────────
|
||||||
|
# Additive + independent of the core `smoke` job: gates ONLY
|
||||||
|
# build-variant-studio, never the core build-variant. A studio build or
|
||||||
|
# smoke failure therefore cannot block the :latest / :vX.Y.Z release.
|
||||||
|
smoke-studio:
|
||||||
|
needs: [base-decide, build-base, resolve-versions]
|
||||||
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.base-decide.result == 'success' &&
|
||||||
|
needs.resolve-versions.result == 'success' &&
|
||||||
|
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
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}
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build amd64 studio variant for smoke
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.variant
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: pi-devbox:smoke-studio
|
||||||
|
build-args: |
|
||||||
|
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
|
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
|
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_ref }}
|
||||||
|
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
|
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
|
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
|
INSTALL_STUDIO=true
|
||||||
|
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
||||||
|
- name: Smoke test studio (amd64)
|
||||||
|
env:
|
||||||
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
|
run: bash scripts/smoke-test.sh pi-devbox:smoke-studio
|
||||||
|
|
||||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||||
build-variant:
|
build-variant:
|
||||||
needs: [base-decide, smoke, resolve-versions]
|
needs: [base-decide, smoke, resolve-versions]
|
||||||
@@ -354,6 +418,89 @@ jobs:
|
|||||||
echo "==> All 3 build+push attempts failed"
|
echo "==> All 3 build+push attempts failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
# ── Phase 4b: multi-arch publish of the studio variant ───────────────
|
||||||
|
# Additive: publishes :vX.Y.Z-studio (+ :latest-studio on release). Gated
|
||||||
|
# on its own smoke-studio, NOT on the core build-variant, so it can ship
|
||||||
|
# or fail independently of the core release.
|
||||||
|
build-variant-studio:
|
||||||
|
needs: [base-decide, smoke-studio, resolve-versions]
|
||||||
|
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 studio version-specific tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
VERSION="${{ env.RELEASE_TAG }}"
|
||||||
|
{ echo "tags<<EOF"
|
||||||
|
echo "${IMAGE}:${VERSION}-studio"
|
||||||
|
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||||
|
echo "${IMAGE}:latest-studio"
|
||||||
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Build and push studio variant (with retry)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAGS: ${{ steps.tags.outputs.tags }}
|
||||||
|
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
|
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
|
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||||
|
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
|
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
|
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
|
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG_FLAGS=()
|
||||||
|
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
||||||
|
# 3-attempt retry (see build-base step for rationale).
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "==> Build+push attempt ${attempt}/3"
|
||||||
|
if docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--file Dockerfile.variant \
|
||||||
|
--push \
|
||||||
|
--build-arg "BASE_IMAGE=${BASE_IMAGE_FULL}" \
|
||||||
|
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||||
|
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||||
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
|
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||||
|
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||||
|
--build-arg "INSTALL_STUDIO=true" \
|
||||||
|
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
||||||
|
"${TAG_FLAGS[@]}" \
|
||||||
|
.; then
|
||||||
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${attempt}" -lt 3 ]]; then
|
||||||
|
backoff=$(( attempt * 15 ))
|
||||||
|
echo "==> Attempt ${attempt} failed, sleeping ${backoff}s before retry"
|
||||||
|
sleep "${backoff}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "==> All 3 build+push attempts failed"
|
||||||
|
exit 1
|
||||||
|
|
||||||
# ── Phase 5: promote base-<hash> → base-latest (manifest copy only) ─
|
# ── Phase 5: promote base-<hash> → base-latest (manifest copy only) ─
|
||||||
promote-base-latest:
|
promote-base-latest:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -23,14 +23,18 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
||||||
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
||||||
build-base → smoke → build-variant → promote-base-latest →
|
build-base → smoke → build-variant → promote-base-latest →
|
||||||
update-description).
|
update-description). The `-studio` variant adds independent
|
||||||
|
`smoke-studio` + `build-variant-studio` jobs that gate only the
|
||||||
|
`-studio` tags (never the core `:latest` release).
|
||||||
|
|
||||||
## Versioning scheme
|
## Versioning scheme
|
||||||
|
|
||||||
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
||||||
minor bumps add variants (`-studio`, `-studio-tex`); patch bumps follow
|
minor bumps add variants (`-studio`, `-studio-tex`); patch bumps follow
|
||||||
pi npm version updates and small fixes.
|
pi npm version updates and small fixes.
|
||||||
- Docker Hub tags: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest`.
|
- Docker Hub tags: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest`
|
||||||
|
+ (since v1.1.0) `joakimp/pi-devbox:vX.Y.Z-studio` +
|
||||||
|
`joakimp/pi-devbox:latest-studio`.
|
||||||
Internal tags: `joakimp/pi-devbox:base-<hash>` (content-addressed) +
|
Internal tags: `joakimp/pi-devbox:base-<hash>` (content-addressed) +
|
||||||
`joakimp/pi-devbox:base-latest` (alias of most recent base).
|
`joakimp/pi-devbox:base-latest` (alias of most recent base).
|
||||||
|
|
||||||
@@ -45,8 +49,8 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||||||
pi version + new-base-tooling presence. Variant build is multi-arch
|
pi version + new-base-tooling presence. Variant build is multi-arch
|
||||||
(amd64 + arm64) only after smoke passes.
|
(amd64 + arm64) only after smoke passes.
|
||||||
6. Verify the Hub tags appear (latest + vX.Y.Z, plus base-latest if the
|
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||||||
base was rebuilt this run).
|
base-latest if the base was rebuilt this run).
|
||||||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
7. **Revoke any short-lived Gitea PAT** used during the release at
|
||||||
`gitea.jordbo.se/user/settings/applications`.
|
`gitea.jordbo.se/user/settings/applications`.
|
||||||
|
|
||||||
@@ -108,12 +112,15 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
|||||||
## What we DON'T install (and why)
|
## What we DON'T install (and why)
|
||||||
|
|
||||||
- **No texlive** (~600 MB–1 GB). Users who need PDF export from pandoc
|
- **No texlive** (~600 MB–1 GB). Users who need PDF export from pandoc
|
||||||
can install on demand: `sudo apt-get install texlive-xetex
|
or pi-studio can install on demand: `sudo apt-get install texlive-xetex
|
||||||
texlive-latex-recommended`. The planned `:latest-studio-tex` variant
|
texlive-latex-recommended`. The planned `:latest-studio-tex` variant
|
||||||
will bake this in.
|
will bake this in.
|
||||||
- **No pi-studio** (yet). Coming in v1.1.0 as the `:latest-studio`
|
- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0),
|
||||||
variant. v1.0.0 is intentionally scope-limited to "decouple, don't
|
vendored to `/opt/pi-studio` and registered at container start via
|
||||||
reshape."
|
`pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`).
|
||||||
|
The default `:latest` image stays studio-free. Note: pi-studio binds
|
||||||
|
`127.0.0.1` inside the container, so browser access needs host
|
||||||
|
networking or a loopback bridge — see README "Using pi-studio".
|
||||||
- **No Julia/R/GHCi/Clojure runtimes**. Use `uv run --with X` for
|
- **No Julia/R/GHCi/Clojure runtimes**. Use `uv run --with X` for
|
||||||
Python REPLs; `apt install` other-language runtimes ad-hoc per
|
Python REPLs; `apt install` other-language runtimes ad-hoc per
|
||||||
container if needed.
|
container if needed.
|
||||||
|
|||||||
+26
-1
@@ -13,7 +13,32 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
_(no changes since v1.0.1)_
|
### Added — `:latest-studio` variant (will tag as **v1.1.0**, minor)
|
||||||
|
|
||||||
|
- **New `-studio` image variant** bundling
|
||||||
|
[pi-studio](https://github.com/omaclaren/pi-studio) — a two-pane
|
||||||
|
browser workspace (prompt/response editor, live KaTeX/Mermaid preview,
|
||||||
|
tmux-backed literate REPLs for Shell/Python/IPython/Julia/R/GHCi/Clojure)
|
||||||
|
plus the `/studio` slash command and `studio_repl_send` /
|
||||||
|
`studio_export_*` agent tools. Published as `:latest-studio` and
|
||||||
|
`:vX.Y.Z-studio` (multi-arch).
|
||||||
|
- pi-studio is **vendored to `/opt/pi-studio`** at build time (gated by
|
||||||
|
`INSTALL_STUDIO=true`, ref pinned via CI-resolved `PI_STUDIO_REF`) and
|
||||||
|
registered on container start by `entrypoint-user.sh` via
|
||||||
|
`pi install /opt/pi-studio` — the same pattern as pi-fork /
|
||||||
|
pi-observational-memory. No build step: pi-studio ships its browser
|
||||||
|
bundle prebuilt in git. The non-studio `:latest` image is unchanged.
|
||||||
|
- CI gains independent `smoke-studio` + `build-variant-studio` jobs that
|
||||||
|
gate **only** the studio tags, so a studio build/smoke failure can
|
||||||
|
never block the core `:latest` / `:vX.Y.Z` release.
|
||||||
|
- `STUDIO_PORT=8765` baked as an advisory default.
|
||||||
|
- **README "Using pi-studio" section.** Documents the container access
|
||||||
|
reality: pi-studio hard-binds `127.0.0.1` inside the container
|
||||||
|
(`.listen(port,"127.0.0.1")`, no `--host` flag), so a plain `-p`
|
||||||
|
publish does not reach it. Documents the two working paths — host
|
||||||
|
networking (recommended on OrbStack) and a loopback bridge for bridge
|
||||||
|
networking — plus the remote `ssh -L` forward and the **mosh caveat**
|
||||||
|
(mosh cannot forward ports; run a parallel `ssh -L` alongside it).
|
||||||
|
|
||||||
## v1.0.1 — 2026-06-10
|
## v1.0.1 — 2026-06-10
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ A self-contained Docker container for the [pi coding-agent](https://github.com/e
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `joakimp/pi-devbox:latest` | amd64, arm64 | ~1.1 GB | Self-contained: base + pi `{{PI_VERSION}}` + companions |
|
| `joakimp/pi-devbox:latest` | amd64, arm64 | ~1.1 GB | Self-contained: base + pi `{{PI_VERSION}}` + companions |
|
||||||
| `joakimp/pi-devbox:vX.Y.Z` | amd64, arm64 | same | Pinned semver release |
|
| `joakimp/pi-devbox:vX.Y.Z` | amd64, arm64 | same | Pinned semver release |
|
||||||
|
| `joakimp/pi-devbox:latest-studio` | amd64, arm64 | ~1.15 GB | `latest` + [pi-studio](https://github.com/omaclaren/pi-studio): browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs |
|
||||||
|
| `joakimp/pi-devbox:vX.Y.Z-studio` | amd64, arm64 | same | Pinned semver studio release |
|
||||||
| `joakimp/pi-devbox:base-latest` | amd64, arm64 | ~1.0 GB | Base layer alias (internal building block; pull `:latest` instead) |
|
| `joakimp/pi-devbox:base-latest` | amd64, arm64 | ~1.0 GB | Base layer alias (internal building block; pull `:latest` instead) |
|
||||||
| `joakimp/pi-devbox:base-<hash>` | amd64, arm64 | ~1.0 GB | Content-addressed base; immutable. Stable parent for variant rebuilds. |
|
| `joakimp/pi-devbox:base-<hash>` | amd64, arm64 | ~1.0 GB | Content-addressed base; immutable. Stable parent for variant rebuilds. |
|
||||||
|
|
||||||
|
> **pi-studio (`-studio` tags):** launch with `/studio --no-browser --port 8765` inside a pi session. The server binds `127.0.0.1` **inside the container**, so reach it via host networking or a loopback bridge (and `ssh -L` for a remote host; mosh needs a parallel `ssh -L`). Full recipe: [README → Using pi-studio](https://gitea.jordbo.se/joakimp/pi-devbox#using-pi-studio--studio-variant).
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
One-shot, no persistence:
|
One-shot, no persistence:
|
||||||
|
|||||||
@@ -88,6 +88,54 @@ RUN set -e && \
|
|||||||
echo "pi-fork at $(cd /opt/pi-fork && 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)"
|
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 <git-url>` 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 ───────────────────────────────────────────
|
# ── Optional: Go toolchain ───────────────────────────────────────────
|
||||||
# Off by default; opt in for users who run Go tools inside the devbox.
|
# Off by default; opt in for users who run Go tools inside the devbox.
|
||||||
ARG INSTALL_GO=false
|
ARG INSTALL_GO=false
|
||||||
|
|||||||
@@ -131,16 +131,91 @@ Currently published:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `joakimp/pi-devbox:latest` | base + pi + tooling | ~3.2 GB |
|
| `joakimp/pi-devbox:latest` | base + pi + tooling | ~3.2 GB |
|
||||||
| `joakimp/pi-devbox:vX.Y.Z` | pinned-version equivalent | ~3.2 GB |
|
| `joakimp/pi-devbox:vX.Y.Z` | pinned-version equivalent | ~3.2 GB |
|
||||||
|
| `joakimp/pi-devbox:latest-studio` | `latest` + [pi-studio](https://github.com/omaclaren/pi-studio) (browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs) | ~3.25 GB |
|
||||||
|
| `joakimp/pi-devbox:vX.Y.Z-studio` | pinned-version studio equivalent | ~3.25 GB |
|
||||||
|
|
||||||
Planned for upcoming minor releases:
|
Planned for an upcoming minor release:
|
||||||
|
|
||||||
- `joakimp/pi-devbox:latest-studio` — adds [pi-studio](https://github.com/omaclaren/pi-studio)
|
- `joakimp/pi-devbox:latest-studio-tex` — `-studio` plus `texlive-xetex`
|
||||||
for browser-based prompt editing, KaTeX/Mermaid preview, and
|
|
||||||
literate REPLs (Shell / Python / IPython / Julia / R / GHCi /
|
|
||||||
Clojure). Adds ~50 MB.
|
|
||||||
- `joakimp/pi-devbox:latest-studio-tex` — also adds `texlive-xetex`
|
|
||||||
for PDF export from Studio. Adds ~600 MB on top of `-studio`.
|
for PDF export from Studio. Adds ~600 MB on top of `-studio`.
|
||||||
|
|
||||||
|
## Using pi-studio (`-studio` variant)
|
||||||
|
|
||||||
|
The `-studio` images bundle [pi-studio](https://github.com/omaclaren/pi-studio):
|
||||||
|
a two-pane browser workspace with a prompt/response editor, live
|
||||||
|
KaTeX/Mermaid preview, and tmux-backed literate REPLs (Shell / Python /
|
||||||
|
IPython / Julia / R / GHCi / Clojure). It is registered automatically on
|
||||||
|
container start (no `pi install` needed) and exposes the `/studio` slash
|
||||||
|
command plus the `studio_repl_send` / `studio_export_*` agent tools.
|
||||||
|
|
||||||
|
Inside a pi session in the container:
|
||||||
|
|
||||||
|
```
|
||||||
|
/studio --no-browser --port 8765 # pin a fixed port; STUDIO_PORT=8765 is the baked default
|
||||||
|
/studio --status # reprint the tokenized URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reaching the UI from your browser (the container caveat)
|
||||||
|
|
||||||
|
pi-studio **hard-binds its server to `127.0.0.1` inside the container**
|
||||||
|
(`index.ts`: `.listen(port, "127.0.0.1")`) and serves a tokenized URL.
|
||||||
|
There is no `--host`/bind flag. This matters for a container: a plain
|
||||||
|
`docker run -p 8765:8765` publish forwards to the container's *external*
|
||||||
|
interface, **not** its loopback, so it will not reach Studio. Two paths
|
||||||
|
work:
|
||||||
|
|
||||||
|
**A. Host networking (simplest — recommended on OrbStack / single-host).**
|
||||||
|
Run the container with host networking so the container's loopback is the
|
||||||
|
host's loopback:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
devbox:
|
||||||
|
network_mode: host # container 127.0.0.1 == host 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `http://127.0.0.1:8765/?token=…` works in a browser on the Docker
|
||||||
|
host. (Note: host networking changes `host.docker.internal` semantics, so
|
||||||
|
weigh it against the LAN-jump SSH feature if you use that.)
|
||||||
|
|
||||||
|
**B. Loopback bridge (portable — bridge networking).** Publish a port and
|
||||||
|
bridge the container's loopback to its external interface with a one-liner
|
||||||
|
(uses the bundled `node`; binds the eth0 IP only, so it never clashes with
|
||||||
|
Studio's own `127.0.0.1:8765` listener):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
devbox:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8765:8765" # host-localhost only
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# inside the container, after /studio --port 8765:
|
||||||
|
EXT=$(hostname -i)
|
||||||
|
node -e 'const net=require("net"),p=process.env.STUDIO_PORT||8765,h=process.argv[1];\
|
||||||
|
net.createServer(c=>{const u=net.connect(p,"127.0.0.1");c.pipe(u);u.pipe(c);u.on("error",()=>c.destroy());c.on("error",()=>u.destroy());}).listen(p,h,()=>console.log("bridge "+h+":"+p+" -> 127.0.0.1:"+p));' "$EXT"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote host (SSH / mosh)
|
||||||
|
|
||||||
|
When the Docker host is remote, keep Studio on localhost and forward the
|
||||||
|
port from your laptop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -L 8765:127.0.0.1:8765 user@docker-host # then open the token URL locally
|
||||||
|
```
|
||||||
|
|
||||||
|
**mosh cannot forward ports** (no `-L`/`-R` equivalent). To use Studio
|
||||||
|
over a mosh session, run a *separate* `ssh -L 8765:127.0.0.1:8765 host`
|
||||||
|
tunnel alongside mosh (mosh for the shell, ssh for the port), or reach the
|
||||||
|
host's published port directly over a trusted network (LAN / Tailscale /
|
||||||
|
WireGuard).
|
||||||
|
|
||||||
|
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
|
||||||
|
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML
|
||||||
|
> export, KaTeX, Mermaid, and all REPL features work without it.
|
||||||
|
|
||||||
## docker-compose.yml — basic shape
|
## docker-compose.yml — basic shape
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
+13
-10
@@ -99,16 +99,19 @@ if command -v pi &>/dev/null; then
|
|||||||
"$HOME/.pi/agent/extensions/mempalace.ts"
|
"$HOME/.pi/agent/extensions/mempalace.ts"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# pi-fork (fork tool) + pi-observational-memory (recall tool).
|
# pi-fork (fork tool) + pi-observational-memory (recall tool) + (in the
|
||||||
# These are pi packages (not symlink-style extensions): they're cloned to
|
# :latest-studio variant only) pi-studio (/studio command + studio_*
|
||||||
# /opt with node_modules baked at BUILD time, then registered here via
|
# tools + theme). These are pi packages (not symlink-style extensions):
|
||||||
# `pi install <local-path>`. A local-path install is instant + in-place
|
# they're cloned to /opt with node_modules baked at BUILD time, then
|
||||||
# (pi loads the extension directly from /opt) + idempotent (no duplicate
|
# registered here via `pi install <local-path>`. A local-path install is
|
||||||
# package entry on re-run), and stores a relative path that resolves into
|
# instant + in-place (pi loads the extension directly from /opt) +
|
||||||
# the image-layer /opt so it survives volume recreate. The fork/recall
|
# idempotent (no duplicate package entry on re-run), and stores a relative
|
||||||
# tools register on the NEXT pi start (extensions bind at startup). Guard
|
# path that resolves into the image-layer /opt so it survives volume
|
||||||
# on settings.json so we only install once per volume.
|
# recreate. The tools/command register on the NEXT pi start (extensions
|
||||||
for _pkg in /opt/pi-fork /opt/pi-observational-memory; do
|
# bind at startup). Guard on settings.json so we only install once per
|
||||||
|
# volume. /opt/pi-studio is present only in the studio variant; the
|
||||||
|
# `[ -d ]` test makes this a no-op everywhere else.
|
||||||
|
for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do
|
||||||
[ -d "$_pkg" ] || continue
|
[ -d "$_pkg" ] || continue
|
||||||
_name=$(basename "$_pkg")
|
_name=$(basename "$_pkg")
|
||||||
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||||
|
|||||||
@@ -15,6 +15,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`
|
||||||
|
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
||||||
|
# client bundle present + registered via `pi install`
|
||||||
# - image size within threshold
|
# - image size within threshold
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -95,6 +97,20 @@ run "pi-fork clone + node_modules" \
|
|||||||
run "pi-observational-memory clone + node_modules" \
|
run "pi-observational-memory clone + node_modules" \
|
||||||
"test -f /opt/pi-observational-memory/package.json && test -d /opt/pi-observational-memory/node_modules"
|
"test -f /opt/pi-observational-memory/package.json && test -d /opt/pi-observational-memory/node_modules"
|
||||||
|
|
||||||
|
# pi-studio is present only in the :latest-studio variant. Auto-detect by
|
||||||
|
# probing /opt/pi-studio so this one script covers both variants.
|
||||||
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c 'test -d /opt/pi-studio' >/dev/null 2>&1; then
|
||||||
|
STUDIO_VARIANT=1
|
||||||
|
echo " ℹ️ pi-studio detected — running studio assertions"
|
||||||
|
run "pi-studio clone + node_modules" \
|
||||||
|
"test -f /opt/pi-studio/package.json && test -d /opt/pi-studio/node_modules"
|
||||||
|
run "pi-studio prebuilt client bundle" \
|
||||||
|
"test -f /opt/pi-studio/client/studio-client.js"
|
||||||
|
else
|
||||||
|
STUDIO_VARIANT=0
|
||||||
|
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Runtime deployment ──"
|
echo "── Runtime deployment ──"
|
||||||
@@ -150,6 +166,20 @@ done
|
|||||||
exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
|
exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
|
||||||
exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
|
exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
|
||||||
|
# pi-studio registration (studio variant only) — registered by the same
|
||||||
|
# entrypoint-user.sh local-path install loop as fork/obsmem.
|
||||||
|
if [ "${STUDIO_VARIANT:-0}" = "1" ]; then
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if docker exec "$CID" grep -q pi-studio \
|
||||||
|
/home/developer/.pi/agent/settings.json 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
exec_test "pi-studio registered (/studio command + studio_* tools)" \
|
||||||
|
'grep -q pi-studio $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
fi
|
||||||
|
|
||||||
# ── /tmp/sshcm directory created by entrypoint ────────────────────────
|
# ── /tmp/sshcm directory created by entrypoint ────────────────────────
|
||||||
exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \
|
exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \
|
||||||
'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'
|
'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'
|
||||||
|
|||||||
Reference in New Issue
Block a user