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/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
- function readCliVersion() {
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
- if (tfFiles.length > 0 && !isUnderModules) {
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
- if (hasBackend || hasProvider || initialised || hasPluginDir || (underStacksDir && hasTfvars) || (underStacksDir && environmentHint)) {
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 isInfrastructure = rootModules.length > 0 || (files.length > 0 && (files.length >= 3 || hasRemoteBackend || hasProviderBlock || moduleDirectories.length > 0));
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
- export async function requestTrustAttestation(agentId) {
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: "instruction_injection",
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.19",
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
+ }