@nakedev/go-scaffold 0.4.0 → 0.5.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.
Files changed (192) hide show
  1. package/README.md +598 -306
  2. package/dist/commands/auth.js +65 -23
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/config.js +50 -0
  5. package/dist/commands/create.js +33 -2
  6. package/dist/commands/generate.js +29 -3
  7. package/dist/commands/method.js +74 -63
  8. package/dist/commands/migration.js +2 -2
  9. package/dist/commands/observability.js +4 -53
  10. package/dist/commands/rbac.js +21 -10
  11. package/dist/commands/undo.js +11 -3
  12. package/dist/commands/worker.js +15 -5
  13. package/dist/index.js +198 -59
  14. package/dist/prompts/auth-wizard.js +40 -6
  15. package/dist/prompts/create-wizard.js +42 -1
  16. package/dist/prompts/generate-wizard.js +89 -9
  17. package/dist/templates/auth-manifest.js +50 -19
  18. package/dist/templates/create-manifest.js +8 -0
  19. package/dist/templates/module-manifest.js +84 -26
  20. package/dist/templates/rbac-manifest.js +16 -11
  21. package/dist/templates/worker-manifest.js +4 -1
  22. package/dist/types.js +8 -0
  23. package/dist/utils/auth-patcher.js +124 -33
  24. package/dist/utils/config.js +167 -4
  25. package/dist/utils/docs-patcher.js +68 -0
  26. package/dist/utils/hexagonal-method-patcher.js +334 -0
  27. package/dist/utils/main-patcher.js +32 -30
  28. package/dist/utils/marker-patch.js +7 -1
  29. package/dist/utils/module-location.js +17 -11
  30. package/dist/utils/module-profile.js +32 -0
  31. package/dist/utils/platform-patcher.js +56 -7
  32. package/dist/utils/rbac-patcher.js +89 -210
  33. package/package.json +7 -2
  34. package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
  35. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  36. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  38. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  39. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  40. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  41. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  42. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  43. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  44. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  45. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  46. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  47. package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
  48. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  49. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
  50. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
  51. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
  52. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
  53. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
  54. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
  55. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
  56. package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
  57. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  58. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  59. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
  60. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
  61. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  62. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  63. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  64. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
  65. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
  66. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
  67. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
  68. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
  69. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  70. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  71. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  72. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
  73. package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
  74. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
  75. package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
  76. package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
  77. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  78. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
  79. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
  80. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
  81. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  82. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
  83. package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
  84. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  85. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
  86. package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
  87. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  88. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  89. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  90. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  91. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  92. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  93. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
  94. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  95. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  96. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  97. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  98. package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
  99. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  100. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  101. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  102. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  103. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  104. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  105. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  106. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  107. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  108. package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
  109. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  110. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  111. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  112. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  113. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  114. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  115. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
  116. package/templates/create/base/.env.example.hbs +0 -1
  117. package/templates/create/base/.golangci.yml.hbs +2 -2
  118. package/templates/create/base/AGENTS.md.hbs +279 -67
  119. package/templates/create/base/Makefile.hbs +2 -1
  120. package/templates/create/base/README.md.hbs +115 -32
  121. package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
  122. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  123. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  124. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  125. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  126. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  127. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  128. package/templates/create/features/docs/architecture.md.hbs +92 -32
  129. package/templates/create/features/docs/patterns.md.hbs +137 -91
  130. package/templates/create/features/docs/techstack.md.hbs +18 -3
  131. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  132. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  133. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  134. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  135. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  136. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  137. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  138. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  139. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  140. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  141. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  142. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  143. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  144. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  145. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  146. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  147. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  148. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  149. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  150. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  151. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  152. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  153. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  154. package/templates/generate/module/migration.up.sql.hbs +1 -1
  155. package/dist/utils/method-patcher.js +0 -357
  156. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  157. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
  158. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
  159. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
  160. package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
  161. package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
  162. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
  163. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
  164. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  165. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
  166. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
  167. package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
  168. package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
  169. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
  170. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
  171. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
  172. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  173. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  174. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  175. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  176. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  177. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  178. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  179. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  180. package/templates/generate/module/dto.go.hbs +0 -36
  181. package/templates/generate/module/errors.go.hbs +0 -33
  182. package/templates/generate/module/handler.go.hbs +0 -134
  183. package/templates/generate/module/handler_test.go.hbs +0 -174
  184. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  185. package/templates/generate/module/minimal/handler.go.hbs +0 -48
  186. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  187. package/templates/generate/module/minimal/service.go.hbs +0 -45
  188. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  189. package/templates/generate/module/model/model.go.hbs +0 -36
  190. package/templates/generate/module/repository.go.hbs +0 -103
  191. package/templates/generate/module/service.go.hbs +0 -108
  192. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -18,6 +18,8 @@ const migrations_1 = require("../utils/migrations");
18
18
  const openapi_patcher_1 = require("../utils/openapi-patcher");
19
19
  const gocheck_1 = require("../utils/gocheck");
