diff --git a/AGENTS.md b/AGENTS.md index 58aa1b5..f8db447 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,9 +11,9 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d - `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup. - `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint). - `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows. -- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow). -- `DOCKER_HUB.md` — **auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes. -- `README.md` — authoritative source documentation. Sections are selected/dropped/replaced for DOCKER_HUB.md per `SECTION_RULES` in `scripts/generate-dockerhub-md.py`. +- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow). +- `DOCKER_HUB.md` — **auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes. +- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth. - `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check. - `.gitea/workflows/docker-publish.yml` — CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description. @@ -35,12 +35,12 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile` - **entrypoint.sh volume ownership loop** — when adding a new named volume mount point, add it to the `for dir in ...` loop in `entrypoint.sh` so root-owned volumes get chowned on startup. The loop writes a `.devbox-owner` sentinel after a successful chown so subsequent starts skip the recursive walk. Users should not touch these files. - **Documentation coupling on release** — four docs co-vary and drift in lockstep when not updated together: - `README.md` is the source of truth for user-facing build/run/config detail. - - `DOCKER_HUB.md` is auto-generated by `scripts/generate-dockerhub-md.py` from README. CI's `--check` run fails if it's stale or any new top-level README section is missing from `SECTION_RULES`. Hard cap: 25 kB Hub limit (current: ~24.9 kB, very tight — trim before adding). + - `DOCKER_HUB.md` is auto-generated from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. CI's `--check` run fails if it's stale. Hub-facing copy is intentionally slim (~5.5 kB, ~78% headroom against the 25 kB Hub limit) — update the template here when image variants, quick-start flow, or the elevator pitch change. README.md no longer feeds into Hub, so README edits do NOT require regenerating DOCKER_HUB.md. - `CHANGELOG.md` records every release. When cutting a tag, **promote `## Unreleased` to `## vX.Y.Z[n] — YYYY-MM-DD` BEFORE pushing the tag** so the tag points at a CHANGELOG that names itself. Keep entries reverse-chronological (newest at top, after the `Unreleased` block). Doc-only updates that happen post-tag (Hub description live-patches, README clarifications) get a fresh `## Unreleased` block with a note that they don't trigger a new image build. - `AGENTS.md` (this file) carries domain facts that change on structural releases — tag-count statements, CI job lists, install contracts. After any change to `.gitea/workflows/*.yml` or the variant matrix, grep this file for stale numbers (`grep -nE "four|eight|all [0-9]"`). - `.env.example` must be hand-updated to match Dockerfile/entrypoint behavior — it is not auto-generated. - Release-day checklist: README → regenerate DOCKER_HUB.md → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag. + Release-day checklist: README → (regenerate DOCKER_HUB.md only if HUB_TEMPLATE changed) → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag. - **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag. - **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`. - **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1e751..9f5b540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ Image changes (will ship in the next tagged release): - **Fix:** `pi install npm:` (and any `npm install -g`) by the `developer` user no longer EACCES against the system npm prefix. `NPM_CONFIG_PREFIX` is now `/home/developer/.pi/npm-global` and the prefix's `bin/` is prepended to `PATH`. The directory lives on the `devbox-pi-config` named volume, so user-installed pi packages (themes, skills, extensions) survive container recreation and image rebuilds. Build-time `npm install -g` calls (opencode, pi, oh-my-opencode-slim) are unaffected because the new ENVs are declared after those steps in the Dockerfile, so the baked binaries still install to `/usr` and are not shadowed by the volume mount. +Docs-only (the DOCKER_HUB.md change can be patched live to Hub without a CI rebuild; AGENTS.md change is repo-internal): + +- **Hub doc rewrite:** `DOCKER_HUB.md` is now generated from a hand-maintained `HUB_TEMPLATE` constant in `scripts/generate-dockerhub-md.py` instead of a section-by-section transformation of `README.md`. Drops from 24 997 bytes (3 byte headroom) to 5 551 bytes (~78% headroom). The old derive-from-README mechanism (`SECTION_RULES`, `TRIM_SUBSECTIONS`, `REPLACEMENTS`, `split_sections`, `trim_subsections`) is gone — README and Hub doc are now independent surfaces. Hub copy stays slim and links out to the gitea README for full depth (build args, multi-user setup, AWS Bedrock walkthrough, MemPalace deep-dive, language-specific dev sections). Trade-off: image-variants table and quick-start flow are now coupled to `HUB_TEMPLATE` and need a manual edit when they change — explicit and local rather than spread across rules. +- **AGENTS.md:** "Documentation coupling on release" rule updated — README edits no longer require regenerating DOCKER_HUB.md. Release-day checklist tightened. + ## v1.14.41b — 2026-05-08 **Optional pi as second harness.** diff --git a/DOCKER_HUB.md b/DOCKER_HUB.md index 14c3e44..803a1f3 100644 --- a/DOCKER_HUB.md +++ b/DOCKER_HUB.md @@ -1,10 +1,10 @@ -# opencode-devbox — Docker Hub +# opencode-devbox Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed. -## Image Variants +Designed for teams who want a reproducible coding-agent setup that runs the same on every laptop and CI runner — without forcing each developer to install Bun, Node, AWS CLI, mempalace, or maintain shell config drift across machines. -Four image variants are published for each release: +## Image Variants | Tag | Description | |---|---| @@ -15,8 +15,6 @@ Four image variants are published for each release: All variants support `linux/amd64` and `linux/arm64`. -> **NOTE:** This file is auto-generated from `README.md` by `scripts/generate-dockerhub-md.py`. Edit README.md and regenerate rather than editing this file directly. - ## Quick Start ```bash @@ -30,9 +28,9 @@ docker run -it --rm \ joakimp/opencode-devbox:latest ``` -This drops you straight into opencode with your project mounted at `/workspace`. +Drops you straight into opencode with your project mounted at `/workspace`. -For an interactive shell first (useful for AWS SSO login): +For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`): ```bash docker run -it --rm \ @@ -43,554 +41,55 @@ docker run -it --rm \ joakimp/opencode-devbox:latest bash ``` -Then run `opencode` when ready. +Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell. -For docker-compose users, see the source repo for `docker-compose.yml` and `.env.example` templates. +For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired. -## Features +## What's Inside -- **Debian trixie** base — glibc, full PTY/terminal support -- **Configurable providers** — Anthropic, OpenAI, AWS Bedrock via env vars -- **Host filesystem access** — bind mount any directory as `/workspace` -- **SSH key forwarding** — git push/pull to private repos -- **MCP server support** — Node.js included for `npx`-based MCP servers -- **Non-root user** — runs as `developer` with UID auto-matched to workspace owner (sudo available) -- **Python via uv** — `uv` package manager included; install Python on demand with `uv python install` -- **Rust via rustup** — `rustup-init` included; bootstrap Rust on demand with `rustup-init -y` -- **Optional runtimes** — Python (apt), Go via build args (Node.js always included — required for opencode v1.x) -- **Multi-agent orchestration** — optional [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) integration via build arg -- **AWS CLI v2** — built-in SSO/Bedrock authentication with headless device-code flow -- **Multi-arch** — amd64 and arm64 +- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.). +- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`. +- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi. +- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents). +- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace. +- **MCP wrappers** for mempalace pre-installed and pre-wired to both harnesses. -## Usage +## Authentication -### Prerequisites +The container reads provider credentials from environment variables and host-mounted config: -Bind-mounted directories must exist on the host before starting the container. Docker creates missing directories as root-owned, which causes permission issues. +- **Anthropic / OpenAI / Groq / others:** set `OPENCODE_PROVIDER` and the corresponding `*_API_KEY` via `-e` or `.env`. +- **AWS Bedrock (SSO):** mount `~/.aws` from the host, `OPENCODE_PROVIDER=amazon-bedrock`, then `aws sso login` inside the container. Tokens persist across container restarts via the host bind-mount. +- **OAuth / device-code providers:** auth state lives in opencode's config, which is persisted via the `devbox-opencode-config` named volume. -```bash -# Required: workspace for your projects -mkdir -p ~/projects -``` +Full Bedrock walkthrough (IAM roles, permissions, multi-account setups): see the [AWS Bedrock Authentication]( +https://gitea.jordbo.se/joakimp/opencode-devbox#aws-bedrock-authentication +) section on gitea. -### Connecting to the container +## Persistence -From your laptop, SSH into the remote server where Docker is running, then start the container: - -```bash -# 1. SSH into the remote server -ssh user@remote-server - -# 2. Navigate to the project -cd opencode-devbox - -# 3. Start the container with an interactive shell -docker compose run --rm devbox bash - -# You're now inside the container — run commands here -aws sso login --sso-session --use-device-code -opencode -``` - -### Running modes - -**Interactive shell** — enter the container, run multiple commands: -```bash -docker compose run --rm devbox bash -``` - -**Direct to opencode** — skips the shell, launches opencode immediately: -```bash -docker compose run --rm devbox -``` - -**Background container** — keep it running, attach when needed: -```bash -# Start in background -docker compose up -d - -# Attach a shell to the running container -docker compose exec -u developer devbox bash - -# Or run a single command inside it -docker compose exec -u developer devbox aws --version -``` - -> `run` creates a new container (cleaned up with `--rm`). `exec` attaches to an already running one. - -## Configuration - -### Environment Variables - -| Variable | Description | Default | +| Volume | Mount | Survives | |---|---|---| -| `OPENCODE_PROVIDER` | LLM provider (`anthropic`, `openai`, `amazon-bedrock`) | `anthropic` | -| `OPENCODE_MODEL` | Model override | Provider default | -| `ANTHROPIC_API_KEY` | Anthropic API key | — | -| `OPENAI_API_KEY` | OpenAI API key | — | -| `AWS_REGION` | AWS region for Bedrock | `us-east-1` | -| `AWS_PROFILE` | AWS SSO profile name | `default` | -| `GIT_USER_NAME` | Git commit author name | — | -| `GIT_USER_EMAIL` | Git commit author email | — | -| `WORKSPACE_PATH` | Host path to mount | `.` | -| `SSH_KEY_PATH` | Host SSH key directory | `~/.ssh` | -| `USER_UID` | Override container user UID | Auto-detect from `/workspace` | -| `USER_GID` | Override container user GID | Auto-detect from `/workspace` | -| `LANG` | System locale | `en_US.UTF-8` | -| `LANGUAGE` | Language priority list | `en_US:en` | -| `LC_ALL` | Override all locale settings | `en_US.UTF-8` | -| `EDITOR` | Default text editor | `nvim` | -| `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` | -| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` | -| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` | -| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` | -| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect | +| `devbox-opencode-config` | `~/.config/opencode` | container recreate, image rebuild | +| `devbox-pi-config` | `~/.pi` | container recreate, image rebuild — incl. user-installed pi packages via `pi install` (`NPM_CONFIG_PREFIX` points into the volume) | +| `devbox-palace` (uncomment) | `~/.mempalace` | container recreate, image rebuild — palace data is precious, treat as primary storage | +| `devbox-chroma-cache` | `~/.cache/chroma` | container recreate (model cache, disposable — re-downloads in seconds) | -### Custom opencode config +Workspace bind-mount (`/workspace`) is your project directory on the host, so source code is never inside the container. -Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation. +Full persistence reference, including multi-user (`SIGNUM`) isolation and host bind-mount alternatives: see the [README on gitea](https://gitea.jordbo.se/joakimp/opencode-devbox#persistence). -When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped. +## Where to Go Next -**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount: +- **Full README** with build args, every feature in detail, troubleshooting: +- **CHANGELOG** for version history: +- **Issues / source / docker-compose templates:** +- **Agent-facing internals** (for future maintainers / coding agents working in the repo): -```yaml -volumes: - - ~/.config/opencode:/home/developer/.config/opencode -``` +## License -> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts. +MIT. See . -### Custom skills +--- -Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order: - -1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container) -2. `~/skillset` mount (if present) -3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory) - -When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed. - -> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references. - -### Neovim configuration - -The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host: - -```yaml -volumes: - - ~/.config/nvim:/home/developer/.config/nvim:ro -``` - -### Python development with uv - -The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv: - -```bash -# Python 3.13 is available out of the box -python3 --version - -# Use uv for package management -uv venv -uv pip install -r requirements.txt - -# Or use uv's project workflow (reads pyproject.toml) -uv sync - -# Run a Python script -uv run python script.py - -# Install standalone Python tools -uvx ruff check . - -# Install a newer Python version (persists with devbox-uv volume) -uv python install 3.14 -``` - -Python installations are stored in `~/.local/share/uv/`. To persist them across container restarts, add the `devbox-uv` named volume to your `docker-compose.yml`: - -```yaml -volumes: - - devbox-uv:/home/developer/.local/share/uv - -volumes: - devbox-uv: -``` - -Project virtual environments (`.venv`) are stored in your workspace directory and persist automatically via the `/workspace` bind mount. - -### Rust development with rustup - -The image includes `rustup-init`, the Rust toolchain installer. Rust is not pre-installed but can be bootstrapped on demand: - -```bash -# One-time setup: install Rust toolchain (~300MB, persists with volumes) -rustup-init -y -source ~/.cargo/env - -# Now use Rust normally -cargo new my-project -cargo build -cargo run -``` - -To persist Rust toolchains and cargo data across container restarts, add named volumes to your `docker-compose.yml`: - -```yaml -volumes: - - devbox-rustup:/home/developer/.rustup - - devbox-cargo:/home/developer/.cargo - -volumes: - devbox-rustup: - devbox-cargo: -``` - -### JavaScript and TypeScript - -The base image includes **Node.js 22** and **npm** — sufficient for most JavaScript and TypeScript development: - -```bash -# Initialize a new project -npm init -y - -# Install dependencies -npm install - -# Run TypeScript (via tsx, ts-node, etc.) -npx tsx src/index.ts - -# Use npx for one-off tools -npx tsc --init -``` - -The OMOS image variant also includes **Bun**, a faster JavaScript runtime and package manager: - -```bash -bun init -bun install -bun run src/index.ts -``` - -Node modules are stored in your project directory under `/workspace` and persist automatically. - -### VS Code integration - -VS Code can connect directly to a running opencode-devbox container for a full IDE experience with IntelliSense, debugging, and extensions running inside the container. - -**Local Docker (Docker running on your workstation):** - -1. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension -2. Start the container: `docker compose up -d` -3. In VS Code: `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container" → select `opencode-devbox` - -**Remote Docker (Docker running on a remote server, e.g. via SSH):** - -1. Install the [Remote - SSH](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh) and [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extensions -2. Connect to the remote host: `Ctrl+Shift+P` → "Remote-SSH: Connect to Host" -3. On the remote host, start the container: `docker compose up -d` -4. In VS Code (now connected to the remote): `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container" - -VS Code extensions installed inside the container persist as long as the container exists (not removed with `docker compose down`). For persistent extension storage across container recreations, add a named volume: - -```yaml -volumes: - - devbox-vscode:/home/developer/.vscode-server -``` - -## oh-my-opencode-slim (Multi-Agent Orchestration) - -[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) adds a multi-agent layer on top of opencode — an Orchestrator delegates tasks to specialized agents (Explorer, Oracle, Librarian, Designer, Fixer), each configurable with different models and providers. - -### Setup - -A pre-built OMOS image is available on Docker Hub as `joakimp/opencode-devbox:latest-omos`. Alternatively, build from source: - -**1. Build the image with OMOS support:** - -```bash -docker compose build --build-arg INSTALL_OMOS=true -``` - -This installs Bun and the oh-my-opencode-slim package into the image. - -**2. Enable in `.env`:** - -```bash -ENABLE_OMOS=true -``` - -**3. Run as normal:** - -```bash -docker compose run --rm devbox -``` - -On first start, the entrypoint runs the oh-my-opencode-slim installer in non-interactive mode. It generates agent configuration at `~/.config/opencode/oh-my-opencode-slim.json` inside the container. The default preset uses OpenAI models — edit the generated config or mount your own to customize. - -### OMOS Environment Variables - -| Variable | Default | Description | -|---|---|---| -| `ENABLE_OMOS` | `false` | Activate oh-my-opencode-slim on container start | -| `OMOS_TMUX` | `false` | Enable tmux pane integration (tmux is included in the base image) | -| `OMOS_SKILLS` | `true` | Install recommended skills (simplify, agent-browser, cartography) | -| `OMOS_RESET` | `false` | Force regenerate config on next start (backs up existing config) | - -### Custom Configuration - -If you mount the opencode config directory (see Custom opencode config above), the `oh-my-opencode-slim.json` file is included and persists across restarts. Edit it directly to control which models power each agent, fallback chains, council setup, and more. - -See the [oh-my-opencode-slim configuration docs](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/docs/configuration.md) for the full reference. - -### Verifying Agents - -After starting opencode with OMOS enabled, run inside the opencode session: - -``` -ping all agents -``` - -All six agents should respond if your provider authentication is working. - -## pi (alternative/complementary harness) - -[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the same mempalace install and palace data — wing/diary entries created by one are visible to the other. Available on the `*-with-pi` and `*-omos-with-pi` image tags. - -### Run - -The default container CMD is `bash -l`, so `compose run --rm devbox` drops to a login shell. From there pick a harness: - -```bash -docker compose run --rm devbox # bash — then run `pi`, `opencode`, or `aws sso login` -docker compose run --rm devbox pi # launch pi directly -docker compose exec -u developer devbox pi # attach into a `compose up -d` container -``` - -### MemPalace integration - -pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is symlinked into `~/.pi/agent/extensions/` at first start, exposing palace MCP tools (search, diary, knowledge graph) to pi natively. The same `~/.mempalace/` palace is shared with opencode, so memories are mutually visible across harnesses. To persist palace data across container recreate, uncomment the `devbox-palace` volume in `docker-compose.yml` (see *MemPalace* section below). - -### Persistence - -| Path in container | Volume | Contains | -|---|---|---| -| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. | -| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode | - -Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`. - -Full build args, extension list, and toolkit detail: - -## AWS Bedrock Authentication - -When using AWS Bedrock as your LLM provider, you need: - -### 1. AWS config on the host - -The container needs access to your `~/.aws/config` with SSO session configuration. If you already have this on another machine, copy it: - -```bash -scp -r user@other-machine:~/.aws ~/.aws -``` - -Or configure from scratch on the host: - -```bash -aws configure sso -``` - -### 2. Mount `~/.aws` into the container - -Uncomment the AWS volume mount in `docker-compose.yml`: - -```yaml -- ~/.aws:/home/developer/.aws -``` - -Note: do **not** use `:ro` — SSO writes token cache files to this directory. - -### 3. Authenticate inside the container - -Since the container runs headless (no browser), use the device-code flow: - -```bash -# Start the container -docker compose up -d -docker compose exec -u developer devbox bash - -# Authenticate — prints a URL and code you open in your local browser -aws sso login --sso-session --use-device-code - -# Once approved in the browser, start opencode -opencode -``` - -The `--use-device-code` flag outputs a URL and short code instead of trying to open a browser. Copy the URL into any browser (on your laptop, phone, etc.), enter the code, and complete the 2FA flow. The CLI in the container picks up the session automatically. - -SSO sessions typically last 8–12 hours before requiring re-authentication. Since `~/.aws` is mounted from the host, tokens persist across container restarts. - -## MemPalace — persistent AI memory - -The image includes [MemPalace](https://github.com/MemPalace/mempalace), a local-first AI memory system that stores conversation history verbatim and retrieves it via semantic search. Nothing leaves your machine. - -> MemPalace adds ~300 MB to the image (chromadb, embedding model deps). If you don't use it, rebuild with `--build-arg INSTALL_MEMPALACE=false` to shrink the image. - -### Enabling persistence - -Uncomment the palace volume in `docker-compose.yml`: - -```yaml -- devbox-palace:/home/developer/.mempalace -``` - -Without the volume, palace data lives in the container's writable layer and is lost on `--force-recreate`. - -### MCP integration with opencode - -Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`): - -```json -{ - "mcp": { - "mempalace": { - "type": "local", - "command": ["mempalace-mcp"] - } - } -} -``` - -> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace/`. `uv tool install` places `mempalace-mcp` on `PATH` as a shim whose shebang points at the venv's Python, so MCP clients can invoke it as a normal binary without worrying about the venv. Do **not** use `["python3", "-m", "mempalace.mcp_server"]` — the system Python cannot import from the uv-managed venv and you'll get `ModuleNotFoundError` / `MCP error -32000: connection closed`. - -This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries. - -### Basic usage - -```bash -# Mine project files into the palace -mempalace mine /workspace - -# Mine conversation transcripts -mempalace mine ~/.local/share/opencode/ --mode convos - -# Search memory -mempalace search "why did we switch to eno1" - -# Load context for a new session -mempalace wake-up -``` - -Each workspace gets its own isolated "wing" — memories never leak between projects. - -### Scheduled mining (mempalace-toolkit) - -The image bakes in [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit), a small set of bash wrappers that pair with mempalace for two common routines: - -```bash -# Mine opencode session history (reads ~/.local/share/opencode/opencode.db, stages JSONL, mines into wing_conversations) -mempalace-session - -# Mine a project's docs into a dedicated wing -mempalace-docs /workspace/my-project -``` - -Both wrappers are idempotent and dedup-aware — re-running them on unchanged input is a cheap no-op. - -For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox. - -Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist. - -### Storage - -Two separate named volumes keep different data classes apart: - -- **Palace data** (`~/.mempalace/`): ChromaDB vectors, SQLite knowledge graph, drawers. This is your memory — back it up, treat it as precious. Persists via the `devbox-palace` named volume. -- **Embedding model cache** (`~/.cache/chroma/`): ONNX model (~79 MB), downloaded automatically on first search. Disposable — blow it away and it re-downloads in ~4 seconds. Persists via the `devbox-chroma-cache` named volume so you don't re-download on every container recreation. -- **No API keys required** for core functionality (local embeddings via ONNX). - -Both volumes are commented out by default in `docker-compose.yml` — uncomment to enable: - -```yaml -- devbox-palace:/home/developer/.mempalace -- devbox-chroma-cache:/home/developer/.cache/chroma -``` - -**Air-gapped environments:** pre-populate the `devbox-chroma-cache` volume with the `all-MiniLM-L6-v2/` model contents. The palace volume needs no pre-population. - -## Gitea MCP server - -The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea-mcp) (`gitea-mcp`), providing 50+ MCP tools for interacting with self-hosted Gitea instances — repositories, issues, pull requests, releases, branches, wiki, and Actions. - -### Setup - -1. Create a Personal Access Token on your Gitea instance (Settings → Applications → Generate Token, scopes: `repo`, `read:user`). - -2. Add to your `.env`: - ```env - GITEA_HOST=https://your-gitea-instance.example.com - GITEA_ACCESS_TOKEN=your_token_here - ``` - -3. Enable the gitea MCP server in your `opencode.jsonc`: - ```json - { - "mcp": { - "gitea": { - "type": "local", - "command": ["gitea-mcp", "-t", "stdio", "--host", "{env:GITEA_HOST}"], - "environment": { - "GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}" - }, - "enabled": true - } - } - } - ``` - -The server is installed but disabled by default — it requires authentication to be useful. - -## Architecture - -``` -Host Machine -├── ~/projects/my-app ──bind mount──▶ /workspace (container) -├── ~/.ssh ──bind mount──▶ /home/developer/.ssh (ro) -├── ~/.aws ──bind mount──▶ /home/developer/.aws (Bedrock SSO) -└── .env ──env vars───▶ provider config + API keys - -Container (Debian trixie) -├── opencode binary -├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun) -├── AWS CLI v2 (SSO + Bedrock auth) -├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync -├── git, git-crypt, age, ssh, ripgrep, fd, fzf, jq, curl, tree -├── Node.js (for MCP servers) -├── Bun (optional — included with oh-my-opencode-slim) -├── entrypoint.sh (UID adjustment, git config, provider setup) -└── /workspace ← your code lives here -``` - -### Data persistence - -| Path in container | Source | Survives `--rm`? | Contains | -|---|---|---|---| -| `/workspace` | Host bind mount | ✅ Yes | Your project files | -| `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes | SSH keys | -| `/home/developer/.aws` | Host bind mount (if configured) | ✅ Yes | AWS credentials/SSO cache | -| `/home/developer/.local/share/opencode` | Named volume `devbox-data` | ✅ Yes | Session history, memory | -| `/home/developer/.local/state/opencode` | Named volume `devbox-state` | ✅ Yes | TUI settings (theme, toggles) | -| `/home/developer/.cache/bash` | Named volume `devbox-shell-history` | ✅ Yes | Bash history (`$HISTFILE`), survives container recreate | -| `/home/developer/.local/share/zoxide` | Named volume `devbox-zoxide` | ✅ Yes | Zoxide directory history (`z ` jump targets) | -| `/home/developer/.local/share/nvim` | Named volume `devbox-nvim-data` | ✅ Yes | Neovim plugins, Mason LSP installs, Lazy plugin cache | -| `/home/developer/.local/share/uv` | Named volume `devbox-uv` (if configured) | ✅ Yes | Python installs, uv tool installs | -| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains | -| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache | -| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions | -| `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant | - -**opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above). - -## Source - -MIT licensed. Source, issues, and `docker-compose.yml` templates: +> This description is generated by `scripts/generate-dockerhub-md.py` from a hand-maintained template. Edit the template (not this file) and regenerate. diff --git a/scripts/generate-dockerhub-md.py b/scripts/generate-dockerhub-md.py index c12b8ff..a792223 100755 --- a/scripts/generate-dockerhub-md.py +++ b/scripts/generate-dockerhub-md.py @@ -1,15 +1,30 @@ #!/usr/bin/env python3 """ -Generate DOCKER_HUB.md from README.md. +Generate DOCKER_HUB.md. Rationale --------- -README.md is the authoritative source. DOCKER_HUB.md is a subset -intended for users pulling the pre-built image from Docker Hub — so -build-from-source instructions, developer setup (git hooks, gitleaks), -and CI/contribution content are dropped. +DOCKER_HUB.md is the public-facing description shown on Docker Hub. It +has two hard constraints the README does not: -Docker Hub enforces a 25 kB limit on the full description field. + 1. A 25 kB byte limit on the full_description field. + 2. A different audience: Hub readers want a 30-second evaluation — + "what is this, how do I run it, does it have what I need" — and + reference material is better consulted in context on gitea. + +For a long time this script tried to derive DOCKER_HUB.md from README.md +by section selection + targeted replacement. As the README grew that +approach pushed against the 25 kB ceiling on every change, costing a +trim-something-else exercise per edit (final state: 3 byte headroom). + +The new approach is much simpler: a hand-written HUB_TEMPLATE below. +The template intentionally stays slim and links out to the gitea README +for everything that benefits from depth. README.md grows freely. + +Trade-off: when image-variants table or quick-start flow changes, +update HUB_TEMPLATE here too. That coupling is now explicit and +local rather than spread across SECTION_RULES + REPLACEMENTS + TRIM +machinery. Usage ----- @@ -19,77 +34,42 @@ Regenerate in place: Fail if DOCKER_HUB.md is out of sync with what this script would emit (run this in CI): python3 scripts/generate-dockerhub-md.py --check - -Design ------- -Sections are selected and in some cases rewritten via `SECTION_RULES` -below. This keeps the transformation explicit and easy to audit — if -a new section is added to README.md that should also appear on Docker -Hub, extend SECTION_RULES rather than inventing implicit heuristics. """ from __future__ import annotations import argparse -import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent -README = REPO_ROOT / "README.md" DOCKER_HUB = REPO_ROOT / "DOCKER_HUB.md" # Max size for Docker Hub full_description (bytes, UTF-8). MAX_SIZE_BYTES = 25_000 -# Per-section transformation. -# -# Each key is a top-level section title as it appears in README.md -# (without the leading "## "). -# -# The value is one of: -# "keep" — include verbatim. -# "drop" — exclude entirely. -# "replace" — substitute a custom body (see REPLACEMENTS). -# "trim" — keep but drop selected level-3 sub-sections listed -# in TRIM_SUBSECTIONS[title]. -# -# Unknown sections default to "drop" with a warning — forcing an -# explicit decision whenever README gains a new section. -SECTION_RULES: dict[str, str] = { - "Why?": "drop", # build-motivation, not user-facing - "Quick Start": "replace", # swap docker compose clone flow for docker run - "Features": "keep", - "Usage": "keep", - "Configuration": "trim", # drop dev-build sub-sections - "oh-my-opencode-slim (Multi-Agent Orchestration)": "keep", - "pi (alternative/complementary harness)": "replace", # tailored Hub version, full detail in README - "AWS Bedrock Authentication": "keep", - "MemPalace — persistent AI memory": "keep", - "Gitea MCP server": "keep", - "Context7 MCP server": "drop", - "Shell defaults": "drop", # detail, full README covers it - "Secret Scanning": "drop", # dev-only — gitleaks is for committers - "Architecture": "keep", - "License": "replace", # point at source repo instead -} +# Where readers go for the full reference. +GITEA = "https://gitea.jordbo.se/joakimp/opencode-devbox" -# Level-3 sub-section titles (without the leading "### ") to drop from -# sections flagged as "trim". These are dev/build-oriented — Docker Hub -# users already have the image and don't need rebuild or multi-user -# compose instructions. -TRIM_SUBSECTIONS: dict[str, set[str]] = { - "Configuration": { - "Multi-user setup", - "Rebuilding the Image", - "Build Args", - }, -} -# Replacement bodies. Keys match SECTION_RULES entries marked "replace". -# Each value is the full section including the "## Title" heading. -REPLACEMENTS: dict[str, str] = { - "Quick Start": """## Quick Start +HUB_TEMPLATE = f"""# opencode-devbox + +Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed. + +Designed for teams who want a reproducible coding-agent setup that runs the same on every laptop and CI runner — without forcing each developer to install Bun, Node, AWS CLI, mempalace, or maintain shell config drift across machines. + +## Image Variants + +| Tag | Description | +|---|---| +| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools | +| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun | +| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) | +| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together | + +All variants support `linux/amd64` and `linux/arm64`. + +## Quick Start ```bash docker run -it --rm \\ @@ -102,9 +82,9 @@ docker run -it --rm \\ joakimp/opencode-devbox:latest ``` -This drops you straight into opencode with your project mounted at `/workspace`. +Drops you straight into opencode with your project mounted at `/workspace`. -For an interactive shell first (useful for AWS SSO login): +For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`): ```bash docker run -it --rm \\ @@ -115,156 +95,63 @@ docker run -it --rm \\ joakimp/opencode-devbox:latest bash ``` -Then run `opencode` when ready. +Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell. -For docker-compose users, see the source repo for `docker-compose.yml` and `.env.example` templates. -""", - "License": """## Source +For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired. -MIT licensed. Source, issues, and `docker-compose.yml` templates: -""", - "pi (alternative/complementary harness)": """## pi (alternative/complementary harness) +## What's Inside -[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the same mempalace install and palace data — wing/diary entries created by one are visible to the other. Available on the `*-with-pi` and `*-omos-with-pi` image tags. +- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.). +- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`. +- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi. +- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents). +- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace. +- **MCP wrappers** for mempalace pre-installed and pre-wired to both harnesses. -### Run +## Authentication -The default container CMD is `bash -l`, so `compose run --rm devbox` drops to a login shell. From there pick a harness: +The container reads provider credentials from environment variables and host-mounted config: -```bash -docker compose run --rm devbox # bash — then run `pi`, `opencode`, or `aws sso login` -docker compose run --rm devbox pi # launch pi directly -docker compose exec -u developer devbox pi # attach into a `compose up -d` container -``` +- **Anthropic / OpenAI / Groq / others:** set `OPENCODE_PROVIDER` and the corresponding `*_API_KEY` via `-e` or `.env`. +- **AWS Bedrock (SSO):** mount `~/.aws` from the host, `OPENCODE_PROVIDER=amazon-bedrock`, then `aws sso login` inside the container. Tokens persist across container restarts via the host bind-mount. +- **OAuth / device-code providers:** auth state lives in opencode's config, which is persisted via the `devbox-opencode-config` named volume. -### MemPalace integration +Full Bedrock walkthrough (IAM roles, permissions, multi-account setups): see the [AWS Bedrock Authentication]( +{GITEA}#aws-bedrock-authentication +) section on gitea. -pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is symlinked into `~/.pi/agent/extensions/` at first start, exposing palace MCP tools (search, diary, knowledge graph) to pi natively. The same `~/.mempalace/` palace is shared with opencode, so memories are mutually visible across harnesses. To persist palace data across container recreate, uncomment the `devbox-palace` volume in `docker-compose.yml` (see *MemPalace* section below). +## Persistence -### Persistence - -| Path in container | Volume | Contains | +| Volume | Mount | Survives | |---|---|---| -| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. | -| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode | +| `devbox-opencode-config` | `~/.config/opencode` | container recreate, image rebuild | +| `devbox-pi-config` | `~/.pi` | container recreate, image rebuild — incl. user-installed pi packages via `pi install` (`NPM_CONFIG_PREFIX` points into the volume) | +| `devbox-palace` (uncomment) | `~/.mempalace` | container recreate, image rebuild — palace data is precious, treat as primary storage | +| `devbox-chroma-cache` | `~/.cache/chroma` | container recreate (model cache, disposable — re-downloads in seconds) | -Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`. +Workspace bind-mount (`/workspace`) is your project directory on the host, so source code is never inside the container. -Full build args, extension list, and toolkit detail: -""", -} +Full persistence reference, including multi-user (`SIGNUM`) isolation and host bind-mount alternatives: see the [README on gitea]({GITEA}#persistence). +## Where to Go Next -# Prepended to the generated file. -HEADER = """# opencode-devbox — Docker Hub +- **Full README** with build args, every feature in detail, troubleshooting: <{GITEA}> +- **CHANGELOG** for version history: <{GITEA}/src/branch/main/CHANGELOG.md> +- **Issues / source / docker-compose templates:** <{GITEA}> +- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <{GITEA}/src/branch/main/AGENTS.md> -Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed. +## License -## Image Variants +MIT. See <{GITEA}/src/branch/main/LICENSE>. -Four image variants are published for each release: - -| Tag | Description | -|---|---| -| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools | -| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun | -| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) | -| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together | - -All variants support `linux/amd64` and `linux/arm64`. - -> **NOTE:** This file is auto-generated from `README.md` by `scripts/generate-dockerhub-md.py`. Edit README.md and regenerate rather than editing this file directly. +--- +> This description is generated by `scripts/generate-dockerhub-md.py` from a hand-maintained template. Edit the template (not this file) and regenerate. """ -def split_sections(md: str) -> list[tuple[str, str]]: - """Split markdown on level-2 headings, returning (title, body) pairs. - - The body includes the heading line and everything up to (but not - including) the next level-2 heading or EOF. Content before the first - ``## `` is returned with an empty title (the document preamble). - """ - pattern = re.compile(r"^## ", re.MULTILINE) - parts = pattern.split(md) - preamble, *rest = parts - - sections: list[tuple[str, str]] = [] - if preamble.strip(): - sections.append(("", preamble)) - for part in rest: - line, _, body = part.partition("\n") - sections.append((line.strip(), f"## {line}\n{body}")) - return sections - - -def trim_subsections(body: str, drop: set[str]) -> str: - """Remove level-3 sub-sections whose title is in `drop`. - - A sub-section starts at a line beginning with "### " and ends at - the next "### " or "## " (or EOF). - """ - if not drop: - return body - - # Split on level-3 headings while preserving the level-2 header - # block. First piece is everything up to the first "### ". - parts = re.split(r"(^### .+\n)", body, flags=re.MULTILINE) - # parts alternates: [before_first_h3, "### Title\n", body, "### Title\n", body, ...] - kept: list[str] = [parts[0]] if parts else [] - i = 1 - while i < len(parts): - heading = parts[i] - content = parts[i + 1] if i + 1 < len(parts) else "" - title = heading[4:].strip() - if title not in drop: - kept.append(heading) - kept.append(content) - i += 2 - return "".join(kept) - - def generate() -> str: - """Produce the DOCKER_HUB.md content string.""" - readme = README.read_text(encoding="utf-8") - sections = split_sections(readme) - - out: list[str] = [HEADER] - unknown: list[str] = [] - - for title, body in sections: - if title == "": - # README preamble is replaced by our HEADER; skip. - continue - - rule = SECTION_RULES.get(title) - if rule is None: - unknown.append(title) - continue - if rule == "drop": - continue - if rule == "keep": - out.append(body.rstrip() + "\n\n") - elif rule == "trim": - trimmed = trim_subsections(body, TRIM_SUBSECTIONS.get(title, set())) - out.append(trimmed.rstrip() + "\n\n") - elif rule == "replace": - out.append(REPLACEMENTS[title].rstrip() + "\n\n") - else: # pragma: no cover — programmer error - raise AssertionError(f"unknown rule {rule!r} for section {title!r}") - - if unknown: - print( - "ERROR: README.md contains sections not classified in " - "SECTION_RULES:\n - " - + "\n - ".join(unknown) - + "\n\nAdd each to SECTION_RULES in " - "scripts/generate-dockerhub-md.py (choose keep/drop/replace).", - file=sys.stderr, - ) - raise SystemExit(2) - - return "".join(out).rstrip() + "\n" + return HUB_TEMPLATE def main() -> int: @@ -290,11 +177,10 @@ def main() -> int: existing = DOCKER_HUB.read_text(encoding="utf-8") if DOCKER_HUB.exists() else "" if existing != content: print( - "ERROR: DOCKER_HUB.md is out of sync with README.md.\n" + "ERROR: DOCKER_HUB.md is out of sync with the template.\n" "Run: python3 scripts/generate-dockerhub-md.py", file=sys.stderr, ) - # Show a small diff hint. import difflib diff = difflib.unified_diff( @@ -307,14 +193,16 @@ def main() -> int: sys.stderr.writelines(list(diff)[:80]) return 1 print( - f"OK: DOCKER_HUB.md is in sync with README.md " - f"({size} bytes, {MAX_SIZE_BYTES} limit).", + f"OK: DOCKER_HUB.md is in sync with HUB_TEMPLATE " + f"({size} bytes, {MAX_SIZE_BYTES} limit, " + f"{MAX_SIZE_BYTES - size} bytes headroom).", ) return 0 DOCKER_HUB.write_text(content, encoding="utf-8") print( - f"Wrote {DOCKER_HUB} ({size} bytes, {MAX_SIZE_BYTES} limit).", + f"Wrote {DOCKER_HUB} ({size} bytes, {MAX_SIZE_BYTES} limit, " + f"{MAX_SIZE_BYTES - size} bytes headroom).", ) return 0