nexarch 0.7.1 → 0.8.1

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.
@@ -207,6 +207,63 @@ function parseEnvKeys(content) {
207
207
  .filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key))
208
208
  .filter((key) => !ENV_KEY_NOISE.test(key));
209
209
  }
210
+ function inferEnvServiceNamesFromKey(key) {
211
+ const upper = key.toUpperCase();
212
+ const names = new Set();
213
+ if (upper.includes("ADSENSE")) {
214
+ names.add("google-adsense");
215
+ names.add("adsense");
216
+ }
217
+ if (upper.includes("VERCEL"))
218
+ names.add("vercel");
219
+ if (upper.includes("NEON"))
220
+ names.add("neon");
221
+ if (upper.includes("TURBO")) {
222
+ names.add("turbo");
223
+ names.add("turborepo");
224
+ }
225
+ return Array.from(names);
226
+ }
227
+ function inferEnvServiceNamesFromValue(valueRaw) {
228
+ const value = valueRaw.replace(/^['"]|['"]$/g, "").toLowerCase();
229
+ const names = new Set();
230
+ if (!value)
231
+ return [];
232
+ if (value.includes("neon.tech") || value.includes("@neondatabase/"))
233
+ names.add("neon");
234
+ if (value.includes("vercel.app") || value.includes("vercel.com"))
235
+ names.add("vercel");
236
+ if (value.includes("googleads.g.doubleclick.net") || value.includes("adsense")) {
237
+ names.add("google-adsense");
238
+ names.add("adsense");
239
+ }
240
+ return Array.from(names);
241
+ }
242
+ function parseEnvSignals(content) {
243
+ const names = new Set();
244
+ const keys = parseEnvKeys(content);
245
+ for (const key of keys) {
246
+ names.add(key);
247
+ for (const n of inferEnvServiceNamesFromKey(key))
248
+ names.add(n);
249
+ }
250
+ for (const rawLine of content.split("\n")) {
251
+ const line = rawLine.trim();
252
+ if (!line || line.startsWith("#") || !line.includes("="))
253
+ continue;
254
+ const normalized = line.replace(/^export\s+/, "");
255
+ const eqIdx = normalized.indexOf("=");
256
+ if (eqIdx < 1)
257
+ continue;
258
+ const key = normalized.slice(0, eqIdx).trim();
259
+ const value = normalized.slice(eqIdx + 1).trim();
260
+ if (!/^[A-Z][A-Z0-9_]{2,}$/.test(key))
261
+ continue;
262
+ for (const n of inferEnvServiceNamesFromValue(value))
263
+ names.add(n);
264
+ }
265
+ return Array.from(names);
266
+ }
210
267
  function gitOriginUrl(dir) {
211
268
  const configPath = join(dir, ".git", "config");
212
269
  if (!existsSync(configPath))
@@ -237,38 +294,80 @@ function detectFromGitConfig(dir) {
237
294
  return [];
238
295
  }
239
296
  function detectFromFilesystem(dir) {
240
- const detected = [];
241
- // CI/CD
242
- if (existsSync(join(dir, ".github", "workflows")))
243
- detected.push("github-actions");
244
- if (existsSync(join(dir, ".gitlab-ci.yml")))
245
- detected.push("gitlab");
246
- if (existsSync(join(dir, "Jenkinsfile")))
247
- detected.push("jenkins");
248
- if (existsSync(join(dir, ".circleci", "config.yml")))
249
- detected.push("circleci");
250
- // Deployment / hosting
251
- if (existsSync(join(dir, "vercel.json")) || existsSync(join(dir, ".vercel")))
252
- detected.push("vercel");
253
- if (existsSync(join(dir, "netlify.toml")))
254
- detected.push("netlify");
255
- if (existsSync(join(dir, "fly.toml")))
256
- detected.push("fly");
257
- if (existsSync(join(dir, "railway.json")) || existsSync(join(dir, "railway.toml")))
258
- detected.push("railway");
259
- if (existsSync(join(dir, "render.yaml")))
260
- detected.push("render");
261
- // Containers
262
- if (existsSync(join(dir, "Dockerfile")))
263
- detected.push("docker");
264
- if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "docker-compose.yaml")))
265
- detected.push("docker");
266
- // Infrastructure
267
- if (existsSync(join(dir, "terraform")) || existsSync(join(dir, "main.tf")))
268
- detected.push("terraform");
269
- if (existsSync(join(dir, "pulumi.yaml")))
270
- detected.push("pulumi");
271
- return detected;
297
+ const detected = new Set();
298
+ const markIfExists = (relPath, name) => {
299
+ if (existsSync(join(dir, relPath)))
300
+ detected.add(name);
301
+ };
302
+ // Root-level checks first (fast path)
303
+ markIfExists(join(".github", "workflows"), "github-actions");
304
+ markIfExists(".gitlab-ci.yml", "gitlab");
305
+ markIfExists("Jenkinsfile", "jenkins");
306
+ markIfExists(join(".circleci", "config.yml"), "circleci");
307
+ markIfExists("vercel.json", "vercel");
308
+ markIfExists(".vercel", "vercel");
309
+ markIfExists("netlify.toml", "netlify");
310
+ markIfExists("fly.toml", "fly");
311
+ markIfExists("railway.json", "railway");
312
+ markIfExists("railway.toml", "railway");
313
+ markIfExists("render.yaml", "render");
314
+ markIfExists("Dockerfile", "docker");
315
+ markIfExists("docker-compose.yml", "docker");
316
+ markIfExists("docker-compose.yaml", "docker");
317
+ markIfExists("terraform", "terraform");
318
+ markIfExists("main.tf", "terraform");
319
+ markIfExists("pulumi.yaml", "pulumi");
320
+ // Monorepo-aware bounded walk for deployment files in sub-app folders.
321
+ const MAX_DEPTH = 3;
322
+ const walk = (current, depth) => {
323
+ if (depth > MAX_DEPTH)
324
+ return;
325
+ let entries = [];
326
+ try {
327
+ entries = readdirSync(current);
328
+ }
329
+ catch {
330
+ return;
331
+ }
332
+ for (const entry of entries) {
333
+ if (entry.startsWith("."))
334
+ continue;
335
+ if (SKIP_DIRS.has(entry))
336
+ continue;
337
+ const full = join(current, entry);
338
+ let isDir = false;
339
+ try {
340
+ isDir = statSync(full).isDirectory();
341
+ }
342
+ catch {
343
+ continue;
344
+ }
345
+ if (!isDir)
346
+ continue;
347
+ if (existsSync(join(full, "vercel.json")) ||
348
+ existsSync(join(full, ".vercel")))
349
+ detected.add("vercel");
350
+ if (existsSync(join(full, "netlify.toml")))
351
+ detected.add("netlify");
352
+ if (existsSync(join(full, "fly.toml")))
353
+ detected.add("fly");
354
+ if (existsSync(join(full, "railway.json")) || existsSync(join(full, "railway.toml")))
355
+ detected.add("railway");
356
+ if (existsSync(join(full, "render.yaml")))
357
+ detected.add("render");
358
+ if (existsSync(join(full, "Dockerfile")))
359
+ detected.add("docker");
360
+ if (existsSync(join(full, "docker-compose.yml")) || existsSync(join(full, "docker-compose.yaml")))
361
+ detected.add("docker");
362
+ if (existsSync(join(full, "main.tf")))
363
+ detected.add("terraform");
364
+ if (existsSync(join(full, "pulumi.yaml")))
365
+ detected.add("pulumi");
366
+ walk(full, depth + 1);
367
+ }
368
+ };
369
+ walk(dir, 1);
370
+ return Array.from(detected);
272
371
  }
