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