@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.
Files changed (119) hide show
  1. package/dist/builder/build-registry.d.ts +1 -1
  2. package/dist/builder/build-registry.js +129 -49
  3. package/dist/builder/build-types.d.ts +1 -1
  4. package/dist/builder/load-module.d.ts +1 -1
  5. package/dist/builder/load-module.js +3 -3
  6. package/dist/cli.js +3 -3
  7. package/dist/commands/add-api.js +2 -2
  8. package/dist/commands/add-domain.js +4 -4
  9. package/dist/commands/add-fixture-factory.js +5 -6
  10. package/dist/commands/build-catalog.d.ts +8 -24
  11. package/dist/commands/build-catalog.js +12 -23
  12. package/dist/commands/build-flows.js +1 -1
  13. package/dist/commands/build.js +7 -6
  14. package/dist/commands/check-hashes.js +2 -2
  15. package/dist/commands/create-project.js +1 -1
  16. package/dist/commands/dev.js +1 -1
  17. package/dist/commands/doctor.d.ts +5 -7
  18. package/dist/commands/doctor.js +110 -202
  19. package/dist/commands/explain.js +13 -13
  20. package/dist/commands/generate-openapi.d.ts +10 -1
  21. package/dist/commands/generate-openapi.js +19 -33
  22. package/dist/commands/power-tune.js +2 -2
  23. package/dist/commands/show-dns.d.ts +1 -1
  24. package/dist/commands/show-dns.js +5 -5
  25. package/dist/commands/show.d.ts +2 -3
  26. package/dist/commands/show.js +0 -2
  27. package/dist/commands/test.js +0 -1
  28. package/dist/commands/upgrade-backend.js +3 -3
  29. package/dist/commands/upgrade.js +17 -11
  30. package/dist/commands/validate.js +144 -39
  31. package/dist/server/api-server.d.ts +1 -1
  32. package/dist/server/mount-routes.d.ts +11 -2
  33. package/dist/server/mount-routes.js +20 -8
  34. package/dist/templates/api-skeleton.d.ts +5 -0
  35. package/dist/templates/api-skeleton.js +28 -27
  36. package/dist/templates/claude-md.js +1 -1
  37. package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
  38. package/dist/templates/patterns/api/create-with-event.js +38 -32
  39. package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
  40. package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
  41. package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
  42. package/dist/templates/patterns/api/paginated-list.js +30 -24
  43. package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
  44. package/dist/templates/patterns/api/simple-crud.js +46 -35
  45. package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
  46. package/dist/templates/patterns/api/streaming-list.js +46 -41
  47. package/dist/templates/patterns/api/system-admin.d.ts +4 -0
  48. package/dist/templates/patterns/api/system-admin.js +59 -52
  49. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
  50. package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
  51. package/dist/types.d.ts +100 -0
  52. package/dist/types.js +1 -0
  53. package/dist/utils/file-helpers.d.ts +0 -2
  54. package/dist/utils/file-helpers.js +2 -3
  55. package/dist/utils/header-inject.js +2 -2
  56. package/dist/utils/install-file.d.ts +1 -1
  57. package/dist/utils/install-file.js +1 -1
  58. package/dist/utils/manifest.js +1 -2
  59. package/dist/utils/scaffold-config.d.ts +8 -2
  60. package/dist/utils/scaffold-config.js +3 -1
  61. package/package.json +1 -1
  62. package/src/__tests__/build-registry.test.ts +43 -20
  63. package/src/__tests__/build-types.test.ts +4 -7
  64. package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
  65. package/src/__tests__/commands/add-api.test.ts +12 -10
  66. package/src/__tests__/commands/add-domain.test.ts +8 -5
  67. package/src/__tests__/commands/check-hashes.test.ts +9 -9
  68. package/src/__tests__/commands/create-project.test.ts +5 -5
  69. package/src/__tests__/commands/dev.test.ts +0 -1
  70. package/src/__tests__/commands/upgrade.test.ts +7 -7
  71. package/src/__tests__/doctor.test.ts +60 -67
  72. package/src/__tests__/mount-routes.test.ts +64 -23
  73. package/src/__tests__/package-freshness.test.ts +94 -0
  74. package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
  75. package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
  76. package/src/__tests__/smoke/scaffold.test.ts +13 -15
  77. package/src/__tests__/utils/install-file.test.ts +2 -2
  78. package/src/__tests__/utils/manifest.test.ts +2 -2
  79. package/src/__tests__/validate.test.ts +570 -1
  80. package/src/builder/build-registry.ts +154 -59
  81. package/src/builder/build-types.ts +1 -1
  82. package/src/builder/load-module.ts +3 -3
  83. package/src/cli.ts +4 -4
  84. package/src/commands/add-api.ts +2 -2
  85. package/src/commands/add-domain.ts +4 -4
  86. package/src/commands/add-fixture-factory.ts +5 -6
  87. package/src/commands/build-catalog.ts +18 -40
  88. package/src/commands/build-flows.ts +1 -1
  89. package/src/commands/build.ts +8 -7
  90. package/src/commands/check-hashes.ts +2 -2
  91. package/src/commands/create-project.ts +1 -1
  92. package/src/commands/dev.ts +1 -1
  93. package/src/commands/doctor.ts +120 -218
  94. package/src/commands/explain.ts +13 -13
  95. package/src/commands/generate-openapi.ts +30 -52
  96. package/src/commands/power-tune.ts +2 -2
  97. package/src/commands/show-dns.ts +5 -5
  98. package/src/commands/show.ts +2 -5
  99. package/src/commands/test.ts +0 -1
  100. package/src/commands/upgrade-backend.ts +3 -3
  101. package/src/commands/upgrade.ts +16 -10
  102. package/src/commands/validate.ts +180 -40
  103. package/src/server/api-server.ts +1 -1
  104. package/src/server/mount-routes.ts +21 -10
  105. package/src/templates/api-skeleton.ts +29 -28
  106. package/src/templates/claude-md.ts +1 -1
  107. package/src/templates/patterns/api/create-with-event.ts +39 -33
  108. package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
  109. package/src/templates/patterns/api/paginated-list.ts +31 -25
  110. package/src/templates/patterns/api/simple-crud.ts +47 -36
  111. package/src/templates/patterns/api/streaming-list.ts +47 -42
  112. package/src/templates/patterns/api/system-admin.ts +60 -53
  113. package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
  114. package/src/types.ts +128 -0
  115. package/src/utils/file-helpers.ts +2 -5
  116. package/src/utils/header-inject.ts +2 -2
  117. package/src/utils/install-file.ts +1 -1
  118. package/src/utils/manifest.ts +1 -2
  119. package/src/utils/scaffold-config.ts +12 -3
