@mettlecast/domain-cli 0.2.59 → 0.2.60
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 +128 -13
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +2 -2
- package/dist/commands/build-catalog.d.ts +2 -2
- package/dist/commands/build-catalog.js +5 -5
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +5 -5
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +44 -165
- package/dist/commands/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +183 -0
- package/dist/utils/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +2 -2
- package/dist/utils/scaffold-config.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/commands/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/package-freshness.test.ts +114 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +652 -1
- package/src/builder/build-registry.ts +147 -15
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +3 -3
- package/src/commands/build-catalog.ts +5 -5
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +5 -5
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/doctor.ts +52 -181
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +226 -1
- package/src/utils/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +3 -3
|
@@ -5,7 +5,7 @@ function toZoneLine(r) {
|
|
|
5
5
|
return `${r.name}. 300 IN ${r.type} ${r.value}.`;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* mc-domain-module show-dns [--env dev|staging|prod] [--project-dir <path>]
|
|
9
9
|
*
|
|
10
10
|
* Reads the scaffold-config.json and prints DNS records that must be added for
|
|
11
11
|
* custom domain wiring: certificate validation CNAMEs (step 1) and traffic
|
|
@@ -16,25 +16,25 @@ function toZoneLine(r) {
|
|
|
16
16
|
*/
|
|
17
17
|
export async function runShowDns(opts) {
|
|
18
18
|
const projectDir = opts.projectDir ? path.resolve(opts.projectDir) : process.cwd();
|
|
19
|
-
const configPath = path.join(projectDir, '.
|
|
19
|
+
const configPath = path.join(projectDir, '.mc', 'scaffold-config.json');
|
|
20
20
|
let config;
|
|
21
21
|
try {
|
|
22
22
|
const raw = await readFile(configPath, 'utf-8');
|
|
23
23
|
config = JSON.parse(raw);
|
|
24
24
|
}
|
|
25
25
|
catch {
|
|
26
|
-
console.error('[
|
|
26
|
+
console.error('[mc-domain-module] Could not read .mc/scaffold-config.json — run from a Mettlecast project root.');
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
29
|
const environments = config.environments ?? {};
|
|
30
30
|
if (Object.keys(environments).length === 0) {
|
|
31
|
-
console.log('[
|
|
31
|
+
console.log('[mc-domain-module] No environments configured. Set a custom domain via `npx mc-domain-module show-dns` or the Setup tab.');
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
// Try to load CDK outputs for routing records
|
|
35
35
|
let cdkOutputs = {};
|
|
36
36
|
try {
|
|
37
|
-
const outputsPath = path.join(projectDir, '.
|
|
37
|
+
const outputsPath = path.join(projectDir, '.mc', 'cdk-outputs.json');
|
|
38
38
|
const raw = await readFile(outputsPath, 'utf-8');
|
|
39
39
|
cdkOutputs = JSON.parse(raw);
|
|
40
40
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createGunzip } from 'node:zlib';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { unlink } from 'node:fs/promises';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
3
|
+
import { mkdir, writeFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import ky from 'ky';
|
|
7
7
|
import { cliLogger } from '../utils/logger.js';
|
|
@@ -176,9 +176,9 @@ function buildPrBody(currentVersion, targetVersion, results, changelogUrl) {
|
|
|
176
176
|
if (conflicts.length > 0) {
|
|
177
177
|
lines.push('### Drift warnings — manual review required');
|
|
178
178
|
lines.push('The following files were locally modified after scaffold installation. ' +
|
|
179
|
-
'New versions have been written to `{path}.
|
|
179
|
+
'New versions have been written to `{path}.mc-upgrade` — merge manually then remove the `.mc-upgrade` file.');
|
|
180
180
|
for (const r of conflicts)
|
|
181
|
-
lines.push(`- \`${r.path}\` → \`${r.path}.
|
|
181
|
+
lines.push(`- \`${r.path}\` → \`${r.path}.mc-upgrade\``);
|
|
182
182
|
lines.push('');
|
|
183
183
|
}
|
|
184
184
|
if (changelogUrl) {
|
|
@@ -235,9 +235,9 @@ function buildFrontendComponentsPrBody(currentVersion, targetVersion, results, c
|
|
|
235
235
|
if (conflicts.length > 0) {
|
|
236
236
|
lines.push('### Drift warnings — manual review required');
|
|
237
237
|
lines.push('The following files were locally modified after scaffold installation. ' +
|
|
238
|
-
'New versions have been written to `{path}.
|
|
238
|
+
'New versions have been written to `{path}.mc-upgrade` — merge manually then remove the `.mc-upgrade` file.');
|
|
239
239
|
for (const r of conflicts)
|
|
240
|
-
lines.push(`- \`${r.path}\` → \`${r.path}.
|
|
240
|
+
lines.push(`- \`${r.path}\` → \`${r.path}.mc-upgrade\``);
|
|
241
241
|
lines.push('');
|
|
242
242
|
}
|
|
243
243
|
if (changelogUrl) {
|
|
@@ -367,12 +367,18 @@ export async function runUpgrade(packageSpec, opts) {
|
|
|
367
367
|
// Skipped files are not tracked in results (e.g., seed file with --frontend-components)
|
|
368
368
|
continue;
|
|
369
369
|
}
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
// update-available: do NOT update manifest (file was not written to disk)
|
|
370
|
+
// update-available: write the proposed new content beside the drifted file
|
|
371
|
+
// for manual merge, but do NOT update the manifest or overwrite the file.
|
|
373
372
|
if (installResult.status === 'update-available') {
|
|
373
|
+
if (!opts.dryRun) {
|
|
374
|
+
await mkdir(dirname(diskPath), { recursive: true });
|
|
375
|
+
await writeFile(`${diskPath}.mc-upgrade`, newContent, 'utf-8');
|
|
376
|
+
}
|
|
377
|
+
allResults.push({ path: installPath, status: 'conflict', module: mod.id });
|
|
374
378
|
continue;
|
|
375
379
|
}
|
|
380
|
+
// Add to results
|
|
381
|
+
allResults.push({ path: installPath, status: installResult.status, module: mod.id });
|
|
376
382
|
// Update manifest if file was not skipped
|
|
377
383
|
const newChecksum = computeChecksumString(newContent);
|
|
378
384
|
upsertManifestFile(updatedManifest, {
|
|
@@ -417,7 +423,7 @@ export async function runUpgrade(packageSpec, opts) {
|
|
|
417
423
|
if (conflicts.length > 0) {
|
|
418
424
|
console.log('\nDrift warnings:');
|
|
419
425
|
for (const r of conflicts) {
|
|
420
|
-
console.log(` ${r.path} — new version written to ${r.path}.
|
|
426
|
+
console.log(` ${r.path} — new version written to ${r.path}.mc-upgrade`);
|
|
421
427
|
}
|
|
422
428
|
}
|
|
423
429
|
if (opts.check) {
|
|
@@ -452,7 +458,7 @@ export async function runUpgrade(packageSpec, opts) {
|
|
|
452
458
|
const updatedConfig = { ...scaffoldConfig, scaffoldVersion: targetVersion };
|
|
453
459
|
await writeScaffoldConfig(projectDir, updatedConfig);
|
|
454
460
|
// 10. Commit on upgrade branch
|
|
455
|
-
const branchName = `
|
|
461
|
+
const branchName = `mc-upgrade/${targetVersion}`;
|
|
456
462
|
try {
|
|
457
463
|
execSync(`git -C "${projectDir}" checkout -b "${branchName}"`, {
|
|
458
464
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -78,6 +78,180 @@ async function checkRawPathViolations(_repoRoot, config) {
|
|
|
78
78
|
return [];
|
|
79
79
|
return [];
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Deployment-time security checks (#4662 Task D).
|
|
83
|
+
*
|
|
84
|
+
* Mirrors the invariants enforced at CDK synth time by
|
|
85
|
+
* `SecurityAssertionAspect` in `@mettlecast/domain-cdk-packer`. Running
|
|
86
|
+
* them at the CLI stage means developers get a structured error code in
|
|
87
|
+
* their terminal and CI fails on `mc-domain-module validate` BEFORE a
|
|
88
|
+
* (potentially expensive) `cdk synth` is attempted.
|
|
89
|
+
*
|
|
90
|
+
* Each rule maps 1:1 to an aspect annotation code so downstream tooling
|
|
91
|
+
* can correlate build-time and synth-time failures.
|
|
92
|
+
*
|
|
93
|
+
* NOTE on `SECURITY_MISSING_SECURITY_EXCEPTION` for legacy `defineApi`
|
|
94
|
+
* entries: the registry builder does not propagate `securityException`
|
|
95
|
+
* from legacy source — that field only exists on action-style API
|
|
96
|
+
* exposures, which are covered by the existing `AUTH_NONE_REQUIRES_EXCEPTION`
|
|
97
|
+
* rule in `checkActionFirstSecurity`. The aspect's corresponding check
|
|
98
|
+
* (`assertAnonymousRoutesHaveException`) catches the legacy case at
|
|
99
|
+
* synth time so a developer migrating to action-style APIs gets the
|
|
100
|
+
* right guidance at the right layer.
|
|
101
|
+
*/
|
|
102
|
+
function checkDeploymentSecurity(apis, actions) {
|
|
103
|
+
const errors = [];
|
|
104
|
+
for (const api of apis) {
|
|
105
|
+
// Non-anonymous APIs whose path lacks the tenant placeholder cannot
|
|
106
|
+
// bind `ctx.tenant.id` from the URL. The aspect surfaces this as
|
|
107
|
+
// `SECURITY_MISSING_TENANT_PATH`; the CLI version is a faster gate so
|
|
108
|
+
// CI doesn't pay the synth cost.
|
|
109
|
+
if (api.authType !== 'none' && !api.path.includes('/v1/tenants/{tenantId}/')) {
|
|
110
|
+
errors.push({
|
|
111
|
+
code: 'SECURITY_MISSING_TENANT_PATH',
|
|
112
|
+
message: `API '${api.id}' (${api.method} ${api.path}) is JWT-protected but its path does not include the canonical '/v1/tenants/{tenantId}/' placeholder. The runtime cannot bind ctx.tenant.id from a path that lacks the placeholder.`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Action API exposures are already covered by `checkActionFirstSecurity`
|
|
117
|
+
// for `tenancy: 'required'` paths and `auth: 'none'` exceptions. The
|
|
118
|
+
// codes there (TENANT_API_PATH_REQUIRED, AUTH_NONE_REQUIRES_EXCEPTION)
|
|
119
|
+
// are kept stable for back-compat — they map to the same aspect codes.
|
|
120
|
+
// Suppress the unused-actions lint while keeping the parameter shape
|
|
121
|
+
// for the future where actions gain new deployment-time invariants.
|
|
122
|
+
void actions;
|
|
123
|
+
return errors;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Action-first security validation rules (#4619, Wave 6 Task 6.1).
|
|
127
|
+
*
|
|
128
|
+
* These checks enforce the action-first security model on the
|
|
129
|
+
* serialised DomainRegistry produced by `buildRegistry`. The rules
|
|
130
|
+
* intentionally mirror the Zod cross-field refinements defined in
|
|
131
|
+
* `@mettlecast/domain-runtime/primitives/action` (`ApiExposureSchema`)
|
|
132
|
+
* so violations are caught at build time, not at runtime.
|
|
133
|
+
*
|
|
134
|
+
* Each rule emits a structured `ValidationError` whose `code` is the
|
|
135
|
+
* rule ID listed in the spec (e.g. `ACTION_EXPOSURE_REQUIRED`,
|
|
136
|
+
* `TENANT_API_PATH_REQUIRED`). Messages include the offending action
|
|
137
|
+
* or API id and the actionable fix.
|
|
138
|
+
*/
|
|
139
|
+
function checkActionFirstSecurity(actions) {
|
|
140
|
+
const errors = [];
|
|
141
|
+
for (const action of actions) {
|
|
142
|
+
const id = action.id;
|
|
143
|
+
// ACTION_EXPOSURE_REQUIRED — every action registry entry must have
|
|
144
|
+
// `exposure` declared in source. The builder defaults missing
|
|
145
|
+
// exposure to `{ type: 'internal' }` for backwards compatibility,
|
|
146
|
+
// and flags the entry with `exposureDeclared: false` so the validator
|
|
147
|
+
// can surface it. Legacy actions that use `visibility` only (no
|
|
148
|
+
// `backendAccess`) are also allowed to default during migration.
|
|
149
|
+
if (action.exposureDeclared === false && action.backendAccess !== 'private') {
|
|
150
|
+
// Only fire for non-private actions: a private action with no
|
|
151
|
+
// exposure is the natural migration state for legacy
|
|
152
|
+
// visibility:'private' handlers, and forcing exposure would
|
|
153
|
+
// produce noisy errors during the migration window.
|
|
154
|
+
// (When Wave 7+ removes the legacy visibility alias this branch
|
|
155
|
+
// becomes a hard error for every action.)
|
|
156
|
+
errors.push({
|
|
157
|
+
code: 'ACTION_EXPOSURE_REQUIRED',
|
|
158
|
+
message: `Action '${id}' has no explicit \`exposure\`. New-style actions must declare \`exposure\` (e.g. \`{ type: 'api', path: '...', method: 'POST', auth: 'required', tenancy: 'required' }\`) or \`{ type: 'internal' }\` to opt out of API exposure.`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (action.exposure.type === 'api') {
|
|
162
|
+
errors.push(...checkApiExposureSecurity(action.id, action.exposure));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return errors;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Validate a single API-exposed action. Pulled out as a separate helper
|
|
169
|
+
* so each sub-rule is independently testable and the messages stay short.
|
|
170
|
+
*/
|
|
171
|
+
function checkApiExposureSecurity(actionId, exposure) {
|
|
172
|
+
const errors = [];
|
|
173
|
+
// API_EXPOSURE_AUTH_REQUIRED — API-exposed actions must declare auth
|
|
174
|
+
// explicitly. The builder defaults auth to 'required' when the source
|
|
175
|
+
// omits it; the validator surfaces the omission so developers are not
|
|
176
|
+
// silently relying on the safe-default.
|
|
177
|
+
if (exposure.authDeclared === false) {
|
|
178
|
+
errors.push({
|
|
179
|
+
code: 'API_EXPOSURE_AUTH_REQUIRED',
|
|
180
|
+
message: `Action '${actionId}' has \`exposure.type: 'api'\` but \`exposure.auth\` is not declared. Set \`exposure.auth\` to 'required' | 'none' | 'service' explicitly.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// TENANT_API_PATH_REQUIRED — API-exposed actions with `tenancy: 'required'`
|
|
184
|
+
// must use a path that includes the canonical tenant placeholder so the
|
|
185
|
+
// runtime can bind `ctx.tenant.id` from the URL.
|
|
186
|
+
if (exposure.tenancy === 'required' && !exposure.path.includes('/v1/tenants/{tenantId}/')) {
|
|
187
|
+
errors.push({
|
|
188
|
+
code: 'TENANT_API_PATH_REQUIRED',
|
|
189
|
+
message: `Action '${actionId}' declares \`exposure.tenancy: 'required'\` but \`exposure.path\` ('${exposure.path}') does not include the canonical tenant placeholder '/v1/tenants/{tenantId}/'.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// AUTH_NONE_REQUIRES_EXCEPTION — `auth: 'none'` (anonymous) routes
|
|
193
|
+
// must carry an explicit `securityException` with a non-empty reason
|
|
194
|
+
// so security reviewers can audit the relaxation.
|
|
195
|
+
if (exposure.auth === 'none') {
|
|
196
|
+
const reason = exposure.securityException?.reason;
|
|
197
|
+
if (!reason || reason.trim().length === 0) {
|
|
198
|
+
errors.push({
|
|
199
|
+
code: 'AUTH_NONE_REQUIRES_EXCEPTION',
|
|
200
|
+
message: `Action '${actionId}' has \`exposure.auth: 'none'\` but no \`exposure.securityException.reason\`. Public/anonymous routes must document the security exception with a non-empty reason (and ideally a tracking reference).`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// TENANCY_NONE_REQUIRES_REASON_WHEN_PUBLIC — `tenancy: 'none'` on a
|
|
205
|
+
// public API route must justify the missing tenant context. If the
|
|
206
|
+
// route is already justified as anonymous via `auth: 'none'` the
|
|
207
|
+
// same `securityException` may be reused; otherwise an exception is
|
|
208
|
+
// required for tenancy: 'none' on its own.
|
|
209
|
+
if (exposure.tenancy === 'none') {
|
|
210
|
+
const reason = exposure.securityException?.reason;
|
|
211
|
+
if (!reason || reason.trim().length === 0) {
|
|
212
|
+
errors.push({
|
|
213
|
+
code: 'TENANCY_NONE_REQUIRES_REASON_WHEN_PUBLIC',
|
|
214
|
+
message: `Action '${actionId}' has \`exposure.tenancy: 'none'\` but no \`exposure.securityException.reason\`. Routes without tenant context must document the security exception with a non-empty reason.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// SYSTEM_API_REQUIRES_ROLE — `tenancy: 'system'` combined with
|
|
219
|
+
// `auth: 'required'` must declare non-empty `roles` so the JWT
|
|
220
|
+
// authorizer can scope the call. Routes that use `auth: 'service'`
|
|
221
|
+
// are service-only and do not require role narrowing.
|
|
222
|
+
if (exposure.tenancy === 'system' && exposure.auth === 'required') {
|
|
223
|
+
if (!Array.isArray(exposure.roles) || exposure.roles.length === 0) {
|
|
224
|
+
errors.push({
|
|
225
|
+
code: 'SYSTEM_API_REQUIRES_ROLE',
|
|
226
|
+
message: `Action '${actionId}' has \`exposure.tenancy: 'system'\` and \`exposure.auth: 'required'\` but no \`exposure.roles\`. System-tenancy routes using user auth must declare at least one required role.`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return errors;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* DEFINE_API_LEGACY_USAGE — the action-first migration replaces
|
|
234
|
+
* standalone `defineApi` calls with `defineAction` + `exposure.type:
|
|
235
|
+
* 'api'`. Every `defineApi` call still in the source produces a row in
|
|
236
|
+
* `registry.apis`; the validator emits one error per legacy API to
|
|
237
|
+
* enforce the alpha breaking migration. The validate command's fixture
|
|
238
|
+
* suite intentionally does not use `defineApi`, so this rule does not
|
|
239
|
+
* break existing unit tests.
|
|
240
|
+
*/
|
|
241
|
+
function checkDefineApiLegacyUsage(apiCount, apis) {
|
|
242
|
+
if (apiCount === 0)
|
|
243
|
+
return [];
|
|
244
|
+
const errors = [];
|
|
245
|
+
for (const api of apis) {
|
|
246
|
+
if (api.kind !== 'api')
|
|
247
|
+
continue;
|
|
248
|
+
errors.push({
|
|
249
|
+
code: 'DEFINE_API_LEGACY_USAGE',
|
|
250
|
+
message: `API '${api.id}' uses legacy \`defineApi\`. Migrate to \`defineAction\` with \`exposure: { type: 'api', path, method, auth, tenancy }\` (#4619).`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return errors;
|
|
254
|
+
}
|
|
81
255
|
/**
|
|
82
256
|
* Run the validate command: build the registry and perform structural validation.
|
|
83
257
|
* Exits the process with code 1 if validation fails (CI gate usage).
|
|
@@ -155,6 +329,15 @@ export async function runValidate(domainRoot, exitOnFailure = true, config = { m
|
|
|
155
329
|
}
|
|
156
330
|
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|
|
157
331
|
errors.push(...rawPathErrors);
|
|
332
|
+
// Wave 6 Task 6.1: action-first security validation gates (#4619).
|
|
333
|
+
// Runs against the registry built above so the rules see the same
|
|
334
|
+
// shape the CDK packer will eventually consume.
|
|
335
|
+
errors.push(...checkActionFirstSecurity(registry.actions));
|
|
336
|
+
errors.push(...checkDefineApiLegacyUsage(registry.apis.length, registry.apis));
|
|
337
|
+
// Issue #4662 Task D — deployment-time security gates. These mirror
|
|
338
|
+
// the CDK synth-time `SecurityAssertionAspect` so violations are
|
|
339
|
+
// caught before any AWS deployment is attempted.
|
|
340
|
+
errors.push(...checkDeploymentSecurity(registry.apis, registry.actions));
|
|
158
341
|
const totalPrimitives = registry.apis.length + registry.webhooks.length +
|
|
159
342
|
registry.subscribers.length + registry.schedules.length +
|
|
160
343
|
registry.jobs.length + registry.actions.length;
|
|
@@ -47,7 +47,7 @@ export function injectHeader(content, filename, moduleId, version) {
|
|
|
47
47
|
function injectLineCommentHeader(content, commentChar, moduleId, version) {
|
|
48
48
|
const header = [
|
|
49
49
|
`${commentChar} @mc-scaffold: ${moduleId}@${version}`,
|
|
50
|
-
`${commentChar} This file is managed by TIB scaffold. Manual edits will be flagged during \`
|
|
50
|
+
`${commentChar} This file is managed by TIB scaffold. Manual edits will be flagged during \`npx mc-domain-module upgrade\`.`,
|
|
51
51
|
`${commentChar} To opt out of upgrade management for this file, remove these header lines.`,
|
|
52
52
|
'',
|
|
53
53
|
].join('\n');
|
|
@@ -63,7 +63,7 @@ function injectLineCommentHeader(content, commentChar, moduleId, version) {
|
|
|
63
63
|
function injectBlockCommentHeader(content, open, close, moduleId, version) {
|
|
64
64
|
const header = [
|
|
65
65
|
`${open} @mc-scaffold: ${moduleId}@${version} ${close}`,
|
|
66
|
-
`${open} This file is managed by TIB scaffold. Manual edits will be flagged during \`
|
|
66
|
+
`${open} This file is managed by TIB scaffold. Manual edits will be flagged during \`npx mc-domain-module upgrade\`. ${close}`,
|
|
67
67
|
'',
|
|
68
68
|
].join('\n');
|
|
69
69
|
return header + content;
|
|
@@ -8,7 +8,7 @@ export interface InstallResult {
|
|
|
8
8
|
*
|
|
9
9
|
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
10
10
|
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
11
|
-
* (does not write .
|
|
11
|
+
* (does not write .mc-upgrade or overwrite — user opts in via Updates tab).
|
|
12
12
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
13
13
|
*/
|
|
14
14
|
export declare function installScaffoldFile(absPath: string, content: string, policy: FilePolicy, currentEntry: ManifestFileEntry | undefined, opts?: {
|
|
@@ -6,7 +6,7 @@ import { computeChecksumString } from './checksum.js';
|
|
|
6
6
|
*
|
|
7
7
|
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
8
8
|
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
9
|
-
* (does not write .
|
|
9
|
+
* (does not write .mc-upgrade or overwrite — user opts in via Updates tab).
|
|
10
10
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
11
11
|
*/
|
|
12
12
|
export async function installScaffoldFile(absPath, content, policy, currentEntry, opts) {
|
package/dist/utils/manifest.js
CHANGED
|
@@ -4,9 +4,8 @@ import { cliLogger } from './logger.js';
|
|
|
4
4
|
const MANIFEST_PATH = '.mc/manifest.json';
|
|
5
5
|
const SCHEMA_URL = 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json';
|
|
6
6
|
export function inferPolicyFromPath(filePath) {
|
|
7
|
-
// owned: infra/,
|
|
7
|
+
// owned: infra/, mc-deploy.yml, .github/workflows/
|
|
8
8
|
if (filePath.match(/^infra\//) ||
|
|
9
|
-
filePath.match(/^\.tib\/infra\//) ||
|
|
10
9
|
filePath === 'mc-deploy.yml' ||
|
|
11
10
|
filePath.match(/^\.github\/workflows\//)) {
|
|
12
11
|
return 'managed';
|
|
@@ -67,9 +67,9 @@ export interface ScaffoldConfig {
|
|
|
67
67
|
awsAccountId: string;
|
|
68
68
|
/** Admin email collected at scaffold time (Cognito seed user). Present only when auth module is enabled. */
|
|
69
69
|
adminEmail?: string;
|
|
70
|
-
/** Domain IDs added via `
|
|
70
|
+
/** Domain IDs added via `mc-domain-module add-domain` — NOT scaffold-owned, user-managed */
|
|
71
71
|
domainIds: string[];
|
|
72
|
-
/** Flow IDs added via `
|
|
72
|
+
/** Flow IDs added via `mc-domain-module add-flow` — user-managed */
|
|
73
73
|
flowIds: string[];
|
|
74
74
|
/** S3 bucket used for scaffold fetches (defaults to public TIB bucket) */
|
|
75
75
|
scaffoldBucket: string;
|
|
@@ -28,7 +28,7 @@ export async function readScaffoldConfig(projectRoot) {
|
|
|
28
28
|
// Backward compatibility: old files may only have domainIds/flowIds
|
|
29
29
|
if (!raw.scaffoldVersion) {
|
|
30
30
|
// eslint-disable-next-line no-console
|
|
31
|
-
console.warn('[
|
|
31
|
+
console.warn('[mc-domain-module] scaffold-config.json is missing scaffoldVersion — project was created before S3 scaffold migration. Run `npx mc-domain-module upgrade` to backfill.');
|
|
32
32
|
}
|
|
33
33
|
return {
|
|
34
34
|
...DEFAULT_SCAFFOLD_CONFIG,
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ describe('check-hashes', () => {
|
|
|
19
19
|
|
|
20
20
|
it('should return ok: true when all files match manifest', async () => {
|
|
21
21
|
// Set up directory structure
|
|
22
|
-
await mkdir(join(tempDir, '.
|
|
22
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
23
23
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
24
24
|
|
|
25
25
|
// Create a test file
|
|
@@ -36,7 +36,7 @@ describe('check-hashes', () => {
|
|
|
36
36
|
'infra/modules/test.ts': hash,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
-
await writeFile(join(tempDir, '.
|
|
39
|
+
await writeFile(join(tempDir, '.mc', 'modules-hashes.json'), JSON.stringify(manifest));
|
|
40
40
|
|
|
41
41
|
const result = await runCheckHashes({ projectRoot: tempDir });
|
|
42
42
|
expect(result.ok).toBe(true);
|
|
@@ -46,7 +46,7 @@ describe('check-hashes', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('should detect modified files', async () => {
|
|
49
|
-
await mkdir(join(tempDir, '.
|
|
49
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
50
50
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
51
51
|
|
|
52
52
|
const originalContent = 'export const MyStack = {};';
|
|
@@ -62,7 +62,7 @@ describe('check-hashes', () => {
|
|
|
62
62
|
'infra/modules/test.ts': originalHash,
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
|
-
await writeFile(join(tempDir, '.
|
|
65
|
+
await writeFile(join(tempDir, '.mc', 'modules-hashes.json'), JSON.stringify(manifest));
|
|
66
66
|
|
|
67
67
|
// Modify the file
|
|
68
68
|
const modifiedContent = 'export const MyStack = { foo: "bar" };';
|
|
@@ -77,7 +77,7 @@ describe('check-hashes', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it('should detect deleted files', async () => {
|
|
80
|
-
await mkdir(join(tempDir, '.
|
|
80
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
81
81
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
82
82
|
|
|
83
83
|
const content = 'export const MyStack = {};';
|
|
@@ -94,7 +94,7 @@ describe('check-hashes', () => {
|
|
|
94
94
|
'infra/modules/deleted.ts': 'somehash',
|
|
95
95
|
},
|
|
96
96
|
};
|
|
97
|
-
await writeFile(join(tempDir, '.
|
|
97
|
+
await writeFile(join(tempDir, '.mc', 'modules-hashes.json'), JSON.stringify(manifest));
|
|
98
98
|
|
|
99
99
|
const result = await runCheckHashes({ projectRoot: tempDir });
|
|
100
100
|
expect(result.ok).toBe(false);
|
|
@@ -103,7 +103,7 @@ describe('check-hashes', () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it('should detect unexpected files', async () => {
|
|
106
|
-
await mkdir(join(tempDir, '.
|
|
106
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
107
107
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
108
108
|
|
|
109
109
|
const content = 'export const MyStack = {};';
|
|
@@ -123,7 +123,7 @@ describe('check-hashes', () => {
|
|
|
123
123
|
'infra/modules/test.ts': hash,
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
|
-
await writeFile(join(tempDir, '.
|
|
126
|
+
await writeFile(join(tempDir, '.mc', 'modules-hashes.json'), JSON.stringify(manifest));
|
|
127
127
|
|
|
128
128
|
const result = await runCheckHashes({ projectRoot: tempDir });
|
|
129
129
|
expect(result.ok).toBe(false);
|
|
@@ -132,7 +132,7 @@ describe('check-hashes', () => {
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
it('should throw with helpful message when manifest is missing', async () => {
|
|
135
|
-
await mkdir(join(tempDir, '.
|
|
135
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
136
136
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
137
137
|
|
|
138
138
|
await expect(runCheckHashes({ projectRoot: tempDir })).rejects.toThrow(
|
|
@@ -55,7 +55,7 @@ describe('upgrade command', () => {
|
|
|
55
55
|
let tmpDir: string;
|
|
56
56
|
|
|
57
57
|
beforeEach(async () => {
|
|
58
|
-
tmpDir = await mkdtemp(join(tmpdir(), '
|
|
58
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'mc-upgrade-test-'));
|
|
59
59
|
vi.clearAllMocks();
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -213,8 +213,8 @@ describe('upgrade command', () => {
|
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
describe('drift detection and .
|
|
217
|
-
it('writes .
|
|
216
|
+
describe('drift detection and .mc-upgrade conflict files', () => {
|
|
217
|
+
it('writes .mc-upgrade file when file is drifted', async () => {
|
|
218
218
|
const manifestChecksum = computeChecksumString('original content');
|
|
219
219
|
const driftedContent = 'locally modified content by user';
|
|
220
220
|
const driftedChecksum = computeChecksumString(driftedContent);
|
|
@@ -313,8 +313,8 @@ describe('upgrade command', () => {
|
|
|
313
313
|
const originalContent = await readFile(join(tmpDir, filePath), 'utf-8');
|
|
314
314
|
expect(originalContent).toBe(driftedContent);
|
|
315
315
|
|
|
316
|
-
// Assert .
|
|
317
|
-
const conflictFile = await readFile(join(tmpDir, `${filePath}.
|
|
316
|
+
// Assert .mc-upgrade file was created with new content
|
|
317
|
+
const conflictFile = await readFile(join(tmpDir, `${filePath}.mc-upgrade`), 'utf-8');
|
|
318
318
|
expect(conflictFile).toContain(newContent);
|
|
319
319
|
});
|
|
320
320
|
});
|
|
@@ -620,8 +620,8 @@ describe('upgrade command', () => {
|
|
|
620
620
|
const originalContent = await readFile(join(tmpDir, filePath), 'utf-8');
|
|
621
621
|
expect(originalContent).toBe(driftedContent);
|
|
622
622
|
|
|
623
|
-
// .
|
|
624
|
-
const conflictFile = await readFile(join(tmpDir, `${filePath}.
|
|
623
|
+
// .mc-upgrade file should be created
|
|
624
|
+
const conflictFile = await readFile(join(tmpDir, `${filePath}.mc-upgrade`), 'utf-8');
|
|
625
625
|
expect(conflictFile).toContain(newContent);
|
|
626
626
|
});
|
|
627
627
|
|