nexarch 0.9.4 → 0.9.6
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/add-relationship.js +79 -41
- package/dist/commands/init-project.js +164 -31
- package/dist/commands/policy-audit-submit.js +12 -7
- package/dist/commands/update-entity.js +107 -52
- package/dist/index.js +20 -17
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from "process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { requireCredentials } from "../lib/credentials.js";
|
|
3
4
|
import { callMcpTool } from "../lib/mcp.js";
|
|
4
5
|
function parseFlag(args, flag) {
|
|
@@ -17,11 +18,65 @@ function parseToolText(result) {
|
|
|
17
18
|
const text = result.content?.[0]?.text ?? "{}";
|
|
18
19
|
return JSON.parse(text);
|
|
19
20
|
}
|
|
21
|
+
function parseRelationships(args) {
|
|
22
|
+
const relationshipsFile = parseOptionValue(args, "--relationships-file");
|
|
23
|
+
const relationshipsJson = parseOptionValue(args, "--relationships-json");
|
|
24
|
+
if (relationshipsFile && relationshipsJson) {
|
|
25
|
+
throw new Error("Use only one of --relationships-file or --relationships-json");
|
|
26
|
+
}
|
|
27
|
+
if (relationshipsFile || relationshipsJson) {
|
|
28
|
+
let raw = relationshipsJson;
|
|
29
|
+
if (relationshipsFile) {
|
|
30
|
+
if (!existsSync(relationshipsFile))
|
|
31
|
+
throw new Error(`--relationships-file not found: ${relationshipsFile}`);
|
|
32
|
+
raw = readFileSync(relationshipsFile, "utf8");
|
|
33
|
+
}
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(raw ?? "[]");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(relationshipsFile ? "--relationships-file must contain valid JSON array" : "--relationships-json must be valid JSON array");
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
42
|
+
throw new Error("Relationships input must be a non-empty JSON array");
|
|
43
|
+
}
|
|
44
|
+
return parsed.map((item, index) => {
|
|
45
|
+
const v = (item ?? {});
|
|
46
|
+
const from = String(v.fromEntityExternalKey ?? v.fromEntityRef ?? v.from ?? "").trim();
|
|
47
|
+
const to = String(v.toEntityExternalKey ?? v.toEntityRef ?? v.to ?? "").trim();
|
|
48
|
+
const type = String(v.relationshipTypeCode ?? v.type ?? "").trim();
|
|
49
|
+
const confidence = Number(v.confidence ?? 1);
|
|
50
|
+
if (!from || !to || !type) {
|
|
51
|
+
throw new Error(`Relationship #${index + 1} is missing required fields (from/to/type)`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
fromEntityExternalKey: from,
|
|
55
|
+
toEntityExternalKey: to,
|
|
56
|
+
relationshipTypeCode: type,
|
|
57
|
+
confidence: Number.isFinite(confidence) ? confidence : 1,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const fromKey = parseOptionValue(args, "--from");
|
|
62
|
+
const toKey = parseOptionValue(args, "--to");
|
|
63
|
+
const relType = parseOptionValue(args, "--type");
|
|
64
|
+
if (!fromKey)
|
|
65
|
+
throw new Error("--from <externalKey> is required");
|
|
66
|
+
if (!toKey)
|
|
67
|
+
throw new Error("--to <externalKey> is required");
|
|
68
|
+
if (!relType)
|
|
69
|
+
throw new Error("--type <relationshipTypeCode> is required");
|
|
70
|
+
return [{
|
|
71
|
+
fromEntityExternalKey: fromKey,
|
|
72
|
+
toEntityExternalKey: toKey,
|
|
73
|
+
relationshipTypeCode: relType,
|
|
74
|
+
confidence: 1,
|
|
75
|
+
}];
|
|
76
|
+
}
|
|
20
77
|
async function tryCreateStubEntity(externalKey, mcpOpts, agentContext, policyContext) {
|
|
21
|
-
// Only auto-create global reference entities — company entities must be created explicitly
|
|
22
78
|
if (!externalKey.startsWith("global:"))
|
|
23
79
|
return false;
|
|
24
|
-
// Derive a normalised alias from the external key (e.g. "global:platform:vercel" → "vercel")
|
|
25
80
|
const parts = externalKey.split(":");
|
|
26
81
|
const alias = parts[parts.length - 1];
|
|
27
82
|
const resolveRaw = await callMcpTool("nexarch_resolve_reference", { names: [alias], companyId: mcpOpts.companyId }, mcpOpts);
|
|
@@ -42,21 +97,7 @@ async function tryCreateStubEntity(externalKey, mcpOpts, agentContext, policyCon
|
|
|
42
97
|
}
|
|
43
98
|
export async function addRelationship(args) {
|
|
44
99
|
const asJson = parseFlag(args, "--json");
|
|
45
|
-
const
|
|
46
|
-
const toKey = parseOptionValue(args, "--to");
|
|
47
|
-
const relType = parseOptionValue(args, "--type");
|
|
48
|
-
if (!fromKey) {
|
|
49
|
-
console.error("error: --from <externalKey> is required");
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
if (!toKey) {
|
|
53
|
-
console.error("error: --to <externalKey> is required");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
if (!relType) {
|
|
57
|
-
console.error("error: --type <relationshipTypeCode> is required");
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
100
|
+
const relationships = parseRelationships(args);
|
|
60
101
|
const creds = requireCredentials();
|
|
61
102
|
const mcpOpts = { companyId: creds.companyId };
|
|
62
103
|
const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
|
|
@@ -66,7 +107,7 @@ export async function addRelationship(args) {
|
|
|
66
107
|
const agentContext = {
|
|
67
108
|
agentId: "nexarch-cli:add-relationship",
|
|
68
109
|
agentRunId: `add-relationship-${Date.now()}`,
|
|
69
|
-
repoRef:
|
|
110
|
+
repoRef: relationships[0]?.fromEntityExternalKey ?? "batch",
|
|
70
111
|
observedAt: nowIso,
|
|
71
112
|
source: "nexarch-cli",
|
|
72
113
|
model: "n/a",
|
|
@@ -75,27 +116,18 @@ export async function addRelationship(args) {
|
|
|
75
116
|
const policyContext = policyBundleHash
|
|
76
117
|
? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
|
|
77
118
|
: undefined;
|
|
78
|
-
|
|
79
|
-
relationshipTypeCode: relType,
|
|
80
|
-
fromEntityExternalKey: fromKey,
|
|
81
|
-
toEntityExternalKey: toKey,
|
|
82
|
-
confidence: 1,
|
|
83
|
-
};
|
|
84
|
-
let raw = await callMcpTool("nexarch_upsert_relationships", { relationships: [relationship], agentContext, policyContext }, mcpOpts);
|
|
119
|
+
let raw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
|
|
85
120
|
let result = parseToolText(raw);
|
|
86
|
-
// Auto-create global reference stubs if endpoints are missing, then retry once
|
|
87
121
|
const missingEndpoint = result.errors?.some((e) => e.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND");
|
|
88
122
|
if (missingEndpoint) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (keysToCreate.length === 0) {
|
|
98
|
-
keysToCreate.push(fromKey, toKey);
|
|
123
|
+
const keysToCreate = new Set();
|
|
124
|
+
for (const err of result.errors ?? []) {
|
|
125
|
+
if (err.error !== "RELATIONSHIP_ENDPOINT_NOT_FOUND")
|
|
126
|
+
continue;
|
|
127
|
+
if (err.fromEntityExternalKey)
|
|
128
|
+
keysToCreate.add(err.fromEntityExternalKey);
|
|
129
|
+
if (err.toEntityExternalKey)
|
|
130
|
+
keysToCreate.add(err.toEntityExternalKey);
|
|
99
131
|
}
|
|
100
132
|
let createdAny = false;
|
|
101
133
|
for (const key of keysToCreate) {
|
|
@@ -104,7 +136,7 @@ export async function addRelationship(args) {
|
|
|
104
136
|
createdAny = true;
|
|
105
137
|
}
|
|
106
138
|
if (createdAny) {
|
|
107
|
-
raw = await callMcpTool("nexarch_upsert_relationships", { relationships
|
|
139
|
+
raw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
|
|
108
140
|
result = parseToolText(raw);
|
|
109
141
|
}
|
|
110
142
|
}
|
|
@@ -117,12 +149,12 @@ export async function addRelationship(args) {
|
|
|
117
149
|
const succeeded = result.summary?.succeeded ?? 0;
|
|
118
150
|
const failed = result.summary?.failed ?? 0;
|
|
119
151
|
if (failed > 0) {
|
|
120
|
-
console.error(`Failed to add
|
|
152
|
+
console.error(`Failed to add relationships (${failed}/${relationships.length}).`);
|
|
121
153
|
const missingFrom = new Set();
|
|
122
154
|
const missingTo = new Set();
|
|
123
155
|
for (const err of result.errors ?? []) {
|
|
124
|
-
const fromEndpoint = err.fromEntityExternalKey ??
|
|
125
|
-
const toEndpoint = err.toEntityExternalKey ??
|
|
156
|
+
const fromEndpoint = err.fromEntityExternalKey ?? "(unknown-from)";
|
|
157
|
+
const toEndpoint = err.toEntityExternalKey ?? "(unknown-to)";
|
|
126
158
|
const message = err.message ?? "(no details returned)";
|
|
127
159
|
if (err.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND") {
|
|
128
160
|
if (err.fromEntityExternalKey)
|
|
@@ -146,5 +178,11 @@ export async function addRelationship(args) {
|
|
|
146
178
|
process.exitCode = 1;
|
|
147
179
|
return;
|
|
148
180
|
}
|
|
149
|
-
|
|
181
|
+
if (relationships.length === 1) {
|
|
182
|
+
const r = relationships[0];
|
|
183
|
+
console.log(`Added ${succeeded} relationship: ${r.fromEntityExternalKey} -[${r.relationshipTypeCode}]-> ${r.toEntityExternalKey}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(`Added ${succeeded}/${relationships.length} relationships.`);
|
|
187
|
+
}
|
|
150
188
|
}
|
|
@@ -453,7 +453,29 @@ function classifySubPackage(pkgPath, relativePath) {
|
|
|
453
453
|
return { entityType: "technology_component", subtype: "tech_library" };
|
|
454
454
|
return { entityType: "technology_component", subtype: "tech_library" };
|
|
455
455
|
}
|
|
456
|
-
// apps/*
|
|
456
|
+
// apps/* → deployable application component under the root application.
|
|
457
|
+
if (topDir === "apps") {
|
|
458
|
+
const name = relativePath.toLowerCase();
|
|
459
|
+
if (name.includes("worker"))
|
|
460
|
+
return { entityType: "application_component", subtype: "app_comp_worker" };
|
|
461
|
+
if (name.includes("job"))
|
|
462
|
+
return { entityType: "application_component", subtype: "app_comp_job" };
|
|
463
|
+
if (name.includes("etl"))
|
|
464
|
+
return { entityType: "application_component", subtype: "app_comp_etl" };
|
|
465
|
+
if (name.includes("adapter"))
|
|
466
|
+
return { entityType: "application_component", subtype: "app_comp_adapter" };
|
|
467
|
+
if (name.includes("batch"))
|
|
468
|
+
return { entityType: "application_component", subtype: "app_comp_batch" };
|
|
469
|
+
if (name.includes("pipeline"))
|
|
470
|
+
return { entityType: "application_component", subtype: "app_comp_data_pipeline" };
|
|
471
|
+
if (name.includes("ui") || name.includes("web") || name.includes("front") || name.includes("site") || name.includes("portal") || name.includes("admin")) {
|
|
472
|
+
return { entityType: "application_component", subtype: "app_comp_ui" };
|
|
473
|
+
}
|
|
474
|
+
if (name.includes("api"))
|
|
475
|
+
return { entityType: "application_component", subtype: "app_comp_api" };
|
|
476
|
+
return { entityType: "application_component", subtype: "app_comp_api" };
|
|
477
|
+
}
|
|
478
|
+
// src/* → deployable application; check scripts
|
|
457
479
|
try {
|
|
458
480
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
459
481
|
const scripts = Object.keys(pkg.scripts ?? {});
|
|
@@ -758,6 +780,9 @@ function scanEcosystems(dir) {
|
|
|
758
780
|
}
|
|
759
781
|
return { names, ecosystems };
|
|
760
782
|
}
|
|
783
|
+
function isApplicationLikeEntityType(entityType) {
|
|
784
|
+
return entityType === "application" || entityType === "application_component";
|
|
785
|
+
}
|
|
761
786
|
function scanProject(dir) {
|
|
762
787
|
const names = new Set();
|
|
763
788
|
let projectName = basename(dir);
|
|
@@ -964,12 +989,34 @@ async function promptApplicationChoice(matches, allApps, suggested) {
|
|
|
964
989
|
export async function initProject(args) {
|
|
965
990
|
const asJson = parseFlag(args, "--json");
|
|
966
991
|
const dryRun = parseFlag(args, "--dry-run");
|
|
992
|
+
const profileEnabled = parseFlag(args, "--profile");
|
|
967
993
|
const commandStartedAt = Date.now();
|
|
994
|
+
const profile = {
|
|
995
|
+
enabled: profileEnabled,
|
|
996
|
+
startedAt: new Date(commandStartedAt).toISOString(),
|
|
997
|
+
events: [],
|
|
998
|
+
mcpCalls: [],
|
|
999
|
+
upsertBatches: {
|
|
1000
|
+
entities: [],
|
|
1001
|
+
relationships: [],
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
968
1004
|
const logProgress = (phase, details) => {
|
|
969
|
-
const
|
|
1005
|
+
const atMs = Date.now() - commandStartedAt;
|
|
1006
|
+
if (profileEnabled)
|
|
1007
|
+
profile.events.push({ phase, atMs, details });
|
|
1008
|
+
const elapsed = formatMs(atMs);
|
|
970
1009
|
const line = details ? `[init-project +${elapsed}] ${phase} — ${details}` : `[init-project +${elapsed}] ${phase}`;
|
|
971
1010
|
process.stderr.write(`${line}\n`);
|
|
972
1011
|
};
|
|
1012
|
+
const callMcpProfiled = async (toolName, input, meta) => {
|
|
1013
|
+
const startedAt = Date.now();
|
|
1014
|
+
const result = await callMcpTool(toolName, input, mcpOpts);
|
|
1015
|
+
if (profileEnabled) {
|
|
1016
|
+
profile.mcpCalls.push({ tool: toolName, durationMs: Date.now() - startedAt, meta });
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
1019
|
+
};
|
|
973
1020
|
const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
|
|
974
1021
|
const dir = resolvePath(dirArg);
|
|
975
1022
|
const nameOverride = parseOptionValue(args, "--name");
|
|
@@ -1030,7 +1077,7 @@ export async function initProject(args) {
|
|
|
1030
1077
|
for (let i = 0; i < detectedNames.length; i += BATCH_SIZE) {
|
|
1031
1078
|
const batch = detectedNames.slice(i, i + BATCH_SIZE);
|
|
1032
1079
|
logProgress("resolve.batch", `${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(detectedNames.length / BATCH_SIZE)} size=${batch.length}`);
|
|
1033
|
-
const raw = await
|
|
1080
|
+
const raw = await callMcpProfiled("nexarch_resolve_reference", { names: batch, companyId: creds.companyId }, { batchSize: batch.length });
|
|
1034
1081
|
const data = parseToolText(raw);
|
|
1035
1082
|
allResolveResults.push(...data.results);
|
|
1036
1083
|
}
|
|
@@ -1070,7 +1117,7 @@ export async function initProject(args) {
|
|
|
1070
1117
|
return;
|
|
1071
1118
|
}
|
|
1072
1119
|
logProgress("preflight.onboarding.check");
|
|
1073
|
-
const onboardingRaw = await
|
|
1120
|
+
const onboardingRaw = await callMcpProfiled("nexarch_get_company_onboarding", {});
|
|
1074
1121
|
const onboarding = parseToolText(onboardingRaw);
|
|
1075
1122
|
if (onboarding.isComplete !== true) {
|
|
1076
1123
|
const message = "Company onboarding is incomplete. Complete onboarding before running init-project.";
|
|
@@ -1083,7 +1130,7 @@ export async function initProject(args) {
|
|
|
1083
1130
|
}
|
|
1084
1131
|
// Policy bootstrap
|
|
1085
1132
|
logProgress("preflight.policies.check");
|
|
1086
|
-
const policiesRaw = await
|
|
1133
|
+
const policiesRaw = await callMcpProfiled("nexarch_get_applied_policies", {});
|
|
1087
1134
|
const policies = parseToolText(policiesRaw);
|
|
1088
1135
|
const policyBundleHash = policies.policyBundleHash ?? null;
|
|
1089
1136
|
if (!policyBundleHash) {
|
|
@@ -1108,7 +1155,7 @@ export async function initProject(args) {
|
|
|
1108
1155
|
console.log(`\nUsing --application-ref target: ${projectExternalKey}`);
|
|
1109
1156
|
}
|
|
1110
1157
|
else {
|
|
1111
|
-
const appsRaw = await
|
|
1158
|
+
const appsRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, { entityTypeCode: "application", limit: 500 });
|
|
1112
1159
|
const appsData = parseToolText(appsRaw);
|
|
1113
1160
|
const apps = (appsData.entities ?? []).filter((e) => (e.entityRef ?? e.externalKey));
|
|
1114
1161
|
if (apps.length > 0) {
|
|
@@ -1125,18 +1172,40 @@ export async function initProject(args) {
|
|
|
1125
1172
|
console.log(`\nAuto-mapped to existing application: ${suggested.name} (${projectExternalKey})`);
|
|
1126
1173
|
}
|
|
1127
1174
|
else if (!interactiveAllowed) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1175
|
+
const message = "Application mapping requires explicit choice in non-interactive mode. Pass --application-ref <entityRef> or --create-application (or run interactively).";
|
|
1176
|
+
const existingApplications = apps.slice(0, 25).map((a) => ({
|
|
1177
|
+
entityRef: a.entityRef ?? a.externalKey,
|
|
1178
|
+
name: a.name,
|
|
1179
|
+
entityTypeCode: a.entityTypeCode,
|
|
1180
|
+
}));
|
|
1181
|
+
if (asJson) {
|
|
1182
|
+
process.stdout.write(`${JSON.stringify({
|
|
1183
|
+
ok: true,
|
|
1184
|
+
status: "input_required",
|
|
1185
|
+
code: "APPLICATION_MAPPING_REQUIRED",
|
|
1186
|
+
message,
|
|
1187
|
+
suggested: suggested ?? null,
|
|
1188
|
+
candidates: matches.slice(0, 10),
|
|
1189
|
+
existingApplications,
|
|
1190
|
+
requiredInput: {
|
|
1191
|
+
applicationRefOption: "--application-ref <entityRef>",
|
|
1192
|
+
createOption: "--create-application",
|
|
1193
|
+
},
|
|
1194
|
+
}, null, 2)}\n`);
|
|
1195
|
+
return;
|
|
1130
1196
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1197
|
+
console.log("\nInput required before continuing:");
|
|
1198
|
+
console.log(` ${message}`);
|
|
1199
|
+
if (suggested) {
|
|
1200
|
+
console.log(` Suggested: ${suggested.name} (${suggested.entityRef}) score=${suggested.score.toFixed(2)}`);
|
|
1201
|
+
}
|
|
1202
|
+
if (existingApplications.length > 0) {
|
|
1203
|
+
console.log(" Existing applications:");
|
|
1204
|
+
for (const app of existingApplications) {
|
|
1205
|
+
console.log(` - ${app.name} (${app.entityRef})`);
|
|
1137
1206
|
}
|
|
1138
|
-
throw new Error(message);
|
|
1139
1207
|
}
|
|
1208
|
+
return;
|
|
1140
1209
|
}
|
|
1141
1210
|
else {
|
|
1142
1211
|
const chosen = await promptApplicationChoice(matches, apps, highConfidence ? suggested : null);
|
|
@@ -1255,10 +1324,15 @@ export async function initProject(args) {
|
|
|
1255
1324
|
// - Sub-packages that are apps → part_of the top-level project
|
|
1256
1325
|
const relationships = [];
|
|
1257
1326
|
const seenRelPairs = new Set();
|
|
1327
|
+
let relationshipAddAttempts = 0;
|
|
1328
|
+
let relationshipSkippedAsDuplicate = 0;
|
|
1258
1329
|
function addRel(type, from, to, confidence = 0.9, attributes) {
|
|
1330
|
+
relationshipAddAttempts += 1;
|
|
1259
1331
|
const key = `${type}::${from}::${to}`;
|
|
1260
|
-
if (seenRelPairs.has(key))
|
|
1332
|
+
if (seenRelPairs.has(key)) {
|
|
1333
|
+
relationshipSkippedAsDuplicate += 1;
|
|
1261
1334
|
return;
|
|
1335
|
+
}
|
|
1262
1336
|
seenRelPairs.add(key);
|
|
1263
1337
|
relationships.push({ relationshipTypeCode: type, fromEntityExternalKey: from, toEntityExternalKey: to, confidence, attributes });
|
|
1264
1338
|
}
|
|
@@ -1281,7 +1355,7 @@ export async function initProject(args) {
|
|
|
1281
1355
|
continue;
|
|
1282
1356
|
seenSubKeys.add(`rel:${sp.externalKey}`);
|
|
1283
1357
|
// Apps are part_of the top-level project
|
|
1284
|
-
if (sp.entityType === "
|
|
1358
|
+
if (sp.entityType === "application_component") {
|
|
1285
1359
|
addRel("part_of", sp.externalKey, projectExternalKey);
|
|
1286
1360
|
}
|
|
1287
1361
|
// Wire each sub-package's resolved deps to itself
|
|
@@ -1299,7 +1373,7 @@ export async function initProject(args) {
|
|
|
1299
1373
|
// Company org is accountable_for the top-level project entity
|
|
1300
1374
|
let orgExternalKey = `organisation:${normalizeToken(creds.companyId)}`;
|
|
1301
1375
|
try {
|
|
1302
|
-
const orgRaw = await
|
|
1376
|
+
const orgRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "organisation", status: "active", limit: 1, companyId: creds.companyId }, { entityTypeCode: "organisation", limit: 1 });
|
|
1303
1377
|
const orgData = parseToolText(orgRaw);
|
|
1304
1378
|
const org = (orgData.entities ?? [])[0];
|
|
1305
1379
|
if (org?.entityRef || org?.externalKey)
|
|
@@ -1311,7 +1385,7 @@ export async function initProject(args) {
|
|
|
1311
1385
|
addRel("accountable_for", orgExternalKey, projectExternalKey, 1);
|
|
1312
1386
|
// Also accountable_for any sub-package applications
|
|
1313
1387
|
for (const sp of subPackages) {
|
|
1314
|
-
if (sp.externalKey && sp.entityType
|
|
1388
|
+
if (sp.externalKey && isApplicationLikeEntityType(sp.entityType)) {
|
|
1315
1389
|
addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
|
|
1316
1390
|
}
|
|
1317
1391
|
}
|
|
@@ -1331,8 +1405,18 @@ export async function initProject(args) {
|
|
|
1331
1405
|
for (let i = 0; i < entityChunks.length; i += 1) {
|
|
1332
1406
|
const chunk = entityChunks[i];
|
|
1333
1407
|
logProgress("upsert.entities.batch.start", `${i + 1}/${entityChunks.length} size=${chunk.length}`);
|
|
1334
|
-
const
|
|
1408
|
+
const entityBatchStartedAt = Date.now();
|
|
1409
|
+
const entitiesRaw = await callMcpProfiled("nexarch_upsert_entities", { entities: chunk, agentContext, policyContext }, { batchIndex: i + 1, totalBatches: entityChunks.length, batchSize: chunk.length });
|
|
1335
1410
|
const chunkResult = parseToolText(entitiesRaw);
|
|
1411
|
+
if (profileEnabled) {
|
|
1412
|
+
profile.upsertBatches.entities.push({
|
|
1413
|
+
index: i + 1,
|
|
1414
|
+
size: chunk.length,
|
|
1415
|
+
durationMs: Date.now() - entityBatchStartedAt,
|
|
1416
|
+
succeeded: Number(chunkResult.summary?.succeeded ?? 0),
|
|
1417
|
+
failed: Number(chunkResult.summary?.failed ?? 0),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1336
1420
|
entitiesResult.summary.requested = Number(entitiesResult.summary.requested ?? 0) + Number(chunkResult.summary?.requested ?? chunk.length);
|
|
1337
1421
|
entitiesResult.summary.succeeded = Number(entitiesResult.summary.succeeded ?? 0) + Number(chunkResult.summary?.succeeded ?? 0);
|
|
1338
1422
|
entitiesResult.summary.failed = Number(entitiesResult.summary.failed ?? 0) + Number(chunkResult.summary?.failed ?? 0);
|
|
@@ -1350,8 +1434,18 @@ export async function initProject(args) {
|
|
|
1350
1434
|
for (let i = 0; i < relationshipChunks.length; i += 1) {
|
|
1351
1435
|
const chunk = relationshipChunks[i];
|
|
1352
1436
|
logProgress("upsert.relationships.batch.start", `${i + 1}/${relationshipChunks.length} size=${chunk.length}`);
|
|
1353
|
-
const
|
|
1437
|
+
const relBatchStartedAt = Date.now();
|
|
1438
|
+
const relsRaw = await callMcpProfiled("nexarch_upsert_relationships", { relationships: chunk, agentContext, policyContext }, { batchIndex: i + 1, totalBatches: relationshipChunks.length, batchSize: chunk.length });
|
|
1354
1439
|
const chunkResult = parseToolText(relsRaw);
|
|
1440
|
+
if (profileEnabled) {
|
|
1441
|
+
profile.upsertBatches.relationships.push({
|
|
1442
|
+
index: i + 1,
|
|
1443
|
+
size: chunk.length,
|
|
1444
|
+
durationMs: Date.now() - relBatchStartedAt,
|
|
1445
|
+
succeeded: Number(chunkResult.summary?.succeeded ?? 0),
|
|
1446
|
+
failed: Number(chunkResult.summary?.failed ?? 0),
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1355
1449
|
relsResult.summary.requested = Number(relsResult.summary.requested ?? 0) + Number(chunkResult.summary?.requested ?? chunk.length);
|
|
1356
1450
|
relsResult.summary.succeeded = Number(relsResult.summary.succeeded ?? 0) + Number(chunkResult.summary?.succeeded ?? 0);
|
|
1357
1451
|
relsResult.summary.failed = Number(relsResult.summary.failed ?? 0) + Number(chunkResult.summary?.failed ?? 0);
|
|
@@ -1364,6 +1458,7 @@ export async function initProject(args) {
|
|
|
1364
1458
|
// Build structured enrichment task (included in JSON output and printed in human mode)
|
|
1365
1459
|
const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
|
|
1366
1460
|
.filter((f) => existsSync(join(dir, f)));
|
|
1461
|
+
const preWiredRelationshipKeys = new Set(relationships.map((rel) => `${rel.relationshipTypeCode}::${rel.fromEntityExternalKey}::${rel.toEntityExternalKey}`));
|
|
1367
1462
|
function buildEnrichmentInstructions() {
|
|
1368
1463
|
const ecosystemLabel = detectedEcosystems.length > 0
|
|
1369
1464
|
? detectedEcosystems.join(", ")
|
|
@@ -1389,10 +1484,14 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
|
|
|
1389
1484
|
then choose the correct entity type and subtype before running update-entity:
|
|
1390
1485
|
|
|
1391
1486
|
CLASSIFICATION GUIDE — pick the best fit:
|
|
1392
|
-
|
|
1393
|
-
→ --entity-type application --subtype
|
|
1394
|
-
Deployable
|
|
1395
|
-
→ --entity-type
|
|
1487
|
+
Root project / top-level product application
|
|
1488
|
+
→ --entity-type application --subtype app_custom_built
|
|
1489
|
+
Deployable sub-app under apps/* with UI
|
|
1490
|
+
→ --entity-type application_component --subtype app_comp_ui
|
|
1491
|
+
Deployable sub-app under apps/* exposing API/backend service
|
|
1492
|
+
→ --entity-type application_component --subtype app_comp_api
|
|
1493
|
+
Deployable background service, worker, job, ETL, or data pipeline under apps/*
|
|
1494
|
+
→ --entity-type application_component --subtype app_comp_worker / app_comp_job / app_comp_etl / app_comp_data_pipeline
|
|
1396
1495
|
Shared internal library or package (imported by other packages, not deployed on its own)
|
|
1397
1496
|
→ --entity-type technology_component --subtype tech_library
|
|
1398
1497
|
Shared UI component library
|
|
@@ -1425,7 +1524,7 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
|
|
|
1425
1524
|
|
|
1426
1525
|
⚠️ DIRECTION MATTERS — wire relationships as follows:
|
|
1427
1526
|
|
|
1428
|
-
Deployable sub-apps (apps/*) are PART OF the parent — child points to parent:
|
|
1527
|
+
Deployable sub-apps (apps/*) are PART OF the parent application — child points to parent:
|
|
1429
1528
|
npx nexarch add-relationship \\
|
|
1430
1529
|
--from "<sub-app-key>" \\
|
|
1431
1530
|
--to "${projectExternalKey}" \\
|
|
@@ -1440,7 +1539,11 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
|
|
|
1440
1539
|
(FROM = ${projectExternalKey}, TO = the library)
|
|
1441
1540
|
|
|
1442
1541
|
⚠️ WIRE DEPENDENCIES TO THE SUB-APP THAT DECLARES THEM, NOT TO THE PARENT.
|
|
1443
|
-
|
|
1542
|
+
|
|
1543
|
+
IMPORTANT: init-project has already written baseline dependency relationships from manifests.
|
|
1544
|
+
Do NOT re-run add-relationship for entries listed as pre-wired below unless one of these is true:
|
|
1545
|
+
• you changed the target entity key (e.g. you replaced a temporary key), or
|
|
1546
|
+
• you are intentionally adding/changing relationship attributes beyond the scanned defaults.
|
|
1444
1547
|
|
|
1445
1548
|
RELATIONSHIP TYPE RULES:
|
|
1446
1549
|
application → technology_component : --type "depends_on"
|
|
@@ -1448,7 +1551,7 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
|
|
|
1448
1551
|
any → platform / platform_component : --type "runs_on" (NOT uses or depends_on)
|
|
1449
1552
|
any → model : --type "uses_model"
|
|
1450
1553
|
|
|
1451
|
-
Pre-resolved dependencies per sub-package (
|
|
1554
|
+
Pre-resolved dependencies per sub-package (already wired by init-project; review only):
|
|
1452
1555
|
${subPackages.map((sp) => {
|
|
1453
1556
|
const resolved = sp.depSpecs
|
|
1454
1557
|
.map((d) => resolvedByInput.get(d.name))
|
|
@@ -1457,9 +1560,11 @@ ${subPackages.map((sp) => {
|
|
|
1457
1560
|
return ` • ${sp.name}: (no pre-resolved deps — check package.json manually)`;
|
|
1458
1561
|
const lines = resolved.map((r) => {
|
|
1459
1562
|
const relType = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1460
|
-
|
|
1563
|
+
const key = `${relType}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1564
|
+
const status = preWiredRelationshipKeys.has(key) ? "[pre-wired]" : "[not pre-wired]";
|
|
1565
|
+
return ` ${status} --from "${sp.externalKey}" --to "${r.canonicalExternalRef}" --type "${relType}" # ${r.canonicalName}`;
|
|
1461
1566
|
});
|
|
1462
|
-
return ` • ${sp.name}
|
|
1567
|
+
return ` • ${sp.name}:\n${lines.join("\n")}`;
|
|
1463
1568
|
}).join("\n\n")}
|
|
1464
1569
|
`;
|
|
1465
1570
|
const adrStepNumber = subPackages.length > 0 ? "STEP 4" : "STEP 3";
|
|
@@ -1586,7 +1691,17 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1586
1691
|
const resolvedDeps = sp.depSpecs
|
|
1587
1692
|
.map((d) => resolvedByInput.get(d.name))
|
|
1588
1693
|
.filter((r) => !!r?.canonicalExternalRef)
|
|
1589
|
-
.map((r) =>
|
|
1694
|
+
.map((r) => {
|
|
1695
|
+
const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1696
|
+
const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1697
|
+
return {
|
|
1698
|
+
canonicalExternalRef: r.canonicalExternalRef,
|
|
1699
|
+
canonicalName: r.canonicalName,
|
|
1700
|
+
entityTypeCode: r.entityTypeCode,
|
|
1701
|
+
relationshipTypeCode,
|
|
1702
|
+
preWired: preWiredRelationshipKeys.has(relationshipKey),
|
|
1703
|
+
};
|
|
1704
|
+
});
|
|
1590
1705
|
return {
|
|
1591
1706
|
name: sp.name,
|
|
1592
1707
|
relativePath: sp.relativePath,
|
|
@@ -1603,12 +1718,27 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1603
1718
|
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
1604
1719
|
entities: entitiesResult.summary ?? {},
|
|
1605
1720
|
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
1721
|
+
metrics: {
|
|
1722
|
+
entitiesSubmitted: entities.length,
|
|
1723
|
+
relationshipsSubmitted: relationships.length,
|
|
1724
|
+
relationshipAddAttempts,
|
|
1725
|
+
relationshipsSkippedAsDuplicate: relationshipSkippedAsDuplicate,
|
|
1726
|
+
},
|
|
1606
1727
|
resolved: resolvedItems.length,
|
|
1607
1728
|
unresolved: unresolvedItems.length,
|
|
1608
1729
|
unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
|
|
1609
1730
|
entityErrors: entitiesResult.errors ?? [],
|
|
1610
1731
|
relationshipErrors: relsResult?.errors ?? [],
|
|
1611
1732
|
enrichmentTask,
|
|
1733
|
+
profile: profileEnabled
|
|
1734
|
+
? {
|
|
1735
|
+
startedAt: profile.startedAt,
|
|
1736
|
+
totalDurationMs: Date.now() - commandStartedAt,
|
|
1737
|
+
events: profile.events,
|
|
1738
|
+
mcpCalls: profile.mcpCalls,
|
|
1739
|
+
upsertBatches: profile.upsertBatches,
|
|
1740
|
+
}
|
|
1741
|
+
: undefined,
|
|
1612
1742
|
};
|
|
1613
1743
|
if (asJson) {
|
|
1614
1744
|
logProgress("complete", `ok=${output.ok}`);
|
|
@@ -1620,6 +1750,9 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1620
1750
|
console.log(`\nDone.`);
|
|
1621
1751
|
console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
|
|
1622
1752
|
console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
|
|
1753
|
+
if (output.metrics.relationshipsSkippedAsDuplicate > 0) {
|
|
1754
|
+
console.log(` Deduped rels : ${output.metrics.relationshipsSkippedAsDuplicate} skipped as duplicates before upsert`);
|
|
1755
|
+
}
|
|
1623
1756
|
if (unresolvedItems.length > 0) {
|
|
1624
1757
|
console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
|
|
1625
1758
|
}
|
|
@@ -81,26 +81,31 @@ function parseFindings(args) {
|
|
|
81
81
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
82
82
|
throw new Error(findingsFile ? "--findings-file must contain a non-empty JSON array" : "--findings-json must be a non-empty JSON array");
|
|
83
83
|
}
|
|
84
|
-
return parsed.map((item) => {
|
|
84
|
+
return parsed.map((item, index) => {
|
|
85
85
|
const value = item;
|
|
86
|
-
const policyControlId = String(value.policyControlId ?? value.controlId ?? "").trim();
|
|
87
|
-
const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? "").trim();
|
|
88
|
-
const result = String(value.result ?? "").toLowerCase();
|
|
86
|
+
const policyControlId = String(value.policyControlId ?? value.controlId ?? value.policy_control_id ?? "").trim();
|
|
87
|
+
const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? value.policy_rule_id ?? "").trim();
|
|
88
|
+
const result = String(value.result ?? value.status ?? "").toLowerCase();
|
|
89
89
|
if (!policyControlId || !policyRuleId || !result) {
|
|
90
|
-
throw new Error(
|
|
90
|
+
throw new Error(`Finding #${index + 1} is missing required fields. Expected keys: policyControlId (or controlId/policy_control_id), policyRuleId (or ruleId/policy_rule_id), and result (or status). Example: [{\"policyControlId\":\"<uuid>\",\"policyRuleId\":\"<uuid>\",\"result\":\"pass\",\"rationale\":\"...\"}]`);
|
|
91
91
|
}
|
|
92
92
|
if (result !== "pass" && result !== "partial" && result !== "fail") {
|
|
93
|
-
throw new Error(`Invalid finding result '${String(value.result)}'. Use pass|partial|fail.`);
|
|
93
|
+
throw new Error(`Invalid finding result '${String(value.result ?? value.status)}' at finding #${index + 1}. Use pass|partial|fail.`);
|
|
94
94
|
}
|
|
95
95
|
const rationale = value.rationale
|
|
96
96
|
? String(value.rationale)
|
|
97
97
|
: [value.summary, value.evidence].filter(Boolean).map(String).join("\n\n");
|
|
98
|
+
const missingRequirements = Array.isArray(value.missingRequirements)
|
|
99
|
+
? value.missingRequirements.map(String)
|
|
100
|
+
: Array.isArray(value.missing_requirements)
|
|
101
|
+
? value.missing_requirements.map(String)
|
|
102
|
+
: undefined;
|
|
98
103
|
return {
|
|
99
104
|
policyControlId,
|
|
100
105
|
policyRuleId,
|
|
101
106
|
result,
|
|
102
107
|
...(rationale ? { rationale } : {}),
|
|
103
|
-
...(
|
|
108
|
+
...(missingRequirements ? { missingRequirements } : {}),
|
|
104
109
|
};
|
|
105
110
|
});
|
|
106
111
|
}
|
|
@@ -22,48 +22,124 @@ function parseAttributesInput(jsonText, filePath) {
|
|
|
22
22
|
if (!jsonText && !filePath)
|
|
23
23
|
return null;
|
|
24
24
|
if (jsonText && filePath) {
|
|
25
|
-
|
|
26
|
-
process.exit(1);
|
|
25
|
+
throw new Error("Use only one of --attributes-json or --attributes-file");
|
|
27
26
|
}
|
|
28
27
|
let raw = jsonText;
|
|
29
28
|
if (filePath) {
|
|
30
29
|
if (!existsSync(filePath)) {
|
|
31
|
-
|
|
32
|
-
process.exit(1);
|
|
30
|
+
throw new Error(`--attributes-file not found: ${filePath}`);
|
|
33
31
|
}
|
|
34
32
|
raw = readFileSync(filePath, "utf8");
|
|
35
33
|
}
|
|
36
34
|
try {
|
|
37
35
|
const parsed = JSON.parse(raw ?? "{}");
|
|
38
36
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
39
|
-
|
|
40
|
-
process.exit(1);
|
|
37
|
+
throw new Error("Attributes input must be a JSON object");
|
|
41
38
|
}
|
|
42
39
|
return parsed;
|
|
43
40
|
}
|
|
44
41
|
catch (error) {
|
|
45
|
-
|
|
46
|
-
process.exit(1);
|
|
42
|
+
throw new Error(`Invalid attributes JSON (${error instanceof Error ? error.message : "parse failed"})`);
|
|
47
43
|
}
|
|
48
44
|
}
|
|
45
|
+
function parseEntityBatch(args) {
|
|
46
|
+
const entitiesFile = parseOptionValue(args, "--entities-file");
|
|
47
|
+
const entitiesJson = parseOptionValue(args, "--entities-json");
|
|
48
|
+
if (!entitiesFile && !entitiesJson)
|
|
49
|
+
return null;
|
|
50
|
+
if (entitiesFile && entitiesJson)
|
|
51
|
+
throw new Error("Use only one of --entities-file or --entities-json");
|
|
52
|
+
let raw = entitiesJson;
|
|
53
|
+
if (entitiesFile) {
|
|
54
|
+
if (!existsSync(entitiesFile))
|
|
55
|
+
throw new Error(`--entities-file not found: ${entitiesFile}`);
|
|
56
|
+
raw = readFileSync(entitiesFile, "utf8");
|
|
57
|
+
}
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(raw ?? "[]");
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(entitiesFile ? "--entities-file must contain valid JSON array" : "--entities-json must be valid JSON array");
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
66
|
+
throw new Error("Entities input must be a non-empty JSON array");
|
|
67
|
+
}
|
|
68
|
+
return parsed.map((item, index) => {
|
|
69
|
+
const v = (item ?? {});
|
|
70
|
+
const externalKey = String(v.externalKey ?? v.key ?? "").trim();
|
|
71
|
+
if (!externalKey)
|
|
72
|
+
throw new Error(`Entity #${index + 1} is missing externalKey/key`);
|
|
73
|
+
const entityTypeCode = String(v.entityTypeCode ?? v.entity_type_code ?? "application").trim() || "application";
|
|
74
|
+
const entitySubtypeCode = String(v.entitySubtypeCode ?? v.entity_subtype_code ?? "").trim();
|
|
75
|
+
const out = {
|
|
76
|
+
externalKey,
|
|
77
|
+
entityTypeCode,
|
|
78
|
+
confidence: 1,
|
|
79
|
+
};
|
|
80
|
+
if (typeof v.name === "string" && v.name.trim())
|
|
81
|
+
out.name = v.name.trim();
|
|
82
|
+
if (typeof v.description === "string" && v.description.trim())
|
|
83
|
+
out.description = v.description.trim();
|
|
84
|
+
if (entitySubtypeCode)
|
|
85
|
+
out.entitySubtypeCode = entitySubtypeCode;
|
|
86
|
+
const attrs = {
|
|
87
|
+
...(v.attributes && typeof v.attributes === "object" && !Array.isArray(v.attributes) ? v.attributes : {}),
|
|
88
|
+
};
|
|
89
|
+
if (typeof v.icon === "string" && v.icon.trim()) {
|
|
90
|
+
attrs.application_icon = { provider: "lucide", name: v.icon.trim() };
|
|
91
|
+
}
|
|
92
|
+
if (Object.keys(attrs).length > 0)
|
|
93
|
+
out.attributes = attrs;
|
|
94
|
+
if (!out.name && !out.description && !out.attributes) {
|
|
95
|
+
throw new Error(`Entity #${index + 1} must include at least one of name, description, icon, or attributes`);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
49
100
|
export async function updateEntity(args) {
|
|
50
101
|
const asJson = parseFlag(args, "--json");
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const entitySubtypeCode = parseOptionValue(args, "--subtype");
|
|
56
|
-
const iconName = parseOptionValue(args, "--icon");
|
|
57
|
-
const attributesJson = parseOptionValue(args, "--attributes-json");
|
|
58
|
-
const attributesFile = parseOptionValue(args, "--attributes-file");
|
|
59
|
-
if (!externalKey) {
|
|
60
|
-
console.error("error: --key <externalKey> is required");
|
|
61
|
-
process.exit(1);
|
|
102
|
+
const batchEntities = parseEntityBatch(args);
|
|
103
|
+
let entities;
|
|
104
|
+
if (batchEntities) {
|
|
105
|
+
entities = batchEntities;
|
|
62
106
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
else {
|
|
108
|
+
const externalKey = parseOptionValue(args, "--key");
|
|
109
|
+
const name = parseOptionValue(args, "--name");
|
|
110
|
+
const description = parseOptionValue(args, "--description");
|
|
111
|
+
const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
|
|
112
|
+
const entitySubtypeCode = parseOptionValue(args, "--subtype");
|
|
113
|
+
const iconName = parseOptionValue(args, "--icon");
|
|
114
|
+
const attributesJson = parseOptionValue(args, "--attributes-json");
|
|
115
|
+
const attributesFile = parseOptionValue(args, "--attributes-file");
|
|
116
|
+
if (!externalKey)
|
|
117
|
+
throw new Error("--key <externalKey> is required (or use --entities-file/--entities-json)");
|
|
118
|
+
const explicitAttributes = parseAttributesInput(attributesJson, attributesFile);
|
|
119
|
+
if (!name && !description && !iconName && !explicitAttributes) {
|
|
120
|
+
throw new Error("Provide at least one of --name, --description, --icon, --attributes-json, or --attributes-file");
|
|
121
|
+
}
|
|
122
|
+
const entity = {
|
|
123
|
+
externalKey,
|
|
124
|
+
entityTypeCode,
|
|
125
|
+
confidence: 1,
|
|
126
|
+
};
|
|
127
|
+
if (name)
|
|
128
|
+
entity.name = name;
|
|
129
|
+
if (description)
|
|
130
|
+
entity.description = description;
|
|
131
|
+
if (entitySubtypeCode)
|
|
132
|
+
entity.entitySubtypeCode = entitySubtypeCode;
|
|
133
|
+
const attributes = {
|
|
134
|
+
...(explicitAttributes ?? {}),
|
|
135
|
+
};
|
|
136
|
+
if (iconName) {
|
|
137
|
+
attributes.application_icon = { provider: "lucide", name: iconName };
|
|
138
|
+
}
|
|
139
|
+
if (Object.keys(attributes).length > 0) {
|
|
140
|
+
entity.attributes = attributes;
|
|
141
|
+
}
|
|
142
|
+
entities = [entity];
|
|
67
143
|
}
|
|
68
144
|
const creds = requireCredentials();
|
|
69
145
|
const mcpOpts = { companyId: creds.companyId };
|
|
@@ -74,7 +150,7 @@ export async function updateEntity(args) {
|
|
|
74
150
|
const agentContext = {
|
|
75
151
|
agentId: "nexarch-cli:update-entity",
|
|
76
152
|
agentRunId: `update-entity-${Date.now()}`,
|
|
77
|
-
repoRef: externalKey,
|
|
153
|
+
repoRef: String(entities[0]?.externalKey ?? "batch"),
|
|
78
154
|
observedAt: nowIso,
|
|
79
155
|
source: "nexarch-cli",
|
|
80
156
|
model: "n/a",
|
|
@@ -83,31 +159,7 @@ export async function updateEntity(args) {
|
|
|
83
159
|
const policyContext = policyBundleHash
|
|
84
160
|
? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
|
|
85
161
|
: undefined;
|
|
86
|
-
const
|
|
87
|
-
externalKey,
|
|
88
|
-
entityTypeCode,
|
|
89
|
-
confidence: 1,
|
|
90
|
-
};
|
|
91
|
-
if (name)
|
|
92
|
-
entity.name = name;
|
|
93
|
-
if (description)
|
|
94
|
-
entity.description = description;
|
|
95
|
-
if (entitySubtypeCode)
|
|
96
|
-
entity.entitySubtypeCode = entitySubtypeCode;
|
|
97
|
-
const attributes = {
|
|
98
|
-
...(explicitAttributes ?? {}),
|
|
99
|
-
};
|
|
100
|
-
// Convenience sugar; still generic because user can fully control payload via --attributes-json/file.
|
|
101
|
-
if (iconName) {
|
|
102
|
-
attributes.application_icon = {
|
|
103
|
-
provider: "lucide",
|
|
104
|
-
name: iconName,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (Object.keys(attributes).length > 0) {
|
|
108
|
-
entity.attributes = attributes;
|
|
109
|
-
}
|
|
110
|
-
const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
|
|
162
|
+
const raw = await callMcpTool("nexarch_upsert_entities", { entities, agentContext, policyContext }, mcpOpts);
|
|
111
163
|
const result = parseToolText(raw);
|
|
112
164
|
if (asJson) {
|
|
113
165
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -118,14 +170,17 @@ export async function updateEntity(args) {
|
|
|
118
170
|
const succeeded = result.summary?.succeeded ?? 0;
|
|
119
171
|
const failed = result.summary?.failed ?? 0;
|
|
120
172
|
if (failed > 0) {
|
|
121
|
-
console.error(`Failed to update
|
|
173
|
+
console.error(`Failed to update entities (${failed}/${entities.length}).`);
|
|
122
174
|
for (const err of result.errors ?? []) {
|
|
123
175
|
console.error(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
124
176
|
}
|
|
125
177
|
process.exitCode = 1;
|
|
126
178
|
return;
|
|
127
179
|
}
|
|
128
|
-
if (
|
|
129
|
-
console.log(`Updated ${succeeded} entity: ${externalKey}`);
|
|
180
|
+
if (entities.length === 1) {
|
|
181
|
+
console.log(`Updated ${succeeded} entity: ${String(entities[0].externalKey)}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`Updated ${succeeded}/${entities.length} entities.`);
|
|
130
185
|
}
|
|
131
186
|
}
|
package/dist/index.js
CHANGED
|
@@ -95,27 +95,30 @@ Usage:
|
|
|
95
95
|
--auto-map-application auto-map only when high confidence
|
|
96
96
|
--non-interactive fail on ambiguous mapping
|
|
97
97
|
--batch-size <n> upsert batch size (default: 10)
|
|
98
|
+
--profile include timing/profile data in JSON output
|
|
98
99
|
--dry-run preview without writing
|
|
99
100
|
--json
|
|
100
101
|
nexarch update-entity
|
|
101
|
-
Update
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
Update existing graph entities (single or batch).
|
|
103
|
+
Single options: --key <externalKey>
|
|
104
|
+
--name <name>
|
|
105
|
+
--description <text>
|
|
106
|
+
--entity-type <code> (default: application)
|
|
107
|
+
--subtype <code>
|
|
108
|
+
--icon <lucide-name> (sets attributes.application_icon)
|
|
109
|
+
--attributes-json '<json object>'
|
|
110
|
+
--attributes-file <path.json>
|
|
111
|
+
Batch options: --entities-json '<json array>'
|
|
112
|
+
--entities-file <path.json>
|
|
113
|
+
--json
|
|
113
114
|
nexarch add-relationship
|
|
114
|
-
Add
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
Add relationships between existing graph entities (single or batch).
|
|
116
|
+
Single options: --from <externalKey>
|
|
117
|
+
--to <externalKey>
|
|
118
|
+
--type <code> (e.g. part_of, depends_on)
|
|
119
|
+
Batch options: --relationships-json '<json array>'
|
|
120
|
+
--relationships-file <path.json>
|
|
121
|
+
--json
|
|
119
122
|
nexarch register-alias
|
|
120
123
|
Register a company-scoped alias for an entity so future
|
|
121
124
|
scans resolve it instead of logging it as a candidate.
|