@mettlecast/domain-cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/build-registry.js +3 -0
- package/dist/builder/load-module.js +5 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/doctor.js +10 -10
- package/dist/commands/upgrade-frontend.js +0 -2
- package/dist/commands/upgrade.js +4 -0
- package/dist/templates/patterns/api/create-with-event.js +3 -7
- package/dist/templates/patterns/api/idempotent-mutation.js +17 -40
- package/dist/templates/patterns/api/paginated-list.js +1 -9
- package/dist/templates/patterns/api/simple-crud.js +2 -2
- package/dist/templates/patterns/api/system-admin.js +1 -4
- package/dist/templates/patterns/api/webhook-receiver-style.js +7 -53
- package/dist/templates/patterns/subscriber/audit-relay.js +14 -24
- package/dist/templates/patterns/subscriber/cascade-deletion.js +6 -22
- package/dist/templates/patterns/subscriber/single-step-projection.js +6 -10
- package/dist/templates/subscriber-skeleton.js +1 -1
- package/dist/utils/install-file.d.ts +5 -6
- package/dist/utils/install-file.js +9 -25
- package/dist/utils/manifest.d.ts +2 -2
- package/dist/utils/manifest.js +47 -5
- package/dist/utils/scaffold-config.d.ts +7 -0
- package/dist/utils/scaffold-config.js +3 -0
- package/package.json +1 -1
- package/src/__tests__/commands/upgrade.test.ts +8 -8
- package/src/__tests__/doctor.test.ts +16 -11
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +0 -8
- package/src/__tests__/scripts/package-scaffold.test.ts +14 -14
- package/src/__tests__/utils/install-file.test.ts +58 -35
- package/src/__tests__/utils/manifest.test.ts +74 -29
- package/src/builder/build-registry.ts +2 -0
- package/src/builder/load-module.ts +5 -3
- package/src/commands/add-module.ts +1 -1
- package/src/commands/create-project.ts +2 -2
- package/src/commands/doctor.ts +11 -11
- package/src/commands/upgrade-frontend.ts +0 -2
- package/src/commands/upgrade.ts +7 -2
- package/src/templates/dashboard-pages/account/api-keys.tsx +1 -1
- package/src/templates/dashboard-pages/account/audit-log.tsx +1 -1
- package/src/templates/dashboard-pages/account/members.tsx +1 -1
- package/src/templates/dashboard-pages/account/profile.tsx +1 -1
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +1 -1
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +1 -1
- package/src/templates/dashboard-pages/auth/choose-org.tsx +1 -1
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +1 -1
- package/src/templates/dashboard-pages/auth/login.tsx +1 -1
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +1 -1
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +1 -1
- package/src/templates/dashboard-pages/auth/reset-password.tsx +1 -1
- package/src/templates/dashboard-pages/auth/signup.tsx +1 -1
- package/src/templates/dashboard-pages/auth/verify-email.tsx +1 -1
- package/src/templates/patterns/api/create-with-event.ts +3 -7
- package/src/templates/patterns/api/idempotent-mutation.ts +17 -40
- package/src/templates/patterns/api/paginated-list.ts +1 -9
- package/src/templates/patterns/api/simple-crud.ts +2 -2
- package/src/templates/patterns/api/system-admin.ts +1 -4
- package/src/templates/patterns/api/webhook-receiver-style.ts +7 -53
- package/src/templates/patterns/subscriber/audit-relay.ts +14 -24
- package/src/templates/patterns/subscriber/cascade-deletion.ts +6 -22
- package/src/templates/patterns/subscriber/single-step-projection.ts +6 -10
- package/src/templates/subscriber-skeleton.ts +1 -1
- package/src/utils/install-file.ts +11 -29
- package/src/utils/manifest.ts +51 -6
- 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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
56
|
-
expect(inferPolicyFromPath('infra/modules/app.ts')).toBe('
|
|
57
|
-
expect(inferPolicyFromPath('infra/main.tf')).toBe('
|
|
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
|
|
61
|
-
expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('
|
|
60
|
+
it('returns editable for .mc/infra/ paths', () => {
|
|
61
|
+
expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('editable');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it('returns
|
|
65
|
-
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('
|
|
64
|
+
it('returns managed for mc-deploy.yml', () => {
|
|
65
|
+
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('managed');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('returns
|
|
69
|
-
expect(inferPolicyFromPath('.github/workflows/test.yml')).toBe('
|
|
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
|
|
82
|
-
expect(inferPolicyFromPath('frontend/src/components/button.tsx')).toBe('
|
|
83
|
-
expect(inferPolicyFromPath('
|
|
84
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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('
|
|
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, '.
|
|
214
|
+
await mkdir(join(tmpDir, '.mc'), { recursive: true });
|
|
204
215
|
await writeFile(
|
|
205
|
-
join(tmpDir, '.
|
|
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
|
|
216
|
-
expect(
|
|
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
|
|
222
|
-
const reread = await readFile(join(tmpDir, '.
|
|
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.
|
|
225
|
-
expect(reparsed.files[0].policy).toBe('
|
|
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
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
3
|
import { join, dirname } from 'node:path';
|
|
5
4
|
import { randomBytes } from 'node:crypto';
|
|
6
5
|
import { createRequire } from 'node:module';
|
|
@@ -107,13 +106,16 @@ export async function loadModuleExports(absoluteFilePath: string): Promise<RawPr
|
|
|
107
106
|
return await new Promise<RawPrimitiveExport[]>((resolve, reject) => {
|
|
108
107
|
const tsxChild = spawn(process.execPath, [tsxCli, tempPath], {
|
|
109
108
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
-
env: { ...process.env },
|
|
111
109
|
});
|
|
112
110
|
|
|
113
111
|
let stdout = '';
|
|
114
112
|
let stderr = '';
|
|
115
113
|
tsxChild.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
|
|
116
|
-
|
|
114
|
+
// Pipe tsx stderr directly to our process stderr so errors are visible in CI logs.
|
|
115
|
+
tsxChild.stderr.on('data', (chunk: Buffer) => {
|
|
116
|
+
stderr += chunk.toString();
|
|
117
|
+
process.stderr.write(chunk);
|
|
118
|
+
});
|
|
117
119
|
tsxChild.on('close', (code) => {
|
|
118
120
|
if (code !== 0) {
|
|
119
121
|
reject(new Error(`loadModuleExports: tsx exited ${code} for ${absoluteFilePath}\n${stderr}`));
|
|
@@ -98,7 +98,7 @@ interface TarballManifestEntry {
|
|
|
98
98
|
sha256: string;
|
|
99
99
|
isTemplate: boolean;
|
|
100
100
|
installedAs?: string;
|
|
101
|
-
policy?: '
|
|
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 ?? '
|
|
320
|
+
policy: fileEntry.policy ?? 'editable',
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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 === '
|
|
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 === '
|
|
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: '
|
|
1213
|
-
{ oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: '
|
|
1214
|
-
{ oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: '
|
|
1215
|
-
{ oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: '
|
|
1216
|
-
{ oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: '
|
|
1217
|
-
{ oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: '
|
|
1218
|
-
{ oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: '
|
|
1219
|
-
{ oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: '
|
|
1220
|
-
{ oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: '
|
|
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
|
];
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -40,12 +40,12 @@ interface TarballManifestEntry {
|
|
|
40
40
|
sha256: string;
|
|
41
41
|
isTemplate: boolean;
|
|
42
42
|
installedAs?: string;
|
|
43
|
-
policy?: '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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
|
-
|
|
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.
|
|
52
|
+
tenantId: ctx.tenant.id,
|
|
53
53
|
},
|
|
54
54
|
1,
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
-
await ctx.
|
|
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
|
-
// ──
|
|
47
|
-
const
|
|
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.
|
|
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:
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
80
|
-
|
|
81
|
-
entityId: input.id,
|
|
82
|
-
ttl: Math.floor(Date.now() / 1000) + 86400,
|
|
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.
|
|
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
|
-
|
|
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 }> = [];
|
|
@@ -44,7 +44,7 @@ export const ${varName} = defineApi({
|
|
|
44
44
|
output: ${varName}Output,
|
|
45
45
|
handler: async (input, ctx) => {
|
|
46
46
|
// Create / Read / Update / Delete based on method
|
|
47
|
-
await ctx.
|
|
47
|
+
await ctx.audit.log('${id}.v1', ctx.tenant.id, { input });
|
|
48
48
|
return {
|
|
49
49
|
id: input.id ?? crypto.randomUUID(),
|
|
50
50
|
createdAt: new Date().toISOString(),
|
|
@@ -90,7 +90,7 @@ export const ${varName}List = defineApi({
|
|
|
90
90
|
input: ${varName}ListInput,
|
|
91
91
|
output: ${varName}ListOutput,
|
|
92
92
|
handler: async (input, ctx) => {
|
|
93
|
-
await ctx.
|
|
93
|
+
await ctx.audit.log('${id}-list.v1', ctx.tenant.id);
|
|
94
94
|
return { items: [], nextCursor: undefined };
|
|
95
95
|
},
|
|
96
96
|
},
|