273
372
  const SKIP_DIRS = new Set([
274
373
  // JavaScript / Node
@@ -286,14 +385,30 @@ const SKIP_DIRS = new Set([
286
385
  // Generic
287
386
  "build", ".git", ".cache", "tmp",
288
387
  ]);
388
+ const ARCHITECTURAL_DEV_DEP_ALLOWLIST = new Set([
389
+ "tailwindcss",
390
+ "turbo",
391
+ "turborepo",
392
+ "@vercel/analytics",
393
+ "@vercel/speed-insights",
394
+ "@vercel/postgres",
395
+ "@vercel/blob",
396
+ "@neondatabase/serverless",
397
+ ]);
289
398
  function collectPackageDeps(pkgPath) {
290
399
  try {
291
400
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
292
- // devDependencies are build/test/type tooling — no architectural value
293
401
  const merged = {
294
- ...pkg.dependencies,
295
- ...pkg.peerDependencies,
402
+ ...(pkg.dependencies ?? {}),
403
+ ...(pkg.peerDependencies ?? {}),
296
404
  };
405
+ // Include a small set of architecturally significant dev dependencies
406
+ // (hosting/build/runtime-defining tools) while still filtering test-only noise.
407
+ for (const [name, versionRaw] of Object.entries(pkg.devDependencies ?? {})) {
408
+ if (ARCHITECTURAL_DEV_DEP_ALLOWLIST.has(name)) {
409
+ merged[name] = versionRaw;
410
+ }
411
+ }
297
412
  return Object.entries(merged).map(([name, versionRaw]) => ({
298
413
  name,
299
414
  versionRaw: typeof versionRaw === "string" ? versionRaw : null,
@@ -696,12 +811,12 @@ function scanProject(dir) {
696
811
  catch { }
697
812
  }
698
813
  // ── .env files ─────────────────────────────────────────────────────────────
699
- for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
814
+ for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime", ".env.production", ".env.development"]) {
700
815
  const envPath = join(dir, envFile);
701
816
  if (existsSync(envPath)) {
702
817
  try {
703
- for (const key of parseEnvKeys(readFileSync(envPath, "utf8")))
704
- names.add(key);
818
+ for (const signal of parseEnvSignals(readFileSync(envPath, "utf8")))
819
+ names.add(signal);
705
820
  }
706
821
  catch { }
707
822
  }
@@ -0,0 +1,46 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function hasFlag(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+ function option(args, key) {
8
+ const i = args.indexOf(key);
9
+ if (i === -1)
10
+ return null;
11
+ const v = args[i + 1];
12
+ if (!v || v.startsWith("--"))
13
+ return null;
14
+ return v;
15
+ }
16
+ export async function listEntities(args) {
17
+ const asJson = hasFlag(args, "--json");
18
+ const entityTypeCode = option(args, "--type") ?? undefined;
19
+ const status = option(args, "--status") ?? undefined;
20
+ const query = option(args, "--query") ?? undefined;
21
+ const limitRaw = option(args, "--limit");
22
+ const limit = limitRaw ? Number(limitRaw) : undefined;
23
+ if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
24
+ console.error("error: --limit must be an integer between 1 and 500");
25
+ process.exit(1);
26
+ }
27
+ const creds = requireCredentials();
28
+ const raw = await callMcpTool("nexarch_list_entities", {
29
+ companyId: creds.companyId,
30
+ entityTypeCode,
31
+ status,
32
+ query,
33
+ limit,
34
+ }, { companyId: creds.companyId });
35
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
36
+ if (asJson) {
37
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
38
+ return;
39
+ }
40
+ console.log(`Found ${result.count ?? 0} entities`);
41
+ for (const e of result.entities ?? []) {
42
+ const subtype = e.entitySubtypeCode ? `/${e.entitySubtypeCode}` : "";
43
+ const key = e.externalKey ? ` (${e.externalKey})` : "";
44
+ console.log(`- ${e.name}${key} [${e.entityTypeCode}${subtype}] status=${e.status}`);
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function hasFlag(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+ function option(args, key) {
8
+ const i = args.indexOf(key);
9
+ if (i === -1)
10
+ return null;
11
+ const v = args[i + 1];
12
+ if (!v || v.startsWith("--"))
13
+ return null;
14
+ return v;
15
+ }
16
+ export async function listRelationships(args) {
17
+ const asJson = hasFlag(args, "--json");
18
+ const relationshipTypeCode = option(args, "--type") ?? undefined;
19
+ const status = option(args, "--status") ?? undefined;
20
+ const fromEntityExternalKey = option(args, "--from") ?? undefined;
21
+ const toEntityExternalKey = option(args, "--to") ?? undefined;
22
+ const limitRaw = option(args, "--limit");
23
+ const limit = limitRaw ? Number(limitRaw) : undefined;
24
+ if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
25
+ console.error("error: --limit must be an integer between 1 and 500");
26
+ process.exit(1);
27
+ }
28
+ const creds = requireCredentials();
29
+ const raw = await callMcpTool("nexarch_list_relationships", {
30
+ companyId: creds.companyId,
31
+ relationshipTypeCode,
32
+ status,
33
+ fromEntityExternalKey,
34
+ toEntityExternalKey,
35
+ limit,
36
+ }, { companyId: creds.companyId });
37
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
38
+ if (asJson) {
39
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
40
+ return;
41
+ }
42
+ console.log(`Found ${result.count ?? 0} relationships`);
43
+ for (const r of result.relationships ?? []) {
44
+ const subtype = r.relationshipSubtypeCode ? `/${r.relationshipSubtypeCode}` : "";
45
+ const from = r.fromEntityExternalKey ?? r.fromEntityName ?? "?";
46
+ const to = r.toEntityExternalKey ?? r.toEntityName ?? "?";
47
+ console.log(`- ${from} -[${r.relationshipTypeCode}${subtype}|${r.status}]-> ${to}`);
48
+ }
49
+ }
@@ -31,7 +31,17 @@ export async function status(_args) {
31
31
  if (registryInfo) {
32
32
  console.log(`\n Integration registry: v${registryInfo.version} (published ${new Date(registryInfo.publishedAt).toLocaleString()})`);
33
33
  }
34
- console.log(`\n Architecture facts: ${governance.canonicalFactCount}`);
34
+ const architectureFacts = governance.architectureFactCount ?? governance.canonicalFactCount;
35
+ console.log(`\n Architecture facts: ${architectureFacts}`);
36
+ if (typeof governance.graphEntityCount === "number" && typeof governance.graphRelationshipCount === "number") {
37
+ console.log(` Graph breakdown: ${governance.graphEntityCount} entities, ${governance.graphRelationshipCount} relationships`);
38
+ }
39
+ if (typeof governance.canonicalCoverageOverallPct === "number") {
40
+ console.log(` Canonical coverage: ${governance.canonicalCoverageOverallPct}%`);
41
+ }
42
+ if (typeof governance.canonicalRegistryCount === "number") {
43
+ console.log(` Canonical registry: ${governance.canonicalRegistryCount}`);
44
+ }
35
45
  console.log(` Pending review: ${governance.reviewQueue.pending_entities} entities, ${governance.reviewQueue.pending_relationships} relationships`);
36
46
  if (governance.latestSnapshot) {
37
47
  const snapshotDate = new Date(governance.latestSnapshot.created_at).toLocaleDateString();
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import { updateEntity } from "./commands/update-entity.js";
12
12
  import { addRelationship } from "./commands/add-relationship.js";
13
13
  import { registerAlias } from "./commands/register-alias.js";
14
14
  import { resolveNames } from "./commands/resolve-names.js";
15
+ import { listEntities } from "./commands/list-entities.js";
16
+ import { listRelationships } from "./commands/list-relationships.js";
15
17
  import { checkIn } from "./commands/check-in.js";
16
18
  import { commandDone } from "./commands/command-done.js";
17
19
  import { commandFail } from "./commands/command-fail.js";
@@ -34,6 +36,8 @@ const commands = {
34
36
  "add-relationship": addRelationship,
35
37
  "register-alias": registerAlias,
36
38
  "resolve-names": resolveNames,
39
+ "list-entities": listEntities,
40
+ "list-relationships": listRelationships,
37
41
  "check-in": checkIn,
38
42
  "command-done": commandDone,
39
43
  "command-fail": commandFail,
@@ -121,6 +125,21 @@ Usage:
121
125
  results before calling add-relationship.
122
126
  Options: --names <csv> (required, e.g. "vercel,neon")
123
127
  --json
128
+ nexarch list-entities
129
+ List entities from the workspace graph.
130
+ Options: --type <entityTypeCode>
131
+ --status <status>
132
+ --query <text>
133
+ --limit <1-500>
134
+ --json
135
+ nexarch list-relationships
136
+ List relationships from the workspace graph.
137
+ Options: --type <relationshipTypeCode>
138
+ --status <status>
139
+ --from <fromExternalKey>
140
+ --to <toExternalKey>
141
+ --limit <1-500>
142
+ --json
124
143
  nexarch check-in Preview pending application-target commands (no auto-claim).
125
144
  Scope is resolved server-side from active company context.
126
145
  Options: --agent-key <key> override stored agent key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",