@nakedev/go-scaffold 0.1.4 → 0.3.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -44
  3. package/dist/commands/auth.js +116 -11
  4. package/dist/commands/create.js +13 -1
  5. package/dist/commands/generate.js +21 -11
  6. package/dist/commands/method.js +32 -3
  7. package/dist/commands/observability.js +114 -0
  8. package/dist/commands/rbac.js +19 -2
  9. package/dist/commands/undo.js +331 -0
  10. package/dist/commands/worker.js +92 -32
  11. package/dist/index.js +366 -63
  12. package/dist/prompts/auth-wizard.js +29 -0
  13. package/dist/prompts/create-wizard.js +5 -2
  14. package/dist/prompts/generate-wizard.js +57 -0
  15. package/dist/prompts/worker-wizard.js +25 -0
  16. package/dist/templates/auth-manifest.js +20 -3
  17. package/dist/templates/create-manifest.js +13 -20
  18. package/dist/templates/observability-manifest.js +24 -0
  19. package/dist/templates/rbac-manifest.js +1 -0
  20. package/dist/templates/worker-manifest.js +23 -6
  21. package/dist/utils/auth-patcher.js +96 -21
  22. package/dist/utils/config.js +58 -10
  23. package/dist/utils/gocheck.js +57 -5
  24. package/dist/utils/golangci-patcher.js +73 -0
  25. package/dist/utils/gomod-patcher.js +53 -0
  26. package/dist/utils/main-patcher.js +58 -4
  27. package/dist/utils/marker-patch.js +125 -3
  28. package/dist/utils/method-patcher.js +17 -2
  29. package/dist/utils/module-location.js +37 -1
  30. package/dist/utils/naming.js +50 -2
  31. package/dist/utils/observability-patcher.js +107 -0
  32. package/dist/utils/platform-patcher.js +98 -12
  33. package/dist/utils/rbac-patcher.js +60 -10
  34. package/package.json +3 -5
  35. package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
  36. package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
  37. package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
  38. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
  39. package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
  40. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
  41. package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
  42. package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
  43. package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
  44. package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
  45. package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
  46. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
  47. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
  48. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
  49. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
  50. package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
  51. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
  52. package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
  53. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
  54. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
  55. package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
  56. package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
  57. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  58. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
  59. package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
  60. package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
  61. package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
  62. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
  63. package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
  64. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
  65. package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
  66. package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
  67. package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
  68. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
  69. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
  70. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
  71. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
  72. package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
  73. package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
  74. package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
  75. package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
  76. package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
  77. package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
  78. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +71 -12
  79. package/templates/create/base/.dockerignore.hbs +13 -0
  80. package/templates/create/base/.env.example.hbs +18 -9
  81. package/templates/create/base/.github/dependabot.yml.hbs +20 -0
  82. package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
  83. package/templates/create/base/.golangci.yml.hbs +27 -0
  84. package/templates/create/base/AGENTS.md.hbs +21 -7
  85. package/templates/create/base/Dockerfile.hbs +42 -0
  86. package/templates/create/base/Makefile.hbs +43 -13
  87. package/templates/create/base/README.md.hbs +45 -8
  88. package/templates/create/base/cmd/api/main.go.hbs +17 -130
  89. package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
  90. package/templates/create/base/go.mod.hbs +4 -4
  91. package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
  92. package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
  93. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
  94. package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
  95. package/templates/create/base/redocly.yaml.hbs +21 -0
  96. package/templates/create/features/docs/architecture.md.hbs +32 -11
  97. package/templates/create/features/docs/openapi.yaml.hbs +0 -4
  98. package/templates/create/features/docs/patterns.md.hbs +82 -8
  99. package/templates/create/features/docs/techstack.md.hbs +8 -3
  100. package/templates/generate/module/dto.go.hbs +8 -1
  101. package/templates/generate/module/errors.go.hbs +5 -0
  102. package/templates/generate/module/field-column.down.sql.hbs +2 -0
  103. package/templates/generate/module/field-column.up.sql.hbs +15 -0
  104. package/templates/generate/module/handler_test.go.hbs +8 -1
  105. package/templates/generate/module/migration.down.sql.hbs +3 -1
  106. package/templates/generate/module/migration.up.sql.hbs +7 -2
  107. package/templates/generate/module/minimal/dto.go.hbs +3 -1
  108. package/templates/generate/module/model/model.go.hbs +10 -1
  109. package/templates/generate/module/permission.up.sql.hbs +3 -1
  110. package/templates/generate/module/repository.go.hbs +60 -6
  111. package/templates/generate/module/repository_test.go.hbs +30 -0
  112. package/templates/generate/module/service.go.hbs +10 -1
  113. package/templates/generate/module/service_test.go.hbs +45 -0
  114. package/dist/commands/remove.js +0 -88
  115. package/scripts/smoke-test.mjs +0 -2058
  116. package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
  117. package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
  118. package/tests/integration/default-module.test.mjs +0 -46
  119. package/tests/integration/generator-naming.test.mjs +0 -81
  120. package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
  121. package/tests/integration/legacy-method-compat.test.mjs +0 -222
  122. package/tests/integration/remove-module.test.mjs +0 -58
  123. package/tests/unit/naming.test.mjs +0 -94
  124. package/tests/unit/smoke-isolation.test.mjs +0 -35
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pranob Onphaeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  A CLI that scaffolds a Gin + GORM + PostgreSQL Go backend, then keeps
4
4
  generating consistent domain modules into that project as it grows — the Go
