@nakedev/go-scaffold 0.1.3 → 0.3.0
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.
- package/LICENSE +21 -0
- package/README.md +143 -50
- package/dist/commands/auth.js +116 -11
- package/dist/commands/create.js +13 -1
- package/dist/commands/generate.js +23 -12
- package/dist/commands/method.js +94 -17
- package/dist/commands/observability.js +114 -0
- package/dist/commands/rbac.js +19 -2
- package/dist/commands/undo.js +331 -0
- package/dist/commands/worker.js +92 -32
- package/dist/index.js +368 -64
- package/dist/prompts/auth-wizard.js +29 -0
- package/dist/prompts/create-wizard.js +5 -2
- package/dist/prompts/generate-wizard.js +57 -0
- package/dist/prompts/worker-wizard.js +25 -0
- package/dist/templates/auth-manifest.js +20 -3
- package/dist/templates/create-manifest.js +13 -20
- package/dist/templates/module-manifest.js +2 -0
- package/dist/templates/observability-manifest.js +24 -0
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/templates/worker-manifest.js +23 -6
- package/dist/utils/auth-patcher.js +96 -21
- package/dist/utils/config.js +58 -10
- package/dist/utils/gocheck.js +57 -5
- package/dist/utils/golangci-patcher.js +73 -0
- package/dist/utils/gomod-patcher.js +53 -0
- package/dist/utils/main-patcher.js +58 -4
- package/dist/utils/marker-patch.js +125 -3
- package/dist/utils/method-patcher.js +97 -18
- package/dist/utils/module-location.js +58 -0
- package/dist/utils/naming.js +125 -14
- package/dist/utils/observability-patcher.js +107 -0
- package/dist/utils/openapi-patcher.js +19 -1
- package/dist/utils/platform-patcher.js +98 -12
- package/dist/utils/rbac-patcher.js +60 -10
- package/dist/utils/smoke-run.js +31 -0
- package/package.json +11 -4
- package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
- package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
- package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
- package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
- package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
- package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
- package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
- package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
- package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
- package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
- package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
- package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
- package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
- package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
- package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
- package/templates/create/base/.dockerignore.hbs +13 -0
- package/templates/create/base/.env.example.hbs +18 -9
- package/templates/create/base/.github/dependabot.yml.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +29 -6
- package/templates/create/base/.golangci.yml.hbs +27 -0
- package/templates/create/base/AGENTS.md.hbs +14 -12
- package/templates/create/base/Dockerfile.hbs +42 -0
- package/templates/create/base/Makefile.hbs +52 -16
- package/templates/create/base/README.md.hbs +61 -12
- package/templates/create/base/cmd/api/main.go.hbs +17 -130
- package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
- package/templates/create/base/go.mod.hbs +4 -4
- package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
- package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
- package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
- package/templates/create/base/redocly.yaml.hbs +21 -0
- package/templates/create/features/docs/architecture.md.hbs +32 -11
- package/templates/create/features/docs/openapi.yaml.hbs +4 -9
- package/templates/create/features/docs/patterns.md.hbs +91 -14
- package/templates/create/features/docs/techstack.md.hbs +8 -3
- package/templates/generate/module/docs/item.yaml.hbs +3 -3
- package/templates/generate/module/dto.go.hbs +8 -1
- package/templates/generate/module/errors.go.hbs +5 -0
- package/templates/generate/module/field-column.down.sql.hbs +2 -0
- package/templates/generate/module/field-column.up.sql.hbs +15 -0
- package/templates/generate/module/handler.go.hbs +18 -4
- package/templates/generate/module/handler_test.go.hbs +88 -62
- package/templates/generate/module/migration.down.sql.hbs +3 -1
- package/templates/generate/module/migration.up.sql.hbs +7 -2
- package/templates/generate/module/minimal/dto.go.hbs +3 -1
- package/templates/generate/module/minimal/handler.go.hbs +8 -2
- package/templates/generate/module/minimal/handler_test.go.hbs +5 -112
- package/templates/generate/module/minimal/service_test.go.hbs +39 -16
- package/templates/generate/module/model/model.go.hbs +16 -0
- package/templates/generate/module/permission.up.sql.hbs +3 -1
- package/templates/generate/module/repository.go.hbs +60 -6
- package/templates/generate/module/repository_test.go.hbs +109 -0
- package/templates/generate/module/service.go.hbs +15 -4
- package/templates/generate/module/service_test.go.hbs +113 -17
- package/dist/commands/remove.js +0 -89
- package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
- package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
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
|
|
5
|
+
counterpart to nest-scaffold.
|
|
6
6
|
|
|
7
|
-
You don't hand-wire a new domain into `cmd/api/
|
|
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
|
|
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
|
-
| `--
|
|
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
|
|
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,25 +93,30 @@ 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 #
|
|
91
|
-
go-scaffold generate module orders --
|
|
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
|
|
|
94
|
-
|
|
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
|
+
|
|
104
|
+
`--full` scaffolds:
|
|
95
105
|
|
|
96
106
|
```text
|
|
97
|
-
internal/app/
|
|
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 #
|
|
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
|
|
104
|
-
├── service_test.go # unit test,
|
|
105
|
-
|
|
114
|
+
├── service_test.go # unit test, function-backed repository stub
|
|
115
|
+
├── handler_test.go # HTTP unit test, service stub, no DB
|
|
116
|
+
└── repository_test.go # Postgres integration test against migrated schema
|
|
106
117
|
```
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
The default minimal mode scaffolds the same `model`/`errors`/`repository` (so `generate
|
|
109
120
|
method` always has a full data-access surface to call), but `dto`/`service`/
|
|
110
121
|
`handler` start empty — no default CRUD, no routes, just the plumbing
|
|
111
122
|
(`Register()`, the `repository` interface, `wrapFindErr`) that `generate
|
|
@@ -114,11 +125,13 @@ surface, or you'd rather add endpoints one at a time.
|
|
|
114
125
|
|
|
115
126
|
Both modes also:
|
|
116
127
|
|
|
117
|
-
- Register the module in `cmd/api/
|
|
128
|
+
- Register the module in `cmd/api/wiring.go` (via marker comments — see
|
|
118
129
|
`// go-scaffold:*` in that file) — full wires an actual route, minimal
|
|
119
130
|
wires an empty route group
|
|
120
|
-
-
|
|
121
|
-
|
|
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
|
|
122
135
|
|
|
123
136
|
What it does **not** do: invent your fields or wire foreign keys between
|
|
124
137
|
domains — see `docs/architect/patterns.md` in the generated project for the
|
|
@@ -145,7 +158,7 @@ overwrites a method with the same name; picks a different one or errors.
|
|
|
145
158
|
| `--type` | Route | What's generated |
|
|
146
159
|
|---|---|---|
|
|
147
160
|
| `get --get-mode all` | `GET /<plural>/<kebab-name>` | reuses `FindAll` — TODO to add real filtering |
|
|
148
|
-
| `get --get-mode one --field <f>` | `GET /<plural>/<f>/:<f>` | a real `FindBy<F>` query added to the repository (+ its interface +
|
|
161
|
+
| `get --get-mode one --field <f>` | `GET /<plural>/<f>/:<f>` | a real `FindBy<F>` query added to the repository (+ its interface + function-backed repository test stub) |
|
|
149
162
|
| `post` | `POST /<plural>/<kebab-name>` | adds a body DTO; service is a TODO stub |
|
|
150
163
|
| `put` / `patch` | `<VERB> /<plural>/:id/<kebab-name>` | finds by id, TODO before saving (safe no-op until implemented) |
|
|
151
164
|
| `delete` | `DELETE /<plural>/:id/<kebab-name>` | TODO stub |
|
|
@@ -154,8 +167,9 @@ Business logic is always left as a `TODO`-marked stub that compiles and
|
|
|
154
167
|
returns a clean `500` rather than inventing behavior — see
|
|
155
168
|
`docs/architect/patterns.md` in the generated project.
|
|
156
169
|
|
|
157
|
-
`generate method`
|
|
158
|
-
`docs
|
|
170
|
+
When OpenAPI docs are enabled, `generate method` also creates a valid TODO stub
|
|
171
|
+
under `docs/<plural>/methods/` and wires the route into `docs/openapi.yaml`.
|
|
172
|
+
Replace its placeholder request/response schemas while implementing the TODO.
|
|
159
173
|
|
|
160
174
|
**Drift check** — `generate` type-checks the project (`go vet ./...`) before and
|
|
161
175
|
after it writes. If the project was fine beforehand and the generated code
|
|
@@ -190,26 +204,75 @@ Creates timestamped `migrations/<version>_<name>.up.sql` and `.down.sql` TODO
|
|
|
190
204
|
stubs. The CLI reserves the names; you own the SQL and should apply it with
|
|
191
205
|
`make migrate-up` (or `migrate -path migrations -database "$DB_DSN" up`).
|
|
192
206
|
|
|
193
|
-
### `add worker` — add
|
|
207
|
+
### `add worker` — add background job processing
|
|
194
208
|
|
|
195
209
|
```bash
|
|
196
|
-
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
|
|
197
214
|
```
|
|
198
215
|
|
|
199
|
-
Adds
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
```
|
|
202
247
|
|
|
203
248
|
### `add auth` — add email/password authentication
|
|
204
249
|
|
|
205
250
|
```bash
|
|
206
|
-
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)
|
|
207
255
|
```
|
|
208
256
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Google OAuth routes. Apply the generated migrations;
|
|
212
|
-
convenient in development, while production should use
|
|
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.
|
|
213
276
|
|
|
214
277
|
### `add rbac` — add roles and permissions
|
|
215
278
|
|
|
@@ -223,29 +286,51 @@ middleware, and role assignment. Its migration seeds the default roles and
|
|
|
223
286
|
permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
|
|
224
287
|
not run SQL seed statements.
|
|
225
288
|
|
|
226
|
-
###
|
|
289
|
+
### `add observability` — add metrics + tracing
|
|
227
290
|
|
|
228
291
|
```bash
|
|
229
|
-
go-scaffold
|
|
292
|
+
go-scaffold add observability # on an existing project
|
|
293
|
+
go-scaffold create my-api --defaults --observability # or at creation time
|
|
230
294
|
```
|
|
231
295
|
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
234
302
|
|
|
235
|
-
### `
|
|
303
|
+
### `undo module <name>` (alias `undo m`) — take back a `generate module`
|
|
236
304
|
|
|
237
305
|
```bash
|
|
238
|
-
go-scaffold
|
|
239
|
-
go-scaffold
|
|
306
|
+
go-scaffold undo module orders # confirms first
|
|
307
|
+
go-scaffold undo m orders --yes # skip the confirm
|
|
240
308
|
```
|
|
241
309
|
|
|
242
|
-
The inverse of `generate module
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
249
334
|
|
|
250
335
|
## Why no per-domain versioning
|
|
251
336
|
|
|
@@ -261,7 +346,7 @@ and `float64` in the other for the *same* column, converging it to
|
|
|
261
346
|
data with different, silently incompatible interpretations.
|
|
262
347
|
|
|
263
348
|
Instead, every route in a project is grouped under a single project-wide
|
|
264
|
-
`--api-prefix`
|
|
349
|
+
`--api-prefix` chosen once at `create` time — opt-in, no prefix unless you ask. A domain that
|
|
265
350
|
needs a real breaking change gets a new domain package, or a new field on
|
|
266
351
|
the existing DTO — not a duplicated model pointed at a table it can drift
|
|
267
352
|
out of sync with.
|
|
@@ -269,10 +354,10 @@ out of sync with.
|
|
|
269
354
|
## Project structure produced by `create`
|
|
270
355
|
|
|
271
356
|
```text
|
|
272
|
-
cmd/api/
|
|
357
|
+
cmd/api/wiring.go
|
|
273
358
|
internal/
|
|
274
359
|
├── platform/database/
|
|
275
|
-
├── shared/{config,apperror,dberr,httpx,id,middleware,pagination}/
|
|
360
|
+
├── shared/{config,apperror,dberr,httpx,id,middleware,pagination,tx}/
|
|
276
361
|
└── app/ # empty until you `generate module`
|
|
277
362
|
docs/
|
|
278
363
|
├── architect/{architecture,patterns,techstack}.md
|
|
@@ -280,8 +365,13 @@ docs/
|
|
|
280
365
|
migrations/
|
|
281
366
|
.github/workflows/ci.yml # build, vet, gofmt check, golangci-lint, go test (with a Postgres service)
|
|
282
367
|
Makefile
|
|
368
|
+
.env.example
|
|
369
|
+
.gitignore
|
|
283
370
|
.golangci.yml
|
|
371
|
+
redocly.yaml # if openapi docs enabled
|
|
372
|
+
docker-compose.yml # if Docker enabled
|
|
284
373
|
.vscode/settings.json
|
|
374
|
+
README.md
|
|
285
375
|
AGENTS.md
|
|
286
376
|
CLAUDE.md
|
|
287
377
|
.claude/skills/go-scaffold/SKILL.md
|
|
@@ -290,11 +380,14 @@ go-scaffold.config.json
|
|
|
290
380
|
|
|
291
381
|
## Supported stack
|
|
292
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
|
+
|
|
293
386
|
| Package | Version |
|
|
294
387
|
|---|---|
|
|
295
|
-
| Gin | v1.10.
|
|
296
|
-
| GORM + postgres driver | v1.
|
|
297
|
-
| validator/v10 | v10.
|
|
388
|
+
| Gin | v1.10.1 |
|
|
389
|
+
| GORM + postgres driver | v1.31.2 / v1.6.2 |
|
|
390
|
+
| validator/v10 | v10.30.3 |
|
|
298
391
|
| google/uuid | v1.6.0 |
|
|
299
392
|
|
|
300
393
|
## License
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// No longer a prerequisite. Without a worker the verification and reset mail
|
|
47
|
+
// goes out inline instead of through a queue — a 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
|
-
|
|
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", "
|
|
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
|
-
|
|
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(
|
|
87
|
-
"
|
|
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
|
-
|
|
104
|
-
|
|
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
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -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
|
-
|
|
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)
|