nexarch 0.1.15 → 0.1.16
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 +339 -0
- package/dist/index.js +12 -0
- package/package.json +1 -1
|
@@ -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.
|
|
7
|
+
const CLI_VERSION = "0.1.16";
|
|
8
8
|
const AGENT_ENTITY_TYPE = "agent";
|
|
9
9
|
const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
|
|
10
10
|
function parseFlag(args, flag) {
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
5
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
6
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
7
|
+
function parseFlag(args, flag) {
|
|
8
|
+
return args.includes(flag);
|
|
9
|
+
}
|
|
10
|
+
function parseOptionValue(args, option) {
|
|
11
|
+
const idx = args.indexOf(option);
|
|
12
|
+
if (idx === -1)
|
|
13
|
+
return null;
|
|
14
|
+
const value = args[idx + 1];
|
|
15
|
+
if (!value || value.startsWith("--"))
|
|
16
|
+
return null;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function parseToolText(result) {
|
|
20
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
function slugify(name) {
|
|
24
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
25
|
+
}
|
|
26
|
+
// ─── Project scanning ─────────────────────────────────────────────────────────
|
|
27
|
+
function parseEnvKeys(content) {
|
|
28
|
+
return content
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
32
|
+
.map((line) => line.replace(/^export\s+/, ""))
|
|
33
|
+
.map((line) => line.split("=")[0].trim())
|
|
34
|
+
.filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key)); // min 3 chars, all caps
|
|
35
|
+
}
|
|
36
|
+
function detectFromGitConfig(dir) {
|
|
37
|
+
const configPath = join(dir, ".git", "config");
|
|
38
|
+
if (!existsSync(configPath))
|
|
39
|
+
return [];
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(configPath, "utf8");
|
|
42
|
+
// Match url under [remote "origin"]
|
|
43
|
+
const match = content.match(/\[remote\s+"origin"\][^\[]*url\s*=\s*([^\n]+)/s);
|
|
44
|
+
if (!match)
|
|
45
|
+
return [];
|
|
46
|
+
const url = match[1].trim();
|
|
47
|
+
if (url.includes("github.com"))
|
|
48
|
+
return ["github"];
|
|
49
|
+
if (url.includes("gitlab.com"))
|
|
50
|
+
return ["gitlab"];
|
|
51
|
+
if (url.includes("bitbucket.org"))
|
|
52
|
+
return ["bitbucket"];
|
|
53
|
+
if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
|
|
54
|
+
return ["azure-devops"];
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
function detectFromFilesystem(dir) {
|
|
60
|
+
const detected = [];
|
|
61
|
+
// CI/CD
|
|
62
|
+
if (existsSync(join(dir, ".github", "workflows")))
|
|
63
|
+
detected.push("github-actions");
|
|
64
|
+
if (existsSync(join(dir, ".gitlab-ci.yml")))
|
|
65
|
+
detected.push("gitlab");
|
|
66
|
+
if (existsSync(join(dir, "Jenkinsfile")))
|
|
67
|
+
detected.push("jenkins");
|
|
68
|
+
if (existsSync(join(dir, ".circleci", "config.yml")))
|
|
69
|
+
detected.push("circleci");
|
|
70
|
+
// Deployment / hosting
|
|
71
|
+
if (existsSync(join(dir, "vercel.json")) || existsSync(join(dir, ".vercel")))
|
|
72
|
+
detected.push("vercel");
|
|
73
|
+
if (existsSync(join(dir, "netlify.toml")))
|
|
74
|
+
detected.push("netlify");
|
|
75
|
+
if (existsSync(join(dir, "fly.toml")))
|
|
76
|
+
detected.push("fly");
|
|
77
|
+
if (existsSync(join(dir, "railway.json")) || existsSync(join(dir, "railway.toml")))
|
|
78
|
+
detected.push("railway");
|
|
79
|
+
if (existsSync(join(dir, "render.yaml")))
|
|
80
|
+
detected.push("render");
|
|
81
|
+
// Containers
|
|
82
|
+
if (existsSync(join(dir, "Dockerfile")))
|
|
83
|
+
detected.push("docker");
|
|
84
|
+
if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "docker-compose.yaml")))
|
|
85
|
+
detected.push("docker");
|
|
86
|
+
// Infrastructure
|
|
87
|
+
if (existsSync(join(dir, "terraform")) || existsSync(join(dir, "main.tf")))
|
|
88
|
+
detected.push("terraform");
|
|
89
|
+
if (existsSync(join(dir, "pulumi.yaml")))
|
|
90
|
+
detected.push("pulumi");
|
|
91
|
+
return detected;
|
|
92
|
+
}
|
|
93
|
+
function collectPackageDeps(pkgPath) {
|
|
94
|
+
try {
|
|
95
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
96
|
+
return Object.keys({
|
|
97
|
+
...pkg.dependencies,
|
|
98
|
+
...pkg.devDependencies,
|
|
99
|
+
...pkg.peerDependencies,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function readProjectName(pkgPath) {
|
|
107
|
+
try {
|
|
108
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
109
|
+
return typeof pkg.name === "string" ? pkg.name : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
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);
|
|
135
|
+
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);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
// .env files
|
|
150
|
+
for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
|
|
151
|
+
const envPath = join(dir, envFile);
|
|
152
|
+
if (existsSync(envPath)) {
|
|
153
|
+
try {
|
|
154
|
+
for (const key of parseEnvKeys(readFileSync(envPath, "utf8")))
|
|
155
|
+
names.add(key);
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Git remote, CI/CD, deployment config
|
|
161
|
+
for (const name of detectFromGitConfig(dir))
|
|
162
|
+
names.add(name);
|
|
163
|
+
for (const name of detectFromFilesystem(dir))
|
|
164
|
+
names.add(name);
|
|
165
|
+
return { projectName, packageJsonCount, detectedNames: Array.from(names) };
|
|
166
|
+
}
|
|
167
|
+
// ─── Relationship type selection ──────────────────────────────────────────────
|
|
168
|
+
function pickRelationshipType(entityTypeCode) {
|
|
169
|
+
switch (entityTypeCode) {
|
|
170
|
+
case "model":
|
|
171
|
+
return "uses_model";
|
|
172
|
+
case "platform":
|
|
173
|
+
case "platform_component":
|
|
174
|
+
case "skill":
|
|
175
|
+
return "uses";
|
|
176
|
+
default:
|
|
177
|
+
return "depends_on";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ─── Main command ─────────────────────────────────────────────────────────────
|
|
181
|
+
export async function initProject(args) {
|
|
182
|
+
const asJson = parseFlag(args, "--json");
|
|
183
|
+
const dryRun = parseFlag(args, "--dry-run");
|
|
184
|
+
const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
|
|
185
|
+
const dir = resolvePath(dirArg);
|
|
186
|
+
const nameOverride = parseOptionValue(args, "--name");
|
|
187
|
+
const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "service";
|
|
188
|
+
const creds = requireCredentials();
|
|
189
|
+
const mcpOpts = { companyId: creds.companyId };
|
|
190
|
+
if (!asJson)
|
|
191
|
+
console.log(`Scanning ${dir}…`);
|
|
192
|
+
const { projectName, packageJsonCount, detectedNames } = scanProject(dir);
|
|
193
|
+
const displayName = nameOverride ?? projectName;
|
|
194
|
+
const projectSlug = slugify(displayName);
|
|
195
|
+
const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
|
|
196
|
+
if (!asJson) {
|
|
197
|
+
console.log(` Project : ${displayName} (${entityTypeOverride})`);
|
|
198
|
+
console.log(` Packages: ${packageJsonCount} package.json file(s)`);
|
|
199
|
+
console.log(` Detected: ${detectedNames.length} raw names`);
|
|
200
|
+
}
|
|
201
|
+
if (detectedNames.length === 0) {
|
|
202
|
+
if (!asJson)
|
|
203
|
+
console.log("Nothing detected — is this a supported project type?");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Resolve all detected names against the reference library
|
|
207
|
+
if (!asJson)
|
|
208
|
+
console.log("\nResolving against reference library…");
|
|
209
|
+
const allResolveResults = [];
|
|
210
|
+
const BATCH_SIZE = 200;
|
|
211
|
+
for (let i = 0; i < detectedNames.length; i += BATCH_SIZE) {
|
|
212
|
+
const batch = detectedNames.slice(i, i + BATCH_SIZE);
|
|
213
|
+
const raw = await callMcpTool("nexarch_resolve_reference", { names: batch, companyId: creds.companyId }, mcpOpts);
|
|
214
|
+
const data = parseToolText(raw);
|
|
215
|
+
allResolveResults.push(...data.results);
|
|
216
|
+
}
|
|
217
|
+
const resolvedItems = allResolveResults.filter((r) => r.resolved);
|
|
218
|
+
const unresolvedItems = allResolveResults.filter((r) => !r.resolved);
|
|
219
|
+
if (!asJson) {
|
|
220
|
+
console.log(` Resolved : ${resolvedItems.length}/${detectedNames.length}`);
|
|
221
|
+
console.log(` Candidates: ${unresolvedItems.length} unresolved (logged to reference candidates)`);
|
|
222
|
+
}
|
|
223
|
+
if (dryRun) {
|
|
224
|
+
const output = {
|
|
225
|
+
dryRun: true,
|
|
226
|
+
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
|
|
227
|
+
resolved: resolvedItems.map((r) => ({
|
|
228
|
+
input: r.input,
|
|
229
|
+
canonicalName: r.canonicalName,
|
|
230
|
+
entityTypeCode: r.entityTypeCode,
|
|
231
|
+
externalRef: r.canonicalExternalRef,
|
|
232
|
+
})),
|
|
233
|
+
unresolved: unresolvedItems.map((r) => r.input),
|
|
234
|
+
};
|
|
235
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Policy bootstrap
|
|
239
|
+
const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
|
|
240
|
+
const policies = parseToolText(policiesRaw);
|
|
241
|
+
const policyBundleHash = policies.policyBundleHash ?? null;
|
|
242
|
+
const nowIso = new Date().toISOString();
|
|
243
|
+
const agentContext = {
|
|
244
|
+
agentId: "nexarch-cli:init-project",
|
|
245
|
+
agentRunId: `init-project-${Date.now()}`,
|
|
246
|
+
repoRef: dir,
|
|
247
|
+
observedAt: nowIso,
|
|
248
|
+
source: "nexarch-cli",
|
|
249
|
+
model: "n/a",
|
|
250
|
+
provider: "n/a",
|
|
251
|
+
};
|
|
252
|
+
const policyContext = policyBundleHash
|
|
253
|
+
? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
|
|
254
|
+
: undefined;
|
|
255
|
+
// Build entity list — project entity + all resolved reference entities
|
|
256
|
+
const entities = [];
|
|
257
|
+
// The project itself
|
|
258
|
+
entities.push({
|
|
259
|
+
externalKey: projectExternalKey,
|
|
260
|
+
entityTypeCode: entityTypeOverride,
|
|
261
|
+
name: displayName,
|
|
262
|
+
description: `Project initialised from ${dir}`,
|
|
263
|
+
confidence: 1,
|
|
264
|
+
attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
|
|
265
|
+
});
|
|
266
|
+
// Resolved reference entities (deduplicated by canonical external ref)
|
|
267
|
+
const seenRefs = new Set();
|
|
268
|
+
for (const r of resolvedItems) {
|
|
269
|
+
if (!r.canonicalExternalRef || seenRefs.has(r.canonicalExternalRef))
|
|
270
|
+
continue;
|
|
271
|
+
seenRefs.add(r.canonicalExternalRef);
|
|
272
|
+
entities.push({
|
|
273
|
+
externalKey: r.canonicalExternalRef,
|
|
274
|
+
entityTypeCode: r.entityTypeCode,
|
|
275
|
+
...(r.entitySubtypeCode ? { entitySubtypeCode: r.entitySubtypeCode } : {}),
|
|
276
|
+
name: r.canonicalName,
|
|
277
|
+
...(r.description ? { description: r.description } : {}),
|
|
278
|
+
confidence: 0.95,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Build relationships from project entity to each resolved dependency
|
|
282
|
+
const relationships = [];
|
|
283
|
+
const seenRelPairs = new Set();
|
|
284
|
+
for (const r of resolvedItems) {
|
|
285
|
+
if (!r.canonicalExternalRef || !r.entityTypeCode)
|
|
286
|
+
continue;
|
|
287
|
+
const relType = pickRelationshipType(r.entityTypeCode);
|
|
288
|
+
const pairKey = `${relType}::${projectExternalKey}::${r.canonicalExternalRef}`;
|
|
289
|
+
if (seenRelPairs.has(pairKey))
|
|
290
|
+
continue;
|
|
291
|
+
seenRelPairs.add(pairKey);
|
|
292
|
+
relationships.push({
|
|
293
|
+
relationshipTypeCode: relType,
|
|
294
|
+
fromEntityExternalKey: projectExternalKey,
|
|
295
|
+
toEntityExternalKey: r.canonicalExternalRef,
|
|
296
|
+
confidence: 0.9,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (!asJson)
|
|
300
|
+
console.log(`\nWriting to graph…`);
|
|
301
|
+
// Upsert entities
|
|
302
|
+
const entitiesRaw = await callMcpTool("nexarch_upsert_entities", { entities, agentContext, policyContext }, mcpOpts);
|
|
303
|
+
const entitiesResult = parseToolText(entitiesRaw);
|
|
304
|
+
// Upsert relationships
|
|
305
|
+
let relsResult = null;
|
|
306
|
+
if (relationships.length > 0) {
|
|
307
|
+
const relsRaw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
|
|
308
|
+
relsResult = parseToolText(relsRaw);
|
|
309
|
+
}
|
|
310
|
+
const output = {
|
|
311
|
+
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
312
|
+
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
|
|
313
|
+
entities: entitiesResult.summary ?? {},
|
|
314
|
+
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
315
|
+
resolved: resolvedItems.length,
|
|
316
|
+
unresolved: unresolvedItems.length,
|
|
317
|
+
unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
|
|
318
|
+
entityErrors: entitiesResult.errors ?? [],
|
|
319
|
+
relationshipErrors: relsResult?.errors ?? [],
|
|
320
|
+
};
|
|
321
|
+
if (asJson) {
|
|
322
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
323
|
+
if (!output.ok)
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
console.log(`\nDone.`);
|
|
328
|
+
console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
|
|
329
|
+
console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
|
|
330
|
+
if (unresolvedItems.length > 0) {
|
|
331
|
+
console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
|
|
332
|
+
}
|
|
333
|
+
if (output.entityErrors.length > 0) {
|
|
334
|
+
console.log("\nEntity errors:");
|
|
335
|
+
for (const err of output.entityErrors) {
|
|
336
|
+
console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { mcpConfig } from "./commands/mcp-config.js";
|
|
|
10
10
|
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
|
+
import { initProject } from "./commands/init-project.js";
|
|
13
14
|
const [, , command, ...args] = process.argv;
|
|
14
15
|
const commands = {
|
|
15
16
|
login,
|
|
@@ -20,6 +21,7 @@ const commands = {
|
|
|
20
21
|
"mcp-proxy": mcpProxy,
|
|
21
22
|
"init-agent": initAgent,
|
|
22
23
|
"agent-identify": agentIdentify,
|
|
24
|
+
"init-project": initProject,
|
|
23
25
|
};
|
|
24
26
|
async function main() {
|
|
25
27
|
if (command === "agent") {
|
|
@@ -69,6 +71,16 @@ Usage:
|
|
|
69
71
|
[--notes <text>] [--json]
|
|
70
72
|
nexarch agent-identify
|
|
71
73
|
Alias of 'nexarch agent identify'
|
|
74
|
+
nexarch init-project
|
|
75
|
+
Scan a project directory, resolve detected packages/env vars/
|
|
76
|
+
config files against the reference library, write entities and
|
|
77
|
+
relationships to the architecture graph, and log unresolved
|
|
78
|
+
names as reference candidates.
|
|
79
|
+
Options: --dir <path> (default: cwd)
|
|
80
|
+
--name <name> override project name
|
|
81
|
+
--entity-type <code> (default: service)
|
|
82
|
+
--dry-run preview without writing
|
|
83
|
+
--json
|
|
72
84
|
`);
|
|
73
85
|
process.exit(command ? 1 : 0);
|
|
74
86
|
}
|