@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,181 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { cliLogger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/** A single API entry in the catalog. */
|
|
6
|
+
export interface CatalogApi {
|
|
7
|
+
id: string;
|
|
8
|
+
domainId: string;
|
|
9
|
+
path: string;
|
|
10
|
+
method: string;
|
|
11
|
+
authType: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
requestSchema?: Record<string, unknown>;
|
|
14
|
+
responseSchema?: Record<string, unknown>;
|
|
15
|
+
versions?: Array<{ version: string; requestSchema?: Record<string, unknown>; responseSchema?: Record<string, unknown> }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A single action entry in the catalog. */
|
|
19
|
+
export interface CatalogAction {
|
|
20
|
+
id: string;
|
|
21
|
+
domainId: string;
|
|
22
|
+
visibility: string;
|
|
23
|
+
idempotent: boolean;
|
|
24
|
+
description?: string;
|
|
25
|
+
inputSchema?: Record<string, unknown>;
|
|
26
|
+
outputSchema?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A single event entry in the catalog. */
|
|
30
|
+
export interface CatalogEvent {
|
|
31
|
+
id: string;
|
|
32
|
+
domainId: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
versions?: Array<{ version: number; schema: Record<string, unknown> }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A single subscriber entry in the catalog. */
|
|
38
|
+
export interface CatalogSubscriber {
|
|
39
|
+
id: string;
|
|
40
|
+
domainId: string;
|
|
41
|
+
event: string;
|
|
42
|
+
semverRange: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A single domain entry in the catalog. */
|
|
47
|
+
export interface CatalogDomain {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
tenancy: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The combined domain catalog written to .mc/domain-registry.json. */
|
|
55
|
+
export interface DomainCatalog {
|
|
56
|
+
version: 2;
|
|
57
|
+
generatedAt: string;
|
|
58
|
+
domains: CatalogDomain[];
|
|
59
|
+
apis: CatalogApi[];
|
|
60
|
+
actions: CatalogAction[];
|
|
61
|
+
events: CatalogEvent[];
|
|
62
|
+
subscribers: CatalogSubscriber[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build the combined domain catalog from all per-domain registry files.
|
|
67
|
+
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
68
|
+
* @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
|
|
69
|
+
* @returns The written catalog.
|
|
70
|
+
*/
|
|
71
|
+
export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
|
|
72
|
+
const dir = tibDir ? resolve(tibDir) : join(process.cwd(), '.tib');
|
|
73
|
+
|
|
74
|
+
// Find all per-domain registry files
|
|
75
|
+
let files: string[];
|
|
76
|
+
try {
|
|
77
|
+
files = await readdir(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(`build-catalog: .tib directory not found at ${dir}. Run tib build first.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
|
|
83
|
+
if (registryFiles.length === 0) {
|
|
84
|
+
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run tib build <domain> first.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const catalog: DomainCatalog = {
|
|
88
|
+
version: 2,
|
|
89
|
+
generatedAt: new Date().toISOString(),
|
|
90
|
+
domains: [],
|
|
91
|
+
apis: [],
|
|
92
|
+
actions: [],
|
|
93
|
+
events: [],
|
|
94
|
+
subscribers: [],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
for (const file of registryFiles) {
|
|
98
|
+
const raw = await readFile(join(dir, file), 'utf8');
|
|
99
|
+
let registry: Record<string, unknown>;
|
|
100
|
+
try {
|
|
101
|
+
registry = JSON.parse(raw) as Record<string, unknown>;
|
|
102
|
+
} catch {
|
|
103
|
+
cliLogger.warn({ file }, 'Skipping invalid JSON registry file');
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const domainEntry = registry['domain'] as Record<string, unknown> | undefined;
|
|
108
|
+
if (!domainEntry) {
|
|
109
|
+
cliLogger.warn({ file }, 'Skipping registry file with no domain entry');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const domainId = String(domainEntry['id']);
|
|
114
|
+
|
|
115
|
+
catalog.domains.push({
|
|
116
|
+
id: domainId,
|
|
117
|
+
name: String(domainEntry['name'] ?? domainId),
|
|
118
|
+
tenancy: String(domainEntry['tenancy'] ?? 'none'),
|
|
119
|
+
description: domainEntry['description'] as string | undefined,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const apis = (registry['apis'] as Record<string, unknown>[] | undefined) ?? [];
|
|
123
|
+
for (const api of apis) {
|
|
124
|
+
catalog.apis.push({
|
|
125
|
+
id: String(api['id']),
|
|
126
|
+
domainId,
|
|
127
|
+
path: String(api['path']),
|
|
128
|
+
method: String(api['method'] ?? 'ANY'),
|
|
129
|
+
authType: String(api['authType'] ?? 'jwt'),
|
|
130
|
+
description: api['description'] as string | undefined,
|
|
131
|
+
requestSchema: api['requestSchema'] as Record<string, unknown> | undefined,
|
|
132
|
+
responseSchema: api['responseSchema'] as Record<string, unknown> | undefined,
|
|
133
|
+
versions: api['versions'] as CatalogApi['versions'],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const actions = (registry['actions'] as Record<string, unknown>[] | undefined) ?? [];
|
|
138
|
+
for (const action of actions) {
|
|
139
|
+
catalog.actions.push({
|
|
140
|
+
id: String(action['id']),
|
|
141
|
+
domainId,
|
|
142
|
+
visibility: String(action['visibility'] ?? 'private'),
|
|
143
|
+
idempotent: Boolean(action['idempotent'] ?? false),
|
|
144
|
+
description: action['description'] as string | undefined,
|
|
145
|
+
inputSchema: action['inputSchema'] as Record<string, unknown> | undefined,
|
|
146
|
+
outputSchema: action['outputSchema'] as Record<string, unknown> | undefined,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const events = (registry['events'] as Record<string, unknown>[] | undefined) ?? [];
|
|
151
|
+
for (const event of events) {
|
|
152
|
+
catalog.events.push({
|
|
153
|
+
id: String(event['id']),
|
|
154
|
+
domainId,
|
|
155
|
+
description: event['description'] as string | undefined,
|
|
156
|
+
versions: event['versions'] as CatalogEvent['versions'],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const subscribers = (registry['subscribers'] as Record<string, unknown>[] | undefined) ?? [];
|
|
161
|
+
for (const sub of subscribers) {
|
|
162
|
+
catalog.subscribers.push({
|
|
163
|
+
id: String(sub['id']),
|
|
164
|
+
domainId,
|
|
165
|
+
event: String(sub['event']),
|
|
166
|
+
semverRange: String(sub['semverRange'] ?? '*'),
|
|
167
|
+
description: sub['description'] as string | undefined,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const outPath = join(dir, 'domain-registry.json');
|
|
173
|
+
await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
|
|
174
|
+
|
|
175
|
+
cliLogger.info(
|
|
176
|
+
{ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length },
|
|
177
|
+
'Domain catalog written'
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return catalog;
|
|
181
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to build flows into a registry JSON file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import type { FlowRegistry, FlowRegistryEntry } from '@mettlecast/domain-cdk-packer';
|
|
9
|
+
import { loadModuleExports } from '../builder/load-module.js';
|
|
10
|
+
import { cliLogger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for the build-flows command.
|
|
14
|
+
*/
|
|
15
|
+
export interface BuildFlowsOptions {
|
|
16
|
+
/** Project root directory. Defaults to current working directory. */
|
|
17
|
+
projectRoot?: string;
|
|
18
|
+
/** Output path for the flows registry JSON file. Defaults to .mc/flows-registry.json. */
|
|
19
|
+
outFile?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Zod schema for validating FlowConfig.
|
|
24
|
+
* Mirrors the schema from domain-runtime's defineFlow.
|
|
25
|
+
*/
|
|
26
|
+
const DomainActionStepSchema = z.object({
|
|
27
|
+
type: z.literal('domain-action'),
|
|
28
|
+
name: z.string().min(1),
|
|
29
|
+
domainId: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
30
|
+
actionId: z.string().min(1),
|
|
31
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
waitForTaskToken: z.boolean().optional(),
|
|
33
|
+
next: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const DomainApiStepSchema = z.object({
|
|
37
|
+
type: z.literal('domain-api'),
|
|
38
|
+
name: z.string().min(1),
|
|
39
|
+
domainId: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
40
|
+
apiId: z.string().min(1),
|
|
41
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
42
|
+
next: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const DomainEventStepSchema = z.object({
|
|
46
|
+
type: z.literal('domain-event'),
|
|
47
|
+
name: z.string().min(1),
|
|
48
|
+
eventId: z.string().refine(val => val.includes('.'), { message: 'eventId must be namespaced (e.g. domain.event)' }),
|
|
49
|
+
payload: z.record(z.string(), z.unknown()),
|
|
50
|
+
version: z.number().int().positive(),
|
|
51
|
+
next: z.string().optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const FlowControlStepSchema = z.discriminatedUnion('control', [
|
|
55
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('choice'), name: z.string(), choices: z.array(z.object({ when: z.string(), next: z.string() })), default: z.string().optional() }),
|
|
56
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('parallel'), name: z.string(), branches: z.array(z.array(z.unknown())), next: z.string().optional() }),
|
|
57
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('wait'), name: z.string(), seconds: z.number().int().positive(), next: z.string().optional() }),
|
|
58
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('succeed'), name: z.string() }),
|
|
59
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('fail'), name: z.string(), cause: z.string().optional(), error: z.string().optional() }),
|
|
60
|
+
z.object({ type: z.literal('flow-control'), control: z.literal('pass'), name: z.string(), result: z.unknown().optional(), next: z.string().optional() }),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const FlowStepSchema = z.union([
|
|
64
|
+
DomainActionStepSchema,
|
|
65
|
+
DomainApiStepSchema,
|
|
66
|
+
DomainEventStepSchema,
|
|
67
|
+
FlowControlStepSchema,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const FlowConfigSchema = z.object({
|
|
71
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
72
|
+
owningDomain: z.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
|
|
73
|
+
type: z.enum(['express', 'standard']).optional(),
|
|
74
|
+
name: z.string().min(1),
|
|
75
|
+
steps: z.array(FlowStepSchema).min(1),
|
|
76
|
+
trigger: z.object({
|
|
77
|
+
type: z.literal('event'),
|
|
78
|
+
eventId: z.string().includes('.'),
|
|
79
|
+
semverRange: z.string().min(1),
|
|
80
|
+
}).optional(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Custom error for flow ID collisions.
|
|
85
|
+
*/
|
|
86
|
+
export class FlowIdCollisionError extends Error {
|
|
87
|
+
constructor(id: string) {
|
|
88
|
+
super(`Flow id "${id}" appears in multiple sources`);
|
|
89
|
+
this.name = 'FlowIdCollisionError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadFlowsFromDir(dir: string, owningDomainOverride?: string): Promise<FlowRegistryEntry[]> {
|
|
94
|
+
const entries: FlowRegistryEntry[] = [];
|
|
95
|
+
let files: string[];
|
|
96
|
+
try {
|
|
97
|
+
files = await readdir(dir);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return entries;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
if (file.endsWith('.ts')) {
|
|
105
|
+
const filePath = join(dir, file);
|
|
106
|
+
const exports = await loadModuleExports(filePath);
|
|
107
|
+
for (const exp of exports) {
|
|
108
|
+
if (exp._kind !== 'flow') continue;
|
|
109
|
+
const config = FlowConfigSchema.parse(exp);
|
|
110
|
+
entries.push({
|
|
111
|
+
id: config.id,
|
|
112
|
+
owningDomain: owningDomainOverride ?? config.owningDomain,
|
|
113
|
+
type: config.type,
|
|
114
|
+
name: config.name,
|
|
115
|
+
steps: config.steps as any,
|
|
116
|
+
trigger: config.trigger as any,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} else if (file.endsWith('.json')) {
|
|
120
|
+
const filePath = join(dir, file);
|
|
121
|
+
const raw = await readFile(filePath, 'utf8');
|
|
122
|
+
const config = FlowConfigSchema.parse(JSON.parse(raw));
|
|
123
|
+
entries.push({
|
|
124
|
+
id: config.id,
|
|
125
|
+
owningDomain: owningDomainOverride ?? config.owningDomain,
|
|
126
|
+
type: config.type,
|
|
127
|
+
name: config.name,
|
|
128
|
+
steps: config.steps as any,
|
|
129
|
+
trigger: config.trigger as any,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute the build-flows command: scan flows/ and domains/x/flows/ directories for .ts and .json files,
|
|
138
|
+
* validate and serialize them, and write .mc/flows-registry.json.
|
|
139
|
+
* @param options - BuildFlowsOptions specifying project root and optional output path.
|
|
140
|
+
* @returns The absolute path to the written registry file.
|
|
141
|
+
* @throws If flow validation fails or IDs collide.
|
|
142
|
+
*/
|
|
143
|
+
export async function runBuildFlows(options: BuildFlowsOptions): Promise<string> {
|
|
144
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
145
|
+
const outFile = options.outFile ?? join(projectRoot, '.tib', 'flows-registry.json');
|
|
146
|
+
|
|
147
|
+
cliLogger.info({ projectRoot }, 'Building flows registry');
|
|
148
|
+
|
|
149
|
+
const allEntries: FlowRegistryEntry[] = [];
|
|
150
|
+
const seenIds = new Set<string>();
|
|
151
|
+
|
|
152
|
+
// Scan root-level flows/ (deprecated)
|
|
153
|
+
const rootFlowsDir = join(projectRoot, 'flows');
|
|
154
|
+
const rootEntries = await loadFlowsFromDir(rootFlowsDir);
|
|
155
|
+
if (rootEntries.length > 0) {
|
|
156
|
+
cliLogger.warn(
|
|
157
|
+
{ dir: rootFlowsDir },
|
|
158
|
+
'Root-level flows/ is deprecated. Move flows to domains/{x}/flows/ and re-run build-flows.'
|
|
159
|
+
);
|
|
160
|
+
for (const e of rootEntries) {
|
|
161
|
+
if (seenIds.has(e.id)) throw new FlowIdCollisionError(e.id);
|
|
162
|
+
seenIds.add(e.id);
|
|
163
|
+
allEntries.push(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Scan domains/*/flows/ (preferred)
|
|
168
|
+
const domainsDir = join(projectRoot, 'domains');
|
|
169
|
+
let domainEntries: string[];
|
|
170
|
+
try {
|
|
171
|
+
domainEntries = (await readdir(domainsDir, { withFileTypes: true }))
|
|
172
|
+
.filter(e => e.isDirectory())
|
|
173
|
+
.map(e => e.name);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
176
|
+
domainEntries = [];
|
|
177
|
+
} else {
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const domainId of domainEntries) {
|
|
183
|
+
const domainFlowsDir = join(domainsDir, domainId, 'flows');
|
|
184
|
+
const domainFlowEntries = await loadFlowsFromDir(domainFlowsDir, domainId);
|
|
185
|
+
for (const e of domainFlowEntries) {
|
|
186
|
+
if (seenIds.has(e.id)) throw new FlowIdCollisionError(e.id);
|
|
187
|
+
seenIds.add(e.id);
|
|
188
|
+
allEntries.push(e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const registry: FlowRegistry = { schemaVersion: '1', flows: allEntries };
|
|
193
|
+
await mkdir(join(outFile, '..'), { recursive: true });
|
|
194
|
+
await writeFile(outFile, JSON.stringify(registry, null, 2), 'utf8');
|
|
195
|
+
|
|
196
|
+
cliLogger.info({ outFile, flowCount: registry.flows.length }, 'Flows registry written');
|
|
197
|
+
return outFile;
|
|
198
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
|
|
4
|
+
import { buildRegistry } from '../builder/build-registry.js';
|
|
5
|
+
import { buildActionsTypes } from '../builder/build-types.js';
|
|
6
|
+
import { cliLogger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for the build command.
|
|
10
|
+
*/
|
|
11
|
+
export interface BuildOptions {
|
|
12
|
+
/** Absolute or relative path to the domain root directory. */
|
|
13
|
+
domainRoot: string;
|
|
14
|
+
/** Output path for the registry JSON file. Defaults to .mc/domain-registry.json relative to cwd. */
|
|
15
|
+
outFile?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute the build command: load the domain source directory, build the DomainRegistry,
|
|
20
|
+
* and write the serialised JSON snapshot to disk.
|
|
21
|
+
* @param options - BuildOptions specifying domain root and optional output path.
|
|
22
|
+
* @returns The absolute path to the written registry file.
|
|
23
|
+
* @throws If domain.config.ts is missing or the registry build fails.
|
|
24
|
+
*/
|
|
25
|
+
export async function runBuild(options: BuildOptions): Promise<string> {
|
|
26
|
+
const domainRoot = resolve(options.domainRoot);
|
|
27
|
+
const outFile = options.outFile
|
|
28
|
+
? resolve(options.outFile)
|
|
29
|
+
: join(process.cwd(), '.tib', 'domain-registry.json');
|
|
30
|
+
|
|
31
|
+
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
32
|
+
|
|
33
|
+
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
34
|
+
|
|
35
|
+
for (const w of warnings) {
|
|
36
|
+
cliLogger.warn(w);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await mkdir(join(outFile, '..'), { recursive: true });
|
|
40
|
+
await writeFile(outFile, JSON.stringify(registry, null, 2), 'utf8');
|
|
41
|
+
|
|
42
|
+
cliLogger.info({ outFile, apis: registry.apis.length, events: registry.events.length }, 'Registry written');
|
|
43
|
+
|
|
44
|
+
// Discover all sibling registries and emit aggregated types
|
|
45
|
+
const tibDir = join(process.cwd(), '.tib');
|
|
46
|
+
const registryFiles = (await readdir(tibDir)).filter(f => f.endsWith('-registry.json'));
|
|
47
|
+
const allRegistries: DomainRegistry[] = [];
|
|
48
|
+
for (const f of registryFiles) {
|
|
49
|
+
const raw = await readFile(join(tibDir, f), 'utf8');
|
|
50
|
+
allRegistries.push(JSON.parse(raw) as DomainRegistry);
|
|
51
|
+
}
|
|
52
|
+
const typesContent = buildActionsTypes(allRegistries);
|
|
53
|
+
const typesFile = join(tibDir, 'actions-types.d.ts');
|
|
54
|
+
await writeFile(typesFile, typesContent);
|
|
55
|
+
|
|
56
|
+
cliLogger.info({ outFile: typesFile }, 'Types file written');
|
|
57
|
+
|
|
58
|
+
return outFile;
|
|
59
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { cliLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for the check-hashes command.
|
|
8
|
+
*/
|
|
9
|
+
export interface CheckHashesOptions {
|
|
10
|
+
/** Root directory of the project (defaults to cwd). */
|
|
11
|
+
projectRoot?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single hash check result.
|
|
16
|
+
*/
|
|
17
|
+
export interface DriftedFile {
|
|
18
|
+
/** File path relative to project root. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Expected SHA256 hash from manifest. */
|
|
21
|
+
expectedHash: string;
|
|
22
|
+
/** Actual SHA256 hash on disk. */
|
|
23
|
+
actualHash: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of the check-hashes command.
|
|
28
|
+
*/
|
|
29
|
+
export interface CheckHashesResult {
|
|
30
|
+
/** Whether all hashes match. */
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** Files that have been modified since scaffold. */
|
|
33
|
+
drifted: DriftedFile[];
|
|
34
|
+
/** Files that existed at scaffold time but are now missing. */
|
|
35
|
+
missing: string[];
|
|
36
|
+
/** Files on disk that were not in the scaffold manifest. */
|
|
37
|
+
unexpected: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Manifest of infra/modules/ files with their SHA256 hashes.
|
|
42
|
+
*/
|
|
43
|
+
interface ModulesHashesManifest {
|
|
44
|
+
/** Schema version. */
|
|
45
|
+
version: '1';
|
|
46
|
+
/** ISO timestamp when the manifest was generated. */
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
/** Map of relative file path to SHA256 hex digest. */
|
|
49
|
+
files: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verify that infra/modules/ has not been hand-edited since last scaffold.
|
|
54
|
+
* Reads .mc/modules-hashes.json manifest and compares each file's SHA256.
|
|
55
|
+
* @param opts Options including project root directory.
|
|
56
|
+
* @returns CheckHashesResult with ok, drifted, missing, and unexpected arrays.
|
|
57
|
+
*/
|
|
58
|
+
export async function runCheckHashes(opts: CheckHashesOptions = {}): Promise<CheckHashesResult> {
|
|
59
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
60
|
+
const manifestPath = join(root, '.tib', 'modules-hashes.json');
|
|
61
|
+
const modulesDir = join(root, 'infra', 'modules');
|
|
62
|
+
|
|
63
|
+
let manifest: ModulesHashesManifest;
|
|
64
|
+
try {
|
|
65
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as ModulesHashesManifest;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new Error(`tib check-hashes: cannot read ${manifestPath} (${(err as Error).message}). Re-scaffold to regenerate the manifest.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const drifted: DriftedFile[] = [];
|
|
71
|
+
const missing: string[] = [];
|
|
72
|
+
const unexpected: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Files/directories to skip when walking infra/modules/
|
|
75
|
+
const SKIP_NAMES = new Set(['node_modules', 'dist', 'cdk.out', 'package-lock.json', '.npmrc']);
|
|
76
|
+
|
|
77
|
+
/** Normalize CRLF → LF so hashes match across platforms (Windows vs Linux). */
|
|
78
|
+
function normalizeLineEndings(buf: Buffer): Buffer {
|
|
79
|
+
const str = buf.toString('utf8');
|
|
80
|
+
if (!str.includes('\r\n')) return buf;
|
|
81
|
+
return Buffer.from(str.replace(/\r\n/g, '\n'), 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Walk infra/modules/ to compute current hashes
|
|
85
|
+
async function walk(dir: string): Promise<string[]> {
|
|
86
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (SKIP_NAMES.has(entry.name)) continue;
|
|
90
|
+
const full = join(dir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) files.push(...await walk(full));
|
|
92
|
+
else if (entry.isFile()) files.push(full);
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const onDiskFiles = await walk(modulesDir);
|
|
98
|
+
const onDiskRel = onDiskFiles.map(f => relative(root, f).replace(/\\/g, '/'));
|
|
99
|
+
const onDiskSet = new Set(onDiskRel);
|
|
100
|
+
|
|
101
|
+
// Check each manifest entry
|
|
102
|
+
for (const [relPath, expectedHash] of Object.entries(manifest.files)) {
|
|
103
|
+
if (!onDiskSet.has(relPath)) {
|
|
104
|
+
missing.push(relPath);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const content = await readFile(join(root, relPath));
|
|
108
|
+
const normalized = normalizeLineEndings(content);
|
|
109
|
+
const actualHash = createHash('sha256').update(normalized).digest('hex');
|
|
110
|
+
if (actualHash !== expectedHash) {
|
|
111
|
+
drifted.push({ path: relPath, expectedHash, actualHash });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Files on disk but not in manifest
|
|
116
|
+
for (const relPath of onDiskRel) {
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(manifest.files, relPath)) {
|
|
118
|
+
unexpected.push(relPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result: CheckHashesResult = {
|
|
123
|
+
ok: drifted.length === 0 && missing.length === 0 && unexpected.length === 0,
|
|
124
|
+
drifted,
|
|
125
|
+
missing,
|
|
126
|
+
unexpected,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
cliLogger.error(
|
|
131
|
+
{ drifted: drifted.length, missing: missing.length, unexpected: unexpected.length },
|
|
132
|
+
'infra/modules/ has drifted from scaffold'
|
|
133
|
+
);
|
|
134
|
+
if (drifted.length > 0) {
|
|
135
|
+
cliLogger.error('Files modified since last scaffold:');
|
|
136
|
+
for (const d of drifted) cliLogger.error(` - ${d.path}`);
|
|
137
|
+
}
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
cliLogger.error('Files deleted since last scaffold:');
|
|
140
|
+
for (const m of missing) cliLogger.error(` - ${m}`);
|
|
141
|
+
}
|
|
142
|
+
if (unexpected.length > 0) {
|
|
143
|
+
cliLogger.error('Files added that were not scaffolded (manually-added):');
|
|
144
|
+
for (const u of unexpected) cliLogger.error(` - ${u}`);
|
|
145
|
+
}
|
|
146
|
+
cliLogger.error('infra/modules/ is TIB-owned. Re-scaffold to update legitimately.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|