@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,63 @@
1
+ /**
2
+ * Cascade-deletion subscriber pattern template.
3
+ * Produces: subscriber that deletes related records when a parent is deleted.
4
+ */
5
+
6
+ function camelCase(s: string): string {
7
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8
+ }
9
+
10
+ export function cascadeDeletionTemplate(domain: string, id: string, event: string): string {
11
+ const varName = camelCase(id);
12
+
13
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
14
+
15
+ /**
16
+ * ${id} — Cascading deletion triggered by ${event}.
17
+ *
18
+ * When a parent ${event} event fires, this subscriber:
19
+ * 1. Queries for all child records owned by the deleted parent.
20
+ * 2. Soft-deletes each child (sets deletedAt timestamp).
21
+ * 3. Emits metrics for observability.
22
+ */
23
+ export const ${varName} = defineSubscriber({
24
+ id: '${domain}.${id}',
25
+ event: '${event}',
26
+ semverRange: '^1',
27
+ handler: async (event, ctx) => {
28
+ const { entityId, tenantId } = event.data;
29
+
30
+ // ── Find all children ────────────────────────────────────
31
+ const children = await ctx.db.query({
32
+ pk: 'TENANT#' + tenantId,
33
+ skPrefix: 'CHILD#' + entityId,
34
+ });
35
+
36
+ // ── Soft-delete each child ───────────────────────────────
37
+ const now = new Date().toISOString();
38
+ for (const child of children) {
39
+ await ctx.db.update({
40
+ key: { pk: child.pk, sk: child.sk },
41
+ updates: {
42
+ deletedAt: now,
43
+ deletedBy: event.data.deletedBy ?? 'system',
44
+ },
45
+ });
46
+ }
47
+
48
+ // ── Audit log ────────────────────────────────────────────
49
+ await ctx.auditLog({
50
+ action: 'cascade.delete',
51
+ tenantId,
52
+ entityId,
53
+ meta: {
54
+ childrenDeleted: children.length,
55
+ triggeredBy: event.id,
56
+ },
57
+ });
58
+
59
+ ctx.metrics?.putMetric('${id}.cascade.deleted', children.length, 'Count');
60
+ },
61
+ });
62
+ `;
63
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Single-step-projection subscriber pattern template.
3
+ * Produces: subscriber that updates a read-model projection table.
4
+ */
5
+
6
+ function camelCase(s: string): string {
7
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8
+ }
9
+
10
+ export function singleStepProjectionTemplate(domain: string, id: string, event: string): string {
11
+ const varName = camelCase(id);
12
+
13
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
14
+
15
+ /**
16
+ * ${id} — Projects ${event} events into a read-optimised view.
17
+ */
18
+ export const ${varName} = defineSubscriber({
19
+ id: '${domain}.${id}',
20
+ event: '${event}',
21
+ semverRange: '^1',
22
+ handler: async (event, ctx) => {
23
+ const { tenantId, ...eventData } = event.data;
24
+
25
+ // ── Upsert projection ────────────────────────────────────
26
+ // Each event updates the projection row for fast reads.
27
+ await ctx.db.put({
28
+ pk: 'TENANT#' + tenantId,
29
+ sk: 'PROJECTION#' + event.id,
30
+ ...eventData,
31
+ lastEventId: event.id,
32
+ projectedAt: new Date().toISOString(),
33
+ });
34
+
35
+ ctx.metrics?.putMetric('${id}.projection.applied', 1, 'Count');
36
+ },
37
+ });
38
+ `;
39
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Template for generating per-domain README.md.
3
+ */
4
+ export function readmeTemplate(id: string): string {
5
+ const titleName = id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
6
+ return `# ${titleName}\n\n> One-line description of what this domain does — update this.\n\n## Overview\n\nDomain ID: \`${id}\`\n\n## API Endpoints\n\nSee \`domains/${id}/api/\` for all endpoint handlers.\n\n## Events Published\n\nSee \`domains/${id}/publishes/events.ts\` for all published event types.\n\n## Development\n\n\`\`\`bash\nnpx mc-domain-module dev domains/${id}\nnpx mc-domain-module validate domains/${id}\nnpx mc-domain-module build domains/${id}\n\`\`\`\n`;
7
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Template for generating subscriber handler skeleton.
3
+ */
4
+
5
+ /**
6
+ * Generates a subscriber handler skeleton.
7
+ * @param domainId - Domain ID in kebab-case.
8
+ * @param subscriberId - Subscriber ID in kebab-case.
9
+ * @param eventId - Event ID to subscribe to (e.g. 'domain.event').
10
+ * @returns TypeScript source code as a string.
11
+ */
12
+ export function subscriberSkeletonTemplate(
13
+ domainId: string,
14
+ subscriberId: string,
15
+ eventId: string
16
+ ): string {
17
+ return `import { defineSubscriber } from '../../../lib/domain-runtime';
18
+
19
+ export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = defineSubscriber({
20
+ id: '${domainId}.${subscriberId}',
21
+ event: '${eventId}',
22
+ semverRange: '*',
23
+ handler: async (_event, _ctx) => {
24
+ // TODO: Implement subscriber handler
25
+ },
26
+ });
27
+ `;
28
+ }
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFile } from 'fs/promises';
3
+
4
+ /**
5
+ * Compute SHA-256 checksum of a Buffer, returning a lowercase hex string.
6
+ */
7
+ export function computeChecksumBuffer(buf: Buffer): string {
8
+ return createHash('sha256').update(buf).digest('hex');
9
+ }
10
+
11
+ /**
12
+ * Compute SHA-256 checksum of a string (encoded as UTF-8), returning a lowercase hex string.
13
+ */
14
+ export function computeChecksumString(content: string): string {
15
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
16
+ }
17
+
18
+ /**
19
+ * Compute SHA-256 checksum of a file on disk, returning a lowercase hex string.
20
+ * Returns null if the file does not exist.
21
+ */
22
+ export async function computeChecksumFile(filePath: string): Promise<string | null> {
23
+ try {
24
+ const buf = await readFile(filePath);
25
+ return computeChecksumBuffer(buf);
26
+ } catch (err: unknown) {
27
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
28
+ throw err;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns true if the file on disk has been modified compared to the expected checksum.
34
+ * Returns false if the checksums match, or null if the file does not exist.
35
+ */
36
+ export async function isDrifted(
37
+ filePath: string,
38
+ expectedChecksum: string
39
+ ): Promise<boolean | null> {
40
+ const actual = await computeChecksumFile(filePath);
41
+ if (actual === null) return null;
42
+ return actual !== expectedChecksum;
43
+ }
@@ -0,0 +1,91 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Maps each convention slot to the absolute file paths found for that slot.
6
+ * For the 'domain' slot there is exactly one path (domain.config.ts).
7
+ * For the 'publishes' slot there is exactly one path (publishes/events.ts).
8
+ * All other slots may have 0..N paths.
9
+ */
10
+ export interface DomainFilePaths {
11
+ /** Path to domain.config.ts if present. */
12
+ domain: string | undefined;
13
+ /** API handler file paths under api/. */
14
+ apis: string[];
15
+ /** Webhook handler file paths under webhooks/. */
16
+ webhooks: string[];
17
+ /** Subscriber handler file paths under subscribers/. */
18
+ subscribers: string[];
19
+ /** Action handler file paths under actions/. */
20
+ actions: string[];
21
+ /** Schedule handler file paths under schedules/. */
22
+ schedules: string[];
23
+ /** Job handler file paths under jobs/. */
24
+ jobs: string[];
25
+ /** Integration definition file paths under integrations/. */
26
+ integrations: string[];
27
+ /** Path to publishes/events.ts if present. */
28
+ publishes: string | undefined;
29
+ }
30
+
31
+ /**
32
+ * Walk a domain root directory and return the conventional file paths for each primitive slot.
33
+ * Only returns files with a `.ts` extension. Recurses into subdirectories but skips
34
+ * node_modules, __tests__, and dist directories.
35
+ * Returns undefined for slots where no files are found (domain.config.ts, publishes/events.ts).
36
+ * @param domainRoot - Absolute path to the domain directory (e.g. `.mc/domains/billing`).
37
+ * @returns DomainFilePaths with absolute paths.
38
+ */
39
+ export async function walkDomainDir(domainRoot: string): Promise<DomainFilePaths> {
40
+ async function tsFiles(dir: string): Promise<string[]> {
41
+ const result: string[] = [];
42
+
43
+ async function walk(current: string): Promise<void> {
44
+ try {
45
+ const entries = await readdir(current, { withFileTypes: true });
46
+
47
+ for (const entry of entries) {
48
+ // Skip node_modules, __tests__, dist directories
49
+ if (entry.isDirectory()) {
50
+ if (['node_modules', '__tests__', 'dist', '.git'].includes(entry.name)) {
51
+ continue;
52
+ }
53
+ // Recurse into subdirectories
54
+ await walk(join(current, entry.name));
55
+ } else if (entry.isFile() && entry.name.endsWith('.ts')) {
56
+ result.push(join(current, entry.name));
57
+ }
58
+ }
59
+ } catch {
60
+ // Directory doesn't exist or is unreadable, return empty
61
+ }
62
+ }
63
+
64
+ await walk(dir);
65
+ return result;
66
+ }
67
+
68
+ async function singleTs(filePath: string): Promise<string | undefined> {
69
+ try {
70
+ await stat(filePath);
71
+ return filePath;
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ const [domain, apis, webhooks, subscribers, actions, schedules, jobs, integrations, publishes] =
78
+ await Promise.all([
79
+ singleTs(join(domainRoot, 'domain.config.ts')),
80
+ tsFiles(join(domainRoot, 'api')),
81
+ tsFiles(join(domainRoot, 'webhooks')),
82
+ tsFiles(join(domainRoot, 'subscribers')),
83
+ tsFiles(join(domainRoot, 'actions')),
84
+ tsFiles(join(domainRoot, 'schedules')),
85
+ tsFiles(join(domainRoot, 'jobs')),
86
+ tsFiles(join(domainRoot, 'integrations')),
87
+ singleTs(join(domainRoot, 'publishes', 'events.ts')),
88
+ ]);
89
+
90
+ return { domain, apis, webhooks, subscribers, actions, schedules, jobs, integrations, publishes };
91
+ }
@@ -0,0 +1,126 @@
1
+ import path from 'path';
2
+
3
+ // Files where adding a key/comment would break schema validation — tracked via manifest only
4
+ const JSON_SKIP_LIST = new Set([
5
+ 'package.json',
6
+ 'tsconfig.json',
7
+ 'tsconfig.base.json',
8
+ ]);
9
+
10
+ /**
11
+ * Inject the @mc-scaffold ownership header into file content.
12
+ * The header format depends on the file type.
13
+ * For JSON files in the skip list, returns content unchanged.
14
+ *
15
+ * @param content The file content (post-token-substitution)
16
+ * @param filename The destination filename (used to determine type)
17
+ * @param moduleId The scaffold module id (e.g. 'backend-lambda')
18
+ * @param version The scaffold module version (e.g. '1.0.0')
19
+ */
20
+ export function injectHeader(
21
+ content: string,
22
+ filename: string,
23
+ moduleId: string,
24
+ version: string
25
+ ): string {
26
+ const basename = path.basename(filename);
27
+ const ext = path.extname(filename).toLowerCase();
28
+
29
+ if (basename.endsWith('.fixture.json')) {
30
+ return content;
31
+ }
32
+
33
+ if (ext === '.json') {
34
+ if (JSON_SKIP_LIST.has(basename)) {
35
+ return content;
36
+ }
37
+ return injectJsonHeader(content, moduleId, version);
38
+ }
39
+
40
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs') {
41
+ return injectLineCommentHeader(content, '//', moduleId, version);
42
+ }
43
+
44
+ if (ext === '.yml' || ext === '.yaml') {
45
+ return injectLineCommentHeader(content, '#', moduleId, version);
46
+ }
47
+
48
+ if (ext === '.css' || ext === '.scss') {
49
+ return injectBlockCommentHeader(content, '/*', '*/', moduleId, version);
50
+ }
51
+
52
+ if (ext === '.html') {
53
+ return injectBlockCommentHeader(content, '<!--', '-->', moduleId, version);
54
+ }
55
+
56
+ if (ext === '.md') {
57
+ return content;
58
+ }
59
+
60
+ return content;
61
+ }
62
+
63
+ function injectLineCommentHeader(
64
+ content: string,
65
+ commentChar: string,
66
+ moduleId: string,
67
+ version: string
68
+ ): string {
69
+ const header = [
70
+ `${commentChar} @mc-scaffold: ${moduleId}@${version}`,
71
+ `${commentChar} This file is managed by TIB scaffold. Manual edits will be flagged during \`tib upgrade\`.`,
72
+ `${commentChar} To opt out of upgrade management for this file, remove these header lines.`,
73
+ '',
74
+ ].join('\n');
75
+
76
+ // Preserve shebang line if present
77
+ if (content.startsWith('#!')) {
78
+ const newlineIdx = content.indexOf('\n');
79
+ if (newlineIdx === -1) return content + '\n' + header;
80
+ return content.slice(0, newlineIdx + 1) + header + content.slice(newlineIdx + 1);
81
+ }
82
+
83
+ return header + content;
84
+ }
85
+
86
+ function injectBlockCommentHeader(
87
+ content: string,
88
+ open: string,
89
+ close: string,
90
+ moduleId: string,
91
+ version: string
92
+ ): string {
93
+ const header = [
94
+ `${open} @mc-scaffold: ${moduleId}@${version} ${close}`,
95
+ `${open} This file is managed by TIB scaffold. Manual edits will be flagged during \`tib upgrade\`. ${close}`,
96
+ '',
97
+ ].join('\n');
98
+ return header + content;
99
+ }
100
+
101
+ function injectJsonHeader(content: string, moduleId: string, version: string): string {
102
+ try {
103
+ const parsed = JSON.parse(content) as Record<string, unknown>;
104
+ const withHeader = { _tibScaffold: `${moduleId}@${version}`, ...parsed };
105
+ return JSON.stringify(withHeader, null, 2);
106
+ } catch {
107
+ return content;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Strip the @mc-scaffold header from file content (used during upgrade conflict detection).
113
+ */
114
+ export function stripHeader(content: string, filename: string): string {
115
+ const ext = path.extname(filename).toLowerCase();
116
+
117
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs') {
118
+ return content.replace(/^\/\/ @mc-scaffold:.*\n\/\/ This file is.*\n\/\/ To opt out.*\n\n?/, '');
119
+ }
120
+
121
+ if (ext === '.yml' || ext === '.yaml') {
122
+ return content.replace(/^# @mc-scaffold:.*\n# This file is.*\n\n?/, '');
123
+ }
124
+
125
+ return content;
126
+ }
@@ -0,0 +1,99 @@
1
+ import { writeFile, mkdir, access } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { computeChecksumString, computeChecksumFile } from './checksum.js';
4
+ import type { ManifestFileEntry } from './manifest.js';
5
+
6
+ export type FilePolicy = 'owned' | 'tracked' | 'seed';
7
+
8
+ export interface InstallResult {
9
+ status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged';
10
+ }
11
+
12
+ /**
13
+ * Install a scaffold file according to its policy.
14
+ *
15
+ * - owned: always overwrite. Return 'added' if new, 'updated' if existed.
16
+ * - tracked: check sha256. If same → 'unchanged'. If different, check disk for drift.
17
+ * If drifted → write .tib-upgrade file → 'conflict'. If not drifted → overwrite → 'updated'.
18
+ * If no currentEntry → write new → 'added'.
19
+ * - seed: if file exists → 'skipped'. Else write → 'added'.
20
+ */
21
+ export async function installScaffoldFile(
22
+ absPath: string,
23
+ content: string,
24
+ policy: FilePolicy,
25
+ currentEntry: ManifestFileEntry | undefined,
26
+ opts?: { dryRun?: boolean }
27
+ ): Promise<InstallResult> {
28
+ const dryRun = opts?.dryRun ?? false;
29
+ const newChecksum = computeChecksumString(content);
30
+
31
+ // Helper to check if file exists
32
+ const fileExists = async (path: string): Promise<boolean> => {
33
+ try {
34
+ await access(path);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
40
+
41
+ if (policy === 'owned') {
42
+ // owned: always overwrite, no sha256 check
43
+ const exists = await fileExists(absPath);
44
+ if (!dryRun) {
45
+ await mkdir(dirname(absPath), { recursive: true });
46
+ await writeFile(absPath, content, 'utf-8');
47
+ }
48
+ return { status: exists ? 'updated' : 'added' };
49
+ }
50
+
51
+ if (policy === 'seed') {
52
+ // seed: never overwrite existing file
53
+ const exists = await fileExists(absPath);
54
+ if (exists) {
55
+ return { status: 'skipped' };
56
+ }
57
+ if (!dryRun) {
58
+ await mkdir(dirname(absPath), { recursive: true });
59
+ await writeFile(absPath, content, 'utf-8');
60
+ }
61
+ return { status: 'added' };
62
+ }
63
+
64
+ // policy === 'tracked'
65
+ if (!currentEntry) {
66
+ // New file: write it
67
+ if (!dryRun) {
68
+ await mkdir(dirname(absPath), { recursive: true });
69
+ await writeFile(absPath, content, 'utf-8');
70
+ }
71
+ return { status: 'added' };
72
+ }
73
+
74
+ // File exists in manifest: check if content changed
75
+ if (newChecksum === currentEntry.sha256) {
76
+ // Content unchanged
77
+ return { status: 'unchanged' };
78
+ }
79
+
80
+ // Content changed: check for drift
81
+ const diskChecksum = await computeChecksumFile(absPath);
82
+ const drifted = diskChecksum !== null && diskChecksum !== currentEntry.sha256;
83
+
84
+ if (drifted) {
85
+ // Drift detected: write .tib-upgrade file, don't touch original
86
+ if (!dryRun) {
87
+ await mkdir(dirname(absPath), { recursive: true });
88
+ await writeFile(`${absPath}.tib-upgrade`, content, 'utf-8');
89
+ }
90
+ return { status: 'conflict' };
91
+ }
92
+
93
+ // No drift: safe to overwrite
94
+ if (!dryRun) {
95
+ await mkdir(dirname(absPath), { recursive: true });
96
+ await writeFile(absPath, content, 'utf-8');
97
+ }
98
+ return { status: 'updated' };
99
+ }
@@ -0,0 +1,26 @@
1
+ import pino, { type Logger } from 'pino';
2
+
3
+ /**
4
+ * Factory that creates a pino logger with pino-pretty transport for CLI output.
5
+ * @param name - Optional logger name shown in pino-pretty output. Defaults to 'tib-domain'.
6
+ * @returns A pino Logger instance configured for human-readable terminal output.
7
+ */
8
+ export function createCliLogger(name = 'tib-domain'): Logger {
9
+ return pino({
10
+ name,
11
+ transport: {
12
+ target: 'pino-pretty',
13
+ options: {
14
+ colorize: true,
15
+ translateTime: 'SYS:HH:MM:ss',
16
+ ignore: 'pid,hostname',
17
+ },
18
+ },
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Default singleton CLI logger used by all domain-cli commands.
24
+ * Pretty-printed with timestamps to stdout.
25
+ */
26
+ export const cliLogger: Logger = createCliLogger();
@@ -0,0 +1,137 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import path from 'path';
3
+ import { cliLogger } from './logger.js';
4
+
5
+ export type FilePolicy = 'owned' | 'tracked' | 'seed';
6
+
7
+ export interface ManifestFileEntry {
8
+ path: string;
9
+ module: string;
10
+ moduleVersion: string;
11
+ sha256: string;
12
+ wasTemplate: boolean;
13
+ installedAt: string;
14
+ policy: FilePolicy;
15
+ }
16
+
17
+ export interface TibManifest {
18
+ $schema: string;
19
+ scaffoldVersion: string;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ projectName: string;
23
+ awsRegion: string;
24
+ enabledModules: string[];
25
+ files: ManifestFileEntry[];
26
+ }
27
+
28
+ const MANIFEST_PATH = '.mc/manifest.json';
29
+ const SCHEMA_URL = 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json';
30
+
31
+ export function inferPolicyFromPath(filePath: string): FilePolicy {
32
+ // owned: infra/, .mc/infra/, mc-deploy.yml, .github/workflows/
33
+ if (
34
+ filePath.match(/^infra\//) ||
35
+ filePath.match(/^\.tib\/infra\//) ||
36
+ filePath === 'mc-deploy.yml' ||
37
+ filePath.match(/^\.github\/workflows\//)
38
+ ) {
39
+ return 'owned';
40
+ }
41
+
42
+ // seed: specific frontend paths
43
+ const seedPatterns = [
44
+ /^frontend\/src\/pages\/auth\//,
45
+ /^frontend\/src\/pages\/account\//,
46
+ /^frontend\/src\/pages\/shell\//,
47
+ /^frontend\/src\/pages\/_errors\//,
48
+ /^frontend\/src\/layouts\//,
49
+ /^frontend\/src\/styles\/brand\.css$/,
50
+ ];
51
+ if (seedPatterns.some((pattern) => pattern.test(filePath))) {
52
+ return 'seed';
53
+ }
54
+
55
+ // tracked: everything else
56
+ return 'tracked';
57
+ }
58
+
59
+ export async function readManifest(projectRoot: string): Promise<TibManifest | null> {
60
+ const manifestPath = path.join(projectRoot, MANIFEST_PATH);
61
+ try {
62
+ const content = await readFile(manifestPath, 'utf-8');
63
+ const manifest = JSON.parse(content) as TibManifest;
64
+
65
+ // v1→v2 migration: infer policy for entries missing it
66
+ let needsMigration = false;
67
+ for (const entry of manifest.files as unknown as Array<Record<string, unknown>>) {
68
+ if (!entry['policy']) {
69
+ entry['policy'] = inferPolicyFromPath(entry['path'] as string);
70
+ needsMigration = true;
71
+ }
72
+ }
73
+
74
+ if (needsMigration) {
75
+ cliLogger.info('manifest: migrating v1→v2 with inferred policies');
76
+ await writeManifest(projectRoot, manifest);
77
+ }
78
+
79
+ return manifest;
80
+ } catch (err: unknown) {
81
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
82
+ throw err;
83
+ }
84
+ }
85
+
86
+ export async function writeManifest(projectRoot: string, manifest: TibManifest): Promise<void> {
87
+ const manifestPath = path.join(projectRoot, MANIFEST_PATH);
88
+ await mkdir(path.dirname(manifestPath), { recursive: true });
89
+ manifest.$schema = SCHEMA_URL;
90
+ manifest.updatedAt = new Date().toISOString();
91
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
92
+ }
93
+
94
+ export function createManifest(
95
+ scaffoldVersion: string,
96
+ projectName: string,
97
+ awsRegion: string,
98
+ enabledModules: string[]
99
+ ): TibManifest {
100
+ const now = new Date().toISOString();
101
+ return {
102
+ $schema: SCHEMA_URL,
103
+ scaffoldVersion,
104
+ createdAt: now,
105
+ updatedAt: now,
106
+ projectName,
107
+ awsRegion,
108
+ enabledModules,
109
+ files: [],
110
+ };
111
+ }
112
+
113
+ export function upsertManifestFile(manifest: TibManifest, entry: ManifestFileEntry): void {
114
+ const idx = manifest.files.findIndex((f) => f.path === entry.path);
115
+ if (idx === -1) {
116
+ manifest.files.push(entry);
117
+ } else {
118
+ manifest.files[idx] = entry;
119
+ }
120
+ }
121
+
122
+ export function removeManifestFile(manifest: TibManifest, filePath: string): void {
123
+ manifest.files = manifest.files.filter((f) => f.path !== filePath);
124
+ }
125
+
126
+ export function getManifestFile(
127
+ manifest: TibManifest,
128
+ filePath: string
129
+ ): ManifestFileEntry | undefined {
130
+ return manifest.files.find((f) => f.path === filePath);
131
+ }
132
+
133
+ export function isScaffoldOwned(manifest: TibManifest, filePath: string): boolean {
134
+ const entry = manifest.files.find((f) => f.path === filePath);
135
+ // Scaffold-managed means in manifest AND policy is not 'seed'
136
+ return entry != null && entry.policy !== 'seed';
137
+ }