20
20
  const gomod_patcher_1 = require("../utils/gomod-patcher");
21
+ const auth_wizard_1 = require("../prompts/auth-wizard");
22
+ const docs_patcher_1 = require("../utils/docs-patcher");
21
23
  // URL (relative to the api prefix) -> docs file (relative to docs/) for every
22
24
  // route `add auth` registers — kept next to AUTH_FILES's route list so the
23
25
  // two are easy to eyeball together when a route changes.
@@ -29,20 +31,26 @@ const AUTH_OPENAPI_PATHS = [
29
31
  { urlPath: "/auth/forgot-password", file: "./auth/forgot-password.yaml" },
30
32
  { urlPath: "/auth/reset-password", file: "./auth/reset-password.yaml" },
31
33
  { urlPath: "/auth/verify-email", file: "./auth/verify-email.yaml" },
32
- { urlPath: "/auth/google/login", file: "./auth/google-login.yaml" },
33
- { urlPath: "/auth/google/callback", file: "./auth/google-callback.yaml" },
34
+ { urlPath: "/auth/{provider}/login", file: "./auth/provider-login.yaml" },
35
+ { urlPath: "/auth/{provider}/exchange", file: "./auth/provider-exchange.yaml" },
34
36
  { urlPath: "/users/me", file: "./auth/users-me.yaml" },
35
37
  { urlPath: "/users/me/resend-verification", file: "./auth/users-me-resend-verification.yaml" },
36
38
  { urlPath: "/users/me/logout-all", file: "./auth/users-me-logout-all.yaml" },
39
+ { urlPath: "/users/me/mfa", file: "./auth/users-me-mfa.yaml" },
40
+ { urlPath: "/users/me/mfa/setup", file: "./auth/users-me-mfa-setup.yaml" },
41
+ { urlPath: "/users/me/mfa/confirm", file: "./auth/users-me-mfa-confirm.yaml" },
42
+ { urlPath: "/users/me/mfa/disable", file: "./auth/users-me-mfa-disable.yaml" },
43
+ { urlPath: "/auth/mfa/verify", file: "./auth/mfa-verify.yaml" },
37
44
  ];
38
45
  // addAuth scaffolds email/password authentication: a users+identities model
39
- // pair, JWT access tokens, a Redis-backed refresh token store with
46
+ // pair, JWT access tokens, a selectable refresh token store with
40
47
  // rotation + reuse detection, and register/login/refresh/logout/me. No RBAC
41
48
  // (no roles/permissions) — that's a separate opt-in on top of this, since
42
49
  // most projects need "is this caller logged in" long before they need "can
43
50
  // this caller do X".
44
- async function addAuth(store = "postgres", projectDir = process.cwd()) {
51
+ async function addAuth(store = "postgres", projectDir = process.cwd(), browserTopology = auth_wizard_1.DEFAULT_BROWSER_TOPOLOGY) {
45
52
  const config = (0, config_1.readConfig)(projectDir);
53
+ const browser = (0, auth_wizard_1.validateBrowserTopology)(browserTopology);
46
54
  // No longer a prerequisite. Without a worker the verification and reset mail
47
55
  // goes out inline instead of through a queue — a real trade (those two
48
56
  // endpoints then block on SMTP), but not one worth forcing a second binary
@@ -63,7 +71,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
63
71
  (0, platform_patcher_1.patchConfigForSMTP)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
64
72
  patchEnvExampleForSMTP(path_1.default.join(projectDir, ".env.example"));
65
73
  }
66
- await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), { goModule: config.goModule });
74
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), {
75
+ goModule: config.goModule,
76
+ redis: store === "redis",
77
+ worker,
78
+ });
67
79
  const migrationsDir = path_1.default.join(projectDir, "migrations");
68
80
  fs_extra_1.default.ensureDirSync(migrationsDir);
69
81
  const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
@@ -71,6 +83,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
71
83
  { template: "add/auth/migrations/create_users.up.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.up.sql`) },
72
84
  { template: "add/auth/migrations/create_users.down.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.down.sql`) },
73
85
  ], {});
86
+ const mfaVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
87
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
88
+ { template: "add/auth/migrations/create_mfa.up.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.up.sql`) },
89
+ { template: "add/auth/migrations/create_mfa.down.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.down.sql`) },
90
+ ], {});
74
91
  // identities references users(id) — must apply strictly after it. A
75
92
  // second newMigrationVersion() call, scanning the dir again now that the
76
93
  // users pair is already written, guarantees a later (or same-second,
@@ -87,13 +104,13 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
87
104
  { template: "add/auth/migrations/create_login_throttle.up.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.up.sql`) },
88
105
  { template: "add/auth/migrations/create_login_throttle.down.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.down.sql`) },
89
106
  ], {});
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
- }
107
+ // Recovery tokens are always stored in Postgres so consumption and the user
108
+ // update can share one transaction, even when refresh rotation uses Redis.
109
+ const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
110
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
111
+ { template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
112
+ { template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
113
+ ], {});
97
114
  // Only meaningful when there is a worker; readConfig fills this from the
