@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
@@ -0,0 +1,334 @@
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.hexagonalMarkersPresent = hexagonalMarkersPresent;
7
+ exports.assertHexagonalMethodAbsent = assertHexagonalMethodAbsent;
8
+ exports.patchHexagonalMethod = patchHexagonalMethod;
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const marker_patch_1 = require("./marker-patch");
11
+ const naming_1 = require("./naming");
12
+ const DTO_MARKER = "// go-scaffold:dto";
13
+ const REPOSITORY_INTERFACE_MARKER = "// go-scaffold:repository-interface";
14
+ const QUERY_REPOSITORY_INTERFACE_MARKER = "// go-scaffold:query-repository-interface";
15
+ const REPOSITORY_METHODS_MARKER = "// go-scaffold:repository-methods";
16
+ const SERVICE_MARKER = "// go-scaffold:service-methods";
17
+ const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
18
+ const COMMAND_MARKER = "// go-scaffold:command-methods";
19
+ const COMMAND_INTERFACE_MARKER = "// go-scaffold:command-interface";
20
+ const QUERY_MARKER = "// go-scaffold:query-methods";
21
+ const QUERY_INTERFACE_MARKER = "// go-scaffold:query-interface";
22
+ const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
23
+ const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
24
+ const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
25
+ const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
26
+ function isCqrs(paths) {
27
+ return Boolean(paths.commandPath && paths.queryPath);
28
+ }
29
+ function read(files, file) {
30
+ const value = files.get(file);
31
+ if (value !== undefined)
32
+ return value;
33
+ const content = fs_extra_1.default.readFileSync(file, "utf8");
34
+ files.set(file, content);
35
+ return content;
36
+ }
37
+ function write(files, file, content) {
38
+ files.set(file, content);
39
+ }
40
+ function addImport(content, importPath) {
41
+ return (0, marker_patch_1.ensureImport)(content, importPath);
42
+ }
43
+ function requestDTOMarker(content) {
44
+ if ((0, marker_patch_1.hasMarker)(content, DTO_MARKER))
45
+ return DTO_MARKER;
46
+ if ((0, marker_patch_1.hasMarker)(content, "// go-scaffold:user-dto"))
47
+ return "// go-scaffold:user-dto";
48
+ throw new Error("inbound HTTP DTO file is missing a go-scaffold DTO marker");
49
+ }
50
+ function insert(content, marker, block) {
51
+ return (0, marker_patch_1.insertBeforeMarker)(content, marker, block);
52
+ }
53
+ function serviceApplicationTarget(paths, type) {
54
+ if (isCqrs(paths)) {
55
+ if (type === "get")
56
+ return { path: paths.queryPath, marker: QUERY_MARKER, receiver: "h *QueryHandler" };
57
+ return { path: paths.commandPath, marker: COMMAND_MARKER, receiver: "h *CommandHandler" };
58
+ }
59
+ if (!paths.servicePath)
60
+ throw new Error("service module is missing application/service.go");
61
+ return { path: paths.servicePath, marker: SERVICE_MARKER, receiver: "s *Service" };
62
+ }
63
+ function applicationInterfaceMarker(paths, type) {
64
+ if (isCqrs(paths)) {
65
+ return type === "get"
66
+ ? { path: paths.queryPath, marker: QUERY_INTERFACE_MARKER }
67
+ : { path: paths.commandPath, marker: COMMAND_INTERFACE_MARKER };
68
+ }
69
+ return { path: paths.servicePath, marker: SERVICE_INTERFACE_MARKER };
70
+ }
71
+ function methodSignature(naming, method, opts) {
72
+ if (opts.type === "get" && opts.getMode === "all") {
73
+ return `${method.pascalName}(context.Context, int, int) ([]domain.${naming.pascalName}, error)`;
74
+ }
75
+ if (opts.type === "get") {
76
+ return `${method.pascalName}(context.Context, string) (*domain.${naming.pascalName}, error)`;
77
+ }
78
+ if (opts.type === "post") {
79
+ return `${method.pascalName}(context.Context, ${method.pascalName}Input) (*domain.${naming.pascalName}, error)`;
80
+ }
81
+ return `${method.pascalName}(context.Context, uuid.UUID) error`;
82
+ }
83
+ function applicationMethod(naming, method, opts, receiver) {
84
+ const target = receiver.startsWith("s ") ? "s" : "h";
85
+ if (opts.type === "get" && opts.getMode === "all") {
86
+ return [
87
+ `func (${receiver}) ${method.pascalName}(ctx context.Context, limit, offset int) ([]domain.${naming.pascalName}, error) {`,
88
+ `\treturn ${target}.repo.FindAll(ctx, limit, offset)`,
89
+ `}`,
90
+ "",
91
+ ].join("\n");
92
+ }
93
+ if (opts.type === "get") {
94
+ const field = (0, naming_1.toCamelCase)(opts.field ?? "field");
95
+ const fieldPascal = (0, naming_1.toPascalCase)(opts.field ?? "field");
96
+ return [
97
+ `func (${receiver}) ${method.pascalName}(ctx context.Context, ${field} string) (*domain.${naming.pascalName}, error) {`,
98
+ `\treturn ${target}.repo.FindBy${fieldPascal}(ctx, ${field})`,
99
+ `}`,
100
+ "",
101
+ ].join("\n");
102
+ }
103
+ if (opts.type === "post") {
104
+ return [
105
+ `func (${receiver}) ${method.pascalName}(ctx context.Context, in ${method.pascalName}Input) (*domain.${naming.pascalName}, error) {`,
106
+ `\t_ = ctx`,
107
+ `\t_ = in`,
108
+ `\treturn nil, domain.ErrNotImplemented`,
109
+ `}`,
110
+ "",
111
+ ].join("\n");
112
+ }
113
+ return [
114
+ `func (${receiver}) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
115
+ `\t_ = ctx`,
116
+ `\t_ = id`,
117
+ `\treturn domain.ErrNotImplemented`,
118
+ `}`,
119
+ "",
120
+ ].join("\n");
121
+ }
122
+ function handlerMethod(naming, method, opts, cqrs, routeReceiver, errorMapper) {
123
+ const receiver = cqrs ? (opts.type === "get" ? "h.queries" : "h.commands") : "h.svc";
124
+ if (opts.type === "get" && opts.getMode === "all") {
125
+ return {
126
+ route: `${routeReceiver}.GET("/${method.pathSegment}", h.${method.handlerName})`,
127
+ imports: ["net/http", "pagination"],
128
+ body: [
129
+ `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
130
+ `\tp := pagination.Parse(c)`,
131
+ `\titems, err := ${receiver}.${method.pascalName}(c.Request.Context(), p.Limit, p.Offset)`,
132
+ `\tif err != nil {`,
133
+ `\t\tc.Error(${errorMapper}(err))`,
134
+ `\t\treturn`,
135
+ `\t}`,
136
+ `\tout := make([]response, len(items))`,
137
+ `\tfor i := range items {`,
138
+ `\t\tout[i] = toResponse(application.ToResponse(&items[i]))`,
139
+ `\t}`,
140
+ `\tc.JSON(http.StatusOK, p.Response(out))`,
141
+ `}`,
142
+ "",
143
+ ].join("\n"),
144
+ };
145
+ }
146
+ if (opts.type === "get") {
147
+ const field = (0, naming_1.toCamelCase)(opts.field ?? "field");
148
+ const column = (0, naming_1.toDbName)(opts.field ?? "field");
149
+ return {
150
+ route: `${routeReceiver}.GET("/${column}/:${field}", h.${method.handlerName})`,
151
+ imports: ["net/http"],
152
+ body: [
153
+ `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
154
+ `\t${field} := c.Param("${field}")`,
155
+ `\tm, err := ${receiver}.${method.pascalName}(c.Request.Context(), ${field})`,
156
+ `\tif err != nil {`,
157
+ `\t\tc.Error(${errorMapper}(err))`,
158
+ `\t\treturn`,
159
+ `\t}`,
160
+ `\tc.JSON(http.StatusOK, toResponse(application.ToResponse(m)))`,
161
+ `}`,
162
+ "",
163
+ ].join("\n"),
164
+ };
165
+ }
166
+ if (opts.type === "post") {
167
+ return {
168
+ route: `${routeReceiver}.POST("/${method.pathSegment}", h.${method.handlerName})`,
169
+ imports: ["net/http", "httpx"],
170
+ body: [
171
+ `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
172
+ `\tvar in ${method.pascalName}Input`,
173
+ `\tif err := c.ShouldBindJSON(&in); err != nil {`,
174
+ `\t\tc.Error(httpx.BindErr(err))`,
175
+ `\t\treturn`,
176
+ `\t}`,
177
+ `\tm, err := ${receiver}.${method.pascalName}(c.Request.Context(), to${method.pascalName}Input(in))`,
178
+ `\tif err != nil {`,
179
+ `\t\tc.Error(${errorMapper}(err))`,
180
+ `\t\treturn`,
181
+ `\t}`,
182
+ `\tc.JSON(http.StatusCreated, toResponse(application.ToResponse(m)))`,
183
+ `}`,
184
+ "",
185
+ ].join("\n"),
186
+ };
187
+ }
188
+ return {
189
+ route: `${routeReceiver}.${opts.type.toUpperCase()}("/:id/${method.pathSegment}", h.${method.handlerName})`,
190
+ imports: ["net/http", "httpx"],
191
+ body: [
192
+ `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
193
+ `\tid, ok := httpx.ParseID(c)`,
194
+ `\tif !ok {`,
195
+ `\t\treturn`,
196
+ `\t}`,
197
+ `\tif err := ${receiver}.${method.pascalName}(c.Request.Context(), id); err != nil {`,
198
+ `\t\tc.Error(${errorMapper}(err))`,
199
+ `\t\treturn`,
200
+ `\t}`,
201
+ `\tc.Status(http.StatusNoContent)`,
202
+ `}`,
203
+ "",
204
+ ].join("\n"),
205
+ };
206
+ }
207
+ function assertNotDuplicate(content, needle, what) {
208
+ if (content.includes(needle))
209
+ throw new Error(`${what} already exists — pick a different method name`);
210
+ }
211
+ function handlerRouteReceiver(content) {
212
+ // Auth's protected routes use usersGroup; generated CRUD modules use the
213
+ // local g group. Both remain the module's inbound adapter boundary.
214
+ return (0, marker_patch_1.hasMarker)(content, "// go-scaffold:user-routes") ? "usersGroup" : "g";
215
+ }
216
+ function hexagonalMarkersPresent(paths) {
217
+ if (!fs_extra_1.default.existsSync(paths.dtoPath) || !fs_extra_1.default.existsSync(paths.requestDTOPath) || !fs_extra_1.default.existsSync(paths.portsPath) || !fs_extra_1.default.existsSync(paths.repositoryAdapterPath) || !fs_extra_1.default.existsSync(paths.handlerPath) || !fs_extra_1.default.existsSync(paths.serviceTestPath))
218
+ return false;
219
+ const handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
220
+ if (!(0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) || !(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER))
221
+ return false;
222
+ const dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
223
+ const requestDTO = fs_extra_1.default.readFileSync(paths.requestDTOPath, "utf8");
224
+ const ports = fs_extra_1.default.readFileSync(paths.portsPath, "utf8");
225
+ const adapter = fs_extra_1.default.readFileSync(paths.repositoryAdapterPath, "utf8");
226
+ if (!(0, marker_patch_1.hasMarker)(dto, DTO_MARKER) ||
227
+ (!(0, marker_patch_1.hasMarker)(requestDTO, DTO_MARKER) && !(0, marker_patch_1.hasMarker)(requestDTO, "// go-scaffold:user-dto")) ||
228
+ !(0, marker_patch_1.hasMarker)(adapter, REPOSITORY_METHODS_MARKER))
229
+ return false;
230
+ if (isCqrs(paths)) {
231
+ return Boolean(paths.commandPath && paths.queryPath && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.commandPath, "utf8"), COMMAND_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.commandPath, "utf8"), COMMAND_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.queryPath, "utf8"), QUERY_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.queryPath, "utf8"), QUERY_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, COMMAND_REPOSITORY_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, QUERY_REPOSITORY_INTERFACE_MARKER));
232
+ }
233
+ return Boolean(paths.servicePath && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), SERVICE_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), SERVICE_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, REPOSITORY_INTERFACE_MARKER));
234
+ }
235
+ const COMMAND_REPOSITORY_INTERFACE_MARKER = "// go-scaffold:command-repository-interface";
236
+ function assertHexagonalMethodAbsent(paths, method) {
237
+ assertNotDuplicate(fs_extra_1.default.readFileSync(paths.handlerPath, "utf8"), `func (h *Handler) ${method.handlerName}(`, `handler method "${method.handlerName}"`);
238
+ const appFiles = [paths.servicePath, paths.commandPath, paths.queryPath].filter((file) => Boolean(file));
239
+ for (const file of appFiles)
240
+ assertNotDuplicate(fs_extra_1.default.readFileSync(file, "utf8"), `) ${method.pascalName}(`, `application method "${method.pascalName}"`);
241
+ }
242
+ function patchHexagonalMethod(paths, naming, method, opts, goModule) {
243
+ assertHexagonalMethodAbsent(paths, method);
244
+ if (opts.type === "get" && opts.getMode !== "all" && !opts.field)
245
+ throw new Error("--field is required for --type get --get-mode one");
246
+ if (opts.type === "get" && opts.field?.toLowerCase() === "id")
247
+ throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
248
+ const files = new Map();
249
+ const cqrs = isCqrs(paths);
250
+ const interfaceTarget = applicationInterfaceMarker(paths, opts.type);
251
+ let interfaceFile = read(files, interfaceTarget.path);
252
+ interfaceFile = insert(interfaceFile, interfaceTarget.marker, methodSignature(naming, method, opts));
253
+ write(files, interfaceTarget.path, interfaceFile);
254
+ const target = serviceApplicationTarget(paths, opts.type);
255
+ let applicationFile = read(files, target.path);
256
+ applicationFile = addImport(applicationFile, "context");
257
+ if (opts.type !== "get" && opts.type !== "post")
258
+ applicationFile = addImport(applicationFile, "github.com/google/uuid");
259
+ applicationFile = insert(applicationFile, target.marker, applicationMethod(naming, method, opts, target.receiver));
260
+ write(files, target.path, applicationFile);
261
+ let handler = read(files, paths.handlerPath);
262
+ const handlerResult = handlerMethod(naming, method, opts, cqrs, handlerRouteReceiver(handler), paths.handlerErrorMapper ?? "appError");
263
+ handler = insert(handler, HANDLER_ROUTES_MARKER, handlerResult.route);
264
+ handler = insert(handler, HANDLER_FUNCS_MARKER, handlerResult.body);
265
+ for (const need of handlerResult.imports)
266
+ handler = addImport(handler, need.startsWith("net/") ? need : `${goModule}/internal/shared/${need}`);
267
+ write(files, paths.handlerPath, handler);
268
+ if (opts.type === "post") {
269
+ let dto = read(files, paths.dtoPath);
270
+ const inputName = `${method.pascalName}Input`;
271
+ assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
272
+ dto = insert(dto, DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ""].join("\n"));
273
+ write(files, paths.dtoPath, dto);
274
+ let requestDTO = read(files, paths.requestDTOPath);
275
+ assertNotDuplicate(requestDTO, `type ${inputName} struct`, `HTTP DTO "${inputName}"`);
276
+ requestDTO = addImport(requestDTO, `${goModule}/internal/app/${naming.pkg}/application`);
277
+ requestDTO = insert(requestDTO, requestDTOMarker(requestDTO), [
278
+ `type ${inputName} struct {`,
279
+ `\t// TODO: mirror request fields from application.${inputName} and add JSON/binding tags`,
280
+ `}`,
281
+ "",
282
+ `func to${inputName}(in ${inputName}) application.${inputName} {`,
283
+ `\t// TODO: map request fields explicitly into the application input.`,
284
+ `\t_ = in`,
285
+ `\treturn application.${inputName}{}`,
286
+ `}`,
287
+ "",
288
+ ].join("\n"));
289
+ write(files, paths.requestDTOPath, requestDTO);
290
+ }
291
+ if (opts.type === "get" && opts.getMode !== "all") {
292
+ const fieldPascal = (0, naming_1.toPascalCase)(opts.field);
293
+ const fieldParam = (0, naming_1.toCamelCase)(opts.field);
294
+ const repoInterfaceMarker = cqrs ? QUERY_REPOSITORY_INTERFACE_MARKER : REPOSITORY_INTERFACE_MARKER;
295
+ let ports = read(files, paths.portsPath);
296
+ ports = insert(ports, repoInterfaceMarker, `FindBy${fieldPascal}(context.Context, string) (*domain.${naming.pascalName}, error)`);
297
+ write(files, paths.portsPath, ports);
298
+ let adapter = read(files, paths.repositoryAdapterPath);
299
+ const column = (0, naming_1.toDbName)(opts.field);
300
+ const modelType = paths.repositoryModelType ?? `${naming.pascalName}Model`;
301
+ const toDomain = paths.repositoryToDomain ?? "toDomain";
302
+ const errorMapper = paths.repositoryErrorMapper ?? "mapDatabaseError";
303
+ adapter = insert(adapter, REPOSITORY_METHODS_MARKER, [
304
+ `func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*domain.${naming.pascalName}, error) {`,
305
+ `\tvar row ${modelType}`,
306
+ `\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&row, "${column} = ?", ${fieldParam}).Error; err != nil {`,
307
+ `\t\treturn nil, ${errorMapper}(err)`,
308
+ `\t}`,
309
+ `\treturn ${toDomain}(&row), nil`,
310
+ `}`,
311
+ "",
312
+ ].join("\n"));
313
+ write(files, paths.repositoryAdapterPath, adapter);
314
+ let test = read(files, paths.serviceTestPath);
315
+ const stubField = `findBy${fieldPascal}Fn`;
316
+ assertNotDuplicate(test, `${stubField} func(`, `repository stub field "${stubField}"`);
317
+ test = insert(test, REPOSITORY_STUB_FIELDS_MARKER, `\t${stubField} func(context.Context, string) (*domain.${naming.pascalName}, error)`);
318
+ const stubReceiver = paths.repositoryStubReceiver ?? "s *repositoryStub";
319
+ const stubVariable = stubReceiver.split(" ")[0];
320
+ assertNotDuplicate(test, `func (${stubReceiver}) FindBy${fieldPascal}(`, `repository stub method "FindBy${fieldPascal}"`);
321
+ test = insert(test, REPOSITORY_STUB_METHODS_MARKER, [
322
+ `func (${stubReceiver}) FindBy${fieldPascal}(ctx context.Context, value string) (*domain.${naming.pascalName}, error) {`,
323
+ `\tif ${stubVariable}.${stubField} == nil {`,
324
+ `\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
325
+ `\t}`,
326
+ `\treturn ${stubVariable}.${stubField}(ctx, value)`,
327
+ `}`,
328
+ "",
329
+ ].join("\n"));
330
+ write(files, paths.serviceTestPath, test);
331
+ }
332
+ for (const [file, content] of files)
333
+ fs_extra_1.default.writeFileSync(file, content);
334
+ }
@@ -18,26 +18,28 @@ const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the
18
18
  // the exact lines patchMainGo inserts for a module — one source of truth so
19
19
  // unpatchMainGo removes precisely what patch added.
20
20
  //
21
- // The service gets its own named variable rather than being constructed
22
- // inline inside NewHandler(...). docs/architect/patterns.md tells you to wire
23
- // one domain's service into another's constructor when it needs behaviour
24
- // from it, which means editing this block by hand — and an inline expression
25
- // has nowhere to hold the result. With a named variable the edit is "add an
26
- // argument to the end of line one", and unpatchMainGo can still find the line
27
- // afterwards because it matches on the `<pkg>Svc :=` prefix, not the whole
28
- // text.
21
+ // Generated features own their repository/service/handler composition. The
22
+ // API binary supplies infrastructure and security dependencies, then only
23
+ // registers the feature's handler.
29
24
  function mainGoLines(patch) {
30
- const modelAlias = `${patch.pkg}model`; // every domain's model subpackage is named "model"
31
- const svcVar = `${patch.pkg}Svc`;
32
- const handlerArgs = [svcVar];
25
+ const modelAlias = `${patch.pkg}postgres`; // every domain's persistence adapter is named "postgres"
26
+ const handlerArgs = ["db"];
27
+ const legacyHandlerArgs = [`${patch.pkg}Svc`];
33
28
  if (patch.auth)
34
29
  handlerArgs.push("cfg.JWTSecret");
30
+ // RBAC's Authz is owned by role/composition.go. wiring.go registers a
31
+ // permission-gated feature with that public capability; it no longer keeps a
32
+ // standalone root variable named `authz`.
35
33
  if (patch.permission)
36
- handlerArgs.push("authz");
34
+ handlerArgs.push("roleComposition.Authz");
35
+ if (patch.auth)
36
+ legacyHandlerArgs.push("cfg.JWTSecret");
37
+ if (patch.permission)
38
+ legacyHandlerArgs.push("roleComposition.Authz");
37
39
  return {
38
40
  importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
39
- modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/model"`,
40
- // AutoMigrate (dev) creates tables but not the schema they live in — see
41
+ modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/adapters/outbound/postgres"`,
42
+ // Development schema bootstrap creates tables but not the schema they live in — see
41
43
  // the comment on go-scaffold:schemas in main.go.hbs. One Exec per schema,
42
44
  // guarded by its own sentinel so two modules sharing a schema name only
43
45
  // ever produce one line (not expected today, but cheap to keep safe).
@@ -47,16 +49,18 @@ function mainGoLines(patch) {
47
49
  `}`,
48
50
  ].join("\n"),
49
51
  schemaSentinel: `CREATE SCHEMA IF NOT EXISTS ${patch.schemaName}`,
50
- migrateLine: `&${modelAlias}.${patch.pascalName}{},`,
51
- serviceLine: `${svcVar} := ${patch.pkg}.NewService(${patch.pkg}.NewRepository(db))`,
52
- // matches the service line however the user has since extended it
53
- servicePrefix: `${svcVar} :=`,
52
+ migrateLine: `&${modelAlias}.${patch.pascalName}Model{},`,
53
+ // kept only so undo can clean projects generated before feature-local
54
+ // composition was introduced.
55
+ legacyServicePrefix: `${patch.pkg}Svc :=`,
56
+ legacyRouteLine: `${patch.pkg}.NewHandler(${legacyHandlerArgs.join(", ")}).Register(api)`,
54
57
  // `api` is the one route group declared by main.go.hbs, prefixed with
55
58
  // whatever apiPrefix the project chose at create time (e.g. /v1, /api,
56
59
  // or none) — every module registers on it, there is no per-module choice.
57
- // `authz` only exists in main.go once `add rbac` has run — patch.permission
58
- // is only ever set once that's already been verified by the caller.
59
- routeLine: `${patch.pkg}.NewHandler(${handlerArgs.join(", ")}).Register(api)`,
60
+ // `roleComposition.Authz` only exists in main.go once `add rbac` has run —
61
+ // patch.permission is only ever set once that has been verified by the
62
+ // caller.
63
+ routeLine: `${patch.pkg}.NewHandlerFromDB(${handlerArgs.join(", ")}).Register(api)`,
60
64
  };
