nexarch 0.4.9 → 0.5.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.
@@ -5,7 +5,7 @@ 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
- const CLI_VERSION = "0.4.9";
8
+ const CLI_VERSION = "0.5.1";
9
9
  const AGENT_ENTITY_TYPE = "agent";
10
10
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
11
11
  function parseFlag(args, flag) {
@@ -1,9 +1,11 @@
1
1
  import process from "process";
2
+ import { execFileSync } from "node:child_process";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
4
  import { basename, join, relative, resolve as resolvePath } from "node:path";
4
5
  import { homedir } from "node:os";
5
6
  import { requireCredentials } from "../lib/credentials.js";
6
7
  import { callMcpTool } from "../lib/mcp.js";
8
+ import { CURATED_AGENT_ICON_NAMES } from "../lib/application-icons.js";
7
9
  // ─── Helpers ─────────────────────────────────────────────────────────────────
8
10
  function parseFlag(args, flag) {
9
11
  return args.includes(flag);
@@ -24,6 +26,175 @@ function parseToolText(result) {
24
26
  function slugify(name) {
25
27
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
26
28
  }
29
+ function safeExec(command, args, cwd) {
30
+ try {
31
+ const out = execFileSync(command, args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
32
+ const trimmed = out.trim();
33
+ return trimmed || null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function normalizeRepoUrl(raw) {
40
+ const trimmed = raw.trim();
41
+ if (!trimmed)
42
+ return null;
43
+ // user@host:path(.git) → https://host/path
44
+ const scpLike = trimmed.match(/^(?:[a-z0-9._-]+)@([^:]+):(.+)$/i);
45
+ if (scpLike) {
46
+ const host = scpLike[1].toLowerCase();
47
+ const path = scpLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
48
+ return `https://${host}/${path}`;
49
+ }
50
+ // ssh://user@host/path(.git) → https://host/path
51
+ const sshLike = trimmed.match(/^ssh:\/\/(?:[a-z0-9._-]+@)?([^/]+)\/(.+)$/i);
52
+ if (sshLike) {
53
+ const host = sshLike[1].toLowerCase();
54
+ const path = sshLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
55
+ return `https://${host}/${path}`;
56
+ }
57
+ try {
58
+ const u = new URL(trimmed);
59
+ if (u.protocol === "http:" || u.protocol === "https:") {
60
+ u.hash = "";
61
+ u.search = "";
62
+ u.pathname = u.pathname.replace(/\.git$/i, "");
63
+ return u.toString().replace(/\/$/, "");
64
+ }
65
+ }
66
+ catch {
67
+ // ignore
68
+ }
69
+ return null;
70
+ }
71
+ function inferProvider(ref) {
72
+ const value = ref.toLowerCase();
73
+ if (value.includes("github.com"))
74
+ return "github";
75
+ if (value.includes("gitlab.com"))
76
+ return "gitlab";
77
+ if (value.includes("bitbucket.org"))
78
+ return "bitbucket";
79
+ if (value.includes("dev.azure.com") || value.includes("visualstudio.com"))
80
+ return "azure-repos";
81
+ if (value.includes("codecommit"))
82
+ return "codecommit";
83
+ if (value.includes("git.sr.ht") || value.includes("sourcehut"))
84
+ return "sourcehut";
85
+ if (value.includes("forgejo"))
86
+ return "forgejo";
87
+ if (value.includes("gitea"))
88
+ return "gitea";
89
+ return "unknown";
90
+ }
91
+ function providerCanonicalRepoRef(provider) {
92
+ switch (provider) {
93
+ case "github": return "global:repo:github";
94
+ case "gitlab": return "global:repo:gitlab";
95
+ case "bitbucket": return "global:repo:bitbucket";
96
+ case "azure-repos": return "global:repo:azure-repos";
97
+ case "codecommit": return "global:repo:codecommit";
98
+ case "gitea": return "global:repo:gitea";
99
+ case "forgejo": return "global:repo:forgejo";
100
+ case "sourcehut": return "global:repo:sourcehut";
101
+ default: return null;
102
+ }
103
+ }
104
+ function readPackageRepositoryField(dir) {
105
+ const pkgPath = join(dir, "package.json");
106
+ if (!existsSync(pkgPath))
107
+ return null;
108
+ try {
109
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
110
+ if (typeof pkg.repository === "string")
111
+ return pkg.repository;
112
+ if (pkg.repository && typeof pkg.repository === "object" && "url" in pkg.repository && typeof pkg.repository.url === "string") {
113
+ return pkg.repository.url;
114
+ }
115
+ }
116
+ catch {
117
+ // ignore
118
+ }
119
+ return null;
120
+ }
121
+ function readHgDefaultPath(dir) {
122
+ const hgrcPath = join(dir, ".hg", "hgrc");
123
+ if (!existsSync(hgrcPath))
124
+ return null;
125
+ try {
126
+ const content = readFileSync(hgrcPath, "utf8");
127
+ const section = content.match(/\[paths\]([\s\S]*?)(\n\[[^\]]+\]|$)/i)?.[1] ?? "";
128
+ const line = section
129
+ .split("\n")
130
+ .map((l) => l.trim())
131
+ .find((l) => /^default\s*=\s*/i.test(l));
132
+ if (!line)
133
+ return null;
134
+ return line.replace(/^default\s*=\s*/i, "").trim() || null;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function readSvnRepositoryUrl(dir) {
141
+ // Best effort using svn CLI if available.
142
+ const fromCli = safeExec("svn", ["info", "--show-item", "repos-root-url"], dir);
143
+ if (fromCli)
144
+ return fromCli;
145
+ // Very rough fallback for legacy .svn/entries format.
146
+ const entriesPath = join(dir, ".svn", "entries");
147
+ if (!existsSync(entriesPath))
148
+ return null;
149
+ try {
150
+ const content = readFileSync(entriesPath, "utf8");
151
+ const firstUrl = content.match(/https?:\/\/[^\s]+/i)?.[0] ?? null;
152
+ return firstUrl;
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function detectSourceRepository(dir) {
159
+ let rawRef = null;
160
+ let vcsType = "unknown";
161
+ // Prefer git when present.
162
+ if (existsSync(join(dir, ".git"))) {
163
+ rawRef = safeExec("git", ["remote", "get-url", "origin"], dir)
164
+ ?? safeExec("git", ["config", "--get", "remote.origin.url"], dir);
165
+ if (rawRef)
166
+ vcsType = "git";
167
+ }
168
+ // Mercurial fallback.
169
+ if (!rawRef && existsSync(join(dir, ".hg"))) {
170
+ rawRef = safeExec("hg", ["paths", "default"], dir) ?? readHgDefaultPath(dir);
171
+ if (rawRef)
172
+ vcsType = "hg";
173
+ }
174
+ // Subversion fallback.
175
+ if (!rawRef && existsSync(join(dir, ".svn"))) {
176
+ rawRef = readSvnRepositoryUrl(dir);
177
+ if (rawRef)
178
+ vcsType = "svn";
179
+ }
180
+ // package.json repository fallback (metadata only).
181
+ if (!rawRef) {
182
+ rawRef = readPackageRepositoryField(dir);
183
+ if (rawRef)
184
+ vcsType = "unknown";
185
+ }
186
+ if (!rawRef)
187
+ return null;
188
+ const url = normalizeRepoUrl(rawRef);
189
+ const provider = inferProvider(url ?? rawRef);
190
+ return {
191
+ rawRef,
192
+ url,
193
+ vcsType,
194
+ provider,
195
+ canonicalRepoRef: providerCanonicalRepoRef(provider),
196
+ };
197
+ }
27
198
  // ─── Project scanning ─────────────────────────────────────────────────────────
28
199
  // Noise patterns for env var keys that are internal config, not external service references
29
200
  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)$|(_URL|_SECRET|_TOKEN|_KEY|_PASSWORD|_CREDENTIAL|_DSN|_URI)$/;
@@ -571,6 +742,7 @@ export async function initProject(args) {
571
742
  if (!asJson)
572
743
  console.log(`Scanning ${dir}…`);
573
744
  const { projectName, packageJsonCount, detectedNames, rootDepNames, subPackages, detectedEcosystems } = scanProject(dir);
745
+ const detectedRepo = detectSourceRepository(dir);
574
746
  const displayName = nameOverride ?? projectName;
575
747
  const projectSlug = slugify(displayName);
576
748
  const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
@@ -587,6 +759,9 @@ export async function initProject(args) {
587
759
  console.log(` Project : ${displayName} (${entityTypeOverride})`);
588
760
  console.log(` Packages: ${packageJsonCount} package.json file(s)`);
589
761
  console.log(` Detected: ${detectedNames.length} raw names`);
762
+ if (detectedRepo) {
763
+ console.log(` Repo : ${detectedRepo.url ?? detectedRepo.rawRef} (${detectedRepo.provider}/${detectedRepo.vcsType})`);
764
+ }
590
765
  }
591
766
  if (detectedNames.length === 0) {
592
767
  if (!asJson)
@@ -613,7 +788,20 @@ export async function initProject(args) {
613
788
  if (dryRun) {
614
789
  const output = {
615
790
  dryRun: true,
616
- project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
791
+ project: {
792
+ name: displayName,
793
+ externalKey: projectExternalKey,
794
+ entityType: entityTypeOverride,
795
+ sourceRepository: detectedRepo
796
+ ? {
797
+ ref: detectedRepo.rawRef,
798
+ url: detectedRepo.url,
799
+ vcsType: detectedRepo.vcsType,
800
+ provider: detectedRepo.provider,
801
+ componentRef: detectedRepo.canonicalRepoRef,
802
+ }
803
+ : null,
804
+ },
617
805
  resolved: resolvedItems.map((r) => ({
618
806
  input: r.input,
619
807
  canonicalName: r.canonicalName,
@@ -630,9 +818,11 @@ export async function initProject(args) {
630
818
  const policies = parseToolText(policiesRaw);
631
819
  const policyBundleHash = policies.policyBundleHash ?? null;
632
820
  const nowIso = new Date().toISOString();
633
- const repoRef = repoRefOverride ?? dir;
821
+ const repoRef = repoRefOverride ?? detectedRepo?.rawRef ?? dir;
634
822
  const repoPath = repoPathOverride ?? dir;
635
- const repoUrl = repoUrlOverride ?? null;
823
+ const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
824
+ const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
825
+ const sourceProvider = detectedRepo?.provider ?? "unknown";
636
826
  const agentContext = {
637
827
  agentId: "nexarch-cli:init-project",
638
828
  agentRunId: `init-project-${Date.now()}`,
@@ -661,8 +851,12 @@ export async function initProject(args) {
661
851
  source_dir: dir,
662
852
  scanned_at: nowIso,
663
853
  package_json_count: packageJsonCount,
664
- ...(repoUrl ? { repository_url: repoUrl } : {}),
854
+ ...(repoUrl ? { repository_url: repoUrl, source_repository_url: repoUrl } : {}),
665
855
  repository_ref: repoRef,
856
+ source_repository_ref: repoRef,
857
+ source_vcs_type: sourceVcsType,
858
+ source_provider: sourceProvider,
859
+ ...(detectedRepo?.canonicalRepoRef ? { source_repository_component_ref: detectedRepo.canonicalRepoRef } : {}),
666
860
  },
667
861
  });
668
862
  // Resolved reference entities (deduplicated by canonical external ref)
@@ -680,6 +874,28 @@ export async function initProject(args) {
680
874
  confidence: 0.95,
681
875
  });
682
876
  }
877
+ // Ensure source repository component is represented when we can infer one.
878
+ if (detectedRepo?.canonicalRepoRef && !seenRefs.has(detectedRepo.canonicalRepoRef)) {
879
+ seenRefs.add(detectedRepo.canonicalRepoRef);
880
+ entities.push({
881
+ externalKey: detectedRepo.canonicalRepoRef,
882
+ entityTypeCode: "technology_component",
883
+ entitySubtypeCode: "tech_infrastructure",
884
+ name: detectedRepo.provider === "unknown" ? "Source Repository" : `Source Repository (${detectedRepo.provider})`,
885
+ confidence: 0.9,
886
+ ...(repoUrl
887
+ ? {
888
+ attributes: {
889
+ repository_url: repoUrl,
890
+ source_repository_url: repoUrl,
891
+ source_repository_ref: repoRef,
892
+ source_vcs_type: sourceVcsType,
893
+ source_provider: sourceProvider,
894
+ },
895
+ }
896
+ : {}),
897
+ });
898
+ }
683
899
  // Auto-create stub entities for each sub-package
684
900
  const seenSubKeys = new Set();
685
901
  for (const sp of subPackages) {
@@ -750,6 +966,10 @@ export async function initProject(args) {
750
966
  addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
751
967
  }
752
968
  }
969
+ // Project depends_on its source repository component when resolved/inferred.
970
+ if (detectedRepo?.canonicalRepoRef) {
971
+ addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
972
+ }
753
973
  if (!asJson)
754
974
  console.log(`\nWriting to graph…`);
755
975
  // Upsert entities
@@ -768,6 +988,7 @@ export async function initProject(args) {
768
988
  const ecosystemLabel = detectedEcosystems.length > 0
769
989
  ? detectedEcosystems.join(", ")
770
990
  : "unknown";
991
+ const curatedIconList = CURATED_AGENT_ICON_NAMES.join(", ");
771
992
  const manifestHint = detectedEcosystems.includes("nodejs")
772
993
  ? "package.json"
773
994
  : detectedEcosystems.includes("python")
@@ -800,6 +1021,11 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
800
1021
  Type definitions or utility package with no runtime
801
1022
  → --entity-type technology_component --subtype tech_library
802
1023
 
1024
+ ICON ASSIGNMENT (applications only):
1025
+ • Pick one icon from this curated set for each application/sub-app.
1026
+ • If confidence is low, skip --icon rather than guessing.
1027
+ • Allowed icons: ${curatedIconList}
1028
+
803
1029
  For each sub-package, run update-entity to register it:
804
1030
 
805
1031
  npx nexarch update-entity \\
@@ -807,7 +1033,8 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
807
1033
  --entity-type "<chosen entity type>" \\
808
1034
  --subtype "<chosen subtype>" \\
809
1035
  --name "<human readable name>" \\
810
- --description "<what this package does and its role in the project>"
1036
+ --description "<what this package does and its role in the project>" \\
1037
+ --icon "<curated icon>" # applications only
811
1038
 
812
1039
  Then register it as a resolvable alias so future scans don't re-surface it as a candidate:
813
1040
 
@@ -922,13 +1149,18 @@ STEP 2 — Enrich the project entity. Run this command with the description you'
922
1149
 
923
1150
  npx nexarch update-entity \\
924
1151
  --key "${projectExternalKey}" \\
925
- --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\"" : ""} \\
1152
+ --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\"\n --icon \"<curated icon>\"" : ""} \\
926
1153
  --name "<proper product name from README>" \\
927
1154
  --description "<2–4 sentence summary of what it does and why>"
928
1155
  ${subPkgSection}${gapCheckSection}`;
929
1156
  }
930
1157
  const enrichmentTask = {
931
1158
  instructions: buildEnrichmentInstructions(),
1159
+ iconHints: {
1160
+ provider: "lucide",
1161
+ curated: CURATED_AGENT_ICON_NAMES,
1162
+ note: "Use curated list for agent-selected enrichment icons; omit icon when low confidence.",
1163
+ },
932
1164
  projectEntity: {
933
1165
  externalKey: projectExternalKey,
934
1166
  entityType: entityTypeOverride,
@@ -1,6 +1,7 @@
1
1
  import process from "process";
2
2
  import { requireCredentials } from "../lib/credentials.js";
3
3
  import { callMcpTool } from "../lib/mcp.js";
4
+ import { CURATED_AGENT_ICON_SET } from "../lib/application-icons.js";
4
5
  function parseFlag(args, flag) {
5
6
  return args.includes(flag);
6
7
  }
@@ -24,6 +25,7 @@ export async function updateEntity(args) {
24
25
  const description = parseOptionValue(args, "--description");
25
26
  const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
26
27
  const entitySubtypeCode = parseOptionValue(args, "--subtype");
28
+ const iconName = parseOptionValue(args, "--icon");
27
29
  if (!externalKey) {
28
30
  console.error("error: --key <externalKey> is required");
29
31
  process.exit(1);
@@ -50,6 +52,10 @@ export async function updateEntity(args) {
50
52
  const policyContext = policyBundleHash
51
53
  ? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
52
54
  : undefined;
55
+ if (iconName && !CURATED_AGENT_ICON_SET.has(iconName)) {
56
+ console.error(`error: --icon must be one of the curated agent icons (got '${iconName}')`);
57
+ process.exit(1);
58
+ }
53
59
  const entity = {
54
60
  externalKey,
55
61
  entityTypeCode,
@@ -61,6 +67,14 @@ export async function updateEntity(args) {
61
67
  entity.description = description;
62
68
  if (entitySubtypeCode)
63
69
  entity.entitySubtypeCode = entitySubtypeCode;
70
+ if (iconName) {
71
+ entity.attributes = {
72
+ application_icon: {
73
+ provider: "lucide",
74
+ name: iconName,
75
+ },
76
+ };
77
+ }
64
78
  const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
65
79
  const result = parseToolText(raw);
66
80
  if (asJson) {
package/dist/index.js CHANGED
@@ -87,6 +87,7 @@ Usage:
87
87
  --description <text>
88
88
  --entity-type <code> (default: application)
89
89
  --subtype <code>
90
+ --icon <lucide-name> (curated set; for agent enrichment)
90
91
  --json
91
92
  nexarch add-relationship
92
93
  Add a relationship between two existing graph entities.
@@ -0,0 +1,49 @@
1
+ export const CURATED_AGENT_ICON_NAMES = [
2
+ "rocket",
3
+ "globe",
4
+ "monitor",
5
+ "smartphone",
6
+ "server",
7
+ "database",
8
+ "cloud",
9
+ "network",
10
+ "shield",
11
+ "lock",
12
+ "cpu",
13
+ "code",
14
+ "terminal",
15
+ "bug",
16
+ "flask-conical",
17
+ "bot",
18
+ "brain",
19
+ "sparkles",
20
+ "activity",
21
+ "gauge",
22
+ "clock-3",
23
+ "calendar",
24
+ "book-open",
25
+ "file-code",
26
+ "folder",
27
+ "search",
28
+ "filter",
29
+ "list-checks",
30
+ "kanban-square",
31
+ "layers",
32
+ "workflow",
33
+ "git-branch",
34
+ "package",
35
+ "wrench",
36
+ "settings",
37
+ "building-2",
38
+ "factory",
39
+ "store",
40
+ "map",
41
+ "users",
42
+ "briefcase",
43
+ "bar-chart-3",
44
+ "trending-up",
45
+ "target",
46
+ "leaf",
47
+ "heart-pulse",
48
+ ];
49
+ export const CURATED_AGENT_ICON_SET = new Set(CURATED_AGENT_ICON_NAMES);
package/dist/lib/mcp.js CHANGED
@@ -68,7 +68,7 @@ export async function mcpInitialize(options = {}) {
68
68
  return callMcpRpc("initialize", {
69
69
  protocolVersion: "2024-11-05",
70
70
  capabilities: {},
71
- clientInfo: { name: "nexarch-cli", version: "0.4.9" },
71
+ clientInfo: { name: "nexarch-cli", version: "0.5.1" },
72
72
  }, options);
73
73
  }
74
74
  export async function mcpListTools(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.4.9",
3
+ "version": "0.5.1",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",