Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab5ff8ec56 | |||
| 421558477d | |||
| b655faab9f | |||
| 3b0335f34e |
@@ -13,6 +13,18 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`dot-watch` helper** (`/usr/local/bin/dot-watch`) — auto-rerenders a
|
||||||
|
Graphviz `.dot` file to PNG on every save via mtime polling (no
|
||||||
|
`inotify` dependency). pi-studio renders Mermaid natively but has no
|
||||||
|
DOT renderer; since its markdown preview displays local PNG/JPG/GIF/WEBP
|
||||||
|
images, this closes the loop for Graphviz: edit `.dot` → `dot-watch`
|
||||||
|
regenerates `<name>.png` → Studio *refresh-from-disk* shows the update.
|
||||||
|
`graphviz` was already in the base image, so no new package. Baked into
|
||||||
|
`Dockerfile.base` following the `studio-expose` pattern; documented in
|
||||||
|
the README Studio section.
|
||||||
|
|
||||||
## v1.1.0 — 2026-06-10
|
## v1.1.0 — 2026-06-10
|
||||||
|
|
||||||
### Added — `:latest-studio` variant
|
### Added — `:latest-studio` variant
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
# preview/export pipelines and broadly useful for any
|
# preview/export pipelines and broadly useful for any
|
||||||
# agent-driven document workflow. ~200 MB.
|
# agent-driven document workflow. ~200 MB.
|
||||||
# graphviz — `dot` rendering for many diagram tools. ~10 MB.
|
# graphviz — `dot` rendering for many diagram tools. ~10 MB.
|
||||||
|
# See the bundled `dot-watch` helper for live .dot -> PNG
|
||||||
|
# re-render (handy with pi-studio's image preview).
|
||||||
# imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB.
|
# imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB.
|
||||||
# yq — YAML-aware companion to jq.
|
# yq — YAML-aware companion to jq.
|
||||||
# socat — TCP relay. Powers `studio-expose`, which bridges
|
# socat — TCP relay. Powers `studio-expose`, which bridges
|
||||||
@@ -436,10 +438,12 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
|||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||||
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
||||||
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
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 entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.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 \
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||||
/usr/local/bin/studio-expose \
|
/usr/local/bin/studio-expose \
|
||||||
|
/usr/local/bin/dot-watch \
|
||||||
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
||||||
|
|
||||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||||
|
|||||||
@@ -199,9 +199,15 @@ With `STUDIO_EXPOSE=1`, the entrypoint starts the bridge for you; just run
|
|||||||
(leave `STUDIO_EXPOSE` unset), run `studio-expose` in a container shell:
|
(leave `STUDIO_EXPOSE` unset), run `studio-expose` in a container shell:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
studio-expose # bridges $STUDIO_PORT (default 8765); --help for details
|
studio-expose & # bridges $STUDIO_PORT (default 8765); --help for details
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **`studio-expose` runs in the foreground** (it's a `socat` relay) — it
|
||||||
|
> blocks the shell until Ctrl-C. Background it with `&` or run it in its
|
||||||
|
> own tmux pane. It only relays traffic; it does **not** print a token.
|
||||||
|
> The lines it prints ending in `...token=...` are literal help text, not
|
||||||
|
> a truncated URL — the real token comes from `/studio` (see below).
|
||||||
|
|
||||||
> **Security:** the bridge intentionally exposes Studio beyond loopback;
|
> **Security:** the bridge intentionally exposes Studio beyond loopback;
|
||||||
> its tokenized URL is the only auth. Keep the host-side publish on
|
> its tokenized URL is the only auth. Keep the host-side publish on
|
||||||
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
|
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
|
||||||
@@ -221,10 +227,75 @@ 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 /
|
host's published port directly over a trusted network (LAN / Tailscale /
|
||||||
WireGuard).
|
WireGuard).
|
||||||
|
|
||||||
|
#### End-to-end recipe: remote host, mosh shell, `studio-expose` bridge
|
||||||
|
|
||||||
|
The full path has four network hops, each added by one step:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
browser["laptop browser"]
|
||||||
|
host["host :8765"]
|
||||||
|
eth0["container eth0 :8765"]
|
||||||
|
loop["container 127.0.0.1 :8765"]
|
||||||
|
studio["pi-studio"]
|
||||||
|
|
||||||
|
browser -->|"ssh -L"| host
|
||||||
|
host -->|"docker -p"| eth0
|
||||||
|
eth0 -->|"studio-expose (socat)"| loop
|
||||||
|
studio -->|"binds"| loop
|
||||||
|
```
|
||||||
|
|
||||||
|
Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
|
||||||
|
|
||||||
|
1. **In a container shell** — start the bridge (skip if `STUDIO_EXPOSE=1`
|
||||||
|
is set in compose, which auto-starts it):
|
||||||
|
```bash
|
||||||
|
studio-expose &
|
||||||
|
```
|
||||||
|
2. **In your pi session** (the pi TUI in the container) — start Studio and
|
||||||
|
print the tokenized URL. `/studio` is a slash command you type in the
|
||||||
|
TUI, not a shell command:
|
||||||
|
```
|
||||||
|
/studio --no-browser --port 8765
|
||||||
|
/studio --status # reprint the URL anytime
|
||||||
|
```
|
||||||
|
Copy the `http://…:8765/?token=<token>` it prints. **This** is where
|
||||||
|
the real token comes from — not `studio-expose`.
|
||||||
|
3. **On your laptop** — open the ssh port-forward alongside mosh:
|
||||||
|
```bash
|
||||||
|
ssh -L 8765:127.0.0.1:8765 user@docker-host
|
||||||
|
```
|
||||||
|
4. **In your laptop browser** — open `http://127.0.0.1:8765/?token=<token>`
|
||||||
|
(keep the port and token verbatim; only the host part is `127.0.0.1`).
|
||||||
|
|
||||||
|
> **Order check:** nothing listens on the container's `127.0.0.1:8765`
|
||||||
|
> until step 2 runs. If the browser can't connect, verify Studio is up
|
||||||
|
> (`/studio --status`) and the bridge is running (`ps aux | grep socat`).
|
||||||
|
|
||||||
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
|
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
|
||||||
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML
|
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML
|
||||||
> export, KaTeX, Mermaid, and all REPL features work without it.
|
> export, KaTeX, Mermaid, and all REPL features work without it.
|
||||||
|
|
||||||
|
### Graphviz diagrams in Studio: `dot-watch`
|
||||||
|
|
||||||
|
pi-studio renders **Mermaid** natively but has **no Graphviz/DOT renderer**.
|
||||||
|
Its markdown preview *does* render local image links (`.png`/`.jpg`/`.gif`/
|
||||||
|
`.webp`), so the workflow for Graphviz is: write a `.dot` file, render it to
|
||||||
|
PNG with `dot`, and preview the PNG (directly, or embedded in a markdown
|
||||||
|
file). The bundled **`dot-watch`** helper automates the re-render so edits
|
||||||
|
show up on Studio's *refresh-from-disk*:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dot-watch graph.dot # dot engine, 150 dpi -> graph.png
|
||||||
|
dot-watch graph.dot neato 200 # pick layout engine + dpi
|
||||||
|
```
|
||||||
|
|
||||||
|
It polls the file's mtime (no `inotify` dependency) and regenerates
|
||||||
|
`<name>.png` on every save, printing timestamped status and indenting any
|
||||||
|
DOT syntax errors instead of crashing. Then in Studio: open the PNG (or a
|
||||||
|
`.md` that embeds it) and hit **refresh-from-disk** after each edit.
|
||||||
|
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
|
||||||
|
|
||||||
## docker-compose.yml — basic shape
|
## docker-compose.yml — basic shape
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -236,10 +307,15 @@ services:
|
|||||||
container_name: pi-devbox
|
container_name: pi-devbox
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
# pi-studio (only on `-studio` images): publish loopback + enable the
|
||||||
|
# socat bridge so the browser UI is reachable. See "Using pi-studio".
|
||||||
|
# ports:
|
||||||
|
# - "127.0.0.1:8765:8765" # host-localhost only; use ssh -L for remote
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- TERM=xterm-256color
|
- TERM=xterm-256color
|
||||||
|
# - STUDIO_EXPOSE=1 # -studio only: auto-start the socat bridge on boot
|
||||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
||||||
- GITEA_HOST=${GITEA_HOST:-}
|
- GITEA_HOST=${GITEA_HOST:-}
|
||||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# dot-watch — auto-rerender a graphviz .dot file to PNG on every save.
|
||||||
|
#
|
||||||
|
# WHY THIS EXISTS
|
||||||
|
# pi-studio renders mermaid natively but has no graphviz/DOT renderer.
|
||||||
|
# Its markdown preview DOES render local image links (.png/.jpg/.gif/.webp),
|
||||||
|
# and the editor offers "refresh from disk". This helper closes the loop:
|
||||||
|
# edit a .dot file -> dot-watch regenerates <name>.png -> hit refresh in
|
||||||
|
# Studio to see the update. Uses mtime polling (no inotify dependency,
|
||||||
|
# which isn't in the trixie-slim base).
|
||||||
|
#
|
||||||
|
# USAGE
|
||||||
|
# dot-watch <file.dot> [layout] [dpi]
|
||||||
|
# layout: dot|neato|fdp|circo|twopi (default: dot)
|
||||||
|
# dpi: output resolution (default: 150)
|
||||||
|
# env: DOT_WATCH_INTERVAL=<seconds> poll interval (default: 1)
|
||||||
|
#
|
||||||
|
# EXAMPLES
|
||||||
|
# dot-watch /workspace/graph.dot
|
||||||
|
# dot-watch graph.dot neato 200
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SRC="${1:?usage: dot-watch <file.dot> [layout] [dpi]}"
|
||||||
|
LAYOUT="${2:-dot}"
|
||||||
|
DPI="${3:-150}"
|
||||||
|
|
||||||
|
[[ -f "$SRC" ]] || { echo "error: no such file: $SRC" >&2; exit 1; }
|
||||||
|
command -v "$LAYOUT" >/dev/null || { echo "error: layout engine '$LAYOUT' not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
OUT="${SRC%.dot}.png"
|
||||||
|
INTERVAL="${DOT_WATCH_INTERVAL:-1}" # seconds between polls
|
||||||
|
ERRLOG="$(mktemp -t dot-watch.XXXXXX.err)"
|
||||||
|
trap 'rm -f "$ERRLOG"' EXIT
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if "$LAYOUT" -Tpng -Gdpi="$DPI" "$SRC" -o "$OUT" 2> "$ERRLOG"; then
|
||||||
|
printf '[%s] rendered -> %s\n' "$(date +%H:%M:%S)" "$OUT"
|
||||||
|
else
|
||||||
|
printf '[%s] DOT error:\n' "$(date +%H:%M:%S)"
|
||||||
|
sed 's/^/ /' "$ERRLOG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# portable mtime (GNU stat, fallback to BSD stat)
|
||||||
|
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
|
||||||
|
|
||||||
|
echo "watching $SRC ($LAYOUT, ${DPI}dpi) -> $OUT [Ctrl-C to stop]"
|
||||||
|
render
|
||||||
|
last="$(mtime "$SRC")"
|
||||||
|
while true; do
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
[[ -f "$SRC" ]] || continue
|
||||||
|
now="$(mtime "$SRC")"
|
||||||
|
if [[ "$now" != "$last" ]]; then
|
||||||
|
last="$now"
|
||||||
|
render
|
||||||
|
fi
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user