diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b4676e7..d5069b9 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -899,10 +899,21 @@ Plans: 5. `docs/setup/installation.md` leads with the one-liner, `go-modules-and-workspaces.md` and the console docs cover `summer new`, and `llms.txt`/`llms-full.txt` include the changes; docs checks pass. 6. Generator Go tests (golden output plus a real build of the scaffold in a temp dir) are the last plan. -**Plans:** 0 plans +**Plans:** 4 plans Plans: -- [ ] TBD (run /gsd-plan-phase 14.3 to break down) + +**Wave 1** +- [ ] 14.3-01-PLAN.md — `summer new` generator: starter plugin via make:plugin/plugin:add, relative go.work, config, .env + lagoon.GenerateAppKey, compose, CLAUDE.md/AGENTS.md, first commit, D-12 docker gate + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 14.3-02-PLAN.md — Bootstrap script in `modules/sunrise`, `GET /new` in the site plugin, node:24-bookworm acceptance, security review + +**Wave 3** *(blocked on Wave 2 completion)* +- [ ] 14.3-03-PLAN.md — Docs (installation one-liner first, go modules, console, llms) and landing page "Get started" with its tests + +**Wave 4** *(blocked on Wave 3 completion)* +- [ ] 14.3-04-PLAN.md — Unit tests last: goldens, real builds, error cases, script matrix, phase gate; post-phase release checklist (tag v0.1.13, site bump, deploy are user actions) ### Phase 15: Journal plugin diff --git a/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-01-PLAN.md b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-01-PLAN.md new file mode 100644 index 0000000..643fec8 --- /dev/null +++ b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-01-PLAN.md @@ -0,0 +1,303 @@ +--- +phase: 14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - internal/build/new.go + - internal/build/new_test.go + - internal/build/stubs/new.tmpl + - internal/build/stubs/plugin.tmpl + - internal/build/scaffold.go + - cmd/summer/main.go + - cmd/summer/main_test.go + - modules/lagoon/keygen.go + - modules/lagoon/README.md + - examples/hello/config/http.yaml + - README.md + - docs/setup/installation.md + - scripts/check-phase14.3.sh +autonomous: true +requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11, D-12, D-22, D-23, D-24, D-25, D-27] +estimate: + tokens: 150000 + raw_tokens: 150000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-02/D-22: `summer new ` accepts only names matching ^[a-z][a-z0-9]*$ that are not a Go keyword, `main` or `internal`; any other name exits non-zero with a message naming the rule, before anything is written." + - "D-02: when `sm--app` already exists in the current directory as a directory, file or symlink (dangling included), `summer new` exits non-zero and leaves it byte-for-byte untouched." + - "D-03/D-04: the generated go.mod declares `module git.golem15.com/golem15/sm--app`, `go 1.27.0`, `toolchain go1.27.0`, requires `git.golem15.com/golem15/summercms v0.0.0` and replaces it with a relative path (starting ./ or ../) to the framework checkout chosen in the order --framework, ../summercms, ../summercms.go (each accepted only when its go.mod declares the framework module path), then the running binary's own source tree." + - "Edge (missing framework): with no --framework, no matching sibling and no usable binary source, `summer new` exits non-zero, the error text contains `--framework`, and no sm--app directory remains." + - "Edge (target inside framework): a target directory inside the resolved framework checkout is refused." + - "D-05/D-27: the starter plugin golem15. is written by the make:plugin and plugin:add code paths with a starter route GET /{$}; plain `summer make:plugin` output is unchanged; every `use` line of the generated go.work is relative (./plugins/), and `summer plugin:add` now writes relative go.work paths too." + - "D-06: `summer new --no-plugin` writes summer.yaml with no plugin entries and the app still builds." + - "D-07/D-27: config/ holds app.yaml, database.yaml, http.yaml (body_limits default_bytes 1048576, upload_bytes 10485760 as YAML numbers) and storage.yaml (uploads.bucket_url file://./storage/app/uploads); examples/hello gains the same http.yaml and `go -C examples/hello test ./...` passes." + - "D-08/D-09/D-23: .env (mode 0600) holds SUMMER_DATABASE__DSN=postgres://summer:summer@127.0.0.1:54320/?sslmode=disable and a 44-character SUMMER_APP__KEY from lagoon.GenerateAppKey; compose.yaml runs postgres:16 published only on 127.0.0.1:54320 with matching user, password and database; .env.example lists the DSN, an empty key placeholder and the commented uploads bucket." + - "D-10/D-01: CLAUDE.md names the sm--app / sm--plugin / sm--theme convention, https://summercms.io/docs/llms.txt and https://summercms.io/docs/llms-full.txt, the commands make:plugin, plugin:add, build, dev, migrate and serve, and says not to edit the framework; AGENTS.md is a relative symlink to CLAUDE.md." + - "D-11: without --no-git the app is a git repository with exactly one commit on branch main whose tree excludes .env; with no git identity configured the commit uses the fallback author SummerCMS for that commit only; a git failure prints a warning and the app stays." + - "D-24: `summer new` prints next steps listing `docker compose up -d --wait`, `summer build`, `summer migrate` and `summer serve`." + - "D-25: the app root has no go.sum; .gitignore lists /bin/, /tmp/, /storage/, .env, go.work.sum and *.exe; go.work, main.go and plugins.gen.go are committed." + - "Edge (partial failure): any error after the target directory was created removes that directory, so a re-run with the same name is not blocked." + - "D-12: with `docker compose up -d --wait` running, `summer build`, `summer migrate` and `summer serve` succeed in the generated app with no edits and GET http://127.0.0.1:8080/ answers 200 JSON containing \"plugin\":\"golem15.\"." + prohibitions: + - requirement_id: D-08 + category: privacy + status: resolved + verification: judgment + resolution: flagged-unverified + reason: "Enforced by the generator gate and plan 04 tests; descriptor-less, human review recommended." + statement: "summer new must never commit .env or any application key, and .env.example must never carry a real key." + - requirement_id: D-03 + category: privacy + status: resolved + verification: judgment + resolution: flagged-unverified + reason: "Golden go.mod/go.work in plan 04 pin relative paths; descriptor-less, human review recommended." + statement: "Committed go.mod and go.work files must never contain absolute, machine-specific paths." + - requirement_id: D-23 + category: safety + status: resolved + verification: judgment + resolution: flagged-unverified + reason: "compose.yaml golden in plan 04; descriptor-less, human review recommended." + statement: "The generated compose.yaml must never publish PostgreSQL on an address other than 127.0.0.1." + - requirement_id: D-10 + category: transparency + status: resolved + verification: judgment + resolution: flagged-unverified + reason: "Framework README rule applies to template text; descriptor-less, human review recommended." + statement: "Generated template text must never name a consuming application." + - requirement_id: D-05 + category: safety + status: resolved + verification: judgment + resolution: flagged-unverified + reason: "Project rules 4 and 5; descriptor-less, human review recommended." + statement: "The phase must add no new Go dependency and no runtime plugin loading." + artifacts: + - path: "internal/build/new.go" + provides: "NewOptions, NewApp, ValidateAppName, planNewApp, framework resolution" + contains: "func NewApp(" + - path: "internal/build/stubs/new.tmpl" + provides: "templates for go.mod, config/*, .env, .env.example, compose.yaml, .gitignore, CLAUDE.md, next steps" + contains: "new/compose.yaml" + - path: "internal/build/stubs/plugin.tmpl" + provides: "routes.go with opt-in starter route" + contains: "StarterRoute" + - path: "cmd/summer/main.go" + provides: "summer new command with --framework, --no-plugin, --no-git" + contains: "\"new\"" + - path: "modules/lagoon/keygen.go" + provides: "exported key generator reused by key:generate and summer new" + contains: "func GenerateAppKey(" + - path: "examples/hello/config/http.yaml" + provides: "body_limits for serve and route:list" + contains: "default_bytes" + - path: "scripts/check-phase14.3.sh" + provides: "generator stage: D-12 docker compose build/migrate/serve chain" + contains: "--generator" + key_links: + - from: "cmd/summer/main.go" + to: "modules/lagoon/keygen.go" + via: "newCommand passes lagoon.GenerateAppKey() into build.NewOptions.AppKey" + pattern: "GenerateAppKey" + - from: "internal/build/new.go" + to: "internal/build/scaffold.go" + via: "starter plugin through makePlugin(StarterRoute) and AddPlugin, then generate()" + pattern: "makePlugin(" + - from: "internal/build/scaffold.go" + to: "go.work" + via: "runGoWorkUse passes a path relative to the go.work directory" + pattern: "go\", \"work\", \"use\"" +--- + + +Add `summer new `: one command that writes a ready-to-run `sm--app/` (go.mod with a relative framework replace, go.work, summer.yaml, a starter plugin with an HTTP route, complete config/, .env with a fresh key, .env.example, compose.yaml, .gitignore, CLAUDE.md/AGENTS.md, first git commit) in the current directory, reusing the make:plugin, plugin:add, generate and key:generate code paths. + +Purpose: remove the hand-written go.mod/summer.yaml/config/ step from starting an app (phase goal, roadmap success criteria 1 and 2) and fix the two shared-path defects research found (absolute go.work paths, no starter route option) plus the examples/hello body_limits gap. +Output: generator in internal/build, CLI command, exported lagoon.GenerateAppKey, examples/hello fix, generator gate stage proving D-12 on real PostgreSQL 16. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/STATE.md +@.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-CONTEXT.md +@.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md +@.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-VALIDATION.md +@internal/build/scaffold.go +@internal/build/build.go +@internal/build/manifest.go +@internal/build/stubs/plugin.tmpl +@cmd/summer/main.go +@modules/lagoon/keygen.go + + +## Spec-less probe fallback + +Phase 14.3 has no REQUIREMENTS.md IDs and no SPEC.md, so requirement probing is visibly skipped. The acceptance contract is D-01..D-28 from 14.3-CONTEXT.md, the six roadmap success criteria, and the edge predicates and prohibitions authored into `must_haves` above (name validation, existing directory, missing framework, target inside framework, partial failure, no git identity; no committed secrets, no absolute paths, loopback-only Postgres, neutral template text, no new dependencies or runtime plugin loading). + +## Artifacts this phase produces (this plan) + +- `build.NewOptions` (fields `Name`, `Parent`, `Framework`, `AppKey`, `NoPlugin`, `NoGit`), `build.NewApp(ctx, opts, out) (string, error)`, `build.ValidateAppName(name) error`. +- Unexported: `planNewApp`, `genFile`, `resolveFramework`, `frameworkFromBinary` (package variable, replaceable in tests), `binarySourceFramework`, `makePlugin` + `pluginOptions{StarterRoute}`, `stubData.StarterRoute`, a relative-path helper shared by `editAppModule`, `runGoWorkUse` and `NewApp`. +- Template file `internal/build/stubs/new.tmpl` with defines `new/go.mod`, `new/config/app.yaml`, `new/config/database.yaml`, `new/config/http.yaml`, `new/config/storage.yaml`, `new/env`, `new/env.example`, `new/compose.yaml`, `new/gitignore`, `new/CLAUDE.md`, `new/next-steps`. +- CLI: `summer new ` with flags `--framework `, `--no-plugin`, `--no-git`. +- `lagoon.GenerateAppKey() (string, error)`; `lagoon.KeyGenerateCommand` now calls it. +- `examples/hello/config/http.yaml`. +- `scripts/check-phase14.3.sh` with the `--generator` stage (final line `Phase 14.3 generator check passed`). +- Generated app layout: `go.mod`, `go.work`, `summer.yaml`, `main.go`, `plugins.gen.go`, `config/{app,database,http,storage}.yaml`, `.env` (0600), `.env.example`, `compose.yaml` (postgres:16 on 127.0.0.1:54320, user/password `summer`, database ``), `.gitignore`, `CLAUDE.md`, `AGENTS.md` -> `CLAUDE.md`, `plugins//` (starter plugin `golem15.`, route `GET /{$}`). + + + + + Task 1: `summer new demo --framework ` writes an app whose starter route builds and lists + D-02 name rule (plus the D-22 denylist) and the `sm--app` directory name are a public CLI contract; loosening the denylist later is non-breaking, changing the regex or directory shape is not. + internal/build/new.go, internal/build/stubs/new.tmpl, internal/build/stubs/plugin.tmpl, internal/build/scaffold.go, cmd/summer/main.go, cmd/summer/main_test.go, internal/build/new_test.go + internal/build/scaffold.go (MakePlugin, AddPlugin, editAppModule lines 318-334, ensureGoWork, writeGoWork, runGoWorkUse, parseGoMod, underRoot, findGoWork, applyGoWork, renderStub), internal/build/build.go (generate, writeIfChanged), internal/build/manifest.go (SaveManifest, LoadManifest, validateBinary), internal/build/stubs/plugin.tmpl (routes.go and go.mod defines), internal/build/build_test.go (copyHelloApp, TestMakeAndAddPluginOnHelloCopy, assertScaffoldFiles), cmd/summer/main.go (makePluginCommand, addPluginCommand, flagTrue), cmd/summer/main_test.go (TestToolCommandNames helpWants), modules/pact/capabilities.go (Router.Get), .planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md (Q1, Q3, Q6, Pattern 1-4, Pitfalls 1-3) + Wire the single happy path end to end, production quality, per D-02, D-03, D-05, D-07, D-22, D-25 and D-27. + +Shared code paths (D-05, D-27): add `StarterRoute bool` to `stubData`; move the body of `MakePlugin` into an unexported `makePlugin(ctx, startDir, id string, opts pluginOptions)` with `pluginOptions{StarterRoute bool}`, and make `MakePlugin` call it with zero options so `summer make:plugin` output stays byte-identical. In the existing `routes.go` define of plugin.tmpl add an `{{if .StarterRoute}}` branch that imports `encoding/json` and `net/http` and registers `r.Get("/{$}", ...)` writing `Content-Type: application/json` and the JSON object with keys `message` = `Hello from SummerCMS` and `plugin` = `p.ID()`; the `{{else}}` branch is the current body unchanged. Do not add a second plugin template. + +Relative go.work (D-27): extract the "./"-prefix logic of `editAppModule` into one helper that turns an absolute target into a slash-separated path relative to a base directory, prefixed with `./` unless it is `.`, `..` or starts with `../`. Use it in `editAppModule` (same behaviour as today), and in `runGoWorkUse`: run `go work use ` with `cmd.Dir` set to the directory that holds the go.work file, the path computed relative to that directory. This changes `summer plugin:add` output from absolute to relative paths. + +Generator (`internal/build/new.go`): exported `NewOptions` (this task uses `Name`, `Parent`, `Framework`), `ValidateAppName(name string) error` and `NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error)`. `ValidateAppName` enforces `^[a-z][a-z0-9]*$` and additionally rejects `go/token.IsKeyword(name)`, `main` and `internal`, each with an error that names the rule and says why (D-22). `NewApp`: validate the name first; make `Parent` absolute with `filepath.Abs` (lexical, never EvalSymlinks); target is `Parent/sm--app`; `os.Lstat` the target and refuse when anything exists there (D-02, no merge); resolve `Framework` relative to `Parent` when not absolute, read its go.mod with `parseGoMod` and require module `frameworkModule`, else error; create the target with `os.Mkdir`. Then write, in this order: go.mod rendered from `new/go.mod` (module `git.golem15.com/golem15/sm--app`, `goVersionLine`, `toolchainLine`, require framework `v0.0.0`, replace to the relative framework path from the helper, D-03); go.work through `writeGoWork` BEFORE the plugin step so an ancestor go.work is never used (research Pattern 2); summer.yaml through `SaveManifest` with module, binary `` and no plugins; `config/app.yaml` (name, debug false, empty key with a comment naming the env override), `config/database.yaml` (empty dsn with a comment naming the env override), `config/http.yaml` (body_limits default_bytes 1048576, upload_bytes 10485760 as YAML numbers with a comment that env values are strings), `config/storage.yaml` (uploads bucket_url `file://./storage/app/uploads`, public_path_prefix `/storage/uploads`) (D-07). Render these through a pure `planNewApp` step that returns a slice of `genFile{Rel, Body, Mode}` and reads nothing but the framework go.mod, so plan 04 can golden-test it. Then `makePlugin(ctx, target, "golem15."+name, pluginOptions{StarterRoute: true})`, `AddPlugin(ctx, target, "plugins/"+name)`, `LoadManifest`, and `generate(target, m)` so main.go and plugins.gen.go exist. Never run `go mod tidy` at the app root (D-25: no root go.sum). Templates live in the new `internal/build/stubs/new.tmpl` (picked up by the existing `stubs/*.tmpl` embed) with define names prefixed `new/` so they cannot collide with plugin defines. + +CLI: add `newCommand()` to `toolCommands()` in cmd/summer/main.go: name `new`, description `Create a SummerCMS application in sm--app`, required arg `name` (description states the letters/digits rule and the Go keyword, main and internal exclusions), flag `framework` (path to the framework checkout). Run uses `os.Getwd()` as `Parent`, prints the created directory. Add `new` to the required names in `TestToolCommandNames` and a helpWants entry `"new": {"--framework"}`. + +Smoke test `TestNewAppSmoke` in internal/build/new_test.go (skip under `testing.Short()`, D-21): temp parent with a `summercms` symlink to the repository root (abs of `../..`); `NewApp` with `Framework` = that symlink path; assert go.mod contains `replace git.golem15.com/golem15/summercms => ../summercms`, go.work contains `./plugins/demo` and no line whose first non-blank character is `/`, `App(ctx, dir, &buf)` succeeds, and `bin/demo route:list` run with `cmd.Dir` = app dir prints `/{$}` and `golem15.demo`. + + go vet ./internal/build/ ./cmd/summer/ && go test ./internal/build/ -run '^(TestNewAppSmoke|TestMakeAndAddPluginOnHelloCopy|TestScaffoldPluginSmoke|TestAddPluginRejectsConflictingID)$' -count=1 -v && go test ./cmd/summer/ -run '^TestToolCommandNames$' -count=1 -v + Non-zero exit; any "--- FAIL"; "no tests to run"; or the output lacks "--- PASS: TestNewAppSmoke", "--- PASS: TestMakeAndAddPluginOnHelloCopy" or "--- PASS: TestToolCommandNames". + + + - `grep -c 'StarterRoute' internal/build/stubs/plugin.tmpl` is at least 1 and plugin.tmpl still has exactly one `{{define "routes.go"}}`. + - `grep -c '{{define "new/' internal/build/stubs/new.tmpl` is at least 5 (go.mod and four config files). + - `grep -n 'func NewApp(\|func ValidateAppName(\|type NewOptions struct\|func planNewApp(' internal/build/new.go` prints four lines. + - `go run ./cmd/summer new --help` output contains `--framework` and `name`. + - TestNewAppSmoke passes: relative replace `../summercms`, relative go.work, build succeeds, route:list shows `GET /{$}` for `golem15.demo`. + - `TestMakeAndAddPluginOnHelloCopy` still passes (make:plugin output unchanged, plugin:add relative). + - `go vet ./...` and `go test ./... -short` pass at the commit. + + `summer new demo --framework ` writes an app with a relative replace, a relative go.work, config/ and a starter plugin whose `GET /{$}` route compiles and is listed by route:list; committed as one feat commit in summercms.go. + + + + Task 2: Framework auto-detection, refusals, cleanup, --no-plugin, and the examples/hello config gap + internal/build/new.go, cmd/summer/main.go, examples/hello/config/http.yaml, README.md, docs/setup/installation.md + internal/build/new.go (from Task 1), internal/build/scaffold.go (parseGoMod, underRoot), cmd/summer/main.go (newCommand, flagTrue), examples/hello/hello_test.go (TestTypedItemRoute), examples/hello/config/app.yaml, README.md (Quick start step 2 and Known issues, lines 19-68), docs/setup/installation.md ("Configure the application" and the WARNING callout, lines 89-126), .planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md (Q2, Q3 examples/hello gap, Q6 binary source, Pitfalls 2 and 4) + Complete the D-04 resolution order, the D-06 flag and the D-07 hello fix. + +Framework resolution (D-04): in new.go add `resolveFramework(parent, flag string) (string, error)`. Order: (1) `--framework` when set: resolved against `parent`, must have a go.mod declaring `frameworkModule`, otherwise error naming the path and the module it found (never fall through); (2) `parent/summercms`, then `parent/summercms.go`, each accepted only when its go.mod declares `frameworkModule` (detection by module path, not folder name; a non-matching candidate is skipped); (3) the running binary's source tree via `binarySourceFramework()`, which uses `runtime.Caller(0)`, ignores a non-absolute file path (a `-trimpath` build), climbs from `internal/build/new.go` to the repository root and accepts it only when its go.mod declares `frameworkModule`; call it through an unexported package variable `frameworkFromBinary` so tests can replace it; (4) otherwise an error that names `--framework` and lists the sibling names it looked for. Keep every candidate path lexical (absolute, not symlink-resolved) so the relative replace for a symlinked sibling is `../summercms`. + +Refusals and cleanup: refuse when the target directory would be inside the resolved framework checkout (use `underRoot`), with a message telling the user to run `summer new` next to the checkout. Refuse when `Parent` is not an existing directory. After `os.Mkdir` of the target succeeds, any later error removes the target with `os.RemoveAll` (it did not exist before this run, D-02) and returns the original error with context; nothing else is ever removed. + +`--no-plugin` (D-06; research Q2 verified an empty plugin list builds and serves): add `NoPlugin` to `NewOptions` and the bare flag `no-plugin` to `newCommand` (read with `flagTrue`); when set skip `makePlugin` and `AddPlugin` and still call `generate`, so summer.yaml lists no plugins and go.work only `use .`. + +examples/hello (D-07, D-27): add `examples/hello/config/http.yaml` with the same `body_limits` block the generator writes (default_bytes 1048576, upload_bytes 10485760). In README.md delete Quick start step 2 (the `config/http.yaml` instruction) and the first Known issues bullet about the missing `http.body_limits`; in docs/setup/installation.md delete the paragraph and yaml fence that tell the reader to create `config/http.yaml` and the matching first bullet of the WARNING callout. Leave the remaining text (the main.go bullet, the one-liner rewrite) to plan 03, which rewrites both pages. + + go vet ./internal/build/ ./cmd/summer/ && go test ./internal/build/ ./cmd/summer/ -count=1 && go -C examples/hello test ./... -count=1 && go run ./cmd/summer docs:build --check && bash -c 'set -e; d=$(mktemp -d); go build -trimpath -o "$d/summer" ./cmd/summer; cd "$d"; for n in go main internal func Demo 1x a-b; do if ./summer new "$n" >out 2>&1; then echo "accepted $n"; exit 1; fi; done; mkdir sm-taken-app; if ./summer new taken >out 2>&1; then exit 1; fi; if ./summer new nofw >out 2>&1; then exit 1; fi; grep -q -- "--framework" out; test ! -e sm-nofw-app; ln -s "$OLDPWD" summercms; ./summer new bare --no-plugin; grep -q "replace git.golem15.com/golem15/summercms => ../summercms" sm-bare-app/go.mod; (cd sm-bare-app && ../summer build); echo RULES-OK' + Non-zero exit; any "FAIL" line; "accepted" printed for a rejected name; `docs:build --check` reports problems; or the output lacks the final "RULES-OK" line. + + + - `grep -n 'func resolveFramework(\|frameworkFromBinary\|func binarySourceFramework(' internal/build/new.go` prints at least three lines. + - A `-trimpath` summer with no sibling checkout fails `summer new nofw` with `--framework` in the error and leaves no `sm-nofw-app`. + - `summer new` refuses `go`, `main`, `internal`, `func`, `Demo`, `1x`, `a-b` and an existing `sm-taken-app`. + - With a `summercms` symlink sibling and no flag, `summer new bare --no-plugin` writes `replace ... => ../summercms` and `summer build` succeeds. + - `examples/hello/config/http.yaml` exists with `default_bytes: 1048576`; `go -C examples/hello test ./...` passes (TestTypedItemRoute green). + - README.md and docs/setup/installation.md no longer instruct creating `config/http.yaml`; `docs:build --check` passes. + + Framework resolution follows D-04 with verified module paths, refusals and cleanup protect the user's files, `--no-plugin` builds, and examples/hello serves without a manual config step; committed as separate commits for the generator change and the hello fix. + + + + Task 3: .env with a fresh key, compose, .gitignore, CLAUDE.md/AGENTS.md, first commit, next steps, and the D-12 docker gate + The Docker daemon is reachable (`docker info` exits 0), the postgres:16 image can be pulled, and host ports 54320 and 8080 are free. + modules/lagoon/keygen.go, modules/lagoon/README.md, internal/build/new.go, internal/build/new_test.go, internal/build/stubs/new.tmpl, cmd/summer/main.go, scripts/check-phase14.3.sh + modules/lagoon/keygen.go, modules/lagoon/keygen_test.go, modules/lagoon/README.md (API reference rows near line 147-150 and the app.key config row), modules/lagoon/encrypted.go (encryptedKeySize, decodeAppKey), modules/compass/env.go (.env parsing, quotes, export prefix), internal/build/new.go, internal/build/stubs/new.tmpl, cmd/summer/main.go, scripts/check-phase14.2.1.sh (gate script conventions: set -euo pipefail, env overrides, final pass line), ../sm-summercmsio-app/.gitignore, ../sm-summercmsio-app/config/app.yaml, ../sm-summercmsio-app/CLAUDE.md (tone reference only; do not copy app-specific text), .planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-RESEARCH.md (Q4, Q5, compose.yaml shape, Pitfalls 5, 10, 11) + Make the generated app run with no edits (D-08 to D-12, D-23 to D-25). + +Key (D-08): export `GenerateAppKey() (string, error)` in modules/lagoon/keygen.go returning `base64.StdEncoding` of `encryptedKeySize` bytes from `crypto/rand`; `KeyGenerateCommand` calls it and still prints through `out.Success` (output unchanged). Add the `lagoon.GenerateAppKey` row to the API reference table of modules/lagoon/README.md in the same commit (project documentation rule). `internal/build` stays free of a lagoon import: add `AppKey` to `NewOptions`; `NewApp` errors when it is empty or does not base64-decode to 32 bytes; `newCommand` calls `lagoon.GenerateAppKey()` and passes the result. Update `TestNewAppSmoke` to pass a fixed valid 44-character test key and `NoGit: true` so it stays hermetic. + +Files, all rendered by `planNewApp` from new.tmpl defines (golden-testable with a fixed key): +- `.env` (mode 0600): `SUMMER_DATABASE__DSN=postgres://summer:summer@127.0.0.1:54320/?sslmode=disable` and `SUMMER_APP__KEY=`, unquoted, no `$` characters, with a header comment that the file holds secrets and is gitignored. +- `.env.example` (0644): the same DSN, `SUMMER_APP__KEY=` empty with a comment on generating one with `./bin/ key:generate` (paste only the key), a commented `SUMMER_STORAGE__UPLOADS__BUCKET_URL=file://./storage/app/uploads`, and a commented `SUMMER_ENV`. +- `compose.yaml` (D-09, D-23): one service `postgres` with image `postgres:16`, environment POSTGRES_USER `summer`, POSTGRES_PASSWORD `summer`, POSTGRES_DB ``, ports `127.0.0.1:54320:5432` only, a named volume `pgdata` (no bind mounts), and a `pg_isready -U summer -d ` healthcheck (interval 2s, timeout 3s, retries 30) so `docker compose up -d --wait` returns once the database accepts connections. +- `.gitignore` (D-08, D-25): `/bin/`, `/tmp/`, `/storage/`, `.env`, `go.work.sum`, `*.exe`. +- `CLAUDE.md` (D-10, D-01): states this is a SummerCMS application built from `git.golem15.com/golem15/summercms`, replaced by the local checkout at the relative replace path, which agents must not edit from here; links https://summercms.io/docs/llms.txt and https://summercms.io/docs/llms-full.txt; lists `summer make:plugin`, `summer plugin:add`, `summer build`, `summer dev`, `summer migrate`, `summer serve` and `docker compose up -d --wait`; explains summer.yaml, plugins/ (starter plugin golem15. only when present), config/, `.env` (secrets, never commit), compose.yaml, and that main.go and plugins.gen.go are generated by `summer build`; states plugins are compiled in at build time; states the Golem15 naming convention: `sm--app` application, `sm--plugin` standalone plugin module, `sm--theme` Nuxt/Vue frontend. Use only neutral wording (no consuming application names). +- `AGENTS.md`: a relative symlink to `CLAUDE.md` (planner's call under D-10: one source, survives git clone on Linux and macOS); represent it in `genFile` as a link target. + +Git (D-11): add `NoGit` to `NewOptions` and the bare flag `no-git` to `newCommand`. Unless set: look up `git` on PATH (missing means a warning and no repository); run `git init --quiet --initial-branch=main`, `git add -A`, then `git commit --quiet -m "Create sm--app with summer new"` with stdin closed and `GIT_TERMINAL_PROMPT=0`. When `git config user.name` or `git config user.email` (run inside the new repository) is unset and the matching `GIT_AUTHOR_*`/`GIT_COMMITTER_*` variables are absent, pass `-c user.name=SummerCMS -c user.email=summer@localhost` to that commit only and print a warning saying so. Any git failure (for example signing with an unavailable key) prints a warning with git's output and keeps the app. The commit message carries no trailers. + +Next steps (D-24): after success, print from the `new/next-steps` define: the created directory, then "Next steps, inside sm--app:" followed by `docker compose up -d --wait`, `summer build`, `summer migrate`, `summer serve`, then `curl http://127.0.0.1:8080/` (with `--no-plugin`, a line pointing at `summer make:plugin` and `summer plugin:add` instead of the curl). Do not print a `cd` line (D-24 assigns it to the bootstrap script). + +Gate (D-12): create executable `scripts/check-phase14.3.sh` (bash, `set -euo pipefail`, `PHASE143_*` overrides, root from the script location) with stage `--generator`: preflight `docker info` and that 127.0.0.1:54320 and :8080 are free (fail with a clear message otherwise); build the CLI with `go build -o "$tmp/bin/summer" ./cmd/summer` and put `$tmp/bin` first on PATH; in a temp parent create a `summercms` symlink to the repo root; run `summer new demo` WITHOUT `--framework` and with HOME set to an empty temp directory and `GIT_CONFIG_NOSYSTEM=1` for that one command (export GOMODCACHE, GOCACHE and GOPATH from `go env` first so the module cache is reused) to exercise sibling detection and the fallback identity; assert `.env` mode is 600, `git -C sm-demo-app rev-list --count HEAD` is 1, `git -C sm-demo-app ls-files .env` prints nothing, `.env.example`, `go.work`, `main.go`, `plugins.gen.go` are tracked, `AGENTS.md` is a symlink to `CLAUDE.md`, there is no root `go.sum`, and go.work has no absolute `use` path; then in the app with a unique `COMPOSE_PROJECT_NAME` run `docker compose up -d --wait`, `summer build`, `summer migrate`, start `summer serve` in the background, poll `http://127.0.0.1:8080/` for up to 60 s and require the body to contain `"plugin":"golem15.demo"`; a trap stops the server and runs `docker compose down -v` on every exit. End with the line `Phase 14.3 generator check passed`. Commit the lagoon change, the generator change and the gate script as separate commits. + + go vet ./... && go test ./modules/lagoon/ -run 'Key' -short -count=1 -v && go test ./internal/build/ -run '^TestNewAppSmoke$' -count=1 -v && bash scripts/check-phase14.3.sh --generator + Non-zero exit; any "--- FAIL"; "--- PASS: TestNewAppSmoke" or "--- PASS: TestEncryptedKeyGeneratePrints32ByteBase64" absent; or the output lacks the final line "Phase 14.3 generator check passed". + + + - `grep -n 'func GenerateAppKey() (string, error)' modules/lagoon/keygen.go` prints one line and `grep -c 'lagoon.GenerateAppKey' modules/lagoon/README.md` is at least 1. + - `grep -c '127.0.0.1:54320:5432' internal/build/stubs/new.tmpl` is 1 and `grep -c 'postgres:16' internal/build/stubs/new.tmpl` is at least 1. + - `grep -c 'https://summercms.io/docs/llms-full.txt' internal/build/stubs/new.tmpl` is at least 1 and `grep -c 'sm--theme\|-theme' internal/build/stubs/new.tmpl` is at least 1. + - `grep -Eci 'fonoteka|p[lł]ytarium' internal/build/stubs/new.tmpl` prints 0. + - The `--generator` stage passes: sibling detection without `--framework`, fallback identity commit, `.env` 0600 and untracked, AGENTS.md symlink, no root go.sum, relative go.work, and the starter route answers 200 JSON after `docker compose up -d --wait`, `summer build`, `summer migrate`, `summer serve`. + - `go run ./cmd/summer new --help` lists `--framework`, `--no-plugin` and `--no-git`. + - `go vet ./...` and `go test ./... -short` pass; `git diff v0.1.12 -- go.mod` adds no require line. + + A generated app holds a fresh key, a loopback-only PostgreSQL 16 compose file, a secret-free first commit and agent instructions, prints its next steps, and the gate proves `summer build`, `summer migrate` and `summer serve` work with no edits. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| CLI argument and flag -> filesystem | `` and `--framework` become paths and module names | +| Generator -> git history | Generated files enter the first commit, which may be pushed | +| Generated compose.yaml -> host network | Docker publishes the dev database on the host | +| crypto/rand -> .env | The application encryption key is created and stored | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-14.3-01 | Tampering | `ValidateAppName`, target path | high | mitigate | Regex `^[a-z][a-z0-9]*$` plus keyword/main/internal denylist before any path is built; target is always `Parent/sm--app` | +| T-14.3-02 | Information Disclosure | `.env`, first git commit | high | mitigate | `.gitignore` written before `git add -A`; `.env` mode 0600; `.env.example` holds no key; gate asserts `git ls-files .env` is empty | +| T-14.3-03 | Information Disclosure | app key generation | high | mitigate | Single implementation `lagoon.GenerateAppKey` on `crypto/rand`; NewApp rejects keys that do not decode to 32 bytes | +| T-14.3-04 | Tampering | existing directory, cleanup | high | mitigate | `os.Lstat` refusal of any existing target; `os.Mkdir` (not MkdirAll); `os.RemoveAll` only on the directory this run created | +| T-14.3-05 | Information Disclosure | compose.yaml Postgres port | medium | mitigate | Publish only `127.0.0.1:54320`; named volume, no bind mounts | +| T-14.3-06 | Spoofing | framework auto-detection | medium | mitigate | Candidates accepted only when go.mod declares `git.golem15.com/golem15/summercms`; explicit `--framework` wins and never falls through | +| T-14.3-07 | Information Disclosure | committed go.mod/go.work | low | mitigate | Relative replace and relative `go work use` paths; no absolute home paths in history | +| T-14.3-08 | Information Disclosure | compose.yaml dev credentials | low | accept | `summer`/`summer` is a documented local default for a loopback-only dev database, not a production credential; the encryption key stays in `.env` | +| T-14.3-09 | Information Disclosure | `summer serve` default `:8080` | low | accept | Existing surf default listens on all interfaces; out of scope for this phase; next steps point at 127.0.0.1 | +| T-14.3-10 | Repudiation | fallback git identity | low | accept | Used only when no identity exists, for the first commit only, and announced with a warning | +| T-14.3-SC | Tampering | npm/pip/cargo installs | high | mitigate | No package installs in this plan; `git diff v0.1.12 -- go.mod` adds no require line | + + + +Run each task's automated command; then `go vet ./... && go test ./... -short && go -C examples/hello test ./... -count=1`. `bash scripts/check-phase14.3.sh --generator` must end with `Phase 14.3 generator check passed`. + + + +- Roadmap success criterion 1: `summer new ` validates, refuses existing directories, resolves the framework and writes the full layout with a first commit. +- Roadmap success criterion 2: the generated app builds, migrates and serves with no edits; the starter route answers over HTTP. +- `summer make:plugin` output unchanged; `summer plugin:add` writes relative go.work paths; examples/hello tests pass. +- No new dependency; lagoon README documents `lagoon.GenerateAppKey`. + + + +Create `.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-01-SUMMARY.md` when done. + + + diff --git a/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-02-PLAN.md b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-02-PLAN.md new file mode 100644 index 0000000..c568898 --- /dev/null +++ b/.planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-02-PLAN.md @@ -0,0 +1,258 @@ +--- +phase: 14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new +plan: 02 +type: execute +wave: 2 +depends_on: ["14.3-01"] +files_modified: + - modules/sunrise/new.sh + - modules/sunrise/sunrise.go + - modules/sunrise/sunrise_test.go + - modules/sunrise/README.md + - README.md + - ../sm-summercmsio-app/plugins/golem15/summercms/script.go + - ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go + - ../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go + - ../sm-summercmsio-app/plugins/golem15/summercms/README.md + - scripts/check-phase14.3.sh + - .planning/phases/14.3-one-command-app-scaffolding-summer-new-and-summercms-io-new/14.3-SECURITY-REVIEW.md +autonomous: true +requirements: [D-13, D-14, D-15, D-16, D-17, D-18, D-20, D-24, D-26, D-28] +estimate: + tokens: 140000 + raw_tokens: 140000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-13/D-26: modules/sunrise embeds new.sh from its own package directory; sunrise.Script() returns exactly those bytes as a fresh copy on every call." + - "D-13: the site plugin answers GET /new with 200, Content-Type `text/plain; charset=utf-8`, `X-Content-Type-Options: nosniff`, `Cache-Control: no-cache`, a strong ETag and a body equal to sunrise.Script(); HEAD /new is 200 with no body; If-None-Match with that ETag is 304; POST /new is 405." + - "D-14: new.sh consists of comments, function definitions and a final line `main \"$@\"`; running any prefix of the file that ends on a line boundary, or any byte prefix of its final line, invokes no external command and creates no file." + - "D-15 step 1: with Go missing or older than 1.27 the script exits non-zero with a hint naming https://go.dev/dl/ before it clones anything; a missing git is an error; a missing Docker or compose plugin is only a warning." + - "D-15 step 2/D-16/D-17: with no checkout at SUMMERCMS_DIR (default ./summercms) the script clones SUMMERCMS_REPO (default https://git.golem15.com/golem15/summercms) and detaches at the newest vX.Y.Z tag, or at SUMMERCMS_REF when set." + - "Edge (dirty clone): an existing checkout with tracked changes, on a branch, whose go.mod is not the framework module, or that is not a git repository is refused with a message and left untouched." + - "Edge (non-fast-forward / moved tag): an existing detached checkout moves only when its HEAD is an ancestor of the target tag; tag fetches never overwrite a local tag; otherwise the script exits non-zero without changing the checkout." + - "D-15 steps 3-4: the script installs summer with `go install ./cmd/summer` into GOBIN (or the first GOPATH entry's bin), then runs `/summer new --framework ` from the checkout's parent directory with stdin closed." + - "D-24: the script prints `cd ` and, only when the install directory is not on PATH, an `export PATH=\":$PATH\"` hint; the generic next steps come from summer new." + - "Edge (invalid name / existing app): a name failing ^[a-z][a-z0-9]*$ or an existing sm--app next to the checkout stops the script before any clone or install." + - "D-20: in a clean node:24-bookworm container with Go 1.27.0, the Docker CLI and compose plugin, git and jq, `curl -fsSL