nexarch 0.8.2 → 0.8.9

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.
@@ -1,12 +1,12 @@
1
1
  import { arch, homedir, hostname, platform, release, type as osType, userInfo } from "os";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import process from "process";
5
5
  import { requireCredentials } from "../lib/credentials.js";
6
6
  import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
7
7
  import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
8
8
  import { buildVersionAttributes } from "../lib/version-normalization.js";
9
- const CLI_VERSION = "0.5.12";
9
+ const CLI_VERSION = "0.8.4";
10
10
  const AGENT_ENTITY_TYPE = "agent";
11
11
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
12
12
  function parseFlag(args, flag) {
@@ -62,17 +62,80 @@ function detectLikelyApplicationInCwd() {
62
62
  "build.gradle",
63
63
  "build.gradle.kts",
64
64
  ];
65
- const evidence = files.filter((file) => existsSync(join(cwd, file)));
65
+ const skipDirs = new Set([
66
+ ".git",
67
+ "node_modules",
68
+ "dist",
69
+ "build",
70
+ ".next",
71
+ ".turbo",
72
+ "coverage",
73
+ "tmp",
74
+ ".cache",
75
+ "target",
76
+ "vendor",
77
+ ".venv",
78
+ "venv",
79
+ "__pycache__",
80
+ ]);
81
+ const evidence = new Set();
82
+ const checkDir = (baseDir, relativePrefix = "") => {
83
+ for (const file of files) {
84
+ if (existsSync(join(baseDir, file))) {
85
+ evidence.add(`${relativePrefix}${file}`);
86
+ }
87
+ }
88
+ try {
89
+ const entries = readdirSync(baseDir);
90
+ if (entries.some((name) => name.toLowerCase().endsWith(".csproj"))) {
91
+ evidence.add(`${relativePrefix}*.csproj`);
92
+ }
93
+ }
94
+ catch {
95
+ // non-fatal; ignore
96
+ }
97
+ };
98
+ // Level 0: current directory
99
+ checkDir(cwd);
100
+ // Levels 1-2: monorepo children
66
101
  try {
67
- const entries = readdirSync(cwd);
68
- if (entries.some((name) => name.toLowerCase().endsWith(".csproj"))) {
69
- evidence.push("*.csproj");
102
+ for (const l1 of readdirSync(cwd)) {
103
+ if (l1.startsWith(".") || skipDirs.has(l1))
104
+ continue;
105
+ const l1Path = join(cwd, l1);
106
+ try {
107
+ if (!statSync(l1Path).isDirectory())
108
+ continue;
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ checkDir(l1Path, `${l1}/`);
114
+ try {
115
+ for (const l2 of readdirSync(l1Path)) {
116
+ if (l2.startsWith(".") || skipDirs.has(l2))
117
+ continue;
118
+ const l2Path = join(l1Path, l2);
119
+ try {
120
+ if (!statSync(l2Path).isDirectory())
121
+ continue;
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ checkDir(l2Path, `${l1}/${l2}/`);
127
+ }
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
70
132
  }
71
133
  }
72
134
  catch {
73
- // non-fatal; ignore
135
+ // ignore
74
136
  }
75
- return { detected: evidence.length > 0, evidence };
137
+ const evidenceList = Array.from(evidence);
138
+ return { detected: evidenceList.length > 0, evidence: evidenceList };
76
139
  }
77
140
  function getSanitizedHostname(redactHostname) {
78
141
  const raw = hostname() || "unknown-host";
@@ -489,24 +489,19 @@ function resolveWorkspaceDirs(rootDir, workspaces) {
489
489
  }
490
490
  return dirs;
491
491
  }
492
- // Collect all package.json paths: root + workspace packages + 2-level fallback
492
+ // Collect all package.json paths: root + workspace packages + fallback scans.
493
493
  function findPackageJsonPaths(rootDir) {
494
494
  const paths = [];
495
+ const seen = new Set();
495
496
  const rootPkgPath = join(rootDir, "package.json");
496
- if (!existsSync(rootPkgPath))
497
- return paths;
498
- paths.push(rootPkgPath);
499
- const rootPkg = readRootPackage(rootPkgPath);
500
- if (rootPkg.workspaces) {
501
- // Workspace monorepo: resolve workspace dirs and find their package.json files
502
- for (const wsDir of resolveWorkspaceDirs(rootDir, rootPkg.workspaces)) {
503
- const pkgPath = join(wsDir, "package.json");
504
- if (existsSync(pkgPath))
505
- paths.push(pkgPath);
506
- }
507
- }
508
- else {
509
- // Non-workspace: scan up to 2 levels deep
497
+ const pushPath = (pkgPath) => {
498
+ const norm = pkgPath.replace(/\\/g, "/");
499
+ if (seen.has(norm))
500
+ return;
501
+ seen.add(norm);
502
+ paths.push(pkgPath);
503
+ };
504
+ const scanTwoLevels = () => {
510
505
  try {
511
506
  for (const l1 of readdirSync(rootDir)) {
512
507
  if (SKIP_DIRS.has(l1) || l1.startsWith("."))
@@ -517,7 +512,7 @@ function findPackageJsonPaths(rootDir) {
517
512
  continue;
518
513
  const l1Pkg = join(l1Path, "package.json");
519
514
  if (existsSync(l1Pkg)) {
520
- paths.push(l1Pkg);
515
+ pushPath(l1Pkg);
521
516
  continue;
522
517
  }
523
518
  for (const l2 of readdirSync(l1Path)) {
@@ -529,7 +524,7 @@ function findPackageJsonPaths(rootDir) {
529
524
  continue;
530
525
  const l2Pkg = join(l2Path, "package.json");
531
526
  if (existsSync(l2Pkg))
532
- paths.push(l2Pkg);
527
+ pushPath(l2Pkg);
533
528
  }
534
529
  catch { }
535
530
  }
@@ -538,7 +533,23 @@ function findPackageJsonPaths(rootDir) {
538
533
  }
539
534
  }
540
535
  catch { }
536
+ };
537
+ if (existsSync(rootPkgPath)) {
538
+ pushPath(rootPkgPath);
539
+ const rootPkg = readRootPackage(rootPkgPath);
540
+ if (rootPkg.workspaces) {
541
+ for (const wsDir of resolveWorkspaceDirs(rootDir, rootPkg.workspaces)) {
542
+ const pkgPath = join(wsDir, "package.json");
543
+ if (existsSync(pkgPath))
544
+ pushPath(pkgPath);
545
+ }
546
+ }
547
+ // Keep fallback in case workspace globs are incomplete.
548
+ scanTwoLevels();
549
+ return paths;
541
550
  }
551
+ // No root package.json: still detect monorepos/components by scanning children.
552
+ scanTwoLevels();
542
553
  return paths;
543
554
  }
544
555
  // ─── Ecosystem scanners ───────────────────────────────────────────────────────
@@ -880,7 +891,13 @@ export async function initProject(args) {
880
891
  for (const sp of subPackages) {
881
892
  const nameSlug = slugify(sp.name);
882
893
  const needsPrefix = !sp.name.includes("/") && !nameSlug.startsWith(projectSlug);
883
- sp.externalKey = `${sp.entityType}:${needsPrefix ? `${projectSlug}-${nameSlug}` : nameSlug}`;
894
+ let keySlug = needsPrefix ? `${projectSlug}-${nameSlug}` : nameSlug;
895
+ // Avoid key collision with the top-level project (e.g. sub-package named "nexarch").
896
+ if (`${sp.entityType}:${keySlug}` === projectExternalKey) {
897
+ const relSlug = slugify(sp.relativePath.replace(/\//g, "-"));
898
+ keySlug = relSlug ? `${projectSlug}-${relSlug}` : `${projectSlug}-${nameSlug}-sub`;
899
+ }
900
+ sp.externalKey = `${sp.entityType}:${keySlug}`;
884
901
  }
885
902
  if (!asJson) {
886
903
  console.log(` Project : ${displayName} (${entityTypeOverride})`);
@@ -1238,7 +1255,55 @@ ${subPackages.map((sp) => {
1238
1255
  return ` • ${sp.name} (--from "${sp.externalKey}"):\n${lines.join("\n")}`;
1239
1256
  }).join("\n\n")}
1240
1257
  `;
1241
- const finalStep = subPackages.length > 0 ? "STEP 4" : "STEP 3";
1258
+ const adrStepNumber = subPackages.length > 0 ? "STEP 4" : "STEP 3";
1259
+ const finalStep = subPackages.length > 0 ? "STEP 5" : "STEP 4";
1260
+ const adrSection = `
1261
+ ${adrStepNumber} — Ask the enrichment agent to extract ADRs and register decision records.
1262
+
1263
+ Do NOT rely on init-project to parse ADR files. The enrichment agent should read ADR markdown and
1264
+ explicitly register decision records with relationships.
1265
+
1266
+ Agent task:
1267
+ • Find ADR files (e.g. docs/adr, adrs, decisions, ADR-*.md)
1268
+ • For each ADR, create/update a decision record entity
1269
+ • Link it using: decision_record --decides--> application/sub-app
1270
+
1271
+ REQUIRED fields to pass per decision:
1272
+ • key: decision_record:<project>-<adr-slug>
1273
+ • subtype: decision_architecture
1274
+ • name: ADR title
1275
+ • description: MUST be rich text (Markdown), not plain key/value text.
1276
+ Use this exact Markdown structure so the web portal renders cleanly:
1277
+
1278
+ ## ADR Metadata
1279
+ - **ADR-ID:** <id/filename slug>
1280
+ - **Status:** <proposed|accepted|superseded|deprecated|...>
1281
+ - **Date:** <YYYY-MM-DD or source value>
1282
+ - **Deciders:** <comma-separated names/roles>
1283
+ - **Source Path:** <repo-relative path>
1284
+
1285
+ ## Context
1286
+ <1-2 lines>
1287
+
1288
+ ## Decision
1289
+ <1-3 lines>
1290
+
1291
+ ## Consequences
1292
+ <1-3 lines>
1293
+
1294
+ Command pattern:
1295
+ npx nexarch update-entity \\
1296
+ --key "decision_record:<project>-<adr-slug>" \\
1297
+ --entity-type "decision_record" \\
1298
+ --subtype "decision_architecture" \\
1299
+ --name "<ADR title>" \\
1300
+ --description "## ADR Metadata\n- **ADR-ID:** <id>\n- **Status:** <status>\n- **Date:** <date>\n- **Deciders:** <deciders>\n- **Source Path:** <path>\n\n## Context\n<context>\n\n## Decision\n<decision>\n\n## Consequences\n<consequences>"
1301
+
1302
+ npx nexarch add-relationship \\
1303
+ --from "decision_record:<project>-<adr-slug>" \\
1304
+ --to "<application-or-sub-app-key>" \\
1305
+ --type "decides"
1306
+ `;
1242
1307
  const gapCheckSection = `
1243
1308
  ${finalStep} — Identify architecturally significant components not auto-detected.
1244
1309
 
@@ -1307,7 +1372,7 @@ STEP 2 — Enrich the project entity. Run this command with the description you'
1307
1372
  --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\" \\\n --icon \"<curated icon>\"" : ""} \\
1308
1373
  --name "<proper product name from README>" \\
1309
1374
  --description "<2–4 sentence summary of what it does and why>"
1310
- ${subPkgSection}${gapCheckSection}`;
1375
+ ${subPkgSection}${adrSection}${gapCheckSection}`;
1311
1376
  }
1312
1377
  const enrichmentTask = {
1313
1378
  instructions: buildEnrichmentInstructions(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.8.2",
3
+ "version": "0.8.9",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",