@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.
- package/dist/builder/build-registry.js +3 -0
- package/dist/builder/load-module.js +15 -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 +16 -2
- 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
|
@@ -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
|
},
|
|
@@ -48,10 +48,7 @@ export const ${varName} = defineApi({
|
|
|
48
48
|
input: ${varName}Input,
|
|
49
49
|
output: ${varName}Output,
|
|
50
50
|
handler: async (input, ctx) => {
|
|
51
|
-
await ctx.
|
|
52
|
-
action: '${id}.system',
|
|
53
|
-
meta: { command: input.command },
|
|
54
|
-
});
|
|
51
|
+
await ctx.audit.log('${id}.system', 'system', { command: input.command });
|
|
55
52
|
|
|
56
53
|
switch (input.command) {
|
|
57
54
|
case 'status':
|
|
@@ -12,7 +12,6 @@ export function webhookReceiverStyleTemplate(domain: string, id: string, tenancy
|
|
|
12
12
|
|
|
13
13
|
return `import { z } from 'zod';
|
|
14
14
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
15
|
-
import crypto from 'crypto';
|
|
16
15
|
|
|
17
16
|
// ── Supported webhook providers ───────────────────────────────
|
|
18
17
|
|
|
@@ -38,40 +37,6 @@ const ${varName}Output = z.object({
|
|
|
38
37
|
provider: ProviderEnum,
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
// ── Signature verification ────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
function verifySignature(
|
|
44
|
-
provider: z.infer<typeof ProviderEnum>,
|
|
45
|
-
payload: string,
|
|
46
|
-
signatureHeader: string,
|
|
47
|
-
secret: string,
|
|
48
|
-
): boolean {
|
|
49
|
-
switch (provider) {
|
|
50
|
-
case 'stripe':
|
|
51
|
-
// Stripe uses HMAC-SHA256
|
|
52
|
-
return crypto.timingSafeEqual(
|
|
53
|
-
Buffer.from(signatureHeader),
|
|
54
|
-
crypto.createHmac('sha256', secret).update(payload).digest(),
|
|
55
|
-
);
|
|
56
|
-
case 'github':
|
|
57
|
-
// GitHub uses HMAC-SHA256 with 'sha256=' prefix
|
|
58
|
-
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
59
|
-
return crypto.timingSafeEqual(
|
|
60
|
-
Buffer.from(signatureHeader),
|
|
61
|
-
Buffer.from(expected),
|
|
62
|
-
);
|
|
63
|
-
case 'slack':
|
|
64
|
-
// Slack uses HMAC-SHA256 with 'v0=' prefix
|
|
65
|
-
const slackExpected = 'v0=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
66
|
-
return crypto.timingSafeEqual(
|
|
67
|
-
Buffer.from(signatureHeader),
|
|
68
|
-
Buffer.from(slackExpected),
|
|
69
|
-
);
|
|
70
|
-
default:
|
|
71
|
-
return true; // custom provider — verify externally
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
40
|
// ── API definition ───────────────────────────────────────────
|
|
76
41
|
|
|
77
42
|
export const ${varName} = defineApi({
|
|
@@ -84,19 +49,8 @@ export const ${varName} = defineApi({
|
|
|
84
49
|
input: ${varName}Input,
|
|
85
50
|
output: ${varName}Output,
|
|
86
51
|
handler: async (input, ctx) => {
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
'${domain.toUpperCase()}_WEBHOOK_SECRET',
|
|
90
|
-
{ provider: input.provider },
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// ── Verify signature ──────────────────────────────────
|
|
94
|
-
const signatureHeader = ctx.headers?.['x-webhook-signature'] ?? '';
|
|
95
|
-
const rawPayload = input.rawBody ?? JSON.stringify(input.payload);
|
|
96
|
-
|
|
97
|
-
if (!verifySignature(input.provider, rawPayload, signatureHeader, secret)) {
|
|
98
|
-
throw new Error('Invalid webhook signature');
|
|
99
|
-
}
|
|
52
|
+
// Signature verification belongs in a Lambda authorizer (x-webhook-signature header
|
|
53
|
+
// is not accessible here). Wire an HttpLambdaAuthorizer in DomainStack for production.
|
|
100
54
|
|
|
101
55
|
const eventId = crypto.randomUUID();
|
|
102
56
|
|
|
@@ -106,13 +60,13 @@ export const ${varName} = defineApi({
|
|
|
106
60
|
provider: input.provider,
|
|
107
61
|
eventType: input.eventType,
|
|
108
62
|
payload: input.payload,
|
|
109
|
-
tenantId: ctx.
|
|
63
|
+
tenantId: ctx.tenant.id,
|
|
110
64
|
}, 1);
|
|
111
65
|
|
|
112
|
-
await ctx.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
66
|
+
await ctx.audit.log('${id}.webhook.received', ctx.tenant.id, {
|
|
67
|
+
provider: input.provider,
|
|
68
|
+
eventType: input.eventType,
|
|
69
|
+
eventId,
|
|
116
70
|
});
|
|
117
71
|
|
|
118
72
|
return {
|
|
@@ -23,31 +23,21 @@ export const ${varName} = defineSubscriber({
|
|
|
23
23
|
event: '${event}',
|
|
24
24
|
semverRange: '^1',
|
|
25
25
|
handler: async (event, ctx) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
sk: 'EVENT#' + event.id,
|
|
29
|
-
eventType: event.type,
|
|
30
|
-
eventVersion: event.version,
|
|
31
|
-
payload: event.data,
|
|
32
|
-
timestamp: event.timestamp,
|
|
33
|
-
source: event.source,
|
|
34
|
-
correlationId: event.correlationId,
|
|
35
|
-
ttl: Math.floor(Date.now() / 1000) + 90 * 86400, // 90-day retention
|
|
36
|
-
};
|
|
26
|
+
const ev = event as { id?: string; type?: string; version?: number; data?: unknown; timestamp?: string };
|
|
27
|
+
const eventId = ev.id ?? crypto.randomUUID();
|
|
37
28
|
|
|
38
|
-
// ── Write
|
|
39
|
-
await ctx.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ctx.metrics?.putMetric('${id}.audit.relayed', 1, 'Count');
|
|
29
|
+
// ── Write immutable audit record ─────────────────────────
|
|
30
|
+
await ctx.store.put(
|
|
31
|
+
'AUDIT#' + eventId,
|
|
32
|
+
{
|
|
33
|
+
eventId,
|
|
34
|
+
eventType: ev.type,
|
|
35
|
+
eventVersion: ev.version,
|
|
36
|
+
payload: ev.data,
|
|
37
|
+
timestamp: ev.timestamp ?? new Date().toISOString(),
|
|
38
|
+
},
|
|
39
|
+
{ ttl: Math.floor(Date.now() / 1000) + 90 * 86400 }, // 90-day retention
|
|
40
|
+
);
|
|
51
41
|
},
|
|
52
42
|
});
|
|
53
43
|
`;
|
|
@@ -25,38 +25,22 @@ export const ${varName} = defineSubscriber({
|
|
|
25
25
|
event: '${event}',
|
|
26
26
|
semverRange: '^1',
|
|
27
27
|
handler: async (event, ctx) => {
|
|
28
|
-
const { entityId
|
|
28
|
+
const data = (event as { data: { entityId: string; deletedBy?: string } }).data;
|
|
29
29
|
|
|
30
30
|
// ── Find all children ────────────────────────────────────
|
|
31
|
-
const children = await ctx.
|
|
32
|
-
pk: 'TENANT#' + tenantId,
|
|
33
|
-
skPrefix: 'CHILD#' + entityId,
|
|
34
|
-
});
|
|
31
|
+
const { items: children } = await ctx.store.query({ skPrefix: 'CHILD#' + data.entityId });
|
|
35
32
|
|
|
36
33
|
// ── Soft-delete each child ───────────────────────────────
|
|
37
34
|
const now = new Date().toISOString();
|
|
38
35
|
for (const child of children) {
|
|
39
|
-
await ctx.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
deletedAt: now,
|
|
43
|
-
deletedBy: event.data.deletedBy ?? 'system',
|
|
44
|
-
},
|
|
36
|
+
await ctx.store.update(child['sk'] as string, {
|
|
37
|
+
deletedAt: now,
|
|
38
|
+
deletedBy: data.deletedBy ?? 'system',
|
|
45
39
|
});
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
// ── Audit log ────────────────────────────────────────────
|
|
49
|
-
await ctx.
|
|
50
|
-
action: 'cascade.delete',
|
|
51
|
-
tenantId,
|
|
52
|
-
entityId,
|
|
53
|
-
meta: {
|
|
54
|
-
childrenDeleted: children.length,
|
|
55
|
-
triggeredBy: event.id,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
ctx.metrics?.putMetric('${id}.cascade.deleted', children.length, 'Count');
|
|
43
|
+
await ctx.audit.log('cascade.delete', data.entityId, { childrenDeleted: children.length });
|
|
60
44
|
},
|
|
61
45
|
});
|
|
62
46
|
`;
|
|
@@ -20,19 +20,15 @@ export const ${varName} = defineSubscriber({
|
|
|
20
20
|
event: '${event}',
|
|
21
21
|
semverRange: '^1',
|
|
22
22
|
handler: async (event, ctx) => {
|
|
23
|
-
const { tenantId, ...eventData } = event.data;
|
|
23
|
+
const { tenantId: _tenantId, ...eventData } = (event as { data: Record<string, unknown> }).data;
|
|
24
|
+
const eventId = (event as { id?: string }).id ?? 'unknown';
|
|
24
25
|
|
|
25
26
|
// ── Upsert projection ────────────────────────────────────
|
|
26
27
|
// Each event updates the projection row for fast reads.
|
|
27
|
-
await ctx.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
lastEventId: event.id,
|
|
32
|
-
projectedAt: new Date().toISOString(),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
ctx.metrics?.putMetric('${id}.projection.applied', 1, 'Count');
|
|
28
|
+
await ctx.store.put(
|
|
29
|
+
'PROJECTION#' + eventId,
|
|
30
|
+
{ ...eventData, lastEventId: eventId, projectedAt: new Date().toISOString() },
|
|
31
|
+
);
|
|
36
32
|
},
|
|
37
33
|
});
|
|
38
34
|
`;
|
|
@@ -21,7 +21,7 @@ export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = d
|
|
|
21
21
|
event: '${eventId}',
|
|
22
22
|
semverRange: '*',
|
|
23
23
|
handler: async (_event, _ctx) => {
|
|
24
|
-
//
|
|
24
|
+
// receive _event.data (typed payload) and use _ctx.store, _ctx.publish, _ctx.audit
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
`;
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { computeChecksumString
|
|
3
|
+
import { computeChecksumString } from './checksum.js';
|
|
4
4
|
import type { ManifestFileEntry } from './manifest.js';
|
|
5
5
|
|
|
6
|
-
export type FilePolicy = '
|
|
6
|
+
export type FilePolicy = 'managed' | 'editable' | 'seed';
|
|
7
7
|
|
|
8
8
|
export interface InstallResult {
|
|
9
|
-
status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged';
|
|
9
|
+
status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged' | 'update-available';
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Install a scaffold file according to its policy.
|
|
14
14
|
*
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* If no currentEntry → write new → 'added'.
|
|
15
|
+
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
16
|
+
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
17
|
+
* (does not write .tib-upgrade or overwrite — user opts in via Updates tab).
|
|
19
18
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
20
19
|
*/
|
|
21
20
|
export async function installScaffoldFile(
|
|
@@ -38,8 +37,8 @@ export async function installScaffoldFile(
|
|
|
38
37
|
}
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
if (policy === '
|
|
42
|
-
//
|
|
40
|
+
if (policy === 'managed') {
|
|
41
|
+
// managed: always overwrite, no sha256 check
|
|
43
42
|
const exists = await fileExists(absPath);
|
|
44
43
|
if (!dryRun) {
|
|
45
44
|
await mkdir(dirname(absPath), { recursive: true });
|
|
@@ -61,7 +60,7 @@ export async function installScaffoldFile(
|
|
|
61
60
|
return { status: 'added' };
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// policy === '
|
|
63
|
+
// policy === 'editable'
|
|
65
64
|
if (!currentEntry) {
|
|
66
65
|
// New file: write it
|
|
67
66
|
if (!dryRun) {
|
|
@@ -77,23 +76,6 @@ export async function installScaffoldFile(
|
|
|
77
76
|
return { status: 'unchanged' };
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
// Content changed:
|
|
81
|
-
|
|
82
|
-
const drifted = diskChecksum !== null && diskChecksum !== currentEntry.sha256;
|
|
83
|
-
|
|
84
|
-
if (drifted) {
|
|
85
|
-
// Drift detected: write .tib-upgrade file, don't touch original
|
|
86
|
-
if (!dryRun) {
|
|
87
|
-
await mkdir(dirname(absPath), { recursive: true });
|
|
88
|
-
await writeFile(`${absPath}.tib-upgrade`, content, 'utf-8');
|
|
89
|
-
}
|
|
90
|
-
return { status: 'conflict' };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// No drift: safe to overwrite
|
|
94
|
-
if (!dryRun) {
|
|
95
|
-
await mkdir(dirname(absPath), { recursive: true });
|
|
96
|
-
await writeFile(absPath, content, 'utf-8');
|
|
97
|
-
}
|
|
98
|
-
return { status: 'updated' };
|
|
79
|
+
// Content changed: report update available, do NOT modify disk
|
|
80
|
+
return { status: 'update-available' };
|
|
99
81
|
}
|
package/src/utils/manifest.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { cliLogger } from './logger.js';
|
|
4
4
|
|
|
5
|
-
export type FilePolicy = '
|
|
5
|
+
export type FilePolicy = 'managed' | 'editable' | 'seed';
|
|
6
6
|
|
|
7
7
|
export interface ManifestFileEntry {
|
|
8
8
|
path: string;
|
|
@@ -36,7 +36,34 @@ export function inferPolicyFromPath(filePath: string): FilePolicy {
|
|
|
36
36
|
filePath === 'mc-deploy.yml' ||
|
|
37
37
|
filePath.match(/^\.github\/workflows\//)
|
|
38
38
|
) {
|
|
39
|
-
return '
|
|
39
|
+
return 'managed';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// managed: core scaffold files not prefixed with infra/
|
|
43
|
+
const managedFilePatterns = [
|
|
44
|
+
/^CLAUDE\.md$/,
|
|
45
|
+
/^\.husky\//,
|
|
46
|
+
/^\.mc\/scaffold-config\.json$/,
|
|
47
|
+
/^\.mc\/modules-hashes\.json$/,
|
|
48
|
+
/^\.npmrc$/,
|
|
49
|
+
/^cdk\.json$/,
|
|
50
|
+
/^\.gitignore$/,
|
|
51
|
+
/^eslint\.config\.js$/,
|
|
52
|
+
/^package\.json$/,
|
|
53
|
+
/^publish-knowledge\.(yml|mjs)$/,
|
|
54
|
+
/^mc-destroy\.yml$/,
|
|
55
|
+
];
|
|
56
|
+
if (managedFilePatterns.some((p) => p.test(filePath))) {
|
|
57
|
+
return 'managed';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// editable: user-customisable config files
|
|
61
|
+
const editableFilePatterns = [
|
|
62
|
+
/^vite\.config\.ts$/,
|
|
63
|
+
/^tailwind\.config\.ts$/,
|
|
64
|
+
];
|
|
65
|
+
if (editableFilePatterns.some((p) => p.test(filePath))) {
|
|
66
|
+
return 'editable';
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
// seed: specific frontend paths
|
|
@@ -53,7 +80,7 @@ export function inferPolicyFromPath(filePath: string): FilePolicy {
|
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
// tracked: everything else
|
|
56
|
-
return '
|
|
83
|
+
return 'editable';
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
export async function readManifest(projectRoot: string): Promise<TibManifest | null> {
|
|
@@ -76,6 +103,24 @@ export async function readManifest(projectRoot: string): Promise<TibManifest | n
|
|
|
76
103
|
await writeManifest(projectRoot, manifest);
|
|
77
104
|
}
|
|
78
105
|
|
|
106
|
+
// v2→v3 migration: rename 'owned'→'managed', 'tracked'→'editable'
|
|
107
|
+
let needsV3Migration = false;
|
|
108
|
+
for (const entry of manifest.files as unknown as Array<Record<string, unknown>>) {
|
|
109
|
+
const rawPolicy = entry['policy'] as string | undefined;
|
|
110
|
+
if (rawPolicy === 'owned') {
|
|
111
|
+
entry['policy'] = 'managed';
|
|
112
|
+
needsV3Migration = true;
|
|
113
|
+
} else if (rawPolicy === 'tracked') {
|
|
114
|
+
entry['policy'] = 'editable';
|
|
115
|
+
needsV3Migration = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (needsV3Migration) {
|
|
120
|
+
cliLogger.info('manifest: migrating v2→v3 with renamed policies');
|
|
121
|
+
await writeManifest(projectRoot, manifest);
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
return manifest;
|
|
80
125
|
} catch (err: unknown) {
|
|
81
126
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
@@ -130,8 +175,8 @@ export function getManifestFile(
|
|
|
130
175
|
return manifest.files.find((f) => f.path === filePath);
|
|
131
176
|
}
|
|
132
177
|
|
|
133
|
-
export function
|
|
178
|
+
export function isScaffoldManaged(manifest: TibManifest, filePath: string): boolean {
|
|
134
179
|
const entry = manifest.files.find((f) => f.path === filePath);
|
|
135
|
-
//
|
|
136
|
-
return entry != null && entry.policy
|
|
180
|
+
// Returns true only when the file is in the manifest with policy 'managed'
|
|
181
|
+
return entry != null && entry.policy === 'managed';
|
|
137
182
|
}
|
|
@@ -43,6 +43,12 @@ export interface CostOptions {
|
|
|
43
43
|
reservedConcurrencyPerDomain?: Record<string, number>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Monitoring / observability feature opt-ins. */
|
|
47
|
+
export interface MonitoringOptions {
|
|
48
|
+
/** Replace basic MonitoringStack with full ObservabilityStack (Grafana + Cost Explorer + CloudWatch). ~$9–18/month */
|
|
49
|
+
enhanced: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/** Lifecycle tunables — Phase B. Schema only in Phase A. */
|
|
47
53
|
export interface LifecycleOptions {
|
|
48
54
|
dlqRetentionDays?: 7 | 14;
|
|
@@ -95,6 +101,8 @@ export interface ScaffoldConfig {
|
|
|
95
101
|
costOptions?: CostOptions;
|
|
96
102
|
/** Lifecycle tunables. Phase B — schema present, CDK not yet wired. */
|
|
97
103
|
lifecycleOptions?: LifecycleOptions;
|
|
104
|
+
/** Monitoring feature opt-ins. All default false. */
|
|
105
|
+
monitoringOptions?: MonitoringOptions;
|
|
98
106
|
/** Per-environment URL configuration for custom domains. */
|
|
99
107
|
environments?: EnvironmentsConfig;
|
|
100
108
|
/** When true, CORS responses include Allow-Credentials: true (requires non-wildcard origins). */
|
|
@@ -117,6 +125,9 @@ const DEFAULT_SCAFFOLD_CONFIG: Omit<ScaffoldConfig, 'domainIds' | 'flowIds'> = {
|
|
|
117
125
|
},
|
|
118
126
|
firstDeployedAt: {},
|
|
119
127
|
logRetentionDays: 30,
|
|
128
|
+
monitoringOptions: {
|
|
129
|
+
enhanced: false,
|
|
130
|
+
},
|
|
120
131
|
};
|
|
121
132
|
|
|
122
133
|
const CONFIG_PATH = '.mc/scaffold-config.json';
|