nexarch 0.1.16 → 0.1.18
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 +164 -39
- 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.18";
|
|
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) {
|
|
@@ -184,12 +263,12 @@ export async function initProject(args) {
|
|
|
184
263
|
const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
|
|
185
264
|
const dir = resolvePath(dirArg);
|
|
186
265
|
const nameOverride = parseOptionValue(args, "--name");
|
|
187
|
-
const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "
|
|
266
|
+
const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "application";
|
|
188
267
|
const creds = requireCredentials();
|
|
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}`;
|
|
@@ -254,12 +333,13 @@ export async function initProject(args) {
|
|
|
254
333
|
: undefined;
|
|
255
334
|
// Build entity list — project entity + all resolved reference entities
|
|
256
335
|
const entities = [];
|
|
257
|
-
// The project itself
|
|
336
|
+
// The project itself — default subtype app_custom_built for application type
|
|
337
|
+
const projectSubtype = entityTypeOverride === "application" ? "app_custom_built" : undefined;
|
|
258
338
|
entities.push({
|
|
259
339
|
externalKey: projectExternalKey,
|
|
260
340
|
entityTypeCode: entityTypeOverride,
|
|
341
|
+
...(projectSubtype ? { entitySubtypeCode: projectSubtype } : {}),
|
|
261
342
|
name: displayName,
|
|
262
|
-
description: `Project initialised from ${dir}`,
|
|
263
343
|
confidence: 1,
|
|
264
344
|
attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
|
|
265
345
|
});
|
|
@@ -336,4 +416,49 @@ export async function initProject(args) {
|
|
|
336
416
|
console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
337
417
|
}
|
|
338
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 — Call nexarch_upsert_entities to enrich the project entity:
|
|
441
|
+
• externalKey: "${projectExternalKey}"
|
|
442
|
+
• name: (proper product/project name from the README)
|
|
443
|
+
• description: (2–4 sentence summary of what it does and why)
|
|
444
|
+
• Preserve existing attributes, add any business context attributes you find.
|
|
445
|
+
• entityTypeCode: "${entityTypeOverride}"${entityTypeOverride === "application" ? '\n • entitySubtypeCode: "app_custom_built"' : ""}
|
|
446
|
+
${subPackages.length > 0 ? `
|
|
447
|
+
STEP 3 — Register each sub-application as its own entity:
|
|
448
|
+
The scanner found ${subPackages.length} workspace package(s). For each one, call
|
|
449
|
+
nexarch_upsert_entities then nexarch_upsert_relationships to wire it to the parent.
|
|
450
|
+
|
|
451
|
+
Sub-packages detected:
|
|
452
|
+
${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")}
|
|
453
|
+
|
|
454
|
+
For each sub-package:
|
|
455
|
+
• Read its own README / package.json description if present
|
|
456
|
+
• entityTypeCode: "application", entitySubtypeCode: "app_custom_built"
|
|
457
|
+
• externalKey: "application:<slug>" (slugify the package name)
|
|
458
|
+
• description: what this specific sub-app does
|
|
459
|
+
• Relationship: fromEntityExternalKey = "<sub-app key>",
|
|
460
|
+
toEntityExternalKey = "${projectExternalKey}",
|
|
461
|
+
relationshipTypeCode = "part_of"
|
|
462
|
+
` : ""}
|
|
463
|
+
Once enrichment is complete, run \`nexarch graph snapshot\` to capture the updated state.`);
|
|
339
464
|
}
|