@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.
Files changed (107) hide show
  1. package/dist/builder/build-registry.d.ts +1 -1
  2. package/dist/builder/build-registry.js +1 -36
  3. package/dist/builder/build-types.d.ts +1 -1
  4. package/dist/builder/load-module.d.ts +1 -1
  5. package/dist/cli.js +21 -3
  6. package/dist/commands/add-api.js +2 -2
  7. package/dist/commands/add-domain.js +4 -4
  8. package/dist/commands/add-fixture-factory.js +5 -6
  9. package/dist/commands/build-catalog.d.ts +6 -22
  10. package/dist/commands/build-catalog.js +7 -18
  11. package/dist/commands/build.js +2 -1
  12. package/dist/commands/check-hashes.d.ts +2 -0
  13. package/dist/commands/check-hashes.js +8 -0
  14. package/dist/commands/dev.js +1 -1
  15. package/dist/commands/doctor.js +70 -41
  16. package/dist/commands/explain.js +13 -13
  17. package/dist/commands/generate-openapi.d.ts +10 -1
  18. package/dist/commands/generate-openapi.js +19 -33
  19. package/dist/commands/regenerate-modules-hashes.d.ts +25 -0
  20. package/dist/commands/regenerate-modules-hashes.js +58 -0
  21. package/dist/commands/show.d.ts +2 -3
  22. package/dist/commands/show.js +0 -2
  23. package/dist/commands/test.js +0 -1
  24. package/dist/commands/update-all.d.ts +21 -0
  25. package/dist/commands/update-all.js +62 -0
  26. package/dist/commands/upgrade-backend.js +3 -3
  27. package/dist/commands/validate.js +12 -90
  28. package/dist/server/api-server.d.ts +1 -1
  29. package/dist/server/mount-routes.d.ts +11 -2
  30. package/dist/server/mount-routes.js +20 -8
  31. package/dist/templates/api-skeleton.d.ts +5 -0
  32. package/dist/templates/api-skeleton.js +28 -27
  33. package/dist/templates/claude-md.js +1 -1
  34. package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
  35. package/dist/templates/patterns/api/create-with-event.js +38 -32
  36. package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
  37. package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
  38. package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
  39. package/dist/templates/patterns/api/paginated-list.js +30 -24
  40. package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
  41. package/dist/templates/patterns/api/simple-crud.js +46 -35
  42. package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
  43. package/dist/templates/patterns/api/streaming-list.js +46 -41
  44. package/dist/templates/patterns/api/system-admin.d.ts +4 -0
  45. package/dist/templates/patterns/api/system-admin.js +59 -52
  46. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
  47. package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
  48. package/dist/types.d.ts +100 -0
  49. package/dist/types.js +1 -0
  50. package/dist/utils/file-helpers.d.ts +0 -2
  51. package/dist/utils/file-helpers.js +2 -3
  52. package/dist/utils/manifest.d.ts +1 -0
  53. package/dist/utils/manifest.js +16 -1
  54. package/dist/utils/scaffold-config.d.ts +6 -0
  55. package/dist/utils/scaffold-config.js +2 -0
  56. package/package.json +1 -1
  57. package/src/__tests__/build-registry.test.ts +43 -20
  58. package/src/__tests__/build-types.test.ts +4 -7
  59. package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
  60. package/src/__tests__/commands/add-api.test.ts +12 -10
  61. package/src/__tests__/commands/add-domain.test.ts +8 -5
  62. package/src/__tests__/commands/build-flows.test.ts +55 -0
  63. package/src/__tests__/commands/check-hashes.test.ts +31 -0
  64. package/src/__tests__/commands/create-project.test.ts +5 -5
  65. package/src/__tests__/commands/dev.test.ts +0 -1
  66. package/src/__tests__/commands/regenerate-modules-hashes.test.ts +170 -0
  67. package/src/__tests__/commands/update-all.test.ts +322 -0
  68. package/src/__tests__/doctor.test.ts +73 -0
  69. package/src/__tests__/mount-routes.test.ts +64 -23
  70. package/src/__tests__/package-freshness.test.ts +1 -21
  71. package/src/__tests__/smoke/scaffold.test.ts +13 -15
  72. package/src/__tests__/utils/manifest.test.ts +128 -0
  73. package/src/__tests__/validate.test.ts +21 -103
  74. package/src/builder/build-registry.ts +7 -44
  75. package/src/builder/build-types.ts +1 -1
  76. package/src/cli.ts +23 -3
  77. package/src/commands/add-api.ts +2 -2
  78. package/src/commands/add-domain.ts +4 -4
  79. package/src/commands/add-fixture-factory.ts +5 -6
  80. package/src/commands/build-catalog.ts +13 -35
  81. package/src/commands/build.ts +3 -2
  82. package/src/commands/check-hashes.ts +12 -0
  83. package/src/commands/dev.ts +1 -1
  84. package/src/commands/doctor.ts +72 -41
  85. package/src/commands/explain.ts +13 -13
  86. package/src/commands/generate-openapi.ts +30 -52
  87. package/src/commands/regenerate-modules-hashes.ts +89 -0
  88. package/src/commands/show.ts +2 -5
  89. package/src/commands/test.ts +0 -1
  90. package/src/commands/update-all.ts +79 -0
  91. package/src/commands/upgrade-backend.ts +3 -3
  92. package/src/commands/validate.ts +11 -96
  93. package/src/server/api-server.ts +1 -1
  94. package/src/server/mount-routes.ts +21 -10
  95. package/src/templates/api-skeleton.ts +29 -28
  96. package/src/templates/claude-md.ts +1 -1
  97. package/src/templates/patterns/api/create-with-event.ts +39 -33
  98. package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
  99. package/src/templates/patterns/api/paginated-list.ts +31 -25
  100. package/src/templates/patterns/api/simple-crud.ts +47 -36
  101. package/src/templates/patterns/api/streaming-list.ts +47 -42
  102. package/src/templates/patterns/api/system-admin.ts +60 -53
  103. package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
  104. package/src/types.ts +128 -0
  105. package/src/utils/file-helpers.ts +2 -5
  106. package/src/utils/manifest.ts +16 -1
  107. package/src/utils/scaffold-config.ts +9 -0