5
- counterpart to [nest-scaffold](../nest-scaffold).
5
+ counterpart to nest-scaffold.
6
6
 
7
- You don't hand-wire a new domain into `cmd/api/main.go`, write the
7
+ You don't hand-wire a new domain into `cmd/api/wiring.go`, write the
8
8
  handler/service/repository boilerplate, or decide error-handling conventions
9
9
  each time — the CLI does that, and every module it generates follows the
10
10
  same shape as the last one.
@@ -52,6 +52,11 @@ go-scaffold generate method orders approve --type patch
52
52
 
53
53
  ## Commands
54
54
 
55
+ Every `add` command shows what it's about to do and asks before writing;
56
+ `-y/--yes` skips that (and `--defaults` implies it) for CI and scripts.
57
+ Running `go-scaffold` with no arguments picks the command from a menu.
58
+
59
+
55
60
  ### `create <name>` — scaffold a new project
56
61
 
57
62
  ```bash
@@ -61,19 +66,20 @@ go-scaffold create my-api --defaults --no-docker --api-prefix beta
61
66
  ```
62
67
 
63
68
  Produces a **bare skeleton only** — `cmd/api`, the shared platform packages
64
- (config/apperror/dberr/httpx/id/middleware/pagination), Docker+Postgres,
69
+ (config/apperror/dberr/httpx/id/middleware/pagination/tx), Docker+Postgres,
65
70
  migrations folder, and the standards docs (`docs/architect/`, `AGENTS.md`,
66
71
  `CLAUDE.md`, `.claude/skills/go-scaffold/`). No domain modules — add those
67
72
  with `generate module`.
68
73
 
69
74
  | Option | Effect |
70
75
  |---|---|
71
- | `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, prefix `v1`) |
76
+ | `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, no route prefix) |
72
77
  | `--no-docker` | Skip `docker-compose.yml` (with `--defaults`) |
73
78
  | `--no-openapi-docs` | Skip `docs/openapi.yaml` (with `--defaults`) |
74
- | `--api-prefix <prefix>` | URL prefix every route is grouped under (with `--defaults`; default `v1`, `""` for none, `/`-separated segments like `api/v1` are fine) |
79
+ | `--observability` | Prometheus `/metrics` + OpenTelemetry tracing (with `--defaults`; off by default `add observability` does the same later) |
80
+ | `--api-prefix <prefix>` | URL prefix every route is grouped under — opt in with e.g. `v1` or `api/v1`; omit it for none |
75
81
 
76
- Without `--defaults`, an interactive wizard asks the same three questions.
82
+ Without `--defaults`, an interactive wizard asks the same four questions.
77
83
  The prefix is a single project-wide choice made once at `create` time —
78
84
  there's no per-domain versioning (a domain that needs a real breaking change
79
85
  gets a new domain package or a new DTO field, not a duplicated model pointed
@@ -87,17 +93,21 @@ directory layout if missing).
87
93
  ### `generate module <name>` (alias `m`) — add a domain module
88
94
 
89
95
  ```bash
90
- go-scaffold generate module orders # safe minimal module (default)
91
- go-scaffold generate module orders --full # opt-in CRUD skeleton
96
+ go-scaffold generate module orders # asks for the shape (and auth, if installed)
97
+ go-scaffold generate module orders --full # opt-in CRUD skeleton, no prompt
98
+ go-scaffold generate module orders --defaults # safe minimal module, no prompt (CI/scripting)
92
99
  ```
93
100
 
101
+ Anything you don't pass as a flag is asked for; `--defaults` takes the
102
+ documented defaults (minimal, no auth) and asks nothing.
103
+
94
104
  `--full` scaffolds:
95
105
 
96
106
  ```text
97
107
  internal/app/order/
98
108
  ├── model/model.go # domain model + GORM table (id/created_at/updated_at — add real fields yourself; a folder so multi-table domains can add more files)
99
109
  ├── dto.go # request/response structs (empty stubs — add real fields yourself)
100
- ├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES
110
+ ├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES / ORDER_STALE
101
111
  ├── repository.go # GORM data access
102
112
  ├── service.go # business logic + repository interface (mockable)
103
113
  ├── handler.go # Gin routes, registered under the project's API prefix
@@ -115,11 +125,13 @@ surface, or you'd rather add endpoints one at a time.
115
125
 
116
126
  Both modes also:
117
127
 
118
- - Register the module in `cmd/api/main.go` (via marker comments — see
128
+ - Register the module in `cmd/api/wiring.go` (via marker comments — see
119
129
  `// go-scaffold:*` in that file) — full wires an actual route, minimal
120
130
  wires an empty route group
121
- - Add the model to the `AutoMigrate(...)` call
122
- - Append `migrations/<seq>_create_<plural>.{up,down}.sql`
131
+ - Create the module's own Postgres schema (`<module>_svc`, e.g. `order_svc`)
132
+ and add the model to the `AutoMigrate(...)` call
133
+ - Append `migrations/<timestamp>_create_<plural>.{up,down}.sql`, which creates that
134
+ same schema for `AUTO_MIGRATE=false`/production
123
135
 