98
115
  // adapter file on disk, so the only way it is still unknown is a project
99
116
  // that has internal/platform/queue with neither adapter in it. Guessing
@@ -120,7 +137,7 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
120
137
  // go-redis only when something in this project actually constructs a client
121
138
  ...(store === "redis" ? ["github.com/redis/go-redis/v9 v9.22.0"] : []),
122
139
  ]);
123
- patchEnvExample(path_1.default.join(projectDir, ".env.example"));
140
+ patchEnvExample(path_1.default.join(projectDir, ".env.example"), browser);
124
141
  patchMakefile(path_1.default.join(projectDir, "Makefile"));
125
142
  let docsMessage = "";
126
143
  const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
@@ -140,19 +157,30 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
140
157
  // parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
141
158
  // printed below, so `go vet` can't be the gate here.
142
159
  (0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
143
- (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, auth: true, authStore: store } });
160
+ const staleDocs = (0, docs_patcher_1.refreshProjectDocs)(projectDir, config, { auth: true, authStore: store });
161
+ if (staleDocs.length)
162
+ docsMessage += (0, docs_patcher_1.docsRefreshWarning)(staleDocs, "add auth");
163
+ (0, config_1.writeConfig)(projectDir, {
164
+ ...config,
165
+ features: { ...config.features, auth: true, authStore: store },
166
+ modules: {
167
+ ...config.modules,
168
+ user: { surface: "minimal", applicationStyle: "service", boundary: "hexagonal", packageLayout: "split" },
169
+ },
170
+ });
144
171
  console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
145
172
  console.log(worker
146
173
  ? "verification + password-reset mail goes through the queue"
147
174
  : "verification + password-reset mail is sent inline (no worker) — run `add worker` later to move it onto the queue");
148
175
  console.log(store === "postgres"
149
- ? "tokens + rate-limit counters: Postgres (user_svc.auth_tokens) and in-process — no Redis"
150
- : "tokens + rate-limit counters: Redis");
176
+ ? "refresh + recovery tokens: Postgres (user_svc.auth_tokens), rate-limit counters in-process — no Redis"
177
+ : "refresh tokens + rate-limit counters: Redis; recovery tokens: Postgres (user_svc.auth_tokens)");
151
178
  console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password,verify-email}, " +
152
- "GET /auth/google/{login,callback}, GET /users/me, and " +
153
- "POST /users/me/{resend-verification,logout-all} in cmd/api/wiring.go" +
179
+ "GET /auth/{provider}/login, POST /auth/{provider}/exchange, GET /users/me, and " +
180
+ "POST /users/me/{resend-verification,logout-all,mfa/setup,mfa/confirm,mfa/disable}, " +
181
+ "GET /users/me/mfa, and POST /auth/mfa/verify in cmd/api/wiring.go" +
154
182
  docsMessage);
155
- 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" +
183
+ console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations with `make migrate-up` before production\n" +
156
184
  "seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
157
185
  }
