@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,495 @@
|
|
|
1
|
+
import { createGunzip } from 'node:zlib';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { unlink } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { cliLogger } from '../utils/logger.js';
|
|
7
|
+
import { fetchVersionsJson, fetchModulesJson, fetchModuleTarball, fetchScaffoldFile, } from '../utils/s3-fetch.js';
|
|
8
|
+
import { computeChecksumString } from '../utils/checksum.js';
|
|
9
|
+
import { applyTokens, buildTokenContext } from '../utils/token-substitution.js';
|
|
10
|
+
import { injectHeader } from '../utils/header-inject.js';
|
|
11
|
+
import { readManifest, writeManifest, upsertManifestFile, removeManifestFile, inferPolicyFromPath, } from '../utils/manifest.js';
|
|
12
|
+
import { installScaffoldFile } from '../utils/install-file.js';
|
|
13
|
+
import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
|
|
14
|
+
const SCAFFOLD_BUCKET = 'mc-scaffold';
|
|
15
|
+
// ── Tarball parsing (same impl as create-project.ts) ──────────────────────────
|
|
16
|
+
async function parseTarball(buf) {
|
|
17
|
+
const decompressed = await new Promise((res, rej) => {
|
|
18
|
+
const gunzip = createGunzip();
|
|
19
|
+
const chunks = [];
|
|
20
|
+
const input = Readable.from(buf);
|
|
21
|
+
input.pipe(gunzip);
|
|
22
|
+
gunzip.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
23
|
+
gunzip.on('end', () => res(Buffer.concat(chunks)));
|
|
24
|
+
gunzip.on('error', rej);
|
|
25
|
+
});
|
|
26
|
+
const entries = [];
|
|
27
|
+
let offset = 0;
|
|
28
|
+
while (offset + 512 <= decompressed.length) {
|
|
29
|
+
const header = decompressed.slice(offset, offset + 512);
|
|
30
|
+
if (header.every((b) => b === 0))
|
|
31
|
+
break;
|
|
32
|
+
let name = header.slice(0, 100).toString('utf-8').replace(/\0.*/, '');
|
|
33
|
+
const prefix = header.slice(345, 500).toString('utf-8').replace(/\0.*/, '');
|
|
34
|
+
if (prefix)
|
|
35
|
+
name = `${prefix}/${name}`;
|
|
36
|
+
const sizeStr = header.slice(124, 136).toString('utf-8').replace(/\0.*/, '').trim();
|
|
37
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
38
|
+
const typeFlag = header[156];
|
|
39
|
+
const isRegularFile = typeFlag === 0x30 || typeFlag === 0 || typeFlag === undefined;
|
|
40
|
+
offset += 512;
|
|
41
|
+
if (isRegularFile && name && size > 0) {
|
|
42
|
+
const content = decompressed.slice(offset, offset + size);
|
|
43
|
+
entries.push({ path: name, content });
|
|
44
|
+
}
|
|
45
|
+
offset += Math.ceil(size / 512) * 512;
|
|
46
|
+
}
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
function getTarEntry(entries, targetPath) {
|
|
50
|
+
return entries.find((e) => {
|
|
51
|
+
const normalized = e.path.replace(/^\.\//, '').replace(/^[^/]+\//, '');
|
|
52
|
+
return normalized === targetPath || e.path === targetPath || e.path.endsWith(`/${targetPath}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if a file path is a seed file (user-owned, not auto-upgraded).
|
|
57
|
+
* Seed files include: pages/auth/*, pages/account/*, pages/shell/*, pages/_errors/*, layouts/*, styles/brand.css
|
|
58
|
+
*/
|
|
59
|
+
function isSeedFile(filePath) {
|
|
60
|
+
const seedPatterns = [
|
|
61
|
+
/^frontend\/src\/pages\/auth\//,
|
|
62
|
+
/^frontend\/src\/pages\/account\//,
|
|
63
|
+
/^frontend\/src\/pages\/shell\//,
|
|
64
|
+
/^frontend\/src\/pages\/_errors\//,
|
|
65
|
+
/^frontend\/src\/layouts\//,
|
|
66
|
+
/^frontend\/src\/styles\/brand\.css$/,
|
|
67
|
+
];
|
|
68
|
+
return seedPatterns.some((pattern) => pattern.test(filePath));
|
|
69
|
+
}
|
|
70
|
+
// ── Topological sort ───────────────────────────────────────────────────────────
|
|
71
|
+
function topoSort(modules, selected) {
|
|
72
|
+
const byId = new Map(modules.map((m) => [m.id, m]));
|
|
73
|
+
const result = [];
|
|
74
|
+
const visited = new Set();
|
|
75
|
+
function visit(id) {
|
|
76
|
+
if (visited.has(id))
|
|
77
|
+
return;
|
|
78
|
+
visited.add(id);
|
|
79
|
+
const mod = byId.get(id);
|
|
80
|
+
if (!mod)
|
|
81
|
+
return;
|
|
82
|
+
for (const dep of mod.dependencies) {
|
|
83
|
+
if (selected.has(dep))
|
|
84
|
+
visit(dep);
|
|
85
|
+
}
|
|
86
|
+
result.push(mod);
|
|
87
|
+
}
|
|
88
|
+
for (const id of ['core', 'backend-lambda']) {
|
|
89
|
+
if (selected.has(id))
|
|
90
|
+
visit(id);
|
|
91
|
+
}
|
|
92
|
+
for (const id of selected)
|
|
93
|
+
visit(id);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
// ── GitHub helpers ─────────────────────────────────────────────────────────────
|
|
97
|
+
function detectGitRemote(projectDir) {
|
|
98
|
+
try {
|
|
99
|
+
const url = execSync('git remote get-url origin', {
|
|
100
|
+
cwd: projectDir,
|
|
101
|
+
encoding: 'utf8',
|
|
102
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
}).trim();
|
|
104
|
+
// SSH: git@github.com:owner/repo.git
|
|
105
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/([^.]+?)(?:\.git)?$/);
|
|
106
|
+
if (sshMatch)
|
|
107
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
108
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
109
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/)?$/);
|
|
110
|
+
if (httpsMatch)
|
|
111
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function createGitHubPR(owner, repo, token, branch, title, body, labels) {
|
|
119
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
Authorization: `Bearer ${token}`,
|
|
124
|
+
Accept: 'application/vnd.github+json',
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
title,
|
|
130
|
+
body,
|
|
131
|
+
head: branch,
|
|
132
|
+
base: 'develop',
|
|
133
|
+
...(labels && labels.length > 0 && { labels }),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const err = await res.text();
|
|
138
|
+
throw new Error(`GitHub PR creation failed (${res.status}): ${err}`);
|
|
139
|
+
}
|
|
140
|
+
const data = (await res.json());
|
|
141
|
+
return { url: data.html_url, number: data.number };
|
|
142
|
+
}
|
|
143
|
+
// ── PR body builder ────────────────────────────────────────────────────────────
|
|
144
|
+
function buildPrBody(currentVersion, targetVersion, results, changelogUrl) {
|
|
145
|
+
const added = results.filter((r) => r.status === 'added');
|
|
146
|
+
const updated = results.filter((r) => r.status === 'updated');
|
|
147
|
+
const deleted = results.filter((r) => r.status === 'deleted');
|
|
148
|
+
const conflicts = results.filter((r) => r.status === 'conflict');
|
|
149
|
+
const lines = [
|
|
150
|
+
`## TIB Scaffold Upgrade: ${currentVersion} → ${targetVersion}`,
|
|
151
|
+
'',
|
|
152
|
+
'### Summary',
|
|
153
|
+
`- **Added:** ${added.length} file(s)`,
|
|
154
|
+
`- **Updated:** ${updated.length} file(s)`,
|
|
155
|
+
`- **Deleted:** ${deleted.length} file(s)`,
|
|
156
|
+
`- **Conflicts (drift):** ${conflicts.length} file(s)`,
|
|
157
|
+
'',
|
|
158
|
+
];
|
|
159
|
+
if (added.length > 0) {
|
|
160
|
+
lines.push('### Added files');
|
|
161
|
+
for (const r of added)
|
|
162
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
if (updated.length > 0) {
|
|
166
|
+
lines.push('### Updated files');
|
|
167
|
+
for (const r of updated)
|
|
168
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
if (deleted.length > 0) {
|
|
172
|
+
lines.push('### Deleted files');
|
|
173
|
+
for (const r of deleted)
|
|
174
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
if (conflicts.length > 0) {
|
|
178
|
+
lines.push('### Drift warnings — manual review required');
|
|
179
|
+
lines.push('The following files were locally modified after scaffold installation. ' +
|
|
180
|
+
'New versions have been written to `{path}.tib-upgrade` — merge manually then remove the `.tib-upgrade` file.');
|
|
181
|
+
for (const r of conflicts)
|
|
182
|
+
lines.push(`- \`${r.path}\` → \`${r.path}.tib-upgrade\``);
|
|
183
|
+
lines.push('');
|
|
184
|
+
}
|
|
185
|
+
if (changelogUrl) {
|
|
186
|
+
lines.push(`### Changelog`);
|
|
187
|
+
lines.push(`See full changelog: ${changelogUrl}`);
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
lines.push('### Visual diff');
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push('This PR has been automatically labelled `tib-frontend-upgrade`.');
|
|
193
|
+
lines.push('The Chromatic job will run and post a visual diff link here.');
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push('**Before merging:** A reviewer must comment `visual diff reviewed` to acknowledge they have checked the Chromatic baseline diff.');
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('> Chromatic baseline link will appear in the Chromatic job output above once CI completes.');
|
|
198
|
+
lines.push('');
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
201
|
+
function buildFrontendComponentsPrBody(currentVersion, targetVersion, results, changelogUrl) {
|
|
202
|
+
const added = results.filter((r) => r.status === 'added');
|
|
203
|
+
const updated = results.filter((r) => r.status === 'updated');
|
|
204
|
+
const deleted = results.filter((r) => r.status === 'deleted');
|
|
205
|
+
const conflicts = results.filter((r) => r.status === 'conflict');
|
|
206
|
+
const lines = [
|
|
207
|
+
`## TIB Frontend Components Upgrade: ${currentVersion} → ${targetVersion}`,
|
|
208
|
+
'',
|
|
209
|
+
'### Summary',
|
|
210
|
+
`Upgrades scaffold-tracked frontend component files only (seed pages excluded).`,
|
|
211
|
+
'',
|
|
212
|
+
`- **Added:** ${added.length} file(s)`,
|
|
213
|
+
`- **Updated:** ${updated.length} file(s)`,
|
|
214
|
+
`- **Deleted:** ${deleted.length} file(s)`,
|
|
215
|
+
`- **Conflicts (drift):** ${conflicts.length} file(s)`,
|
|
216
|
+
'',
|
|
217
|
+
];
|
|
218
|
+
if (added.length > 0) {
|
|
219
|
+
lines.push('### Added files');
|
|
220
|
+
for (const r of added)
|
|
221
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
if (updated.length > 0) {
|
|
225
|
+
lines.push('### Updated files');
|
|
226
|
+
for (const r of updated)
|
|
227
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
}
|
|
230
|
+
if (deleted.length > 0) {
|
|
231
|
+
lines.push('### Deleted files');
|
|
232
|
+
for (const r of deleted)
|
|
233
|
+
lines.push(`- \`${r.path}\` _(${r.module})_`);
|
|
234
|
+
lines.push('');
|
|
235
|
+
}
|
|
236
|
+
if (conflicts.length > 0) {
|
|
237
|
+
lines.push('### Drift warnings — manual review required');
|
|
238
|
+
lines.push('The following files were locally modified after scaffold installation. ' +
|
|
239
|
+
'New versions have been written to `{path}.tib-upgrade` — merge manually then remove the `.tib-upgrade` file.');
|
|
240
|
+
for (const r of conflicts)
|
|
241
|
+
lines.push(`- \`${r.path}\` → \`${r.path}.tib-upgrade\``);
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
if (changelogUrl) {
|
|
245
|
+
lines.push(`### Visual diff`);
|
|
246
|
+
lines.push(`See Chromatic visual diff (when available): ${changelogUrl}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push(`> **Reviewer action required:** Comment \`visual diff reviewed\` on this PR after checking the Chromatic baseline diff. The \`require-visual-diff-review\` CI check will block merge until this comment is present.`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
export async function runUpgrade(packageSpec, opts) {
|
|
254
|
+
const projectDir = resolve(opts.projectDir ?? process.cwd());
|
|
255
|
+
console.log(`\nTIB Upgrade${opts.dryRun ? ' (dry run)' : ''}\n`);
|
|
256
|
+
console.log(`Project: ${projectDir}\n`);
|
|
257
|
+
// 1. Read manifest + scaffold-config
|
|
258
|
+
const manifest = await readManifest(projectDir);
|
|
259
|
+
if (!manifest) {
|
|
260
|
+
throw new Error(`.mc/manifest.json not found in ${projectDir}. Is this a TIB scaffold project?`);
|
|
261
|
+
}
|
|
262
|
+
const scaffoldConfig = await readScaffoldConfig(projectDir);
|
|
263
|
+
const currentVersion = manifest.scaffoldVersion;
|
|
264
|
+
// 2. Resolve target version
|
|
265
|
+
const versionsJson = await fetchVersionsJson(scaffoldConfig.scaffoldBucket ?? SCAFFOLD_BUCKET);
|
|
266
|
+
const targetVersion = packageSpec ?? versionsJson.latest;
|
|
267
|
+
console.log(`Current version : ${currentVersion}`);
|
|
268
|
+
console.log(`Target version : ${targetVersion}\n`);
|
|
269
|
+
if (currentVersion === targetVersion) {
|
|
270
|
+
console.log('Already at target version — nothing to do.');
|
|
271
|
+
cliLogger.info({ currentVersion, targetVersion }, 'upgrade: already at target version');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const bucket = scaffoldConfig.scaffoldBucket ?? SCAFFOLD_BUCKET;
|
|
275
|
+
const targetModulesJson = await fetchModulesJson(bucket, targetVersion);
|
|
276
|
+
// 3. Resolve enabled modules in topological order
|
|
277
|
+
let enabledSet = new Set(scaffoldConfig.enabledModules);
|
|
278
|
+
// If --frontend-components flag is set, scope to frontend module only
|
|
279
|
+
if (opts.frontendComponents) {
|
|
280
|
+
enabledSet = new Set(['frontend']);
|
|
281
|
+
}
|
|
282
|
+
const orderedModules = topoSort(targetModulesJson.modules, enabledSet);
|
|
283
|
+
// Build current version module map (for change detection)
|
|
284
|
+
let currentModulesJson = null;
|
|
285
|
+
try {
|
|
286
|
+
currentModulesJson = await fetchModulesJson(bucket, currentVersion);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// If current release is gone from S3, proceed without skipping unchanged modules
|
|
290
|
+
}
|
|
291
|
+
const currentModuleVersionMap = new Map((currentModulesJson?.modules ?? []).map((m) => [m.id, m.version]));
|
|
292
|
+
// 4. Get CLI version
|
|
293
|
+
let cliVersion = '0.0.0';
|
|
294
|
+
try {
|
|
295
|
+
const { createRequire } = await import('node:module');
|
|
296
|
+
const require = createRequire(import.meta.url);
|
|
297
|
+
const pkgJson = require('../../package.json');
|
|
298
|
+
cliVersion = pkgJson.version;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// ignore
|
|
302
|
+
}
|
|
303
|
+
// 5. Build token context
|
|
304
|
+
const tokenCtx = buildTokenContext({
|
|
305
|
+
projectName: manifest.projectName,
|
|
306
|
+
awsRegion: manifest.awsRegion,
|
|
307
|
+
awsAccountId: scaffoldConfig.awsAccountId,
|
|
308
|
+
adminEmail: scaffoldConfig.adminEmail,
|
|
309
|
+
}, targetVersion, cliVersion);
|
|
310
|
+
// 6. Process each module
|
|
311
|
+
const allResults = [];
|
|
312
|
+
const updatedManifest = { ...manifest, files: [...manifest.files] };
|
|
313
|
+
// Track all file paths present in the target release (to detect deletions)
|
|
314
|
+
const targetFilePaths = new Set();
|
|
315
|
+
for (const mod of orderedModules) {
|
|
316
|
+
const currentModVer = currentModuleVersionMap.get(mod.id);
|
|
317
|
+
if (currentModVer !== undefined && currentModVer === mod.version) {
|
|
318
|
+
// Module version unchanged — no file changes possible; still track paths
|
|
319
|
+
const currentModFiles = manifest.files.filter((f) => f.module === mod.id);
|
|
320
|
+
for (const f of currentModFiles)
|
|
321
|
+
targetFilePaths.add(f.path);
|
|
322
|
+
cliLogger.debug({ moduleId: mod.id, version: mod.version }, 'upgrade: module unchanged, skipping');
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
console.log(` Processing module: ${mod.id}@${mod.version}...`);
|
|
326
|
+
const tarBuf = await fetchModuleTarball(bucket, targetVersion, mod.tarball, mod.sha256);
|
|
327
|
+
const tarEntries = await parseTarball(tarBuf);
|
|
328
|
+
const manifestEntry = getTarEntry(tarEntries, 'MANIFEST.json');
|
|
329
|
+
if (!manifestEntry) {
|
|
330
|
+
cliLogger.warn({ moduleId: mod.id }, 'upgrade: No MANIFEST.json in tarball — skipping module');
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const tarManifest = JSON.parse(manifestEntry.content.toString('utf-8'));
|
|
334
|
+
for (const fileEntry of tarManifest) {
|
|
335
|
+
const tarEntry = getTarEntry(tarEntries, fileEntry.path);
|
|
336
|
+
if (!tarEntry) {
|
|
337
|
+
cliLogger.warn({ moduleId: mod.id, path: fileEntry.path }, 'upgrade: file listed in MANIFEST.json not found in tarball');
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Resolve install path
|
|
341
|
+
let installPath = fileEntry.installedAs ?? fileEntry.path;
|
|
342
|
+
if (installPath.endsWith('.tpl'))
|
|
343
|
+
installPath = installPath.slice(0, -4);
|
|
344
|
+
// When --frontend-components flag is set, skip seed files
|
|
345
|
+
if (opts.frontendComponents && isSeedFile(installPath)) {
|
|
346
|
+
cliLogger.debug({ moduleId: mod.id, path: installPath }, 'upgrade: skipping seed file (--frontend-components scoped)');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
targetFilePaths.add(installPath);
|
|
350
|
+
// Process new content
|
|
351
|
+
let newContent = tarEntry.content.toString('utf-8');
|
|
352
|
+
if (fileEntry.isTemplate) {
|
|
353
|
+
newContent = applyTokens(newContent, tokenCtx);
|
|
354
|
+
}
|
|
355
|
+
newContent = injectHeader(newContent, installPath, mod.id, mod.version);
|
|
356
|
+
// Determine policy: use fileEntry.policy if present, else infer from path
|
|
357
|
+
const policy = fileEntry.policy ?? inferPolicyFromPath(installPath);
|
|
358
|
+
// Look up current manifest entry for this file
|
|
359
|
+
const currentEntry = updatedManifest.files.find((f) => f.path === installPath);
|
|
360
|
+
const diskPath = join(projectDir, installPath);
|
|
361
|
+
// Install file using policy-based dispatch
|
|
362
|
+
const installResult = await installScaffoldFile(diskPath, newContent, policy, currentEntry, {
|
|
363
|
+
dryRun: opts.dryRun,
|
|
364
|
+
});
|
|
365
|
+
// Handle result
|
|
366
|
+
if (installResult.status === 'skipped') {
|
|
367
|
+
// Skipped files are not tracked in results (e.g., seed file with --frontend-components)
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
// Add to results
|
|
371
|
+
allResults.push({ path: installPath, status: installResult.status, module: mod.id });
|
|
372
|
+
// Update manifest if file was not skipped
|
|
373
|
+
const newChecksum = computeChecksumString(newContent);
|
|
374
|
+
upsertManifestFile(updatedManifest, {
|
|
375
|
+
path: installPath,
|
|
376
|
+
module: mod.id,
|
|
377
|
+
moduleVersion: mod.version,
|
|
378
|
+
sha256: newChecksum,
|
|
379
|
+
wasTemplate: fileEntry.isTemplate,
|
|
380
|
+
installedAt: new Date().toISOString(),
|
|
381
|
+
policy,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// 7. Handle deleted files (in current manifest but not in target release)
|
|
386
|
+
const currentManagedFiles = manifest.files.filter((f) => enabledSet.has(f.module));
|
|
387
|
+
for (const f of currentManagedFiles) {
|
|
388
|
+
if (!targetFilePaths.has(f.path)) {
|
|
389
|
+
const diskPath = join(projectDir, f.path);
|
|
390
|
+
if (!opts.dryRun) {
|
|
391
|
+
try {
|
|
392
|
+
await unlink(diskPath);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
if (err.code !== 'ENOENT')
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
removeManifestFile(updatedManifest, f.path);
|
|
400
|
+
allResults.push({ path: f.path, status: 'deleted', module: f.module });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// 8. Print upgrade report
|
|
404
|
+
const added = allResults.filter((r) => r.status === 'added');
|
|
405
|
+
const updated = allResults.filter((r) => r.status === 'updated');
|
|
406
|
+
const deleted = allResults.filter((r) => r.status === 'deleted');
|
|
407
|
+
const conflicts = allResults.filter((r) => r.status === 'conflict');
|
|
408
|
+
console.log('\nUpgrade report:');
|
|
409
|
+
console.log(` Added : ${added.length}`);
|
|
410
|
+
console.log(` Updated : ${updated.length}`);
|
|
411
|
+
console.log(` Deleted : ${deleted.length}`);
|
|
412
|
+
console.log(` Conflicts (drift): ${conflicts.length}`);
|
|
413
|
+
if (conflicts.length > 0) {
|
|
414
|
+
console.log('\nDrift warnings:');
|
|
415
|
+
for (const r of conflicts) {
|
|
416
|
+
console.log(` ${r.path} — new version written to ${r.path}.tib-upgrade`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (opts.dryRun) {
|
|
420
|
+
console.log('\nDry run complete — no files were written.\n');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// 9. Update manifest + scaffold-config
|
|
424
|
+
updatedManifest.scaffoldVersion = targetVersion;
|
|
425
|
+
await writeManifest(projectDir, updatedManifest);
|
|
426
|
+
const updatedConfig = { ...scaffoldConfig, scaffoldVersion: targetVersion };
|
|
427
|
+
await writeScaffoldConfig(projectDir, updatedConfig);
|
|
428
|
+
// 10. Commit on upgrade branch
|
|
429
|
+
const branchName = `tib-upgrade/${targetVersion}`;
|
|
430
|
+
try {
|
|
431
|
+
execSync(`git -C "${projectDir}" checkout -b "${branchName}"`, {
|
|
432
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Branch may already exist
|
|
437
|
+
execSync(`git -C "${projectDir}" checkout "${branchName}"`, {
|
|
438
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
execSync(`git -C "${projectDir}" add -A`, { stdio: 'pipe' });
|
|
442
|
+
let hasChanges = false;
|
|
443
|
+
try {
|
|
444
|
+
const status = execSync(`git -C "${projectDir}" status --porcelain`, {
|
|
445
|
+
encoding: 'utf8',
|
|
446
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
447
|
+
}).trim();
|
|
448
|
+
hasChanges = status.length > 0;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// ignore
|
|
452
|
+
}
|
|
453
|
+
if (hasChanges) {
|
|
454
|
+
execSync(`git -C "${projectDir}" commit -m "chore: tib scaffold upgrade ${currentVersion} -> ${targetVersion}"`, { stdio: 'pipe' });
|
|
455
|
+
try {
|
|
456
|
+
execSync(`git -C "${projectDir}" push --set-upstream origin "${branchName}"`, {
|
|
457
|
+
stdio: 'pipe',
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
cliLogger.warn({ branchName }, 'upgrade: git push failed — skipping PR creation');
|
|
462
|
+
console.log(`\nCommit created on branch: ${branchName}`);
|
|
463
|
+
console.log('Note: push failed — create the PR manually.\n');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// 11. Create GitHub PR
|
|
467
|
+
const token = opts.githubToken ?? process.env['GITHUB_TOKEN'];
|
|
468
|
+
const remote = detectGitRemote(projectDir);
|
|
469
|
+
if (!token) {
|
|
470
|
+
console.log(`\nBranch pushed: ${branchName}`);
|
|
471
|
+
console.log('Set GITHUB_TOKEN or pass --github-token to create a PR automatically.\n');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (!remote) {
|
|
475
|
+
console.log(`\nBranch pushed: ${branchName}`);
|
|
476
|
+
console.log('Could not detect GitHub owner/repo from git remote — create the PR manually.\n');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const changelogUrl = await fetchScaffoldFile(bucket, targetVersion, 'CHANGELOG.md')
|
|
480
|
+
.then((c) => (c ? `https://${bucket}.s3.amazonaws.com/versions/${targetVersion}/CHANGELOG.md` : null))
|
|
481
|
+
.catch(() => null);
|
|
482
|
+
const prBody = opts.frontendComponents
|
|
483
|
+
? buildFrontendComponentsPrBody(currentVersion, targetVersion, allResults, changelogUrl)
|
|
484
|
+
: buildPrBody(currentVersion, targetVersion, allResults, changelogUrl);
|
|
485
|
+
const prResult = await createGitHubPR(remote.owner, remote.repo, token, branchName, opts.frontendComponents
|
|
486
|
+
? `chore: tib scaffold frontend components upgrade ${currentVersion} → ${targetVersion}`
|
|
487
|
+
: `chore: tib scaffold upgrade ${currentVersion} → ${targetVersion}`, prBody, opts.frontendComponents ? ['tib-frontend-upgrade'] : undefined);
|
|
488
|
+
console.log(`\nPR created: ${prResult.url}\n`);
|
|
489
|
+
cliLogger.info({ prUrl: prResult.url, targetVersion }, 'upgrade: PR created');
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log('\nNo file changes — skipping commit and PR.\n');
|
|
493
|
+
}
|
|
494
|
+
cliLogger.info({ currentVersion, targetVersion, added: added.length, updated: updated.length, deleted: deleted.length, conflicts: conflicts.length }, 'upgrade complete');
|
|
495
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DomainModuleConfig } from '@mettlecast/domain-runtime/types';
|
|
2
|
+
/**
|
|
3
|
+
* A single validation failure.
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationError {
|
|
6
|
+
/** Short error code for CI parsing. */
|
|
7
|
+
code: string;
|
|
8
|
+
/** Human-readable message. */
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Result of the validate command.
|
|
13
|
+
*/
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
/** true if all checks passed. */
|
|
16
|
+
valid: boolean;
|
|
17
|
+
/** List of validation failures. Empty when valid is true. */
|
|
18
|
+
errors: ValidationError[];
|
|
19
|
+
/** Non-fatal warnings from the registry build. */
|
|
20
|
+
warnings: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run the validate command: build the registry and perform structural validation.
|
|
24
|
+
* Exits the process with code 1 if validation fails (CI gate usage).
|
|
25
|
+
* @param domainRoot - Absolute or relative path to the domain root directory.
|
|
26
|
+
* @param exitOnFailure - If true, calls process.exit(1) on failure. Defaults to true.
|
|
27
|
+
* @param config - Optional domain module configuration. Defaults to { mode: 'optional' }.
|
|
28
|
+
* @returns ValidationResult for programmatic use (e.g. in tests).
|
|
29
|
+
*/
|
|
30
|
+
export declare function runValidate(domainRoot: string, exitOnFailure?: boolean, config?: DomainModuleConfig): Promise<ValidationResult>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { validRange } from 'semver';
|
|
4
|
+
import { buildRegistry } from '../builder/build-registry.js';
|
|
5
|
+
import { cliLogger } from '../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Check for duplicate IDs in a list of registry entries.
|
|
8
|
+
* @param entries - Array of registry entries to check.
|
|
9
|
+
* @param kind - Kind name for error messages (e.g. 'api', 'webhook').
|
|
10
|
+
* @returns Array of ValidationError objects for any duplicates found.
|
|
11
|
+
*/
|
|
12
|
+
function checkDuplicateIds(entries, kind) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const errors = [];
|
|
15
|
+
for (const e of entries) {
|
|
16
|
+
if (seen.has(e.id)) {
|
|
17
|
+
errors.push({ code: 'DUPLICATE_ID', message: `Duplicate ${kind} id: '${e.id}'` });
|
|
18
|
+
}
|
|
19
|
+
seen.add(e.id);
|
|
20
|
+
}
|
|
21
|
+
return errors;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check that every subscriber entry has a valid semver range string.
|
|
25
|
+
* @param entries - Subscriber registry entries to check.
|
|
26
|
+
* @returns Validation errors for any entry with an unparseable semver range.
|
|
27
|
+
*/
|
|
28
|
+
function checkSubscriberSemverRanges(entries) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.semverRange && !validRange(entry.semverRange)) {
|
|
32
|
+
errors.push({
|
|
33
|
+
code: 'INVALID_SEMVER_RANGE',
|
|
34
|
+
message: `Subscriber '${entry.id}' has invalid semver range: '${entry.semverRange}'`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check version lifecycle consistency: sunsetAt must be present and at least
|
|
42
|
+
* 90 days after deprecatedAt when both are specified.
|
|
43
|
+
* @param lifecycle - Optional lifecycle metadata from a registry entry.
|
|
44
|
+
* @param entryLabel - Human-readable label for error messages.
|
|
45
|
+
* @returns Validation errors for lifecycle inconsistencies.
|
|
46
|
+
*/
|
|
47
|
+
function checkLifecycleConsistency(lifecycle, entryLabel) {
|
|
48
|
+
if (!lifecycle?.deprecatedAt)
|
|
49
|
+
return [];
|
|
50
|
+
const errors = [];
|
|
51
|
+
const deprecated = new Date(lifecycle.deprecatedAt);
|
|
52
|
+
if (!lifecycle.sunsetAt) {
|
|
53
|
+
errors.push({
|
|
54
|
+
code: 'MISSING_SUNSET_DATE',
|
|
55
|
+
message: `${entryLabel}: deprecatedAt is set but sunsetAt is missing (90-day rule requires a sunset date)`,
|
|
56
|
+
});
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
const sunset = new Date(lifecycle.sunsetAt);
|
|
60
|
+
const minSunset = new Date(deprecated.getTime() + 90 * 24 * 60 * 60 * 1000);
|
|
61
|
+
if (sunset < minSunset) {
|
|
62
|
+
errors.push({
|
|
63
|
+
code: 'SUNSET_TOO_SOON',
|
|
64
|
+
message: `${entryLabel}: sunsetAt must be at least 90 days after deprecatedAt (90-day deprecation rule)`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Stub: check for backend files not under `.mc/domains/` when mode is enforced or strict.
|
|
71
|
+
* Full file-tree scan is deferred to v1.x — this stub records the intent.
|
|
72
|
+
* @param _repoRoot - Absolute path to the repository root.
|
|
73
|
+
* @param config - Enforcement configuration.
|
|
74
|
+
* @returns Empty array (scan not yet implemented).
|
|
75
|
+
*/
|
|
76
|
+
async function checkRawPathViolations(_repoRoot, config) {
|
|
77
|
+
if (config.mode === 'optional')
|
|
78
|
+
return [];
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run the validate command: build the registry and perform structural validation.
|
|
83
|
+
* Exits the process with code 1 if validation fails (CI gate usage).
|
|
84
|
+
* @param domainRoot - Absolute or relative path to the domain root directory.
|
|
85
|
+
* @param exitOnFailure - If true, calls process.exit(1) on failure. Defaults to true.
|
|
86
|
+
* @param config - Optional domain module configuration. Defaults to { mode: 'optional' }.
|
|
87
|
+
* @returns ValidationResult for programmatic use (e.g. in tests).
|
|
88
|
+
*/
|
|
89
|
+
export async function runValidate(domainRoot, exitOnFailure = true, config = { mode: 'optional' }) {
|
|
90
|
+
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
91
|
+
const errors = [];
|
|
92
|
+
// Log any build warnings (e.g. tsx import failures) immediately.
|
|
93
|
+
for (const w of warnings) {
|
|
94
|
+
cliLogger.warn(w);
|
|
95
|
+
}
|
|
96
|
+
errors.push(...checkDuplicateIds(registry.apis, 'api'), ...checkDuplicateIds(registry.webhooks, 'webhook'), ...checkDuplicateIds(registry.subscribers, 'subscriber'), ...checkDuplicateIds(registry.schedules, 'schedule'), ...checkDuplicateIds(registry.jobs, 'job'), ...checkDuplicateIds(registry.actions, 'action'), ...checkDuplicateIds(registry.integrations, 'integration'), ...checkDuplicateIds(registry.events, 'event'));
|
|
97
|
+
const entriesWithFiles = [
|
|
98
|
+
...registry.apis,
|
|
99
|
+
...registry.webhooks,
|
|
100
|
+
...registry.subscribers,
|
|
101
|
+
...registry.schedules,
|
|
102
|
+
...registry.jobs,
|
|
103
|
+
...registry.actions,
|
|
104
|
+
];
|
|
105
|
+
await Promise.all(entriesWithFiles.map(async (entry) => {
|
|
106
|
+
if (!entry.handlerFile)
|
|
107
|
+
return;
|
|
108
|
+
const absPath = join(registry.domainRoot, entry.handlerFile);
|
|
109
|
+
try {
|
|
110
|
+
await stat(absPath);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
errors.push({
|
|
114
|
+
code: 'MISSING_HANDLER_FILE',
|
|
115
|
+
message: `Handler file not found for ${entry.kind} '${entry.id}': ${absPath}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
119
|
+
errors.push(...checkSubscriberSemverRanges(registry.subscribers));
|
|
120
|
+
for (const api of registry.apis) {
|
|
121
|
+
const entry = api;
|
|
122
|
+
errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
|
|
123
|
+
}
|
|
124
|
+
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|
|
125
|
+
errors.push(...rawPathErrors);
|
|
126
|
+
const totalPrimitives = registry.apis.length + registry.webhooks.length +
|
|
127
|
+
registry.subscribers.length + registry.schedules.length +
|
|
128
|
+
registry.jobs.length + registry.actions.length;
|
|
129
|
+
if (totalPrimitives === 0) {
|
|
130
|
+
errors.push({ code: 'EMPTY_DOMAIN', message: 'Domain has no primitives defined' });
|
|
131
|
+
}
|
|
132
|
+
const valid = errors.length === 0;
|
|
133
|
+
if (valid) {
|
|
134
|
+
cliLogger.info({ domain: registry.domain.id, primitives: totalPrimitives, warnings: warnings.length }, 'Validation passed');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const e of errors) {
|
|
138
|
+
cliLogger.error({ code: e.code }, e.message);
|
|
139
|
+
}
|
|
140
|
+
if (warnings.length > 0) {
|
|
141
|
+
cliLogger.warn({ count: warnings.length }, 'Build warnings were encountered (see above)');
|
|
142
|
+
}
|
|
143
|
+
if (exitOnFailure) {
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { valid, errors, warnings };
|
|
148
|
+
}
|