@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
|
@@ -141,8 +141,7 @@ export async function buildRegistry(domainRoot) {
|
|
|
141
141
|
tenancy: String(domainRaw['tenancy']),
|
|
142
142
|
defaultDeployment: deployment(domainRaw),
|
|
143
143
|
};
|
|
144
|
-
const [
|
|
145
|
-
Promise.all(paths.apis.map(load)),
|
|
144
|
+
const [webhookExports, subscriberExports, actionExports, scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
|
|
146
145
|
Promise.all(paths.webhooks.map(load)),
|
|
147
146
|
Promise.all(paths.subscribers.map(load)),
|
|
148
147
|
Promise.all(paths.actions.map(load)),
|
|
@@ -151,39 +150,6 @@ export async function buildRegistry(domainRoot) {
|
|
|
151
150
|
Promise.all(paths.integrations.map(load)),
|
|
152
151
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
153
152
|
]);
|
|
154
|
-
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
155
|
-
const apis = paths.apis.flatMap((filePath, i) => (apiExports[i] ?? [])
|
|
156
|
-
.filter(e => e['_kind'] === 'api')
|
|
157
|
-
.map(e => {
|
|
158
|
-
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
159
|
-
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
160
|
-
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
161
|
-
}
|
|
162
|
-
const rawVersions = e['versions'];
|
|
163
|
-
const versionSnapshots = rawVersions
|
|
164
|
-
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
165
|
-
version: ver,
|
|
166
|
-
requestSchema: isJsonSchema(v?.input) ? v.input : undefined,
|
|
167
|
-
responseSchema: isJsonSchema(v?.output) ? v.output : undefined,
|
|
168
|
-
}))
|
|
169
|
-
: [];
|
|
170
|
-
const latestVersion = versionSnapshots[versionSnapshots.length - 1];
|
|
171
|
-
return {
|
|
172
|
-
id: String(e['id']),
|
|
173
|
-
kind: 'api',
|
|
174
|
-
handlerFile: relPath(filePath),
|
|
175
|
-
path: String(e['path']),
|
|
176
|
-
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
177
|
-
authType: e['auth']?.type ?? 'jwt',
|
|
178
|
-
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
179
|
-
deployment: deployment(e),
|
|
180
|
-
outboundAccess: outboundAccess(e),
|
|
181
|
-
requestSchema: latestVersion?.requestSchema,
|
|
182
|
-
responseSchema: latestVersion?.responseSchema,
|
|
183
|
-
versions: versionSnapshots.length > 0 ? versionSnapshots : undefined,
|
|
184
|
-
examples: e['examples'],
|
|
185
|
-
};
|
|
186
|
-
}));
|
|
187
153
|
const webhooks = paths.webhooks.flatMap((filePath, i) => (webhookExports[i] ?? [])
|
|
188
154
|
.filter(e => e['_kind'] === 'webhook')
|
|
189
155
|
.map(e => ({
|
|
@@ -298,7 +264,6 @@ export async function buildRegistry(domainRoot) {
|
|
|
298
264
|
schemaVersion: '1',
|
|
299
265
|
domainRoot,
|
|
300
266
|
domain,
|
|
301
|
-
apis,
|
|
302
267
|
webhooks,
|
|
303
268
|
subscribers,
|
|
304
269
|
schedules,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DomainRegistry } from '
|
|
1
|
+
import type { DomainRegistry } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Generate a .d.ts file declaring typed `ctx.actions.call(actionId, input)`
|
|
4
4
|
* overrides for the union of actions across all registries. The generated
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The set of `_kind` discriminant values that identify a domain primitive export.
|
|
3
3
|
*/
|
|
4
|
-
export declare const PRIMITIVE_KINDS: Set<"
|
|
4
|
+
export declare const PRIMITIVE_KINDS: Set<"domain" | "api" | "webhook" | "subscriber" | "schedule" | "job" | "action" | "integration" | "event">;
|
|
5
5
|
/**
|
|
6
6
|
* A raw primitive export extracted from a domain source file.
|
|
7
7
|
* Functions (handler) and Zod schemas (input/output/versions) are stripped
|
package/dist/cli.js
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';
|
|
@@ -67,7 +69,7 @@ program
|
|
|
67
69
|
});
|
|
68
70
|
program
|
|
69
71
|
.command('dev <domain>')
|
|
70
|
-
.description('Start a local HTTP server simulating API Gateway for all
|
|
72
|
+
.description('Start a local HTTP server simulating API Gateway for all API-exposed defineAction handlers')
|
|
71
73
|
.option('--port <n>', 'Port to listen on', '3000')
|
|
72
74
|
.action(async (domain, opts) => {
|
|
73
75
|
await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
|
|
@@ -173,10 +175,26 @@ program
|
|
|
173
175
|
program
|
|
174
176
|
.command('check-hashes')
|
|
175
177
|
.description('Verify infra/modules/ has not been hand-edited since last scaffold')
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
+
.option('--write', 'Regenerate modules-hashes.json from current disk state instead of verifying')
|
|
179
|
+
.action(async (opts) => {
|
|
180
|
+
const result = await runCheckHashes({ write: opts.write });
|
|
178
181
|
process.exit(result.ok ? 0 : 1);
|
|
179
182
|
});
|
|
183
|
+
program
|
|
184
|
+
.command('update-all')
|
|
185
|
+
.description('Full refresh: build all domains, build catalog, build flows, build UI, regenerate hashes, run doctor')
|
|
186
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
187
|
+
.action(async (opts) => {
|
|
188
|
+
const result = await runUpdateAll({ projectRoot: opts.projectRoot });
|
|
189
|
+
process.exit(result.success ? 0 : 1);
|
|
190
|
+
});
|
|
191
|
+
program
|
|
192
|
+
.command('regenerate-modules-hashes')
|
|
193
|
+
.description('Walk infra/modules/, compute SHA256 hashes, and write .mc/modules-hashes.json')
|
|
194
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
195
|
+
.action(async (opts) => {
|
|
196
|
+
await runRegenerateModulesHashes({ projectRoot: opts.projectRoot });
|
|
197
|
+
});
|
|
180
198
|
program
|
|
181
199
|
.command('upgrade-backend <target-major>')
|
|
182
200
|
.description('Run jscodeshift/ts-morph migrations between major versions of domain-runtime')
|
package/dist/commands/add-api.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function runAddApi(opts) {
|
|
|
48
48
|
catch {
|
|
49
49
|
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
50
50
|
}
|
|
51
|
-
const apiFilePath = join(domainDir, '
|
|
51
|
+
const apiFilePath = join(domainDir, 'actions', `${opts.id}.ts`);
|
|
52
52
|
// Refuse if API already exists
|
|
53
53
|
try {
|
|
54
54
|
await access(apiFilePath);
|
|
@@ -74,7 +74,7 @@ export async function runAddApi(opts) {
|
|
|
74
74
|
// Write API file
|
|
75
75
|
await writeFile(apiFilePath, apiContent);
|
|
76
76
|
// Create fixture with the input schema's default shape as the example payload
|
|
77
|
-
const apiTestDir = join(domainDir, '
|
|
77
|
+
const apiTestDir = join(domainDir, 'actions', '__tests__');
|
|
78
78
|
await mkdir(apiTestDir, { recursive: true });
|
|
79
79
|
await writeFile(join(apiTestDir, `${opts.id}.fixture.json`), apiFixtureSkeleton(opts.domain, opts.id, exampleBody));
|
|
80
80
|
cliLogger.info({ domain: opts.domain, api: opts.id, method: opts.method ?? 'GET', pattern: opts.pattern ?? 'skeleton' }, 'API added');
|
|
@@ -34,13 +34,13 @@ export async function runAddDomain(opts) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
// Create directory structure
|
|
37
|
-
await mkdir(join(domainDir, '
|
|
37
|
+
await mkdir(join(domainDir, 'actions', '__tests__'), { recursive: true });
|
|
38
38
|
await mkdir(join(domainDir, 'subscribers'), { recursive: true });
|
|
39
39
|
await mkdir(join(domainDir, 'publishes'), { recursive: true });
|
|
40
40
|
// Write skeleton files
|
|
41
41
|
await writeFile(join(domainDir, 'domain.config.ts'), domainConfigTemplate(opts.id, opts.tenancy));
|
|
42
|
-
await writeFile(join(domainDir, '
|
|
43
|
-
await writeFile(join(domainDir, '
|
|
42
|
+
await writeFile(join(domainDir, 'actions', 'example.ts'), apiSkeletonTemplate(opts.id, 'example', opts.tenancy));
|
|
43
|
+
await writeFile(join(domainDir, 'actions', '__tests__', 'example.fixture.json'), apiFixtureSkeleton(opts.id, 'example'));
|
|
44
44
|
await writeFile(join(domainDir, 'publishes', 'events.ts'), eventsSkeletonTemplate(opts.id));
|
|
45
45
|
await writeFile(join(domainDir, 'CLAUDE.md'), claudeMdTemplate(opts.id));
|
|
46
46
|
await writeFile(join(domainDir, 'README.md'), readmeTemplate(opts.id));
|
|
@@ -66,6 +66,6 @@ export async function runAddDomain(opts) {
|
|
|
66
66
|
await addDomainToScaffoldConfig(opts.id, configRoot);
|
|
67
67
|
cliLogger.info({ id: opts.id, dir: domainDir }, 'Domain scaffolded');
|
|
68
68
|
// eslint-disable-next-line no-console
|
|
69
|
-
console.log(`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit
|
|
69
|
+
console.log(`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit actions/example.ts, ` +
|
|
70
70
|
`then run \`mc-domain-module build ${opts.id}\` to generate the registry.`);
|
|
71
71
|
}
|
|
@@ -37,18 +37,17 @@ export async function runAddFixtureFactory(options) {
|
|
|
37
37
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
38
38
|
const domain = options.domain;
|
|
39
39
|
const apiId = options.apiId;
|
|
40
|
-
const apiFile = join(projectRoot, 'domains', domain, '
|
|
40
|
+
const apiFile = join(projectRoot, 'domains', domain, 'actions', `${apiId}.ts`);
|
|
41
41
|
await access(apiFile).catch(() => {
|
|
42
|
-
throw new Error(`
|
|
42
|
+
throw new Error(`Action file not found: ${apiFile}. Run add-api first.`);
|
|
43
43
|
});
|
|
44
|
-
// Read the
|
|
44
|
+
// Read the action file to extract the input type name
|
|
45
45
|
const content = await readFile(apiFile, 'utf8');
|
|
46
|
-
const
|
|
47
|
-
const factoryFile = join(projectRoot, 'domains', domain, 'api', '__tests__', `${apiId}.factory.ts`);
|
|
46
|
+
const factoryFile = join(projectRoot, 'domains', domain, 'actions', '__tests__', `${apiId}.factory.ts`);
|
|
48
47
|
const factoryContent = FACTORY_TEMPLATE
|
|
49
48
|
.replace(/\{domain\}/g, domain)
|
|
50
49
|
.replace(/\{apiId\}/g, apiId);
|
|
51
|
-
await mkdir(join(projectRoot, 'domains', domain, '
|
|
50
|
+
await mkdir(join(projectRoot, 'domains', domain, 'actions', '__tests__'), { recursive: true });
|
|
52
51
|
await writeFile(factoryFile, factoryContent, 'utf8');
|
|
53
52
|
cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
|
|
54
53
|
return factoryFile;
|
|
@@ -1,30 +1,15 @@
|
|
|
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
|
-
examples?: {
|
|
17
|
-
request?: Record<string, unknown>;
|
|
18
|
-
response?: Record<string, unknown>;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
1
|
/** A single action entry in the catalog. */
|
|
22
2
|
export interface CatalogAction {
|
|
23
3
|
id: string;
|
|
24
4
|
domainId: string;
|
|
25
|
-
|
|
5
|
+
backendAccess: string;
|
|
6
|
+
exposure: Record<string, unknown>;
|
|
7
|
+
exposureDeclared?: boolean;
|
|
26
8
|
idempotent: boolean;
|
|
27
9
|
description?: string;
|
|
10
|
+
handlerFile?: string;
|
|
11
|
+
deployment?: Record<string, unknown>;
|
|
12
|
+
outboundAccess?: string;
|
|
28
13
|
inputSchema?: Record<string, unknown>;
|
|
29
14
|
outputSchema?: Record<string, unknown>;
|
|
30
15
|
}
|
|
@@ -81,7 +66,6 @@ export interface DomainCatalog {
|
|
|
81
66
|
version: 2;
|
|
82
67
|
generatedAt: string;
|
|
83
68
|
domains: CatalogDomain[];
|
|
84
|
-
apis: CatalogApi[];
|
|
85
69
|
actions: CatalogAction[];
|
|
86
70
|
events: CatalogEvent[];
|
|
87
71
|
subscribers: CatalogSubscriber[];
|
|
@@ -25,7 +25,6 @@ export async function runBuildCatalog(registryDir) {
|
|
|
25
25
|
version: 2,
|
|
26
26
|
generatedAt: new Date().toISOString(),
|
|
27
27
|
domains: [],
|
|
28
|
-
apis: [],
|
|
29
28
|
actions: [],
|
|
30
29
|
events: [],
|
|
31
30
|
subscribers: [],
|
|
@@ -55,29 +54,19 @@ export async function runBuildCatalog(registryDir) {
|
|
|
55
54
|
tenancy: String(domainEntry['tenancy'] ?? 'none'),
|
|
56
55
|
description: domainEntry['description'],
|
|
57
56
|
});
|
|
58
|
-
const apis = registry['apis'] ?? [];
|
|
59
|
-
for (const api of apis) {
|
|
60
|
-
catalog.apis.push({
|
|
61
|
-
id: String(api['id']),
|
|
62
|
-
domainId,
|
|
63
|
-
path: String(api['path']),
|
|
64
|
-
method: String(api['method'] ?? 'ANY'),
|
|
65
|
-
authType: String(api['authType'] ?? 'jwt'),
|
|
66
|
-
description: api['description'],
|
|
67
|
-
requestSchema: api['requestSchema'],
|
|
68
|
-
responseSchema: api['responseSchema'],
|
|
69
|
-
versions: api['versions'],
|
|
70
|
-
examples: api['examples'],
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
57
|
const actions = registry['actions'] ?? [];
|
|
74
58
|
for (const action of actions) {
|
|
75
59
|
catalog.actions.push({
|
|
76
60
|
id: String(action['id']),
|
|
77
61
|
domainId,
|
|
78
|
-
|
|
62
|
+
backendAccess: String(action['backendAccess'] ?? 'private'),
|
|
63
|
+
exposure: action['exposure'] ?? { type: 'internal' },
|
|
64
|
+
exposureDeclared: action['exposureDeclared'],
|
|
79
65
|
idempotent: Boolean(action['idempotent'] ?? false),
|
|
80
66
|
description: action['description'],
|
|
67
|
+
handlerFile: action['handlerFile'],
|
|
68
|
+
deployment: action['deployment'],
|
|
69
|
+
outboundAccess: action['outboundAccess'],
|
|
81
70
|
inputSchema: action['inputSchema'],
|
|
82
71
|
outputSchema: action['outputSchema'],
|
|
83
72
|
});
|
|
@@ -135,6 +124,6 @@ export async function runBuildCatalog(registryDir) {
|
|
|
135
124
|
await mkdir(mcDir, { recursive: true });
|
|
136
125
|
const outPath = join(mcDir, 'domain-registry.json');
|
|
137
126
|
await writeFile(outPath, JSON.stringify(catalog), 'utf8');
|
|
138
|
-
cliLogger.info({ outPath, domains: catalog.domains.length,
|
|
127
|
+
cliLogger.info({ 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 }, 'Domain catalog written');
|
|
139
128
|
return catalog;
|
|
140
129
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -23,7 +23,8 @@ export async function runBuild(options) {
|
|
|
23
23
|
}
|
|
24
24
|
await mkdir(join(outFile, '..'), { recursive: true });
|
|
25
25
|
await writeFile(outFile, JSON.stringify(registry), 'utf8');
|
|
26
|
-
|
|
26
|
+
const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
|
|
27
|
+
cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
|
|
27
28
|
// Discover all sibling registries and emit aggregated types
|
|
28
29
|
const registryDir = join(outFile, '..');
|
|
29
30
|
const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
export interface CheckHashesOptions {
|
|
5
5
|
/** Root directory of the project (defaults to cwd). */
|
|
6
6
|
projectRoot?: string;
|
|
7
|
+
/** If true, regenerate .mc/modules-hashes.json from current infra/modules/ before checking. */
|
|
8
|
+
write?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* A single hash check result.
|
|
@@ -10,6 +10,14 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function runCheckHashes(opts = {}) {
|
|
12
12
|
const root = opts.projectRoot ?? process.cwd();
|
|
13
|
+
// --write bootstraps the manifest from current infra/modules/ before verification.
|
|
14
|
+
// Must run before the manifest read below so it can create a missing manifest.
|
|
15
|
+
if (opts.write) {
|
|
16
|
+
const { runRegenerateModulesHashes } = await import('./regenerate-modules-hashes.js');
|
|
17
|
+
const outPath = await runRegenerateModulesHashes({ projectRoot: root });
|
|
18
|
+
cliLogger.info({ outPath }, 'modules-hashes.json regenerated');
|
|
19
|
+
return { ok: true, drifted: [], missing: [], unexpected: [] };
|
|
20
|
+
}
|
|
13
21
|
const manifestPath = join(root, '.mc', 'modules-hashes.json');
|
|
14
22
|
const modulesDir = join(root, 'infra', 'modules');
|
|
15
23
|
let manifest;
|
package/dist/commands/dev.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function startDev(options) {
|
|
|
21
21
|
for (const w of warnings) {
|
|
22
22
|
cliLogger.warn(w);
|
|
23
23
|
}
|
|
24
|
-
cliLogger.info({ domain: registry.domain.id,
|
|
24
|
+
cliLogger.info({ domain: registry.domain.id, apiActions: registry.actions.filter(a => a.exposure?.type === 'api').length, port }, 'Starting local dev server');
|
|
25
25
|
const server = await createApiServer({ port, registry, domainRoot });
|
|
26
26
|
// Spawn Vite if frontend/ exists; skip with warning otherwise.
|
|
27
27
|
const frontendDir = join(projectRoot, 'frontend');
|
package/dist/commands/doctor.js
CHANGED
|
@@ -59,8 +59,10 @@ async function checkHandlersUseResult(projectRoot) {
|
|
|
59
59
|
const missing = [];
|
|
60
60
|
for (const file of apiFiles) {
|
|
61
61
|
const content = await readFile(file, 'utf8');
|
|
62
|
-
// A file contains a handler export — check the return type
|
|
63
|
-
|
|
62
|
+
// A file contains a handler export — check the return type.
|
|
63
|
+
// Issue #4689: defineApi was removed. The action-first
|
|
64
|
+
// contract uses defineAction with exposure.type='api'.
|
|
65
|
+
if (/defineAction\s*\(/.test(content)) {
|
|
64
66
|
// Look for Result<T> in the handler's return type annotation
|
|
65
67
|
if (!/: .*Result</.test(content)) {
|
|
66
68
|
missing.push(relative(projectRoot, file));
|
|
@@ -361,38 +363,52 @@ async function checkNoCrossDomainImports(projectRoot) {
|
|
|
361
363
|
async function checkApisHaveVersions(projectRoot) {
|
|
362
364
|
try {
|
|
363
365
|
const domainsDir = join(projectRoot, 'domains');
|
|
364
|
-
const
|
|
365
|
-
// Find
|
|
366
|
+
const allActionFiles = [];
|
|
367
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
368
|
+
// Issue #4689: defineApi was removed. API-exposed actions live in
|
|
369
|
+
// domains/*/actions/ but legacy api/ directories may still exist.
|
|
366
370
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
367
371
|
for (const entry of entries) {
|
|
368
372
|
if (!entry.isDirectory())
|
|
369
373
|
continue;
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
374
|
+
for (const dir of ['actions', 'api']) {
|
|
375
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
376
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
377
|
+
allActionFiles.push(...files);
|
|
378
|
+
}
|
|
373
379
|
}
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
// Only consider files that define an API-exposed action (defineAction
|
|
381
|
+
// with exposure.type === 'api' or exposure: { type: 'api' }).
|
|
382
|
+
const apiExposedFiles = [];
|
|
383
|
+
for (const file of allActionFiles) {
|
|
384
|
+
const content = await readFile(file, 'utf8');
|
|
385
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
386
|
+
apiExposedFiles.push(file);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (apiExposedFiles.length === 0) {
|
|
376
390
|
return {
|
|
377
391
|
name: 'APIs declare versions',
|
|
378
392
|
status: 'PASS',
|
|
379
|
-
message: 'No
|
|
393
|
+
message: 'No API-exposed actions found (optional)',
|
|
380
394
|
kNodeRef: 'K:runbook:add-domain',
|
|
381
395
|
};
|
|
382
396
|
}
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
397
|
+
// Issue #4689: action-first contract requires input/output Zod schemas
|
|
398
|
+
// with `.default({...})` example data. Check that each API-exposed
|
|
399
|
+
// action file contains `.default(` (indicating example data).
|
|
400
|
+
let missingDefaults = 0;
|
|
401
|
+
for (const file of apiExposedFiles) {
|
|
386
402
|
const content = await readFile(file, 'utf8');
|
|
387
|
-
if (!content.includes('
|
|
388
|
-
|
|
403
|
+
if (!content.includes('.default(')) {
|
|
404
|
+
missingDefaults++;
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
|
-
if (
|
|
407
|
+
if (missingDefaults === 0) {
|
|
392
408
|
return {
|
|
393
409
|
name: 'APIs declare versions',
|
|
394
410
|
status: 'PASS',
|
|
395
|
-
message: `All ${
|
|
411
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) have .default() example data`,
|
|
396
412
|
kNodeRef: 'K:runbook:add-domain',
|
|
397
413
|
};
|
|
398
414
|
}
|
|
@@ -400,8 +416,8 @@ async function checkApisHaveVersions(projectRoot) {
|
|
|
400
416
|
return {
|
|
401
417
|
name: 'APIs declare versions',
|
|
402
418
|
status: 'FAIL',
|
|
403
|
-
message: `${
|
|
404
|
-
fixHint:
|
|
419
|
+
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.`,
|
|
420
|
+
fixHint: "Add `input` and `output` Zod schemas with `.default({...})` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
405
421
|
kNodeRef: 'K:runbook:add-domain',
|
|
406
422
|
};
|
|
407
423
|
}
|
|
@@ -410,7 +426,7 @@ async function checkApisHaveVersions(projectRoot) {
|
|
|
410
426
|
return {
|
|
411
427
|
name: 'APIs declare versions',
|
|
412
428
|
status: 'WARN',
|
|
413
|
-
message: `Could not check API
|
|
429
|
+
message: `Could not check API schemas: ${String(err)}`,
|
|
414
430
|
kNodeRef: 'K:runbook:add-domain',
|
|
415
431
|
};
|
|
416
432
|
}
|
|
@@ -418,28 +434,39 @@ async function checkApisHaveVersions(projectRoot) {
|
|
|
418
434
|
async function checkApisHaveTenancy(projectRoot) {
|
|
419
435
|
try {
|
|
420
436
|
const domainsDir = join(projectRoot, 'domains');
|
|
421
|
-
const
|
|
422
|
-
// Find
|
|
437
|
+
const allActionFiles = [];
|
|
438
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
439
|
+
// Issue #4689: defineApi was removed. Tenancy is now declared on the
|
|
440
|
+
// action's exposure block via `exposure.tenancy`.
|
|
423
441
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
424
442
|
for (const entry of entries) {
|
|
425
443
|
if (!entry.isDirectory())
|
|
426
444
|
continue;
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
445
|
+
for (const dir of ['actions', 'api']) {
|
|
446
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
447
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
448
|
+
allActionFiles.push(...files);
|
|
449
|
+
}
|
|
430
450
|
}
|
|
431
|
-
|
|
432
|
-
|
|
451
|
+
// Only consider files that define an API-exposed action.
|
|
452
|
+
const apiExposedFiles = [];
|
|
453
|
+
for (const file of allActionFiles) {
|
|
454
|
+
const content = await readFile(file, 'utf8');
|
|
455
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
456
|
+
apiExposedFiles.push(file);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (apiExposedFiles.length === 0) {
|
|
433
460
|
return {
|
|
434
461
|
name: 'APIs declare tenancy',
|
|
435
462
|
status: 'PASS',
|
|
436
|
-
message: 'No
|
|
463
|
+
message: 'No API-exposed actions found (optional)',
|
|
437
464
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
438
465
|
};
|
|
439
466
|
}
|
|
440
|
-
//
|
|
467
|
+
// Check for `tenancy:` anywhere in each API-exposed action file.
|
|
441
468
|
let missingTenancy = 0;
|
|
442
|
-
for (const file of
|
|
469
|
+
for (const file of apiExposedFiles) {
|
|
443
470
|
const content = await readFile(file, 'utf8');
|
|
444
471
|
if (!content.includes('tenancy:')) {
|
|
445
472
|
missingTenancy++;
|
|
@@ -449,7 +476,7 @@ async function checkApisHaveTenancy(projectRoot) {
|
|
|
449
476
|
return {
|
|
450
477
|
name: 'APIs declare tenancy',
|
|
451
478
|
status: 'PASS',
|
|
452
|
-
message: `All ${
|
|
479
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) declare tenancy`,
|
|
453
480
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
454
481
|
};
|
|
455
482
|
}
|
|
@@ -457,8 +484,8 @@ async function checkApisHaveTenancy(projectRoot) {
|
|
|
457
484
|
return {
|
|
458
485
|
name: 'APIs declare tenancy',
|
|
459
486
|
status: 'FAIL',
|
|
460
|
-
message: `${missingTenancy}/${
|
|
461
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to
|
|
487
|
+
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'\`.`,
|
|
488
|
+
fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
462
489
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
463
490
|
};
|
|
464
491
|
}
|
|
@@ -493,9 +520,9 @@ async function checkScaffoldConfigMatchesDisk(projectRoot) {
|
|
|
493
520
|
catch {
|
|
494
521
|
return {
|
|
495
522
|
name: 'scaffold-config.json matches on-disk',
|
|
496
|
-
status: '
|
|
497
|
-
message: '
|
|
498
|
-
fixHint: '
|
|
523
|
+
status: 'WARN',
|
|
524
|
+
message: '.mc/scaffold-config.json not found or unreadable — skipping domain list sync check (expected on fresh clones before first scaffold)',
|
|
525
|
+
fixHint: 'Run mc-domain-module update-all to regenerate scaffold metadata',
|
|
499
526
|
kNodeRef: 'K:runbook:add-domain',
|
|
500
527
|
};
|
|
501
528
|
}
|
|
@@ -607,7 +634,7 @@ async function checkRootLevelFlows(projectRoot) {
|
|
|
607
634
|
}
|
|
608
635
|
return {
|
|
609
636
|
name: 'No root-level flows (deprecated)',
|
|
610
|
-
status: '
|
|
637
|
+
status: 'FAIL',
|
|
611
638
|
message: `${flowFiles.length} flow(s) still in root-level flows/: ${flowFiles.join(', ')}`,
|
|
612
639
|
fixHint: 'Move these flows to domains/{owningDomain}/flows/ and delete the root-level copies.',
|
|
613
640
|
kNodeRef: 'K:convention:flow-vs-subscriber-rule',
|
|
@@ -776,7 +803,7 @@ async function checkAllRoutesUseTanStackRouter(projectRoot) {
|
|
|
776
803
|
}
|
|
777
804
|
/**
|
|
778
805
|
* Check W5-4: Lambda handler files contain initOtel() call.
|
|
779
|
-
|
|
806
|
+
* Scans `domains/*\/actions/*.ts` and `domains/*\/api/*.ts` and FAILs if any handler is missing initOtel().
|
|
780
807
|
*/
|
|
781
808
|
async function checkOtelInitInLambdas(projectRoot) {
|
|
782
809
|
try {
|
|
@@ -794,9 +821,11 @@ async function checkOtelInitInLambdas(projectRoot) {
|
|
|
794
821
|
for (const entry of entries) {
|
|
795
822
|
if (!entry.isDirectory())
|
|
796
823
|
continue;
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
824
|
+
for (const dir of ['actions', 'api']) {
|
|
825
|
+
const handlerDir = join(domainsDir, entry.name, dir);
|
|
826
|
+
const files = findFiles(handlerDir, /\.ts$/);
|
|
827
|
+
handlerFiles.push(...files);
|
|
828
|
+
}
|
|
800
829
|
}
|
|
801
830
|
if (handlerFiles.length === 0) {
|
|
802
831
|
return { name: 'OTel init in Lambdas', status: 'PASS',
|
package/dist/commands/explain.js
CHANGED
|
@@ -31,24 +31,24 @@ const RULE_EXPLANATIONS = {
|
|
|
31
31
|
'no-raw-http-server': {
|
|
32
32
|
ruleId: 'no-raw-http-server',
|
|
33
33
|
title: 'No raw HTTP server in domain code',
|
|
34
|
-
description: 'Domain code must not create raw HTTP servers (express, fastify, etc.). All HTTP handling goes through
|
|
34
|
+
description: 'Domain code must not create raw HTTP servers (express, fastify, etc.). All HTTP handling goes through defineAction with exposure.type=\'api\' which is wired to API Gateway by the scaffold.',
|
|
35
35
|
severity: 'error',
|
|
36
36
|
category: 'structure',
|
|
37
37
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
38
|
-
fixHint: 'Wrap your HTTP handler with
|
|
38
|
+
fixHint: 'Wrap your HTTP handler with defineAction({ exposure: { type: \'api\', ... } }). The scaffold handles API Gateway wiring.',
|
|
39
39
|
exampleBad: "import express from 'express';\nconst app = express();",
|
|
40
|
-
exampleGood: "export const
|
|
40
|
+
exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
|
|
41
41
|
},
|
|
42
42
|
'require-define-primitive': {
|
|
43
43
|
ruleId: 'require-define-primitive',
|
|
44
44
|
title: 'Require define primitive factories',
|
|
45
|
-
description: 'All domain handlers must use the appropriate factory function:
|
|
45
|
+
description: 'All domain handlers must use the appropriate factory function: defineAction (HTTP APIs via exposure.type=\'api\', or internal-only actions), defineSubscriber for event subscribers, defineJob for background jobs, defineWebhook for webhooks, defineEvent for event types.',
|
|
46
46
|
severity: 'error',
|
|
47
47
|
category: 'structure',
|
|
48
48
|
kNodeRef: 'K:runbook:add-domain',
|
|
49
49
|
fixHint: "Wrap your handler with the appropriate define* factory from @mettlecast/domain-runtime.",
|
|
50
50
|
exampleBad: "export const handler = async (event) => ({ statusCode: 200 });",
|
|
51
|
-
exampleGood: "export const
|
|
51
|
+
exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', ... }, ... });",
|
|
52
52
|
},
|
|
53
53
|
'flow-domain-ownership': {
|
|
54
54
|
ruleId: 'flow-domain-ownership',
|
|
@@ -75,24 +75,24 @@ const RULE_EXPLANATIONS = {
|
|
|
75
75
|
'apis-have-versions': {
|
|
76
76
|
ruleId: 'apis-have-versions',
|
|
77
77
|
title: 'APIs declare versions',
|
|
78
|
-
description: 'Every
|
|
78
|
+
description: 'Every API-exposed action (defineAction with exposure.type=\'api\') must declare input and output Zod schemas with .default() so the runtime has a concrete example payload. The action-first contract replaced legacy defineApi\'s versions map (#4689).',
|
|
79
79
|
severity: 'error',
|
|
80
80
|
category: 'correctness',
|
|
81
81
|
kNodeRef: 'K:runbook:add-domain',
|
|
82
|
-
fixHint: 'Add
|
|
83
|
-
exampleBad: "
|
|
84
|
-
exampleGood: "
|
|
82
|
+
fixHint: 'Add Zod input/output schemas to your defineAction call (e.g. z.object({...}).default({...})).',
|
|
83
|
+
exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', ... }, handler: ... }) // missing input/output schemas",
|
|
84
|
+
exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', ... }, input: z.object({...}).default({...}), output: z.object({...}).default({...}), handler: ... });",
|
|
85
85
|
},
|
|
86
86
|
'apis-have-tenancy': {
|
|
87
87
|
ruleId: 'apis-have-tenancy',
|
|
88
88
|
title: 'APIs declare tenancy',
|
|
89
|
-
description: 'Every
|
|
89
|
+
description: 'Every defineAction with exposure.type=\'api\' must declare exposure.tenancy: required (tenant-scoped), none (tenant-agnostic like registration), or system (system-internal admin). The action-first contract enforces this at registration time (#4689).',
|
|
90
90
|
severity: 'error',
|
|
91
91
|
category: 'correctness',
|
|
92
92
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
93
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to your
|
|
94
|
-
exampleBad: "
|
|
95
|
-
exampleGood: "
|
|
93
|
+
fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to your defineAction call.",
|
|
94
|
+
exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action' }, ... }) // missing exposure.tenancy",
|
|
95
|
+
exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
|
|
96
96
|
},
|
|
97
97
|
'no-cross-domain-imports': {
|
|
98
98
|
ruleId: 'no-cross-domain-imports',
|