124
136
  What it does **not** do: invent your fields or wire foreign keys between
125
137
  domains — see `docs/architect/patterns.md` in the generated project for the
@@ -192,26 +204,75 @@ Creates timestamped `migrations/<version>_<name>.up.sql` and `.down.sql` TODO
192
204
  stubs. The CLI reserves the names; you own the SQL and should apply it with
193
205
  `make migrate-up` (or `migrate -path migrations -database "$DB_DSN" up`).
194
206
 
195
- ### `add worker` — add Redis-backed background work
207
+ ### `add worker` — add background job processing
196
208
 
197
209
  ```bash
198
- go-scaffold add worker
210
+ go-scaffold add worker # asks where jobs should live
211
+ go-scaffold add worker --queue postgres # River (default)
212
+ go-scaffold add worker --queue redis # Asynq
213
+ go-scaffold add worker --defaults # no prompt, Postgres
199
214
  ```
200
215
 
201
- Adds Redis cache/queue support, async email delivery, and `cmd/worker`. It also
202
- makes `/readyz` report unavailable when Redis is down. Run `go mod tidy` and
203
- provide `REDIS_URL` before starting the API or worker.
216
+ Adds `internal/platform/queue` (a backend-neutral contract plus one adapter),
217
+ async email delivery, and `cmd/worker`.
218
+
219
+ | | `--queue postgres` (River) | `--queue redis` (Asynq) |
220
+ |---|---|---|
221
+ | Extra service to run | none | Redis |
222
+ | Needed by `add auth` | no | no — `add auth --store` decides that separately |
223
+ | Enqueue joins your DB transaction | yes | **no** — needs an outbox |
224
+ | Throughput | thousands/sec | tens of thousands/sec |
225
+ | Inspect pending jobs | plain SQL | asynqmon |
226
+
227
+ The default is Postgres because a job enqueued inside `tx.Do` is then only
228
+ delivered if that transaction commits — no more welcome emails for signups
229
+ that rolled back. Run `make river-migrate` once per database to create
230
+ River's tables, then `make worker`.
231
+
232
+ Application code only ever sees `queue.Job`, `queue.Enqueuer` and
233
+ `queue.Handler` — no backend package appears outside its own adapter file, so
234
+ switching later means writing one adapter, not touching every module that
235
+ enqueues something.
236
+
237
+ ```go
238
+ type WelcomeEmail struct{ To string `json:"to"` }
239
+ func (WelcomeEmail) Kind() string { return "email:welcome" }
240
+
241
+ // cmd/api — the job is discarded with the transaction if this fails
242
+ tx.Do(ctx, db, func(ctx context.Context) error {
243
+ if err := repo.Create(ctx, u); err != nil { return err }
244
+ return jobs.Enqueue(ctx, WelcomeEmail{To: u.Email}, nil)
245
+ })
246
+ ```
204
247
 
205
248
  ### `add auth` — add email/password authentication
206
249
 
207
250
  ```bash
208
- go-scaffold add auth
251
+ go-scaffold add auth # asks where tokens should live, then confirms
252
+ go-scaffold add auth --store postgres # tokens in Postgres, no extra service
253
+ go-scaffold add auth --store redis # tokens in Redis, exact across replicas
254
+ go-scaffold add auth --defaults # Postgres, no prompt at all (CI/scripting)
209
255
  ```
210
256
 
211
- Requires `add worker`. Adds JWT access tokens, Redis-backed refresh-token
212
- rotation, registration/login/logout, password reset, email verification, and
213
- Google OAuth routes. Apply the generated migrations; `AUTO_MIGRATE=true` is
214
- convenient in development, while production should use `migrate up`.
257
+ Adds JWT access tokens, refresh-token rotation with reuse detection,
258
+ registration/login/logout, password reset, email verification, failed-login
259
+ lockout, and Google OAuth routes. Apply the generated migrations;
260
+ `AUTO_MIGRATE=true` is convenient in development, while production should use
261
+ `migrate up`.
262
+
263
+ No prerequisites. On a project with no worker the verification and reset mail
264
+ is sent inline, and `add worker` later moves it onto the queue for you — the
265
+ two endpoints that send mail block on SMTP until you do.
266
+
267
+ | `--store` | Tokens and rate-limit counters | Extra service |
268
+ |---|---|---|
269
+ | `postgres` (default) | `user_svc.auth_tokens`, counters in-process | none |
270
+ | `redis` | Redis | Redis |
271
+
272
+ The rate limiter follows the store rather than being chosen separately, because
273
+ "I want this exact across replicas" is one decision. With `postgres` the per-IP
274
+ budget is per-replica; the failed-login lockout is in Postgres either way, since
275
+ that one can't be approximate.
215
276
 
216
277
  ### `add rbac` — add roles and permissions
217
278
 
@@ -225,31 +286,51 @@ middleware, and role assignment. Its migration seeds the default roles and
225
286
  permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
226
287
  not run SQL seed statements.
227
288
 
228
- ### Observability at project creation
289
+ ### `add observability` add metrics + tracing
229
290
 
230
291
  ```bash
231
- go-scaffold create my-api --defaults --observability
292
+ go-scaffold add observability # on an existing project
293
+ go-scaffold create my-api --defaults --observability # or at creation time
232
294
  ```
