nexarch 0.12.18 → 0.12.22
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/commands/ingest-infra.js +60 -8
- package/dist/commands/init-project-infra.js +46 -12
- package/dist/commands/init-project.js +65 -4
- package/dist/index.js +13 -0
- package/dist/lib/iac-catalogue-seed.js +540 -1
- package/dist/lib/mcp.js +2 -15
- package/dist/lib/terraform-detect.js +70 -3
- package/dist/lib/terraform-projection.js +1 -1
- package/dist/lib/version.js +41 -0
- package/package.json +1 -1
|
@@ -6,6 +6,45 @@ import { parseTerraformStateBuffer, projectTerraformState, inferEnvironmentFromT
|
|
|
6
6
|
import { loadIacCatalogue, describeCatalogue } from "../lib/iac-catalogue.js";
|
|
7
7
|
import { discoverRootModules, environmentFromPath } from "../lib/terraform-detect.js";
|
|
8
8
|
const ENTITY_BATCH = 50;
|
|
9
|
+
/**
|
|
10
|
+
* Terraform provider prefix -> the platform it provisions.
|
|
11
|
+
*
|
|
12
|
+
* Keyed off the resource type prefix rather than a provider block, because a
|
|
13
|
+
* prefix is available for every resource the projector already read and
|
|
14
|
+
* needs no second pass over the raw state. `resolveNames` are tried against
|
|
15
|
+
* the reference library first, so the platform entity stays canonical rather
|
|
16
|
+
* than a tenant inventing its own "AWS" every time; `fallbackRef`/`fallbackName`
|
|
17
|
+
* are what is used when that resolution is skipped (a dry run) or fails.
|
|
18
|
+
*/
|
|
19
|
+
const PROVIDER_PLATFORMS = [
|
|
20
|
+
{ prefix: "azurerm_", resolveNames: ["azure", "microsoft azure"], fallbackRef: "platform:azure", fallbackName: "Microsoft Azure" },
|
|
21
|
+
{ prefix: "aws_", resolveNames: ["aws", "amazon web services"], fallbackRef: "platform:aws", fallbackName: "Amazon Web Services" },
|
|
22
|
+
{ prefix: "google_", resolveNames: ["gcp", "google cloud platform"], fallbackRef: "platform:gcp", fallbackName: "Google Cloud Platform" },
|
|
23
|
+
{ prefix: "google-beta_", resolveNames: ["gcp", "google cloud platform"], fallbackRef: "platform:gcp", fallbackName: "Google Cloud Platform" },
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Picks the platform an estate is provisioned on from the resource types
|
|
27
|
+
* actually seen — projected and unrecognised alike, since even a type the
|
|
28
|
+
* catalogue does not cover yet still carries its provider prefix. Ties and
|
|
29
|
+
* mixed-provider states resolve to whichever provider has the most
|
|
30
|
+
* resources: real multi-cloud-in-one-root estates are rare, and a single
|
|
31
|
+
* dominant platform is a far better default than reverting to a hardcoded
|
|
32
|
+
* one. A root module ingested with none of the providers above recognised
|
|
33
|
+
* gets no free-text guess — better to say nothing than invent a platform
|
|
34
|
+
* name from an unfamiliar prefix.
|
|
35
|
+
*/
|
|
36
|
+
function detectPlatform(resourceTypes) {
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
for (const resourceType of resourceTypes) {
|
|
39
|
+
const platform = PROVIDER_PLATFORMS.find((p) => resourceType.startsWith(p.prefix));
|
|
40
|
+
if (platform)
|
|
41
|
+
counts.set(platform.prefix, (counts.get(platform.prefix) ?? 0) + 1);
|
|
42
|
+
}
|
|
43
|
+
if (counts.size === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const [topPrefix] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
46
|
+
return PROVIDER_PLATFORMS.find((p) => p.prefix === topPrefix) ?? null;
|
|
47
|
+
}
|
|
9
48
|
function readResult(raw) {
|
|
10
49
|
return JSON.parse(raw.content?.[0]?.text ?? "{}");
|
|
11
50
|
}
|
|
@@ -174,7 +213,13 @@ export function buildIngestPayload(params) {
|
|
|
174
213
|
attributes: { source: "terraform_ingest", terraform_version: projection.terraformVersion },
|
|
175
214
|
},
|
|
176
215
|
];
|
|
177
|
-
const relationships = [
|
|
216
|
+
const relationships = [
|
|
217
|
+
// The environment lives on the platform. Without this edge the two nodes
|
|
218
|
+
// this payload always creates float beside each other, and no query can
|
|
219
|
+
// answer "which cloud is dev on" — every resource knows both, but the
|
|
220
|
+
// environment itself knows neither.
|
|
221
|
+
{ relationshipTypeCode: "runs_on", fromEntityRef: environmentRef, toEntityRef: platformRef },
|
|
222
|
+
];
|
|
178
223
|
// Lookup tables for the topology pass below, populated alongside entities so
|
|
179
224
|
// a single loop over projection.resources is enough for both.
|
|
180
225
|
const entityRefByTypeAndName = new Map();
|
|
@@ -403,14 +448,21 @@ export async function ingestInfra(args) {
|
|
|
403
448
|
}
|
|
404
449
|
const { environment, how } = resolveEnvironment(projection, environmentOverride, workingDir, statePath ? null : workingDir);
|
|
405
450
|
projection.environment = environment;
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
451
|
+
// Detected from what is actually in state, not assumed: an estate is read
|
|
452
|
+
// once and the provider it is on follows from that, the same way the
|
|
453
|
+
// catalogue itself covers whichever providers have entries rather than
|
|
454
|
+
// just Azure. Resolving the detected platform through the reference
|
|
455
|
+
// library keeps the entity canonical rather than inventing a second "AWS"
|
|
456
|
+
// for every tenant.
|
|
457
|
+
const detectedPlatform = detectPlatform([
|
|
458
|
+
...projection.resources.map((r) => r.resourceType),
|
|
459
|
+
...projection.unknownTypes.map((u) => u.resourceType),
|
|
460
|
+
]);
|
|
461
|
+
let platformRef = detectedPlatform?.fallbackRef ?? "platform:unknown";
|
|
462
|
+
let platformName = detectedPlatform?.fallbackName ?? "Unrecognised cloud platform";
|
|
463
|
+
if (!dryRun && detectedPlatform) {
|
|
412
464
|
try {
|
|
413
|
-
const raw = await callMcpTool("nexarch_resolve_reference", { names:
|
|
465
|
+
const raw = await callMcpTool("nexarch_resolve_reference", { names: detectedPlatform.resolveNames });
|
|
414
466
|
const resolved = readResult(raw);
|
|
415
467
|
const hit = (resolved.results ?? []).find((r) => r.resolved && r.entityTypeCode === "platform" && r.canonicalExternalRef);
|
|
416
468
|
if (hit?.canonicalExternalRef) {
|
|
@@ -65,15 +65,17 @@ function printNextSteps(steps, registered) {
|
|
|
65
65
|
*/
|
|
66
66
|
function reportDryRun(params) {
|
|
67
67
|
const { asJson, projectRef, detection, entities, relationships } = params;
|
|
68
|
-
const
|
|
68
|
+
const isLibrary = detection.repoKind === "module_library";
|
|
69
|
+
const nextSteps = isLibrary ? [] : nextStepsFor(projectRef, detection);
|
|
69
70
|
if (asJson) {
|
|
70
71
|
process.stdout.write(`${JSON.stringify({
|
|
71
72
|
ok: true,
|
|
72
73
|
dryRun: true,
|
|
73
74
|
wrote: false,
|
|
74
75
|
projectRef,
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
repoKind: detection.repoKind,
|
|
77
|
+
registrationStatus: isLibrary ? "complete" : "skeleton_only",
|
|
78
|
+
enrichmentCompleted: isLibrary,
|
|
77
79
|
detection,
|
|
78
80
|
plan: { entities, relationships },
|
|
79
81
|
nextSteps,
|
|
@@ -88,6 +90,20 @@ function reportDryRun(params) {
|
|
|
88
90
|
for (const rel of relationships)
|
|
89
91
|
console.log(` ${rel.fromEntityRef} --${rel.relationshipTypeCode}--> ${rel.toEntityRef}`);
|
|
90
92
|
}
|
|
93
|
+
// A module library has no estate, so registration really is the whole job —
|
|
94
|
+
// and saying "run the commands above" when there are none is the same defect
|
|
95
|
+
// as the missing handoff, pointed the other way.
|
|
96
|
+
if (isLibrary) {
|
|
97
|
+
console.log(`\nThis repository publishes reusable modules; it does not provision an estate,`);
|
|
98
|
+
console.log(`so there is nothing to ingest and registration is the whole job.`);
|
|
99
|
+
if (detection.publishedModules.length > 0) {
|
|
100
|
+
console.log(`\n Modules published here:`);
|
|
101
|
+
for (const name of detection.publishedModules)
|
|
102
|
+
console.log(` ${name}`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`\nNothing was written. Re-run without --dry-run to register.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
91
107
|
// The dry run used to stop here, which made registration look like the whole
|
|
92
108
|
// job: a reader saw four entities and no hint that seven ingest runs were the
|
|
93
109
|
// actual work. A preview has to preview the handoff too.
|
|
@@ -116,6 +132,8 @@ export async function runInfrastructureOnboarding(options, detection) {
|
|
|
116
132
|
entityRef: projectRef,
|
|
117
133
|
entityTypeCode: "project",
|
|
118
134
|
entitySubtypeCode: "project_infrastructure",
|
|
135
|
+
// Both shapes are infrastructure projects; repo_kind above carries which,
|
|
136
|
+
// rather than inventing a subtype the ontology does not define.
|
|
119
137
|
name: displayName,
|
|
120
138
|
description: `Infrastructure-as-code repository provisioning the estate (${detection.signals.join(", ")}).`,
|
|
121
139
|
attributes: {
|
|
@@ -124,6 +142,8 @@ export async function runInfrastructureOnboarding(options, detection) {
|
|
|
124
142
|
terraform_file_count: detection.terraformFileCount,
|
|
125
143
|
has_remote_backend: detection.hasRemoteBackend,
|
|
126
144
|
root_module_count: detection.rootModules.length,
|
|
145
|
+
repo_kind: detection.repoKind,
|
|
146
|
+
...(detection.publishedModules.length > 0 ? { published_modules: detection.publishedModules.slice(0, 40) } : {}),
|
|
127
147
|
...(detection.ciSystem ? { ci_system: detection.ciSystem } : {}),
|
|
128
148
|
...(detection.moduleDirectories.length > 0 ? { terraform_modules: detection.moduleDirectories.slice(0, 25) } : {}),
|
|
129
149
|
},
|
|
@@ -177,11 +197,23 @@ export async function runInfrastructureOnboarding(options, detection) {
|
|
|
177
197
|
// the map up front — rather than letting the caller discover it through a
|
|
178
198
|
// failure — is what lets an agent finish the job unaided.
|
|
179
199
|
const roots = detection.rootModules;
|
|
180
|
-
const
|
|
181
|
-
|
|
200
|
+
const isLibrary = detection.repoKind === "module_library";
|
|
201
|
+
const nextSteps = isLibrary ? [] : nextStepsFor(projectRef, detection);
|
|
202
|
+
if (!asJson && !isLibrary && roots.length > 1)
|
|
182
203
|
printNextSteps(nextSteps, true);
|
|
204
|
+
if (!asJson && isLibrary) {
|
|
205
|
+
console.log(`\nThis repository publishes reusable modules; it does not provision an estate.`);
|
|
206
|
+
console.log(`There is no state to read, so there is nothing to ingest — and that is the`);
|
|
207
|
+
console.log(`correct outcome rather than a gap. Its modules become architecture when a`);
|
|
208
|
+
console.log(`repository that *does* provision an estate calls them and is ingested.`);
|
|
209
|
+
if (detection.publishedModules.length > 0) {
|
|
210
|
+
console.log(`\n Modules published here:`);
|
|
211
|
+
for (const name of detection.publishedModules)
|
|
212
|
+
console.log(` ${name}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
183
215
|
let ingested = false;
|
|
184
|
-
if (!skipIngest && roots.length <= 1) {
|
|
216
|
+
if (!skipIngest && !isLibrary && roots.length <= 1) {
|
|
185
217
|
const shouldIngest = nonInteractive ? false : await confirm("\nRead the current state and populate the graph now?");
|
|
186
218
|
if (shouldIngest) {
|
|
187
219
|
try {
|
|
@@ -199,7 +231,7 @@ export async function runInfrastructureOnboarding(options, detection) {
|
|
|
199
231
|
// caller's report honest: the numbers it needs to decide "am I finished?"
|
|
200
232
|
// come from the command that did the work, not from a convention it half
|
|
201
233
|
// remembers.
|
|
202
|
-
const outstanding = ingested ? nextSteps.filter((step) => step.rootModule !== "." && roots.length > 1) : nextSteps;
|
|
234
|
+
const outstanding = isLibrary ? [] : ingested ? nextSteps.filter((step) => step.rootModule !== "." && roots.length > 1) : nextSteps;
|
|
203
235
|
const { file, snippet } = ciSnippetFor(detection.ciSystem, detection.ciFile);
|
|
204
236
|
if (asJson) {
|
|
205
237
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -214,11 +246,13 @@ export async function runInfrastructureOnboarding(options, detection) {
|
|
|
214
246
|
}, null, 2)}\n`);
|
|
215
247
|
return;
|
|
216
248
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
if (!isLibrary) {
|
|
250
|
+
console.log(`\nInfrastructure changes continuously, so ingestion belongs in the pipeline, not in someone's memory.`);
|
|
251
|
+
console.log(`Add this to ${file}:\n`);
|
|
252
|
+
console.log(snippet);
|
|
253
|
+
console.log(`\nNEXARCH_TOKEN is a service credential — create one in the workspace under`);
|
|
254
|
+
console.log(`Discovery → Agents, then store it as a masked CI variable.`);
|
|
255
|
+
}
|
|
222
256
|
if (outstanding.length > 0) {
|
|
223
257
|
console.log(`\nThis run registered the repository only. Until the ${outstanding.length} ingest command${outstanding.length === 1 ? "" : "s"} above`);
|
|
224
258
|
console.log(`have run, the graph knows the repo exists but nothing it provisions.`);
|
|
@@ -1003,7 +1003,63 @@ export function scanProject(dir) {
|
|
|
1003
1003
|
};
|
|
1004
1004
|
}
|
|
1005
1005
|
// ─── Relationship type selection ──────────────────────────────────────────────
|
|
1006
|
-
|
|
1006
|
+
/**
|
|
1007
|
+
* The relationship pairs this workspace's ontology actually permits.
|
|
1008
|
+
*
|
|
1009
|
+
* Populated once per run from the ingest contract. Empty means the contract
|
|
1010
|
+
* could not be read, in which case selection falls back to its preferences
|
|
1011
|
+
* unchecked — a scan that records slightly wrong relationships is worth more
|
|
1012
|
+
* than a scan that refuses to run because a read failed.
|
|
1013
|
+
*/
|
|
1014
|
+
let allowedLinks = new Set();
|
|
1015
|
+
function linkKey(from, rel, to) {
|
|
1016
|
+
return `${from}|${rel}|${to}`;
|
|
1017
|
+
}
|
|
1018
|
+
export async function loadAllowedLinks(companyId) {
|
|
1019
|
+
try {
|
|
1020
|
+
const raw = await callMcpTool("nexarch_get_ingest_contract", { companyId }, { companyId });
|
|
1021
|
+
const parsed = parseToolText(raw);
|
|
1022
|
+
const links = parsed.ontology?.allowedLinks ?? [];
|
|
1023
|
+
allowedLinks = new Set(links.map((l) => linkKey(l.fromEntityTypeCode, l.relationshipTypeCode, l.toEntityTypeCode)));
|
|
1024
|
+
return allowedLinks.size;
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
allowedLinks = new Set();
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Narrows a preferred relationship type to one the ontology accepts.
|
|
1033
|
+
*
|
|
1034
|
+
* The preference below encodes what a relationship *means*; this decides
|
|
1035
|
+
* whether the graph will take it from this particular source. Those came apart
|
|
1036
|
+
* in practice: a technology_component depending on a managed service was
|
|
1037
|
+
* offered `integrates_with`, which only accepts application-shaped sources, and
|
|
1038
|
+
* every such write was refused server-side. Refusals are silent unless someone
|
|
1039
|
+
* reads telemetry, so the scan looked clean and the edges simply were not there.
|
|
1040
|
+
*
|
|
1041
|
+
* Checking here rather than hoping also means an ontology change cannot quietly
|
|
1042
|
+
* invalidate a shipped CLI: the contract is read at run time, so the constraint
|
|
1043
|
+
* the gateway will apply is the constraint used to choose.
|
|
1044
|
+
*/
|
|
1045
|
+
function conformRelationshipType(preferred, fromEntityTypeCode, toEntityTypeCode) {
|
|
1046
|
+
if (allowedLinks.size === 0)
|
|
1047
|
+
return preferred;
|
|
1048
|
+
if (allowedLinks.has(linkKey(fromEntityTypeCode, preferred, toEntityTypeCode)))
|
|
1049
|
+
return preferred;
|
|
1050
|
+
// depends_on is the broadest runtime statement and the gateway's own
|
|
1051
|
+
// suggestion when it rejects one of these; part_of covers containment when
|
|
1052
|
+
// even that is not permitted.
|
|
1053
|
+
for (const fallback of ["depends_on", "runs_on", "part_of"]) {
|
|
1054
|
+
if (fallback !== preferred && allowedLinks.has(linkKey(fromEntityTypeCode, fallback, toEntityTypeCode)))
|
|
1055
|
+
return fallback;
|
|
1056
|
+
}
|
|
1057
|
+
// Nothing legal connects these two. Returning the preference lets the write
|
|
1058
|
+
// be refused with the gateway's own explanation, which is more useful than a
|
|
1059
|
+
// silently dropped relationship.
|
|
1060
|
+
return preferred;
|
|
1061
|
+
}
|
|
1062
|
+
function pickRelationshipType(toEntityTypeCode, toEntitySubtypeCode, fromEntityTypeCode = "application") {
|
|
1007
1063
|
switch (toEntityTypeCode) {
|
|
1008
1064
|
case "model":
|
|
1009
1065
|
return "uses_model";
|
|
@@ -1134,6 +1190,11 @@ export async function initProject(args) {
|
|
|
1134
1190
|
return;
|
|
1135
1191
|
}
|
|
1136
1192
|
}
|
|
1193
|
+
// Read before anything is chosen: relationship selection below narrows its
|
|
1194
|
+
// preferences to what this workspace's ontology actually accepts, and an
|
|
1195
|
+
// empty set means every preference goes through unchecked.
|
|
1196
|
+
const linkCount = await loadAllowedLinks(creds.companyId);
|
|
1197
|
+
logProgress("contract.loaded", `allowedLinks=${linkCount}`);
|
|
1137
1198
|
if (!asJson)
|
|
1138
1199
|
console.log(`Scanning ${dir}…`);
|
|
1139
1200
|
logProgress("scan.start", dir);
|
|
@@ -1511,7 +1572,7 @@ export async function initProject(args) {
|
|
|
1511
1572
|
if (!rootDepNames.has(r.input) && !rootDepNames.has(r.normalised))
|
|
1512
1573
|
continue;
|
|
1513
1574
|
const depSpec = rootDepVersions.get(r.input) ?? rootDepVersions.get(r.normalised);
|
|
1514
|
-
addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
|
|
1575
|
+
addRel(conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode), entityTypeOverride, r.entityTypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
|
|
1515
1576
|
source: depSpec?.source ?? "manifest_scan",
|
|
1516
1577
|
detected_at: nowIso,
|
|
1517
1578
|
...buildVersionAttributes(depSpec?.versionRaw ?? null, depSpec?.source ?? "manifest_scan"),
|
|
@@ -1541,7 +1602,7 @@ export async function initProject(args) {
|
|
|
1541
1602
|
for (const dep of sp.depSpecs) {
|
|
1542
1603
|
const r = resolvedByInput.get(dep.name);
|
|
1543
1604
|
if (r?.canonicalExternalRef && r.entityTypeCode) {
|
|
1544
|
-
addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.externalKey, r.canonicalExternalRef, 0.9, {
|
|
1605
|
+
addRel(conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.entityType, r.entityTypeCode), sp.externalKey, r.canonicalExternalRef, 0.9, {
|
|
1545
1606
|
source: dep.source,
|
|
1546
1607
|
detected_at: nowIso,
|
|
1547
1608
|
...buildVersionAttributes(dep.versionRaw, dep.source),
|
|
@@ -1829,7 +1890,7 @@ export async function initProject(args) {
|
|
|
1829
1890
|
.map((d) => resolvedByInput.get(d.name))
|
|
1830
1891
|
.filter((r) => !!r?.canonicalExternalRef)
|
|
1831
1892
|
.map((r) => {
|
|
1832
|
-
const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType);
|
|
1893
|
+
const relationshipTypeCode = conformRelationshipType(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.entityType, r.entityTypeCode);
|
|
1833
1894
|
const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1834
1895
|
return {
|
|
1835
1896
|
canonicalExternalRef: r.canonicalExternalRef,
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { proposalsStart } from "./commands/proposals-start.js";
|
|
|
28
28
|
import { registerRuntime } from "./commands/register-runtime.js";
|
|
29
29
|
import { ingestInfra } from "./commands/ingest-infra.js";
|
|
30
30
|
import { verifyTrust } from "./commands/verify-trust.js";
|
|
31
|
+
import { cliBuild, formatBuild } from "./lib/version.js";
|
|
31
32
|
const [, , command, ...args] = process.argv;
|
|
32
33
|
const commands = {
|
|
33
34
|
login,
|
|
@@ -75,6 +76,17 @@ async function main() {
|
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
// Answered before dispatch, and before any credential is required: "which
|
|
80
|
+
// copy am I running" has to work when nothing else does.
|
|
81
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
82
|
+
const build = cliBuild();
|
|
83
|
+
if (args.includes("--json"))
|
|
84
|
+
process.stdout.write(`${JSON.stringify(build, null, 2)}
|
|
85
|
+
`);
|
|
86
|
+
else
|
|
87
|
+
console.log(formatBuild(build));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
78
90
|
const handler = commands[command ?? ""];
|
|
79
91
|
if (!handler) {
|
|
80
92
|
console.log(`
|
|
@@ -83,6 +95,7 @@ nexarch — Your architecture workspace for AI delivery.
|
|
|
83
95
|
Usage:
|
|
84
96
|
nexarch login Authenticate in browser and store company-scoped credentials
|
|
85
97
|
Option: --company <id>
|
|
98
|
+
nexarch --version Print the version and the path this copy runs from
|
|
86
99
|
nexarch logout Remove stored credentials
|
|
87
100
|
nexarch status Check connection and show architecture summary
|
|
88
101
|
nexarch verify-trust Verify this repo's Nexarch trust attestation. Reads the token
|
|
@@ -12,9 +12,18 @@
|
|
|
12
12
|
* ahead of it: editing it by hand recreates the two-sources-of-truth problem
|
|
13
13
|
* moving the catalogue out of the CLI was meant to end.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* 86 entries, exported 2026-08-22.
|
|
16
16
|
*/
|
|
17
17
|
export const IAC_CATALOGUE_SEED = [
|
|
18
|
+
{
|
|
19
|
+
resourceType: "aws_ecs_cluster",
|
|
20
|
+
entityTypeCode: "platform_component",
|
|
21
|
+
entitySubtypeCode: "platform_runtime",
|
|
22
|
+
role: "compute_environment",
|
|
23
|
+
attributes: ["name"],
|
|
24
|
+
canonicalEntityRef: "global:platform_component:aws_ecs",
|
|
25
|
+
cloudResourceType: "AWS::ECS::Cluster",
|
|
26
|
+
},
|
|
18
27
|
{
|
|
19
28
|
resourceType: "azurerm_container_app_environment",
|
|
20
29
|
entityTypeCode: "platform_component",
|
|
@@ -34,6 +43,71 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
34
43
|
canonicalEntityRef: "global:platform_component:azure_app_service",
|
|
35
44
|
cloudResourceType: "Microsoft.Web/serverfarms",
|
|
36
45
|
},
|
|
46
|
+
{
|
|
47
|
+
resourceType: "aws_ecs_service",
|
|
48
|
+
entityTypeCode: "platform_component",
|
|
49
|
+
entitySubtypeCode: "platform_runtime",
|
|
50
|
+
role: "compute_host",
|
|
51
|
+
bindable: true,
|
|
52
|
+
attributes: ["name", "cluster", "task_definition", "desired_count", "launch_type"],
|
|
53
|
+
references: { "cluster": "aws_ecs_cluster" },
|
|
54
|
+
nameReferences: { "task_definition": "aws_ecs_task_definition" },
|
|
55
|
+
canonicalEntityRef: "global:platform_component:aws_ecs",
|
|
56
|
+
cloudResourceType: "AWS::ECS::Service",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
resourceType: "aws_ecs_task_definition",
|
|
60
|
+
entityTypeCode: "platform_component",
|
|
61
|
+
entitySubtypeCode: "platform_runtime",
|
|
62
|
+
role: "compute_host",
|
|
63
|
+
attributes: ["family", "network_mode", "requires_compatibilities", "cpu", "memory"],
|
|
64
|
+
canonicalEntityRef: "global:platform_component:aws_ecs",
|
|
65
|
+
cloudResourceType: "AWS::ECS::TaskDefinition",
|
|
66
|
+
nameAttribute: "family",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
resourceType: "aws_instance",
|
|
70
|
+
entityTypeCode: "platform_component",
|
|
71
|
+
entitySubtypeCode: "platform_runtime",
|
|
72
|
+
role: "compute_host",
|
|
73
|
+
bindable: true,
|
|
74
|
+
attributes: ["ami", "instance_type", "availability_zone", "subnet_id", "vpc_security_group_ids", "private_ip", "associate_public_ip_address", "iam_instance_profile"],
|
|
75
|
+
references: { "subnet_id": "aws_subnet" },
|
|
76
|
+
canonicalEntityRef: "global:platform_component:aws_ec2",
|
|
77
|
+
cloudResourceType: "AWS::EC2::Instance",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
resourceType: "aws_lambda_function",
|
|
81
|
+
entityTypeCode: "platform_component",
|
|
82
|
+
entitySubtypeCode: "platform_runtime",
|
|
83
|
+
role: "compute_host",
|
|
84
|
+
bindable: true,
|
|
85
|
+
attributes: ["function_name", "runtime", "handler", "memory_size", "timeout", "role", "architectures", "package_type"],
|
|
86
|
+
references: { "role": "aws_iam_role" },
|
|
87
|
+
canonicalEntityRef: "global:platform_component:aws_lambda",
|
|
88
|
+
cloudResourceType: "AWS::Lambda::Function",
|
|
89
|
+
nameAttribute: "function_name",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
resourceType: "azurerm_ai_foundry",
|
|
93
|
+
entityTypeCode: "platform_component",
|
|
94
|
+
entitySubtypeCode: "platform_service",
|
|
95
|
+
role: "compute_host",
|
|
96
|
+
attributes: ["name", "location", "resource_group_name", "storage_account_id", "key_vault_id", "public_network_access"],
|
|
97
|
+
references: { "key_vault_id": "azurerm_key_vault", "storage_account_id": "azurerm_storage_account" },
|
|
98
|
+
canonicalEntityRef: "global:platform_component:azure_ai_foundry",
|
|
99
|
+
cloudResourceType: "Microsoft.MachineLearningServices/workspaces",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
resourceType: "azurerm_ai_foundry_project",
|
|
103
|
+
entityTypeCode: "platform_component",
|
|
104
|
+
entitySubtypeCode: "platform_service",
|
|
105
|
+
role: "compute_host",
|
|
106
|
+
attributes: ["name", "location", "ai_services_hub_id", "description"],
|
|
107
|
+
references: { "ai_services_hub_id": "azurerm_ai_foundry" },
|
|
108
|
+
canonicalEntityRef: "global:platform_component:azure_ai_foundry",
|
|
109
|
+
cloudResourceType: "Microsoft.MachineLearningServices/workspaces/projects",
|
|
110
|
+
},
|
|
37
111
|
{
|
|
38
112
|
resourceType: "azurerm_cognitive_account",
|
|
39
113
|
entityTypeCode: "platform_component",
|
|
@@ -86,6 +160,45 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
86
160
|
canonicalEntityRef: "global:platform_component:azure_static_web_apps",
|
|
87
161
|
cloudResourceType: "Microsoft.Web/staticSites",
|
|
88
162
|
},
|
|
163
|
+
{
|
|
164
|
+
resourceType: "aws_db_instance",
|
|
165
|
+
entityTypeCode: "data_store",
|
|
166
|
+
entitySubtypeCode: "store_relational",
|
|
167
|
+
role: "data_store",
|
|
168
|
+
attributes: ["identifier", "engine", "engine_version", "instance_class", "allocated_storage", "publicly_accessible", "multi_az", "db_subnet_group_name"],
|
|
169
|
+
canonicalEntityRef: "global:platform_component:aws_rds",
|
|
170
|
+
cloudResourceType: "AWS::RDS::DBInstance",
|
|
171
|
+
nameAttribute: "identifier",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
resourceType: "aws_dynamodb_table",
|
|
175
|
+
entityTypeCode: "data_store",
|
|
176
|
+
entitySubtypeCode: "store_document",
|
|
177
|
+
role: "data_store",
|
|
178
|
+
attributes: ["name", "billing_mode", "hash_key", "range_key", "stream_enabled"],
|
|
179
|
+
canonicalEntityRef: "global:platform_component:aws_dynamodb",
|
|
180
|
+
cloudResourceType: "AWS::DynamoDB::Table",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
resourceType: "aws_elasticache_cluster",
|
|
184
|
+
entityTypeCode: "data_store",
|
|
185
|
+
entitySubtypeCode: "store_cache",
|
|
186
|
+
role: "data_store",
|
|
187
|
+
attributes: ["cluster_id", "engine", "engine_version", "node_type", "num_cache_nodes"],
|
|
188
|
+
canonicalEntityRef: "global:platform_component:aws_elasticache",
|
|
189
|
+
cloudResourceType: "AWS::ElastiCache::CacheCluster",
|
|
190
|
+
nameAttribute: "cluster_id",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
resourceType: "aws_s3_bucket",
|
|
194
|
+
entityTypeCode: "data_store",
|
|
195
|
+
entitySubtypeCode: "store_object",
|
|
196
|
+
role: "data_store",
|
|
197
|
+
attributes: ["bucket", "bucket_prefix", "force_destroy"],
|
|
198
|
+
canonicalEntityRef: "global:platform_component:aws_s3",
|
|
199
|
+
cloudResourceType: "AWS::S3::Bucket",
|
|
200
|
+
nameAttribute: "bucket",
|
|
201
|
+
},
|
|
89
202
|
{
|
|
90
203
|
resourceType: "azurerm_cosmosdb_account",
|
|
91
204
|
entityTypeCode: "data_store",
|
|
@@ -131,6 +244,65 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
131
244
|
canonicalEntityRef: "global:platform_component:azure_storage",
|
|
132
245
|
cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
|
|
133
246
|
},
|
|
247
|
+
{
|
|
248
|
+
resourceType: "aws_cloudfront_distribution",
|
|
249
|
+
entityTypeCode: "platform_component",
|
|
250
|
+
entitySubtypeCode: "platform_service",
|
|
251
|
+
role: "edge",
|
|
252
|
+
attributes: ["enabled", "comment", "default_root_object", "domain_name"],
|
|
253
|
+
canonicalEntityRef: "global:platform_component:aws_cloudfront",
|
|
254
|
+
cloudResourceType: "AWS::CloudFront::Distribution",
|
|
255
|
+
nameAttribute: "domain_name",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
resourceType: "aws_lb",
|
|
259
|
+
entityTypeCode: "platform_component",
|
|
260
|
+
entitySubtypeCode: "platform_service",
|
|
261
|
+
role: "edge",
|
|
262
|
+
attributes: ["name", "internal", "load_balancer_type", "security_groups", "subnets", "dns_name"],
|
|
263
|
+
canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
|
|
264
|
+
cloudResourceType: "AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
resourceType: "aws_lb_listener",
|
|
268
|
+
entityTypeCode: "platform_component",
|
|
269
|
+
entitySubtypeCode: "platform_service",
|
|
270
|
+
role: "edge",
|
|
271
|
+
attributes: ["port", "protocol", "load_balancer_arn"],
|
|
272
|
+
references: { "load_balancer_arn": "aws_lb" },
|
|
273
|
+
canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
|
|
274
|
+
cloudResourceType: "AWS::ElasticLoadBalancingV2::Listener",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
resourceType: "aws_lb_target_group",
|
|
278
|
+
entityTypeCode: "platform_component",
|
|
279
|
+
entitySubtypeCode: "platform_service",
|
|
280
|
+
role: "edge",
|
|
281
|
+
attributes: ["name", "port", "protocol", "target_type", "vpc_id"],
|
|
282
|
+
references: { "vpc_id": "aws_vpc" },
|
|
283
|
+
canonicalEntityRef: "global:platform_component:aws_elastic_load_balancing",
|
|
284
|
+
cloudResourceType: "AWS::ElasticLoadBalancingV2::TargetGroup",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
resourceType: "azurerm_cdn_frontdoor_custom_domain",
|
|
288
|
+
entityTypeCode: "platform_component",
|
|
289
|
+
entitySubtypeCode: "platform_service",
|
|
290
|
+
role: "edge",
|
|
291
|
+
attributes: ["name", "host_name", "cdn_frontdoor_profile_id", "dns_zone_id"],
|
|
292
|
+
references: { "dns_zone_id": "azurerm_dns_zone", "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
|
|
293
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
294
|
+
cloudResourceType: "Microsoft.Cdn/profiles/customDomains",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
resourceType: "azurerm_cdn_frontdoor_custom_domain_association",
|
|
298
|
+
entityTypeCode: "platform_component",
|
|
299
|
+
entitySubtypeCode: "platform_service",
|
|
300
|
+
role: "edge",
|
|
301
|
+
attributes: ["cdn_frontdoor_custom_domain_id"],
|
|
302
|
+
references: { "cdn_frontdoor_custom_domain_id": "azurerm_cdn_frontdoor_custom_domain" },
|
|
303
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
304
|
+
cloudResourceType: "Microsoft.Cdn/profiles/customDomains/associations",
|
|
305
|
+
},
|
|
134
306
|
{
|
|
135
307
|
resourceType: "azurerm_cdn_frontdoor_endpoint",
|
|
136
308
|
entityTypeCode: "platform_component",
|
|
@@ -149,6 +321,26 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
149
321
|
canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
|
|
150
322
|
cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
|
|
151
323
|
},
|
|
324
|
+
{
|
|
325
|
+
resourceType: "azurerm_cdn_frontdoor_origin",
|
|
326
|
+
entityTypeCode: "platform_component",
|
|
327
|
+
entitySubtypeCode: "platform_service",
|
|
328
|
+
role: "edge",
|
|
329
|
+
attributes: ["name", "host_name", "enabled", "certificate_name_check_enabled", "cdn_frontdoor_origin_group_id"],
|
|
330
|
+
references: { "cdn_frontdoor_origin_group_id": "azurerm_cdn_frontdoor_origin_group" },
|
|
331
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
332
|
+
cloudResourceType: "Microsoft.Cdn/profiles/originGroups/origins",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
resourceType: "azurerm_cdn_frontdoor_origin_group",
|
|
336
|
+
entityTypeCode: "platform_component",
|
|
337
|
+
entitySubtypeCode: "platform_service",
|
|
338
|
+
role: "edge",
|
|
339
|
+
attributes: ["name", "session_affinity_enabled", "cdn_frontdoor_profile_id"],
|
|
340
|
+
references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
|
|
341
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
342
|
+
cloudResourceType: "Microsoft.Cdn/profiles/originGroups",
|
|
343
|
+
},
|
|
152
344
|
{
|
|
153
345
|
resourceType: "azurerm_cdn_frontdoor_profile",
|
|
154
346
|
entityTypeCode: "platform_component",
|
|
@@ -158,6 +350,25 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
158
350
|
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
159
351
|
cloudResourceType: "Microsoft.Cdn/profiles",
|
|
160
352
|
},
|
|
353
|
+
{
|
|
354
|
+
resourceType: "azurerm_cdn_frontdoor_route",
|
|
355
|
+
entityTypeCode: "platform_component",
|
|
356
|
+
entitySubtypeCode: "platform_service",
|
|
357
|
+
role: "edge",
|
|
358
|
+
attributes: ["name", "enabled", "patterns_to_match", "supported_protocols", "forwarding_protocol", "https_redirect_enabled", "cdn_frontdoor_endpoint_id", "cdn_frontdoor_origin_group_id"],
|
|
359
|
+
references: { "cdn_frontdoor_endpoint_id": "azurerm_cdn_frontdoor_endpoint", "cdn_frontdoor_origin_group_id": "azurerm_cdn_frontdoor_origin_group" },
|
|
360
|
+
canonicalEntityRef: "global:platform_component:azure_front_door",
|
|
361
|
+
cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints/routes",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
resourceType: "azurerm_cdn_frontdoor_security_policy",
|
|
365
|
+
entityTypeCode: "policy_control",
|
|
366
|
+
role: "edge",
|
|
367
|
+
attributes: ["name", "cdn_frontdoor_profile_id"],
|
|
368
|
+
references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
|
|
369
|
+
canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
|
|
370
|
+
cloudResourceType: "Microsoft.Cdn/profiles/securityPolicies",
|
|
371
|
+
},
|
|
161
372
|
{
|
|
162
373
|
resourceType: "azurerm_static_web_app_custom_domain",
|
|
163
374
|
entityTypeCode: "platform_component",
|
|
@@ -187,6 +398,100 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
187
398
|
canonicalEntityRef: "global:platform_component:azure_traffic_manager",
|
|
188
399
|
cloudResourceType: "Microsoft.Network/trafficManagerProfiles",
|
|
189
400
|
},
|
|
401
|
+
{
|
|
402
|
+
resourceType: "aws_iam_policy",
|
|
403
|
+
entityTypeCode: "policy_control",
|
|
404
|
+
role: "governance",
|
|
405
|
+
attributes: ["name", "path", "description"],
|
|
406
|
+
canonicalEntityRef: "global:platform_component:aws_iam",
|
|
407
|
+
cloudResourceType: "AWS::IAM::ManagedPolicy",
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
resourceType: "aws_iam_role_policy_attachment",
|
|
411
|
+
entityTypeCode: "policy_control",
|
|
412
|
+
role: "governance",
|
|
413
|
+
attributes: ["role", "policy_arn"],
|
|
414
|
+
canonicalEntityRef: "global:platform_component:aws_iam",
|
|
415
|
+
cloudResourceType: "AWS::IAM::RolePolicyAttachment",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
resourceType: "azurerm_policy_definition_built_in",
|
|
419
|
+
entityTypeCode: "policy_control",
|
|
420
|
+
role: "governance",
|
|
421
|
+
attributes: ["name", "display_name", "policy_type", "mode"],
|
|
422
|
+
canonicalEntityRef: "global:platform_component:azure_policy",
|
|
423
|
+
cloudResourceType: "Microsoft.Authorization/policyDefinitions",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
resourceType: "azurerm_policy_set_definition",
|
|
427
|
+
entityTypeCode: "policy_control",
|
|
428
|
+
role: "governance",
|
|
429
|
+
attributes: ["name", "display_name", "policy_type", "management_group_id"],
|
|
430
|
+
canonicalEntityRef: "global:platform_component:azure_policy",
|
|
431
|
+
cloudResourceType: "Microsoft.Authorization/policySetDefinitions",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
resourceType: "azurerm_security_center_contact",
|
|
435
|
+
entityTypeCode: "policy_control",
|
|
436
|
+
role: "governance",
|
|
437
|
+
attributes: ["name", "alert_notifications", "alerts_to_admins"],
|
|
438
|
+
canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
|
|
439
|
+
cloudResourceType: "Microsoft.Security/securityContacts",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
resourceType: "azurerm_security_center_subscription_pricing",
|
|
443
|
+
entityTypeCode: "policy_control",
|
|
444
|
+
role: "governance",
|
|
445
|
+
attributes: ["tier", "resource_type"],
|
|
446
|
+
canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
|
|
447
|
+
cloudResourceType: "Microsoft.Security/pricings",
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
resourceType: "azurerm_security_center_workspace",
|
|
451
|
+
entityTypeCode: "policy_control",
|
|
452
|
+
role: "governance",
|
|
453
|
+
attributes: ["scope", "workspace_id"],
|
|
454
|
+
references: { "workspace_id": "azurerm_log_analytics_workspace" },
|
|
455
|
+
canonicalEntityRef: "global:platform_component:azure_defender_for_cloud",
|
|
456
|
+
cloudResourceType: "Microsoft.Security/workspaceSettings",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
resourceType: "azurerm_storage_container_immutability_policy",
|
|
460
|
+
entityTypeCode: "policy_control",
|
|
461
|
+
role: "governance",
|
|
462
|
+
attributes: ["storage_container_resource_manager_id", "immutability_period_in_days", "protected_append_writes_all_enabled"],
|
|
463
|
+
references: { "storage_container_resource_manager_id": "azurerm_storage_container" },
|
|
464
|
+
canonicalEntityRef: "global:platform_component:azure_storage",
|
|
465
|
+
cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies",
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
resourceType: "azurerm_subscription_policy_assignment",
|
|
469
|
+
entityTypeCode: "policy_control",
|
|
470
|
+
role: "governance",
|
|
471
|
+
attributes: ["name", "display_name", "policy_definition_id", "subscription_id", "enforce"],
|
|
472
|
+
references: { "policy_definition_id": "azurerm_policy_set_definition" },
|
|
473
|
+
canonicalEntityRef: "global:platform_component:azure_policy",
|
|
474
|
+
cloudResourceType: "Microsoft.Authorization/policyAssignments",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
resourceType: "aws_iam_role",
|
|
478
|
+
entityTypeCode: "platform_component",
|
|
479
|
+
entitySubtypeCode: "platform_control_plane",
|
|
480
|
+
role: "identity",
|
|
481
|
+
attributes: ["name", "path", "description", "max_session_duration"],
|
|
482
|
+
canonicalEntityRef: "global:platform_component:aws_iam",
|
|
483
|
+
cloudResourceType: "AWS::IAM::Role",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
resourceType: "azurerm_federated_identity_credential",
|
|
487
|
+
entityTypeCode: "platform_component",
|
|
488
|
+
entitySubtypeCode: "platform_control_plane",
|
|
489
|
+
role: "identity",
|
|
490
|
+
attributes: ["name", "resource_group_name", "audience", "issuer", "subject", "parent_id"],
|
|
491
|
+
references: { "parent_id": "azurerm_user_assigned_identity" },
|
|
492
|
+
canonicalEntityRef: "global:platform_component:azure_managed_identity",
|
|
493
|
+
cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials",
|
|
494
|
+
},
|
|
190
495
|
{
|
|
191
496
|
resourceType: "azurerm_role_assignment",
|
|
192
497
|
entityTypeCode: "policy_control",
|
|
@@ -204,6 +509,34 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
204
509
|
canonicalEntityRef: "global:platform_component:azure_managed_identity",
|
|
205
510
|
cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
206
511
|
},
|
|
512
|
+
{
|
|
513
|
+
resourceType: "aws_sns_topic",
|
|
514
|
+
entityTypeCode: "platform_component",
|
|
515
|
+
entitySubtypeCode: "platform_service",
|
|
516
|
+
role: "messaging",
|
|
517
|
+
attributes: ["name", "display_name", "fifo_topic"],
|
|
518
|
+
canonicalEntityRef: "global:platform_component:aws_sns",
|
|
519
|
+
cloudResourceType: "AWS::SNS::Topic",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
resourceType: "aws_sns_topic_subscription",
|
|
523
|
+
entityTypeCode: "platform_component",
|
|
524
|
+
entitySubtypeCode: "platform_service",
|
|
525
|
+
role: "messaging",
|
|
526
|
+
attributes: ["protocol", "topic_arn"],
|
|
527
|
+
references: { "topic_arn": "aws_sns_topic" },
|
|
528
|
+
canonicalEntityRef: "global:platform_component:aws_sns",
|
|
529
|
+
cloudResourceType: "AWS::SNS::Subscription",
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
resourceType: "aws_sqs_queue",
|
|
533
|
+
entityTypeCode: "platform_component",
|
|
534
|
+
entitySubtypeCode: "platform_service",
|
|
535
|
+
role: "messaging",
|
|
536
|
+
attributes: ["name", "fifo_queue", "visibility_timeout_seconds", "message_retention_seconds", "delay_seconds"],
|
|
537
|
+
canonicalEntityRef: "global:platform_component:aws_sqs",
|
|
538
|
+
cloudResourceType: "AWS::SQS::Queue",
|
|
539
|
+
},
|
|
207
540
|
{
|
|
208
541
|
resourceType: "azurerm_servicebus_namespace",
|
|
209
542
|
entityTypeCode: "platform_component",
|
|
@@ -223,6 +556,133 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
223
556
|
canonicalEntityRef: "global:platform_component:azure_service_bus",
|
|
224
557
|
cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
|
|
225
558
|
},
|
|
559
|
+
{
|
|
560
|
+
resourceType: "aws_db_subnet_group",
|
|
561
|
+
entityTypeCode: "platform_component",
|
|
562
|
+
entitySubtypeCode: "platform_service",
|
|
563
|
+
role: "network_boundary",
|
|
564
|
+
attributes: ["name", "vpc_id"],
|
|
565
|
+
references: { "vpc_id": "aws_vpc" },
|
|
566
|
+
canonicalEntityRef: "global:platform_component:aws_rds",
|
|
567
|
+
cloudResourceType: "AWS::RDS::DBSubnetGroup",
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
resourceType: "aws_internet_gateway",
|
|
571
|
+
entityTypeCode: "platform_component",
|
|
572
|
+
entitySubtypeCode: "platform_service",
|
|
573
|
+
role: "network_boundary",
|
|
574
|
+
attributes: ["vpc_id"],
|
|
575
|
+
references: { "vpc_id": "aws_vpc" },
|
|
576
|
+
canonicalEntityRef: "global:platform_component:aws_vpc",
|
|
577
|
+
cloudResourceType: "AWS::EC2::InternetGateway",
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
resourceType: "aws_nat_gateway",
|
|
581
|
+
entityTypeCode: "platform_component",
|
|
582
|
+
entitySubtypeCode: "platform_service",
|
|
583
|
+
role: "network_boundary",
|
|
584
|
+
attributes: ["subnet_id", "allocation_id", "connectivity_type"],
|
|
585
|
+
references: { "subnet_id": "aws_subnet" },
|
|
586
|
+
canonicalEntityRef: "global:platform_component:aws_vpc",
|
|
587
|
+
cloudResourceType: "AWS::EC2::NatGateway",
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
resourceType: "aws_route53_record",
|
|
591
|
+
entityTypeCode: "platform_component",
|
|
592
|
+
entitySubtypeCode: "platform_service",
|
|
593
|
+
role: "network_boundary",
|
|
594
|
+
attributes: ["name", "type", "ttl", "zone_id"],
|
|
595
|
+
canonicalEntityRef: "global:platform_component:aws_route53",
|
|
596
|
+
cloudResourceType: "AWS::Route53::RecordSet",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
resourceType: "aws_route53_zone",
|
|
600
|
+
entityTypeCode: "platform_component",
|
|
601
|
+
entitySubtypeCode: "platform_service",
|
|
602
|
+
role: "network_boundary",
|
|
603
|
+
attributes: ["name", "comment"],
|
|
604
|
+
canonicalEntityRef: "global:platform_component:aws_route53",
|
|
605
|
+
cloudResourceType: "AWS::Route53::HostedZone",
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
resourceType: "aws_route_table",
|
|
609
|
+
entityTypeCode: "platform_component",
|
|
610
|
+
entitySubtypeCode: "platform_service",
|
|
611
|
+
role: "network_boundary",
|
|
612
|
+
attributes: ["vpc_id"],
|
|
613
|
+
references: { "vpc_id": "aws_vpc" },
|
|
614
|
+
canonicalEntityRef: "global:platform_component:aws_vpc",
|
|
615
|
+
cloudResourceType: "AWS::EC2::RouteTable",
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
resourceType: "aws_security_group",
|
|
619
|
+
entityTypeCode: "platform_component",
|
|
620
|
+
entitySubtypeCode: "platform_service",
|
|
621
|
+
role: "network_boundary",
|
|
622
|
+
attributes: ["name", "description", "vpc_id"],
|
|
623
|
+
references: { "vpc_id": "aws_vpc" },
|
|
624
|
+
canonicalEntityRef: "global:platform_component:aws_security_group",
|
|
625
|
+
cloudResourceType: "AWS::EC2::SecurityGroup",
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
resourceType: "aws_subnet",
|
|
629
|
+
entityTypeCode: "platform_component",
|
|
630
|
+
entitySubtypeCode: "platform_service",
|
|
631
|
+
role: "network_boundary",
|
|
632
|
+
attributes: ["cidr_block", "availability_zone", "vpc_id", "map_public_ip_on_launch"],
|
|
633
|
+
references: { "vpc_id": "aws_vpc" },
|
|
634
|
+
canonicalEntityRef: "global:platform_component:aws_vpc",
|
|
635
|
+
cloudResourceType: "AWS::EC2::Subnet",
|
|
636
|
+
nameAttribute: "id",
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
resourceType: "aws_vpc",
|
|
640
|
+
entityTypeCode: "platform_component",
|
|
641
|
+
entitySubtypeCode: "platform_service",
|
|
642
|
+
role: "network_boundary",
|
|
643
|
+
attributes: ["cidr_block", "enable_dns_support", "enable_dns_hostnames", "instance_tenancy"],
|
|
644
|
+
canonicalEntityRef: "global:platform_component:aws_vpc",
|
|
645
|
+
cloudResourceType: "AWS::EC2::VPC",
|
|
646
|
+
nameAttribute: "id",
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
resourceType: "azurerm_dns_cname_record",
|
|
650
|
+
entityTypeCode: "platform_component",
|
|
651
|
+
entitySubtypeCode: "platform_service",
|
|
652
|
+
role: "network_boundary",
|
|
653
|
+
attributes: ["name", "resource_group_name", "zone_name", "ttl", "record"],
|
|
654
|
+
nameReferences: { "zone_name": "azurerm_dns_zone" },
|
|
655
|
+
canonicalEntityRef: "global:platform_component:azure_dns",
|
|
656
|
+
cloudResourceType: "Microsoft.Network/dnsZones/CNAME",
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
resourceType: "azurerm_dns_txt_record",
|
|
660
|
+
entityTypeCode: "platform_component",
|
|
661
|
+
entitySubtypeCode: "platform_service",
|
|
662
|
+
role: "network_boundary",
|
|
663
|
+
attributes: ["name", "resource_group_name", "zone_name", "ttl"],
|
|
664
|
+
nameReferences: { "zone_name": "azurerm_dns_zone" },
|
|
665
|
+
canonicalEntityRef: "global:platform_component:azure_dns",
|
|
666
|
+
cloudResourceType: "Microsoft.Network/dnsZones/TXT",
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
resourceType: "azurerm_dns_zone",
|
|
670
|
+
entityTypeCode: "platform_component",
|
|
671
|
+
entitySubtypeCode: "platform_service",
|
|
672
|
+
role: "network_boundary",
|
|
673
|
+
attributes: ["name", "resource_group_name", "number_of_record_sets", "name_servers"],
|
|
674
|
+
canonicalEntityRef: "global:platform_component:azure_dns",
|
|
675
|
+
cloudResourceType: "Microsoft.Network/dnsZones",
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
resourceType: "azurerm_network_security_group",
|
|
679
|
+
entityTypeCode: "platform_component",
|
|
680
|
+
entitySubtypeCode: "platform_service",
|
|
681
|
+
role: "network_boundary",
|
|
682
|
+
attributes: ["name", "location", "resource_group_name"],
|
|
683
|
+
canonicalEntityRef: "global:platform_component:azure_network_security_group",
|
|
684
|
+
cloudResourceType: "Microsoft.Network/networkSecurityGroups",
|
|
685
|
+
},
|
|
226
686
|
{
|
|
227
687
|
resourceType: "azurerm_private_dns_zone",
|
|
228
688
|
entityTypeCode: "platform_component",
|
|
@@ -232,6 +692,17 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
232
692
|
canonicalEntityRef: "global:platform_component:azure_private_dns",
|
|
233
693
|
cloudResourceType: "Microsoft.Network/privateDnsZones",
|
|
234
694
|
},
|
|
695
|
+
{
|
|
696
|
+
resourceType: "azurerm_private_dns_zone_virtual_network_link",
|
|
697
|
+
entityTypeCode: "platform_component",
|
|
698
|
+
entitySubtypeCode: "platform_service",
|
|
699
|
+
role: "network_boundary",
|
|
700
|
+
attributes: ["name", "resource_group_name", "private_dns_zone_name", "virtual_network_id", "registration_enabled"],
|
|
701
|
+
references: { "virtual_network_id": "azurerm_virtual_network" },
|
|
702
|
+
nameReferences: { "private_dns_zone_name": "azurerm_private_dns_zone" },
|
|
703
|
+
canonicalEntityRef: "global:platform_component:azure_private_dns",
|
|
704
|
+
cloudResourceType: "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
|
|
705
|
+
},
|
|
235
706
|
{
|
|
236
707
|
resourceType: "azurerm_private_endpoint",
|
|
237
708
|
entityTypeCode: "platform_component",
|
|
@@ -252,6 +723,16 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
252
723
|
canonicalEntityRef: "global:platform_component:azure_virtual_network",
|
|
253
724
|
cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
|
|
254
725
|
},
|
|
726
|
+
{
|
|
727
|
+
resourceType: "azurerm_subnet_network_security_group_association",
|
|
728
|
+
entityTypeCode: "platform_component",
|
|
729
|
+
entitySubtypeCode: "platform_service",
|
|
730
|
+
role: "network_boundary",
|
|
731
|
+
attributes: ["subnet_id", "network_security_group_id"],
|
|
732
|
+
references: { "subnet_id": "azurerm_subnet", "network_security_group_id": "azurerm_network_security_group" },
|
|
733
|
+
canonicalEntityRef: "global:platform_component:azure_network_security_group",
|
|
734
|
+
cloudResourceType: "Microsoft.Network/virtualNetworks/subnets/networkSecurityGroupAssociations",
|
|
735
|
+
},
|
|
255
736
|
{
|
|
256
737
|
resourceType: "azurerm_virtual_network",
|
|
257
738
|
entityTypeCode: "platform_component",
|
|
@@ -261,6 +742,25 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
261
742
|
canonicalEntityRef: "global:platform_component:azure_virtual_network",
|
|
262
743
|
cloudResourceType: "Microsoft.Network/virtualNetworks",
|
|
263
744
|
},
|
|
745
|
+
{
|
|
746
|
+
resourceType: "aws_cloudwatch_log_group",
|
|
747
|
+
entityTypeCode: "platform_component",
|
|
748
|
+
entitySubtypeCode: "platform_service",
|
|
749
|
+
role: "observability",
|
|
750
|
+
attributes: ["name", "retention_in_days"],
|
|
751
|
+
canonicalEntityRef: "global:platform_component:aws_cloudwatch",
|
|
752
|
+
cloudResourceType: "AWS::Logs::LogGroup",
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
resourceType: "aws_cloudwatch_metric_alarm",
|
|
756
|
+
entityTypeCode: "platform_component",
|
|
757
|
+
entitySubtypeCode: "platform_service",
|
|
758
|
+
role: "observability",
|
|
759
|
+
attributes: ["alarm_name", "comparison_operator", "evaluation_periods", "metric_name", "namespace", "period", "statistic", "threshold"],
|
|
760
|
+
canonicalEntityRef: "global:platform_component:aws_cloudwatch",
|
|
761
|
+
cloudResourceType: "AWS::CloudWatch::Alarm",
|
|
762
|
+
nameAttribute: "alarm_name",
|
|
763
|
+
},
|
|
264
764
|
{
|
|
265
765
|
resourceType: "azurerm_log_analytics_workspace",
|
|
266
766
|
entityTypeCode: "platform_component",
|
|
@@ -270,6 +770,25 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
270
770
|
canonicalEntityRef: "global:platform_component:azure_monitor",
|
|
271
771
|
cloudResourceType: "Microsoft.OperationalInsights/workspaces",
|
|
272
772
|
},
|
|
773
|
+
{
|
|
774
|
+
resourceType: "azurerm_monitor_diagnostic_setting",
|
|
775
|
+
entityTypeCode: "platform_component",
|
|
776
|
+
entitySubtypeCode: "platform_service",
|
|
777
|
+
role: "observability",
|
|
778
|
+
attributes: ["name", "target_resource_id", "log_analytics_workspace_id", "storage_account_id"],
|
|
779
|
+
references: { "storage_account_id": "azurerm_storage_account", "log_analytics_workspace_id": "azurerm_log_analytics_workspace" },
|
|
780
|
+
canonicalEntityRef: "global:platform_component:azure_monitor",
|
|
781
|
+
cloudResourceType: "Microsoft.Insights/diagnosticSettings",
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
resourceType: "aws_ecr_repository",
|
|
785
|
+
entityTypeCode: "platform_component",
|
|
786
|
+
entitySubtypeCode: "platform_service",
|
|
787
|
+
role: "registry",
|
|
788
|
+
attributes: ["name", "image_tag_mutability", "force_delete"],
|
|
789
|
+
canonicalEntityRef: "global:platform_component:aws_ecr",
|
|
790
|
+
cloudResourceType: "AWS::ECR::Repository",
|
|
791
|
+
},
|
|
273
792
|
{
|
|
274
793
|
resourceType: "azurerm_container_registry",
|
|
275
794
|
entityTypeCode: "platform_component",
|
|
@@ -288,6 +807,26 @@ export const IAC_CATALOGUE_SEED = [
|
|
|
288
807
|
canonicalEntityRef: "global:platform_component:azure_resource_group",
|
|
289
808
|
cloudResourceType: "Microsoft.Resources/resourceGroups",
|
|
290
809
|
},
|
|
810
|
+
{
|
|
811
|
+
resourceType: "aws_kms_key",
|
|
812
|
+
entityTypeCode: "platform_component",
|
|
813
|
+
entitySubtypeCode: "platform_service",
|
|
814
|
+
role: "secret_store",
|
|
815
|
+
attributes: ["description", "key_usage", "deletion_window_in_days", "enable_key_rotation"],
|
|
816
|
+
canonicalEntityRef: "global:platform_component:aws_kms",
|
|
817
|
+
cloudResourceType: "AWS::KMS::Key",
|
|
818
|
+
nameAttribute: "id",
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
resourceType: "aws_secretsmanager_secret",
|
|
822
|
+
entityTypeCode: "platform_component",
|
|
823
|
+
entitySubtypeCode: "platform_service",
|
|
824
|
+
role: "secret_store",
|
|
825
|
+
attributes: ["name", "description", "kms_key_id"],
|
|
826
|
+
references: { "kms_key_id": "aws_kms_key" },
|
|
827
|
+
canonicalEntityRef: "global:platform_component:aws_secrets_manager",
|
|
828
|
+
cloudResourceType: "AWS::SecretsManager::Secret",
|
|
829
|
+
},
|
|
291
830
|
{
|
|
292
831
|
resourceType: "azurerm_key_vault",
|
|
293
832
|
entityTypeCode: "platform_component",
|
package/dist/lib/mcp.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import https from "https";
|
|
2
|
-
import { readFileSync } from "fs";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
2
|
import { requireCredentials } from "./credentials.js";
|
|
3
|
+
import { cliVersion } from "./version.js";
|
|
6
4
|
const MCP_GATEWAY_URL = "https://mcp.nexarch.ai";
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const pkgPath = join(here, "..", "..", "package.json");
|
|
11
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
12
|
-
return pkg.version ?? "0.0.0";
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return "0.0.0";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const CLI_VERSION = readCliVersion();
|
|
5
|
+
const CLI_VERSION = cliVersion();
|
|
19
6
|
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.NEXARCH_MCP_TIMEOUT_MS ?? "90000", 10) || 90_000;
|
|
20
7
|
const REQUEST_RETRIES = Math.max(0, Number.parseInt(process.env.NEXARCH_MCP_RETRIES ?? "2", 10) || 2);
|
|
21
8
|
function sleep(ms) {
|
|
@@ -2,6 +2,17 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
|
2
2
|
import { join, sep } from "path";
|
|
3
3
|
const SCAN_DEPTH = 8;
|
|
4
4
|
const IGNORED_DIRS = new Set(["node_modules", ".git", ".terraform", "dist", "build", ".next"]);
|
|
5
|
+
/**
|
|
6
|
+
* Path segments whose contents demonstrate Terraform rather than run it.
|
|
7
|
+
*
|
|
8
|
+
* An `examples/complete` directory configures a provider and looks exactly like
|
|
9
|
+
* an estate root by every local signal — which is why terraform-aws-vpc
|
|
10
|
+
* reported thirteen of them as environments while missing the modules that are
|
|
11
|
+
* the point of the repository. Worse than missing the real architecture: the
|
|
12
|
+
* examples are mutually exclusive demonstrations, so graphing them together
|
|
13
|
+
* describes an estate that has never existed anywhere.
|
|
14
|
+
*/
|
|
15
|
+
const NON_ESTATE_SEGMENT = /(^|[\\/])(examples?|docs?|test|tests|fixtures?|testdata|_example|sample|samples)([\\/]|$)/i;
|
|
5
16
|
function collectTerraformFiles(dir, depth = 0, out = []) {
|
|
6
17
|
if (depth > SCAN_DEPTH)
|
|
7
18
|
return out;
|
|
@@ -115,6 +126,43 @@ function collectModuleDirectories(dir, depth = 0, out = []) {
|
|
|
115
126
|
}
|
|
116
127
|
return out;
|
|
117
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Recognises a repository that publishes reusable modules rather than running an estate.
|
|
131
|
+
*
|
|
132
|
+
* The Terraform registry convention is a module at the repository root —
|
|
133
|
+
* `main.tf` with `variables.tf` and `outputs.tf` beside it — deliberately
|
|
134
|
+
* configuring no provider and no backend, because both are supplied by whoever
|
|
135
|
+
* calls it. Every signal that identifies an estate is therefore absent by
|
|
136
|
+
* design, and the signals that *are* present belong to the examples directory.
|
|
137
|
+
*
|
|
138
|
+
* This matters more than tidiness. terraform-aws-vpc has no estate to ingest:
|
|
139
|
+
* asking it for one and getting thirteen mutually exclusive demonstrations is
|
|
140
|
+
* worse than getting nothing, because nothing is honest.
|
|
141
|
+
*/
|
|
142
|
+
function detectPublishedModules(dir) {
|
|
143
|
+
const rootFiles = (() => {
|
|
144
|
+
try {
|
|
145
|
+
return readdirSync(dir);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
const hasRootModule = rootFiles.includes("variables.tf") && rootFiles.includes("outputs.tf") && rootFiles.some((f) => f.endsWith(".tf") && f !== "variables.tf" && f !== "outputs.tf");
|
|
152
|
+
const modules = [];
|
|
153
|
+
const modulesDir = join(dir, "modules");
|
|
154
|
+
try {
|
|
155
|
+
for (const child of readdirSync(modulesDir)) {
|
|
156
|
+
const full = join(modulesDir, child);
|
|
157
|
+
if (statSync(full).isDirectory() && readdirSync(full).some((f) => f.endsWith(".tf")))
|
|
158
|
+
modules.push(child);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// No modules directory; a single-module repository is still a library.
|
|
163
|
+
}
|
|
164
|
+
return { isLibrary: hasRootModule, modules: modules.sort() };
|
|
165
|
+
}
|
|
118
166
|
const ROOT_SCAN_DEPTH = 4;
|
|
119
167
|
const ENVIRONMENT_DIR = /^(dev|development|test|qa|uat|sit|stag|staging|preprod|pre-prod|prod|production|live|dr)$/i;
|
|
120
168
|
/**
|
|
@@ -159,7 +207,8 @@ export function discoverRootModules(repoDir) {
|
|
|
159
207
|
}
|
|
160
208
|
const tfFiles = entries.filter((entry) => entry.endsWith(".tf"));
|
|
161
209
|
const isUnderModules = /(^|[\\/])modules([\\/]|$)/i.test(relative);
|
|
162
|
-
|
|
210
|
+
const isDemonstration = NON_ESTATE_SEGMENT.test(relative);
|
|
211
|
+
if (tfFiles.length > 0 && !isUnderModules && !isDemonstration) {
|
|
163
212
|
const signals = [];
|
|
164
213
|
let hasBackend = false;
|
|
165
214
|
let hasLocalBackend = false;
|
|
@@ -206,7 +255,13 @@ export function discoverRootModules(repoDir) {
|
|
|
206
255
|
signals.push("tfvars");
|
|
207
256
|
if (underStacksDir)
|
|
208
257
|
signals.push("stacks directory");
|
|
209
|
-
|
|
258
|
+
// A provider block alone no longer qualifies. It is the one signal an
|
|
259
|
+
// example shares with a real estate, and treating it as sufficient is
|
|
260
|
+
// what let demonstration fixtures in. What distinguishes an estate is
|
|
261
|
+
// evidence that state exists or is configured somewhere: a backend, an
|
|
262
|
+
// initialised working directory, or a conventional stacks layout.
|
|
263
|
+
const isEstateRoot = hasBackend || initialised || hasPluginDir || (underStacksDir && (hasTfvars || Boolean(environmentHint)));
|
|
264
|
+
if (isEstateRoot) {
|
|
210
265
|
found.push({ dir, relative: relative || ".", signals, environmentHint, initialised, usesLocalBackend: hasLocalBackend });
|
|
211
266
|
// A root module's subdirectories are its own modules, not further roots.
|
|
212
267
|
return;
|
|
@@ -278,7 +333,17 @@ export function detectInfrastructureProject(dir) {
|
|
|
278
333
|
if (rootModules.length > 0) {
|
|
279
334
|
signals.push(`${rootModules.length} root module${rootModules.length === 1 ? "" : "s"}`);
|
|
280
335
|
}
|
|
281
|
-
const
|
|
336
|
+
const library = detectPublishedModules(dir);
|
|
337
|
+
// An estate is claimed only by root modules that survived the tightened
|
|
338
|
+
// acceptance above. A repository with a root module and no estate roots is
|
|
339
|
+
// publishing modules, not running anything.
|
|
340
|
+
const repoKind = rootModules.length > 0 ? "estate" : library.isLibrary ? "module_library" : "unknown";
|
|
341
|
+
if (repoKind === "module_library") {
|
|
342
|
+
signals.push(library.modules.length > 0 ? `publishes ${library.modules.length + 1} modules` : "publishes a reusable module");
|
|
343
|
+
}
|
|
344
|
+
const isInfrastructure = rootModules.length > 0 ||
|
|
345
|
+
repoKind === "module_library" ||
|
|
346
|
+
(files.length > 0 && (files.length >= 3 || hasRemoteBackend || hasProviderBlock || moduleDirectories.length > 0));
|
|
282
347
|
return {
|
|
283
348
|
isInfrastructure,
|
|
284
349
|
rootModules,
|
|
@@ -287,6 +352,8 @@ export function detectInfrastructureProject(dir) {
|
|
|
287
352
|
hasRemoteBackend,
|
|
288
353
|
hasProviderBlock,
|
|
289
354
|
moduleDirectories,
|
|
355
|
+
repoKind,
|
|
356
|
+
publishedModules: repoKind === "module_library" ? library.modules : [],
|
|
290
357
|
...detectCiSystem(dir),
|
|
291
358
|
};
|
|
292
359
|
}
|
|
@@ -208,7 +208,7 @@ export function projectTerraformState(params) {
|
|
|
208
208
|
entitySubtypeCode: entry.entitySubtypeCode ?? null,
|
|
209
209
|
role: entry.role,
|
|
210
210
|
mode: typeof resource.mode === "string" ? resource.mode : "managed",
|
|
211
|
-
name: typeof values.name === "string" ? values.name : null,
|
|
211
|
+
name: typeof values[entry.nameAttribute ?? "name"] === "string" ? values[entry.nameAttribute ?? "name"] : null,
|
|
212
212
|
attributes,
|
|
213
213
|
tags: projectTags(values, sensitiveValues),
|
|
214
214
|
identityIds: projectIdentityIds(values),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join, resolve } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
export function cliBuild() {
|
|
5
|
+
let version = "0.0.0";
|
|
6
|
+
let installPath = "unknown";
|
|
7
|
+
try {
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const root = resolve(here, "..", "..");
|
|
10
|
+
installPath = root;
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
12
|
+
version = pkg.version ?? "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// A CLI that cannot read its own manifest should still run; it just cannot
|
|
16
|
+
// say which build it is.
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
version,
|
|
20
|
+
installPath,
|
|
21
|
+
viaNpx: /[\\/]_npx[\\/]/.test(installPath),
|
|
22
|
+
nodeVersion: process.version,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Just the version, for the MCP client's user agent. */
|
|
26
|
+
export function cliVersion() {
|
|
27
|
+
return cliBuild().version;
|
|
28
|
+
}
|
|
29
|
+
/** The block printed by `nexarch --version`. */
|
|
30
|
+
export function formatBuild(build) {
|
|
31
|
+
return [
|
|
32
|
+
`nexarch ${build.version}`,
|
|
33
|
+
` running from ${build.installPath}${build.viaNpx ? " (npx cache)" : ""}`,
|
|
34
|
+
` node ${build.nodeVersion}`,
|
|
35
|
+
build.viaNpx
|
|
36
|
+
? ""
|
|
37
|
+
: `\n This is an installed copy, not a fresh download. If it is older than you\n expect, a global install is shadowing \`npx nexarch@latest\`:\n npm rm -g nexarch`,
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|