@mettlecast/domain-cli 0.2.59 → 0.2.61
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 +129 -49
- package/dist/builder/build-types.d.ts +1 -1
- package/dist/builder/load-module.d.ts +1 -1
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +3 -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 +8 -24
- package/dist/commands/build-catalog.js +12 -23
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +7 -6
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +110 -202
- 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/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- 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/upgrade-backend.js +3 -3
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +144 -39
- 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/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +8 -2
- package/dist/utils/scaffold-config.js +3 -1
- 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/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/create-project.test.ts +5 -5
- package/src/__tests__/commands/dev.test.ts +0 -1
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/mount-routes.test.ts +64 -23
- package/src/__tests__/package-freshness.test.ts +94 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/smoke/scaffold.test.ts +13 -15
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +570 -1
- package/src/builder/build-registry.ts +154 -59
- package/src/builder/build-types.ts +1 -1
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +4 -4
- 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 +18 -40
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +8 -7
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/dev.ts +1 -1
- package/src/commands/doctor.ts +120 -218
- package/src/commands/explain.ts +13 -13
- package/src/commands/generate-openapi.ts +30 -52
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/show.ts +2 -5
- package/src/commands/test.ts +0 -1
- package/src/commands/upgrade-backend.ts +3 -3
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +180 -40
- 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/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +12 -3
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
import { relative, resolve } from 'node:path';
|
|
2
2
|
import { walkDomainDir } from '../utils/file-helpers.js';
|
|
3
3
|
import { loadModuleExports } from './load-module.js';
|
|
4
|
+
/**
|
|
5
|
+
* Coerce a raw value into a valid backendAccess scope, defaulting to
|
|
6
|
+
* 'private' when the value is missing or unrecognized. Used by the
|
|
7
|
+
* builder when reading the new-style `backendAccess` field directly.
|
|
8
|
+
*/
|
|
9
|
+
function toBackendAccess(raw) {
|
|
10
|
+
return raw === 'domain' || raw === 'platform' ? raw : 'private';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Map a legacy `visibility` scope to a `backendAccess` scope for the
|
|
14
|
+
* action-first migration. The legacy `workspace` scope (which previously
|
|
15
|
+
* permitted unauthenticated Function URL exposure) collapses to `domain`
|
|
16
|
+
* so that all cross-domain callers must go through `ctx.actions`.
|
|
17
|
+
*/
|
|
18
|
+
function visibilityToBackendAccess(visibility) {
|
|
19
|
+
if (visibility === 'workspace')
|
|
20
|
+
return 'domain';
|
|
21
|
+
if (visibility === 'domain')
|
|
22
|
+
return 'domain';
|
|
23
|
+
return 'private';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Best-effort reverse mapping from `backendAccess` to the legacy
|
|
27
|
+
* `visibility` field. Used to keep the deprecated field populated so
|
|
28
|
+
* existing CDK constructs that read it continue to behave the same way.
|
|
29
|
+
*
|
|
30
|
+
* - private -> private
|
|
31
|
+
* - domain -> domain
|
|
32
|
+
* - platform -> workspace (closest legacy equivalent for platform-level)
|
|
33
|
+
*/
|
|
34
|
+
function backendAccessToVisibility(backendAccess) {
|
|
35
|
+
if (backendAccess === 'platform')
|
|
36
|
+
return 'workspace';
|
|
37
|
+
return backendAccess;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Type guard + sanitizer for an action's `exposure` field. Returns a
|
|
41
|
+
* well-typed `ActionExposure` (extended with `authDeclared` /
|
|
42
|
+
* `tenancyDeclared` tracking flags) or the provided fallback when the
|
|
43
|
+
* raw value does not match a supported exposure shape.
|
|
44
|
+
*
|
|
45
|
+
* The `authDeclared` / `tenancyDeclared` flags record whether the source
|
|
46
|
+
* code explicitly declared each field, or whether the builder fell back
|
|
47
|
+
* to the safe default. They are validation-only and consumed by the
|
|
48
|
+
* domain CLI's validate command (Wave 6 Task 6.1) to enforce the
|
|
49
|
+
* action-first security model (#4619).
|
|
50
|
+
*/
|
|
51
|
+
function toExposure(raw, fallback) {
|
|
52
|
+
if (!raw || typeof raw !== 'object')
|
|
53
|
+
return fallback;
|
|
54
|
+
const candidate = raw;
|
|
55
|
+
if (candidate.type === 'internal')
|
|
56
|
+
return { type: 'internal' };
|
|
57
|
+
if (candidate.type !== 'api')
|
|
58
|
+
return fallback;
|
|
59
|
+
// Best-effort validation of api exposure fields; any missing required
|
|
60
|
+
// string field falls back to the supplied default exposure.
|
|
61
|
+
const api = raw;
|
|
62
|
+
if (typeof api.path !== 'string' || typeof api.method !== 'string')
|
|
63
|
+
return fallback;
|
|
64
|
+
const authRaw = api.auth;
|
|
65
|
+
const tenancyRaw = api.tenancy;
|
|
66
|
+
const auth = authRaw === 'required' || authRaw === 'none' || authRaw === 'service' ? authRaw : 'required';
|
|
67
|
+
const tenancy = tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system' ? tenancyRaw : 'required';
|
|
68
|
+
const out = {
|
|
69
|
+
type: 'api',
|
|
70
|
+
path: api.path,
|
|
71
|
+
method: api.method,
|
|
72
|
+
auth,
|
|
73
|
+
tenancy,
|
|
74
|
+
authDeclared: authRaw === 'required' || authRaw === 'none' || authRaw === 'service',
|
|
75
|
+
tenancyDeclared: tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system',
|
|
76
|
+
};
|
|
77
|
+
if (Array.isArray(api.roles)) {
|
|
78
|
+
out.roles = api.roles.filter((r) => typeof r === 'string');
|
|
79
|
+
}
|
|
80
|
+
if (api.securityException && typeof api.securityException === 'object') {
|
|
81
|
+
const reason = api.securityException.reason;
|
|
82
|
+
if (typeof reason === 'string') {
|
|
83
|
+
out.securityException = { reason };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
4
88
|
/** Returns true if a value looks like a JSON Schema object (has a 'type' or '$schema' property). */
|
|
5
89
|
function isJsonSchema(v) {
|
|
6
90
|
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
@@ -47,7 +131,7 @@ export async function buildRegistry(domainRoot) {
|
|
|
47
131
|
if (!domainRaw) {
|
|
48
132
|
// Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
|
|
49
133
|
for (const w of warnings)
|
|
50
|
-
process.stderr.write(`[
|
|
134
|
+
process.stderr.write(`[mc-domain-module validate] ${w}\n`);
|
|
51
135
|
throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
|
|
52
136
|
}
|
|
53
137
|
const domain = {
|
|
@@ -57,8 +141,7 @@ export async function buildRegistry(domainRoot) {
|
|
|
57
141
|
tenancy: String(domainRaw['tenancy']),
|
|
58
142
|
defaultDeployment: deployment(domainRaw),
|
|
59
143
|
};
|
|
60
|
-
const [
|
|
61
|
-
Promise.all(paths.apis.map(load)),
|
|
144
|
+
const [webhookExports, subscriberExports, actionExports, scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
|
|
62
145
|
Promise.all(paths.webhooks.map(load)),
|
|
63
146
|
Promise.all(paths.subscribers.map(load)),
|
|
64
147
|
Promise.all(paths.actions.map(load)),
|
|
@@ -67,39 +150,6 @@ export async function buildRegistry(domainRoot) {
|
|
|
67
150
|
Promise.all(paths.integrations.map(load)),
|
|
68
151
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
69
152
|
]);
|
|
70
|
-
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
71
|
-
const apis = paths.apis.flatMap((filePath, i) => (apiExports[i] ?? [])
|
|
72
|
-
.filter(e => e['_kind'] === 'api')
|
|
73
|
-
.map(e => {
|
|
74
|
-
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
75
|
-
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
76
|
-
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
77
|
-
}
|
|
78
|
-
const rawVersions = e['versions'];
|
|
79
|
-
const versionSnapshots = rawVersions
|
|
80
|
-
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
81
|
-
version: ver,
|
|
82
|
-
requestSchema: isJsonSchema(v?.input) ? v.input : undefined,
|
|
83
|
-
responseSchema: isJsonSchema(v?.output) ? v.output : undefined,
|
|
84
|
-
}))
|
|
85
|
-
: [];
|
|
86
|
-
const latestVersion = versionSnapshots[versionSnapshots.length - 1];
|
|
87
|
-
return {
|
|
88
|
-
id: String(e['id']),
|
|
89
|
-
kind: 'api',
|
|
90
|
-
handlerFile: relPath(filePath),
|
|
91
|
-
path: String(e['path']),
|
|
92
|
-
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
93
|
-
authType: e['auth']?.type ?? 'jwt',
|
|
94
|
-
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
95
|
-
deployment: deployment(e),
|
|
96
|
-
outboundAccess: outboundAccess(e),
|
|
97
|
-
requestSchema: latestVersion?.requestSchema,
|
|
98
|
-
responseSchema: latestVersion?.responseSchema,
|
|
99
|
-
versions: versionSnapshots.length > 0 ? versionSnapshots : undefined,
|
|
100
|
-
examples: e['examples'],
|
|
101
|
-
};
|
|
102
|
-
}));
|
|
103
153
|
const webhooks = paths.webhooks.flatMap((filePath, i) => (webhookExports[i] ?? [])
|
|
104
154
|
.filter(e => e['_kind'] === 'webhook')
|
|
105
155
|
.map(e => ({
|
|
@@ -149,18 +199,49 @@ export async function buildRegistry(domainRoot) {
|
|
|
149
199
|
})));
|
|
150
200
|
const actions = paths.actions.flatMap((filePath, i) => (actionExports[i] ?? [])
|
|
151
201
|
.filter(e => e['_kind'] === 'action')
|
|
152
|
-
.map(e =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
202
|
+
.map(e => {
|
|
203
|
+
// Resolve backendAccess. New-style actions carry `backendAccess`
|
|
204
|
+
// directly. Legacy actions only carry `visibility`; in that case
|
|
205
|
+
// we collapse workspace -> domain per the migration spec, and we
|
|
206
|
+
// also remember the legacy flag so we can default exposure to
|
|
207
|
+
// `{ type: 'internal' }` for actions that have not opted in yet.
|
|
208
|
+
const hasBackendAccess = 'backendAccess' in e;
|
|
209
|
+
const rawBackendAccess = e['backendAccess'];
|
|
210
|
+
const rawVisibility = e['visibility'];
|
|
211
|
+
const backendAccess = hasBackendAccess
|
|
212
|
+
? toBackendAccess(rawBackendAccess)
|
|
213
|
+
: visibilityToBackendAccess(rawVisibility);
|
|
214
|
+
const legacyVisibility = backendAccessToVisibility(backendAccess);
|
|
215
|
+
// Resolve exposure. New-style actions must declare their exposure;
|
|
216
|
+
// legacy actions that did not opt in default to `{ type: 'internal' }`
|
|
217
|
+
// so existing internal-only behavior is preserved during migration.
|
|
218
|
+
const rawExposure = e['exposure'];
|
|
219
|
+
const exposure = toExposure(rawExposure, { type: 'internal' });
|
|
220
|
+
// Record whether the source explicitly declared the `exposure` field.
|
|
221
|
+
// Validation-only; consumed by the domain CLI's validate command
|
|
222
|
+
// (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
|
|
223
|
+
const exposureDeclared = rawExposure !== undefined && rawExposure !== null
|
|
224
|
+
&& typeof rawExposure === 'object';
|
|
225
|
+
return {
|
|
226
|
+
id: String(e['id']),
|
|
227
|
+
kind: 'action',
|
|
228
|
+
handlerFile: relPath(filePath),
|
|
229
|
+
backendAccess,
|
|
230
|
+
exposure,
|
|
231
|
+
exposureDeclared,
|
|
232
|
+
// Keep the legacy field populated so CDK constructs that still
|
|
233
|
+
// read `visibility` (e.g. action-construct.ts) keep working
|
|
234
|
+
// through the migration window. New constructs should read
|
|
235
|
+
// `backendAccess` and `exposure` instead.
|
|
236
|
+
visibility: legacyVisibility,
|
|
237
|
+
idempotent: Boolean(e['idempotent'] ?? false),
|
|
238
|
+
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
239
|
+
deployment: deployment(e),
|
|
240
|
+
outboundAccess: outboundAccess(e),
|
|
241
|
+
inputSchema: isJsonSchema(e['input']) ? e['input'] : undefined,
|
|
242
|
+
outputSchema: isJsonSchema(e['output']) ? e['output'] : undefined,
|
|
243
|
+
};
|
|
244
|
+
}));
|
|
164
245
|
const integrations = paths.integrations.flatMap((_filePath, i) => (integrationExports[i] ?? [])
|
|
165
246
|
.filter(e => e['_kind'] === 'integration')
|
|
166
247
|
.map(e => ({
|
|
@@ -183,7 +264,6 @@ export async function buildRegistry(domainRoot) {
|
|
|
183
264
|
schemaVersion: '1',
|
|
184
265
|
domainRoot,
|
|
185
266
|
domain,
|
|
186
|
-
apis,
|
|
187
267
|
webhooks,
|
|
188
268
|
subscribers,
|
|
189
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
|
|
@@ -95,9 +95,9 @@ process.stdout.write(JSON.stringify(results));
|
|
|
95
95
|
export async function loadModuleExports(absoluteFilePath) {
|
|
96
96
|
// Use a subdir of the project root rather than OS tmpdir so that ESM import
|
|
97
97
|
// resolution can walk up and find node_modules packages like zod-to-json-schema.
|
|
98
|
-
const
|
|
99
|
-
await mkdir(
|
|
100
|
-
const tempPath = join(
|
|
98
|
+
const mcTmpDir = join(process.cwd(), '.mc', 'tmp');
|
|
99
|
+
await mkdir(mcTmpDir, { recursive: true }).catch(() => undefined);
|
|
100
|
+
const tempPath = join(mcTmpDir, `mc-load-${randomBytes(8).toString('hex')}.mts`);
|
|
101
101
|
await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
|
|
102
102
|
try {
|
|
103
103
|
return await new Promise((resolve, reject) => {
|
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ program
|
|
|
67
67
|
});
|
|
68
68
|
program
|
|
69
69
|
.command('dev <domain>')
|
|
70
|
-
.description('Start a local HTTP server simulating API Gateway for all
|
|
70
|
+
.description('Start a local HTTP server simulating API Gateway for all API-exposed defineAction handlers')
|
|
71
71
|
.option('--port <n>', 'Port to listen on', '3000')
|
|
72
72
|
.action(async (domain, opts) => {
|
|
73
73
|
await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
|
|
@@ -75,9 +75,9 @@ program
|
|
|
75
75
|
program
|
|
76
76
|
.command('build-catalog')
|
|
77
77
|
.description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
|
|
78
|
-
.option('--
|
|
78
|
+
.option('--mc-dir <path>', 'Path to the .mc registry directory (defaults to .mc in cwd)')
|
|
79
79
|
.action(async (opts) => {
|
|
80
|
-
await runBuildCatalog(opts.
|
|
80
|
+
await runBuildCatalog(opts.mcDir);
|
|
81
81
|
});
|
|
82
82
|
program
|
|
83
83
|
.command('add-domain <id>')
|
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[];
|
|
@@ -92,7 +76,7 @@ export interface DomainCatalog {
|
|
|
92
76
|
/**
|
|
93
77
|
* Build the combined domain catalog from all per-domain registry files.
|
|
94
78
|
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
95
|
-
* @param
|
|
79
|
+
* @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
|
|
96
80
|
* @returns The written catalog.
|
|
97
81
|
*/
|
|
98
|
-
export declare function runBuildCatalog(
|
|
82
|
+
export declare function runBuildCatalog(registryDir?: string): Promise<DomainCatalog>;
|
|
@@ -4,28 +4,27 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Build the combined domain catalog from all per-domain registry files.
|
|
6
6
|
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
7
|
-
* @param
|
|
7
|
+
* @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
|
|
8
8
|
* @returns The written catalog.
|
|
9
9
|
*/
|
|
10
|
-
export async function runBuildCatalog(
|
|
11
|
-
const dir =
|
|
10
|
+
export async function runBuildCatalog(registryDir) {
|
|
11
|
+
const dir = registryDir ? resolve(registryDir) : join(process.cwd(), '.mc');
|
|
12
12
|
// Find all per-domain registry files
|
|
13
13
|
let files;
|
|
14
14
|
try {
|
|
15
15
|
files = await readdir(dir);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
|
-
throw new Error(`build-catalog: .
|
|
18
|
+
throw new Error(`build-catalog: .mc registry directory not found at ${dir}. Run mc-domain-module build first.`);
|
|
19
19
|
}
|
|
20
20
|
const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
|
|
21
21
|
if (registryFiles.length === 0) {
|
|
22
|
-
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run
|
|
22
|
+
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run mc-domain-module build <domain> first.`);
|
|
23
23
|
}
|
|
24
24
|
const catalog = {
|
|
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(tibDir) {
|
|
|
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(tibDir) {
|
|
|
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
|
}
|
|
@@ -253,7 +253,7 @@ async function loadFlowsFromDir(dir, owningDomainOverride) {
|
|
|
253
253
|
*/
|
|
254
254
|
export async function runBuildFlows(options) {
|
|
255
255
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
256
|
-
const outFile = options.outFile ?? join(projectRoot, '.
|
|
256
|
+
const outFile = options.outFile ?? join(projectRoot, '.mc', 'flows-registry.json');
|
|
257
257
|
cliLogger.info({ projectRoot }, 'Building flows registry');
|
|
258
258
|
const allEntries = [];
|
|
259
259
|
const seenIds = new Set();
|
package/dist/commands/build.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function runBuild(options) {
|
|
|
15
15
|
const domainId = basename(domainRoot);
|
|
16
16
|
const outFile = options.outFile
|
|
17
17
|
? resolve(options.outFile)
|
|
18
|
-
: join(process.cwd(), '.
|
|
18
|
+
: join(process.cwd(), '.mc', `${domainId}-registry.json`);
|
|
19
19
|
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
20
20
|
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
21
21
|
for (const w of warnings) {
|
|
@@ -23,17 +23,18 @@ 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
|
-
const
|
|
29
|
-
const registryFiles = (await readdir(
|
|
29
|
+
const registryDir = join(outFile, '..');
|
|
30
|
+
const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
|
|
30
31
|
const allRegistries = [];
|
|
31
32
|
for (const f of registryFiles) {
|
|
32
|
-
const raw = await readFile(join(
|
|
33
|
+
const raw = await readFile(join(registryDir, f), 'utf8');
|
|
33
34
|
allRegistries.push(JSON.parse(raw));
|
|
34
35
|
}
|
|
35
36
|
const typesContent = buildActionsTypes(allRegistries);
|
|
36
|
-
const typesFile = join(
|
|
37
|
+
const typesFile = join(registryDir, 'actions-types.d.ts');
|
|
37
38
|
await writeFile(typesFile, typesContent);
|
|
38
39
|
cliLogger.info({ outFile: typesFile }, 'Types file written');
|
|
39
40
|
return outFile;
|
|
@@ -10,14 +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
|
-
const manifestPath = join(root, '.
|
|
13
|
+
const manifestPath = join(root, '.mc', 'modules-hashes.json');
|
|
14
14
|
const modulesDir = join(root, 'infra', 'modules');
|
|
15
15
|
let manifest;
|
|
16
16
|
try {
|
|
17
17
|
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
|
-
throw new Error(`
|
|
20
|
+
throw new Error(`mc-domain-module check-hashes: cannot read ${manifestPath} (${err.message}). Re-scaffold to regenerate the manifest.`);
|
|
21
21
|
}
|
|
22
22
|
const drifted = [];
|
|
23
23
|
const missing = [];
|
|
@@ -160,7 +160,7 @@ export async function runCreateProject(opts) {
|
|
|
160
160
|
const orderedModules = topoSort(modulesJson.modules, selectedIds);
|
|
161
161
|
// 5. Prepare output directory
|
|
162
162
|
await mkdir(outputDir, { recursive: true });
|
|
163
|
-
await mkdir(join(outputDir, '.
|
|
163
|
+
await mkdir(join(outputDir, '.mc'), { recursive: true });
|
|
164
164
|
// 6. Get CLI version from package.json
|
|
165
165
|
let cliVersion = '0.0.0';
|
|
166
166
|
try {
|
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');
|
|
@@ -11,15 +11,13 @@ export declare const DOCTOR_FIX_FLAG = "--fix";
|
|
|
11
11
|
export interface DoctorOptions {
|
|
12
12
|
/** Root directory of the project (defaults to cwd). */
|
|
13
13
|
projectRoot?: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Deprecated legacy flag retained as a no-op for older automation. */
|
|
15
15
|
relocate?: boolean;
|
|
16
16
|
/**
|
|
17
|
-
* Auto-remediate before reporting.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (0 files moved, exit 0). Does NOT auto-generate missing fixtures
|
|
22
|
-
* (deferred).
|
|
17
|
+
* Auto-remediate before reporting. The legacy relocation routine is now a
|
|
18
|
+
* no-op because current scaffold manifests intentionally track managed files
|
|
19
|
+
* in their installed project locations (infra/modules, domains, .github, etc.).
|
|
20
|
+
* Does NOT auto-generate missing fixtures (deferred).
|
|
23
21
|
*/
|
|
24
22
|
fix?: boolean;
|
|
25
23
|
/**
|