233
295
 
234
- Opt-in observability adds Prometheus metrics at `/metrics` and OpenTelemetry
235
- tracing. Tracing is disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured.
296
+ Adds Prometheus metrics at `/metrics` and OpenTelemetry tracing for Gin +
297
+ GORM, patched into `cmd/api/wiring.go` and `internal/platform/database` the same
298
+ way `add worker`/`add auth`/`add rbac` patch an existing project. Tracing is
299
+ disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured; `/metrics` works
300
+ either way. `create --observability` is exactly this command run right after
301
+ scaffolding — the two produce the same project.
236
302
 
237
- ### `remove module <name>` (alias `rm m`) — drop a domain
303
+ ### `undo module <name>` (alias `undo m`) — take back a `generate module`
238
304
 
239
305
  ```bash
240
- go-scaffold remove module orders # confirms first
241
- go-scaffold rm m orders --yes # skip the confirm
306
+ go-scaffold undo module orders # confirms first
307
+ go-scaffold undo m orders --yes # skip the confirm
242
308
  ```
243
309
 
244
- The inverse of `generate module`: deletes `internal/app/<name>/` and reverses
245
- the import/AutoMigrate/route in `main.go`, paths/schemas in `docs/openapi.yaml`,
246
- and the per-module docs folder. **Existing migrations are preserved** because
247
- production may already have recorded those immutable versions. The table/data
248
- are also untouched; create a new `generate migration drop_<table>` migration
249
- when removal is intentional. Restores the `_ = api` placeholder if it was the
250
- last module, so the project still builds. Use this instead of hand-deleting
251
- the folder a partial hand-delete leaves stale wiring that duplicates on the
252
- next `generate module` (which would panic gin at startup).
310
+ The inverse of `generate module`, for the case it's actually the inverse of:
311
+ a module you didn't mean to generate — a typo'd name, a domain you decided
312
+ against. It deletes `internal/app/<name>/`, the per-module docs folder, **and
313
+ the module's migration files**, and reverses the import/AutoMigrate/route in
314
+ `main.go` plus the paths/schemas in `docs/openapi.yaml`. Restores the
315
+ `_ = api` placeholder if it was the last module, so the project still builds.
316
+
317
+ Deleting the migrations is the point. `migrations/embed.go` is a `//go:embed
318
+ *`, so a typo'd `create_oders` left behind once ran on every database created
319
+ from then on. That's only safe while those files exist nowhere but your
320
+ working tree, so `undo` proves it first and refuses loudly otherwise:
321
+
322
+ - **any of them is tracked by git** — it may already have been pulled or
323
+ deployed somewhere, so nothing is deleted. Retire that domain the explicit
324
+ way instead: `go-scaffold generate migration drop_<name>`.
325
+ - **your database is already at or past that version** (read via the `migrate`
326
+ CLI when it's on `PATH` and a DSN is configured) — deleting the files would
327
+ strand `schema_migrations` at a version with no migration behind it. Run
328
+ `migrate ... down` first, then try again.
329
+
330
+ The table itself is never dropped either way — `undo` only reverses what the
331
+ CLI wrote. Prefer it to hand-deleting the folder: it also un-wires `main.go`,
332
+ `.golangci.yml` and the OpenAPI index, and it refuses when another domain
333
+ still imports this one rather than leaving you an un-compilable project.
253
334
 
254
335
  ## Why no per-domain versioning
255
336
 
@@ -265,7 +346,7 @@ and `float64` in the other for the *same* column, converging it to
265
346
  data with different, silently incompatible interpretations.
266
347
 
267
348
  Instead, every route in a project is grouped under a single project-wide
268
- `--api-prefix` (default `v1`) chosen once at `create` time. A domain that
349
+ `--api-prefix` chosen once at `create` time — opt-in, no prefix unless you ask. A domain that
269
350
  needs a real breaking change gets a new domain package, or a new field on
270
351
  the existing DTO — not a duplicated model pointed at a table it can drift
271
352
  out of sync with.
@@ -273,10 +354,10 @@ out of sync with.
273
354
  ## Project structure produced by `create`
274
355
 
275
356
  ```text
276
- cmd/api/main.go
357
+ cmd/api/wiring.go
277
358
  internal/
278
359
  ├── platform/database/
279
- ├── shared/{config,apperror,dberr,httpx,id,middleware,pagination}/
360
+ ├── shared/{config,apperror,dberr,httpx,id,middleware,pagination,tx}/
280
361
  └── app/ # empty until you `generate module`
281
362
  docs/
282
363
  ├── architect/{architecture,patterns,techstack}.md
@@ -284,8 +365,13 @@ docs/
284
365
  migrations/
285
366
  .github/workflows/ci.yml # build, vet, gofmt check, golangci-lint, go test (with a Postgres service)
286
367
  Makefile
368
+ .env.example
369
+ .gitignore
287
370
  .golangci.yml
371
+ redocly.yaml # if openapi docs enabled
372
+ docker-compose.yml # if Docker enabled
288
373
  .vscode/settings.json
374
+ README.md
289
375
  AGENTS.md
290
376
  CLAUDE.md
291
377
  .claude/skills/go-scaffold/SKILL.md
@@ -294,11 +380,14 @@ go-scaffold.config.json
294
380
 
295
381
  ## Supported stack
296
382
 
383
+ Pinned in the generated `go.mod` — this table mirrors
384
+ `templates/create/base/go.mod.hbs`, which is the source of truth.
385
+
297
386
  | Package | Version |
298
387
  |---|---|
299
- | Gin | v1.10.0 |
300
- | GORM + postgres driver | v1.25.12 / v1.5.9 |
301
- | validator/v10 | v10.20.0 |
388
+ | Gin | v1.10.1 |
389
+ | GORM + postgres driver | v1.31.2 / v1.6.2 |
390
+ | validator/v10 | v10.30.3 |
302
391
  | google/uuid | v1.6.0 |
303
392
 
304
393
  ## License
@@ -11,8 +11,13 @@ const config_1 = require("../utils/config");
11
11
  const template_renderer_1 = require("../utils/template-renderer");
12
12
  const auth_manifest_1 = require("../templates/auth-manifest");
13
13
  const auth_patcher_1 = require("../utils/auth-patcher");
14
+ const platform_patcher_1 = require("../utils/platform-patcher");
15
+ const worker_manifest_1 = require("../templates/worker-manifest");
16
+ const golangci_patcher_1 = require("../utils/golangci-patcher");
14
17
  const migrations_1 = require("../utils/migrations");
15
18
  const openapi_patcher_1 = require("../utils/openapi-patcher");
19
+ const gocheck_1 = require("../utils/gocheck");
20
+ const gomod_patcher_1 = require("../utils/gomod-patcher");
16
21
  // URL (relative to the api prefix) -> docs file (relative to docs/) for every
17
22
  // route `add auth` registers — kept next to AUTH_FILES's route list so the
18
23
  // two are easy to eyeball together when a route changes.
@@ -36,16 +41,29 @@ const AUTH_OPENAPI_PATHS = [
36
41
  // (no roles/permissions) — that's a separate opt-in on top of this, since
37
42
  // most projects need "is this caller logged in" long before they need "can
38
43
  // this caller do X".
39
- async function addAuth(projectDir = process.cwd()) {
44
+ async function addAuth(store = "postgres", projectDir = process.cwd()) {
40
45
  const config = (0, config_1.readConfig)(projectDir);
41
- if (!config.features.worker) {
42
- throw new Error("`go-scaffold add auth` requires `go-scaffold add worker` first the refresh token store needs Redis");
43
- }
46
+ // No longer a prerequisite. Without a worker the verification and reset mail
47
+ // goes out inline instead of through a queuea real trade (those two
48
+ // endpoints then block on SMTP), but not one worth forcing a second binary
49
+ // and a queue-backend decision on someone who only wanted login.
50
+ const worker = config.features.worker ?? false;
44
51
  const userDir = path_1.default.join(projectDir, "internal", "app", "user");
45
52
  if (fs_extra_1.default.existsSync(userDir)) {
46
53
  throw new Error(`${userDir} already exists — auth looks like it's already been added`);
47
54
  }