@@ -1,4 +1,4 @@
1
- import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
1
+ import type { DomainRegistry } from '../types.js';
2
2
  /**
3
3
  * Result of a registry build operation.
4
4
  */
@@ -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(`[tib validate] ${w}\n`);
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 [apiExports, webhookExports, subscriberExports, actionExports, scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
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
- id: String(e['id']),
154
- kind: 'action',
155
- handlerFile: relPath(filePath),
156
- visibility: String(e['visibility']),
157
- idempotent: Boolean(e['idempotent'] ?? false),
158
- description: typeof e['description'] === 'string' ? e['description'] : undefined,
159
- deployment: deployment(e),
160
- outboundAccess: outboundAccess(e),
161
- inputSchema: isJsonSchema(e['input']) ? e['input'] : undefined,
162
- outputSchema: isJsonSchema(e['output']) ? e['output'] : undefined,
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 '@mettlecast/domain-cdk-packer';
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<"api" | "domain" | "webhook" | "subscriber" | "schedule" | "job" | "action" | "integration" | "event">;
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 tibTmpDir = join(process.cwd(), '.tib', 'tmp');
99
- await mkdir(tibTmpDir, { recursive: true }).catch(() => undefined);
100
- const tempPath = join(tibTmpDir, `tib-load-${randomBytes(8).toString('hex')}.mts`);
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 defineApi handlers')
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('--tib-dir <path>', 'Path to the .tib directory (defaults to .tib in cwd)')
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.tibDir);
80
+ await runBuildCatalog(opts.mcDir);
81
81
  });
82
82
  program
83
83
  .command('add-domain <id>')
@@ -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, 'api', `${opts.id}.ts`);
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, 'api', '__tests__');
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, 'api', '__tests__'), { recursive: true });
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, 'api', 'example.ts'), apiSkeletonTemplate(opts.id, 'example', opts.tenancy));
43
- await writeFile(join(domainDir, 'api', '__tests__', 'example.fixture.json'), apiFixtureSkeleton(opts.id, 'example'));
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 api/example.ts, ` +
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, 'api', `${apiId}.ts`);
40
+ const apiFile = join(projectRoot, 'domains', domain, 'actions', `${apiId}.ts`);
41
41
  await access(apiFile).catch(() => {
42
- throw new Error(`API file not found: ${apiFile}. Run add-api first.`);
42
+ throw new Error(`Action file not found: ${apiFile}. Run add-api first.`);
43
43
  });
44
- // Read the API file to extract the input type name
44
+ // Read the action file to extract the input type name
45
45
  const content = await readFile(apiFile, 'utf8');
46
- const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
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, 'api', '__tests__'), { recursive: true });
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
- visibility: string;
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 tibDir - Path to the .tib directory. Defaults to .tib in cwd.
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(tibDir?: string): Promise<DomainCatalog>;
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 tibDir - Path to the .tib directory. Defaults to .tib in cwd.
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(tibDir) {
11
- const dir = tibDir ? resolve(tibDir) : join(process.cwd(), '.tib');
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: .tib directory not found at ${dir}. Run tib build first.`);
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 tib build <domain> first.`);
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
- visibility: String(action['visibility'] ?? 'private'),
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, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length }, 'Domain catalog written');
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, '.tib', 'flows-registry.json');
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();
@@ -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(), '.tib', `${domainId}-registry.json`);
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
- cliLogger.info({ outFile, apis: registry.apis.length, events: registry.events.length }, 'Registry written');
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 tibDir = join(process.cwd(), '.tib');
29
- const registryFiles = (await readdir(tibDir)).filter(f => f.endsWith('-registry.json'));
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(tibDir, f), 'utf8');
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(tibDir, 'actions-types.d.ts');
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, '.tib', 'modules-hashes.json');
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(`tib check-hashes: cannot read ${manifestPath} (${err.message}). Re-scaffold to regenerate the manifest.`);
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, '.tib'), { recursive: true });
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 {
@@ -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, apis: registry.apis.length, port }, 'Starting local dev server');
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
- /** Flag to run relocation of old-layout scaffold files to new layout. */
14
+ /** Deprecated legacy flag retained as a no-op for older automation. */
15
15
  relocate?: boolean;
16
16
  /**
17
- * Auto-remediate before reporting. --fix is a superset of --relocate: it
18
- * first runs the relocation routine (auto-moves owned scaffold files outside
19
- * .mc/ into .mc/ and updates .mc/manifest.json), then continues with the
20
- * full doctor report. On a project that is already clean, --fix is a no-op
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
  /**