@mettlecast/domain-cli 0.2.60 → 0.2.62
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/dist/builder/build-registry.d.ts +1 -1
- package/dist/builder/build-registry.js +1 -36
- package/dist/builder/build-types.d.ts +1 -1
- package/dist/builder/load-module.d.ts +1 -1
- package/dist/cli.js +21 -3
- package/dist/commands/add-api.js +2 -2
- package/dist/commands/add-domain.js +4 -4
- package/dist/commands/add-fixture-factory.js +5 -6
- package/dist/commands/build-catalog.d.ts +6 -22
- package/dist/commands/build-catalog.js +7 -18
- package/dist/commands/build.js +2 -1
- package/dist/commands/check-hashes.d.ts +2 -0
- package/dist/commands/check-hashes.js +8 -0
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.js +70 -41
- package/dist/commands/explain.js +13 -13
- package/dist/commands/generate-openapi.d.ts +10 -1
- package/dist/commands/generate-openapi.js +19 -33
- package/dist/commands/regenerate-modules-hashes.d.ts +25 -0
- package/dist/commands/regenerate-modules-hashes.js +58 -0
- package/dist/commands/show.d.ts +2 -3
- package/dist/commands/show.js +0 -2
- package/dist/commands/test.js +0 -1
- package/dist/commands/update-all.d.ts +21 -0
- package/dist/commands/update-all.js +62 -0
- package/dist/commands/upgrade-backend.js +3 -3
- package/dist/commands/validate.js +12 -90
- package/dist/server/api-server.d.ts +1 -1
- package/dist/server/mount-routes.d.ts +11 -2
- package/dist/server/mount-routes.js +20 -8
- package/dist/templates/api-skeleton.d.ts +5 -0
- package/dist/templates/api-skeleton.js +28 -27
- package/dist/templates/claude-md.js +1 -1
- package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
- package/dist/templates/patterns/api/create-with-event.js +38 -32
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
- package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
- package/dist/templates/patterns/api/paginated-list.js +30 -24
- package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
- package/dist/templates/patterns/api/simple-crud.js +46 -35
- package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
- package/dist/templates/patterns/api/streaming-list.js +46 -41
- package/dist/templates/patterns/api/system-admin.d.ts +4 -0
- package/dist/templates/patterns/api/system-admin.js +59 -52
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/dist/utils/file-helpers.d.ts +0 -2
- package/dist/utils/file-helpers.js +2 -3
- package/dist/utils/manifest.d.ts +1 -0
- package/dist/utils/manifest.js +16 -1
- package/dist/utils/scaffold-config.d.ts +6 -0
- package/dist/utils/scaffold-config.js +2 -0
- package/package.json +1 -1
- package/src/__tests__/build-registry.test.ts +43 -20
- package/src/__tests__/build-types.test.ts +4 -7
- package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
- package/src/__tests__/commands/add-api.test.ts +12 -10
- package/src/__tests__/commands/add-domain.test.ts +8 -5
- package/src/__tests__/commands/build-flows.test.ts +55 -0
- package/src/__tests__/commands/check-hashes.test.ts +31 -0
- package/src/__tests__/commands/create-project.test.ts +5 -5
- package/src/__tests__/commands/dev.test.ts +0 -1
- package/src/__tests__/commands/regenerate-modules-hashes.test.ts +170 -0
- package/src/__tests__/commands/update-all.test.ts +322 -0
- package/src/__tests__/doctor.test.ts +73 -0
- package/src/__tests__/mount-routes.test.ts +64 -23
- package/src/__tests__/package-freshness.test.ts +1 -21
- package/src/__tests__/smoke/scaffold.test.ts +13 -15
- package/src/__tests__/utils/manifest.test.ts +128 -0
- package/src/__tests__/validate.test.ts +21 -103
- package/src/builder/build-registry.ts +7 -44
- package/src/builder/build-types.ts +1 -1
- package/src/cli.ts +23 -3
- package/src/commands/add-api.ts +2 -2
- package/src/commands/add-domain.ts +4 -4
- package/src/commands/add-fixture-factory.ts +5 -6
- package/src/commands/build-catalog.ts +13 -35
- package/src/commands/build.ts +3 -2
- package/src/commands/check-hashes.ts +12 -0
- package/src/commands/dev.ts +1 -1
- package/src/commands/doctor.ts +72 -41
- package/src/commands/explain.ts +13 -13
- package/src/commands/generate-openapi.ts +30 -52
- package/src/commands/regenerate-modules-hashes.ts +89 -0
- package/src/commands/show.ts +2 -5
- package/src/commands/test.ts +0 -1
- package/src/commands/update-all.ts +79 -0
- package/src/commands/upgrade-backend.ts +3 -3
- package/src/commands/validate.ts +11 -96
- package/src/server/api-server.ts +1 -1
- package/src/server/mount-routes.ts +21 -10
- package/src/templates/api-skeleton.ts +29 -28
- package/src/templates/claude-md.ts +1 -1
- package/src/templates/patterns/api/create-with-event.ts +39 -33
- package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
- package/src/templates/patterns/api/paginated-list.ts +31 -25
- package/src/templates/patterns/api/simple-crud.ts +47 -36
- package/src/templates/patterns/api/streaming-list.ts +47 -42
- package/src/templates/patterns/api/system-admin.ts +60 -53
- package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
- package/src/types.ts +128 -0
- package/src/utils/file-helpers.ts +2 -5
- package/src/utils/manifest.ts +16 -1
- package/src/utils/scaffold-config.ts +9 -0
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { relative, resolve } from 'node:path';
|
|
2
2
|
import type {
|
|
3
3
|
DomainRegistry,
|
|
4
|
-
|
|
5
|
-
ApiVersionSnapshot,
|
|
4
|
+
ActionRegistryEntry,
|
|
6
5
|
SchemaSnapshot,
|
|
7
6
|
WebhookRegistryEntry,
|
|
8
7
|
SubscriberRegistryEntry,
|
|
9
8
|
ScheduleRegistryEntry,
|
|
10
9
|
JobRegistryEntry,
|
|
11
|
-
ActionRegistryEntry,
|
|
12
10
|
IntegrationRegistryEntry,
|
|
13
11
|
EventRegistryEntry,
|
|
14
12
|
DomainRegistryEntry,
|
|
15
13
|
SerialDeploymentConfig,
|
|
16
|
-
} from '
|
|
14
|
+
} from '../types.js';
|
|
17
15
|
import { walkDomainDir } from '../utils/file-helpers.js';
|
|
18
16
|
import { loadModuleExports, type RawPrimitiveExport } from './load-module.js';
|
|
19
17
|
|
|
18
|
+
// Issue #4689: the legacy `defineApi` primitive was removed from
|
|
19
|
+
// `@mettlecast/domain-runtime`. The only HTTP endpoint surface in
|
|
20
|
+
// the new registry is `actions[]` whose `exposure.type === 'api'`.
|
|
21
|
+
|
|
20
22
|
type ActionBackendAccess = 'private' | 'domain' | 'platform';
|
|
21
23
|
|
|
22
24
|
type ActionExposure =
|
|
@@ -190,9 +192,8 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
190
192
|
defaultDeployment: deployment(domainRaw),
|
|
191
193
|
};
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
const [webhookExports, subscriberExports, actionExports,
|
|
194
196
|
scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
|
|
195
|
-
Promise.all(paths.apis.map(load)),
|
|
196
197
|
Promise.all(paths.webhooks.map(load)),
|
|
197
198
|
Promise.all(paths.subscribers.map(load)),
|
|
198
199
|
Promise.all(paths.actions.map(load)),
|
|
@@ -202,43 +203,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
202
203
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
203
204
|
]);
|
|
204
205
|
|
|
205
|
-
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
206
|
-
|
|
207
|
-
const apis: ApiRegistryEntry[] = paths.apis.flatMap((filePath, i) =>
|
|
208
|
-
(apiExports[i] ?? [])
|
|
209
|
-
.filter(e => e['_kind'] === 'api')
|
|
210
|
-
.map(e => {
|
|
211
|
-
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
212
|
-
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
213
|
-
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
214
|
-
}
|
|
215
|
-
const rawVersions = e['versions'] as Record<string, { input?: unknown; output?: unknown }> | undefined;
|
|
216
|
-
const versionSnapshots: ApiVersionSnapshot[] = rawVersions
|
|
217
|
-
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
218
|
-
version: ver,
|
|
219
|
-
requestSchema: isJsonSchema(v?.input) ? (v.input as SchemaSnapshot) : undefined,
|
|
220
|
-
responseSchema: isJsonSchema(v?.output) ? (v.output as SchemaSnapshot) : undefined,
|
|
221
|
-
}))
|
|
222
|
-
: [];
|
|
223
|
-
const latestVersion = versionSnapshots[versionSnapshots.length - 1];
|
|
224
|
-
return {
|
|
225
|
-
id: String(e['id']),
|
|
226
|
-
kind: 'api' as const,
|
|
227
|
-
handlerFile: relPath(filePath),
|
|
228
|
-
path: String(e['path']),
|
|
229
|
-
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
230
|
-
authType: ((e['auth'] as { type?: string } | undefined)?.type as 'jwt' | 'api-key' | 'none' | undefined) ?? 'jwt',
|
|
231
|
-
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
232
|
-
deployment: deployment(e),
|
|
233
|
-
outboundAccess: outboundAccess(e),
|
|
234
|
-
requestSchema: latestVersion?.requestSchema,
|
|
235
|
-
responseSchema: latestVersion?.responseSchema,
|
|
236
|
-
versions: versionSnapshots.length > 0 ? versionSnapshots : undefined,
|
|
237
|
-
examples: (e['examples'] as { request?: Record<string, unknown>; response?: Record<string, unknown> } | undefined),
|
|
238
|
-
};
|
|
239
|
-
})
|
|
240
|
-
);
|
|
241
|
-
|
|
242
206
|
const webhooks: WebhookRegistryEntry[] = paths.webhooks.flatMap((filePath, i) =>
|
|
243
207
|
(webhookExports[i] ?? [])
|
|
244
208
|
.filter(e => e['_kind'] === 'webhook')
|
|
@@ -374,7 +338,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
374
338
|
schemaVersion: '1',
|
|
375
339
|
domainRoot,
|
|
376
340
|
domain,
|
|
377
|
-
apis,
|
|
378
341
|
webhooks,
|
|
379
342
|
subscribers,
|
|
380
343
|
schedules,
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { runAddModule } from './commands/add-module.js';
|
|
|
13
13
|
import { runBuildFlows } from './commands/build-flows.js';
|
|
14
14
|
import { runDoctor } from './commands/doctor.js';
|
|
15
15
|
import { runCheckHashes } from './commands/check-hashes.js';
|
|
16
|
+
import { runUpdateAll } from './commands/update-all.js';
|
|
17
|
+
import { runRegenerateModulesHashes } from './commands/regenerate-modules-hashes.js';
|
|
16
18
|
import { runUpgradeBackend } from './commands/upgrade-backend.js';
|
|
17
19
|
import { runAddPage } from './commands/add-page.js';
|
|
18
20
|
import { runCreateProject } from './commands/create-project.js';
|
|
@@ -73,7 +75,7 @@ program
|
|
|
73
75
|
|
|
74
76
|
program
|
|
75
77
|
.command('dev <domain>')
|
|
76
|
-
.description('Start a local HTTP server simulating API Gateway for all
|
|
78
|
+
.description('Start a local HTTP server simulating API Gateway for all API-exposed defineAction handlers')
|
|
77
79
|
.option('--port <n>', 'Port to listen on', '3000')
|
|
78
80
|
.action(async (domain: string, opts: { port?: string }) => {
|
|
79
81
|
await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
|
|
@@ -195,11 +197,29 @@ program
|
|
|
195
197
|
program
|
|
196
198
|
.command('check-hashes')
|
|
197
199
|
.description('Verify infra/modules/ has not been hand-edited since last scaffold')
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
+
.option('--write', 'Regenerate modules-hashes.json from current disk state instead of verifying')
|
|
201
|
+
.action(async (opts: { write?: boolean }) => {
|
|
202
|
+
const result = await runCheckHashes({ write: opts.write });
|
|
200
203
|
process.exit(result.ok ? 0 : 1);
|
|
201
204
|
});
|
|
202
205
|
|
|
206
|
+
program
|
|
207
|
+
.command('update-all')
|
|
208
|
+
.description('Full refresh: build all domains, build catalog, build flows, build UI, regenerate hashes, run doctor')
|
|
209
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
210
|
+
.action(async (opts: { projectRoot?: string }) => {
|
|
211
|
+
const result = await runUpdateAll({ projectRoot: opts.projectRoot });
|
|
212
|
+
process.exit(result.success ? 0 : 1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
program
|
|
216
|
+
.command('regenerate-modules-hashes')
|
|
217
|
+
.description('Walk infra/modules/, compute SHA256 hashes, and write .mc/modules-hashes.json')
|
|
218
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
219
|
+
.action(async (opts: { projectRoot?: string }) => {
|
|
220
|
+
await runRegenerateModulesHashes({ projectRoot: opts.projectRoot });
|
|
221
|
+
});
|
|
222
|
+
|
|
203
223
|
program
|
|
204
224
|
.command('upgrade-backend <target-major>')
|
|
205
225
|
.description('Run jscodeshift/ts-morph migrations between major versions of domain-runtime')
|
package/src/commands/add-api.ts
CHANGED
|
@@ -96,7 +96,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
96
96
|
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const apiFilePath = join(domainDir, '
|
|
99
|
+
const apiFilePath = join(domainDir, 'actions', `${opts.id}.ts`);
|
|
100
100
|
|
|
101
101
|
// Refuse if API already exists
|
|
102
102
|
try {
|
|
@@ -124,7 +124,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
124
124
|
await writeFile(apiFilePath, apiContent);
|
|
125
125
|
|
|
126
126
|
// Create fixture with the input schema's default shape as the example payload
|
|
127
|
-
const apiTestDir = join(domainDir, '
|
|
127
|
+
const apiTestDir = join(domainDir, 'actions', '__tests__');
|
|
128
128
|
await mkdir(apiTestDir, { recursive: true });
|
|
129
129
|
await writeFile(
|
|
130
130
|
join(apiTestDir, `${opts.id}.fixture.json`),
|
|
@@ -51,18 +51,18 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Create directory structure
|
|
54
|
-
await mkdir(join(domainDir, '
|
|
54
|
+
await mkdir(join(domainDir, 'actions', '__tests__'), { recursive: true });
|
|
55
55
|
await mkdir(join(domainDir, 'subscribers'), { recursive: true });
|
|
56
56
|
await mkdir(join(domainDir, 'publishes'), { recursive: true });
|
|
57
57
|
|
|
58
58
|
// Write skeleton files
|
|
59
59
|
await writeFile(join(domainDir, 'domain.config.ts'), domainConfigTemplate(opts.id, opts.tenancy));
|
|
60
60
|
await writeFile(
|
|
61
|
-
join(domainDir, '
|
|
61
|
+
join(domainDir, 'actions', 'example.ts'),
|
|
62
62
|
apiSkeletonTemplate(opts.id, 'example', opts.tenancy)
|
|
63
63
|
);
|
|
64
64
|
await writeFile(
|
|
65
|
-
join(domainDir, '
|
|
65
|
+
join(domainDir, 'actions', '__tests__', 'example.fixture.json'),
|
|
66
66
|
apiFixtureSkeleton(opts.id, 'example')
|
|
67
67
|
);
|
|
68
68
|
await writeFile(join(domainDir, 'publishes', 'events.ts'), eventsSkeletonTemplate(opts.id));
|
|
@@ -94,7 +94,7 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
|
|
|
94
94
|
cliLogger.info({ id: opts.id, dir: domainDir }, 'Domain scaffolded');
|
|
95
95
|
// eslint-disable-next-line no-console
|
|
96
96
|
console.log(
|
|
97
|
-
`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit
|
|
97
|
+
`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit actions/example.ts, ` +
|
|
98
98
|
`then run \`mc-domain-module build ${opts.id}\` to generate the registry.`
|
|
99
99
|
);
|
|
100
100
|
}
|
|
@@ -47,21 +47,20 @@ export async function runAddFixtureFactory(options: AddFixtureFactoryOptions): P
|
|
|
47
47
|
const domain = options.domain;
|
|
48
48
|
const apiId = options.apiId;
|
|
49
49
|
|
|
50
|
-
const apiFile = join(projectRoot, 'domains', domain, '
|
|
50
|
+
const apiFile = join(projectRoot, 'domains', domain, 'actions', `${apiId}.ts`);
|
|
51
51
|
await access(apiFile).catch(() => {
|
|
52
|
-
throw new Error(`
|
|
52
|
+
throw new Error(`Action file not found: ${apiFile}. Run add-api first.`);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// Read the
|
|
55
|
+
// Read the action file to extract the input type name
|
|
56
56
|
const content = await readFile(apiFile, 'utf8');
|
|
57
|
-
const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
|
|
58
57
|
|
|
59
|
-
const factoryFile = join(projectRoot, 'domains', domain, '
|
|
58
|
+
const factoryFile = join(projectRoot, 'domains', domain, 'actions', '__tests__', `${apiId}.factory.ts`);
|
|
60
59
|
const factoryContent = FACTORY_TEMPLATE
|
|
61
60
|
.replace(/\{domain\}/g, domain)
|
|
62
61
|
.replace(/\{apiId\}/g, apiId);
|
|
63
62
|
|
|
64
|
-
await mkdir(join(projectRoot, 'domains', domain, '
|
|
63
|
+
await mkdir(join(projectRoot, 'domains', domain, 'actions', '__tests__'), { recursive: true });
|
|
65
64
|
await writeFile(factoryFile, factoryContent, 'utf8');
|
|
66
65
|
|
|
67
66
|
cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
|
|
@@ -2,27 +2,18 @@ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { cliLogger } from '../utils/logger.js';
|
|
4
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
|
-
examples?: { request?: Record<string, unknown>; response?: Record<string, unknown> };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
5
|
/** A single action entry in the catalog. */
|
|
20
6
|
export interface CatalogAction {
|
|
21
7
|
id: string;
|
|
22
8
|
domainId: string;
|
|
23
|
-
|
|
9
|
+
backendAccess: string;
|
|
10
|
+
exposure: Record<string, unknown>;
|
|
11
|
+
exposureDeclared?: boolean;
|
|
24
12
|
idempotent: boolean;
|
|
25
13
|
description?: string;
|
|
14
|
+
handlerFile?: string;
|
|
15
|
+
deployment?: Record<string, unknown>;
|
|
16
|
+
outboundAccess?: string;
|
|
26
17
|
inputSchema?: Record<string, unknown>;
|
|
27
18
|
outputSchema?: Record<string, unknown>;
|
|
28
19
|
}
|
|
@@ -83,7 +74,6 @@ export interface DomainCatalog {
|
|
|
83
74
|
version: 2;
|
|
84
75
|
generatedAt: string;
|
|
85
76
|
domains: CatalogDomain[];
|
|
86
|
-
apis: CatalogApi[];
|
|
87
77
|
actions: CatalogAction[];
|
|
88
78
|
events: CatalogEvent[];
|
|
89
79
|
subscribers: CatalogSubscriber[];
|
|
@@ -118,7 +108,6 @@ export async function runBuildCatalog(registryDir?: string): Promise<DomainCatal
|
|
|
118
108
|
version: 2,
|
|
119
109
|
generatedAt: new Date().toISOString(),
|
|
120
110
|
domains: [],
|
|
121
|
-
apis: [],
|
|
122
111
|
actions: [],
|
|
123
112
|
events: [],
|
|
124
113
|
subscribers: [],
|
|
@@ -152,30 +141,19 @@ export async function runBuildCatalog(registryDir?: string): Promise<DomainCatal
|
|
|
152
141
|
description: domainEntry['description'] as string | undefined,
|
|
153
142
|
});
|
|
154
143
|
|
|
155
|
-
const apis = (registry['apis'] as Record<string, unknown>[] | undefined) ?? [];
|
|
156
|
-
for (const api of apis) {
|
|
157
|
-
catalog.apis.push({
|
|
158
|
-
id: String(api['id']),
|
|
159
|
-
domainId,
|
|
160
|
-
path: String(api['path']),
|
|
161
|
-
method: String(api['method'] ?? 'ANY'),
|
|
162
|
-
authType: String(api['authType'] ?? 'jwt'),
|
|
163
|
-
description: api['description'] as string | undefined,
|
|
164
|
-
requestSchema: api['requestSchema'] as Record<string, unknown> | undefined,
|
|
165
|
-
responseSchema: api['responseSchema'] as Record<string, unknown> | undefined,
|
|
166
|
-
versions: api['versions'] as CatalogApi['versions'],
|
|
167
|
-
examples: api['examples'] as CatalogApi['examples'],
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
144
|
const actions = (registry['actions'] as Record<string, unknown>[] | undefined) ?? [];
|
|
172
145
|
for (const action of actions) {
|
|
173
146
|
catalog.actions.push({
|
|
174
147
|
id: String(action['id']),
|
|
175
148
|
domainId,
|
|
176
|
-
|
|
149
|
+
backendAccess: String(action['backendAccess'] ?? 'private'),
|
|
150
|
+
exposure: (action['exposure'] as Record<string, unknown> | undefined) ?? { type: 'internal' },
|
|
151
|
+
exposureDeclared: action['exposureDeclared'] as boolean | undefined,
|
|
177
152
|
idempotent: Boolean(action['idempotent'] ?? false),
|
|
178
153
|
description: action['description'] as string | undefined,
|
|
154
|
+
handlerFile: action['handlerFile'] as string | undefined,
|
|
155
|
+
deployment: action['deployment'] as Record<string, unknown> | undefined,
|
|
156
|
+
outboundAccess: action['outboundAccess'] as string | undefined,
|
|
179
157
|
inputSchema: action['inputSchema'] as Record<string, unknown> | undefined,
|
|
180
158
|
outputSchema: action['outputSchema'] as Record<string, unknown> | undefined,
|
|
181
159
|
});
|
|
@@ -241,7 +219,7 @@ export async function runBuildCatalog(registryDir?: string): Promise<DomainCatal
|
|
|
241
219
|
await writeFile(outPath, JSON.stringify(catalog), 'utf8');
|
|
242
220
|
|
|
243
221
|
cliLogger.info(
|
|
244
|
-
{ outPath, domains: catalog.domains.length,
|
|
222
|
+
{ outPath, domains: catalog.domains.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
|
|
245
223
|
'Domain catalog written'
|
|
246
224
|
);
|
|
247
225
|
|
package/src/commands/build.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve, basename } from 'node:path';
|
|
3
|
-
import type { DomainRegistry } from '
|
|
3
|
+
import type { DomainRegistry } from '../types.js';
|
|
4
4
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
5
5
|
import { buildActionsTypes } from '../builder/build-types.js';
|
|
6
6
|
import { cliLogger } from '../utils/logger.js';
|
|
@@ -40,7 +40,8 @@ export async function runBuild(options: BuildOptions): Promise<string> {
|
|
|
40
40
|
await mkdir(join(outFile, '..'), { recursive: true });
|
|
41
41
|
await writeFile(outFile, JSON.stringify(registry), 'utf8');
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
|
|
44
|
+
cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
|
|
44
45
|
|
|
45
46
|
// Discover all sibling registries and emit aggregated types
|
|
46
47
|
const registryDir = join(outFile, '..');
|
|
@@ -9,6 +9,8 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
9
9
|
export interface CheckHashesOptions {
|
|
10
10
|
/** Root directory of the project (defaults to cwd). */
|
|
11
11
|
projectRoot?: string;
|
|
12
|
+
/** If true, regenerate .mc/modules-hashes.json from current infra/modules/ before checking. */
|
|
13
|
+
write?: boolean;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -57,6 +59,16 @@ interface ModulesHashesManifest {
|
|
|
57
59
|
*/
|
|
58
60
|
export async function runCheckHashes(opts: CheckHashesOptions = {}): Promise<CheckHashesResult> {
|
|
59
61
|
const root = opts.projectRoot ?? process.cwd();
|
|
62
|
+
|
|
63
|
+
// --write bootstraps the manifest from current infra/modules/ before verification.
|
|
64
|
+
// Must run before the manifest read below so it can create a missing manifest.
|
|
65
|
+
if (opts.write) {
|
|
66
|
+
const { runRegenerateModulesHashes } = await import('./regenerate-modules-hashes.js');
|
|
67
|
+
const outPath = await runRegenerateModulesHashes({ projectRoot: root });
|
|
68
|
+
cliLogger.info({ outPath }, 'modules-hashes.json regenerated');
|
|
69
|
+
return { ok: true, drifted: [], missing: [], unexpected: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
const manifestPath = join(root, '.mc', 'modules-hashes.json');
|
|
61
73
|
const modulesDir = join(root, 'infra', 'modules');
|
|
62
74
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function startDev(options: DevOptions): Promise<DevHandle> {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
cliLogger.info(
|
|
58
|
-
{ domain: registry.domain.id,
|
|
58
|
+
{ domain: registry.domain.id, apiActions: registry.actions.filter(a => a.exposure?.type === 'api').length, port },
|
|
59
59
|
'Starting local dev server'
|
|
60
60
|
);
|
|
61
61
|
|
package/src/commands/doctor.ts
CHANGED
|
@@ -121,8 +121,10 @@ async function checkHandlersUseResult(projectRoot: string): Promise<DoctorCheck>
|
|
|
121
121
|
const missing: string[] = [];
|
|
122
122
|
for (const file of apiFiles) {
|
|
123
123
|
const content = await readFile(file, 'utf8');
|
|
124
|
-
// A file contains a handler export — check the return type
|
|
125
|
-
|
|
124
|
+
// A file contains a handler export — check the return type.
|
|
125
|
+
// Issue #4689: defineApi was removed. The action-first
|
|
126
|
+
// contract uses defineAction with exposure.type='api'.
|
|
127
|
+
if (/defineAction\s*\(/.test(content)) {
|
|
126
128
|
// Look for Result<T> in the handler's return type annotation
|
|
127
129
|
if (!/: .*Result</.test(content)) {
|
|
128
130
|
missing.push(relative(projectRoot, file));
|
|
@@ -448,49 +450,64 @@ async function checkNoCrossDomainImports(projectRoot: string): Promise<DoctorChe
|
|
|
448
450
|
async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck> {
|
|
449
451
|
try {
|
|
450
452
|
const domainsDir = join(projectRoot, 'domains');
|
|
451
|
-
const
|
|
453
|
+
const allActionFiles: string[] = [];
|
|
452
454
|
|
|
453
|
-
// Find
|
|
455
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
456
|
+
// Issue #4689: defineApi was removed. API-exposed actions live in
|
|
457
|
+
// domains/*/actions/ but legacy api/ directories may still exist.
|
|
454
458
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
455
459
|
for (const entry of entries) {
|
|
456
460
|
if (!entry.isDirectory()) continue;
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
461
|
+
for (const dir of ['actions', 'api']) {
|
|
462
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
463
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
464
|
+
allActionFiles.push(...files);
|
|
465
|
+
}
|
|
460
466
|
}
|
|
461
|
-
const apiFiles = allApiFiles;
|
|
462
467
|
|
|
463
|
-
|
|
468
|
+
// Only consider files that define an API-exposed action (defineAction
|
|
469
|
+
// with exposure.type === 'api' or exposure: { type: 'api' }).
|
|
470
|
+
const apiExposedFiles: string[] = [];
|
|
471
|
+
for (const file of allActionFiles) {
|
|
472
|
+
const content = await readFile(file, 'utf8');
|
|
473
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
474
|
+
apiExposedFiles.push(file);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (apiExposedFiles.length === 0) {
|
|
464
479
|
return {
|
|
465
480
|
name: 'APIs declare versions',
|
|
466
481
|
status: 'PASS',
|
|
467
|
-
message: 'No
|
|
482
|
+
message: 'No API-exposed actions found (optional)',
|
|
468
483
|
kNodeRef: 'K:runbook:add-domain',
|
|
469
484
|
};
|
|
470
485
|
}
|
|
471
486
|
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
487
|
+
// Issue #4689: action-first contract requires input/output Zod schemas
|
|
488
|
+
// with `.default({...})` example data. Check that each API-exposed
|
|
489
|
+
// action file contains `.default(` (indicating example data).
|
|
490
|
+
let missingDefaults = 0;
|
|
491
|
+
for (const file of apiExposedFiles) {
|
|
475
492
|
const content = await readFile(file, 'utf8');
|
|
476
|
-
if (!content.includes('
|
|
477
|
-
|
|
493
|
+
if (!content.includes('.default(')) {
|
|
494
|
+
missingDefaults++;
|
|
478
495
|
}
|
|
479
496
|
}
|
|
480
497
|
|
|
481
|
-
if (
|
|
498
|
+
if (missingDefaults === 0) {
|
|
482
499
|
return {
|
|
483
500
|
name: 'APIs declare versions',
|
|
484
501
|
status: 'PASS',
|
|
485
|
-
message: `All ${
|
|
502
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) have .default() example data`,
|
|
486
503
|
kNodeRef: 'K:runbook:add-domain',
|
|
487
504
|
};
|
|
488
505
|
} else {
|
|
489
506
|
return {
|
|
490
507
|
name: 'APIs declare versions',
|
|
491
508
|
status: 'FAIL',
|
|
492
|
-
message: `${
|
|
493
|
-
fixHint:
|
|
509
|
+
message: `${missingDefaults}/${apiExposedFiles.length} API-exposed action(s) missing .default() example data. Add \`.default({...})\` to the top-level input and output Zod schemas in each defineAction({ exposure: { type: 'api', ... } }) call.`,
|
|
510
|
+
fixHint: "Add `input` and `output` Zod schemas with `.default({...})` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
494
511
|
kNodeRef: 'K:runbook:add-domain',
|
|
495
512
|
};
|
|
496
513
|
}
|
|
@@ -498,7 +515,7 @@ async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck>
|
|
|
498
515
|
return {
|
|
499
516
|
name: 'APIs declare versions',
|
|
500
517
|
status: 'WARN',
|
|
501
|
-
message: `Could not check API
|
|
518
|
+
message: `Could not check API schemas: ${String(err)}`,
|
|
502
519
|
kNodeRef: 'K:runbook:add-domain',
|
|
503
520
|
};
|
|
504
521
|
}
|
|
@@ -507,30 +524,42 @@ async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck>
|
|
|
507
524
|
async function checkApisHaveTenancy(projectRoot: string): Promise<DoctorCheck> {
|
|
508
525
|
try {
|
|
509
526
|
const domainsDir = join(projectRoot, 'domains');
|
|
510
|
-
const
|
|
527
|
+
const allActionFiles: string[] = [];
|
|
511
528
|
|
|
512
|
-
// Find
|
|
529
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
530
|
+
// Issue #4689: defineApi was removed. Tenancy is now declared on the
|
|
531
|
+
// action's exposure block via `exposure.tenancy`.
|
|
513
532
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
514
533
|
for (const entry of entries) {
|
|
515
534
|
if (!entry.isDirectory()) continue;
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
535
|
+
for (const dir of ['actions', 'api']) {
|
|
536
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
537
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
538
|
+
allActionFiles.push(...files);
|
|
539
|
+
}
|
|
519
540
|
}
|
|
520
|
-
const apiFiles = allApiFiles;
|
|
521
541
|
|
|
522
|
-
|
|
542
|
+
// Only consider files that define an API-exposed action.
|
|
543
|
+
const apiExposedFiles: string[] = [];
|
|
544
|
+
for (const file of allActionFiles) {
|
|
545
|
+
const content = await readFile(file, 'utf8');
|
|
546
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
547
|
+
apiExposedFiles.push(file);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (apiExposedFiles.length === 0) {
|
|
523
552
|
return {
|
|
524
553
|
name: 'APIs declare tenancy',
|
|
525
554
|
status: 'PASS',
|
|
526
|
-
message: 'No
|
|
555
|
+
message: 'No API-exposed actions found (optional)',
|
|
527
556
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
528
557
|
};
|
|
529
558
|
}
|
|
530
559
|
|
|
531
|
-
//
|
|
560
|
+
// Check for `tenancy:` anywhere in each API-exposed action file.
|
|
532
561
|
let missingTenancy = 0;
|
|
533
|
-
for (const file of
|
|
562
|
+
for (const file of apiExposedFiles) {
|
|
534
563
|
const content = await readFile(file, 'utf8');
|
|
535
564
|
if (!content.includes('tenancy:')) {
|
|
536
565
|
missingTenancy++;
|
|
@@ -541,15 +570,15 @@ async function checkApisHaveTenancy(projectRoot: string): Promise<DoctorCheck> {
|
|
|
541
570
|
return {
|
|
542
571
|
name: 'APIs declare tenancy',
|
|
543
572
|
status: 'PASS',
|
|
544
|
-
message: `All ${
|
|
573
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) declare tenancy`,
|
|
545
574
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
546
575
|
};
|
|
547
576
|
} else {
|
|
548
577
|
return {
|
|
549
578
|
name: 'APIs declare tenancy',
|
|
550
579
|
status: 'FAIL',
|
|
551
|
-
message: `${missingTenancy}/${
|
|
552
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to
|
|
580
|
+
message: `${missingTenancy}/${apiExposedFiles.length} API-exposed action(s) missing tenancy. Tenancy must be declared on the action's exposure block: \`exposure.tenancy: 'required' | 'none' | 'system'\`.`,
|
|
581
|
+
fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
553
582
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
554
583
|
};
|
|
555
584
|
}
|
|
@@ -584,9 +613,9 @@ async function checkScaffoldConfigMatchesDisk(projectRoot: string): Promise<Doct
|
|
|
584
613
|
} catch {
|
|
585
614
|
return {
|
|
586
615
|
name: 'scaffold-config.json matches on-disk',
|
|
587
|
-
status: '
|
|
588
|
-
message: '
|
|
589
|
-
fixHint: '
|
|
616
|
+
status: 'WARN',
|
|
617
|
+
message: '.mc/scaffold-config.json not found or unreadable — skipping domain list sync check (expected on fresh clones before first scaffold)',
|
|
618
|
+
fixHint: 'Run mc-domain-module update-all to regenerate scaffold metadata',
|
|
590
619
|
kNodeRef: 'K:runbook:add-domain',
|
|
591
620
|
};
|
|
592
621
|
}
|
|
@@ -699,7 +728,7 @@ async function checkRootLevelFlows(projectRoot: string): Promise<DoctorCheck> {
|
|
|
699
728
|
}
|
|
700
729
|
return {
|
|
701
730
|
name: 'No root-level flows (deprecated)',
|
|
702
|
-
status: '
|
|
731
|
+
status: 'FAIL',
|
|
703
732
|
message: `${flowFiles.length} flow(s) still in root-level flows/: ${flowFiles.join(', ')}`,
|
|
704
733
|
fixHint: 'Move these flows to domains/{owningDomain}/flows/ and delete the root-level copies.',
|
|
705
734
|
kNodeRef: 'K:convention:flow-vs-subscriber-rule',
|
|
@@ -873,7 +902,7 @@ async function checkAllRoutesUseTanStackRouter(projectRoot: string): Promise<Doc
|
|
|
873
902
|
|
|
874
903
|
/**
|
|
875
904
|
* Check W5-4: Lambda handler files contain initOtel() call.
|
|
876
|
-
|
|
905
|
+
* Scans `domains/*\/actions/*.ts` and `domains/*\/api/*.ts` and FAILs if any handler is missing initOtel().
|
|
877
906
|
*/
|
|
878
907
|
async function checkOtelInitInLambdas(projectRoot: string): Promise<DoctorCheck> {
|
|
879
908
|
try {
|
|
@@ -890,9 +919,11 @@ async function checkOtelInitInLambdas(projectRoot: string): Promise<DoctorCheck>
|
|
|
890
919
|
const handlerFiles: string[] = [];
|
|
891
920
|
for (const entry of entries) {
|
|
892
921
|
if (!entry.isDirectory()) continue;
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
922
|
+
for (const dir of ['actions', 'api']) {
|
|
923
|
+
const handlerDir = join(domainsDir, entry.name, dir);
|
|
924
|
+
const files = findFiles(handlerDir, /\.ts$/);
|
|
925
|
+
handlerFiles.push(...files);
|
|
926
|
+
}
|
|
896
927
|
}
|
|
897
928
|
|
|
898
929
|
if (handlerFiles.length === 0) {
|