nexarch 0.12.19 → 0.12.23
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 +53 -7
- package/dist/commands/init-agent.js +93 -46
- package/dist/commands/init-project-infra.js +46 -12
- package/dist/commands/init-project.js +65 -4
- package/dist/commands/verify-trust.js +32 -1
- 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/trust.js +9 -2
- package/dist/lib/version.js +41 -0
- package/package.json +35 -35
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),
|
package/dist/lib/trust.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import https from "https";
|
|
2
2
|
import { requireCredentials } from "./credentials.js";
|
|
3
3
|
const MCP_GATEWAY_URL = "https://mcp.nexarch.ai";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `contentHash` (sha256 hex of the exact managed-section text being written)
|
|
6
|
+
* binds the signature to the instructions themselves, not just to the claim
|
|
7
|
+
* that Nexarch minted *a* token for this agent — see ADR-0112. Omit it only
|
|
8
|
+
* when there is no section body yet to hash.
|
|
9
|
+
*/
|
|
10
|
+
export async function requestTrustAttestation(agentId, contentHash) {
|
|
5
11
|
const creds = requireCredentials();
|
|
6
12
|
const body = JSON.stringify({
|
|
7
13
|
agentId,
|
|
8
|
-
scope: "
|
|
14
|
+
scope: "agent_config_write",
|
|
15
|
+
...(contentHash ? { contentHash } : {}),
|
|
9
16
|
});
|
|
10
17
|
return new Promise((resolve) => {
|
|
11
18
|
const url = new URL("/trust/attest", MCP_GATEWAY_URL);
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nexarch",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"nexarch",
|
|
7
|
-
"mcp",
|
|
8
|
-
"architecture",
|
|
9
|
-
"ai"
|
|
10
|
-
],
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
-
"homepage": "https://nexarch.ai",
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18"
|
|
16
|
-
},
|
|
17
|
-
"type": "module",
|
|
18
|
-
"bin": {
|
|
19
|
-
"nexarch": "dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"prepublishOnly": "tsc",
|
|
27
|
-
"dev": "tsx src/index.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^22",
|
|
32
|
-
"tsx": "^4",
|
|
33
|
-
"typescript": "^5"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nexarch",
|
|
3
|
+
"version": "0.12.23",
|
|
4
|
+
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nexarch",
|
|
7
|
+
"mcp",
|
|
8
|
+
"architecture",
|
|
9
|
+
"ai"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
+
"homepage": "https://nexarch.ai",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"nexarch": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepublishOnly": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22",
|
|
32
|
+
"tsx": "^4",
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
}
|
|
35
|
+
}
|