158
186
  function patchMakefile(makefilePath) {
@@ -183,7 +211,7 @@ function patchMakefile(makefilePath) {
183
211
  content = content.replace(/\nbuild:/, () => `${target}\nbuild:`);
184
212
  fs_extra_1.default.writeFileSync(makefilePath, content);
185
213
  }
186
- function patchEnvExample(envExamplePath) {
214
+ function patchEnvExample(envExamplePath, browserTopology) {
187
215
  if (!fs_extra_1.default.existsSync(envExamplePath))
188
216
  return;
189
217
  let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
@@ -195,13 +223,15 @@ function patchEnvExample(envExamplePath) {
195
223
  "JWT_SECRET=dev-secret-change-me\n" +
196
224
  "JWT_ACCESS_TTL_MIN=15\n" +
197
225
  "JWT_REFRESH_TTL_MIN=43200\n" +
198
- "COOKIE_SECURE=false\n" +
226
+ "JWT_REFRESH_MAX_TTL_MIN=43200\n" +
227
+ "OAUTH_STATE_TTL_MIN=10\n" +
228
+ `COOKIE_SECURE=${browserTopology === "cross-site" ? "true" : "false"}\n` +
199
229
  "# strict | lax | none — the refresh cookie's SameSite. Keep strict while the\n" +
200
230
  "# frontend is the same site as this API (localhost:3000 -> localhost:8080 is,\n" +
201
231
  "# and so is app.example.com -> api.example.com). A frontend on a different\n" +
202
232
  "# site entirely needs none, together with COOKIE_SECURE=true, or the browser\n" +
203
233
  "# never sends the cookie to /auth/refresh and sessions die at every expiry.\n" +
204
- "COOKIE_SAMESITE=strict\n" +
234
+ `COOKIE_SAMESITE=${browserTopology === "cross-site" ? "none" : "strict"}\n` +
205
235
  "\nPASSWORD_RESET_TTL_MIN=30\n" +
206
236
  "PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
207
237
  "\nEMAIL_VERIFY_TTL_MIN=1440\n" +
@@ -209,7 +239,19 @@ function patchEnvExample(envExamplePath) {
209
239
  "\n# leave the Google vars unset to disable Google login (register/login/refresh still work)\n" +
210
240
  "GOOGLE_CLIENT_ID=\n" +
211
241
  "GOOGLE_CLIENT_SECRET=\n" +
212
- "GOOGLE_REDIRECT_URL=\n";
242
+ "# exact browser callback URI registered with the provider (frontend-owned route)\n" +
243
+ "GOOGLE_OAUTH_REDIRECT_URI=\n" +
244
+ "\n# cookie/CORS deployment topology; the frontend owns its provider callback route\n" +
245
+ `AUTH_BROWSER_TOPOLOGY=${browserTopology}\n` +
246
+ "\n# MFA is globally off by default. When enabled, set a base64-encoded 32-byte\n" +
247
+ "# AES-256 key (for example: openssl rand -base64 32). Users still opt in\n" +
248
+ "# individually through /users/me/mfa/setup and /users/me/mfa/confirm.\n" +
249
+ "AUTH_MFA_ENABLED=false\n" +
250
+ "MFA_ISSUER=go-scaffold\n" +
251
+ "MFA_ENCRYPTION_KEY=\n" +
252
+ "MFA_CHALLENGE_TTL_MIN=5\n" +
253
+ "MFA_TOTP_WINDOW=1\n" +
254
+ "MFA_RECOVERY_CODE_COUNT=10\n";
213
255
  fs_extra_1.default.writeFileSync(envExamplePath, content);
214
256
  }
215
257
  // ensureRedis adds internal/platform/cache + its config/env/main.go wiring
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkProject = checkProject;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const config_1 = require("../utils/config");
11
+ const forbiddenByLayer = {
12
+ domain: [
13
+ /github\.com\/gin-gonic\/gin/,
14
+ /gorm\.io\//,
15
+ /net\/http/,
16
+ /internal\/(shared|platform)\//,
17
+ /redis/,
18
+ ],
19
+ application: [
20
+ /github\.com\/gin-gonic\/gin/,
21
+ /gorm\.io\//,
22
+ /database\/sql/,
23
+ /internal\/platform\//,
24
+ /internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
25
+ /redis/,
26
+ ],
27
+ ports: [
28
+ /github\.com\/gin-gonic\/gin/,
29
+ /gorm\.io\//,
30
+ /database\/sql/,
31
+ /internal\/platform\//,
32
+ /internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
33
+ /redis/,
34
+ ],
35
+ inbound: [/gorm\.io\//, /internal\/platform\//],
36
+ outbound: [/github\.com\/gin-gonic\/gin/, /internal\/shared\/httpx\//, /internal\/shared\/middleware\//],
37
+ };
38
+ // A split module is not hexagonal merely because its folders have the right
39
+ // names. Keep dependencies flowing toward the application core. Composition
40
+ // is the only module-local exception because it is the explicit wiring edge.
41
+ const forbiddenInternalLayers = {
42
+ domain: ["application", "ports", "inbound", "outbound", "composition"],
43
+ ports: ["application", "inbound", "outbound", "composition"],
44
+ application: ["inbound", "outbound", "composition"],
45
+ inbound: ["outbound", "composition"],
46
+ outbound: ["application", "inbound", "composition"],
47
+ };
48
+ function goFiles(dir) {
49
+ if (!fs_extra_1.default.existsSync(dir))
50
+ return [];
51
+ const files = [];
52
+ for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
53
+ const entryPath = path_1.default.join(dir, entry.name);
54
+ if (entry.isDirectory())
55
+ files.push(...goFiles(entryPath));
56
+ else if (entry.isFile() && entry.name.endsWith(".go"))
57
+ files.push(entryPath);
58
+ }
59
+ return files;
60
+ }
61
+ function directoriesNamed(dir, names) {
62
+ if (!fs_extra_1.default.existsSync(dir))
63
+ return [];
64
+ const found = [];
65
+ for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
66
+ if (!entry.isDirectory())
67
+ continue;
68
+ const entryPath = path_1.default.join(dir, entry.name);
69
+ if (names.has(entry.name))
70
+ found.push(entryPath);
71
+ found.push(...directoriesNamed(entryPath, names));
72
+ }
73
+ return found;
74
+ }
75
+ function imports(content) {
76
+ const found = [];
77
+ for (const match of content.matchAll(/^\s*(?:import\s+)?(?:[._\w]+\s+)?"([^"]+)"\s*$/gm))
78
+ found.push(match[1]);
79
+ return found;
80
+ }
81
+ function internalLayer(imported, moduleName) {
82
+ const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)\/(.+)$/);
83
+ if (!match || match[1] !== moduleName)
84
+ return undefined;
85
+ const relative = match[2];
86
+ if (relative === "domain" || relative.startsWith("domain/"))
87
+ return "domain";
88
+ if (relative === "application" || relative.startsWith("application/"))
89
+ return "application";
90
+ if (relative === "ports" || relative.startsWith("ports/"))
91
+ return "ports";
92
+ if (relative === "adapters/inbound" || relative.startsWith("adapters/inbound/"))
93
+ return "inbound";
94
+ if (relative === "adapters/outbound" || relative.startsWith("adapters/outbound/"))
95
+ return "outbound";
96
+ if (relative === "composition.go" || relative === "composition")
97
+ return "composition";
98
+ return undefined;
99
+ }
100
+ function siblingModule(imported, current) {
101
+ const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/|$)/);
102
+ if (!match || match[1] === current)
103
+ return undefined;
104
+ return match[1];
105
+ }
106
+ function appImport(imported) {
107
+ const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/(.*))?$/);
108
+ if (!match)
109
+ return undefined;
110
+ return { module: match[1], privatePath: match[2] ?? "" };
111
+ }
112
+ function filesForLayer(moduleDir, layer) {
113
+ if (layer === "composition")
114
+ return goFiles(moduleDir).filter((file) => path_1.default.basename(file) === "composition.go");
115
+ const relative = layer === "inbound"
116
+ ? path_1.default.join("adapters", "inbound", "http")
117
+ : layer === "outbound"
118
+ ? path_1.default.join("adapters", "outbound")
119
+ : layer;
120
+ return goFiles(path_1.default.join(moduleDir, relative));
121
+ }
122
+ function moduleNames(projectDir, config) {
123
+ const appDir = path_1.default.join(projectDir, "internal", "app");
124
+ const discovered = fs_extra_1.default.existsSync(appDir)
125
+ ? fs_extra_1.default.readdirSync(appDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
126
+ : [];
127
+ return [...new Set([...Object.keys(config.modules), ...discovered])].sort();
128
+ }
129
+ function expectedFiles(moduleDir, module) {
130
+ const required = [
131
+ "composition.go",
132
+ "domain/entity.go",
133
+ "domain/errors.go",
134
+ "ports/repository.go",
135
+ "application/dto.go",
136
+ "adapters/inbound/http/dto.go",
137
+ "adapters/inbound/http/handler.go",
138
+ "adapters/outbound/postgres/model.go",
139
+ "adapters/outbound/postgres/repository.go",
140
+ ];
141
+ if (module.applicationStyle === "service")
142
+ required.push("application/service.go");
143
+ else
144
+ required.push("application/commands.go", "application/queries.go");
145
+ return required.map((file) => path_1.default.join(moduleDir, file));
146
+ }
147
+ function checkModule(projectDir, config, name, module) {
148
+ const errors = [];
149
+ const moduleDir = path_1.default.join(projectDir, "internal", "app", name);
150
+ if (!module) {
151
+ errors.push(`internal/app/${name}: missing module metadata in go-scaffold.config.json`);
152
+ return errors;
153
+ }
154
+ if (module.boundary !== "hexagonal" || module.packageLayout !== "split") {
155
+ errors.push(`internal/app/${name}: module must declare boundary=hexagonal and packageLayout=split`);
156
+ }
157
+ for (const forbidden of directoriesNamed(moduleDir, new Set(["model"]))) {
158
+ errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level model package is forbidden; keep persistence models in the outbound adapter`);
159
+ }
160
+ for (const forbidden of directoriesNamed(moduleDir, new Set(["compat"]))) {
161
+ errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level compat directory is forbidden; keep legacy compatibility out of the canonical split architecture`);
162
+ }
163
+ for (const file of expectedFiles(moduleDir, module)) {
164
+ if (!fs_extra_1.default.existsSync(file))
165
+ errors.push(`${path_1.default.relative(projectDir, file)}: required by the split module contract`);
166
+ }
167
+ const applicationDir = path_1.default.join(moduleDir, "application");
168
+ if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service.go"))) {
169
+ errors.push(`internal/app/${name}: CQRS module must not contain application/service.go; use commands.go and queries.go`);
170
+ }
171
+ if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service_test.go"))) {
172
+ errors.push(`internal/app/${name}: CQRS module must not contain application/service_test.go; use cqrs_test.go`);
173
+ }
174
+ if (module.applicationStyle === "service" && (fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "commands.go")) || fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "queries.go")))) {
175
+ errors.push(`internal/app/${name}: service module must not contain application/commands.go or queries.go`);
176
+ }
177
+ if (module.applicationStyle === "service" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "cqrs_test.go"))) {
178
+ errors.push(`internal/app/${name}: service module must not contain application/cqrs_test.go; use service_test.go`);
179
+ }
180
+ for (const [layer, patterns] of Object.entries(forbiddenByLayer)) {
181
+ for (const file of filesForLayer(moduleDir, layer)) {
182
+ const content = fs_extra_1.default.readFileSync(file, "utf8");
183
+ for (const imported of imports(content)) {
184
+ const rule = patterns.find((pattern) => pattern.test(imported));
185
+ if (rule)
186
+ errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden dependency ${imported}`);
187
+ const importedLayer = internalLayer(imported, name);
188
+ if (importedLayer && forbiddenInternalLayers[layer].includes(importedLayer)) {
189
+ errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden internal layer ${imported}`);
190
+ }
191
+ const other = siblingModule(imported, name);
192
+ if (other) {
193
+ errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
194
+ }
195
+ }
196
+ if (layer === "application" && path_1.default.basename(file) === "dto.go" && content.includes("json:")) {
197
+ errors.push(`${path_1.default.relative(projectDir, file)}: application DTOs must be transport-neutral; keep JSON tags in adapters/inbound/http/dto.go`);
198
+ }
199
+ }
200
+ }
201
+ for (const file of filesForLayer(moduleDir, "composition")) {
202
+ const content = fs_extra_1.default.readFileSync(file, "utf8");
203
+ for (const imported of imports(content)) {
204
+ const other = siblingModule(imported, name);
205
+ if (other) {
206
+ errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
207
+ }
208
+ }
209
+ }
210
+ for (const file of goFiles(moduleDir)) {
211
+ const content = fs_extra_1.default.readFileSync(file, "utf8");
212
+ for (const imported of imports(content)) {
213
+ if (/\/internal\/app\/[^/]+\/model(?:\/|$)/.test(imported)) {
214
+ errors.push(`${path_1.default.relative(projectDir, file)}: imports a forbidden feature-level model package ${imported}`);
215
+ }
216
+ }
217
+ }
218
+ const rootGoFiles = fs_extra_1.default.existsSync(moduleDir)
219
+ ? fs_extra_1.default
220
+ .readdirSync(moduleDir, { withFileTypes: true })
221
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".go"))
222
+ .map((entry) => path_1.default.join(moduleDir, entry.name))
223
+ : [];
224
+ for (const file of rootGoFiles) {
225
+ if (path_1.default.basename(file) !== "composition.go") {
226
+ errors.push(`${path_1.default.relative(projectDir, file)}: root module package may only contain composition.go`);
227
+ }
228
+ }
229
+ return errors;
230
+ }
231
+ function checkProcessComposition(projectDir) {
232
+ const errors = [];
233
+ const compositionDir = path_1.default.join(projectDir, "internal", "composition");
234
+ const docPath = path_1.default.join(compositionDir, "doc.go");
235
+ if (!fs_extra_1.default.existsSync(docPath)) {
236
+ errors.push("internal/composition/doc.go: required process-level composition package");
237
+ }
238
+ for (const file of goFiles(compositionDir)) {
239
+ const content = fs_extra_1.default.readFileSync(file, "utf8");
240
+ for (const imported of imports(content)) {
241
+ const feature = appImport(imported);
242
+ if (feature?.privatePath) {
243
+ errors.push(`${path_1.default.relative(projectDir, file)}: process composition must import the public internal/app/${feature.module} package, not private path ${imported}`);
244
+ }
245
+ }
246
+ }
247
+ // wiring.go is allowed to see every feature package because it is the
248
+ // executable's root. Other cmd/api files must not become hidden
249
+ // cross-feature adapters; those belong here so integration tests can reuse
250
+ // the same mapping without importing package main.
251
+ const apiDir = path_1.default.join(projectDir, "cmd", "api");
252
+ for (const file of goFiles(apiDir)) {
253
+ const base = path_1.default.basename(file);
254
+ if (base === "main.go" || base === "wiring.go")
255
+ continue;
256
+ for (const imported of imports(fs_extra_1.default.readFileSync(file, "utf8"))) {
257
+ if (appImport(imported)) {
258
+ errors.push(`${path_1.default.relative(projectDir, file)}: process-level feature adapters belong in internal/composition; keep cmd/api for entrypoint and wiring`);
259
+ break;
260
+ }
261
+ }
262
+ }
263
+ return errors;
264
+ }
265
+ function checkProject(projectDir = process.cwd()) {
266
+ const config = (0, config_1.readConfig)(projectDir);
267
+ const errors = [];
268
+ if (config.architecture.style !== "modular-monolith")
269
+ errors.push("architecture.style must be modular-monolith");
270
+ if (config.architecture.boundary !== "hexagonal")
271
+ errors.push("architecture.boundary must be hexagonal");
272
+ if (config.architecture.packageLayout !== "split")
273
+ errors.push("architecture.packageLayout must be split");
274
+ errors.push(...checkProcessComposition(projectDir));
275
+ for (const name of moduleNames(projectDir, config))
276
+ errors.push(...checkModule(projectDir, config, name, config.modules[name]));
277
+ if (errors.length) {
278
+ throw new Error(`${picocolors_1.default.red("architecture check failed")}:\n${errors.map((error) => ` - ${error}`).join("\n")}`);
279
+ }
280
+ console.log(picocolors_1.default.green(`architecture check passed: ${moduleNames(projectDir, config).length} split module(s), hexagonal boundary`));
281
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureProject = configureProject;
7
+ exports.showProjectConfig = showProjectConfig;
8
+ exports.validateProjectConfig = validateProjectConfig;
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const generate_wizard_1 = require("../prompts/generate-wizard");
11
+ const interactive_1 = require("../prompts/interactive");
12
+ const config_1 = require("../utils/config");
13
+ const module_profile_1 = require("../utils/module-profile");
14
+ /**
15
+ * Interactive project-level defaults. Existing modules are not rewritten: the
16
+ * wizard only controls what a future `generate module` receives when the user
17
+ * chooses `--defaults` or accepts the generated-module prompt defaults.
18
+ */
19
+ async function configureProject(projectDir = process.cwd()) {
20
+ const config = (0, config_1.readConfig)(projectDir);
21
+ console.log("\nConfigure future generated modules:\n");
22
+ const profile = await (0, generate_wizard_1.promptModuleProfile)((0, module_profile_1.moduleProfileFor)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle));
23
+ const { moduleSurface: defaultModuleSurface, applicationStyle: defaultApplicationStyle } = profile === "advanced"
24
+ ? await (0, generate_wizard_1.promptAdvancedModuleArchitecture)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle)
25
+ : (0, generate_wizard_1.moduleArchitectureForProfile)(profile);
26
+ console.log("\nSummary:");
27
+ console.log(` Default module profile: ${(0, generate_wizard_1.moduleProfileDescription)(defaultModuleSurface, defaultApplicationStyle)}`);
28
+ console.log(` Default module surface: ${defaultModuleSurface}`);
29
+ console.log(` Default application style: ${defaultApplicationStyle}`);
30
+ console.log(" Existing modules: unchanged");
31
+ const proceed = await (0, interactive_1.confirm)({ message: "Save these project defaults?", default: true });
32
+ if (!proceed)
33
+ throw new Error("configuration cancelled");
34
+ (0, config_1.writeConfig)(projectDir, {
35
+ ...config,
36
+ architecture: {
37
+ ...config.architecture,
38
+ defaultModuleSurface,
39
+ defaultApplicationStyle,
40
+ },
41
+ });
42
+ console.log(picocolors_1.default.green("\nupdated go-scaffold.config.json"));
43
+ }
44
+ function showProjectConfig(projectDir = process.cwd()) {
45
+ console.log(JSON.stringify((0, config_1.readConfig)(projectDir), null, 2));
46
+ }
47
+ function validateProjectConfig(projectDir = process.cwd()) {
48
+ const config = (0, config_1.readConfig)(projectDir);
49
+ console.log(picocolors_1.default.green(`valid go-scaffold.config.json (schema ${config.schemaVersion})`));
50
+ }
@@ -13,7 +13,10 @@ 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 types_1 = require("../types");
17
+ const config_2 = require("../utils/config");
16
18
  const observability_1 = require("./observability");
