nexarch 0.1.16 → 0.1.18

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.16";
7
+ const CLI_VERSION = "0.1.18";
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) {
@@ -184,12 +263,12 @@ export async function initProject(args) {
184
263
  const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
185
264
  const dir = resolvePath(dirArg);
186
265
  const nameOverride = parseOptionValue(args, "--name");
187
- const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "service";
266
+ const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "application";
188
267
  const creds = requireCredentials();
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}`;
@@ -254,12 +333,13 @@ export async function initProject(args) {
254
333
  : undefined;
255
334
  // Build entity list — project entity + all resolved reference entities
256
335
  const entities = [];
257
- // The project itself
336
+ // The project itself — default subtype app_custom_built for application type
337
+ const projectSubtype = entityTypeOverride === "application" ? "app_custom_built" : undefined;
258
338
  entities.push({
259
339
  externalKey: projectExternalKey,
260
340
  entityTypeCode: entityTypeOverride,
341
+ ...(projectSubtype ? { entitySubtypeCode: projectSubtype } : {}),
261
342
  name: displayName,
262
- description: `Project initialised from ${dir}`,
263
343
  confidence: 1,
264
344
  attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
265
345
  });
@@ -336,4 +416,49 @@ export async function initProject(args) {
336
416
  console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
337
417
  }
338
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 — Call nexarch_upsert_entities to enrich the project entity:
441
+ • externalKey: "${projectExternalKey}"
442
+ • name: (proper product/project name from the README)
443
+ • description: (2–4 sentence summary of what it does and why)
444
+ • Preserve existing attributes, add any business context attributes you find.
445
+ • entityTypeCode: "${entityTypeOverride}"${entityTypeOverride === "application" ? '\n • entitySubtypeCode: "app_custom_built"' : ""}
446
+ ${subPackages.length > 0 ? `
447
+ STEP 3 — Register each sub-application as its own entity:
448
+ The scanner found ${subPackages.length} workspace package(s). For each one, call
449
+ nexarch_upsert_entities then nexarch_upsert_relationships to wire it to the parent.
450
+
451
+ Sub-packages detected:
452
+ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")}
453
+
454
+ For each sub-package:
455
+ • Read its own README / package.json description if present
456
+ • entityTypeCode: "application", entitySubtypeCode: "app_custom_built"
457
+ • externalKey: "application:<slug>" (slugify the package name)
458
+ • description: what this specific sub-app does
459
+ • Relationship: fromEntityExternalKey = "<sub-app key>",
460
+ toEntityExternalKey = "${projectExternalKey}",
461
+ relationshipTypeCode = "part_of"
462
+ ` : ""}
463
+ Once enrichment is complete, run \`nexarch graph snapshot\` to capture the updated state.`);
339
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Connect AI coding tools to your Nexarch architecture workspace",
5
5
  "keywords": [
6
6
  "nexarch",