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.
- package/dist/commands/init-project.js +153 -38
- package/dist/commands/list-entities.js +46 -0
- package/dist/commands/list-relationships.js +49 -0
- package/dist/commands/status.js +11 -1
- package/dist/index.js +19 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
704
|
-
names.add(
|
|
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
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
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
|