@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
package/src/cli.ts ADDED
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { runBuild } from './commands/build.js';
4
+ import { runValidate } from './commands/validate.js';
5
+ import { runTest } from './commands/test.js';
6
+ import { runDev } from './commands/dev.js';
7
+ import { runBuildCatalog } from './commands/build-catalog.js';
8
+ import { runAddDomain } from './commands/add-domain.js';
9
+ import { runAddApi, API_PATTERNS } from './commands/add-api.js';
10
+ import { runAddSubscriber, SUBSCRIBER_PATTERNS } from './commands/add-subscriber.js';
11
+ import { runAddFlow, FLOW_PATTERNS } from './commands/add-flow.js';
12
+ import { runAddModule } from './commands/add-module.js';
13
+ import { runBuildFlows } from './commands/build-flows.js';
14
+ import { runDoctor } from './commands/doctor.js';
15
+ import { runCheckHashes } from './commands/check-hashes.js';
16
+ import { runUpgradeBackend } from './commands/upgrade-backend.js';
17
+ import { runAddPage } from './commands/add-page.js';
18
+ import { runCreateProject } from './commands/create-project.js';
19
+ import { runUpgradeFrontend } from './commands/upgrade-frontend.js';
20
+ import { runUpgrade } from './commands/upgrade.js';
21
+ import { runAddMigration, MIGRATION_PATTERNS } from './commands/add-migration.js';
22
+ import { runAddTest } from './commands/add-test.js';
23
+ import { runUpgradeEvent } from './commands/upgrade-event.js';
24
+ import { runShowDomainCli } from './commands/show.js';
25
+ import { runAuditCli } from './commands/audit.js';
26
+ import { runExplainCli } from './commands/explain.js';
27
+ import { runAddHook, type HookPattern, HOOK_PATTERNS } from './commands/add-hook.js';
28
+ import { runAddComponent, type ComponentTier, COMPONENT_TIERS } from './commands/add-component.js';
29
+ import { runPowerTune } from './commands/power-tune.js';
30
+ import { runReseedPage } from './commands/reseed-page.js';
31
+ import { runAddSeedPage } from './commands/add-seed-page.js';
32
+ import { runShowDns } from './commands/show-dns.js';
33
+
34
+ /**
35
+ * Root CLI program for the TIB Domain Module local developer toolchain.
36
+ * Provides build, validate, test, dev, add-domain, add-api, and add-subscriber subcommands.
37
+ */
38
+ const program = new Command();
39
+
40
+ program
41
+ .name('mc-domain-module')
42
+ .description('TIB Domain Module local developer toolchain')
43
+ .version('0.1.0');
44
+
45
+ program
46
+ .command('build <domain>')
47
+ .description('Build the DomainRegistry from source and write .mc/domain-registry.json')
48
+ .option('--out <path>', 'Override the output file path')
49
+ .action(async (domain: string, opts: { out?: string }) => {
50
+ await runBuild({ domainRoot: domain, outFile: opts.out });
51
+ });
52
+
53
+ program
54
+ .command('validate <domain>')
55
+ .description('Build the registry and validate structure (CI gate — exits 1 on failure)')
56
+ .action(async (domain: string) => {
57
+ await runValidate(domain, true);
58
+ });
59
+
60
+ program
61
+ .command('test <domain> <handler-id>')
62
+ .description('Invoke a handler with fixture data and print the result')
63
+ .requiredOption('--fixture <path>', 'Path to fixture JSON file ({ event, ctx? })')
64
+ .action(async (domain: string, handlerId: string, opts: { fixture: string }) => {
65
+ await runTest({ domainRoot: domain, handlerId, fixturePath: opts.fixture });
66
+ });
67
+
68
+ program
69
+ .command('dev <domain>')
70
+ .description('Start a local HTTP server simulating API Gateway for all defineApi handlers')
71
+ .option('--port <n>', 'Port to listen on', '3000')
72
+ .action(async (domain: string, opts: { port?: string }) => {
73
+ await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
74
+ });
75
+
76
+ program
77
+ .command('build-catalog')
78
+ .description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
79
+ .option('--tib-dir <path>', 'Path to the .tib directory (defaults to .tib in cwd)')
80
+ .action(async (opts: { tibDir?: string }) => {
81
+ await runBuildCatalog(opts.tibDir);
82
+ });
83
+
84
+ program
85
+ .command('add-domain <id>')
86
+ .description(
87
+ 'Scaffold a new domain under domains/<id> and register it in .mc/scaffold-config.json'
88
+ )
89
+ .option('--tenancy <mode>', 'Tenancy mode (required|none|system)', 'required')
90
+ .action(async (id: string, opts: { tenancy: 'required' | 'none' | 'system' }) => {
91
+ await runAddDomain({ id, tenancy: opts.tenancy });
92
+ });
93
+
94
+ program
95
+ .command('add-api <domain> <id>')
96
+ .description('Add a new API primitive to an existing domain')
97
+ .option('--method <method>', 'HTTP method', 'GET')
98
+ .option('--tenancy <mode>', 'Tenancy mode', 'required')
99
+ .option('--pattern <pattern>', `Pattern template: ${API_PATTERNS.join(' | ')}`)
100
+ .action(
101
+ async (
102
+ domain: string,
103
+ id: string,
104
+ opts: { method: string; tenancy: 'required' | 'none' | 'system'; pattern?: string }
105
+ ) => {
106
+ await runAddApi({
107
+ domain,
108
+ id,
109
+ method: opts.method,
110
+ tenancy: opts.tenancy,
111
+ pattern: opts.pattern as typeof API_PATTERNS[number] | undefined,
112
+ });
113
+ }
114
+ );
115
+
116
+ program
117
+ .command('add-subscriber <domain> <id>')
118
+ .description('Add a new event subscriber to an existing domain')
119
+ .requiredOption('--event <eventId>', 'Event id to subscribe to')
120
+ .action(async (domain: string, id: string, opts: { event: string }) => {
121
+ await runAddSubscriber({ domain, id, event: opts.event });
122
+ });
123
+
124
+ program
125
+ .command('add-flow <domain> <id>')
126
+ .description('Scaffold a new flow under domains/<domain>/flows/<id>.ts')
127
+ .option('--type <type>', 'Flow type: express (default) or standard', 'express')
128
+ .action(async (domain: string, id: string, opts: { type: string }) => {
129
+ await runAddFlow({ domain, id, type: opts.type as 'express' | 'standard' });
130
+ });
131
+
132
+ program
133
+ .command('add-module <module>')
134
+ .description('Install a scaffold module from S3 at the project\'s pinned scaffold version and raise a PR')
135
+ .option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
136
+ .option('--dry-run', 'Show what would be installed without making changes', false)
137
+ .action(async (module: string, opts: { githubToken?: string; dryRun: boolean }) => {
138
+ await runAddModule({ moduleId: module, githubToken: opts.githubToken, dryRun: opts.dryRun });
139
+ });
140
+
141
+ program
142
+ .command('build-flows')
143
+ .description('Build all flows into .mc/flows-registry.json')
144
+ .option('--out <path>', 'Override the output file path')
145
+ .action(async (opts: { out?: string }) => {
146
+ await runBuildFlows({ outFile: opts.out });
147
+ });
148
+
149
+ program
150
+ .command('doctor')
151
+ .description('Run all structural checks for the current TIB project (strict by default — any FAIL causes exit code 1)')
152
+ .option('--strict', 'DEPRECATED: Doctor is always strict now. This flag is a no-op.', false)
153
+ .option('--json', 'Output JSON instead of pretty table', false)
154
+ .action(async (opts: { strict: boolean; json: boolean }) => {
155
+ if (opts.strict) {
156
+ // eslint-disable-next-line no-console
157
+ console.warn('⚠ --strict is deprecated: doctor is always strict now. This flag is a no-op.');
158
+ }
159
+ const report = await runDoctor({ projectRoot: process.cwd() });
160
+ if (opts.json) {
161
+ // eslint-disable-next-line no-console
162
+ console.log(JSON.stringify(report, null, 2));
163
+ } else {
164
+ // Pretty-print table
165
+ // eslint-disable-next-line no-console
166
+ console.log('\n=== TIB Project Health Check ===\n');
167
+ for (const check of report.checks) {
168
+ const symbol = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '⚠' : '✗';
169
+ // eslint-disable-next-line no-console
170
+ console.log(`${symbol} ${check.name}: ${check.message}`);
171
+ if (check.fixHint) {
172
+ // eslint-disable-next-line no-console
173
+ console.log(` Fix: ${check.fixHint}`);
174
+ }
175
+ }
176
+ // eslint-disable-next-line no-console
177
+ console.log(`\nExit code: ${report.exitCode}\n`);
178
+ }
179
+ process.exit(report.exitCode);
180
+ });
181
+
182
+ program
183
+ .command('check-hashes')
184
+ .description('Verify infra/modules/ has not been hand-edited since last scaffold')
185
+ .action(async () => {
186
+ const result = await runCheckHashes({});
187
+ process.exit(result.ok ? 0 : 1);
188
+ });
189
+
190
+ program
191
+ .command('upgrade-backend <target-major>')
192
+ .description('Run jscodeshift/ts-morph migrations between major versions of domain-runtime')
193
+ .option('--repo-root <path>', 'Root of the repo (defaults to cwd)')
194
+ .option('--dry-run', 'Print changes without applying', false)
195
+ .action(async (targetMajor: string, opts: { repoRoot?: string; dryRun: boolean }) => {
196
+ await runUpgradeBackend({ targetMajor, repoRoot: opts.repoRoot, dryRun: opts.dryRun });
197
+ });
198
+
199
+ program
200
+ .command('add-page <pageId>')
201
+ .description('Scaffold a seed page from the dashboard-pages template registry')
202
+ .option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
203
+ .option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
204
+ .option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
205
+ .option('--auth', 'Scaffold with auth guard wiring (login-redirect on unauthenticated)')
206
+ .option('--protected', 'Add requireAuth() loader to the route')
207
+ .option('--public', 'No auth guard (public page)')
208
+ .action(async (pageId: string, opts: { output?: string; pattern?: string; layout: string; auth?: boolean; protected?: boolean; public?: boolean }) => {
209
+ // Validate pattern early to give a clear error before the inner command
210
+ if (opts.pattern && !['list', 'detail', 'form', 'wizard', 'dashboard', 'settings'].includes(opts.pattern)) {
211
+ throw new Error(
212
+ `Unknown page pattern "${opts.pattern}". Available patterns: list, detail, form, wizard, dashboard, settings`
213
+ );
214
+ }
215
+ if (!['app', 'auth'].includes(opts.layout)) {
216
+ throw new Error(
217
+ `Unknown layout "${opts.layout}". Available layouts: app, auth`
218
+ );
219
+ }
220
+ await runAddPage({
221
+ pageId,
222
+ outputDir: opts.output,
223
+ pattern: opts.pattern as import('./commands/add-page.js').AddPageOptions['pattern'],
224
+ layout: opts.layout as 'app' | 'auth',
225
+ auth: opts.auth,
226
+ protected: opts.protected,
227
+ public: opts.public,
228
+ });
229
+ });
230
+
231
+ program
232
+ .command('add-hook <name>')
233
+ .description('Scaffold a typed data-fetching hook (TanStack Query + @mettlecast/sdk)')
234
+ .option('--pattern <pattern>', 'Hook pattern', 'paginated-list')
235
+ .option('--output <dir>', 'Override the output directory (default: src/hooks/)')
236
+ .action(async (name: string, opts: { pattern: string; output?: string }) => {
237
+ if (!HOOK_PATTERNS.includes(opts.pattern as HookPattern)) {
238
+ throw new Error(
239
+ `Unknown hook pattern "${opts.pattern}". Available patterns: ${HOOK_PATTERNS.join(', ')}`
240
+ );
241
+ }
242
+ await runAddHook({
243
+ name,
244
+ pattern: opts.pattern as HookPattern,
245
+ outputDir: opts.output,
246
+ });
247
+ });
248
+
249
+ program
250
+ .command('add-component <name>')
251
+ .description('Scaffold a design-system component with Storybook story and a11y check')
252
+ .option('--pattern <tier>', 'Component tier: atom, molecule, or organism', 'atom')
253
+ .option('--output <dir>', 'Override the output directory (default: src/components/)')
254
+ .action(async (name: string, opts: { pattern: string; output?: string }) => {
255
+ if (!COMPONENT_TIERS.includes(opts.pattern as ComponentTier)) {
256
+ throw new Error(
257
+ `Unknown component tier "${opts.pattern}". Available tiers: ${COMPONENT_TIERS.join(', ')}`
258
+ );
259
+ }
260
+ await runAddComponent({
261
+ name,
262
+ tier: opts.pattern as ComponentTier,
263
+ outputDir: opts.output,
264
+ });
265
+ });
266
+
267
+ program
268
+ .command('create-project <name>')
269
+ .description('Initialise a new TIB frontend project with seed pages and router wiring')
270
+ .option('--out <dir>', 'Parent directory for the new project (default: current directory)')
271
+ .action(async (name: string, opts: { out?: string }) => {
272
+ await runCreateProject({ name, outDir: opts.out });
273
+ });
274
+
275
+ program
276
+ .command('upgrade-frontend')
277
+ .description('Check @mettlecast/* frontend package versions and report missing/outdated packages')
278
+ .option('--dry-run', 'Report issues without making changes', false)
279
+ .option('--project-dir <path>', 'Path to the project to check (default: current directory)')
280
+ .action(async (opts: { dryRun: boolean; projectDir?: string }) => {
281
+ await runUpgradeFrontend({ dryRun: opts.dryRun, projectDir: opts.projectDir });
282
+ });
283
+
284
+ program
285
+ .command('upgrade [version]')
286
+ .description('Upgrade scaffold to a new version — diffs S3 tarballs, raises a GitHub PR with exact file changes')
287
+ .option('--dry-run', 'Preview changes without writing files or creating PR', false)
288
+ .option('--project-dir <path>', 'Path to the project to upgrade')
289
+ .option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
290
+ .option('--frontend-components', 'Scope upgrade to scaffold-tracked frontend files only (opens a PR with Chromatic visual diff)', false)
291
+ .action(async (version: string | undefined, opts: { dryRun: boolean; projectDir?: string; githubToken?: string; frontendComponents: boolean }) => {
292
+ await runUpgrade(version, { dryRun: opts.dryRun, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
293
+ });
294
+
295
+ // ── New W4 commands ───────────────────────────────────────────────
296
+
297
+ program
298
+ .command('add-migration <domain> <name>')
299
+ .description('Add a new database migration to an existing domain')
300
+ .requiredOption('--pattern <pattern>', `Migration pattern: ${MIGRATION_PATTERNS.join(' | ')}`)
301
+ .action(async (domain: string, name: string, opts: { pattern: string }) => {
302
+ await runAddMigration({
303
+ domain,
304
+ name,
305
+ pattern: opts.pattern as typeof MIGRATION_PATTERNS[number],
306
+ });
307
+ });
308
+
309
+ program
310
+ .command('add-test <domain> <primitive-id>')
311
+ .description('Scaffold a vitest fixture for an existing domain primitive')
312
+ .action(async (domain: string, primitiveId: string) => {
313
+ await runAddTest({ domain, primitiveId });
314
+ });
315
+
316
+ program
317
+ .command('upgrade-event <domain> <event-id>')
318
+ .description('Bump an event definition to a new version with sunset semantics')
319
+ .requiredOption('--to-version <N>', 'Target version number (integer >= 2)', parseInt)
320
+ .action(async (domain: string, eventId: string, opts: { toVersion: number }) => {
321
+ await runUpgradeEvent({ domain, eventId, toVersion: opts.toVersion });
322
+ });
323
+
324
+ program
325
+ .command('show domain <domain>')
326
+ .description('Show a structured JSON summary of a domain')
327
+ .action(async (domain: string) => {
328
+ await runShowDomainCli({ domain });
329
+ });
330
+
331
+ program
332
+ .command('audit [domain]')
333
+ .description('Run structural audit checks, optionally filtered to one domain')
334
+ .option('--json', 'Output structured JSON', false)
335
+ .action(async (domain: string | undefined, opts: { json: boolean }) => {
336
+ await runAuditCli({ domain, json: opts.json });
337
+ });
338
+
339
+ program
340
+ .command('explain <rule-id>')
341
+ .description('Explain a rule ID with description, examples, and K-node reference')
342
+ .action(async (ruleId: string) => {
343
+ runExplainCli({ ruleId });
344
+ });
345
+
346
+ program
347
+ .command('power-tune <domain> <primitive>')
348
+ .description('Run AWS Lambda Power Tuning on a deployed Lambda. Returns recommended memorySize.')
349
+ .option('--strategy <strategy>', 'Optimization strategy: cost, speed, or balanced', 'cost')
350
+ .action(async (domain: string, primitive: string) => {
351
+ await runPowerTune(domain, primitive);
352
+ });
353
+
354
+ program
355
+ .command('reseed-page [name]')
356
+ .description('Force-reseed a seed page (or all seed pages) with the latest scaffold version. Opens a [DESTRUCTIVE] PR.')
357
+ .option('--all', 'Reseed all seed pages in bulk (single PR)', false)
358
+ .option('--project-dir <path>', 'Path to the project to reseed')
359
+ .action(async (name: string | undefined, opts: { all: boolean; projectDir?: string }) => {
360
+ if (!name && !opts.all) {
361
+ throw new Error('Provide a page name or use --all to reseed all seed pages');
362
+ }
363
+ await runReseedPage({ pageName: name, all: opts.all, projectDir: opts.projectDir });
364
+ });
365
+
366
+ program
367
+ .command('add-seed-page <name>')
368
+ .description('Add a seed page from the scaffold library. The page is yours immediately — TIB will never auto-update it.')
369
+ .option('--project-dir <path>', 'Path to the project')
370
+ .action(async (name: string, opts: { projectDir?: string }) => {
371
+ await runAddSeedPage({ pageName: name, projectDir: opts.projectDir });
372
+ });
373
+
374
+ program
375
+ .command('show-dns')
376
+ .description('Print DNS records (zone-file format) needed to activate custom domains')
377
+ .option('--env <env>', 'Filter to a single environment (dev|staging|prod)')
378
+ .option('--project-dir <path>', 'Path to the project root (defaults to cwd)')
379
+ .action(async (opts: { env?: string; projectDir?: string }) => {
380
+ await runShowDns(opts);
381
+ });
382
+
383
+ program.parseAsync(process.argv).catch((err: unknown) => {
384
+ // eslint-disable-next-line no-console
385
+ console.error(err);
386
+ process.exit(1);
387
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Command to add a new API to an existing domain.
3
+ * Supports --pattern to scaffold from predefined templates.
4
+ */
5
+
6
+ import { writeFile, access, mkdir } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { apiSkeletonTemplate, apiFixtureSkeleton } from '../templates/api-skeleton.js';
9
+ import { simpleCrudTemplate, simpleCrudListTemplate } from '../templates/patterns/api/simple-crud.js';
10
+ import { paginatedListTemplate } from '../templates/patterns/api/paginated-list.js';
11
+ import { createWithEventTemplate } from '../templates/patterns/api/create-with-event.js';
12
+ import { idempotentMutationTemplate } from '../templates/patterns/api/idempotent-mutation.js';
13
+ import { webhookReceiverStyleTemplate } from '../templates/patterns/api/webhook-receiver-style.js';
14
+ import { systemAdminTemplate } from '../templates/patterns/api/system-admin.js';
15
+ import { cliLogger } from '../utils/logger.js';
16
+
17
+ /**
18
+ * Valid API pattern IDs.
19
+ */
20
+ export const API_PATTERNS = [
21
+ 'simple-crud',
22
+ 'paginated-list',
23
+ 'create-with-event',
24
+ 'idempotent-mutation',
25
+ 'webhook-receiver-style',
26
+ 'system-admin',
27
+ ] as const;
28
+
29
+ export type ApiPattern = (typeof API_PATTERNS)[number];
30
+
31
+ /**
32
+ * Options for the add-api command.
33
+ */
34
+ export interface AddApiOptions {
35
+ /** Domain ID in kebab-case. */
36
+ domain: string;
37
+ /** API ID in kebab-case. */
38
+ id: string;
39
+ /** HTTP method (defaults to GET). */
40
+ method?: string;
41
+ /** Tenancy mode: 'required', 'none', or 'system'. */
42
+ tenancy: 'required' | 'none' | 'system';
43
+ /** Pattern template ID (optional — when provided, uses pattern instead of skeleton). */
44
+ pattern?: ApiPattern;
45
+ /** Override directory for domain root (used in tests). */
46
+ domainsDir?: string;
47
+ }
48
+
49
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
50
+
51
+ /**
52
+ * Execute the add-api command: create a new API handler in an existing domain.
53
+ * @param opts - AddApiOptions specifying domain, API ID, and tenancy.
54
+ * @throws If domain does not exist, API ID is invalid, or file already exists.
55
+ */
56
+ export async function runAddApi(opts: AddApiOptions): Promise<void> {
57
+ if (!KEBAB_REGEX.test(opts.id)) {
58
+ throw new Error(`Invalid API id "${opts.id}" — must be kebab-case`);
59
+ }
60
+
61
+ if (!KEBAB_REGEX.test(opts.domain)) {
62
+ throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
63
+ }
64
+
65
+ const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
66
+ const domainDir = join(root, opts.domain);
67
+ const domainConfigPath = join(domainDir, 'domain.config.ts');
68
+
69
+ // Verify domain exists
70
+ try {
71
+ await access(domainConfigPath);
72
+ } catch {
73
+ throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
74
+ }
75
+
76
+ const apiFilePath = join(domainDir, 'api', `${opts.id}.ts`);
77
+
78
+ // Refuse if API already exists
79
+ try {
80
+ await access(apiFilePath);
81
+ throw new Error(`API "${opts.id}" already exists at ${apiFilePath}`);
82
+ } catch (err) {
83
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
84
+ throw err;
85
+ }
86
+ }
87
+
88
+ // Resolve template content
89
+ let apiContent: string;
90
+ if (opts.pattern) {
91
+ apiContent = resolveApiPattern(opts.pattern, opts.domain, opts.id, opts.tenancy);
92
+ cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
93
+ } else {
94
+ apiContent = apiSkeletonTemplate(opts.domain, opts.id, opts.tenancy);
95
+ }
96
+
97
+ // Write API file
98
+ await writeFile(apiFilePath, apiContent);
99
+
100
+ // Create fixture
101
+ const apiTestDir = join(domainDir, 'api', '__tests__');
102
+ await mkdir(apiTestDir, { recursive: true });
103
+ await writeFile(
104
+ join(apiTestDir, `${opts.id}.fixture.json`),
105
+ apiFixtureSkeleton(opts.domain, opts.id)
106
+ );
107
+
108
+ cliLogger.info({ domain: opts.domain, api: opts.id, method: opts.method ?? 'GET', pattern: opts.pattern ?? 'skeleton' }, 'API added');
109
+ // eslint-disable-next-line no-console
110
+ console.log(`\n✓ API "${opts.id}" added to domain "${opts.domain}" at ${apiFilePath}`);
111
+ }
112
+
113
+ /**
114
+ * Resolve a pattern template to its generated TypeScript source.
115
+ * @param pattern - The pattern ID.
116
+ * @param domain - Domain ID.
117
+ * @param id - API ID.
118
+ * @param tenancy - Tenancy mode.
119
+ * @returns TypeScript source code as a string.
120
+ */
121
+ function resolveApiPattern(
122
+ pattern: ApiPattern,
123
+ domain: string,
124
+ id: string,
125
+ tenancy: string,
126
+ ): string {
127
+ switch (pattern) {
128
+ case 'simple-crud':
129
+ return simpleCrudTemplate(domain, id, tenancy) +
130
+ '\n' +
131
+ simpleCrudListTemplate(domain, id, tenancy);
132
+ case 'paginated-list':
133
+ return paginatedListTemplate(domain, id, tenancy);
134
+ case 'create-with-event':
135
+ return createWithEventTemplate(domain, id, tenancy);
136
+ case 'idempotent-mutation':
137
+ return idempotentMutationTemplate(domain, id, tenancy);
138
+ case 'webhook-receiver-style':
139
+ return webhookReceiverStyleTemplate(domain, id, tenancy);
140
+ case 'system-admin':
141
+ return systemAdminTemplate(domain, id, tenancy);
142
+ default:
143
+ throw new Error(`Unknown API pattern "${pattern}". Available: ${API_PATTERNS.join(', ')}`);
144
+ }
145
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Command: tib add-component <name> --pattern <tier>
3
+ *
4
+ * Scaffolds a design-system component with Storybook story and a11y check.
5
+ *
6
+ * Tiers:
7
+ * - atom: Single design token usage (Button, Input, Icon).
8
+ * - molecule: Composition of 2+ atoms (SearchBar, FormField, DataRow).
9
+ * - organism: Complex section (DataTable, WizardFlow, DashboardWidget).
10
+ */
11
+ import { writeFile, mkdir, access } from 'node:fs/promises';
12
+ import { join, resolve } from 'node:path';
13
+ import { cliLogger } from '../utils/logger.js';
14
+ import {
15
+ atomTemplate,
16
+ moleculeTemplate,
17
+ organismTemplate,
18
+ } from '../templates/patterns/component/index.js';
19
+
20
+ /** Supported component tiers. */
21
+ export type ComponentTier = 'atom' | 'molecule' | 'organism';
22
+
23
+ export const COMPONENT_TIERS: readonly ComponentTier[] = [
24
+ 'atom',
25
+ 'molecule',
26
+ 'organism',
27
+ ] as const;
28
+
29
+ const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
30
+
31
+ const TIER_TEMPLATES: Record<
32
+ ComponentTier,
33
+ (name: string) => { component: string; story: string; test: string }
34
+ > = {
35
+ atom: atomTemplate,
36
+ molecule: moleculeTemplate,
37
+ organism: organismTemplate,
38
+ };
39
+
40
+ export interface AddComponentOptions {
41
+ /** Component name in kebab-case (e.g. "action-button"). */
42
+ name: string;
43
+ /** Component tier / pattern. */
44
+ tier: ComponentTier;
45
+ /** Override the output base directory (default: src/components/). */
46
+ outputDir?: string;
47
+ }
48
+
49
+ /**
50
+ * Scaffold a design-system component with story + test files.
51
+ */
52
+ export async function runAddComponent(opts: AddComponentOptions): Promise<void> {
53
+ const { name, tier, outputDir } = opts;
54
+
55
+ if (!KEBAB_REGEX.test(name)) {
56
+ throw new Error(`Invalid component name "${name}" — must be kebab-case`);
57
+ }
58
+
59
+ if (!COMPONENT_TIERS.includes(tier)) {
60
+ const available = COMPONENT_TIERS.join(', ');
61
+ throw new Error(
62
+ `Unknown component tier "${tier}".\n\nAvailable tiers: ${available}`
63
+ );
64
+ }
65
+
66
+ const templateFn = TIER_TEMPLATES[tier];
67
+ const { component, story, test } = templateFn(name);
68
+
69
+ const baseDir = outputDir
70
+ ? resolve(outputDir)
71
+ : resolve(process.cwd(), 'src', 'components');
72
+
73
+ const compDir = join(baseDir, name);
74
+
75
+ const files: Array<{ name: string; content: string }> = [
76
+ { name: `${name}.tsx`, content: component },
77
+ { name: `${name}.stories.tsx`, content: story },
78
+ { name: `${name}.test.tsx`, content: test },
79
+ ];
80
+
81
+ // Check for existing component directory first
82
+ try {
83
+ await access(compDir);
84
+ throw new Error(
85
+ `Component directory already exists at ${compDir}. Remove it first or use a different --output directory.`
86
+ );
87
+ } catch (err) {
88
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
89
+ }
90
+
91
+ await mkdir(compDir, { recursive: true });
92
+
93
+ for (const file of files) {
94
+ const outPath = join(compDir, file.name);
95
+ await writeFile(outPath, file.content, 'utf8');
96
+ }
97
+
98
+ cliLogger.info({ name, tier, compDir }, 'Component scaffolded');
99
+ // eslint-disable-next-line no-console
100
+ console.log(
101
+ `\n✓ Component "${name}" (tier: ${tier}) added in ${compDir}/\n` +
102
+ ` ${name}.tsx — component implementation\n` +
103
+ ` ${name}.stories.tsx — Storybook story (2+ variants)\n` +
104
+ ` ${name}.test.tsx — Playwright smoke test + a11y check\n` +
105
+ `\n` +
106
+ ` Import: import { ${capitalize(name)} } from './components/${name}/${name}';\n`
107
+ );
108
+ }
109
+
110
+ /** Capitalizes the first character of a kebab-case name into PascalCase. */
111
+ function capitalize(s: string): string {
112
+ return s
113
+ .split('-')
114
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
115
+ .join('');
116
+ }