@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.
- package/CHANGELOG.md +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { mkdir, writeFile, 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 { extract } from 'tar';
|
|
7
|
+
import { cliLogger } from '../utils/logger.js';
|
|
8
|
+
import {
|
|
9
|
+
fetchVersionsJson,
|
|
10
|
+
fetchModulesJson,
|
|
11
|
+
fetchModuleTarball,
|
|
12
|
+
} from '../utils/s3-fetch.js';
|
|
13
|
+
import { readManifest } from '../utils/manifest.js';
|
|
14
|
+
import { readScaffoldConfig } from '../utils/scaffold-config.js';
|
|
15
|
+
|
|
16
|
+
const SCAFFOLD_BUCKET = 'mc-scaffold';
|
|
17
|
+
|
|
18
|
+
// āā Tarball types āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
19
|
+
|
|
20
|
+
interface TarEntry {
|
|
21
|
+
path: string;
|
|
22
|
+
content: Buffer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AddSeedPageOptions {
|
|
26
|
+
pageName: string;
|
|
27
|
+
projectDir?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// āā GitHub helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
31
|
+
|
|
32
|
+
function detectGitRemote(projectDir: string): { owner: string; repo: string } | null {
|
|
33
|
+
try {
|
|
34
|
+
const url = execSync('git remote get-url origin', {
|
|
35
|
+
cwd: projectDir,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
}).trim();
|
|
39
|
+
|
|
40
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/([^.]+?)(?:\.git)?$/);
|
|
41
|
+
if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
42
|
+
|
|
43
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/);
|
|
44
|
+
if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function createGitHubPR(
|
|
53
|
+
owner: string,
|
|
54
|
+
repo: string,
|
|
55
|
+
token: string,
|
|
56
|
+
branch: string,
|
|
57
|
+
title: string,
|
|
58
|
+
body: string
|
|
59
|
+
): Promise<{ url: string; number: number }> {
|
|
60
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
61
|
+
const res = await fetch(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${token}`,
|
|
65
|
+
Accept: 'application/vnd.github+json',
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
title,
|
|
71
|
+
body,
|
|
72
|
+
head: branch,
|
|
73
|
+
base: 'develop',
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const err = await res.text();
|
|
79
|
+
throw new Error(`GitHub PR creation failed (${res.status}): ${err}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await res.json()) as { html_url: string; number: number };
|
|
83
|
+
return { url: data.html_url, number: data.number };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// āā Page discovery āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
87
|
+
|
|
88
|
+
function getCategoryForPageId(pageId: string): string {
|
|
89
|
+
const authPages = [
|
|
90
|
+
'login',
|
|
91
|
+
'signup',
|
|
92
|
+
'verify-email',
|
|
93
|
+
'forgot-password',
|
|
94
|
+
'reset-password',
|
|
95
|
+
'mfa-setup',
|
|
96
|
+
'mfa-verify',
|
|
97
|
+
'choose-org',
|
|
98
|
+
'accept-invitation',
|
|
99
|
+
'sign-out',
|
|
100
|
+
];
|
|
101
|
+
const accountPages = ['profile', 'workspace-settings', 'members', 'api-keys', 'audit-log'];
|
|
102
|
+
|
|
103
|
+
if (authPages.includes(pageId)) return 'auth';
|
|
104
|
+
if (accountPages.includes(pageId)) return 'account';
|
|
105
|
+
return 'shell';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// āā Tarball parsing āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
109
|
+
|
|
110
|
+
async function parseTarball(buf: Buffer): Promise<TarEntry[]> {
|
|
111
|
+
const decompressed = await new Promise<Buffer>((res, rej) => {
|
|
112
|
+
const gunzip = createGunzip();
|
|
113
|
+
const chunks: Buffer[] = [];
|
|
114
|
+
const input = Readable.from(buf);
|
|
115
|
+
input.pipe(gunzip);
|
|
116
|
+
gunzip.on('data', (chunk: Buffer) =>
|
|
117
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as ArrayBuffer))
|
|
118
|
+
);
|
|
119
|
+
gunzip.on('end', () => res(Buffer.concat(chunks)));
|
|
120
|
+
gunzip.on('error', rej);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const entries: TarEntry[] = [];
|
|
124
|
+
let offset = 0;
|
|
125
|
+
|
|
126
|
+
while (offset + 512 <= decompressed.length) {
|
|
127
|
+
const header = decompressed.slice(offset, offset + 512);
|
|
128
|
+
if (header.every((b) => b === 0)) break;
|
|
129
|
+
|
|
130
|
+
let name = header.slice(0, 100).toString('utf-8').replace(/\0.*/, '');
|
|
131
|
+
const prefix = header.slice(345, 500).toString('utf-8').replace(/\0.*/, '');
|
|
132
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
133
|
+
|
|
134
|
+
const sizeStr = header.slice(124, 136).toString('utf-8').replace(/\0.*/, '').trim();
|
|
135
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
136
|
+
const typeFlag = header[156];
|
|
137
|
+
const isRegularFile = typeFlag === 0x30 || typeFlag === 0 || typeFlag === undefined;
|
|
138
|
+
|
|
139
|
+
offset += 512;
|
|
140
|
+
|
|
141
|
+
if (isRegularFile && name && size > 0) {
|
|
142
|
+
const content = decompressed.slice(offset, offset + size);
|
|
143
|
+
entries.push({ path: name, content });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
offset += Math.ceil(size / 512) * 512;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return entries;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getTarEntry(entries: TarEntry[], targetPath: string): TarEntry | undefined {
|
|
153
|
+
return entries.find((e) => {
|
|
154
|
+
const normalized = e.path.replace(/^\.\//, '').replace(/^[^/]+\//, '');
|
|
155
|
+
return normalized === targetPath || e.path === targetPath || e.path.endsWith(`/${targetPath}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// āā Main export āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
160
|
+
|
|
161
|
+
export async function runAddSeedPage(opts: AddSeedPageOptions): Promise<void> {
|
|
162
|
+
const projectDir = resolve(opts.projectDir ?? process.cwd());
|
|
163
|
+
const { pageName } = opts;
|
|
164
|
+
|
|
165
|
+
console.log(`\nTIB Add Seed Page\n`);
|
|
166
|
+
console.log(`Project: ${projectDir}\n`);
|
|
167
|
+
console.log(`Page: ${pageName}\n`);
|
|
168
|
+
|
|
169
|
+
// 1. Read manifest + scaffold-config
|
|
170
|
+
const manifest = await readManifest(projectDir);
|
|
171
|
+
if (!manifest) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`.mc/manifest.json not found in ${projectDir}. Is this a TIB scaffold project?`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const scaffoldConfig = await readScaffoldConfig(projectDir);
|
|
178
|
+
const bucket = scaffoldConfig.scaffoldBucket ?? SCAFFOLD_BUCKET;
|
|
179
|
+
|
|
180
|
+
// 2. Get latest version
|
|
181
|
+
const versionsJson = await fetchVersionsJson(bucket);
|
|
182
|
+
const latestVersion = versionsJson.latest;
|
|
183
|
+
|
|
184
|
+
console.log(`Latest scaffold version: ${latestVersion}\n`);
|
|
185
|
+
|
|
186
|
+
// 3. Fetch frontend module tarball
|
|
187
|
+
const modulesJson = await fetchModulesJson(bucket, latestVersion);
|
|
188
|
+
const frontendModule = modulesJson.modules.find((m) => m.id === 'frontend');
|
|
189
|
+
if (!frontendModule) {
|
|
190
|
+
throw new Error(`Frontend module not found in modules.json for version ${latestVersion}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`Fetching frontend module...\n`);
|
|
194
|
+
|
|
195
|
+
const tarBuf = await fetchModuleTarball(
|
|
196
|
+
bucket,
|
|
197
|
+
latestVersion,
|
|
198
|
+
frontendModule.tarball,
|
|
199
|
+
frontendModule.sha256
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const tarEntries = await parseTarball(tarBuf);
|
|
203
|
+
|
|
204
|
+
// 4. Find the page in the tarball
|
|
205
|
+
const category = getCategoryForPageId(pageName);
|
|
206
|
+
const expectedPath = `frontend/src/pages/${category}/${pageName}.tsx`;
|
|
207
|
+
|
|
208
|
+
const pageEntry = getTarEntry(tarEntries, expectedPath);
|
|
209
|
+
if (!pageEntry) {
|
|
210
|
+
throw new Error(`Seed page not found at ${expectedPath} in frontend tarball`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const pageContent = pageEntry.content.toString('utf-8');
|
|
214
|
+
|
|
215
|
+
// 5. Create git branch for the addition
|
|
216
|
+
const branchName = `tib-add-seed-page/${pageName}`;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
execSync(`git -C "${projectDir}" checkout -b "${branchName}"`, {
|
|
220
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
// Branch may already exist
|
|
224
|
+
try {
|
|
225
|
+
execSync(`git -C "${projectDir}" checkout "${branchName}"`, {
|
|
226
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
throw new Error(`Failed to create or checkout branch: ${branchName}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 6. Write the seed page to disk (WITHOUT @mc-scaffold header ā it's user-owned)
|
|
234
|
+
const diskPath = join(projectDir, 'frontend', 'src', 'pages', category, `${pageName}.tsx`);
|
|
235
|
+
|
|
236
|
+
// Check if file already exists
|
|
237
|
+
try {
|
|
238
|
+
await access(diskPath);
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Seed page already exists at ${diskPath}. Remove it first before adding a new one.`
|
|
241
|
+
);
|
|
242
|
+
} catch (err: unknown) {
|
|
243
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(`Writing ${diskPath}...`);
|
|
249
|
+
await mkdir(dirname(diskPath), { recursive: true });
|
|
250
|
+
await writeFile(diskPath, pageContent, 'utf-8');
|
|
251
|
+
|
|
252
|
+
// 7. Commit changes
|
|
253
|
+
execSync(`git -C "${projectDir}" add -A`, { stdio: 'pipe' });
|
|
254
|
+
|
|
255
|
+
let hasChanges = false;
|
|
256
|
+
try {
|
|
257
|
+
const status = execSync(`git -C "${projectDir}" status --porcelain`, {
|
|
258
|
+
encoding: 'utf8',
|
|
259
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
260
|
+
}).trim();
|
|
261
|
+
hasChanges = status.length > 0;
|
|
262
|
+
} catch {
|
|
263
|
+
// ignore
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!hasChanges) {
|
|
267
|
+
console.log('No changes to commit.\n');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const commitMsg = `feat: add ${pageName} seed page (user-owned, scaffold v${latestVersion})`;
|
|
272
|
+
|
|
273
|
+
execSync(`git -C "${projectDir}" commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
execSync(`git -C "${projectDir}" push --set-upstream origin "${branchName}"`, {
|
|
277
|
+
stdio: 'pipe',
|
|
278
|
+
});
|
|
279
|
+
} catch {
|
|
280
|
+
cliLogger.warn({ branchName }, 'add-seed-page: git push failed ā skipping PR creation');
|
|
281
|
+
console.log(`\nCommit created on branch: ${branchName}`);
|
|
282
|
+
console.log('Note: push failed ā create the PR manually.\n');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 8. Create GitHub PR
|
|
287
|
+
const token = process.env['GITHUB_TOKEN'];
|
|
288
|
+
const remote = detectGitRemote(projectDir);
|
|
289
|
+
|
|
290
|
+
if (!token) {
|
|
291
|
+
console.log(`\nBranch pushed: ${branchName}`);
|
|
292
|
+
console.log('Set GITHUB_TOKEN env var to create a PR automatically.\n');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!remote) {
|
|
297
|
+
console.log(`\nBranch pushed: ${branchName}`);
|
|
298
|
+
console.log('Could not detect GitHub owner/repo from git remote ā create the PR manually.\n');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const prTitle = `Add ${pageName} seed page (user-owned, scaffold v${latestVersion})`;
|
|
303
|
+
|
|
304
|
+
const prBody = buildAddSeedPagePrBody(pageName, latestVersion, diskPath);
|
|
305
|
+
|
|
306
|
+
const prResult = await createGitHubPR(
|
|
307
|
+
remote.owner,
|
|
308
|
+
remote.repo,
|
|
309
|
+
token,
|
|
310
|
+
branchName,
|
|
311
|
+
prTitle,
|
|
312
|
+
prBody
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
console.log(`\nPR created: ${prResult.url}\n`);
|
|
316
|
+
cliLogger.info({ prUrl: prResult.url, pageName }, 'add-seed-page: PR created');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildAddSeedPagePrBody(pageName: string, version: string, filePath: string): string {
|
|
320
|
+
const lines: string[] = [
|
|
321
|
+
`## Add ${pageName} Seed Page`,
|
|
322
|
+
'',
|
|
323
|
+
'This PR adds a new seed page from the TIB scaffold library.',
|
|
324
|
+
'',
|
|
325
|
+
'### About seed pages',
|
|
326
|
+
'Seed pages are **user-owned from day one**. TIB will never auto-update them during scaffold upgrades.',
|
|
327
|
+
'You are free to customize, refactor, or remove them at any time.',
|
|
328
|
+
'',
|
|
329
|
+
'### Files added',
|
|
330
|
+
`- \`${filePath}\``,
|
|
331
|
+
'',
|
|
332
|
+
'### Scaffold version',
|
|
333
|
+
`This page was sourced from scaffold \`v${version}\`.`,
|
|
334
|
+
'',
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
return lines.join('\n');
|
|
338
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to add a new event subscriber to an existing domain.
|
|
3
|
+
* Supports --pattern to scaffold from predefined templates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFile, access } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { subscriberSkeletonTemplate } from '../templates/subscriber-skeleton.js';
|
|
9
|
+
import { singleStepProjectionTemplate } from '../templates/patterns/subscriber/single-step-projection.js';
|
|
10
|
+
import { cascadeDeletionTemplate } from '../templates/patterns/subscriber/cascade-deletion.js';
|
|
11
|
+
import { auditRelayTemplate } from '../templates/patterns/subscriber/audit-relay.js';
|
|
12
|
+
import { cliLogger } from '../utils/logger.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Valid subscriber pattern IDs.
|
|
16
|
+
*/
|
|
17
|
+
export const SUBSCRIBER_PATTERNS = [
|
|
18
|
+
'single-step-projection',
|
|
19
|
+
'cascade-deletion',
|
|
20
|
+
'audit-relay',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type SubscriberPattern = (typeof SUBSCRIBER_PATTERNS)[number];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for the add-subscriber command.
|
|
27
|
+
*/
|
|
28
|
+
export interface AddSubscriberOptions {
|
|
29
|
+
/** Domain ID in kebab-case. */
|
|
30
|
+
domain: string;
|
|
31
|
+
/** Subscriber ID in kebab-case. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Event ID to subscribe to (e.g., 'domain.event'). */
|
|
34
|
+
event: string;
|
|
35
|
+
/** Pattern template ID (optional). */
|
|
36
|
+
pattern?: SubscriberPattern;
|
|
37
|
+
/** Override directory for domain root (used in tests). */
|
|
38
|
+
domainsDir?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute the add-subscriber command: create a new event subscriber in an existing domain.
|
|
45
|
+
* @param opts - AddSubscriberOptions specifying domain, subscriber ID, and event.
|
|
46
|
+
* @throws If domain does not exist, subscriber ID is invalid, or file already exists.
|
|
47
|
+
*/
|
|
48
|
+
export async function runAddSubscriber(opts: AddSubscriberOptions): Promise<void> {
|
|
49
|
+
if (!KEBAB_REGEX.test(opts.id)) {
|
|
50
|
+
throw new Error(`Invalid subscriber id "${opts.id}" ā must be kebab-case`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!KEBAB_REGEX.test(opts.domain)) {
|
|
54
|
+
throw new Error(`Invalid domain id "${opts.domain}" ā must be kebab-case`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!opts.event || opts.event.trim() === '') {
|
|
58
|
+
throw new Error(`Event ID is required and cannot be empty`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
|
|
62
|
+
const domainDir = join(root, opts.domain);
|
|
63
|
+
const domainConfigPath = join(domainDir, 'domain.config.ts');
|
|
64
|
+
|
|
65
|
+
// Verify domain exists
|
|
66
|
+
try {
|
|
67
|
+
await access(domainConfigPath);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const subscriberFilePath = join(domainDir, 'subscribers', `${opts.id}.ts`);
|
|
73
|
+
|
|
74
|
+
// Refuse if subscriber already exists
|
|
75
|
+
try {
|
|
76
|
+
await access(subscriberFilePath);
|
|
77
|
+
throw new Error(`Subscriber "${opts.id}" already exists at ${subscriberFilePath}`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve template content
|
|
85
|
+
let subscriberContent: string;
|
|
86
|
+
if (opts.pattern) {
|
|
87
|
+
subscriberContent = resolveSubscriberPattern(opts.pattern, opts.domain, opts.id, opts.event);
|
|
88
|
+
cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
|
|
89
|
+
} else {
|
|
90
|
+
subscriberContent = subscriberSkeletonTemplate(opts.domain, opts.id, opts.event);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Write subscriber file
|
|
94
|
+
await writeFile(subscriberFilePath, subscriberContent);
|
|
95
|
+
|
|
96
|
+
cliLogger.info({ domain: opts.domain, subscriber: opts.id, event: opts.event, pattern: opts.pattern ?? 'skeleton' }, 'Subscriber added');
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.log(
|
|
99
|
+
`\nā Subscriber "${opts.id}" added to domain "${opts.domain}" ` +
|
|
100
|
+
`listening to event "${opts.event}" at ${subscriberFilePath}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a subscriber pattern template to its generated TypeScript source.
|
|
106
|
+
*/
|
|
107
|
+
function resolveSubscriberPattern(
|
|
108
|
+
pattern: SubscriberPattern,
|
|
109
|
+
domain: string,
|
|
110
|
+
id: string,
|
|
111
|
+
event: string,
|
|
112
|
+
): string {
|
|
113
|
+
switch (pattern) {
|
|
114
|
+
case 'single-step-projection':
|
|
115
|
+
return singleStepProjectionTemplate(domain, id, event);
|
|
116
|
+
case 'cascade-deletion':
|
|
117
|
+
return cascadeDeletionTemplate(domain, id, event);
|
|
118
|
+
case 'audit-relay':
|
|
119
|
+
return auditRelayTemplate(domain, id, event);
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unknown subscriber pattern "${pattern}". Available: ${SUBSCRIBER_PATTERNS.join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
}
|