@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
package/dist/utils/manifest.js
CHANGED
|
@@ -9,7 +9,32 @@ export function inferPolicyFromPath(filePath) {
|
|
|
9
9
|
filePath.match(/^\.tib\/infra\//) ||
|
|
10
10
|
filePath === 'mc-deploy.yml' ||
|
|
11
11
|
filePath.match(/^\.github\/workflows\//)) {
|
|
12
|
-
return '
|
|
12
|
+
return 'managed';
|
|
13
|
+
}
|
|
14
|
+
// managed: core scaffold files not prefixed with infra/
|
|
15
|
+
const managedFilePatterns = [
|
|
16
|
+
/^CLAUDE\.md$/,
|
|
17
|
+
/^\.husky\//,
|
|
18
|
+
/^\.mc\/scaffold-config\.json$/,
|
|
19
|
+
/^\.mc\/modules-hashes\.json$/,
|
|
20
|
+
/^\.npmrc$/,
|
|
21
|
+
/^cdk\.json$/,
|
|
22
|
+
/^\.gitignore$/,
|
|
23
|
+
/^eslint\.config\.js$/,
|
|
24
|
+
/^package\.json$/,
|
|
25
|
+
/^publish-knowledge\.(yml|mjs)$/,
|
|
26
|
+
/^mc-destroy\.yml$/,
|
|
27
|
+
];
|
|
28
|
+
if (managedFilePatterns.some((p) => p.test(filePath))) {
|
|
29
|
+
return 'managed';
|
|
30
|
+
}
|
|
31
|
+
// editable: user-customisable config files
|
|
32
|
+
const editableFilePatterns = [
|
|
33
|
+
/^vite\.config\.ts$/,
|
|
34
|
+
/^tailwind\.config\.ts$/,
|
|
35
|
+
];
|
|
36
|
+
if (editableFilePatterns.some((p) => p.test(filePath))) {
|
|
37
|
+
return 'editable';
|
|
13
38
|
}
|
|
14
39
|
// seed: specific frontend paths
|
|
15
40
|
const seedPatterns = [
|
|
@@ -24,7 +49,7 @@ export function inferPolicyFromPath(filePath) {
|
|
|
24
49
|
return 'seed';
|
|
25
50
|
}
|
|
26
51
|
// tracked: everything else
|
|
27
|
-
return '
|
|
52
|
+
return 'editable';
|
|
28
53
|
}
|
|
29
54
|
export async function readManifest(projectRoot) {
|
|
30
55
|
const manifestPath = path.join(projectRoot, MANIFEST_PATH);
|
|
@@ -43,6 +68,23 @@ export async function readManifest(projectRoot) {
|
|
|
43
68
|
cliLogger.info('manifest: migrating v1→v2 with inferred policies');
|
|
44
69
|
await writeManifest(projectRoot, manifest);
|
|
45
70
|
}
|
|
71
|
+
// v2→v3 migration: rename 'owned'→'managed', 'tracked'→'editable'
|
|
72
|
+
let needsV3Migration = false;
|
|
73
|
+
for (const entry of manifest.files) {
|
|
74
|
+
const rawPolicy = entry['policy'];
|
|
75
|
+
if (rawPolicy === 'owned') {
|
|
76
|
+
entry['policy'] = 'managed';
|
|
77
|
+
needsV3Migration = true;
|
|
78
|
+
}
|
|
79
|
+
else if (rawPolicy === 'tracked') {
|
|
80
|
+
entry['policy'] = 'editable';
|
|
81
|
+
needsV3Migration = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (needsV3Migration) {
|
|
85
|
+
cliLogger.info('manifest: migrating v2→v3 with renamed policies');
|
|
86
|
+
await writeManifest(projectRoot, manifest);
|
|
87
|
+
}
|
|
46
88
|
return manifest;
|
|
47
89
|
}
|
|
48
90
|
catch (err) {
|
|
@@ -86,8 +128,8 @@ export function removeManifestFile(manifest, filePath) {
|
|
|
86
128
|
export function getManifestFile(manifest, filePath) {
|
|
87
129
|
return manifest.files.find((f) => f.path === filePath);
|
|
88
130
|
}
|
|
89
|
-
export function
|
|
131
|
+
export function isScaffoldManaged(manifest, filePath) {
|
|
90
132
|
const entry = manifest.files.find((f) => f.path === filePath);
|
|
91
|
-
//
|
|
92
|
-
return entry != null && entry.policy
|
|
133
|
+
// Returns true only when the file is in the manifest with policy 'managed'
|
|
134
|
+
return entry != null && entry.policy === 'managed';
|
|
93
135
|
}
|
|
@@ -29,6 +29,11 @@ export interface CostOptions {
|
|
|
29
29
|
xRaySamplingRate?: number;
|
|
30
30
|
reservedConcurrencyPerDomain?: Record<string, number>;
|
|
31
31
|
}
|
|
32
|
+
/** Monitoring / observability feature opt-ins. */
|
|
33
|
+
export interface MonitoringOptions {
|
|
34
|
+
/** Replace basic MonitoringStack with full ObservabilityStack (Grafana + Cost Explorer + CloudWatch). ~$9–18/month */
|
|
35
|
+
enhanced: boolean;
|
|
36
|
+
}
|
|
32
37
|
/** Lifecycle tunables — Phase B. Schema only in Phase A. */
|
|
33
38
|
export interface LifecycleOptions {
|
|
34
39
|
dlqRetentionDays?: 7 | 14;
|
|
@@ -81,6 +86,8 @@ export interface ScaffoldConfig {
|
|
|
81
86
|
costOptions?: CostOptions;
|
|
82
87
|
/** Lifecycle tunables. Phase B — schema present, CDK not yet wired. */
|
|
83
88
|
lifecycleOptions?: LifecycleOptions;
|
|
89
|
+
/** Monitoring feature opt-ins. All default false. */
|
|
90
|
+
monitoringOptions?: MonitoringOptions;
|
|
84
91
|
/** Per-environment URL configuration for custom domains. */
|
|
85
92
|
environments?: EnvironmentsConfig;
|
|
86
93
|
/** When true, CORS responses include Allow-Credentials: true (requires non-wildcard origins). */
|
package/package.json
CHANGED
|
@@ -135,7 +135,7 @@ describe('upgrade command', () => {
|
|
|
135
135
|
sha256: oldChecksum,
|
|
136
136
|
wasTemplate: false,
|
|
137
137
|
installedAt: '2026-01-01T00:00:00Z',
|
|
138
|
-
policy: '
|
|
138
|
+
policy: 'editable',
|
|
139
139
|
},
|
|
140
140
|
],
|
|
141
141
|
});
|
|
@@ -242,7 +242,7 @@ describe('upgrade command', () => {
|
|
|
242
242
|
sha256: manifestChecksum, // Original checksum in manifest
|
|
243
243
|
wasTemplate: false,
|
|
244
244
|
installedAt: '2026-01-01T00:00:00Z',
|
|
245
|
-
policy: '
|
|
245
|
+
policy: 'editable',
|
|
246
246
|
},
|
|
247
247
|
],
|
|
248
248
|
});
|
|
@@ -300,7 +300,7 @@ describe('upgrade command', () => {
|
|
|
300
300
|
|
|
301
301
|
const tarballContent = createMockTarball([
|
|
302
302
|
{ path: 'MANIFEST.json', content: JSON.stringify([
|
|
303
|
-
{ path: filePath, sha256: newChecksum, isTemplate: false, policy: '
|
|
303
|
+
{ path: filePath, sha256: newChecksum, isTemplate: false, policy: 'editable' },
|
|
304
304
|
]) },
|
|
305
305
|
{ path: filePath, content: newContent },
|
|
306
306
|
]);
|
|
@@ -346,7 +346,7 @@ describe('upgrade command', () => {
|
|
|
346
346
|
sha256: oldChecksum,
|
|
347
347
|
wasTemplate: false,
|
|
348
348
|
installedAt: '2026-01-01T00:00:00Z',
|
|
349
|
-
policy: '
|
|
349
|
+
policy: 'managed',
|
|
350
350
|
},
|
|
351
351
|
],
|
|
352
352
|
});
|
|
@@ -449,7 +449,7 @@ describe('upgrade command', () => {
|
|
|
449
449
|
sha256: checksum,
|
|
450
450
|
wasTemplate: false,
|
|
451
451
|
installedAt: '2026-01-01T00:00:00Z',
|
|
452
|
-
policy: '
|
|
452
|
+
policy: 'managed',
|
|
453
453
|
},
|
|
454
454
|
],
|
|
455
455
|
});
|
|
@@ -507,7 +507,7 @@ describe('upgrade command', () => {
|
|
|
507
507
|
|
|
508
508
|
const tarballContent = createMockTarball([
|
|
509
509
|
{ path: 'MANIFEST.json', content: JSON.stringify([
|
|
510
|
-
{ path: filePath, sha256: checksum, isTemplate: false, policy: '
|
|
510
|
+
{ path: filePath, sha256: checksum, isTemplate: false, policy: 'managed' },
|
|
511
511
|
]) },
|
|
512
512
|
{ path: filePath, content },
|
|
513
513
|
]);
|
|
@@ -549,7 +549,7 @@ describe('upgrade command', () => {
|
|
|
549
549
|
sha256: manifestChecksum,
|
|
550
550
|
wasTemplate: false,
|
|
551
551
|
installedAt: '2026-01-01T00:00:00Z',
|
|
552
|
-
policy: '
|
|
552
|
+
policy: 'editable',
|
|
553
553
|
},
|
|
554
554
|
],
|
|
555
555
|
});
|
|
@@ -607,7 +607,7 @@ describe('upgrade command', () => {
|
|
|
607
607
|
|
|
608
608
|
const tarballContent = createMockTarball([
|
|
609
609
|
{ path: 'MANIFEST.json', content: JSON.stringify([
|
|
610
|
-
{ path: filePath, sha256: newChecksum, isTemplate: false, policy: '
|
|
610
|
+
{ path: filePath, sha256: newChecksum, isTemplate: false, policy: 'editable' },
|
|
611
611
|
]) },
|
|
612
612
|
{ path: filePath, content: newContent },
|
|
613
613
|
]);
|
|
@@ -114,6 +114,7 @@ describe('runDoctor', () => {
|
|
|
114
114
|
describe('layout validation checks', () => {
|
|
115
115
|
it('PASS when manifest has owned files in .mc/', async () => {
|
|
116
116
|
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
117
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
117
118
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
118
119
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
119
120
|
await mkdir(join(tempDir, '.tib', 'infra', 'modules'), { recursive: true });
|
|
@@ -132,10 +133,10 @@ describe('runDoctor', () => {
|
|
|
132
133
|
enabledModules: ['backend-lambda'],
|
|
133
134
|
files: [
|
|
134
135
|
{ path: '.mc/infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
135
|
-
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: '
|
|
136
|
+
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
136
137
|
]
|
|
137
138
|
};
|
|
138
|
-
await writeFile(join(tempDir, '.
|
|
139
|
+
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
139
140
|
|
|
140
141
|
const report = await runDoctor({ projectRoot: tempDir });
|
|
141
142
|
const check = report.checks.find(c => c.name === 'Layout: owned files in .mc/');
|
|
@@ -144,6 +145,7 @@ describe('runDoctor', () => {
|
|
|
144
145
|
|
|
145
146
|
it('FAIL when owned file is outside .mc/ (old layout)', async () => {
|
|
146
147
|
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
148
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
147
149
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
148
150
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
149
151
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
@@ -162,10 +164,10 @@ describe('runDoctor', () => {
|
|
|
162
164
|
enabledModules: ['backend-lambda'],
|
|
163
165
|
files: [
|
|
164
166
|
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
165
|
-
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: '
|
|
167
|
+
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
166
168
|
]
|
|
167
169
|
};
|
|
168
|
-
await writeFile(join(tempDir, '.
|
|
170
|
+
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
169
171
|
|
|
170
172
|
const report = await runDoctor({ projectRoot: tempDir });
|
|
171
173
|
const check = report.checks.find(c => c.name === 'Layout: owned files in .mc/');
|
|
@@ -174,6 +176,7 @@ describe('runDoctor', () => {
|
|
|
174
176
|
|
|
175
177
|
it('FAIL when manifest entry points at missing file', async () => {
|
|
176
178
|
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
179
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
177
180
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
178
181
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
179
182
|
|
|
@@ -190,10 +193,10 @@ describe('runDoctor', () => {
|
|
|
190
193
|
enabledModules: ['backend-lambda'],
|
|
191
194
|
files: [
|
|
192
195
|
{ path: '.mc/infra/missing-file.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
193
|
-
sha256: 'abc', wasTemplate: false, installedAt: '2026-01-01T00:00:00Z', policy: '
|
|
196
|
+
sha256: 'abc', wasTemplate: false, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
194
197
|
]
|
|
195
198
|
};
|
|
196
|
-
await writeFile(join(tempDir, '.
|
|
199
|
+
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
197
200
|
|
|
198
201
|
const report = await runDoctor({ projectRoot: tempDir });
|
|
199
202
|
const check = report.checks.find(c => c.name === 'Layout: manifest entries exist on disk');
|
|
@@ -204,6 +207,7 @@ describe('runDoctor', () => {
|
|
|
204
207
|
describe('--relocate flag', () => {
|
|
205
208
|
it('moves scaffold-owned files from old layout to .mc/', async () => {
|
|
206
209
|
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
210
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
207
211
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
208
212
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
209
213
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
@@ -226,16 +230,16 @@ describe('runDoctor', () => {
|
|
|
226
230
|
enabledModules: ['backend-lambda'],
|
|
227
231
|
files: [
|
|
228
232
|
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
229
|
-
sha256, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: '
|
|
233
|
+
sha256, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
230
234
|
]
|
|
231
235
|
};
|
|
232
|
-
await writeFile(join(tempDir, '.
|
|
236
|
+
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
233
237
|
|
|
234
238
|
const report = await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
235
239
|
|
|
236
240
|
// File moved
|
|
237
241
|
const { existsSync } = await import('node:fs');
|
|
238
|
-
expect(existsSync(join(tempDir, '.
|
|
242
|
+
expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
239
243
|
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
240
244
|
|
|
241
245
|
// Summary pass
|
|
@@ -245,6 +249,7 @@ describe('runDoctor', () => {
|
|
|
245
249
|
|
|
246
250
|
it('leaves hand-edited files in place with warning on --relocate', async () => {
|
|
247
251
|
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
252
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
248
253
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
249
254
|
|
|
250
255
|
const originalContent = '// @mc-scaffold: backend-lambda@1.0.0\noriginal\n';
|
|
@@ -265,10 +270,10 @@ describe('runDoctor', () => {
|
|
|
265
270
|
enabledModules: ['backend-lambda'],
|
|
266
271
|
files: [
|
|
267
272
|
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
268
|
-
sha256: originalSha, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: '
|
|
273
|
+
sha256: originalSha, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
269
274
|
]
|
|
270
275
|
};
|
|
271
|
-
await writeFile(join(tempDir, '.
|
|
276
|
+
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
272
277
|
|
|
273
278
|
await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
274
279
|
|
|
@@ -35,12 +35,4 @@ describe('scaffold-src layout (Part A)', () => {
|
|
|
35
35
|
expect(content).toContain('tib-ci.yml');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it('backend-lambda tarball-manifest.json maps .mc/deploy.yml.tpl to .github/workflows/mc-deploy.yml', () => {
|
|
39
|
-
const tmPath = join(backendLambdaDir, 'tarball-manifest.json');
|
|
40
|
-
expect(existsSync(tmPath)).toBe(true);
|
|
41
|
-
const tm = JSON.parse(readFileSync(tmPath, 'utf-8'));
|
|
42
|
-
const deployOverride = tm.fileOverrides.find((e: { path: string }) => e.path === '.mc/deploy.yml.tpl');
|
|
43
|
-
expect(deployOverride?.installedAs).toBe('.github/workflows/mc-deploy.yml');
|
|
44
|
-
expect(deployOverride?.policy).toBe('owned');
|
|
45
|
-
});
|
|
46
38
|
});
|
|
@@ -4,7 +4,7 @@ describe('package-scaffold policy inference', () => {
|
|
|
4
4
|
function inferPolicyFromPath(filePath: string): string {
|
|
5
5
|
if (/^infra\//.test(filePath) || /^\.tib\/infra\//.test(filePath) ||
|
|
6
6
|
filePath === 'mc-deploy.yml' || /^\.github\/workflows\//.test(filePath)) {
|
|
7
|
-
return '
|
|
7
|
+
return 'managed';
|
|
8
8
|
}
|
|
9
9
|
const seedPatterns = [
|
|
10
10
|
/^frontend\/src\/pages\/auth\//,
|
|
@@ -15,18 +15,18 @@ describe('package-scaffold policy inference', () => {
|
|
|
15
15
|
/^frontend\/src\/styles\/brand\.css$/,
|
|
16
16
|
];
|
|
17
17
|
if (seedPatterns.some(p => p.test(filePath))) return 'seed';
|
|
18
|
-
return '
|
|
18
|
+
return 'editable';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
it('returns
|
|
22
|
-
expect(inferPolicyFromPath('.mc/infra/modules/app.ts')).toBe('
|
|
23
|
-
expect(inferPolicyFromPath('.mc/infra/cdk.json')).toBe('
|
|
24
|
-
expect(inferPolicyFromPath('.mc/infra/tsconfig.json')).toBe('
|
|
21
|
+
it('returns managed for .mc/infra/ paths', () => {
|
|
22
|
+
expect(inferPolicyFromPath('.mc/infra/modules/app.ts')).toBe('managed');
|
|
23
|
+
expect(inferPolicyFromPath('.mc/infra/cdk.json')).toBe('managed');
|
|
24
|
+
expect(inferPolicyFromPath('.mc/infra/tsconfig.json')).toBe('managed');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('returns
|
|
28
|
-
expect(inferPolicyFromPath('.github/workflows/tib-ci.yml')).toBe('
|
|
29
|
-
expect(inferPolicyFromPath('.github/workflows/mc-deploy.yml')).toBe('
|
|
27
|
+
it('returns managed for .github/workflows/ paths', () => {
|
|
28
|
+
expect(inferPolicyFromPath('.github/workflows/tib-ci.yml')).toBe('managed');
|
|
29
|
+
expect(inferPolicyFromPath('.github/workflows/mc-deploy.yml')).toBe('managed');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('returns seed for frontend page paths', () => {
|
|
@@ -34,12 +34,12 @@ describe('package-scaffold policy inference', () => {
|
|
|
34
34
|
expect(inferPolicyFromPath('frontend/src/styles/brand.css')).toBe('seed');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('returns
|
|
38
|
-
expect(inferPolicyFromPath('domains/payments/api/charge.ts')).toBe('
|
|
39
|
-
expect(inferPolicyFromPath('packages/domain-cli/src/utils/manifest.ts')).toBe('
|
|
37
|
+
it('returns editable for domain files', () => {
|
|
38
|
+
expect(inferPolicyFromPath('domains/payments/api/charge.ts')).toBe('editable');
|
|
39
|
+
expect(inferPolicyFromPath('packages/domain-cli/src/utils/manifest.ts')).toBe('editable');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('returns
|
|
43
|
-
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('
|
|
42
|
+
it('returns managed for mc-deploy.yml', () => {
|
|
43
|
+
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('managed');
|
|
44
44
|
});
|
|
45
45
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtemp, rmdir, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { mkdtemp, rmdir, writeFile, readFile, mkdir, access } from 'node:fs/promises';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { installScaffoldFile, type FilePolicy } from '../../utils/install-file.js';
|
|
6
7
|
import { computeChecksumString } from '../../utils/checksum.js';
|
|
7
8
|
import type { ManifestFileEntry } from '../../utils/manifest.js';
|
|
@@ -52,15 +53,15 @@ describe('installScaffoldFile', () => {
|
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
describe('
|
|
56
|
+
describe('managed policy', () => {
|
|
56
57
|
it('overwrites existing file with updated status', async () => {
|
|
57
|
-
const filePath = join(tmpDir, 'test-
|
|
58
|
+
const filePath = join(tmpDir, 'test-managed.txt');
|
|
58
59
|
const oldContent = 'old content';
|
|
59
60
|
const newContent = 'new content';
|
|
60
61
|
|
|
61
62
|
await writeFile(filePath, oldContent, 'utf-8');
|
|
62
63
|
|
|
63
|
-
const result = await installScaffoldFile(filePath, newContent, '
|
|
64
|
+
const result = await installScaffoldFile(filePath, newContent, 'managed', undefined);
|
|
64
65
|
|
|
65
66
|
expect(result.status).toBe('updated');
|
|
66
67
|
|
|
@@ -70,10 +71,10 @@ describe('installScaffoldFile', () => {
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
it('writes new file with added status', async () => {
|
|
73
|
-
const filePath = join(tmpDir, 'test-
|
|
74
|
+
const filePath = join(tmpDir, 'test-managed-new.txt');
|
|
74
75
|
const content = 'new content';
|
|
75
76
|
|
|
76
|
-
const result = await installScaffoldFile(filePath, content, '
|
|
77
|
+
const result = await installScaffoldFile(filePath, content, 'managed', undefined);
|
|
77
78
|
|
|
78
79
|
expect(result.status).toBe('added');
|
|
79
80
|
|
|
@@ -83,25 +84,25 @@ describe('installScaffoldFile', () => {
|
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
describe('
|
|
87
|
+
describe('editable policy', () => {
|
|
87
88
|
it('returns unchanged when checksum matches', async () => {
|
|
88
|
-
const filePath = join(tmpDir, 'test-
|
|
89
|
+
const filePath = join(tmpDir, 'test-editable.txt');
|
|
89
90
|
const content = 'content';
|
|
90
91
|
const checksum = computeChecksumString(content);
|
|
91
92
|
|
|
92
93
|
await writeFile(filePath, content, 'utf-8');
|
|
93
94
|
|
|
94
95
|
const currentEntry: ManifestFileEntry = {
|
|
95
|
-
path: 'test-
|
|
96
|
+
path: 'test-editable.txt',
|
|
96
97
|
module: 'core',
|
|
97
98
|
moduleVersion: '1.0.0',
|
|
98
99
|
sha256: checksum,
|
|
99
100
|
wasTemplate: false,
|
|
100
101
|
installedAt: '2026-01-01T00:00:00Z',
|
|
101
|
-
policy: '
|
|
102
|
+
policy: 'editable',
|
|
102
103
|
};
|
|
103
104
|
|
|
104
|
-
const result = await installScaffoldFile(filePath, content, '
|
|
105
|
+
const result = await installScaffoldFile(filePath, content, 'editable', currentEntry);
|
|
105
106
|
|
|
106
107
|
expect(result.status).toBe('unchanged');
|
|
107
108
|
|
|
@@ -110,8 +111,8 @@ describe('installScaffoldFile', () => {
|
|
|
110
111
|
expect(fileContent).toBe(content);
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
it('
|
|
114
|
-
const filePath = join(tmpDir, 'test-
|
|
114
|
+
it('returns update-available when no drift and sha256 differs', async () => {
|
|
115
|
+
const filePath = join(tmpDir, 'test-editable-update.txt');
|
|
115
116
|
const oldContent = 'old content';
|
|
116
117
|
const newContent = 'new content';
|
|
117
118
|
const oldChecksum = computeChecksumString(oldContent);
|
|
@@ -119,26 +120,52 @@ describe('installScaffoldFile', () => {
|
|
|
119
120
|
await writeFile(filePath, oldContent, 'utf-8');
|
|
120
121
|
|
|
121
122
|
const currentEntry: ManifestFileEntry = {
|
|
122
|
-
path: 'test-
|
|
123
|
+
path: 'test-editable-update.txt',
|
|
123
124
|
module: 'core',
|
|
124
125
|
moduleVersion: '1.0.0',
|
|
125
126
|
sha256: oldChecksum,
|
|
126
127
|
wasTemplate: false,
|
|
127
128
|
installedAt: '2026-01-01T00:00:00Z',
|
|
128
|
-
policy: '
|
|
129
|
+
policy: 'editable',
|
|
129
130
|
};
|
|
130
131
|
|
|
131
|
-
const result = await installScaffoldFile(filePath, newContent, '
|
|
132
|
+
const result = await installScaffoldFile(filePath, newContent, 'editable', currentEntry);
|
|
132
133
|
|
|
133
|
-
expect(result.status).toBe('
|
|
134
|
+
expect(result.status).toBe('update-available');
|
|
134
135
|
|
|
135
|
-
// File should be
|
|
136
|
+
// File should NOT be overwritten
|
|
136
137
|
const content = await readFile(filePath, 'utf-8');
|
|
137
|
-
expect(content).toBe(
|
|
138
|
+
expect(content).toBe(oldContent);
|
|
138
139
|
});
|
|
139
140
|
|
|
140
|
-
it('
|
|
141
|
-
const
|
|
141
|
+
it('returns update-available when editable content differs and file exists', async () => {
|
|
142
|
+
const existingContent = 'old content';
|
|
143
|
+
const newContent = 'updated content';
|
|
144
|
+
const filePath = join(tmpDir, 'lib', 'utils.ts');
|
|
145
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
146
|
+
await writeFile(filePath, existingContent, 'utf-8');
|
|
147
|
+
|
|
148
|
+
const currentEntry = {
|
|
149
|
+
path: 'lib/utils.ts',
|
|
150
|
+
module: 'frontend',
|
|
151
|
+
moduleVersion: '1.0.0',
|
|
152
|
+
sha256: createHash('sha256').update(existingContent).digest('hex'),
|
|
153
|
+
wasTemplate: false,
|
|
154
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
155
|
+
policy: 'editable' as const,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = await installScaffoldFile(filePath, newContent, 'editable', currentEntry);
|
|
159
|
+
expect(result.status).toBe('update-available');
|
|
160
|
+
// Verify NO .tib-upgrade file written
|
|
161
|
+
await expect(access(filePath + '.tib-upgrade')).rejects.toThrow();
|
|
162
|
+
// Verify original file NOT overwritten
|
|
163
|
+
const diskContent = await readFile(filePath, 'utf-8');
|
|
164
|
+
expect(diskContent).toBe(existingContent);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns update-available when file drifted', async () => {
|
|
168
|
+
const filePath = join(tmpDir, 'test-editable-drift.txt');
|
|
142
169
|
const manifestContent = 'original content';
|
|
143
170
|
const driftedContent = 'user modified content';
|
|
144
171
|
const newContent = 'new content from upgrade';
|
|
@@ -148,33 +175,29 @@ describe('installScaffoldFile', () => {
|
|
|
148
175
|
await writeFile(filePath, driftedContent, 'utf-8');
|
|
149
176
|
|
|
150
177
|
const currentEntry: ManifestFileEntry = {
|
|
151
|
-
path: 'test-
|
|
178
|
+
path: 'test-editable-drift.txt',
|
|
152
179
|
module: 'core',
|
|
153
180
|
moduleVersion: '1.0.0',
|
|
154
181
|
sha256: manifestChecksum,
|
|
155
182
|
wasTemplate: false,
|
|
156
183
|
installedAt: '2026-01-01T00:00:00Z',
|
|
157
|
-
policy: '
|
|
184
|
+
policy: 'editable',
|
|
158
185
|
};
|
|
159
186
|
|
|
160
|
-
const result = await installScaffoldFile(filePath, newContent, '
|
|
187
|
+
const result = await installScaffoldFile(filePath, newContent, 'editable', currentEntry);
|
|
161
188
|
|
|
162
|
-
expect(result.status).toBe('
|
|
189
|
+
expect(result.status).toBe('update-available');
|
|
163
190
|
|
|
164
191
|
// Original file should not be modified
|
|
165
192
|
const originalContent = await readFile(filePath, 'utf-8');
|
|
166
193
|
expect(originalContent).toBe(driftedContent);
|
|
167
|
-
|
|
168
|
-
// .tib-upgrade file should be created
|
|
169
|
-
const conflictFile = await readFile(`${filePath}.tib-upgrade`, 'utf-8');
|
|
170
|
-
expect(conflictFile).toBe(newContent);
|
|
171
194
|
});
|
|
172
195
|
|
|
173
|
-
it('adds new
|
|
174
|
-
const filePath = join(tmpDir, 'test-
|
|
175
|
-
const content = 'new
|
|
196
|
+
it('adds new editable file when no currentEntry', async () => {
|
|
197
|
+
const filePath = join(tmpDir, 'test-editable-new.txt');
|
|
198
|
+
const content = 'new editable file';
|
|
176
199
|
|
|
177
|
-
const result = await installScaffoldFile(filePath, content, '
|
|
200
|
+
const result = await installScaffoldFile(filePath, content, 'editable', undefined);
|
|
178
201
|
|
|
179
202
|
expect(result.status).toBe('added');
|
|
180
203
|
|
|
@@ -189,7 +212,7 @@ describe('installScaffoldFile', () => {
|
|
|
189
212
|
const filePath = join(tmpDir, 'test-dryrun.txt');
|
|
190
213
|
const content = 'new content';
|
|
191
214
|
|
|
192
|
-
const result = await installScaffoldFile(filePath, content, '
|
|
215
|
+
const result = await installScaffoldFile(filePath, content, 'managed', undefined, { dryRun: true });
|
|
193
216
|
|
|
194
217
|
expect(result.status).toBe('added');
|
|
195
218
|
|