48
- await (0, template_renderer_1.applyTemplateEntries)(projectDir, auth_manifest_1.AUTH_FILES, { goModule: config.goModule });
55
+ const parsedBefore = (0, gocheck_1.parseChecks)(projectDir);
56
+ // Only `--store redis` needs Redis. With `--store postgres` (the default)
57
+ // auth adds no service at all — which is the whole point of the option.
58
+ if (store === "redis")
59
+ await ensureRedis(projectDir, config.goModule);
60
+ // the SMTP client, and the config it reads, normally arrive with `add worker`
61
+ if (!worker) {
62
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, worker_manifest_1.MAIL_CLIENT_ONLY, { goModule: config.goModule });
63
+ (0, platform_patcher_1.patchConfigForSMTP)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
64
+ patchEnvExampleForSMTP(path_1.default.join(projectDir, ".env.example"));
65
+ }
66
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), { goModule: config.goModule });
49
67
  const migrationsDir = path_1.default.join(projectDir, "migrations");
50
68
  fs_extra_1.default.ensureDirSync(migrationsDir);
51
69
  const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
@@ -62,8 +80,35 @@ async function addAuth(projectDir = process.cwd()) {
62
80
  { template: "add/auth/migrations/create_identities.up.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.up.sql`) },
63
81
  { template: "add/auth/migrations/create_identities.down.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.down.sql`) },
64
82
  ], {});
83
+ // the failed-attempt counter is not a store choice — lockout has to survive a
84
+ // deploy and mean the same thing on every replica whichever store holds tokens
85
+ const throttleVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
86
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
87
+ { template: "add/auth/migrations/create_login_throttle.up.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.up.sql`) },
88
+ { template: "add/auth/migrations/create_login_throttle.down.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.down.sql`) },
89
+ ], {});
90
+ if (store === "postgres") {
91
+ const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
92
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
93
+ { template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
94
+ { template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
95
+ ], {});
96
+ }
97
+ (0, golangci_patcher_1.patchGolangciForModule)(path_1.default.join(projectDir, ".golangci.yml"), config.goModule, "user");
65
98
  (0, auth_patcher_1.patchConfigForAuth)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
66
- (0, auth_patcher_1.patchMainGoForAuth)(path_1.default.join(projectDir, "cmd", "api", "main.go"), config.goModule);
99
+ (0, auth_patcher_1.patchMainGoForAuth)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), {
100
+ goModule: config.goModule,
101
+ queueBackend: config.features.queue ?? "asynq",
102
+ store,
103
+ worker,
104
+ });
105
+ (0, gomod_patcher_1.patchGoModRequires)(path_1.default.join(projectDir, "go.mod"), [
106
+ "github.com/golang-jwt/jwt/v5 v5.3.1",
107
+ "golang.org/x/crypto v0.52.0",
108
+ "golang.org/x/oauth2 v0.36.0",
109
+ // go-redis only when something in this project actually constructs a client
110
+ ...(store === "redis" ? ["github.com/redis/go-redis/v9 v9.22.0"] : []),
111
+ ]);
67
112
  patchEnvExample(path_1.default.join(projectDir, ".env.example"));
