@neat.is/core 0.4.10 → 0.4.12
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/{chunk-RC3CIDZO.js → chunk-7JY6F7BY.js} +4 -2
- package/dist/{chunk-RC3CIDZO.js.map → chunk-7JY6F7BY.js.map} +1 -1
- package/dist/{chunk-J5CEKCTR.js → chunk-WDG4QEFO.js} +782 -192
- package/dist/chunk-WDG4QEFO.js.map +1 -0
- package/dist/cli.cjs +1387 -550
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +442 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +831 -239
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +829 -237
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +884 -364
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +5 -2
- package/dist/chunk-J5CEKCTR.js.map +0 -1
|
@@ -454,19 +454,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
454
454
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
455
455
|
let best = { path: [start], edges: [] };
|
|
456
456
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
457
|
-
function step(node,
|
|
458
|
-
if (
|
|
459
|
-
best = { path: [...
|
|
457
|
+
function step(node, path36, edges) {
|
|
458
|
+
if (path36.length > best.path.length) {
|
|
459
|
+
best = { path: [...path36], edges: [...edges] };
|
|
460
460
|
}
|
|
461
|
-
if (
|
|
461
|
+
if (path36.length - 1 >= maxDepth) return;
|
|
462
462
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
463
463
|
for (const [srcId, edge] of incoming) {
|
|
464
464
|
if (visited.has(srcId)) continue;
|
|
465
465
|
visited.add(srcId);
|
|
466
|
-
|
|
466
|
+
path36.push(srcId);
|
|
467
467
|
edges.push(edge);
|
|
468
|
-
step(srcId,
|
|
469
|
-
|
|
468
|
+
step(srcId, path36, edges);
|
|
469
|
+
path36.pop();
|
|
470
470
|
edges.pop();
|
|
471
471
|
visited.delete(srcId);
|
|
472
472
|
}
|
|
@@ -655,8 +655,9 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
// src/ingest.ts
|
|
658
|
-
import { promises as fs3 } from "fs";
|
|
658
|
+
import { promises as fs3, existsSync, readFileSync } from "fs";
|
|
659
659
|
import path3 from "path";
|
|
660
|
+
import * as sourceMapJs from "source-map-js";
|
|
660
661
|
|
|
661
662
|
// src/policy.ts
|
|
662
663
|
import { promises as fs2 } from "fs";
|
|
@@ -1103,6 +1104,14 @@ function warnUnidentifiedSpan(project) {
|
|
|
1103
1104
|
`[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
|
|
1104
1105
|
);
|
|
1105
1106
|
}
|
|
1107
|
+
var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
|
|
1108
|
+
function warnNoSourceMaps(serviceName) {
|
|
1109
|
+
if (noSourceMapWarnedServices.has(serviceName)) return;
|
|
1110
|
+
noSourceMapWarnedServices.add(serviceName);
|
|
1111
|
+
console.warn(
|
|
1112
|
+
`[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1106
1115
|
function pickAttr(span, ...keys) {
|
|
1107
1116
|
for (const k of keys) {
|
|
1108
1117
|
const v = span.attributes[k];
|
|
@@ -1145,8 +1154,13 @@ function languageForExt(relPath) {
|
|
|
1145
1154
|
return void 0;
|
|
1146
1155
|
}
|
|
1147
1156
|
}
|
|
1148
|
-
function relPathForRuntimeFile(filepath, serviceNode) {
|
|
1157
|
+
function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
|
|
1149
1158
|
let p = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1159
|
+
if (scanPath && scanPath.length > 0) {
|
|
1160
|
+
const absRoot = toPosix(path3.resolve(scanPath, serviceNode?.repoPath ?? ""));
|
|
1161
|
+
const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
|
|
1162
|
+
if (p.startsWith(anchor)) return p.slice(anchor.length);
|
|
1163
|
+
}
|
|
1150
1164
|
const root = serviceNode?.repoPath;
|
|
1151
1165
|
if (root && root !== "." && root.length > 0) {
|
|
1152
1166
|
const rootPosix = toPosix(root);
|
|
@@ -1163,16 +1177,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
|
|
|
1163
1177
|
p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
|
|
1164
1178
|
return p.length > 0 ? p : null;
|
|
1165
1179
|
}
|
|
1166
|
-
|
|
1180
|
+
var sourceMapCache = /* @__PURE__ */ new Map();
|
|
1181
|
+
function resolveDistToSrc(absFilepath, line) {
|
|
1182
|
+
if (!absFilepath.endsWith(".js")) return null;
|
|
1183
|
+
let entry = sourceMapCache.get(absFilepath);
|
|
1184
|
+
if (entry === void 0) {
|
|
1185
|
+
entry = null;
|
|
1186
|
+
const mapPath = `${absFilepath}.map`;
|
|
1187
|
+
try {
|
|
1188
|
+
if (existsSync(mapPath)) {
|
|
1189
|
+
const raw = JSON.parse(readFileSync(mapPath, "utf8"));
|
|
1190
|
+
const consumer = new sourceMapJs.SourceMapConsumer(raw);
|
|
1191
|
+
entry = { consumer, dir: path3.dirname(mapPath) };
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
entry = null;
|
|
1195
|
+
}
|
|
1196
|
+
sourceMapCache.set(absFilepath, entry);
|
|
1197
|
+
}
|
|
1198
|
+
if (!entry) return null;
|
|
1199
|
+
try {
|
|
1200
|
+
const pos = entry.consumer.originalPositionFor({
|
|
1201
|
+
line: line !== void 0 && Number.isFinite(line) ? line : 1,
|
|
1202
|
+
column: 0
|
|
1203
|
+
});
|
|
1204
|
+
if (!pos || !pos.source) return null;
|
|
1205
|
+
const root = entry.consumer.sourceRoot ?? "";
|
|
1206
|
+
const resolved = path3.resolve(entry.dir, root, pos.source);
|
|
1207
|
+
return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
|
|
1208
|
+
} catch {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
1167
1213
|
const filepath = span.attributes[CODE_FILEPATH_ATTR];
|
|
1168
1214
|
if (typeof filepath !== "string" || filepath.length === 0) return null;
|
|
1169
|
-
const relPath = relPathForRuntimeFile(filepath, serviceNode);
|
|
1170
|
-
if (!relPath) return null;
|
|
1171
1215
|
const linenoRaw = span.attributes[CODE_LINENO_ATTR];
|
|
1172
|
-
|
|
1216
|
+
let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1217
|
+
const abs = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1218
|
+
const resolved = resolveDistToSrc(abs, line);
|
|
1219
|
+
let effectivePath = filepath;
|
|
1220
|
+
let originalRelPath;
|
|
1221
|
+
if (resolved) {
|
|
1222
|
+
originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
|
|
1223
|
+
effectivePath = resolved.filepath;
|
|
1224
|
+
if (resolved.line !== void 0) line = resolved.line;
|
|
1225
|
+
}
|
|
1226
|
+
const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
|
|
1227
|
+
if (!relPath) return null;
|
|
1228
|
+
if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
|
|
1229
|
+
warnNoSourceMaps(serviceNode.name);
|
|
1230
|
+
}
|
|
1173
1231
|
const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
|
|
1174
1232
|
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1175
|
-
return {
|
|
1233
|
+
return {
|
|
1234
|
+
relPath,
|
|
1235
|
+
...line !== void 0 ? { line } : {},
|
|
1236
|
+
...fn ? { fn } : {},
|
|
1237
|
+
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1238
|
+
};
|
|
1176
1239
|
}
|
|
1177
1240
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1178
1241
|
const fileNodeId = fileId(serviceName, callSite.relPath);
|
|
@@ -1184,6 +1247,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1184
1247
|
service: serviceName,
|
|
1185
1248
|
path: callSite.relPath,
|
|
1186
1249
|
...language ? { language } : {},
|
|
1250
|
+
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1187
1251
|
discoveredVia: "otel"
|
|
1188
1252
|
};
|
|
1189
1253
|
graph.addNode(fileNodeId, node);
|
|
@@ -1209,6 +1273,12 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1209
1273
|
}
|
|
1210
1274
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1211
1275
|
var STITCH_MAX_DEPTH = 2;
|
|
1276
|
+
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1277
|
+
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1278
|
+
function spanMintsObservedEdge(kind) {
|
|
1279
|
+
if (kind === void 0 || kind === 0) return true;
|
|
1280
|
+
return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
|
|
1281
|
+
}
|
|
1212
1282
|
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
1213
1283
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1214
1284
|
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
@@ -1315,7 +1385,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1315
1385
|
graph.addNode(id, node);
|
|
1316
1386
|
return id;
|
|
1317
1387
|
}
|
|
1318
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1388
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
1319
1389
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1320
1390
|
const id = makeObservedEdgeId(type, source, target);
|
|
1321
1391
|
if (graph.hasEdge(id)) {
|
|
@@ -1352,7 +1422,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1352
1422
|
confidence: confidenceForObservedSignal(signal),
|
|
1353
1423
|
lastObserved: ts,
|
|
1354
1424
|
callCount: 1,
|
|
1355
|
-
signal
|
|
1425
|
+
signal,
|
|
1426
|
+
// Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
|
|
1427
|
+
// Only set when code.filepath was present on the span — never fabricated.
|
|
1428
|
+
...evidence ? { evidence } : {}
|
|
1356
1429
|
};
|
|
1357
1430
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1358
1431
|
return { edge, created: true };
|
|
@@ -1444,12 +1517,14 @@ async function handleSpan(ctx, span) {
|
|
|
1444
1517
|
const isError = span.statusCode === 2;
|
|
1445
1518
|
cacheSpanService(span, nowMs);
|
|
1446
1519
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1447
|
-
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
1520
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1448
1521
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1522
|
+
const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
|
|
1449
1523
|
let affectedNode = sourceId;
|
|
1524
|
+
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1450
1525
|
if (span.dbSystem) {
|
|
1451
1526
|
const host = pickAddress(span);
|
|
1452
|
-
if (host) {
|
|
1527
|
+
if (mintsFromCallerSide && host) {
|
|
1453
1528
|
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
1454
1529
|
const targetId = databaseId(host);
|
|
1455
1530
|
const result = upsertObservedEdge(
|
|
@@ -1458,14 +1533,15 @@ async function handleSpan(ctx, span) {
|
|
|
1458
1533
|
observedSource(),
|
|
1459
1534
|
targetId,
|
|
1460
1535
|
ts,
|
|
1461
|
-
isError
|
|
1536
|
+
isError,
|
|
1537
|
+
callSiteEvidence
|
|
1462
1538
|
);
|
|
1463
1539
|
if (result) affectedNode = targetId;
|
|
1464
1540
|
}
|
|
1465
1541
|
} else {
|
|
1466
1542
|
const host = pickAddress(span);
|
|
1467
1543
|
let resolvedViaAddress = false;
|
|
1468
|
-
if (host && host !== span.service) {
|
|
1544
|
+
if (mintsFromCallerSide && host && host !== span.service) {
|
|
1469
1545
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1470
1546
|
if (targetId && targetId !== sourceId) {
|
|
1471
1547
|
upsertObservedEdge(
|
|
@@ -1474,7 +1550,8 @@ async function handleSpan(ctx, span) {
|
|
|
1474
1550
|
observedSource(),
|
|
1475
1551
|
targetId,
|
|
1476
1552
|
ts,
|
|
1477
|
-
isError
|
|
1553
|
+
isError,
|
|
1554
|
+
callSiteEvidence
|
|
1478
1555
|
);
|
|
1479
1556
|
affectedNode = targetId;
|
|
1480
1557
|
resolvedViaAddress = true;
|
|
@@ -1486,7 +1563,8 @@ async function handleSpan(ctx, span) {
|
|
|
1486
1563
|
observedSource(),
|
|
1487
1564
|
frontierNodeId,
|
|
1488
1565
|
ts,
|
|
1489
|
-
isError
|
|
1566
|
+
isError,
|
|
1567
|
+
callSiteEvidence
|
|
1490
1568
|
);
|
|
1491
1569
|
affectedNode = frontierNodeId;
|
|
1492
1570
|
resolvedViaAddress = true;
|
|
@@ -2442,19 +2520,122 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2442
2520
|
}
|
|
2443
2521
|
|
|
2444
2522
|
// src/extract/databases/index.ts
|
|
2445
|
-
import
|
|
2523
|
+
import path18 from "path";
|
|
2524
|
+
import {
|
|
2525
|
+
EdgeType as EdgeType5,
|
|
2526
|
+
NodeType as NodeType7,
|
|
2527
|
+
Provenance as Provenance3,
|
|
2528
|
+
databaseId as databaseId2,
|
|
2529
|
+
confidenceForExtracted as confidenceForExtracted2
|
|
2530
|
+
} from "@neat.is/types";
|
|
2531
|
+
|
|
2532
|
+
// src/extract/calls/shared.ts
|
|
2533
|
+
import { promises as fs10 } from "fs";
|
|
2534
|
+
import path10 from "path";
|
|
2446
2535
|
import {
|
|
2447
2536
|
EdgeType as EdgeType4,
|
|
2448
2537
|
NodeType as NodeType6,
|
|
2449
2538
|
Provenance as Provenance2,
|
|
2450
|
-
|
|
2451
|
-
|
|
2539
|
+
confidenceForExtracted,
|
|
2540
|
+
extractedEdgeId as extractedEdgeId3,
|
|
2541
|
+
fileId as fileId2
|
|
2452
2542
|
} from "@neat.is/types";
|
|
2543
|
+
async function walkSourceFiles(dir) {
|
|
2544
|
+
const out = [];
|
|
2545
|
+
async function walk(current) {
|
|
2546
|
+
const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2547
|
+
for (const entry of entries) {
|
|
2548
|
+
const full = path10.join(current, entry.name);
|
|
2549
|
+
if (entry.isDirectory()) {
|
|
2550
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2551
|
+
if (await isPythonVenvDir(full)) continue;
|
|
2552
|
+
await walk(full);
|
|
2553
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
|
|
2554
|
+
out.push(full);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
await walk(dir);
|
|
2559
|
+
return out;
|
|
2560
|
+
}
|
|
2561
|
+
async function loadSourceFiles(dir) {
|
|
2562
|
+
const paths = await walkSourceFiles(dir);
|
|
2563
|
+
const out = [];
|
|
2564
|
+
for (const p of paths) {
|
|
2565
|
+
try {
|
|
2566
|
+
const content = await fs10.readFile(p, "utf8");
|
|
2567
|
+
out.push({ path: p, content });
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return out;
|
|
2572
|
+
}
|
|
2573
|
+
function lineOf(text, needle) {
|
|
2574
|
+
const idx = text.indexOf(needle);
|
|
2575
|
+
if (idx < 0) return 1;
|
|
2576
|
+
return text.slice(0, idx).split("\n").length;
|
|
2577
|
+
}
|
|
2578
|
+
function snippet(text, line) {
|
|
2579
|
+
const lines = text.split("\n");
|
|
2580
|
+
return (lines[line - 1] ?? "").trim();
|
|
2581
|
+
}
|
|
2582
|
+
function toPosix2(p) {
|
|
2583
|
+
return p.split("\\").join("/");
|
|
2584
|
+
}
|
|
2585
|
+
function languageForPath(relPath) {
|
|
2586
|
+
switch (path10.extname(relPath).toLowerCase()) {
|
|
2587
|
+
case ".py":
|
|
2588
|
+
return "python";
|
|
2589
|
+
case ".ts":
|
|
2590
|
+
case ".tsx":
|
|
2591
|
+
return "typescript";
|
|
2592
|
+
case ".js":
|
|
2593
|
+
case ".jsx":
|
|
2594
|
+
case ".mjs":
|
|
2595
|
+
case ".cjs":
|
|
2596
|
+
return "javascript";
|
|
2597
|
+
default:
|
|
2598
|
+
return void 0;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
2602
|
+
let nodesAdded = 0;
|
|
2603
|
+
let edgesAdded = 0;
|
|
2604
|
+
const fileNodeId = fileId2(serviceName, relPath);
|
|
2605
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
2606
|
+
const language = languageForPath(relPath);
|
|
2607
|
+
const node = {
|
|
2608
|
+
id: fileNodeId,
|
|
2609
|
+
type: NodeType6.FileNode,
|
|
2610
|
+
service: serviceName,
|
|
2611
|
+
path: relPath,
|
|
2612
|
+
...language ? { language } : {},
|
|
2613
|
+
discoveredVia: "static"
|
|
2614
|
+
};
|
|
2615
|
+
graph.addNode(fileNodeId, node);
|
|
2616
|
+
nodesAdded++;
|
|
2617
|
+
}
|
|
2618
|
+
const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType4.CONTAINS);
|
|
2619
|
+
if (!graph.hasEdge(containsId)) {
|
|
2620
|
+
const edge = {
|
|
2621
|
+
id: containsId,
|
|
2622
|
+
source: serviceNodeId,
|
|
2623
|
+
target: fileNodeId,
|
|
2624
|
+
type: EdgeType4.CONTAINS,
|
|
2625
|
+
provenance: Provenance2.EXTRACTED,
|
|
2626
|
+
confidence: confidenceForExtracted("structural"),
|
|
2627
|
+
evidence: { file: relPath }
|
|
2628
|
+
};
|
|
2629
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
2630
|
+
edgesAdded++;
|
|
2631
|
+
}
|
|
2632
|
+
return { fileNodeId, nodesAdded, edgesAdded };
|
|
2633
|
+
}
|
|
2453
2634
|
|
|
2454
2635
|
// src/extract/databases/db-config-yaml.ts
|
|
2455
|
-
import
|
|
2636
|
+
import path11 from "path";
|
|
2456
2637
|
async function parse(serviceDir) {
|
|
2457
|
-
const yamlPath =
|
|
2638
|
+
const yamlPath = path11.join(serviceDir, "db-config.yaml");
|
|
2458
2639
|
if (!await exists(yamlPath)) return [];
|
|
2459
2640
|
const raw = await readYaml(yamlPath);
|
|
2460
2641
|
return [
|
|
@@ -2471,12 +2652,12 @@ async function parse(serviceDir) {
|
|
|
2471
2652
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
2472
2653
|
|
|
2473
2654
|
// src/extract/databases/dotenv.ts
|
|
2474
|
-
import { promises as
|
|
2475
|
-
import
|
|
2655
|
+
import { promises as fs12 } from "fs";
|
|
2656
|
+
import path13 from "path";
|
|
2476
2657
|
|
|
2477
2658
|
// src/extract/databases/shared.ts
|
|
2478
|
-
import { promises as
|
|
2479
|
-
import
|
|
2659
|
+
import { promises as fs11 } from "fs";
|
|
2660
|
+
import path12 from "path";
|
|
2480
2661
|
function schemeToEngine(scheme) {
|
|
2481
2662
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2482
2663
|
switch (s) {
|
|
@@ -2515,14 +2696,14 @@ function parseConnectionString(url) {
|
|
|
2515
2696
|
}
|
|
2516
2697
|
async function readIfExists(filePath) {
|
|
2517
2698
|
try {
|
|
2518
|
-
return await
|
|
2699
|
+
return await fs11.readFile(filePath, "utf8");
|
|
2519
2700
|
} catch {
|
|
2520
2701
|
return null;
|
|
2521
2702
|
}
|
|
2522
2703
|
}
|
|
2523
2704
|
async function findFirst(serviceDir, candidates) {
|
|
2524
2705
|
for (const rel of candidates) {
|
|
2525
|
-
const abs =
|
|
2706
|
+
const abs = path12.join(serviceDir, rel);
|
|
2526
2707
|
const content = await readIfExists(abs);
|
|
2527
2708
|
if (content !== null) return abs;
|
|
2528
2709
|
}
|
|
@@ -2573,15 +2754,15 @@ function parseDotenvLine(line) {
|
|
|
2573
2754
|
return { key, value };
|
|
2574
2755
|
}
|
|
2575
2756
|
async function parse2(serviceDir) {
|
|
2576
|
-
const entries = await
|
|
2757
|
+
const entries = await fs12.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2577
2758
|
const configs = [];
|
|
2578
2759
|
const seen = /* @__PURE__ */ new Set();
|
|
2579
2760
|
for (const entry of entries) {
|
|
2580
2761
|
if (!entry.isFile()) continue;
|
|
2581
2762
|
const match = isConfigFile(entry.name);
|
|
2582
2763
|
if (!match.match || match.fileType !== "env") continue;
|
|
2583
|
-
const filePath =
|
|
2584
|
-
const content = await
|
|
2764
|
+
const filePath = path13.join(serviceDir, entry.name);
|
|
2765
|
+
const content = await fs12.readFile(filePath, "utf8");
|
|
2585
2766
|
for (const line of content.split("\n")) {
|
|
2586
2767
|
const parsed = parseDotenvLine(line);
|
|
2587
2768
|
if (!parsed) continue;
|
|
@@ -2599,9 +2780,9 @@ async function parse2(serviceDir) {
|
|
|
2599
2780
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2600
2781
|
|
|
2601
2782
|
// src/extract/databases/prisma.ts
|
|
2602
|
-
import
|
|
2783
|
+
import path14 from "path";
|
|
2603
2784
|
async function parse3(serviceDir) {
|
|
2604
|
-
const schemaPath =
|
|
2785
|
+
const schemaPath = path14.join(serviceDir, "prisma", "schema.prisma");
|
|
2605
2786
|
const content = await readIfExists(schemaPath);
|
|
2606
2787
|
if (!content) return [];
|
|
2607
2788
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2730,10 +2911,10 @@ async function parse5(serviceDir) {
|
|
|
2730
2911
|
var knexParser = { name: "knex", parse: parse5 };
|
|
2731
2912
|
|
|
2732
2913
|
// src/extract/databases/ormconfig.ts
|
|
2733
|
-
import
|
|
2914
|
+
import path15 from "path";
|
|
2734
2915
|
async function parse6(serviceDir) {
|
|
2735
2916
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2736
|
-
const abs =
|
|
2917
|
+
const abs = path15.join(serviceDir, candidate);
|
|
2737
2918
|
if (!await exists(abs)) continue;
|
|
2738
2919
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2739
2920
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2791,9 +2972,9 @@ async function parse7(serviceDir) {
|
|
|
2791
2972
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2792
2973
|
|
|
2793
2974
|
// src/extract/databases/sequelize.ts
|
|
2794
|
-
import
|
|
2975
|
+
import path16 from "path";
|
|
2795
2976
|
async function parse8(serviceDir) {
|
|
2796
|
-
const configPath =
|
|
2977
|
+
const configPath = path16.join(serviceDir, "config", "config.json");
|
|
2797
2978
|
if (!await exists(configPath)) return [];
|
|
2798
2979
|
const raw = await readJson(configPath);
|
|
2799
2980
|
const out = [];
|
|
@@ -2819,7 +3000,7 @@ async function parse8(serviceDir) {
|
|
|
2819
3000
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2820
3001
|
|
|
2821
3002
|
// src/extract/databases/docker-compose.ts
|
|
2822
|
-
import
|
|
3003
|
+
import path17 from "path";
|
|
2823
3004
|
function portFromService(svc) {
|
|
2824
3005
|
for (const raw of svc.ports ?? []) {
|
|
2825
3006
|
const str = String(raw);
|
|
@@ -2846,7 +3027,7 @@ function databaseFromEnv(svc) {
|
|
|
2846
3027
|
}
|
|
2847
3028
|
async function parse9(serviceDir) {
|
|
2848
3029
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2849
|
-
const abs =
|
|
3030
|
+
const abs = path17.join(serviceDir, name);
|
|
2850
3031
|
if (!await exists(abs)) continue;
|
|
2851
3032
|
const raw = await readYaml(abs);
|
|
2852
3033
|
if (!raw?.services) return [];
|
|
@@ -2888,7 +3069,7 @@ function compatibleDriversFor(engine) {
|
|
|
2888
3069
|
function toDatabaseNode(config) {
|
|
2889
3070
|
return {
|
|
2890
3071
|
id: databaseId2(config.host),
|
|
2891
|
-
type:
|
|
3072
|
+
type: NodeType7.DatabaseNode,
|
|
2892
3073
|
name: config.database || config.host,
|
|
2893
3074
|
engine: config.engine,
|
|
2894
3075
|
engineVersion: config.engineVersion,
|
|
@@ -3018,19 +3199,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3018
3199
|
discoveredVia: mergedDiscoveredVia
|
|
3019
3200
|
});
|
|
3020
3201
|
}
|
|
3202
|
+
const relConfigFile = toPosix2(path18.relative(service.dir, config.sourceFile));
|
|
3203
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
3204
|
+
graph,
|
|
3205
|
+
service.pkg.name,
|
|
3206
|
+
service.node.id,
|
|
3207
|
+
relConfigFile
|
|
3208
|
+
);
|
|
3209
|
+
nodesAdded += fn;
|
|
3210
|
+
edgesAdded += fe;
|
|
3211
|
+
const evidenceFile = toPosix2(path18.relative(scanPath, config.sourceFile));
|
|
3021
3212
|
const edge = {
|
|
3022
|
-
id: extractedEdgeId2(
|
|
3023
|
-
source:
|
|
3213
|
+
id: extractedEdgeId2(fileNodeId, dbNode.id, EdgeType5.CONNECTS_TO),
|
|
3214
|
+
source: fileNodeId,
|
|
3024
3215
|
target: dbNode.id,
|
|
3025
|
-
type:
|
|
3026
|
-
provenance:
|
|
3027
|
-
confidence:
|
|
3028
|
-
|
|
3029
|
-
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3030
|
-
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3031
|
-
evidence: {
|
|
3032
|
-
file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
|
|
3033
|
-
}
|
|
3216
|
+
type: EdgeType5.CONNECTS_TO,
|
|
3217
|
+
provenance: Provenance3.EXTRACTED,
|
|
3218
|
+
confidence: confidenceForExtracted2("structural"),
|
|
3219
|
+
evidence: { file: evidenceFile }
|
|
3034
3220
|
};
|
|
3035
3221
|
if (!graph.hasEdge(edge.id)) {
|
|
3036
3222
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3055,21 +3241,21 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3055
3241
|
}
|
|
3056
3242
|
|
|
3057
3243
|
// src/extract/configs.ts
|
|
3058
|
-
import { promises as
|
|
3059
|
-
import
|
|
3244
|
+
import { promises as fs13 } from "fs";
|
|
3245
|
+
import path19 from "path";
|
|
3060
3246
|
import {
|
|
3061
|
-
EdgeType as
|
|
3062
|
-
NodeType as
|
|
3063
|
-
Provenance as
|
|
3247
|
+
EdgeType as EdgeType6,
|
|
3248
|
+
NodeType as NodeType8,
|
|
3249
|
+
Provenance as Provenance4,
|
|
3064
3250
|
configId,
|
|
3065
|
-
confidenceForExtracted as
|
|
3251
|
+
confidenceForExtracted as confidenceForExtracted3
|
|
3066
3252
|
} from "@neat.is/types";
|
|
3067
3253
|
async function walkConfigFiles(dir) {
|
|
3068
3254
|
const out = [];
|
|
3069
3255
|
async function walk(current) {
|
|
3070
|
-
const entries = await
|
|
3256
|
+
const entries = await fs13.readdir(current, { withFileTypes: true });
|
|
3071
3257
|
for (const entry of entries) {
|
|
3072
|
-
const full =
|
|
3258
|
+
const full = path19.join(current, entry.name);
|
|
3073
3259
|
if (entry.isDirectory()) {
|
|
3074
3260
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3075
3261
|
if (await isPythonVenvDir(full)) continue;
|
|
@@ -3088,26 +3274,35 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3088
3274
|
for (const service of services) {
|
|
3089
3275
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3090
3276
|
for (const file of configFiles) {
|
|
3091
|
-
const relPath =
|
|
3277
|
+
const relPath = path19.relative(scanPath, file);
|
|
3092
3278
|
const node = {
|
|
3093
3279
|
id: configId(relPath),
|
|
3094
|
-
type:
|
|
3095
|
-
name:
|
|
3280
|
+
type: NodeType8.ConfigNode,
|
|
3281
|
+
name: path19.basename(file),
|
|
3096
3282
|
path: relPath,
|
|
3097
|
-
fileType: isConfigFile(
|
|
3283
|
+
fileType: isConfigFile(path19.basename(file)).fileType
|
|
3098
3284
|
};
|
|
3099
3285
|
if (!graph.hasNode(node.id)) {
|
|
3100
3286
|
graph.addNode(node.id, node);
|
|
3101
3287
|
nodesAdded++;
|
|
3102
3288
|
}
|
|
3289
|
+
const relToService = toPosix2(path19.relative(service.dir, file));
|
|
3290
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
3291
|
+
graph,
|
|
3292
|
+
service.pkg.name,
|
|
3293
|
+
service.node.id,
|
|
3294
|
+
relToService
|
|
3295
|
+
);
|
|
3296
|
+
nodesAdded += fn;
|
|
3297
|
+
edgesAdded += fe;
|
|
3103
3298
|
const edge = {
|
|
3104
|
-
id: extractedEdgeId2(
|
|
3105
|
-
source:
|
|
3299
|
+
id: extractedEdgeId2(fileNodeId, node.id, EdgeType6.CONFIGURED_BY),
|
|
3300
|
+
source: fileNodeId,
|
|
3106
3301
|
target: node.id,
|
|
3107
|
-
type:
|
|
3108
|
-
provenance:
|
|
3109
|
-
confidence:
|
|
3110
|
-
evidence: { file: relPath.split(
|
|
3302
|
+
type: EdgeType6.CONFIGURED_BY,
|
|
3303
|
+
provenance: Provenance4.EXTRACTED,
|
|
3304
|
+
confidence: confidenceForExtracted3("structural"),
|
|
3305
|
+
evidence: { file: relPath.split(path19.sep).join("/") }
|
|
3111
3306
|
};
|
|
3112
3307
|
if (!graph.hasEdge(edge.id)) {
|
|
3113
3308
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3138,111 +3333,6 @@ import {
|
|
|
3138
3333
|
confidenceForExtracted as confidenceForExtracted4,
|
|
3139
3334
|
passesExtractedFloor
|
|
3140
3335
|
} from "@neat.is/types";
|
|
3141
|
-
|
|
3142
|
-
// src/extract/calls/shared.ts
|
|
3143
|
-
import { promises as fs13 } from "fs";
|
|
3144
|
-
import path19 from "path";
|
|
3145
|
-
import {
|
|
3146
|
-
EdgeType as EdgeType6,
|
|
3147
|
-
NodeType as NodeType8,
|
|
3148
|
-
Provenance as Provenance4,
|
|
3149
|
-
confidenceForExtracted as confidenceForExtracted3,
|
|
3150
|
-
extractedEdgeId as extractedEdgeId3,
|
|
3151
|
-
fileId as fileId2
|
|
3152
|
-
} from "@neat.is/types";
|
|
3153
|
-
async function walkSourceFiles(dir) {
|
|
3154
|
-
const out = [];
|
|
3155
|
-
async function walk(current) {
|
|
3156
|
-
const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3157
|
-
for (const entry of entries) {
|
|
3158
|
-
const full = path19.join(current, entry.name);
|
|
3159
|
-
if (entry.isDirectory()) {
|
|
3160
|
-
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3161
|
-
if (await isPythonVenvDir(full)) continue;
|
|
3162
|
-
await walk(full);
|
|
3163
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
|
|
3164
|
-
out.push(full);
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
await walk(dir);
|
|
3169
|
-
return out;
|
|
3170
|
-
}
|
|
3171
|
-
async function loadSourceFiles(dir) {
|
|
3172
|
-
const paths = await walkSourceFiles(dir);
|
|
3173
|
-
const out = [];
|
|
3174
|
-
for (const p of paths) {
|
|
3175
|
-
try {
|
|
3176
|
-
const content = await fs13.readFile(p, "utf8");
|
|
3177
|
-
out.push({ path: p, content });
|
|
3178
|
-
} catch {
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
return out;
|
|
3182
|
-
}
|
|
3183
|
-
function lineOf(text, needle) {
|
|
3184
|
-
const idx = text.indexOf(needle);
|
|
3185
|
-
if (idx < 0) return 1;
|
|
3186
|
-
return text.slice(0, idx).split("\n").length;
|
|
3187
|
-
}
|
|
3188
|
-
function snippet(text, line) {
|
|
3189
|
-
const lines = text.split("\n");
|
|
3190
|
-
return (lines[line - 1] ?? "").trim();
|
|
3191
|
-
}
|
|
3192
|
-
function toPosix2(p) {
|
|
3193
|
-
return p.split("\\").join("/");
|
|
3194
|
-
}
|
|
3195
|
-
function languageForPath(relPath) {
|
|
3196
|
-
switch (path19.extname(relPath).toLowerCase()) {
|
|
3197
|
-
case ".py":
|
|
3198
|
-
return "python";
|
|
3199
|
-
case ".ts":
|
|
3200
|
-
case ".tsx":
|
|
3201
|
-
return "typescript";
|
|
3202
|
-
case ".js":
|
|
3203
|
-
case ".jsx":
|
|
3204
|
-
case ".mjs":
|
|
3205
|
-
case ".cjs":
|
|
3206
|
-
return "javascript";
|
|
3207
|
-
default:
|
|
3208
|
-
return void 0;
|
|
3209
|
-
}
|
|
3210
|
-
}
|
|
3211
|
-
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
3212
|
-
let nodesAdded = 0;
|
|
3213
|
-
let edgesAdded = 0;
|
|
3214
|
-
const fileNodeId = fileId2(serviceName, relPath);
|
|
3215
|
-
if (!graph.hasNode(fileNodeId)) {
|
|
3216
|
-
const language = languageForPath(relPath);
|
|
3217
|
-
const node = {
|
|
3218
|
-
id: fileNodeId,
|
|
3219
|
-
type: NodeType8.FileNode,
|
|
3220
|
-
service: serviceName,
|
|
3221
|
-
path: relPath,
|
|
3222
|
-
...language ? { language } : {},
|
|
3223
|
-
discoveredVia: "static"
|
|
3224
|
-
};
|
|
3225
|
-
graph.addNode(fileNodeId, node);
|
|
3226
|
-
nodesAdded++;
|
|
3227
|
-
}
|
|
3228
|
-
const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType6.CONTAINS);
|
|
3229
|
-
if (!graph.hasEdge(containsId)) {
|
|
3230
|
-
const edge = {
|
|
3231
|
-
id: containsId,
|
|
3232
|
-
source: serviceNodeId,
|
|
3233
|
-
target: fileNodeId,
|
|
3234
|
-
type: EdgeType6.CONTAINS,
|
|
3235
|
-
provenance: Provenance4.EXTRACTED,
|
|
3236
|
-
confidence: confidenceForExtracted3("structural"),
|
|
3237
|
-
evidence: { file: relPath }
|
|
3238
|
-
};
|
|
3239
|
-
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
3240
|
-
edgesAdded++;
|
|
3241
|
-
}
|
|
3242
|
-
return { fileNodeId, nodesAdded, edgesAdded };
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
// src/extract/calls/http.ts
|
|
3246
3336
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3247
3337
|
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3248
3338
|
function isInsideJsxExternalLink(node) {
|
|
@@ -3270,8 +3360,14 @@ function collectStringLiterals(node, out) {
|
|
|
3270
3360
|
if (child) collectStringLiterals(child, out);
|
|
3271
3361
|
}
|
|
3272
3362
|
}
|
|
3363
|
+
var PARSE_CHUNK = 16384;
|
|
3364
|
+
function parseSource(parser, source) {
|
|
3365
|
+
return parser.parse(
|
|
3366
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3273
3369
|
function callsFromSource(source, parser, knownHosts) {
|
|
3274
|
-
const tree = parser
|
|
3370
|
+
const tree = parseSource(parser, source);
|
|
3275
3371
|
const literals = [];
|
|
3276
3372
|
collectStringLiterals(tree.rootNode, literals);
|
|
3277
3373
|
const out = [];
|
|
@@ -3516,6 +3612,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
|
|
|
3516
3612
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3517
3613
|
function isLikelyAddress(value) {
|
|
3518
3614
|
if (!value) return false;
|
|
3615
|
+
if (/\s/.test(value) || value.startsWith("{")) return false;
|
|
3519
3616
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3520
3617
|
}
|
|
3521
3618
|
function normaliseForMatch(s) {
|
|
@@ -3821,17 +3918,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3821
3918
|
graph.addNode(node.id, node);
|
|
3822
3919
|
nodesAdded++;
|
|
3823
3920
|
}
|
|
3824
|
-
const
|
|
3921
|
+
const relDockerfile = toPosix2(path26.relative(service.dir, dockerfilePath));
|
|
3922
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
3923
|
+
graph,
|
|
3924
|
+
service.pkg.name,
|
|
3925
|
+
service.node.id,
|
|
3926
|
+
relDockerfile
|
|
3927
|
+
);
|
|
3928
|
+
nodesAdded += fn;
|
|
3929
|
+
edgesAdded += fe;
|
|
3930
|
+
const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType10.RUNS_ON);
|
|
3825
3931
|
if (!graph.hasEdge(edgeId)) {
|
|
3826
3932
|
const edge = {
|
|
3827
3933
|
id: edgeId,
|
|
3828
|
-
source:
|
|
3934
|
+
source: fileNodeId,
|
|
3829
3935
|
target: node.id,
|
|
3830
3936
|
type: EdgeType10.RUNS_ON,
|
|
3831
3937
|
provenance: Provenance8.EXTRACTED,
|
|
3832
3938
|
confidence: confidenceForExtracted7("structural"),
|
|
3833
3939
|
evidence: {
|
|
3834
|
-
file: path26.relative(scanPath, dockerfilePath)
|
|
3940
|
+
file: toPosix2(path26.relative(scanPath, dockerfilePath))
|
|
3835
3941
|
}
|
|
3836
3942
|
};
|
|
3837
3943
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3952,7 +4058,7 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3952
4058
|
import path30 from "path";
|
|
3953
4059
|
|
|
3954
4060
|
// src/extract/retire.ts
|
|
3955
|
-
import { existsSync } from "fs";
|
|
4061
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3956
4062
|
import path29 from "path";
|
|
3957
4063
|
import { NodeType as NodeType11, Provenance as Provenance9 } from "@neat.is/types";
|
|
3958
4064
|
function dropOrphanedFileNodes(graph) {
|
|
@@ -3988,10 +4094,10 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
3988
4094
|
const evidenceFile = edge.evidence?.file;
|
|
3989
4095
|
if (!evidenceFile) return;
|
|
3990
4096
|
if (path29.isAbsolute(evidenceFile)) {
|
|
3991
|
-
if (!
|
|
4097
|
+
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
3992
4098
|
return;
|
|
3993
4099
|
}
|
|
3994
|
-
const found = bases.some((base) =>
|
|
4100
|
+
const found = bases.some((base) => existsSync2(path29.join(base, evidenceFile)));
|
|
3995
4101
|
if (!found) toDrop.push(id);
|
|
3996
4102
|
});
|
|
3997
4103
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -4567,6 +4673,66 @@ function registryPath() {
|
|
|
4567
4673
|
function registryLockPath() {
|
|
4568
4674
|
return path33.join(neatHome(), "projects.json.lock");
|
|
4569
4675
|
}
|
|
4676
|
+
function daemonPidPath() {
|
|
4677
|
+
return path33.join(neatHome(), "neatd.pid");
|
|
4678
|
+
}
|
|
4679
|
+
function isPidAliveDefault(pid) {
|
|
4680
|
+
try {
|
|
4681
|
+
process.kill(pid, 0);
|
|
4682
|
+
return true;
|
|
4683
|
+
} catch (err) {
|
|
4684
|
+
return err.code === "EPERM";
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
async function readPidFile(file) {
|
|
4688
|
+
try {
|
|
4689
|
+
const raw = await fs19.readFile(file, "utf8");
|
|
4690
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
4691
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
4692
|
+
} catch {
|
|
4693
|
+
return void 0;
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
var defaultLockHolderProbe = {
|
|
4697
|
+
isPidAlive: isPidAliveDefault,
|
|
4698
|
+
daemonPidFromFile: () => readPidFile(daemonPidPath()),
|
|
4699
|
+
async daemonResponds() {
|
|
4700
|
+
const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
4701
|
+
try {
|
|
4702
|
+
await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
|
|
4703
|
+
return true;
|
|
4704
|
+
} catch {
|
|
4705
|
+
return false;
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
async function readLockPid(lockPath) {
|
|
4710
|
+
return readPidFile(lockPath);
|
|
4711
|
+
}
|
|
4712
|
+
async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
|
|
4713
|
+
const lockPid = await readLockPid(lockPath);
|
|
4714
|
+
const daemonPid = await probe.daemonPidFromFile();
|
|
4715
|
+
if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
|
|
4716
|
+
// Either confirms a live daemon is in the picture (the second guards
|
|
4717
|
+
// against a stale pidfile whose PID got reused).
|
|
4718
|
+
(daemonPid === lockPid || await probe.daemonResponds())) {
|
|
4719
|
+
return { kind: "daemon", pid: daemonPid };
|
|
4720
|
+
}
|
|
4721
|
+
if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
|
|
4722
|
+
return { kind: "command", pid: lockPid };
|
|
4723
|
+
}
|
|
4724
|
+
return { kind: "stale" };
|
|
4725
|
+
}
|
|
4726
|
+
function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
4727
|
+
switch (holder.kind) {
|
|
4728
|
+
case "daemon":
|
|
4729
|
+
return `The neat daemon (pid ${holder.pid}) is holding the registry lock. Register this project through the daemon, or stop neatd and re-run \`neat init\`.`;
|
|
4730
|
+
case "command":
|
|
4731
|
+
return `Another neat command (pid ${holder.pid}) is holding the registry lock. Wait for it to finish, or check \`ps\` if you're not sure what's running.`;
|
|
4732
|
+
case "stale":
|
|
4733
|
+
return `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`;
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4570
4736
|
async function normalizeProjectPath(input) {
|
|
4571
4737
|
const resolved = path33.resolve(input);
|
|
4572
4738
|
try {
|
|
@@ -4587,21 +4753,31 @@ async function writeAtomically(target, contents) {
|
|
|
4587
4753
|
}
|
|
4588
4754
|
await fs19.rename(tmp, target);
|
|
4589
4755
|
}
|
|
4590
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4756
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
4591
4757
|
const deadline = Date.now() + timeoutMs;
|
|
4592
4758
|
await fs19.mkdir(path33.dirname(lockPath), { recursive: true });
|
|
4759
|
+
let probedHolder = false;
|
|
4593
4760
|
while (true) {
|
|
4594
4761
|
try {
|
|
4595
4762
|
const fd = await fs19.open(lockPath, "wx");
|
|
4596
|
-
|
|
4763
|
+
try {
|
|
4764
|
+
await fd.writeFile(`${process.pid}
|
|
4765
|
+
`, "utf8");
|
|
4766
|
+
} finally {
|
|
4767
|
+
await fd.close();
|
|
4768
|
+
}
|
|
4597
4769
|
return;
|
|
4598
4770
|
} catch (err) {
|
|
4599
4771
|
const code = err.code;
|
|
4600
4772
|
if (code !== "EEXIST") throw err;
|
|
4773
|
+
if (!probedHolder) {
|
|
4774
|
+
probedHolder = true;
|
|
4775
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
4776
|
+
if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
4777
|
+
}
|
|
4601
4778
|
if (Date.now() >= deadline) {
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
);
|
|
4779
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
4780
|
+
throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
4605
4781
|
}
|
|
4606
4782
|
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4607
4783
|
}
|
|
@@ -4721,6 +4897,319 @@ import Fastify from "fastify";
|
|
|
4721
4897
|
import cors from "@fastify/cors";
|
|
4722
4898
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
4723
4899
|
|
|
4900
|
+
// src/extend/index.ts
|
|
4901
|
+
import { promises as fs21 } from "fs";
|
|
4902
|
+
import path35 from "path";
|
|
4903
|
+
import os3 from "os";
|
|
4904
|
+
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
4905
|
+
|
|
4906
|
+
// src/installers/package-manager.ts
|
|
4907
|
+
import { promises as fs20 } from "fs";
|
|
4908
|
+
import path34 from "path";
|
|
4909
|
+
import { spawn } from "child_process";
|
|
4910
|
+
var LOCKFILE_PRIORITY = [
|
|
4911
|
+
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
4912
|
+
{ lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
|
|
4913
|
+
{ lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
|
|
4914
|
+
{
|
|
4915
|
+
lockfile: "package-lock.json",
|
|
4916
|
+
pm: "npm",
|
|
4917
|
+
args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
|
|
4918
|
+
}
|
|
4919
|
+
];
|
|
4920
|
+
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
4921
|
+
async function exists2(p) {
|
|
4922
|
+
try {
|
|
4923
|
+
await fs20.access(p);
|
|
4924
|
+
return true;
|
|
4925
|
+
} catch {
|
|
4926
|
+
return false;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
async function detectPackageManager(serviceDir) {
|
|
4930
|
+
let dir = path34.resolve(serviceDir);
|
|
4931
|
+
const stops = /* @__PURE__ */ new Set();
|
|
4932
|
+
for (let i = 0; i < 64; i++) {
|
|
4933
|
+
if (stops.has(dir)) break;
|
|
4934
|
+
stops.add(dir);
|
|
4935
|
+
for (const candidate of LOCKFILE_PRIORITY) {
|
|
4936
|
+
const lockPath = path34.join(dir, candidate.lockfile);
|
|
4937
|
+
if (await exists2(lockPath)) {
|
|
4938
|
+
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
const parent = path34.dirname(dir);
|
|
4942
|
+
if (parent === dir) break;
|
|
4943
|
+
dir = parent;
|
|
4944
|
+
}
|
|
4945
|
+
return { pm: "npm", cwd: path34.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
4946
|
+
}
|
|
4947
|
+
async function runPackageManagerInstall(cmd) {
|
|
4948
|
+
return new Promise((resolve) => {
|
|
4949
|
+
const child = spawn(cmd.pm, cmd.args, {
|
|
4950
|
+
cwd: cmd.cwd,
|
|
4951
|
+
// Inherit PATH + HOME so the user's installed managers resolve.
|
|
4952
|
+
env: process.env,
|
|
4953
|
+
// `false` keeps the parent in control of cleanup if the orchestrator
|
|
4954
|
+
// exits before install finishes. Cross-platform-safe.
|
|
4955
|
+
shell: false,
|
|
4956
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
4957
|
+
});
|
|
4958
|
+
let stderr = "";
|
|
4959
|
+
child.stderr?.on("data", (chunk) => {
|
|
4960
|
+
stderr += chunk.toString("utf8");
|
|
4961
|
+
});
|
|
4962
|
+
child.on("error", (err) => {
|
|
4963
|
+
resolve({
|
|
4964
|
+
pm: cmd.pm,
|
|
4965
|
+
cwd: cmd.cwd,
|
|
4966
|
+
args: cmd.args,
|
|
4967
|
+
exitCode: 127,
|
|
4968
|
+
stderr: stderr + `
|
|
4969
|
+
${err.message}`
|
|
4970
|
+
});
|
|
4971
|
+
});
|
|
4972
|
+
child.on("close", (code) => {
|
|
4973
|
+
resolve({
|
|
4974
|
+
pm: cmd.pm,
|
|
4975
|
+
cwd: cmd.cwd,
|
|
4976
|
+
args: cmd.args,
|
|
4977
|
+
exitCode: code ?? 1,
|
|
4978
|
+
stderr: stderr.trim()
|
|
4979
|
+
});
|
|
4980
|
+
});
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
// src/extend/index.ts
|
|
4985
|
+
async function fileExists(p) {
|
|
4986
|
+
try {
|
|
4987
|
+
await fs21.access(p);
|
|
4988
|
+
return true;
|
|
4989
|
+
} catch {
|
|
4990
|
+
return false;
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async function readPackageJson(scanPath) {
|
|
4994
|
+
const pkgPath = path35.join(scanPath, "package.json");
|
|
4995
|
+
const raw = await fs21.readFile(pkgPath, "utf8");
|
|
4996
|
+
return JSON.parse(raw);
|
|
4997
|
+
}
|
|
4998
|
+
async function findHookFiles(scanPath) {
|
|
4999
|
+
const entries = await fs21.readdir(scanPath);
|
|
5000
|
+
return entries.filter(
|
|
5001
|
+
(e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
|
|
5002
|
+
).sort();
|
|
5003
|
+
}
|
|
5004
|
+
function extendLogPath() {
|
|
5005
|
+
return process.env.NEAT_EXTEND_LOG ?? path35.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
5006
|
+
}
|
|
5007
|
+
async function appendExtendLog(entry) {
|
|
5008
|
+
const logPath = extendLogPath();
|
|
5009
|
+
await fs21.mkdir(path35.dirname(logPath), { recursive: true });
|
|
5010
|
+
await fs21.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
5011
|
+
}
|
|
5012
|
+
function splicedContent(fileContent, snippet2) {
|
|
5013
|
+
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
5014
|
+
return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
|
|
5015
|
+
__INSTRUMENTATION_BLOCK__`);
|
|
5016
|
+
}
|
|
5017
|
+
const lines = fileContent.split("\n");
|
|
5018
|
+
let lastPushIdx = -1;
|
|
5019
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5020
|
+
if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
|
|
5021
|
+
}
|
|
5022
|
+
if (lastPushIdx >= 0) {
|
|
5023
|
+
lines.splice(lastPushIdx + 1, 0, snippet2);
|
|
5024
|
+
return lines.join("\n");
|
|
5025
|
+
}
|
|
5026
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5027
|
+
if (lines[i].includes("new NodeSDK(")) {
|
|
5028
|
+
lines.splice(i, 0, snippet2);
|
|
5029
|
+
return lines.join("\n");
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
return null;
|
|
5033
|
+
}
|
|
5034
|
+
async function listUninstrumented(ctx) {
|
|
5035
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5036
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5037
|
+
const results = [];
|
|
5038
|
+
for (const [library, installedVersion] of Object.entries(allDeps)) {
|
|
5039
|
+
const entry = registryResolve(library, installedVersion);
|
|
5040
|
+
if (!entry) continue;
|
|
5041
|
+
if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
|
|
5042
|
+
results.push({
|
|
5043
|
+
library,
|
|
5044
|
+
coverage: entry.coverage,
|
|
5045
|
+
installedVersion,
|
|
5046
|
+
instrumentation_package: entry.instrumentation_package,
|
|
5047
|
+
package_version: entry.package_version,
|
|
5048
|
+
registration: entry.registration,
|
|
5049
|
+
notes: entry.notes
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
return results;
|
|
5053
|
+
}
|
|
5054
|
+
function lookupInstrumentation(library, installedVersion) {
|
|
5055
|
+
const entry = registryResolve(library, installedVersion);
|
|
5056
|
+
if (!entry) return null;
|
|
5057
|
+
return {
|
|
5058
|
+
library: entry.library,
|
|
5059
|
+
coverage: entry.coverage,
|
|
5060
|
+
instrumentation_package: entry.instrumentation_package,
|
|
5061
|
+
package_version: entry.package_version,
|
|
5062
|
+
registration: entry.registration,
|
|
5063
|
+
notes: entry.notes
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
async function describeProjectInstrumentation(ctx) {
|
|
5067
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5068
|
+
const envNeat = await fileExists(path35.join(ctx.scanPath, ".env.neat"));
|
|
5069
|
+
const registryInstrPackages = new Set(
|
|
5070
|
+
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
5071
|
+
);
|
|
5072
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5073
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5074
|
+
const installedDeps = {};
|
|
5075
|
+
for (const [key, version] of Object.entries(allDeps)) {
|
|
5076
|
+
if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
|
|
5077
|
+
installedDeps[key] = version;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
return { hookFiles, envNeat, installedDeps };
|
|
5081
|
+
}
|
|
5082
|
+
async function applyExtension(ctx, args, options) {
|
|
5083
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5084
|
+
if (hookFiles.length === 0) {
|
|
5085
|
+
throw new Error(
|
|
5086
|
+
`No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
|
|
5087
|
+
);
|
|
5088
|
+
}
|
|
5089
|
+
for (const file of hookFiles) {
|
|
5090
|
+
const content = await fs21.readFile(path35.join(ctx.scanPath, file), "utf8");
|
|
5091
|
+
if (content.includes(args.registration_snippet)) {
|
|
5092
|
+
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
const primaryFile = hookFiles[0];
|
|
5096
|
+
const primaryPath = path35.join(ctx.scanPath, primaryFile);
|
|
5097
|
+
const filesTouched = [];
|
|
5098
|
+
const depsAdded = [];
|
|
5099
|
+
const pkgPath = path35.join(ctx.scanPath, "package.json");
|
|
5100
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5101
|
+
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5102
|
+
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
5103
|
+
await fs21.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
5104
|
+
filesTouched.push("package.json");
|
|
5105
|
+
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
5106
|
+
}
|
|
5107
|
+
const hookContent = await fs21.readFile(primaryPath, "utf8");
|
|
5108
|
+
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5109
|
+
if (!patched) {
|
|
5110
|
+
throw new Error(
|
|
5111
|
+
`Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
await fs21.writeFile(primaryPath, patched, "utf8");
|
|
5115
|
+
filesTouched.push(primaryFile);
|
|
5116
|
+
const cmd = await detectPackageManager(ctx.scanPath);
|
|
5117
|
+
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
5118
|
+
const install = await installer(cmd);
|
|
5119
|
+
const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
|
|
5120
|
+
await appendExtendLog({
|
|
5121
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5122
|
+
project: ctx.project,
|
|
5123
|
+
library: args.library,
|
|
5124
|
+
instrumentation_package: args.instrumentation_package,
|
|
5125
|
+
version: args.version,
|
|
5126
|
+
registration_snippet: args.registration_snippet,
|
|
5127
|
+
filesTouched,
|
|
5128
|
+
depsAdded,
|
|
5129
|
+
installOutput
|
|
5130
|
+
});
|
|
5131
|
+
return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
|
|
5132
|
+
}
|
|
5133
|
+
async function dryRunExtension(ctx, args) {
|
|
5134
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5135
|
+
if (hookFiles.length === 0) {
|
|
5136
|
+
return {
|
|
5137
|
+
library: args.library,
|
|
5138
|
+
filesTouched: [],
|
|
5139
|
+
depsToAdd: [],
|
|
5140
|
+
packageJsonPatch: {},
|
|
5141
|
+
templatePatch: "No hook files found. Run 'neat init' first."
|
|
5142
|
+
};
|
|
5143
|
+
}
|
|
5144
|
+
for (const file of hookFiles) {
|
|
5145
|
+
const content = await fs21.readFile(path35.join(ctx.scanPath, file), "utf8");
|
|
5146
|
+
if (content.includes(args.registration_snippet)) {
|
|
5147
|
+
return {
|
|
5148
|
+
library: args.library,
|
|
5149
|
+
filesTouched: [],
|
|
5150
|
+
depsToAdd: [],
|
|
5151
|
+
packageJsonPatch: {},
|
|
5152
|
+
templatePatch: "Already applied \u2014 no changes would be made."
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const primaryFile = hookFiles[0];
|
|
5157
|
+
const filesTouched = [];
|
|
5158
|
+
const depsToAdd = [];
|
|
5159
|
+
let packageJsonPatch = {};
|
|
5160
|
+
let templatePatch = "";
|
|
5161
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5162
|
+
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5163
|
+
packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
|
|
5164
|
+
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
5165
|
+
filesTouched.push("package.json");
|
|
5166
|
+
}
|
|
5167
|
+
const hookContent = await fs21.readFile(path35.join(ctx.scanPath, primaryFile), "utf8");
|
|
5168
|
+
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5169
|
+
if (patched) {
|
|
5170
|
+
filesTouched.push(primaryFile);
|
|
5171
|
+
templatePatch = `+ ${args.registration_snippet}`;
|
|
5172
|
+
} else {
|
|
5173
|
+
templatePatch = "Could not find insertion point in hook file.";
|
|
5174
|
+
}
|
|
5175
|
+
return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
|
|
5176
|
+
}
|
|
5177
|
+
async function rollbackExtension(ctx, args) {
|
|
5178
|
+
const logPath = extendLogPath();
|
|
5179
|
+
if (!await fileExists(logPath)) {
|
|
5180
|
+
return { undone: false, message: "no apply found for library" };
|
|
5181
|
+
}
|
|
5182
|
+
const raw = await fs21.readFile(logPath, "utf8");
|
|
5183
|
+
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
5184
|
+
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
5185
|
+
if (!match) {
|
|
5186
|
+
return { undone: false, message: "no apply found for library" };
|
|
5187
|
+
}
|
|
5188
|
+
const pkgPath = path35.join(ctx.scanPath, "package.json");
|
|
5189
|
+
if (await fileExists(pkgPath)) {
|
|
5190
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5191
|
+
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
5192
|
+
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
5193
|
+
pkg.dependencies = rest;
|
|
5194
|
+
await fs21.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5198
|
+
for (const file of hookFiles) {
|
|
5199
|
+
const filePath = path35.join(ctx.scanPath, file);
|
|
5200
|
+
const content = await fs21.readFile(filePath, "utf8");
|
|
5201
|
+
if (content.includes(match.registration_snippet)) {
|
|
5202
|
+
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
5203
|
+
await fs21.writeFile(filePath, filtered, "utf8");
|
|
5204
|
+
break;
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
return {
|
|
5208
|
+
undone: true,
|
|
5209
|
+
message: `rolled back ${match.library} (${match.instrumentation_package})`
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
|
|
4724
5213
|
// src/streaming.ts
|
|
4725
5214
|
var SSE_HEARTBEAT_MS = 3e4;
|
|
4726
5215
|
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
@@ -5177,6 +5666,105 @@ function registerRoutes(scope, ctx) {
|
|
|
5177
5666
|
violations
|
|
5178
5667
|
};
|
|
5179
5668
|
});
|
|
5669
|
+
scope.get("/extend/list-uninstrumented", async (req, reply) => {
|
|
5670
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5671
|
+
if (!proj) return;
|
|
5672
|
+
if (!proj.scanPath) {
|
|
5673
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5674
|
+
}
|
|
5675
|
+
try {
|
|
5676
|
+
const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
|
|
5677
|
+
return { libraries: results };
|
|
5678
|
+
} catch (err) {
|
|
5679
|
+
return reply.code(500).send({ error: err.message });
|
|
5680
|
+
}
|
|
5681
|
+
});
|
|
5682
|
+
scope.get("/extend/lookup", async (req, reply) => {
|
|
5683
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5684
|
+
if (!proj) return;
|
|
5685
|
+
const { library, version } = req.query;
|
|
5686
|
+
if (!library) {
|
|
5687
|
+
return reply.code(400).send({ error: "query parameter `library` is required" });
|
|
5688
|
+
}
|
|
5689
|
+
const result = lookupInstrumentation(library, version);
|
|
5690
|
+
if (!result) {
|
|
5691
|
+
return reply.code(404).send({ error: "library not found in registry", library });
|
|
5692
|
+
}
|
|
5693
|
+
return result;
|
|
5694
|
+
});
|
|
5695
|
+
scope.get("/extend/describe", async (req, reply) => {
|
|
5696
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5697
|
+
if (!proj) return;
|
|
5698
|
+
if (!proj.scanPath) {
|
|
5699
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5700
|
+
}
|
|
5701
|
+
try {
|
|
5702
|
+
const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
|
|
5703
|
+
return state;
|
|
5704
|
+
} catch (err) {
|
|
5705
|
+
return reply.code(500).send({ error: err.message });
|
|
5706
|
+
}
|
|
5707
|
+
});
|
|
5708
|
+
scope.post("/extend/apply", async (req, reply) => {
|
|
5709
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5710
|
+
if (!proj) return;
|
|
5711
|
+
if (!proj.scanPath) {
|
|
5712
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5713
|
+
}
|
|
5714
|
+
const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
|
|
5715
|
+
if (!library || !instrumentation_package || !version || !registration_snippet) {
|
|
5716
|
+
return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
|
|
5717
|
+
}
|
|
5718
|
+
try {
|
|
5719
|
+
const result = await applyExtension(
|
|
5720
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
5721
|
+
{ library, instrumentation_package, version, registration_snippet }
|
|
5722
|
+
);
|
|
5723
|
+
return result;
|
|
5724
|
+
} catch (err) {
|
|
5725
|
+
return reply.code(500).send({ error: err.message });
|
|
5726
|
+
}
|
|
5727
|
+
});
|
|
5728
|
+
scope.post("/extend/dry-run", async (req, reply) => {
|
|
5729
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5730
|
+
if (!proj) return;
|
|
5731
|
+
if (!proj.scanPath) {
|
|
5732
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5733
|
+
}
|
|
5734
|
+
const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
|
|
5735
|
+
if (!library || !instrumentation_package || !version || !registration_snippet) {
|
|
5736
|
+
return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
|
|
5737
|
+
}
|
|
5738
|
+
try {
|
|
5739
|
+
const result = await dryRunExtension(
|
|
5740
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
5741
|
+
{ library, instrumentation_package, version, registration_snippet }
|
|
5742
|
+
);
|
|
5743
|
+
return result;
|
|
5744
|
+
} catch (err) {
|
|
5745
|
+
return reply.code(500).send({ error: err.message });
|
|
5746
|
+
}
|
|
5747
|
+
});
|
|
5748
|
+
scope.post("/extend/rollback", async (req, reply) => {
|
|
5749
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5750
|
+
if (!proj) return;
|
|
5751
|
+
if (!proj.scanPath) {
|
|
5752
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5753
|
+
}
|
|
5754
|
+
const { library } = req.body ?? {};
|
|
5755
|
+
if (!library) {
|
|
5756
|
+
return reply.code(400).send({ error: "body must include library" });
|
|
5757
|
+
}
|
|
5758
|
+
try {
|
|
5759
|
+
const result = await rollbackExtension(
|
|
5760
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
5761
|
+
{ library }
|
|
5762
|
+
);
|
|
5763
|
+
return result;
|
|
5764
|
+
} catch (err) {
|
|
5765
|
+
return reply.code(500).send({ error: err.message });
|
|
5766
|
+
}
|
|
5767
|
+
});
|
|
5180
5768
|
}
|
|
5181
5769
|
async function buildApi(opts) {
|
|
5182
5770
|
const app = Fastify({ logger: false });
|
|
@@ -5322,6 +5910,8 @@ export {
|
|
|
5322
5910
|
saveGraphToDisk,
|
|
5323
5911
|
loadGraphFromDisk,
|
|
5324
5912
|
startPersistLoop,
|
|
5913
|
+
detectPackageManager,
|
|
5914
|
+
runPackageManagerInstall,
|
|
5325
5915
|
loadSnapshotForDiff,
|
|
5326
5916
|
computeGraphDiff,
|
|
5327
5917
|
pathsForProject,
|
|
@@ -5341,4 +5931,4 @@ export {
|
|
|
5341
5931
|
removeProject,
|
|
5342
5932
|
buildApi
|
|
5343
5933
|
};
|
|
5344
|
-
//# sourceMappingURL=chunk-
|
|
5934
|
+
//# sourceMappingURL=chunk-WDG4QEFO.js.map
|