docs(34): create phase plan
This commit is contained in:
@@ -1735,10 +1735,24 @@ Plans:
|
||||
4. The admin Plugins screen lists installed plugins with versions and available updates, and its Marketplace tab searches the registry and shows the install command with a copy button.
|
||||
5. The new code has unit tests, delivered in the phase's last plan.
|
||||
|
||||
**Plans:** 0 plans
|
||||
**Plans:** 6 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (run /msd-plan-phase 34 to break down)
|
||||
**Wave 1**
|
||||
- [ ] 34-01-PLAN.md — kiosk module: plugin.yaml format, `summer plugin:manifest [--check]`, const-resolving ID reader, registry v1 wire types, PluginInfo/FrameworkVersion baked by summer build (wave 1)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
- [ ] 34-03-PLAN.md — summercms.io registry: tables, pull ingester, /api/registry/v1, admin curation, `registry` scope ingest-now, hourly schedule, /marketplace pages, deploy, changelog-post.py hooks (wave 2)
|
||||
- [ ] 34-04-PLAN.md — `summer plugin:install` (module, --source, git URL) and `summer plugin:update`, `plugins.registry_url`, go.work overrides (wave 2)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
- [ ] 34-05-PLAN.md — admin Plugins screen: cabana endpoints, backend.view_plugins, Installed + Marketplace tabs with copy, openapi and dist (wave 3)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 completion)*
|
||||
- [ ] 34-02-PLAN.md — plugin.yaml and TestPluginManifest in every public sm-*-plugin repo, framework minimum v0.6.0, app pointer bumps after Jakub's push (wave 4)
|
||||
|
||||
**Wave 5** *(blocked on Wave 4 completion)*
|
||||
- [ ] 34-06-PLAN.md — unit tests across all repos, scripts/check-phase34.sh, release ledger and validation map (wave 5)
|
||||
|
||||
## Progress
|
||||
|
||||
|
||||
461
.planning/phases/34-plugin-marketplace/34-01-PLAN.md
Normal file
461
.planning/phases/34-plugin-marketplace/34-01-PLAN.md
Normal file
@@ -0,0 +1,461 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- modules/kiosk/kiosk.go
|
||||
- modules/kiosk/manifest.go
|
||||
- modules/kiosk/derive.go
|
||||
- modules/kiosk/identity.go
|
||||
- modules/kiosk/check.go
|
||||
- modules/kiosk/version.go
|
||||
- modules/kiosk/registry.go
|
||||
- modules/kiosk/README.md
|
||||
- modules/kiosk/manifest_smoke_test.go
|
||||
- modules/kiosk/testdata/literal/go.mod
|
||||
- modules/kiosk/testdata/literal/plugin.go
|
||||
- modules/kiosk/testdata/literal/README.md
|
||||
- modules/kiosk/testdata/consts/go.mod
|
||||
- modules/kiosk/testdata/consts/plugin.go
|
||||
- modules/kiosk/testdata/consts/ids/ids.go
|
||||
- modules/kiosk/testdata/consts/nested/plugin.go
|
||||
- modules/kiosk/testdata/consts/nested/README.md
|
||||
- modules/kiosk/testdata/consts/README.md
|
||||
- modules/kiosk/testdata/other/go.mod
|
||||
- modules/kiosk/testdata/other/plugin.go
|
||||
- modules/kiosk/testdata/other/README.md
|
||||
- modules/kiosk/testdata/framework/go.mod
|
||||
- modules/party/info.go
|
||||
- modules/party/README.md
|
||||
- internal/build/build.go
|
||||
- internal/build/new.go
|
||||
- internal/build/scaffold.go
|
||||
- internal/build/plugindirs.go
|
||||
- internal/build/plugininfo_smoke_test.go
|
||||
- internal/build/build_test.go
|
||||
- internal/build/testdata/new/main.go.golden
|
||||
- internal/build/testdata/new/plugins.gen.go.golden
|
||||
- internal/build/testdata/new-no-plugin/plugins.gen.go.golden
|
||||
- examples/hello/main.go
|
||||
- examples/hello/plugins.gen.go
|
||||
- cmd/summer/main.go
|
||||
- README.md
|
||||
- docs/plugins/marketplace.md
|
||||
- docs/console/scaffolding.md
|
||||
- docs/architecture/go-modules-and-workspaces.md
|
||||
autonomous: false
|
||||
requirements: [SC-1, SC-4, D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-16]
|
||||
|
||||
estimate:
|
||||
tokens: 150000
|
||||
raw_tokens: 150000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "`summer plugin:manifest [dir]` writes `<dir>/plugin.yaml` whose every field is derived from the plugin's own sources: id and requires from plugin.go, module from go.mod, package from the directory's import path, framework and plugin minimums from go.mod require lines, name and description from the README's H1 and one-sentence summary; plugin.go and party.Plugin are unchanged (D-01, D-02, D-05)"
|
||||
- "The ID reader follows string constants statically: a literal, a package-level const in the same package, a const in another package of the same module, and a const in another module resolved through `go list -m -json`; no plugin code is compiled or run, and any other expression is an error naming ID() or Requires() (D-02 as extended by the user on 2026-10-29)"
|
||||
- "`plugin:add` registers a plugin whose ID() returns a constant, because pluginIDFromGo now calls the same resolver (fix for research F1; literal-ID plugins register exactly as before)"
|
||||
- "`--name` and `--description` persist in an `overrides:` block, survive regeneration, and `--name=\"\"` clears the override; a README whose H1 equals the plugin ID and no name override is an error that names `--name` (D-03, research F7)"
|
||||
- "`summer plugin:manifest --check [dir]` exits non-zero and prints what it expected when the committed plugin.yaml is missing or stale, and `kiosk.CheckT(t, dir)` fails a plugin repo's `go test` the same way (D-04)"
|
||||
- "Version constraints are Go-style minimums: `framework: vX.Y.Z` and `requires: {id: vX.Y.Z}`; a plugin requirement at v0.0.0 or a missing framework require is an error, a pseudo-version minimum is accepted with a warning, and a required ID whose module is not in go.mod has the empty minimum (D-05, research F8)"
|
||||
- "`summer build` bakes `PluginInfo` (ID, module, package, name, description, version, source per plugin) and `FrameworkVersion` into plugins.gen.go, and the generated main publishes them with party.Describe before party.Activate; a plugin without plugin.yaml has only its ID and version, and a metadata failure warns and never fails the build (D-06)"
|
||||
- "Versions are D-07's: a module-cache plugin reports its go.mod/build-info version, a plugin directory that is its own git repository reports `git describe --tags --match 'v[0-9]*'` without the dirty marker, an in-app plugin reports an empty version, and FrameworkVersion is the effective framework version (the newest tag reachable in a locally replaced framework checkout, otherwise the go.mod require) (D-07, research F3)"
|
||||
- "The registry v1 wire types (kiosk.RegistryPlugin, kiosk.RegistryPluginDetail, kiosk.RegistryVersion, envelopes and error) match the shape confirmed at Task 1, so the summercms.io server (34-03), the CLI (34-04) and cabana (34-05) encode and decode the same JSON (D-16)"
|
||||
- "Every new identifier is documented in modules/kiosk/README.md, modules/party/README.md and docs/plugins/marketplace.md, the root README table lists kiosk, and the docs checker passes"
|
||||
artifacts:
|
||||
- path: "modules/kiosk/manifest.go"
|
||||
provides: "plugin.yaml types, Parse, Marshal, Validate"
|
||||
contains: "SchemaVersion"
|
||||
- path: "modules/kiosk/identity.go"
|
||||
provides: "const-resolving ID()/Requires() reader"
|
||||
contains: "func ReadIdentity("
|
||||
- path: "modules/kiosk/check.go"
|
||||
provides: "Write, Check, StaleError, CheckT"
|
||||
contains: "func CheckT("
|
||||
- path: "modules/kiosk/registry.go"
|
||||
provides: "registry v1 wire types"
|
||||
contains: "RegistryAPIPath"
|
||||
- path: "modules/kiosk/version.go"
|
||||
provides: "strict vX.Y.Z rules"
|
||||
contains: "func CompareVersions("
|
||||
- path: "modules/party/info.go"
|
||||
provides: "party.Info, party.Installed, party.Describe"
|
||||
contains: "func Describe("
|
||||
- path: "internal/build/plugindirs.go"
|
||||
provides: "go list plugin dir resolver, git describe, EffectiveFramework"
|
||||
contains: "func EffectiveFramework("
|
||||
- path: "modules/kiosk/manifest_smoke_test.go"
|
||||
provides: "TestManifestSmoke (tracer)"
|
||||
contains: "func TestManifestSmoke("
|
||||
- path: "internal/build/plugininfo_smoke_test.go"
|
||||
provides: "TestPluginInfoSmoke"
|
||||
contains: "func TestPluginInfoSmoke("
|
||||
- path: "docs/plugins/marketplace.md"
|
||||
provides: "plugin.yaml, plugin:manifest, registry API reference"
|
||||
contains: "plugin:manifest"
|
||||
key_links:
|
||||
- from: "modules/kiosk/identity.go"
|
||||
to: "internal/build/scaffold.go"
|
||||
via: "pluginIDFromGo delegates to kiosk.ReadIdentity, so plugin:add and plugin:manifest read IDs the same way"
|
||||
pattern: "kiosk\\.ReadIdentity"
|
||||
- from: "internal/build/build.go"
|
||||
to: "modules/party/info.go"
|
||||
via: "generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before party.Activate"
|
||||
pattern: "party\\.Describe\\("
|
||||
- from: "cmd/summer/main.go"
|
||||
to: "modules/kiosk/check.go"
|
||||
via: "plugin:manifest runs kiosk.Write, kiosk.Check or kiosk.Generate"
|
||||
pattern: "kiosk\\.(Write|Check|Generate)\\("
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
People browse the public Golem15 plugins at summercms.io/marketplace and add one to an application with `summer plugin:install <id>`; the same command installs private plugins from a git URL; the admin SPA gains a read-only Plugins screen. This plan lays the shared foundation every other plan of Phase 34 builds on: the plugin.yaml format and its generator, the registry v1 contract in Go, and the build-time metadata the admin's Installed tab reads.
|
||||
|
||||
Spec-less probe fallback skipped: Phase 34 has no requirement IDs and no SPEC.md, so this plan set derives must_haves from ROADMAP success criteria SC-1..SC-5 and CONTEXT decisions D-01..D-19 (a visible choice, not an omission).
|
||||
|
||||
<objective>
|
||||
Create the public framework module `kiosk` (name confirmed at Task 1) and wire it through the CLI and the build:
|
||||
- the plugin.yaml format, generated by `summer plugin:manifest [dir] [--check] [--pretend] [--name] [--description]` from the plugin's own sources (D-01..D-05);
|
||||
- a static, const-following ID()/Requires() reader shared with `plugin:add` (user decision 1 of 2026-10-29);
|
||||
- `kiosk.CheckT`, the helper each plugin repo's `go test` calls (user decision 2);
|
||||
- the registry v1 wire types and strict version rules (D-16, confirmed at Task 1);
|
||||
- `summer build` baking `PluginInfo` and `FrameworkVersion` into plugins.gen.go and publishing them through `party.Describe` (D-06, D-07).
|
||||
|
||||
Purpose: plans 34-02 (plugin repo rollout), 34-03 (registry server), 34-04 (install CLI) and 34-05 (admin screen) all consume these types; defining them once keeps server, CLI and admin on one contract.
|
||||
|
||||
Output: commits in this repository only (code and its docs together, per commit group below). Nothing is pushed or tagged.
|
||||
|
||||
**Planner decisions recorded here (confirmed or adjusted at Task 1):**
|
||||
- **P34-01 plugin.yaml format** (D-04 costly): one file per plugin package directory (research F6). Header comment line `# Generated by summer plugin:manifest. Do not edit; rerun the command.` then, in this order: `schema: 1`, `id`, `module` (go.mod module path, what `go get` takes), `package` (import path summer.yaml lists; differs from module for a nested plugin such as `…/sm-pgstripe-plugin/subscriptions`), `name`, `description`, `framework` (go.mod require of git.golem15.com/golem15/summercms), `requires` (map of required plugin ID to its minimum, sorted by ID, omitted when empty; empty string when the required plugin ships in the same module or is not a go.mod require), `overrides` (`name`/`description` set with `--name`/`--description`, omitted when unused). A newer `schema` than the reader knows is an error that says to update summer.
|
||||
- **P34-02 registry API v1** (D-16 one-way): base path `/api/registry/v1`; `GET /plugins?q=&category=` → `{"data":[RegistryPlugin…],"meta":{"total":N}}`; `GET /plugins/{id}` → `{"data":RegistryPluginDetail}`; `GET /plugins/{id}/versions` → `{"data":[RegistryVersion…]}` newest first; errors `{"error":{"code":"…","message":"…"}}` (404 `not_found`, 400 `invalid_query`); `POST /ingest` (authenticated, body `{"repository":"<https url>"}`) → 202 `{"data":{"repository":"…","queued":true}}`. RegistryPlugin fields: `id`, `name`, `description`, `module`, `package`, `repository`, `category`, `featured`, `icon_url`, `latest_version`, `latest_framework`, `updated_at` (RFC 3339). RegistryPluginDetail adds `readme_markdown`, `readme_html`, `screenshots` (`[{url, caption}]`), `requires` (of the latest version). RegistryVersion: `version`, `framework`, `requires`, `module`, `package`, `commit`, `tagged_at`. Rules: always an envelope; clients ignore unknown fields; only additive changes within v1; GET-only, no credentials, open CORS, `Cache-Control: public, max-age=300` plus ETag.
|
||||
- **P34-03 names**: module `kiosk`; `party.Info{ID, Module, Package, Name, Description, Version, Source}` with Source `module` | `submodule` | `local`; `party.Installed{Framework string; Plugins []Info}`; `party.Describe(app, plugins, framework)`; generated `PluginInfo` and `FrameworkVersion`; config key `plugins.registry_url` (default `https://summercms.io`, empty disables); permission `backend.view_plugins`; admin route `/system/plugins`; marketplace URLs `/marketplace` and `/marketplace/<vendor>/<name>`; token scope `registry`.
|
||||
- **P34-04 release number**: the framework release that carries this phase is v0.6.0 (next minor after v0.5.3: a new module and three new commands); 34-02 writes it as each plugin's framework minimum (user decision 2), and plugin tags wait for that tag (user-gated).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
|
||||
@.planning/notes/2026-10-29-plugin-marketplace.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
internal/build/scaffold.go: pluginIDFromGo(path string) (string, error) accepts only a string literal returned by ID() (lines ~360-392); AddPlugin(ctx, startDir, pluginDir) (line 144) calls it; editAppModule writes -require=mod@v0.0.0 and a local replace; runGo(ctx, dir, args...) and applyGoWork(cmd, dir) (lines ~621-640); parseGoMod is a line scanner — do not extend it, use `go mod edit -json`.
|
||||
internal/build/build.go: App(ctx, appDir, out) → generate(appDir, m) → generateMain(m), generatePluginsGen(m), formatSource, writeIfChanged; generated main calls party.Activate(app, PluginIDs) (line ~125). internal/build/new.go:453 calls generate(target, m); goldens in internal/build/testdata/new*/ (rewrite with `go test ./internal/build -run TestNew -update`).
|
||||
internal/build/manifest.go: Manifest{Module, Binary, Plugins []Plugin{ID, Module}, Tags}, LoadManifest, SaveManifest, ValidatePluginID, validateModulePath (rejects a leading "-").
|
||||
modules/party/registry.go: Plugin interface {ID; Requires; Register; Boot}; Activated []Plugin published with app.Publish; Activate(app, ids).
|
||||
modules/backpack: app.Publish[T](v) error, app.Lookup[T]() (T, bool) (generic methods, Go 1.27).
|
||||
modules/bonfire: Command{Name, Description, Long, Examples, Args, Flags, Run}; Input.Flag(name) (string, bool) returns ("", true) for an explicitly empty --name=""; FlagBool(in, name) for Bare switches; Output has Success, Warning, Info, Println.
|
||||
goccy/go-yaml (already required) for YAML; github.com/yuin/goldmark (already required) for the README AST; no new dependency.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Each task ends with the commit listed below, staged by explicit path (never `-A` or `.`). Other sessions may hold uncommitted `.planning/STATE.md`, `.planning/state.json`, `.planning/ROADMAP.md`, `.planning/G15OFFICE.md` or cabana edits; leave them alone. Before editing `internal/build` or `cmd/summer`, run `git status --short internal/build cmd/summer modules/party` and stop if another session has uncommitted edits there.
|
||||
- Resume check: `git log --format=%s | grep -Fx '<subject>'`; present subjects mean the task is done: re-run its `<verify>` and continue with the next one.
|
||||
|
||||
| Task | Commit subject | Resume point |
|
||||
|------|----------------|--------------|
|
||||
| 1 | none (answer recorded in the SUMMARY) | answer recorded |
|
||||
| 2 | `feat(kiosk): plugin.yaml manifest and summer plugin:manifest` | TestManifestSmoke passes |
|
||||
| 3 | `feat(kiosk): resolve constant plugin IDs, overrides and the CheckT helper` | TestManifestSmoke/consts passes |
|
||||
| 4 | `feat(kiosk): registry v1 wire types and version rules` | TestManifestSmoke/registry passes |
|
||||
| 5 | `feat(build): bake plugin metadata and the framework version into plugins.gen.go` | TestPluginInfoSmoke passes |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 1: Confirm the plugin.yaml format, the /api/registry/v1 shape, the names and the release number before any code depends on them</name>
|
||||
<reversibility rating="one-way">Released summer binaries and cabana call /api/registry/v1 and published plugin tags carry plugin.yaml forever (tags are never moved, research Pitfall 7); a later change needs a v2 path and an ingester that reads both formats.</reversibility>
|
||||
<decision>Adopt P34-01 (plugin.yaml format), P34-02 (registry API v1 shape), P34-03 (names) and P34-04 (v0.6.0 as the framework minimum) as written in the objective, or adjust them.</decision>
|
||||
<context>
|
||||
D-04 rates the plugin.yaml format costly (every public repo, the ingester and git-URL installs read it) and D-16 rates the API one-way. Research marked the field names `schema`, `package`, `overrides` (A2), the JSON shape (A3), the module name `kiosk` (A10), `party.Info`/`party.Describe` (A8) and `backend.view_plugins` with `/system/plugins` (A6) as assumptions. 34-03 (server) and 34-04 (client) run in parallel after this plan and both encode the shape fixed here through kiosk's wire types, so this is the only point before both. The release number decides the `framework:` minimum 34-02 writes into 19 plugin go.mod files.
|
||||
</context>
|
||||
<options>
|
||||
<option id="as-planned">
|
||||
<name>Adopt P34-01..P34-04 as written</name>
|
||||
<pros>Matches the research recommendation; a schema field and an envelope leave room for additive change; one release (v0.6.0) carries the helper every plugin test imports</pros>
|
||||
<cons>Freezes field names now; every plugin moves its framework minimum to v0.6.0</cons>
|
||||
</option>
|
||||
<option id="adjust">
|
||||
<name>Adopt with the changes Jakub states (field names, JSON fields, module name, release number)</name>
|
||||
<pros>Names fixed before they become a contract</pros>
|
||||
<cons>The executor records the adjusted names in the SUMMARY; plans 34-02..34-06 use the SUMMARY's names wherever they differ from their text</cons>
|
||||
</option>
|
||||
<option id="no-schema-field">
|
||||
<name>Adopt without the `schema:` field and the `overrides:` block (overrides stored as plain name/description plus a marker comment)</name>
|
||||
<pros>Smaller file</pros>
|
||||
<cons>No versioning hook for a later format change; regeneration must parse comments to keep overrides</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply "as-planned", "adjust: <changes>" or "no-schema-field"</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 2: End-to-end "a plugin author runs summer plugin:manifest and gets a committed, checkable plugin.yaml" — literal-ID plugin only</name>
|
||||
<files>modules/kiosk/kiosk.go, modules/kiosk/manifest.go, modules/kiosk/derive.go, modules/kiosk/check.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/testdata/literal/go.mod, modules/kiosk/testdata/literal/plugin.go, modules/kiosk/testdata/literal/README.md, modules/kiosk/README.md, cmd/summer/main.go, README.md, docs/plugins/marketplace.md, docs/console/scaffolding.md</files>
|
||||
<read_first>
|
||||
- internal/build/scaffold.go (pluginIDFromGo ~360-392, runGo/applyGoWork ~621-640: exec style to copy)
|
||||
- internal/build/manifest.go (goccy YAML use, ValidatePluginID, validateModulePath)
|
||||
- cmd/summer/main.go (addPluginCommand ~381-415, toolCommands ~31)
|
||||
- cmd/summer/commands_documented_test.go (what TestCommandsDocumented demands of a command)
|
||||
- modules/fetchguard/README.md (module README structure to copy)
|
||||
- modules/breakwater/breakwater.go (goldmark usage)
|
||||
- docs/plugins/testing.md (front matter: title, description, section, order) and docs/console/scaffolding.md
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Pattern 1, F6, F7)
|
||||
</read_first>
|
||||
<action>
|
||||
Implement P34-01 as answered at Task 1, per D-01, D-02, D-04 and D-05, for a plugin whose ID() and Requires() return literals. Create the package `modules/kiosk` (package doc: plugin manifests and the plugin registry contract; it never names a consuming application):
|
||||
- `kiosk.go`: constants `FileName = "plugin.yaml"`, `SchemaVersion = 1`, `FrameworkModule = "git.golem15.com/golem15/summercms"`, `Header` (the generated-file comment line).
|
||||
- `manifest.go`: `Manifest` with yaml tags in P34-01 order, `Overrides{Name, Description}`, `Marshal(Manifest) ([]byte, error)` (Header plus goccy output; requires emitted as a goccy MapSlice sorted by ID so the bytes are stable), `Parse([]byte) (Manifest, error)`.
|
||||
- `derive.go`: `DeriveOptions{Name, Description *string}` (nil keeps the persisted override, a pointer to "" clears it), `Derive(ctx, dir string, opts DeriveOptions) (Manifest, []string, error)` returning warnings. It finds the module root by walking up from dir to the nearest go.mod, reads it with `go mod edit -json` (exec, cmd.Dir = module root, args after the subcommand, never a shell), sets module, package (module path plus the dir's slash path relative to the root), framework (the FrameworkModule require; missing or v0.0.0 is an error), reads `<dir>/plugin.go` for ID()/Requires() string literals (non-literal returns are the D-02 error naming the function; Task 3 widens this), reads `<dir>/README.md` with goldmark: name is the first H1's text, description the first paragraph after it (plain text, whitespace collapsed); a missing README or H1 is an error. A README H1 equal to the ID with no name override is an error whose message names `--name` (research F7). Overrides from an existing plugin.yaml are kept unless opts changes them.
|
||||
- `check.go`: `Generate(ctx, dir, opts) ([]byte, []string, error)`, `Write(ctx, dir, opts) (path string, warnings []string, err error)`, `Check(ctx, dir) error` returning `*StaleError{Path string; Missing bool; Want, Got []byte}` whose Error() names the path and the first differing line.
|
||||
- Fixture `testdata/literal` (module `example.test/sm-acme-plugin`, requires FrameworkModule at v0.5.3, plugin.go returning "acme.blog" and nil, README `# Blog` plus one sentence).
|
||||
- `manifest_smoke_test.go` with `TestManifestSmoke` subtest `literal`: copy the fixture to t.TempDir(), Write, assert every field and the header, Check returns nil, edit the README sentence and Check returns a *StaleError, delete plugin.yaml and Check reports Missing.
|
||||
- `cmd/summer/main.go`: `pluginManifestCommand()` registered in toolCommands after `addPluginCommand()`: Name `plugin:manifest`, Description, Long (what is derived from where, that plugin.yaml is committed and never hand-edited, --check exit status), Examples (plain, --check, --pretend, --name), optional Arg `dir` (default "."), Flags `check` and `pretend` (Bare, read with bonfire.FlagBool), `name`, `description` (read with in.Flag so an explicit empty value clears). --check rejects --name/--description; --pretend prints Generate's bytes and writes nothing; otherwise Write and print `Wrote <path>` with out.Success; warnings go to out.Warning.
|
||||
- Docs: `modules/kiosk/README.md` in the standard structure (H1, one-sentence summary, import line, `**Name:**` mnemonic for a beach kiosk where every plugin on the shelf carries its label, Overview, Features, Usage, API reference, CLI commands, Dependencies, Testing); a row in the root `README.md` module table reusing the summary; new page `docs/plugins/marketplace.md` (section plugins, an order after testing) with "The plugin.yaml manifest" and "Generating and checking it" sections; a `plugin:manifest` row in `docs/console/scaffolding.md`. Every identifier must exist (docs checker).
|
||||
Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/kiosk -run 'TestManifestSmoke' -count=1 -v 2>&1 | grep -E '^(--- |=== RUN|FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/literal" line</fails_when>
|
||||
<automated>go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree' -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL" or "unknown identifier" line in the output</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "SchemaVersion\|FrameworkModule" modules/kiosk/kiosk.go` prints at least two lines
|
||||
- `go doc ./modules/kiosk Manifest` lists Schema, ID, Module, Package, Name, Description, Framework, Requires and Overrides
|
||||
- `grep -n '"plugin:manifest"' cmd/summer/main.go` prints one line
|
||||
- `cp -r modules/kiosk/testdata/literal "$TMP/acme" && go run ./cmd/summer plugin:manifest "$TMP/acme" && go run ./cmd/summer plugin:manifest --check "$TMP/acme"` exits 0 and `$TMP/acme/plugin.yaml` starts with the header line and contains `id: acme.blog` (with TMP set to a scratch directory)
|
||||
- `grep -n "kiosk" README.md` prints the module table row; `grep -n "plugin:manifest" docs/console/scaffolding.md docs/plugins/marketplace.md` prints at least two lines
|
||||
</acceptance_criteria>
|
||||
<done>A plugin author can generate, preview and check plugin.yaml for a literal-ID plugin from the CLI, and the format is documented.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Every public plugin layout derives a valid plugin.yaml: constant IDs, nested plugins, overrides, version checks and the CheckT helper</name>
|
||||
<files>modules/kiosk/identity.go, modules/kiosk/derive.go, modules/kiosk/check.go, modules/kiosk/manifest.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/testdata/consts/go.mod, modules/kiosk/testdata/consts/plugin.go, modules/kiosk/testdata/consts/ids/ids.go, modules/kiosk/testdata/consts/nested/plugin.go, modules/kiosk/testdata/consts/nested/README.md, modules/kiosk/testdata/consts/README.md, modules/kiosk/testdata/other/go.mod, modules/kiosk/testdata/other/plugin.go, modules/kiosk/testdata/other/README.md, modules/kiosk/testdata/framework/go.mod, internal/build/scaffold.go, internal/build/build_test.go, modules/kiosk/README.md, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- modules/kiosk/derive.go and modules/kiosk/check.go (Task 2 output)
|
||||
- internal/build/scaffold.go (pluginIDFromGo and its callers: AddPlugin, make:* paths)
|
||||
- internal/build/build_test.go (TestMakeAndAddPluginOnHelloCopy: the temp-app pattern)
|
||||
- ../sm-mfa-plugin/plugin.go (same-file const), ../sm-paymentgateway-plugin/plugin.go and ../sm-paymentgateway-plugin/payments/service.go (cross-package const), ../sm-pgstripe-plugin/subscriptions/plugin.go (Requires with a same-module and an other-module const) — read only
|
||||
- ../sm-chatbots-plugin/go.mod (a plugin requirement at v0.0.0, research F8) — read only
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (F1, F6, F8, Pattern 2)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- consts fixture: ID() returning a same-package const resolves; Requires() returning a slice of a cross-package const (same module, `ids.Translate`) and a const from the `other` fixture module (resolved through `go list -m -json` via a local replace) resolves, and the other-module requirement's minimum is its go.mod require version
|
||||
- nested fixture `consts/nested`: package is `<module>/nested`, module is the root module, and a Requires() entry naming the root plugin of the same module gets the empty minimum
|
||||
- ID() returning a function call, a var or a concatenation with a non-const fails with an error containing "ID()"; the same for Requires() with "Requires()"
|
||||
- a plugin requirement at v0.0.0 fails with "not a released version"; a pseudo-version minimum passes and returns a warning
|
||||
- --name override persists across a regeneration without flags; an explicit empty name clears it and then an H1 equal to the ID fails with a message containing "--name"
|
||||
- CheckT on a stale fixture calls Fatalf once with the path; on a fresh one it does not
|
||||
- Validate rejects a bad ID, a module with a leading "-", a non-vX.Y.Z framework, a schema greater than SchemaVersion
|
||||
- plugin:add on a copy of examples/hello registers a fixture plugin whose ID() returns a const (regression for research F1); a literal-ID plugin registers exactly as before
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-02 as extended by user decision 1 (2026-10-29): add `identity.go` with `Identity{ID string; Requires []string}` and `ReadIdentity(ctx, dir string) (Identity, error)`. It parses the non-test .go files of dir with go/parser (never builds or runs them), finds the methods ID() and Requires() on any receiver, and resolves each returned expression statically: a string literal; an identifier naming a package-level const in any non-test file of the same directory (recursively, guarded against cycles); `pkg.Name` where the file's import (named imports honoured) is in the same module (directory = module root plus the import path suffix) or in another module (directory from `go list -m -json <module>` run in the module root, `.Dir`; with applyGoWork-style GOWORK handling left to Go's defaults). Requires() accepts nil, an empty slice, or a []string composite literal whose elements resolve the same way. Anything else is an error of the form `<file>: ID() must return a string literal or constant` (or `Requires()`).
|
||||
Switch Derive to ReadIdentity. Map each required ID to its providing module: the plugin's own module (any plugin.go directory under the module root) gives the empty minimum; otherwise each non-framework go.mod require is resolved with `go list -m -json` and its root identity read; a found ID takes that require's version; an ID found nowhere gets the empty minimum (soft dependency). A plugin requirement at v0.0.0 is an error ("not a released version", research F8); a pseudo-version is accepted with a warning. Add `Validate(Manifest) error` (ID via the same rule as internal/build ValidatePluginID, re-implemented in kiosk since internal/build cannot be imported; module and package paths reject a leading "-", spaces and backslashes; framework and non-empty minimums must be strict vX.Y.Z or a pseudo-version; schema between 1 and SchemaVersion) and call it from Parse and Generate.
|
||||
Overrides per D-03: `--name`/`--description` set `Overrides`; the top-level name/description are the effective values; regenerating without flags keeps Overrides; an explicit empty value removes that override.
|
||||
Add `TB interface{ Helper(); Fatalf(format string, args ...any) }` and `CheckT(t TB, dir string)`, which calls Check and fails with the StaleError text plus the command to rerun (`summer plugin:manifest <dir>`); kiosk must not import the testing package.
|
||||
Replace the body of `internal/build/scaffold.go` pluginIDFromGo with a call to kiosk.ReadIdentity on the plugin.go's directory (keep the function name and its error prefix "build:"); every caller keeps working.
|
||||
Fixtures: `testdata/consts` (module `example.test/sm-consts-plugin`, ID via `const PluginID`, Requires `ids.Translate` and `other.PluginID`, go.mod require and replace of `example.test/sm-other-plugin => ../other`, framework require v0.5.3), `testdata/consts/nested`, `testdata/other` (module `example.test/sm-other-plugin`, require v0.2.0 in consts' go.mod), and `testdata/framework` (a stub go.mod declaring the framework module path) that every fixture go.mod replaces the framework with, so `go list -m -json` runs offline; tests copy the testdata tree into t.TempDir() keeping the relative layout. Write the behavior subtests in TestManifestSmoke first (subtest names `consts`, `nested`, `errors`, `overrides`, `checkt`, `validate`) plus `TestAddPluginConstID` in internal/build/build_test.go, then implement.
|
||||
Document ReadIdentity, Validate, Overrides and CheckT in modules/kiosk/README.md and add a "Testing the manifest in your plugin" section with the one-line `kiosk.CheckT(t, ".")` test to docs/plugins/marketplace.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/kiosk ./internal/build -run 'TestManifestSmoke|TestAddPluginConstID|TestMakeAndAddPluginOnHelloCopy' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/consts" or "--- PASS: TestAddPluginConstID" line</fails_when>
|
||||
<automated>go run ./cmd/summer plugin:manifest --pretend --name=MFA ../sm-mfa-plugin 2>&1 | grep -E '^(id|module|framework):'</automated>
|
||||
<fails_when>non-zero exit, or fewer than three lines, or no "id: golem15.mfa" line</fails_when>
|
||||
<automated>go run ./cmd/summer plugin:manifest --pretend --name="Stripe subscriptions" ../sm-pgstripe-plugin/subscriptions 2>&1 | grep -E '^(id|package):|golem15\.'</automated>
|
||||
<fails_when>non-zero exit, or no "id: golem15.pgstripesubscriptions" line, or no "package: git.golem15.com/golem15/sm-pgstripe-plugin/subscriptions" line</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "func ReadIdentity(ctx context.Context, dir string) (Identity, error)" modules/kiosk/identity.go` prints one line
|
||||
- `grep -n "kiosk.ReadIdentity" internal/build/scaffold.go` prints at least one line
|
||||
- `grep -n "func CheckT(t TB, dir string)" modules/kiosk/check.go` prints one line; `go list -deps ./modules/kiosk | grep -x testing` prints nothing
|
||||
- `go run ./cmd/summer plugin:manifest --pretend ../sm-paymentgateway-plugin --name="Payment gateway"` prints `id: golem15.paymentgateway` (cross-package const)
|
||||
- `go run ./cmd/summer plugin:manifest --pretend ../sm-chatbots-plugin` exits non-zero with "not a released version" (research F8), unless the sibling has already been fixed
|
||||
- No file under ../sm-*-plugin is created or changed by this task (`git -C ../sm-mfa-plugin status --short` shows no plugin.yaml)
|
||||
</acceptance_criteria>
|
||||
<done>plugin:manifest derives a valid manifest for literal, constant, cross-package and nested plugins without running their code, plugin:add works for constant-ID plugins, and plugin repos have a one-line test helper.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: The registry v1 contract in Go: wire types, envelopes and strict version rules</name>
|
||||
<files>modules/kiosk/registry.go, modules/kiosk/version.go, modules/kiosk/manifest_smoke_test.go, modules/kiosk/README.md, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- the Task 1 answer (P34-02 as confirmed or adjusted)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md ("Proposed registry API shape", Anti-Patterns on git describe and semver)
|
||||
- modules/kiosk/manifest.go (Requires type to reuse)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- RegistryPlugin, RegistryPluginDetail and RegistryVersion marshal to exactly the P34-02 field names (golden JSON in the test) and unmarshal JSON carrying unknown extra fields without error
|
||||
- ValidVersion accepts v0.6.0 and v10.2.33, rejects 0.6.0, v0.6, v0.6.0-rc1, v0.6.0+meta and a leading "-"
|
||||
- CompareVersions orders v0.10.0 after v0.9.9 and returns 0 for equal versions
|
||||
- BaseVersion returns v0.3.1 for "v0.3.1-4-gabc1234" and for "v0.3.1", and "" for "abc1234" or ""
|
||||
- Satisfies(have, min) is true when min is "", true when have >= min, false when have < min, and treats a pseudo-version minimum vX.Y.(Z+1)-0.<ts>-<hash> as satisfied by vX.Y.(Z+1) and later
|
||||
- NewestCompatible(versions, framework) returns the newest RegistryVersion whose framework minimum Satisfies the given framework, and false when none does
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-16 and the Task 1 answer: add `registry.go` with `RegistryAPIPath = "/api/registry/v1"`, `RegistryPlugin`, `RegistryPluginDetail` (embeds RegistryPlugin plus ReadmeMarkdown, ReadmeHTML, Screenshots []RegistryScreenshot, Requires), `RegistryScreenshot{URL, Caption}`, `RegistryVersion`, `ListResponse{Data []RegistryPlugin; Meta ListMeta{Total int}}`, `DetailResponse`, `VersionsResponse`, `ErrorResponse{Error RegistryError{Code, Message}}`, `IngestRequest{Repository}`, `IngestResponse`, with JSON tags exactly as confirmed; time fields are time.Time encoded RFC 3339. These types are the contract: 34-03 encodes responses with them and 34-04/34-05 decode with them, so no other copy of the shape may exist.
|
||||
Add `version.go` with `ValidVersion(v string) bool` (strict `^v\d+\.\d+\.\d+$`), `CompareVersions(a, b string) int` (numeric per component; only defined for valid versions), `BaseVersion(describe string) string` (the vX.Y.Z prefix of a git describe string or ""), `Satisfies(have, min string) bool`, `NewestCompatible(versions []RegistryVersion, framework string) (RegistryVersion, bool)`. Never feed a describe string to the comparer (research anti-pattern). Write the behavior subtests (`registry`, `versions`) in TestManifestSmoke first, then implement. Document the types and functions in modules/kiosk/README.md and add a "Registry API v1" reference section (paths, envelopes, fields, compatibility rules) to docs/plugins/marketplace.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/kiosk -run 'TestManifestSmoke/(registry|versions)' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestManifestSmoke/registry" line</fails_when>
|
||||
<automated>go test ./cmd/summer -run TestDocsTree -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL" or "unknown identifier" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'RegistryAPIPath = "/api/registry/v1"' modules/kiosk/registry.go` prints one line
|
||||
- `grep -n 'json:"latest_version"\|json:"readme_html"\|json:"tagged_at"' modules/kiosk/registry.go` prints three lines (or the names confirmed at Task 1)
|
||||
- `grep -n "func NewestCompatible(" modules/kiosk/version.go` prints one line
|
||||
- docs/plugins/marketplace.md contains a "Registry API v1" heading and names `/api/registry/v1/plugins/{id}/versions`
|
||||
</acceptance_criteria>
|
||||
<done>The registry contract exists once, in Go, with version rules every consumer shares.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 5: summer build bakes plugin metadata and the effective framework version, and the app publishes them</name>
|
||||
<files>internal/build/plugindirs.go, internal/build/build.go, internal/build/new.go, internal/build/plugininfo_smoke_test.go, internal/build/testdata/new/main.go.golden, internal/build/testdata/new/plugins.gen.go.golden, internal/build/testdata/new-no-plugin/plugins.gen.go.golden, modules/party/info.go, modules/party/README.md, examples/hello/main.go, examples/hello/plugins.gen.go, docs/architecture/go-modules-and-workspaces.md, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- internal/build/build.go (whole file)
|
||||
- internal/build/new.go (~430-460, the generate call) and internal/build/new_golden_test.go (the -update flag)
|
||||
- internal/build/leaf.go (localModuleDir/defaultPluginDir: module-cache plugins are skipped there, no change needed)
|
||||
- internal/dev/watch.go (~74, build.App reuse)
|
||||
- modules/party/registry.go and modules/party/README.md
|
||||
- modules/bonfire/version.go (BuildVersion: what runtime already reports)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (F3, F5, Pattern 3, Pitfall 6)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- generatePluginsGen with fixed metadata emits `var PluginInfo = []party.Info{...}` in summer.yaml order with every string strconv-quoted and `const FrameworkVersion = "<v>"`, byte-stable across two calls
|
||||
- generateMain emits `party.Describe(app, PluginInfo, FrameworkVersion)` with the usual failure branch, before `party.Activate(app, PluginIDs)`
|
||||
- resolvePluginDirs on a temp app with one in-app plugin (no own repo) and one plugin directory that is its own git repo with tag v0.1.0 plus one commit returns Source local with empty version and Source submodule with a version starting "v0.1.0-1-g"
|
||||
- a plugin directory with uncommitted changes still reports the describe string without a dirty suffix
|
||||
- EffectiveFramework returns the newest reachable tag of a locally replaced framework checkout, and the go.mod require version when the framework is not replaced; when git fails it returns the require version and a warning
|
||||
- build.App on an app whose metadata resolution fails (go list error) still generates ID-only PluginInfo, prints one warning and builds
|
||||
- party.Describe publishes party.Installed so app.Lookup[party.Installed]() returns the plugins and framework; calling it with a nil app is an error
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-06 and D-07. In `modules/party/info.go` (additive, no change to Plugin or Activate): `Info{ID, Module, Package, Name, Description, Version, Source string}` with Source constants `SourceModule`, `SourceSubmodule`, `SourceLocal`; `Installed{Framework string; Plugins []Info}`; `Describe(app *backpack.App, plugins []Info, framework string) error` publishing Installed. Document them in modules/party/README.md.
|
||||
Add `internal/build/plugindirs.go`: `pluginMeta` resolution through one `go list -e -json=ImportPath,Dir,Module,Error <every summer.yaml module>` run in the app dir with applyGoWork (research F5); per plugin read `<Dir>/plugin.yaml` with kiosk.Parse when present (name, description, module, package; a parse error warns and is ignored); version: module-cache plugin → Module.Version; a local dir whose `git -C <dir> rev-parse --show-toplevel` equals the dir → `git describe --tags --match v[0-9]*` (no dirty flag, research Pitfall 6) and Source submodule; any other local dir → Source local and empty version. Never bake a filesystem path. Add exported `EffectiveFramework(ctx, appDir string) (string, error)` per research F3 (`go mod edit -json` of go.mod, `go work edit -json` of a go.work found by findGoWork; a local replace of kiosk.FrameworkModule → `git -C <dir> describe --tags --abbrev=0 --match v[0-9]*`; otherwise the require version; on git failure return the require version plus a non-nil warning error the caller prints).
|
||||
Change `generate` to take the resolved metadata (`buildMeta{Plugins []party.Info; Framework string}`): build.App resolves it once per call, prints warnings to out and falls back to ID-only entries on any failure (never fails the build); `generatePluginsGen` imports party and emits PluginInfo and FrameworkVersion after PluginIDs; `generateMain` adds the Describe call before Activate. new.go passes ID-only metadata with an empty FrameworkVersion (the first summer build fills it), which keeps the `summer new` goldens deterministic; regenerate the goldens with -update and review the diff. Regenerate examples/hello's main.go and plugins.gen.go with `go run ./cmd/summer build` run from examples/hello (or the package function) and commit them.
|
||||
Write `internal/build/plugininfo_smoke_test.go` (`TestPluginInfoSmoke`, subtests per behavior line, git fixtures created in t.TempDir with `git init` and a test-local identity; skip a subtest when git is not on PATH) first, then implement. Document the generated PluginInfo/FrameworkVersion in docs/architecture/go-modules-and-workspaces.md and an "Installed plugin metadata" section in docs/plugins/marketplace.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run 'TestPluginInfoSmoke|TestNew|TestGenerate' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginInfoSmoke" line</fails_when>
|
||||
<automated>cd examples/hello && go test ./... 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "func Describe(app \*backpack.App, plugins \[\]Info, framework string) error" modules/party/info.go` prints one line
|
||||
- `grep -n "party.Describe(app, PluginInfo, FrameworkVersion)" examples/hello/main.go` prints one line; `grep -n "var PluginInfo = \[\]party.Info" examples/hello/plugins.gen.go` prints one line
|
||||
- `grep -n "func EffectiveFramework(ctx context.Context, appDir string) (string, error)" internal/build/plugindirs.go` prints one line
|
||||
- `grep -n "describe" internal/build/plugindirs.go` shows `--tags` and `--match` arguments
|
||||
- `git show --stat <the Task 5 commit sha> -- internal/build/testdata` lists only the three goldens named in files
|
||||
- docs/architecture/go-modules-and-workspaces.md names PluginInfo and FrameworkVersion
|
||||
</acceptance_criteria>
|
||||
<done>Every built app carries its plugins' names, descriptions, versions and the framework version it was built against, ready for the admin's Installed tab.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| plugin source → kiosk reader | plugin.go, go.mod and README.md of any directory a developer points the CLI at |
|
||||
| CLI args → go/git subprocesses | dir paths and module paths reach `go mod edit`, `go list`, `git describe` |
|
||||
| plugin.yaml in a module-cache or submodule plugin → summer build | third-party text baked into generated Go source |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-01 | Elevation of privilege | kiosk.ReadIdentity | high | mitigate | AST-only: go/parser on source text, no `go run`, no build, no init(); TestManifestSmoke/errors proves non-const expressions are refused rather than evaluated |
|
||||
| T-34-02 | Tampering | exec of go/git with paths and module strings | high | mitigate | exec.CommandContext without a shell, fixed argument positions, module paths validated (leading "-" rejected) before use, `--` before positional git arguments where git accepts it |
|
||||
| T-34-03 | Tampering | plugin.yaml text in plugins.gen.go | high | mitigate | every baked string goes through strconv.Quote and formatSource; a malformed plugin.yaml is ignored with a warning (TestPluginInfoSmoke) |
|
||||
| T-34-04 | Information disclosure | plugins.gen.go | low | mitigate | only IDs, module paths, texts and versions are baked; no Dir or absolute path (acceptance grep in review) |
|
||||
| T-34-05 | Denial of service | summer build | medium | mitigate | metadata resolution failure degrades to ID-only and warns; build never fails on it (behavior test) |
|
||||
| T-34-06 | Repudiation | stale committed plugin.yaml | medium | mitigate | `--check` and kiosk.CheckT fail on any byte difference |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | this plan installs no package (research: no new dependency) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go vet ./... && go test -short ./...` green after every commit; `cd examples/hello && go test ./...` green after Task 5.
|
||||
- `go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree'` and `go run ./cmd/summer docs:build --check` pass.
|
||||
- `go run ./cmd/summer plugin:manifest --pretend …` derives manifests for ../sm-mfa-plugin, ../sm-paymentgateway-plugin and ../sm-pgstripe-plugin/subscriptions without writing into them.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- kiosk exists with manifest, identity, check, registry and version APIs, documented and in the root README table.
|
||||
- `summer plugin:manifest` generates, previews and checks plugin.yaml for every public plugin layout.
|
||||
- plugins.gen.go carries PluginInfo and FrameworkVersion; the generated main publishes them.
|
||||
- No push, no tag; no file in a sibling repository changed.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md` when done; record the Task 1 answer verbatim and every name that differs from this plan's text.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Module: `modules/kiosk` (README, root README row).
|
||||
- Types: `kiosk.Manifest`, `kiosk.Overrides`, `kiosk.DeriveOptions`, `kiosk.Identity`, `kiosk.StaleError`, `kiosk.TB`, `kiosk.RegistryPlugin`, `kiosk.RegistryPluginDetail`, `kiosk.RegistryScreenshot`, `kiosk.RegistryVersion`, `kiosk.ListResponse`, `kiosk.ListMeta`, `kiosk.DetailResponse`, `kiosk.VersionsResponse`, `kiosk.ErrorResponse`, `kiosk.RegistryError`, `kiosk.IngestRequest`, `kiosk.IngestResponse`, `party.Info`, `party.Installed`.
|
||||
- Functions: `kiosk.Marshal`, `kiosk.Parse`, `kiosk.Validate`, `kiosk.Derive`, `kiosk.Generate`, `kiosk.Write`, `kiosk.Check`, `kiosk.CheckT`, `kiosk.ReadIdentity`, `kiosk.ValidVersion`, `kiosk.CompareVersions`, `kiosk.BaseVersion`, `kiosk.Satisfies`, `kiosk.NewestCompatible`, `party.Describe`, `build.EffectiveFramework`.
|
||||
- Constants: `kiosk.FileName`, `kiosk.SchemaVersion`, `kiosk.FrameworkModule`, `kiosk.Header`, `kiosk.RegistryAPIPath`, `party.SourceModule`, `party.SourceSubmodule`, `party.SourceLocal`.
|
||||
- Generated code: `PluginInfo` and `FrameworkVersion` in every app's plugins.gen.go; `party.Describe(app, PluginInfo, FrameworkVersion)` in generated main.
|
||||
- CLI: `summer plugin:manifest [dir]` with `--check`, `--pretend`, `--name`, `--description`.
|
||||
- Files: `plugin.yaml` format (schema 1); `docs/plugins/marketplace.md`; tests `TestManifestSmoke`, `TestPluginInfoSmoke`, `TestAddPluginConstID`.
|
||||
- Config keys: none in this plan (`plugins.registry_url` arrives in 34-04).
|
||||
|
||||
## Phase-wide source coverage audit
|
||||
|
||||
| Source | ID | Item | Plan | Status |
|
||||
|--------|----|------|------|--------|
|
||||
| GOAL | — | Browse at summercms.io/marketplace, install with one command, private git URLs, read-only admin Plugins screen | 01-06 | COVERED |
|
||||
| REQ | — | none (Requirements: TBD); SC-1..SC-5 used instead | — | n/a |
|
||||
| ROADMAP | SC-1 | plugin.yaml in every public sm-*-plugin repo, validated, anonymous fetch | 01, 02 | COVERED |
|
||||
| ROADMAP | SC-2 | ingest on tag, registry API, /marketplace list/search/category/detail, presentation fields in admin | 03 | COVERED |
|
||||
| ROADMAP | SC-3 | plugin:install ID (module, --source) and git URL, requirement checks, plugin:add registration | 04 | COVERED |
|
||||
| ROADMAP | SC-4 | admin Plugins screen Installed + Marketplace with copy | 01, 05 | COVERED |
|
||||
| ROADMAP | SC-5 | unit tests in the last plan | 06 | COVERED |
|
||||
| CONTEXT | D-01..D-05 | manifest generation, derivation, overrides, --check, minimums | 01, 02 | COVERED |
|
||||
| CONTEXT | D-06, D-07 | build metadata and versions | 01 | COVERED |
|
||||
| CONTEXT | D-08..D-12 | install resolution, deps + --yes, plugin:update, git URL submodule-only, --offline | 04 | COVERED |
|
||||
| CONTEXT | D-13..D-16 | pull ingester + ingest-now, admin-curated listing, version rows, public API v1 | 03 (contract types in 01) | COVERED |
|
||||
| CONTEXT | D-17..D-19 | cabana endpoints, plugins.registry_url, permission | 04 (config), 05 | COVERED |
|
||||
| RESEARCH | F1 | const IDs (user decision 1) | 01 | COVERED |
|
||||
| RESEARCH | F2 | go.work replace overrides for submodule installs | 04 | COVERED |
|
||||
| RESEARCH | F3 | effective framework version | 01, 04 | COVERED |
|
||||
| RESEARCH | F4 | ConfirmProceed auto-confirms; use Interactive + Confirm | 04 | COVERED |
|
||||
| RESEARCH | F5 | go list dir resolution | 01 | COVERED |
|
||||
| RESEARCH | F6 | plugin.yaml per plugin package dir | 01, 02 | COVERED |
|
||||
| RESEARCH | F7 | --name where README H1 == ID | 01, 02 | COVERED |
|
||||
| RESEARCH | F8 | v0.0.0 plugin requirements | 01, 02 | COVERED |
|
||||
| RESEARCH | F9 | untagged repos not listed; anonymous fetch | 02, 03 | COVERED |
|
||||
| RESEARCH | F10 | nginx, work_in_serve, nodePaths, app regeneration | 03, 06 (ledger) | COVERED |
|
||||
| RESEARCH | Pitfall 4 | summer.yaml comments preserved (user decision 5) | 04 | COVERED |
|
||||
| RESEARCH | Pitfall 13 | ingest identity checks (ID unique, module == repo path) | 03 | COVERED |
|
||||
| RESEARCH | OQ 3/6 | launch list = tagged sibling plugins; --source in a non-git app refuses (user decision 5) | 03, 04, 06 | COVERED |
|
||||
| CONTEXT | Deferred | --module for git URLs; paid plugins, themes, admin installs | — | excluded (deferred) |
|
||||
331
.planning/phases/34-plugin-marketplace/34-02-PLAN.md
Normal file
331
.planning/phases/34-plugin-marketplace/34-02-PLAN.md
Normal file
@@ -0,0 +1,331 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["34-01", "34-03", "34-05"]
|
||||
files_modified:
|
||||
- ../sm-journal-plugin/plugin.yaml
|
||||
- ../sm-journal-plugin/manifest_test.go
|
||||
- ../sm-journal-plugin/go.mod
|
||||
- ../sm-journal-plugin/go.sum
|
||||
- ../sm-chat-plugin/plugin.yaml
|
||||
- ../sm-chat-plugin/manifest_test.go
|
||||
- ../sm-chat-plugin/go.mod
|
||||
- ../sm-faq-plugin/plugin.yaml
|
||||
- ../sm-faq-plugin/manifest_test.go
|
||||
- ../sm-faq-plugin/go.mod
|
||||
- ../sm-menu-plugin/plugin.yaml
|
||||
- ../sm-menu-plugin/manifest_test.go
|
||||
- ../sm-menu-plugin/go.mod
|
||||
- ../sm-mfa-plugin/plugin.yaml
|
||||
- ../sm-mfa-plugin/manifest_test.go
|
||||
- ../sm-mfa-plugin/go.mod
|
||||
- ../sm-oauthserver-plugin/plugin.yaml
|
||||
- ../sm-oauthserver-plugin/manifest_test.go
|
||||
- ../sm-oauthserver-plugin/go.mod
|
||||
- ../sm-paymentgateway-plugin/plugin.yaml
|
||||
- ../sm-paymentgateway-plugin/manifest_test.go
|
||||
- ../sm-paymentgateway-plugin/go.mod
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/plugin.yaml
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/manifest_test.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/go.mod
|
||||
- ../sm-pgstripe-plugin/plugin.yaml
|
||||
- ../sm-pgstripe-plugin/manifest_test.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/plugin.yaml
|
||||
- ../sm-pgstripe-plugin/subscriptions/manifest_test.go
|
||||
- ../sm-pgstripe-plugin/go.mod
|
||||
- ../sm-seo-plugin/plugin.yaml
|
||||
- ../sm-seo-plugin/manifest_test.go
|
||||
- ../sm-seo-plugin/go.mod
|
||||
- ../sm-sitemap-plugin/plugin.yaml
|
||||
- ../sm-sitemap-plugin/manifest_test.go
|
||||
- ../sm-sitemap-plugin/go.mod
|
||||
- ../sm-sociallogin-plugin/plugin.yaml
|
||||
- ../sm-sociallogin-plugin/manifest_test.go
|
||||
- ../sm-sociallogin-plugin/go.mod
|
||||
- ../sm-translate-plugin/plugin.yaml
|
||||
- ../sm-translate-plugin/manifest_test.go
|
||||
- ../sm-translate-plugin/go.mod
|
||||
- ../sm-user-plugin/plugin.yaml
|
||||
- ../sm-user-plugin/manifest_test.go
|
||||
- ../sm-user-plugin/go.mod
|
||||
- ../sm-chatbots-plugin/plugin.yaml
|
||||
- ../sm-chatbots-plugin/manifest_test.go
|
||||
- ../sm-chatbots-plugin/go.mod
|
||||
- ../sm-chatchannels-plugin/plugin.yaml
|
||||
- ../sm-chatchannels-plugin/manifest_test.go
|
||||
- ../sm-chatchannels-plugin/go.mod
|
||||
- ../sm-chatvideo-plugin/plugin.yaml
|
||||
- ../sm-chatvideo-plugin/manifest_test.go
|
||||
- ../sm-chatvideo-plugin/go.mod
|
||||
- ../sm-forum-plugin/plugin.yaml
|
||||
- ../sm-forum-plugin/manifest_test.go
|
||||
- ../sm-forum-plugin/go.mod
|
||||
- ../sm-herald-plugin/plugin.yaml
|
||||
- ../sm-herald-plugin/manifest_test.go
|
||||
- ../sm-herald-plugin/go.mod
|
||||
- ../sm-userfriends-plugin/plugin.yaml
|
||||
- ../sm-userfriends-plugin/manifest_test.go
|
||||
- ../sm-userfriends-plugin/go.mod
|
||||
autonomous: false
|
||||
requirements: [SC-1, D-01, D-02, D-03, D-04, D-05]
|
||||
|
||||
estimate:
|
||||
tokens: 90000
|
||||
raw_tokens: 90000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every public sm-*-plugin repository checked out next to the framework (20 repositories, 21 plugin packages including sm-pgstripe-plugin/subscriptions) commits a plugin.yaml generated by `summer plugin:manifest`, never hand-edited (SC-1, D-01, D-02)"
|
||||
- "Each plugin.yaml carries id, module, package, name, description, framework and requires derived from the repo; READMEs whose H1 is the plugin ID get a persisted `--name` override, the others keep their README H1 (D-02, D-03, research F7)"
|
||||
- "Each repository's go.mod requires the framework release that contains kiosk (v0.6.0 or the number confirmed at 34-01 Task 1), which therefore is its `framework:` minimum (user decision 2 of 2026-10-29, D-05)"
|
||||
- "Each repository's `go test` runs TestPluginManifest, which calls kiosk.CheckT and fails when plugin.yaml is stale (D-04)"
|
||||
- "A plugin requirement at v0.0.0 (sm-chatbots-plugin on sm-chat-plugin) is replaced by a released version, or the repository is left without plugin.yaml and the reason is recorded (research F8)"
|
||||
- "Each listed repository answers an anonymous `git ls-remote` over HTTPS (SC-1)"
|
||||
- "No plugin repository is pushed or tagged by this plan; plugin commits come first and app submodule pointer bumps are separate commits made only after Jakub reports the push (CLAUDE.md, user decision 2)"
|
||||
artifacts:
|
||||
- path: "../sm-journal-plugin/plugin.yaml"
|
||||
provides: "tracer manifest"
|
||||
contains: "id: golem15.journal"
|
||||
- path: "../sm-journal-plugin/manifest_test.go"
|
||||
provides: "TestPluginManifest"
|
||||
contains: "kiosk.CheckT"
|
||||
- path: "../sm-pgstripe-plugin/subscriptions/plugin.yaml"
|
||||
provides: "nested plugin manifest"
|
||||
contains: "package: git.golem15.com/golem15/sm-pgstripe-plugin/subscriptions"
|
||||
key_links:
|
||||
- from: "../sm-*-plugin/manifest_test.go"
|
||||
to: "modules/kiosk/check.go"
|
||||
via: "TestPluginManifest calls kiosk.CheckT(t, \".\") against the local framework through the repo's replace"
|
||||
pattern: "kiosk\\.CheckT\\(t, \"\\.\"\\)"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
People browse the public Golem15 plugins at summercms.io/marketplace and install one with `summer plugin:install <id>`. For the registry to list a plugin, its repository must carry a valid plugin.yaml at a tag. This plan puts a generated, test-guarded plugin.yaml into every public plugin repository; the tags that publish them are user-gated and follow the framework release.
|
||||
|
||||
<objective>
|
||||
Roll out plugin.yaml and its check test to every public `sm-*-plugin` repository checked out next to the framework (user decision 5: these are the launch set; quizzes, presentations, golem and feedback have no sibling checkout and are out of this phase), using the 34-01 command. Untagged repositories (chatbots, chatchannels, chatvideo, forum, herald, userfriends) get plugin.yaml too but are not listed until Jakub tags them (D-14).
|
||||
|
||||
Purpose: SC-1 and the input the registry ingester (34-03) and git-URL installs (34-04) read.
|
||||
|
||||
Output: one commit per plugin repository (plugin.yaml, manifest_test.go, go.mod/go.sum), and, only after Jakub pushes, one pointer-bump commit per app. No push, no tag.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-03-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-05-SUMMARY.md
|
||||
@.planning/notes/core-plugins-own-repos.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
From 34-01: `summer plugin:manifest [dir] [--check] [--pretend] [--name=…] [--description=…]`; `kiosk.CheckT(t TB, dir string)` (TB = Helper + Fatalf); plugin.yaml schema 1 per plugin package directory. Use the names in 34-01-SUMMARY.md where Task 1 adjusted them.
|
||||
Every sibling plugin go.mod replaces git.golem15.com/golem15/summercms with ../summercms.go, so a require of the unreleased framework version builds locally against this checkout.
|
||||
README H1 equals the plugin ID in faq, journal, menu, mfa, oauthserver, paymentgateway, paymentgatewaysubscriptions, pgstripe, pgstripe/subscriptions, seo, sitemap, sociallogin, translate, user (research F7). Human H1 in chat ("Chat"), chatbots ("Chat bots"), chatchannels ("ChatChannels"), chatvideo ("Chat video"), forum ("Forum"), herald ("Herald"), userfriends ("Friends").
|
||||
sm-chatbots-plugin/go.mod requires git.golem15.com/golem15/sm-chat-plugin v0.0.0 with a local replace (research F8); sm-chat-plugin's newest tag is v0.1.3.
|
||||
Apps mounting these plugins as submodules: sm-demo-app, sm-grzybyfunkcjonalne-app, sm-bm-app, sm-summercmsio-app, fonoteka.go.
|
||||
Wave order: this plan runs after 34-03 and 34-05 (declared in depends_on) because every plugin test compiles kiosk and most compile cabana from ../summercms.go, which 34-04 and 34-05 edit, and because Task 5 commits gitlinks in sm-summercmsio-app, where 34-03 commits. 34-03-SUMMARY.md lists the unpushed site plugin (sm-summercmsio-plugin) and vue-summercmsio-app commits whose gitlinks sm-summercmsio-app still has to record.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Build the CLI once from this checkout into the executor's scratch directory (`go build -o <scratch>/summer ./cmd/summer`) and call that binary; never `go install` over Jakub's installed summer.
|
||||
- Per repository: `git -C <repo> status --short` first. If another session has uncommitted changes in go.mod, go.sum, plugin.go or README.md, skip the repository, record it in the SUMMARY and retry it at the end of the task. Stage only the files named here, by path. No co-author tag.
|
||||
- Commit subject in each plugin repository: `feat: plugin.yaml manifest for the SummerCMS marketplace`. Resume check per repository: `git -C <repo> log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commits | Resume point |
|
||||
|------|---------|--------------|
|
||||
| 1 | sm-journal-plugin | TestPluginManifest passes in sm-journal-plugin |
|
||||
| 2 | 13 tagged repositories (pgstripe once, both plugin dirs) | every listed repo's TestPluginManifest passes |
|
||||
| 3 | 6 untagged repositories | same, or the recorded F8 skip |
|
||||
| 4 | none (answer recorded in the SUMMARY) | answer recorded |
|
||||
| 5 | one `chore: bump plugin pointers for plugin.yaml` per app, or none when deferred | pointers recorded |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "sm-journal-plugin commits a generated plugin.yaml that its own go test guards" — one repository only</name>
|
||||
<files>../sm-journal-plugin/plugin.yaml, ../sm-journal-plugin/manifest_test.go, ../sm-journal-plugin/go.mod, ../sm-journal-plugin/go.sum</files>
|
||||
<read_first>
|
||||
- .planning/phases/34-plugin-marketplace/34-01-SUMMARY.md (confirmed names, release number, anything adjusted)
|
||||
- modules/kiosk/README.md (CheckT usage)
|
||||
- ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/go.mod, ../sm-journal-plugin/README.md (H1 and summary)
|
||||
- ../sm-faq-plugin/commands_documented_test.go (the plugin-test style that imports a framework helper)
|
||||
</read_first>
|
||||
<action>
|
||||
Per D-01..D-05 and user decision 2. In ../sm-journal-plugin: set the framework requirement to the release number confirmed at 34-01 Task 1 with `go -C ../sm-journal-plugin mod edit -require=git.golem15.com/golem15/summercms@<release>`; add `manifest_test.go` in the package of plugin.go with `func TestPluginManifest(t *testing.T) { kiosk.CheckT(t, ".") }` and the kiosk import; run `go -C ../sm-journal-plugin mod tidy` and review the go.mod diff (new indirect requirements for kiosk's dependencies are expected; if tidy removes or downgrades an existing requirement, stop and record it); run `<scratch>/summer plugin:manifest --name=Journal ../sm-journal-plugin` (README H1 is the ID). Confirm the test guards the file: change one word of plugin.yaml's description, run TestPluginManifest and see it fail, then restore with `git -C ../sm-journal-plugin checkout -- plugin.yaml` or by regenerating. Commit plugin.yaml, manifest_test.go, go.mod and go.sum in ../sm-journal-plugin.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-journal-plugin test -run TestPluginManifest -count=1 -v . 2>&1 | grep -E '^(--- |FAIL|ok|PASS)'</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginManifest" line</fails_when>
|
||||
<automated>go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -short ./... 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
<automated>go run ./cmd/summer plugin:manifest --check ../sm-journal-plugin && grep -E '^(id|name|framework):' ../sm-journal-plugin/plugin.yaml</automated>
|
||||
<fails_when>non-zero exit, or no "id: golem15.journal" and "name: Journal" lines</fails_when>
|
||||
<automated>GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-journal-plugin.git | head -1</automated>
|
||||
<fails_when>non-zero exit, "Authentication failed" in the output, or empty output</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "git.golem15.com/golem15/summercms v0.6.0" ../sm-journal-plugin/go.mod` prints one line (or the confirmed release number)
|
||||
- `grep -n "framework: v0.6.0" ../sm-journal-plugin/plugin.yaml` prints one line and `grep -n "golem15.translate" ../sm-journal-plugin/plugin.yaml` prints the requirement with a vX.Y.Z minimum
|
||||
- `grep -n "overrides:" ../sm-journal-plugin/plugin.yaml` prints one line
|
||||
- `git -C ../sm-journal-plugin log -1 --format=%s` is the subject from Execution units and `git -C ../sm-journal-plugin show --stat HEAD` lists only the four files
|
||||
- `git -C ../sm-journal-plugin tag --points-at HEAD` prints nothing (no tag)
|
||||
</acceptance_criteria>
|
||||
<done>One real plugin repository carries a generated, test-guarded plugin.yaml and builds against the local framework.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: The 13 other tagged plugin repositories carry plugin.yaml and TestPluginManifest</name>
|
||||
<files>../sm-chat-plugin/plugin.yaml, ../sm-chat-plugin/manifest_test.go, ../sm-chat-plugin/go.mod, ../sm-faq-plugin/plugin.yaml, ../sm-faq-plugin/manifest_test.go, ../sm-faq-plugin/go.mod, ../sm-menu-plugin/plugin.yaml, ../sm-menu-plugin/manifest_test.go, ../sm-menu-plugin/go.mod, ../sm-mfa-plugin/plugin.yaml, ../sm-mfa-plugin/manifest_test.go, ../sm-mfa-plugin/go.mod, ../sm-oauthserver-plugin/plugin.yaml, ../sm-oauthserver-plugin/manifest_test.go, ../sm-oauthserver-plugin/go.mod, ../sm-paymentgateway-plugin/plugin.yaml, ../sm-paymentgateway-plugin/manifest_test.go, ../sm-paymentgateway-plugin/go.mod, ../sm-paymentgatewaysubscriptions-plugin/plugin.yaml, ../sm-paymentgatewaysubscriptions-plugin/manifest_test.go, ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-pgstripe-plugin/plugin.yaml, ../sm-pgstripe-plugin/manifest_test.go, ../sm-pgstripe-plugin/subscriptions/plugin.yaml, ../sm-pgstripe-plugin/subscriptions/manifest_test.go, ../sm-pgstripe-plugin/go.mod, ../sm-seo-plugin/plugin.yaml, ../sm-seo-plugin/manifest_test.go, ../sm-seo-plugin/go.mod, ../sm-sitemap-plugin/plugin.yaml, ../sm-sitemap-plugin/manifest_test.go, ../sm-sitemap-plugin/go.mod, ../sm-sociallogin-plugin/plugin.yaml, ../sm-sociallogin-plugin/manifest_test.go, ../sm-sociallogin-plugin/go.mod, ../sm-translate-plugin/plugin.yaml, ../sm-translate-plugin/manifest_test.go, ../sm-translate-plugin/go.mod, ../sm-user-plugin/plugin.yaml, ../sm-user-plugin/manifest_test.go, ../sm-user-plugin/go.mod</files>
|
||||
<read_first>
|
||||
- ../sm-journal-plugin/manifest_test.go (Task 1, the template)
|
||||
- each repository's plugin.go (package name, ID/Requires form) and README.md (H1) before generating
|
||||
- ../sm-pgstripe-plugin/subscriptions/plugin.go (nested plugin, package name)
|
||||
</read_first>
|
||||
<action>
|
||||
Repeat Task 1 for each repository, in dependency order so a required plugin's repository is done before its dependants: translate, user, chat, faq, menu, seo, sitemap, mfa, oauthserver, sociallogin, paymentgateway, paymentgatewaysubscriptions, pgstripe. Names per D-03 for READMEs whose H1 is the ID (Claude's discretion, reversible with a later `--name`): faq "FAQ", menu "Menu", mfa "Multi-factor authentication", oauthserver "OAuth server", paymentgateway "Payment gateway", paymentgatewaysubscriptions "Payment gateway subscriptions", pgstripe "Stripe payments", pgstripe/subscriptions "Stripe subscriptions", seo "SEO", sitemap "Sitemap", sociallogin "Social login", translate "Translate", user "User"; chat keeps its README H1 (no override). In sm-pgstripe-plugin generate both `plugin.yaml` (root) and `subscriptions/plugin.yaml` (research F6), add a manifest test in each of the two packages (the subscriptions test calls `kiosk.CheckT(t, ".")` from its own directory), and commit both in one commit. Stage go.sum too where tidy changed it (it is not listed per repository above when unchanged). Record in the SUMMARY a table of repository, plugin ID, name, framework minimum, requires and commit SHA.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do go -C ../sm-$r-plugin test -run TestPluginManifest -count=1 ./... >/dev/null 2>&1 || echo "FAIL $r"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <repo>" line before "done"</fails_when>
|
||||
<automated>for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do (go -C ../sm-$r-plugin vet ./... && go -C ../sm-$r-plugin test -short ./...) >/dev/null 2>&1 || echo "FAIL $r"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <repo>" line before "done"</fails_when>
|
||||
<automated>for r in chat faq menu mfa oauthserver paymentgateway paymentgatewaysubscriptions pgstripe seo sitemap sociallogin translate user; do GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-$r-plugin.git >/dev/null 2>&1 || echo "FAIL $r"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <repo>" line before "done"</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ls ../sm-*-plugin/plugin.yaml ../sm-pgstripe-plugin/subscriptions/plugin.yaml | wc -l` is at least 15 after this task (journal plus these 13 plus subscriptions)
|
||||
- `grep -h '^id:' ../sm-{chat,faq,menu,mfa,oauthserver,paymentgateway,paymentgatewaysubscriptions,pgstripe,seo,sitemap,sociallogin,translate,user}-plugin/plugin.yaml | sort -u | wc -l` is 13
|
||||
- `grep -L '^name: golem15\.' ../sm-*-plugin/plugin.yaml` lists every plugin.yaml (no display name equals an ID)
|
||||
- every repository's latest commit has the Execution units subject and no tag points at it
|
||||
</acceptance_criteria>
|
||||
<done>Every tagged public plugin is ready to publish its manifest at its next tag.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: The 6 untagged plugin repositories carry plugin.yaml, with the v0.0.0 chat requirement resolved</name>
|
||||
<files>../sm-chatbots-plugin/plugin.yaml, ../sm-chatbots-plugin/manifest_test.go, ../sm-chatbots-plugin/go.mod, ../sm-chatchannels-plugin/plugin.yaml, ../sm-chatchannels-plugin/manifest_test.go, ../sm-chatchannels-plugin/go.mod, ../sm-chatvideo-plugin/plugin.yaml, ../sm-chatvideo-plugin/manifest_test.go, ../sm-chatvideo-plugin/go.mod, ../sm-forum-plugin/plugin.yaml, ../sm-forum-plugin/manifest_test.go, ../sm-forum-plugin/go.mod, ../sm-herald-plugin/plugin.yaml, ../sm-herald-plugin/manifest_test.go, ../sm-herald-plugin/go.mod, ../sm-userfriends-plugin/plugin.yaml, ../sm-userfriends-plugin/manifest_test.go, ../sm-userfriends-plugin/go.mod</files>
|
||||
<read_first>
|
||||
- ../sm-chatbots-plugin/go.mod (the v0.0.0 chat requirement and its replace)
|
||||
- ../sm-userfriends-plugin/go.mod (pseudo-version requirement, accepted with a warning)
|
||||
- each repository's plugin.go and README.md
|
||||
</read_first>
|
||||
<action>
|
||||
Repeat Task 1 for chatchannels, chatvideo, forum, herald, userfriends and chatbots (no name overrides: their H1s are human names). For sm-chatbots-plugin (research F8): set the sm-chat-plugin requirement to v0.1.3 (its newest tag) with `go mod edit -require`, keep the local replace, and confirm chatbots still builds against v0.1.3's API by building in a scratch copy with the chat replace dropped (`GOFLAGS=-mod=mod`, network allowed for the proxy). If it does not build against v0.1.3, restore go.mod, leave chatbots without plugin.yaml, and record in the SUMMARY that chatbots needs a sm-chat-plugin tag before it can publish (it is untagged and unlisted, so SC-1 lists it as pending, not failed). Pseudo-version warnings (userfriends) are expected; record them.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>for r in chatbots chatchannels chatvideo forum herald userfriends; do test -f ../sm-$r-plugin/plugin.yaml || { echo "SKIP $r"; continue; }; (go -C ../sm-$r-plugin vet ./... && go -C ../sm-$r-plugin test -short ./... && go -C ../sm-$r-plugin test -run TestPluginManifest -count=1 ./...) >/dev/null 2>&1 || echo "FAIL $r"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <repo>" line, or a "SKIP" line for a repository other than chatbots</fails_when>
|
||||
<automated>for r in chatbots chatchannels chatvideo forum herald userfriends; do GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --heads https://git.golem15.com/golem15/sm-$r-plugin.git >/dev/null 2>&1 || echo "FAIL $r"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <repo>" line before "done"</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ls ../sm-*-plugin/plugin.yaml | wc -l` is 20, or 19 with the chatbots skip recorded in the SUMMARY with its build error
|
||||
- `grep -n "sm-chat-plugin v0.0.0" ../sm-chatbots-plugin/go.mod` prints nothing when chatbots has a plugin.yaml
|
||||
- no repository is tagged by this task (`git -C ../sm-forum-plugin tag` prints nothing)
|
||||
</acceptance_criteria>
|
||||
<done>All public plugin repositories carry plugin.yaml (or a recorded, explained exception) and are ready for their first tag.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 4: Push the framework and plugin commits now, or defer the app pointer bumps to the release</name>
|
||||
<decision>Jakub pushes (no tags) the framework master (34-01, 34-04 and 34-05 commits), the plugin repositories committed in Tasks 1-3, and the sm-summercmsio-plugin and vue-summercmsio-app commits from 34-03, so the apps' submodule pointers can be bumped in this plan; or the pointer bumps move to the release ledger 34-06 writes.</decision>
|
||||
<context>
|
||||
CLAUDE.md: plugin commits are pushed first, then the app pointer bump is its own commit; plans never push or tag. Every plugin now requires framework v0.6.0, which is not tagged yet: apps build in workspace mode (all five replace the framework locally), but app release builds and module consumers need the v0.6.0 tag first. The SUMMARY lists every repository with its new commit SHA in push order (framework, then translate, user, chat, the other plugins, then sm-summercmsio-plugin and vue-summercmsio-app).
|
||||
</context>
|
||||
<options>
|
||||
<option id="pushed">
|
||||
<name>Jakub has pushed the listed repositories (no tags); bump the pointers now</name>
|
||||
<pros>Apps track the manifests immediately; no leftover pointer work at release</pros>
|
||||
<cons>Apps' release builds still wait for v0.6.0; committed app pointers reference plugin commits that require an untagged framework</cons>
|
||||
</option>
|
||||
<option id="defer">
|
||||
<name>Do not bump app pointers in this phase; record them for the release</name>
|
||||
<pros>Apps stay on released plugin versions until the framework and plugins are tagged</pros>
|
||||
<cons>The release step has one more pass over five apps</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply "pushed: <repositories>" or "defer"</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: App submodule pointers follow the pushed plugin commits, or are recorded for the release</name>
|
||||
<files>none in this repository; app gitlinks under ../sm-demo-app, ../sm-grzybyfunkcjonalne-app, ../sm-bm-app, ../sm-summercmsio-app (plugin submodules plus plugins/golem15/summercms and vue-summercmsio-app), ../fonoteka.go when "pushed"</files>
|
||||
<read_first>
|
||||
- the Task 4 answer
|
||||
- each app's .gitmodules (which plugins it mounts)
|
||||
- .planning/phases/34-plugin-marketplace/34-03-SUMMARY.md (pending site plugin and vue app SHAs)
|
||||
- ../sm-summercmsio-app/CLAUDE.md (submodule rules; use ssu for status)
|
||||
</read_first>
|
||||
<action>
|
||||
If Task 4 answered "defer": write the pending bumps (app, submodule path, target SHA) into the SUMMARY under "Pending pointer bumps" and stop. If "pushed": for each app and each mounted repository that Jakub pushed (in sm-summercmsio-app this includes plugins/golem15/summercms and vue-summercmsio-app at the 34-03 SHAs), run `git -C <app>/<path> fetch origin` and check out the pushed SHA, confirm `git -C <app> status --short` shows only those gitlinks (plus any other session's own edits, which stay unstaged), run `go -C <app> build ./...` in workspace mode, and commit only the gitlinks with `chore: bump plugin pointers for plugin.yaml`. Do not run summer build in the apps and do not commit regenerated main.go or plugins.gen.go: that regeneration belongs to each app's framework upgrade at release (research F10). Never push the apps.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>for a in sm-demo-app sm-grzybyfunkcjonalne-app sm-bm-app sm-summercmsio-app fonoteka.go; do test -d ../$a || continue; go -C ../$a build ./... >/dev/null 2>&1 || echo "FAIL $a"; done; echo done</automated>
|
||||
<fails_when>any "FAIL <app>" line before "done"</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- With "pushed": every bumped submodule's HEAD equals a SHA Jakub listed, and `git -C <app> show --stat HEAD` lists only gitlinks
|
||||
- With "defer": the SUMMARY contains a "Pending pointer bumps" table with app, path and SHA for every mounted plugin repository changed in Tasks 1-3
|
||||
- No app is pushed (`git -C ../sm-demo-app status -sb | head -1` shows no change in the ahead count beyond the new commit)
|
||||
</acceptance_criteria>
|
||||
<done>Apps either track the new plugin commits or the release has an exact list of pointer bumps to make.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| plugin repository → registry and installers | plugin.yaml in a tag is read by summercms.io and by `summer plugin:install` |
|
||||
| developer workstation → shared remotes | pushes and tags (user-gated) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-07 | Tampering | hand-edited or stale plugin.yaml | medium | mitigate | the file is generated only; TestPluginManifest in each repo fails on drift (Task 1 proves the failure) |
|
||||
| T-34-08 | Spoofing | a manifest claiming another module or ID | medium | mitigate | module and package come from go.mod and the directory; IDs from plugin.go; the ingester re-checks module == repository path (34-03) |
|
||||
| T-34-09 | Tampering | app pointers referencing unpushed commits | high | mitigate | pointer bumps only after Jakub reports the push (Task 4), and only gitlinks are committed |
|
||||
| T-34-10 | Denial of service | consumers resolving the unreleased framework minimum | medium | accept | plugins are not tagged until v0.6.0 exists (release order recorded in 34-06's ledger); existing tags are never moved |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | no package is installed; go.sum changes come from `go mod tidy` against modules the framework already requires |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Every listed repository: `go vet ./...`, `go test -short ./...` and `TestPluginManifest` pass; `summer plugin:manifest --check` passes.
|
||||
- Anonymous `git ls-remote` succeeds for every listed repository.
|
||||
- No tags or pushes made by the agent.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 20 public plugin repositories (21 plugin packages) carry a generated plugin.yaml and its test, or a recorded F8 exception for chatbots.
|
||||
- Each requires the framework release containing kiosk.
|
||||
- App pointers are bumped after a reported push, or listed for the release.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-02-SUMMARY.md` when done, with the per-repository table (ID, name, framework minimum, requires, SHA), warnings, skips and the pointer-bump outcome.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Files: `plugin.yaml` and `manifest_test.go` in each of ../sm-{chat,chatbots,chatchannels,chatvideo,faq,forum,herald,journal,menu,mfa,oauthserver,paymentgateway,paymentgatewaysubscriptions,pgstripe,seo,sitemap,sociallogin,translate,user,userfriends}-plugin, plus `subscriptions/plugin.yaml` and `subscriptions/manifest_test.go` in sm-pgstripe-plugin.
|
||||
- Tests: `TestPluginManifest` in every listed repository.
|
||||
- go.mod: framework requirement raised to the confirmed release (v0.6.0); sm-chatbots-plugin's sm-chat-plugin requirement raised from v0.0.0 to v0.1.3 when it builds.
|
||||
- Display names (plugin.yaml `overrides.name`): FAQ, Journal, Menu, Multi-factor authentication, OAuth server, Payment gateway, Payment gateway subscriptions, Stripe payments, Stripe subscriptions, SEO, Sitemap, Social login, Translate, User.
|
||||
- Config keys, CLI commands, types: none.
|
||||
409
.planning/phases/34-plugin-marketplace/34-03-PLAN.md
Normal file
409
.planning/phases/34-plugin-marketplace/34-03-PLAN.md
Normal file
@@ -0,0 +1,409 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["34-01"]
|
||||
coupling_justified: ["34-04: same wave; 34-04 adds modules/kiosk/client.go and edits internal/build and cmd/summer in the framework checkout this plan compiles through go.work; only the kiosk package is shared, the change is one new file and each 34-04 commit compiles"]
|
||||
files_modified:
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/go.mod
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/go.sum
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/README.md
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/updates/registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/repository.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/version.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/columns.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/fields.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/columns.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/fields.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/git.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/query.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_list.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_form.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_list.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_form.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/console/registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/jobs.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/schedule.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/token_scopes.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/admin.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/admin_navigation.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/admin_permissions.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/lang.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/lang/en/lang.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/lang/pl/lang.yaml
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/testmain_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go
|
||||
- ../sm-summercmsio-app/config/queue.yaml
|
||||
- ../sm-summercmsio-app/config/http.yaml
|
||||
- ../sm-summercmsio-app/deploy/nginx/summercms.io.conf
|
||||
- ../sm-summercmsio-app/deploy/supervisor/summercms-io-changelog.conf
|
||||
- ../sm-summercmsio-app/deploy/changelog.env.example
|
||||
- ../sm-summercmsio-app/scripts/changelog-post.py
|
||||
- ../sm-summercmsio-app/scripts/test_changelog_post.py
|
||||
- ../sm-summercmsio-app/scripts/smoke.sh
|
||||
- ../sm-summercmsio-app/DEPLOY.md
|
||||
- ../sm-summercmsio-app/README.md
|
||||
- ../sm-summercmsio-app/CLAUDE.md
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/nuxt.config.ts
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/i18n/locales/en.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/index.vue
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/[vendor]/[name].vue
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useRegistry.ts
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/marketplace.ts
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/marketplace-smoke.mjs
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/expect-marketplace.txt
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugins.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugin-golem15.journal.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/versions-golem15.journal.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/package.json
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/README.md
|
||||
autonomous: true
|
||||
requirements: [SC-2, D-04, D-13, D-14, D-15, D-16]
|
||||
user_setup:
|
||||
- service: summercms-io-registry-token
|
||||
why: "changelog-post.py calls the ingest-now endpoint after a plugin release; without a token scoped `registry` the call answers 403, is tolerated, and the hourly ingest picks the tag up"
|
||||
dashboard_config:
|
||||
- task: "Mint a personal API token with the scope `registry` (Account → API tokens on the summercms.io admin, through the SSH tunnel) and store it in ~/.config/summercms/registry-token with mode 600; the agent never mints, prints or copies tokens"
|
||||
location: "summercms.io admin over the SSH tunnel (DEPLOY.md)"
|
||||
- service: rome-git
|
||||
why: "the ingester shells out to git; production must have it"
|
||||
dashboard_config:
|
||||
- task: "Before the deploy that ships the registry, confirm `command -v git` on rome (DEPLOY.md step added in Task 4)"
|
||||
location: "rome (debian@10.8.3.118 -p 2222)"
|
||||
|
||||
estimate:
|
||||
tokens: 190000
|
||||
raw_tokens: 190000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A summercms.io administrator curates the listing by adding a repository URL; the ingester fills every fact from the repository and a plugin is published only once one of its vX.Y.Z tags carries a valid plugin.yaml; tags without one are skipped (D-13, D-14)"
|
||||
- "The ingester clones anonymously over HTTPS into a temporary bare partial clone, reads tags, each tag's plugin.yaml files and the latest tag's README, never checks out a tree, never recurses into submodules, caps every blob it reads, and refuses a plugin.yaml whose module differs from the repository path or whose ID another listed repository already owns (D-13, research Pitfall 13)"
|
||||
- "Every vX.Y.Z tag becomes a version row with its framework minimum and plugin minimums; the README is stored for the latest tag only, as markdown and as breakwater-sanitized HTML (D-15)"
|
||||
- "Ingestion runs hourly as a scheduled console command and on demand through POST /api/registry/v1/ingest, which needs a backend principal holding golem15.summercms.registry_ingest through a token scoped `registry`; no forge webhooks (D-13, user decision 3)"
|
||||
- "GET /api/registry/v1/plugins?q=&category=, /plugins/{id} and /plugins/{id}/versions answer without auth with open CORS, Cache-Control and an ETag, encoded with kiosk's wire types so their shape is exactly the one frozen at 34-01 Task 1 (D-16)"
|
||||
- "Category, featured flag, screenshots, icon and sort order are edited in the summercms.io admin and appear in the API; the facts (versions, requirements, module, README) are read-only there (SC-2, decision note)"
|
||||
- "/marketplace lists, searches and filters plugins by category and /marketplace/<vendor>/<name> shows the detail with the install command, README and versions, rendered by the changelog Node SSR app from the registry API (SC-2)"
|
||||
- "`changelog-post.py publish … --repo` refuses a plugin release whose committed plugin.yaml fails `summer plugin:manifest --check`, and after a successful post asks the registry to ingest now, treating a 401/403/404 or network failure as a warning (D-04, D-13, user decision 3)"
|
||||
- "Production config serves jobs (queue.work_in_serve true), nginx routes /marketplace to the Node app and allows POST only on the ingest path, and DEPLOY.md requires git on the server (research F10, Pitfalls 9-10)"
|
||||
artifacts:
|
||||
- path: "../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go"
|
||||
provides: "pull ingester"
|
||||
contains: "breakwater.Render"
|
||||
- path: "../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go"
|
||||
provides: "public registry API and ingest-now endpoint"
|
||||
contains: "kiosk.ListResponse"
|
||||
- path: "../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go"
|
||||
provides: "registry tables"
|
||||
contains: "golem15_summercms_registry_versions"
|
||||
- path: "../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go"
|
||||
provides: "TestRegistrySmoke (tracer)"
|
||||
contains: "func TestRegistrySmoke("
|
||||
- path: "../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/index.vue"
|
||||
provides: "marketplace list page"
|
||||
contains: "plugin:install"
|
||||
- path: "../sm-summercmsio-app/scripts/changelog-post.py"
|
||||
provides: "manifest check and ingest-now hooks"
|
||||
contains: "api/registry/v1/ingest"
|
||||
key_links:
|
||||
- from: "../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go"
|
||||
to: "modules/kiosk/manifest.go"
|
||||
via: "each tag's plugin.yaml goes through kiosk.Parse and kiosk.Validate before a version row is written"
|
||||
pattern: "kiosk\\.Parse\\("
|
||||
- from: "../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go"
|
||||
to: "modules/kiosk/registry.go"
|
||||
via: "responses are kiosk.ListResponse, kiosk.DetailResponse, kiosk.VersionsResponse and kiosk.ErrorResponse values"
|
||||
pattern: "kiosk\\.(ListResponse|DetailResponse|VersionsResponse|ErrorResponse)"
|
||||
- from: "../sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useRegistry.ts"
|
||||
to: "/api/registry/v1"
|
||||
via: "SSR fetches through runtimeConfig.registry.apiBase (NUXT_REGISTRY_API_BASE), browser fetches through the same-origin path"
|
||||
pattern: "registry"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
People browse the public Golem15 plugins at summercms.io/marketplace. This plan builds the marketplace itself: the registry that ingests plugin repositories into Postgres, its public read-only API, the admin that curates and decorates the listing, the pages, the deploy configuration and the release-script hooks.
|
||||
|
||||
<objective>
|
||||
In the summercms.io application (`../sm-summercmsio-app`) and its two submodules, the site plugin `golem15.summercms` (`plugins/golem15/summercms`, repository sm-summercmsio-plugin) and `vue-summercmsio-app`:
|
||||
- registry tables, models and a pull ingester (D-13, D-14, D-15);
|
||||
- the public API at /api/registry/v1 using kiosk's wire types (D-16, shape frozen at 34-01 Task 1);
|
||||
- admin curation and presentation fields, the `registry` token scope and the ingest-now endpoint (user decision 3);
|
||||
- hourly scheduled ingestion with jobs enabled in serve;
|
||||
- /marketplace pages in the changelog Node SSR app (research A4), a landing header link;
|
||||
- deploy configs, DEPLOY.md and the changelog-post.py hooks (D-04, D-13).
|
||||
|
||||
Launch listing (user decision 5): the tagged public plugins checked out next to the framework; the curated URLs are recorded in DEPLOY.md and added after the plugins' first plugin.yaml tags exist (user-gated release, ledger in 34-06).
|
||||
|
||||
Output: commits in the site plugin repository, the vue app repository and the app repository (each its own commits). Submodule pointer bumps in sm-summercmsio-app are not committed in this plan: they follow Jakub's push (34-02 Task 4) or move to the release ledger (CLAUDE.md). Nothing is pushed, tagged or deployed.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
|
||||
@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
|
||||
@../sm-summercmsio-app/CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
From 34-01 (names may be adjusted in its SUMMARY): kiosk.Parse, kiosk.Validate, kiosk.FileName, kiosk.ValidVersion, kiosk.CompareVersions; wire types kiosk.RegistryPlugin, RegistryPluginDetail, RegistryScreenshot, RegistryVersion, ListResponse{Data, Meta{Total}}, DetailResponse, VersionsResponse, ErrorResponse{Error{Code, Message}}, IngestRequest{Repository}, IngestResponse; kiosk.RegistryAPIPath = "/api/registry/v1".
|
||||
Framework: breakwater.Render(markdown string) string; cabana.ResolveBackend(app, r, cover...) and cabana.Allows(principal, []string); pact.HasMigrations/HasModels/HasAdminControllers/HasPermissions/HasNavigation/HasTokenScopes/HasJobs/HasSchedule/HasCommands/HasRoutes; pact.ScheduledCommand{Command, Cadence: pact.Every(time.Hour)}; conga.Job[T](fn, opts...) and conga.From(app) for enqueueing; surf middleware "throttle:<bucket>" and "cors:<profile>" with http.cors_profiles.<name> in config; lagoon/attach relations (sm-journal-plugin/models/post.go AttachRelations).
|
||||
Templates: ../sm-faq-plugin (updates/registry.go, migrations, models/registry.go, admin.go, admin_navigation.go, admin_permissions.go, controllers/*/config_*.yaml, routes.go, lang/, testmain_test.go with testcontainers), ../sm-journal-plugin (token_scopes.go, controllers/api/auth.go ~83-106), ../sm-chatvideo-plugin/schedule.go.
|
||||
Site plugin today: plugin.go declares only pact.HasRoutes; Routes fails closed without the public tree (tests use the fsys field); links_test.go nodePaths = ["/changelog"]; go.mod requires the framework v0.4.0 and the app's go.work replaces it with ../summercms.go.
|
||||
App: config/queue.yaml work_in_serve false; nginx denies non-GET outside /_journal/api/v1/ and 404s /backend; changelog Node app on 127.0.0.1:3095 with NUXT_JOURNAL_API_BASE; scripts/changelog-post.py has call(method, path, token, body=None, base=None), cmd_publish, SITE, FRAMEWORK, TOKEN_FILE.
|
||||
Changelog app: changelog/nuxt.config.ts (extends '..' and vue-journal-layer, buildAssetsDir /changelog/_nuxt/, runtimeConfig.journal.apiBase, routeRules swr 60, pages:extend removes the landing index); testing via vue-journal-layer's testing/nuxt-server.mjs startNuxtServer({outputDir, apiBase, port, env}) and smoke-ssr.mjs parseExpectLine.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Three repositories, committed separately and in this order within a task: site plugin (`git -C ../sm-summercmsio-app/plugins/golem15/summercms`), vue app (`git -C ../sm-summercmsio-app/vue-summercmsio-app`), app (`git -C ../sm-summercmsio-app`). Stage explicit paths; never stage a submodule gitlink in the app (the gitlinks stay modified until Jakub pushes; record the pending SHAs in the SUMMARY). Check `git status --short` in each repository first and leave other sessions' edits alone. No co-author tag.
|
||||
- Docker-backed tests: if the socket is denied, run them through `sudo -u golem` (CLAUDE.md).
|
||||
- Resume check: `git -C <repo> log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commit subjects (repository) | Resume point |
|
||||
|------|------------------------------|--------------|
|
||||
| 1 | `feat: plugin registry tables, ingester and plugin detail API` (site) | TestRegistrySmoke/detail passes |
|
||||
| 2 | `feat: registry list, search and versions API, hourly ingest` (site); `feat: registry CORS profile and jobs in serve` (app) | TestRegistrySmoke passes |
|
||||
| 3 | `feat: registry admin, token scope and ingest-now endpoint` (site); `feat: manifest check and ingest-now in changelog-post` (app) | test_changelog_post.py passes |
|
||||
| 4 | `feat: marketplace pages` (vue); `feat: marketplace link` (site, links_test); `feat: marketplace deploy config and docs` (app) | marketplace smoke passes |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "a curated repository with a tagged plugin.yaml is served at GET /api/registry/v1/plugins/{id}" — one repository, one tag, one plugin</name>
|
||||
<reversibility rating="one-way">The JSON this endpoint returns is the D-16 contract; its shape was confirmed at 34-01 Task 1 and is encoded only through kiosk's wire types, so this task adds no field of its own.</reversibility>
|
||||
<precondition>.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md records the 34-01 Task 1 answer that froze the /api/registry/v1 shape (the checkpoint:decision for this one-way door); halt if it is absent</precondition>
|
||||
<files>../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/go.mod, ../sm-summercmsio-app/plugins/golem15/summercms/go.sum, ../sm-summercmsio-app/plugins/golem15/summercms/updates/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/repository.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/models/version.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/git.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/query.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/testmain_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/README.md</files>
|
||||
<read_first>
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, static.go, smoke_test.go (how tests build the plugin without the public tree)
|
||||
- ../sm-faq-plugin/updates/registry.go, ../sm-faq-plugin/updates/202610110001_create_golem15_faq_faqs.go, ../sm-faq-plugin/models/registry.go, ../sm-faq-plugin/models/faq.go, ../sm-faq-plugin/routes.go, ../sm-faq-plugin/controllers/api/faqs.go, ../sm-faq-plugin/testmain_test.go
|
||||
- modules/kiosk/registry.go, modules/kiosk/manifest.go, modules/kiosk/README.md (34-01 output)
|
||||
- modules/breakwater/README.md
|
||||
- internal/build/new.go (~286 runGit: exec pattern)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md ("Hardened git for the ingester", Pattern 7, Pitfall 13)
|
||||
</read_first>
|
||||
<action>
|
||||
Per D-13, D-14, D-15, D-16 (shape from 34-01 Task 1). In the site plugin: raise the framework requirement to the release confirmed at 34-01 Task 1 (`go mod edit -require`, then `go mod tidy`; the app's go.work keeps replacing it). Migration `202610300001_create_golem15_summercms_registry` (raw SQL like faq, with Rollback dropping in reverse order) creates `golem15_summercms_registry_repositories` (id, url text unique, module text, enabled bool default true, tags_digest text, last_ingested_at, last_error text, sort timestamps), `golem15_summercms_registry_plugins` (id, repository_id fk, plugin_id text unique, path text, name, description, module, package, latest_version, latest_framework, readme_markdown, readme_html, category text, featured bool default false, sort_order int default 0, published bool default false, timestamps) and `golem15_summercms_registry_versions` (id, plugin_row_id fk on delete cascade, version, framework, requires jsonb, module, package, commit text, tagged_at, unique (plugin_row_id, version)). Models `Repository`, `Plugin`, `Version` with a models registry like faq. `Repository.module` is derived from an https URL (host plus path without `.git` and trailing slash) when saved; it is what plugin.yaml's module must equal.
|
||||
`registry/git.go`: a runner that executes git with exec.CommandContext, `--` before every URL and path, a minimal environment (PATH plus a temporary HOME) and GIT_TERMINAL_PROMPT=0, GIT_ALLOW_PROTOCOL from a package variable `allowedProtocols` (default "https"; the test sets "https:file"), GIT_CONFIG_NOSYSTEM=1, GIT_CONFIG_GLOBAL=/dev/null; `exec.LookPath("git")` failure is a clear error recorded on the repository.
|
||||
`registry/ingest.go`: `Ingester{DB *gorm.DB}` and `Ingester.Repository(ctx, repositoryID uint) error`: clone `--bare --filter=blob:none` into a temp dir (retry without the filter when the server refuses it; remove the dir always), list tags with for-each-ref (peeled commit, creator date), keep strict vX.Y.Z tags (kiosk.ValidVersion), find `(^|/)plugin.yaml$` with ls-tree per tag, read each with `git show <tag>:<path>` through io.LimitReader (64 KiB), kiosk.Parse plus kiosk.Validate, require module == repository.module, upsert the plugin row (by plugin_id; reject an ID owned by another repository and record the error), insert the version row; for the newest valid tag, read `<dir>/README.md` (cap 512 KiB) into readme_markdown and `breakwater.Render` into readme_html, and set latest_version, latest_framework, name, description, module, package, published = true. A tag without a valid plugin.yaml is skipped and noted in last_error, never fatal (D-14). In this task handle the single-tag path fully; Task 2 adds the change check and the multi-tag/edge rules.
|
||||
`registry/query.go`: `Detail(ctx, db, pluginID) (kiosk.RegistryPluginDetail, bool, error)`. `controllers/api/registry.go`: `Show` handler writing `kiosk.DetailResponse` or 404 `kiosk.ErrorResponse{not_found}`. `routes_registry.go`: `registerRegistryRoutes(r pact.Router, app)` mounting `GET /api/registry/v1/plugins/{id}` (callable from tests without the public tree); plugin.go calls it from Routes and declares HasMigrations and HasModels (update the package doc: the plugin now has registry tables). testmain_test.go starts testcontainers Postgres like faq (flag.Parse, testing.Short skip). `registry_smoke_test.go` `TestRegistrySmoke/detail`: create a source repo with git in t.TempDir (commit a plugin.yaml produced by kiosk.Marshal for module `example.test/acme/sm-acme-plugin`, ID `acme.blog`, plus a README), tag v0.1.0, `git clone --bare` it, insert a repository row with url `file://<bare>` and module `example.test/acme/sm-acme-plugin`, run the Ingester, then GET /api/registry/v1/plugins/acme.blog through an httptest server and assert latest_version v0.1.0, framework, readme_html non-empty and free of `<script`, and GET of an unknown ID answers 404 with code not_found. Document the registry in the site plugin README.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms test -run 'TestRegistrySmoke' -count=1 -v ./... 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", a "--- SKIP: TestRegistrySmoke" line (Docker unavailable: rerun through sudo -u golem), or no "--- PASS: TestRegistrySmoke/detail" line</fails_when>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercmsio-app/plugins/golem15/summercms test -short ./... 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "golem15_summercms_registry_versions" ../sm-summercmsio-app/plugins/golem15/summercms/updates/202610300001_create_golem15_summercms_registry.go` prints at least two lines (create and drop)
|
||||
- `grep -n "GIT_ALLOW_PROTOCOL\|GIT_TERMINAL_PROMPT=0\|GIT_CONFIG_NOSYSTEM" ../sm-summercmsio-app/plugins/golem15/summercms/registry/git.go` prints three lines
|
||||
- `grep -n "kiosk.DetailResponse" ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go` prints at least one line, and no struct in the site plugin redeclares a json tag `latest_version` (`grep -rn 'json:"latest_version"' ../sm-summercmsio-app/plugins/golem15/summercms` prints nothing)
|
||||
- `grep -n "io.LimitReader" ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go` prints at least one line
|
||||
</acceptance_criteria>
|
||||
<done>One tagged plugin repository flows through clone, parse, Postgres and the public detail endpoint.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: The full public API (list, search, category, versions) with CORS and caching, and hourly ingestion that skips unchanged repositories</name>
|
||||
<reversibility rating="one-way">Adds the list and versions endpoints of the frozen v1 contract; query parameters q and category and the envelopes are part of it.</reversibility>
|
||||
<precondition>the 34-01 Task 1 answer in 34-01-SUMMARY.md covers the list and versions shapes; halt if it adjusted them in a way kiosk's wire types do not reflect</precondition>
|
||||
<files>../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/query.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/console/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/schedule.go, ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/README.md, ../sm-summercmsio-app/config/queue.yaml, ../sm-summercmsio-app/config/http.yaml</files>
|
||||
<read_first>
|
||||
- modules/surf/README.md (named CORS profiles, throttle buckets) and ../sm-faq-plugin/routes.go (bucket constant)
|
||||
- ../sm-chatvideo-plugin/schedule.go and modules/conga/README.md (scheduled commands, work_in_serve)
|
||||
- ../sm-summercmsio-app/config/queue.yaml, ../sm-summercmsio-app/config/http.yaml
|
||||
- the Task 1 files
|
||||
</read_first>
|
||||
<behavior>
|
||||
- GET /api/registry/v1/plugins lists only published plugins ordered featured first, then sort_order, then name, with meta.total equal to the count
|
||||
- q matches id, name or description case-insensitively with LIKE wildcards in q escaped; category filters exactly; q over 100 characters or a category not matching ^[a-z0-9-]{1,40}$ answers 400 invalid_query
|
||||
- GET /plugins/{id}/versions lists every version newest first with framework, requires, commit and tagged_at
|
||||
- every GET carries Access-Control-Allow-Origin * (Origin sent), Cache-Control public max-age=300 and an ETag, and a matching If-None-Match answers 304
|
||||
- a repository with three tags (v0.1.0 without plugin.yaml, v0.2.0 valid, v0.3.0 with an invalid ID) yields one version, latest v0.2.0, and records the skipped tags in last_error
|
||||
- a nested plugin.yaml (sub/plugin.yaml) yields a second plugin with its own package
|
||||
- a plugin.yaml whose module differs from the repository's module is rejected; an ID already owned by another repository is rejected on the newer repository
|
||||
- a second ingest with unchanged ls-remote tags does not clone (tags_digest unchanged); a new tag triggers a clone
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-13..D-16. Add `List(ctx, db, q, category)` and `Versions(ctx, db, pluginID)` to registry/query.go returning kiosk wire types; `List`, `Versions` handlers; mount the group `/api/registry/v1` with `surf.Use("throttle:registry-api", "cors:registry")`, GET only, setting Cache-Control and a strong ETag computed from the response bytes (304 on If-None-Match). Add the `registry` profile under `http.cors_profiles` in ../sm-summercmsio-app/config/http.yaml (origins *, methods GET and HEAD, no credentials) and the matching profile in the test config. Ingest rules: compute a digest of `git ls-remote --tags -- <url>` output first and skip the clone when it equals tags_digest; process every strict tag; nested plugin.yaml files; module and ID rules from the behavior list; ingestion of one repository never aborts the others. Add `IngestAll(ctx)` over enabled repositories. Console command `summercms:registry-ingest [url]` (all enabled repositories, or the one matching url; prints one line per repository with versions added or the error) in console/registry.go with full bonfire docs; `Schedule()` returning `{Command: "summercms:registry-ingest", Cadence: pact.Every(time.Hour)}` in schedule.go, with HasSchedule and HasCommands asserted in plugin.go. Set `work_in_serve: true` in ../sm-summercmsio-app/config/queue.yaml and replace its comment (the site now runs the hourly registry ingest). Write the behavior subtests (`list`, `search`, `versions`, `headers`, `tags`, `nested`, `identity`, `unchanged`) in TestRegistrySmoke first, then implement. Document the API, the command and the schedule in the site plugin README. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms test -run 'TestRegistrySmoke' -count=1 -v ./... 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", a "--- SKIP: TestRegistrySmoke" line, or no "--- PASS: TestRegistrySmoke/unchanged" line</fails_when>
|
||||
<automated>go -C ../sm-summercmsio-app vet ./... && go -C ../sm-summercmsio-app test ./... 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "work_in_serve: true" ../sm-summercmsio-app/config/queue.yaml` prints one line
|
||||
- `grep -n "registry:" ../sm-summercmsio-app/config/http.yaml` prints the CORS profile
|
||||
- `grep -n "pact.Every(time.Hour)" ../sm-summercmsio-app/plugins/golem15/summercms/schedule.go` prints one line
|
||||
- `grep -n "summercms:registry-ingest" ../sm-summercmsio-app/plugins/golem15/summercms/console/registry.go` prints at least one line
|
||||
- every TestRegistrySmoke subtest listed in the behavior block passes
|
||||
</acceptance_criteria>
|
||||
<done>The registry API serves list, search, category, detail and versions publicly and stays fresh through an hourly ingest that only clones when tags change.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Admin curation and presentation fields, the registry token scope and ingest-now, and the release-script hooks</name>
|
||||
<files>../sm-summercmsio-app/plugins/golem15/summercms/admin.go, ../sm-summercmsio-app/plugins/golem15/summercms/admin_navigation.go, ../sm-summercmsio-app/plugins/golem15/summercms/admin_permissions.go, ../sm-summercmsio-app/plugins/golem15/summercms/token_scopes.go, ../sm-summercmsio-app/plugins/golem15/summercms/jobs.go, ../sm-summercmsio-app/plugins/golem15/summercms/lang.go, ../sm-summercmsio-app/plugins/golem15/summercms/lang/en/lang.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/lang/pl/lang.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_list.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/repositories/config_form.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_list.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/plugins/config_form.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/columns.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/models/repository/fields.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/columns.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin/fields.yaml, ../sm-summercmsio-app/plugins/golem15/summercms/models/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/routes_registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/console/registry.go, ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry_smoke_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/README.md, ../sm-summercmsio-app/scripts/changelog-post.py, ../sm-summercmsio-app/scripts/test_changelog_post.py</files>
|
||||
<read_first>
|
||||
- ../sm-faq-plugin/admin.go, admin_navigation.go, admin_permissions.go, controllers/faqs.go, controllers/faqs/config_*.yaml, models/faq/*.yaml, lang/en/lang.yaml
|
||||
- ../sm-journal-plugin/token_scopes.go, ../sm-journal-plugin/controllers/api/auth.go (~83-106), ../sm-journal-plugin/models/post.go (AttachRelations for icon and screenshots)
|
||||
- modules/conga/README.md (Job, enqueue through conga.From(app))
|
||||
- ../sm-summercmsio-app/scripts/changelog-post.py (cmd_publish ~262-300, call ~204, read_token ~236, is_framework ~103) and scripts/test_changelog_post.py
|
||||
- ../sm-summercmsio-app/DEPLOY.md ("Posting to the changelog")
|
||||
</read_first>
|
||||
<behavior>
|
||||
- POST /api/registry/v1/ingest without credentials answers 401; with a token lacking the `registry` scope or the permission answers 403; with a token scoped `registry` for a curated repository answers 202 with kiosk.IngestResponse and enqueues one ingest job; for an uncurated repository answers 404
|
||||
- an https URL saved in the repositories admin form gets its module derived; a non-https URL or one with credentials, a query or a fragment is a validation error
|
||||
- presentation fields (category from a fixed list, featured, sort_order, icon, screenshots with captions) saved in the admin appear in GET /plugins and /plugins/{id} as category, featured, icon_url and screenshots
|
||||
- changelog-post.py: https_remote converts git@host:path.git and ssh://git@host/path.git to https://host/path.git; manifest_dirs finds the root and nested plugin.yaml directories; ingest outcome 202 is success and 401, 403, 404 and a network error are warnings that do not fail publish
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-13, D-14 and user decision 3. Site plugin: permissions `golem15.summercms.registry` (admin screens) and `golem15.summercms.registry_ingest` (endpoint), tab label from lang, roles granted none (superusers pass); navigation main item "Marketplace" with side items Repositories and Plugins; admin controllers `golem15.summercms.repositories` (list url, enabled, last_ingested_at, last_error; form url and enabled only) and `golem15.summercms.plugins` (list plugin_id, name, latest_version, category, featured, published; form: presentation fields editable — category dropdown with content, users, commerce, communication, seo, tools; featured switch; sort_order number; icon attachOne and screenshots attachMany with captions via lagoon/attach — and the facts shown read-only); `Plugin.AttachRelations` for icon and screenshots; query.go fills icon_url and screenshots from the attachments' public URLs. Token scope `registry` in token_scopes.go (Permissions `golem15.summercms.*`, Methods POST). Ingest job `conga.Job[IngestArgs]` in jobs.go (HasJobs) running Ingester.Repository; POST /api/registry/v1/ingest in its own group with `throttle:registry-ingest` and no public CORS, authorized with cabana.ResolveBackend plus cabana.Allows for `golem15.summercms.registry_ingest`, body kiosk.IngestRequest normalized like the admin form, curated and enabled repository required, enqueue through conga.From(app), answer 202 kiosk.IngestResponse. Console `summercms:registry-add <url>...` (same validation as the form) in console/registry.go. Lang strings en and pl. Write the endpoint and admin-save subtests (`ingest_auth`, `curation`, `presentation`) in TestRegistrySmoke first.
|
||||
App script: add `REGISTRY_API` (env SUMMERCMS_REGISTRY_API, default SITE + "/api/registry/v1"), `REGISTRY_TOKEN_FILE` (~/.config/summercms/registry-token, falling back to the journal token), pure helpers `https_remote(url)`, `manifest_dirs(paths)`, `ingest_outcome(status)`; in cmd_publish for a plugin repository (not is_framework()): before posting, require HEAD to be exactly the tag (`git describe --exact-match`) and a clean worktree, list the repository's plugin.yaml files with `git ls-files`, refuse when none exists (hint: summer plugin:manifest) and run `summer plugin:manifest --check <dir>` for each (summer from PATH, else `go -C FRAMEWORK run ./cmd/summer`), refusing on failure; after a successful post call `call("POST", "/ingest", token, body, base=REGISTRY_API)` with the https remote and print a warning instead of failing on 401/403/404 or a network error ("the hourly ingest will pick it up"). The script never prints a token. Add unittest cases for the three helpers in scripts/test_changelog_post.py. Update the site plugin README (admin, scope, endpoint) and the "Posting to the changelog" part of DEPLOY.md is done in Task 4. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms test -run 'TestRegistrySmoke/(ingest_auth|curation|presentation)' -count=1 -v ./... 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestRegistrySmoke/ingest_auth" line</fails_when>
|
||||
<automated>cd ../sm-summercmsio-app && python3 -B scripts/test_changelog_post.py 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit, or a "FAILED" or "Error" line in the summary</fails_when>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercmsio-app/plugins/golem15/summercms test -short ./... 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'Name: *"registry"' ../sm-summercmsio-app/plugins/golem15/summercms/token_scopes.go` prints one line
|
||||
- `grep -n "golem15.summercms.registry_ingest" ../sm-summercmsio-app/plugins/golem15/summercms/admin_permissions.go ../sm-summercmsio-app/plugins/golem15/summercms/controllers/api/registry.go` prints at least two lines
|
||||
- `grep -n "def https_remote\|def manifest_dirs\|def ingest_outcome" ../sm-summercmsio-app/scripts/changelog-post.py` prints three lines
|
||||
- `grep -n "plugin:manifest" ../sm-summercmsio-app/scripts/changelog-post.py` prints at least one line
|
||||
- `grep -n "print(.*token" ../sm-summercmsio-app/scripts/changelog-post.py` prints nothing new compared with the file before this task
|
||||
</acceptance_criteria>
|
||||
<done>Jakub curates and decorates the listing in the admin, a plugin release checks its manifest before posting and asks the registry to ingest at once, and a missing scope only delays ingestion to the next hour.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: /marketplace pages in the changelog SSR app, the landing link, deploy configuration and docs</name>
|
||||
<files>../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/index.vue, ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/[vendor]/[name].vue, ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useRegistry.ts, ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/marketplace.ts, ../sm-summercmsio-app/vue-summercmsio-app/changelog/nuxt.config.ts, ../sm-summercmsio-app/vue-summercmsio-app/changelog/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/marketplace-smoke.mjs, ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/expect-marketplace.txt, ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugins.json, ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/plugin-golem15.journal.json, ../sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/registry/versions-golem15.journal.json, ../sm-summercmsio-app/vue-summercmsio-app/package.json, ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue, ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app/README.md, ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go, ../sm-summercmsio-app/deploy/nginx/summercms.io.conf, ../sm-summercmsio-app/deploy/supervisor/summercms-io-changelog.conf, ../sm-summercmsio-app/deploy/changelog.env.example, ../sm-summercmsio-app/scripts/smoke.sh, ../sm-summercmsio-app/DEPLOY.md, ../sm-summercmsio-app/README.md, ../sm-summercmsio-app/CLAUDE.md</files>
|
||||
<read_first>
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/nuxt.config.ts (whole file), changelog/app/pages/changelog/plugins.vue and index.vue, changelog/app/components/ChangelogIndex.vue, changelog/app/utils/streams.ts
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/node_modules/vue-journal-layer/testing/nuxt-server.mjs and smoke-ssr.mjs (startNuxtServer, parseExpectLine, X-Forwarded-For forwarding), and the layer's API client and HTML sanitizer used for post bodies
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue (current prop, changelog links)
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go (nodePaths)
|
||||
- ../sm-summercmsio-app/deploy/nginx/summercms.io.conf (the /changelog locations to mirror), deploy/supervisor/summercms-io-changelog.conf, scripts/check-deploy.sh, scripts/smoke.sh
|
||||
- ../sm-summercmsio-app/DEPLOY.md, README.md, CLAUDE.md (cross-repo contracts)
|
||||
</read_first>
|
||||
<action>
|
||||
Pages (layout and visuals are Claude's discretion per CONTEXT; follow the landing look through SiteHeader/SiteFooter and the changelog CSS):
|
||||
- `useRegistry.ts`: server-side requests use `runtimeConfig.registry.apiBase` (`NUXT_REGISTRY_API_BASE`, default `${api}/api/registry/v1` like the journal base) and forward the visitor's X-Forwarded-For; browser requests use the same-origin path `/api/registry/v1`; add a nitro devProxy entry for `/api/registry`. Decode with TypeScript types mirroring kiosk's JSON field names.
|
||||
- `utils/marketplace.ts`: `pluginPath(id)` (`/marketplace/<vendor>/<name>`), `idFromRoute(vendor, name)`, `installCommand(id)` (`summer plugin:install <id>`), validation of vendor/name segments (lower-case letters and digits).
|
||||
- `pages/marketplace/index.vue`: list with a search box bound to `?q=`, category links bound to `?category=`, cards with icon, name, description, latest version and a featured mark; empty state; SEO title and description.
|
||||
- `pages/marketplace/[vendor]/[name].vue`: name, description, install command with a copy button (clipboard with a fallback, as on the landing's terminal card), README HTML passed through the layer's sanitizer before v-html, screenshots, versions table (version, framework minimum, requires, date), repository link; unknown plugin → 404 page.
|
||||
- nuxt.config.ts: runtimeConfig.registry.apiBase, routeRules `/marketplace` and `/marketplace/**` with swr 300; keep buildAssetsDir and the pages:extend hook working for the new pages.
|
||||
- `testing/marketplace-smoke.mjs` plus `expect-marketplace.txt` and three fixture JSON files: start a node:http registry fixture serving the fixtures under /api/registry/v1 (404 envelope otherwise, recording X-Forwarded-For), start the built server with startNuxtServer and env NUXT_REGISTRY_API_BASE, check each line with parseExpectLine (list 200 contains Journal, ?q=journal, ?category=content, detail 200 contains `summer plugin:install golem15.journal`, unknown plugin 404, a README script tag absent), and require that a fixture request carried the visitor address. Script `"test:marketplace": "node changelog/testing/marketplace-smoke.mjs"` in package.json.
|
||||
- SiteHeader.vue: a Marketplace link (plain anchor to /marketplace, `current === 'marketplace'` active) and its landing string in i18n/locales/en.json; add "/marketplace" to nodePaths in links_test.go.
|
||||
App deploy: nginx `location = /marketplace` and `location ^~ /marketplace/` proxied to 127.0.0.1:3095 with GET/HEAD only like /changelog; `location = /api/registry/v1/ingest` proxied to 127.0.0.1:8095 with `limit_except POST { deny all; }`; supervisor env `NUXT_REGISTRY_API_BASE="http://127.0.0.1:8095/api/registry/v1"` and the same in changelog.env.example; scripts/smoke.sh additionally checks that GET /api/registry/v1/plugins answers 200 with a `data` array and that the changelog process answers /marketplace. DEPLOY.md: git required on rome (`command -v git` check), migrate creates the registry tables, `work_in_serve` now true, adding the launch repositories (the tagged sibling plugins: chat, faq, journal, menu, mfa, oauthserver, paymentgateway, paymentgatewaysubscriptions, pgstripe, seo, sitemap, sociallogin, translate, user — https URLs on git.golem15.com) through the admin over the SSH tunnel or `summercms:registry-add`, the registry token (scope `registry`, file ~/.config/summercms/registry-token; Jakub mints it), and the publish flow's new manifest check and ingest-now step. README.md and CLAUDE.md: the /marketplace prefix belongs to the Node app, the registry API and its shape are a contract with released summer binaries.
|
||||
Commit as listed in Execution units (vue, then site links_test, then app).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd ../sm-summercmsio-app/vue-summercmsio-app && pnpm build:changelog >/dev/null 2>&1 && pnpm test:marketplace 2>&1 | tail -8</automated>
|
||||
<fails_when>non-zero exit, or a line reporting a failed expectation, or no line reporting all checks passed</fails_when>
|
||||
<automated>cd ../sm-summercmsio-app/vue-summercmsio-app && pnpm test:changelog 2>&1 | tail -4 && pnpm test 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit or a "not ok" line</fails_when>
|
||||
<automated>cd ../sm-summercmsio-app && scripts/check-deploy.sh 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit or an nginx "emerg" line</fails_when>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms test -short -run 'TestLanding|TestRoutes' ./... 2>&1 | tail -3</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n '"/marketplace"' ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go` prints one line
|
||||
- `grep -n "location ^~ /marketplace/\|location = /api/registry/v1/ingest" ../sm-summercmsio-app/deploy/nginx/summercms.io.conf` prints two lines
|
||||
- `grep -n "NUXT_REGISTRY_API_BASE" ../sm-summercmsio-app/deploy/supervisor/summercms-io-changelog.conf` prints one line
|
||||
- `grep -n "command -v git" ../sm-summercmsio-app/DEPLOY.md` prints at least one line, and DEPLOY.md names the fourteen launch repositories
|
||||
- `grep -n "test:marketplace" ../sm-summercmsio-app/vue-summercmsio-app/package.json` prints one line
|
||||
</acceptance_criteria>
|
||||
<done>summercms.io/marketplace renders list, search, category and detail pages from the registry, the landing links to it, and the deploy is described and validated.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| public internet → GET /api/registry/v1 | anonymous, cross-origin readers |
|
||||
| release script (token) → POST /api/registry/v1/ingest | authenticated, scoped |
|
||||
| curated git repository → ingester | repository content controlled by the plugin's maintainers |
|
||||
| ingested README → marketplace HTML | third-party markdown rendered on summercms.io |
|
||||
| summercms.io admin → presentation fields and repository URLs | trusted administrators behind the SSH tunnel |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-11 | Elevation of privilege | git invocation with repository URLs | high | mitigate | exec without shell, `--` before URLs, GIT_ALLOW_PROTOCOL=https (file only in tests), no system/global git config, no prompt; https-only URLs validated at curation |
|
||||
| T-34-12 | Denial of service | malicious repository content | high | mitigate | bare partial clone, no checkout/submodules/hooks, io.LimitReader caps (64 KiB plugin.yaml, 512 KiB README), per-command context timeouts, temp dir always removed |
|
||||
| T-34-13 | Tampering (XSS) | README rendered on /marketplace | high | mitigate | breakwater.Render at ingest (raw HTML stripped, http/https/mailto links only, nofollow) and the layer's sanitizer again before v-html; smoke asserts no script tag |
|
||||
| T-34-14 | Spoofing | repository claiming another plugin's ID or module | high | mitigate | admin-curated repositories only; module must equal the repository path; ID owned by the first-listed repository; TestRegistrySmoke/identity |
|
||||
| T-34-15 | Elevation of privilege | POST /api/registry/v1/ingest | high | mitigate | cabana.ResolveBackend plus golem15.summercms.registry_ingest via the `registry` token scope (POST only); unknown repositories 404 (no listing creation); nginx allows POST only on that exact path |
|
||||
| T-34-16 | Denial of service | ingest-now flooding | medium | mitigate | throttle:registry-ingest bucket, one queued job per repository, ls-remote digest check before cloning |
|
||||
| T-34-17 | Information disclosure | open CORS on the registry API | low | mitigate | only public listing data; GET/HEAD only; no credentials; no admin fields (last_error, enabled) in the API |
|
||||
| T-34-18 | Denial of service | public API abuse | medium | mitigate | throttle:registry-api, Cache-Control plus ETag/304, q length cap and category regex, LIKE wildcards escaped |
|
||||
| T-34-19 | Information disclosure | release script token handling | medium | mitigate | token read from a mode-600 file, never printed; the agent never mints or copies tokens; failures report status only |
|
||||
| T-34-20 | Tampering | stale manifest in a published tag | medium | mitigate | changelog-post.py refuses unless HEAD is the tag, the tree is clean and `summer plugin:manifest --check` passes for every plugin.yaml |
|
||||
| T-34-21 | Denial of service | jobs never run in production | medium | mitigate | queue.work_in_serve true; DEPLOY.md checks git and the schedule; smoke checks the API |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | no package is added (Go modules already required; no new npm dependency) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Site plugin: `go vet ./...`, `go test -short ./...`, and `go test -run TestRegistrySmoke ./...` with Docker.
|
||||
- App: `go vet ./... && go test ./...`, `python3 -B scripts/test_changelog_post.py`, `scripts/check-deploy.sh`.
|
||||
- Vue: `pnpm build:changelog`, `pnpm test:changelog`, `pnpm test:marketplace`, `pnpm test`.
|
||||
- Manual (34-VALIDATION.md): production ingest after deploy and the visual check of /marketplace.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The registry ingests curated repositories hourly and on demand and serves the frozen v1 API.
|
||||
- /marketplace renders list, search, category and detail pages.
|
||||
- Presentation fields are edited in the admin.
|
||||
- The release script checks manifests and triggers ingestion; nothing pushed, tagged or deployed.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-03-SUMMARY.md` when done, including the pending submodule SHAs (site plugin, vue app) for the release ledger.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Tables: `golem15_summercms_registry_repositories`, `golem15_summercms_registry_plugins`, `golem15_summercms_registry_versions` (migration `202610300001_create_golem15_summercms_registry`).
|
||||
- Site plugin Go: models `Repository`, `Plugin`, `Version`; package `registry` with `Ingester`, `Ingester.Repository`, `Ingester.IngestAll` (or `IngestAll`), `List`, `Detail`, `Versions`, `allowedProtocols`; handlers in `controllers/api/registry.go`; `registerRegistryRoutes`; `IngestArgs` job.
|
||||
- Endpoints: `GET /api/registry/v1/plugins`, `GET /api/registry/v1/plugins/{id}`, `GET /api/registry/v1/plugins/{id}/versions`, `POST /api/registry/v1/ingest`.
|
||||
- Admin controllers: `golem15.summercms.repositories`, `golem15.summercms.plugins`; navigation "Marketplace".
|
||||
- Permissions: `golem15.summercms.registry`, `golem15.summercms.registry_ingest`. Token scope: `registry`.
|
||||
- Console commands: `summercms:registry-ingest [url]`, `summercms:registry-add <url>...`; schedule hourly.
|
||||
- Throttle buckets: `registry-api`, `registry-ingest`. CORS profile: `http.cors_profiles.registry`.
|
||||
- Config: `queue.work_in_serve: true`; env `NUXT_REGISTRY_API_BASE`, `SUMMERCMS_REGISTRY_API`; token file `~/.config/summercms/registry-token`.
|
||||
- Script helpers: `https_remote`, `manifest_dirs`, `ingest_outcome`, `REGISTRY_API`, `REGISTRY_TOKEN_FILE`.
|
||||
- Pages: `/marketplace`, `/marketplace/<vendor>/<name>`; composable `useRegistry`; utils `pluginPath`, `idFromRoute`, `installCommand`; script `test:marketplace`.
|
||||
- Tests: `TestRegistrySmoke` (subtests detail, list, search, versions, headers, tags, nested, identity, unchanged, ingest_auth, curation, presentation), marketplace SSR smoke, changelog-post unittest cases.
|
||||
309
.planning/phases/34-plugin-marketplace/34-04-PLAN.md
Normal file
309
.planning/phases/34-plugin-marketplace/34-04-PLAN.md
Normal file
@@ -0,0 +1,309 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["34-01"]
|
||||
coupling_justified: ["34-03: same wave; 34-04 adds the new file modules/kiosk/client.go and edits internal/build and cmd/summer, while 34-03 compiles only the kiosk package from this checkout (through its go.work replace); the addition is one new file and every 34-04 commit compiles, so either order gives a correct result"]
|
||||
files_modified:
|
||||
- modules/kiosk/client.go
|
||||
- modules/kiosk/README.md
|
||||
- internal/build/install.go
|
||||
- internal/build/source.go
|
||||
- internal/build/update.go
|
||||
- internal/build/manifest.go
|
||||
- internal/build/scaffold.go
|
||||
- internal/build/install_smoke_test.go
|
||||
- internal/build/fixtures_test.go
|
||||
- cmd/summer/main.go
|
||||
- docs/plugins/marketplace.md
|
||||
- docs/console/scaffolding.md
|
||||
- docs/architecture/go-modules-and-workspaces.md
|
||||
- .planning/research/questions.md
|
||||
autonomous: true
|
||||
requirements: [SC-3, D-08, D-09, D-10, D-11, D-12, D-18]
|
||||
|
||||
estimate:
|
||||
tokens: 160000
|
||||
raw_tokens: 160000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "`summer plugin:install golem15.<name>` asks the registry for the plugin's versions, picks the newest tag whose framework minimum the app's effective framework version satisfies, installs it as a Go module with `go get`, and registers it in summer.yaml; `@vX.Y.Z` pins a version; when only tags needing a newer framework exist it fails and names the framework version to move to, without bumping the framework (D-08, SC-3)"
|
||||
- "Missing required plugins are resolved transitively against the registry, the full list of additions (id@version and mode) is printed and confirmed; `--yes` skips the prompt, a non-interactive run without `--yes` is an error naming `--yes`, and an installed dependency older than its minimum is an error naming `summer plugin:update <id>` (D-09, research F4)"
|
||||
- "`--source` installs the plugin and its dependencies as git submodules under plugins/<vendor>/<name> at the chosen tag, registered like plugin:add with the tag as the require version, plus go.work replace overrides for every local replace in the plugin's go.mod so the app builds (D-09, research F2)"
|
||||
- "`summer plugin:install <git-url>` never sends the plugin's repository to the registry: it clones over https or ssh, reads plugin.yaml at the newest tag whose framework minimum is satisfied, installs a submodule only, and resolves the plugin's required IDs through the registry; `--offline` makes no registry call and then requires those dependencies to be installed already (D-11, D-12)"
|
||||
- "`summer plugin:update [id]` moves module installs to the newest compatible tag with `go get` and checks submodule installs out at the newest compatible tag; without an ID it updates every plugin it can resolve and reports the ones it skipped (D-10)"
|
||||
- "The CLI reads `plugins.registry_url` from the app's config (default https://summercms.io, `SUMMER_PLUGINS__REGISTRY_URL` overrides, an explicitly empty value disables registry calls) and calls the registry through kiosk.Client over fetchguard with a timeout, a size cap and no redirects (D-18)"
|
||||
- "Editing summer.yaml keeps its comments and blank lines (plugin:add and plugin:install both append entries in place; user decision 5)"
|
||||
- "`--source` or a git URL in an app directory that is not a git repository refuses with a message saying a submodule needs a git repository (user decision 5)"
|
||||
- "Registry-provided module paths, repository URLs and versions are validated before any go or git call (strict vX.Y.Z, no leading \"-\", https repository URLs)"
|
||||
artifacts:
|
||||
- path: "modules/kiosk/client.go"
|
||||
provides: "registry HTTP client and plugins.registry_url"
|
||||
contains: "func RegistryURL("
|
||||
- path: "internal/build/install.go"
|
||||
provides: "resolution and module installs"
|
||||
contains: "func PlanInstall("
|
||||
- path: "internal/build/source.go"
|
||||
provides: "submodule and git-URL installs, go.work overrides"
|
||||
contains: "work\", \"edit\""
|
||||
- path: "internal/build/update.go"
|
||||
provides: "plugin:update"
|
||||
contains: "func Update("
|
||||
- path: "internal/build/install_smoke_test.go"
|
||||
provides: "TestInstallSmoke (tracer)"
|
||||
contains: "func TestInstallSmoke("
|
||||
key_links:
|
||||
- from: "cmd/summer/main.go"
|
||||
to: "internal/build/install.go"
|
||||
via: "plugin:install builds InstallOptions and calls build.PlanInstall then build.ApplyInstall"
|
||||
pattern: "build\\.(PlanInstall|ApplyInstall)\\("
|
||||
- from: "internal/build/install.go"
|
||||
to: "modules/kiosk/client.go"
|
||||
via: "versions come from kiosk.Client.Versions and the choice from kiosk.NewestCompatible against build.EffectiveFramework"
|
||||
pattern: "NewestCompatible"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
People add a marketplace plugin to their application with a single command, `summer plugin:install <id>`, and install private plugins from a git URL without contacting summercms.io. This plan builds that command and its companion `summer plugin:update`.
|
||||
|
||||
<objective>
|
||||
Add `summer plugin:install <id[@version]|git-url> [--source] [--yes] [--offline]` and `summer plugin:update [id] [--yes] [--offline]` (D-08..D-12), the registry client and the `plugins.registry_url` setting in kiosk (D-18), comment-preserving summer.yaml edits, and go.work overrides for submodule installs (research F2). Close the `--module` research question as "not in v1" (D-11).
|
||||
|
||||
Purpose: SC-3; the same client and setting serve the admin screen in 34-05.
|
||||
|
||||
Output: commits in this repository (code with its docs; the research-question update as a separate planning-docs commit). Nothing pushed or tagged.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
|
||||
@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
|
||||
@.planning/notes/2026-10-29-plugin-marketplace.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
From 34-01 (names as adjusted in its SUMMARY): kiosk wire types and RegistryAPIPath; kiosk.Parse, kiosk.Validate, kiosk.ValidVersion, kiosk.CompareVersions, kiosk.Satisfies, kiosk.NewestCompatible, kiosk.ReadIdentity; build.EffectiveFramework(ctx, appDir) (string, error) where a non-nil error with a version is a warning.
|
||||
internal/build: AddPlugin(ctx, startDir, pluginDir) (unchanged signature; v0.0.0 require stays its default), editAppModule(ctx, appDir, pluginDir, modPath), ensureGoWork, runGoWorkUse, findGoWork, applyGoWork, runGo, underRoot, findAppDir/FindAppDir, LoadManifest, SaveManifest (rewrites the file and drops comments), validateModulePath, ValidatePluginID; internal/build/new.go runGit (~286).
|
||||
modules/fetchguard: NewClient(Policy{Mode: TrustedMode, MaxBytes, Timeout}, cfg *compass.Config); Client.Get(ctx, url, header) (*Result, error); WithTransport(ctx, rt) test seam. modules/compass: Open(Options{Dir, Env}) loads config/<file>.yaml plus SUMMER_<FILE>__<KEY> env; Config.Has(path), Config.String(path).
|
||||
modules/bonfire: Interactive(out) bool; Output.Confirm(question, def) (bool, error); the auto-confirming helper in helpers.go returns true off a terminal and must not be used here (research F4); FlagBool(in, name); Input.Argument(name).
|
||||
Verified Go behaviour (research F5): `go get mod@v` works inside a go.work workspace and edits the app module; `go list -m -json` resolves module-cache dirs; a go.work replace overrides a workspace module's own replace of the same path (F2); `git submodule add <url> <path>` adopts an existing clone and `git submodule absorbgitdirs` moves its .git.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Run `git status --short internal/build cmd/summer modules/kiosk docs` before each task; stop if another session has uncommitted edits in these files. Stage explicit paths. No co-author tag.
|
||||
- Resume check: `git log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commit subject | Resume point |
|
||||
|------|----------------|--------------|
|
||||
| 1 | `feat(build): summer plugin:install for registry plugins as Go modules` | TestInstallSmoke/module passes |
|
||||
| 2 | `feat(build): plugin:install resolves requirements, pins and prompts` | TestInstallSmoke/deps passes |
|
||||
| 3 | `feat(build): plugin:install --source and git URLs as submodules` | TestInstallSmoke/source passes |
|
||||
| 4 | `feat(build): summer plugin:update`; `docs(34): close the --module question for v1` | TestInstallSmoke/update passes |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "summer plugin:install acme.blog --yes adds a registry plugin to an app as a Go module" — one plugin, no dependencies, module mode only</name>
|
||||
<reversibility rating="costly">`plugins.registry_url` becomes a key in apps' config files and in released binaries; renaming it later needs a fallback read of the old key.</reversibility>
|
||||
<files>modules/kiosk/client.go, modules/kiosk/README.md, internal/build/install.go, internal/build/manifest.go, internal/build/scaffold.go, internal/build/install_smoke_test.go, internal/build/fixtures_test.go, cmd/summer/main.go, docs/plugins/marketplace.md, docs/console/scaffolding.md</files>
|
||||
<read_first>
|
||||
- modules/kiosk/registry.go, modules/kiosk/version.go, internal/build/plugindirs.go (34-01 output)
|
||||
- modules/fetchguard/client.go (NewClient, Get, WithTransport) and modules/fetchguard/README.md (TrustedMode rules)
|
||||
- modules/compass/config.go (Open, Has, String)
|
||||
- internal/build/scaffold.go (AddPlugin, editAppModule, ensureGoWork, runGo, applyGoWork) and internal/build/manifest.go (LoadManifest, SaveManifest)
|
||||
- internal/build/build_test.go (TestMakeAndAddPluginOnHelloCopy: the hello-copy app with an absolute framework replace)
|
||||
- cmd/summer/main.go (addPluginCommand) and cmd/summer/commands_documented_test.go
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Pattern 4, F3, F5, Pitfalls 4-5, the Validation Architecture row on the file:// GOPROXY)
|
||||
</read_first>
|
||||
<action>
|
||||
Per D-08 (single plugin, newest compatible) and D-18. kiosk `client.go`: `DefaultRegistryURL = "https://summercms.io"`, `RegistryURL(cfg *compass.Config) string` (default when the key is absent, the configured value otherwise, "" meaning disabled), `ErrRegistryDisabled`, `ErrNotFound`, `Client` with `NewClient(baseURL string, cfg *compass.Config) (*Client, error)` over `fetchguard.NewClient(fetchguard.Policy{Mode: fetchguard.TrustedMode, MaxBytes: 4 MiB, Timeout: 10s}, cfg)` (base URL must be http or https without user info; no redirects are followed, so it must be the final origin), and methods `List(ctx, q, category string) (ListResponse, error)`, `Plugin(ctx, id string) (RegistryPluginDetail, error)`, `Versions(ctx, id string) ([]RegistryVersion, error)` decoding kiosk's wire types (404 → ErrNotFound, other non-2xx → error with the registry's error code).
|
||||
internal/build `install.go`: `InstallOptions{StartDir, Arg string; Source, Yes, Offline bool; Registry Registry; Interactive bool; Confirm func(summary string) (bool, error); Out io.Writer}` where `Registry` is the interface of the three client methods (tests pass a fake or an httptest-backed client); `InstallPlan{AppDir, Framework string; Steps []InstallStep}`; `InstallStep{ID, Version, Module, Package, Repository, Mode string}`; `PlanInstall(ctx, opts) (*InstallPlan, error)` and `ApplyInstall(ctx, plan *InstallPlan, out io.Writer) error`. In this task: parse `<id>`, fetch versions, filter with kiosk.NewestCompatible against build.EffectiveFramework (print its warning), validate the chosen module (validateModulePath) and version (kiosk.ValidVersion) before exec, apply with one `go get <module>@<version>` run in the app dir with applyGoWork, then append `{id, package}` to summer.yaml. Replace SaveManifest use in AddPlugin with a new comment-preserving `appendManifestPlugins(path string, plugins []Plugin) error` that inserts entries after the last item of the `plugins:` block at the existing indentation (or appends a block), re-parses the result with LoadManifest and restores the original bytes on any error. An ID already in summer.yaml is an error suggesting `summer plugin:update <id>`. Finish with "Installed … ; run summer build" (do not run go mod tidy: research Pitfall 5).
|
||||
cmd/summer: `pluginInstallCommand()` registered after plugin:manifest: Name `plugin:install`, Description, Long (argument forms, modes, framework rule, prompt and --yes, --offline, plugins.registry_url), Examples, Arg `plugin` (required: `vendor.name[@vX.Y.Z]` or a git URL), Flags `source`, `yes`, `offline` (Bare, read with bonfire.FlagBool). It opens the app's config with compass.Open(Options{Dir: <app>/config}), builds the client from kiosk.RegistryURL, sets Interactive from bonfire.Interactive(out) and Confirm from out.Confirm. Prompt rule (research F4): with --yes no prompt; otherwise interactive → Confirm(summary, false); non-interactive → an error naming `--yes`.
|
||||
`fixtures_test.go`: helpers `writeProxyModule(t, proxyDir, modPath, version string, files map[string]string)` building `@v/list`, `.info`, `.mod` and `.zip` with archive/zip (zip entries prefixed `<module>@<version>/`), `fakeRegistry(t, plugins)` serving kiosk JSON through httptest, and `helloCopy(t)` reusing the TestMakeAndAddPluginOnHelloCopy setup. `install_smoke_test.go` `TestInstallSmoke/module` (skipped under -short): registry lists acme.blog with v0.1.0 (framework v0.5.0) and v0.2.0 (framework v9.0.0); the go subprocesses run with GOPROXY=`file://<fixture>,file://<real GOMODCACHE>/cache/download`, GOMODCACHE=t.TempDir(), GOSUMDB=off, GOFLAGS=-modcacherw (offline; record the exact env that worked in the SUMMARY); after PlanInstall plus ApplyInstall with Yes, assert go.mod requires the fixture module at v0.1.0, summer.yaml lists acme.blog and still has a comment planted before the install, and build.App builds the app. A second subtest asserts a pin to v0.2.0 fails naming v9.0.0.
|
||||
Docs in the same commit: client and RegistryURL in modules/kiosk/README.md; "Installing plugins" and the `plugins.registry_url` key in docs/plugins/marketplace.md; a `plugin:install` row in docs/console/scaffolding.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run 'TestInstallSmoke/module' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestInstallSmoke/module" line</fails_when>
|
||||
<automated>go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree' -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL" or "unknown identifier" line</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'DefaultRegistryURL = "https://summercms.io"' modules/kiosk/client.go` prints one line
|
||||
- `grep -n '"plugin:install"' cmd/summer/main.go` prints one line
|
||||
- `grep -n "bonfire.Interactive" cmd/summer/main.go` prints at least one line, and `grep -n "ConfirmProceed" cmd/summer/main.go internal/build/install.go` shows no call inside the plugin:install code path
|
||||
- `grep -n "appendManifestPlugins" internal/build/scaffold.go internal/build/install.go` prints at least two lines
|
||||
- `grep -n "plugins.registry_url" docs/plugins/marketplace.md` prints at least one line
|
||||
</acceptance_criteria>
|
||||
<done>A registry plugin installs into an app as a Go module with one command and the app builds.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Requirements, pins, prompts and offline rules for registry installs</name>
|
||||
<files>internal/build/install.go, internal/build/install_smoke_test.go, internal/build/fixtures_test.go, cmd/summer/main.go, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- internal/build/install.go and install_smoke_test.go (Task 1)
|
||||
- modules/kiosk/version.go (Satisfies, NewestCompatible)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Pattern 4 closure rules)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- installing a plugin that requires golem15.translate v0.1.0 when translate is absent plans two steps, dependency first, each the newest compatible version that also satisfies the minimum, and one go get installs both
|
||||
- a dependency already in summer.yaml whose installed version is below the minimum is an error naming `summer plugin:update golem15.translate`
|
||||
- a dependency cycle in registry data terminates and is reported, it does not loop
|
||||
- `name@v0.1.0` installs exactly that version when compatible; an unknown version is an error listing the available ones
|
||||
- an unknown ID answers "not in the registry" with the registry URL used
|
||||
- with Yes false and Interactive false the plan is printed and PlanInstall's caller gets an error containing "--yes"; with Interactive true and Confirm returning false nothing is changed
|
||||
- with Offline true or an empty plugins.registry_url, an ID install is an error that says so and mentions git URL installs
|
||||
- a registry entry whose module starts with "-" or whose version is not vX.Y.Z is refused before any exec
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-08 and D-09. Extend PlanInstall with the transitive closure over RegistryVersion.Requires (for each missing ID: newest version satisfying both the framework and the minimum; installed IDs: compare the installed version — module version from go.mod, or `git describe` base through kiosk.BaseVersion for local plugins — with the minimum), cycle detection, pins, the not-found and disabled cases, and validation of every registry string before ApplyInstall runs anything. Render the confirmation summary as one line per step (`id@version (module|submodule)`), printed before the prompt. ApplyInstall runs a single `go get` with every module@version so MVS sees the whole set, then appends all summer.yaml entries in dependency order. Write the behavior subtests (`deps`, `installed_old`, `cycle`, `pin`, `unknown`, `prompt`, `offline`, `invalid`) first; they use the fake registry and, where nothing is applied, run under -short. Document the requirement rules and the prompt in docs/plugins/marketplace.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run 'TestInstallSmoke' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestInstallSmoke/deps" or "--- PASS: TestInstallSmoke/prompt" line</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- every behavior subtest named in the action exists and passes
|
||||
- `grep -n "summer plugin:update" internal/build/install.go` prints at least one line (the installed-too-old hint)
|
||||
- `grep -n -- "--yes" internal/build/install.go cmd/summer/main.go` prints at least one line in an error message
|
||||
</acceptance_criteria>
|
||||
<done>Registry installs bring their missing requirements, refuse incompatible ones with the version to move to, and never proceed without a confirmation or --yes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: --source and git-URL installs as git submodules with go.work overrides</name>
|
||||
<files>internal/build/source.go, internal/build/install.go, internal/build/scaffold.go, internal/build/install_smoke_test.go, internal/build/fixtures_test.go, cmd/summer/main.go, docs/plugins/marketplace.md, docs/architecture/go-modules-and-workspaces.md</files>
|
||||
<read_first>
|
||||
- internal/build/install.go (Tasks 1-2), internal/build/scaffold.go (editAppModule, ensureGoWork, runGoWorkUse)
|
||||
- internal/build/new.go (~286 runGit)
|
||||
- ../sm-demo-app/go.work (hand-written replace overrides for submodule plugins) — read only
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (F2, Pattern 4, "CLI git" code example, Security Domain)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `--source` for an ID clones the registry's repository at the chosen tag into plugins/<vendor>/<name>, adds it as a submodule (`.gitmodules` lists the path, `git submodule status` shows the tag), registers it with require <module>@<tag> plus a local replace and a go.work use, and the app builds although the plugin's go.mod replaces the framework with ../summercms.go (go.work replace override)
|
||||
- dependencies of a `--source` install are installed as submodules too
|
||||
- a git URL install reads plugin.yaml at the newest tag whose framework minimum is satisfied, falls back to the default branch head with a warning when no tag qualifies, never calls the registry for the plugin itself (the fake registry records no request for its ID), and resolves its required IDs through the registry
|
||||
- a git URL install with --offline and a required ID not in summer.yaml fails naming the ID; with it installed it succeeds without any registry request
|
||||
- an app directory that is not a git repository refuses --source and git URLs with a message about submodules
|
||||
- URLs starting with "-", ext:: or file:: forms and file:// (outside tests) are refused; https, ssh:// and scp-like user@host:path are accepted
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-09, D-11, D-12, user decision 5 and research F2. `source.go`: git runner for the CLI that keeps the user's git config (credential helpers, insteadOf), sets `GIT_ALLOW_PROTOCOL` from a package variable `cliGitProtocols` (default "https:ssh"; tests set "https:ssh:file"), sets GIT_TERMINAL_PROMPT=0 when not interactive, and puts `--` before URLs and paths. Git URL detection and validation (`isGitURL`, `validateGitURL`). Target directory: plugins/<vendor>/<name> from the ID in the repository root's plugin.yaml (a repository without one uses its name stripped of `sm-` and `-plugin`); a nested plugin's package directory lives inside it and the module root is what go.work uses. Flow: clone into a temp directory under plugins/, pick the tag (git URL: `git tag` plus `git show <tag>:<path>/plugin.yaml` with kiosk.Parse and the framework rule; ID: the registry's chosen version and repository URL, validated as https), check it out, move it to the target, `git submodule add -- <url> <path>` (adopts the clone) and `git submodule absorbgitdirs -- <path>`, then register through an internal variant of AddPlugin that takes the require version (editAppModule gains a version parameter; plugin:add keeps v0.0.0), and add go.work replace overrides: for every replace in the plugin's go.mod (`go mod edit -json`) whose path the go.work does not already replace, add `go work edit -replace=<path>=<target>` where target is the app's own local replace of that path when present, the workspace directory of that module when it is another installed submodule, otherwise `<path>@<version the app's go.mod requires>`. Non-git app: refuse before cloning. Wire `--source` and git URLs into PlanInstall/ApplyInstall (steps carry Mode submodule) and the CLI. Fixture repositories in tests are created with git init in t.TempDir and addressed with file:// URLs. Write the behavior subtests (`source`, `source_deps`, `giturl`, `giturl_offline`, `nogit`, `urls`) first. Document submodule installs, git URLs and the go.work overrides in docs/plugins/marketplace.md and docs/architecture/go-modules-and-workspaces.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run 'TestInstallSmoke/(source|source_deps|giturl|giturl_offline|nogit|urls)' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestInstallSmoke/source" line</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "absorbgitdirs" internal/build/source.go` prints one line
|
||||
- `grep -n '"https:ssh"' internal/build/source.go` prints one line
|
||||
- `grep -n "go work edit\|\"work\", \"edit\"" internal/build/source.go` prints at least one line
|
||||
- `go run ./cmd/summer plugin:add --help` output is unchanged in meaning, and TestMakeAndAddPluginOnHelloCopy still passes (plugin:add keeps require v0.0.0)
|
||||
</acceptance_criteria>
|
||||
<done>Plugins install from source or from any git URL the user can reach, as submodules that build inside the app's workspace.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: summer plugin:update [id] for module and submodule installs, and the --module question closed</name>
|
||||
<files>internal/build/update.go, internal/build/install_smoke_test.go, cmd/summer/main.go, docs/plugins/marketplace.md, docs/console/scaffolding.md, .planning/research/questions.md</files>
|
||||
<read_first>
|
||||
- internal/build/install.go and internal/build/source.go (Tasks 1-3)
|
||||
- internal/build/plugindirs.go (34-01: per-plugin source and version)
|
||||
- .planning/research/questions.md (the --module entry)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- a module install at v0.1.0 updates to v0.1.1 when the registry adds it (compatible), and stays when the only newer version needs a newer framework, reporting that version and the framework it needs
|
||||
- a submodule install checks out the newest compatible tag and its go.mod require follows the tag; the framework pin in go.work is refreshed when the app's framework requirement changed
|
||||
- `plugin:update` without an ID updates every resolvable plugin, lists in-app plugins and unlisted plugins as skipped, and exits zero unless a resolvable update failed
|
||||
- the update plan is printed and needs confirmation or --yes exactly like install
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-10 (no-ID semantics are Claude's discretion: skip and report what cannot be resolved; fail only when a resolvable update fails). `update.go`: `UpdateOptions{StartDir, ID string; Yes, Offline, Interactive bool; Registry Registry; Confirm func(string) (bool, error); Out io.Writer}` and `Update(ctx, opts) error`: resolve each summer.yaml plugin's source and version with the 34-01 resolver; module plugins → newest compatible registry version above the installed one → one `go get` for all; submodule plugins → `git -C <dir> fetch --tags`, newest compatible tag (registry for listed IDs, the submodule's own tags and plugin.yaml for git-URL plugins), `git -C <dir> checkout <tag>`, `go mod edit -require=<module>@<tag>`, then refresh go.work overrides; --offline restricts to submodules and their own tags. CLI `pluginUpdateCommand()`: Name `plugin:update`, full docs, optional Arg `plugin`, Flags `yes`, `offline`. Write the behavior subtests (`update`, `update_submodule`, `update_all`, `update_prompt`) first. Docs: "Updating plugins" in docs/plugins/marketplace.md and a `plugin:update` row in docs/console/scaffolding.md (same commit). Then, as a separate planning-docs commit, mark the `--module` question in .planning/research/questions.md answered: not in v1, git URLs install as submodules only (D-11), deferred until submodule-only proves limiting.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run 'TestInstallSmoke/update' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestInstallSmoke/update" line</fails_when>
|
||||
<automated>go test ./cmd/summer -run 'TestCommandsDocumented|TestDocsTree' -count=1 2>&1 | tail -3 && go run ./cmd/summer docs:build --check 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL" or "unknown identifier" line</fails_when>
|
||||
<automated>go vet ./... && go test ./internal/build ./cmd/summer ./modules/kiosk 2>&1 | tail -5</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n '"plugin:update"' cmd/summer/main.go` prints one line
|
||||
- `grep -n "func Update(ctx context.Context, opts UpdateOptions) error" internal/build/update.go` prints one line
|
||||
- `grep -n -i "not in v1" .planning/research/questions.md` prints a line in the --module entry
|
||||
- docs/console/scaffolding.md names plugin:install and plugin:update
|
||||
</acceptance_criteria>
|
||||
<done>The admin's "update available" badge has a matching command, and the --module question is closed for v1.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| registry JSON → CLI → go/git | module paths, versions and repository URLs from summercms.io (or a configured registry) |
|
||||
| user-supplied git URL → git | private or third-party repositories |
|
||||
| third-party plugin code → app binary | compiled in after confirmation |
|
||||
| CLI → app directory | summer.yaml, go.mod, go.work, .gitmodules, plugins/ |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-22 | Elevation of privilege | registry strings passed to go get / git clone | high | mitigate | validateModulePath, kiosk.ValidVersion, https-only repository URLs, exec without shell, `--` before URLs and paths; TestInstallSmoke/invalid and /urls |
|
||||
| T-34-23 | Elevation of privilege | ext:: or file:: transports via a git URL | high | mitigate | GIT_ALLOW_PROTOCOL=https:ssh (file only in tests), URL validator refuses leading "-" and transport prefixes |
|
||||
| T-34-24 | Tampering | malicious plugin code compiled into the app | high | accept | inherent to compiled plugins; mitigated by printing every addition and requiring confirmation or --yes (D-09); sumdb verification stays on for module installs (GONOSUMDB is never set) |
|
||||
| T-34-25 | Tampering | writes outside the app root | medium | mitigate | targets derived from validated IDs under plugins/, underRoot checks before clone, move and registration |
|
||||
| T-34-26 | Denial of service | slow or hostile registry | medium | mitigate | fetchguard client with timeout, 4 MiB cap and no redirects; clear error and --offline for git URLs |
|
||||
| T-34-27 | Spoofing | registry pointing at a different module than it lists | medium | mitigate | the ingester ties module to repository (34-03); the CLI re-reads plugin.yaml from the checkout for submodule installs and refuses an ID mismatch |
|
||||
| T-34-28 | Tampering | summer.yaml damaged by an edit | low | mitigate | appendManifestPlugins re-parses and restores the original bytes on error |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | the CLI installs Go modules chosen by the user; no package is added to the framework |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go vet ./... && go test -short ./...` green after every commit; `go test ./internal/build -run TestInstallSmoke` (non-short) green.
|
||||
- TestCommandsDocumented, TestDocsTree and `summer docs:build --check` pass.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- plugin:install covers ID (module), ID --source and git URL; plugin:update covers modules and submodules.
|
||||
- Requirement checks, prompts and --offline behave per D-08..D-12.
|
||||
- No push or tag.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-04-SUMMARY.md` when done, including the go env that made the offline module-install test work.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Types: `kiosk.Client`, `build.InstallOptions`, `build.InstallPlan`, `build.InstallStep`, `build.Registry`, `build.UpdateOptions`.
|
||||
- Functions: `kiosk.NewClient`, `kiosk.RegistryURL`, `kiosk.Client.List`, `kiosk.Client.Plugin`, `kiosk.Client.Versions`, `build.PlanInstall`, `build.ApplyInstall`, `build.Update`; internal `appendManifestPlugins`, `isGitURL`, `validateGitURL`.
|
||||
- Constants and variables: `kiosk.DefaultRegistryURL`, `kiosk.ErrRegistryDisabled`, `kiosk.ErrNotFound`, `cliGitProtocols`.
|
||||
- CLI: `summer plugin:install <id[@vX.Y.Z]|git-url>` with `--source`, `--yes`, `--offline`; `summer plugin:update [id]` with `--yes`, `--offline`.
|
||||
- Config key: `plugins.registry_url` (file config/plugins.yaml, env `SUMMER_PLUGINS__REGISTRY_URL`; default https://summercms.io; empty disables).
|
||||
- Tests: `TestInstallSmoke` (module, deps, installed_old, cycle, pin, unknown, prompt, offline, invalid, source, source_deps, giturl, giturl_offline, nogit, urls, update, update_submodule, update_all, update_prompt); helpers `writeProxyModule`, `fakeRegistry`, `helloCopy`.
|
||||
- Planning: `.planning/research/questions.md` --module entry answered (not in v1).
|
||||
275
.planning/phases/34-plugin-marketplace/34-05-PLAN.md
Normal file
275
.planning/phases/34-plugin-marketplace/34-05-PLAN.md
Normal file
@@ -0,0 +1,275 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["34-01", "34-04"]
|
||||
files_modified:
|
||||
- modules/cabana/plugins.go
|
||||
- modules/cabana/http.go
|
||||
- modules/cabana/backend_screens.go
|
||||
- modules/cabana/navigation.go
|
||||
- modules/cabana/admin_openapi.go
|
||||
- modules/cabana/phase09_contract_test.go
|
||||
- modules/cabana/security_coverage_test.go
|
||||
- modules/cabana/plugins_smoke_test.go
|
||||
- modules/cabana/README.md
|
||||
- modules/phrasebook/backend/lang/en/lang.yaml
|
||||
- modules/phrasebook/backend/lang/pl/lang.yaml
|
||||
- admin/openapi/admin.json
|
||||
- admin/src/api/schema.d.ts
|
||||
- admin/src/views/PluginsView.vue
|
||||
- admin/src/app/router.ts
|
||||
- admin/src/state/useSettings.ts
|
||||
- admin/tests/views/PluginsView.test.ts
|
||||
- modules/boardwalk/dist
|
||||
- docs/backend/users-and-permissions.md
|
||||
- docs/backend/admin-spa.md
|
||||
- docs/plugins/marketplace.md
|
||||
autonomous: true
|
||||
requirements: [SC-4, D-06, D-07, D-17, D-18, D-19]
|
||||
|
||||
estimate:
|
||||
tokens: 140000
|
||||
raw_tokens: 140000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "The admin has a read-only Plugins screen at /system/plugins, linked from Settings → System, visible to administrators holding the new permission backend.view_plugins; superusers have it without any grant (D-19)"
|
||||
- "The Installed tab lists every activated plugin with the name, description, version and source baked by summer build (ID only for a plugin without plugin.yaml) and, for plugins listed in the registry, whether an update compatible with the built framework version exists, with the matching `summer plugin:update <id>` command (D-06, D-07, D-17, SC-4)"
|
||||
- "The SPA calls only cabana: GET /plugins and GET /plugins/marketplace are swag-annotated cabana endpoints that call the registry server-side with a timeout and a short in-process cache and compute update availability in Go; no CSP or CORS change in the admin (D-17)"
|
||||
- "The Marketplace tab searches the registry by text and category and shows `summer plugin:install <id>` with a copy button; there is no install button (SC-4, decision note)"
|
||||
- "An empty plugins.registry_url hides the Marketplace tab and the update badges while the Installed tab still works; a registry that is down or slow leaves the Installed tab rendering with a registry-unavailable notice (D-18)"
|
||||
- "admin/openapi/admin.json and admin/src/api/schema.d.ts are regenerated from the annotations, the contract inventories list the new routes, and modules/boardwalk/dist is rebuilt and passes scripts/check-admin-dist.sh"
|
||||
artifacts:
|
||||
- path: "modules/cabana/plugins.go"
|
||||
provides: "Plugins screen endpoints, registry cache, update computation"
|
||||
contains: "PermissionViewPlugins"
|
||||
- path: "admin/src/views/PluginsView.vue"
|
||||
provides: "Installed and Marketplace tabs"
|
||||
contains: "plugin:install"
|
||||
- path: "modules/cabana/plugins_smoke_test.go"
|
||||
provides: "TestPluginsScreenSmoke (tracer)"
|
||||
contains: "func TestPluginsScreenSmoke("
|
||||
- path: "admin/tests/views/PluginsView.test.ts"
|
||||
provides: "PluginsView smoke spec"
|
||||
contains: "PluginsView"
|
||||
key_links:
|
||||
- from: "modules/cabana/plugins.go"
|
||||
to: "modules/party/info.go"
|
||||
via: "the Installed handler reads app.Lookup[party.Installed]() (falls back to party.Activated IDs for an app built before 34-01)"
|
||||
pattern: "party\\.Installed"
|
||||
- from: "modules/cabana/plugins.go"
|
||||
to: "modules/kiosk/client.go"
|
||||
via: "registry calls through kiosk.Client built from kiosk.RegistryURL(app config), update rule through kiosk.NewestCompatible and kiosk.BaseVersion"
|
||||
pattern: "kiosk\\.(NewClient|RegistryURL|NewestCompatible|BaseVersion)"
|
||||
- from: "admin/src/views/PluginsView.vue"
|
||||
to: "modules/cabana/plugins.go"
|
||||
via: "typed api client calls GET /plugins and GET /plugins/marketplace only"
|
||||
pattern: "/plugins"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
The admin SPA gains a read-only Plugins screen listing installed plugins with versions and available updates and searching the marketplace, which shows the install command with a copy button. This plan builds that screen on top of the metadata summer build bakes (34-01) and the registry client (34-04).
|
||||
|
||||
<objective>
|
||||
Add cabana endpoints GET /plugins and GET /plugins/marketplace with the permission backend.view_plugins (D-17, D-19), a Settings → System entry that opens a custom SPA route, and PluginsView with Installed and Marketplace tabs; regenerate the OpenAPI document and the SPA types, update the contract inventories, rebuild and commit the embedded dist.
|
||||
|
||||
Purpose: SC-4.
|
||||
|
||||
Output: commits in this repository (code with its docs; the rebuilt dist in its own commit). Nothing pushed or tagged.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-CONTEXT.md
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-PATTERNS.md
|
||||
@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-04-SUMMARY.md
|
||||
@.planning/notes/2026-10-07-embedded-admin-release-gate.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
From 34-01: party.Info{ID, Module, Package, Name, Description, Version, Source}, party.Installed{Framework, Plugins} published by the generated main; kiosk.BaseVersion, kiosk.CompareVersions, kiosk.NewestCompatible, kiosk wire types. From 34-04: kiosk.Client (List, Plugin, Versions), kiosk.NewClient(baseURL, cfg), kiosk.RegistryURL(cfg) ("" = disabled), kiosk.ErrNotFound.
|
||||
modules/cabana: routes in http.go inside `r.GroupRaw(api, []string{"backend", "locale.from-principal"}, …)` (~316-339, next to `g.Get("/settings", s.settingsList)`); handler pattern settingsList (~560) with bouncer.User and WriteData/WriteError; permissions in backend_screens.go (PermissionManageUsers = "backend.manage_users", Permissions() with Tab "backend::lang.permissions.tab_system"); SettingsEntry in navigation.go (~23-35) and Registry.Metadata builds the permission-filtered settings list; cabana.Allows(principal, []string); swag blocks in admin_openapi.go (~469-479 AdminSettingsList); TestPhase09ContractInventory (phase09_contract_test.go) and TestPhase09PermissionMatrix/TestPhase09SecurityCoverage (security_coverage_test.go ~55, ~184) list every route and handler.
|
||||
admin: router.ts routes (/settings, /account, then /:vendor/:plugin/:controller); useSettings.ts settingsPath(entry) (~30-39); typed api client admin/src/api/client.ts; reka-ui 2.9.10 (Tabs primitives); copy pattern in admin/src/components/account/ApiTokenCreateDialog.vue (~255-268); tests in admin/tests/views/*.test.ts with admin/tests/helpers.ts.
|
||||
Strings: modules/phrasebook/backend/lang/{en,pl}/lang.yaml (permissions block ~548 en, ~584 pl).
|
||||
Other sessions often edit cabana: check `git status --short modules/cabana admin` and `git log -3 -- modules/cabana` before each task.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Before each task: `git status --short modules/cabana admin modules/phrasebook modules/boardwalk docs/backend`; stop if another session has uncommitted edits in files this task touches. Stage explicit paths. No co-author tag.
|
||||
- Resume check: `git log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commit subject | Resume point |
|
||||
|------|----------------|--------------|
|
||||
| 1 | `feat(cabana): read-only Plugins screen with installed plugins` | TestPluginsScreenSmoke/installed and the PluginsView spec pass |
|
||||
| 2 | `feat(cabana): plugin updates and marketplace search on the Plugins screen` | TestPluginsScreenSmoke passes |
|
||||
| 3 | `build(admin): rebuild the embedded admin for the Plugins screen` | check-admin-dist.sh passes |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "an administrator with backend.view_plugins opens Settings → Plugins and sees the installed plugins with their versions" — Installed tab only, no registry</name>
|
||||
<files>modules/cabana/plugins.go, modules/cabana/http.go, modules/cabana/backend_screens.go, modules/cabana/navigation.go, modules/cabana/admin_openapi.go, modules/cabana/phase09_contract_test.go, modules/cabana/security_coverage_test.go, modules/cabana/plugins_smoke_test.go, modules/cabana/README.md, modules/phrasebook/backend/lang/en/lang.yaml, modules/phrasebook/backend/lang/pl/lang.yaml, admin/openapi/admin.json, admin/src/api/schema.d.ts, admin/src/views/PluginsView.vue, admin/src/app/router.ts, admin/src/state/useSettings.ts, admin/tests/views/PluginsView.test.ts, docs/backend/users-and-permissions.md, docs/backend/admin-spa.md, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- modules/cabana/http.go (~300-345 routes, ~560 settingsList), modules/cabana/backend_screens.go, modules/cabana/navigation.go (SettingsEntry, Registry.Metadata)
|
||||
- modules/cabana/admin_openapi.go (~469-479) and scripts/check-admin-openapi.sh
|
||||
- modules/cabana/phase09_contract_test.go and modules/cabana/security_coverage_test.go (route and handler lists)
|
||||
- modules/party/info.go (34-01)
|
||||
- admin/src/app/router.ts, admin/src/state/useSettings.ts, admin/src/views/SettingsIndexView.vue, admin/src/views/AccountView.vue (a shell view to copy), admin/tests/views/SettingsFormView.test.ts and admin/tests/helpers.ts
|
||||
- docs/backend/users-and-permissions.md, docs/backend/admin-spa.md
|
||||
</read_first>
|
||||
<action>
|
||||
Per D-06, D-07, D-17 and D-19. cabana: `PermissionViewPlugins = "backend.view_plugins"` declared in backendScreens.Permissions() with Tab tab_system and Label `backend::lang.permissions.view_plugins` (no role grants; superusers pass every check). `SettingsEntry` gains `Path string json:"path,omitempty"` (additive); Registry.Metadata appends cabana's own entry (code `plugins`, category system, label and description from lang, Path `/system/plugins`) only when the principal is allowed backend.view_plugins. `plugins.go`: types `PluginsOverview{Framework string; Registry RegistryState; Plugins []InstalledPlugin}`, `RegistryState{Enabled bool; Status string}` (status `ok`, `disabled`, `unavailable`), `InstalledPlugin{ID, Name, Description, Version, Source, Module, Package string; Listed bool; Latest, LatestFramework string; UpdateAvailable bool; UpdateCommand string}` with lower camel JSON tags matching the existing admin API style; handler `pluginsInstalled` (401 without a backend principal, 403 via WriteError when not allowed backend.view_plugins) reading `app.Lookup[party.Installed]()` and falling back to `app.Lookup[party.Activated]()` IDs for an app built before 34-01; Framework from party.Installed, else bonfire.BuildVersion().Framework. In this task Registry is reported as configured or `disabled` only (kiosk.RegistryURL of the app config) and no registry call is made. Route `g.Get("/plugins", s.pluginsInstalled)` in the backend group. swag block `AdminPluginsInstalled` (@Router /plugins [get], Envelope[PluginsOverview], 401, 403); run `scripts/check-admin-openapi.sh` to regenerate admin/openapi/admin.json and admin/src/api/schema.d.ts; add the route and handler to the contract inventories. Lang keys en and pl: permission label, settings entry label and description, screen strings (title, tabs, columns, source names, empty state).
|
||||
SPA: route `{ path: '/system/plugins', name: 'plugins', component: PluginsView, meta: { shell: true } }` before the `/:vendor/:plugin/:controller` routes; `settingsPath` returns `entry.path` first when set; `PluginsView.vue` uses reka-ui Tabs with the Installed tab (name or ID, description, version, source) loaded through the typed api client from GET /plugins, and the Marketplace tab present but filled in Task 2. All text through the existing translation helper; render registry text as text, never HTML.
|
||||
Tests first: `TestPluginsScreenSmoke/installed` (an app with party.Installed published: superuser 200 with every plugin and its version; an administrator without the permission 403; with it 200; settings list contains the plugins entry with path only for allowed principals; an app without party.Installed lists Activated IDs) and `admin/tests/views/PluginsView.test.ts` (renders the Installed rows from a mocked /plugins response). Docs in the same commit: permission in docs/backend/users-and-permissions.md, the screen in docs/backend/admin-spa.md and an "Admin Plugins screen" section in docs/plugins/marketplace.md, new identifiers in modules/cabana/README.md. Do not rebuild the dist here (Task 3). Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/cabana -run 'TestPluginsScreenSmoke|TestPhase09ContractInventory|TestPhase09PermissionMatrix|TestPhase09SecurityCoverage' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginsScreenSmoke/installed" line</fails_when>
|
||||
<automated>scripts/check-admin-openapi.sh --check</automated>
|
||||
<fails_when>non-zero exit (committed OpenAPI or schema.d.ts differs from the generated one)</fails_when>
|
||||
<automated>npm --prefix admin run test -- PluginsView 2>&1 | tail -6 && npm --prefix admin run typecheck 2>&1 | tail -3</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, or "No test files found"</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'PermissionViewPlugins = "backend.view_plugins"' modules/cabana/backend_screens.go modules/cabana/plugins.go` prints one line
|
||||
- `grep -n 'g.Get("/plugins", s.pluginsInstalled)' modules/cabana/http.go` prints one line
|
||||
- `grep -n "@Router /plugins \[get\]" modules/cabana/admin_openapi.go` prints one line; `grep -n '"/plugins"' admin/openapi/admin.json` prints at least one line
|
||||
- `grep -n "/system/plugins" admin/src/app/router.ts` prints one line and `grep -n "entry.path" admin/src/state/useSettings.ts` prints at least one line
|
||||
- `grep -n "view_plugins" modules/phrasebook/backend/lang/en/lang.yaml modules/phrasebook/backend/lang/pl/lang.yaml` prints two lines
|
||||
</acceptance_criteria>
|
||||
<done>An allowed administrator reaches the Plugins screen from Settings and sees every installed plugin with its version and source.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Update badges and marketplace search through cabana, degraded when the registry is down or disabled</name>
|
||||
<files>modules/cabana/plugins.go, modules/cabana/http.go, modules/cabana/admin_openapi.go, modules/cabana/phase09_contract_test.go, modules/cabana/security_coverage_test.go, modules/cabana/plugins_smoke_test.go, modules/cabana/README.md, modules/phrasebook/backend/lang/en/lang.yaml, modules/phrasebook/backend/lang/pl/lang.yaml, admin/openapi/admin.json, admin/src/api/schema.d.ts, admin/src/views/PluginsView.vue, admin/tests/views/PluginsView.test.ts, docs/plugins/marketplace.md</files>
|
||||
<read_first>
|
||||
- modules/cabana/plugins.go (Task 1)
|
||||
- modules/kiosk/client.go, modules/kiosk/version.go (34-01, 34-04)
|
||||
- modules/fetchguard/client.go (WithTransport) for the fake registry in tests
|
||||
- admin/src/components/account/ApiTokenCreateDialog.vue (~255-268 clipboard with fallback)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Pattern 5: timeouts, cache, concurrency, update rule)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- an installed module plugin at v0.1.0 with registry versions v0.1.1 (framework satisfied) and v0.2.0 (newer framework) reports Latest v0.1.1, UpdateAvailable true and UpdateCommand `summer plugin:update <id>`
|
||||
- a submodule plugin at "v0.1.1-3-gabc1234" compares by its base v0.1.1 and reports no update when v0.1.1 is the newest compatible version
|
||||
- a plugin the registry does not list has Listed false and no update fields; a local plugin (empty version) gets no registry call
|
||||
- with the registry timing out or answering 500, GET /plugins answers 200 with Registry.Status unavailable and the installed list intact, and GET /plugins/marketplace answers 200 with status unavailable and no results
|
||||
- with plugins.registry_url set to "", Registry.Enabled is false, no request leaves the process, and the SPA hides the Marketplace tab and the badges
|
||||
- GET /plugins/marketplace?q=journal&category=content forwards both parameters, marks installed plugins and plugins whose framework minimum the built framework does not satisfy, and returns InstallCommand `summer plugin:install <id>`; q longer than 100 characters or an invalid category answers 400
|
||||
- a second identical request within the cache window makes no registry request
|
||||
- the copy button writes the command to the clipboard and shows a copied state; when the clipboard API is missing it shows the fallback state
|
||||
</behavior>
|
||||
<action>
|
||||
Per D-17 and D-18. In plugins.go: a registry dependency behind an unexported interface (List, Versions) built lazily from kiosk.NewClient(kiosk.RegistryURL(app config)) with a 4-second per-call timeout; an in-process TTL cache (marketplace queries 5 minutes, per-ID versions 15 minutes, failures cached 1 minute) guarded by a mutex; per-installed-plugin version lookups with at most 4 in flight. Update rule (research Pattern 5): latest = kiosk.NewestCompatible(versions, framework); base = kiosk.BaseVersion(installed version); UpdateAvailable = base != "" and latest is newer by kiosk.CompareVersions; LatestFramework names the framework a newer incompatible version needs. Types `MarketplacePlugin{ID, Name, Description, Category string; Featured bool; IconURL, LatestVersion, LatestFramework string; Installed, Compatible bool; InstallCommand string}`; handler `pluginsMarketplace` with the same auth and permission, q/category validation as the registry's, `g.Get("/plugins/marketplace", s.pluginsMarketplace)`; errors from the registry never surface as non-200 (status in the payload meta and the RegistryState). swag block `AdminPluginsMarketplace`; regenerate with scripts/check-admin-openapi.sh; add the route to the inventories. SPA: Installed rows show an update badge with the copyable update command; the Marketplace tab (hidden when Registry.Enabled is false) has a debounced search box and a category select, result cards with name, description, version, an Installed or Needs framework vX marker and the install command with a copy button using the ApiTokenCreateDialog clipboard pattern; a notice when the registry is unavailable; no install button anywhere. Lang strings en and pl. Write the behavior subtests (`updates`, `describe_base`, `unlisted`, `unavailable`, `disabled`, `marketplace`, `cache`) in TestPluginsScreenSmoke using a fake registry, and the copy and hidden-tab cases in PluginsView.test.ts, first. Document update and marketplace behaviour in docs/plugins/marketplace.md and modules/cabana/README.md. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/cabana -run 'TestPluginsScreenSmoke|TestPhase09' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -30</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginsScreenSmoke/updates" or "--- PASS: TestPluginsScreenSmoke/unavailable" line</fails_when>
|
||||
<automated>scripts/check-admin-openapi.sh --check</automated>
|
||||
<fails_when>non-zero exit</fails_when>
|
||||
<automated>npm --prefix admin run test -- PluginsView 2>&1 | tail -6 && npm --prefix admin run typecheck 2>&1 | tail -3</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, or "No test files found"</fails_when>
|
||||
<automated>go vet ./... && go test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'g.Get("/plugins/marketplace", s.pluginsMarketplace)' modules/cabana/http.go` prints one line
|
||||
- `grep -n "kiosk.NewestCompatible\|kiosk.BaseVersion" modules/cabana/plugins.go` prints at least two lines
|
||||
- `grep -n "navigator.clipboard" admin/src/views/PluginsView.vue` prints at least one line
|
||||
- `grep -rn "summercms.io" admin/src` prints nothing (the SPA never calls the registry; D-17)
|
||||
- every behavior subtest named in the action passes
|
||||
</acceptance_criteria>
|
||||
<done>Administrators see which plugins can be updated and search the marketplace from the admin, and nothing breaks when the registry is off or down.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Rebuild and commit the embedded admin, and pass the admin gates</name>
|
||||
<files>modules/boardwalk/dist</files>
|
||||
<read_first>
|
||||
- .planning/notes/2026-10-07-embedded-admin-release-gate.md
|
||||
- scripts/check-admin-dist.sh
|
||||
- modules/boardwalk/README.md
|
||||
</read_first>
|
||||
<action>
|
||||
Run `npm --prefix admin ci` only if node_modules is missing, then `npm --prefix admin run build`, which writes modules/boardwalk/dist. Stage the whole modules/boardwalk/dist directory by path (it contains only build output) and commit it alone with the subject from Execution units. Run the browser tests for the new view if the admin has a browser suite covering views (`npm --prefix admin run test:browser`); a failing unrelated browser test is recorded, not fixed here.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>scripts/check-admin-dist.sh 2>&1 | tail -3</automated>
|
||||
<fails_when>non-zero exit, or output reporting that the committed dist differs from a fresh build</fails_when>
|
||||
<automated>go test ./modules/boardwalk -count=1 2>&1 | tail -3 && npm --prefix admin run test 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git show --stat <the Task 3 commit sha>` lists only paths under modules/boardwalk/dist
|
||||
- scripts/check-admin-dist.sh exits 0 on a clean tree
|
||||
</acceptance_criteria>
|
||||
<done>The embedded admin in the binary carries the Plugins screen and the release gate for admin changes passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser (admin SPA) → cabana /plugins endpoints | authenticated administrators |
|
||||
| cabana → registry | outbound call to an operator-configured URL |
|
||||
| registry JSON → admin UI | third-party text shown to administrators |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-29 | Elevation of privilege | GET /plugins, GET /plugins/marketplace | medium | mitigate | backend guard plus cabana.Allows backend.view_plugins (403 otherwise); permission matrix and security coverage inventories include both routes |
|
||||
| T-34-30 | Information disclosure | installed plugin list | low | mitigate | only administrators with the permission see versions and modules; no filesystem paths in party.Info (34-01) |
|
||||
| T-34-31 | Denial of service | registry slow or down | medium | mitigate | 4 s timeout, TTL cache including failure caching, at most 4 concurrent lookups, degraded 200 responses (TestPluginsScreenSmoke/unavailable) |
|
||||
| T-34-32 | Tampering (XSS) | registry names and descriptions in the SPA | medium | mitigate | rendered as text through Vue interpolation, no v-html; README HTML is not shown in the admin |
|
||||
| T-34-33 | Server-side request forgery | plugins.registry_url | low | accept | operator-configured trusted setting (fetchguard TrustedMode by design), no redirects followed, size-capped; administrators cannot change it from the admin |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | no package is added (reka-ui and the build toolchain are already dependencies) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go vet ./... && go test -short ./...`; `go test ./modules/cabana -run 'TestPluginsScreenSmoke|TestPhase09'`.
|
||||
- `scripts/check-admin-openapi.sh --check`, `npm --prefix admin run test`, `npm --prefix admin run typecheck`, `scripts/check-admin-dist.sh`.
|
||||
- `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check`.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The Plugins screen shows installed plugins with versions and updates and searches the marketplace with copyable install commands.
|
||||
- Registry disabled or down never breaks the Installed tab.
|
||||
- OpenAPI, SPA types and the embedded dist are regenerated and committed; no push or tag.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-05-SUMMARY.md` when done.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Constants: `cabana.PermissionViewPlugins` (`backend.view_plugins`).
|
||||
- Types: `cabana.PluginsOverview`, `cabana.RegistryState`, `cabana.InstalledPlugin`, `cabana.MarketplacePlugin`.
|
||||
- Struct fields: `cabana.SettingsEntry.Path` (JSON `path`).
|
||||
- Endpoints (admin API prefix): `GET /plugins`, `GET /plugins/marketplace?q=&category=`; swag operations `AdminPluginsInstalled`, `AdminPluginsMarketplace`.
|
||||
- Handlers: `pluginsInstalled`, `pluginsMarketplace`.
|
||||
- SPA: route `/system/plugins` (name `plugins`), `admin/src/views/PluginsView.vue`, `settingsPath` honours `entry.path`.
|
||||
- Lang keys: `backend::lang.permissions.view_plugins` plus the Plugins screen strings (en, pl).
|
||||
- Tests: `TestPluginsScreenSmoke` (installed, updates, describe_base, unlisted, unavailable, disabled, marketplace, cache), `admin/tests/views/PluginsView.test.ts`.
|
||||
- Generated: admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/boardwalk/dist.
|
||||
- Config keys: reads `plugins.registry_url` (added in 34-04).
|
||||
329
.planning/phases/34-plugin-marketplace/34-06-PLAN.md
Normal file
329
.planning/phases/34-plugin-marketplace/34-06-PLAN.md
Normal file
@@ -0,0 +1,329 @@
|
||||
---
|
||||
phase: 34-plugin-marketplace
|
||||
plan: 06
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on: ["34-01", "34-02", "34-03", "34-04", "34-05"]
|
||||
files_modified:
|
||||
- scripts/check-phase34.sh
|
||||
- modules/kiosk/manifest_test.go
|
||||
- modules/kiosk/identity_test.go
|
||||
- modules/kiosk/derive_test.go
|
||||
- modules/kiosk/version_test.go
|
||||
- modules/kiosk/client_test.go
|
||||
- modules/kiosk/example_test.go
|
||||
- modules/party/info_test.go
|
||||
- internal/build/plugindirs_test.go
|
||||
- internal/build/install_test.go
|
||||
- internal/build/manifest_append_test.go
|
||||
- cmd/summer/plugin_commands_test.go
|
||||
- modules/cabana/plugins_test.go
|
||||
- admin/tests/views/PluginsView.test.ts
|
||||
- admin/tests/state/useSettings.test.ts
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/query_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry_api_test.go
|
||||
- ../sm-summercmsio-app/scripts/test_changelog_post.py
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/tests/marketplace.test.ts
|
||||
- .planning/phases/34-plugin-marketplace/34-release-log.md
|
||||
- .planning/phases/34-plugin-marketplace/34-VALIDATION.md
|
||||
autonomous: true
|
||||
requirements: [SC-5, SC-1, SC-2, SC-3, SC-4, D-02, D-03, D-04, D-05, D-07, D-08, D-09, D-10, D-12, D-13, D-14, D-15, D-16, D-17, D-18, D-19]
|
||||
|
||||
estimate:
|
||||
tokens: 180000
|
||||
raw_tokens: 180000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "scripts/check-phase34.sh runs every Phase 34 check across the framework, the plugin repositories and the summercms.io repositories by stage, fails closed on a missing repository or a missing required test, and its self-test detects every planted failure (SC-5, Wave 0 of 34-VALIDATION.md)"
|
||||
- "The new framework code has unit tests: plugin.yaml derivation for literal, same-package, cross-package and other-module constants and their error messages, README extraction and overrides, check/stale detection, validation, version rules including git describe bases and pseudo-version minimums, the registry client, party.Describe, plugin directory and version resolution, effective framework version, install resolution, prompts and offline rules, summer.yaml comment preservation, and the cabana Plugins endpoints (SC-5; D-02..D-05, D-07..D-10, D-12, D-17..D-19)"
|
||||
- "The summercms.io code has unit tests: ingest rules (tags, nested manifests, module and ID identity, digest skip, caps), API shapes, query validation and caching headers, the ingest endpoint's auth, the release script's new helpers and refusals, and the marketplace page utilities (SC-5; D-13..D-16)"
|
||||
- "Unit tests never write into a sibling repository or the developer's module cache: fixtures live in t.TempDir() and module downloads use a temporary GOMODCACHE"
|
||||
- "34-release-log.md lists, in release order, what v0.6.0 of the framework carries, every plugin, site plugin, vue app and app commit waiting for push or tag, the pending submodule pointer bumps, the app regenerations of main.go and plugins.gen.go, the summercms.io deploy steps and the changelog posts; nothing is pushed, tagged or deployed by this plan (memory: tag only at phase completion, on the default branch)"
|
||||
- "34-VALIDATION.md's per-task verification map is filled for every task of plans 34-01..34-06 and its Wave 0 items are checked"
|
||||
artifacts:
|
||||
- path: "scripts/check-phase34.sh"
|
||||
provides: "phase gate (tracer)"
|
||||
contains: "--self-test"
|
||||
- path: "modules/kiosk/identity_test.go"
|
||||
provides: "ID reader unit tests"
|
||||
contains: "func TestReadIdentity"
|
||||
- path: "internal/build/install_test.go"
|
||||
provides: "resolution unit tests"
|
||||
contains: "func TestResolveInstall"
|
||||
- path: "modules/cabana/plugins_test.go"
|
||||
provides: "Plugins endpoint unit tests"
|
||||
contains: "func TestPluginsEndpoints"
|
||||
- path: ".planning/phases/34-plugin-marketplace/34-release-log.md"
|
||||
provides: "release ledger"
|
||||
contains: "v0.6.0"
|
||||
key_links:
|
||||
- from: "scripts/check-phase34.sh"
|
||||
to: "every Phase 34 test"
|
||||
via: "stages run the tests by required name and fail when a name is missing from the go test -json output"
|
||||
pattern: "TestManifestSmoke|TestInstallSmoke|TestPluginsScreenSmoke|TestRegistrySmoke"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
People browse the marketplace, install plugins with one command and see them in the admin. This last plan proves the phase with unit tests across every repository it touched (project rule: unit tests are the last plan) and writes down the user-gated release.
|
||||
|
||||
<objective>
|
||||
- `scripts/check-phase34.sh` (Wave 0): the phase gate over all repositories, by stage.
|
||||
- Unit tests for kiosk, party, internal/build, cmd/summer, cabana, the admin view, the summercms.io site plugin, the release script and the marketplace utilities (SC-5).
|
||||
- `34-release-log.md`: the release order and every pending push, tag, pointer bump, regeneration, deploy step and changelog post for Jakub.
|
||||
- `34-VALIDATION.md`: per-task map and Wave 0 filled.
|
||||
|
||||
Output: test commits in each repository (framework, site plugin, app, vue app), the gate script commit, and one planning-docs commit. Nothing pushed, tagged or deployed.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/34-plugin-marketplace/34-RESEARCH.md
|
||||
@.planning/phases/34-plugin-marketplace/34-VALIDATION.md
|
||||
@.planning/phases/34-plugin-marketplace/34-01-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-02-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-03-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-04-SUMMARY.md
|
||||
@.planning/phases/34-plugin-marketplace/34-05-SUMMARY.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
Smoke tests already in place (plans 01-05): TestManifestSmoke and TestPluginInfoSmoke and TestAddPluginConstID (framework), TestPluginManifest (every plugin repository), TestRegistrySmoke (site plugin, Docker), marketplace SSR smoke (`pnpm test:marketplace`), TestInstallSmoke (framework, non-short subtests apply installs), TestPluginsScreenSmoke and admin/tests/views/PluginsView.test.ts.
|
||||
Gate template: scripts/check-phase32.sh (stage engine: usage, detect_go, go_named required-name checks over go test -json, run_* stages, run_self_test planting, marker and additive stages via scripts/apisurface against a tag, missing-repository failure).
|
||||
Framework tag base: v0.5.3 (newest tag before this phase). Consuming application names the framework must not contain: grzyby, fonoteka, plytarium, summercmsio, sm-demo-app, sm-bm-app (summercms.io, the framework's own site and default registry, is allowed).
|
||||
Docker-backed tests skip under -short; run them through `sudo -u golem` if the socket is denied.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Each task commits per repository, staging explicit paths; test-only commits use `test:` subjects. Check `git status --short` in each repository first and leave other sessions' edits alone. No co-author tag.
|
||||
- Resume check: `git -C <repo> log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commit subjects (repository) | Resume point |
|
||||
|------|------------------------------|--------------|
|
||||
| 1 | `test: phase 34 gate script` (framework) | `--self-test` passes |
|
||||
| 2 | `test(kiosk): manifest, identity, version and client unit tests`; `test(build): plugin dirs, install resolution and summer.yaml edits`; `test(summer): plugin command prompts` (framework) | --go stage passes |
|
||||
| 3 | `test(cabana): Plugins screen endpoints`; `test(admin): PluginsView and settings paths` (framework) | --admin stage passes |
|
||||
| 4 | `test: registry ingest, queries and API` (site); `test: changelog-post manifest and ingest helpers` (app); `test: marketplace utilities` (vue) | --site stage passes |
|
||||
| 5 | `docs(34): release ledger and validation map` (framework, planning docs) | ledger committed |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "one command runs every Phase 34 check across all repositories" — the gate script over the existing smoke tests</name>
|
||||
<files>scripts/check-phase34.sh</files>
|
||||
<read_first>
|
||||
- scripts/check-phase32.sh (whole file: stage engine, go_named, self-test planting, marker, additive, missing-repository failure)
|
||||
- scripts/apisurface/main.go
|
||||
- .planning/phases/34-plugin-marketplace/34-VALIDATION.md and the five plan SUMMARYs (test names and commands that worked)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `scripts/check-phase34.sh` from check-phase32.sh's engine, labelled Phase 34, with stages (each a flag; `--all` runs them in this order; no argument prints usage listing every flag and exits 2):
|
||||
- `--self-test`: plants a failing go test name, a missing required test name, a marker hit in a scratch repository, an additive removal and a missing repository, and requires each to be detected.
|
||||
- `--go`: `go vet ./...`, `go test -short ./...`, plus required names TestManifestSmoke, TestPluginInfoSmoke, TestAddPluginConstID, TestPluginsScreenSmoke and the unit tests Task 2 and 3 add (the list is a variable GO_TESTS extended by later tasks); `cd examples/hello && go test ./...`.
|
||||
- `--docker`: Docker host only (NOT RUN otherwise, which fails under --all unless PHASE34_ALLOW_NO_DOCKER=1): `go test ./...` in the framework including TestInstallSmoke's non-short subtests, and the site plugin's TestRegistrySmoke.
|
||||
- `--plugins`: for each of the 20 sibling sm-*-plugin repositories: `go vet ./...`, `go test -short ./...`, TestPluginManifest by required name, and `summer plugin:manifest --check` (built from this checkout into a temp dir) for each plugin.yaml directory; sm-chatbots-plugin may be listed in PHASE34_PENDING_MANIFESTS when 34-02 recorded the F8 skip.
|
||||
- `--site`: in ../sm-summercmsio-app: site plugin `go vet` and `go test -short`, app `go vet ./... && go test ./...`, `python3 -B scripts/test_changelog_post.py`, `scripts/check-deploy.sh` when nginx is available, and in vue-summercmsio-app `pnpm test`, `pnpm build:changelog`, `pnpm test:changelog`, `pnpm test:marketplace`.
|
||||
- `--admin`: `npm --prefix admin run test`, `npm --prefix admin run typecheck`, `scripts/check-admin-openapi.sh --check`, `scripts/check-admin-dist.sh`.
|
||||
- `--docs`: TestDocsTree, TestCommandsDocumented and `go run ./cmd/summer docs:build --check`.
|
||||
- `--marker`: the lines this phase added since v0.5.3 to non-test Go files, READMEs and docs under modules/, cmd/, internal/ and docs/ name no consuming application (word list in interfaces).
|
||||
- `--additive`: apisurface of every framework package changed since v0.5.3 shows no removed or changed exported element (kiosk is new; party, cabana and fetchguard-users only gain elements).
|
||||
A stage whose repository or directory is missing fails with `missing <path>`. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash scripts/check-phase34.sh --self-test</automated>
|
||||
<fails_when>non-zero exit, or a planted case reported as undetected</fails_when>
|
||||
<automated>bash scripts/check-phase34.sh --go --docs --marker --additive</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL", "missing " or marker hit line</fails_when>
|
||||
<automated>bash scripts/check-phase34.sh --plugins --admin</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `bash scripts/check-phase34.sh; echo $?` prints a usage naming --self-test, --go, --docker, --plugins, --site, --admin, --docs, --marker, --additive, --all and exits 2
|
||||
- `grep -c "missing " scripts/check-phase34.sh` is at least 1
|
||||
- `grep -n "TestPluginManifest" scripts/check-phase34.sh` prints at least one line
|
||||
</acceptance_criteria>
|
||||
<done>The phase has one gate that exercises every repository through the smoke tests plans 01-05 left behind.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Unit tests for kiosk, party, internal/build and the plugin commands</name>
|
||||
<files>modules/kiosk/manifest_test.go, modules/kiosk/identity_test.go, modules/kiosk/derive_test.go, modules/kiosk/version_test.go, modules/kiosk/client_test.go, modules/kiosk/example_test.go, modules/party/info_test.go, internal/build/plugindirs_test.go, internal/build/install_test.go, internal/build/manifest_append_test.go, cmd/summer/plugin_commands_test.go, scripts/check-phase34.sh</files>
|
||||
<read_first>
|
||||
- modules/kiosk/*.go and their smoke tests; modules/party/info.go; internal/build/{plugindirs,install,source,update,manifest,scaffold}.go; cmd/summer/main.go (plugin:* commands)
|
||||
- .planning/phases/34-plugin-marketplace/34-RESEARCH.md (Validation Architecture rows for SC-1, SC-3, D-06/07)
|
||||
- cmd/summer/main_test.go (how commands are run with bonfire.Call or a test root)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestReadIdentity: table of fixtures (literal, same-file const, const in another file, chained const, cross-package same module, named import alias, other module via replace, Requires nil/empty/mixed literals and consts) and refusals (function call, var, concatenation, missing ID method, two ID methods returning different values) with the function named in each error
|
||||
- TestDerive: README H1/summary extraction (inline code, links and emphasis flattened to text, multi-line paragraph collapsed), missing README or H1 errors, H1 == ID needs --name, override set/keep/clear, framework missing or v0.0.0 errors, soft dependency empty minimum, same-module dependency empty minimum, pseudo-version warning
|
||||
- TestManifestFormat: Marshal is byte-stable and ordered, requires sorted, Parse round-trips, unknown field and newer schema refused, Validate table (ID, module, package, versions)
|
||||
- TestCheck: missing, stale (first differing line in the message), fresh; CheckT calls Fatalf exactly once on stale and never on fresh (fake TB)
|
||||
- TestVersions: ValidVersion, CompareVersions, BaseVersion, Satisfies (including pseudo-version minimums), NewestCompatible edge cases (empty list, all incompatible, unsorted input)
|
||||
- TestRegistryClient: List/Plugin/Versions decode, query escaping, 404 → ErrNotFound, 500 with error envelope → error carrying the code, oversized body refused, redirect not followed, disabled URL → ErrRegistryDisabled
|
||||
- TestDescribe (party): Describe publishes Installed; nil app error; a second Describe replaces the first
|
||||
- TestPluginDirs and TestEffectiveFramework: go list JSON fixtures parsed into module/submodule/local sources; describe only for a dir that is its own repository top level; no dirty suffix; framework replaced in go.mod vs go.work vs not replaced; git missing falls back with a warning
|
||||
- TestResolveInstall: newest compatible, pin, pin incompatible names the framework, no compatible version names the minimum framework, transitive closure order, dependency minimum above newest compatible errors, installed-too-old hint, cycle, offline and disabled refusals, invalid registry strings refused
|
||||
- TestAppendManifestPlugins: comments and blank lines preserved, indentation matched, missing plugins block appended, invalid result restores the original bytes
|
||||
- TestPluginCommands (cmd/summer): plugin:install without --yes off a terminal errors naming --yes; --offline ID install errors; plugin:manifest --check exit status and output on missing and stale files; --check with --name refused
|
||||
</behavior>
|
||||
<action>
|
||||
Write the tests listed in the behavior block in the files named (unit level: fixtures in t.TempDir(), fake registries through httptest or fetchguard.WithTransport, go list output as JSON fixtures where running go is not the point; tests that need git or go skip when the binary is missing). Add a runnable `Example` for kiosk.Marshal and kiosk.NewestCompatible in example_test.go with Output comments (docs examples stay runnable). Fix any defect the tests expose in the production code in the same commit as the test that found it, and note it in the SUMMARY. Add the new test names to GO_TESTS in scripts/check-phase34.sh. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test -short ./modules/kiosk ./modules/party ./internal/build ./cmd/summer -count=1 2>&1 | tail -6</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
<automated>go test -short ./modules/kiosk ./internal/build -run 'TestReadIdentity|TestDerive|TestManifestFormat|TestCheck|TestVersions|TestRegistryClient|TestPluginDirs|TestEffectiveFramework|TestResolveInstall|TestAppendManifestPlugins' -count=1 -v 2>&1 | grep -c '^--- PASS'</automated>
|
||||
<fails_when>a count below 10, or non-zero exit</fails_when>
|
||||
<automated>bash scripts/check-phase34.sh --go</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" or "missing" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- every test function named in the behavior block exists (`grep -rn "func TestReadIdentity\|func TestResolveInstall\|func TestAppendManifestPlugins\|func TestPluginCommands" modules internal cmd` prints at least four lines)
|
||||
- `go test -cover ./modules/kiosk` reports coverage of at least 70%
|
||||
- `git -C ../sm-mfa-plugin status --short` shows no change made by the tests
|
||||
</acceptance_criteria>
|
||||
<done>The framework side of the marketplace is covered by fast unit tests that pin every rule the decisions set.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Unit tests for the cabana Plugins endpoints and the admin view</name>
|
||||
<files>modules/cabana/plugins_test.go, admin/tests/views/PluginsView.test.ts, admin/tests/state/useSettings.test.ts, scripts/check-phase34.sh</files>
|
||||
<read_first>
|
||||
- modules/cabana/plugins.go and plugins_smoke_test.go (34-05)
|
||||
- admin/src/views/PluginsView.vue, admin/src/state/useSettings.ts, admin/tests/helpers.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestPluginsEndpoints: unauthenticated 401, no permission 403, permission 200, superuser 200; settings entry present only when allowed and carrying path /system/plugins; update rule table (exact tag, describe base, local, unlisted, newer incompatible only, registry error); cache hit and expiry (injected clock); concurrency bound of 4 (fake registry counting in-flight calls); marketplace query validation and installed/compatible flags; disabled registry makes no call
|
||||
- PluginsView: both tabs render; Marketplace tab hidden when registry is disabled; search debounce sends one request; category select filters; copy writes the exact command and shows the copied state; clipboard missing shows the fallback; registry unavailable notice; update badge with the update command; names fall back to IDs
|
||||
- useSettings.settingsPath prefers entry.path, then controller, then the singleton form
|
||||
</behavior>
|
||||
<action>
|
||||
Write the tests in the behavior block; inject the clock and the registry through the unexported seams 34-05 created (add a minimal seam if missing, without changing exported API). Add the Go test name to GO_TESTS and the vitest files to the --admin stage. Commit as listed in Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test -short ./modules/cabana -run 'TestPluginsEndpoints|TestPluginsScreenSmoke' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok|PASS)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestPluginsEndpoints" line</fails_when>
|
||||
<automated>npm --prefix admin run test -- PluginsView useSettings 2>&1 | tail -6</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, or "No test files found"</fails_when>
|
||||
<automated>bash scripts/check-phase34.sh --admin</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "func TestPluginsEndpoints" modules/cabana/plugins_test.go` prints one line
|
||||
- `grep -c "it(\|test(" admin/tests/views/PluginsView.test.ts` is at least 9
|
||||
- `bash scripts/check-phase34.sh --additive` passes after this task's commit (any seam added for the tests is unexported)
|
||||
</acceptance_criteria>
|
||||
<done>The admin side of the marketplace is covered by Go and vitest unit tests.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: Unit tests for the summercms.io registry, the release script and the marketplace utilities</name>
|
||||
<files>../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry/query_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/registry_api_test.go, ../sm-summercmsio-app/scripts/test_changelog_post.py, ../sm-summercmsio-app/vue-summercmsio-app/tests/marketplace.test.ts, scripts/check-phase34.sh</files>
|
||||
<read_first>
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/registry/*.go, controllers/api/registry.go, registry_smoke_test.go, testmain_test.go (34-03)
|
||||
- ../sm-summercmsio-app/scripts/changelog-post.py and scripts/test_changelog_post.py
|
||||
- ../sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/marketplace.ts and tests/changelog.test.ts (node --test style)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- ingest (unit, no Docker where possible): repository URL normalization and module derivation (https only; credentials, query, fragment refused), strict tag filter, plugin.yaml path filter, tags digest stability, blob caps enforced, module mismatch and ID ownership decisions, skipped-tag notes in last_error, git missing error, allowedProtocols default https
|
||||
- queries and API (Docker where they need Postgres): LIKE escaping of % and _, ordering, category regex, ETag stable for equal bodies and 304 on match, 404 and 400 envelopes, versions newest first, icon_url and screenshots URLs, no admin-only fields in any response
|
||||
- ingest endpoint: 401, 403 (no scope, wrong permission), 404 uncurated, 202 queued once for two rapid calls on the same repository
|
||||
- changelog-post.py: refusal when HEAD is not the tag, when the tree is dirty, when no plugin.yaml is tracked, when the manifest check fails (check runner stubbed); framework releases skip both; ingest call skipped on dry runs and its 401/403/404/network outcomes are warnings
|
||||
- marketplace utilities: pluginPath and idFromRoute round-trip, invalid segments refused, installCommand exact text
|
||||
</behavior>
|
||||
<action>
|
||||
Write the tests in the behavior block (site plugin tests reuse testmain_test.go's container and skip under -short where they need Postgres; Python tests stub subprocess and network calls the way the existing tests stub `call`; the vue test imports the utilities with node --test like the existing tests). Fix defects found in the same repository's commit and record them in the SUMMARY. Wire the new tests into the --site stage by required name. Commit per repository as listed in Execution units; do not commit submodule gitlinks in sm-summercmsio-app.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-summercmsio-app/plugins/golem15/summercms test -count=1 ./... 2>&1 | tail -6</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, or "--- SKIP" for the registry tests on a Docker host</fails_when>
|
||||
<automated>cd ../sm-summercmsio-app && python3 -B scripts/test_changelog_post.py 2>&1 | tail -4 && cd vue-summercmsio-app && pnpm test 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit, a "FAILED" line, or a "not ok" line</fails_when>
|
||||
<automated>bash scripts/check-phase34.sh --site</automated>
|
||||
<fails_when>non-zero exit, or a "FAIL" or "missing" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "func Test" ../sm-summercmsio-app/plugins/golem15/summercms/registry/ingest_test.go` prints at least three lines
|
||||
- `grep -c "def test_" ../sm-summercmsio-app/scripts/test_changelog_post.py` grew by at least six compared with 34-03's version
|
||||
- `grep -n "installCommand" ../sm-summercmsio-app/vue-summercmsio-app/tests/marketplace.test.ts` prints at least one line
|
||||
</acceptance_criteria>
|
||||
<done>The registry, its API, the release hooks and the marketplace utilities are unit-tested in their own repositories.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Release ledger for Jakub and the filled validation map</name>
|
||||
<files>.planning/phases/34-plugin-marketplace/34-release-log.md, .planning/phases/34-plugin-marketplace/34-VALIDATION.md</files>
|
||||
<read_first>
|
||||
- the five plan SUMMARYs (commit SHAs, pending pointer bumps, skips, test names)
|
||||
- .planning/phases/32-seo-foundation/32-06-release-log.md (ledger format)
|
||||
- ../sm-summercmsio-app/DEPLOY.md (deploy and changelog posting) and CLAUDE.md (changelog post rules)
|
||||
- .planning/phases/34-plugin-marketplace/34-VALIDATION.md
|
||||
</read_first>
|
||||
<action>
|
||||
Write `34-release-log.md` with sections: "Planned contents" (framework v0.6.0: the commits of 34-01, 34-04, 34-05, 34-06 by subject and SHA; each plugin repository's new commit and its proposed next tag, a minor or patch bump chosen from its own log since its last tag; sm-summercmsio-plugin and vue-summercmsio-app commits); "Release order" (push framework master, tag v0.6.0 on master, changelog post; for each plugin in dependency order: verify `summer plugin:manifest --check`, tag, push, changelog post through `changelog-post.py publish … --repo`, which now checks the manifest and triggers ingestion; then sm-summercmsio-plugin and vue-summercmsio-app pushes; then each app: framework `go get` to v0.6.0, plugin pointer bumps as separate commits after each plugin push, `summer build` to regenerate main.go and plugins.gen.go committed separately (research F10), release build); "summercms.io deploy" (git on rome, migrate, work_in_serve, nginx and supervisor configs, `check-deploy.sh`, add the fourteen launch repositories, mint the `registry` token, first ingest, curl the API, visual check of /marketplace); "Rules" (never move a tag: proxy and sumdb cache it forever, research Pitfall 7; tags only on the default branch; every tag gets its changelog post the same day); and an empty "Executed steps" table. Fill 34-VALIDATION.md's per-task verification map with one row per task of plans 34-01..34-06 (task ID written as 34-0N-TM, for example 34-03-T2; plan, wave, requirement or decision, threat ref, test type, automated command, file exists, status) and tick the Wave 0 items; leave `nyquist_compliant` and `status` to /msd-validate-phase. Commit both as one planning-docs commit with the subject from Execution units.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "^| 34-0" .planning/phases/34-plugin-marketplace/34-VALIDATION.md</automated>
|
||||
<fails_when>a count below 26 (one row per task of the six plans: 5+5+4+4+3+5), or non-zero exit</fails_when>
|
||||
<automated>grep -n "v0.6.0\|Release order\|Executed steps\|never move" .planning/phases/34-plugin-marketplace/34-release-log.md</automated>
|
||||
<fails_when>non-zero exit, or fewer than four matching lines</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 34-release-log.md names every repository listed in the SUMMARYs with a pending action, and the fourteen launch repositories
|
||||
- 34-VALIDATION.md has no remaining "_filled by the planner/executor" placeholder row
|
||||
- no tag or push was made (`git tag --points-at HEAD` prints nothing)
|
||||
</acceptance_criteria>
|
||||
<done>Jakub has an exact, ordered release list, and the validation map reflects every task's automated check.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| gate script → sibling repositories | runs tests and read-only checks in other repositories |
|
||||
| tests → developer environment | module cache, sibling checkouts, Docker |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-34-34 | Tampering | tests writing into sibling repositories or the module cache | medium | mitigate | fixtures in t.TempDir(), temporary GOMODCACHE for downloads, acceptance check that sibling worktrees stay clean |
|
||||
| T-34-35 | Repudiation | a gate that passes because a test silently did not run | medium | mitigate | required test names checked in go test -json output; Docker stages report NOT RUN and fail under --all unless explicitly allowed; self-test plants each failure |
|
||||
| T-34-36 | Information disclosure | release ledger | low | mitigate | the ledger names repositories, SHAs and steps only; no tokens, hosts' secrets or token file contents |
|
||||
| T-34-SC | Tampering | npm/pip/cargo installs | high | accept | no package is added |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `bash scripts/check-phase34.sh --all` passes on a Docker host (or with PHASE34_ALLOW_NO_DOCKER=1 and the Docker stage run separately through sudo -u golem).
|
||||
- Every repository touched by the phase has its tests green.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Unit tests cover the rules of D-02..D-19 in every repository (SC-5).
|
||||
- The gate script and the release ledger exist; VALIDATION's map is filled.
|
||||
- No push, tag or deploy.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/34-plugin-marketplace/34-06-SUMMARY.md` when done.
|
||||
</output>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Script: `scripts/check-phase34.sh` with `--self-test`, `--go`, `--docker`, `--plugins`, `--site`, `--admin`, `--docs`, `--marker`, `--additive`, `--all`; env `PHASE34_ALLOW_NO_DOCKER`, `PHASE34_PENDING_MANIFESTS`.
|
||||
- Tests: `TestReadIdentity`, `TestDerive`, `TestManifestFormat`, `TestCheck`, `TestVersions`, `TestRegistryClient`, `TestDescribe`, `TestPluginDirs`, `TestEffectiveFramework`, `TestResolveInstall`, `TestAppendManifestPlugins`, `TestPluginCommands`, `TestPluginsEndpoints`, PluginsView and useSettings vitest specs, site plugin registry unit tests, changelog-post unittest cases, `tests/marketplace.test.ts`; runnable examples for `kiosk.Marshal` and `kiosk.NewestCompatible`.
|
||||
- Files: `.planning/phases/34-plugin-marketplace/34-release-log.md`; filled `34-VALIDATION.md` map.
|
||||
- Config keys, CLI commands, exported types: none new.
|
||||
349
.planning/phases/34-plugin-marketplace/34-PATTERNS.md
Normal file
349
.planning/phases/34-plugin-marketplace/34-PATTERNS.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Phase 34: Plugin marketplace - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-29
|
||||
**Files analyzed:** 24 (grouped by plan)
|
||||
**Analogs found:** 22 / 24
|
||||
|
||||
Paths are relative to `summercms.go/` unless prefixed with `../` (sibling repos under the meta repo). All analogs are git-tracked sources (no `.msd/` mirrors).
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Plan | Role | Data Flow | Closest Analog | Match |
|
||||
|---|---|---|---|---|---|
|
||||
| `modules/<beach-name>/manifest.go` (plugin.yaml types, Parse/Validate/Marshal) | 01 | model/utility | transform | `internal/build/manifest.go` (`Manifest`, `LoadManifest`/`SaveManifest`, `ValidatePluginID`) | exact |
|
||||
| `modules/<beach-name>/resolve_ast.go` (const-resolving ID/Requires) | 01 | utility | transform (AST) | `internal/build/scaffold.go:360-392` `pluginIDFromGo` | exact (extend) |
|
||||
| `modules/<beach-name>/derive.go` (go.mod via `go mod edit -json`, README via goldmark) | 01 | utility | file-I/O | `internal/build/scaffold.go` `runGo`/`applyGoWork` (l.632); goldmark use in `modules/breakwater` / `internal/docsite` | role-match |
|
||||
| `modules/<beach-name>/check.go` (`Check`, `CheckT(t, dir)` test helper) | 01 | test helper | file-I/O | `../sm-faq-plugin/commands_documented_test.go` (bonfire.CheckDocs helper used by plugin tests) | role-match |
|
||||
| `modules/<beach-name>/client.go` (registry HTTP client) | 01/04/05 | service | request-response | `modules/fetchguard/client.go:40` `NewClient` | role-match |
|
||||
| `modules/<beach-name>/version.go` (strict vX.Y.Z compare) | 01 | utility | transform | none | no analog |
|
||||
| `modules/<beach-name>/README.md` + root `README.md` row | 01 | docs | - | any `modules/*/README.md` (e.g. `modules/fetchguard/README.md`) | exact |
|
||||
| `internal/build/scaffold.go` (`pluginIDFromGo` -> shared resolver; `AddPlugin` require-version option) | 01/04 | utility | file-I/O | itself (l.144, l.360, l.394-406) | modify |
|
||||
| `internal/build/build.go` `generatePluginsGen`/`generateMain` (PluginInfo, FrameworkVersion) | 01 | codegen | transform | itself l.145-163, l.125 | modify |
|
||||
| `internal/build/plugindirs.go` (go list -json, git describe) | 01 | utility | file-I/O (exec) | `internal/build/new.go:286` `runGit`; `scaffold.go:452` `runGoWorkUse` | role-match |
|
||||
| `modules/party` Info type + Describe/lookup | 01 | provider | request-response (registry in app) | `party.Activate` (called from generated main, build.go:125) | role-match |
|
||||
| `cmd/summer/main.go` `plugin:manifest` command | 01 | controller (CLI) | file-I/O | `cmd/summer/main.go:381-415` `addPluginCommand` | exact |
|
||||
| `../sm-*-plugin/plugin.yaml` + `manifest_test.go` per repo | 02 | config + test | file-I/O | `../sm-faq-plugin/commands_documented_test.go` | role-match |
|
||||
| `../sm-summercmsio-app/plugins/golem15/summercms/updates/*.go` (registry tables) | 03 | migration | CRUD | `../sm-faq-plugin/updates/registry.go`, `202610110001_create_golem15_faq_faqs.go` | exact |
|
||||
| `.../summercms/models/*` + admin controllers YAML | 03 | model/controller | CRUD | `../sm-faq-plugin/models/`, `controllers/faqs/`, `admin.go`, `admin_navigation.go` | exact |
|
||||
| `.../summercms/admin_permissions.go` | 03 | config | - | `../sm-faq-plugin/admin_permissions.go` | exact |
|
||||
| `.../summercms/token_scopes.go` (`registry` scope) | 03 | config | - | `../sm-journal-plugin/token_scopes.go` | exact |
|
||||
| `.../summercms/controllers/api/registry.go` (public GET + ingest POST) | 03 | controller | request-response | `../sm-faq-plugin/routes.go` + `controllers/api`; auth: `../sm-journal-plugin/controllers/api/auth.go:83-106` | exact |
|
||||
| `.../summercms/console/registry_ingest.go` + `schedule.go` | 03 | command/job | batch (scheduled) | `../sm-chatvideo-plugin/schedule.go` | exact |
|
||||
| `.../summercms/plugin.go` (gains capabilities) | 03 | provider | - | itself (routes-only today) + `../sm-faq-plugin/plugin.go` | modify |
|
||||
| `../sm-summercmsio-app/scripts/changelog-post.py` (`--check` + ingest-now) | 03 | script | request-response | itself `cmd_publish` l.262-300 | modify |
|
||||
| `../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/marketplace/*.vue` | 03 | component | request-response (SSR) | `.../changelog/app/pages/changelog/plugins.vue`, `index.vue` | role-match |
|
||||
| `cmd/summer/main.go` `plugin:install`, `plugin:update` + `internal/build/install.go` | 04 | controller (CLI) + service | file-I/O + request-response | `addPluginCommand` + `build.AddPlugin` | role-match |
|
||||
| `modules/cabana/plugins.go`, `http.go` routes, `admin_openapi.go`, `backend_screens.go` permission | 05 | controller | request-response | `modules/cabana/http.go:560` `settingsList`; `admin_openapi.go:469-479`; `backend_screens.go:16-25,80-87` | exact |
|
||||
| `admin/src/views/PluginsView.vue`, `router.ts`, `useSettings.ts` | 05 | component | request-response | `admin/src/app/router.ts:70-74`; `useSettings.ts:30-39`; `ApiTokenCreateDialog.vue:255-268` | role-match |
|
||||
| Unit tests (plan 06) | 06 | test | - | `internal/build/*_test.go`, `modules/cabana/phase09_contract_test.go` | role-match |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### Plan 01 — const-resolving reader (`resolve_ast.go`)
|
||||
|
||||
**Analog:** `internal/build/scaffold.go` lines 360-392 (current, literal-only; to be replaced by a call into the shared resolver):
|
||||
```go
|
||||
func pluginIDFromGo(path string) (string, error) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, path, nil, 0)
|
||||
...
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
fn, ok := n.(*ast.FuncDecl)
|
||||
if !ok || fn.Name == nil || fn.Name.Name != "ID" || fn.Recv == nil || fn.Body == nil {
|
||||
return true
|
||||
}
|
||||
for _, stmt := range fn.Body.List {
|
||||
ret, ok := stmt.(*ast.ReturnStmt)
|
||||
if !ok || len(ret.Results) != 1 { continue }
|
||||
lit, ok := ret.Results[0].(*ast.BasicLit)
|
||||
if !ok || lit.Kind != token.STRING { continue }
|
||||
...
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("build: %s does not return a plugin id", path)
|
||||
}
|
||||
```
|
||||
Extend per RESEARCH Pattern 2: switch on `*ast.BasicLit` / `*ast.Ident` (package-level const in any non-test `.go` file of the dir) / `*ast.SelectorExpr` (same-module dir or `go list -m -json` Dir). Locked: same-package and cross-package constants, never execute code. Error must name the function (`ID()`/`Requires()`). Test fixtures: `sm-mfa-plugin` (same-file const), `sm-paymentgateway-plugin/plugin.go:47` (cross-pkg), `sm-pgstripe-plugin/subscriptions/plugin.go:83-85` (cross-module Requires).
|
||||
|
||||
### Plan 01 — `summer plugin:manifest` command
|
||||
|
||||
**Analog:** `cmd/summer/main.go` lines 381-415:
|
||||
```go
|
||||
func addPluginCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "plugin:add",
|
||||
Description: "Register a local plugin module in the app manifest and workspace",
|
||||
Long: `Adds a plugin that already exists on disk ...`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: "plugin:add plugins/acme/blog", Description: "Register the plugin in plugins/acme/blog"},
|
||||
},
|
||||
Args: []bonfire.Arg{{Name: "dir", Description: "Local plugin directory", Required: true}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
pluginDir, ok := in.Argument("dir")
|
||||
...
|
||||
dir, err := os.Getwd()
|
||||
if err := build.AddPlugin(ctx, dir, pluginDir); err != nil { return err }
|
||||
out.Success("Added " + pluginDir + " to summer.yaml, go.mod and go.work; run summer build")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
Every new command (`plugin:manifest`, `plugin:install`, `plugin:update`) needs Description, Long, Examples and described Args/Flags (`TestCommandsDocumented`), and a mention in `docs/console/scaffolding.md` (docs checker). Thin Run, logic in `internal/build` or the new module.
|
||||
|
||||
### Plan 01 — metadata baking in `generatePluginsGen`
|
||||
|
||||
**Analog:** `internal/build/build.go` lines 145-163:
|
||||
```go
|
||||
func generatePluginsGen(m Manifest) ([]byte, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by summer build. DO NOT EDIT.\n\n")
|
||||
...
|
||||
b.WriteString("// PluginIDs is the ordered plugin list from summer.yaml.\n")
|
||||
b.WriteString("var PluginIDs = []string{\n")
|
||||
for _, p := range m.Plugins {
|
||||
fmt.Fprintf(&b, "\t%s,\n", strconv.Quote(p.ID))
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
return formatSource([]byte(b.String()))
|
||||
}
|
||||
```
|
||||
Add `var PluginInfo = []party.Info{...}` and `const FrameworkVersion` the same way (strconv.Quote every string, then `formatSource`; output written through `writeIfChanged`). Generated main (line 125, `plugins, err := party.Activate(app, PluginIDs)`) gains one `party.Describe(...)` line before it. The signature must take resolved metadata (a new param or struct) so failure of `go list` degrades to ID-only and never fails the build.
|
||||
|
||||
### Plan 01 — exec helpers (`plugindirs.go`, `derive.go`)
|
||||
|
||||
**Analogs:** `internal/build/new.go:286` `func runGit(ctx, dir, args...) ([]byte, error)`; `internal/build/scaffold.go:452` `runGoWorkUse`, `:632` `applyGoWork(cmd *exec.Cmd, dir string)`, and `editAppModule` (l.394-406):
|
||||
```go
|
||||
if err := runGo(ctx, appDir, "mod", "edit", "-require="+modPath+"@v0.0.0"); err != nil { return err }
|
||||
...
|
||||
if err := runGo(ctx, appDir, "mod", "edit", "-replace="+modPath+"="+rel); err != nil { return err }
|
||||
return ensureToolchain(filepath.Join(appDir, "go.mod"))
|
||||
```
|
||||
Use `go list -e -json=...` with `applyGoWork(cmd, appDir)`; `go mod edit -json` for go.mod parsing (do not extend `parseGoMod`). `editAppModule` needs a version parameter for `--source` installs (plan 04) while `plugin:add` keeps `v0.0.0`.
|
||||
|
||||
### Plan 01 — registry client (`client.go`)
|
||||
|
||||
**Analog:** `modules/fetchguard/client.go:40` `func NewClient(policy Policy, cfg *compass.Config) (*Client, error)`; use `Policy{Mode: fetchguard.TrustedMode, MaxBytes: ...}`; tests use its `WithTransport` seam. No redirects, so `plugins.registry_url` must be the final origin.
|
||||
|
||||
### Plan 02 — per-repo `plugin.yaml` + check test
|
||||
|
||||
**Analog:** `../sm-faq-plugin/commands_documented_test.go`:
|
||||
```go
|
||||
package faq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
)
|
||||
|
||||
func TestCommandsDocumented(t *testing.T) {
|
||||
commands := (&Plugin{}).Commands()
|
||||
...
|
||||
for _, problem := range bonfire.CheckDocs(commands) {
|
||||
t.Logf("%s", problem)
|
||||
}
|
||||
}
|
||||
```
|
||||
New `manifest_test.go`: `func TestPluginManifest(t *testing.T) { <module>.CheckT(t, ".") }` (fails, not logs). For `sm-pgstripe-plugin` add one for `subscriptions/` too (plugin.yaml per plugin package dir). Repos with H1 == ID need `--name` (F7). Commit in plugin repo first, pointer bump in apps separately.
|
||||
|
||||
### Plan 03 — summercms.io site plugin
|
||||
|
||||
**Migrations** — `../sm-faq-plugin/updates/registry.go` (Register/All verbatim) and `202610110001_create_golem15_faq_faqs.go`:
|
||||
```go
|
||||
var createFAQs = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610110001_create_golem15_faq_faqs",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
for _, stmt := range []string{
|
||||
`CREATE TABLE golem15_faq_faqs ( id SERIAL PRIMARY KEY, ... created_at TIMESTAMPTZ NULL, updated_at TIMESTAMPTZ NULL )`,
|
||||
`CREATE INDEX ... ON golem15_faq_faqs (slug, locale_id)`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil { return err }
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error { return tx.Exec(`DROP TABLE IF EXISTS golem15_faq_faqs`).Error },
|
||||
},
|
||||
}
|
||||
func init() { Register(createFAQs...) }
|
||||
```
|
||||
Tables `golem15_summercms_registry_plugins`, `golem15_summercms_registry_versions`, IDs `2026103000NN_create_golem15_summercms_registry_*`.
|
||||
|
||||
**Permissions** — `../sm-faq-plugin/admin_permissions.go`:
|
||||
```go
|
||||
func (*Plugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{
|
||||
Code: controllers.PermissionFAQs,
|
||||
Tab: "golem15.faq::lang.plugin.name",
|
||||
Label: "golem15.faq::lang.permissions.faqs",
|
||||
Roles: []string{"developer", "publisher"},
|
||||
}}
|
||||
}
|
||||
var _ pact.HasPermissions = (*Plugin)(nil)
|
||||
```
|
||||
Codes `golem15.summercms.registry` (admin) and `golem15.summercms.registry_ingest`.
|
||||
|
||||
**Token scope** — `../sm-journal-plugin/token_scopes.go`:
|
||||
```go
|
||||
var _ pact.HasTokenScopes = (*Plugin)(nil)
|
||||
func (p *Plugin) TokenScopes() []pact.TokenScope {
|
||||
return []pact.TokenScope{{
|
||||
Name: "journal", Label: "golem15.journal::lang.scopes.journal",
|
||||
Description: "golem15.journal::lang.scopes.journal_description",
|
||||
Permissions: []string{"golem15.journal.*"}, Methods: all,
|
||||
}, ...}
|
||||
}
|
||||
```
|
||||
New scope `Name: "registry"`, `Permissions: []string{"golem15.summercms.*"}`, `Methods: []string{"POST"}` (locked decision).
|
||||
|
||||
**Ingest auth** — `../sm-journal-plugin/controllers/api/auth.go` lines 83-106:
|
||||
```go
|
||||
func requireBackendPrincipal(app *backpack.App, w http.ResponseWriter, r *http.Request) (*http.Request, *bouncer.Principal, bool) {
|
||||
out, pr, err := cabana.ResolveBackend(app, r, journalCover)
|
||||
if err != nil || pr == nil { resolveError(w, err); return r, nil, false }
|
||||
return out, pr, true
|
||||
}
|
||||
func requireAccessPosts(w http.ResponseWriter, pr *bouncer.Principal) bool {
|
||||
if !cabana.Allows(pr, []string{permAccessPosts}) {
|
||||
writeAPIError(w, http.StatusForbidden, errInsufficientPerms); return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
**Public routes** — `../sm-faq-plugin/routes.go`:
|
||||
```go
|
||||
const BucketPublicAPI = "faq-public-api"
|
||||
func (p *Plugin) Routes(r pact.Router) error {
|
||||
r.Group("/_faq/api/v1", surf.Use("throttle:"+BucketPublicAPI), func(g pact.Router) {
|
||||
g.Get("/faqs/{slug}", apicontrollers.Show(p.app))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
```
|
||||
Use `r.Group("/api/registry/v1", surf.Use("throttle:registry-api", "cors:registry"), ...)`, plus `Cache-Control`; ingest POST in a separate group without public CORS. Add `http.cors_profiles.registry` in `../sm-summercmsio-app/config/http.yaml`.
|
||||
|
||||
**Schedule** — `../sm-chatvideo-plugin/schedule.go`:
|
||||
```go
|
||||
func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
||||
return []pact.ScheduledCommand{
|
||||
{Command: console.ReconcileSessionsName, Cadence: pact.Every(5 * time.Minute)},
|
||||
}
|
||||
}
|
||||
```
|
||||
-> `{Command: console.RegistryIngestName, Cadence: pact.Every(time.Hour)}`; set `queue.work_in_serve: true` in `config/queue.yaml` (F10).
|
||||
|
||||
**Plugin shell** — `../sm-summercmsio-app/plugins/golem15/summercms/plugin.go` currently declares only `pact.HasRoutes`; `Register`/`Boot` return nil and the package doc says "declares no admin controllers, so ... no /backend route exists" — update doc comment, add `var _ pact.HasMigrations/HasModels/HasAdminControllers/HasPermissions/HasTokenScopes/HasSchedule/HasCommands = (*Plugin)(nil)` like faq. Add `/marketplace` to `nodePaths` in `links_test.go:45`.
|
||||
|
||||
**changelog-post.py** — `cmd_publish` l.262-300 already validates tag on origin, then GETs/POSTs via `call(method, path, token)`. Insert (a) before posting, when `kind == "plugins"`: run `summer plugin:manifest --check` in `--repo`; (b) after a successful post: `call("POST", <ingest-now URL>, token)` — note `call` targets the Journal API base, so add a sibling helper for `/api/registry/v1/ingest`. Extend `test_changelog_post.py`.
|
||||
|
||||
**Marketplace pages** — `../sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/changelog/plugins.vue`:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ key: (route) => route.fullPath })
|
||||
</script>
|
||||
<template>
|
||||
<ChangelogIndex stream="plugins" />
|
||||
</template>
|
||||
```
|
||||
Same Node SSR app; new `pages/marketplace/index.vue` and `[id].vue` with a component like `ChangelogIndex`; runtimeConfig registry base mirrors `NUXT_JOURNAL_API_BASE` (supervisor conf); nginx `/marketplace` -> :3095 as for `/changelog`.
|
||||
|
||||
### Plan 04 — `plugin:install` / `plugin:update`
|
||||
|
||||
**Analog:** `addPluginCommand` (above) + `build.AddPlugin` (`internal/build/scaffold.go:144`). Prompt: do NOT use `bonfire.ConfirmProceed` (`modules/bonfire/helpers.go:144-149` returns true off a terminal). Use:
|
||||
```go
|
||||
if !yes {
|
||||
if !bonfire.Interactive(out) { return fmt.Errorf("plugin:install: non-interactive run needs --yes") }
|
||||
ok, err := out.Confirm(question, false)
|
||||
...
|
||||
}
|
||||
```
|
||||
Validate registry-provided module paths with `validateModulePath` (`internal/build/manifest.go:270-292`) and versions with `^v\d+\.\d+\.\d+$` before `go get`. go.work overrides via `go work edit -replace=` (F2), invoked like `runGoWorkUse`.
|
||||
|
||||
### Plan 05 — cabana Plugins endpoints
|
||||
|
||||
**Handler analog:** `modules/cabana/http.go:560-568`:
|
||||
```go
|
||||
func (s *service) settingsList(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := bouncer.User(r.Context())
|
||||
if !ok || principal == nil || !principal.Backend {
|
||||
WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated)
|
||||
return
|
||||
}
|
||||
...
|
||||
WriteData(w, http.StatusOK, settings, map[string]any{"locale": ...})
|
||||
}
|
||||
```
|
||||
Add permission check for the new key (403 via `WriteError`). Routes go into the `r.GroupRaw(api, []string{"backend", "locale.from-principal"}, ...)` block at `http.go:316-339`: `g.Get("/plugins", s.pluginsInstalled)`, `g.Get("/plugins/marketplace", s.pluginsMarketplace)`.
|
||||
|
||||
**Permission analog:** `modules/cabana/backend_screens.go:16-25, 80-87`:
|
||||
```go
|
||||
PermissionManageUsers = "backend.manage_users"
|
||||
...
|
||||
func (*backendScreens) Permissions() []pact.Permission {
|
||||
return []pact.Permission{
|
||||
{Code: PermissionManageUsers, Tab: "backend::lang.permissions.tab_system", Label: "backend::lang.permissions.manage_users"},
|
||||
...
|
||||
```
|
||||
Add `PermissionViewPlugins = "backend.view_plugins"` (no Roles; superusers pass all checks). Strings in `modules/phrasebook/backend/lang/{en,pl}/lang.yaml`.
|
||||
|
||||
**swag analog:** `modules/cabana/admin_openapi.go:469-479`:
|
||||
```go
|
||||
// AdminSettingsList documents GET /settings.
|
||||
//
|
||||
// @Summary List admin settings
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Success 200 {object} Envelope[[]SettingsEntry]
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Router /settings [get]
|
||||
```
|
||||
Then `scripts/check-admin-openapi.sh`; update `phase09_contract_test.go` / `security_coverage_test.go` inventories. Check for in-flight cabana edits from other sessions first.
|
||||
|
||||
### Plan 05 — boardwalk view
|
||||
|
||||
- Router (`admin/src/app/router.ts:70-74`): `{ path: '/settings', name: 'settings', component: SettingsIndexView, meta: { shell: true } }` — add `{ path: '/system/plugins', name: 'plugins', component: PluginsView, meta: { shell: true } }` before the `/:vendor/:plugin/:controller` routes.
|
||||
- Settings link (`admin/src/state/useSettings.ts:30-39`): `settingsPath(entry)` returns `controllerPath(entry.controller)` or `/settings/<code>`; add a leading `if (entry.path) return entry.path` with an additive `Path` field on `cabana.SettingsEntry` (`modules/cabana/navigation.go:23-35`).
|
||||
- Copy button (`admin/src/components/account/ApiTokenCreateDialog.vue:255-268`):
|
||||
```ts
|
||||
try {
|
||||
if (!navigator.clipboard?.writeText) { throw new Error('clipboard unavailable') }
|
||||
await navigator.clipboard.writeText(value)
|
||||
markCopied()
|
||||
} catch {
|
||||
copyFailed.value = true
|
||||
...
|
||||
}
|
||||
```
|
||||
- Tabs via `reka-ui`; data via typed `api` client only. Rebuild and commit `modules/boardwalk/dist`, run `scripts/check-admin-dist.sh`.
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### CLI command shape
|
||||
**Source:** `cmd/summer/main.go:381-415`. **Apply to:** plugin:manifest, plugin:install, plugin:update. Full docs fields; Run delegates to a package function; `out.Success(...)` at the end.
|
||||
|
||||
### Shelling out to go/git
|
||||
**Source:** `internal/build/new.go:286` `runGit`, `scaffold.go` `runGo` + `applyGoWork` (l.632). **Apply to:** plugindirs, derive, install, ingester (ingester adds hardened env: `GIT_TERMINAL_PROMPT=0`, `GIT_ALLOW_PROTOCOL=https`, args after `--`).
|
||||
|
||||
### YAML
|
||||
`goccy/go-yaml` only (as `internal/build/manifest.go`); struct field order = output order.
|
||||
|
||||
### Plugin capability assertions
|
||||
`var _ pact.HasX = (*Plugin)(nil)` next to each capability method (faq, journal files above).
|
||||
|
||||
### Error envelopes
|
||||
cabana: `WriteError(w, status, code, msg)` / `WriteData(w, status, data, meta)`. Site plugin public API: `{"data":..., "meta":...}` / `{"error":{...}}` per RESEARCH registry shape.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|---|---|---|---|
|
||||
| `modules/<beach-name>/version.go` | utility | transform | No semver comparer in tree; write a strict `vX.Y.Z` comparer (~20 lines), strip `-N-gHASH` from git describe |
|
||||
| ingester core (`classes/ingest/*.go` on site plugin) | service | batch (git clone/ls-tree) | No git-cloning service exists; follow RESEARCH "Hardened git for the ingester" and use `breakwater.Render` for README HTML |
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `summercms.go/{cmd,internal/build,modules/{cabana,bonfire,fetchguard,party},admin/src}`, `../sm-faq-plugin`, `../sm-journal-plugin`, `../sm-chatvideo-plugin`, `../sm-summercmsio-app/{plugins/golem15/summercms,scripts,vue-summercmsio-app/changelog}`
|
||||
**Files scanned:** ~35
|
||||
**Pattern extraction date:** 2026-10-29
|
||||
@@ -567,18 +567,24 @@ Plans 4 and 5 both depend only on plan 1 and the API shape (fixtures), not on pl
|
||||
| A9 | Which repos are "public plugins" to list (quizzes, presentations, golem, feedback exist publicly but have no sibling checkout; six chat-family repos are untagged) | F9 | Wrong listing scope |
|
||||
| A10 | Proposed new module name (e.g. `kiosk`) | Structure | Naming only |
|
||||
|
||||
## Open Questions
|
||||
## Open Questions (RESOLVED)
|
||||
|
||||
All six were answered by the user during /msd-plan-phase 34 on 2026-10-29.
|
||||
|
||||
1. **Constant IDs vs D-02's "literal returns" (A1).**
|
||||
- What we know: 13/22 plugins return `PluginID`; two use cross-package constants; `plugin:add` already fails on them.
|
||||
- Recommendation: approve the static const resolver; it keeps D-02's "no code execution" intent.
|
||||
- **RESOLVED:** follow constants (same-package and cross-package); `plugin:add` uses the same resolver. No `plugin.go` edits.
|
||||
2. **How the plugin repos' `--check` runs in `go test` without inflating framework minimums.**
|
||||
- Options: (a) import a framework helper. Each plugin's go.mod then requires the release containing it, which becomes its `framework:` minimum (likely the phase's release tag), and plugin tags wait for that framework tag. (b) The test shells out to `summer` from PATH or `go run git.golem15.com/golem15/summercms/cmd/summer@<pinned>` (the framework go.mod has no replace, so `go run pkg@version` works) and skips under `-short` or when unavailable. No go.mod change, but weaker and network-bound.
|
||||
- Recommendation: (a) for simplicity, provided the user accepts that every listed plugin then requires the new framework release. Otherwise (b) plus the mandatory check in `changelog-post.py`.
|
||||
- **RESOLVED:** (a) framework test helper; plugins require the phase's framework release and their tags wait for it.
|
||||
3. **Which repos are listed at launch (A9)**, and whether untagged repos get a first tag in this phase (tags are user-gated).
|
||||
- **RESOLVED:** only tagged public plugins checked out next to the framework; no new tags in this phase.
|
||||
4. **Ingest token scope (A7)**: new `registry` scope (user mints a token), or accept the existing `journal`-scoped token for the ingest endpoint (lower friction, scope semantics blur).
|
||||
- **RESOLVED:** new `registry` scope; the user mints the token, ingest-now is best-effort until then.
|
||||
5. **`summer.yaml` comment preservation**: implement now (recommended) or document the loss.
|
||||
- **RESOLVED:** preserve comments.
|
||||
6. **plugin:install in a non-git app directory** (`summer new --no-git`) with `--source`: clone without a submodule, or refuse.
|
||||
- **RESOLVED:** refuse with a clear message.
|
||||
|
||||
## Environment Availability
|
||||
|
||||
|
||||
18
.planning/phases/34-plugin-marketplace/COVERAGE.md
Normal file
18
.planning/phases/34-plugin-marketplace/COVERAGE.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# API Coverage — SummerCMS plugin registry API v1
|
||||
|
||||
> Full coverage by default. Opt-outs are explicit, reasoned decisions. Phase 34 builds the registry API itself (server in 34-03, contract types in 34-01) and consumes it from the CLI (34-04), cabana (34-05), the /marketplace pages (34-03) and the release script (34-03). It also reaches two outside services through their own tools: the Go module proxy through the go command and git hosting through git.
|
||||
|
||||
| capability | decision | reason |
|
||||
|---|---|---|
|
||||
| GET /api/registry/v1/plugins with q and category (list and search) | INTEGRATE | |
|
||||
| GET /api/registry/v1/plugins/{id} (detail: README, screenshots, requires) | INTEGRATE | |
|
||||
| GET /api/registry/v1/plugins/{id}/versions (tags with their minimums) | INTEGRATE | |
|
||||
| POST /api/registry/v1/ingest (authenticated ingest-now, token scope registry) | INTEGRATE | |
|
||||
| error envelope (404 not_found, 400 invalid_query) decoded by clients | INTEGRATE | |
|
||||
| Cache-Control and ETag with 304 on If-None-Match (server side) | INTEGRATE | |
|
||||
| conditional requests (If-None-Match) sent by kiosk.Client and cabana | OPT-OUT | not needed yet: cabana keeps an in-process TTL cache and the CLI makes one-shot calls; adding revalidation later changes no contract |
|
||||
| pagination of the plugin list (page, per_page) | OPT-OUT | not needed yet: the launch listing is fourteen plugins; adding pagination later is an additive change within v1 |
|
||||
| Go module proxy (proxy.golang.org) through go get and go list -m | INTEGRATE | |
|
||||
| git hosting: anonymous HTTPS (ingester), user's https or ssh (CLI) | INTEGRATE | |
|
||||
| forge webhooks (push or tag events) | OPT-OUT | explicitly out of scope: D-13 chose a pull ingester plus ingest-now, no forge webhooks |
|
||||
| forge HTTP APIs (raw files, tag listings) | OPT-OUT | explicitly out of scope: D-13 locks anonymous clones, which keeps ingestion independent of the forge |
|
||||
Reference in New Issue
Block a user