@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,179 @@
1
+ /**
2
+ * Command to show a structured JSON summary of a domain's structure.
3
+ * Reads the domains/<id>/ directory and builds a summary of primitives,
4
+ * tables, events, and files.
5
+ */
6
+
7
+ import { readdir, stat } from 'node:fs/promises';
8
+ import { join, resolve, basename } from 'node:path';
9
+ import { cliLogger } from '../utils/logger.js';
10
+
11
+ /**
12
+ * Options for the show command.
13
+ */
14
+ export interface ShowDomainOptions {
15
+ /** Domain ID in kebab-case. */
16
+ domain: string;
17
+ /** Override directory for domains. */
18
+ domainsDir?: string;
19
+ }
20
+
21
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
22
+
23
+ /**
24
+ * Represents a primitive entry (API, subscriber, action, or job).
25
+ */
26
+ interface PrimitiveEntry {
27
+ type: 'api' | 'subscriber' | 'action' | 'job';
28
+ id: string;
29
+ file: string;
30
+ }
31
+
32
+ /**
33
+ * Represents a domain summary.
34
+ */
35
+ interface DomainSummary {
36
+ domain: string;
37
+ primitives: {
38
+ apis: PrimitiveEntry[];
39
+ subscribers: PrimitiveEntry[];
40
+ actions: PrimitiveEntry[];
41
+ jobs: PrimitiveEntry[];
42
+ };
43
+ tables: string[];
44
+ events: string[];
45
+ files: string[];
46
+ }
47
+
48
+ /**
49
+ * Recursively list all files in a directory, excluding node_modules and __tests__.
50
+ */
51
+ async function listFilesRecursive(dir: string): Promise<string[]> {
52
+ const result: string[] = [];
53
+
54
+ async function walk(current: string, base: string): Promise<void> {
55
+ try {
56
+ const entries = await readdir(current, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const fullPath = join(current, entry.name);
59
+ const relativePath = join(base, entry.name);
60
+ if (entry.isDirectory()) {
61
+ if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
62
+ await walk(fullPath, relativePath);
63
+ } else {
64
+ result.push(relativePath.replace(/\\/g, '/'));
65
+ }
66
+ }
67
+ } catch {
68
+ // Silently skip inaccessible directories
69
+ }
70
+ }
71
+
72
+ await walk(dir, '');
73
+ return result.sort();
74
+ }
75
+
76
+ /**
77
+ * Scan a subdirectory for TypeScript primitives.
78
+ */
79
+ async function scanPrimitives(dir: string, type: PrimitiveEntry['type']): Promise<PrimitiveEntry[]> {
80
+ const results: PrimitiveEntry[] = [];
81
+ try {
82
+ const entries = await readdir(dir, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ if (entry.isFile() && entry.name.endsWith('.ts')) {
85
+ const id = entry.name.replace(/\.ts$/, '');
86
+ results.push({
87
+ type,
88
+ id,
89
+ file: `${type === 'subscriber' ? 'subscribers' : type + 's'}/${entry.name}`,
90
+ });
91
+ }
92
+ }
93
+ } catch {
94
+ // Directory doesn't exist — no primitives of this type
95
+ }
96
+ return results.sort((a, b) => a.id.localeCompare(b.id));
97
+ }
98
+
99
+ /**
100
+ * Execute the show domain command.
101
+ */
102
+ export async function runShowDomain(opts: ShowDomainOptions): Promise<DomainSummary> {
103
+ if (!KEBAB_REGEX.test(opts.domain)) {
104
+ throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
105
+ }
106
+
107
+ const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
108
+ const domainDir = join(root, opts.domain);
109
+
110
+ try {
111
+ await stat(domainDir);
112
+ } catch {
113
+ throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
114
+ }
115
+
116
+ // Scan primitives
117
+ const apis = await scanPrimitives(join(domainDir, 'api'), 'api');
118
+ const subscribers = await scanPrimitives(join(domainDir, 'subscribers'), 'subscriber');
119
+ const actions = await scanPrimitives(join(domainDir, 'actions'), 'action');
120
+ const jobs = await scanPrimitives(join(domainDir, 'jobs'), 'job');
121
+
122
+ // Scan migrations for table definitions
123
+ const tables: string[] = [];
124
+ try {
125
+ const migrationDir = join(domainDir, 'db', 'migrations');
126
+ const migrationFiles = await readdir(migrationDir, { withFileTypes: true });
127
+ for (const entry of migrationFiles) {
128
+ if (entry.isFile() && entry.name.endsWith('.sql')) {
129
+ tables.push(entry.name);
130
+ }
131
+ }
132
+ tables.sort();
133
+ } catch {
134
+ // No migrations directory
135
+ }
136
+
137
+ // Scan events
138
+ const events: string[] = [];
139
+ try {
140
+ const publishesDir = join(domainDir, 'publishes');
141
+ const publishFiles = await readdir(publishesDir, { withFileTypes: true });
142
+ for (const entry of publishFiles) {
143
+ if (entry.isFile() && entry.name.endsWith('.ts')) {
144
+ events.push(entry.name);
145
+ }
146
+ }
147
+ events.sort();
148
+ } catch {
149
+ // No publishes directory
150
+ }
151
+
152
+ // List all files
153
+ const files = await listFilesRecursive(domainDir);
154
+
155
+ const summary: DomainSummary = {
156
+ domain: opts.domain,
157
+ primitives: {
158
+ apis,
159
+ subscribers,
160
+ actions,
161
+ jobs,
162
+ },
163
+ tables,
164
+ events,
165
+ files,
166
+ };
167
+
168
+ cliLogger.info({ domain: opts.domain }, 'Domain summary generated');
169
+ return summary;
170
+ }
171
+
172
+ /**
173
+ * CLI entry point: print the structured JSON to stdout.
174
+ */
175
+ export async function runShowDomainCli(opts: ShowDomainOptions): Promise<void> {
176
+ const summary = await runShowDomain(opts);
177
+ // eslint-disable-next-line no-console
178
+ console.log(JSON.stringify(summary, null, 2));
179
+ }
@@ -0,0 +1,91 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, join } from 'node:path';
3
+ import type { RegistryEntry } from '@mettlecast/domain-cdk-packer';
4
+ import { buildRegistry } from '../builder/build-registry.js';
5
+ import { hydrateLocalCtx } from '../runtime-stubs/hydrate-local-ctx.js';
6
+ import { cliLogger } from '../utils/logger.js';
7
+
8
+ /**
9
+ * Options for the test command.
10
+ */
11
+ export interface TestOptions {
12
+ /** Absolute or relative path to the domain root. */
13
+ domainRoot: string;
14
+ /** Handler id to invoke (e.g. 'get-invoice'). */
15
+ handlerId: string;
16
+ /** Absolute or relative path to the fixture JSON file. */
17
+ fixturePath: string;
18
+ }
19
+
20
+ interface Fixture {
21
+ event: unknown;
22
+ ctx?: {
23
+ tenantId?: string;
24
+ actorSub?: string;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Execute the test command: build the registry, find the named handler, load the fixture,
30
+ * hydrate a local dev ctx, invoke the handler, and print results to stdout as JSON.
31
+ * @param options - TestOptions specifying domain root, handler id, and fixture path.
32
+ * @throws If the handler id is not found in the registry or the fixture file cannot be read.
33
+ */
34
+ export async function runTest(options: TestOptions): Promise<void> {
35
+ const domainRoot = resolve(options.domainRoot);
36
+ const fixturePath = resolve(options.fixturePath);
37
+
38
+ const { registry } = await buildRegistry(domainRoot);
39
+
40
+ const allEntries: RegistryEntry[] = [
41
+ ...registry.apis,
42
+ ...registry.webhooks,
43
+ ...registry.subscribers,
44
+ ...registry.schedules,
45
+ ...registry.jobs,
46
+ ...registry.actions,
47
+ ];
48
+
49
+ const entry = allEntries.find(e => e.id === options.handlerId);
50
+ if (!entry) {
51
+ throw new Error(`runTest: handler '${options.handlerId}' not found. Available: ${allEntries.map(e => e.id).join(', ')}`);
52
+ }
53
+
54
+ if (!entry.handlerFile) {
55
+ throw new Error(`runTest: entry '${entry.id}' has no handlerFile`);
56
+ }
57
+
58
+ const fixtureRaw = await readFile(fixturePath, 'utf8');
59
+ const fixture = JSON.parse(fixtureRaw) as Fixture;
60
+
61
+ const { ctx, captures } = await hydrateLocalCtx({
62
+ domainRoot,
63
+ tenantId: fixture.ctx?.tenantId,
64
+ actorSub: fixture.ctx?.actorSub,
65
+ });
66
+
67
+ const handlerAbsPath = join(domainRoot, entry.handlerFile);
68
+ const mod = await import(handlerAbsPath) as Record<string, unknown>;
69
+ const def = Object.values(mod).find(
70
+ v => v && typeof v === 'object' && (v as Record<string, unknown>)['id'] === entry.id
71
+ ) as Record<string, unknown> | undefined;
72
+
73
+ if (!def || typeof def['handler'] !== 'function') {
74
+ throw new Error(`runTest: no handler function found for '${entry.id}' in ${handlerAbsPath}`);
75
+ }
76
+
77
+ const handler = def['handler'] as (event: unknown, ctx: unknown) => Promise<unknown>;
78
+
79
+ cliLogger.info({ handlerId: entry.id, kind: entry.kind }, 'Invoking handler');
80
+
81
+ let result: unknown;
82
+ try {
83
+ result = await handler(fixture.event, ctx);
84
+ } catch (err) {
85
+ cliLogger.error({ err }, 'Handler threw an error');
86
+ process.stdout.write(JSON.stringify({ error: String(err), captures }, null, 2) + '\n');
87
+ process.exit(1);
88
+ }
89
+
90
+ process.stdout.write(JSON.stringify({ result, captures }, null, 2) + '\n');
91
+ }
@@ -0,0 +1,72 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ export interface UpgradeBackendOptions {
6
+ /** Target major version, e.g. '2' */
7
+ targetMajor: string;
8
+ /** Root of the repo (defaults to cwd) */
9
+ repoRoot?: string;
10
+ /** Dry run — print changes without applying */
11
+ dryRun?: boolean;
12
+ }
13
+
14
+ interface Migration {
15
+ fromMajor: number;
16
+ toMajor: number;
17
+ description: string;
18
+ transform: (repoRoot: string, dryRun: boolean) => void;
19
+ }
20
+
21
+ const MIGRATIONS: Migration[] = [
22
+ {
23
+ fromMajor: 1,
24
+ toMajor: 2,
25
+ description: 'Migrate defineApi from v1 to v2: rename `versions.v1.handler` shape',
26
+ transform(repoRoot: string, dryRun: boolean): void {
27
+ // Placeholder: real migration uses ts-morph to rewrite handler signatures
28
+ console.log(`[G20] Would apply: defineApi v1→v2 migration in ${repoRoot}/domains/**`);
29
+ if (!dryRun) {
30
+ console.log('[G20] ts-morph transform: (not yet implemented — add jscodeshift transforms here)');
31
+ }
32
+ },
33
+ },
34
+ ];
35
+
36
+ export async function runUpgradeBackend(opts: UpgradeBackendOptions): Promise<void> {
37
+ const { targetMajor, repoRoot = process.cwd(), dryRun = false } = opts;
38
+ const target = parseInt(targetMajor, 10);
39
+ if (isNaN(target) || target < 1) {
40
+ throw new Error(`Invalid target major version: ${targetMajor}`);
41
+ }
42
+
43
+ // Detect current major from package.json
44
+ const pkgPath = join(repoRoot, 'packages', 'domain-runtime', 'package.json');
45
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };
46
+ const currentMajor = parseInt(pkg.version.split('.')[0] ?? '1', 10);
47
+
48
+ if (currentMajor >= target) {
49
+ console.log(`Already at v${currentMajor} — nothing to upgrade.`);
50
+ return;
51
+ }
52
+
53
+ const applicableMigrations = MIGRATIONS.filter(
54
+ m => m.fromMajor >= currentMajor && m.toMajor <= target,
55
+ );
56
+
57
+ if (applicableMigrations.length === 0) {
58
+ console.log(`No migrations found from v${currentMajor} to v${target}.`);
59
+ return;
60
+ }
61
+
62
+ for (const migration of applicableMigrations) {
63
+ console.log(`Applying: v${migration.fromMajor} → v${migration.toMajor}: ${migration.description}`);
64
+ migration.transform(repoRoot, dryRun);
65
+ }
66
+
67
+ if (!dryRun) {
68
+ console.log(`\nUpgrade to v${target} complete. Run: npm run lint && npm test`);
69
+ } else {
70
+ console.log(`\n[dry-run] No files changed.`);
71
+ }
72
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Command to upgrade an event definition to a new version.
3
+ * Reads the existing event definition from publishes/events.ts,
4
+ * adds a new version entry with proper sunset semantics on the previous version.
5
+ */
6
+
7
+ import { readFile, writeFile, access } from 'node:fs/promises';
8
+ import { join, resolve } from 'node:path';
9
+ import { cliLogger } from '../utils/logger.js';
10
+
11
+ /**
12
+ * Options for the upgrade-event command.
13
+ */
14
+ export interface UpgradeEventOptions {
15
+ /** Domain ID in kebab-case. */
16
+ domain: string;
17
+ /** Event ID (e.g., 'domain.event'). */
18
+ eventId: string;
19
+ /** Target version number. */
20
+ toVersion: number;
21
+ /** Override directory for domain root. */
22
+ domainsDir?: string;
23
+ }
24
+
25
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
26
+
27
+ /**
28
+ * Execute the upgrade-event command: bump an event definition to a new version.
29
+ */
30
+ export async function runUpgradeEvent(opts: UpgradeEventOptions): Promise<void> {
31
+ if (!KEBAB_REGEX.test(opts.domain)) {
32
+ throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
33
+ }
34
+
35
+ if (!Number.isInteger(opts.toVersion) || opts.toVersion < 2) {
36
+ throw new Error(`--to-version must be an integer >= 2, got "${opts.toVersion}"`);
37
+ }
38
+
39
+ const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
40
+ const domainDir = join(root, opts.domain);
41
+ const eventsFilePath = join(domainDir, 'publishes', 'events.ts');
42
+
43
+ // Verify events file exists
44
+ try {
45
+ await access(eventsFilePath);
46
+ } catch {
47
+ throw new Error(`Events file not found at ${eventsFilePath}. Run add-domain first.`);
48
+ }
49
+
50
+ const content = await readFile(eventsFilePath, 'utf8');
51
+ const newContent = upgradeEventInSource(content, opts.eventId, opts.toVersion, opts.domain);
52
+
53
+ if (newContent === content) {
54
+ throw new Error(
55
+ `Event "${opts.eventId}" not found in ${eventsFilePath}, or version ${opts.toVersion - 1} not found.`,
56
+ );
57
+ }
58
+
59
+ await writeFile(eventsFilePath, newContent);
60
+
61
+ cliLogger.info({ domain: opts.domain, eventId: opts.eventId, toVersion: opts.toVersion }, 'Event upgraded');
62
+ // eslint-disable-next-line no-console
63
+ console.log(
64
+ `\n✓ Event "${opts.eventId}" upgraded to version ${opts.toVersion} in ${eventsFilePath}`,
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Upgrade an event definition in-source by adding a new version entry
70
+ * and marking the previous version as deprecated with a sunset date.
71
+ */
72
+ function upgradeEventInSource(
73
+ content: string,
74
+ eventId: string,
75
+ toVersion: number,
76
+ domain: string,
77
+ ): string {
78
+ const prevVersion = toVersion - 1;
79
+ const sunsetDate = new Date(Date.now() + 180 * 86400 * 1000).toISOString().split('T')[0]; // 180 days from now
80
+
81
+ // Find the event definition block
82
+ // Pattern: export const eventName = defineEvent({ ... versions: [ ... ] ... });
83
+ const eventPattern = new RegExp(
84
+ `(export\\s+const\\s+\\w+\\s*=\\s*defineEvent\\(\\{[^}]*id:\\s*['"]${escapeRegex(eventId)}['"][^}]*versions:\\s*\\[)([^\\]]*)(\\][^}]*\\}\\);)`,
85
+ 's',
86
+ );
87
+
88
+ const match = eventPattern.exec(content);
89
+ if (!match) {
90
+ // Try without the export name assumption — search for the eventId
91
+ const altPattern = new RegExp(
92
+ `('${escapeRegex(eventId)}'|"${escapeRegex(eventId)}")\\s*,\\s*\\n?\\s*versions:\\s*\\[`,
93
+ 's',
94
+ );
95
+ const altMatch = altPattern.exec(content);
96
+ if (!altMatch) {
97
+ return content; // Not found — caller will detect unchanged content
98
+ }
99
+ throw new Error(`Found event "${eventId}" but could not parse its versions array. Manual edit required.`);
100
+ }
101
+
102
+ const beforeVersions = match[1];
103
+ const versionsContent = match[2];
104
+ const afterVersions = match[3];
105
+
106
+ // Check if the target version already exists
107
+ if (versionsContent.includes(`version: ${toVersion}`)) {
108
+ throw new Error(`Event "${eventId}" already has version ${toVersion}.`);
109
+ }
110
+
111
+ // Find the previous version and mark it deprecated
112
+ let updatedVersions = versionsContent;
113
+
114
+ // Add sunset to the previous version entry
115
+ const prevVersionRegex = new RegExp(
116
+ `(version:\\s*${prevVersion}\\s*,[^}]*?)(\\s*\\})`,
117
+ 's',
118
+ );
119
+ const prevMatch = prevVersionRegex.exec(updatedVersions);
120
+ if (prevMatch) {
121
+ // Check if already has status
122
+ if (!prevMatch[1].includes('status:')) {
123
+ updatedVersions = updatedVersions.replace(
124
+ prevMatch[0],
125
+ prevMatch[1].replace(
126
+ `version: ${prevVersion},`,
127
+ `version: ${prevVersion},\n status: 'deprecated',\n sunset: '${sunsetDate}',`,
128
+ ) + prevMatch[2],
129
+ );
130
+ } else if (!prevMatch[1].includes('sunset:')) {
131
+ updatedVersions = updatedVersions.replace(
132
+ prevMatch[0],
133
+ prevMatch[1].replace(
134
+ /status:\s*'stable'/,
135
+ "status: 'deprecated'",
136
+ ) + `\n sunset: '${sunsetDate}',` + prevMatch[2],
137
+ );
138
+ }
139
+ } else {
140
+ throw new Error(`Could not find version ${prevVersion} in event "${eventId}".`);
141
+ }
142
+
143
+ // Add the new version entry
144
+ const newVersionEntry = `,
145
+ {
146
+ version: ${toVersion},
147
+ status: 'stable',
148
+ schema: z.object({
149
+ // TODO: Define the v${toVersion} schema — extend or replace v${prevVersion}
150
+ }),
151
+ }`;
152
+
153
+ // Insert before the closing bracket of the versions array
154
+ const closingBracketIndex = updatedVersions.lastIndexOf(']');
155
+ if (closingBracketIndex === -1) {
156
+ throw new Error(`Could not find closing bracket of versions array for event "${eventId}".`);
157
+ }
158
+
159
+ updatedVersions =
160
+ updatedVersions.slice(0, closingBracketIndex) +
161
+ newVersionEntry +
162
+ '\n ' +
163
+ updatedVersions.slice(closingBracketIndex);
164
+
165
+ const result = beforeVersions + updatedVersions + afterVersions;
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Escape special regex characters in a string.
172
+ */
173
+ function escapeRegex(s: string): string {
174
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
175
+ }
@@ -0,0 +1,135 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { cliLogger } from '../utils/logger.js';
4
+
5
+ export interface UpgradeFrontendOptions {
6
+ dryRun?: boolean;
7
+ projectDir?: string;
8
+ }
9
+
10
+ interface PackageJson {
11
+ name?: string;
12
+ dependencies?: Record<string, string>;
13
+ devDependencies?: Record<string, string>;
14
+ }
15
+
16
+ const TIB_PACKAGES = [
17
+ '@mettlecast/design-system',
18
+ '@mettlecast/dashboard-shell',
19
+ '@mettlecast/sdk',
20
+ '@mettlecast/observability',
21
+ '@mettlecast/eslint-config-react',
22
+ '@mettlecast/tsconfig-react',
23
+ ] as const;
24
+
25
+ type TibPackage = (typeof TIB_PACKAGES)[number];
26
+
27
+ const REQUIRED_PACKAGES: TibPackage[] = [
28
+ '@mettlecast/design-system',
29
+ '@mettlecast/dashboard-shell',
30
+ '@mettlecast/sdk',
31
+ ];
32
+
33
+ const RECOMMENDED_PACKAGES: TibPackage[] = [
34
+ '@mettlecast/observability',
35
+ '@mettlecast/eslint-config-react',
36
+ '@mettlecast/tsconfig-react',
37
+ ];
38
+
39
+ interface CheckResult {
40
+ pkg: string;
41
+ status: 'ok' | 'missing' | 'outdated' | 'recommended';
42
+ installedVersion?: string;
43
+ message: string;
44
+ }
45
+
46
+ function allDeps(pkg: PackageJson): Record<string, string> {
47
+ return { ...pkg.dependencies, ...pkg.devDependencies };
48
+ }
49
+
50
+ export async function runUpgradeFrontend(opts: UpgradeFrontendOptions = {}): Promise<void> {
51
+ const root = opts.projectDir ? resolve(opts.projectDir) : process.cwd();
52
+ const pkgPath = resolve(root, 'package.json');
53
+
54
+ let pkg: PackageJson;
55
+ try {
56
+ const raw = await readFile(pkgPath, 'utf8');
57
+ pkg = JSON.parse(raw) as PackageJson;
58
+ } catch {
59
+ throw new Error(`Could not read package.json at ${pkgPath}`);
60
+ }
61
+
62
+ const deps = allDeps(pkg);
63
+ const results: CheckResult[] = [];
64
+
65
+ for (const p of REQUIRED_PACKAGES) {
66
+ const installed = deps[p];
67
+ if (!installed) {
68
+ results.push({ pkg: p, status: 'missing', message: `Not installed — required. Add to dependencies.` });
69
+ } else {
70
+ results.push({ pkg: p, status: 'ok', installedVersion: installed, message: `Installed at ${installed}` });
71
+ }
72
+ }
73
+
74
+ for (const p of RECOMMENDED_PACKAGES) {
75
+ const installed = deps[p];
76
+ if (!installed) {
77
+ results.push({ pkg: p, status: 'recommended', message: `Not installed — recommended for full functionality.` });
78
+ } else {
79
+ results.push({ pkg: p, status: 'ok', installedVersion: installed, message: `Installed at ${installed}` });
80
+ }
81
+ }
82
+
83
+ // Print results
84
+ const symbols: Record<CheckResult['status'], string> = {
85
+ ok: '✓',
86
+ missing: '✗',
87
+ outdated: '⚠',
88
+ recommended: '·',
89
+ };
90
+
91
+ // eslint-disable-next-line no-console
92
+ console.log(`\nTIB frontend upgrade check for ${pkg.name ?? root}\n`);
93
+
94
+ let hasErrors = false;
95
+ for (const r of results) {
96
+ // eslint-disable-next-line no-console
97
+ console.log(` ${symbols[r.status]} ${r.pkg.padEnd(32)} ${r.message}`);
98
+ if (r.status === 'missing') hasErrors = true;
99
+ }
100
+
101
+ const missingCount = results.filter(r => r.status === 'missing').length;
102
+ const recommendedCount = results.filter(r => r.status === 'recommended').length;
103
+
104
+ // eslint-disable-next-line no-console
105
+ console.log('');
106
+
107
+ if (missingCount > 0) {
108
+ // eslint-disable-next-line no-console
109
+ console.log(` ${missingCount} required package(s) missing. Run:\n`);
110
+ const missing = results.filter(r => r.status === 'missing').map(r => r.pkg);
111
+ // eslint-disable-next-line no-console
112
+ console.log(` npm install ${missing.join(' ')}\n`);
113
+ }
114
+
115
+ if (recommendedCount > 0) {
116
+ // eslint-disable-next-line no-console
117
+ console.log(` ${recommendedCount} recommended package(s) not installed. See docs/migration/v0-to-v1.md\n`);
118
+ }
119
+
120
+ if (!hasErrors && missingCount === 0) {
121
+ // eslint-disable-next-line no-console
122
+ console.log(' All required @mettlecast/* packages are installed.\n');
123
+ }
124
+
125
+ if (opts.dryRun) {
126
+ // eslint-disable-next-line no-console
127
+ console.log(' (dry-run mode — no changes made)\n');
128
+ }
129
+
130
+ cliLogger.info({ missingCount, recommendedCount }, 'upgrade-frontend check complete');
131
+
132
+ if (hasErrors && !opts.dryRun) {
133
+ process.exit(1);
134
+ }
135
+ }