@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,14 @@
1
+ /**
2
+ * Valid flow pattern IDs.
3
+ */
4
+ export declare const FLOW_PATTERNS: readonly ["fan-out-reaction", "multi-step-orchestration", "autonomous-remediation", "human-in-loop"];
5
+ export type FlowPattern = (typeof FLOW_PATTERNS)[number];
6
+ export interface AddFlowOptions {
7
+ domain: string;
8
+ id: string;
9
+ type?: 'express' | 'standard';
10
+ /** Pattern template ID (optional). */
11
+ pattern?: FlowPattern;
12
+ projectRoot?: string;
13
+ }
14
+ export declare function runAddFlow(opts: AddFlowOptions): Promise<void>;
@@ -0,0 +1,108 @@
1
+ import { mkdir, writeFile, access } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { fanOutReactionTemplate } from '../templates/patterns/flow/fan-out-reaction.js';
4
+ import { multiStepOrchestrationTemplate } from '../templates/patterns/flow/multi-step-orchestration.js';
5
+ import { autonomousRemediationTemplate } from '../templates/patterns/flow/autonomous-remediation.js';
6
+ import { humanInLoopTemplate } from '../templates/patterns/flow/human-in-loop.js';
7
+ import { cliLogger } from '../utils/logger.js';
8
+ /**
9
+ * Valid flow pattern IDs.
10
+ */
11
+ export const FLOW_PATTERNS = [
12
+ 'fan-out-reaction',
13
+ 'multi-step-orchestration',
14
+ 'autonomous-remediation',
15
+ 'human-in-loop',
16
+ ];
17
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
18
+ function toCamelCase(s) {
19
+ return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
20
+ }
21
+ function flowSkeletonTemplate(domain, id, type) {
22
+ const varName = toCamelCase(id);
23
+ return `import { defineFlow } from '@mettlecast/domain-runtime';
24
+
25
+ export const ${varName} = defineFlow({
26
+ id: '${id}',
27
+ owningDomain: '${domain}',
28
+ type: '${type}',
29
+ name: '${id}',
30
+ steps: [
31
+ {
32
+ type: 'domain-action',
33
+ name: 'ExampleAction',
34
+ domainId: '${domain}',
35
+ actionId: 'example-action',
36
+ next: 'Done',
37
+ },
38
+ {
39
+ type: 'flow-control',
40
+ control: 'succeed',
41
+ name: 'Done',
42
+ },
43
+ ],
44
+ });
45
+ `;
46
+ }
47
+ export async function runAddFlow(opts) {
48
+ if (!KEBAB_REGEX.test(opts.domain)) {
49
+ throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
50
+ }
51
+ if (!KEBAB_REGEX.test(opts.id)) {
52
+ throw new Error(`Invalid flow id "${opts.id}" — must be kebab-case (lowercase with hyphens)`);
53
+ }
54
+ const root = opts.projectRoot ?? resolve(process.cwd());
55
+ const flowType = opts.type ?? 'express';
56
+ // Verify domain exists
57
+ const domainDir = join(root, 'domains', opts.domain);
58
+ try {
59
+ await access(domainDir);
60
+ }
61
+ catch {
62
+ throw new Error(`Domain "${opts.domain}" does not exist at ${domainDir}`);
63
+ }
64
+ const flowsDir = join(domainDir, 'flows');
65
+ const flowFile = join(flowsDir, `${opts.id}.ts`);
66
+ // Refuse if flow already exists
67
+ try {
68
+ await access(flowFile);
69
+ throw new Error(`Flow "${opts.id}" already exists at ${flowFile}`);
70
+ }
71
+ catch (err) {
72
+ if (err.code !== 'ENOENT') {
73
+ throw err;
74
+ }
75
+ }
76
+ await mkdir(flowsDir, { recursive: true });
77
+ // Resolve template content
78
+ let flowContent;
79
+ if (opts.pattern) {
80
+ flowContent = resolveFlowPattern(opts.pattern, opts.domain, opts.id);
81
+ cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
82
+ }
83
+ else {
84
+ flowContent = flowSkeletonTemplate(opts.domain, opts.id, flowType);
85
+ }
86
+ await writeFile(flowFile, flowContent);
87
+ cliLogger.info({ domain: opts.domain, id: opts.id, file: flowFile, pattern: opts.pattern ?? 'skeleton' }, 'Flow scaffolded');
88
+ // eslint-disable-next-line no-console
89
+ console.log(`\n✓ Flow "${opts.id}" added at ${flowFile}\nNext: edit the flow definition, ` +
90
+ `then run \`mc-domain-module build-flows\` to generate the registry.`);
91
+ }
92
+ /**
93
+ * Resolve a flow pattern template to its generated TypeScript source.
94
+ */
95
+ function resolveFlowPattern(pattern, domain, id) {
96
+ switch (pattern) {
97
+ case 'fan-out-reaction':
98
+ return fanOutReactionTemplate(domain, id);
99
+ case 'multi-step-orchestration':
100
+ return multiStepOrchestrationTemplate(domain, id);
101
+ case 'autonomous-remediation':
102
+ return autonomousRemediationTemplate(domain, id);
103
+ case 'human-in-loop':
104
+ return humanInLoopTemplate(domain, id);
105
+ default:
106
+ throw new Error(`Unknown flow pattern "${pattern}". Available: ${FLOW_PATTERNS.join(', ')}`);
107
+ }
108
+ }
@@ -0,0 +1,15 @@
1
+ /** Supported hook patterns. */
2
+ export type HookPattern = 'paginated-list' | 'mutation' | 'feature-flag' | 'subscription';
3
+ export declare const HOOK_PATTERNS: readonly HookPattern[];
4
+ export interface AddHookOptions {
5
+ /** Hook name in kebab-case (e.g. "search-items"). */
6
+ name: string;
7
+ /** Hook pattern to scaffold. */
8
+ pattern: HookPattern;
9
+ /** Override the output directory (default: src/hooks/). */
10
+ outputDir?: string;
11
+ }
12
+ /**
13
+ * Scaffold a typed data-fetching hook file.
14
+ */
15
+ export declare function runAddHook(opts: AddHookOptions): Promise<void>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Command: tib add-hook <name> --pattern <pattern>
3
+ *
4
+ * Scaffolds a typed data-fetching hook using TanStack Query and @mettlecast/sdk.
5
+ */
6
+ import { writeFile, mkdir, access } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { cliLogger } from '../utils/logger.js';
9
+ import { paginatedListHookTemplate, mutationHookTemplate, featureFlagHookTemplate, subscriptionHookTemplate, } from '../templates/patterns/hook/index.js';
10
+ export const HOOK_PATTERNS = [
11
+ 'paginated-list',
12
+ 'mutation',
13
+ 'feature-flag',
14
+ 'subscription',
15
+ ];
16
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
17
+ const PATTERN_TEMPLATES = {
18
+ 'paginated-list': paginatedListHookTemplate,
19
+ mutation: mutationHookTemplate,
20
+ 'feature-flag': featureFlagHookTemplate,
21
+ subscription: subscriptionHookTemplate,
22
+ };
23
+ /**
24
+ * Scaffold a typed data-fetching hook file.
25
+ */
26
+ export async function runAddHook(opts) {
27
+ const { name, pattern, outputDir } = opts;
28
+ if (!KEBAB_REGEX.test(name)) {
29
+ throw new Error(`Invalid hook name "${name}" — must be kebab-case`);
30
+ }
31
+ if (!HOOK_PATTERNS.includes(pattern)) {
32
+ const available = HOOK_PATTERNS.join(', ');
33
+ throw new Error(`Unknown hook pattern "${pattern}".\n\nAvailable patterns: ${available}`);
34
+ }
35
+ const templateFn = PATTERN_TEMPLATES[pattern];
36
+ const content = templateFn(name);
37
+ const outDir = outputDir
38
+ ? resolve(outputDir)
39
+ : resolve(process.cwd(), 'src', 'hooks');
40
+ await mkdir(outDir, { recursive: true });
41
+ const fileName = `use-${name}.ts`;
42
+ const outPath = join(outDir, fileName);
43
+ // Refuse if target already exists
44
+ try {
45
+ await access(outPath);
46
+ throw new Error(`Hook file already exists at ${outPath}. Remove it first or use a different --output directory.`);
47
+ }
48
+ catch (err) {
49
+ if (err.code !== 'ENOENT')
50
+ throw err;
51
+ }
52
+ await writeFile(outPath, content, 'utf8');
53
+ cliLogger.info({ name, pattern, outPath }, 'Hook scaffolded');
54
+ // eslint-disable-next-line no-console
55
+ console.log(`\n✓ Hook "use-${name}" (pattern: ${pattern}) added at ${outPath}\n` +
56
+ ` Import: import { use${capitalize(name)} } from './hooks/use-${name}';\n` +
57
+ ` Pattern: ${pattern}\n`);
58
+ }
59
+ /** Capitalizes the first character of a kebab-case name into PascalCase. */
60
+ function capitalize(s) {
61
+ return s
62
+ .split('-')
63
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
64
+ .join('');
65
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Command to add a new database migration to an existing domain.
3
+ * Supports --pattern for predefined migration templates.
4
+ */
5
+ /**
6
+ * Valid migration pattern IDs.
7
+ */
8
+ export declare const MIGRATION_PATTERNS: readonly ["tenant-scoped-table", "rls-policy", "column-add", "index", "event-store"];
9
+ export type MigrationPattern = (typeof MIGRATION_PATTERNS)[number];
10
+ /**
11
+ * Options for the add-migration command.
12
+ */
13
+ export interface AddMigrationOptions {
14
+ /** Domain ID in kebab-case. */
15
+ domain: string;
16
+ /** Migration name in kebab-case. */
17
+ name: string;
18
+ /** Pattern template ID (required). */
19
+ pattern: MigrationPattern;
20
+ /** Override directory for domain root (used in tests). */
21
+ domainsDir?: string;
22
+ }
23
+ /**
24
+ * Execute the add-migration command: create a new SQL migration in an existing domain.
25
+ */
26
+ export declare function runAddMigration(opts: AddMigrationOptions): Promise<void>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Command to add a new database migration to an existing domain.
3
+ * Supports --pattern for predefined migration templates.
4
+ */
5
+ import { writeFile, access, mkdir } from 'node:fs/promises';
6
+ import { join, resolve } from 'node:path';
7
+ import { tenantScopedTableTemplate } from '../templates/patterns/migration/tenant-scoped-table.js';
8
+ import { rlsPolicyTemplate } from '../templates/patterns/migration/rls-policy.js';
9
+ import { columnAddTemplate } from '../templates/patterns/migration/column-add.js';
10
+ import { indexTemplate } from '../templates/patterns/migration/index.js';
11
+ import { eventStoreTemplate } from '../templates/patterns/migration/event-store.js';
12
+ import { cliLogger } from '../utils/logger.js';
13
+ /**
14
+ * Valid migration pattern IDs.
15
+ */
16
+ export const MIGRATION_PATTERNS = [
17
+ 'tenant-scoped-table',
18
+ 'rls-policy',
19
+ 'column-add',
20
+ 'index',
21
+ 'event-store',
22
+ ];
23
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
24
+ /**
25
+ * Generates a timestamp prefix matching the project convention.
26
+ */
27
+ function generateTimestamp() {
28
+ return new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
29
+ }
30
+ /**
31
+ * Execute the add-migration command: create a new SQL migration in an existing domain.
32
+ */
33
+ export async function runAddMigration(opts) {
34
+ if (!KEBAB_REGEX.test(opts.domain)) {
35
+ throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
36
+ }
37
+ if (!KEBAB_REGEX.test(opts.name)) {
38
+ throw new Error(`Invalid migration name "${opts.name}" — must be kebab-case`);
39
+ }
40
+ if (!MIGRATION_PATTERNS.includes(opts.pattern)) {
41
+ throw new Error(`Unknown migration pattern "${opts.pattern}". Available: ${MIGRATION_PATTERNS.join(', ')}`);
42
+ }
43
+ const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
44
+ const domainDir = join(root, opts.domain);
45
+ const domainConfigPath = join(domainDir, 'domain.config.ts');
46
+ // Verify domain exists
47
+ try {
48
+ await access(domainConfigPath);
49
+ }
50
+ catch {
51
+ throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
52
+ }
53
+ const migrationsDir = join(domainDir, 'db', 'migrations');
54
+ await mkdir(migrationsDir, { recursive: true });
55
+ const timestamp = generateTimestamp();
56
+ const fileName = `${timestamp}_${opts.name}.sql`;
57
+ const filePath = join(migrationsDir, fileName);
58
+ // Refuse if migration already exists
59
+ try {
60
+ await access(filePath);
61
+ throw new Error(`Migration "${fileName}" already exists at ${filePath}`);
62
+ }
63
+ catch (err) {
64
+ if (err.code !== 'ENOENT') {
65
+ throw err;
66
+ }
67
+ }
68
+ // Resolve pattern template
69
+ const sqlContent = resolveMigrationPattern(opts.pattern, opts.domain, opts.name);
70
+ await writeFile(filePath, sqlContent);
71
+ cliLogger.info({ domain: opts.domain, migration: opts.name, pattern: opts.pattern, file: filePath }, 'Migration added');
72
+ // eslint-disable-next-line no-console
73
+ console.log(`\n✓ Migration "${opts.name}" added to domain "${opts.domain}" at ${filePath}`);
74
+ }
75
+ /**
76
+ * Resolve a migration pattern template to its generated SQL source.
77
+ */
78
+ function resolveMigrationPattern(pattern, domain, name) {
79
+ switch (pattern) {
80
+ case 'tenant-scoped-table':
81
+ return tenantScopedTableTemplate(domain, name);
82
+ case 'rls-policy':
83
+ return rlsPolicyTemplate(domain, name);
84
+ case 'column-add':
85
+ return columnAddTemplate(domain, name);
86
+ case 'index':
87
+ return indexTemplate(domain, name);
88
+ case 'event-store':
89
+ return eventStoreTemplate(domain, name);
90
+ default:
91
+ throw new Error(`Unknown migration pattern "${pattern}". Available: ${MIGRATION_PATTERNS.join(', ')}`);
92
+ }
93
+ }
@@ -0,0 +1,7 @@
1
+ export interface AddModuleOptions {
2
+ moduleId: string;
3
+ githubToken?: string;
4
+ dryRun?: boolean;
5
+ projectDir?: string;
6
+ }
7
+ export declare function runAddModule(opts: AddModuleOptions): Promise<void>;
@@ -0,0 +1,261 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { Readable } from 'node:stream';
4
+ import { Parser as TarParser } from 'tar';
5
+ import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
6
+ import { readManifest, writeManifest, upsertManifestFile, inferPolicyFromPath } from '../utils/manifest.js';
7
+ import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
8
+ import { applyTokens, buildTokenContext, stripTplExtension } from '../utils/token-substitution.js';
9
+ import { injectHeader } from '../utils/header-inject.js';
10
+ import { computeChecksumString } from '../utils/checksum.js';
11
+ import { installScaffoldFile } from '../utils/install-file.js';
12
+ import { cliLogger } from '../utils/logger.js';
13
+ async function parseTarball(buf) {
14
+ const entries = [];
15
+ const chunks = {};
16
+ await new Promise((resolve, reject) => {
17
+ const parser = new TarParser({
18
+ onentry: (entry) => {
19
+ const p = entry.path;
20
+ chunks[p] = [];
21
+ entry.on('data', (chunk) => chunks[p].push(chunk));
22
+ },
23
+ });
24
+ parser.on('finish', resolve);
25
+ parser.on('error', reject);
26
+ Readable.from(buf).pipe(parser);
27
+ });
28
+ for (const [path, buffers] of Object.entries(chunks)) {
29
+ if (buffers.length > 0) {
30
+ entries.push({ path, content: Buffer.concat(buffers) });
31
+ }
32
+ }
33
+ return entries;
34
+ }
35
+ function getTarEntry(entries, path) {
36
+ return entries.find((e) => e.path === path);
37
+ }
38
+ async function getCliVersion() {
39
+ try {
40
+ const { createRequire } = await import('node:module');
41
+ const require = createRequire(import.meta.url);
42
+ const pkgJson = require('../../package.json');
43
+ return pkgJson.version;
44
+ }
45
+ catch {
46
+ return '0.0.0';
47
+ }
48
+ }
49
+ async function detectRepoInfo() {
50
+ try {
51
+ const remoteUrl = execSync('git remote get-url origin', {
52
+ encoding: 'utf8',
53
+ stdio: 'pipe',
54
+ }).trim();
55
+ // Parse git@github.com:owner/repo.git or https://github.com/owner/repo.git
56
+ let match = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(remoteUrl);
57
+ if (match) {
58
+ return { owner: match[1], repo: match[2].replace(/\.git$/, '') };
59
+ }
60
+ return null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ async function createGitHubPR(owner, repo, branchName, title, body, token) {
67
+ try {
68
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
69
+ method: 'POST',
70
+ headers: {
71
+ Authorization: `Bearer ${token}`,
72
+ 'Content-Type': 'application/json',
73
+ },
74
+ body: JSON.stringify({
75
+ title,
76
+ body,
77
+ head: branchName,
78
+ base: 'develop',
79
+ }),
80
+ });
81
+ if (!response.ok) {
82
+ const error = await response.json();
83
+ cliLogger.warn(error, 'Failed to create GitHub PR');
84
+ return null;
85
+ }
86
+ const data = (await response.json());
87
+ if (data.html_url && data.number) {
88
+ return { prUrl: data.html_url, prNumber: data.number };
89
+ }
90
+ return null;
91
+ }
92
+ catch (err) {
93
+ cliLogger.warn(err, 'Failed to create GitHub PR');
94
+ return null;
95
+ }
96
+ }
97
+ export async function runAddModule(opts) {
98
+ const projectDir = opts.projectDir ?? process.cwd();
99
+ const moduleId = opts.moduleId;
100
+ const dryRun = opts.dryRun ?? false;
101
+ // 1. Read .mc/scaffold-config.json
102
+ let config;
103
+ try {
104
+ config = await readScaffoldConfig(projectDir);
105
+ }
106
+ catch (err) {
107
+ throw new Error(`Failed to read scaffold config from ${projectDir}: ${err}`);
108
+ }
109
+ // 2. Fetch modules.json from S3
110
+ let modulesJson;
111
+ try {
112
+ modulesJson = await fetchModulesJson(config.scaffoldBucket, config.scaffoldVersion);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`Failed to fetch modules.json for version ${config.scaffoldVersion}: ${err}`);
116
+ }
117
+ // 3. Validate moduleId is a known module
118
+ const module = modulesJson.modules.find((m) => m.id === moduleId);
119
+ if (!module) {
120
+ const available = modulesJson.modules.map((m) => m.id).join(', ');
121
+ throw new Error(`Unknown module "${moduleId}". Available modules: ${available}`);
122
+ }
123
+ // 4. Check moduleId is NOT already installed
124
+ if (config.enabledModules.includes(moduleId)) {
125
+ throw new Error(`Module "${moduleId}" is already installed`);
126
+ }
127
+ // 5. Check all dependencies of moduleId ARE installed
128
+ for (const dep of module.dependencies) {
129
+ if (!config.enabledModules.includes(dep)) {
130
+ throw new Error(`Module "${moduleId}" requires "${dep}" which is not installed. ` +
131
+ `Install "${dep}" first with: tib add-module ${dep}`);
132
+ }
133
+ }
134
+ // 6. Fetch tarball from S3
135
+ let tarBuf;
136
+ try {
137
+ tarBuf = await fetchModuleTarball(config.scaffoldBucket, config.scaffoldVersion, module.tarball, module.sha256);
138
+ }
139
+ catch (err) {
140
+ throw new Error(`Failed to fetch tarball for ${moduleId}: ${err}`);
141
+ }
142
+ // 7. Parse tarball
143
+ const tarEntries = await parseTarball(tarBuf);
144
+ // 8. Extract MANIFEST.json
145
+ const manifestEntry = getTarEntry(tarEntries, 'MANIFEST.json');
146
+ if (!manifestEntry) {
147
+ throw new Error(`No MANIFEST.json found in tarball for ${moduleId}`);
148
+ }
149
+ const tarManifest = JSON.parse(manifestEntry.content.toString('utf-8'));
150
+ // 9. Read manifest.json
151
+ let manifest = await readManifest(projectDir);
152
+ if (!manifest) {
153
+ throw new Error(`No .mc/manifest.json found in ${projectDir}`);
154
+ }
155
+ // 10. Get CLI version for header
156
+ const cliVersion = await getCliVersion();
157
+ // 11. Build token context
158
+ const tokenCtx = buildTokenContext(config, config.scaffoldVersion, cliVersion);
159
+ // 12. Process files from tarball
160
+ const filesInstalled = [];
161
+ for (const fileEntry of tarManifest) {
162
+ const tarEntry = getTarEntry(tarEntries, fileEntry.path);
163
+ if (!tarEntry) {
164
+ cliLogger.warn({ moduleId, path: fileEntry.path }, 'File in MANIFEST.json not found in tarball');
165
+ continue;
166
+ }
167
+ // Determine install path
168
+ let installPath = fileEntry.installedAs ?? fileEntry.path;
169
+ // Strip .tpl extension
170
+ if (installPath.endsWith('.tpl')) {
171
+ installPath = stripTplExtension(installPath);
172
+ }
173
+ // Process content
174
+ let content = tarEntry.content.toString('utf-8');
175
+ // Token substitution for template files
176
+ if (fileEntry.isTemplate) {
177
+ content = applyTokens(content, tokenCtx);
178
+ }
179
+ // Inject scaffold header
180
+ content = injectHeader(content, installPath, moduleId, module.version);
181
+ if (!dryRun) {
182
+ // Determine policy: use fileEntry.policy if present, else infer from path
183
+ const policy = fileEntry.policy ?? inferPolicyFromPath(installPath);
184
+ // Get current entry if exists
185
+ const currentEntry = manifest.files.find((f) => f.path === installPath);
186
+ // Install file using policy-based dispatch
187
+ const destPath = join(projectDir, installPath);
188
+ const installResult = await installScaffoldFile(destPath, content, policy, currentEntry, {
189
+ dryRun: false,
190
+ });
191
+ // Only update manifest if file was not skipped
192
+ if (installResult.status !== 'skipped') {
193
+ const sha256 = computeChecksumString(content);
194
+ upsertManifestFile(manifest, {
195
+ path: installPath,
196
+ module: moduleId,
197
+ moduleVersion: module.version,
198
+ sha256,
199
+ wasTemplate: fileEntry.isTemplate,
200
+ installedAt: new Date().toISOString(),
201
+ policy,
202
+ });
203
+ }
204
+ }
205
+ filesInstalled.push(installPath);
206
+ }
207
+ if (dryRun) {
208
+ console.log(`\n✓ Dry run: would install ${filesInstalled.length} file(s) from ${moduleId}@${module.version}`);
209
+ filesInstalled.forEach((f) => console.log(` ${f}`));
210
+ console.log(`\nTo apply changes, run: tib add-module ${moduleId}\n`);
211
+ return;
212
+ }
213
+ // 13. Update .mc/manifest.json
214
+ await writeManifest(projectDir, manifest);
215
+ // 14. Update .mc/scaffold-config.json
216
+ config.enabledModules = [...config.enabledModules, moduleId];
217
+ await writeScaffoldConfig(projectDir, config);
218
+ console.log(`\n✓ Module "${moduleId}" installed (${filesInstalled.length} file(s) added)`);
219
+ // 15. Git operations
220
+ const branchName = `tib/add-module-${moduleId}`;
221
+ let commitSha = null;
222
+ try {
223
+ // Create new branch
224
+ execSync(`git -C "${projectDir}" checkout -b ${branchName}`, { stdio: 'pipe' });
225
+ // Stage changes
226
+ execSync(`git -C "${projectDir}" add .mc/`, { stdio: 'pipe' });
227
+ // Commit
228
+ const commitMsg = `feat: add tib module ${moduleId}@${config.scaffoldVersion}`;
229
+ execSync(`git -C "${projectDir}" commit -m "${commitMsg}"`, { stdio: 'pipe' });
230
+ commitSha = execSync(`git -C "${projectDir}" rev-parse HEAD`, {
231
+ encoding: 'utf8',
232
+ stdio: 'pipe',
233
+ }).trim();
234
+ cliLogger.info({ moduleId, commitSha, branchName }, 'Module commit created');
235
+ }
236
+ catch (err) {
237
+ cliLogger.warn(err, `Failed to create git commit for module ${moduleId}`);
238
+ }
239
+ // 16. GitHub PR creation if token available
240
+ const githubToken = opts.githubToken ?? process.env.GITHUB_TOKEN;
241
+ if (githubToken && commitSha) {
242
+ try {
243
+ // Push branch to remote
244
+ execSync(`git -C "${projectDir}" push -u origin ${branchName}`, { stdio: 'pipe' });
245
+ const repoInfo = await detectRepoInfo();
246
+ if (repoInfo) {
247
+ const prResult = await createGitHubPR(repoInfo.owner, repoInfo.repo, branchName, `feat: add tib module ${moduleId}`, `Adds optional scaffold module: ${moduleId}@${config.scaffoldVersion}\n\nFiles added: ${filesInstalled.length}`, githubToken);
248
+ if (prResult) {
249
+ console.log(`\nPR created: ${prResult.prUrl}`);
250
+ cliLogger.info({ prUrl: prResult.prUrl, prNumber: prResult.prNumber }, 'Module PR created');
251
+ }
252
+ }
253
+ }
254
+ catch (err) {
255
+ cliLogger.warn(err, 'Failed to push and create PR');
256
+ }
257
+ }
258
+ else if (commitSha) {
259
+ console.log(`\nBranch "${branchName}" created. Push with: git push -u origin ${branchName}`);
260
+ }
261
+ }
@@ -0,0 +1,13 @@
1
+ import { type PagePattern } from '../templates/patterns/page/index.js';
2
+ export interface AddPageOptions {
3
+ pageId: string;
4
+ outputDir?: string;
5
+ auth?: boolean;
6
+ protected?: boolean;
7
+ public?: boolean;
8
+ /** Pattern-based scaffolding (list, detail, form, wizard, dashboard, settings). */
9
+ pattern?: PagePattern;
10
+ /** Layout to use: app (default) or auth. */
11
+ layout?: 'app' | 'auth';
12
+ }
13
+ export declare function runAddPage(opts: AddPageOptions): Promise<void>;