@mettlecast/domain-cli 0.2.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 (301) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +133 -0
  3. package/dist/builder/build-registry.d.ts +20 -0
  4. package/dist/builder/build-registry.js +178 -0
  5. package/dist/builder/build-types.d.ts +10 -0
  6. package/dist/builder/build-types.js +32 -0
  7. package/dist/builder/load-domain-module-config.d.ts +12 -0
  8. package/dist/builder/load-domain-module-config.js +34 -0
  9. package/dist/builder/load-module.d.ts +26 -0
  10. package/dist/builder/load-module.js +114 -0
  11. package/dist/builder/zod-to-json.d.ts +10 -0
  12. package/dist/builder/zod-to-json.js +15 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +338 -0
  15. package/dist/commands/add-api.d.ts +32 -0
  16. package/dist/commands/add-api.js +106 -0
  17. package/dist/commands/add-component.d.ts +15 -0
  18. package/dist/commands/add-component.js +78 -0
  19. package/dist/commands/add-domain.d.ts +21 -0
  20. package/dist/commands/add-domain.js +71 -0
  21. package/dist/commands/add-flow.d.ts +14 -0
  22. package/dist/commands/add-flow.js +108 -0
  23. package/dist/commands/add-hook.d.ts +15 -0
  24. package/dist/commands/add-hook.js +65 -0
  25. package/dist/commands/add-migration.d.ts +26 -0
  26. package/dist/commands/add-migration.js +93 -0
  27. package/dist/commands/add-module.d.ts +7 -0
  28. package/dist/commands/add-module.js +261 -0
  29. package/dist/commands/add-page.d.ts +13 -0
  30. package/dist/commands/add-page.js +170 -0
  31. package/dist/commands/add-seed-page.d.ts +5 -0
  32. package/dist/commands/add-seed-page.js +251 -0
  33. package/dist/commands/add-subscriber.d.ts +30 -0
  34. package/dist/commands/add-subscriber.js +87 -0
  35. package/dist/commands/add-test.d.ts +22 -0
  36. package/dist/commands/add-test.js +285 -0
  37. package/dist/commands/audit.d.ts +39 -0
  38. package/dist/commands/audit.js +171 -0
  39. package/dist/commands/build-catalog.d.ts +68 -0
  40. package/dist/commands/build-catalog.js +105 -0
  41. package/dist/commands/build-flows.d.ts +26 -0
  42. package/dist/commands/build-flows.js +174 -0
  43. package/dist/commands/build.d.ts +17 -0
  44. package/dist/commands/build.js +39 -0
  45. package/dist/commands/check-hashes.d.ts +38 -0
  46. package/dist/commands/check-hashes.js +97 -0
  47. package/dist/commands/create-project.d.ts +5 -0
  48. package/dist/commands/create-project.js +272 -0
  49. package/dist/commands/dev.d.ts +15 -0
  50. package/dist/commands/dev.js +27 -0
  51. package/dist/commands/doctor.d.ts +48 -0
  52. package/dist/commands/doctor.js +1301 -0
  53. package/dist/commands/explain.d.ts +34 -0
  54. package/dist/commands/explain.js +167 -0
  55. package/dist/commands/power-tune.d.ts +1 -0
  56. package/dist/commands/power-tune.js +87 -0
  57. package/dist/commands/reseed-page.d.ts +6 -0
  58. package/dist/commands/reseed-page.js +297 -0
  59. package/dist/commands/show-dns.d.ts +15 -0
  60. package/dist/commands/show-dns.js +101 -0
  61. package/dist/commands/show.d.ts +46 -0
  62. package/dist/commands/show.js +135 -0
  63. package/dist/commands/test.d.ts +18 -0
  64. package/dist/commands/test.js +56 -0
  65. package/dist/commands/upgrade-backend.d.ts +9 -0
  66. package/dist/commands/upgrade-backend.js +46 -0
  67. package/dist/commands/upgrade-event.d.ts +22 -0
  68. package/dist/commands/upgrade-event.js +111 -0
  69. package/dist/commands/upgrade-frontend.d.ts +5 -0
  70. package/dist/commands/upgrade-frontend.js +99 -0
  71. package/dist/commands/upgrade.d.ts +7 -0
  72. package/dist/commands/upgrade.js +495 -0
  73. package/dist/commands/validate.d.ts +30 -0
  74. package/dist/commands/validate.js +148 -0
  75. package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
  76. package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
  77. package/dist/runtime-stubs/load-secrets.d.ts +11 -0
  78. package/dist/runtime-stubs/load-secrets.js +37 -0
  79. package/dist/server/api-server.d.ts +21 -0
  80. package/dist/server/api-server.js +14 -0
  81. package/dist/server/mount-routes.d.ts +19 -0
  82. package/dist/server/mount-routes.js +42 -0
  83. package/dist/templates/api-skeleton.d.ts +18 -0
  84. package/dist/templates/api-skeleton.js +69 -0
  85. package/dist/templates/claude-md.d.ts +4 -0
  86. package/dist/templates/claude-md.js +7 -0
  87. package/dist/templates/dashboard-pages/index.d.ts +101 -0
  88. package/dist/templates/dashboard-pages/index.js +22 -0
  89. package/dist/templates/domain-config.d.ts +10 -0
  90. package/dist/templates/domain-config.js +19 -0
  91. package/dist/templates/events-skeleton.d.ts +9 -0
  92. package/dist/templates/events-skeleton.js +23 -0
  93. package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
  94. package/dist/templates/patterns/api/create-with-event.js +69 -0
  95. package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
  96. package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
  97. package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
  98. package/dist/templates/patterns/api/paginated-list.js +71 -0
  99. package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
  100. package/dist/templates/patterns/api/simple-crud.js +95 -0
  101. package/dist/templates/patterns/api/system-admin.d.ts +5 -0
  102. package/dist/templates/patterns/api/system-admin.js +98 -0
  103. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
  104. package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
  105. package/dist/templates/patterns/component/index.d.ts +25 -0
  106. package/dist/templates/patterns/component/index.js +814 -0
  107. package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
  108. package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
  109. package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
  110. package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
  111. package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
  112. package/dist/templates/patterns/flow/human-in-loop.js +90 -0
  113. package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
  114. package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
  115. package/dist/templates/patterns/hook/index.d.ts +7 -0
  116. package/dist/templates/patterns/hook/index.js +378 -0
  117. package/dist/templates/patterns/migration/column-add.d.ts +5 -0
  118. package/dist/templates/patterns/migration/column-add.js +52 -0
  119. package/dist/templates/patterns/migration/event-store.d.ts +6 -0
  120. package/dist/templates/patterns/migration/event-store.js +97 -0
  121. package/dist/templates/patterns/migration/index.d.ts +5 -0
  122. package/dist/templates/patterns/migration/index.js +52 -0
  123. package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
  124. package/dist/templates/patterns/migration/rls-policy.js +78 -0
  125. package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
  126. package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
  127. package/dist/templates/patterns/page/dashboard.d.ts +4 -0
  128. package/dist/templates/patterns/page/dashboard.js +164 -0
  129. package/dist/templates/patterns/page/detail.d.ts +4 -0
  130. package/dist/templates/patterns/page/detail.js +162 -0
  131. package/dist/templates/patterns/page/form.d.ts +4 -0
  132. package/dist/templates/patterns/page/form.js +192 -0
  133. package/dist/templates/patterns/page/index.d.ts +8 -0
  134. package/dist/templates/patterns/page/index.js +14 -0
  135. package/dist/templates/patterns/page/list.d.ts +4 -0
  136. package/dist/templates/patterns/page/list.js +138 -0
  137. package/dist/templates/patterns/page/settings.d.ts +4 -0
  138. package/dist/templates/patterns/page/settings.js +318 -0
  139. package/dist/templates/patterns/page/wizard.d.ts +4 -0
  140. package/dist/templates/patterns/page/wizard.js +293 -0
  141. package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
  142. package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
  143. package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
  144. package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
  145. package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
  146. package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
  147. package/dist/templates/readme.d.ts +4 -0
  148. package/dist/templates/readme.js +7 -0
  149. package/dist/templates/subscriber-skeleton.d.ts +11 -0
  150. package/dist/templates/subscriber-skeleton.js +23 -0
  151. package/dist/utils/checksum.d.ts +18 -0
  152. package/dist/utils/checksum.js +39 -0
  153. package/dist/utils/file-helpers.d.ts +35 -0
  154. package/dist/utils/file-helpers.js +59 -0
  155. package/dist/utils/header-inject.d.ts +15 -0
  156. package/dist/utils/header-inject.js +93 -0
  157. package/dist/utils/install-file.d.ts +17 -0
  158. package/dist/utils/install-file.js +78 -0
  159. package/dist/utils/logger.d.ts +12 -0
  160. package/dist/utils/logger.js +24 -0
  161. package/dist/utils/manifest.d.ts +28 -0
  162. package/dist/utils/manifest.js +93 -0
  163. package/dist/utils/s3-fetch.d.ts +25 -0
  164. package/dist/utils/s3-fetch.js +52 -0
  165. package/dist/utils/scaffold-config.d.ts +95 -0
  166. package/dist/utils/scaffold-config.js +53 -0
  167. package/dist/utils/token-substitution.d.ts +35 -0
  168. package/dist/utils/token-substitution.js +52 -0
  169. package/dist/utils/zod-extractor.d.ts +10 -0
  170. package/dist/utils/zod-extractor.js +28 -0
  171. package/package.json +43 -0
  172. package/src/__tests__/build-registry.test.ts +115 -0
  173. package/src/__tests__/build-types.test.ts +100 -0
  174. package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
  175. package/src/__tests__/commands/add-domain.test.ts +174 -0
  176. package/src/__tests__/commands/add-flow.test.ts +69 -0
  177. package/src/__tests__/commands/add-module.test.ts +207 -0
  178. package/src/__tests__/commands/build-flows.test.ts +154 -0
  179. package/src/__tests__/commands/check-hashes.test.ts +142 -0
  180. package/src/__tests__/commands/create-project.test.ts +712 -0
  181. package/src/__tests__/commands/upgrade.test.ts +762 -0
  182. package/src/__tests__/doctor.test.ts +291 -0
  183. package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
  184. package/src/__tests__/load-domain-module-config.test.ts +10 -0
  185. package/src/__tests__/mount-routes.test.ts +100 -0
  186. package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
  187. package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
  188. package/src/__tests__/utils/checksum.test.ts +90 -0
  189. package/src/__tests__/utils/header-inject.test.ts +89 -0
  190. package/src/__tests__/utils/install-file.test.ts +207 -0
  191. package/src/__tests__/utils/manifest.test.ts +229 -0
  192. package/src/__tests__/utils/token-substitution.test.ts +134 -0
  193. package/src/__tests__/validate.test.ts +18 -0
  194. package/src/builder/build-registry.ts +237 -0
  195. package/src/builder/build-types.ts +36 -0
  196. package/src/builder/load-domain-module-config.ts +43 -0
  197. package/src/builder/load-module.ts +133 -0
  198. package/src/builder/zod-to-json.ts +16 -0
  199. package/src/cli.ts +387 -0
  200. package/src/commands/add-api.ts +145 -0
  201. package/src/commands/add-component.ts +116 -0
  202. package/src/commands/add-domain.ts +100 -0
  203. package/src/commands/add-flow.ts +136 -0
  204. package/src/commands/add-hook.ts +100 -0
  205. package/src/commands/add-migration.ts +129 -0
  206. package/src/commands/add-module.ts +347 -0
  207. package/src/commands/add-page.ts +242 -0
  208. package/src/commands/add-seed-page.ts +338 -0
  209. package/src/commands/add-subscriber.ts +123 -0
  210. package/src/commands/add-test.ts +328 -0
  211. package/src/commands/audit.ts +217 -0
  212. package/src/commands/build-catalog.ts +181 -0
  213. package/src/commands/build-flows.ts +198 -0
  214. package/src/commands/build.ts +59 -0
  215. package/src/commands/check-hashes.ts +150 -0
  216. package/src/commands/create-project.ts +367 -0
  217. package/src/commands/dev.ts +47 -0
  218. package/src/commands/doctor.ts +1362 -0
  219. package/src/commands/explain.ts +209 -0
  220. package/src/commands/power-tune.ts +112 -0
  221. package/src/commands/reseed-page.ts +410 -0
  222. package/src/commands/show-dns.ts +139 -0
  223. package/src/commands/show.ts +179 -0
  224. package/src/commands/test.ts +91 -0
  225. package/src/commands/upgrade-backend.ts +72 -0
  226. package/src/commands/upgrade-event.ts +175 -0
  227. package/src/commands/upgrade-frontend.ts +135 -0
  228. package/src/commands/upgrade.ts +668 -0
  229. package/src/commands/validate.ts +208 -0
  230. package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
  231. package/src/runtime-stubs/load-secrets.ts +40 -0
  232. package/src/server/api-server.ts +34 -0
  233. package/src/server/mount-routes.ts +68 -0
  234. package/src/templates/api-skeleton.ts +72 -0
  235. package/src/templates/claude-md.ts +7 -0
  236. package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
  237. package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
  238. package/src/templates/dashboard-pages/account/index.ts +5 -0
  239. package/src/templates/dashboard-pages/account/members.tsx +107 -0
  240. package/src/templates/dashboard-pages/account/profile.tsx +53 -0
  241. package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
  242. package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
  243. package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
  244. package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
  245. package/src/templates/dashboard-pages/auth/index.ts +10 -0
  246. package/src/templates/dashboard-pages/auth/login.tsx +75 -0
  247. package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
  248. package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
  249. package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
  250. package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
  251. package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
  252. package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
  253. package/src/templates/dashboard-pages/index.ts +22 -0
  254. package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
  255. package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
  256. package/src/templates/dashboard-pages/shell/index.ts +5 -0
  257. package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
  258. package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
  259. package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
  260. package/src/templates/domain-config.ts +20 -0
  261. package/src/templates/events-skeleton.ts +24 -0
  262. package/src/templates/patterns/api/create-with-event.ts +72 -0
  263. package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
  264. package/src/templates/patterns/api/paginated-list.ts +74 -0
  265. package/src/templates/patterns/api/simple-crud.ts +100 -0
  266. package/src/templates/patterns/api/system-admin.ts +101 -0
  267. package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
  268. package/src/templates/patterns/component/index.ts +833 -0
  269. package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
  270. package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
  271. package/src/templates/patterns/flow/human-in-loop.ts +93 -0
  272. package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
  273. package/src/templates/patterns/hook/index.ts +382 -0
  274. package/src/templates/patterns/migration/column-add.ts +54 -0
  275. package/src/templates/patterns/migration/event-store.ts +99 -0
  276. package/src/templates/patterns/migration/index.ts +54 -0
  277. package/src/templates/patterns/migration/rls-policy.ts +80 -0
  278. package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
  279. package/src/templates/patterns/page/dashboard.ts +164 -0
  280. package/src/templates/patterns/page/detail.ts +162 -0
  281. package/src/templates/patterns/page/form.ts +192 -0
  282. package/src/templates/patterns/page/index.ts +17 -0
  283. package/src/templates/patterns/page/list.ts +138 -0
  284. package/src/templates/patterns/page/settings.ts +318 -0
  285. package/src/templates/patterns/page/wizard.ts +293 -0
  286. package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
  287. package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
  288. package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
  289. package/src/templates/readme.ts +7 -0
  290. package/src/templates/subscriber-skeleton.ts +28 -0
  291. package/src/utils/checksum.ts +43 -0
  292. package/src/utils/file-helpers.ts +91 -0
  293. package/src/utils/header-inject.ts +126 -0
  294. package/src/utils/install-file.ts +99 -0
  295. package/src/utils/logger.ts +26 -0
  296. package/src/utils/manifest.ts +137 -0
  297. package/src/utils/s3-fetch.ts +91 -0
  298. package/src/utils/scaffold-config.ts +172 -0
  299. package/src/utils/token-substitution.ts +80 -0
  300. package/src/utils/zod-extractor.ts +31 -0
  301. package/tsconfig.json +13 -0
