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,116 @@
|
||||
# Watermill Router Option + Publisher Client Scaffold Template
|
||||
|
||||
Generate the `WithWatermillRouter` server option in `internal/common/server/` and the publisher client factory in `internal/common/client/`. Requires the unified server scaffold (`/threedotslabs scaffold unified_server`) to be in place first.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{module_common}}` — Go module path to `internal/common` (e.g., `github.com/example/myproject/internal/common`)
|
||||
|
||||
## File 1: `internal/common/server/watermill.go`
|
||||
|
||||
```go
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
wmMiddleware "github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
)
|
||||
|
||||
func WithWatermillRouter(
|
||||
name string,
|
||||
configure func(*message.Router, message.Subscriber),
|
||||
) Option {
|
||||
return func(s *Server) {
|
||||
wmLogger := watermill.NewStdLoggerWithOut(os.Stdout, true, false)
|
||||
|
||||
amqpURI := os.Getenv("AMQP_URI")
|
||||
if amqpURI == "" {
|
||||
amqpURI = "amqp://guest:guest@rabbitmq:5672/"
|
||||
}
|
||||
amqpConfig := amqp.NewDurableQueueConfig(amqpURI)
|
||||
|
||||
sub, err := amqp.NewSubscriber(amqpConfig, wmLogger)
|
||||
if err != nil {
|
||||
panic("cannot create watermill subscriber: " + err.Error())
|
||||
}
|
||||
|
||||
r, err := message.NewRouter(message.RouterConfig{}, wmLogger)
|
||||
if err != nil {
|
||||
panic("cannot create watermill router: " + err.Error())
|
||||
}
|
||||
|
||||
r.AddMiddleware(
|
||||
wmMiddleware.CorrelationID,
|
||||
wmMiddleware.Recoverer,
|
||||
wmMiddleware.Retry{MaxRetries: 3}.Middleware,
|
||||
)
|
||||
|
||||
configure(r, sub)
|
||||
|
||||
s.addComponent(name, component{
|
||||
name: name,
|
||||
start: func(ctx context.Context) error {
|
||||
return r.Run(ctx)
|
||||
},
|
||||
stop: func(ctx context.Context) error {
|
||||
return r.Close()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File 2: `internal/common/client/watermill.go`
|
||||
|
||||
```go
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewWatermillPublisher() (pub message.Publisher, close func() error, err error) {
|
||||
amqpURI := os.Getenv("AMQP_URI")
|
||||
if amqpURI == "" {
|
||||
return nil, func() error { return nil }, errors.New("empty env AMQP_URI")
|
||||
}
|
||||
|
||||
logger := watermill.NewStdLoggerWithOut(os.Stdout, true, false)
|
||||
config := amqp.NewDurableQueueConfig(amqpURI)
|
||||
|
||||
publisher, err := amqp.NewPublisher(config, logger)
|
||||
if err != nil {
|
||||
return nil, func() error { return nil }, errors.Wrap(err, "cannot create watermill publisher")
|
||||
}
|
||||
|
||||
return publisher, publisher.Close, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Instructions
|
||||
|
||||
After creating the Watermill option and publisher:
|
||||
|
||||
1. Add `github.com/ThreeDotsLabs/watermill` and `github.com/ThreeDotsLabs/watermill-amqp/v3` to `go.mod`
|
||||
2. Add `AMQP_URI` to `.env`, `.test.env`, and `docker-compose.yml`
|
||||
3. Add a RabbitMQ service to `docker-compose.yml`:
|
||||
```yaml
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
```
|
||||
4. Use `/3dl scaffold event_handler <Name>` to create event handlers in a service
|
||||
5. Use `/3dl scaffold event_publisher <Name>` to create a publisher adapter
|
||||
6. Add `server.WithWatermillRouter("events", ...)` and include `"events"` in `OnShutdown`
|
||||
Reference in New Issue
Block a user