68
113
  patchMakefile(path_1.default.join(projectDir, "Makefile"));
69
114
  let docsMessage = "";
@@ -81,10 +126,20 @@ async function addAuth(projectDir = process.cwd()) {
81
126
  docsMessage = "\ndocs: docs/auth/*.yaml, wired into docs/openapi.yaml";
82
127
  }
83
128
  (0, template_renderer_1.gofmtTree)(projectDir);
84
- (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, auth: true } });
129
+ // parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
130
+ // printed below, so `go vet` can't be the gate here.
131
+ (0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
132
+ (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, auth: true, authStore: store } });
85
133
  console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
86
- console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password}, " +
87
- "GET /auth/google/{login,callback}, and GET /users/me in cmd/api/main.go" +
134
+ console.log(worker
135
+ ? "verification + password-reset mail goes through the queue"
136
+ : "verification + password-reset mail is sent inline (no worker) — run `add worker` later to move it onto the queue");
137
+ console.log(store === "postgres"
138
+ ? "tokens + rate-limit counters: Postgres (user_svc.auth_tokens) and in-process — no Redis"
139
+ : "tokens + rate-limit counters: Redis");
140
+ console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password,verify-email}, " +
141
+ "GET /auth/google/{login,callback}, GET /users/me, and " +
142
+ "POST /users/me/{resend-verification,logout-all} in cmd/api/wiring.go" +
88
143
  docsMessage);
89
144
  console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations (AUTO_MIGRATE=true picks them up automatically in dev)\n" +
90
145
  "seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
@@ -100,8 +155,14 @@ function patchMakefile(makefilePath) {
100
155
  "# environment, not .env, so a real secret never sits in a checked-in file.\n" +
101
156
  "# --fixtures adds throwaway dev sample users, never use it outside dev.\n" +
102
157
  "seed:\n" +
103
- "\t@[ -f .env ] && export $$(grep -v '^#' .env | sed -E 's/[[:space:]]+#.*$//' | xargs); go run ./cmd/seed $(ARGS)\n";
104
- content = content.replace(/\nbuild:/, `${target}\nbuild:`);
158
+ // `$$` throughout: make expands a single `$` as a variable reference, so
159
+ // `$//` became `//` and left sed an unterminated s/// expression — the
160
+ // recipe then failed, `.env` never loaded, and a bare `export` dumped the
161
+ // whole environment. Matches every target in Makefile.hbs verbatim.
162
+ "\t@set -a; [ -f $(ENV_FILE) ] && . ./$(ENV_FILE); set +a; go run ./cmd/seed $(ARGS)\n";
163
+ // Function replacer — target contains a literal "$$", see worker.ts's
164
+ // patchMakefile for why a string replacement would silently mangle it.
165
+ content = content.replace(/\nbuild:/, () => `${target}\nbuild:`);
105
166
  fs_extra_1.default.writeFileSync(makefilePath, content);
106
167
  }
