@mettlecast/domain-cli 0.2.0 → 0.2.1

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 (63) hide show
  1. package/dist/builder/build-registry.js +3 -0
  2. package/dist/builder/load-module.js +15 -2
  3. package/dist/commands/create-project.js +1 -1
  4. package/dist/commands/doctor.js +10 -10
  5. package/dist/commands/upgrade-frontend.js +0 -2
  6. package/dist/commands/upgrade.js +4 -0
  7. package/dist/templates/patterns/api/create-with-event.js +3 -7
  8. package/dist/templates/patterns/api/idempotent-mutation.js +17 -40
  9. package/dist/templates/patterns/api/paginated-list.js +1 -9
  10. package/dist/templates/patterns/api/simple-crud.js +2 -2
  11. package/dist/templates/patterns/api/system-admin.js +1 -4
  12. package/dist/templates/patterns/api/webhook-receiver-style.js +7 -53
  13. package/dist/templates/patterns/subscriber/audit-relay.js +14 -24
  14. package/dist/templates/patterns/subscriber/cascade-deletion.js +6 -22
  15. package/dist/templates/patterns/subscriber/single-step-projection.js +6 -10
  16. package/dist/templates/subscriber-skeleton.js +1 -1
  17. package/dist/utils/install-file.d.ts +5 -6
  18. package/dist/utils/install-file.js +9 -25
  19. package/dist/utils/manifest.d.ts +2 -2
  20. package/dist/utils/manifest.js +47 -5
  21. package/dist/utils/scaffold-config.d.ts +7 -0
  22. package/dist/utils/scaffold-config.js +3 -0
  23. package/package.json +1 -1
  24. package/src/__tests__/commands/upgrade.test.ts +8 -8
  25. package/src/__tests__/doctor.test.ts +16 -11
  26. package/src/__tests__/scaffold-src/part-a-layout.test.ts +0 -8
  27. package/src/__tests__/scripts/package-scaffold.test.ts +14 -14
  28. package/src/__tests__/utils/install-file.test.ts +58 -35
  29. package/src/__tests__/utils/manifest.test.ts +74 -29
  30. package/src/builder/build-registry.ts +2 -0
  31. package/src/builder/load-module.ts +16 -2
  32. package/src/commands/add-module.ts +1 -1
  33. package/src/commands/create-project.ts +2 -2
  34. package/src/commands/doctor.ts +11 -11
  35. package/src/commands/upgrade-frontend.ts +0 -2
  36. package/src/commands/upgrade.ts +7 -2
  37. package/src/templates/dashboard-pages/account/api-keys.tsx +1 -1
  38. package/src/templates/dashboard-pages/account/audit-log.tsx +1 -1
  39. package/src/templates/dashboard-pages/account/members.tsx +1 -1
  40. package/src/templates/dashboard-pages/account/profile.tsx +1 -1
  41. package/src/templates/dashboard-pages/account/workspace-settings.tsx +1 -1
  42. package/src/templates/dashboard-pages/auth/accept-invitation.tsx +1 -1
  43. package/src/templates/dashboard-pages/auth/choose-org.tsx +1 -1
  44. package/src/templates/dashboard-pages/auth/forgot-password.tsx +1 -1
  45. package/src/templates/dashboard-pages/auth/login.tsx +1 -1
  46. package/src/templates/dashboard-pages/auth/mfa-setup.tsx +1 -1
  47. package/src/templates/dashboard-pages/auth/mfa-verify.tsx +1 -1
  48. package/src/templates/dashboard-pages/auth/reset-password.tsx +1 -1
  49. package/src/templates/dashboard-pages/auth/signup.tsx +1 -1
  50. package/src/templates/dashboard-pages/auth/verify-email.tsx +1 -1
  51. package/src/templates/patterns/api/create-with-event.ts +3 -7
  52. package/src/templates/patterns/api/idempotent-mutation.ts +17 -40
  53. package/src/templates/patterns/api/paginated-list.ts +1 -9
  54. package/src/templates/patterns/api/simple-crud.ts +2 -2
  55. package/src/templates/patterns/api/system-admin.ts +1 -4
  56. package/src/templates/patterns/api/webhook-receiver-style.ts +7 -53
  57. package/src/templates/patterns/subscriber/audit-relay.ts +14 -24
  58. package/src/templates/patterns/subscriber/cascade-deletion.ts +6 -22
  59. package/src/templates/patterns/subscriber/single-step-projection.ts +6 -10
  60. package/src/templates/subscriber-skeleton.ts +1 -1
  61. package/src/utils/install-file.ts +11 -29
  62. package/src/utils/manifest.ts +51 -6
  63. package/src/utils/scaffold-config.ts +11 -0
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdtemp, rmdir, writeFile, readFile, mkdir } from 'node:fs/promises';
2
+ import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
 
