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,258 @@
|
||||
# Service Scaffold Template
|
||||
|
||||
Generate a complete service skeleton with all standard directories and stub files.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase service/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
|
||||
|
||||
## Files to Create
|
||||
|
||||
### 1. `domain/{{name_lower}}/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type {{Name}} struct {
|
||||
uuid string
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
func New{{Name}}(uuid string) (*{{Name}}, error) {
|
||||
if uuid == "" {
|
||||
return nil, errors.New("empty {{name_lower}} uuid")
|
||||
}
|
||||
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Unmarshal{{Name}}FromDatabase(uuid string, createdAt time.Time) *{{Name}} {
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (t {{Name}}) UUID() string {
|
||||
return t.uuid
|
||||
}
|
||||
|
||||
func (t {{Name}}) CreatedAt() time.Time {
|
||||
return t.createdAt
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `domain/{{name_lower}}/repository.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "context"
|
||||
|
||||
type Repository interface {
|
||||
Get{{Name}}(ctx context.Context, uuid string) (*{{Name}}, error)
|
||||
Update{{Name}}(ctx context.Context, uuid string,
|
||||
updateFn func(t *{{Name}}) (*{{Name}}, error)) error
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `domain/{{name_lower}}/errors.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("{{name_lower}} not found")
|
||||
)
|
||||
```
|
||||
|
||||
### 4. `app/app.go`
|
||||
|
||||
```go
|
||||
package app
|
||||
|
||||
import (
|
||||
"{{module}}/app/command"
|
||||
"{{module}}/app/query"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Commands Commands
|
||||
Queries Queries
|
||||
}
|
||||
|
||||
type Commands struct {
|
||||
// Add command handlers here, e.g.:
|
||||
// Create{{Name}} command.Create{{Name}}Handler
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
// Add query handlers here, e.g.:
|
||||
// {{Name}}ByUUID query.{{Name}}ByUUIDHandler
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `app/command/.gitkeep`
|
||||
|
||||
Create empty directory placeholder.
|
||||
|
||||
### 6. `app/query/.gitkeep`
|
||||
|
||||
Create empty directory placeholder.
|
||||
|
||||
### 7. `ports/http.go`
|
||||
|
||||
```go
|
||||
package ports
|
||||
|
||||
import (
|
||||
"{{module}}/app"
|
||||
)
|
||||
|
||||
type HttpServer struct {
|
||||
app app.Application
|
||||
}
|
||||
|
||||
func NewHttpServer(application app.Application) HttpServer {
|
||||
return HttpServer{app: application}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. `main.go`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"{{module_common}}/logs"
|
||||
"{{module_common}}/server"
|
||||
"{{module}}/ports"
|
||||
"{{module}}/service"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logs.Init()
|
||||
ctx := context.Background()
|
||||
|
||||
app := service.NewApplication(ctx)
|
||||
|
||||
server.New(
|
||||
server.WithHTTPHandler("api", func(router chi.Router) http.Handler {
|
||||
return ports.HandlerFromMux(ports.NewHttpServer(app), router)
|
||||
}),
|
||||
server.OnShutdown(
|
||||
server.Stop("api"),
|
||||
),
|
||||
).Run(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
### 9. `adapters/memory_{{name_snake}}_repository.go`
|
||||
|
||||
```go
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"{{module}}/domain/{{name_lower}}"
|
||||
)
|
||||
|
||||
type Memory{{Name}}Repository struct {
|
||||
{{name_lower}}s map[string]{{name_lower}}.{{Name}}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMemory{{Name}}Repository() *Memory{{Name}}Repository {
|
||||
return &Memory{{Name}}Repository{
|
||||
{{name_lower}}s: make(map[string]{{name_lower}}.{{Name}}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Get{{Name}}(ctx context.Context, uuid string) (*{{name_lower}}.{{Name}}, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
t, ok := r.{{name_lower}}s[uuid]
|
||||
if !ok {
|
||||
return nil, {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Update{{Name}}(
|
||||
ctx context.Context,
|
||||
uuid string,
|
||||
updateFn func(t *{{name_lower}}.{{Name}}) (*{{name_lower}}.{{Name}}, error),
|
||||
) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
current, ok := r.{{name_lower}}s[uuid]
|
||||
if !ok {
|
||||
return {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
updated, err := updateFn(¤t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.{{name_lower}}s[uuid] = *updated
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 10. `service/application.go`
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"{{module}}/adapters"
|
||||
"{{module}}/app"
|
||||
)
|
||||
|
||||
func NewApplication(ctx context.Context) app.Application {
|
||||
{{name_lower}}Repository := adapters.NewMemory{{Name}}Repository()
|
||||
_ = {{name_lower}}Repository // wire into handlers
|
||||
|
||||
return app.Application{
|
||||
Commands: app.Commands{},
|
||||
Queries: app.Queries{},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Instructions
|
||||
|
||||
After creating the service skeleton:
|
||||
|
||||
1. Ensure unified server exists: `/3dl scaffold unified_server`
|
||||
2. Add your first command with `/3dl scaffold command <ActionName>`
|
||||
3. Add your first query with `/3dl scaffold query <QueryName>`
|
||||
4. Wire them in `service/application.go`
|
||||
5. Add HTTP/gRPC handlers in `ports/`
|
||||
6. When adding Watermill: `/3dl scaffold watermill_router` then `/3dl scaffold event_handler <Name>`
|
||||
Reference in New Issue
Block a user