107
168
  function patchEnvExample(envExamplePath) {
@@ -117,6 +178,12 @@ function patchEnvExample(envExamplePath) {
117
178
  "JWT_ACCESS_TTL_MIN=15\n" +
118
179
  "JWT_REFRESH_TTL_MIN=43200\n" +
119
180
  "COOKIE_SECURE=false\n" +
181
+ "# strict | lax | none — the refresh cookie's SameSite. Keep strict while the\n" +
182
+ "# frontend is the same site as this API (localhost:3000 -> localhost:8080 is,\n" +
183
+ "# and so is app.example.com -> api.example.com). A frontend on a different\n" +
184
+ "# site entirely needs none, together with COOKIE_SECURE=true, or the browser\n" +
185
+ "# never sends the cookie to /auth/refresh and sessions die at every expiry.\n" +
186
+ "COOKIE_SAMESITE=strict\n" +
120
187
  "\nPASSWORD_RESET_TTL_MIN=30\n" +
121
188
  "PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
122
189
  "\nEMAIL_VERIFY_TTL_MIN=1440\n" +
@@ -127,3 +194,41 @@ function patchEnvExample(envExamplePath) {
127
194
  "GOOGLE_REDIRECT_URL=\n";
128
195
  fs_extra_1.default.writeFileSync(envExamplePath, content);
129
196
  }
197
+ // ensureRedis adds internal/platform/cache + its config/env/main.go wiring
198
+ // when the project doesn't have Redis yet — which is the normal case once
199
+ // the queue lives in Postgres. Idempotent: a project whose queue is Redis
200
+ // already has all of this and nothing happens.
201
+ async function ensureRedis(projectDir, goModule) {
202
+ const cacheGo = path_1.default.join(projectDir, "internal", "platform", "cache", "redis.go");
203
+ if (fs_extra_1.default.existsSync(cacheGo))
204
+ return;
205
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [{ template: "add/worker/internal/platform/cache/redis.go.hbs", output: "internal/platform/cache/redis.go" }], { goModule });
206
+ (0, platform_patcher_1.patchConfigForRedis)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
207
+ (0, platform_patcher_1.patchMainGoForWorker)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), goModule);
208
+ (0, platform_patcher_1.patchComposeForRedis)(path_1.default.join(projectDir, "docker-compose.yml"));
209
+ (0, platform_patcher_1.patchCiForRedis)(path_1.default.join(projectDir, ".github", "workflows", "ci.yml"));
210
+ const envExamplePath = path_1.default.join(projectDir, ".env.example");
211
+ if (fs_extra_1.default.existsSync(envExamplePath)) {
212
+ const content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
213
+ if (!content.includes("REDIS_URL")) {
214
+ fs_extra_1.default.writeFileSync(envExamplePath, content.replace(/\n?$/, "\n") + "\n# refresh-token store\nREDIS_URL=redis://localhost:6379/0\n");
215
+ }
216
+ }
217
+ }
218
+ // patchEnvExampleForSMTP mirrors `add worker`'s own SMTP block, for the case
219
+ // where auth installed the mail client without a worker. Same append-once
220
+ // shape as every other env patcher.
221
+ function patchEnvExampleForSMTP(envExamplePath) {
222
+ if (!fs_extra_1.default.existsSync(envExamplePath))
223
+ return;
224
+ const content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
225
+ if (content.includes("SMTP_HOST"))
226
+ return;
227
+ fs_extra_1.default.writeFileSync(envExamplePath, content.replace(/\n?$/, "\n") +
228
+ "\n# leave SMTP_HOST unset to log emails instead of sending them (dev default)\n" +
229
+ "SMTP_HOST=\n" +
230
+ "SMTP_PORT=587\n" +
231
+ "SMTP_USERNAME=\n" +
232
+ "SMTP_PASSWORD=\n" +
233
+ "SMTP_FROM=no-reply@example.local\n");
234
+ }
@@ -13,6 +13,7 @@ const config_1 = require("../utils/config");
13
13
  const naming_1 = require("../utils/naming");
14
14
  const create_wizard_1 = require("../prompts/create-wizard");
15
15
  const version_1 = require("../utils/version");
