nexarch 0.1.17 → 0.1.19

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.
@@ -4,7 +4,7 @@ import { join } from "path";
4
4
  import process from "process";
5
5
  import { requireCredentials } from "../lib/credentials.js";
6
6
  import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
7
- const CLI_VERSION = "0.1.17";
7
+ const CLI_VERSION = "0.1.19";
8
8
  const AGENT_ENTITY_TYPE = "agent";
9
9
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
10
10
  function parseFlag(args, flag) {
@@ -24,6 +24,8 @@ function slugify(name) {
24
24
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
25
25
  }
26
26
  // ─── Project scanning ─────────────────────────────────────────────────────────
27
+ // Noise patterns for env var keys that are internal config, not external service references
28
+ const ENV_KEY_NOISE = /^(NODE_ENV|PORT|HOST|DEBUG|LOG_LEVEL|TZ|LANG|PATH|HOME|USER|SHELL|TERM)$|(_LOG_LEVEL|_MAX_|_MIN_|_DEFAULT_|_TIMEOUT|_DELAY|_JOBS|_INTERVAL|_LIMIT|_RETRIES|_CONCURREN|_WORKERS)$|^NEXT_PUBLIC_(APP|ADMIN|API|SITE|BASE|WEB)_URL$/;
27
29
  function parseEnvKeys(content) {
28
30
  return content
29
31
  .split("\n")
@@ -31,7 +33,8 @@ function parseEnvKeys(content) {
31
33
  .filter((line) => line && !line.startsWith("#"))
32
34
  .map((line) => line.replace(/^export\s+/, ""))
33
35
  .map((line) => line.split("=")[0].trim())
34
- .filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key)); // min 3 chars, all caps
36
+ .filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key))
37
+ .filter((key) => !ENV_KEY_NOISE.test(key));
35
38
  }
