refactor: turn the repo into a two-plugin marketplace
tea and tdl were two repositories, each carrying its own
.claude-plugin/marketplace.json — two marketplaces to register for what
is one collection. Fold them into one.
The repo root is now the marketplace and nothing else: a single
.claude-plugin/marketplace.json whose entries point at ./plugins/tea and
./plugins/tdl. A plugin's root is its own directory under plugins/, so
${CLAUDE_PLUGIN_ROOT} still resolves inside it and every path a plugin
uses stays relative to itself — the hooks and the test roots needed no
adjustment beyond the move.
tea's files move with git mv, so its history and blame follow. tdl
arrives as a plain copy; its history stays in claude-skills/threedotslab.
test_payload_root asserted `tmp/` was ignored by REPO/.gitignore. The
rule is that tmp/ is ignored, not which file says so, and git reads every
.gitignore on the way up — so the test now walks up to the repo root the
same way git does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Event Publisher Adapter Scaffold Template
|
||||
|
||||
Generate a Watermill publisher adapter that implements a domain/app-layer interface. The adapter lives in `adapters/` and translates domain operations into published messages. The interface lives in `app/command/services.go`.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase aggregate name (e.g., `Training`)
|
||||
- `{{name}}` — camelCase (e.g., `training`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `training`)
|
||||
- `{{name_lower}}` — all lowercase (e.g., `training`)
|
||||
- `{{module}}` — Go module path from go.mod
|
||||
- `{{event}}` — PascalCase first event name (e.g., `TrainingScheduled`)
|
||||
- `{{topic}}` — Dot-notation topic (e.g., `training.scheduled`)
|
||||
|
||||
## File 1: `app/command/services.go`
|
||||
|
||||
If this file already exists, add the interface. Otherwise create it:
|
||||
|
||||
```go
|
||||
package command
|
||||
|
||||
import "context"
|
||||
|
||||
// {{Name}}EventPublisher defines events that can be emitted for {{name_lower}} operations.
|
||||
// Implemented by adapters (e.g., Watermill AMQP adapter).
|
||||
type {{Name}}EventPublisher interface {
|
||||
{{event}}(ctx context.Context) error
|
||||
// TODO: Add more event methods as needed
|
||||
// Example:
|
||||
// {{Name}}Cancelled(ctx context.Context, uuid string) error
|
||||
}
|
||||
```
|
||||
|
||||
## File 2: `adapters/{{name_snake}}_event_publisher.go`
|
||||
|
||||
```go
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
)
|
||||
|
||||
type Watermill{{Name}}EventPublisher struct {
|
||||
pub message.Publisher
|
||||
}
|
||||
|
||||
func NewWatermill{{Name}}EventPublisher(pub message.Publisher) Watermill{{Name}}EventPublisher {
|
||||
return Watermill{{Name}}EventPublisher{pub: pub}
|
||||
}
|
||||
|
||||
// {{event}}Event is the wire format for the {{topic}} topic.
|
||||
type {{event}}Event struct {
|
||||
// TODO: Add event payload fields
|
||||
// Example:
|
||||
// UUID string `json:"uuid"`
|
||||
// Hour time.Time `json:"hour"`
|
||||
}
|
||||
|
||||
func (p Watermill{{Name}}EventPublisher) {{event}}(ctx context.Context) error {
|
||||
event := {{event}}Event{
|
||||
// TODO: Map domain data to event fields
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := message.NewMessage(watermill.NewUUID(), payload)
|
||||
middleware.SetCorrelationID(watermill.NewUUID(), msg)
|
||||
|
||||
return p.pub.Publish("{{topic}}", msg)
|
||||
}
|
||||
```
|
||||
|
||||
## Update `service/application.go`
|
||||
|
||||
Wire the publisher adapter in the composition root:
|
||||
|
||||
```go
|
||||
func NewApplication(ctx context.Context) (app.Application, func()) {
|
||||
// ... existing clients ...
|
||||
|
||||
publisher, closePub, err := client.NewWatermillPublisher()
|
||||
if err != nil { panic(err) }
|
||||
|
||||
eventPublisher := adapters.NewWatermill{{Name}}EventPublisher(publisher)
|
||||
|
||||
return newApplication(ctx, eventPublisher),
|
||||
func() {
|
||||
// ... existing cleanup ...
|
||||
_ = closePub()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update the private `newApplication` to accept the publisher interface:
|
||||
|
||||
```go
|
||||
func newApplication(
|
||||
ctx context.Context,
|
||||
eventPublisher command.{{Name}}EventPublisher,
|
||||
// ... existing deps ...
|
||||
) app.Application {
|
||||
// ... pass eventPublisher to command handlers that need it
|
||||
}
|
||||
```
|
||||
|
||||
## Update command handler
|
||||
|
||||
Inject the publisher into the command handler that triggers the event:
|
||||
|
||||
```go
|
||||
type {{name}}Handler struct {
|
||||
{{name_lower}}Repo {{name_lower}}.Repository
|
||||
eventPublisher command.{{Name}}EventPublisher
|
||||
}
|
||||
|
||||
func (h {{name}}Handler) Handle(ctx context.Context, cmd {{command}}) error {
|
||||
// ... domain logic ...
|
||||
return h.eventPublisher.{{event}}(ctx)
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user