16
+ const observability_1 = require("./observability");
16
17
  async function createProject(rawName, opts) {
17
18
  const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
18
19
  if (!trimmed)
@@ -28,7 +29,11 @@ async function createProject(rawName, opts) {
28
29
  let apiPrefix;
29
30
  if (opts.defaults) {
30
31
  features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true, observability: opts.observability ?? false };
31
- apiPrefix = (0, naming_1.normalizeApiPrefix)(opts.apiPrefix ?? "v1");
32
+ // "" to match what the wizard's Enter now gives. --defaults means "don't
33
+ // ask me", not "give me something I didn't ask for", and a prefix is a
34
+ // project-wide decision you cannot change later without rewriting routes
35
+ // and every OpenAPI path.
36
+ apiPrefix = (0, naming_1.normalizeApiPrefix)(opts.apiPrefix ?? "");
32
37
  const check = (0, naming_1.validateApiPrefix)(apiPrefix);
33
38
  if (check !== true)
34
39
  throw new Error(check);
@@ -47,6 +52,13 @@ async function createProject(rawName, opts) {
47
52
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
48
53
  (0, template_renderer_1.gofmtTree)(projectDir);
49
54
  (0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features, scaffoldVersion: (0, version_1.cliVersion)() });
55
+ // Composed the same way a user would do it by hand — `create` always
56
+ // renders the plain base, then this layers the same patches `add
57
+ // observability` would apply on an existing project, so the two paths
58
+ // produce identical output.
59
+ if (features.observability) {
60
+ await (0, observability_1.addObservability)(projectDir, { silent: true });
61
+ }
50
62
  console.log(picocolors_1.default.green(`\ncreated ${projectName}/`));
51
63
  console.log(`\ncd ${projectName}`);
52
64
  if (features.docker)
@@ -14,6 +14,7 @@ const template_renderer_1 = require("../utils/template-renderer");
14
14
  const module_manifest_1 = require("../templates/module-manifest");
15
15
  const main_patcher_1 = require("../utils/main-patcher");
16
16
  const openapi_patcher_1 = require("../utils/openapi-patcher");
17
+ const golangci_patcher_1 = require("../utils/golangci-patcher");
17
18
  const migrations_1 = require("../utils/migrations");
18
19
  const gocheck_1 = require("../utils/gocheck");
19
20
  const generate_wizard_1 = require("../prompts/generate-wizard");
@@ -43,6 +44,11 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
43
44
  if (fs_extra_1.default.existsSync(moduleDir) && fs_extra_1.default.readdirSync(moduleDir).length > 0) {
44
45
  throw new Error(`${moduleDir} already exists — pick a different name or delete it first`);
45
46
  }
47
+ // Every marker this command patches has to be there before the first file
48
+ // is written — a failure after the module folder and its migration exist is
49
+ // a dead end, since the retry trips the "already exists" guard above.
50
+ const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "wiring.go");
51
+ (0, main_patcher_1.assertMainGoPatchable)(mainGoPath);
46
52
  // snapshot before writing anything, so assertNoDrift below can tell "we broke
47
53
  // it" from "it was already broken"
48
54
  const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
@@ -63,19 +69,22 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
63
69
  // skip if a create_<plural> migration already exists — re-running after
64
70
  // only the module folder was deleted shouldn't leave a duplicate migration.
65
71
  const migrationsDir = path_1.default.join(projectDir, "migrations");
72
+ const slug = (0, naming_1.migrationSlug)(naming);
66
73
  const migrationExists = fs_extra_1.default.existsSync(migrationsDir) &&
67
- fs_extra_1.default.readdirSync(migrationsDir).some((f) => f.endsWith(`_create_${naming.plural}.up.sql`));
74
+ fs_extra_1.default
75
+ .readdirSync(migrationsDir)
76
+ .some((f) => (0, naming_1.migrationSlugAliases)(naming).some((alias) => f.endsWith(`_create_${alias}.up.sql`)));
68
77
  let seq = "";
69
78
  if (!migrationExists) {
70
79
  seq = (0, migrations_1.newMigrationVersion)(migrationsDir);
71
80
  const migrationEntries = [
72
81
  {
73
82
  template: "generate/module/migration.up.sql.hbs",
74
- output: path_1.default.join("migrations", `${seq}_create_${naming.plural}.up.sql`),
83
+ output: path_1.default.join("migrations", `${seq}_create_${slug}.up.sql`),
75
84
  },
76
85
  {
77
86
  template: "generate/module/migration.down.sql.hbs",
78
- output: path_1.default.join("migrations", `${seq}_create_${naming.plural}.down.sql`),
87
+ output: path_1.default.join("migrations", `${seq}_create_${slug}.down.sql`),
79
88
  },
80
89
  ];
81
90
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, migrationEntries, context);
@@ -90,21 +99,22 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
90
99
  const permissionEntries = [
91
100
  {
92
101
  template: "generate/module/permission.up.sql.hbs",
93
- output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.up.sql`),
102
+ output: path_1.default.join("migrations", `${permissionSeq}_add_${slug}_permission.up.sql`),
94
103
  },
95
104
  {
96
105
  template: "generate/module/permission.down.sql.hbs",
97
- output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.down.sql`),
106
+ output: path_1.default.join("migrations", `${permissionSeq}_add_${slug}_permission.down.sql`),
98
107
  },
99
108
  ];
100
109
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, permissionEntries, context);
101
110
  }
102
- const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "main.go");
111
+ (0, golangci_patcher_1.patchGolangciForModule)(path_1.default.join(projectDir, ".golangci.yml"), config.goModule, naming.pkg);
103
112
  (0, main_patcher_1.patchMainGo)(mainGoPath, {
104
113
  goModule: config.goModule,
105
114
  modulePath,
106
115
  pkg: naming.pkg,
107
116
  pascalName: naming.pascalName,
117
+ schemaName: naming.schemaName,
108
118
  auth: opts.auth,
109
119
  permission: opts.permission,
110
120
  });
@@ -125,10 +135,10 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
125
135
  const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
126
136
  console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
127
137
  if (opts.full) {
128
- console.log(`registered route ${routePath} in cmd/api/main.go`);
138
+ console.log(`registered route ${routePath} in cmd/api/wiring.go`);
129
139
  }
130
140
  else {
131
- console.log(`registered empty route group ${routePath} in cmd/api/main.go — ` +
141
+ console.log(`registered empty route group ${routePath} in cmd/api/wiring.go — ` +
132
142
  `add endpoints with \`go-scaffold generate method ${naming.pkg} <name> --type ...\``);
133
143
  }
134
144
  if (opts.permission) {
@@ -141,13 +151,13 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
141
151
  console.log(picocolors_1.default.yellow(`note: this project has auth installed, but ${routePath} is PUBLIC — re-run with --auth (and --permission <code> if you also have rbac) to require login`));
142
152
  }
143
153
  if (seq) {
144
- console.log(`migration: migrations/${seq}_create_${naming.plural}.{up,down}.sql`);
154
+ console.log(`migration: migrations/${seq}_create_${slug}.{up,down}.sql`);
145
155
  }
146
156
  else {
147
- console.log(`migration: reused existing migrations/*_create_${naming.plural}.{up,down}.sql`);
157
+ console.log(`migration: reused existing migrations/*_create_${slug}.{up,down}.sql`);
148
158
  }
149
159
  if (permissionSeq) {
150
- console.log(`migration: migrations/${permissionSeq}_add_${naming.plural}_permission.{up,down}.sql (seeds the "${opts.permission}" permission — grant it to a role via PATCH /roles/:code/permissions)`);
160
+ console.log(`migration: migrations/${permissionSeq}_add_${slug}_permission.{up,down}.sql (seeds the "${opts.permission}" permission — grant it to a role via PATCH /roles/:code/permissions)`);
151
161
  }
152
162
  if (docsMessage)
153
163
  console.log(docsMessage);