diff --git a/Dockerfile.base b/Dockerfile.base index e2c6fd3..183bad7 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -50,6 +50,7 @@ RUN apt-get update && \ openssh-client \ gnupg \ jq \ + yq \ ripgrep \ fd-find \ tree \ @@ -72,6 +73,8 @@ RUN apt-get update && \ python3-pip \ python3-venv \ nano \ + pandoc \ + graphviz \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -295,6 +298,22 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" rm -rf /tmp/uv-* && \ uv --version +# tealdeer — Rust port of tldr (community-maintained command examples) +# Provides the `tldr` command; ~5 MB static binary, ~135 MB smaller than +# the Node tldr global. Same UX as the Node version. +ARG TEALDEER_VERSION=latest +RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ + V="${TEALDEER_VERSION}" && \ + if [ "$V" = "latest" ]; then \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + fi && \ + V="${V#v}" && \ + [ -n "$V" ] && \ + echo "Installing tealdeer ${V}" && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/download/v${V}/tealdeer-linux-${ARCH}-musl" -o /usr/local/bin/tldr && \ + chmod +x /usr/local/bin/tldr && \ + tldr --version + # ── MemPalace — local-first AI memory system ───────────────────────── # Provides semantic search over conversation history via 29 MCP tools. # Always installed in the base (variant-independent). Set @@ -466,6 +485,7 @@ COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global # ── Entrypoint ──────────────────────────────────────────────────────── COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/ +COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch # Image-baked skills + harness instruction. Under /usr/local so a named volume # over a home dir (e.g. devbox-opencode-config on ~/.config/opencode) can't # shadow them; entrypoint-user.sh links them into ~/.agents/skills/ and @@ -475,6 +495,7 @@ COPY rootfs/usr/local/share/opencode-devbox/ /usr/local/share/opencode-devbox/ COPY entrypoint.sh /usr/local/bin/entrypoint.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 \ + /usr/local/bin/dot-watch \ /usr/local/lib/opencode-devbox/*.py # Start as root — entrypoint adjusts UID/GID then drops to developer diff --git a/README.md b/README.md index 03ab771..4f94a3e 100644 --- a/README.md +++ b/README.md @@ -835,7 +835,11 @@ Container (Debian trixie) ├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun) ├── AWS CLI v2 (SSO + Bedrock auth) ├── neovim 0.12, nano, micro, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync -├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree +├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, yq, curl, tree +├── pandoc (Markdown↔HTML/PDF/etc. conversion) +├── graphviz (dot diagram rendering) +├── tldr (tealdeer — quick command examples) +├── dot-watch (auto-render .dot to PNG on save) ├── Node.js (for MCP servers) ├── Bun (optional — included with oh-my-opencode-slim) ├── entrypoint.sh (UID adjustment, git config, provider setup) diff --git a/rootfs/usr/local/bin/dot-watch b/rootfs/usr/local/bin/dot-watch new file mode 100755 index 0000000..0b1dd23 --- /dev/null +++ b/rootfs/usr/local/bin/dot-watch @@ -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 .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 [layout] [dpi] +# layout: dot|neato|fdp|circo|twopi (default: dot) +# dpi: output resolution (default: 150) +# env: DOT_WATCH_INTERVAL= poll interval (default: 1) +# +# EXAMPLES +# dot-watch /workspace/graph.dot +# dot-watch graph.dot neato 200 + +set -euo pipefail + +SRC="${1:?usage: dot-watch [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 diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 41a6f7c..9c9bf96 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -131,12 +131,17 @@ run "fzf" "fzf --version" run "fd" "fd --version" run "rg" "rg --version | head -1" run "jq" "jq --version" +run "yq" "yq --version" run "git-crypt" "git-crypt --version | head -1" run "gitleaks" "gitleaks version" run "aws" "aws --version" run "gitea-mcp" "gitea-mcp --version" run "gosu" "gosu --version" run "tmux" "tmux -V" +run "pandoc" "pandoc --version | head -1" +run "graphviz (dot)" "dot -V" +run "tldr (tealdeer)" "tldr --version" +run "dot-watch" "test -x /usr/local/bin/dot-watch && bash -n /usr/local/bin/dot-watch && echo ok" # SSH ControlMaster baked defaults: the config file must exist (image-level) # and ssh -G must report ControlPath rooted at /tmp/sshcm/ for an arbitrary