@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
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
import { relative, resolve } from 'node:path';
|
|
2
2
|
import { walkDomainDir } from '../utils/file-helpers.js';
|
|
3
3
|
import { loadModuleExports } from './load-module.js';
|
|
4
|
+
/**
|
|
5
|
+
* Coerce a raw value into a valid backendAccess scope, defaulting to
|
|
6
|
+
* 'private' when the value is missing or unrecognized. Used by the
|
|
7
|
+
* builder when reading the new-style `backendAccess` field directly.
|
|
8
|
+
*/
|
|
9
|
+
function toBackendAccess(raw) {
|
|
10
|
+
return raw === 'domain' || raw === 'platform' ? raw : 'private';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Map a legacy `visibility` scope to a `backendAccess` scope for the
|
|
14
|
+
* action-first migration. The legacy `workspace` scope (which previously
|
|
15
|
+
* permitted unauthenticated Function URL exposure) collapses to `domain`
|
|
16
|
+
* so that all cross-domain callers must go through `ctx.actions`.
|
|
17
|
+
*/
|
|
18
|
+
function visibilityToBackendAccess(visibility) {
|
|
19
|
+
if (visibility === 'workspace')
|
|
20
|
+
return 'domain';
|
|
21
|
+
if (visibility === 'domain')
|
|
22
|
+
return 'domain';
|
|
23
|
+
return 'private';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Best-effort reverse mapping from `backendAccess` to the legacy
|
|
27
|
+
* `visibility` field. Used to keep the deprecated field populated so
|
|
28
|
+
* existing CDK constructs that read it continue to behave the same way.
|
|
29
|
+
*
|
|
30
|
+
* - private -> private
|
|
31
|
+
* - domain -> domain
|
|
32
|
+
* - platform -> workspace (closest legacy equivalent for platform-level)
|
|
33
|
+
*/
|
|
34
|
+
function backendAccessToVisibility(backendAccess) {
|
|
35
|
+
if (backendAccess === 'platform')
|
|
36
|
+
return 'workspace';
|
|
37
|
+
return backendAccess;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Type guard + sanitizer for an action's `exposure` field. Returns a
|
|
41
|
+
* well-typed `ActionExposure` (extended with `authDeclared` /
|
|
42
|
+
* `tenancyDeclared` tracking flags) or the provided fallback when the
|
|
43
|
+
* raw value does not match a supported exposure shape.
|
|
44
|
+
*
|
|
45
|
+
* The `authDeclared` / `tenancyDeclared` flags record whether the source
|
|
46
|
+
* code explicitly declared each field, or whether the builder fell back
|
|
47
|
+
* to the safe default. They are validation-only and consumed by the
|
|
48
|
+
* domain CLI's validate command (Wave 6 Task 6.1) to enforce the
|
|
49
|
+
* action-first security model (#4619).
|
|
50
|
+
*/
|
|
51
|
+
function toExposure(raw, fallback) {
|
|
52
|
+
if (!raw || typeof raw !== 'object')
|
|
53
|
+
return fallback;
|
|
54
|
+
const candidate = raw;
|
|
55
|
+
if (candidate.type === 'internal')
|
|
56
|
+
return { type: 'internal' };
|
|
57
|
+
if (candidate.type !== 'api')
|
|
58
|
+
return fallback;
|
|
59
|
+
// Best-effort validation of api exposure fields; any missing required
|
|
60
|
+
// string field falls back to the supplied default exposure.
|
|
61
|
+
const api = raw;
|
|
62
|
+
if (typeof api.path !== 'string' || typeof api.method !== 'string')
|
|
63
|
+
return fallback;
|
|
64
|
+
const authRaw = api.auth;
|
|
65
|
+
const tenancyRaw = api.tenancy;
|
|
66
|
+
const auth = authRaw === 'required' || authRaw === 'none' || authRaw === 'service' ? authRaw : 'required';
|
|
67
|
+
const tenancy = tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system' ? tenancyRaw : 'required';
|
|
68
|
+
const out = {
|
|
69
|
+
type: 'api',
|
|
70
|
+
path: api.path,
|
|
71
|
+
method: api.method,
|
|
72
|
+
auth,
|
|
73
|
+
tenancy,
|
|
74
|
+
authDeclared: authRaw === 'required' || authRaw === 'none' || authRaw === 'service',
|
|
75
|
+
tenancyDeclared: tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system',
|
|
76
|
+
};
|
|
77
|
+
if (Array.isArray(api.roles)) {
|
|
78
|
+
out.roles = api.roles.filter((r) => typeof r === 'string');
|
|
79
|
+
}
|
|
80
|
+
if (api.securityException && typeof api.securityException === 'object') {
|
|
81
|
+
const reason = api.securityException.reason;
|
|
82
|
+
if (typeof reason === 'string') {
|
|
83
|
+
out.securityException = { reason };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
4
88
|
/** Returns true if a value looks like a JSON Schema object (has a 'type' or '$schema' property). */
|
|
5
89
|
function isJsonSchema(v) {
|
|
6
90
|
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
@@ -47,7 +131,7 @@ export async function buildRegistry(domainRoot) {
|
|
|
47
131
|
if (!domainRaw) {
|
|
48
132
|
// Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
|
|
49
133
|
for (const w of warnings)
|
|
50
|
-
process.stderr.write(`[
|
|
134
|
+
process.stderr.write(`[mc-domain-module validate] ${w}\n`);
|
|
51
135
|
throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
|
|
52
136
|
}
|
|
53
137
|
const domain = {
|
|
@@ -149,18 +233,49 @@ export async function buildRegistry(domainRoot) {
|
|
|
149
233
|
})));
|
|
150
234
|
const actions = paths.actions.flatMap((filePath, i) => (actionExports[i] ?? [])
|
|
151
235
|
.filter(e => e['_kind'] === 'action')
|
|
152
|
-
.map(e =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
.map(e => {
|
|
237
|
+
// Resolve backendAccess. New-style actions carry `backendAccess`
|
|
238
|
+
// directly. Legacy actions only carry `visibility`; in that case
|
|
239
|
+
// we collapse workspace -> domain per the migration spec, and we
|
|
240
|
+
// also remember the legacy flag so we can default exposure to
|
|
241
|
+
// `{ type: 'internal' }` for actions that have not opted in yet.
|
|
242
|
+
const hasBackendAccess = 'backendAccess' in e;
|
|
243
|
+
const rawBackendAccess = e['backendAccess'];
|
|
244
|
+
const rawVisibility = e['visibility'];
|
|
245
|
+
const backendAccess = hasBackendAccess
|
|
246
|
+
? toBackendAccess(rawBackendAccess)
|
|
247
|
+
: visibilityToBackendAccess(rawVisibility);
|
|
248
|
+
const legacyVisibility = backendAccessToVisibility(backendAccess);
|
|
249
|
+
// Resolve exposure. New-style actions must declare their exposure;
|
|
250
|
+
// legacy actions that did not opt in default to `{ type: 'internal' }`
|
|
251
|
+
// so existing internal-only behavior is preserved during migration.
|
|
252
|
+
const rawExposure = e['exposure'];
|
|
253
|
+
const exposure = toExposure(rawExposure, { type: 'internal' });
|
|
254
|
+
// Record whether the source explicitly declared the `exposure` field.
|
|
255
|
+
// Validation-only; consumed by the domain CLI's validate command
|
|
256
|
+
// (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
|
|
257
|
+
const exposureDeclared = rawExposure !== undefined && rawExposure !== null
|
|
258
|
+
&& typeof rawExposure === 'object';
|
|
259
|
+
return {
|
|
260
|
+
id: String(e['id']),
|
|
261
|
+
kind: 'action',
|
|
262
|
+
handlerFile: relPath(filePath),
|
|
263
|
+
backendAccess,
|
|
264
|
+
exposure,
|
|
265
|
+
exposureDeclared,
|
|
266
|
+
// Keep the legacy field populated so CDK constructs that still
|
|
267
|
+
// read `visibility` (e.g. action-construct.ts) keep working
|
|
268
|
+
// through the migration window. New constructs should read
|
|
269
|
+
// `backendAccess` and `exposure` instead.
|
|
270
|
+
visibility: legacyVisibility,
|
|
271
|
+
idempotent: Boolean(e['idempotent'] ?? false),
|
|
272
|
+
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
273
|
+
deployment: deployment(e),
|
|
274
|
+
outboundAccess: outboundAccess(e),
|
|
275
|
+
inputSchema: isJsonSchema(e['input']) ? e['input'] : undefined,
|
|
276
|
+
outputSchema: isJsonSchema(e['output']) ? e['output'] : undefined,
|
|
277
|
+
};
|
|
278
|
+
}));
|
|
164
279
|
const integrations = paths.integrations.flatMap((_filePath, i) => (integrationExports[i] ?? [])
|
|
165
280
|
.filter(e => e['_kind'] === 'integration')
|
|
166
281
|
.map(e => ({
|
|
@@ -95,9 +95,9 @@ process.stdout.write(JSON.stringify(results));
|
|
|
95
95
|
export async function loadModuleExports(absoluteFilePath) {
|
|
96
96
|
// Use a subdir of the project root rather than OS tmpdir so that ESM import
|
|
97
97
|
// resolution can walk up and find node_modules packages like zod-to-json-schema.
|
|
98
|
-
const
|
|
99
|
-
await mkdir(
|
|
100
|
-
const tempPath = join(
|
|
98
|
+
const mcTmpDir = join(process.cwd(), '.mc', 'tmp');
|
|
99
|
+
await mkdir(mcTmpDir, { recursive: true }).catch(() => undefined);
|
|
100
|
+
const tempPath = join(mcTmpDir, `mc-load-${randomBytes(8).toString('hex')}.mts`);
|
|
101
101
|
await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
|
|
102
102
|
try {
|
|
103
103
|
return await new Promise((resolve, reject) => {
|
package/dist/cli.js
CHANGED
|
@@ -75,9 +75,9 @@ program
|
|
|
75
75
|
program
|
|
76
76
|
.command('build-catalog')
|
|
77
77
|
.description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
|
|
78
|
-
.option('--
|
|
78
|
+
.option('--mc-dir <path>', 'Path to the .mc registry directory (defaults to .mc in cwd)')
|
|
79
79
|
.action(async (opts) => {
|
|
80
|
-
await runBuildCatalog(opts.
|
|
80
|
+
await runBuildCatalog(opts.mcDir);
|
|
81
81
|
});
|
|
82
82
|
program
|
|
83
83
|
.command('add-domain <id>')
|
|
@@ -92,7 +92,7 @@ export interface DomainCatalog {
|
|
|
92
92
|
/**
|
|
93
93
|
* Build the combined domain catalog from all per-domain registry files.
|
|
94
94
|
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
95
|
-
* @param
|
|
95
|
+
* @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
|
|
96
96
|
* @returns The written catalog.
|
|
97
97
|
*/
|
|
98
|
-
export declare function runBuildCatalog(
|
|
98
|
+
export declare function runBuildCatalog(registryDir?: string): Promise<DomainCatalog>;
|
|
@@ -4,22 +4,22 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Build the combined domain catalog from all per-domain registry files.
|
|
6
6
|
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
7
|
-
* @param
|
|
7
|
+
* @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
|
|
8
8
|
* @returns The written catalog.
|
|
9
9
|
*/
|
|
10
|
-
export async function runBuildCatalog(
|
|
11
|
-
const dir =
|
|
10
|
+
export async function runBuildCatalog(registryDir) {
|
|
11
|
+
const dir = registryDir ? resolve(registryDir) : join(process.cwd(), '.mc');
|
|
12
12
|
// Find all per-domain registry files
|
|
13
13
|
let files;
|
|
14
14
|
try {
|
|
15
15
|
files = await readdir(dir);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
|
-
throw new Error(`build-catalog: .
|
|
18
|
+
throw new Error(`build-catalog: .mc registry directory not found at ${dir}. Run mc-domain-module build first.`);
|
|
19
19
|
}
|
|
20
20
|
const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
|
|
21
21
|
if (registryFiles.length === 0) {
|
|
22
|
-
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run
|
|
22
|
+
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run mc-domain-module build <domain> first.`);
|
|
23
23
|
}
|
|
24
24
|
const catalog = {
|
|
25
25
|
version: 2,
|
|
@@ -253,7 +253,7 @@ async function loadFlowsFromDir(dir, owningDomainOverride) {
|
|
|
253
253
|
*/
|
|
254
254
|
export async function runBuildFlows(options) {
|
|
255
255
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
256
|
-
const outFile = options.outFile ?? join(projectRoot, '.
|
|
256
|
+
const outFile = options.outFile ?? join(projectRoot, '.mc', 'flows-registry.json');
|
|
257
257
|
cliLogger.info({ projectRoot }, 'Building flows registry');
|
|
258
258
|
const allEntries = [];
|
|
259
259
|
const seenIds = new Set();
|
package/dist/commands/build.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function runBuild(options) {
|
|
|
15
15
|
const domainId = basename(domainRoot);
|
|
16
16
|
const outFile = options.outFile
|
|
17
17
|
? resolve(options.outFile)
|
|
18
|
-
: join(process.cwd(), '.
|
|
18
|
+
: join(process.cwd(), '.mc', `${domainId}-registry.json`);
|
|
19
19
|
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
20
20
|
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
21
21
|
for (const w of warnings) {
|
|
@@ -25,15 +25,15 @@ export async function runBuild(options) {
|
|
|
25
25
|
await writeFile(outFile, JSON.stringify(registry), 'utf8');
|
|
26
26
|
cliLogger.info({ outFile, apis: registry.apis.length, events: registry.events.length }, 'Registry written');
|
|
27
27
|
// Discover all sibling registries and emit aggregated types
|
|
28
|
-
const
|
|
29
|
-
const registryFiles = (await readdir(
|
|
28
|
+
const registryDir = join(outFile, '..');
|
|
29
|
+
const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
|
|
30
30
|
const allRegistries = [];
|
|
31
31
|
for (const f of registryFiles) {
|
|
32
|
-
const raw = await readFile(join(
|
|
32
|
+
const raw = await readFile(join(registryDir, f), 'utf8');
|
|
33
33
|
allRegistries.push(JSON.parse(raw));
|
|
34
34
|
}
|
|
35
35
|
const typesContent = buildActionsTypes(allRegistries);
|
|
36
|
-
const typesFile = join(
|
|
36
|
+
const typesFile = join(registryDir, 'actions-types.d.ts');
|
|
37
37
|
await writeFile(typesFile, typesContent);
|
|
38
38
|
cliLogger.info({ outFile: typesFile }, 'Types file written');
|
|
39
39
|
return outFile;
|
|
@@ -10,14 +10,14 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function runCheckHashes(opts = {}) {
|
|
12
12
|
const root = opts.projectRoot ?? process.cwd();
|
|
13
|
-
const manifestPath = join(root, '.
|
|
13
|
+
const manifestPath = join(root, '.mc', 'modules-hashes.json');
|
|
14
14
|
const modulesDir = join(root, 'infra', 'modules');
|
|
15
15
|
let manifest;
|
|
16
16
|
try {
|
|
17
17
|
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
|
-
throw new Error(`
|
|
20
|
+
throw new Error(`mc-domain-module check-hashes: cannot read ${manifestPath} (${err.message}). Re-scaffold to regenerate the manifest.`);
|
|
21
21
|
}
|
|
22
22
|
const drifted = [];
|
|
23
23
|
const missing = [];
|
|
@@ -160,7 +160,7 @@ export async function runCreateProject(opts) {
|
|
|
160
160
|
const orderedModules = topoSort(modulesJson.modules, selectedIds);
|
|
161
161
|
// 5. Prepare output directory
|
|
162
162
|
await mkdir(outputDir, { recursive: true });
|
|
163
|
-
await mkdir(join(outputDir, '.
|
|
163
|
+
await mkdir(join(outputDir, '.mc'), { recursive: true });
|
|
164
164
|
// 6. Get CLI version from package.json
|
|
165
165
|
let cliVersion = '0.0.0';
|
|
166
166
|
try {
|
|
@@ -11,15 +11,13 @@ export declare const DOCTOR_FIX_FLAG = "--fix";
|
|
|
11
11
|
export interface DoctorOptions {
|
|
12
12
|
/** Root directory of the project (defaults to cwd). */
|
|
13
13
|
projectRoot?: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Deprecated legacy flag retained as a no-op for older automation. */
|
|
15
15
|
relocate?: boolean;
|
|
16
16
|
/**
|
|
17
|
-
* Auto-remediate before reporting.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (0 files moved, exit 0). Does NOT auto-generate missing fixtures
|
|
22
|
-
* (deferred).
|
|
17
|
+
* Auto-remediate before reporting. The legacy relocation routine is now a
|
|
18
|
+
* no-op because current scaffold manifests intentionally track managed files
|
|
19
|
+
* in their installed project locations (infra/modules, domains, .github, etc.).
|
|
20
|
+
* Does NOT auto-generate missing fixtures (deferred).
|
|
23
21
|
*/
|
|
24
22
|
fix?: boolean;
|
|
25
23
|
/**
|
package/dist/commands/doctor.js
CHANGED
|
@@ -157,15 +157,14 @@ async function checkGeneratedClientsFresh(projectRoot) {
|
|
|
157
157
|
*/
|
|
158
158
|
export async function runDoctor(opts = {}) {
|
|
159
159
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
160
|
-
// --relocate
|
|
161
|
-
// --fix
|
|
160
|
+
// --relocate is retained as a safe no-op for older automation.
|
|
161
|
+
// --fix runs that no-op preflight first, then the full report.
|
|
162
162
|
// When both are set, --fix takes precedence so the report still runs.
|
|
163
163
|
if (opts.relocate && !opts.fix) {
|
|
164
164
|
return runRelocate(projectRoot);
|
|
165
165
|
}
|
|
166
|
-
// --fix preflight: run the relocation
|
|
167
|
-
// the doctor report.
|
|
168
|
-
// runRelocate returns a no-op summary check (0 moved, 0 warned, 8 skipped).
|
|
166
|
+
// --fix preflight: run the legacy relocation no-op and merge its check into
|
|
167
|
+
// the doctor report.
|
|
169
168
|
let preflightChecks = [];
|
|
170
169
|
if (opts.fix) {
|
|
171
170
|
const relocationReport = await runRelocate(projectRoot);
|
|
@@ -475,13 +474,21 @@ async function checkApisHaveTenancy(projectRoot) {
|
|
|
475
474
|
}
|
|
476
475
|
async function checkScaffoldConfigMatchesDisk(projectRoot) {
|
|
477
476
|
try {
|
|
478
|
-
const configPath = join(projectRoot, '.
|
|
477
|
+
const configPath = join(projectRoot, '.mc', 'scaffold-config.json');
|
|
479
478
|
const domainsDir = join(projectRoot, 'domains');
|
|
480
479
|
let configDomains = [];
|
|
481
480
|
try {
|
|
482
481
|
const configContent = await readFile(configPath, 'utf8');
|
|
483
482
|
const config = JSON.parse(configContent);
|
|
484
|
-
|
|
483
|
+
if (!Array.isArray(config.domainIds)) {
|
|
484
|
+
return {
|
|
485
|
+
name: 'scaffold-config.json matches on-disk',
|
|
486
|
+
status: 'PASS',
|
|
487
|
+
message: '.mc/scaffold-config.json has no domainIds array — domain list sync check skipped',
|
|
488
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
configDomains = config.domainIds;
|
|
485
492
|
}
|
|
486
493
|
catch {
|
|
487
494
|
return {
|
|
@@ -1114,7 +1121,7 @@ async function checkDomainBrainReachable(projectRoot) {
|
|
|
1114
1121
|
*/
|
|
1115
1122
|
async function checkActionsHaveTypes(projectRoot) {
|
|
1116
1123
|
try {
|
|
1117
|
-
const typesPath = join(projectRoot, '.
|
|
1124
|
+
const typesPath = join(projectRoot, '.mc', 'actions-types.d.ts');
|
|
1118
1125
|
const domainsDir = join(projectRoot, 'domains');
|
|
1119
1126
|
const discoveredActions = [];
|
|
1120
1127
|
try {
|
|
@@ -1347,7 +1354,7 @@ async function checkSeedPagesHaveNoHeaders(projectRoot) {
|
|
|
1347
1354
|
}
|
|
1348
1355
|
/**
|
|
1349
1356
|
* Check Part C: Layout validation checks
|
|
1350
|
-
* 1.
|
|
1357
|
+
* 1. managed files are not registered in obsolete legacy locations
|
|
1351
1358
|
* 2. tracked/seed files inside .mc/
|
|
1352
1359
|
* 3. manifest entries pointing at missing files
|
|
1353
1360
|
* 4. scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
|
|
@@ -1361,25 +1368,26 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1361
1368
|
// No manifest — skip layout checks
|
|
1362
1369
|
return [];
|
|
1363
1370
|
}
|
|
1364
|
-
// Check 1:
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1371
|
+
// Check 1: managed files should not be registered in obsolete legacy
|
|
1372
|
+
// locations. Current scaffold manifests intentionally track managed files at
|
|
1373
|
+
// their installed paths (infra/modules, domains, .github, CLAUDE.md, etc.).
|
|
1374
|
+
const legacyManagedLocations = manifest.files.filter(f => f.policy === 'managed' && (f.path.startsWith('.tib/') ||
|
|
1375
|
+
f.path.startsWith('.mc/infra/') ||
|
|
1376
|
+
f.path === 'mc-deploy.yml'));
|
|
1377
|
+
if (legacyManagedLocations.length === 0) {
|
|
1370
1378
|
results.push({
|
|
1371
|
-
name: 'Layout:
|
|
1379
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1372
1380
|
status: 'PASS',
|
|
1373
|
-
message: '
|
|
1381
|
+
message: 'Managed scaffold files use current manifest paths',
|
|
1374
1382
|
});
|
|
1375
1383
|
}
|
|
1376
1384
|
else {
|
|
1377
|
-
for (const f of
|
|
1385
|
+
for (const f of legacyManagedLocations) {
|
|
1378
1386
|
results.push({
|
|
1379
|
-
name: 'Layout:
|
|
1387
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1380
1388
|
status: 'FAIL',
|
|
1381
|
-
message: `
|
|
1382
|
-
fixHint:
|
|
1389
|
+
message: `Managed scaffold file registered in legacy location: ${f.path}`,
|
|
1390
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
|
|
1383
1391
|
file: f.path,
|
|
1384
1392
|
});
|
|
1385
1393
|
}
|
|
@@ -1428,7 +1436,7 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1428
1436
|
name: 'Layout: manifest entries exist on disk',
|
|
1429
1437
|
status: 'FAIL',
|
|
1430
1438
|
message: `${missingFiles.length} manifest entry/entries point to missing files:\n ${missingFiles.join('\n ')}`,
|
|
1431
|
-
fixHint: 'Run
|
|
1439
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata, or restore the missing files',
|
|
1432
1440
|
});
|
|
1433
1441
|
}
|
|
1434
1442
|
// Check 4: scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
|
|
@@ -1483,154 +1491,25 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1483
1491
|
name: 'Layout: scaffold-tracked files have manifest entries',
|
|
1484
1492
|
status: 'WARN',
|
|
1485
1493
|
message: `${untracked.length} file(s) with @mc-scaffold: header not in manifest:\n ${untracked.join('\n ')}`,
|
|
1486
|
-
fixHint: 'Run
|
|
1494
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
|
|
1487
1495
|
});
|
|
1488
1496
|
}
|
|
1489
1497
|
return results;
|
|
1490
1498
|
}
|
|
1491
1499
|
/**
|
|
1492
|
-
*
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
{ oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
|
|
1499
|
-
{ oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
|
|
1500
|
-
{ oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
|
|
1501
|
-
{ oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
|
|
1502
|
-
{ oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
|
|
1503
|
-
];
|
|
1504
|
-
/**
|
|
1505
|
-
* Run relocation of old-layout scaffold files to new layout
|
|
1500
|
+
* Legacy relocation entrypoint retained for older automation.
|
|
1501
|
+
*
|
|
1502
|
+
* Earlier scaffold versions tried to move generated infra into .mc/infra. The
|
|
1503
|
+
* current scaffold contract keeps generated infra under infra/modules and uses
|
|
1504
|
+
* .mc/manifest.json plus .mc/modules-hashes.json for ownership/drift tracking.
|
|
1505
|
+
* Moving files here would corrupt modern projects, so relocation is now a no-op.
|
|
1506
1506
|
*/
|
|
1507
1507
|
async function runRelocate(projectRoot) {
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
checks.push({
|
|
1516
|
-
name: 'Relocate: manifest present',
|
|
1517
|
-
status: 'FAIL',
|
|
1518
|
-
message: '.mc/manifest.json not found — cannot relocate without a manifest',
|
|
1519
|
-
fixHint: 'This project may not be a TIB scaffold project',
|
|
1520
|
-
});
|
|
1521
|
-
return { checks, exitCode: 1, pass: false };
|
|
1522
|
-
}
|
|
1523
|
-
let moved = 0;
|
|
1524
|
-
let warned = 0;
|
|
1525
|
-
let skipped = 0;
|
|
1526
|
-
for (const { oldPath, newPath, policy } of RELOCATION_MAP) {
|
|
1527
|
-
const oldAbsPath = join(projectRoot, oldPath);
|
|
1528
|
-
const newAbsPath = join(projectRoot, newPath);
|
|
1529
|
-
// Check if old file exists
|
|
1530
|
-
const { access: fsAccess } = await import('node:fs/promises');
|
|
1531
|
-
try {
|
|
1532
|
-
await fsAccess(oldAbsPath);
|
|
1533
|
-
}
|
|
1534
|
-
catch {
|
|
1535
|
-
skipped++;
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
// Check if it has the scaffold header (confirm it's scaffold-owned)
|
|
1539
|
-
let content;
|
|
1540
|
-
try {
|
|
1541
|
-
content = await readFile(oldAbsPath, 'utf-8');
|
|
1542
|
-
}
|
|
1543
|
-
catch {
|
|
1544
|
-
skipped++;
|
|
1545
|
-
continue;
|
|
1546
|
-
}
|
|
1547
|
-
if (!content.includes('@mc-scaffold:')) {
|
|
1548
|
-
// File exists but no header — user-owned, leave it
|
|
1549
|
-
checks.push({
|
|
1550
|
-
name: 'Relocate: checking ' + oldPath,
|
|
1551
|
-
status: 'WARN',
|
|
1552
|
-
message: `${oldPath} has no @mc-scaffold: header — left in place (user-owned)`,
|
|
1553
|
-
file: oldPath,
|
|
1554
|
-
});
|
|
1555
|
-
warned++;
|
|
1556
|
-
continue;
|
|
1557
|
-
}
|
|
1558
|
-
// Check for user drift
|
|
1559
|
-
const manifestEntry = manifest.files.find(f => f.path === oldPath);
|
|
1560
|
-
if (manifestEntry) {
|
|
1561
|
-
const diskSha = await computeChecksumFile(oldAbsPath);
|
|
1562
|
-
if (diskSha && diskSha !== manifestEntry.sha256) {
|
|
1563
|
-
// File has been modified — leave with warning
|
|
1564
|
-
checks.push({
|
|
1565
|
-
name: 'Relocate: checking ' + oldPath,
|
|
1566
|
-
status: 'WARN',
|
|
1567
|
-
message: `${oldPath} has local modifications — left in place. Migrate manually: move to ${newPath}`,
|
|
1568
|
-
file: oldPath,
|
|
1569
|
-
});
|
|
1570
|
-
warned++;
|
|
1571
|
-
continue;
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
// Check if destination already exists
|
|
1575
|
-
try {
|
|
1576
|
-
await fsAccess(newAbsPath);
|
|
1577
|
-
checks.push({
|
|
1578
|
-
name: 'Relocate: ' + oldPath,
|
|
1579
|
-
status: 'WARN',
|
|
1580
|
-
message: `${newPath} already exists — skipping move of ${oldPath}`,
|
|
1581
|
-
file: newPath,
|
|
1582
|
-
});
|
|
1583
|
-
warned++;
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
catch { /* destination doesn't exist, good */ }
|
|
1587
|
-
// Move the file
|
|
1588
|
-
try {
|
|
1589
|
-
await mkdir(dirname(newAbsPath), { recursive: true });
|
|
1590
|
-
// Copy + delete (rename may fail across filesystems)
|
|
1591
|
-
await writeFile(newAbsPath, content, 'utf-8');
|
|
1592
|
-
// Update manifest: remove old entry, add new with new path + policy
|
|
1593
|
-
manifest.files = manifest.files.filter(f => f.path !== oldPath);
|
|
1594
|
-
const sha256 = manifestEntry?.sha256 ?? computeChecksumString(content);
|
|
1595
|
-
upsertManifestFile(manifest, {
|
|
1596
|
-
path: newPath,
|
|
1597
|
-
module: manifestEntry?.module ?? 'domain',
|
|
1598
|
-
moduleVersion: manifestEntry?.moduleVersion ?? '1.0.0',
|
|
1599
|
-
sha256,
|
|
1600
|
-
wasTemplate: manifestEntry?.wasTemplate ?? false,
|
|
1601
|
-
installedAt: manifestEntry?.installedAt ?? new Date().toISOString(),
|
|
1602
|
-
policy,
|
|
1603
|
-
});
|
|
1604
|
-
// Delete old file
|
|
1605
|
-
await unlink(oldAbsPath);
|
|
1606
|
-
checks.push({
|
|
1607
|
-
name: 'Relocate: ' + oldPath,
|
|
1608
|
-
status: 'PASS',
|
|
1609
|
-
message: `Moved ${oldPath} → ${newPath} (policy: ${policy})`,
|
|
1610
|
-
file: newPath,
|
|
1611
|
-
});
|
|
1612
|
-
moved++;
|
|
1613
|
-
}
|
|
1614
|
-
catch (err) {
|
|
1615
|
-
checks.push({
|
|
1616
|
-
name: 'Relocate: ' + oldPath,
|
|
1617
|
-
status: 'FAIL',
|
|
1618
|
-
message: `Failed to move ${oldPath} → ${newPath}: ${String(err)}`,
|
|
1619
|
-
file: oldPath,
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
// Write updated manifest
|
|
1624
|
-
if (moved > 0) {
|
|
1625
|
-
await writeManifest(projectRoot, manifest);
|
|
1626
|
-
}
|
|
1627
|
-
// Summary check
|
|
1628
|
-
checks.push({
|
|
1629
|
-
name: 'Relocate: summary',
|
|
1630
|
-
status: warned > 0 ? 'WARN' : 'PASS',
|
|
1631
|
-
message: `Relocation complete: ${moved} file(s) moved, ${warned} warning(s), ${skipped} skipped (not present)`,
|
|
1632
|
-
});
|
|
1633
|
-
const hasFail = checks.some(c => c.status === 'FAIL');
|
|
1634
|
-
const exitCode = hasFail ? 1 : 0;
|
|
1635
|
-
return { checks, exitCode, pass: exitCode === 0 };
|
|
1508
|
+
void projectRoot;
|
|
1509
|
+
const checks = [{
|
|
1510
|
+
name: 'Relocate: deprecated no-op',
|
|
1511
|
+
status: 'PASS',
|
|
1512
|
+
message: 'No files moved. Current scaffold layout keeps generated files at their installed paths and tracks ownership via .mc/manifest.json.',
|
|
1513
|
+
}];
|
|
1514
|
+
return { checks, exitCode: 0, pass: true };
|
|
1636
1515
|
}
|
|
@@ -4,13 +4,13 @@ import path from 'path';
|
|
|
4
4
|
import { cliLogger } from '../utils/logger.js';
|
|
5
5
|
export async function runPowerTune(domain, primitive) {
|
|
6
6
|
// Load registry to find Lambda ARN
|
|
7
|
-
const registryPath = path.join(process.cwd(), '.
|
|
7
|
+
const registryPath = path.join(process.cwd(), '.mc', `${domain}-registry.json`);
|
|
8
8
|
let registry;
|
|
9
9
|
try {
|
|
10
10
|
registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
|
-
cliLogger.error({}, `Registry not found at ${registryPath}. Run:
|
|
13
|
+
cliLogger.error({}, `Registry not found at ${registryPath}. Run: npx mc-domain-module build domains/${domain}`);
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
16
|
// Find Lambda ARN for the primitive
|
|
@@ -3,7 +3,7 @@ export interface ShowDnsOptions {
|
|
|
3
3
|
projectDir?: string;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* mc-domain-module show-dns [--env dev|staging|prod] [--project-dir <path>]
|
|
7
7
|
*
|
|
8
8
|
* Reads the scaffold-config.json and prints DNS records that must be added for
|
|
9
9
|
* custom domain wiring: certificate validation CNAMEs (step 1) and traffic
|