@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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to scaffold a vitest test for an existing domain primitive.
|
|
3
|
+
* Detects the primitive type (api, subscriber, action, job) and emits
|
|
4
|
+
* an appropriate vitest fixture.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFile, access, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { cliLogger } from '../utils/logger.js';
|
|
9
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
10
|
+
/**
|
|
11
|
+
* CamelCase conversion.
|
|
12
|
+
*/
|
|
13
|
+
function camelCase(s) {
|
|
14
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Detect the primitive type by looking for the source file in domain subdirectories.
|
|
18
|
+
*/
|
|
19
|
+
async function detectPrimitiveType(domainDir, primitiveId) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
{ type: 'api', dir: 'api', testDir: 'api/__tests__' },
|
|
22
|
+
{ type: 'subscriber', dir: 'subscribers', testDir: 'subscribers/__tests__' },
|
|
23
|
+
{ type: 'action', dir: 'actions', testDir: 'actions/__tests__' },
|
|
24
|
+
{ type: 'job', dir: 'jobs', testDir: 'jobs/__tests__' },
|
|
25
|
+
];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
const sourceFile = join(domainDir, candidate.dir, `${primitiveId}.ts`);
|
|
28
|
+
try {
|
|
29
|
+
await access(sourceFile);
|
|
30
|
+
const testDir = join(domainDir, candidate.testDir);
|
|
31
|
+
return { type: candidate.type, sourceFile, testDir };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Continue to next candidate
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Could not find primitive "${primitiveId}" in domain. ` +
|
|
38
|
+
`Looked in: api/, subscribers/, actions/, jobs/`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate a vitest test template for the given primitive type.
|
|
42
|
+
*/
|
|
43
|
+
function testTemplate(domain, primitiveId, primitiveType) {
|
|
44
|
+
const varName = camelCase(primitiveId);
|
|
45
|
+
switch (primitiveType) {
|
|
46
|
+
case 'api':
|
|
47
|
+
return `import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
48
|
+
import { ${varName} } from '../${primitiveId}.js';
|
|
49
|
+
|
|
50
|
+
describe('${domain}.${primitiveId} (API)', () => {
|
|
51
|
+
const mockCtx = {
|
|
52
|
+
tenantId: 'test-tenant-000',
|
|
53
|
+
auditLog: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
publish: vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
db: {
|
|
56
|
+
get: vi.fn().mockResolvedValue(null),
|
|
57
|
+
put: vi.fn().mockResolvedValue({}),
|
|
58
|
+
query: vi.fn().mockResolvedValue([]),
|
|
59
|
+
update: vi.fn().mockResolvedValue({}),
|
|
60
|
+
},
|
|
61
|
+
metrics: {
|
|
62
|
+
putMetric: vi.fn(),
|
|
63
|
+
},
|
|
64
|
+
secrets: {
|
|
65
|
+
get: vi.fn().mockResolvedValue('test-secret'),
|
|
66
|
+
},
|
|
67
|
+
actions: {
|
|
68
|
+
call: vi.fn().mockResolvedValue({}),
|
|
69
|
+
},
|
|
70
|
+
headers: {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
beforeAll(() => {
|
|
74
|
+
// Mock crypto.randomUUID
|
|
75
|
+
vi.stubGlobal('crypto', {
|
|
76
|
+
randomUUID: () => '00000000-0000-0000-0000-000000000000',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should define an API with id "${primitiveId}"', () => {
|
|
81
|
+
expect(${varName}).toBeDefined();
|
|
82
|
+
expect(${varName}.meta?.id).toBe('${primitiveId}');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should have a v1 handler', () => {
|
|
86
|
+
const v1Config = ${varName}.meta?.versions?.v1;
|
|
87
|
+
expect(v1Config).toBeDefined();
|
|
88
|
+
expect(v1Config.status).toBe('stable');
|
|
89
|
+
expect(typeof v1Config.handler).toBe('function');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should accept valid input and return a response', async () => {
|
|
93
|
+
const v1Config = ${varName}.meta?.versions?.v1;
|
|
94
|
+
if (!v1Config) throw new Error('No v1 config');
|
|
95
|
+
|
|
96
|
+
const input = {};
|
|
97
|
+
const result = await v1Config.handler(input, mockCtx);
|
|
98
|
+
|
|
99
|
+
expect(result).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
`;
|
|
103
|
+
case 'subscriber':
|
|
104
|
+
return `import { describe, it, expect, vi } from 'vitest';
|
|
105
|
+
import { ${varName} } from '../${primitiveId}.js';
|
|
106
|
+
|
|
107
|
+
describe('${domain}.${primitiveId} (Subscriber)', () => {
|
|
108
|
+
const mockCtx = {
|
|
109
|
+
tenantId: 'test-tenant-000',
|
|
110
|
+
auditLog: vi.fn().mockResolvedValue(undefined),
|
|
111
|
+
publish: vi.fn().mockResolvedValue(undefined),
|
|
112
|
+
db: {
|
|
113
|
+
get: vi.fn().mockResolvedValue(null),
|
|
114
|
+
put: vi.fn().mockResolvedValue({}),
|
|
115
|
+
query: vi.fn().mockResolvedValue([]),
|
|
116
|
+
update: vi.fn().mockResolvedValue({}),
|
|
117
|
+
},
|
|
118
|
+
metrics: {
|
|
119
|
+
putMetric: vi.fn(),
|
|
120
|
+
},
|
|
121
|
+
actions: {
|
|
122
|
+
call: vi.fn().mockResolvedValue({}),
|
|
123
|
+
},
|
|
124
|
+
fetch: vi.fn().mockResolvedValue({ ok: true }),
|
|
125
|
+
integrations: {},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it('should define a subscriber', () => {
|
|
129
|
+
expect(${varName}).toBeDefined();
|
|
130
|
+
expect(${varName}.meta?.event).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have a handler function', () => {
|
|
134
|
+
expect(typeof ${varName}.meta?.handler).toBe('function');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should process an event without throwing', async () => {
|
|
138
|
+
const mockEvent = {
|
|
139
|
+
id: 'evt-001',
|
|
140
|
+
type: 'test.event',
|
|
141
|
+
version: 1,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
data: { tenantId: 'test-tenant-000', entityId: 'ent-001' },
|
|
144
|
+
source: 'test',
|
|
145
|
+
correlationId: 'corr-001',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await expect(
|
|
149
|
+
${varName}.meta?.handler(mockEvent, mockCtx),
|
|
150
|
+
).resolves.not.toThrow();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
`;
|
|
154
|
+
case 'action':
|
|
155
|
+
return `import { describe, it, expect, vi } from 'vitest';
|
|
156
|
+
import { ${varName} } from '../${primitiveId}.js';
|
|
157
|
+
|
|
158
|
+
describe('${domain}.${primitiveId} (Action)', () => {
|
|
159
|
+
const mockCtx = {
|
|
160
|
+
tenantId: 'test-tenant-000',
|
|
161
|
+
auditLog: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
publish: vi.fn().mockResolvedValue(undefined),
|
|
163
|
+
db: {
|
|
164
|
+
get: vi.fn().mockResolvedValue(null),
|
|
165
|
+
put: vi.fn().mockResolvedValue({}),
|
|
166
|
+
query: vi.fn().mockResolvedValue([]),
|
|
167
|
+
update: vi.fn().mockResolvedValue({}),
|
|
168
|
+
},
|
|
169
|
+
metrics: {
|
|
170
|
+
putMetric: vi.fn(),
|
|
171
|
+
},
|
|
172
|
+
actions: {
|
|
173
|
+
call: vi.fn().mockResolvedValue({}),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
it('should define an action', () => {
|
|
178
|
+
expect(${varName}).toBeDefined();
|
|
179
|
+
expect(${varName}.meta?.id).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should have a handler function', () => {
|
|
183
|
+
expect(typeof ${varName}.meta?.handler).toBe('function');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should execute action with valid input', async () => {
|
|
187
|
+
const input = { tenantId: 'test-tenant-000' };
|
|
188
|
+
const result = await ${varName}.meta?.handler(input, mockCtx);
|
|
189
|
+
|
|
190
|
+
expect(result).toBeDefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
`;
|
|
194
|
+
case 'job':
|
|
195
|
+
return `import { describe, it, expect, vi } from 'vitest';
|
|
196
|
+
import { ${varName} } from '../${primitiveId}.js';
|
|
197
|
+
|
|
198
|
+
describe('${domain}.${primitiveId} (Job)', () => {
|
|
199
|
+
const mockCtx = {
|
|
200
|
+
tenantId: 'test-tenant-000',
|
|
201
|
+
auditLog: vi.fn().mockResolvedValue(undefined),
|
|
202
|
+
publish: vi.fn().mockResolvedValue(undefined),
|
|
203
|
+
db: {
|
|
204
|
+
get: vi.fn().mockResolvedValue(null),
|
|
205
|
+
put: vi.fn().mockResolvedValue({}),
|
|
206
|
+
query: vi.fn().mockResolvedValue([]),
|
|
207
|
+
update: vi.fn().mockResolvedValue({}),
|
|
208
|
+
},
|
|
209
|
+
metrics: {
|
|
210
|
+
putMetric: vi.fn(),
|
|
211
|
+
},
|
|
212
|
+
actions: {
|
|
213
|
+
call: vi.fn().mockResolvedValue({}),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
it('should define a job', () => {
|
|
218
|
+
expect(${varName}).toBeDefined();
|
|
219
|
+
expect(${varName}.meta?.id).toBeDefined();
|
|
220
|
+
expect(${varName}.meta?.maxRetries).toBeGreaterThanOrEqual(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should have a handler function', () => {
|
|
224
|
+
expect(typeof ${varName}.meta?.handler).toBe('function');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should execute job without throwing', async () => {
|
|
228
|
+
const input = {};
|
|
229
|
+
|
|
230
|
+
await expect(
|
|
231
|
+
${varName}.meta?.handler(input, mockCtx),
|
|
232
|
+
).resolves.not.toThrow();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
`;
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Unknown primitive type "${primitiveType}"`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Execute the add-test command: scaffold a vitest test for a domain primitive.
|
|
242
|
+
*/
|
|
243
|
+
export async function runAddTest(opts) {
|
|
244
|
+
if (!KEBAB_REGEX.test(opts.domain)) {
|
|
245
|
+
throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
|
|
246
|
+
}
|
|
247
|
+
if (!KEBAB_REGEX.test(opts.primitiveId)) {
|
|
248
|
+
throw new Error(`Invalid primitive id "${opts.primitiveId}" — must be kebab-case`);
|
|
249
|
+
}
|
|
250
|
+
const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
|
|
251
|
+
const domainDir = join(root, opts.domain);
|
|
252
|
+
const domainConfigPath = join(domainDir, 'domain.config.ts');
|
|
253
|
+
// Verify domain exists
|
|
254
|
+
try {
|
|
255
|
+
await access(domainConfigPath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
259
|
+
}
|
|
260
|
+
// Detect primitive type
|
|
261
|
+
const { type, testDir } = await detectPrimitiveType(domainDir, opts.primitiveId);
|
|
262
|
+
// Create test directory if needed
|
|
263
|
+
try {
|
|
264
|
+
await access(testDir);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
await mkdir(testDir, { recursive: true });
|
|
268
|
+
}
|
|
269
|
+
const testFilePath = join(testDir, `${opts.primitiveId}.test.ts`);
|
|
270
|
+
// Refuse if test already exists
|
|
271
|
+
try {
|
|
272
|
+
await access(testFilePath);
|
|
273
|
+
throw new Error(`Test file already exists at ${testFilePath}`);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
if (err.code !== 'ENOENT') {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const content = testTemplate(opts.domain, opts.primitiveId, type);
|
|
281
|
+
await writeFile(testFilePath, content);
|
|
282
|
+
cliLogger.info({ domain: opts.domain, primitiveId: opts.primitiveId, type, file: testFilePath }, 'Test scaffolded');
|
|
283
|
+
// eslint-disable-next-line no-console
|
|
284
|
+
console.log(`\n✓ Test for "${opts.primitiveId}" (${type}) added at ${testFilePath}`);
|
|
285
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to run the doctor checks, optionally filtered to one domain.
|
|
3
|
+
* Wraps the runDoctor function from doctor.ts.
|
|
4
|
+
* When --json is passed, returns structured JSON with per-check ruleId, file, line, message, fixHint, kNodeRef.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Options for the audit command.
|
|
8
|
+
*/
|
|
9
|
+
export interface AuditOptions {
|
|
10
|
+
/** Optional domain filter (only run checks relevant to this domain). */
|
|
11
|
+
domain?: string;
|
|
12
|
+
/** Output structured JSON instead of pretty table. */
|
|
13
|
+
json?: boolean;
|
|
14
|
+
/** Root directory of the project. */
|
|
15
|
+
projectRoot?: string;
|
|
16
|
+
/** Exit with code 1 on any FAIL. */
|
|
17
|
+
strict?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Enhanced check result for JSON output.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuditCheckResult {
|
|
23
|
+
ruleId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
status: 'PASS' | 'WARN' | 'FAIL';
|
|
26
|
+
message: string;
|
|
27
|
+
file?: string;
|
|
28
|
+
line?: number;
|
|
29
|
+
fixHint?: string;
|
|
30
|
+
kNodeRef?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Execute the audit command.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runAudit(opts?: AuditOptions): Promise<AuditCheckResult[]>;
|
|
36
|
+
/**
|
|
37
|
+
* CLI entry point: runs audit and optionally prints JSON.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runAuditCli(opts?: AuditOptions): Promise<void>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to run the doctor checks, optionally filtered to one domain.
|
|
3
|
+
* Wraps the runDoctor function from doctor.ts.
|
|
4
|
+
* When --json is passed, returns structured JSON with per-check ruleId, file, line, message, fixHint, kNodeRef.
|
|
5
|
+
*/
|
|
6
|
+
import { runDoctor } from './doctor.js';
|
|
7
|
+
import { cliLogger } from '../utils/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Map doctor check names to rule IDs and K-node references.
|
|
10
|
+
*/
|
|
11
|
+
const RULE_MAP = {
|
|
12
|
+
'Domain configs valid': {
|
|
13
|
+
ruleId: 'domain-configs-valid',
|
|
14
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
15
|
+
},
|
|
16
|
+
'No raw AWS SDK in domains': {
|
|
17
|
+
ruleId: 'no-raw-aws-sdk',
|
|
18
|
+
kNodeRef: 'K:convention:tier-1-foundations',
|
|
19
|
+
},
|
|
20
|
+
'No cross-domain imports': {
|
|
21
|
+
ruleId: 'no-cross-domain-internal-import',
|
|
22
|
+
kNodeRef: 'K:convention:no-cross-domain-import',
|
|
23
|
+
},
|
|
24
|
+
'APIs declare versions': {
|
|
25
|
+
ruleId: 'apis-have-versions',
|
|
26
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
27
|
+
},
|
|
28
|
+
'APIs declare tenancy': {
|
|
29
|
+
ruleId: 'apis-have-tenancy',
|
|
30
|
+
kNodeRef: 'K:convention:tier-1-foundations',
|
|
31
|
+
},
|
|
32
|
+
'scaffold-config.json matches on-disk': {
|
|
33
|
+
ruleId: 'scaffold-config-matches',
|
|
34
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
35
|
+
},
|
|
36
|
+
'Husky hooks installed': {
|
|
37
|
+
ruleId: 'husky-hooks-installed',
|
|
38
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
39
|
+
},
|
|
40
|
+
'No root-level flows (deprecated)': {
|
|
41
|
+
ruleId: 'flow-domain-ownership',
|
|
42
|
+
kNodeRef: 'K:convention:flow-vs-subscriber-rule',
|
|
43
|
+
},
|
|
44
|
+
// W2 checks
|
|
45
|
+
'Every API has a fixture': {
|
|
46
|
+
ruleId: 'api-fixtures-exist',
|
|
47
|
+
kNodeRef: 'K:exit-gate:domain-complete',
|
|
48
|
+
},
|
|
49
|
+
'Every event has consumer or annotation': {
|
|
50
|
+
ruleId: 'event-consumers-exist',
|
|
51
|
+
kNodeRef: 'K:convention:flow-vs-subscriber-rule',
|
|
52
|
+
},
|
|
53
|
+
'Migrations declare tenancy trio': {
|
|
54
|
+
ruleId: 'migration-tenancy-trio',
|
|
55
|
+
kNodeRef: 'K:convention:tenancy-trio',
|
|
56
|
+
},
|
|
57
|
+
'Domain has RLS test': {
|
|
58
|
+
ruleId: 'domain-rls-test',
|
|
59
|
+
kNodeRef: 'K:convention:rls-per-domain-role',
|
|
60
|
+
},
|
|
61
|
+
'Domain reachable in brain': {
|
|
62
|
+
ruleId: 'domain-brain-reachable',
|
|
63
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
64
|
+
},
|
|
65
|
+
'Actions have types': {
|
|
66
|
+
ruleId: 'actions-have-types',
|
|
67
|
+
kNodeRef: 'K:exit-gate:domain-complete',
|
|
68
|
+
},
|
|
69
|
+
'Conventions have evidence': {
|
|
70
|
+
ruleId: 'conventions-have-evidence',
|
|
71
|
+
kNodeRef: 'K:exit-gate:domain-complete',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Parse file and line information from a doctor check message.
|
|
76
|
+
*/
|
|
77
|
+
function parseFileAndLine(message) {
|
|
78
|
+
// Try to match patterns like "path/to/file.ts:42" or "file.ts:42"
|
|
79
|
+
const match = message.match(/([^\s:"]+\.ts):(\d+)/);
|
|
80
|
+
if (match) {
|
|
81
|
+
return { file: match[1], line: parseInt(match[2], 10) };
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Convert a DoctorCheck to an AuditCheckResult.
|
|
87
|
+
*/
|
|
88
|
+
function toAuditCheck(check) {
|
|
89
|
+
const mapping = RULE_MAP[check.name] ?? {
|
|
90
|
+
ruleId: check.name.toLowerCase().replace(/\s+/g, '-'),
|
|
91
|
+
kNodeRef: undefined,
|
|
92
|
+
};
|
|
93
|
+
const { file, line } = parseFileAndLine(check.message);
|
|
94
|
+
return {
|
|
95
|
+
ruleId: mapping.ruleId,
|
|
96
|
+
name: check.name,
|
|
97
|
+
status: check.status,
|
|
98
|
+
message: check.message,
|
|
99
|
+
// Use parsed file/line from message
|
|
100
|
+
file,
|
|
101
|
+
line,
|
|
102
|
+
fixHint: check.fixHint,
|
|
103
|
+
kNodeRef: check.kNodeRef ?? mapping.kNodeRef,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Filter doctor checks to a specific domain (heuristic: check if domain name appears in message or file).
|
|
108
|
+
*/
|
|
109
|
+
function filterChecksForDomain(checks, domain) {
|
|
110
|
+
return checks.filter((check) => {
|
|
111
|
+
if (check.message.includes(domain) || (check.fixHint && check.fixHint.includes(domain))) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return check.message.includes(`domains/${domain}/`) || check.message.includes(`'${domain}'`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Execute the audit command.
|
|
119
|
+
*/
|
|
120
|
+
export async function runAudit(opts = {}) {
|
|
121
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
122
|
+
cliLogger.info({ domain: opts.domain, json: opts.json }, 'Running audit');
|
|
123
|
+
const report = await runDoctor({
|
|
124
|
+
projectRoot,
|
|
125
|
+
});
|
|
126
|
+
let checks = report.checks;
|
|
127
|
+
// Filter by domain if specified
|
|
128
|
+
if (opts.domain) {
|
|
129
|
+
const filtered = filterChecksForDomain(checks, opts.domain);
|
|
130
|
+
if (filtered.length === 0) {
|
|
131
|
+
checks = checks.filter((c) => c.name === 'Domain configs valid' ||
|
|
132
|
+
c.name === 'scaffold-config.json matches on-disk');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
checks = filtered;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const results = checks.map(toAuditCheck);
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
// Pretty-print table
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log('\n=== TIB Audit' + (opts.domain ? ` — ${opts.domain}` : '') + ' ===\n');
|
|
145
|
+
for (const result of results) {
|
|
146
|
+
const symbol = result.status === 'PASS' ? '✓' : result.status === 'WARN' ? '⚠' : '✗';
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.log(`${symbol} [${result.ruleId}] ${result.message}`);
|
|
149
|
+
if (result.fixHint) {
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.log(` Fix: ${result.fixHint}`);
|
|
152
|
+
}
|
|
153
|
+
if (result.kNodeRef) {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.log(` Docs: ${result.kNodeRef}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log('');
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* CLI entry point: runs audit and optionally prints JSON.
|
|
164
|
+
*/
|
|
165
|
+
export async function runAuditCli(opts = {}) {
|
|
166
|
+
const results = await runAudit(opts);
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.log(JSON.stringify(results, null, 2));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** A single API entry in the catalog. */
|
|
2
|
+
export interface CatalogApi {
|
|
3
|
+
id: string;
|
|
4
|
+
domainId: string;
|
|
5
|
+
path: string;
|
|
6
|
+
method: string;
|
|
7
|
+
authType: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
requestSchema?: Record<string, unknown>;
|
|
10
|
+
responseSchema?: Record<string, unknown>;
|
|
11
|
+
versions?: Array<{
|
|
12
|
+
version: string;
|
|
13
|
+
requestSchema?: Record<string, unknown>;
|
|
14
|
+
responseSchema?: Record<string, unknown>;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** A single action entry in the catalog. */
|
|
18
|
+
export interface CatalogAction {
|
|
19
|
+
id: string;
|
|
20
|
+
domainId: string;
|
|
21
|
+
visibility: string;
|
|
22
|
+
idempotent: boolean;
|
|
23
|
+
description?: string;
|
|
24
|
+
inputSchema?: Record<string, unknown>;
|
|
25
|
+
outputSchema?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** A single event entry in the catalog. */
|
|
28
|
+
export interface CatalogEvent {
|
|
29
|
+
id: string;
|
|
30
|
+
domainId: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
versions?: Array<{
|
|
33
|
+
version: number;
|
|
34
|
+
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
|
+
/** A single domain entry in the catalog. */
|
|
46
|
+
export interface CatalogDomain {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
tenancy: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
/** The combined domain catalog written to .mc/domain-registry.json. */
|
|
53
|
+
export interface DomainCatalog {
|
|
54
|
+
version: 2;
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
domains: CatalogDomain[];
|
|
57
|
+
apis: CatalogApi[];
|
|
58
|
+
actions: CatalogAction[];
|
|
59
|
+
events: CatalogEvent[];
|
|
60
|
+
subscribers: CatalogSubscriber[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the combined domain catalog from all per-domain registry files.
|
|
64
|
+
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
65
|
+
* @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
|
|
66
|
+
* @returns The written catalog.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runBuildCatalog(tibDir?: string): Promise<DomainCatalog>;
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
* Build the combined domain catalog from all per-domain registry files.
|
|
6
|
+
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
7
|
+
* @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
|
|
8
|
+
* @returns The written catalog.
|
|
9
|
+
*/
|
|
10
|
+
export async function runBuildCatalog(tibDir) {
|
|
11
|
+
const dir = tibDir ? resolve(tibDir) : join(process.cwd(), '.tib');
|
|
12
|
+
// Find all per-domain registry files
|
|
13
|
+
let files;
|
|
14
|
+
try {
|
|
15
|
+
files = await readdir(dir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`build-catalog: .tib directory not found at ${dir}. Run tib build first.`);
|
|
19
|
+
}
|
|
20
|
+
const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
|
|
21
|
+
if (registryFiles.length === 0) {
|
|
22
|
+
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run tib build <domain> first.`);
|
|
23
|
+
}
|
|
24
|
+
const catalog = {
|
|
25
|
+
version: 2,
|
|
26
|
+
generatedAt: new Date().toISOString(),
|
|
27
|
+
domains: [],
|
|
28
|
+
apis: [],
|
|
29
|
+
actions: [],
|
|
30
|
+
events: [],
|
|
31
|
+
subscribers: [],
|
|
32
|
+
};
|
|
33
|
+
for (const file of registryFiles) {
|
|
34
|
+
const raw = await readFile(join(dir, file), 'utf8');
|
|
35
|
+
let registry;
|
|
36
|
+
try {
|
|
37
|
+
registry = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
cliLogger.warn({ file }, 'Skipping invalid JSON registry file');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const domainEntry = registry['domain'];
|
|
44
|
+
if (!domainEntry) {
|
|
45
|
+
cliLogger.warn({ file }, 'Skipping registry file with no domain entry');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const domainId = String(domainEntry['id']);
|
|
49
|
+
catalog.domains.push({
|
|
50
|
+
id: domainId,
|
|
51
|
+
name: String(domainEntry['name'] ?? domainId),
|
|
52
|
+
tenancy: String(domainEntry['tenancy'] ?? 'none'),
|
|
53
|
+
description: domainEntry['description'],
|
|
54
|
+
});
|
|
55
|
+
const apis = registry['apis'] ?? [];
|
|
56
|
+
for (const api of apis) {
|
|
57
|
+
catalog.apis.push({
|
|
58
|
+
id: String(api['id']),
|
|
59
|
+
domainId,
|
|
60
|
+
path: String(api['path']),
|
|
61
|
+
method: String(api['method'] ?? 'ANY'),
|
|
62
|
+
authType: String(api['authType'] ?? 'jwt'),
|
|
63
|
+
description: api['description'],
|
|
64
|
+
requestSchema: api['requestSchema'],
|
|
65
|
+
responseSchema: api['responseSchema'],
|
|
66
|
+
versions: api['versions'],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const actions = registry['actions'] ?? [];
|
|
70
|
+
for (const action of actions) {
|
|
71
|
+
catalog.actions.push({
|
|
72
|
+
id: String(action['id']),
|
|
73
|
+
domainId,
|
|
74
|
+
visibility: String(action['visibility'] ?? 'private'),
|
|
75
|
+
idempotent: Boolean(action['idempotent'] ?? false),
|
|
76
|
+
description: action['description'],
|
|
77
|
+
inputSchema: action['inputSchema'],
|
|
78
|
+
outputSchema: action['outputSchema'],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const events = registry['events'] ?? [];
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
catalog.events.push({
|
|
84
|
+
id: String(event['id']),
|
|
85
|
+
domainId,
|
|
86
|
+
description: event['description'],
|
|
87
|
+
versions: event['versions'],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const subscribers = registry['subscribers'] ?? [];
|
|
91
|
+
for (const sub of subscribers) {
|
|
92
|
+
catalog.subscribers.push({
|
|
93
|
+
id: String(sub['id']),
|
|
94
|
+
domainId,
|
|
95
|
+
event: String(sub['event']),
|
|
96
|
+
semverRange: String(sub['semverRange'] ?? '*'),
|
|
97
|
+
description: sub['description'],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const outPath = join(dir, 'domain-registry.json');
|
|
102
|
+
await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
|
|
103
|
+
cliLogger.info({ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length }, 'Domain catalog written');
|
|
104
|
+
return catalog;
|
|
105
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to build flows into a registry JSON file.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Options for the build-flows command.
|
|
6
|
+
*/
|
|
7
|
+
export interface BuildFlowsOptions {
|
|
8
|
+
/** Project root directory. Defaults to current working directory. */
|
|
9
|
+
projectRoot?: string;
|
|
10
|
+
/** Output path for the flows registry JSON file. Defaults to .mc/flows-registry.json. */
|
|
11
|
+
outFile?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Custom error for flow ID collisions.
|
|
15
|
+
*/
|
|
16
|
+
export declare class FlowIdCollisionError extends Error {
|
|
17
|
+
constructor(id: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute the build-flows command: scan flows/ and domains/x/flows/ directories for .ts and .json files,
|
|
21
|
+
* validate and serialize them, and write .mc/flows-registry.json.
|
|
22
|
+
* @param options - BuildFlowsOptions specifying project root and optional output path.
|
|
23
|
+
* @returns The absolute path to the written registry file.
|
|
24
|
+
* @throws If flow validation fails or IDs collide.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runBuildFlows(options: BuildFlowsOptions): Promise<string>;
|