@nakedev/go-scaffold 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +567 -513
  2. package/dist/commands/auth.js +12 -1
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/create.js +2 -1
  5. package/dist/commands/generate.js +5 -2
  6. package/dist/commands/method.js +66 -70
  7. package/dist/commands/observability.js +4 -53
  8. package/dist/commands/rbac.js +19 -8
  9. package/dist/commands/undo.js +6 -3
  10. package/dist/commands/worker.js +14 -4
  11. package/dist/index.js +13 -1
  12. package/dist/templates/auth-manifest.js +46 -45
  13. package/dist/templates/create-manifest.js +4 -0
  14. package/dist/templates/module-manifest.js +82 -60
  15. package/dist/templates/rbac-manifest.js +15 -11
  16. package/dist/templates/worker-manifest.js +4 -1
  17. package/dist/types.js +2 -0
  18. package/dist/utils/auth-patcher.js +22 -22
  19. package/dist/utils/config.js +24 -5
  20. package/dist/utils/docs-patcher.js +68 -0
  21. package/dist/utils/hexagonal-method-patcher.js +334 -0
  22. package/dist/utils/main-patcher.js +3 -3
  23. package/dist/utils/module-location.js +17 -11
  24. package/dist/utils/platform-patcher.js +27 -0
  25. package/dist/utils/rbac-patcher.js +73 -216
  26. package/package.json +1 -1
  27. package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
  28. package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
  29. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  30. package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
  31. package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
  32. package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
  33. package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
  34. package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
  35. package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
  36. package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
  37. package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
  38. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  39. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  40. package/templates/add/auth/internal/app/user/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
  41. package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
  42. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  43. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  44. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  45. package/templates/add/auth/internal/app/user/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
  46. package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
  47. package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
  48. package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
  49. package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
  50. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  51. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  52. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  53. package/templates/add/auth/internal/app/user/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
  54. package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
  55. package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
  56. package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
  57. package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
  58. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
  59. package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
  60. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
  61. package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
  62. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  63. package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
  64. package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
  65. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  66. package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
  67. package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
  68. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  69. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  70. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  71. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  72. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +1 -1
  73. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  74. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
  75. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  76. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  77. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  78. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  79. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  80. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  81. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  82. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  83. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  84. package/templates/add/rbac/internal/app/role/composition.go.hbs +23 -10
  85. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  86. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  87. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  88. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  89. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  90. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  91. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +54 -36
  92. package/templates/create/base/.golangci.yml.hbs +2 -2
  93. package/templates/create/base/AGENTS.md.hbs +55 -31
  94. package/templates/create/base/README.md.hbs +77 -22
  95. package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
  96. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  97. package/templates/create/features/docs/architecture.md.hbs +64 -26
  98. package/templates/create/features/docs/patterns.md.hbs +117 -90
  99. package/templates/create/features/docs/techstack.md.hbs +17 -2
  100. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  101. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  102. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  103. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  104. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  105. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  106. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  107. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  108. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  109. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  110. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  111. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  112. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  113. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  114. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  115. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  116. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  117. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  118. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  119. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  120. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  121. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  122. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  123. package/templates/generate/module/migration.up.sql.hbs +1 -1
  124. package/dist/utils/method-patcher.js +0 -537
  125. package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
  126. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
  127. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
  128. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
  129. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
  130. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  131. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
  132. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
  133. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
  134. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
  135. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
  136. package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
  137. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
  138. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  139. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  140. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  141. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  142. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  143. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  144. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  145. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  146. package/templates/generate/module/commands.go.hbs +0 -95
  147. package/templates/generate/module/composition.go.hbs +0 -23
  148. package/templates/generate/module/cqrs_test.go.hbs +0 -7
  149. package/templates/generate/module/dto.go.hbs +0 -36
  150. package/templates/generate/module/errors.go.hbs +0 -33
  151. package/templates/generate/module/handler.go.hbs +0 -179
  152. package/templates/generate/module/handler_test.go.hbs +0 -174
  153. package/templates/generate/module/minimal/commands.go.hbs +0 -34
  154. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  155. package/templates/generate/module/minimal/handler.go.hbs +0 -82
  156. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  157. package/templates/generate/module/minimal/queries.go.hbs +0 -45
  158. package/templates/generate/module/minimal/service.go.hbs +0 -71
  159. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  160. package/templates/generate/module/model/model.go.hbs +0 -36
  161. package/templates/generate/module/queries.go.hbs +0 -62
  162. package/templates/generate/module/repository.go.hbs +0 -103
  163. package/templates/generate/module/service.go.hbs +0 -164
  164. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -19,6 +19,7 @@ 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