61
65
  }
62
66
  // assertMainGoPatchable is the pre-flight for it: every marker patchMainGo
@@ -79,13 +83,13 @@ function assertMainGoPatchable(mainGoPath) {
79
83
  }
80
84
  }
81
85
  // patchMainGo wires a newly generated module into cmd/api/wiring.go: its
82
- // import, its model in the AutoMigrate call, and its route registration —
86
+ // import, its model in the development bootstrap, and its route registration —
83
87
  // via marker comments rather than a Go AST rewrite (ponytail: text insertion
84
88
  // at a fixed marker is enough here; reach for go/ast if main.go ever needs
85
89
  // edits markers can't express).
86
90
  function patchMainGo(mainGoPath, patch) {
87
91
  let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
88
- const { importLine, modelImportLine, schemaLines, schemaSentinel, migrateLine, serviceLine, servicePrefix, routeLine } = mainGoLines(patch);
92
+ const { importLine, modelImportLine, schemaLines, schemaSentinel, migrateLine, routeLine } = mainGoLines(patch);
89
93
  // each guarded by its own sentinel so re-running after only the module
90
94
  // folder was deleted (main.go still wired) is a no-op, not a dup that
91
95
  // panics gin at startup.
@@ -93,9 +97,6 @@ function patchMainGo(mainGoPath, patch) {
93
97
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
94
98
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaLines, schemaSentinel);
95
99
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine, migrateLine);
96
- // sentinel is the prefix, not the whole line: a re-run must not add a
97
- // second service line just because the first one gained a dependency.
98
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, serviceLine, servicePrefix);
99
100
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
100
101
  content = (0, marker_patch_1.removeLines)(content, [UNUSED_API_LINE]);
