From a78e59fb5bd0916e2036befe795249cb36450417 Mon Sep 17 00:00:00 2001 From: pi Date: Wed, 10 Jun 2026 23:15:29 +0200 Subject: [PATCH] feat(studio): add :latest-studio variant (PR-3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitea/workflows/docker-publish.yml | 147 ++++++++++++++++++++++++++++ AGENTS.md | 23 +++-- CHANGELOG.md | 27 ++++- DOCKER_HUB.md | 4 + Dockerfile.variant | 48 +++++++++ README.md | 87 ++++++++++++++-- entrypoint-user.sh | 23 +++-- scripts/smoke-test.sh | 30 ++++++ 8 files changed, 364 insertions(+), 25 deletions(-) diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index 3469938..75ef08c 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -116,6 +116,7 @@ jobs: obsmem_ref: ${{ steps.resolve.outputs.obsmem_ref }} toolkit_ref: ${{ steps.resolve.outputs.toolkit_ref }} extensions_ref: ${{ steps.resolve.outputs.extensions_ref }} + studio_ref: ${{ steps.resolve.outputs.studio_ref }} steps: - name: Resolve pi version + companion refs id: resolve @@ -150,9 +151,16 @@ jobs: [ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main echo "toolkit_ref=${TOOLKIT_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_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_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 ────── build-base: @@ -277,6 +285,62 @@ jobs: EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }} 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 ───────────────────────────────────── build-variant: needs: [base-decide, smoke, resolve-versions] @@ -354,6 +418,89 @@ jobs: echo "==> All 3 build+push attempts failed" 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<> "$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- → base-latest (manifest copy only) ─ promote-base-latest: needs: diff --git a/AGENTS.md b/AGENTS.md index 2587464..3f9edfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide → 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 - Tags follow semver. **v1.0.0** is the first decoupled release; future minor bumps add variants (`-studio`, `-studio-tex`); patch bumps follow 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-` (content-addressed) + `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 + pi version + new-base-tooling presence. Variant build is multi-arch (amd64 + arm64) only after smoke passes. -6. Verify the Hub tags appear (latest + vX.Y.Z, plus base-latest if the - base was rebuilt this run). +6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus + base-latest if the base was rebuilt this run). 7. **Revoke any short-lived Gitea PAT** used during the release at `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) - **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 will bake this in. -- **No pi-studio** (yet). Coming in v1.1.0 as the `:latest-studio` - variant. v1.0.0 is intentionally scope-limited to "decouple, don't - reshape." +- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0), + vendored to `/opt/pi-studio` and registered at container start via + `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 Python REPLs; `apt install` other-language runtimes ad-hoc per container if needed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bfb176..fbf1b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,32 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). ## 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 diff --git a/DOCKER_HUB.md b/DOCKER_HUB.md index 863a594..742a2ba 100644 --- a/DOCKER_HUB.md +++ b/DOCKER_HUB.md @@ -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: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-` | 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 One-shot, no persistence: diff --git a/Dockerfile.variant b/Dockerfile.variant index f39864f..e7c0132 100644 --- a/Dockerfile.variant +++ b/Dockerfile.variant @@ -88,6 +88,54 @@ RUN set -e && \ 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 diff --git a/README.md b/README.md index e969a61..378cc96 100644 --- a/README.md +++ b/README.md @@ -131,16 +131,91 @@ Currently published: |---|---|---| | `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: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) - 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` +- `joakimp/pi-devbox:latest-studio-tex` — `-studio` plus `texlive-xetex` 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 ```yaml diff --git a/entrypoint-user.sh b/entrypoint-user.sh index 0c8ba41..ca9dd5b 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -99,16 +99,19 @@ if command -v pi &>/dev/null; then "$HOME/.pi/agent/extensions/mempalace.ts" fi - # pi-fork (fork tool) + pi-observational-memory (recall tool). - # These are pi packages (not symlink-style extensions): they're cloned to - # /opt with node_modules baked at BUILD time, then registered here via - # `pi install `. A local-path install is instant + in-place - # (pi loads the extension directly from /opt) + idempotent (no duplicate - # package entry on re-run), and stores a relative path that resolves into - # the image-layer /opt so it survives volume recreate. The fork/recall - # tools register on the NEXT pi start (extensions bind at startup). Guard - # on settings.json so we only install once per volume. - for _pkg in /opt/pi-fork /opt/pi-observational-memory; do + # pi-fork (fork tool) + pi-observational-memory (recall tool) + (in the + # :latest-studio variant only) pi-studio (/studio command + studio_* + # tools + theme). These are pi packages (not symlink-style extensions): + # they're cloned to /opt with node_modules baked at BUILD time, then + # registered here via `pi install `. A local-path install is + # instant + in-place (pi loads the extension directly from /opt) + + # idempotent (no duplicate package entry on re-run), and stores a relative + # path that resolves into the image-layer /opt so it survives volume + # recreate. The tools/command register on the NEXT pi start (extensions + # 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 _name=$(basename "$_pkg") if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 3d9ef67..80af250 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -15,6 +15,8 @@ # - mempalace bridge symlink present # - settings.json bootstrapped # - 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 set -euo pipefail @@ -95,6 +97,20 @@ run "pi-fork 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" +# 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) ────────────────────── echo "" 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-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 ──────────────────────── exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \ 'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'