@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,293 @@
1
+ /**
2
+ * Wizard page pattern template — multi-step form with progress bar.
3
+ */
4
+ export function wizardPageTemplate(pageName) {
5
+ const componentName = `${pageName}Page`;
6
+ return `import { useState, useCallback } from 'react';
7
+ import { useNavigate } from 'react-router-dom';
8
+ import { useForm } from 'react-hook-form';
9
+ import { zodResolver } from '@hookform/resolvers/zod';
10
+ import { z } from 'zod';
11
+ import { ArrowLeft, ArrowRight, Check, Save } from 'lucide-react';
12
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
13
+ import { useTib } from '@/hooks/useTib';
14
+ import { Page } from '@/components/layout/Page';
15
+ import { PageHeader } from '@/components/layout/PageHeader';
16
+ import { Button } from '@/components/ui/button';
17
+ import { Input } from '@/components/ui/input';
18
+ import { Label } from '@/components/ui/label';
19
+ import { Textarea } from '@/components/ui/textarea';
20
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
21
+ import { Progress } from '@/components/ui/progress';
22
+ import { toast } from 'sonner';
23
+
24
+ interface ${componentName}Props {
25
+ workspaceId: string;
26
+ }
27
+
28
+ /** Steps definition */
29
+ const STEPS = [
30
+ { id: 'basics', label: 'Basics', description: 'Basic information' },
31
+ { id: 'details', label: 'Details', description: 'Additional details' },
32
+ { id: 'review', label: 'Review', description: 'Review and confirm' },
33
+ ] as const;
34
+
35
+ type StepId = (typeof STEPS)[number]['id'];
36
+
37
+ /** Full form schema across all steps */
38
+ const wizardSchema = z.object({
39
+ name: z.string().min(1, 'Name is required'),
40
+ description: z.string().optional(),
41
+ type: z.string().min(1, 'Type is required'),
42
+ status: z.enum(['active', 'inactive']),
43
+ notes: z.string().optional(),
44
+ });
45
+
46
+ type WizardValues = z.infer<typeof wizardSchema>;
47
+
48
+ export function ${componentName}({ workspaceId }: ${componentName}Props) {
49
+ const tib = useTib();
50
+ const navigate = useNavigate();
51
+ const queryClient = useQueryClient();
52
+ const [currentStep, setCurrentStep] = useState<number>(0);
53
+ const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set([0]));
54
+
55
+ const form = useForm<WizardValues>({
56
+ resolver: zodResolver(wizardSchema),
57
+ defaultValues: {
58
+ name: '',
59
+ description: '',
60
+ type: '',
61
+ status: 'active',
62
+ notes: '',
63
+ },
64
+ mode: 'onChange',
65
+ });
66
+
67
+ const { register, handleSubmit, formState: { errors }, setValue, watch, trigger } = form;
68
+
69
+ const mutation = useMutation({
70
+ mutationFn: (values: WizardValues) =>
71
+ tib.data.create({ workspaceId, ...values }),
72
+ onSuccess: () => {
73
+ toast.success('\${pageName} created successfully');
74
+ queryClient.invalidateQueries({ queryKey: ['data-list', workspaceId] });
75
+ navigate('..');
76
+ },
77
+ onError: (err) => {
78
+ toast.error('Failed to create', {
79
+ description: err instanceof Error ? err.message : 'Unknown error',
80
+ });
81
+ },
82
+ });
83
+
84
+ const progress = ((completedSteps.size) / STEPS.length) * 100;
85
+
86
+ const isFirstStep = currentStep === 0;
87
+ const isLastStep = currentStep === STEPS.length - 1;
88
+
89
+ const goNext = useCallback(async () => {
90
+ const fieldsToValidate = getFieldsForStep(currentStep);
91
+ const valid = await trigger(fieldsToValidate);
92
+ if (valid) {
93
+ const next = currentStep + 1;
94
+ setCompletedSteps(prev => new Set([...prev, next]));
95
+ setCurrentStep(next);
96
+ }
97
+ }, [currentStep, trigger]);
98
+
99
+ const goBack = useCallback(() => {
100
+ setCurrentStep(prev => Math.max(0, prev - 1));
101
+ }, []);
102
+
103
+ const onSubmit = (values: WizardValues) => mutation.mutate(values);
104
+
105
+ return (
106
+ <Page title="Create ${pageName}" description="Follow the steps to create a new entity">
107
+ <Page.Header
108
+ breadcrumb={[
109
+ { label: '${pageName}', to: '..' },
110
+ { label: 'Create' },
111
+ ]}
112
+ />
113
+ <Page.Body>
114
+ <div className="max-w-2xl mx-auto">
115
+ {/* Progress bar */}
116
+ <div className="mb-8">
117
+ <Progress value={progress} className="h-2" />
118
+ <div className="flex justify-between mt-2">
119
+ {STEPS.map((step, idx) => (
120
+ <div
121
+ key={step.id}
122
+ className={[
123
+ 'flex items-center gap-2 text-sm',
124
+ idx <= currentStep ? 'text-primary' : 'text-muted-foreground',
125
+ ].join(' ')}
126
+ >
127
+ <span className={[
128
+ 'flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium',
129
+ completedSteps.has(idx)
130
+ ? 'bg-primary text-primary-foreground'
131
+ : 'border border-muted-foreground text-muted-foreground',
132
+ ].join(' ')}>
133
+ {completedSteps.has(idx) ? <Check className="h-3 w-3" /> : idx + 1}
134
+ </span>
135
+ <span className="hidden sm:inline">{step.label}</span>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ </div>
140
+
141
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
142
+ {/* Step 1: Basics */}
143
+ {currentStep === 0 && (
144
+ <div className="space-y-4">
145
+ <h2 className="text-lg font-semibold">Basic Information</h2>
146
+ <div className="space-y-2">
147
+ <Label htmlFor="name">Name</Label>
148
+ <Input
149
+ id="name"
150
+ {...register('name')}
151
+ placeholder="Enter name"
152
+ aria-invalid={!!errors.name}
153
+ />
154
+ {errors.name && (
155
+ <p className="text-xs text-destructive">{errors.name.message}</p>
156
+ )}
157
+ </div>
158
+ <div className="space-y-2">
159
+ <Label htmlFor="description">Description</Label>
160
+ <Textarea
161
+ id="description"
162
+ {...register('description')}
163
+ placeholder="Optional description"
164
+ rows={3}
165
+ />
166
+ </div>
167
+ <div className="space-y-2">
168
+ <Label htmlFor="type">Type</Label>
169
+ <Input
170
+ id="type"
171
+ {...register('type')}
172
+ placeholder="Enter type"
173
+ aria-invalid={!!errors.type}
174
+ />
175
+ {errors.type && (
176
+ <p className="text-xs text-destructive">{errors.type.message}</p>
177
+ )}
178
+ </div>
179
+ </div>
180
+ )}
181
+
182
+ {/* Step 2: Details */}
183
+ {currentStep === 1 && (
184
+ <div className="space-y-4">
185
+ <h2 className="text-lg font-semibold">Additional Details</h2>
186
+ <div className="space-y-2">
187
+ <Label htmlFor="wiz-status">Status</Label>
188
+ <Select
189
+ value={watch('status')}
190
+ onValueChange={(value) => setValue('status', value as 'active' | 'inactive')}
191
+ >
192
+ <SelectTrigger id="wiz-status">
193
+ <SelectValue placeholder="Select status" />
194
+ </SelectTrigger>
195
+ <SelectContent>
196
+ <SelectItem value="active">Active</SelectItem>
197
+ <SelectItem value="inactive">Inactive</SelectItem>
198
+ </SelectContent>
199
+ </Select>
200
+ </div>
201
+ <div className="space-y-2">
202
+ <Label htmlFor="notes">Notes</Label>
203
+ <Textarea
204
+ id="notes"
205
+ {...register('notes')}
206
+ placeholder="Add any notes"
207
+ rows={3}
208
+ />
209
+ </div>
210
+ </div>
211
+ )}
212
+
213
+ {/* Step 3: Review */}
214
+ {currentStep === 2 && (
215
+ <div className="space-y-4">
216
+ <h2 className="text-lg font-semibold">Review & Confirm</h2>
217
+ <div className="rounded-lg border border-border p-4 space-y-3">
218
+ <div className="flex justify-between">
219
+ <span className="text-sm text-muted-foreground">Name</span>
220
+ <span className="text-sm font-medium">{watch('name') || '—'}</span>
221
+ </div>
222
+ <div className="flex justify-between">
223
+ <span className="text-sm text-muted-foreground">Description</span>
224
+ <span className="text-sm font-medium">{watch('description') || '—'}</span>
225
+ </div>
226
+ <div className="flex justify-between">
227
+ <span className="text-sm text-muted-foreground">Type</span>
228
+ <span className="text-sm font-medium">{watch('type') || '—'}</span>
229
+ </div>
230
+ <div className="flex justify-between">
231
+ <span className="text-sm text-muted-foreground">Status</span>
232
+ <span className="text-sm font-medium">{watch('status')}</span>
233
+ </div>
234
+ <div className="flex justify-between">
235
+ <span className="text-sm text-muted-foreground">Notes</span>
236
+ <span className="text-sm font-medium">{watch('notes') || '—'}</span>
237
+ </div>
238
+ </div>
239
+ <p className="text-xs text-muted-foreground">
240
+ Review the details above and click &ldquo;Create&rdquo; to finalize.
241
+ </p>
242
+ </div>
243
+ )}
244
+
245
+ {/* Navigation buttons */}
246
+ <div className="flex items-center justify-between pt-4 border-t border-border">
247
+ <Button
248
+ type="button"
249
+ variant="outline"
250
+ size="sm"
251
+ onClick={goBack}
252
+ disabled={isFirstStep}
253
+ >
254
+ <ArrowLeft className="h-4 w-4 mr-1" />
255
+ Back
256
+ </Button>
257
+ {!isLastStep ? (
258
+ <Button type="button" variant="teal" size="sm" onClick={goNext}>
259
+ Next
260
+ <ArrowRight className="h-4 w-4 ml-1" />
261
+ </Button>
262
+ ) : (
263
+ <Button
264
+ type="submit"
265
+ variant="teal"
266
+ size="sm"
267
+ disabled={mutation.isPending}
268
+ >
269
+ <Save className="h-4 w-4 mr-1" />
270
+ {mutation.isPending ? 'Creating...' : 'Create'}
271
+ </Button>
272
+ )}
273
+ </div>
274
+ </form>
275
+ </div>
276
+ </Page.Body>
277
+ </Page>
278
+ );
279
+ }
280
+
281
+ /** Returns the field names that belong to each step for validation. */
282
+ function getFieldsForStep(step: number): Array<keyof WizardValues> {
283
+ switch (step) {
284
+ case 0: return ['name', 'description', 'type'];
285
+ case 1: return ['status', 'notes'];
286
+ case 2: return []; // review step — already valid
287
+ default: return [];
288
+ }
289
+ }
290
+
291
+ export default ${componentName};
292
+ `;
293
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Audit-relay subscriber pattern template.
3
+ * Produces: subscriber that relays domain events to an audit event-store or external sink.
4
+ */
5
+ export declare function auditRelayTemplate(domain: string, id: string, event: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Audit-relay subscriber pattern template.
3
+ * Produces: subscriber that relays domain events to an audit event-store or external sink.
4
+ */
5
+ function camelCase(s) {
6
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7
+ }
8
+ export function auditRelayTemplate(domain, id, event) {
9
+ const varName = camelCase(id);
10
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
11
+
12
+ /**
13
+ * ${id} — Relays ${event} to the audit event-store for compliance.
14
+ *
15
+ * Captures every instance of ${event} and stores an immutable
16
+ * audit record including caller identity, timestamp, and event payload.
17
+ */
18
+ export const ${varName} = defineSubscriber({
19
+ id: '${domain}.${id}',
20
+ event: '${event}',
21
+ semverRange: '^1',
22
+ handler: async (event, ctx) => {
23
+ const auditRecord = {
24
+ pk: 'AUDIT#' + event.data.tenantId,
25
+ sk: 'EVENT#' + event.id,
26
+ eventType: event.type,
27
+ eventVersion: event.version,
28
+ payload: event.data,
29
+ timestamp: event.timestamp,
30
+ source: event.source,
31
+ correlationId: event.correlationId,
32
+ ttl: Math.floor(Date.now() / 1000) + 90 * 86400, // 90-day retention
33
+ };
34
+
35
+ // ── Write to audit store ─────────────────────────────────
36
+ await ctx.db.put(auditRecord);
37
+
38
+ // ── Optionally forward to external sink ──────────────────
39
+ if (ctx.integrations?.auditSink) {
40
+ await ctx.fetch(ctx.integrations.auditSink, {
41
+ method: 'POST',
42
+ body: JSON.stringify(auditRecord),
43
+ headers: { 'content-type': 'application/json' },
44
+ });
45
+ }
46
+
47
+ ctx.metrics?.putMetric('${id}.audit.relayed', 1, 'Count');
48
+ },
49
+ });
50
+ `;
51
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Cascade-deletion subscriber pattern template.
3
+ * Produces: subscriber that deletes related records when a parent is deleted.
4
+ */
5
+ export declare function cascadeDeletionTemplate(domain: string, id: string, event: string): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Cascade-deletion subscriber pattern template.
3
+ * Produces: subscriber that deletes related records when a parent is deleted.
4
+ */
5
+ function camelCase(s) {
6
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7
+ }
8
+ export function cascadeDeletionTemplate(domain, id, event) {
9
+ const varName = camelCase(id);
10
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
11
+
12
+ /**
13
+ * ${id} — Cascading deletion triggered by ${event}.
14
+ *
15
+ * When a parent ${event} event fires, this subscriber:
16
+ * 1. Queries for all child records owned by the deleted parent.
17
+ * 2. Soft-deletes each child (sets deletedAt timestamp).
18
+ * 3. Emits metrics for observability.
19
+ */
20
+ export const ${varName} = defineSubscriber({
21
+ id: '${domain}.${id}',
22
+ event: '${event}',
23
+ semverRange: '^1',
24
+ handler: async (event, ctx) => {
25
+ const { entityId, tenantId } = event.data;
26
+
27
+ // ── Find all children ────────────────────────────────────
28
+ const children = await ctx.db.query({
29
+ pk: 'TENANT#' + tenantId,
30
+ skPrefix: 'CHILD#' + entityId,
31
+ });
32
+
33
+ // ── Soft-delete each child ───────────────────────────────
34
+ const now = new Date().toISOString();
35
+ for (const child of children) {
36
+ await ctx.db.update({
37
+ key: { pk: child.pk, sk: child.sk },
38
+ updates: {
39
+ deletedAt: now,
40
+ deletedBy: event.data.deletedBy ?? 'system',
41
+ },
42
+ });
43
+ }
44
+
45
+ // ── Audit log ────────────────────────────────────────────
46
+ await ctx.auditLog({
47
+ action: 'cascade.delete',
48
+ tenantId,
49
+ entityId,
50
+ meta: {
51
+ childrenDeleted: children.length,
52
+ triggeredBy: event.id,
53
+ },
54
+ });
55
+
56
+ ctx.metrics?.putMetric('${id}.cascade.deleted', children.length, 'Count');
57
+ },
58
+ });
59
+ `;
60
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Single-step-projection subscriber pattern template.
3
+ * Produces: subscriber that updates a read-model projection table.
4
+ */
5
+ export declare function singleStepProjectionTemplate(domain: string, id: string, event: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Single-step-projection subscriber pattern template.
3
+ * Produces: subscriber that updates a read-model projection table.
4
+ */
5
+ function camelCase(s) {
6
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7
+ }
8
+ export function singleStepProjectionTemplate(domain, id, event) {
9
+ const varName = camelCase(id);
10
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
11
+
12
+ /**
13
+ * ${id} — Projects ${event} events into a read-optimised view.
14
+ */
15
+ export const ${varName} = defineSubscriber({
16
+ id: '${domain}.${id}',
17
+ event: '${event}',
18
+ semverRange: '^1',
19
+ handler: async (event, ctx) => {
20
+ const { tenantId, ...eventData } = event.data;
21
+
22
+ // ── Upsert projection ────────────────────────────────────
23
+ // Each event updates the projection row for fast reads.
24
+ await ctx.db.put({
25
+ pk: 'TENANT#' + tenantId,
26
+ sk: 'PROJECTION#' + event.id,
27
+ ...eventData,
28
+ lastEventId: event.id,
29
+ projectedAt: new Date().toISOString(),
30
+ });
31
+
32
+ ctx.metrics?.putMetric('${id}.projection.applied', 1, 'Count');
33
+ },
34
+ });
35
+ `;
36
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Template for generating per-domain README.md.
3
+ */
4
+ export declare function readmeTemplate(id: string): string;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Template for generating per-domain README.md.
3
+ */
4
+ export function readmeTemplate(id) {
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,11 @@
1
+ /**
2
+ * Template for generating subscriber handler skeleton.
3
+ */
4
+ /**
5
+ * Generates a subscriber handler skeleton.
6
+ * @param domainId - Domain ID in kebab-case.
7
+ * @param subscriberId - Subscriber ID in kebab-case.
8
+ * @param eventId - Event ID to subscribe to (e.g. 'domain.event').
9
+ * @returns TypeScript source code as a string.
10
+ */
11
+ export declare function subscriberSkeletonTemplate(domainId: string, subscriberId: string, eventId: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Template for generating subscriber handler skeleton.
3
+ */
4
+ /**
5
+ * Generates a subscriber handler skeleton.
6
+ * @param domainId - Domain ID in kebab-case.
7
+ * @param subscriberId - Subscriber ID in kebab-case.
8
+ * @param eventId - Event ID to subscribe to (e.g. 'domain.event').
9
+ * @returns TypeScript source code as a string.
10
+ */
11
+ export function subscriberSkeletonTemplate(domainId, subscriberId, eventId) {
12
+ return `import { defineSubscriber } from '../../../lib/domain-runtime';
13
+
14
+ export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = defineSubscriber({
15
+ id: '${domainId}.${subscriberId}',
16
+ event: '${eventId}',
17
+ semverRange: '*',
18
+ handler: async (_event, _ctx) => {
19
+ // TODO: Implement subscriber handler
20
+ },
21
+ });
22
+ `;
23
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Compute SHA-256 checksum of a Buffer, returning a lowercase hex string.
3
+ */
4
+ export declare function computeChecksumBuffer(buf: Buffer): string;
5
+ /**
6
+ * Compute SHA-256 checksum of a string (encoded as UTF-8), returning a lowercase hex string.
7
+ */
8
+ export declare function computeChecksumString(content: string): string;
9
+ /**
10
+ * Compute SHA-256 checksum of a file on disk, returning a lowercase hex string.
11
+ * Returns null if the file does not exist.
12
+ */
13
+ export declare function computeChecksumFile(filePath: string): Promise<string | null>;
14
+ /**
15
+ * Returns true if the file on disk has been modified compared to the expected checksum.
16
+ * Returns false if the checksums match, or null if the file does not exist.
17
+ */
18
+ export declare function isDrifted(filePath: string, expectedChecksum: string): Promise<boolean | null>;
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFile } from 'fs/promises';
3
+ /**
4
+ * Compute SHA-256 checksum of a Buffer, returning a lowercase hex string.
5
+ */
6
+ export function computeChecksumBuffer(buf) {
7
+ return createHash('sha256').update(buf).digest('hex');
8
+ }
9
+ /**
10
+ * Compute SHA-256 checksum of a string (encoded as UTF-8), returning a lowercase hex string.
11
+ */
12
+ export function computeChecksumString(content) {
13
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
14
+ }
15
+ /**
16
+ * Compute SHA-256 checksum of a file on disk, returning a lowercase hex string.
17
+ * Returns null if the file does not exist.
18
+ */
19
+ export async function computeChecksumFile(filePath) {
20
+ try {
21
+ const buf = await readFile(filePath);
22
+ return computeChecksumBuffer(buf);
23
+ }
24
+ catch (err) {
25
+ if (err.code === 'ENOENT')
26
+ return null;
27
+ throw err;
28
+ }
29
+ }
30
+ /**
31
+ * Returns true if the file on disk has been modified compared to the expected checksum.
32
+ * Returns false if the checksums match, or null if the file does not exist.
33
+ */
34
+ export async function isDrifted(filePath, expectedChecksum) {
35
+ const actual = await computeChecksumFile(filePath);
36
+ if (actual === null)
37
+ return null;
38
+ return actual !== expectedChecksum;
39
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Maps each convention slot to the absolute file paths found for that slot.
3
+ * For the 'domain' slot there is exactly one path (domain.config.ts).
4
+ * For the 'publishes' slot there is exactly one path (publishes/events.ts).
5
+ * All other slots may have 0..N paths.
6
+ */
7
+ export interface DomainFilePaths {
8
+ /** Path to domain.config.ts if present. */
9
+ domain: string | undefined;
10
+ /** API handler file paths under api/. */
11
+ apis: string[];
12
+ /** Webhook handler file paths under webhooks/. */
13
+ webhooks: string[];
14
+ /** Subscriber handler file paths under subscribers/. */
15
+ subscribers: string[];
16
+ /** Action handler file paths under actions/. */
17
+ actions: string[];
18
+ /** Schedule handler file paths under schedules/. */
19
+ schedules: string[];
20
+ /** Job handler file paths under jobs/. */
21
+ jobs: string[];
22
+ /** Integration definition file paths under integrations/. */
23
+ integrations: string[];
24
+ /** Path to publishes/events.ts if present. */
25
+ publishes: string | undefined;
26
+ }
27
+ /**
28
+ * Walk a domain root directory and return the conventional file paths for each primitive slot.
29
+ * Only returns files with a `.ts` extension. Recurses into subdirectories but skips
30
+ * node_modules, __tests__, and dist directories.
31
+ * Returns undefined for slots where no files are found (domain.config.ts, publishes/events.ts).
32
+ * @param domainRoot - Absolute path to the domain directory (e.g. `.mc/domains/billing`).
33
+ * @returns DomainFilePaths with absolute paths.
34
+ */
35
+ export declare function walkDomainDir(domainRoot: string): Promise<DomainFilePaths>;
@@ -0,0 +1,59 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Walk a domain root directory and return the conventional file paths for each primitive slot.
5
+ * Only returns files with a `.ts` extension. Recurses into subdirectories but skips
6
+ * node_modules, __tests__, and dist directories.
7
+ * Returns undefined for slots where no files are found (domain.config.ts, publishes/events.ts).
8
+ * @param domainRoot - Absolute path to the domain directory (e.g. `.mc/domains/billing`).
9
+ * @returns DomainFilePaths with absolute paths.
10
+ */
11
+ export async function walkDomainDir(domainRoot) {
12
+ async function tsFiles(dir) {
13
+ const result = [];
14
+ async function walk(current) {
15
+ try {
16
+ const entries = await readdir(current, { withFileTypes: true });
17
+ for (const entry of entries) {
18
+ // Skip node_modules, __tests__, dist directories
19
+ if (entry.isDirectory()) {
20
+ if (['node_modules', '__tests__', 'dist', '.git'].includes(entry.name)) {
21
+ continue;
22
+ }
23
+ // Recurse into subdirectories
24
+ await walk(join(current, entry.name));
25
+ }
26
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
27
+ result.push(join(current, entry.name));
28
+ }
29
+ }
30
+ }
31
+ catch {
32
+ // Directory doesn't exist or is unreadable, return empty
33
+ }
34
+ }
35
+ await walk(dir);
36
+ return result;
37
+ }
38
+ async function singleTs(filePath) {
39
+ try {
40
+ await stat(filePath);
41
+ return filePath;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ const [domain, apis, webhooks, subscribers, actions, schedules, jobs, integrations, publishes] = await Promise.all([
48
+ singleTs(join(domainRoot, 'domain.config.ts')),
49
+ tsFiles(join(domainRoot, 'api')),
50
+ tsFiles(join(domainRoot, 'webhooks')),
51
+ tsFiles(join(domainRoot, 'subscribers')),
52
+ tsFiles(join(domainRoot, 'actions')),
53
+ tsFiles(join(domainRoot, 'schedules')),
54
+ tsFiles(join(domainRoot, 'jobs')),
55
+ tsFiles(join(domainRoot, 'integrations')),
56
+ singleTs(join(domainRoot, 'publishes', 'events.ts')),
57
+ ]);
58
+ return { domain, apis, webhooks, subscribers, actions, schedules, jobs, integrations, publishes };
59
+ }