@@ -24,7 +24,6 @@ describe('buildRegistry', () => {
24
24
  it('throws if domain.config.ts is missing', async () => {
25
25
  mockWalkDomainDir.mockResolvedValue({
26
26
  domain: undefined,
27
- apis: [],
28
27
  webhooks: [],
29
28
  subscribers: [],
30
29
  actions: [],
@@ -40,7 +39,6 @@ describe('buildRegistry', () => {
40
39
  it('throws if domain.config.ts has no domain export', async () => {
41
40
  mockWalkDomainDir.mockResolvedValue({
42
41
  domain: '/fake/domains/billing/domain.config.ts',
43
- apis: [],
44
42
  webhooks: [],
45
43
  subscribers: [],
46
44
  actions: [],
@@ -54,13 +52,15 @@ describe('buildRegistry', () => {
54
52
  await expect(buildRegistry(DOMAIN_ROOT)).rejects.toThrow("no 'domain' export found");
55
53
  });
56
54
 
57
- it('builds a registry with domain + one api', async () => {
55
+ it('builds a registry with domain + one API-exposed action', async () => {
56
+ // Issue #4689: defineApi was removed; the canonical HTTP endpoint
57
+ // surface is `defineAction({ exposure: { type: 'api', ... } })`.
58
+ // The action is recorded under `registry.actions` with API exposure metadata.
58
59
  mockWalkDomainDir.mockResolvedValue({
59
60
  domain: '/fake/domains/billing/domain.config.ts',
60
- apis: ['/fake/domains/billing/api/get-invoice.ts'],
61
61
  webhooks: [],
62
62
  subscribers: [],
63
- actions: [],
63
+ actions: ['/fake/domains/billing/actions/get-invoice.ts'],
64
64
  schedules: [],
65
65
  jobs: [],
66
66
  integrations: [],
@@ -72,7 +72,20 @@ describe('buildRegistry', () => {
72
72
  { _kind: 'domain', _exportName: 'default', id: 'billing', name: 'Billing', tenancy: 'required' },
73
73
  ])
74
74
  .mockResolvedValueOnce([
75
- { _kind: 'api', _exportName: 'getInvoice', id: 'get-invoice', path: '/invoices/:id', method: 'GET', auth: { type: 'jwt' } },
75
+ {
76
+ _kind: 'action',
77
+ _exportName: 'getInvoice',
78
+ id: 'get-invoice',
79
+ backendAccess: 'domain',
80
+ exposure: {
81
+ type: 'api',
82
+ path: '/v1/tenants/{tenantId}/billing/invoices/{id}',
83
+ method: 'GET',
84
+ auth: 'required',
85
+ tenancy: 'required',
86
+ },
87
+ idempotent: true,
88
+ },
76
89
  ]);
77
90
 
78
91
  const { registry, warnings } = await buildRegistry(DOMAIN_ROOT);
@@ -80,21 +93,18 @@ describe('buildRegistry', () => {
80
93
  expect(warnings).toHaveLength(0);
81
94
  expect(registry.schemaVersion).toBe('1');
82
95
  expect(registry.domain.id).toBe('billing');
83
- expect(registry.apis).toHaveLength(1);
84
- expect(registry.apis[0]!.id).toBe('get-invoice');
85
- expect(registry.apis[0]!.path).toBe('/invoices/:id');
86
- expect(registry.apis[0]!.authType).toBe('jwt');
87
- expect(registry.apis[0]!.handlerFile.replaceAll('\\', '/')).toBe('api/get-invoice.ts');
88
- expect(registry.apis[0]!.outboundAccess).toBe('internal');
96
+ expect(registry.actions).toHaveLength(1);
97
+ expect(registry.actions[0]!.id).toBe('get-invoice');
98
+ expect(registry.actions[0]!.backendAccess).toBe('domain');
99
+ expect(registry.actions[0]!.exposure.type).toBe('api');
89
100
  });
90
101
 
91
- it('preserves explicit outboundAccess on primitives', async () => {
102
+ it('preserves explicit outboundAccess on actions', async () => {
92
103
  mockWalkDomainDir.mockResolvedValue({
93
104
  domain: '/fake/domains/billing/domain.config.ts',
94
- apis: ['/fake/domains/billing/api/sync-invoice.ts'],
95
105
  webhooks: [],
96
106
  subscribers: [],
97
- actions: [],
107
+ actions: ['/fake/domains/billing/actions/sync-invoice.ts'],
98
108
  schedules: [],
99
109
  jobs: [],
100
110
  integrations: [],
@@ -106,21 +116,34 @@ describe('buildRegistry', () => {
106
116
  { _kind: 'domain', _exportName: 'default', id: 'billing', name: 'Billing', tenancy: 'required' },
107
117
  ])
108
118
  .mockResolvedValueOnce([
109
- { _kind: 'api', _exportName: 'syncInvoice', id: 'sync-invoice', path: '/invoices/sync', method: 'POST', auth: { type: 'jwt' }, outboundAccess: 'internet' },
119
+ {
120
+ _kind: 'action',
121
+ _exportName: 'syncInvoice',
122
+ id: 'sync-invoice',
123
+ backendAccess: 'domain',
124
+ exposure: {
125
+ type: 'api',
126
+ path: '/v1/tenants/{tenantId}/billing/invoices/sync',
127
+ method: 'POST',
128
+ auth: 'required',
129
+ tenancy: 'required',
130
+ },
131
+ outboundAccess: 'internet',
132
+ idempotent: false,
133
+ },
110
134
  ]);
111
135
 
112
136
  const { registry } = await buildRegistry(DOMAIN_ROOT);
113
137
 
114
- expect(registry.apis[0]!.outboundAccess).toBe('internet');
138
+ expect(registry.actions[0]!.outboundAccess).toBe('internet');
115
139
  });
116
140
 
117
141
  it('collects warnings when a file fails to load', async () => {
118
142
  mockWalkDomainDir.mockResolvedValue({
119
143
  domain: '/fake/domains/billing/domain.config.ts',
120
- apis: ['/fake/domains/billing/api/broken.ts'],
121
144
  webhooks: [],
122
145
  subscribers: [],
123
- actions: [],
146
+ actions: ['/fake/domains/billing/actions/broken.ts'],
124
147
  schedules: [],
125
148
  jobs: [],
126
149
  integrations: [],
@@ -137,6 +160,6 @@ describe('buildRegistry', () => {
137
160
 
138
161
  expect(warnings).toHaveLength(1);
139
162
  expect(warnings[0]).toContain('tsx error');
140
- expect(registry.apis).toHaveLength(0);
163
+ expect(registry.actions).toHaveLength(0);
141
164
  });
142
165
  });
@@ -8,7 +8,6 @@ describe('buildActionsTypes', () => {
8
8
  schemaVersion: '1',
9
9
  domainRoot: '/test/auth',
10
10
  domain: { id: 'auth', kind: 'domain', name: 'Auth', tenancy: 'required' },
11
- apis: [],
12
11
  webhooks: [],
13
12
  subscribers: [],
14
13
  schedules: [],
@@ -18,7 +17,8 @@ describe('buildActionsTypes', () => {
18
17
  id: 'verify-token',
19
18
  kind: 'action',
20
19
  handlerFile: 'verify-token.ts',
21
- visibility: 'domain',
20
+ backendAccess: 'domain',
21
+ exposure: { type: 'internal' },
22
22
  idempotent: false,
23
23
  },
24
24
  ],
@@ -39,13 +39,12 @@ describe('buildActionsTypes', () => {
39
39
  schemaVersion: '1',
40
40
  domainRoot: '/test/auth',
41
41
  domain: { id: 'auth', kind: 'domain', name: 'Auth', tenancy: 'required' },
42
- apis: [],
43
42
  webhooks: [],
44
43
  subscribers: [],
45
44
  schedules: [],
46
45
  jobs: [],
47
46
  actions: [
48
- { id: 'verify', kind: 'action', handlerFile: 'verify.ts', visibility: 'workspace', idempotent: true },
47
+ { id: 'verify', kind: 'action', handlerFile: 'verify.ts', backendAccess: 'platform', exposure: { type: 'internal' }, idempotent: true },
49
48
  ],
50
49
  integrations: [],
51
50
  events: [],
@@ -55,13 +54,12 @@ describe('buildActionsTypes', () => {
55
54
  schemaVersion: '1',
56
55
  domainRoot: '/test/payments',
57
56
  domain: { id: 'payments', kind: 'domain', name: 'Payments', tenancy: 'required' },
58
- apis: [],
59
57
  webhooks: [],
60
58
  subscribers: [],
61
59
  schedules: [],
62
60
  jobs: [],
63
61
  actions: [
64
- { id: 'charge', kind: 'action', handlerFile: 'charge.ts', visibility: 'domain', idempotent: false },
62
+ { id: 'charge', kind: 'action', handlerFile: 'charge.ts', backendAccess: 'domain', exposure: { type: 'internal' }, idempotent: false },
65
63
  ],
66
64
  integrations: [],
67
65
  events: [],
@@ -78,7 +76,6 @@ describe('buildActionsTypes', () => {
78
76
  schemaVersion: '1',
79
77
  domainRoot: '/test',
80
78
  domain: { id: 'test', kind: 'domain', name: 'Test', tenancy: 'none' },
81
- apis: [],
82
79
  webhooks: [],
83
80
  subscribers: [],
84
81
  schedules: [],
@@ -19,59 +19,59 @@ describe('walkDomainDir recursive walk', () => {
19
19
  }
20
20
  });
21
21
 
22
- it('should find .ts files at root of api directory', async () => {
23
- const apiDir = join(tmpDir, 'api');
24
- await mkdir(apiDir, { recursive: true });
25
- await writeFile(join(apiDir, 'simple.ts'), 'export const api = {}');
22
+ it('should find .ts files at root of actions directory', async () => {
23
+ const actionDir = join(tmpDir, 'actions');
24
+ await mkdir(actionDir, { recursive: true });
25
+ await writeFile(join(actionDir, 'simple.ts'), 'export const action = {}');
26
26
 
27
27
  const paths = await walkDomainDir(tmpDir);
28
28
 
29
- expect(paths.apis).toContain(join(apiDir, 'simple.ts'));
29
+ expect(paths.actions).toContain(join(actionDir, 'simple.ts'));
30
30
  });
31
31
 
32
- it('should recursively find .ts files in nested api directories', async () => {
33
- const nestedDir = join(tmpDir, 'api', 'users', 'v1');
32
+ it('should recursively find .ts files in nested action directories', async () => {
33
+ const nestedDir = join(tmpDir, 'actions', 'users', 'v1');
34
34
  await mkdir(nestedDir, { recursive: true });
35
- await writeFile(join(nestedDir, 'get.ts'), 'export const api = {}');
35
+ await writeFile(join(nestedDir, 'get.ts'), 'export const action = {}');
36
36
 
37
37
  const paths = await walkDomainDir(tmpDir);
38
38
 
39
- expect(paths.apis).toContain(join(nestedDir, 'get.ts'));
39
+ expect(paths.actions).toContain(join(nestedDir, 'get.ts'));
40
40
  });
41
41
 
42
42
  it('should skip __tests__ directories', async () => {
43
- const testDir = join(tmpDir, 'api', '__tests__');
43
+ const testDir = join(tmpDir, 'actions', '__tests__');
44
44
  await mkdir(testDir, { recursive: true });
45
45
  await writeFile(join(testDir, 'test.ts'), 'export const test = {}');
46
46
 
47
47
  const paths = await walkDomainDir(tmpDir);
48
48
 
49
- expect(paths.apis).not.toContain(join(testDir, 'test.ts'));
49
+ expect(paths.actions).not.toContain(join(testDir, 'test.ts'));
50
50
  });
51
51
 
52
52
  it('should skip dist directories', async () => {
53
- const distDir = join(tmpDir, 'api', 'dist');
53
+ const distDir = join(tmpDir, 'actions', 'dist');
54
54
  await mkdir(distDir, { recursive: true });
55
55
  await writeFile(join(distDir, 'bundle.ts'), 'export const bundle = {}');
56
56
 
57
57
  const paths = await walkDomainDir(tmpDir);
58
58
 
59
- expect(paths.apis).not.toContain(join(distDir, 'bundle.ts'));
59
+ expect(paths.actions).not.toContain(join(distDir, 'bundle.ts'));
60
60
  });
61
61
 
62
62
  it('should skip node_modules directories', async () => {
63
- const nodeModulesDir = join(tmpDir, 'api', 'node_modules');
63
+ const nodeModulesDir = join(tmpDir, 'actions', 'node_modules');
64
64
  await mkdir(nodeModulesDir, { recursive: true });
65
65
  await writeFile(join(nodeModulesDir, 'dep.ts'), 'export const dep = {}');
66
66
 
67
67
  const paths = await walkDomainDir(tmpDir);
68
68
 
69
- expect(paths.apis).not.toContain(join(nodeModulesDir, 'dep.ts'));
69
+ expect(paths.actions).not.toContain(join(nodeModulesDir, 'dep.ts'));
70
70
  });
71
71
 
72
72
  it('should discover files in all domain primitive directories', async () => {
73
73
  // Create files in all conventional directories
74
- const dirs = ['api', 'webhooks', 'subscribers', 'actions', 'schedules', 'jobs', 'integrations'];
74
+ const dirs = ['webhooks', 'subscribers', 'actions', 'schedules', 'jobs', 'integrations'];
75
75
 
76
76
  for (const dir of dirs) {
77
77
  const dirPath = join(tmpDir, dir, 'nested', 'deep');
@@ -82,7 +82,6 @@ describe('walkDomainDir recursive walk', () => {
82
82
  const paths = await walkDomainDir(tmpDir);
83
83
 
84
84
  // Verify files are found in correct slots
85
- expect(paths.apis.some(p => p.includes('api-handler.ts'))).toBe(true);
86
85
  expect(paths.webhooks.some(p => p.includes('webhooks-handler.ts'))).toBe(true);
87
86
  expect(paths.subscribers.some(p => p.includes('subscribers-handler.ts'))).toBe(true);
88
87
  expect(paths.actions.some(p => p.includes('actions-handler.ts'))).toBe(true);
@@ -112,7 +111,6 @@ describe('walkDomainDir recursive walk', () => {
112
111
  it('should return empty arrays for missing directories', async () => {
113
112
  const paths = await walkDomainDir(tmpDir);
114
113
 
115
- expect(paths.apis).toEqual([]);
116
114
  expect(paths.webhooks).toEqual([]);
117
115
  expect(paths.subscribers).toEqual([]);
118
116
  expect(paths.actions).toEqual([]);
@@ -129,12 +127,12 @@ describe('walkDomainDir recursive walk', () => {
129
127
  });
130
128
 
131
129
  it('should handle deeply nested files', async () => {
132
- const deepPath = join(tmpDir, 'api', 'v1', 'users', 'admin', 'permissions');
130
+ const deepPath = join(tmpDir, 'actions', 'v1', 'users', 'admin', 'permissions');
133
131
  await mkdir(deepPath, { recursive: true });
134
- await writeFile(join(deepPath, 'check.ts'), 'export const api = {}');
132
+ await writeFile(join(deepPath, 'check.ts'), 'export const action = {}');
135
133
 
136
134
  const paths = await walkDomainDir(tmpDir);
137
135
 
138
- expect(paths.apis.some(p => p.includes('check.ts'))).toBe(true);
136
+ expect(paths.actions.some(p => p.includes('check.ts'))).toBe(true);
139
137
  });
140
138
  });
@@ -11,7 +11,7 @@ describe('add-api command', () => {
11
11
  beforeEach(async () => {
12
12
  tmpDir = await mkdtemp(join(tmpdir(), 'tib-add-api-test-'));
13
13
  domainDir = join(tmpDir, 'demo');
14
- await mkdir(join(domainDir, 'api'), { recursive: true });
14
+ await mkdir(join(domainDir, 'actions'), { recursive: true });
15
15
  await writeFile(
16
16
  join(domainDir, 'domain.config.ts'),
17
17
  `export default { id: 'demo', tenancy: 'required' };\n`,
@@ -34,8 +34,8 @@ describe('add-api command', () => {
34
34
  domainsDir: tmpDir,
35
35
  });
36
36
 
37
- const apiFile = join(domainDir, 'api', 'ping-thing.ts');
38
- const fixtureFile = join(domainDir, 'api', '__tests__', 'ping-thing.fixture.json');
37
+ const apiFile = join(domainDir, 'actions', 'ping-thing.ts');
38
+ const fixtureFile = join(domainDir, 'actions', '__tests__', 'ping-thing.fixture.json');
39
39
 
40
40
  const apiContent = await readFile(apiFile, 'utf8');
41
41
  const fixtureContent = await readFile(fixtureFile, 'utf8');
@@ -45,9 +45,11 @@ describe('add-api command', () => {
45
45
  const parsed = JSON.parse(fixtureContent) as { event: { body: string } };
46
46
  expect(parsed.event.body).toBe('{}');
47
47
 
48
- // The API file must contain .default() on input and output.
48
+ // The API file must contain .default() on input. Output uses a variable.
49
49
  expect(apiContent).toMatch(/input:\s*z\.object\(\{\}\)\.default\(\{\}\)/);
50
- expect(apiContent).toMatch(/output:\s*z\.object\(\{\s*ok:\s*z\.boolean\(\)\s*\}\)\.default\(\{\s*ok:\s*true\s*\}\)/);
50
+ // The output schema is assigned to a variable then referenced via `output: outputSchema`.
51
+ expect(apiContent).toMatch(/output:\s*outputSchema/);
52
+ expect(apiContent).toMatch(/const outputSchema = z\.object\(\{\s*ok:\s*z\.boolean\(\)\s*\}\)\.default\(\{\s*ok:\s*true\s*\}\)/);
51
53
  });
52
54
 
53
55
  it('scaffolds a simple-crud pattern API with .default() on schemas', async () => {
@@ -59,8 +61,8 @@ describe('add-api command', () => {
59
61
  domainsDir: tmpDir,
60
62
  });
61
63
 
62
- const apiFile = join(domainDir, 'api', 'widget.ts');
63
- const fixtureFile = join(domainDir, 'api', '__tests__', 'widget.fixture.json');
64
+ const apiFile = join(domainDir, 'actions', 'widget.ts');
65
+ const fixtureFile = join(domainDir, 'actions', '__tests__', 'widget.fixture.json');
64
66
 
65
67
  const apiContent = await readFile(apiFile, 'utf8');
66
68
  const fixtureContent = await readFile(fixtureFile, 'utf8');
@@ -83,8 +85,8 @@ describe('add-api command', () => {
83
85
  domainsDir: tmpDir,
84
86
  });
85
87
 
86
- const apiFile = join(domainDir, 'api', 'order.ts');
87
- const fixtureFile = join(domainDir, 'api', '__tests__', 'order.fixture.json');
88
+ const apiFile = join(domainDir, 'actions', 'order.ts');
89
+ const fixtureFile = join(domainDir, 'actions', '__tests__', 'order.fixture.json');
88
90
 
89
91
  const apiContent = await readFile(apiFile, 'utf8');
90
92
  const fixtureContent = await readFile(fixtureFile, 'utf8');
@@ -106,7 +108,7 @@ describe('add-api command', () => {
106
108
  domainsDir: tmpDir,
107
109
  });
108
110
 
109
- const apiFile = join(domainDir, 'api', 'console.ts');
111
+ const apiFile = join(domainDir, 'actions', 'console.ts');
110
112
  const apiContent = await readFile(apiFile, 'utf8');
111
113
 
112
114
  // First API: input defaults to command: 'status'; output defaults to { ok: true }.
@@ -33,7 +33,7 @@ describe('add-domain command', () => {
33
33
 
34
34
  // Verify directory structure
35
35
  const domainDir = join(tmpDir, 'test-domain');
36
- await stat(join(domainDir, 'api'));
36
+ await stat(join(domainDir, 'actions'));
37
37
  await stat(join(domainDir, 'subscribers'));
38
38
  await stat(join(domainDir, 'publishes'));
39
39
 
@@ -42,9 +42,10 @@ describe('add-domain command', () => {
42
42
  expect(configContent).toContain(`id: 'test-domain'`);
43
43
  expect(configContent).toContain(`tenancy: 'required'`);
44
44
 
45
- const apiContent = await readFile(join(domainDir, 'api', 'example.ts'), 'utf8');
45
+ const apiContent = await readFile(join(domainDir, 'actions', 'example.ts'), 'utf8');
46
46
  expect(apiContent).toContain(`id: 'example'`);
47
- expect(apiContent).toContain('defineApi');
47
+ expect(apiContent).toContain('defineAction');
48
+ expect(apiContent).toContain("type: 'api'");
48
49
 
49
50
  const eventsContent = await readFile(join(domainDir, 'publishes', 'events.ts'), 'utf8');
50
51
  expect(eventsContent).toContain(`id: 'test-domain.example'`);
@@ -154,10 +155,12 @@ describe('add-domain command', () => {
154
155
  const configContent = await readFile(join(tmpDir, 'system-domain', 'domain.config.ts'), 'utf8');
155
156
  expect(configContent).toContain(`tenancy: 'system'`);
156
157
 
157
- const apiContent = await readFile(join(tmpDir, 'system-domain', 'api', 'example.ts'), 'utf8');
158
+ const apiContent = await readFile(join(tmpDir, 'system-domain', 'actions', 'example.ts'), 'utf8');
158
159
  expect(apiContent).toContain(`tenancy: 'system'`);
159
160
  // For 'system' tenancy, path should not include /v1/tenants/{tenantId}
160
161
  expect(apiContent).toContain(`path: '/v1/system-domain/example'`);
162
+ // System tenancy should default to service-to-service auth.
163
+ expect(apiContent).toContain(`auth: 'service'`);
161
164
  });
162
165
 
163
166
  it('should use correct path structure for required tenancy', async () => {
@@ -167,7 +170,7 @@ describe('add-domain command', () => {
167
170
  domainsDir: tmpDir,
168
171
  });
169
172
 
170
- const apiContent = await readFile(join(tmpDir, 'tenant-domain', 'api', 'example.ts'), 'utf8');
173
+ const apiContent = await readFile(join(tmpDir, 'tenant-domain', 'actions', 'example.ts'), 'utf8');
171
174
  // For 'required' tenancy, path should include /v1/tenants/{tenantId}
172
175
  expect(apiContent).toContain(`path: '/v1/tenants/{tenantId}/tenant-domain/example'`);
173
176
  });
@@ -151,4 +151,59 @@ describe('build-flows command', () => {
151
151
 
152
152
  expect(registry.flows[0]?.trigger?.eventId).toBe('domain.event');
153
153
  });
154
+
155
+ it('picks up flow from domains/auth/flows/ with owningDomain=\'auth\'', async () => {
156
+ const domainFlowsDir = join(tmpDir, 'domains', 'auth', 'flows');
157
+ await mkdir(domainFlowsDir, { recursive: true });
158
+
159
+ const flow = {
160
+ id: 'onboarding-flow',
161
+ name: 'Onboarding Flow',
162
+ steps: [
163
+ {
164
+ type: 'flow-control',
165
+ control: 'succeed',
166
+ name: 'End',
167
+ },
168
+ ],
169
+ };
170
+
171
+ await writeFile(join(domainFlowsDir, 'onboarding.json'), JSON.stringify(flow));
172
+
173
+ const outFile = await runBuildFlows({ projectRoot: tmpDir });
174
+ const content = await readFile(outFile, 'utf8');
175
+ const registry = JSON.parse(content) as FlowRegistry;
176
+
177
+ expect(registry.flows).toHaveLength(1);
178
+ expect(registry.flows[0]?.id).toBe('onboarding-flow');
179
+ expect(registry.flows[0]?.owningDomain).toBe('auth');
180
+ });
181
+
182
+ it('detects ID collision between root-level and domain-level flows (FlowIdCollisionError)', async () => {
183
+ // Same flow ID in BOTH root flows/ and a domain's flows/.
184
+ const rootFlowsDir = join(tmpDir, 'flows');
185
+ await mkdir(rootFlowsDir, { recursive: true });
186
+
187
+ const domainFlowsDir = join(tmpDir, 'domains', 'auth', 'flows');
188
+ await mkdir(domainFlowsDir, { recursive: true });
189
+
190
+ const flow = {
191
+ id: 'shared-flow',
192
+ name: 'Shared Flow',
193
+ steps: [
194
+ {
195
+ type: 'flow-control',
196
+ control: 'succeed',
197
+ name: 'End',
198
+ },
199
+ ],
200
+ };
201
+
202
+ await writeFile(join(rootFlowsDir, 'shared.json'), JSON.stringify(flow));
203
+ await writeFile(join(domainFlowsDir, 'shared.json'), JSON.stringify(flow));
204
+
205
+ await expect(runBuildFlows({ projectRoot: tmpDir }))
206
+ .rejects
207
+ .toThrow(FlowIdCollisionError);
208
+ });
154
209
  });
@@ -139,4 +139,35 @@ describe('check-hashes', () => {
139
139
  /cannot read.*modules-hashes\.json/
140
140
  );
141
141
  });
142
+
143
+ it('--write regenerates manifest from disk (even if manifest was missing)', async () => {
144
+ // Set up infra/modules/ with a file, but NO .mc/modules-hashes.json.
145
+ await mkdir(join(tempDir, '.mc'), { recursive: true });
146
+ await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
147
+
148
+ const content = 'export const MyStack = {};';
149
+ await writeFile(join(tempDir, 'infra', 'modules', 'fresh.ts'), content);
150
+
151
+ // Confirm the manifest doesn't exist yet.
152
+ const { existsSync } = await import('node:fs');
153
+ expect(existsSync(join(tempDir, '.mc', 'modules-hashes.json'))).toBe(false);
154
+
155
+ // --write should bootstrap the manifest from disk and succeed.
156
+ const result = await runCheckHashes({ projectRoot: tempDir, write: true });
157
+ expect(result.ok).toBe(true);
158
+ expect(result.drifted).toHaveLength(0);
159
+ expect(result.missing).toHaveLength(0);
160
+ expect(result.unexpected).toHaveLength(0);
161
+
162
+ // The manifest should now exist on disk with the file's hash.
163
+ expect(existsSync(join(tempDir, '.mc', 'modules-hashes.json'))).toBe(true);
164
+ const manifestContent = JSON.parse(
165
+ await (await import('node:fs/promises')).readFile(
166
+ join(tempDir, '.mc', 'modules-hashes.json'),
167
+ 'utf8'
168
+ )
169
+ );
170
+ const expectedHash = createHash('sha256').update(content).digest('hex');
171
+ expect(manifestContent.files['infra/modules/fresh.ts']).toBe(expectedHash);
172
+ });
142
173
  });
@@ -263,7 +263,7 @@ describe('runCreateProject', () => {
263
263
  const coreTarball = await createTestTarball([
264
264
  {
265
265
  path: 'lib/domain-runtime.ts',
266
- content: 'export const defineApi = () => {};',
266
+ content: 'export const defineAction = () => {};',
267
267
  isTemplate: false,
268
268
  },
269
269
  {
@@ -285,14 +285,14 @@ describe('runCreateProject', () => {
285
285
 
286
286
  const authTarball = await createTestTarball([
287
287
  {
288
- path: 'domains/auth/api/login.ts',
289
- content: 'import { defineApi } from "../../lib/domain-runtime";',
288
+ path: 'domains/auth/actions/login.ts',
289
+ content: 'import { defineAction } from "../../lib/domain-runtime";',
290
290
  isTemplate: false,
291
291
  },
292
292
  {
293
293
  path: 'MANIFEST.json',
294
294
  content: createTarballManifest([
295
- { path: 'domains/auth/api/login.ts', isTemplate: false },
295
+ { path: 'domains/auth/actions/login.ts', isTemplate: false },
296
296
  ]),
297
297
  isTemplate: false,
298
298
  },
@@ -319,7 +319,7 @@ describe('runCreateProject', () => {
319
319
  await runCreateProject({ name: 'test-project', outDir: tempDir });
320
320
 
321
321
  // Read the generated auth login file
322
- const authPath = join(outputDir, 'domains/auth/api/login.ts');
322
+ const authPath = join(outputDir, 'domains/auth/actions/login.ts');
323
323
  const authContent = await readFile(authPath, 'utf-8');
324
324
 
325
325
  expect(authContent).toContain('../../lib/domain-runtime');
@@ -34,7 +34,6 @@ const mocks = vi.hoisted(() => {
34
34
  buildRegistry: vi.fn().mockResolvedValue({
35
35
  registry: {
36
36
  domain: { id: 'mock-domain' },
37
- apis: [],
38
37
  webhooks: [],
39
38
  subscribers: [],
40
39
  schedules: [],