ga-plugins-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/config-patcher.d.ts +20 -50
  2. package/dist/config-patcher.d.ts.map +1 -1
  3. package/dist/config-patcher.js +138 -102
  4. package/dist/config-patcher.js.map +1 -1
  5. package/dist/index.js +41 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/installer.d.ts +0 -18
  8. package/dist/installer.d.ts.map +1 -1
  9. package/dist/installer.js +19 -39
  10. package/dist/installer.js.map +1 -1
  11. package/dist/types.d.ts +10 -6
  12. package/dist/types.d.ts.map +1 -1
  13. package/dist/uninstaller.d.ts +0 -23
  14. package/dist/uninstaller.d.ts.map +1 -1
  15. package/dist/uninstaller.js +22 -68
  16. package/dist/uninstaller.js.map +1 -1
  17. package/package.json +3 -2
  18. package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
  19. package/plugins/go-reviewer/commands/go-review.md +424 -0
  20. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
  21. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
  22. package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
  23. package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
  24. package/plugins/go-scaffolder/reference-service/.env.example +27 -0
  25. package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
  26. package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
  27. package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
  28. package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
  29. package/plugins/go-scaffolder/reference-service/go.mod +17 -0
  30. package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
  31. package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
  32. package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
  33. package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
  34. package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
  35. package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
  36. package/plugins/go-standards/commands/go-standards-check.md +232 -0
  37. package/plugins/go-standards/skills/concurrency.md +336 -0
  38. package/plugins/go-standards/skills/config.md +267 -0
  39. package/plugins/go-standards/skills/error-handling.md +286 -0
  40. package/plugins/go-standards/skills/http-chi.md +390 -0
  41. package/plugins/go-standards/skills/logging-observability.md +340 -0
  42. package/plugins/go-standards/skills/naming-and-style.md +315 -0
  43. package/plugins/go-standards/skills/project-layout.md +313 -0
  44. package/plugins/go-standards/skills/testing.md +366 -0
  45. package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
  46. package/plugins/java2go-porter/agents/analyzer.md +232 -0
  47. package/plugins/java2go-porter/agents/reviewer.md +241 -0
  48. package/plugins/java2go-porter/agents/test-pairer.md +365 -0
  49. package/plugins/java2go-porter/agents/translator.md +419 -0
  50. package/plugins/java2go-porter/commands/port-java-service.md +149 -0
  51. package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
  52. package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
  53. package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
  54. package/plugins/migration-safety/commands/strangler-plan.md +356 -0
  55. package/plugins/migration-safety/skills/strangler-fig.md +382 -0
