nexarch 0.1.17 → 0.1.19
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 +169 -37
- package/dist/commands/update-entity.js +84 -0
- package/dist/index.js +13 -1
- 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.19";
|
|
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))
|
|
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
|
|
110
|
+
function readRootPackage(pkgPath) {
|
|
107
111
|
try {
|
|
108
|
-
|
|
109
|
-
return typeof pkg.name === "string" ? pkg.name : null;
|
|
112
|
+
return JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
110
113
|
}
|
|
111
114
|
catch {
|
|
112
|
-
return
|
|
115
|
+
return {};
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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) {
|
|
@@ -189,7 +268,7 @@ export async function initProject(args) {
|
|
|
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}`;
|
|
@@ -261,7 +340,6 @@ export async function initProject(args) {
|
|
|
261
340
|
entityTypeCode: entityTypeOverride,
|
|
262
341
|
...(projectSubtype ? { entitySubtypeCode: projectSubtype } : {}),
|
|
263
342
|
name: displayName,
|
|
264
|
-
description: `Project initialised from ${dir}`,
|
|
265
343
|
confidence: 1,
|
|
266
344
|
attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
|
|
267
345
|
});
|
|
@@ -338,4 +416,58 @@ export async function initProject(args) {
|
|
|
338
416
|
console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
339
417
|
}
|
|
340
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 — Enrich the project entity. Run this command with the description you've written:
|
|
441
|
+
|
|
442
|
+
npx nexarch update-entity \\
|
|
443
|
+
--key "${projectExternalKey}" \\
|
|
444
|
+
--entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' \\\n --subtype "app_custom_built"' : ""} \\
|
|
445
|
+
--name "<proper product name from README>" \\
|
|
446
|
+
--description "<2–4 sentence summary of what it does and why>"
|
|
447
|
+
${subPackages.length > 0 ? `
|
|
448
|
+
STEP 3 — Register each sub-application as its own entity.
|
|
449
|
+
The scanner found ${subPackages.length} workspace package(s). For each one, read its
|
|
450
|
+
README / package.json description, then run:
|
|
451
|
+
|
|
452
|
+
Sub-packages detected:
|
|
453
|
+
${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")}
|
|
454
|
+
|
|
455
|
+
For each sub-package run:
|
|
456
|
+
|
|
457
|
+
npx nexarch update-entity \\
|
|
458
|
+
--key "application:<slugified-package-name>" \\
|
|
459
|
+
--entity-type "application" \\
|
|
460
|
+
--subtype "app_custom_built" \\
|
|
461
|
+
--name "<sub-app name>" \\
|
|
462
|
+
--description "<what this sub-app does>"
|
|
463
|
+
|
|
464
|
+
Then wire it to the parent project:
|
|
465
|
+
|
|
466
|
+
npx nexarch add-relationship \\
|
|
467
|
+
--from "application:<slugified-package-name>" \\
|
|
468
|
+
--to "${projectExternalKey}" \\
|
|
469
|
+
--type "part_of"
|
|
470
|
+
|
|
471
|
+
(Note: add-relationship is coming soon — skip if not yet available)
|
|
472
|
+
` : ""}`);
|
|
341
473
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function parseFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function parseOptionValue(args, option) {
|
|
8
|
+
const idx = args.indexOf(option);
|
|
9
|
+
if (idx === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const value = args[idx + 1];
|
|
12
|
+
if (!value || value.startsWith("--"))
|
|
13
|
+
return null;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function parseToolText(result) {
|
|
17
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
export async function updateEntity(args) {
|
|
21
|
+
const asJson = parseFlag(args, "--json");
|
|
22
|
+
const externalKey = parseOptionValue(args, "--key");
|
|
23
|
+
const name = parseOptionValue(args, "--name");
|
|
24
|
+
const description = parseOptionValue(args, "--description");
|
|
25
|
+
const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
|
|
26
|
+
const entitySubtypeCode = parseOptionValue(args, "--subtype");
|
|
27
|
+
if (!externalKey) {
|
|
28
|
+
console.error("error: --key <externalKey> is required");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (!name && !description) {
|
|
32
|
+
console.error("error: at least one of --name or --description is required");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const creds = requireCredentials();
|
|
36
|
+
const mcpOpts = { companyId: creds.companyId };
|
|
37
|
+
const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
|
|
38
|
+
const policies = parseToolText(policiesRaw);
|
|
39
|
+
const policyBundleHash = policies.policyBundleHash ?? null;
|
|
40
|
+
const nowIso = new Date().toISOString();
|
|
41
|
+
const agentContext = {
|
|
42
|
+
agentId: "nexarch-cli:update-entity",
|
|
43
|
+
agentRunId: `update-entity-${Date.now()}`,
|
|
44
|
+
observedAt: nowIso,
|
|
45
|
+
source: "nexarch-cli",
|
|
46
|
+
model: "n/a",
|
|
47
|
+
provider: "n/a",
|
|
48
|
+
};
|
|
49
|
+
const policyContext = policyBundleHash
|
|
50
|
+
? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
|
|
51
|
+
: undefined;
|
|
52
|
+
const entity = {
|
|
53
|
+
externalKey,
|
|
54
|
+
entityTypeCode,
|
|
55
|
+
confidence: 1,
|
|
56
|
+
};
|
|
57
|
+
if (name)
|
|
58
|
+
entity.name = name;
|
|
59
|
+
if (description)
|
|
60
|
+
entity.description = description;
|
|
61
|
+
if (entitySubtypeCode)
|
|
62
|
+
entity.entitySubtypeCode = entitySubtypeCode;
|
|
63
|
+
const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
|
|
64
|
+
const result = parseToolText(raw);
|
|
65
|
+
if (asJson) {
|
|
66
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
67
|
+
if (Number(result.summary?.failed ?? 0) > 0)
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const succeeded = result.summary?.succeeded ?? 0;
|
|
72
|
+
const failed = result.summary?.failed ?? 0;
|
|
73
|
+
if (failed > 0) {
|
|
74
|
+
console.error(`Failed to update entity: ${externalKey}`);
|
|
75
|
+
for (const err of result.errors ?? []) {
|
|
76
|
+
console.error(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!asJson) {
|
|
82
|
+
console.log(`Updated ${succeeded} entity: ${externalKey}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ 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
13
|
import { initProject } from "./commands/init-project.js";
|
|
14
|
+
import { updateEntity } from "./commands/update-entity.js";
|
|
14
15
|
const [, , command, ...args] = process.argv;
|
|
15
16
|
const commands = {
|
|
16
17
|
login,
|
|
@@ -22,6 +23,7 @@ const commands = {
|
|
|
22
23
|
"init-agent": initAgent,
|
|
23
24
|
"agent-identify": agentIdentify,
|
|
24
25
|
"init-project": initProject,
|
|
26
|
+
"update-entity": updateEntity,
|
|
25
27
|
};
|
|
26
28
|
async function main() {
|
|
27
29
|
if (command === "agent") {
|
|
@@ -78,9 +80,19 @@ Usage:
|
|
|
78
80
|
names as reference candidates.
|
|
79
81
|
Options: --dir <path> (default: cwd)
|
|
80
82
|
--name <name> override project name
|
|
81
|
-
--entity-type <code> (default:
|
|
83
|
+
--entity-type <code> (default: application)
|
|
82
84
|
--dry-run preview without writing
|
|
83
85
|
--json
|
|
86
|
+
nexarch update-entity
|
|
87
|
+
Update the name and/or description of an existing graph entity.
|
|
88
|
+
Use this after init-project to enrich the entity with meaningful
|
|
89
|
+
content from the project README or docs.
|
|
90
|
+
Options: --key <externalKey> (required)
|
|
91
|
+
--name <name>
|
|
92
|
+
--description <text>
|
|
93
|
+
--entity-type <code> (default: application)
|
|
94
|
+
--subtype <code>
|
|
95
|
+
--json
|
|
84
96
|
`);
|
|
85
97
|
process.exit(command ? 1 : 0);
|
|
86
98
|
}
|