21
  const auth_wizard_1 = require("../prompts/auth-wizard");
22
+ const docs_patcher_1 = require("../utils/docs-patcher");
22
23
  // URL (relative to the api prefix) -> docs file (relative to docs/) for every
23
24
  // route `add auth` registers — kept next to AUTH_FILES's route list so the
24
25
  // two are easy to eyeball together when a route changes.
@@ -156,7 +157,17 @@ async function addAuth(store = "postgres", projectDir = process.cwd(), browserTo
156
157
  // parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
157
158
  // printed below, so `go vet` can't be the gate here.
158
159
  (0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
159
- (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
+ });
160
171
  console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
161
172
  console.log(worker
162
173
  ? "verification + password-reset mail goes through the queue"
@@ -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
+ }
@@ -14,6 +14,7 @@ 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
16
  const types_1 = require("../types");
17
+ const config_2 = require("../utils/config");
17
18
  const observability_1 = require("./observability");
18
19
  const module_profile_1 = require("../utils/module-profile");
19
20
  async function createProject(rawName, opts) {
@@ -81,7 +82,7 @@ async function createProject(rawName, opts) {
81
82
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
82
83
  (0, template_renderer_1.gofmtTree)(projectDir);
83
84
  (0, config_1.writeConfig)(projectDir, {
84
- schemaVersion: 1,
85
+ schemaVersion: config_2.CONFIG_SCHEMA_VERSION,
85
86
  projectName,
86
87
  goModule,
87
88
  apiPrefix,
@@ -59,6 +59,7 @@ 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,
62
63
  cqrs: opts.cqrs,
63
64
  moduleSurface: opts.full ? "crud" : "minimal",
64
65
  applicationStyle: opts.cqrs ? "cqrs" : "service",
@@ -148,13 +149,15 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
148
149
  [modulePath]: {
149
150
  surface: opts.full ? "crud" : "minimal",
150
151
  applicationStyle: opts.cqrs ? "cqrs" : "service",
152
+ boundary: "hexagonal",
153
+ packageLayout: "split",
151
154
  },
152
155
  },
153
156
  });
154
157
  const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
155
158
  console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
156
159
  if (opts.cqrs) {
157
- console.log("application boundary: CQRS command/query handlers in commands.go and queries.go");
160
+ console.log("application boundary: CQRS command/query handlers in application/commands.go and application/queries.go");
158
161
  }
159
162
  console.log(`recorded module defaults: ${opts.full ? "crud" : "minimal"} + ${opts.cqrs ? "cqrs" : "service"}`);
160
163
  if (opts.full) {
@@ -184,6 +187,6 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
184
187
  }
185
188
  if (docsMessage)
186
189
  console.log(docsMessage);
187
- console.log(picocolors_1.default.dim(`\nnext: add real fields to model.go/dto.go, run \`go build ./...\`, then apply the migration ` +
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 ` +
188
191
  `(use the development bootstrap locally, or \`migrate -path migrations -database "$DB_DSN" up\` before production)`));
189
192
  }
@@ -10,13 +10,13 @@ const picocolors_1 = __importDefault(require("picocolors"));
10
10
  const config_1 = require("../utils/config");
11
11
  const naming_1 = require("../utils/naming");
12
12
  const module_location_1 = require("../utils/module-location");
13
- const method_patcher_1 = require("../utils/method-patcher");
14
13
  const gocheck_1 = require("../utils/gocheck");
15
14
  const template_renderer_1 = require("../utils/template-renderer");
16
15
  const migrations_1 = require("../utils/migrations");
17
16
  const openapi_patcher_1 = require("../utils/openapi-patcher");
18
17
  const generate_wizard_1 = require("../prompts/generate-wizard");
19
- // URL path registered in method-patcher.ts, excluding the project-wide API
18
+ const hexagonal_method_patcher_1 = require("../utils/hexagonal-method-patcher");
19
+ // URL path registered in the inbound adapter, excluding the project-wide API
20
20
  // prefix so it can be passed directly to patchOpenapiIndexRaw.
21
21
  function methodRoutePath(naming, method, type, getMode, field) {
22
22
  const base = `/${naming.plural}`;
@@ -62,109 +62,105 @@ function methodOpenapiDocument(naming, method, type, getMode, field) {
62
62
  }
63
63
  return [...pathParameters, ...operation, ""].join("\n");
64
64
  }
65
- async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = process.cwd()) {
66
- const config = (0, config_1.readConfig)(projectDir);
67
- const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, moduleNameArg ?? (await (0, generate_wizard_1.promptExistingModule)((0, module_location_1.existingModulePackages)(projectDir), "add a method to")));
68
- const modulePath = naming.pkg;
69
- const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
65
+ async function generateHexagonalMethod(config, naming, methodNameArg, opts, projectDir) {
66
+ const moduleDir = path_1.default.join(projectDir, "internal", "app", naming.pkg);
67
+ const moduleConfig = config.modules[naming.pkg];
68
+ if (!moduleConfig) {
69
+ throw new Error("module " + naming.pkg + " has no split-layout metadata in go-scaffold.config.json; run go-scaffold check first");
70
+ }
71
+ const cqrs = moduleConfig.applicationStyle === "cqrs";
72
+ const authModule = naming.pkg === "user" && fs_extra_1.default.existsSync(path_1.default.join(moduleDir, "application", "contracts.go"));
70
73
  const paths = {
71
- dtoPath: path_1.default.join(moduleDir, "dto.go"),
72
- repositoryPath: path_1.default.join(moduleDir, "repository.go"),
73
- commandPath: path_1.default.join(moduleDir, "commands.go"),
74
- queryPath: path_1.default.join(moduleDir, "queries.go"),
75
- servicePath: path_1.default.join(moduleDir, "service.go"),
76
- handlerPath: path_1.default.join(moduleDir, "handler.go"),
77
- serviceTestPath: path_1.default.join(moduleDir, "service_test.go"),
74
+ dtoPath: path_1.default.join(moduleDir, "application", "dto.go"),
75
+ requestDTOPath: path_1.default.join(moduleDir, "adapters", "inbound", "http", "dto.go"),
76
+ portsPath: path_1.default.join(moduleDir, "ports", "repository.go"),
77
+ repositoryAdapterPath: path_1.default.join(moduleDir, "adapters", "outbound", "postgres", "repository.go"),
78
+ servicePath: cqrs ? undefined : path_1.default.join(moduleDir, "application", "service.go"),
79
+ commandPath: cqrs ? path_1.default.join(moduleDir, "application", "commands.go") : undefined,
80
+ queryPath: cqrs ? path_1.default.join(moduleDir, "application", "queries.go") : undefined,
81
+ handlerPath: path_1.default.join(moduleDir, "adapters", "inbound", "http", "handler.go"),
82
+ serviceTestPath: path_1.default.join(moduleDir, "application", moduleConfig.applicationStyle === "cqrs" ? "cqrs_test.go" : "service_test.go"),
83
+ ...(authModule
84
+ ? {
85
+ repositoryModelType: "User",
86
+ repositoryToDomain: "toDomainUser",
87
+ repositoryErrorMapper: "persistenceError",
88
+ repositoryStubReceiver: "f *fakeRepo",
89
+ handlerErrorMapper: "toHTTPError",
90
+ }
91
+ : {}),
78
92
  };
79
- const requiredPaths = [
93
+ const required = [
80
94
  paths.dtoPath,
81
- paths.repositoryPath,
82
- paths.servicePath,
95
+ paths.requestDTOPath,
96
+ paths.portsPath,
97
+ paths.repositoryAdapterPath,
83
98
  paths.handlerPath,
84
99
  paths.serviceTestPath,
100
+ ...(cqrs ? [paths.commandPath, paths.queryPath] : [paths.servicePath]),
85
101
  ];
86
- for (const p of requiredPaths) {
87
- if (!fs_extra_1.default.existsSync(p)) {
88
- throw new Error(`module "${naming.pkg}" not found at ${moduleDir} (missing ${path_1.default.basename(p)}) — ` +
89
- `run \`go-scaffold generate module ${naming.pkg}\` first`);
90
- }
91
- }
92
- const cqrsPaths = [paths.commandPath, paths.queryPath].filter((candidate) => candidate !== undefined && fs_extra_1.default.existsSync(candidate));
93
- if (cqrsPaths.length === 1) {
94
- throw new Error(`module "${naming.pkg}" has an incomplete CQRS boundary at ${moduleDir} — ` +
95
- `commands.go and queries.go must be present together`);
102
+ const missing = required.filter((file) => !fs_extra_1.default.existsSync(file));
103
+ if (missing.length) {
104
+ throw new Error("module " +
105
+ naming.pkg +
106
+ " is not a complete hexagonal split module (missing " +
107
+ missing.map((file) => path_1.default.relative(projectDir, file)).join(", ") +
108
+ "); run go-scaffold check");
96
109
  }
97
110
  const type = opts.type ?? (await (0, generate_wizard_1.promptMethodType)());
98
- if (opts.getMode && type !== "get") {
111
+ if (opts.getMode && type !== "get")
99
112
  throw new Error("--get-mode can only be used with --type get");
100
- }
101
- if (opts.field && !(type === "get" && opts.getMode === "one")) {
113
+ if (opts.field && !(type === "get" && opts.getMode === "one"))
102
114
  throw new Error("--field can only be used with --type get --get-mode one");
103
- }
104
115
  const getMode = type === "get" ? opts.getMode ?? (await (0, generate_wizard_1.promptGetMode)()) : undefined;
105
116
  const field = type === "get" && getMode === "one" ? opts.field ?? (await (0, generate_wizard_1.promptLookupField)()) : undefined;
106
- // field becomes a Go param name (`func (...)(ctx, <field> string)`) and a
107
- // column name in the generated `WHERE <field> = ?`
108
117
  if (field)
109
118
  (0, naming_1.assertGoIdentifier)((0, naming_1.toCamelCase)(field), "lookup field");
110
119
  const method = (0, naming_1.resolveMethodNaming)(methodNameArg ?? (await (0, generate_wizard_1.promptMethodName)()));
111
- // Every marker this command patches has to exist before the first write:
112
- // patchMethod writes dto.go, then handler.go, then reads service.go, so a
113
- // missing service marker used to leave two files patched and the method
114
- // permanently un-retryable (assertNotDuplicate then sees it as existing).
115
- if (!(0, method_patcher_1.markersPresentForPaths)(paths)) {
116
- throw new Error(`internal/app/${naming.pkg} is missing the marker comments \`generate method\` patches at.\n` +
117
- `handler.go and service.go must both still carry their \`// go-scaffold:*\` markers —\n` +
118
- `restore them, or add this method by hand.`);
120
+ if (!(0, hexagonal_method_patcher_1.hexagonalMarkersPresent)(paths)) {
121
+ throw new Error("internal/app/" + naming.pkg + " is missing a split-layout generate-method marker; restore the go-scaffold markers or edit this endpoint by hand");
119
122
  }
120
- // Before the docs check below, so a name that's already taken is reported as
121
- // exactly that on every project otherwise the leftover OpenAPI document
122
- // gets the blame on a docs-enabled project and the real cause (pick another
123
- // method name) is the one thing the message doesn't say.
124
- (0, method_patcher_1.assertMethodAbsent)(paths, method);
125
- const docsRelativePath = config.features.openapiDocs
126
- ? `${naming.plural}/methods/${method.pathSegment}.yaml`
127
- : undefined;
123
+ (0, hexagonal_method_patcher_1.assertHexagonalMethodAbsent)(paths, method);
124
+ const docsRelativePath = config.features.openapiDocs ? naming.plural + "/methods/" + method.pathSegment + ".yaml" : undefined;
128
125
  if (docsRelativePath && fs_extra_1.default.existsSync(path_1.default.join(projectDir, "docs", docsRelativePath))) {
129
- throw new Error(`OpenAPI method document already exists: docs/${docsRelativePath}`);
126
+ throw new Error("OpenAPI method document already exists: docs/" + docsRelativePath);
130
127
  }
131
128
  const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
132
- (0, method_patcher_1.patchMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
129
+ (0, hexagonal_method_patcher_1.patchHexagonalMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
133
130
  (0, template_renderer_1.gofmtTree)(projectDir);
134
131
  (0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
135
- // A --field lookup queries a column the table doesn't have yet. GORM builds
136
- // that SQL at runtime, so nothing before the first real request notices:
137
- // the build passes, vet passes, the generated tests pass. Ship the column
138
- // and its index with the code that needs them.
139
132
  let fieldMigration = "";
140
133
  if (field) {
141
134
  const migrationsDir = path_1.default.join(projectDir, "migrations");
142
135
  fieldMigration = (0, migrations_1.newMigrationVersion)(migrationsDir);
143
136
  const fieldColumn = (0, naming_1.toDbName)(field);
144
137
  await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
145
- { template: "generate/module/field-column.up.sql.hbs", output: path_1.default.join("migrations", `${fieldMigration}_add_${naming.tableName}_${fieldColumn}.up.sql`) },
146
- { template: "generate/module/field-column.down.sql.hbs", output: path_1.default.join("migrations", `${fieldMigration}_add_${naming.tableName}_${fieldColumn}.down.sql`) },
147
- ], { ...naming, pkg: naming.pkg, methodName: method.name, fieldColumn });
138
+ { template: "generate/module/field-column.up.sql.hbs", output: path_1.default.join("migrations", fieldMigration + "_add_" + naming.tableName + "_" + fieldColumn + ".up.sql") },
139
+ { template: "generate/module/field-column.down.sql.hbs", output: path_1.default.join("migrations", fieldMigration + "_add_" + naming.tableName + "_" + fieldColumn + ".down.sql") },
140
+ ], { ...naming, methodName: method.name, fieldColumn });
148
141
  }
149
142
  if (fieldMigration) {
150
- console.log(`migration: migrations/${fieldMigration}_add_${naming.tableName}_${(0, naming_1.toDbName)(field)}.{up,down}.sql ` +
151
- `(adds the column the lookup queries, plus an index on it — check the type before applying)`);
143
+ console.log("migration: migrations/" + fieldMigration + "_add_" + naming.tableName + "_" + (0, naming_1.toDbName)(field) + ".{up,down}.sql (adds the column the lookup queries, plus an index on it; check the type before applying)");
152
144
  }
153
145
  if (docsRelativePath) {
154
146
  const docsPath = path_1.default.join(projectDir, "docs", docsRelativePath);
155
147
  fs_extra_1.default.outputFileSync(docsPath, methodOpenapiDocument(naming, method, type, getMode, field));
156
148
  (0, openapi_patcher_1.patchOpenapiIndexRaw)(path_1.default.join(projectDir, "docs", "openapi.yaml"), config.apiPrefix, [
157
- {
158
- urlPath: methodRoutePath(naming, method, type, getMode, field),
159
- file: `./${docsRelativePath}`,
160
- },
149
+ { urlPath: methodRoutePath(naming, method, type, getMode, field), file: "./" + docsRelativePath },
161
150
  ]);
162
151
  }
163
- console.log(picocolors_1.default.green(`\nadded "${method.name}" to internal/app/${modulePath}/`));
164
- console.log(`route: ${routeHint(naming, method, type, config.apiPrefix, getMode, field)}`);
165
- if (docsRelativePath) {
166
- console.log(picocolors_1.default.green(`docs: docs/${docsRelativePath} (wired into docs/openapi.yaml)`));
152
+ console.log(picocolors_1.default.green("\nadded \"" + method.name + "\" to internal/app/" + naming.pkg + "/"));
153
+ console.log("route: " + routeHint(naming, method, type, config.apiPrefix, getMode, field));
154
+ if (docsRelativePath)
155
+ console.log(picocolors_1.default.green("docs: docs/" + docsRelativePath + " (wired into docs/openapi.yaml)"));
156
+ const implementationFile = cqrs ? "application/" + (type === "get" ? "queries.go" : "commands.go") : "application/service.go";
157
+ console.log(picocolors_1.default.dim("\nnext: fill in the TODO in " + implementationFile + ", then go build ./... / go test ./..."));
158
+ }
159
+ async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = process.cwd()) {
160
+ const config = (0, config_1.readConfig)(projectDir);
161
+ const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, moduleNameArg ?? (await (0, generate_wizard_1.promptExistingModule)((0, module_location_1.existingModulePackages)(projectDir), "add a method to")));
162
+ if (config.architecture.packageLayout !== "split") {
163
+ throw new Error("this scaffold only supports the canonical hexagonal split layout");
167
164
  }
168
- const implementationFile = cqrsPaths.length === 2 ? (type === "get" ? "queries.go" : "commands.go") : "service.go";
169
- console.log(picocolors_1.default.dim(`\nnext: fill in the TODO in ${implementationFile}, then \`go build ./...\` / \`go test ./...\``));
165
+ await generateHexagonalMethod(config, naming, methodNameArg, opts, projectDir);
170
166
  }
@@ -13,6 +13,7 @@ const observability_manifest_1 = require("../templates/observability-manifest");
13
13
  const observability_patcher_1 = require("../utils/observability-patcher");
14
14
  const gocheck_1 = require("../utils/gocheck");
15
15
  const gomod_patcher_1 = require("../utils/gomod-patcher");
16
+ const docs_patcher_1 = require("../utils/docs-patcher");
16
17
  // addObservability scaffolds Prometheus metrics (GET /metrics) and
17
18
  // OpenTelemetry tracing for Gin + GORM, wiring both into the files `create`
18
19
  // already wrote. Opt-in and separate from `create`: most projects don't want
@@ -52,7 +53,7 @@ async function addObservability(projectDir = process.cwd(), opts = {}) {
52
53
  // docs saying `enabled` while the config it never reached still said false —
53
54
  // a disagreement no later command could converge, since the "already added"
54
55
  // guard then blocks a re-run.
55
- const staleDocs = refreshArchitectDocs(projectDir, config);
56
+ const staleDocs = (0, docs_patcher_1.refreshProjectDocs)(projectDir, config, { observability: true });
56
57
  (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, observability: true } });
57
58
  if (opts.silent)
58
59
  return;
@@ -66,57 +67,7 @@ async function addObservability(projectDir = process.cwd(), opts = {}) {
66
67
  if (config.features.worker) {
67
68
  console.log(picocolors_1.default.yellow("cmd/worker is not instrumented — it initialises no tracer provider, so its spans are dropped and it exposes no /metrics"));
68
69
  }
69
- if (staleDocs.length) {
70
- // Deliberately not "you edited these". The comparison is a whole-file
71
- // match against today's template, and techstack.md embeds pinned
72
- // dependency versions — so every release that bumps one makes every
73
- // project scaffolded before it look edited. Which is exactly the
74
- // population this function exists for.
75
- console.log(picocolors_1.default.yellow(`\nnote: couldn't safely rewrite ${staleDocs.join(" and ")} — ` +
76
- `${staleDocs.length > 1 ? "they don't" : "it doesn't"} match what this go-scaffold\n` +
77
- ` would have generated, so ${staleDocs.length > 1 ? "they were" : "it was"} left alone rather than overwriting your edits.\n` +
78
- ` ${staleDocs.length > 1 ? "They still describe" : "It still describes"} this project as having no metrics or tracing; update by hand.`));
79
- }
70
+ if (staleDocs.length)
71
+ console.log(picocolors_1.default.yellow((0, docs_patcher_1.docsRefreshWarning)(staleDocs, "add observability")));
80
72
  console.log(picocolors_1.default.dim("\nnext: go mod tidy, then set OTEL_EXPORTER_OTLP_ENDPOINT to export traces (empty = tracing no-ops, /metrics works either way)"));
81
73
  }
82
- // The architecture docs gate their observability sections on a `create`-time
83
- // flag, so adding the feature afterwards used to leave techstack.md saying
84
- // `disabled` and architecture.md missing the section entirely — while the
85
- // README promises `create --observability` and this command produce the same
86
- // project.
87
- //
88
- // Re-rendering from the templates rather than string-patching keeps the prose
89
- // in one place (the .hbs), but would also silently discard a user's edits. So
90
- // it first renders what the file *should* look like today, with observability
91
- // still off: only a byte-for-byte match proves nobody has touched it. Returns
92
- // the docs it declined to overwrite, for the caller to warn about.
93
- function refreshArchitectDocs(projectDir, config) {
94
- const docs = [
95
- { template: "create/features/docs/architecture.md.hbs", output: path_1.default.join("docs", "architect", "architecture.md") },
96
- { template: "create/features/docs/techstack.md.hbs", output: path_1.default.join("docs", "architect", "techstack.md") },
97
- ];
98
- // only what the two templates actually reference — projectName, apiPrefix,
99
- // architecture defaults and the feature flags
100
- const base = { projectName: config.projectName, apiPrefix: config.apiPrefix, ...config.architecture, ...config.features };
101
- const root = (0, template_renderer_1.getTemplatesRoot)();
102
- const skipped = [];
103
- for (const doc of docs) {
104
- const outputPath = path_1.default.join(projectDir, doc.output);
105
- if (!fs_extra_1.default.existsSync(outputPath))
106
- continue;
107
- const source = fs_extra_1.default.readFileSync(path_1.default.join(root, doc.template), "utf8");
108
- // compare with line endings normalised — a Windows checkout with
109
- // core.autocrlf=true would otherwise never match, and the feature would
110
- // silently never fire there
111
- const onDisk = lf(fs_extra_1.default.readFileSync(outputPath, "utf8"));
112
- if (onDisk !== lf((0, template_renderer_1.renderString)(source, { ...base, observability: false }))) {
113
- skipped.push(doc.output);
114
- continue;
115
- }
116
- fs_extra_1.default.writeFileSync(outputPath, (0, template_renderer_1.renderString)(source, { ...base, observability: true }));
117
- }
118
- return skipped;
119
- }
120
- function lf(text) {
121
- return text.replace(/\r\n/g, "\n");
122
- }