101
102
  fs_extra_1.default.writeFileSync(mainGoPath, content);
@@ -105,10 +106,11 @@ function patchMainGo(mainGoPath, patch) {
105
106
  // main.go still compiles (api would otherwise be declared-and-unused).
106
107
  function unpatchMainGo(mainGoPath, patch) {
107
108
  let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
108
- const { importLine, modelImportLine, schemaLines, migrateLine, servicePrefix, routeLine } = mainGoLines(patch);
109
- content = (0, marker_patch_1.removeLines)(content, [importLine, modelImportLine, migrateLine, routeLine]);
110
- // by prefix: the service line may have grown arguments since it was written
111
- content = (0, marker_patch_1.removeLinesByPrefix)(content, [servicePrefix]);
109
+ const { importLine, modelImportLine, schemaLines, migrateLine, legacyServicePrefix, legacyRouteLine, routeLine } = mainGoLines(patch);
110
+ content = (0, marker_patch_1.removeLines)(content, [importLine, modelImportLine, migrateLine, routeLine, legacyRouteLine]);
111
+ // by prefix: remove the named service line from projects generated before
112
+ // composition became feature-local.
113
+ content = (0, marker_patch_1.removeLinesByPrefix)(content, [legacyServicePrefix]);
112
114
  // by contiguous block, not removeLines: every module's schema block is
113
115
  // identical except the schema name, so a line-by-line removal would also
114
116
  // strip another module's matching lines.
@@ -31,7 +31,13 @@ function ensureImport(content, importPath) {
31
31
  // the search argument is a plain string, not a regex — collapsing any "$$"
32
32
  // that happens to appear in importLine. A function's return value is spliced
33
33
  // in literally, so this holds regardless of what importPath contains.
34
- return content.replace(/import \(\n/, () => `import (\n\t${importLine}\n`);
34
+ if (content.includes("import (\n")) {
35
+ return content.replace(/import \(\n/, () => `import (\n\t${importLine}\n`);
36
+ }
37
+ // A minimal CQRS service has no imports until the first method is added.
38
+ // Create a complete block after the package clause so later generated
39
+ // methods can use the same idempotent path.
40
+ return content.replace(/^(package [^\n]+\n)/, (_match, packageLine) => `${packageLine}\nimport (\n\t${importLine}\n)\n`);
35
41
  }
36
42
  // insertBeforeMarkerOnce: like insertBeforeMarker but a no-op if `sentinel`
37
43
  // already appears in the file. Makes module wiring idempotent — re-running
@@ -18,7 +18,7 @@ function existingModulePackages(projectDir) {
18
18
  .map((entry) => entry.name);
19
19
  }
20
20
  // A Go package name has no word boundaries: "orderitem" cannot tell you it
21
- // came from "order-items". Everything else a command derives — the model type
21
+ // came from "order-items". Everything else a command derives — the entity type
22
22
  // (OrderItem), the route (/order-items), the table (order_items), the
23
23
  // migration filename — does depend on those boundaries, so re-deriving them
24
24
  // from the package name yields Orderitem/orderitems and code that doesn't
@@ -29,15 +29,21 @@ function existingModulePackages(projectDir) {
29
29
  // prints as the next command to run. Following that instruction produced
30
30
  // `undefined: model.Orderitem (but have OrderItem)`.
31
31
  //
32
- // model/model.go is where the boundaries survive — the struct name is
33
- // PascalCase, so kebab-casing it recovers the exact string `generate module`
34
- // was originally given.
32
+ // The outbound persistence model is where the generated PascalCase type
33
+ // survives, so kebab-casing it recovers the exact string `generate module` was
34
+ // originally given.
35
35
  function pascalNameOnDisk(projectDir, pkg) {
36
- const modelPath = path_1.default.join(projectDir, "internal", "app", pkg, "model", "model.go");
37
- if (!fs_extra_1.default.existsSync(modelPath))
38
- return null;
39
- const match = fs_extra_1.default.readFileSync(modelPath, "utf8").match(/^type\s+([A-Z]\w*)\s+struct\b/m);
40
- return match ? match[1] : null;
36
+ // Split hexagonal modules keep persistence models in the outbound adapter.
37
+ // The package name still loses word boundaries (`orderitem`), so recover
38
+ // the original PascalCase entity from the generated `OrderItemModel` type
39
+ // before commands such as `undo module orderitem` rebuild their wiring.
40
+ const adapterModelPath = path_1.default.join(projectDir, "internal", "app", pkg, "adapters", "outbound", "postgres", "model.go");
41
+ if (fs_extra_1.default.existsSync(adapterModelPath)) {
42
+ const match = fs_extra_1.default.readFileSync(adapterModelPath, "utf8").match(/^type\s+([A-Z]\w*)Model\s+struct\b/m);
43
+ if (match)
44
+ return match[1];
45
+ }
46
+ return null;
41
47
  }
42
48
  function resolveProjectModuleNaming(projectDir, rawName) {
43
49
  const located = (0, naming_1.resolveExistingModuleNaming)(rawName, existingModulePackages(projectDir));
@@ -52,7 +58,7 @@ function resolveProjectModuleNaming(projectDir, rawName) {
52
58
  return located; // model.go holds something we can't derive a module name from
53
59
  }
54
60
  // Only trust the correction when it still points at the same package.
55
- // A hand-renamed struct, or a legacy module whose type never matched its
56
- // folder, would otherwise silently retarget the command at another module.
61
+ // A hand-renamed struct whose type no longer matches its folder would
62
+ // otherwise silently retarget the command at another module.
57
63
  return fromDisk.pkg === located.pkg ? fromDisk : located;
58
64
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.moduleProfileFor = moduleProfileFor;
4
+ exports.architectureForModuleProfile = architectureForModuleProfile;
5
+ exports.describeModuleProfile = describeModuleProfile;
6
+ function moduleProfileFor(moduleSurface, applicationStyle) {
7
+ if (moduleSurface === "minimal" && applicationStyle === "service")
8
+ return "lean";
9
+ if (moduleSurface === "crud" && applicationStyle === "service")
10
+ return "crud";
11
+ if (moduleSurface === "minimal" && applicationStyle === "cqrs")
12
+ return "cqrs";
13
+ return "advanced";
14
+ }
15
+ function architectureForModuleProfile(profile) {
16
+ if (profile === "crud") {
17
+ return { moduleSurface: "crud", applicationStyle: "service" };
18
+ }
19
+ if (profile === "cqrs") {
20
+ return { moduleSurface: "minimal", applicationStyle: "cqrs" };
21
+ }
22
+ return { moduleSurface: "minimal", applicationStyle: "service" };
23
+ }
24
+ function describeModuleProfile(profile) {
25
+ if (profile === "lean")
26
+ return "Lean (minimal + service)";
27
+ if (profile === "crud")
28
+ return "CRUD (CRUD surface + service)";
29
+ if (profile === "cqrs")
30
+ return "CQRS (minimal surface + CQRS)";
31
+ return "Advanced (choose surface + application boundary)";
32
+ }