@@ -0,0 +1,208 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { RegistryEntry } from '@mettlecast/domain-cdk-packer';
4
+ import { validRange } from 'semver';
5
+ import type { DomainModuleConfig } from '@mettlecast/domain-runtime/types';
6
+ import { buildRegistry } from '../builder/build-registry.js';
7
+ import { cliLogger } from '../utils/logger.js';
8
+
9
+ /**
10
+ * A single validation failure.
11
+ */
12
+ export interface ValidationError {
13
+ /** Short error code for CI parsing. */
14
+ code: string;
15
+ /** Human-readable message. */
16
+ message: string;
17
+ }
18
+
19
+ /**
20
+ * Result of the validate command.
21
+ */
22
+ export interface ValidationResult {
23
+ /** true if all checks passed. */
24
+ valid: boolean;
25
+ /** List of validation failures. Empty when valid is true. */
26
+ errors: ValidationError[];
27
+ /** Non-fatal warnings from the registry build. */
28
+ warnings: string[];
29
+ }
30
+
31
+ /**
32
+ * Check for duplicate IDs in a list of registry entries.
33
+ * @param entries - Array of registry entries to check.
34
+ * @param kind - Kind name for error messages (e.g. 'api', 'webhook').
35
+ * @returns Array of ValidationError objects for any duplicates found.
36
+ */
37
+ function checkDuplicateIds(entries: RegistryEntry[], kind: string): ValidationError[] {
38
+ const seen = new Set<string>();
39
+ const errors: ValidationError[] = [];
40
+ for (const e of entries) {
41
+ if (seen.has(e.id)) {
42
+ errors.push({ code: 'DUPLICATE_ID', message: `Duplicate ${kind} id: '${e.id}'` });
43
+ }
44
+ seen.add(e.id);
45
+ }
46
+ return errors;
47
+ }
48
+
49
+ /**
50
+ * Check that every subscriber entry has a valid semver range string.
51
+ * @param entries - Subscriber registry entries to check.
52
+ * @returns Validation errors for any entry with an unparseable semver range.
53
+ */
54
+ function checkSubscriberSemverRanges(
55
+ entries: import('@mettlecast/domain-cdk-packer').SubscriberRegistryEntry[],
56
+ ): ValidationError[] {
57
+ const errors: ValidationError[] = [];
58
+ for (const entry of entries) {
59
+ if (entry.semverRange && !validRange(entry.semverRange)) {
60
+ errors.push({
61
+ code: 'INVALID_SEMVER_RANGE',
62
+ message: `Subscriber '${entry.id}' has invalid semver range: '${entry.semverRange}'`,
63
+ });
64
+ }
65
+ }
66
+ return errors;
67
+ }
68
+
69
+ /**
70
+ * Check version lifecycle consistency: sunsetAt must be present and at least
71
+ * 90 days after deprecatedAt when both are specified.
72
+ * @param lifecycle - Optional lifecycle metadata from a registry entry.
73
+ * @param entryLabel - Human-readable label for error messages.
74
+ * @returns Validation errors for lifecycle inconsistencies.
75
+ */
76
+ function checkLifecycleConsistency(
77
+ lifecycle: { deprecatedAt?: string; sunsetAt?: string } | undefined,
78
+ entryLabel: string,
79
+ ): ValidationError[] {
80
+ if (!lifecycle?.deprecatedAt) return [];
81
+ const errors: ValidationError[] = [];
82
+ const deprecated = new Date(lifecycle.deprecatedAt);
83
+
84
+ if (!lifecycle.sunsetAt) {
85
+ errors.push({
86
+ code: 'MISSING_SUNSET_DATE',
87
+ message: `${entryLabel}: deprecatedAt is set but sunsetAt is missing (90-day rule requires a sunset date)`,
88
+ });
89
+ return errors;
90
+ }
91
+
92
+ const sunset = new Date(lifecycle.sunsetAt);
93
+ const minSunset = new Date(deprecated.getTime() + 90 * 24 * 60 * 60 * 1000);
94
+ if (sunset < minSunset) {
95
+ errors.push({
96
+ code: 'SUNSET_TOO_SOON',
97
+ message: `${entryLabel}: sunsetAt must be at least 90 days after deprecatedAt (90-day deprecation rule)`,
98
+ });
99
+ }
100
+
101
+ return errors;
102
+ }
103
+
104
+ /**
105
+ * Stub: check for backend files not under `.mc/domains/` when mode is enforced or strict.
106
+ * Full file-tree scan is deferred to v1.x — this stub records the intent.
107
+ * @param _repoRoot - Absolute path to the repository root.
108
+ * @param config - Enforcement configuration.
109
+ * @returns Empty array (scan not yet implemented).
110
+ */
111
+ async function checkRawPathViolations(
112
+ _repoRoot: string,
113
+ config: DomainModuleConfig,
114
+ ): Promise<ValidationError[]> {
115
+ if (config.mode === 'optional') return [];
116
+ return [];
117
+ }
118
+
119
+ /**
120
+ * Run the validate command: build the registry and perform structural validation.
121
+ * Exits the process with code 1 if validation fails (CI gate usage).
122
+ * @param domainRoot - Absolute or relative path to the domain root directory.
123
+ * @param exitOnFailure - If true, calls process.exit(1) on failure. Defaults to true.
124
+ * @param config - Optional domain module configuration. Defaults to { mode: 'optional' }.
125
+ * @returns ValidationResult for programmatic use (e.g. in tests).
126
+ */
127
+ export async function runValidate(
128
+ domainRoot: string,
129
+ exitOnFailure = true,
130
+ config: DomainModuleConfig = { mode: 'optional' },
131
+ ): Promise<ValidationResult> {
132
+ const { registry, warnings } = await buildRegistry(domainRoot);
133
+ const errors: ValidationError[] = [];
134
+
135
+ // Log any build warnings (e.g. tsx import failures) immediately.
136
+ for (const w of warnings) {
137
+ cliLogger.warn(w);
138
+ }
139
+
140
+ errors.push(
141
+ ...checkDuplicateIds(registry.apis, 'api'),
142
+ ...checkDuplicateIds(registry.webhooks, 'webhook'),
143
+ ...checkDuplicateIds(registry.subscribers, 'subscriber'),
144
+ ...checkDuplicateIds(registry.schedules, 'schedule'),
145
+ ...checkDuplicateIds(registry.jobs, 'job'),
146
+ ...checkDuplicateIds(registry.actions, 'action'),
147
+ ...checkDuplicateIds(registry.integrations, 'integration'),
148
+ ...checkDuplicateIds(registry.events, 'event'),
149
+ );
150
+
151
+ const entriesWithFiles = [
152
+ ...registry.apis,
153
+ ...registry.webhooks,
154
+ ...registry.subscribers,
155
+ ...registry.schedules,
156
+ ...registry.jobs,
157
+ ...registry.actions,
158
+ ];
159
+ await Promise.all(
160
+ entriesWithFiles.map(async (entry) => {
161
+ if (!entry.handlerFile) return;
162
+ const absPath = join(registry.domainRoot, entry.handlerFile);
163
+ try {
164
+ await stat(absPath);
165
+ } catch {
166
+ errors.push({
167
+ code: 'MISSING_HANDLER_FILE',
168
+ message: `Handler file not found for ${entry.kind} '${entry.id}': ${absPath}`,
169
+ });
170
+ }
171
+ })
172
+ );
173
+
174
+ errors.push(...checkSubscriberSemverRanges(registry.subscribers));
175
+
176
+ for (const api of registry.apis) {
177
+ const entry = api as unknown as { deprecatedAt?: string; sunsetAt?: string };
178
+ errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
179
+ }
180
+
181
+ const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
182
+ errors.push(...rawPathErrors);
183
+
184
+ const totalPrimitives = registry.apis.length + registry.webhooks.length +
185
+ registry.subscribers.length + registry.schedules.length +
186
+ registry.jobs.length + registry.actions.length;
187
+ if (totalPrimitives === 0) {
188
+ errors.push({ code: 'EMPTY_DOMAIN', message: 'Domain has no primitives defined' });
189
+ }
190
+
191
+ const valid = errors.length === 0;
192
+
193
+ if (valid) {
194
+ cliLogger.info({ domain: registry.domain.id, primitives: totalPrimitives, warnings: warnings.length }, 'Validation passed');
195
+ } else {
196
+ for (const e of errors) {
197
+ cliLogger.error({ code: e.code }, e.message);
198
+ }
199
+ if (warnings.length > 0) {
200
+ cliLogger.warn({ count: warnings.length }, 'Build warnings were encountered (see above)');
201
+ }
202
+ if (exitOnFailure) {
203
+ process.exit(1);
204
+ }
205
+ }
206
+
207
+ return { valid, errors, warnings };
208
+ }
@@ -0,0 +1,102 @@
1
+ import {
2
+ hydrateCtx,
3
+ noopTracer,
4
+ createCapturingPublisher,
5
+ createCapturingJobsProxy,
6
+ createCapturingFlowsProxy,
7
+ createNoopIdempotency,
8
+ createCache,
9
+ createFetch,
10
+ createMockDb,
11
+ createEmptyActionsProxy,
12
+ createEmptyIntegrationsProxy,
13
+ createEmptySecrets,
14
+ } from '@mettlecast/domain-runtime';
15
+ import type { DomainContext } from '@mettlecast/domain-runtime';
16
+ import { createCliLogger } from '../utils/logger.js';
17
+ import { loadLocalSecrets } from './load-secrets.js';
18
+
19
+ /**
20
+ * Captured side-effects from a local-dev ctx for test inspection.
21
+ */
22
+ export interface LocalCtxCaptures {
23
+ /** Events published via ctx.publish during this invocation. */
24
+ events: Array<{ eventType: string; data: unknown }>;
25
+ /** Jobs enqueued via ctx.jobs.enqueue during this invocation. */
26
+ jobs: Array<{ jobName: string; input: unknown }>;
27
+ /** Flows started via ctx.flows.start during this invocation. */
28
+ executions: Array<{ flowName: string; input: unknown }>;
29
+ }
30
+
31
+ /**
32
+ * Options for creating a local dev DomainContext.
33
+ */
34
+ export interface LocalCtxOptions {
35
+ /** Absolute path to the domain root (used to read .env.local). */
36
+ domainRoot: string;
37
+ /** Override tenant id. Defaults to a placeholder UUID. */
38
+ tenantId?: string;
39
+ /** Override actor sub. Defaults to 'local-dev'. */
40
+ actorSub?: string;
41
+ }
42
+
43
+ /**
44
+ * Result of hydrateLocalCtx — the context plus capture handles.
45
+ */
46
+ export interface LocalCtxResult {
47
+ /** The fully hydrated DomainContext with all stubs. */
48
+ ctx: DomainContext;
49
+ /** Captures for inspecting published events, enqueued jobs, started flows. */
50
+ captures: LocalCtxCaptures;
51
+ }
52
+
53
+ /**
54
+ * Hydrates a DomainContext with local-dev stubs — no AWS calls.
55
+ * Reads .env.local from domainRoot for the secrets stub.
56
+ * Uses capturing publishers/proxies so test output can inspect side effects.
57
+ * @param options - LocalCtxOptions controlling domain root and optional overrides.
58
+ * @returns LocalCtxResult with the hydrated ctx and capture handles.
59
+ */
60
+ export async function hydrateLocalCtx(options: LocalCtxOptions): Promise<LocalCtxResult> {
61
+ const {
62
+ domainRoot,
63
+ tenantId = '00000000-0000-7000-8000-000000000000',
64
+ actorSub = 'local-dev',
65
+ } = options;
66
+
67
+ const fileSecrets = await loadLocalSecrets(domainRoot);
68
+ // Use loaded secrets; fall back to empty secrets if none found
69
+ const secrets = fileSecrets && Object.keys(fileSecrets).length > 0
70
+ ? fileSecrets
71
+ : createEmptySecrets();
72
+
73
+ const publisherCapture = createCapturingPublisher();
74
+ const jobsCapture = createCapturingJobsProxy();
75
+ const flowsCapture = createCapturingFlowsProxy();
76
+
77
+ const ctx = await hydrateCtx({}, {
78
+ actor: { type: 'user', sub: actorSub, email: 'dev@local' },
79
+ tenant: { id: tenantId, workspaceId: tenantId },
80
+ logger: createCliLogger('domain-dev'),
81
+ tracer: noopTracer,
82
+ fetch: createFetch(),
83
+ cache: createCache(),
84
+ idempotency: createNoopIdempotency(actorSub),
85
+ secrets,
86
+ db: createMockDb(),
87
+ publish: publisherCapture.publish,
88
+ actions: createEmptyActionsProxy(),
89
+ jobs: jobsCapture.proxy,
90
+ flows: flowsCapture.proxy,
91
+ integrations: createEmptyIntegrationsProxy(),
92
+ });
93
+
94
+ return {
95
+ ctx,
96
+ captures: {
97
+ events: publisherCapture.events,
98
+ jobs: jobsCapture.jobs,
99
+ executions: flowsCapture.executions,
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { SecretsProxy } from '@mettlecast/domain-runtime';
4
+
5
+ /**
6
+ * Read a `.env.local` file from the given domain root directory and return
7
+ * its entries as a SecretsProxy (Record<string, string>).
8
+ * Lines starting with `#` are treated as comments and skipped.
9
+ * Lines without `=` are skipped. Values may be quoted with single or double quotes.
10
+ * Returns an empty frozen object if the file does not exist.
11
+ * @param domainRoot - Absolute path to the domain directory.
12
+ * @returns A frozen SecretsProxy mapping env var names to string values.
13
+ */
14
+ export async function loadLocalSecrets(domainRoot: string): Promise<SecretsProxy> {
15
+ const filePath = join(domainRoot, '.env.local');
16
+ let raw: string;
17
+ try {
18
+ raw = await readFile(filePath, 'utf8');
19
+ } catch {
20
+ return Object.freeze({}) as SecretsProxy;
21
+ }
22
+
23
+ const entries: [string, string][] = raw
24
+ .split('\n')
25
+ .map(line => line.trim())
26
+ .filter(line => line.length > 0 && !line.startsWith('#') && line.includes('='))
27
+ .map(line => {
28
+ const idx = line.indexOf('=');
29
+ const key = line.slice(0, idx).trim();
30
+ let value = line.slice(idx + 1).trim();
31
+ if ((value.startsWith('"') && value.endsWith('"')) ||
32
+ (value.startsWith("'") && value.endsWith("'"))) {
33
+ value = value.slice(1, -1);
34
+ }
35
+ return [key, value] as [string, string];
36
+ })
37
+ .filter(([key]) => key.length > 0);
38
+
39
+ return Object.freeze(Object.fromEntries(entries)) as SecretsProxy;
40
+ }
@@ -0,0 +1,34 @@
1
+ import Fastify, { type FastifyInstance } from 'fastify';
2
+ import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
3
+ import { mountRoutes } from './mount-routes.js';
4
+
5
+ /**
6
+ * Options for creating the local API server.
7
+ */
8
+ export interface ApiServerOptions {
9
+ /** Port to listen on. Defaults to 3000. */
10
+ port?: number;
11
+ /** Host to bind to. Defaults to '0.0.0.0'. */
12
+ host?: string;
13
+ /** The built DomainRegistry. */
14
+ registry: DomainRegistry;
15
+ /** Absolute path to the domain root. */
16
+ domainRoot: string;
17
+ }
18
+
19
+ /**
20
+ * Create and start the local Fastify API server, mounting all routes from the registry.
21
+ * @param options - ApiServerOptions.
22
+ * @returns The running FastifyInstance (caller can call server.close() to stop).
23
+ */
24
+ export async function createApiServer(options: ApiServerOptions): Promise<FastifyInstance> {
25
+ const { port = 3000, host = '0.0.0.0', registry, domainRoot } = options;
26
+
27
+ const server = Fastify({ logger: true });
28
+
29
+ await mountRoutes({ server, registry, domainRoot });
30
+
31
+ await server.listen({ port, host });
32
+
33
+ return server;
34
+ }
@@ -0,0 +1,68 @@
1
+ import { join } from 'node:path';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
4
+ import { hydrateLocalCtx } from '../runtime-stubs/hydrate-local-ctx.js';
5
+
6
+ /**
7
+ * Options for mountRoutes.
8
+ */
9
+ export interface MountRoutesOptions {
10
+ /** The Fastify server instance. */
11
+ server: FastifyInstance;
12
+ /** The built DomainRegistry. */
13
+ registry: DomainRegistry;
14
+ /** Absolute path to the domain root (needed for ctx hydration). */
15
+ domainRoot: string;
16
+ }
17
+
18
+ function toFastifyPath(path: string): string {
19
+ return path.replace(/\{(\w+)\}/g, ':$1');
20
+ }
21
+
22
+ /**
23
+ * Mount all API routes from the DomainRegistry onto the Fastify server.
24
+ * Each route dynamically imports its handler file and hydrates a local dev ctx per request.
25
+ * @param options - MountRoutesOptions.
26
+ */
27
+ export async function mountRoutes(options: MountRoutesOptions): Promise<void> {
28
+ const { server, registry, domainRoot } = options;
29
+
30
+ for (const api of registry.apis) {
31
+ const handlerAbsPath = join(domainRoot, api.handlerFile);
32
+ const fastifyPath = toFastifyPath(api.path);
33
+
34
+ const mod = await import(handlerAbsPath) as Record<string, unknown>;
35
+ const def = Object.values(mod).find(
36
+ v => v && typeof v === 'object' && (v as Record<string, unknown>)['id'] === api.id
37
+ ) as Record<string, unknown> | undefined;
38
+
39
+ if (!def || typeof def['handler'] !== 'function') {
40
+ server.log.warn({ apiId: api.id, handlerAbsPath }, 'No handler function found, skipping route');
41
+ continue;
42
+ }
43
+
44
+ const handler = def['handler'] as (event: unknown, ctx: unknown) => Promise<unknown>;
45
+
46
+ server.route({
47
+ method: api.method === 'ANY' ? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] : [api.method as never],
48
+ url: fastifyPath,
49
+ handler: async (request, reply) => {
50
+ const { ctx } = await hydrateLocalCtx({ domainRoot });
51
+
52
+ const event = {
53
+ method: request.method,
54
+ path: request.url,
55
+ params: request.params,
56
+ query: request.query,
57
+ body: request.body,
58
+ headers: request.headers,
59
+ };
60
+
61
+ const result = await handler(event, ctx);
62
+ await reply.send(result);
63
+ },
64
+ });
65
+
66
+ server.log.info({ method: api.method, path: fastifyPath, apiId: api.id }, 'Route mounted');
67
+ }
68
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Template for generating API handler skeleton.
3
+ */
4
+
5
+ /**
6
+ * Convert kebab-case to camelCase.
7
+ * @param s - String in kebab-case.
8
+ * @returns String in camelCase.
9
+ */
10
+ function camelCase(s: string): string {
11
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
12
+ }
13
+
14
+ /**
15
+ * Generates an API handler skeleton with the given domain, API, and tenancy.
16
+ * @param domainId - Domain ID in kebab-case.
17
+ * @param apiId - API ID in kebab-case.
18
+ * @param tenancy - Tenancy mode: 'required', 'none', or 'system'.
19
+ * @returns TypeScript source code as a string.
20
+ */
21
+ export function apiSkeletonTemplate(domainId: string, apiId: string, tenancy: string): string {
22
+ const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
23
+ return `import { z } from 'zod';
24
+ import { defineApi } from '../../../lib/domain-runtime';
25
+
26
+ export const ${camelCase(apiId)} = defineApi({
27
+ id: '${apiId}',
28
+ path: '${tenantPath}/${domainId}/${apiId}',
29
+ tenancy: '${tenancy}',
30
+ versions: {
31
+ v1: {
32
+ status: 'stable',
33
+ input: z.object({}).optional(),
34
+ output: z.object({ ok: z.boolean() }),
35
+ handler: async (_input, _ctx) => {
36
+ return { ok: true };
37
+ },
38
+ },
39
+ },
40
+ });
41
+ `;
42
+ }
43
+
44
+ /**
45
+ * Generates a fixture JSON skeleton for API testing.
46
+ * @param domainId - Domain ID in kebab-case.
47
+ * @param apiId - API ID in kebab-case.
48
+ * @returns JSON string with mock event and context for testing.
49
+ */
50
+ export function apiFixtureSkeleton(domainId: string, apiId: string): string {
51
+ return JSON.stringify({
52
+ description: `Fixture for ${domainId}.${apiId}`,
53
+ event: {
54
+ version: '2.0',
55
+ requestContext: {
56
+ http: { method: 'GET', path: `/v1/${domainId}/${apiId}` },
57
+ authorizer: {
58
+ jwt: {
59
+ claims: {
60
+ sub: '00000000-0000-0000-0000-000000000000',
61
+ email: 'test@example.com',
62
+ 'custom:tenantId': '11111111-1111-1111-1111-111111111111',
63
+ },
64
+ },
65
+ },
66
+ },
67
+ pathParameters: {},
68
+ headers: { 'content-type': 'application/json', 'accept-version': '1' },
69
+ body: null,
70
+ },
71
+ }, null, 2) + '\n';
72
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Template for generating per-domain CLAUDE.md ownership file.
3
+ */
4
+ export function claudeMdTemplate(id: string): string {
5
+ const titleName = id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
6
+ return `# ${titleName} Domain — Ownership\n\nYou are the domain owner for \`${id}\`.\n\n## Responsibilities\n\n- All source files under \`domains/${id}/\` are owned by you.\n- API handlers must use \`defineApi\` from \`@mettlecast/domain-runtime\`.\n- Cross-domain calls must use \`ctx.actions.call()\`, never direct imports.\n- AWS SDK must never be imported directly — use \`ctx\` helpers.\n\n## Scaffold Rules\n\nSee the root \`CLAUDE.md\` for full scaffold rules, anti-patterns, and workflow commands.\n\n## Domain Config\n\nSee \`domains/${id}/domain.config.ts\` for the domain definition.\n`;
7
+ }
@@ -0,0 +1,107 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useApi } from '@mettlecast/sdk';
3
+ import { useProject } from '@mettlecast/dashboard-shell';
4
+ import { TibFormError, TibSubmitButton } from '@mettlecast/design-system';
5
+
6
+ interface ApiKey {
7
+ keyId: string;
8
+ label: string;
9
+ prefix: string;
10
+ createdAt: string;
11
+ }
12
+
13
+ export function ApiKeysPage() {
14
+ const tib = useApi();
15
+ const { workspaceId } = useProject();
16
+ const [keys, setKeys] = useState<ApiKey[]>([]);
17
+ const [label, setLabel] = useState('');
18
+ const [newKey, setNewKey] = useState<string | null>(null);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [creating, setCreating] = useState(false);
22
+
23
+ useEffect(() => {
24
+ if (!workspaceId) return;
25
+ tib?.listApiKeys(workspaceId)
26
+ .then(res => setKeys((res as any)?.items ?? []))
27
+ .catch(err => setError(err instanceof Error ? err.message : 'Failed to load API keys'))
28
+ .finally(() => setLoading(false));
29
+ }, [tib, workspaceId]);
30
+
31
+ async function handleCreate(e: React.FormEvent) {
32
+ e.preventDefault();
33
+ if (!workspaceId) return;
34
+ setCreating(true);
35
+ setError(null);
36
+ setNewKey(null);
37
+ try {
38
+ const res = await tib?.createApiKey(workspaceId, { label }) as any;
39
+ setNewKey(res?.key ?? null);
40
+ setLabel('');
41
+ const refreshed = await tib?.listApiKeys(workspaceId) as any;
42
+ setKeys(refreshed?.items ?? []);
43
+ } catch (err) {
44
+ setError(err instanceof Error ? err.message : 'Create failed');
45
+ } finally {
46
+ setCreating(false);
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="p-6 max-w-2xl space-y-8">
52
+ <div>
53
+ <h1 className="text-2xl font-semibold tracking-tight">API keys</h1>
54
+ <p className="text-sm text-muted-foreground mt-1">Manage programmatic access credentials</p>
55
+ </div>
56
+
57
+ {newKey && (
58
+ <div className="rounded-md bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 p-4 space-y-1" role="alert">
59
+ <p className="text-sm font-medium text-green-800 dark:text-green-200">API key created — copy it now, it won't be shown again</p>
60
+ <code className="block text-xs font-mono bg-white dark:bg-black rounded px-2 py-1 break-all">{newKey}</code>
61
+ </div>
62
+ )}
63
+
64
+ <form onSubmit={handleCreate} className="flex gap-2 items-end">
65
+ <div className="flex-1">
66
+ <label htmlFor="label" className="text-sm font-medium">Label</label>
67
+ <input id="label" type="text" value={label} onChange={e => setLabel(e.target.value)}
68
+ className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
69
+ placeholder="My integration" required />
70
+ </div>
71
+ <TibSubmitButton loading={creating}>Create key</TibSubmitButton>
72
+ </form>
73
+ <TibFormError error={error} />
74
+
75
+ {loading ? <p className="text-sm text-muted-foreground">Loading…</p> : (
76
+ <table className="w-full text-sm">
77
+ <thead>
78
+ <tr className="border-b border-border">
79
+ <th className="text-left py-2 font-medium text-muted-foreground">Label</th>
80
+ <th className="text-left py-2 font-medium text-muted-foreground">Key prefix</th>
81
+ <th className="text-left py-2 font-medium text-muted-foreground">Created</th>
82
+ <th className="py-2" />
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ {keys.map(k => (
87
+ <tr key={k.keyId} className="border-b border-border/50">
88
+ <td className="py-3 font-medium">{k.label}</td>
89
+ <td className="py-3 font-mono text-xs">{k.prefix}…</td>
90
+ <td className="py-3 text-muted-foreground">{new Date(k.createdAt).toLocaleDateString()}</td>
91
+ <td className="py-3 text-right">
92
+ <button type="button"
93
+ onClick={() => window.confirm('Revoke this key?') && workspaceId && tib?.revokeApiKey(workspaceId, k.keyId).then(() => setKeys(keys.filter(x => x.keyId !== k.keyId)))}
94
+ className="text-xs text-destructive hover:underline underline-offset-4">
95
+ Revoke
96
+ </button>
97
+ </td>
98
+ </tr>
99
+ ))}
100
+ </tbody>
101
+ </table>
102
+ )}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ export default ApiKeysPage;