@mettlecast/domain-cli 0.2.86 → 0.2.87
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.d.ts +8 -2
- package/dist/builder/build-registry.js +80 -88
- package/dist/commands/update-all.d.ts +1 -0
- package/dist/commands/update-all.js +174 -0
- package/dist/commands/validate.js +26 -59
- package/dist/utils/toolchain-manifest.d.ts +90 -0
- package/dist/utils/toolchain-manifest.js +144 -0
- package/package.json +3 -3
- package/scripts/generate-toolchain-manifest.mjs +163 -0
- package/src/__tests__/build-registry.test.ts +125 -3
- package/src/__tests__/commands/update-all.test.ts +341 -3
- package/src/__tests__/package-freshness.test.ts +8 -4
- package/src/__tests__/toolchain-manifest.test.ts +185 -0
- package/src/__tests__/validate.test.ts +183 -108
- package/src/builder/build-registry.ts +116 -92
- package/src/commands/update-all.ts +216 -0
- package/src/commands/validate.ts +29 -70
- package/src/utils/toolchain-manifest.ts +236 -0
- package/toolchain-manifest.json +10 -0
package/src/commands/validate.ts
CHANGED
|
@@ -21,12 +21,6 @@ type ActionApiExposureWithDeclaration = {
|
|
|
21
21
|
tenancyDeclared?: boolean;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
type ActionRegistryEntryWithDeclaration = ActionRegistryEntry & {
|
|
25
|
-
backendAccess?: 'private' | 'domain' | 'platform';
|
|
26
|
-
exposureDeclared?: boolean;
|
|
27
|
-
exposure: { type: 'internal' } | ActionApiExposureWithDeclaration;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
24
|
/**
|
|
31
25
|
* A single validation failure.
|
|
32
26
|
*/
|
|
@@ -138,32 +132,7 @@ async function checkRawPathViolations(
|
|
|
138
132
|
}
|
|
139
133
|
|
|
140
134
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* Mirrors the invariants enforced at CDK synth time by
|
|
144
|
-
* `SecurityAssertionAspect` in `@mettlecast/domain-cdk-packer`. Running
|
|
145
|
-
* them at the CLI stage means developers get a structured error code in
|
|
146
|
-
* their terminal and CI fails on `mc-domain-module validate` BEFORE a
|
|
147
|
-
* (potentially expensive) `cdk synth` is attempted.
|
|
148
|
-
*
|
|
149
|
-
* Each rule maps 1:1 to an aspect annotation code so downstream tooling
|
|
150
|
-
* can correlate build-time and synth-time failures.
|
|
151
|
-
*
|
|
152
|
-
* Issue #4689: the legacy `defineApi` factory and `registry.apis` field
|
|
153
|
-
* were removed. Deployment-time API invariants are enforced against the
|
|
154
|
-
* `actions[]` API-exposure surface — see `checkActionFirstSecurity`.
|
|
155
|
-
*/
|
|
156
|
-
function checkDeploymentSecurity(_actions: ActionRegistryEntry[]): ValidationError[] {
|
|
157
|
-
// Action API exposures are already covered by `checkActionFirstSecurity`
|
|
158
|
-
// for `tenancy: 'required'` paths and `auth: 'none'` exceptions. The
|
|
159
|
-
// codes there (TENANT_API_PATH_REQUIRED, AUTH_NONE_REQUIRES_EXCEPTION)
|
|
160
|
-
// are kept stable for back-compat — they map to the same aspect codes.
|
|
161
|
-
//
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Action-first security validation rules (#4619, Wave 6 Task 6.1).
|
|
135
|
+
* Check action-first security invariants.
|
|
167
136
|
*
|
|
168
137
|
* These checks enforce the action-first security model on the
|
|
169
138
|
* serialised DomainRegistry produced by `buildRegistry`. The rules
|
|
@@ -173,36 +142,42 @@ function checkDeploymentSecurity(_actions: ActionRegistryEntry[]): ValidationErr
|
|
|
173
142
|
*
|
|
174
143
|
* Each rule emits a structured `ValidationError` whose `code` is the
|
|
175
144
|
* rule ID listed in the spec (e.g. `ACTION_EXPOSURE_REQUIRED`,
|
|
176
|
-
* `TENANT_API_PATH_REQUIRED`).
|
|
177
|
-
* or API id and the actionable fix.
|
|
145
|
+
* `TENANT_API_PATH_REQUIRED`, `API_INPUT_SCHEMA_REQUIRED`).
|
|
178
146
|
*/
|
|
179
147
|
function checkActionFirstSecurity(actions: ActionRegistryEntry[]): ValidationError[] {
|
|
180
148
|
const errors: ValidationError[] = [];
|
|
181
149
|
|
|
182
|
-
for (const action of actions
|
|
150
|
+
for (const action of actions) {
|
|
183
151
|
const id = action.id;
|
|
184
152
|
|
|
185
153
|
// ACTION_EXPOSURE_REQUIRED — every action registry entry must have
|
|
186
|
-
// `exposure` declared
|
|
187
|
-
// exposure
|
|
188
|
-
// and
|
|
189
|
-
|
|
190
|
-
// `backendAccess`) are also allowed to default during migration.
|
|
191
|
-
if (action.exposureDeclared === false && action.backendAccess !== 'private') {
|
|
192
|
-
// Only fire for non-private actions: a private action with no
|
|
193
|
-
// exposure is the natural migration state for legacy
|
|
194
|
-
// visibility:'private' handlers, and forcing exposure would
|
|
195
|
-
// produce noisy errors during the migration window.
|
|
196
|
-
// (When Wave 7+ removes the legacy visibility alias this branch
|
|
197
|
-
// becomes a hard error for every action.)
|
|
154
|
+
// `exposure` declared and valid. The buildRegistry now rejects
|
|
155
|
+
// missing/invalid exposure at build time, but we also verify here
|
|
156
|
+
// for belt-and-suspenders.
|
|
157
|
+
if (!action.exposure) {
|
|
198
158
|
errors.push({
|
|
199
159
|
code: 'ACTION_EXPOSURE_REQUIRED',
|
|
200
|
-
message: `Action '${id}' has no
|
|
160
|
+
message: `Action '${id}' has no \`exposure\`. Every action must declare \`exposure\` explicitly.`,
|
|
201
161
|
});
|
|
162
|
+
continue;
|
|
202
163
|
}
|
|
203
164
|
|
|
204
165
|
if (action.exposure.type === 'api') {
|
|
205
|
-
errors.push(...checkApiExposureSecurity(action.id, action.exposure));
|
|
166
|
+
errors.push(...checkApiExposureSecurity(action.id, action.exposure as unknown as ActionApiExposureWithDeclaration));
|
|
167
|
+
|
|
168
|
+
// API actions must have input/output schema snapshots
|
|
169
|
+
if (!action.inputSchema) {
|
|
170
|
+
errors.push({
|
|
171
|
+
code: 'API_INPUT_SCHEMA_REQUIRED',
|
|
172
|
+
message: `Action '${id}' is API-exposed but has no \`inputSchema\`. API-exposed actions must declare input/output Zod schemas.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!action.outputSchema) {
|
|
176
|
+
errors.push({
|
|
177
|
+
code: 'API_OUTPUT_SCHEMA_REQUIRED',
|
|
178
|
+
message: `Action '${id}' is API-exposed but has no \`outputSchema\`. API-exposed actions must declare input/output Zod schemas.`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
206
181
|
}
|
|
207
182
|
}
|
|
208
183
|
|
|
@@ -217,9 +192,8 @@ function checkApiExposureSecurity(actionId: string, exposure: ActionApiExposureW
|
|
|
217
192
|
const errors: ValidationError[] = [];
|
|
218
193
|
|
|
219
194
|
// API_EXPOSURE_AUTH_REQUIRED — API-exposed actions must declare auth
|
|
220
|
-
// explicitly. The builder
|
|
221
|
-
//
|
|
222
|
-
// silently relying on the safe-default.
|
|
195
|
+
// explicitly. The builder now rejects missing auth, but we validate
|
|
196
|
+
// here too for completeness.
|
|
223
197
|
if (exposure.authDeclared === false) {
|
|
224
198
|
errors.push({
|
|
225
199
|
code: 'API_EXPOSURE_AUTH_REQUIRED',
|
|
@@ -251,10 +225,7 @@ function checkApiExposureSecurity(actionId: string, exposure: ActionApiExposureW
|
|
|
251
225
|
}
|
|
252
226
|
|
|
253
227
|
// TENANCY_NONE_REQUIRES_REASON_WHEN_PUBLIC — `tenancy: 'none'` on a
|
|
254
|
-
// public API route must justify the missing tenant context.
|
|
255
|
-
// route is already justified as anonymous via `auth: 'none'` the
|
|
256
|
-
// same `securityException` may be reused; otherwise an exception is
|
|
257
|
-
// required for tenancy: 'none' on its own.
|
|
228
|
+
// public API route must justify the missing tenant context.
|
|
258
229
|
if (exposure.tenancy === 'none') {
|
|
259
230
|
const reason = exposure.securityException?.reason;
|
|
260
231
|
if (!reason || reason.trim().length === 0) {
|
|
@@ -267,8 +238,7 @@ function checkApiExposureSecurity(actionId: string, exposure: ActionApiExposureW
|
|
|
267
238
|
|
|
268
239
|
// SYSTEM_API_REQUIRES_ROLE — `tenancy: 'system'` combined with
|
|
269
240
|
// `auth: 'required'` must declare non-empty `roles` so the JWT
|
|
270
|
-
// authorizer can scope the call.
|
|
271
|
-
// are service-only and do not require role narrowing.
|
|
241
|
+
// authorizer can scope the call.
|
|
272
242
|
if (exposure.tenancy === 'system' && exposure.auth === 'required') {
|
|
273
243
|
if (!Array.isArray(exposure.roles) || exposure.roles.length === 0) {
|
|
274
244
|
errors.push({
|
|
@@ -336,23 +306,12 @@ export async function runValidate(
|
|
|
336
306
|
|
|
337
307
|
errors.push(...checkSubscriberSemverRanges(registry.subscribers));
|
|
338
308
|
|
|
339
|
-
// Issue #4689: defineApi and registry.apis were removed. The action-first
|
|
340
|
-
// validation rules in `checkActionFirstSecurity` cover the action surface
|
|
341
|
-
// that now owns all HTTP endpoints.
|
|
342
|
-
|
|
343
309
|
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|
|
344
310
|
errors.push(...rawPathErrors);
|
|
345
311
|
|
|
346
|
-
//
|
|
347
|
-
// Runs against the registry built above so the rules see the same
|
|
348
|
-
// shape the CDK packer will eventually consume.
|
|
312
|
+
// Action-first security validation gates (#5090).
|
|
349
313
|
errors.push(...checkActionFirstSecurity(registry.actions));
|
|
350
314
|
|
|
351
|
-
// Issue #4662 Task D — deployment-time security gates. These mirror
|
|
352
|
-
// the CDK synth-time `SecurityAssertionAspect` so violations are
|
|
353
|
-
// caught before any AWS deployment is attempted.
|
|
354
|
-
errors.push(...checkDeploymentSecurity(registry.actions));
|
|
355
|
-
|
|
356
315
|
const totalPrimitives = registry.webhooks.length +
|
|
357
316
|
registry.subscribers.length + registry.schedules.length +
|
|
358
317
|
registry.jobs.length + registry.actions.length;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Toolchain manifest types and factory for the @mettlecast publish workflow.
|
|
8
|
+
*
|
|
9
|
+
* The manifest is included in the published domain-cli tarball so that
|
|
10
|
+
* `mc-domain-module update-all` (or a future standalone updater) can
|
|
11
|
+
* determine exact package versions to install / pin.
|
|
12
|
+
*
|
|
13
|
+
* Manifest schema:
|
|
14
|
+
* ```json
|
|
15
|
+
* {
|
|
16
|
+
* "schemaVersion": 1,
|
|
17
|
+
* "registrySchemaVersion": "1",
|
|
18
|
+
* "packages": {
|
|
19
|
+
* "domainCli": "0.2.53",
|
|
20
|
+
* "domainCdkPacker": "0.2.53",
|
|
21
|
+
* "domainRuntime": "0.2.53",
|
|
22
|
+
* "eslintPluginDomainModule": "0.2.53"
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* `registrySchemaVersion` matches the existing `DomainRegistry.schemaVersion`
|
|
28
|
+
* string literal `'1'` so that consumer validation can compare the manifest
|
|
29
|
+
* registry schema version to the generated registry schema version directly.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Shape of the published toolchain-manifest.json at package root.
|
|
34
|
+
*/
|
|
35
|
+
export interface ToolchainManifest {
|
|
36
|
+
/** Manifest schema version (integer). Must be 1 for this generation. */
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
/**
|
|
39
|
+
* Registry schema version — a string matching the `DomainRegistry.schemaVersion`
|
|
40
|
+
* literal (currently `'1'`). Consumer validation compares this against the
|
|
41
|
+
* generated registry schema version to verify contract compatibility.
|
|
42
|
+
*/
|
|
43
|
+
registrySchemaVersion: string;
|
|
44
|
+
packages: ToolchainPackages;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ToolchainPackages {
|
|
48
|
+
domainCli: string;
|
|
49
|
+
domainCdkPacker: string;
|
|
50
|
+
domainRuntime: string;
|
|
51
|
+
eslintPluginDomainModule: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The expected registry schema version that matches `DomainRegistry.schemaVersion`.
|
|
56
|
+
* Consumer validation compares the manifest's `registrySchemaVersion` against
|
|
57
|
+
* this constant and the generated registry's `schemaVersion`.
|
|
58
|
+
*/
|
|
59
|
+
export const EXPECTED_REGISTRY_SCHEMA_VERSION = '1';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Package identifiers used in the manifest.
|
|
63
|
+
* Maps to the `packages` key in ToolchainManifest.
|
|
64
|
+
*/
|
|
65
|
+
export type ToolchainPackageId = keyof ToolchainPackages;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a ToolchainManifest from exact resolved versions.
|
|
69
|
+
* This is a pure factory function (no side-effects).
|
|
70
|
+
*
|
|
71
|
+
* @param versions - Exact semver strings for each package
|
|
72
|
+
* @returns A complete ToolchainManifest
|
|
73
|
+
*/
|
|
74
|
+
export function createToolchainManifest(versions: ToolchainPackages): ToolchainManifest {
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
registrySchemaVersion: EXPECTED_REGISTRY_SCHEMA_VERSION,
|
|
78
|
+
packages: {
|
|
79
|
+
domainCli: versions.domainCli,
|
|
80
|
+
domainCdkPacker: versions.domainCdkPacker,
|
|
81
|
+
domainRuntime: versions.domainRuntime,
|
|
82
|
+
eslintPluginDomainModule: versions.eslintPluginDomainModule,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Known @mettlecast dependency names that the domain-cli may reference
|
|
89
|
+
* with wildcard ranges and should be replaced with exact versions.
|
|
90
|
+
*/
|
|
91
|
+
export const METTLECAST_CLI_DEPS: Record<ToolchainPackageId, string> = {
|
|
92
|
+
domainCli: '@mettlecast/domain-cli',
|
|
93
|
+
domainCdkPacker: '@mettlecast/domain-cdk-packer',
|
|
94
|
+
domainRuntime: '@mettlecast/domain-runtime',
|
|
95
|
+
eslintPluginDomainModule: '@mettlecast/eslint-plugin-domain-module',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rewrite the domain-cli package.json's @mettlecast dependencies from
|
|
100
|
+
* wildcard / workspace ranges to exact resolved versions.
|
|
101
|
+
*
|
|
102
|
+
* @param pkgJson - Parsed package.json content (mutated in place)
|
|
103
|
+
* @param versions - Exact semver versions for each toolchain package
|
|
104
|
+
* @returns True if any dependency was rewritten
|
|
105
|
+
*/
|
|
106
|
+
export function rewriteCliDependencies(
|
|
107
|
+
pkgJson: { dependencies?: Record<string, string> },
|
|
108
|
+
versions: ToolchainPackages,
|
|
109
|
+
): boolean {
|
|
110
|
+
let changed = false;
|
|
111
|
+
|
|
112
|
+
if (!pkgJson.dependencies) return changed;
|
|
113
|
+
|
|
114
|
+
for (const [id, depName] of Object.entries(METTLECAST_CLI_DEPS)) {
|
|
115
|
+
const packageId = id as ToolchainPackageId;
|
|
116
|
+
if (depName in pkgJson.dependencies) {
|
|
117
|
+
const exact = versions[packageId];
|
|
118
|
+
pkgJson.dependencies[depName] = exact;
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return changed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the toolchain manifest from the installed @mettlecast/domain-cli package.
|
|
128
|
+
* Tries the project's `node_modules/@mettlecast/domain-cli/` first, then falls
|
|
129
|
+
* back to the CLI's own package root (for global/npx installations).
|
|
130
|
+
*
|
|
131
|
+
* @param projectRoot - Project root directory.
|
|
132
|
+
* @returns The parsed toolchain manifest.
|
|
133
|
+
* @throws If the manifest is missing, malformed, or has an incompatible schema version.
|
|
134
|
+
*/
|
|
135
|
+
export async function loadToolchainManifest(projectRoot: string): Promise<ToolchainManifest> {
|
|
136
|
+
// Primary path: project's node_modules
|
|
137
|
+
const projectManifestPath = join(
|
|
138
|
+
projectRoot,
|
|
139
|
+
'node_modules',
|
|
140
|
+
'@mettlecast',
|
|
141
|
+
'domain-cli',
|
|
142
|
+
'toolchain-manifest.json',
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Fallback path: CLI's own package root (resolved from this module's location)
|
|
146
|
+
const cliPackageRoot = resolveCliPackageRoot();
|
|
147
|
+
const cliManifestPath = join(cliPackageRoot, 'toolchain-manifest.json');
|
|
148
|
+
|
|
149
|
+
let manifestPath: string;
|
|
150
|
+
|
|
151
|
+
if (existsSync(projectManifestPath)) {
|
|
152
|
+
manifestPath = projectManifestPath;
|
|
153
|
+
} else if (existsSync(cliManifestPath)) {
|
|
154
|
+
manifestPath = cliManifestPath;
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`toolchain-manifest.json not found at ${projectManifestPath} or ${cliManifestPath}. ` +
|
|
158
|
+
"Run 'npm install' or 'npm ci' in the project root first, then retry.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const raw = await readFile(manifestPath, 'utf-8');
|
|
163
|
+
let manifest: ToolchainManifest;
|
|
164
|
+
try {
|
|
165
|
+
manifest = JSON.parse(raw) as ToolchainManifest;
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`toolchain-manifest.json at ${manifestPath} is not valid JSON.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate schema version
|
|
173
|
+
if (typeof manifest.schemaVersion !== 'number' || manifest.schemaVersion !== 1) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`toolchain manifest schemaVersion is ${String(manifest.schemaVersion)}, expected 1. ` +
|
|
176
|
+
'The installed @mettlecast/domain-cli version is incompatible with this toolchain resolver.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Validate registry schema version equals expected value
|
|
181
|
+
if (
|
|
182
|
+
typeof manifest.registrySchemaVersion !== 'string' ||
|
|
183
|
+
manifest.registrySchemaVersion !== EXPECTED_REGISTRY_SCHEMA_VERSION
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`toolchain manifest registrySchemaVersion is "${String(manifest.registrySchemaVersion)}", ` +
|
|
187
|
+
`expected "${EXPECTED_REGISTRY_SCHEMA_VERSION}". ` +
|
|
188
|
+
'The registry schema contract is incompatible with this toolchain resolver.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validate all package keys are present and non-empty
|
|
193
|
+
if (!manifest.packages || typeof manifest.packages !== 'object') {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'toolchain manifest is missing required field "packages".',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const TOOLCHAIN_PACKAGE_KEYS: Array<keyof ToolchainPackages> = [
|
|
200
|
+
'domainCli',
|
|
201
|
+
'domainCdkPacker',
|
|
202
|
+
'domainRuntime',
|
|
203
|
+
'eslintPluginDomainModule',
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (const key of TOOLCHAIN_PACKAGE_KEYS) {
|
|
207
|
+
const version = manifest.packages[key];
|
|
208
|
+
if (typeof version !== 'string' || version.length === 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`toolchain manifest is missing or has empty version for package "${key}".`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return manifest;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve the @mettlecast/domain-cli package root directory.
|
|
220
|
+
* Uses the location of this module file (toolchain-manifest.ts) to find
|
|
221
|
+
* the CLI package root, supporting both source and dist layouts.
|
|
222
|
+
*/
|
|
223
|
+
function resolveCliPackageRoot(): string {
|
|
224
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
225
|
+
// In source: packages/domain-cli/src/utils/toolchain-manifest.ts -> up 3 levels
|
|
226
|
+
// In dist: packages/domain-cli/dist/utils/toolchain-manifest.js -> up 2 levels
|
|
227
|
+
const candidate = dirname(dirname(dirname(thisFile)));
|
|
228
|
+
// Check if candidate has package.json with @mettlecast/domain-cli name
|
|
229
|
+
const pkgJsonPath = join(candidate, 'package.json');
|
|
230
|
+
if (existsSync(pkgJsonPath)) {
|
|
231
|
+
return candidate;
|
|
232
|
+
}
|
|
233
|
+
// Fall back to dirname(dirname(thisFile)) for dist layout
|
|
234
|
+
const candidate2 = dirname(dirname(thisFile));
|
|
235
|
+
return candidate2;
|
|
236
|
+
}
|