36
39
  function detectFromGitConfig(dir) {
37
40
  const configPath = join(dir, ".git", "config");
@@ -90,6 +93,7 @@ function detectFromFilesystem(dir) {
90
93
  detected.push("pulumi");
91
94
  return detected;
92
95
  }
96
+ const SKIP_DIRS = new Set(["node_modules", "dist", ".next", ".turbo", "build", "coverage", ".git"]);
93
97
  function collectPackageDeps(pkgPath) {
94
98
  try {
95
99
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
@@ -103,50 +107,125 @@ function collectPackageDeps(pkgPath) {
103
107
  return [];
104
108
  }
105
109
  }
106
- function readProjectName(pkgPath) {
110
+ function readRootPackage(pkgPath) {
107
111
  try {
108
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
109
- return typeof pkg.name === "string" ? pkg.name : null;
112
+ return JSON.parse(readFileSync(pkgPath, "utf8"));
110
113
  }
111
114
  catch {
112
- return null;
115
+ return {};
113
116
  }
114
117
  }
115
- function scanProject(dir) {
116
- const names = new Set();
117
- let projectName = basename(dir);
118
- let packageJsonCount = 0;
119
- // Root package.json
120
- const rootPkgPath = join(dir, "package.json");
121
- if (existsSync(rootPkgPath)) {
122
- packageJsonCount++;
123
- const rootName = readProjectName(rootPkgPath);
124
- if (rootName)
125
- projectName = rootName;
126
- for (const dep of collectPackageDeps(rootPkgPath))
127
- names.add(dep);
128
- }
129
- // One level of subdirectories (monorepo packages)
130
- try {
131
- for (const entry of readdirSync(dir)) {
132
- if (entry.startsWith(".") || entry === "node_modules" || entry === "dist")
133
- continue;
134
- const entryPath = join(dir, entry);
118
+ // Resolve workspace glob patterns (e.g. "apps/*", "packages/*") to actual directories.
119
+ // Handles the common case of a single wildcard — no full glob library needed.
120
+ function resolveWorkspaceDirs(rootDir, workspaces) {
121
+ const patterns = Array.isArray(workspaces) ? workspaces : (workspaces.packages ?? []);
122
+ const dirs = [];
123
+ for (const pattern of patterns) {
124
+ if (pattern.endsWith("/*")) {
125
+ const parent = join(rootDir, pattern.slice(0, -2));
135
126
  try {
136
- if (!statSync(entryPath).isDirectory())
137
- continue;
138
- const subPkgPath = join(entryPath, "package.json");
139
- if (existsSync(subPkgPath)) {
140
- packageJsonCount++;
141
- for (const dep of collectPackageDeps(subPkgPath))
142
- names.add(dep);
127
+ for (const entry of readdirSync(parent)) {
128
+ if (SKIP_DIRS.has(entry))
129
+ continue;
130
+ const full = join(parent, entry);
131
+ try {
132
+ if (statSync(full).isDirectory())
133
+ dirs.push(full);
134
+ }
135
+ catch { }
143
136
  }
144
137
  }
145
138
  catch { }
146
139
  }
140
+ else {
141
+ // Exact path
142
+ const full = join(rootDir, pattern);
143
+ try {
144
+ if (statSync(full).isDirectory())
145
+ dirs.push(full);
146
+ }
147
+ catch { }
148
+ }
147
149
  }
148
- catch { }
149
- // .env files
150
+ return dirs;
151
+ }
152
+ // Collect all package.json paths: root + workspace packages + 2-level fallback
153
+ function findPackageJsonPaths(rootDir) {
154
+ const paths = [];
155
+ const rootPkgPath = join(rootDir, "package.json");
156
+ if (!existsSync(rootPkgPath))
157
+ return paths;
158
+ paths.push(rootPkgPath);
159
+ const rootPkg = readRootPackage(rootPkgPath);
160
+ if (rootPkg.workspaces) {
161
+ // Workspace monorepo: resolve workspace dirs and find their package.json files
162
+ for (const wsDir of resolveWorkspaceDirs(rootDir, rootPkg.workspaces)) {
163
+ const pkgPath = join(wsDir, "package.json");
164
+ if (existsSync(pkgPath))
165
+ paths.push(pkgPath);
166
+ }
167
+ }
168
+ else {
169
+ // Non-workspace: scan up to 2 levels deep
170
+ try {
171
+ for (const l1 of readdirSync(rootDir)) {
172
+ if (SKIP_DIRS.has(l1) || l1.startsWith("."))
173
+ continue;
174
+ const l1Path = join(rootDir, l1);
175
+ try {
176
+ if (!statSync(l1Path).isDirectory())
177
+ continue;
178
+ const l1Pkg = join(l1Path, "package.json");
179
+ if (existsSync(l1Pkg)) {
180
+ paths.push(l1Pkg);
181
+ continue;
182
+ }
183
+ for (const l2 of readdirSync(l1Path)) {
184
+ if (SKIP_DIRS.has(l2) || l2.startsWith("."))
185
+ continue;
186
+ const l2Path = join(l1Path, l2);
187
+ try {
188
+ if (!statSync(l2Path).isDirectory())
189
+ continue;
190
+ const l2Pkg = join(l2Path, "package.json");
191
+ if (existsSync(l2Pkg))
192
+ paths.push(l2Pkg);
193
+ }
194
+ catch { }
195
+ }
196
+ }
197
+ catch { }
198
+ }
199
+ }
200
+ catch { }
201
+ }
202
+ return paths;
203
+ }
204
+ function scanProject(dir) {
205
+ const names = new Set();
206
+ let projectName = basename(dir);
207
+ const subPackages = [];
208
+ const pkgPaths = findPackageJsonPaths(dir);
209
+ const rootPkgPath = join(dir, "package.json");
210
+ for (const pkgPath of pkgPaths) {
211
+ const pkg = readRootPackage(pkgPath);
212
+ if (pkgPath === rootPkgPath) {
213
+ if (pkg.name)
214
+ projectName = pkg.name;
215
+ }
216
+ else {
217
+ // Record as a sub-package for enrichment task
218
+ const relativePath = pkgPath.replace(dir + "/", "").replace("/package.json", "");
219
+ subPackages.push({
220
+ name: pkg.name ?? relativePath,
221
+ relativePath,
222
+ packageJsonPath: pkgPath,
223
+ });
224
+ }
225
+ for (const dep of collectPackageDeps(pkgPath))
226
+ names.add(dep);
227
+ }
228
+ // .env files at root
150
229
  for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
151
230
  const envPath = join(dir, envFile);
152
231
  if (existsSync(envPath)) {
@@ -162,7 +241,7 @@ function scanProject(dir) {
162
241
  names.add(name);
163
242
  for (const name of detectFromFilesystem(dir))
164
243
  names.add(name);
165
- return { projectName, packageJsonCount, detectedNames: Array.from(names) };
244
+ return { projectName, packageJsonCount: pkgPaths.length, detectedNames: Array.from(names), subPackages };
166
245
  }
167
246
  // ─── Relationship type selection ──────────────────────────────────────────────
168
247
  function pickRelationshipType(entityTypeCode) {
@@ -189,7 +268,7 @@ export async function initProject(args) {
189
268
  const mcpOpts = { companyId: creds.companyId };
190
269
  if (!asJson)
191
270
  console.log(`Scanning ${dir}…`);
192
- const { projectName, packageJsonCount, detectedNames } = scanProject(dir);
271
+ const { projectName, packageJsonCount, detectedNames, subPackages } = scanProject(dir);
193
272
  const displayName = nameOverride ?? projectName;
194
273
  const projectSlug = slugify(displayName);
195
274
  const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
@@ -261,7 +340,6 @@ export async function initProject(args) {
261
340
  entityTypeCode: entityTypeOverride,
262
341
  ...(projectSubtype ? { entitySubtypeCode: projectSubtype } : {}),
263
342
  name: displayName,
264
- description: `Project initialised from ${dir}`,
265
343
  confidence: 1,
266
344
  attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
267
345
  });
@@ -338,4 +416,58 @@ export async function initProject(args) {
338
416
  console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
339
417
  }
340
418
  }
419
+ // ─── Enrichment task ────────────────────────────────────────────────────────
420
+ const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
421
+ .filter((f) => existsSync(join(dir, f)));
422
+ console.log(`
423
+ ╔══════════════════════════════════════════════════════════════════╗
424
+ ║ ENRICHMENT TASK — action required ║
425
+ ╚══════════════════════════════════════════════════════════════════╝
426
+
427
+ The mechanical scan has written a skeleton graph entry for this project.
428
+ You now need to enrich it with meaningful content.
429
+
430
+ PROJECT ENTITY : ${projectExternalKey}
431
+ PROJECT DIR : ${dir}
432
+ ${readmeHints.length > 0 ? `README FILES : ${readmeHints.join(", ")}` : "README FILES : (none found — check docs/)"}
433
+
434
+ STEP 1 — Read the project README / docs and build your understanding of:
435
+ • What does this project do? (business purpose, key features)
436
+ • Who are the users?
437
+ • What is the high-level architecture?
438
+ • Any notable technical decisions or constraints?
439
+
440
+ STEP 2 — Enrich the project entity. Run this command with the description you've written:
441
+
442
+ npx nexarch update-entity \\
443
+ --key "${projectExternalKey}" \\
444
+ --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' \\\n --subtype "app_custom_built"' : ""} \\
445
+ --name "<proper product name from README>" \\
446
+ --description "<2–4 sentence summary of what it does and why>"
447
+ ${subPackages.length > 0 ? `
448
+ STEP 3 — Register each sub-application as its own entity.
449
+ The scanner found ${subPackages.length} workspace package(s). For each one, read its
450
+ README / package.json description, then run:
451
+
452
+ Sub-packages detected:
453
+ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")}
454
+
455
+ For each sub-package run:
456
+
457
+ npx nexarch update-entity \\
458
+ --key "application:<slugified-package-name>" \\
459
+ --entity-type "application" \\
460
+ --subtype "app_custom_built" \\
461
+ --name "<sub-app name>" \\
462
+ --description "<what this sub-app does>"
463
+
464
+ Then wire it to the parent project:
465
+
466
+ npx nexarch add-relationship \\
467
+ --from "application:<slugified-package-name>" \\
468
+ --to "${projectExternalKey}" \\
469
+ --type "part_of"
470
+
471
+ (Note: add-relationship is coming soon — skip if not yet available)
472
+ ` : ""}`);
341
473
  }
@@ -0,0 +1,84 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function parseFlag(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+ function parseOptionValue(args, option) {
8
+ const idx = args.indexOf(option);
9
+ if (idx === -1)
10
+ return null;
11
+ const value = args[idx + 1];
12
+ if (!value || value.startsWith("--"))
13
+ return null;
14
+ return value;
15
+ }
16
+ function parseToolText(result) {
17
+ const text = result.content?.[0]?.text ?? "{}";
18
+ return JSON.parse(text);
19
+ }
20
+ export async function updateEntity(args) {
21
+ const asJson = parseFlag(args, "--json");
22
+ const externalKey = parseOptionValue(args, "--key");
23
+ const name = parseOptionValue(args, "--name");
24
+ const description = parseOptionValue(args, "--description");
25
+ const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
26
+ const entitySubtypeCode = parseOptionValue(args, "--subtype");
27
+ if (!externalKey) {
28
+ console.error("error: --key <externalKey> is required");
29
+ process.exit(1);
30
+ }
31
+ if (!name && !description) {
32
+ console.error("error: at least one of --name or --description is required");
33
+ process.exit(1);
34
+ }
35
+ const creds = requireCredentials();
36
+ const mcpOpts = { companyId: creds.companyId };
37
+ const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
38
+ const policies = parseToolText(policiesRaw);
39
+ const policyBundleHash = policies.policyBundleHash ?? null;
40
+ const nowIso = new Date().toISOString();
41
+ const agentContext = {
42
+ agentId: "nexarch-cli:update-entity",
43
+ agentRunId: `update-entity-${Date.now()}`,
44
+ observedAt: nowIso,
45
+ source: "nexarch-cli",
46
+ model: "n/a",
47
+ provider: "n/a",
48
+ };
49
+ const policyContext = policyBundleHash
50
+ ? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
51
+ : undefined;
52
+ const entity = {
53
+ externalKey,
54
+ entityTypeCode,
55
+ confidence: 1,
56
+ };
57
+ if (name)
58
+ entity.name = name;
59
+ if (description)
60
+ entity.description = description;
61
+ if (entitySubtypeCode)
62
+ entity.entitySubtypeCode = entitySubtypeCode;
63
+ const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
64
+ const result = parseToolText(raw);
65
+ if (asJson) {
66
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
67
+ if (Number(result.summary?.failed ?? 0) > 0)
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const succeeded = result.summary?.succeeded ?? 0;
72
+ const failed = result.summary?.failed ?? 0;
73
+ if (failed > 0) {
74
+ console.error(`Failed to update entity: ${externalKey}`);
75
+ for (const err of result.errors ?? []) {
76
+ console.error(` ${err.externalKey}: ${err.error} — ${err.message}`);
77
+ }
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ if (!asJson) {
82
+ console.log(`Updated ${succeeded} entity: ${externalKey}`);
83
+ }
84
+ }
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { mcpProxy } from "./commands/mcp-proxy.js";
11
11
  import { initAgent } from "./commands/init-agent.js";
12
12
  import { agentIdentify } from "./commands/agent-identify.js";
13
13
  import { initProject } from "./commands/init-project.js";
14
+ import { updateEntity } from "./commands/update-entity.js";
14
15
  const [, , command, ...args] = process.argv;
15
16
  const commands = {
16
17
  login,
@@ -22,6 +23,7 @@ const commands = {
22
23
  "init-agent": initAgent,
23
24
  "agent-identify": agentIdentify,
24
25
  "init-project": initProject,
26
+ "update-entity": updateEntity,
25
27
  };
26
28
  async function main() {
27
29
  if (command === "agent") {
@@ -78,9 +80,19 @@ Usage:
78
80
  names as reference candidates.
79
81
  Options: --dir <path> (default: cwd)
80
82
  --name <name> override project name
81
- --entity-type <code> (default: service)
83
+ --entity-type <code> (default: application)
82
84
  --dry-run preview without writing
83
85
  --json
86
+ nexarch update-entity
87
+ Update the name and/or description of an existing graph entity.
88
+ Use this after init-project to enrich the entity with meaningful
89
+ content from the project README or docs.
90
+ Options: --key <externalKey> (required)
91
+ --name <name>
92
+ --description <text>
93
+ --entity-type <code> (default: application)
94
+ --subtype <code>
95
+ --json
84
96
  `);
85
97
  process.exit(command ? 1 : 0);
86
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Connect AI coding tools to your Nexarch architecture workspace",
5
5
  "keywords": [
6
6
  "nexarch",