@@ -33,16 +33,16 @@ describe('manifest utilities', () => {
33
33
 
34
34
  afterEach(async () => {
35
35
  try {
36
- await rmdir(tmpDir, { recursive: true });
36
+ await rm(tmpDir, { recursive: true, force: true });
37
37
  } catch {
38
38
  // Ignore cleanup errors
39
39
  }
40
40
  });
41
41
 
42
42
  describe('createManifest', () => {
43
- it('produces manifest with v2 schema', () => {
43
+ it('produces manifest with v3 schema', () => {
44
44
  const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
45
- expect(manifest.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json');
45
+ expect(manifest.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json');
46
46
  expect(manifest.scaffoldVersion).toBe('1.0.0');
47
47
  expect(manifest.projectName).toBe('test-project');
48
48
  expect(manifest.awsRegion).toBe('eu-north-1');
@@ -52,21 +52,21 @@ describe('manifest utilities', () => {
52
52
  });
53
53
 
54
54
  describe('inferPolicyFromPath', () => {
55
- it('returns owned for infra/ paths', () => {
56
- expect(inferPolicyFromPath('infra/modules/app.ts')).toBe('owned');
57
- expect(inferPolicyFromPath('infra/main.tf')).toBe('owned');
55
+ it('returns managed for infra/ paths', () => {
56
+ expect(inferPolicyFromPath('infra/modules/app.ts')).toBe('managed');
57
+ expect(inferPolicyFromPath('infra/main.tf')).toBe('managed');
58
58
  });
59
59
 
60
- it('returns owned for .mc/infra/ paths', () => {
61
- expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('owned');
60
+ it('returns editable for .mc/infra/ paths', () => {
61
+ expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('editable');
62
62
  });
63
63
 
64
- it('returns owned for mc-deploy.yml', () => {
65
- expect(inferPolicyFromPath('mc-deploy.yml')).toBe('owned');
64
+ it('returns managed for mc-deploy.yml', () => {
65
+ expect(inferPolicyFromPath('mc-deploy.yml')).toBe('managed');
66
66
  });
67
67
 
68
- it('returns owned for .github/workflows/ paths', () => {
69
- expect(inferPolicyFromPath('.github/workflows/test.yml')).toBe('owned');
68
+ it('returns managed for .github/workflows/ paths', () => {
69
+ expect(inferPolicyFromPath('.github/workflows/test.yml')).toBe('managed');
70
70
  });
71
71
 
72
72
  it('returns seed for frontend seed paths', () => {
@@ -78,10 +78,21 @@ describe('manifest utilities', () => {
78
78
  expect(inferPolicyFromPath('frontend/src/styles/brand.css')).toBe('seed');
79
79
  });
80
80
 
81
- it('returns tracked for other paths', () => {
82
- expect(inferPolicyFromPath('frontend/src/components/button.tsx')).toBe('tracked');
83
- expect(inferPolicyFromPath('package.json')).toBe('tracked');
84
- expect(inferPolicyFromPath('src/utils/helper.ts')).toBe('tracked');
81
+ it('returns editable for other paths', () => {
82
+ expect(inferPolicyFromPath('frontend/src/components/button.tsx')).toBe('editable');
83
+ expect(inferPolicyFromPath('src/utils/helper.ts')).toBe('editable');
84
+ });
85
+
86
+ it('returns managed for CLAUDE.md, .husky, scaffold-config, modules-hashes', () => {
87
+ expect(inferPolicyFromPath('CLAUDE.md')).toBe('managed');
88
+ expect(inferPolicyFromPath('.husky/pre-commit')).toBe('managed');
89
+ expect(inferPolicyFromPath('.mc/scaffold-config.json')).toBe('managed');
90
+ expect(inferPolicyFromPath('.mc/modules-hashes.json')).toBe('managed');
91
+ });
92
+
93
+ it('returns editable for vite.config.ts and tailwind.config.ts', () => {
94
+ expect(inferPolicyFromPath('vite.config.ts')).toBe('editable');
95
+ expect(inferPolicyFromPath('tailwind.config.ts')).toBe('editable');
85
96
  });
86
97
  });
87
98
 
@@ -95,7 +106,7 @@ describe('manifest utilities', () => {
95
106
  sha256: 'abc123',
96
107
  wasTemplate: false,
97
108
  installedAt: '2026-01-01T00:00:00Z',
98
- policy: 'owned',
109
+ policy: 'managed',
99
110
  };
100
111
 
101
112
  upsertManifestFile(manifest, entry);
@@ -112,7 +123,7 @@ describe('manifest utilities', () => {
112
123
  sha256: 'abc123',
113
124
  wasTemplate: false,
114
125
  installedAt: '2026-01-01T00:00:00Z',
115
- policy: 'owned',
126
+ policy: 'managed',
116
127
  };
117
128
 
118
129
  upsertManifestFile(manifest, entry1);
@@ -158,7 +169,7 @@ describe('manifest utilities', () => {
158
169
  sha256: 'abc123',
159
170
  wasTemplate: false,
160
171
  installedAt: '2026-01-01T00:00:00Z',
161
- policy: 'owned',
172
+ policy: 'managed',
162
173
  };
163
174
 
164
175
  upsertManifestFile(manifest, entry);
@@ -167,7 +178,7 @@ describe('manifest utilities', () => {
167
178
  const read = await readManifest(tmpDir);
168
179
  expect(read).not.toBeNull();
169
180
  expect(read?.files).toHaveLength(1);
170
- expect(read?.files[0].policy).toBe('owned');
181
+ expect(read?.files[0].policy).toBe('managed');
171
182
  });
172
183
 
173
184
  it('auto-migrates v1 entries (missing policy) on read', async () => {
@@ -200,9 +211,9 @@ describe('manifest utilities', () => {
200
211
  ],
201
212
  };
202
213
 
203
- await mkdir(join(tmpDir, '.tib'), { recursive: true });
214
+ await mkdir(join(tmpDir, '.mc'), { recursive: true });
204
215
  await writeFile(
205
- join(tmpDir, '.tib', 'manifest.json'),
216
+ join(tmpDir, '.mc', 'manifest.json'),
206
217
  JSON.stringify(v1Manifest, null, 2) + '\n',
207
218
  'utf-8'
208
219
  );
@@ -212,18 +223,52 @@ describe('manifest utilities', () => {
212
223
  expect(read!.files).toHaveLength(2);
213
224
 
214
225
  // Check that policies were inferred
215
- const ownedEntry = read!.files.find((f) => f.path === 'infra/main.tf');
216
- expect(ownedEntry?.policy).toBe('owned');
226
+ const managedEntry = read!.files.find((f) => f.path === 'infra/main.tf');
227
+ expect(managedEntry?.policy).toBe('managed');
217
228
 
218
229
  const seedEntry = read!.files.find((f) => f.path === 'frontend/src/pages/auth/login.tsx');
219
230
  expect(seedEntry?.policy).toBe('seed');
220
231
 
221
- // Check that manifest was written back with v2 schema and policies
222
- const reread = await readFile(join(tmpDir, '.tib', 'manifest.json'), 'utf-8');
232
+ // Check that manifest was written back with v3 schema and policies
233
+ const reread = await readFile(join(tmpDir, '.mc', 'manifest.json'), 'utf-8');
223
234
  const reparsed = JSON.parse(reread);
224
- expect(reparsed.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json');
225
- expect(reparsed.files[0].policy).toBe('owned');
235
+ expect(reparsed.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json');
236
+ expect(reparsed.files[0].policy).toBe('managed');
226
237
  expect(reparsed.files[1].policy).toBe('seed');
227
238
  });
228
239
  });
240
+
241
+ describe('readManifest > v2→v3 migration', () => {
242
+ it('renames owned→managed and tracked→editable', async () => {
243
+ // Create a v2 manifest with old policy names in a temp dir
244
+ const tmpDir = await mkdtemp(join(tmpdir(), 'v2v3-'));
245
+ const mcDir = join(tmpDir, '.mc');
246
+ await mkdir(mcDir, { recursive: true });
247
+
248
+ const v2Manifest = {
249
+ $schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json',
250
+ scaffoldVersion: '0.1.0',
251
+ createdAt: '2026-01-01T00:00:00Z',
252
+ updatedAt: '2026-01-01T00:00:00Z',
253
+ projectName: 'test',
254
+ awsRegion: 'eu-north-1',
255
+ enabledModules: ['shared'],
256
+ files: [
257
+ { path: 'infra/modules/app.ts', module: 'core', moduleVersion: '0.1.0', sha256: 'abc', wasTemplate: false, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' },
258
+ { path: 'frontend/src/lib/utils.ts', module: 'frontend', moduleVersion: '0.1.0', sha256: 'def', wasTemplate: false, installedAt: '2026-01-01T00:00:00Z', policy: 'tracked' },
259
+ { path: 'frontend/src/pages/auth/login.tsx', module: 'frontend', moduleVersion: '0.1.0', sha256: 'ghi', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'seed' },
260
+ ],
261
+ };
262
+ await writeFile(join(mcDir, 'manifest.json'), JSON.stringify(v2Manifest, null, 2), 'utf-8');
263
+
264
+ const manifest = await readManifest(tmpDir);
265
+ expect(manifest).not.toBeNull();
266
+ expect(manifest!.files[0].policy).toBe('managed');
267
+ expect(manifest!.files[1].policy).toBe('editable');
268
+ expect(manifest!.files[2].policy).toBe('seed'); // unchanged
269
+
270
+ // Clean up
271
+ await rm(tmpDir, { recursive: true, force: true });
272
+ });
273
+ });
229
274
  });
@@ -74,6 +74,8 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
74
74
  const domainExports = await load(paths.domain);
75
75
  const domainRaw = domainExports.find(e => e['_kind'] === 'domain');
76
76
  if (!domainRaw) {
77
+ // Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
78
+ for (const w of warnings) process.stderr.write(`[tib validate] ${w}\n`);
77
79
  throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
78
80
  }
79
81
 
@@ -103,17 +103,31 @@ export async function loadModuleExports(absoluteFilePath: string): Promise<RawPr
103
103
  const tempPath = join(tibTmpDir, `tib-load-${randomBytes(8).toString('hex')}.mts`);
104
104
  await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
105
105
 
106
+ // TSX_TSCONFIG_PATH forces tsx to use the specified tsconfig for ALL files it processes,
107
+ // including dynamically imported domain source files. Without this, tsx v4 uses the nearest
108
+ // package.json "type" field to decide CJS vs ESM — domain files without "type":"module"
109
+ // default to CJS, which cannot require() @mettlecast/domain-runtime (pure ESM).
110
+ // infra/modules/tsconfig.json (module: ESNext) is always present in scaffold projects.
111
+ const tsxEnv = {
112
+ ...process.env,
113
+ TSX_TSCONFIG_PATH: join(process.cwd(), 'tsconfig.json'),
114
+ };
115
+
106
116
  try {
107
117
  return await new Promise<RawPrimitiveExport[]>((resolve, reject) => {
108
118
  const tsxChild = spawn(process.execPath, [tsxCli, tempPath], {
109
119
  stdio: ['ignore', 'pipe', 'pipe'],
110
- env: { ...process.env },
120
+ env: tsxEnv,
111
121
  });
112
122
 
113
123
  let stdout = '';
114
124
  let stderr = '';
115
125
  tsxChild.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
116
- tsxChild.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
126
+ // Pipe tsx stderr directly to our process stderr so errors are visible in CI logs.
127
+ tsxChild.stderr.on('data', (chunk: Buffer) => {
128
+ stderr += chunk.toString();
129
+ process.stderr.write(chunk);
130
+ });
117
131
  tsxChild.on('close', (code) => {
118
132
  if (code !== 0) {
119
133
  reject(new Error(`loadModuleExports: tsx exited ${code} for ${absoluteFilePath}\n${stderr}`));
@@ -23,7 +23,7 @@ interface TarballManifestEntry {
23
23
  path: string;
24
24
  installedAs?: string;
25
25
  isTemplate: boolean;
26
- policy?: 'owned' | 'tracked' | 'seed';
26
+ policy?: 'managed' | 'editable' | 'seed';
27
27
  }
28
28
 
29
29
  interface TarEntry {
@@ -98,7 +98,7 @@ interface TarballManifestEntry {
98
98
  sha256: string;
99
99
  isTemplate: boolean;
100
100
  installedAs?: string;
101
- policy?: 'owned' | 'tracked' | 'seed';
101
+ policy?: 'managed' | 'editable' | 'seed';
102
102
  }
103
103
 
104
104
  async function parseTarball(buf: Buffer): Promise<TarEntry[]> {
@@ -317,7 +317,7 @@ export async function runCreateProject(opts: CreateProjectOptions): Promise<void
317
317
  sha256,
318
318
  wasTemplate: fileEntry.isTemplate,
319
319
  installedAt: new Date().toISOString(),
320
- policy: fileEntry.policy ?? 'tracked',
320
+ policy: fileEntry.policy ?? 'editable',
321
321
  });
322
322
  }
323
323
  }
@@ -1088,7 +1088,7 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
1088
1088
  // Check 1: owned files in unexpected locations
1089
1089
  // Policy: owned files should be in .mc/ OR .github/workflows/tib-*
1090
1090
  const ownedOutsideTib = manifest.files.filter(f =>
1091
- f.policy === 'owned' &&
1091
+ f.policy === 'managed' &&
1092
1092
  !f.path.startsWith('.mc/') &&
1093
1093
  !/^\.github\/workflows\/tib-/.test(f.path)
1094
1094
  );
@@ -1112,7 +1112,7 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
1112
1112
 
1113
1113
  // Check 2: tracked/seed files inside .mc/
1114
1114
  const nonOwnedInsideTib = manifest.files.filter(f =>
1115
- (f.policy === 'tracked' || f.policy === 'seed') &&
1115
+ (f.policy === 'editable' || f.policy === 'seed') &&
1116
1116
  f.path.startsWith('.mc/')
1117
1117
  );
1118
1118
  if (nonOwnedInsideTib.length === 0) {
@@ -1209,15 +1209,15 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
1209
1209
  /**
1210
1210
  * Old path → new path mapping for known scaffold files
1211
1211
  */
1212
- const RELOCATION_MAP: Array<{ oldPath: string; newPath: string; policy: 'owned' | 'tracked' | 'seed' }> = [
1213
- { oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'owned' },
1214
- { oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'owned' },
1215
- { oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'owned' },
1216
- { oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'owned' },
1217
- { oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'owned' },
1218
- { oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'owned' },
1219
- { oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'owned' },
1220
- { oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'owned' },
1212
+ const RELOCATION_MAP: Array<{ oldPath: string; newPath: string; policy: 'managed' | 'editable' | 'seed' }> = [
1213
+ { oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'managed' },
1214
+ { oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'managed' },
1215
+ { oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'managed' },
1216
+ { oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
1217
+ { oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
1218
+ { oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
1219
+ { oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
1220
+ { oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
1221
1221
  ];
1222
1222
 
1223
1223
  /**
@@ -14,7 +14,6 @@ interface PackageJson {
14
14
  }
15
15
 
16
16
  const TIB_PACKAGES = [
17
- '@mettlecast/design-system',
18
17
  '@mettlecast/dashboard-shell',
19
18
  '@mettlecast/sdk',
20
19
  '@mettlecast/observability',
@@ -25,7 +24,6 @@ const TIB_PACKAGES = [
25
24
  type TibPackage = (typeof TIB_PACKAGES)[number];
26
25
 
27
26
  const REQUIRED_PACKAGES: TibPackage[] = [
28
- '@mettlecast/design-system',
29
27
  '@mettlecast/dashboard-shell',
30
28
  '@mettlecast/sdk',
31
29
  ];
@@ -40,12 +40,12 @@ interface TarballManifestEntry {
40
40
  sha256: string;
41
41
  isTemplate: boolean;
42
42
  installedAs?: string;
43
- policy?: 'owned' | 'tracked' | 'seed';
43
+ policy?: 'managed' | 'editable' | 'seed';
44
44
  }
45
45
 
46
46
  // ── Upgrade result tracking ────────────────────────────────────────────────────
47
47
 
48
- type FileStatus = 'added' | 'updated' | 'conflict' | 'deleted' | 'unchanged';
48
+ type FileStatus = 'added' | 'updated' | 'conflict' | 'deleted' | 'unchanged' | 'update-available';
49
49
 
50
50
  interface UpgradeFileResult {
51
51
  path: string;
@@ -513,6 +513,11 @@ export async function runUpgrade(
513
513
  // Add to results
514
514
  allResults.push({ path: installPath, status: installResult.status, module: mod.id });
515
515
 
516
+ // update-available: do NOT update manifest (file was not written to disk)
517
+ if (installResult.status === 'update-available') {
518
+ continue;
519
+ }
520
+
516
521
  // Update manifest if file was not skipped
517
522
  const newChecksum = computeChecksumString(newContent);
518
523
  upsertManifestFile(updatedManifest, {
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useApi } from '@mettlecast/sdk';
3
3
  import { useProject } from '@mettlecast/dashboard-shell';
4
- import { TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  interface ApiKey {
7
7
  keyId: string;
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useApi } from '@mettlecast/sdk';
3
3
  import { useProject } from '@mettlecast/dashboard-shell';
4
- import { TibFormError } from '@mettlecast/design-system';
4
+ import { TibFormError } from '../../components/ui/mc-form';
5
5
 
6
6
  interface ActivityEvent {
7
7
  id: string;
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useApi } from '@mettlecast/sdk';
3
3
  import { useProject } from '@mettlecast/dashboard-shell';
4
- import { TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  interface Member {
7
7
  userId: string;
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { useApi } from '@mettlecast/sdk';
3
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
3
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
4
4
 
5
5
  export function ProfilePage() {
6
6
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useApi } from '@mettlecast/sdk';
3
3
  import { useProject } from '@mettlecast/dashboard-shell';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function WorkspaceSettingsPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function AcceptInvitationPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormError } from '@mettlecast/design-system';
4
+ import { TibFormError } from '../../components/ui/mc-form';
5
5
 
6
6
  interface OrgItem {
7
7
  orgId: string;
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function ForgotPasswordPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate, Link } from 'react-router-dom';
3
3
  import { signIn } from '@mettlecast/dashboard-shell';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function LoginPage() {
7
7
  const navigate = useNavigate();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function MfaSetupPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function MfaVerifyPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function ResetPasswordPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate, Link } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function SignupPage() {
7
7
  const tib = useApi();
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
3
  import { useApi } from '@mettlecast/sdk';
4
- import { TibFormField, TibFormError, TibSubmitButton } from '@mettlecast/design-system';
4
+ import { TibFormField, TibFormError, TibSubmitButton } from '../../components/ui/mc-form';
5
5
 
6
6
  export function VerifyEmailPage() {
7
7
  const tib = useApi();
@@ -41,7 +41,7 @@ export const ${varName} = defineApi({
41
41
  const entityId = crypto.randomUUID();
42
42
 
43
43
  // ── Persist entity ────────────────────────────────────
44
- // ctx.db.put({ ... })
44
+ await ctx.store.put(entityId, { id: entityId, name: input.name });
45
45
 
46
46
  // ── Publish domain event ──────────────────────────────
47
47
  await ctx.publish(
@@ -49,16 +49,12 @@ export const ${varName} = defineApi({
49
49
  {
50
50
  id: entityId,
51
51
  name: input.name,
52
- tenantId: ctx.tenantId,
52
+ tenantId: ctx.tenant.id,
53
53
  },
54
54
  1,
55
55
  );
56
56
 
57
- await ctx.auditLog({
58
- action: '${id}.create',
59
- tenantId: ctx.tenantId,
60
- entityId,
61
- });
57
+ await ctx.audit.log('${id}.create', entityId, { tenantId: ctx.tenant.id });
62
58
 
63
59
  return {
64
60
  id: entityId,
@@ -14,10 +14,6 @@ export function idempotentMutationTemplate(domain: string, id: string, tenancy:
14
14
  return `import { z } from 'zod';
15
15
  import { defineApi } from '@mettlecast/domain-runtime';
16
16
 
17
- // ── Idempotency key header ───────────────────────────────────
18
-
19
- const IdempotencyKey = z.string().min(1);
20
-
21
17
  // ── Zod schemas ──────────────────────────────────────────────
22
18
 
23
19
  const ${varName}Input = z.object({
@@ -43,56 +39,37 @@ export const ${varName} = defineApi({
43
39
  input: ${varName}Input,
44
40
  output: ${varName}Output,
45
41
  handler: async (input, ctx) => {
46
- // ── Extract idempotency key from headers ──────────────
47
- const idempotencyKey = ctx.headers?.['x-idempotency-key'];
48
- if (!idempotencyKey) {
49
- throw new Error('Missing x-idempotency-key header');
50
- }
51
- IdempotencyKey.parse(idempotencyKey);
52
-
53
- // ── Check for duplicate ───────────────────────────────
54
- const existing = await ctx.db.get({
55
- key: { pk: 'IDEMPOTENCY', sk: idempotencyKey },
56
- });
42
+ // ── Check for duplicate (idempotency key = input.id) ──
43
+ const existing = await ctx.store.get('IDEMPOTENT#' + input.id);
57
44
 
58
45
  if (existing) {
59
- await ctx.auditLog({
60
- action: '${id}.idempotent.duplicate',
61
- tenantId: ctx.tenantId,
62
- meta: { idempotencyKey },
63
- });
46
+ await ctx.audit.log('${id}.idempotent.duplicate', input.id);
64
47
  return {
65
- id: existing.entityId as string,
48
+ id: input.id,
66
49
  status: 'already-processed',
67
- idempotencyKey,
50
+ idempotencyKey: input.id,
68
51
  };
69
52
  }
70
53
 
71
54
  // ── Apply mutation ────────────────────────────────────
72
- const result = await ctx.db.put({
73
- pk: '${id.toUpperCase()}',
74
- sk: input.id,
75
- payload: input.payload,
76
- });
55
+ await ctx.store.put(
56
+ '${id.toUpperCase()}#' + input.id,
57
+ { id: input.id, payload: input.payload, createdAt: new Date().toISOString() },
58
+ );
77
59
 
78
- // ── Record idempotency token ──────────────────────────
79
- await ctx.db.put({
80
- key: { pk: 'IDEMPOTENCY', sk: idempotencyKey },
81
- entityId: input.id,
82
- ttl: Math.floor(Date.now() / 1000) + 86400, // 24h TTL
83
- });
60
+ // ── Record idempotency token with 24h TTL ─────────────
61
+ await ctx.store.put(
62
+ 'IDEMPOTENT#' + input.id,
63
+ { entityId: input.id },
64
+ { ttl: Math.floor(Date.now() / 1000) + 86400 },
65
+ );
84
66
 
85
- await ctx.auditLog({
86
- action: '${id}.idempotent.applied',
87
- tenantId: ctx.tenantId,
88
- entityId: input.id,
89
- meta: { idempotencyKey },
90
- });
67
+ await ctx.audit.log('${id}.idempotent.applied', input.id, { tenantId: ctx.tenant.id });
91
68
 
92
69
  return {
93
70
  id: input.id,
94
71
  status: 'applied',
95
- idempotencyKey,
72
+ idempotencyKey: input.id,
96
73
  };
97
74
  },
98
75
  },
@@ -47,15 +47,7 @@ export const ${varName} = defineApi({
47
47
  handler: async (input, ctx) => {
48
48
  const { cursor, limit, filter } = input;
49
49
 
50
- // Emit EMF (Embedded Metric Format) metrics
51
- ctx.metrics?.putMetric('${id}.pageRequest', 1, 'Count');
52
- ctx.metrics?.putMetric('${id}.pageSize', limit, 'Count');
53
-
54
- await ctx.auditLog({
55
- action: '${id}.list',
56
- tenantId: ctx.tenantId,
57
- meta: { cursor, limit },
58
- });
50
+ await ctx.audit.log('${id}.list', ctx.tenant.id, { cursor, limit });
59
51
 
60
52
  // ── paginated data fetch ──────────────────────────────
61
53
  const items: Array<{ id: string }> = [];