19
+ const module_profile_1 = require("../utils/module-profile");
17
20
  async function createProject(rawName, opts) {
18
21
  const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
19
22
  if (!trimmed)
@@ -27,6 +30,13 @@ async function createProject(rawName, opts) {
27
30
  }
28
31
  let features;
29
32
  let apiPrefix;
33
+ let architecture;
34
+ const moduleProfile = (0, config_1.parseModuleProfile)(opts.moduleProfile, "--module-profile");
35
+ const moduleSurface = (0, config_1.parseModuleSurface)(opts.moduleSurface);
36
+ const applicationStyle = (0, config_1.parseApplicationStyle)(opts.applicationStyle);
37
+ if (moduleProfile && (moduleSurface || applicationStyle)) {
38
+ throw new Error("--module-profile cannot be combined with --module-surface or --application-style — choose one configuration style");
39
+ }
30
40
  if (opts.defaults) {
31
41
  features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true, observability: opts.observability ?? false };
32
42
  // "" to match what the wizard's Enter now gives. --defaults means "don't
@@ -37,16 +47,27 @@ async function createProject(rawName, opts) {
37
47
  const check = (0, naming_1.validateApiPrefix)(apiPrefix);
38
48
  if (check !== true)
39
49
  throw new Error(check);
50
+ const profileArchitecture = moduleProfile ? (0, module_profile_1.architectureForModuleProfile)(moduleProfile) : undefined;
51
+ architecture = {
52
+ ...types_1.DEFAULT_ARCHITECTURE_CONFIG,
53
+ ...(profileArchitecture ? { defaultModuleSurface: profileArchitecture.moduleSurface } : {}),
54
+ ...(profileArchitecture ? { defaultApplicationStyle: profileArchitecture.applicationStyle } : {}),
55
+ ...(!profileArchitecture && moduleSurface ? { defaultModuleSurface: moduleSurface } : {}),
56
+ ...(!profileArchitecture && applicationStyle ? { defaultApplicationStyle: applicationStyle } : {}),
57
+ };
40
58
  }
41
59
  else {
42
60
  // commander gives `--no-x` options a default of true, and there is no
43
61
  // `--docker`/`--openapi-docs` to pass, so false here can only mean the
44
62
  // caller opted out explicitly. undefined leaves the question to the wizard.
45
- ({ features, apiPrefix } = await (0, create_wizard_1.runCreateWizard)({
63
+ ({ features, apiPrefix, architecture } = await (0, create_wizard_1.runCreateWizard)({
46
64
  docker: opts.docker === false ? false : undefined,
47
65
  openapiDocs: opts.openapiDocs === false ? false : undefined,
48
66
  observability: opts.observability === true ? true : undefined,
49
67
  apiPrefix: opts.apiPrefix,
68
+ moduleProfile,
69
+ moduleSurface,
70
+ applicationStyle,
50
71
  }));
51
72
  }
52
73
  const context = {
@@ -54,12 +75,22 @@ async function createProject(rawName, opts) {
54
75
  goModule,
55
76
  dbName: (0, naming_1.toDbName)(projectName),
56
77
  apiPrefix,
78
+ ...architecture,
57
79
  ...features,
58
80
  };
59
81
  await fs_extra_1.default.ensureDir(projectDir);
60
82
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
61
83
  (0, template_renderer_1.gofmtTree)(projectDir);
62
- (0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features, scaffoldVersion: (0, version_1.cliVersion)() });
84
+ (0, config_1.writeConfig)(projectDir, {
85
+ schemaVersion: config_2.CONFIG_SCHEMA_VERSION,
86
+ projectName,
87
+ goModule,
88
+ apiPrefix,
89
+ features,
90
+ architecture,
91
+ modules: {},
92
+ scaffoldVersion: (0, version_1.cliVersion)(),
93
+ });
63
94
  // Composed the same way a user would do it by hand — `create` always
64
95
  // renders the plain base, then this layers the same patches `add
65
96
  // observability` would apply on an existing project, so the two paths
@@ -59,8 +59,18 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
59
59
  modulePath,
60
60
  auth: opts.auth,
61
61
  permission: opts.permission,
62
+ full: opts.full,
63
+ cqrs: opts.cqrs,
64
+ moduleSurface: opts.full ? "crud" : "minimal",
65
+ applicationStyle: opts.cqrs ? "cqrs" : "service",
62
66
  };
63
- const moduleFiles = opts.full ? module_manifest_1.MODULE_FILES : module_manifest_1.MODULE_FILES_MINIMAL;
67
+ const moduleFiles = opts.cqrs
68
+ ? opts.full
69
+ ? module_manifest_1.MODULE_FILES_CQRS
70
+ : module_manifest_1.MODULE_FILES_MINIMAL_CQRS
71
+ : opts.full
72
+ ? module_manifest_1.MODULE_FILES
73
+ : module_manifest_1.MODULE_FILES_MINIMAL;
64
74
  const moduleEntries = moduleFiles.map((f) => ({
65
75
  template: f.template,
66
76
  output: path_1.default.join("internal", "app", modulePath, f.output),
@@ -132,8 +142,24 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
132
142
  }
133
143
  (0, template_renderer_1.gofmtTree)(projectDir);
134
144
  (0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
145
+ (0, config_1.writeConfig)(projectDir, {
146
+ ...config,
147
+ modules: {
148
+ ...config.modules,
149
+ [modulePath]: {
150
+ surface: opts.full ? "crud" : "minimal",
151
+ applicationStyle: opts.cqrs ? "cqrs" : "service",
152
+ boundary: "hexagonal",
153
+ packageLayout: "split",
154
+ },
155
+ },
156
+ });
135
157
  const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
136
158
  console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
159
+ if (opts.cqrs) {
160
+ console.log("application boundary: CQRS command/query handlers in application/commands.go and application/queries.go");
161
+ }
162
+ console.log(`recorded module defaults: ${opts.full ? "crud" : "minimal"} + ${opts.cqrs ? "cqrs" : "service"}`);
137
163
  if (opts.full) {
138
164
  console.log(`registered route ${routePath} in cmd/api/wiring.go`);
139
165
  }
@@ -161,6 +187,6 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
161
187
  }
162
188
  if (docsMessage)
163
189
  console.log(docsMessage);
164
- console.log(picocolors_1.default.dim(`\nnext: add real fields to model.go/dto.go, run \`go build ./...\`, then apply the migration ` +
165
- `(AUTO_MIGRATE=true handles it in dev, or \`migrate -path migrations -database "$DB_DSN" up\`)`));
190
+ console.log(picocolors_1.default.dim(`\nnext: add real fields to domain/entity.go, application/dto.go, adapters/inbound/http/dto.go, and the outbound persistence model, then run \`go build ./...\` and apply the migration ` +
191
+ `(use the development bootstrap locally, or \`migrate -path migrations -database "$DB_DSN" up\` before production)`));
166
192
  }