@@ -0,0 +1,802 @@
1
+ # Command: scaffold-service
2
+
3
+ Generate a complete, production-ready thin Go microservice wired to go-ga-lib. The wizard asks exactly what dependencies the service needs and imports ONLY those packages — no unused imports, no dead code.
4
+
5
+ ---
6
+
7
+ ## Step 1: Interactive Wizard
8
+
9
+ Ask ALL questions in order before writing any files. Collect all answers first, then generate everything at once.
10
+
11
+ ### Q1 — Service name
12
+
13
+ Ask:
14
+
15
+ > "Domain / service name? (e.g. booking, payment, notification)"
16
+
17
+ Record as `<service-name>`. This becomes:
18
+ - The output folder name: `<service-name>-service/`
19
+ - Part of the default module path: `github.com/zokypesch/<service-name>-service`
20
+ - Package names throughout the code
21
+
22
+ ### Q2 — Service type
23
+
24
+ Ask:
25
+
26
+ > "Service type?
27
+ > (1) API server — HTTP only, no message consumption
28
+ > (2) Message consumer — consumes messages, no HTTP API (worker process)
29
+ > (3) Both — HTTP API + message consumption"
30
+
31
+ Record as `<service-type>`. Values: `api`, `consumer`, `both`.
32
+
33
+ ### Q3 — SQL database
34
+
35
+ Ask:
36
+
37
+ > "SQL database?
38
+ > (1) MySQL 8.4
39
+ > (2) PostgreSQL 18.4
40
+ > (3) None"
41
+
42
+ Record as `<sql-choice>`. Values: `mysql`, `postgres`, `none`.
43
+
44
+ ### Q4 — NoSQL stores
45
+
46
+ Ask:
47
+
48
+ > "NoSQL store(s)? Select all that apply (comma-separated numbers, e.g. 1,3):
49
+ > (1) Redis
50
+ > (2) Elasticsearch
51
+ > (3) MongoDB
52
+ > (4) Cassandra
53
+ > (5) ClickHouse
54
+ > (6) pgvector
55
+ > (7) Neo4j
56
+ > (8) None"
57
+
58
+ Record as `<nosql-choices>` (a list). If the user picks 8 or leaves blank, set to empty list.
59
+
60
+ ### Q5 — Message broker (only if `<service-type>` is `consumer` or `both`)
61
+
62
+ If `<service-type>` is `api`, skip this question and set `<broker>` to `none`.
63
+
64
+ Otherwise ask:
65
+
66
+ > "Message broker?
67
+ > (1) Kafka
68
+ > (2) RabbitMQ
69
+ > (3) ActiveMQ
70
+ > (4) NSQ"
71
+
72
+ Record as `<broker>`. Values: `kafka`, `rabbitmq`, `activemq`, `nsq`.
73
+
74
+ ### Q6 — HTTP port
75
+
76
+ Ask:
77
+
78
+ > "HTTP port? (press Enter for default: 8080)"
79
+
80
+ Record as `<http-port>`. Default: `8080`. Only used if `<service-type>` is `api` or `both`.
81
+
82
+ ### Q7 — Go module path
83
+
84
+ Ask:
85
+
86
+ > "Go module path for this service? (press Enter for default: github.com/zokypesch/<service-name>-service)"
87
+
88
+ Record as `<module-path>`. Default: `github.com/zokypesch/<service-name>-service`.
89
+
90
+ ### Q8 — go-ga-lib version
91
+
92
+ Ask:
93
+
94
+ > "go-ga-lib version to pin?
95
+ > Enter a git tag (e.g. v1.0.0) for a production service.
96
+ > Enter 'local' to use a replace directive pointing to the local go-ga-lib clone (D:/project/asyst/go-ga-lib).
97
+ > (default: local)"
98
+
99
+ Record as `<ga-lib-version>`. Default: `local`.
100
+
101
+ ---
102
+
103
+ ## Step 2: Confirm Before Generating
104
+
105
+ Print a confirmation summary:
106
+
107
+ ```
108
+ Generating <service-name>-service with:
109
+ Module: <module-path>
110
+ Type: <service-type>
111
+ SQL: <sql-choice>
112
+ NoSQL: <nosql-choices or "none">
113
+ Broker: <broker or "n/a">
114
+ Port: <http-port or "n/a (consumer only)">
115
+ go-ga-lib: <ga-lib-version>
116
+ Output dir: <service-name>-service/
117
+
118
+ Proceed? (Y/n)
119
+ ```
120
+
121
+ If the user says n or no, restart from Q1.
122
+
123
+ ---
124
+
125
+ ## Step 3: Generate Files
126
+
127
+ Generate ALL files listed below. Apply the DEPENDENCY RULES table strictly — if a dependency was not chosen, its import does NOT appear anywhere in the generated code.
128
+
129
+ ### DEPENDENCY RULES TABLE
130
+
131
+ | Dependency | Chosen when | go-ga-lib subpackage to import | Env vars generated |
132
+ |---|---|---|---|
133
+ | PostgreSQL | `<sql-choice>` == `postgres` | `github.com/zokypesch/go-ga-lib/sql` | DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME, DB_SSLMODE |
134
+ | MySQL | `<sql-choice>` == `mysql` | `github.com/zokypesch/go-ga-lib/sql/mysql` | DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME |
135
+ | Redis | `nosql-choices` contains `1` | `github.com/zokypesch/go-ga-lib/nosql/redis` | REDIS_ADDR, REDIS_PASSWORD, REDIS_DB |
136
+ | Elasticsearch | `nosql-choices` contains `2` | `github.com/zokypesch/go-ga-lib/nosql/elasticsearch` | ES_ADDRS, ES_USERNAME, ES_PASSWORD |
137
+ | MongoDB | `nosql-choices` contains `3` | `github.com/zokypesch/go-ga-lib/nosql/mongo` | MONGO_URI, MONGO_DATABASE |
138
+ | Cassandra | `nosql-choices` contains `4` | `github.com/zokypesch/go-ga-lib/nosql/cassandra` | CASSANDRA_HOSTS, CASSANDRA_KEYSPACE |
139
+ | ClickHouse | `nosql-choices` contains `5` | `github.com/zokypesch/go-ga-lib/nosql/clickhouse` | CLICKHOUSE_ADDR, CLICKHOUSE_DATABASE |
140
+ | pgvector | `nosql-choices` contains `6` | `github.com/zokypesch/go-ga-lib/nosql/pgvector` | PGVECTOR_HOST, PGVECTOR_PORT, PGVECTOR_USER, PGVECTOR_PASS, PGVECTOR_NAME |
141
+ | Neo4j | `nosql-choices` contains `7` | `github.com/zokypesch/go-ga-lib/nosql/neo4j` | NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD |
142
+ | Kafka | `<broker>` == `kafka` | `github.com/zokypesch/go-ga-lib/broker/kafka` | KAFKA_BROKERS, KAFKA_GROUP_ID, KAFKA_TOPIC |
143
+ | RabbitMQ | `<broker>` == `rabbitmq` | `github.com/zokypesch/go-ga-lib/broker/rabbitmq` | RABBITMQ_URL, RABBITMQ_QUEUE |
144
+ | ActiveMQ | `<broker>` == `activemq` | `github.com/zokypesch/go-ga-lib/broker/activemq` | ACTIVEMQ_URL, ACTIVEMQ_QUEUE |
145
+ | NSQ | `<broker>` == `nsq` | `github.com/zokypesch/go-ga-lib/broker/nsq` | NSQ_ADDR, NSQ_TOPIC, NSQ_CHANNEL |
146
+
147
+ Always import regardless of choices:
148
+ - `github.com/zokypesch/go-ga-lib/config` — config loading
149
+ - `github.com/zokypesch/go-ga-lib/observability` — OTel tracing + Prometheus + Loki
150
+ - `github.com/zokypesch/go-ga-lib/logger` — structured zap logger
151
+
152
+ Import only if `<service-type>` is `api` or `both`:
153
+ - `github.com/zokypesch/go-ga-lib/httpx` — chi router factory + JSON/error helpers
154
+ - `github.com/zokypesch/go-ga-lib/server` — HTTP server with graceful shutdown
155
+
156
+ ---
157
+
158
+ ### File: `<service-name>-service/go.mod`
159
+
160
+ ```
161
+ module <module-path>
162
+
163
+ go 1.23
164
+
165
+ require (
166
+ github.com/go-chi/chi/v5 v5.2.1
167
+ github.com/zokypesch/go-ga-lib <version-line>
168
+ go.uber.org/zap v1.27.0
169
+ github.com/google/uuid v1.6.0
170
+ github.com/jmoiron/sqlx v1.4.0
171
+ github.com/stretchr/testify v1.9.0
172
+ github.com/caarlos0/env/v11 v11.3.1
173
+ )
174
+ ```
175
+
176
+ `<version-line>` rules:
177
+ - If `<ga-lib-version>` == `local`: write `v0.0.0` AND add the replace directive below the require block:
178
+ ```
179
+ replace github.com/zokypesch/go-ga-lib => D:/project/asyst/go-ga-lib
180
+ ```
181
+ - If `<ga-lib-version>` is a tag (e.g. `v1.0.0`): write that tag as the version, NO replace directive.
182
+
183
+ Only include driver dependencies that match chosen dependencies:
184
+ - postgres: add `github.com/lib/pq v1.10.9`
185
+ - mysql: add `github.com/go-sql-driver/mysql v1.8.1`
186
+ - mongo: add `go.mongodb.org/mongo-driver v1.15.0`
187
+ - redis: add `github.com/redis/go-redis/v9 v9.5.1`
188
+ - kafka: add `github.com/IBM/sarama v1.43.2`
189
+ - rabbitmq: add `github.com/rabbitmq/amqp091-go v1.10.0`
190
+ - nsq: add `github.com/nsqio/go-nsq v1.1.0`
191
+
192
+ ---
193
+
194
+ ### File: `<service-name>-service/config/config.go`
195
+
196
+ ```go
197
+ package config
198
+
199
+ import (
200
+ "fmt"
201
+ "github.com/caarlos0/env/v11"
202
+ )
203
+
204
+ // Config holds all configuration for the <service-name> service.
205
+ // All values come from environment variables — no config files at runtime.
206
+ type Config struct {
207
+ // Application
208
+ Port int `env:"PORT" envDefault:"<http-port>"`
209
+ LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
210
+ ServiceName string `env:"SERVICE_NAME" envDefault:"<service-name>-service"`
211
+ ServiceVersion string `env:"SERVICE_VERSION" envDefault:"0.1.0"`
212
+ ShutdownTimeout int `env:"SHUTDOWN_TIMEOUT" envDefault:"10"` // seconds
213
+
214
+ // OpenTelemetry
215
+ OTLPEndpoint string `env:"OTLP_ENDPOINT" envDefault:"localhost:4317"`
216
+
217
+ // [ONLY include the blocks below that match chosen dependencies]
218
+ // PostgreSQL block — only if sql-choice == postgres
219
+ DBHost string `env:"DB_HOST" required:"true"`
220
+ DBPort string `env:"DB_PORT" envDefault:"5432"`
221
+ DBUser string `env:"DB_USER" required:"true"`
222
+ DBPass string `env:"DB_PASS" required:"true"`
223
+ DBName string `env:"DB_NAME" required:"true"`
224
+ DBSSLMode string `env:"DB_SSLMODE" envDefault:"disable"`
225
+
226
+ // Redis block — only if Redis chosen
227
+ RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
228
+ RedisPassword string `env:"REDIS_PASSWORD" envDefault:""`
229
+ RedisDB int `env:"REDIS_DB" envDefault:"0"`
230
+
231
+ // [Continue pattern for each chosen dependency per DEPENDENCY RULES TABLE]
232
+ }
233
+
234
+ // Load parses environment variables into Config.
235
+ // Returns an error if any required variable is missing or invalid.
236
+ func Load() (*Config, error) {
237
+ cfg := &Config{}
238
+ if err := env.Parse(cfg); err != nil {
239
+ return nil, fmt.Errorf("loading config: %w", err)
240
+ }
241
+ return cfg, nil
242
+ }
243
+
244
+ // DSN builds the PostgreSQL connection string. [Only include if postgres chosen]
245
+ // Never log the output of this method.
246
+ func (c *Config) DSN() string {
247
+ return fmt.Sprintf(
248
+ "host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
249
+ c.DBHost, c.DBPort, c.DBName, c.DBUser, c.DBPass, c.DBSSLMode,
250
+ )
251
+ }
252
+
253
+ // String returns a safe representation with secrets redacted.
254
+ func (c *Config) String() string {
255
+ return fmt.Sprintf(
256
+ "Config{Port:%d ServiceName:%s ServiceVersion:%s LogLevel:%s}",
257
+ c.Port, c.ServiceName, c.ServiceVersion, c.LogLevel,
258
+ )
259
+ }
260
+ ```
261
+
262
+ RULE: Only generate config fields for chosen dependencies. If SQL is None, no DB_ fields. If no NoSQL chosen, no NoSQL fields. If no broker, no broker fields.
263
+
264
+ ---
265
+
266
+ ### File: `<service-name>-service/internal/domain/<service-name>.go`
267
+
268
+ Generate the domain layer:
269
+
270
+ ```go
271
+ package domain
272
+
273
+ import (
274
+ "context"
275
+ "errors"
276
+ "time"
277
+ )
278
+
279
+ // Sentinel errors — map these to HTTP status codes in the handler layer.
280
+ var (
281
+ ErrNotFound = errors.New("<service-name> not found")
282
+ ErrAlreadyExists = errors.New("<service-name> already exists")
283
+ ErrInvalidInput = errors.New("invalid input")
284
+ ErrUnauthorized = errors.New("unauthorized")
285
+ ErrForbidden = errors.New("forbidden")
286
+ )
287
+
288
+ // <ServiceName> is the core domain entity.
289
+ type <ServiceName> struct {
290
+ ID string `db:"id" json:"id"`
291
+ // Add 3-5 realistic fields for the domain (e.g. for booking: CustomerID, FlightID, Status)
292
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
293
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
294
+ }
295
+
296
+ // <ServiceName>Repository defines the data access contract.
297
+ type <ServiceName>Repository interface {
298
+ Create(ctx context.Context, entity *<ServiceName>) error
299
+ GetByID(ctx context.Context, id string) (*<ServiceName>, error)
300
+ Update(ctx context.Context, entity *<ServiceName>) error
301
+ List(ctx context.Context, limit, offset int) ([]*<ServiceName>, error)
302
+ Delete(ctx context.Context, id string) error
303
+ }
304
+
305
+ // <ServiceName>Usecase defines the business logic contract.
306
+ type <ServiceName>Usecase interface {
307
+ Create(ctx context.Context, input Create<ServiceName>Input) (*<ServiceName>, error)
308
+ GetByID(ctx context.Context, id string) (*<ServiceName>, error)
309
+ Update(ctx context.Context, id string, input Update<ServiceName>Input) (*<ServiceName>, error)
310
+ List(ctx context.Context, limit, offset int) ([]*<ServiceName>, error)
311
+ Delete(ctx context.Context, id string) error
312
+ }
313
+
314
+ // Create<ServiceName>Input is the payload for creating a new entity.
315
+ type Create<ServiceName>Input struct {
316
+ // Fields derived from <service-name> domain
317
+ }
318
+
319
+ // Update<ServiceName>Input is the payload for updating an existing entity.
320
+ type Update<ServiceName>Input struct {
321
+ // Fields derived from <service-name> domain (all optional via pointers)
322
+ }
323
+ ```
324
+
325
+ ---
326
+
327
+ ### File: `<service-name>-service/internal/repository/<service-name>_<sql-engine>.go`
328
+
329
+ [Only generate if `<sql-choice>` != `none`]
330
+
331
+ ```go
332
+ package repository
333
+
334
+ import (
335
+ "context"
336
+ "database/sql"
337
+ "fmt"
338
+
339
+ "github.com/jmoiron/sqlx"
340
+ "<module-path>/internal/domain"
341
+ )
342
+
343
+ type <sqlEngine><ServiceName>Repo struct {
344
+ db *sqlx.DB
345
+ }
346
+
347
+ func New<SQLEngine><ServiceName>Repo(db *sqlx.DB) domain.<ServiceName>Repository {
348
+ return &<sqlEngine><ServiceName>Repo{db: db}
349
+ }
350
+
351
+ func (r *<sqlEngine><ServiceName>Repo) Create(ctx context.Context, entity *domain.<ServiceName>) error {
352
+ query := `INSERT INTO <service-name>s (id, ..., created_at, updated_at)
353
+ VALUES (:id, ..., :created_at, :updated_at)`
354
+ _, err := r.db.NamedExecContext(ctx, query, entity)
355
+ if err != nil {
356
+ return fmt.Errorf("repository.Create: %w", err)
357
+ }
358
+ return nil
359
+ }
360
+
361
+ func (r *<sqlEngine><ServiceName>Repo) GetByID(ctx context.Context, id string) (*domain.<ServiceName>, error) {
362
+ entity := &domain.<ServiceName>{}
363
+ err := r.db.GetContext(ctx, entity, `SELECT * FROM <service-name>s WHERE id = $1`, id)
364
+ if err != nil {
365
+ if err == sql.ErrNoRows {
366
+ return nil, domain.ErrNotFound
367
+ }
368
+ return nil, fmt.Errorf("repository.GetByID: %w", err)
369
+ }
370
+ return entity, nil
371
+ }
372
+
373
+ // [Implement Update, List, Delete following the same pattern]
374
+ ```
375
+
376
+ If `<sql-choice>` is `mysql`, use `?` placeholders. If `postgres`, use `$1`, `$2`, etc.
377
+
378
+ ---
379
+
380
+ ### File: `<service-name>-service/internal/usecase/<service-name>.go`
381
+
382
+ ```go
383
+ package usecase
384
+
385
+ import (
386
+ "context"
387
+ "fmt"
388
+ "time"
389
+
390
+ "github.com/google/uuid"
391
+ "go.uber.org/zap"
392
+
393
+ "<module-path>/internal/domain"
394
+ )
395
+
396
+ type <service-name>Usecase struct {
397
+ repo domain.<ServiceName>Repository
398
+ logger *zap.Logger
399
+ }
400
+
401
+ func New<ServiceName>Usecase(repo domain.<ServiceName>Repository, logger *zap.Logger) domain.<ServiceName>Usecase {
402
+ return &<service-name>Usecase{repo: repo, logger: logger}
403
+ }
404
+
405
+ func (uc *<service-name>Usecase) Create(ctx context.Context, input domain.Create<ServiceName>Input) (*domain.<ServiceName>, error) {
406
+ // Validate input
407
+ // [add real validation here]
408
+
409
+ entity := &domain.<ServiceName>{
410
+ ID: uuid.NewString(),
411
+ CreatedAt: time.Now().UTC(),
412
+ UpdatedAt: time.Now().UTC(),
413
+ // map input fields
414
+ }
415
+
416
+ if err := uc.repo.Create(ctx, entity); err != nil {
417
+ return nil, fmt.Errorf("usecase.Create: %w", err)
418
+ }
419
+
420
+ uc.logger.Info("created <service-name>", zap.String("id", entity.ID))
421
+ return entity, nil
422
+ }
423
+
424
+ // [Implement GetByID, Update, List, Delete with proper error wrapping and logging]
425
+ ```
426
+
427
+ ---
428
+
429
+ ### File: `<service-name>-service/internal/handler/<service-name>.go`
430
+
431
+ [Only generate if `<service-type>` is `api` or `both`]
432
+
433
+ ```go
434
+ package handler
435
+
436
+ import (
437
+ "encoding/json"
438
+ "errors"
439
+ "io"
440
+ "net/http"
441
+ "strconv"
442
+
443
+ "github.com/go-chi/chi/v5"
444
+ "github.com/go-chi/chi/v5/middleware"
445
+ "go.uber.org/zap"
446
+
447
+ "<module-path>/internal/domain"
448
+ )
449
+
450
+ const maxBodyBytes = 1 << 20 // 1 MB
451
+
452
+ type <ServiceName>Handler struct {
453
+ uc domain.<ServiceName>Usecase
454
+ logger *zap.Logger
455
+ }
456
+
457
+ func New<ServiceName>Handler(uc domain.<ServiceName>Usecase, logger *zap.Logger) *<ServiceName>Handler {
458
+ return &<ServiceName>Handler{uc: uc, logger: logger}
459
+ }
460
+
461
+ // Routes mounts all <service-name> routes on the provided chi.Router.
462
+ func (h *<ServiceName>Handler) Routes(r chi.Router) {
463
+ r.Get("/", h.List)
464
+ r.Post("/", h.Create)
465
+ r.Get("/{id}", h.GetByID)
466
+ r.Put("/{id}", h.Update)
467
+ r.Delete("/{id}", h.Delete)
468
+ }
469
+
470
+ func (h *<ServiceName>Handler) Create(w http.ResponseWriter, r *http.Request) {
471
+ var input domain.Create<ServiceName>Input
472
+ if err := decodeJSON(r, &input); err != nil {
473
+ respondErrorStatus(w, http.StatusBadRequest, err.Error(), "BAD_REQUEST")
474
+ return
475
+ }
476
+ result, err := h.uc.Create(r.Context(), input)
477
+ if err != nil {
478
+ h.logger.Error("create <service-name>", zap.Error(err),
479
+ zap.String("request_id", middleware.GetReqID(r.Context())))
480
+ respondError(w, err)
481
+ return
482
+ }
483
+ respondJSON(w, http.StatusCreated, result)
484
+ }
485
+
486
+ // [Implement GetByID, Update, List, Delete following the same thin-handler pattern]
487
+
488
+ // --- helpers ---
489
+
490
+ func decodeJSON(r *http.Request, dst any) error {
491
+ r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
492
+ dec := json.NewDecoder(r.Body)
493
+ dec.DisallowUnknownFields()
494
+ if err := dec.Decode(dst); err != nil {
495
+ if errors.Is(err, io.EOF) {
496
+ return errors.New("request body must not be empty")
497
+ }
498
+ return fmt.Errorf("decoding request: %w", err)
499
+ }
500
+ return nil
501
+ }
502
+
503
+ type errorBody struct {
504
+ Error string `json:"error"`
505
+ Code string `json:"code"`
506
+ }
507
+
508
+ func respondJSON(w http.ResponseWriter, status int, data any) {
509
+ w.Header().Set("Content-Type", "application/json")
510
+ w.WriteHeader(status)
511
+ if data != nil {
512
+ _ = json.NewEncoder(w).Encode(data)
513
+ }
514
+ }
515
+
516
+ func respondError(w http.ResponseWriter, err error) {
517
+ switch {
518
+ case errors.Is(err, domain.ErrNotFound):
519
+ respondErrorStatus(w, http.StatusNotFound, err.Error(), "NOT_FOUND")
520
+ case errors.Is(err, domain.ErrAlreadyExists):
521
+ respondErrorStatus(w, http.StatusConflict, err.Error(), "CONFLICT")
522
+ case errors.Is(err, domain.ErrInvalidInput):
523
+ respondErrorStatus(w, http.StatusBadRequest, err.Error(), "BAD_REQUEST")
524
+ case errors.Is(err, domain.ErrUnauthorized):
525
+ respondErrorStatus(w, http.StatusUnauthorized, err.Error(), "UNAUTHORIZED")
526
+ case errors.Is(err, domain.ErrForbidden):
527
+ respondErrorStatus(w, http.StatusForbidden, err.Error(), "FORBIDDEN")
528
+ default:
529
+ respondErrorStatus(w, http.StatusInternalServerError, "internal server error", "INTERNAL")
530
+ }
531
+ }
532
+
533
+ func respondErrorStatus(w http.ResponseWriter, status int, message, code string) {
534
+ w.Header().Set("Content-Type", "application/json")
535
+ w.WriteHeader(status)
536
+ _ = json.NewEncoder(w).Encode(errorBody{Error: message, Code: code})
537
+ }
538
+ ```
539
+
540
+ ---
541
+
542
+ ### File: `<service-name>-service/internal/consumer/<service-name>_consumer.go`
543
+
544
+ [Only generate if `<service-type>` is `consumer` or `both`]
545
+
546
+ Generate a consumer struct that:
547
+ - Imports ONLY the chosen broker subpackage from go-ga-lib
548
+ - Has a `Start(ctx context.Context) error` method
549
+ - Has a `processMessage(ctx context.Context, msg []byte) error` method with example logic
550
+ - Stops cleanly when the context is cancelled
551
+
552
+ ---
553
+
554
+ ### File: `<service-name>-service/cmd/server/main.go`
555
+
556
+ ```go
557
+ package main
558
+
559
+ import (
560
+ "context"
561
+ "fmt"
562
+ "log"
563
+ "net/http"
564
+ "os"
565
+ "os/signal"
566
+ "syscall"
567
+ "time"
568
+
569
+ "github.com/go-chi/chi/v5"
570
+ "github.com/go-chi/chi/v5/middleware"
571
+ "go.uber.org/zap"
572
+
573
+ "<module-path>/config"
574
+ "<module-path>/internal/domain"
575
+ "<module-path>/internal/handler" // [only if api or both]
576
+ "<module-path>/internal/repository" // [only if sql != none]
577
+ "<module-path>/internal/usecase"
578
+ // [Add chosen go-ga-lib subpackage imports per DEPENDENCY RULES TABLE]
579
+ )
580
+
581
+ func main() {
582
+ // 1. Load config — fail fast if required vars are missing
583
+ cfg, err := config.Load()
584
+ if err != nil {
585
+ log.Fatalf("loading config: %v", err)
586
+ }
587
+
588
+ // 2. Initialize logger
589
+ // [call go-ga-lib/logger.New with cfg.LogLevel, cfg.ServiceName, cfg.ServiceVersion]
590
+
591
+ // 3. Initialize observability (OTel tracing + Prometheus + Loki)
592
+ // [call go-ga-lib/observability.Init with cfg.OTLPEndpoint, cfg.ServiceName, cfg.ServiceVersion]
593
+ // defer shutdown
594
+
595
+ // 4. Initialize chosen dependencies (only include blocks for chosen deps):
596
+
597
+ // [SQL block — only if sql != none]
598
+ // db, err := <go-ga-lib/sql>.Open(cfg.DSN())
599
+ // if err != nil { logger.Fatal("opening db", zap.Error(err)) }
600
+ // defer db.Close()
601
+
602
+ // [Redis block — only if Redis chosen]
603
+ // rdb := <go-ga-lib/nosql/redis>.NewClient(...)
604
+ // defer rdb.Close()
605
+
606
+ // [Broker block — only if consumer or both]
607
+ // brokerClient := <go-ga-lib/broker/<broker>>.NewClient(...)
608
+ // defer brokerClient.Close()
609
+
610
+ // 5. Wire layers
611
+ // repo := repository.New<SQLEngine><ServiceName>Repo(db) // [only if sql != none]
612
+ // uc := usecase.New<ServiceName>Usecase(repo, logger)
613
+
614
+ // 6. Start consumer (only if consumer or both)
615
+ // consumer := consumer.New<ServiceName>Consumer(brokerClient, uc, logger)
616
+ // go consumer.Start(ctx)
617
+
618
+ // 7. Start HTTP server (only if api or both)
619
+ r := chi.NewRouter()
620
+ r.Use(middleware.RequestID)
621
+ r.Use(middleware.RealIP)
622
+ r.Use(middleware.Recoverer)
623
+
624
+ r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
625
+ w.Header().Set("Content-Type", "application/json")
626
+ w.WriteHeader(http.StatusOK)
627
+ fmt.Fprint(w, `{"status":"ok"}`)
628
+ })
629
+ r.Get("/ready", func(w http.ResponseWriter, r *http.Request) {
630
+ // [ping each dependency; return 503 if any fail]
631
+ w.Header().Set("Content-Type", "application/json")
632
+ w.WriteHeader(http.StatusOK)
633
+ fmt.Fprint(w, `{"status":"ready"}`)
634
+ })
635
+
636
+ r.Route("/api/v1", func(r chi.Router) {
637
+ <service-name>Handler := handler.New<ServiceName>Handler(uc, logger)
638
+ r.Route("/<service-name>s", func(r chi.Router) {
639
+ <service-name>Handler.Routes(r)
640
+ })
641
+ })
642
+
643
+ srv := &http.Server{
644
+ Addr: fmt.Sprintf(":%d", cfg.Port),
645
+ Handler: r,
646
+ }
647
+
648
+ // 8. Graceful shutdown
649
+ quit := make(chan os.Signal, 1)
650
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
651
+
652
+ go func() {
653
+ logger.Info("server starting", zap.Int("port", cfg.Port))
654
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
655
+ logger.Fatal("server failed", zap.Error(err))
656
+ }
657
+ }()
658
+
659
+ <-quit
660
+ logger.Info("shutting down")
661
+
662
+ shutdownCtx, cancel := context.WithTimeout(context.Background(),
663
+ time.Duration(cfg.ShutdownTimeout)*time.Second)
664
+ defer cancel()
665
+
666
+ if err := srv.Shutdown(shutdownCtx); err != nil {
667
+ logger.Error("shutdown error", zap.Error(err))
668
+ }
669
+
670
+ // [close each initialized dependency in reverse init order]
671
+ logger.Info("server stopped")
672
+ }
673
+ ```
674
+
675
+ ---
676
+
677
+ ### File: `<service-name>-service/Dockerfile`
678
+
679
+ Multi-stage Dockerfile. Stage 1 builds, stage 2 is distroless minimal runtime.
680
+
681
+ ```dockerfile
682
+ # syntax=docker/dockerfile:1
683
+
684
+ # ---- Stage 1: Build ----
685
+ FROM golang:1.23-alpine AS builder
686
+
687
+ RUN apk add --no-cache git ca-certificates tzdata
688
+
689
+ WORKDIR /app
690
+
691
+ COPY go.mod go.sum ./
692
+ RUN go mod download
693
+
694
+ COPY . .
695
+
696
+ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
697
+ go build -trimpath -ldflags="-s -w" \
698
+ -o /app/server ./cmd/server
699
+
700
+ # ---- Stage 2: Runtime ----
701
+ FROM gcr.io/distroless/static:nonroot
702
+
703
+ COPY --from=builder /app/server /server
704
+ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
705
+ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
706
+
707
+ USER nonroot:nonroot
708
+
709
+ EXPOSE <http-port>
710
+
711
+ ENTRYPOINT ["/server"]
712
+ ```
713
+
714
+ ---
715
+
716
+ ### File: `<service-name>-service/.env.example`
717
+
718
+ Generate only the env vars relevant to chosen dependencies. Always include the core app/OTel block.
719
+
720
+ ```bash
721
+ # Copy to .env for local development. Never commit .env.
722
+
723
+ # --- Application ---
724
+ PORT=<http-port>
725
+ LOG_LEVEL=info # debug | info | warn | error
726
+ SERVICE_NAME=<service-name>-service
727
+ SERVICE_VERSION=0.1.0
728
+ SHUTDOWN_TIMEOUT=10 # seconds to wait for in-flight requests on SIGTERM
729
+
730
+ # --- OpenTelemetry (Grafana Tempo) ---
731
+ OTLP_ENDPOINT=localhost:4317
732
+
733
+ # [Only include sections below for chosen dependencies]
734
+
735
+ # --- PostgreSQL ---
736
+ DB_HOST=localhost
737
+ DB_PORT=5432
738
+ DB_USER=<service-name>_user
739
+ DB_PASS=changeme
740
+ DB_NAME=<service-name>_db
741
+ DB_SSLMODE=disable # use 'require' in production
742
+
743
+ # --- Redis ---
744
+ REDIS_ADDR=localhost:6379
745
+ REDIS_PASSWORD= # leave empty for no-auth local dev
746
+ REDIS_DB=0
747
+
748
+ # [Continue pattern for each chosen dependency]
749
+ ```
750
+
751
+ ---
752
+
753
+ ## Step 4: Post-generation Summary
754
+
755
+ After writing all files, print:
756
+
757
+ ```
758
+ Generated: <service-name>-service/
759
+
760
+ go.mod (module <module-path>)
761
+ config/config.go (env config)
762
+ internal/domain/<service-name>.go (interfaces + structs)
763
+ internal/repository/<service-name>_<db>.go (if sql chosen)
764
+ internal/usecase/<service-name>.go (business logic)
765
+ internal/handler/<service-name>.go (HTTP handlers, if api or both)
766
+ internal/consumer/<service-name>_consumer.go (if consumer or both)
767
+ cmd/server/main.go (entry point)
768
+ Dockerfile
769
+ .env.example
770
+
771
+ Dependencies wired:
772
+ SQL: <sql-choice>
773
+ NoSQL: <nosql-choices or "none">
774
+ Broker: <broker or "n/a">
775
+
776
+ Next steps:
777
+ 1. Copy .env.example to .env and fill in real values
778
+ 2. Run: go mod tidy
779
+ 3. Run: go build ./...
780
+ 4. See reference-service/ for a complete working exemplar
781
+
782
+ NOTE: If you used 'local' for go-ga-lib, the go.mod contains a replace directive.
783
+ Switch to a pinned version before committing to the shared repository:
784
+ 1. Remove the replace directive
785
+ 2. Set: require github.com/zokypesch/go-ga-lib v<tag>
786
+ ```
787
+
788
+ ---
789
+
790
+ ## Constraints (strictly enforced)
791
+
792
+ - NEVER import a go-ga-lib subpackage that was not chosen.
793
+ - NEVER add a Config field for an env var that was not chosen.
794
+ - NEVER add a `.env.example` line for an unchosen dependency.
795
+ - NEVER generate a consumer file if `<service-type>` is `api`.
796
+ - NEVER generate an HTTP handler if `<service-type>` is `consumer`.
797
+ - The `replace` directive only appears in `go.mod` when `<ga-lib-version>` == `local`. Production services MUST NOT have a replace directive.
798
+ - All generated Go files must compile (no placeholder syntax errors, no missing imports).
799
+ - Use `context.Context` as the first argument of every repository and usecase method.
800
+ - All repository methods must map `sql.ErrNoRows` to `domain.ErrNotFound`.
801
+ - All usecase methods must wrap errors with `fmt.Errorf("usecase.Method: %w", err)`.
802
+ - Dockerfile stage 2 must use `gcr.io/distroless/static:nonroot` — never `alpine` or `ubuntu` in the runtime stage.