nexarch 0.4.9 → 0.5.0
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-agent.js +1 -1
- package/dist/commands/init-project.js +223 -4
- package/dist/lib/mcp.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
8
|
+
const CLI_VERSION = "0.5.0";
|
|
9
9
|
const AGENT_ENTITY_TYPE = "agent";
|
|
10
10
|
const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
|
|
11
11
|
function parseFlag(args, flag) {
|
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -24,6 +25,175 @@ function parseToolText(result) {
|
|
|
24
25
|
function slugify(name) {
|
|
25
26
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
26
27
|
}
|
|
28
|
+
function safeExec(command, args, cwd) {
|
|
29
|
+
try {
|
|
30
|
+
const out = execFileSync(command, args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
31
|
+
const trimmed = out.trim();
|
|
32
|
+
return trimmed || null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function normalizeRepoUrl(raw) {
|
|
39
|
+
const trimmed = raw.trim();
|
|
40
|
+
if (!trimmed)
|
|
41
|
+
return null;
|
|
42
|
+
// user@host:path(.git) → https://host/path
|
|
43
|
+
const scpLike = trimmed.match(/^(?:[a-z0-9._-]+)@([^:]+):(.+)$/i);
|
|
44
|
+
if (scpLike) {
|
|
45
|
+
const host = scpLike[1].toLowerCase();
|
|
46
|
+
const path = scpLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
|
|
47
|
+
return `https://${host}/${path}`;
|
|
48
|
+
}
|
|
49
|
+
// ssh://user@host/path(.git) → https://host/path
|
|
50
|
+
const sshLike = trimmed.match(/^ssh:\/\/(?:[a-z0-9._-]+@)?([^/]+)\/(.+)$/i);
|
|
51
|
+
if (sshLike) {
|
|
52
|
+
const host = sshLike[1].toLowerCase();
|
|
53
|
+
const path = sshLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
|
|
54
|
+
return `https://${host}/${path}`;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const u = new URL(trimmed);
|
|
58
|
+
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
59
|
+
u.hash = "";
|
|
60
|
+
u.search = "";
|
|
61
|
+
u.pathname = u.pathname.replace(/\.git$/i, "");
|
|
62
|
+
return u.toString().replace(/\/$/, "");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function inferProvider(ref) {
|
|
71
|
+
const value = ref.toLowerCase();
|
|
72
|
+
if (value.includes("github.com"))
|
|
73
|
+
return "github";
|
|
74
|
+
if (value.includes("gitlab.com"))
|
|
75
|
+
return "gitlab";
|
|
76
|
+
if (value.includes("bitbucket.org"))
|
|
77
|
+
return "bitbucket";
|
|
78
|
+
if (value.includes("dev.azure.com") || value.includes("visualstudio.com"))
|
|
79
|
+
return "azure-repos";
|
|
80
|
+
if (value.includes("codecommit"))
|
|
81
|
+
return "codecommit";
|
|
82
|
+
if (value.includes("git.sr.ht") || value.includes("sourcehut"))
|
|
83
|
+
return "sourcehut";
|
|
84
|
+
if (value.includes("forgejo"))
|
|
85
|
+
return "forgejo";
|
|
86
|
+
if (value.includes("gitea"))
|
|
87
|
+
return "gitea";
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
function providerCanonicalRepoRef(provider) {
|
|
91
|
+
switch (provider) {
|
|
92
|
+
case "github": return "global:repo:github";
|
|
93
|
+
case "gitlab": return "global:repo:gitlab";
|
|
94
|
+
case "bitbucket": return "global:repo:bitbucket";
|
|
95
|
+
case "azure-repos": return "global:repo:azure-repos";
|
|
96
|
+
case "codecommit": return "global:repo:codecommit";
|
|
97
|
+
case "gitea": return "global:repo:gitea";
|
|
98
|
+
case "forgejo": return "global:repo:forgejo";
|
|
99
|
+
case "sourcehut": return "global:repo:sourcehut";
|
|
100
|
+
default: return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function readPackageRepositoryField(dir) {
|
|
104
|
+
const pkgPath = join(dir, "package.json");
|
|
105
|
+
if (!existsSync(pkgPath))
|
|
106
|
+
return null;
|
|
107
|
+
try {
|
|
108
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
109
|
+
if (typeof pkg.repository === "string")
|
|
110
|
+
return pkg.repository;
|
|
111
|
+
if (pkg.repository && typeof pkg.repository === "object" && "url" in pkg.repository && typeof pkg.repository.url === "string") {
|
|
112
|
+
return pkg.repository.url;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function readHgDefaultPath(dir) {
|
|
121
|
+
const hgrcPath = join(dir, ".hg", "hgrc");
|
|
122
|
+
if (!existsSync(hgrcPath))
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const content = readFileSync(hgrcPath, "utf8");
|
|
126
|
+
const section = content.match(/\[paths\]([\s\S]*?)(\n\[[^\]]+\]|$)/i)?.[1] ?? "";
|
|
127
|
+
const line = section
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((l) => l.trim())
|
|
130
|
+
.find((l) => /^default\s*=\s*/i.test(l));
|
|
131
|
+
if (!line)
|
|
132
|
+
return null;
|
|
133
|
+
return line.replace(/^default\s*=\s*/i, "").trim() || null;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function readSvnRepositoryUrl(dir) {
|
|
140
|
+
// Best effort using svn CLI if available.
|
|
141
|
+
const fromCli = safeExec("svn", ["info", "--show-item", "repos-root-url"], dir);
|
|
142
|
+
if (fromCli)
|
|
143
|
+
return fromCli;
|
|
144
|
+
// Very rough fallback for legacy .svn/entries format.
|
|
145
|
+
const entriesPath = join(dir, ".svn", "entries");
|
|
146
|
+
if (!existsSync(entriesPath))
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
const content = readFileSync(entriesPath, "utf8");
|
|
150
|
+
const firstUrl = content.match(/https?:\/\/[^\s]+/i)?.[0] ?? null;
|
|
151
|
+
return firstUrl;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function detectSourceRepository(dir) {
|
|
158
|
+
let rawRef = null;
|
|
159
|
+
let vcsType = "unknown";
|
|
160
|
+
// Prefer git when present.
|
|
161
|
+
if (existsSync(join(dir, ".git"))) {
|
|
162
|
+
rawRef = safeExec("git", ["remote", "get-url", "origin"], dir)
|
|
163
|
+
?? safeExec("git", ["config", "--get", "remote.origin.url"], dir);
|
|
164
|
+
if (rawRef)
|
|
165
|
+
vcsType = "git";
|
|
166
|
+
}
|
|
167
|
+
// Mercurial fallback.
|
|
168
|
+
if (!rawRef && existsSync(join(dir, ".hg"))) {
|
|
169
|
+
rawRef = safeExec("hg", ["paths", "default"], dir) ?? readHgDefaultPath(dir);
|
|
170
|
+
if (rawRef)
|
|
171
|
+
vcsType = "hg";
|
|
172
|
+
}
|
|
173
|
+
// Subversion fallback.
|
|
174
|
+
if (!rawRef && existsSync(join(dir, ".svn"))) {
|
|
175
|
+
rawRef = readSvnRepositoryUrl(dir);
|
|
176
|
+
if (rawRef)
|
|
177
|
+
vcsType = "svn";
|
|
178
|
+
}
|
|
179
|
+
// package.json repository fallback (metadata only).
|
|
180
|
+
if (!rawRef) {
|
|
181
|
+
rawRef = readPackageRepositoryField(dir);
|
|
182
|
+
if (rawRef)
|
|
183
|
+
vcsType = "unknown";
|
|
184
|
+
}
|
|
185
|
+
if (!rawRef)
|
|
186
|
+
return null;
|
|
187
|
+
const url = normalizeRepoUrl(rawRef);
|
|
188
|
+
const provider = inferProvider(url ?? rawRef);
|
|
189
|
+
return {
|
|
190
|
+
rawRef,
|
|
191
|
+
url,
|
|
192
|
+
vcsType,
|
|
193
|
+
provider,
|
|
194
|
+
canonicalRepoRef: providerCanonicalRepoRef(provider),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
27
197
|
// ─── Project scanning ─────────────────────────────────────────────────────────
|
|
28
198
|
// Noise patterns for env var keys that are internal config, not external service references
|
|
29
199
|
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 +741,7 @@ export async function initProject(args) {
|
|
|
571
741
|
if (!asJson)
|
|
572
742
|
console.log(`Scanning ${dir}…`);
|
|
573
743
|
const { projectName, packageJsonCount, detectedNames, rootDepNames, subPackages, detectedEcosystems } = scanProject(dir);
|
|
744
|
+
const detectedRepo = detectSourceRepository(dir);
|
|
574
745
|
const displayName = nameOverride ?? projectName;
|
|
575
746
|
const projectSlug = slugify(displayName);
|
|
576
747
|
const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
|
|
@@ -587,6 +758,9 @@ export async function initProject(args) {
|
|
|
587
758
|
console.log(` Project : ${displayName} (${entityTypeOverride})`);
|
|
588
759
|
console.log(` Packages: ${packageJsonCount} package.json file(s)`);
|
|
589
760
|
console.log(` Detected: ${detectedNames.length} raw names`);
|
|
761
|
+
if (detectedRepo) {
|
|
762
|
+
console.log(` Repo : ${detectedRepo.url ?? detectedRepo.rawRef} (${detectedRepo.provider}/${detectedRepo.vcsType})`);
|
|
763
|
+
}
|
|
590
764
|
}
|
|
591
765
|
if (detectedNames.length === 0) {
|
|
592
766
|
if (!asJson)
|
|
@@ -613,7 +787,20 @@ export async function initProject(args) {
|
|
|
613
787
|
if (dryRun) {
|
|
614
788
|
const output = {
|
|
615
789
|
dryRun: true,
|
|
616
|
-
project: {
|
|
790
|
+
project: {
|
|
791
|
+
name: displayName,
|
|
792
|
+
externalKey: projectExternalKey,
|
|
793
|
+
entityType: entityTypeOverride,
|
|
794
|
+
sourceRepository: detectedRepo
|
|
795
|
+
? {
|
|
796
|
+
ref: detectedRepo.rawRef,
|
|
797
|
+
url: detectedRepo.url,
|
|
798
|
+
vcsType: detectedRepo.vcsType,
|
|
799
|
+
provider: detectedRepo.provider,
|
|
800
|
+
componentRef: detectedRepo.canonicalRepoRef,
|
|
801
|
+
}
|
|
802
|
+
: null,
|
|
803
|
+
},
|
|
617
804
|
resolved: resolvedItems.map((r) => ({
|
|
618
805
|
input: r.input,
|
|
619
806
|
canonicalName: r.canonicalName,
|
|
@@ -630,9 +817,11 @@ export async function initProject(args) {
|
|
|
630
817
|
const policies = parseToolText(policiesRaw);
|
|
631
818
|
const policyBundleHash = policies.policyBundleHash ?? null;
|
|
632
819
|
const nowIso = new Date().toISOString();
|
|
633
|
-
const repoRef = repoRefOverride ?? dir;
|
|
820
|
+
const repoRef = repoRefOverride ?? detectedRepo?.rawRef ?? dir;
|
|
634
821
|
const repoPath = repoPathOverride ?? dir;
|
|
635
|
-
const repoUrl = repoUrlOverride ?? null;
|
|
822
|
+
const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
|
|
823
|
+
const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
|
|
824
|
+
const sourceProvider = detectedRepo?.provider ?? "unknown";
|
|
636
825
|
const agentContext = {
|
|
637
826
|
agentId: "nexarch-cli:init-project",
|
|
638
827
|
agentRunId: `init-project-${Date.now()}`,
|
|
@@ -661,8 +850,12 @@ export async function initProject(args) {
|
|
|
661
850
|
source_dir: dir,
|
|
662
851
|
scanned_at: nowIso,
|
|
663
852
|
package_json_count: packageJsonCount,
|
|
664
|
-
...(repoUrl ? { repository_url: repoUrl } : {}),
|
|
853
|
+
...(repoUrl ? { repository_url: repoUrl, source_repository_url: repoUrl } : {}),
|
|
665
854
|
repository_ref: repoRef,
|
|
855
|
+
source_repository_ref: repoRef,
|
|
856
|
+
source_vcs_type: sourceVcsType,
|
|
857
|
+
source_provider: sourceProvider,
|
|
858
|
+
...(detectedRepo?.canonicalRepoRef ? { source_repository_component_ref: detectedRepo.canonicalRepoRef } : {}),
|
|
666
859
|
},
|
|
667
860
|
});
|
|
668
861
|
// Resolved reference entities (deduplicated by canonical external ref)
|
|
@@ -680,6 +873,28 @@ export async function initProject(args) {
|
|
|
680
873
|
confidence: 0.95,
|
|
681
874
|
});
|
|
682
875
|
}
|
|
876
|
+
// Ensure source repository component is represented when we can infer one.
|
|
877
|
+
if (detectedRepo?.canonicalRepoRef && !seenRefs.has(detectedRepo.canonicalRepoRef)) {
|
|
878
|
+
seenRefs.add(detectedRepo.canonicalRepoRef);
|
|
879
|
+
entities.push({
|
|
880
|
+
externalKey: detectedRepo.canonicalRepoRef,
|
|
881
|
+
entityTypeCode: "technology_component",
|
|
882
|
+
entitySubtypeCode: "tech_infrastructure",
|
|
883
|
+
name: detectedRepo.provider === "unknown" ? "Source Repository" : `Source Repository (${detectedRepo.provider})`,
|
|
884
|
+
confidence: 0.9,
|
|
885
|
+
...(repoUrl
|
|
886
|
+
? {
|
|
887
|
+
attributes: {
|
|
888
|
+
repository_url: repoUrl,
|
|
889
|
+
source_repository_url: repoUrl,
|
|
890
|
+
source_repository_ref: repoRef,
|
|
891
|
+
source_vcs_type: sourceVcsType,
|
|
892
|
+
source_provider: sourceProvider,
|
|
893
|
+
},
|
|
894
|
+
}
|
|
895
|
+
: {}),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
683
898
|
// Auto-create stub entities for each sub-package
|
|
684
899
|
const seenSubKeys = new Set();
|
|
685
900
|
for (const sp of subPackages) {
|
|
@@ -750,6 +965,10 @@ export async function initProject(args) {
|
|
|
750
965
|
addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
|
|
751
966
|
}
|
|
752
967
|
}
|
|
968
|
+
// Project depends_on its source repository component when resolved/inferred.
|
|
969
|
+
if (detectedRepo?.canonicalRepoRef) {
|
|
970
|
+
addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
|
|
971
|
+
}
|
|
753
972
|
if (!asJson)
|
|
754
973
|
console.log(`\nWriting to graph…`);
|
|
755
974
|
// Upsert entities
|
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.
|
|
71
|
+
clientInfo: { name: "nexarch-cli", version: "0.5.0" },
|
|
72
72
|
}, options);
|
|
73
73
|
}
|
|
74
74
|
export async function mcpListTools(options = {}) {
|