diff --git a/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md new file mode 100644 index 0000000..b1c43fa --- /dev/null +++ b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md @@ -0,0 +1,653 @@ +# Phase 14.3: One-command app scaffolding (`summer new` + summercms.io/new) - Research + +**Researched:** 2026-10-08 +**Domain:** Go CLI scaffolding (internal/build), Go modules/workspaces, curl|bash bootstrap, static route on the site plugin, docs pipeline +**Confidence:** HIGH (most claims were checked empirically in a scratch directory against the local framework and a throwaway PostgreSQL 16 container) + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +#### Naming (Golem15 convention) +- **D-01:** `sm--app` is an application (this phase), `sm--plugin` a standalone plugin module, `sm--theme` a Nuxt/Vue frontend (later). The generated `CLAUDE.md` states this convention. +- **D-02:** `` is validated as lower-case letters and digits, starting with a letter (`^[a-z][a-z0-9]*$`); it also becomes the second part of the starter plugin ID `golem15.`. `summer new` refuses to overwrite an existing `sm--app/` directory (exists at all → error, no merge). — **Reversibility:** costly — the name rule is a public CLI contract. + +#### `summer new` output +- **D-03:** `go.mod`: module `git.golem15.com/golem15/sm--app`, `go 1.27.0`, requiring `git.golem15.com/golem15/summercms` with a `replace` to the local framework checkout, written as a **relative** path. — **Reversibility:** reversible. +- **D-04:** Framework checkout resolution order: (1) `--framework ` flag; (2) auto-detection of sibling directories of the target parent (at least `../summercms` and `../summercms.go`) whose `go.mod` declares module `git.golem15.com/golem15/summercms` — detection is by module path, not folder name, because the local checkout is `summercms.go` while the bootstrap clones `summercms`; (3) otherwise a clear error naming `--framework`. Research may add the running binary's own source location as a further candidate if cheap and reliable. +- **D-05:** `summer.yaml` plus one starter plugin `golem15.` created through the **same code paths** as `make:plugin` and `plugin:add` (no duplicated templates), so `go.work` is created too. The starter plugin exposes at least one HTTP route so acceptance can hit it. +- **D-06:** Research must check whether an empty `plugins: []` builds. If it does, add `--no-plugin`; if it does not, either fix that in the build pipeline (preferred if small) or drop `--no-plugin` and record why. +- **D-07:** `config/` contains everything `serve` and `route:list` need, including `http.yaml` with `body_limits` (the known gap in `examples/hello`), app key handling, and storage bucket defaults (uploads bucket). Fix the `examples/hello` gap too if it is the same config. +- **D-08:** `.env.example` lists the `SUMMER_*` variables (DSN, `APP__KEY`, uploads bucket). `.env` is generated with a fresh app key by reusing the `key:generate` logic in `modules/lagoon/keygen.go` (export or move a helper; do not copy it). `.gitignore` covers `bin/`, `.env`, and generated files that should not be committed (research decides which: e.g. `plugins.gen.go`? `storage/`?). +- **D-09:** `compose.yaml` runs PostgreSQL 16 with credentials matching `.env`'s DSN. +- **D-10:** `CLAUDE.md` for AI agents, also usable as `AGENTS.md` (symlink or identical copy — planner's call, but it must survive `git clone` on Linux/macOS). It says: this is a SummerCMS app; do not edit the framework; docs at https://summercms.io/docs/llms.txt and https://summercms.io/docs/llms-full.txt; core commands `make:plugin`, `plugin:add`, `build`, `dev`, `migrate`, `serve`; the naming convention (D-01). Neutral of any consuming application (framework README rule applies to the template text). +- **D-11:** `git init` and a first commit, skippable with `--no-git`. The commit must not fail when the user has no git identity configured (set a fallback author for that commit only, or skip with a warning — planner's call). +- **D-12:** The generated app works with **no edits**: `summer build`, `summer migrate` and `summer serve` succeed once `docker compose up -d` is running. + +#### Bootstrap script +- **D-13:** Script lives in `summercms.go` (e.g. `scripts/new.sh`) and is embedded in an exported framework package so it is versioned with the framework tag. `sm-summercmsio-plugin` serves it at `GET /new` as `text/plain; charset=utf-8`. No nginx change (nginx already allows GET/HEAD and proxies everything except `/backend`). No Cloudflare Worker. msd-core's `scripts/bootstrap.sh` is a pattern reference only. — **Reversibility:** reversible. +- **D-14:** The whole script is wrapped in `main()` invoked on the last line, so a truncated download runs nothing. +- **D-15:** Steps: (1) check Go ≥ 1.27 (clear install hint if missing/old) and Docker (warn only if missing); (2) clone the framework or update an existing clone, fast-forward only; (3) `go install ./cmd/summer`, then print a PATH hint if `$(go env GOPATH)/bin` (or `GOBIN`) is not on `PATH`; (4) run `summer new ` next to the framework (sibling directory, so D-04 auto-detection finds it); (5) print next steps: `docker compose up -d`, `summer build`, `summer migrate`, `summer serve`. +- **D-16:** The script checks out the **latest release tag** (`vX.Y.Z`) of `git.golem15.com/golem15/summercms`, not `master`. Re-runs fetch tags and move forward to the newest tag (never a non-fast-forward). Research settles how this interacts with an existing clone that has local changes or sits on a branch (refuse with a clear message, do not reset). +- **D-17:** Where the framework is cloned (the script's working directory, e.g. `./summercms`) and any env overrides (clone URL/dir, e.g. for tests) are planner's call; the clone URL defaults to the public HTTPS remote. + +#### Docs and site +- **D-18:** Rewrite `docs/setup/installation.md`: the one-liner first, the manual `examples/hello` walkthrough second. Update `docs/architecture/go-modules-and-workspaces.md` to cover `summer new`. Add `summer new` to the console docs. `llms.txt`/`llms-full.txt` pick up the changes (regenerate; `summer docs:build --check` and `TestDocsTree` stay green). Module READMEs for any package whose exported API changes (e.g. `lagoon` if the keygen helper is exported) update in the same change. +- **D-19:** The summercms.io landing page "Get started" switches to the one-liner in this phase (`vue-summercmsio-app`), and `TestTerminalCommands` in `sm-summercmsio-app` is updated to match. The site is redeployed after `v0.1.13` is tagged (deploy itself is a user action per `sm-summercmsio-app/DEPLOY.md`). + +#### Acceptance +- **D-20:** In a clean `node:24-bookworm`-style container with Go 1.27, Docker access, git and jq, starting from nothing: `curl … | bash -s -- demo` → `cd sm-demo-app && docker compose up -d && summer build && summer migrate && summer serve`, then a starter-plugin route answers over HTTP. Before the site is deployed, the run may fetch the script from a local server or the repo file, and clone from a local path/tag via the D-17 overrides; the final run against https://summercms.io/new happens after deploy. +- **D-21:** Generator Go tests follow the repo's existing test conventions (stdlib `testing`, golden files under `testdata/`, `-short` gating for slow paths such as the real build). Unit tests are the last plan. + +### Claude's Discretion +- Exact package layout inside `internal/build` (e.g. `new.go` + templates under `internal/build/stubs/new/`), flag names beyond those listed, and template engine (`text/template` like existing stubs). +- Whether `summer new` also runs `summer build` at the end (default: no — the next-steps text covers it). +- Default DB name/user/password and host port in `compose.yaml`/`.env` (avoid clashing with a host Postgres on 5432 if cheap, e.g. a non-default host port). +- Security review: the bootstrap script is piped into a shell and served publicly — run the security-review agent for plan 2. + +### Agreed Plan Split (checkpoint confirmed 2026-10-08) +1. **Generator** — `summer new` in `cmd/summer` + `internal/build` (D-02..D-12), with a smoke test. Repo: `summercms.go`. +2. **Bootstrap script + `/new` route + acceptance** — `scripts/new.sh` embedded and exported (D-13..D-17), `GET /new` in `sm-summercmsio-plugin`, container acceptance run (D-20). Repos: `summercms.go`, `sm-summercmsio-app` (plugin submodule). +3. **Docs and landing page** — D-18, D-19. Repos: `summercms.go`, `sm-summercmsio-app` / `vue-summercmsio-app`. +4. **Unit tests** — golden output + real build in a temp dir (D-21). Repo: `summercms.go`. + +### Deferred Ideas (OUT OF SCOPE) +- `summer new --theme` / `sm--theme` Nuxt scaffold. +- Publishing `summer` as prebuilt release binaries (would remove the Go prerequisite for running the CLI, not for building apps). + + +## Project Constraints (from CLAUDE.md) + +- Stdlib first (net/http ServeMux, text/template, encoding/json, os/exec, embed). **No new Go dependency is needed for this phase** — everything below uses stdlib plus existing framework packages. +- Compiled plugins only; no runtime plugin loading. +- `go vet ./...` and `go test ./...` green at every commit; one logical change per commit; planning docs and code in separate commits; **no co-author tags** in commit messages (user global rule; overrides the harness attribution reminder). +- Unit tests are the last plan of the phase (plan 4). +- Any change to a package under `modules/` that touches its exported API → update that module's `README.md` in the same change; a **new** module ships a README with the standard structure (H1, one-sentence summary, import line, Overview, Features, Usage, API reference, optional Configuration/CLI commands, Dependencies, Testing) and a row in the root `README.md` modules table reusing the summary sentence. +- Framework READMEs and docs never name a consuming application (applies to the generated `CLAUDE.md` template text too, per D-10). Use neutral names (`acme`, `blog`, `demo`). +- Every identifier named in a README/docs page must exist (docs checker). A change to exported API/config/CLI updates `docs/` in the same change; `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check` must pass. +- Config keys in docs are not machine-checked — review by hand. +- Core plugins (user, blog, pages, payment) must not get breaking changes; the site plugin and `sm-summercmsio-app` are not core plugins. +- Use `ssu` for submodule status/push in `sm-summercmsio-app` (user global rule). +- GSD workflow: work through `/gsd-execute-phase`. + +## Summary + +Almost everything `summer new` needs already exists as callable library code: `build.MakePlugin(ctx, startDir, id)` and `build.AddPlugin(ctx, startDir, pluginDir)` already take a directory argument (not cwd), and the unexported `generate(appDir, m)` writes `main.go` + `plugins.gen.go` without compiling. A new `internal/build/new.go` in the same package can orchestrate them. Two real defects surfaced that the plan must fix: (1) `plugin:add` writes an **absolute** path into `go.work` (because it passes an absolute dir to `go work use`), which would make the committed `go.work` of every generated app machine-specific; (2) the `make:plugin` `routes.go` stub registers no route, so the starter route needs a template option, not a second template. + +An empty plugin list builds and serves (verified: `plugins: []` → `built empty in 1.947s`, `route:list` and `serve` work once config is present), so `--no-plugin` should be added. The minimal runtime contract was established empirically against PostgreSQL 16: `serve`/`route:list` require `http.body_limits.{default_bytes,upload_bytes}` as YAML numbers; `migrate`/`serve` require `database.dsn` and a valid `app.key` (32 bytes base64); `serve` additionally requires `storage.uploads.bucket_url`. Nothing else (no admin JWT secret — that is only required when an admin controller exists). The hand-built scratch app (`go.mod`, `go.work`, `summer.yaml`, `config/http.yaml`, `config/storage.yaml`, env for DSN+key, starter plugin) built, migrated and answered `GET /` with 200 JSON through `summer serve`. + +The bootstrap script is a straightforward bash `main()` with git tag selection, `go install`, and `summer new --framework`. The main traps are pipefail+`head` SIGPIPE, git credential prompts hanging, `key:generate` printing a `✓ ` prefix (so it cannot be captured with `$(...)`), and `summer serve --addr` being rejected by the delegating `summer` command. The site serves the script from a new exported framework package; `//go:embed` cannot reach `../scripts/`, so the script file must live **inside** that package directory (precedent: `modules/boardwalk` embeds `dist/`). + +**Primary recommendation:** Implement `build.NewApp(ctx, NewOptions)` in `internal/build/new.go` as a pure "plan files" step + an executor that writes `go.mod`/`go.work`/config first, then calls the existing `MakePlugin` (with a `StarterRoute` option rendered by the same `routes.go` template) and a fixed `AddPlugin` (relative `go work use`), then `generate()`, then git. Put the bootstrap script in a new `modules//new.sh` embedded by that package, and name all script env overrides `SUMMERCMS_*` (never `SUMMER_*`). + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Name validation, target dir guard | CLI (`cmd/summer` → `internal/build`) | — | Public CLI contract (D-02); lives next to `ValidatePluginID` | +| Framework checkout detection, relative replace | CLI (`internal/build/new.go`) | Bootstrap passes `--framework` | Filesystem-only logic; reuse `parseGoMod` | +| Starter plugin + go.work | CLI (existing `MakePlugin`/`AddPlugin`) | — | D-05 "same code paths" | +| App key generation | Framework module `modules/lagoon` (exported helper) | CLI calls it | D-08: reuse, do not copy | +| Bootstrap (clone/update, install, run) | Shell script embedded in framework package | — | Versioned with the tag (D-13) | +| Serving `/new` | Site plugin (`sm-summercmsio-plugin`) route | nginx (unchanged passthrough) | D-13; nginx already proxies GET/HEAD | +| Local DB | `compose.yaml` (Docker) | `.env` DSN | D-09 | +| Docs/llms | `docs/` source; llms built by `docs:build` at site build time | site build.sh | llms files are build output, not committed | + +## Standard Stack + +### Core (all already in the repo; no new dependencies) +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| `text/template` + `embed` | stdlib (Go 1.27.0 local) | Render app files from `internal/build/stubs/*.tmpl` | Existing stub mechanism `stubTmpl = template.Must(template.ParseFS(stubFS, "stubs/*.tmpl"))` [VERIFIED: internal/build/scaffold.go:20-23] | +| `os/exec` (`go`, `git`) | stdlib | `go mod tidy`, `go mod edit`, `go work use`, `git init/commit` | Existing `runGo`/`applyGoWork` helpers [VERIFIED: internal/build/scaffold.go:508-541] | +| `runtime.Caller` / `runtime/debug` | stdlib | Optional framework source-location candidate (D-04) | Verified behaviour below | +| `go/token.IsKeyword` | stdlib | Reject Go keywords as `` | See Pitfall 3 | +| `modules/bonfire` | in-repo | `summer new` command definition (Args, Flags with `Bare`) | `type Flag struct { Name, Shorthand, Description, Default string; Bare bool; Repeatable bool }` [VERIFIED: modules/bonfire/command.go:24-38] | +| `modules/lagoon` | in-repo | Key generation helper to export | [VERIFIED: modules/lagoon/keygen.go:13-26] | + +### Supporting (external tools, not Go deps) +| Tool | Version found | Purpose | +|------|---------------|---------| +| shellcheck | 0.11.0 on host [VERIFIED: `shellcheck --version`] | Lint `new.sh` in a test (skip when absent) | +| bash | 5.3.15 host, 5.2.15 in node:24-bookworm [VERIFIED] | Script runtime | +| git | 2.55.0 host, 2.39.5 in node:24-bookworm [VERIFIED] | `init -b`, `ls-remote --sort` both supported | +| Docker + compose | 29.7.2 / Compose 5.5.0 host [VERIFIED] | compose.yaml, acceptance | +| postgres:16, node:24-bookworm images | present locally [VERIFIED: `docker images`] | DB + acceptance base | + +**Installation:** none (no `go get`). + +## Package Legitimacy Audit + +No external packages are installed by this phase: no new Go modules, no npm packages. The acceptance container downloads the official Go tarball (`https://go.dev/dl/go1.27.0.linux-amd64.tar.gz`, which redirects to `dl.google.com` [VERIFIED: curl -sI]) and the Docker CLI/compose plugin from Docker's official distribution. + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| (none) | — | — | — | — | — | — | + +**Packages removed due to [SLOP] verdict:** none +**Packages flagged as suspicious [SUS]:** none + +## Verified Internals (questions 1-6, 11) + +### Q1. make:plugin / plugin:add internals and what `summer new` calls + +- `func MakePlugin(ctx context.Context, startDir, id string) (string, error)` — validates the ID, `findAppDir(startDir)` (walks up for `summer.yaml`), creates `plugins/` (refuses if it exists), reads `summer.yaml` for `m.Module`, sets plugin module `m.Module + "/plugins/" + name`, copies the app's framework `replace` rewritten relative to the plugin dir, writes the scaffold, runs `go mod tidy` in the plugin dir, appends `toolchain go1.27.0`. [VERIFIED: internal/build/scaffold.go:51-103] +- `func AddPlugin(ctx context.Context, startDir, pluginDir string) error` — resolves the plugin dir, reads its module path and ID from `plugin.go`, appends `{ID, Module}` to `summer.yaml`, `go mod edit -require=@v0.0.0` and `-replace==./plugins/` on the app `go.mod`, then `ensureGoWork` (creates `go.work` in the app root if none is found upward, then `go work use`). [VERIFIED: internal/build/scaffold.go:105-173, 318-352] +- Both already take a directory parameter, so **no cwd refactor is needed**; `cmd/summer` passes `os.Getwd()` [VERIFIED: cmd/summer/main.go:62-90, 282-298]. +- Scaffold layout written: `plugin.go`, `routes.go`, `registry.gen.go`, `go.mod` (+`go.sum` from tidy), leaf packages `models classes controllers console jobs middleware updates` (each `doc.go`), assets `config/config.yaml` (`{}`), `lang/en/lang.yaml` (`{}`), `views/mail/welcome.htm`. [VERIFIED: internal/build/scaffold.go:25-33, 180-231] +- **The stub registers no route:** `{{define "routes.go"}}... func (p *Plugin) Routes(pact.Router) error { return nil }` [VERIFIED: internal/build/stubs/plugin.tmpl:63-70]. The plugin `go.mod` stub is `require {{.Framework}} v0.0.0` + optional `replace {{.Framework}} => {{.Replace}}` [VERIFIED: internal/build/stubs/plugin.tmpl:72-81]. +- Constants: `frameworkModule = "git.golem15.com/golem15/summercms"`, `goVersionLine = "go 1.27.0"`, `toolchainLine = "toolchain go1.27.0"` [VERIFIED: internal/build/scaffold.go:45-49]. +- `generate(appDir string, m Manifest) error` writes `main.go` and `plugins.gen.go` via `writeIfChanged` with no compile [VERIFIED: internal/build/build.go:62-78] — `summer new` (same package) can call it so the first commit contains the generated files. +- `SaveManifest` writes `module:`, `binary:`, `plugins:` with entries ` - id:` / ` module:` [VERIFIED: internal/build/manifest.go:261-287]; with zero plugins it writes `plugins:` (null) which `LoadManifest` accepts (hello tests use it). + +**Refactors required:** +1. **AddPlugin writes an absolute path into `go.work`.** Empirically, `summer plugin:add plugins/demo` produced: + ``` + use ( + . + /tmp/.../scratchpad/sib/sm-demo-app/plugins/demo + ) + ``` + because `runGoWorkUse(ctx, appDir, gw, pluginDir)` is called with the absolute `pluginDir` [VERIFIED: internal/build/scaffold.go:351, 362-371; empirical run]. Running `go work use ./plugins/demo` from the app dir writes `./plugins/demo` [VERIFIED: empirical]. Fix: compute `rel, _ := filepath.Rel(filepath.Dir(goWork), pluginDir)` and prefix `./` (same logic as `editAppModule` lines 322-329), also in the `ensureGoWork` loop for existing plugins (line 344-346). This changes `plugin:add` output for existing users from absolute to relative (an improvement; `TestMakeAndAddPluginOnHelloCopy` only checks `bytes.Contains(work, "plugins/demo")` [VERIFIED: internal/build/build_test.go:268-274]). Update the `go-modules-and-workspaces.md` text if it describes the path. +2. **Starter route option.** Add a field to `stubData` (e.g. `StarterRoute bool`) and an `{{if .StarterRoute}}` branch in the existing `routes.go` define; add an unexported `makePlugin(ctx, dir, id, opts)` that `MakePlugin` wraps with zero options. This keeps one template (D-05) and leaves `make:plugin` output unchanged (two `make:plugin` plugins would otherwise collide on the same path). + +### Q2. Does an empty `plugins: []` build and serve? — YES + +Scratch app: `go.mod` (module + `require summercms v0.0.0` + relative replace), `summer.yaml` with `plugins: []`, no `go.work`: +``` +$ summer build +built empty in 1.947s +$ ./bin/empty route:list +surf: config http.body_limits.default_bytes is required # before config/http.yaml +$ ./bin/empty route:list # after config/http.yaml +Method Pattern Plugin Middleware Raw +``` +`plugins.gen.go` became `var PluginIDs = []string{}` with no import block [VERIFIED: empirical; internal/build/build.go:139-157]. With DSN + key + bucket set, `serve` started (`job worker started on queues: default, scheduled`, `listening on 127.0.0.1:18731`) and returned 404 for `/` (no routes). The same app with its own `go.work` (`use .`) also builds (workspace mode writes `go.work.sum` automatically) [VERIFIED: empirical]. **Decision input for D-06: add `--no-plugin`; no pipeline fix needed.** + +### Q3. Minimal complete config — verified against PostgreSQL 16 + +Failure sequence observed on the empty app (each error appeared until the next item was added) [VERIFIED: empirical]: +1. `route:list`/`serve`: `surf: config http.body_limits.default_bytes is required` — from `requiredBytes(app, "http.body_limits.default_bytes")` and `"http.body_limits.upload_bytes"` [VERIFIED: modules/surf/router.go:491-498]. +2. `migrate`/`serve`: `lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN)` [VERIFIED: modules/lagoon/connection.go:25]. +3. `migrate`/`serve`: `lagoon: app.key is empty or invalid (set SUMMER_APP__KEY to a 32-byte base64 value)` — `decodeAppKey` requires StdEncoding base64 of exactly `encryptedKeySize = 32` bytes [VERIFIED: modules/lagoon/encrypted.go:23, 197-207]. +4. `serve` only: `attach: storage.uploads.bucket_url is empty (set SUMMER_STORAGE__UPLOADS__BUCKET_URL)` [VERIFIED: modules/lagoon/attach/bucket.go:66-69]. `file://` URLs get `create_dir=true` added automatically [VERIFIED: modules/lagoon/attach/bucket.go:47-53]; `file://./storage/app/uploads` created `storage/app/uploads` relative to the working dir [VERIFIED: empirical]. +5. Admin JWT secret is **not** required: `cabana.Activate` returns early when no plugin has admin controllers ("No controllers means no admin routes and no secret check") [VERIFIED: modules/cabana/http.go:73-84]. + +Then `summer migrate` → `✓ migrations applied`; `summer serve` → `GET /` 200 `{"message":"root","plugin":"golem15.demo"}`, `GET /api/demo/hello` 200, `/nope` 404 [VERIFIED: empirical, starter plugin with routes `GET /{$}` and `GET /api/demo/hello`]. + +**Env mapping:** prefix `const envPrefix = "SUMMER_"`; `__` splits path segments, segments lower-cased (`SUMMER_DATABASE__DSN` → `database.dsn`); `SUMMER_ENV` is skipped and selects the environment (default `"production"`) [VERIFIED: modules/compass/env.go:13, 31-44; modules/compass/config.go:187-199]. The `.env` file is read from `filepath.Join(filepath.Dir(c.dir), ".env")`, i.e. **next to `config/`** (app root); real environment variables win over `.env`; values may be wrapped in single or double quotes, `export ` prefix allowed, `#` comment lines skipped [VERIFIED: modules/compass/config.go:187; modules/compass/env.go:46-98]. Env values are strings — `body_limits` must therefore be YAML numbers in `config/http.yaml` (existing app comment says the same) [VERIFIED: ../sm-summercmsio-app/config/http.yaml]. + +**Recommended generated file set** (each verified to be sufficient): + +| File | Content | Required by | +|------|---------|-------------| +| `go.mod` | `module git.golem15.com/golem15/sm--app`, `go 1.27.0`, `toolchain go1.27.0`, `require git.golem15.com/golem15/summercms v0.0.0`, `replace … => ../summercms` (relative) | build | +| `go.work` | `go 1.27.0` / `toolchain go1.27.0` / `use ( . ./plugins/ )` — relative | build (workspace) | +| `summer.yaml` | `module:`, `binary: `, `plugins:` | build | +| `main.go`, `plugins.gen.go` | from `generate()` | committed (see Q5) | +| `config/app.yaml` | `name: `, `debug: false`, `key: ""` with a comment pointing at `SUMMER_APP__KEY` | documentation only | +| `config/http.yaml` | `body_limits: {default_bytes: 1048576, upload_bytes: 10485760}` | serve, route:list | +| `config/storage.yaml` | `uploads: {bucket_url: "file://./storage/app/uploads", public_path_prefix: "/storage/uploads"}` | serve | +| `config/database.yaml` | `dsn: ""` + comment pointing at `SUMMER_DATABASE__DSN` | documentation only | +| `.env` (mode 0600) | `SUMMER_DATABASE__DSN=postgres://…@127.0.0.1:/?sslmode=disable`, `SUMMER_APP__KEY=` | migrate, serve | +| `.env.example` | same keys, placeholder key, plus commented `SUMMER_STORAGE__UPLOADS__BUCKET_URL` and `SUMMER_ENV` | docs | +| `compose.yaml` | `postgres:16`, credentials = DSN, port bound to `127.0.0.1`, named volume, `pg_isready` healthcheck | D-09 | +| `.gitignore`, `CLAUDE.md`, `AGENTS.md`, `plugins//…` | see Q5 / D-10 | | + +Optional `config/queue.yaml` is not needed: with no setting the in-process River worker starts in `serve` and works after `migrate` [VERIFIED: empirical log `River client started`]. + +**`examples/hello` gap (D-07):** confirmed — `go test ./...` in `examples/hello` fails today: `--- FAIL: TestTypedItemRoute ... surf: config http.body_limits.default_bytes is required` [VERIFIED: empirical]. Adding `examples/hello/config/http.yaml` with the same `body_limits` fixes it (hello tests load `compass.Load("config")`). The docs warning about a stale `examples/hello/main.go` is **obsolete**: a fresh `summer build` of a copy produced a byte-identical `main.go` [VERIFIED: empirical `diff` empty] — remove both bullets of the "Known issues" warning in `installation.md` when D-07 lands. + +### Q4. key:generate reuse + +```go +// modules/lagoon/keygen.go:13-26 +func KeyGenerateCommand() bonfire.Command { + ... + Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { + key := make([]byte, encryptedKeySize) + if _, err := rand.Read(key); err != nil { + return err + } + out.Success(base64.StdEncoding.EncodeToString(key)) + return nil + }, +``` +[VERIFIED: modules/lagoon/keygen.go:13-26]. **Format:** `base64.StdEncoding` of 32 random bytes = 44 chars (with `=` padding). **Pitfall:** `out.Success` prefixes a check mark — `./bin/app key:generate | od -c` started with `342 234 223` (`✓`) and a space; `export SUMMER_APP__KEY=$(./bin/app key:generate)` produced a 46-char value that `decodeAppKey` rejects [VERIFIED: empirical]. Recommendation: export `func GenerateAppKey() (string, error)` in `keygen.go`, make `KeyGenerateCommand` call it, and have `cmd/summer` call it and pass the key into `build.NewOptions` (keeps `internal/build` free of a lagoon import; `cmd/summer` already imports lagoon [VERIFIED: cmd/summer/docs.go:18]). Update `modules/lagoon/README.md` API table (it already lists `lagoon.KeyGenerateCommand` at line 150) [VERIFIED: grep modules/lagoon/README.md:147-150]. + +### Q5. What to gitignore + +Both hand-built apps use the identical `.gitignore`: `/bin/`, `/tmp/`, `*.exe`, `go.work.sum`, `.env`, `/storage/` and **commit** `main.go`, `plugins.gen.go`, `go.work` [VERIFIED: ../sm-bm-app/.gitignore, ../sm-summercmsio-app/.gitignore, `git ls-files`]. The site's `CLAUDE.md` states `main.go` and `plugins.gen.go` are "generated and committed". The framework itself ignores `go.work.sum` and `.env*` except `.env.example`. + +Recommendation: generated `.gitignore` = `/bin/`, `/tmp/`, `/storage/`, `.env`, `go.work.sum` (+ `*.exe`). Commit `go.work`, `main.go`, `plugins.gen.go`, plugin `go.sum`. **Verified:** after committing with that `.gitignore`, a fresh `git clone` into another sibling dir ran `summer build` successfully and left `git status` clean (`go.work.sum` regenerated, ignored) [VERIFIED: empirical]. Note the app root has no `go.sum` (only `go.work.sum`) — fine in workspace mode. + +### Q6. Framework detection and relative replace + +- Read a candidate's module path with the existing `parseGoMod(path) (module string, replaces map[string]string, err error)` [VERIFIED: internal/build/scaffold.go:399-440]; compare to `frameworkModule`. +- Relative replace: `filepath.Rel(appDir, frameworkAbs)` on **lexical** absolute paths for both sides (do not `EvalSymlinks` one side only); `filepath.ToSlash`; Go requires local replace paths to start with `./` or `../` — reuse the prefix logic from `editAppModule` [VERIFIED: internal/build/scaffold.go:322-329]. A symlinked sibling (`sib/summercms -> framework`) with `replace => ../summercms` built fine; the plugin got `../../../summercms` and the workspace accepted both replaces (same target) [VERIFIED: empirical]. +- **Binary source location (D-04 optional candidate): cheap and reliable enough as a last candidate.** `runtime.Caller(0)` inside `internal/build` returns the absolute compile-time path when built without `-trimpath` (the scratch `summer` binary embeds `/media/nvme/.../summercms.go/internal/build/scaffold.go`); with `-trimpath` it returns a module-relative path (`example.com/callertest/main.go`), so guard with `filepath.IsAbs` and always confirm `go.mod`'s module path at `/../../go.mod` [VERIFIED: empirical]. `debug.ReadBuildInfo()` gives no directory, only `Main.Path`/`Main.Version`; a build inside the checkout carries a VCS version such as `v0.1.13-0.20261007223035-d94f804dddd6+dirty` [VERIFIED: `go version -m`]. Order: `--framework` > siblings > binary source > error naming `--framework`. +- Recommended sibling candidates: `/summercms`, `/summercms.go`; optionally scan all immediate subdirectories of the parent in sorted order (one `go.mod` read each). Refuse when the target would be created **inside** the framework checkout. +- Require version: use `v0.0.0` (consistent with the plugin stub and `examples/hello`); a release tag from `ReadBuildInfo` is a possible refinement but not needed for building because the replace wins. + +### Q11. Test conventions + +- Real `go build` tests in `internal/build` are **not** `-short` gated today (`TestMakeAndAddPluginOnHelloCopy`, `TestScaffoldPluginSmoke` run `MakePlugin`+`AddPlugin`+`App`) [VERIFIED: internal/build/build_test.go:226-325, 423-494]; the whole package runs in ~17s [VERIFIED: `go test ./internal/build/` 17.3s]. `-short` gating with `t.Skip("requires testcontainers postgres")` is the convention for Docker-dependent tests [VERIFIED: modules/lagoon/postgres_test.go:91-93]. +- No golden-file infrastructure exists anywhere (no `-update` flag, no `testdata/*.golden`) [VERIFIED: grep]. Plan 4 introduces it: `internal/build/testdata/new/.golden` + `var update = flag.Bool("update", false, "rewrite golden files")`. +- `cmd/summer` tests drive commands through `bonfire.NewRoot("summer", toolCommands(), &buf)`, `t.Chdir`, and a `copyHelloApp` helper that rewrites the framework replace to an absolute path [VERIFIED: cmd/summer/main_test.go:102-218]. `TestToolCommandNames` lists required tool commands — add `"new"` there and a `helpWants` entry for its flags [VERIFIED: cmd/summer/main_test.go:17-70]. +- Phase gate scripts live in `scripts/check-phase.sh` with `set -euo pipefail`, `PHASE_*` env overrides and `go -C` for sibling repos [VERIFIED: scripts/check-phase14.2.1.sh:1-40]. Lean mode: optional, but the container acceptance (D-20) fits this convention as `scripts/check-phase14.3.sh`. + +## Architecture Patterns + +### System Architecture Diagram + +``` +user shell + │ curl -fsSL https://summercms.io/new | bash -s -- demo + ▼ +nginx (GET/HEAD passthrough) ──► summercms-io binary ──► site plugin GET /new + │ bytes = .Script() (embedded new.sh at tag v0.1.13) + ◄──────────── text/plain; charset=utf-8 ────────────────┘ +bash: main "$@" + ├─ check: go ≥ 1.27 (fail) · docker (warn) · git (fail) · name regex (fail) + ├─ framework dir ($SUMMERCMS_DIR or ./summercms) + │ ├─ absent → git clone $SUMMERCMS_REPO → checkout latest vX.Y.Z (or $SUMMERCMS_REF) + │ └─ present → verify go.mod module · clean · detached · fetch --tags · ff-only to newest tag, else refuse + ├─ go -C install ./cmd/summer → $GOBIN or $(first GOPATH)/bin (PATH hint if missing) + ├─ "$gobin/summer" new demo --framework (cwd = parent of ) + │ └─ internal/build.NewApp + │ ├─ validate name / refuse existing sm-demo-app + │ ├─ resolve framework (flag > siblings > binary source) → relative replace + │ ├─ write go.mod, go.work, summer.yaml, config/, .env(0600), .env.example, compose.yaml, .gitignore, CLAUDE.md, AGENTS.md + │ ├─ makePlugin(golem15.demo, StarterRoute) → go mod tidy (network/module cache) + │ ├─ AddPlugin (relative go work use) + │ ├─ generate() → main.go, plugins.gen.go + │ └─ git init + commit (fallback identity) unless --no-git + └─ print next steps +then: cd sm-demo-app → docker compose up -d --wait → summer build → summer migrate → summer serve → curl :8080/ +``` + +### Recommended Project Structure + +``` +internal/build/ +├── new.go # NewOptions, NewApp, validateAppName, findFramework, planNewApp (pure) + executor +├── stubs/new.tmpl # {{define "new/go.mod"}}, "new/compose.yaml", "new/CLAUDE.md", "new/config/http.yaml", ... (flat: embed pattern is stubs/*.tmpl) +├── stubs/plugin.tmpl # routes.go gains {{if .StarterRoute}} +└── testdata/new/*.golden (plan 4) +cmd/summer/main.go # newCommand(): Args name; Flags framework, no-git, no-plugin (Bare) +modules/lagoon/keygen.go # + GenerateAppKey() (string, error) +modules// # NEW exported package (name is planner's call, e.g. "sunrise") +├── new.sh # the bootstrap script (single source of truth) +├── .go # //go:embed new.sh ; func Script() []byte +├── _test.go +└── README.md # standard module README + row in root README.md +examples/hello/config/http.yaml # D-07 gap fix +``` + +`//go:embed` patterns cannot contain `..`, so `scripts/new.sh` cannot be embedded from `modules/` [CITED: pkg.go.dev/embed — "Patterns may not contain '.' or '..' path elements"; ASSUMED wording]. Precedent for "the embedded file lives in the package dir": `modules/boardwalk` uses `//go:embed all:dist` on its own `dist/` [VERIFIED: modules/boardwalk/boardwalk.go:24]. The root README describes `scripts/` as "Verification scripts used as phase gates" [VERIFIED: README.md:79], another reason to keep `new.sh` in the package. + +### Pattern 1: Pure plan + executor +**What:** `planNewApp(opts) ([]genFile, error)` renders every file `summer new` writes itself (no I/O beyond reading the framework `go.mod`); `NewApp` writes them, then runs the go/git steps. +**Why:** plan 4 golden tests render without network or `go mod tidy`, and the deterministic files (everything except `.env`'s key and the plugin `go.sum`) can be compared byte for byte. + +### Pattern 2: Write `go.work` before `makePlugin` +`findGoWork` walks **up** from the app dir [VERIFIED: internal/build/scaffold.go:493-506]; if an ancestor directory has a `go.work`, `make:plugin`'s `go mod tidy` and `plugin:add` would use it. Writing the app's own `go.work` (`use .`) first isolates the new app; `make:plugin` with a pre-existing app `go.work` succeeded [VERIFIED: empirical], and `ensureGoWork` then just runs `go work use` [VERIFIED: internal/build/scaffold.go:336-352]. This also gives `--no-plugin` apps a consistent layout (a `use .`-only workspace builds [VERIFIED: empirical]). + +### Pattern 3: Starter route +```go +// routes.go, rendered by the existing "routes.go" define when StarterRoute is true (verified to work through surf) +func (p *Plugin) Routes(r pact.Router) error { + r.Get("/{$}", func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"plugin": p.ID(), "message": "Hello from SummerCMS"}) + }) + return nil +} +``` +`pact.Router` has `Get(path string, handler http.HandlerFunc, middleware ...string)` [VERIFIED: modules/pact/capabilities.go:64-74]. `GET /{$}` (exact root, Go ServeMux syntax) appeared in `route:list` as `GET /{$} golem15.demo false` and answered 200 while `/nope` stayed 404 [VERIFIED: empirical]. An extra namespaced route (e.g. `/api//hello`) is optional. + +### Pattern 4: `summer new` command wiring +```go +bonfire.Command{ + Name: "new", Description: "Create a SummerCMS application in sm--app", + Args: []bonfire.Arg{{Name: "name", Description: "Lower-case letters and digits, starting with a letter", Required: true}}, + Flags: []bonfire.Flag{ + {Name: "framework", Description: "Path to the framework checkout"}, + {Name: "no-plugin", Description: "Do not create the starter plugin", Bare: true}, + {Name: "no-git", Description: "Do not initialise a git repository", Bare: true}, + }, + Run: ..., // os.Getwd() as parent; lagoon.GenerateAppKey(); build.NewApp(ctx, opts, out) +} +``` +`flagTrue(in, name)` already parses Bare flags [VERIFIED: cmd/summer/main.go:240-251]. Adding the command to `toolCommands()` automatically allows `summer new` in docs (the docs checker's tool list is derived from `toolCommands()`) [VERIFIED: cmd/summer/docs.go:144-148]. + +### Anti-Patterns to Avoid +- **A second plugin template for the starter plugin** — violates D-05; use the `StarterRoute` option. +- **Running `go mod tidy` on the app before `main.go`/`plugins.gen.go` exist** — tidy would drop the plugin `require` added by `plugin:add` (nothing imports it yet). If the app should get a `go.sum`, run tidy only after `generate()`. Not required for building. +- **Capturing `key:generate` output in shell** (`$(bin/app key:generate)`) — includes `✓ `. +- **`summer serve --addr …` in docs/next steps** — the delegating `summer serve` declares no flags: `unknown flag: --addr`; `summer serve -- --addr X` or `./bin/ serve --addr X` work [VERIFIED: empirical; cmd/summer/runtime.go:16-26]. +- **Naming script env overrides `SUMMER_*`** — compass would read them as config keys, and `terminalEnv` in `terminal_check_test.go` strips every `SUMMER_*` variable [VERIFIED: ../sm-summercmsio-app/terminal_check_test.go:66-88]. Use `SUMMERCMS_*` (matches the site's existing `SUMMERCMS_FRAMEWORK`, `SUMMERCMS_CLONE_URL`). + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Plugin scaffold | new templates | `MakePlugin`/`AddPlugin` (+option) | D-05; byte-stable existing behaviour | +| `go.mod`/`go.work` edits | string editing | `go mod edit`, `go work use` via `runGo`/`runGoWorkUse` | Go validates syntax | +| `main.go` generation | copy of generator | `generate(appDir, m)` | single source | +| App key | new random code | exported `lagoon.GenerateAppKey` | D-08 | +| Go keyword check | hand list | `go/token.IsKeyword` | complete, stdlib | +| Module path read | regex | `parseGoMod` | existing helper | +| ETag / headers for `/new` | new scheme | site plugin's `file{body, etag, ctype}` + `setCommonHeaders` + `cacheNoCache` | consistent with every other site response [VERIFIED: plugins/golem15/summercms/static.go:17-23, 140-147, 187-221] | + +## Common Pitfalls + +### Pitfall 1: Absolute paths in the committed go.work +**What goes wrong:** generated app works on the author's machine only. **Why:** `AddPlugin` passes an absolute dir to `go work use`. **Avoid:** relative arg (see Q1 refactor 1). **Warning sign:** `/` at the start of a `use` line in the golden `go.work`. + +### Pitfall 2: Ancestor `go.work` capture +Creating the app under a directory tree with a `go.work` would route `go mod tidy`/`go build` through that workspace. **Avoid:** write the app `go.work` first (Pattern 2); refuse a target inside the framework checkout. + +### Pitfall 3: Names that pass `^[a-z][a-z0-9]*$` but break the build +Verified with `make:plugin` + `plugin:add` + `build` [VERIFIED: empirical]: +- `main` → `import ".../plugins/main" is a program, not an importable package` +- `internal` → `use of internal package .../plugins/internal not allowed` +- `go` (any Go keyword) → stub render fails (`package go` does not parse) +- `init`, `string`, `vendor`, `testdata` → built fine. +**Avoid:** `summer new` additionally rejects `token.IsKeyword(name)`, `main`, `internal`, with a message naming the rule. This is stricter than D-02's regex; it is a necessary refinement of the costly contract — surface to the user in the plan checkpoint. (`make:plugin` has the same latent issue; out of scope unless trivial.) + +### Pitfall 4: Partial directory blocks a re-run +`summer new` refuses an existing `sm--app` (D-02), so a failure in `go mod tidy` (e.g. offline) would leave a directory that blocks retry. **Avoid:** on error, `os.RemoveAll` the target **only** because it did not exist before this run; print the cause. + +### Pitfall 5: git commit in a fresh container/CI +`node:24-bookworm` runs as root with no git identity [VERIFIED: `id -u` → 0]. **Avoid:** when `git config user.name`/`user.email` are unset (and `GIT_AUTHOR_*` absent), run the first commit as `git -c user.name=SummerCMS -c user.email=summer@localhost commit ...` [VERIFIED: empirical commit with `HOME=` succeeded]. Run git with stdin closed; treat a commit failure (e.g. `commit.gpgsign=true` with an unavailable key) as a warning, not a fatal error — the app is still usable. Use `git init -b main` (git ≥ 2.28; bookworm has 2.39.5). + +### Pitfall 6: pipefail + `head` in the script +`git tag … | grep … | head -1` under `set -o pipefail` can fail with SIGPIPE (exit 141). **Avoid:** capture the whole list into a variable (`tags=$(git tag -l 'v*' --sort=-v:refname)`) and pick the first exact `^v[0-9]+\.[0-9]+\.[0-9]+$` match in a bash loop. [ASSUMED: general bash behaviour; not reproduced here] + +### Pitfall 7: hanging git credential prompts / stdin +With `curl | bash`, stdin is the script pipe. **Avoid:** `export GIT_TERMINAL_PROMPT=0`; run children with `` is the host's loopback, so the acceptance container needs `--network host`; (b) bind mounts in `compose.yaml` would resolve host paths — use a **named volume** only; (c) the compose project name defaults to the directory name `sm-demo-app` and would collide with a real host project of that name — set a unique `COMPOSE_PROJECT_NAME` (or demo name) and `docker compose down -v` in cleanup; (d) the host port must be free. [ASSUMED: standard Docker behaviour] + +### Pitfall 12: Cloning a host checkout inside a root container +`git clone /fw x` with a host-owned bind mount fails: `fatal: detected dubious ownership in repository at '/fw'` [VERIFIED: empirical in node:24-bookworm]. **Avoid:** `git config --global --add safe.directory '*'` in the throwaway container, or clone from a bare mirror created inside the container. The clone only sees **committed** HEAD and tags. + +### Pitfall 13: Tags vs. testing before v0.1.13 +Before `v0.1.13` exists, "latest tag" = `v0.1.12`, which has no `summer new`. **Avoid:** a `SUMMERCMS_REF` override (tag or commit, skips latest-tag selection) for pre-tag acceptance; optionally exercise tag selection with a bare mirror + throwaway tag (e.g. `v9.9.9`) in the gated script test. + +### Pitfall 14: Release-build coupling on the site +`scripts/build.sh` (release) takes the framework version from the app's `go.mod` (`go list -m -f '{{.Version}}' git.golem15.com/golem15/summercms`) and refuses if that tag is missing [VERIFIED: ../sm-summercmsio-app/scripts/build.sh:56-66]. The app requires `v0.1.12` and the site plugin `require git.golem15.com/golem15/summercms v0.1.12` [VERIFIED: grep go.mod files]. Serving `.Script()` needs both bumped to `v0.1.13` **after** the tag exists; the docs embedded in the site are also built from that tag. Therefore every framework change of this phase (generator, script, docs, tests) must land before tagging `v0.1.13`; the site require bump and `/new` route go last; the site plugin can be developed against the local framework with `scripts/build.sh dev`. + +## Bootstrap script design (Q10) + +Reference read (pattern only): msd-core `scripts/bootstrap.sh` — `main() { set -euo pipefail; ... }` / `main "$@"`, env overrides `MSD_REPO/MSD_REF/MSD_HOME`, `--help` heredoc, refuses update when `git status --porcelain --untracked-files=no` is non-empty, `say()` helper with colored `==>` [VERIFIED: read from a scratch clone of https://git.golem15.com/golem15/msd-core, branch `next`; clone deleted afterwards]. Its update step does `fetch` + `checkout --detach FETCH_HEAD` (not fast-forward checked) — this phase needs a stricter rule (D-16). + +Recommended semantics (D-16 settled): + +| State of `$dir` | Action | +|-----------------|--------| +| does not exist | `git clone --quiet "$repo" "$dir"`; pick tag; `git -c advice.detachedHead=false -C "$dir" checkout --quiet --detach "refs/tags/$tag"` | +| exists, not a git repo or `go.mod` module ≠ `git.golem15.com/golem15/summercms` | refuse: "… is not a SummerCMS framework checkout; set SUMMERCMS_DIR" | +| tracked local changes (`git status --porcelain --untracked-files=no` non-empty) | refuse, do not reset | +| HEAD on a branch (`git symbolic-ref -q HEAD` succeeds) | refuse: "on branch X; the installer manages a detached release checkout — use SUMMERCMS_DIR for a separate clone, or run `summer new --framework` yourself" | +| detached | `git fetch --quiet --tags origin` (no `--force`: git refuses to clobber a moved tag); pick newest tag; if `HEAD` = tag commit → nothing; elif `git merge-base --is-ancestor HEAD "refs/tags/$tag"` → checkout tag; else refuse (not a fast-forward) | + +- Tag discovery on fresh clone can also use `git ls-remote --tags --refs --sort=-v:refname "$repo" 'v*'` (verified against the public remote: newest `refs/tags/v0.1.12`) [VERIFIED: empirical]; the public HTTPS remote answers without credentials [VERIFIED: `git ls-remote`]. Full clone is small: packs 10.5 MiB, 1000 commits [VERIFIED: `git count-objects -vH`]; shallow clones complicate the ancestry check — use a full clone. +- Env overrides (D-17): `SUMMERCMS_REPO` (default `https://git.golem15.com/golem15/summercms`), `SUMMERCMS_DIR` (default `./summercms`, relative to cwd), `SUMMERCMS_REF` (pin a tag/commit; skips "latest tag"). +- Always call `summer new "$name" --framework "$dir"` from `$(dirname "$dir")`-equivalent cwd (explicit beats detection; detection still works for manual use). +- Validate `$name` in the script with the same regex for an early error before cloning; also fail early if `sm-$name-app` already exists in cwd. +- Docker: `command -v docker` and `docker compose version` → warning only. +- Next steps printed (by the script or by `summer new`, not both): `cd sm--app`, `docker compose up -d --wait`, `summer build`, `summer migrate`, `summer serve`, then `curl http://127.0.0.1:8080/`. + +### Container acceptance (D-20) +`node:24-bookworm` has git 2.39.5, bash 5.2.15, curl; **no** jq, docker or go; runs as root [VERIFIED: docker run]. Host Docker CLI is dynamically linked (do not bind-mount it) [VERIFIED: `file /usr/bin/docker`]. Recommended: `docker run --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -v :/fw:ro node:24-bookworm bash -c '...'` that installs jq (`apt-get install -y jq`), the Go 1.27.0 tarball from go.dev, and the Docker CLI + compose plugin (Docker static binaries or Docker's apt repo), sets `safe.directory`, serves/reads the script (`bash /fw/modules//new.sh demo` or `python3 -m http.server`/`curl file://`), sets `SUMMERCMS_REPO=/fw SUMMERCMS_REF=`, then runs the D-20 chain with `summer serve &` and a curl retry loop on the starter route, then `docker compose down -v`. go1.27.0 tarball exists (302 to dl.google.com); current Go is 1.27.1 [VERIFIED: curl go.dev/dl]. [ASSUMED: exact Docker CLI install method — pick one in the plan] + +## Site plugin, docs, landing page (Q7-Q9) + +### Q7. Site plugin routing +- `Routes` registers inside `r.GroupRaw("", nil, func(g pact.Router) { g.Get("/docs", …); g.Get("/docs/{path...}", docs.ServeHTTP); g.Get("/", site.ServeHTTP) })` [VERIFIED: plugins/golem15/summercms/plugin.go:43-62]. Add `g.Get("/new", …)` there; `GET /new` is more specific than the catch-all `GET /`, and Go's ServeMux serves HEAD for GET patterns (existing tests check HEAD on `/`) [VERIFIED: routes_test.go:36-47]. +- Plugin imports framework packages via `require git.golem15.com/golem15/summercms v0.1.12` + `replace … => ../../../../summercms.go` [VERIFIED: plugin go.mod]. Needs `v0.1.13` (Pitfall 14). +- Headers: reuse `file{body, etag, ctype}` with ctype `"text/plain; charset=utf-8"` (already the `.txt` mapping), `Cache-Control: no-cache` + strong ETag, and `setCommonHeaders` (`X-Content-Type-Options: nosniff`, `Referrer-Policy`) [VERIFIED: static.go:17-47, 140-147, 187-221]. No robots-blocking header, no CSP (site contract). +- nginx: `location ^~ /backend { return 404; }` and `location / { limit_except GET HEAD { deny all; } proxy_pass http://127.0.0.1:8095; … }`; `gzip_types` includes `text/plain` [VERIFIED: deploy/nginx/summercms.io.conf:62-83]. No nginx change needed. Today `https://summercms.io/new` returns `404 text/html` [VERIFIED: curl], so `curl -f` fails cleanly until deploy. The Nuxt app has only `app/pages/index.vue` — no `/new` collision [VERIFIED: ls]. +- Tests to extend: `TestRoutesAssemble` cases table (add `GET /new` 200 body = script, `HEAD /new`, `POST /new` 405) [VERIFIED: routes_test.go:26-69]; plugin `README.md` route table (lines 11-15). + +### Q8. Docs pipeline +- `docsite.Check(Options{Root, Commands: docsCommands()})`; `TestDocsTree` reports every problem [VERIFIED: cmd/summer/docs_test.go:24-34]. Command names in shell fences and code spans are checked: a line counts when it starts with `summer`, `go run ./cmd/summer`, `./bin/` or `bin/`; segments are split on `&&`, `||`, `;`, `|`; `--` ends flag parsing (so `summer serve -- --addr …` is not checked) [VERIFIED: internal/docsite/check_commands.go:33-34, 108-151]. `curl -fsSL https://summercms.io/new | bash -s -- demo` is not checked (neither segment starts with a known program). `docker compose …` lines are ignored. +- `llms.txt`/`llms-full.txt` are emitted by `docs:build` (`s.outputs["llms.txt"] = s.llmsTxt()`, `llms-full.txt` concatenates pages in reading order) [VERIFIED: internal/docsite/emit.go:165-166, 228-259]; they are **not committed** in the framework (`git ls-files | grep -i llms` empty; `/site/` ignored) — they are regenerated by the site build from the tag. "Regenerate" in this phase = `summer docs:build --out ` and check the one-liner and `summer new` appear in `llms-full.txt`; `TestDocsBuildRealTree` already asserts both files exist [VERIFIED: docs_test.go:52-72]. `site.yaml` `llms_notes` could gain a note about `summer new` (optional) [VERIFIED: docs/site.yaml:7-11]. +- Pages: `docs/setup/installation.md` (order 20; currently clone → `go install` → `examples/hello`; contains the obsolete "Known issues" warning at the end) [VERIFIED: file read]; `docs/architecture/go-modules-and-workspaces.md` (shows `replace … => ../summercms.go`; says `plugin:add` "adds the plugin directory to the nearest `go.work`") [VERIFIED]; `docs/console/scaffolding.md` has "Building and watching", "Creating a plugin", "Generating plugin parts" — add "Creating an application" with `summer new` [VERIFIED: headings grep]. + +### Q9. Landing page and TestTerminalCommands +- Single source: `vue-summercmsio-app/app/data/terminal.json` (3 groups, 6 commands: `git clone https://git.golem15.com/golem15/summercms`, `cd summercms`, `go install ./cmd/summer`, `cd examples/hello`, `summer build`, `./bin/hello greeter:hello`) with comment keys `terminal.getFramework|installCli|buildExample` in `i18n/locales/en.json` [VERIFIED: files read]. +- Tests that pin today's content and must change together: + - `sm-summercmsio-app/terminal_check_test.go`: `terminalCloneURL` const, `TestLoadTerminal` asserts `len(groups) != 3 || commands != 6` and first command = clone, `TestTerminalScript` verbatim list, gated `TestTerminalCommands` (`SUMMERCMS_TERMINAL_CHECK=1`, runs `bash -euo pipefail -c