@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,410 @@
1
+ import { mkdir, writeFile, unlink, access } from 'node:fs/promises';
2
+ import { join, resolve, dirname } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { createGunzip } from 'node:zlib';
5
+ import { Readable } from 'node:stream';
6
+ import { cliLogger } from '../utils/logger.js';
7
+ import {
8
+ fetchVersionsJson,
9
+ fetchModulesJson,
10
+ fetchModuleTarball,
11
+ type S3ModuleEntry,
12
+ } from '../utils/s3-fetch.js';
13
+ import { computeChecksumString } from '../utils/checksum.js';
14
+ import { injectHeader } from '../utils/header-inject.js';
15
+ import { readManifest } from '../utils/manifest.js';
16
+ import { readScaffoldConfig } from '../utils/scaffold-config.js';
17
+
18
+ const SCAFFOLD_BUCKET = 'mc-scaffold';
19
+
20
+ // ── Tarball types ──────────────────────────────────────────────────────────────
21
+
22
+ interface TarEntry {
23
+ path: string;
24
+ content: Buffer;
25
+ }
26
+
27
+ interface TarballManifestEntry {
28
+ path: string;
29
+ sha256: string;
30
+ isTemplate: boolean;
31
+ installedAs?: string;
32
+ }
33
+
34
+ // ── Seed page categories ───────────────────────────────────────────────────────
35
+
36
+ const SEED_PAGE_CATEGORIES = ['auth', 'account', 'shell', '_errors'];
37
+ const SEED_LAYOUT_DIR = 'layouts';
38
+
39
+ export interface ReseedPageOptions {
40
+ pageName?: string;
41
+ all?: boolean;
42
+ projectDir?: string;
43
+ }
44
+
45
+ // ── Tarball parsing ────────────────────────────────────────────────────────────
46
+
47
+ async function parseTarball(buf: Buffer): Promise<TarEntry[]> {
48
+ const decompressed = await new Promise<Buffer>((res, rej) => {
49
+ const gunzip = createGunzip();
50
+ const chunks: Buffer[] = [];
51
+ const input = Readable.from(buf);
52
+ input.pipe(gunzip);
53
+ gunzip.on('data', (chunk: Buffer) =>
54
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as ArrayBuffer))
55
+ );
56
+ gunzip.on('end', () => res(Buffer.concat(chunks)));
57
+ gunzip.on('error', rej);
58
+ });
59
+
60
+ const entries: TarEntry[] = [];
61
+ let offset = 0;
62
+
63
+ while (offset + 512 <= decompressed.length) {
64
+ const header = decompressed.slice(offset, offset + 512);
65
+ if (header.every((b) => b === 0)) break;
66
+
67
+ let name = header.slice(0, 100).toString('utf-8').replace(/\0.*/, '');
68
+ const prefix = header.slice(345, 500).toString('utf-8').replace(/\0.*/, '');
69
+ if (prefix) name = `${prefix}/${name}`;
70
+
71
+ const sizeStr = header.slice(124, 136).toString('utf-8').replace(/\0.*/, '').trim();
72
+ const size = parseInt(sizeStr, 8) || 0;
73
+ const typeFlag = header[156];
74
+ const isRegularFile = typeFlag === 0x30 || typeFlag === 0 || typeFlag === undefined;
75
+
76
+ offset += 512;
77
+
78
+ if (isRegularFile && name && size > 0) {
79
+ const content = decompressed.slice(offset, offset + size);
80
+ entries.push({ path: name, content });
81
+ }
82
+
83
+ offset += Math.ceil(size / 512) * 512;
84
+ }
85
+
86
+ return entries;
87
+ }
88
+
89
+ function getTarEntry(entries: TarEntry[], targetPath: string): TarEntry | undefined {
90
+ return entries.find((e) => {
91
+ const normalized = e.path.replace(/^\.\//, '').replace(/^[^/]+\//, '');
92
+ return normalized === targetPath || e.path === targetPath || e.path.endsWith(`/${targetPath}`);
93
+ });
94
+ }
95
+
96
+ // ── GitHub helpers ─────────────────────────────────────────────────────────────
97
+
98
+ function detectGitRemote(projectDir: string): { owner: string; repo: string } | null {
99
+ try {
100
+ const url = execSync('git remote get-url origin', {
101
+ cwd: projectDir,
102
+ encoding: 'utf8',
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ }).trim();
105
+
106
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^.]+?)(?:\.git)?$/);
107
+ if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
108
+
109
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/);
110
+ if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
111
+
112
+ return null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ async function createGitHubPR(
119
+ owner: string,
120
+ repo: string,
121
+ token: string,
122
+ branch: string,
123
+ title: string,
124
+ body: string,
125
+ labels?: string[]
126
+ ): Promise<{ url: string; number: number }> {
127
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
128
+ const res = await fetch(url, {
129
+ method: 'POST',
130
+ headers: {
131
+ Authorization: `Bearer ${token}`,
132
+ Accept: 'application/vnd.github+json',
133
+ 'Content-Type': 'application/json',
134
+ 'X-GitHub-Api-Version': '2022-11-28',
135
+ },
136
+ body: JSON.stringify({
137
+ title,
138
+ body,
139
+ head: branch,
140
+ base: 'develop',
141
+ labels,
142
+ }),
143
+ });
144
+
145
+ if (!res.ok) {
146
+ const err = await res.text();
147
+ throw new Error(`GitHub PR creation failed (${res.status}): ${err}`);
148
+ }
149
+
150
+ const data = (await res.json()) as { html_url: string; number: number };
151
+ return { url: data.html_url, number: data.number };
152
+ }
153
+
154
+ // ── Seed page fetching ─────────────────────────────────────────────────────────
155
+
156
+ async function fetchSeedPagesToReseed(
157
+ tarEntries: TarEntry[],
158
+ tarManifest: TarballManifestEntry[],
159
+ pageName?: string
160
+ ): Promise<Array<{ path: string; content: string }>> {
161
+ const results: Array<{ path: string; content: string }> = [];
162
+
163
+ if (pageName) {
164
+ // Single page mode: fetch one specific page
165
+ for (const fileEntry of tarManifest) {
166
+ if (fileEntry.path.includes(`/pages/`) && fileEntry.path.includes(`${pageName}.tsx`)) {
167
+ const tarEntry = getTarEntry(tarEntries, fileEntry.path);
168
+ if (tarEntry) {
169
+ let installPath = fileEntry.installedAs ?? fileEntry.path;
170
+ if (installPath.endsWith('.tpl')) installPath = installPath.slice(0, -4);
171
+ results.push({
172
+ path: installPath,
173
+ content: tarEntry.content.toString('utf-8'),
174
+ });
175
+ }
176
+ }
177
+ }
178
+ } else {
179
+ // Bulk mode (--all): fetch all seed pages
180
+ for (const fileEntry of tarManifest) {
181
+ const isSeedPage = isSeedPagePath(fileEntry.path);
182
+ if (isSeedPage) {
183
+ const tarEntry = getTarEntry(tarEntries, fileEntry.path);
184
+ if (tarEntry) {
185
+ let installPath = fileEntry.installedAs ?? fileEntry.path;
186
+ if (installPath.endsWith('.tpl')) installPath = installPath.slice(0, -4);
187
+ results.push({
188
+ path: installPath,
189
+ content: tarEntry.content.toString('utf-8'),
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ return results;
197
+ }
198
+
199
+ function isSeedPagePath(filePath: string): boolean {
200
+ const seedPatterns = [
201
+ /^frontend\/src\/pages\/auth\//,
202
+ /^frontend\/src\/pages\/account\//,
203
+ /^frontend\/src\/pages\/shell\//,
204
+ /^frontend\/src\/pages\/_errors\//,
205
+ /^frontend\/src\/layouts\//,
206
+ ];
207
+ return seedPatterns.some((pattern) => pattern.test(filePath));
208
+ }
209
+
210
+ // ── Main export ────────────────────────────────────────────────────────────────
211
+
212
+ export async function runReseedPage(opts: ReseedPageOptions): Promise<void> {
213
+ const projectDir = resolve(opts.projectDir ?? process.cwd());
214
+
215
+ if (!opts.pageName && !opts.all) {
216
+ throw new Error('Provide a page name or use --all to reseed all seed pages');
217
+ }
218
+
219
+ const mode = opts.all ? 'bulk' : 'single';
220
+ console.log(`\nTIB Reseed Page (${mode} mode)\n`);
221
+ console.log(`Project: ${projectDir}\n`);
222
+
223
+ // 1. Read manifest + scaffold-config
224
+ const manifest = await readManifest(projectDir);
225
+ if (!manifest) {
226
+ throw new Error(
227
+ `.mc/manifest.json not found in ${projectDir}. Is this a TIB scaffold project?`
228
+ );
229
+ }
230
+
231
+ const scaffoldConfig = await readScaffoldConfig(projectDir);
232
+ const bucket = scaffoldConfig.scaffoldBucket ?? SCAFFOLD_BUCKET;
233
+
234
+ // 2. Get latest version
235
+ const versionsJson = await fetchVersionsJson(bucket);
236
+ const latestVersion = versionsJson.latest;
237
+
238
+ console.log(`Current version : ${manifest.scaffoldVersion}`);
239
+ console.log(`Latest version : ${latestVersion}\n`);
240
+
241
+ // 3. Fetch frontend module tarball
242
+ const modulesJson = await fetchModulesJson(bucket, latestVersion);
243
+ const frontendModule = modulesJson.modules.find((m) => m.id === 'frontend');
244
+ if (!frontendModule) {
245
+ throw new Error(`Frontend module not found in modules.json for version ${latestVersion}`);
246
+ }
247
+
248
+ console.log(`Fetching frontend module ${frontendModule.id}@${frontendModule.version}...\n`);
249
+
250
+ const tarBuf = await fetchModuleTarball(
251
+ bucket,
252
+ latestVersion,
253
+ frontendModule.tarball,
254
+ frontendModule.sha256
255
+ );
256
+
257
+ const tarEntries = await parseTarball(tarBuf);
258
+ const manifestEntry = getTarEntry(tarEntries, 'MANIFEST.json');
259
+ if (!manifestEntry) {
260
+ throw new Error('No MANIFEST.json in frontend module tarball');
261
+ }
262
+
263
+ const tarManifest = JSON.parse(
264
+ manifestEntry.content.toString('utf-8')
265
+ ) as TarballManifestEntry[];
266
+
267
+ // 4. Fetch seed pages to reseed
268
+ const seedPages = await fetchSeedPagesToReseed(tarEntries, tarManifest, opts.pageName);
269
+
270
+ if (seedPages.length === 0) {
271
+ const msg = opts.pageName
272
+ ? `No seed page found matching "${opts.pageName}"`
273
+ : 'No seed pages found to reseed';
274
+ throw new Error(msg);
275
+ }
276
+
277
+ console.log(`Found ${seedPages.length} seed page(s) to reseed`);
278
+ seedPages.forEach((p) => {
279
+ console.log(` - ${p.path}`);
280
+ });
281
+ console.log('');
282
+
283
+ // 5. Create git branch for the reseed
284
+ const branchName = opts.all
285
+ ? `tib-reseed/all-v${latestVersion}`
286
+ : `tib-reseed/${opts.pageName}-v${latestVersion}`;
287
+
288
+ try {
289
+ execSync(`git -C "${projectDir}" checkout -b "${branchName}"`, {
290
+ stdio: ['pipe', 'pipe', 'pipe'],
291
+ });
292
+ } catch {
293
+ // Branch may already exist
294
+ try {
295
+ execSync(`git -C "${projectDir}" checkout "${branchName}"`, {
296
+ stdio: ['pipe', 'pipe', 'pipe'],
297
+ });
298
+ } catch {
299
+ throw new Error(`Failed to create or checkout branch: ${branchName}`);
300
+ }
301
+ }
302
+
303
+ // 6. Write seed pages to disk
304
+ for (const seedPage of seedPages) {
305
+ const diskPath = join(projectDir, seedPage.path);
306
+ console.log(`Writing ${seedPage.path}...`);
307
+ await mkdir(dirname(diskPath), { recursive: true });
308
+ await writeFile(diskPath, seedPage.content, 'utf-8');
309
+ }
310
+
311
+ // 7. Commit changes
312
+ execSync(`git -C "${projectDir}" add -A`, { stdio: 'pipe' });
313
+
314
+ let hasChanges = false;
315
+ try {
316
+ const status = execSync(`git -C "${projectDir}" status --porcelain`, {
317
+ encoding: 'utf8',
318
+ stdio: ['pipe', 'pipe', 'pipe'],
319
+ }).trim();
320
+ hasChanges = status.length > 0;
321
+ } catch {
322
+ // ignore
323
+ }
324
+
325
+ if (!hasChanges) {
326
+ console.log('No changes to commit — seed pages are already up to date.\n');
327
+ return;
328
+ }
329
+
330
+ const commitMsg = opts.all
331
+ ? `chore: [DESTRUCTIVE] bulk reseed all seed pages to v${latestVersion}`
332
+ : `chore: [DESTRUCTIVE] reseed ${opts.pageName} to v${latestVersion}`;
333
+
334
+ execSync(`git -C "${projectDir}" commit -m "${commitMsg}"`, { stdio: 'pipe' });
335
+
336
+ try {
337
+ execSync(`git -C "${projectDir}" push --set-upstream origin "${branchName}"`, {
338
+ stdio: 'pipe',
339
+ });
340
+ } catch {
341
+ cliLogger.warn({ branchName }, 'reseed-page: git push failed — skipping PR creation');
342
+ console.log(`\nCommit created on branch: ${branchName}`);
343
+ console.log('Note: push failed — create the PR manually.\n');
344
+ return;
345
+ }
346
+
347
+ // 8. Create GitHub PR
348
+ const token = process.env['GITHUB_TOKEN'];
349
+ const remote = detectGitRemote(projectDir);
350
+
351
+ if (!token) {
352
+ console.log(`\nBranch pushed: ${branchName}`);
353
+ console.log('Set GITHUB_TOKEN env var to create a PR automatically.\n');
354
+ return;
355
+ }
356
+
357
+ if (!remote) {
358
+ console.log(`\nBranch pushed: ${branchName}`);
359
+ console.log('Could not detect GitHub owner/repo from git remote — create the PR manually.\n');
360
+ return;
361
+ }
362
+
363
+ const prTitle = opts.all
364
+ ? `[DESTRUCTIVE] Bulk reseed all seed pages to scaffold v${latestVersion}`
365
+ : `[DESTRUCTIVE] Reseed ${opts.pageName} to scaffold v${latestVersion}`;
366
+
367
+ const prBody = buildReseedPagePrBody(
368
+ opts.pageName ?? 'all pages',
369
+ latestVersion,
370
+ seedPages
371
+ );
372
+
373
+ const prResult = await createGitHubPR(
374
+ remote.owner,
375
+ remote.repo,
376
+ token,
377
+ branchName,
378
+ prTitle,
379
+ prBody,
380
+ ['tib-destructive-reseed']
381
+ );
382
+
383
+ console.log(`\nPR created: ${prResult.url}\n`);
384
+ cliLogger.info({ prUrl: prResult.url, latestVersion }, 'reseed-page: PR created');
385
+ }
386
+
387
+ function buildReseedPagePrBody(pageName: string, version: string, pages: Array<{ path: string; content: string }>): string {
388
+ const lines: string[] = [
389
+ '## ⚠️ DESTRUCTIVE: Reseed Seed Pages',
390
+ '',
391
+ 'This PR **overwrites** the following seed page(s) with the latest scaffold version.',
392
+ 'This is a destructive operation — verify changes carefully.',
393
+ '',
394
+ '### Seed pages to be overwritten',
395
+ '',
396
+ ];
397
+
398
+ for (const page of pages) {
399
+ lines.push(`- \`${page.path}\``);
400
+ }
401
+
402
+ lines.push('');
403
+ lines.push('### What to do');
404
+ lines.push('1. Review the file diffs in this PR');
405
+ lines.push('2. If acceptable, merge this PR');
406
+ lines.push('3. The seed pages will be reset to their scaffold defaults');
407
+ lines.push('');
408
+
409
+ return lines.join('\n');
410
+ }
@@ -0,0 +1,139 @@
1
+ import { readFile } from 'fs/promises';
2
+ import path from 'path';
3
+ import { cliLogger } from '../utils/logger.js';
4
+
5
+ export interface ShowDnsOptions {
6
+ env?: string;
7
+ projectDir?: string;
8
+ }
9
+
10
+ interface DnsRecord {
11
+ type: string;
12
+ name: string;
13
+ value: string;
14
+ }
15
+
16
+ interface EnvDnsInfo {
17
+ dashboardUrl?: string;
18
+ status: string;
19
+ validationRecords: DnsRecord[];
20
+ routingRecords: DnsRecord[];
21
+ errorMessage?: string;
22
+ }
23
+
24
+ function toZoneLine(r: DnsRecord): string {
25
+ return `${r.name}. 300 IN ${r.type} ${r.value}.`;
26
+ }
27
+
28
+ /**
29
+ * tib show-dns [--env dev|staging|prod] [--project-dir <path>]
30
+ *
31
+ * Reads the scaffold-config.json and prints DNS records that must be added for
32
+ * custom domain wiring: certificate validation CNAMEs (step 1) and traffic
33
+ * routing CNAMEs (step 2).
34
+ *
35
+ * For the routing CNAME the CloudFront domain is read from the CDK outputs file
36
+ * (.mc/cdk-outputs.json) if available; otherwise it is shown as a placeholder.
37
+ */
38
+ export async function runShowDns(opts: ShowDnsOptions): Promise<void> {
39
+ const projectDir = opts.projectDir ? path.resolve(opts.projectDir) : process.cwd();
40
+ const configPath = path.join(projectDir, '.tib', 'scaffold-config.json');
41
+
42
+ let config: { environments?: Record<string, { dashboardUrl?: string }> };
43
+ try {
44
+ const raw = await readFile(configPath, 'utf-8');
45
+ config = JSON.parse(raw) as typeof config;
46
+ } catch {
47
+ console.error('[tib] Could not read .mc/scaffold-config.json — run from a TIB project root.');
48
+ process.exit(1);
49
+ }
50
+
51
+ const environments = config.environments ?? {};
52
+ if (Object.keys(environments).length === 0) {
53
+ console.log('[tib] No environments configured. Set a custom domain via `tib show-dns` or the Setup tab.');
54
+ return;
55
+ }
56
+
57
+ // Try to load CDK outputs for routing records
58
+ let cdkOutputs: Record<string, Record<string, string>> = {};
59
+ try {
60
+ const outputsPath = path.join(projectDir, '.tib', 'cdk-outputs.json');
61
+ const raw = await readFile(outputsPath, 'utf-8');
62
+ cdkOutputs = JSON.parse(raw) as typeof cdkOutputs;
63
+ } catch {
64
+ // No CDK outputs yet — routing CNAMEs will show a placeholder
65
+ }
66
+
67
+ const targetEnvs = opts.env
68
+ ? [opts.env]
69
+ : Object.keys(environments);
70
+
71
+ for (const env of targetEnvs) {
72
+ const cfg = environments[env];
73
+ if (!cfg?.dashboardUrl) {
74
+ console.log(`\n;; ${env}: no custom domain configured`);
75
+ continue;
76
+ }
77
+
78
+ const info = await buildEnvDnsInfo(env, cfg.dashboardUrl, cdkOutputs);
79
+ printEnvDns(env, info);
80
+ }
81
+ }
82
+
83
+ async function buildEnvDnsInfo(
84
+ env: string,
85
+ dashboardUrl: string,
86
+ cdkOutputs: Record<string, Record<string, string>>
87
+ ): Promise<EnvDnsInfo> {
88
+ // Look for cert validation records from CDK outputs
89
+ // Cert validation CNAMEs are emitted by ACM and captured in CDK outputs under the cert resource
90
+ const certArnOutputKey = `tib-cert-${env}-arn`;
91
+ const certArn = Object.values(cdkOutputs)
92
+ .flatMap(stack => Object.entries(stack))
93
+ .find(([k]) => k === certArnOutputKey)?.[1];
94
+
95
+ // Look for the CloudFront distribution domain from CDK outputs
96
+ const distributionDomainName = Object.values(cdkOutputs)
97
+ .flatMap(stack => Object.entries(stack))
98
+ .find(([k]) => k.toLowerCase().includes('distributiondomain') && k.toLowerCase().includes(env))?.[1]
99
+ ?? `<run cdk synth — distribution domain not yet available>`;
100
+
101
+ const validationRecords: DnsRecord[] = certArn
102
+ // When CDK outputs include validation record overrides, they'd be here.
103
+ // Without a live describe-certificate call, we surface a placeholder.
104
+ ? [{ type: 'CNAME', name: `_acm-validation.${dashboardUrl}`, value: `<see ACM console for cert ${certArn}>` }]
105
+ : [{ type: 'CNAME', name: `_acm-validation.${dashboardUrl}`, value: `<deploy the FrontendCustomDomainStack first>` }];
106
+
107
+ const routingRecords: DnsRecord[] = [
108
+ { type: 'CNAME', name: dashboardUrl, value: distributionDomainName },
109
+ ];
110
+
111
+ return {
112
+ dashboardUrl,
113
+ status: certArn ? 'deployed' : 'not_deployed',
114
+ validationRecords,
115
+ routingRecords,
116
+ };
117
+ }
118
+
119
+ function printEnvDns(env: string, info: EnvDnsInfo): void {
120
+ console.log(`\n;; ${env} — ${info.dashboardUrl}`);
121
+
122
+ if (info.validationRecords.length > 0) {
123
+ console.log(';; Certificate validation records (add once, shared across deploys)');
124
+ for (const r of info.validationRecords) {
125
+ console.log(toZoneLine(r));
126
+ }
127
+ }
128
+
129
+ if (info.routingRecords.length > 0) {
130
+ console.log(';; Traffic routing records (update when CloudFront domain changes)');
131
+ for (const r of info.routingRecords) {
132
+ console.log(toZoneLine(r));
133
+ }
134
+ }
135
+
136
+ if (info.errorMessage) {
137
+ cliLogger.error({ env, errorMessage: info.errorMessage }, 'DNS info error');
138
+ }
139
+ }