diff --git a/.dockerignore b/.dockerignore index d99e19b..a982e68 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ .git .env* node_modules +dist +.playwright-cli AGENTS.md README.md LICENSE diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4edeef3..15f10a8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -36,7 +36,7 @@ jobs: submodules: recursive - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/AGENTS.md b/AGENTS.md index d78f117..49e4779 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,44 +2,47 @@ - This repo owns the static-web wrapper only. `opencode/` is an upstream git submodule; do not edit files under it here. The only allowed upstream change is moving the submodule pointer. - Wrapper-owned code lives in `build/`, `runtime/`, `config/`, `scripts/`, `tests/`, and root Docker/CI/package config. -- If you need upstream context, read the closest `AGENTS.md` inside `opencode/` first. +- If you must inspect upstream code, read the closest `AGENTS.md` inside `opencode/` first; upstream's default branch is `dev`, not `main`. -## Entry Points +## Runtime Wiring -- `Dockerfile` is the real integration path: it runs `bun run test:compat`, bundles `runtime/index.ts` with `bun run build:runtime`, builds `opencode/packages/app`, patches the built app with `bun run build:prepare-static`, then serves it with nginx. -- `runtime/generate-nginx-config.sh` is the runtime generation source of truth. It validates `SERVER__HOST` and `SERVER__BACKEND`, requires contiguous unpadded indexes starting at `1`, normalizes hostnames and backend URLs, writes per-host runtime configs under `/opt/opencode-web/runtime-configs/.js`, and regenerates `/etc/nginx/conf.d/default.conf` from `config/nginx.conf.template`. +- `Dockerfile` is the real integration path: it installs the filtered upstream app workspace, builds `opencode/packages/app`, copies wrapper sources, runs `build/check-runtime-config-compat.ts`, bundles `runtime/index.ts`, patches `opencode/packages/app/dist`, then serves it with nginx. +- `runtime/generate-nginx-config.sh` is the runtime generation source of truth. It runs as `/docker-entrypoint.d/40-opencode-web.sh` under Alpine `/bin/sh`, requires contiguous unpadded indexes starting at `1`, normalizes hostnames/backend URLs, writes per-host configs under `/opt/opencode-web/runtime-configs/.js`, and regenerates nginx config from `config/nginx.conf.template`. - `runtime/runtime-config-core.ts` is the browser bootstrap: it replaces the persisted server list and default server URL with the injected backend for that host, prunes other servers/credentials, and removes legacy `server.v3`. -- `build/prepare-static-web.ts` injects `/runtime-config.js` and external `opencode-web-customizations.css` into `index.html`, then patches referenced built JS so the app uses `window.__OPENCODE_SERVER_URL` instead of `location.origin`. +- `build/prepare-static-web.ts` injects `/runtime-config.js`, writes `opencode-web-customizations.css` from `build/customization-css.ts`, then patches referenced built JS so the app uses `window.__OPENCODE_SERVER_URL` instead of `location.origin`. - `config/nginx.conf.template` is the base nginx cache/CSP contract: unmatched hosts return 404 except `/health`, configured hosts are appended by the generator, only `/assets/` is immutable, and all other configured-host responses stay `no-store` with `add_header ... always`. ## Compatibility Contracts - Root `bun test` runs only `./tests` because root `bunfig.toml` sets `[test].root = "./tests"`. - `tests/*.contracts.ts` encode every wrapper assumption about upstream app internals, runtime persistence, CSS selectors, and nginx CSP/cache behavior. If upstream changes break the wrapper, update the contract and wrapper code together. -- `bun run test:compat` is the same upstream-compat guard used during `docker build`. +- `bun run test:compat` is the same upstream-compat guard used during `docker build`; keep it free of extra root-only dependencies because Docker invokes `bun ./build/check-runtime-config-compat.ts` without installing the root package. - CSP/cache headers are intentionally duplicated in `config/nginx.conf.template` and `runtime/generate-nginx-config.sh`; keep them in sync. ## Commands - First-time setup: `git submodule update --init --recursive` - Install root dependencies: `bun install --frozen-lockfile` +- Root dependency install is wrapper-only; upstream app deps are installed separately by Docker or the Docker-equivalent upstream app build command below. - Fast root checks matching CI's build-compat job: `bun test && bun run test:compat && bun run typecheck && bun run lint && bun run format:check` - Apply formatting: `bun run format` - Upstream compatibility check only: `bun run test:compat` - Runtime bundle only: `bun run build:runtime` -- Static web preparation only: `bun run build:prepare-static -- ` +- Static web preparation only, after the upstream app dist exists: `bun run build:prepare-static -- opencode/packages/app/dist` - Focused root tests: `bun test tests/.test.ts`, for example `bun test tests/compatibility-contracts.test.ts` - Runtime/image regression check: `bun run test:runtime-config -- --build`; without `--build`, the script expects an existing image tag, defaulting to `opencode-web-docker`. -- End-to-end build: `docker build -t opencode-web-docker .` -- Quick upstream app build smoke check: `bun run --cwd opencode/packages/app build` +- End-to-end build: `bun run docker:build` or `docker build -t opencode-web-docker .` +- Docker-equivalent upstream app build: `bun install --cwd opencode --filter @opencode-ai/app --frozen-lockfile --ignore-scripts` then `OPENCODE_CHANNEL=prod bun run --cwd opencode/packages/app build -- --sourcemap false` - Update upstream submodule: `bun run upstream:update -- [tag]` - Dry-run upstream update: `bun run upstream:update -- --dry-run [tag]` ## Gotchas - CI enters through `.github/workflows/ci.yml` and reusable `validate.yml`; it includes Docker runtime regression, `bun test`, `test:compat`, typecheck, Biome lint/format, `actionlint`, `zizmor`, and `shellcheck`. -- Root `typecheck`, `lint`, and `format` scripts cover `build/`, `runtime/`, `tests/`, and `scripts/`; edits in `config/` or `.github/workflows/` need separate review/tooling. +- GitHub Actions are pinned by full SHA in workflows; preserve pinning when editing `.github/workflows/*`. +- Root `typecheck`, `lint`, and `format` scripts cover wrapper TS/JSON in `build/`, `runtime/`, `tests/`, and `scripts/`; nginx config, workflows, Docker files, and shell semantics need separate review/tooling. - The Docker build context excludes `scripts/` and most upstream docs/tests via `.dockerignore`; update `.dockerignore` before relying on ignored files in `Dockerfile`. -- Shell scripts are `/bin/sh`/Alpine-compatible; do not use Bash-only syntax in `runtime/generate-nginx-config.sh`. +- Shell scripts are `/bin/sh`/Alpine-compatible; keep `runtime/generate-nginx-config.sh` env scanning newline-safe so multiline values cannot create fake `SERVER__*` names. +- The final image runs as non-root `nginx` on port `8080`; keep `/opt/opencode-web/public` read-only and generated config/writable state under `/etc/nginx/conf.d`, `/opt/opencode-web/runtime-configs`, or `/var/cache/nginx`. +- Root TypeScript uses `erasableSyntaxOnly`; avoid enums, namespaces, parameter properties, or other TS syntax that needs transpilation in wrapper `.ts` files. - Bun version sources are duplicated: `package.json` `packageManager` drives GitHub `setup-bun`, while `Dockerfile` and `@types/bun` must be reviewed separately when changing Bun. -- Upstream OpenCode's default branch is `dev`, not `main`. diff --git a/Dockerfile b/Dockerfile index adbe074..784e6c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,16 +20,15 @@ COPY --parents \ RUN bun install --cwd opencode --filter @opencode-ai/app --frozen-lockfile --ignore-scripts COPY opencode ./opencode -COPY package.json bun.lock tsconfig.json biome.json ./ -RUN bun install --frozen-lockfile +# Keep wrapper-only edits from invalidating the upstream app build layer. +RUN OPENCODE_CHANNEL=prod bun run --cwd opencode/packages/app build -- --sourcemap false COPY build ./build/ COPY runtime ./runtime/ COPY tests ./tests/ COPY config ./config/ -RUN bun run test:compat -RUN bun run build:runtime -RUN OPENCODE_CHANNEL=prod bun run --cwd opencode/packages/app build -- --sourcemap false -RUN bun run build:prepare-static -- ./opencode/packages/app/dist +RUN bun ./build/check-runtime-config-compat.ts +RUN bun ./build/transpile-runtime.ts +RUN bun ./build/prepare-static-web.ts ./opencode/packages/app/dist RUN mkdir -p release/public release/runtime release/config \ && cp -r config/. release/config/ \ && cp dist/runtime/runtime-bundle.js release/runtime/ \ diff --git a/README.md b/README.md index 4e2db68..26f4606 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Requests with an unmatched `Host` header never receive a generated runtime confi ## How It Works -1. **Build:** the Docker build compiles the upstream app, injects the runtime bootstrap into `index.html`, patches the built frontend to use `window.__OPENCODE_SERVER_URL`, and runs a compatibility check so upstream persistence changes fail early. +1. **Build:** the Docker build compiles the upstream app, runs a compatibility check before assembling the release, injects the runtime bootstrap into `index.html`, and patches the built frontend to use `window.__OPENCODE_SERVER_URL`. 2. **Runtime:** nginx's official entrypoint runs `/docker-entrypoint.d/40-opencode-web.sh`, which validates `SERVER__HOST` and `SERVER__BACKEND`, generates per-host runtime configs under `/opt/opencode-web/runtime-configs/.js`, and writes `/etc/nginx/conf.d/default.conf` from `config/nginx.conf.template`. 3. **Serving:** nginx serves shared static files from `/opt/opencode-web/public`. Configured hosts get exact server blocks, `/runtime-config.js` aliases the matching generated config, extension-like missing static files return `404`, route-like extensionless paths fall back to `/index.html`, and only `/assets/` uses immutable caching. diff --git a/biome.json b/biome.json index 94c877a..4ec6757 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", "files": { "includes": [ "**", diff --git a/bun.lock b/bun.lock index 9e43399..be9aa55 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "opencode-web-docker", "devDependencies": { - "@biomejs/biome": "^2.4.15", + "@biomejs/biome": "^2.4.16", "@types/bun": "1.3.14", "@types/node": "25.9.1", "typescript": "^6.0.3", @@ -13,23 +13,23 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], + "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], @@ -40,9 +40,5 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - - "bun-types/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], - - "bun-types/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], } } diff --git a/package.json b/package.json index 04aee9b..1cd946a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "format:check": "biome format build/ runtime/ tests/ scripts/ package.json tsconfig.json biome.json" }, "devDependencies": { - "@biomejs/biome": "^2.4.15", + "@biomejs/biome": "^2.4.16", "@types/bun": "1.3.14", "@types/node": "25.9.1", "typescript": "^6.0.3"