docs(14.3): research phase domain
This commit is contained in:
@@ -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>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
#### Naming (Golem15 convention)
|
||||
- **D-01:** `sm-<name>-app` is an application (this phase), `sm-<name>-plugin` a standalone plugin module, `sm-<name>-theme` a Nuxt/Vue frontend (later). The generated `CLAUDE.md` states this convention.
|
||||
- **D-02:** `<name>` 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.<name>`. `summer new` refuses to overwrite an existing `sm-<name>-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-<name>-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 <path>` 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.<name>` 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 <name>` 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-<name>-theme` Nuxt scaffold.
|
||||
- Publishing `summer` as prebuilt release binaries (would remove the Go prerequisite for running the CLI, not for building apps).
|
||||
</user_constraints>
|
||||
|
||||
## 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/<pkg>/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 `<name>` | 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/<name>` (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=<mod>@v0.0.0` and `-replace=<mod>=./plugins/<name>` 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-<name>-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/<name> )` — relative | build (workspace) |
|
||||
| `summer.yaml` | `module:`, `binary: <name>`, `plugins:` | build |
|
||||
| `main.go`, `plugins.gen.go` | from `generate()` | committed (see Q5) |
|
||||
| `config/app.yaml` | `name: <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:<port>/<db>?sslmode=disable`, `SUMMER_APP__KEY=<fresh>` | 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/<name>/…` | 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 `<dir>/../../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: `<parent>/summercms`, `<parent>/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/<file>.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<N>.sh` with `set -euo pipefail`, `PHASE<N>_*` 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 = <pkg>.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 <fw> install ./cmd/summer → $GOBIN or $(first GOPATH)/bin (PATH hint if missing)
|
||||
├─ "$gobin/summer" new demo --framework <fw> (cwd = parent of <fw>)
|
||||
│ └─ 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/<pkg>/ # NEW exported package (name is planner's call, e.g. "sunrise")
|
||||
├── new.sh # the bootstrap script (single source of truth)
|
||||
├── <pkg>.go # //go:embed new.sh ; func Script() []byte
|
||||
├── <pkg>_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/<pkg>` [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/<name>/hello`) is optional.
|
||||
|
||||
### Pattern 4: `summer new` command wiring
|
||||
```go
|
||||
bonfire.Command{
|
||||
Name: "new", Description: "Create a SummerCMS application in sm-<name>-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/<app> 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-<name>-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=<empty>` 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 `</dev/null`; keep `main "$@"` as the very last line so nothing follows it in the pipe.
|
||||
|
||||
### Pitfall 8: `go env GOVERSION` formats
|
||||
Local toolchain reports `go1.27.0-X:nodwarf5` [VERIFIED: `go env GOVERSION`]; release candidates look like `go1.27rc1`; dev builds start with `devel`. Parse with `[[ $v =~ ^go([0-9]+)\.([0-9]+) ]]`; treat no match as "unknown" (warn, continue or fail — planner's call). Run it from a directory without `go.mod` so `GOTOOLCHAIN=auto` does not switch toolchains during the check.
|
||||
|
||||
### Pitfall 9: GOBIN/GOPATH
|
||||
`go install` writes to `$GOBIN`, else the **first** element of `$GOPATH` + `/bin` (GOPATH may be a list). Host: `GOBIN=/home/jin/go/bin` [VERIFIED: `go env`]. The script must invoke `summer new` via the absolute `"$gobin/summer"` and print `export PATH="$gobin:$PATH"` when `":$PATH:"` lacks it. In the root container this is `/root/go/bin`, not on PATH by default.
|
||||
|
||||
### Pitfall 10: Postgres not ready yet
|
||||
`docker compose up -d` returns before PostgreSQL accepts connections; `lagoon` opens once. **Avoid:** healthcheck in `compose.yaml` and next steps `docker compose up -d --wait` (compatible with D-15's step). [ASSUMED: `--wait` available in compose v2.1+; host has 5.5.0]
|
||||
|
||||
### Pitfall 11: Docker socket mount in the acceptance container
|
||||
With `-v /var/run/docker.sock:/var/run/docker.sock`, `docker compose up` starts the DB on the **host** daemon: (a) the published `127.0.0.1:<port>` 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 `<pkg>.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-<name>-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>:/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/<pkg>/new.sh demo` or `python3 -m http.server`/`curl file://`), sets `SUMMERCMS_REPO=/fw SUMMERCMS_REF=<commit>`, 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/<x>` or `bin/<x>`; 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 <tmp>` 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 <script>` in a temp dir with `terminalEnv` that drops `SUMMER_*`, `GOWORK`, `GOBIN` and puts a temp GOBIN first on PATH; expects `handled=true`) [VERIFIED: terminal_check_test.go:19-137, 160-190].
|
||||
- `vue-summercmsio-app/tests/terminal.test.ts`: `copyPayload` equals the six-command string, `split('\n').length === 6` [VERIFIED].
|
||||
- `vue-summercmsio-app/tests/output.test.ts`: "Get started shows the chips, the six commands…" and **"every en.json string is rendered on the page"** (`assertLeavesRendered(readIndex(), undefined, ['terminal.copied'])`) — removing a group requires removing its `en.json` key [VERIFIED: output.test.ts:190-216].
|
||||
- Plugin `links_test.go` `TestTerminalCommandsInPage` (needs `SUMMERCMS_REQUIRE_BUILD`) checks every command and comment appears in the built page [VERIFIED: links_test.go:226-260].
|
||||
- Recommended new terminal content: group 1 `curl -fsSL https://summercms.io/new | bash -s -- demo`; group 2 `cd sm-demo-app`, `docker compose up -d --wait`; group 3 `summer build`, `summer migrate`, `summer serve`. The gated runner then needs: an override for the script source (e.g. `SUMMERCMS_NEW_URL` replacing the URL in the curl command, analogous to the clone-URL rewrite), passthrough of `SUMMERCMS_REPO/REF` (they survive `terminalEnv` because they are not `SUMMER_*`), a unique `COMPOSE_PROJECT_NAME` + `docker compose down -v` cleanup, and special handling of the blocking `summer serve` (run in background, curl the starter route, assert its JSON, kill). Keep `TestLoadTerminal`'s shape assertions updated to the new counts.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Go: relative `go work use`
|
||||
```go
|
||||
// Source: derived from internal/build/scaffold.go:322-329 (editAppModule prefix logic)
|
||||
rel, err := filepath.Rel(filepath.Dir(goWork), dir)
|
||||
if err != nil { return fmt.Errorf("build: go work use: %w", err) }
|
||||
rel = filepath.ToSlash(rel)
|
||||
if rel != "." && rel != ".." && !strings.HasPrefix(rel, "../") && !strings.HasPrefix(rel, "./") {
|
||||
rel = "./" + rel
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "go", "work", "use", rel)
|
||||
cmd.Dir = filepath.Dir(goWork)
|
||||
```
|
||||
|
||||
### Go: framework source candidate
|
||||
```go
|
||||
// Verified behaviour: absolute without -trimpath, module-relative with it.
|
||||
func binarySourceFramework() (string, bool) {
|
||||
_, file, _, ok := runtime.Caller(0) // .../internal/build/new.go
|
||||
if !ok || !filepath.IsAbs(file) {
|
||||
return "", false
|
||||
}
|
||||
root := filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||
mod, _, err := parseGoMod(filepath.Join(root, "go.mod"))
|
||||
return root, err == nil && mod == frameworkModule
|
||||
}
|
||||
```
|
||||
|
||||
### Bash: skeleton
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# SummerCMS bootstrap: curl -fsSL https://summercms.io/new | bash -s -- <name>
|
||||
# Everything lives in main() so a truncated download runs nothing.
|
||||
main() {
|
||||
set -euo pipefail
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
local repo="${SUMMERCMS_REPO:-https://git.golem15.com/golem15/summercms}"
|
||||
local dir="${SUMMERCMS_DIR:-summercms}" ref="${SUMMERCMS_REF:-}"
|
||||
local name="${1:-}"
|
||||
[[ $name =~ ^[a-z][a-z0-9]*$ ]] || { echo "usage: … | bash -s -- <name> (lower-case letters and digits, starting with a letter)" >&2; exit 2; }
|
||||
local v; v="$(cd / && go env GOVERSION 2>/dev/null || true)"
|
||||
if [[ ! $v =~ ^go([0-9]+)\.([0-9]+) ]] || (( BASH_REMATCH[1] == 1 && BASH_REMATCH[2] < 27 )); then
|
||||
echo "ERROR: SummerCMS needs Go 1.27 or newer (found: ${v:-none}). Install it from https://go.dev/dl/" >&2; exit 1
|
||||
fi
|
||||
# … clone/update (table above), go -C "$dir" install ./cmd/summer </dev/null,
|
||||
# gobin=$(go env GOBIN); [[ -n $gobin ]] || { IFS=: read -r gp _ <<<"$(go env GOPATH)"; gobin="$gp/bin"; }
|
||||
# "$gobin/summer" new "$name" --framework "$dir" </dev/null
|
||||
}
|
||||
main "$@"
|
||||
```
|
||||
(Skeleton values `SUMMERCMS_REPO/DIR/REF` are recommendations, `[ASSUMED]` names.)
|
||||
|
||||
### compose.yaml shape
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_USER: summer
|
||||
POSTGRES_PASSWORD: summer
|
||||
POSTGRES_DB: <name>
|
||||
ports:
|
||||
- "127.0.0.1:<port>:5432" # loopback only
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U summer -d <name>"]
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
Values are discretionary; verified working DSN shape: `postgres://summer:summer@127.0.0.1:55439/summer?sslmode=disable` against `postgres:16` [VERIFIED: empirical]. Compose reads `.env` in the project dir for interpolation only; keep `.env` values unquoted and free of `$` (base64 keys and the DSN contain none). [ASSUMED: compose dotenv parsing details]
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Hand-written go.mod/summer.yaml/config per docs | `summer new <name>` | this phase | Installation docs and landing page lead with the one-liner |
|
||||
| Landing page: clone + install + build `examples/hello` | `curl -fsSL https://summercms.io/new \| bash -s -- demo` | this phase | `terminal.json` + four test files change |
|
||||
| `plugin:add` writes absolute `go.work` paths | relative paths | this phase | Fix existing behaviour |
|
||||
|
||||
**Deprecated/outdated:** the `installation.md` warning about a stale `examples/hello/main.go` (no longer true).
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | `//go:embed` forbids `..` path elements, so `scripts/new.sh` cannot be embedded from `modules/<pkg>` | Recommended Project Structure | Low — alternative is a Go package in `scripts/` itself |
|
||||
| A2 | `pipefail` + early-exiting `head`/`awk` can yield SIGPIPE exit 141 | Pitfall 6 | Low — the capture-to-variable approach is safe regardless |
|
||||
| A3 | `docker compose up -d --wait` waits for the healthcheck | Pitfall 10 | Low — fall back to a retry loop |
|
||||
| A4 | Docker socket mount behaviour (host network, host paths, project-name collisions) | Pitfall 11 | Medium — acceptance harness may need adjusting |
|
||||
| A5 | Script env names `SUMMERCMS_REPO/DIR/REF` and the new-module name | Bootstrap design | Low — naming only |
|
||||
| A6 | Compose `.env` interpolation tolerates the generated `.env` | compose.yaml | Low — values verified free of `$` |
|
||||
| A7 | Exact Docker CLI install method for node:24-bookworm | Container acceptance | Low |
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Denylist beyond D-02's regex (`main`, `internal`, Go keywords).** Known: they break the build (verified). Unclear: whether the user wants this stated in the public contract. Recommendation: reject them in `summer new` and mention the rule in docs and `--help`; confirm at the plan checkpoint.
|
||||
2. **Default DB port/credentials.** Recommendation: user/password `summer`/`summer`, DB `<name>`, a fixed non-5432 loopback host port (e.g. 54320–54399; optionally derived from the name to avoid clashes between several apps). Planner's call.
|
||||
3. **Where next steps are printed** (script vs `summer new`). Recommendation: `summer new` prints them (useful for manual runs); the script adds only the PATH hint.
|
||||
4. **New module name** for the embedded script (e.g. `sunrise`) — planner's call; it needs a README and root table row.
|
||||
5. **`go.sum` at the app root.** Not needed in workspace mode; running `go mod tidy` after `generate()` would add one for `GOWORK=off` builds. Recommendation: skip (matches both reference apps' reliance on `go.work`).
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Go | everything | ✓ | go1.27.0-X:nodwarf5 | — |
|
||||
| git | generator git step, bootstrap | ✓ | 2.55.0 | `--no-git` |
|
||||
| Docker + compose | compose.yaml, acceptance, gated tests | ✓ | 29.7.2 / Compose 5.5.0 | skip gated tests with `-short` |
|
||||
| postgres:16 image | migrate/serve acceptance | ✓ | local image | pulled on demand |
|
||||
| node:24-bookworm image | D-20 acceptance | ✓ | local image | — |
|
||||
| shellcheck | script lint test | ✓ | 0.11.0 | `t.Skip` when absent |
|
||||
| jq | D-20 (listed prerequisite) | ✓ host / ✗ in node:24-bookworm | 1.x | `apt-get install jq` in container |
|
||||
| Network to proxy.golang.org | `go mod tidy` in `make:plugin` | ✓ | — | warm module cache |
|
||||
| https://git.golem15.com/golem15/summercms | bootstrap clone | ✓ public | tags up to v0.1.12 | `SUMMERCMS_REPO` local path |
|
||||
|
||||
**Missing dependencies with no fallback:** none.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Go stdlib `testing` (Go 1.27.0); node `node:test` in vue-summercmsio-app |
|
||||
| Config file | none |
|
||||
| Quick run command | `go test ./internal/build/ ./cmd/summer/ ./modules/lagoon/ ./modules/<pkg>/ -short` |
|
||||
| Full suite command | `go vet ./... && go test ./...` (framework root) + `go -C examples/hello test ./...` |
|
||||
|
||||
### Per-plan commands
|
||||
|
||||
| Plan | Behaviour | Test Type | Automated Command |
|
||||
|------|-----------|-----------|-------------------|
|
||||
| 1 Generator | `summer new demo` in a temp parent with a `summercms` symlink to the repo builds | smoke (real build) | `go test ./internal/build/ -run 'TestNewApp' -count=1` |
|
||||
| 1 | `summer new` wired in CLI, help shows flags | unit | `go test ./cmd/summer/ -run 'TestToolCommandNames' -count=1` |
|
||||
| 1 | `GenerateAppKey` returns 44-char StdEncoding of 32 bytes; `key:generate` uses it | unit | `go test ./modules/lagoon/ -run 'Key' -short -count=1` |
|
||||
| 1 | plugin:add writes relative `go.work` paths | unit | `go test ./internal/build/ -run 'TestMakeAndAddPluginOnHelloCopy' -count=1` |
|
||||
| 1 | hello gap fixed | integration | `go -C examples/hello test ./... -count=1` |
|
||||
| 2 Script | shebang, `main "$@"` last line, `bash -n`, shellcheck, truncation runs nothing, name validation, tag selection on a temp repo | unit + gated e2e | `go test ./modules/<pkg>/ -count=1` ; gated: `SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/<pkg>/ -run TestBootstrapEndToEnd -count=1` |
|
||||
| 2 Site route | `GET /new` 200 `text/plain; charset=utf-8`, body = `Script()`, HEAD ok, POST 405, nosniff, ETag/no-cache | unit | `go -C ../sm-summercmsio-app/plugins/golem15/summercms test ./... -count=1` |
|
||||
| 2 Acceptance | D-20 container chain ends with starter route 200 | manual/gated | `bash scripts/check-phase14.3.sh --acceptance` (or the documented `docker run` command) |
|
||||
| 3 Docs | docs tree valid, `summer new` known command, llms-full contains one-liner | unit | `go test ./cmd/summer -run 'TestDocsTree|TestDocsBuildRealTree' -count=1` and `go run ./cmd/summer docs:build --check` |
|
||||
| 3 Landing | terminal.json shape, copy payload, page render | unit | `go -C ../sm-summercmsio-app test -run 'TestLoadTerminal|TestTerminalScript|TestTerminalEnv' .` ; `cd ../sm-summercmsio-app/vue-summercmsio-app && pnpm generate && pnpm test` |
|
||||
| 3 Landing (gated) | one-liner runs end to end | gated e2e | `SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_NEW_URL=… SUMMERCMS_REPO=… go -C ../sm-summercmsio-app test -run TestTerminalCommands .` |
|
||||
| 4 Unit tests | golden output for every rendered file; name/denylist/existing-dir/framework-detection errors; relative replace; `--no-plugin`, `--no-git`; no-identity commit; cleanup on failure; real build (`-short` skip) | unit + golden | `go test ./internal/build/ -run 'TestNew' -count=1` ; update goldens: `go test ./internal/build/ -run TestNewGolden -update` |
|
||||
|
||||
### Sampling Rate
|
||||
- **Per task commit:** quick run command above (~40s incl. real builds).
|
||||
- **Per plan:** full suite + the plan's sibling-repo command.
|
||||
- **Phase gate:** full suite green, docs check green, D-20 acceptance run recorded, before `/gsd-verify-work` and before tagging `v0.1.13`.
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] `internal/build/testdata/new/` golden files + `-update` flag (no golden infra exists yet).
|
||||
- [ ] `modules/<pkg>/` package, README, tests (new).
|
||||
- [ ] Test helper to create `<tmp>/summercms` → repo-root symlink so the generated replace is deterministic (`../summercms`).
|
||||
- [ ] Optional `scripts/check-phase14.3.sh` acceptance runner.
|
||||
|
||||
## Security Domain
|
||||
|
||||
`security_enforcement` is absent from `.planning/config.json` (treated as enabled). Plan 2 gets the security-review agent (CONTEXT discretion).
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | no | — |
|
||||
| V3 Session Management | no | — |
|
||||
| V4 Access Control | no | `/new` is public and read-only by design |
|
||||
| V5 Input Validation | yes | `<name>` regex + keyword/`main`/`internal` denylist before any path use; `--framework` resolved and verified by `go.mod` module path; all script variables quoted |
|
||||
| V6 Cryptography | yes (key gen) | `crypto/rand` via exported `lagoon.GenerateAppKey`; never hand-roll |
|
||||
| V10 Malicious Code / supply chain | yes | HTTPS only, `curl -fsSL` (fails on HTTP errors), script versioned by tag, tags fetched without `--force`, no `eval`, no `sudo` |
|
||||
| V12 Files and Resources | yes | refuse existing target; create only under cwd; `.env` mode 0600; no writes outside cwd and GOBIN |
|
||||
| V14 Configuration | yes | secrets only in `.env` (gitignored); `.env.example` placeholders; compose port bound to `127.0.0.1` |
|
||||
|
||||
### Known Threat Patterns
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| Truncated download executes partial script | Tampering | `main()` wrapper, call on last line (D-14); test truncations |
|
||||
| HTML error page piped to bash | Tampering | `curl -f`; today `/new` answers 404 so curl exits non-zero [VERIFIED] |
|
||||
| Browser renders script as HTML | Spoofing/XSS | `text/plain; charset=utf-8` + `X-Content-Type-Options: nosniff`; no user input reflected |
|
||||
| Moved/re-pointed upstream tag | Tampering | `git fetch --tags` without `--force` refuses to clobber an existing tag; ff-only check |
|
||||
| Credential prompt hang / leak | DoS/Info | `GIT_TERMINAL_PROMPT=0`, children with `</dev/null` |
|
||||
| Dev Postgres exposed on all interfaces (Docker bypasses host firewalls) | Info disclosure | publish on `127.0.0.1:<port>` only |
|
||||
| Secret committed | Info disclosure | `.env` in `.gitignore` before the first `git add -A`; golden test asserts `.env` not in the commit |
|
||||
| Path traversal via name | Tampering | regex excludes `/`, `.`; `underRoot` checks already in `MakePlugin` [VERIFIED: scaffold.go:62-64] |
|
||||
| Running the user's existing checkout code | Tampering | refuse dirty/branch checkouts instead of resetting (D-16) |
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Framework source read this session: `cmd/summer/{main,runtime,docs}.go`, `internal/build/{scaffold,build,manifest}.go`, `internal/build/stubs/plugin.tmpl`, `internal/build/build_test.go`, `cmd/summer/{main,docs}_test.go`, `modules/lagoon/{keygen,encrypted,commands,connection}.go`, `modules/lagoon/attach/bucket.go`, `modules/compass/{env,config}.go`, `modules/surf/{serve,router}.go`, `modules/cabana/http.go`, `modules/pact/capabilities.go`, `modules/bonfire/{command,output}.go`, `internal/docsite/{check_commands,emit}.go`, `docs/setup/installation.md`, `docs/architecture/go-modules-and-workspaces.md`, `docs/site.yaml`, `scripts/check-phase14.2.1.sh`.
|
||||
- Sibling repos: `../sm-bm-app` and `../sm-summercmsio-app` (`.gitignore`, `summer.yaml`, `go.work`, `go.mod`, `config/*.yaml`, `CLAUDE.md`, `scripts/build.sh`, `deploy/nginx/summercms.io.conf`, `terminal_check_test.go`, plugin `plugin.go`/`static.go`/`routes_test.go`/`links_test.go`, `vue-summercmsio-app/app/data/terminal.json`, `i18n/locales/en.json`, `tests/{terminal,output}.test.ts`).
|
||||
- Empirical runs (scratchpad): empty-plugin build/route:list/serve; full simulated `summer new` (make:plugin + plugin:add + starter routes) with migrate + serve + curl against `postgres:16`; fresh-clone rebuild; keyword/`main`/`internal` names; `go work use` relative vs absolute; `runtime.Caller` with/without `-trimpath`; `go version -m`; container git `safe.directory`; `git ls-remote --sort`.
|
||||
- msd-core `scripts/bootstrap.sh` (scratch clone of the public remote, reference only, deleted).
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- go.dev/dl availability of go1.27.0 / current go1.27.1 (curl).
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- General bash/Docker behaviours listed in the Assumptions Log.
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — no new dependencies; all internals read and exercised.
|
||||
- Architecture: HIGH — the full generated layout was hand-built and served end to end.
|
||||
- Pitfalls: HIGH for the empirically reproduced ones (1, 3, 5, 12, 14, `--addr`, `✓` prefix); MEDIUM for Docker/compose harness details.
|
||||
|
||||
**Research date:** 2026-10-08
|
||||
**Valid until:** 2026-11-07 (framework internals change quickly; re-check `internal/build` line numbers before executing)
|
||||
Reference in New Issue
Block a user