@neat.is/core 0.4.11 → 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-W4RNPPB5.js → chunk-7JY6F7BY.js} +2 -2
- package/dist/{chunk-OJHI33LD.js → chunk-WDG4QEFO.js} +616 -173
- package/dist/chunk-WDG4QEFO.js.map +1 -0
- package/dist/cli.cjs +994 -534
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +189 -169
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +669 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +667 -224
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +802 -359
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-OJHI33LD.js.map +0 -1
- /package/dist/{chunk-W4RNPPB5.js.map → chunk-7JY6F7BY.js.map} +0 -0
|
@@ -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
|
}
|
|
@@ -1385,7 +1385,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1385
1385
|
graph.addNode(id, node);
|
|
1386
1386
|
return id;
|
|
1387
1387
|
}
|
|
1388
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1388
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
1389
1389
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1390
1390
|
const id = makeObservedEdgeId(type, source, target);
|
|
1391
1391
|
if (graph.hasEdge(id)) {
|
|
@@ -1422,7 +1422,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1422
1422
|
confidence: confidenceForObservedSignal(signal),
|
|
1423
1423
|
lastObserved: ts,
|
|
1424
1424
|
callCount: 1,
|
|
1425
|
-
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 } : {}
|
|
1426
1429
|
};
|
|
1427
1430
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1428
1431
|
return { edge, created: true };
|
|
@@ -1516,6 +1519,7 @@ async function handleSpan(ctx, span) {
|
|
|
1516
1519
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1517
1520
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1518
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;
|
|
1519
1523
|
let affectedNode = sourceId;
|
|
1520
1524
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1521
1525
|
if (span.dbSystem) {
|
|
@@ -1529,7 +1533,8 @@ async function handleSpan(ctx, span) {
|
|
|
1529
1533
|
observedSource(),
|
|
1530
1534
|
targetId,
|
|
1531
1535
|
ts,
|
|
1532
|
-
isError
|
|
1536
|
+
isError,
|
|
1537
|
+
callSiteEvidence
|
|
1533
1538
|
);
|
|
1534
1539
|
if (result) affectedNode = targetId;
|
|
1535
1540
|
}
|
|
@@ -1545,7 +1550,8 @@ async function handleSpan(ctx, span) {
|
|
|
1545
1550
|
observedSource(),
|
|
1546
1551
|
targetId,
|
|
1547
1552
|
ts,
|
|
1548
|
-
isError
|
|
1553
|
+
isError,
|
|
1554
|
+
callSiteEvidence
|
|
1549
1555
|
);
|
|
1550
1556
|
affectedNode = targetId;
|
|
1551
1557
|
resolvedViaAddress = true;
|
|
@@ -1557,7 +1563,8 @@ async function handleSpan(ctx, span) {
|
|
|
1557
1563
|
observedSource(),
|
|
1558
1564
|
frontierNodeId,
|
|
1559
1565
|
ts,
|
|
1560
|
-
isError
|
|
1566
|
+
isError,
|
|
1567
|
+
callSiteEvidence
|
|
1561
1568
|
);
|
|
1562
1569
|
affectedNode = frontierNodeId;
|
|
1563
1570
|
resolvedViaAddress = true;
|
|
@@ -2513,19 +2520,122 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2513
2520
|
}
|
|
2514
2521
|
|
|
2515
2522
|
// src/extract/databases/index.ts
|
|
2516
|
-
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";
|
|
2517
2535
|
import {
|
|
2518
2536
|
EdgeType as EdgeType4,
|
|
2519
2537
|
NodeType as NodeType6,
|
|
2520
2538
|
Provenance as Provenance2,
|
|
2521
|
-
|
|
2522
|
-
|
|
2539
|
+
confidenceForExtracted,
|
|
2540
|
+
extractedEdgeId as extractedEdgeId3,
|
|
2541
|
+
fileId as fileId2
|
|
2523
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
|
+
}
|
|
2524
2634
|
|
|
2525
2635
|
// src/extract/databases/db-config-yaml.ts
|
|
2526
|
-
import
|
|
2636
|
+
import path11 from "path";
|
|
2527
2637
|
async function parse(serviceDir) {
|
|
2528
|
-
const yamlPath =
|
|
2638
|
+
const yamlPath = path11.join(serviceDir, "db-config.yaml");
|
|
2529
2639
|
if (!await exists(yamlPath)) return [];
|
|
2530
2640
|
const raw = await readYaml(yamlPath);
|
|
2531
2641
|
return [
|
|
@@ -2542,12 +2652,12 @@ async function parse(serviceDir) {
|
|
|
2542
2652
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
2543
2653
|
|
|
2544
2654
|
// src/extract/databases/dotenv.ts
|
|
2545
|
-
import { promises as
|
|
2546
|
-
import
|
|
2655
|
+
import { promises as fs12 } from "fs";
|
|
2656
|
+
import path13 from "path";
|
|
2547
2657
|
|
|
2548
2658
|
// src/extract/databases/shared.ts
|
|
2549
|
-
import { promises as
|
|
2550
|
-
import
|
|
2659
|
+
import { promises as fs11 } from "fs";
|
|
2660
|
+
import path12 from "path";
|
|
2551
2661
|
function schemeToEngine(scheme) {
|
|
2552
2662
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2553
2663
|
switch (s) {
|
|
@@ -2586,14 +2696,14 @@ function parseConnectionString(url) {
|
|
|
2586
2696
|
}
|
|
2587
2697
|
async function readIfExists(filePath) {
|
|
2588
2698
|
try {
|
|
2589
|
-
return await
|
|
2699
|
+
return await fs11.readFile(filePath, "utf8");
|
|
2590
2700
|
} catch {
|
|
2591
2701
|
return null;
|
|
2592
2702
|
}
|
|
2593
2703
|
}
|
|
2594
2704
|
async function findFirst(serviceDir, candidates) {
|
|
2595
2705
|
for (const rel of candidates) {
|
|
2596
|
-
const abs =
|
|
2706
|
+
const abs = path12.join(serviceDir, rel);
|
|
2597
2707
|
const content = await readIfExists(abs);
|
|
2598
2708
|
if (content !== null) return abs;
|
|
2599
2709
|
}
|
|
@@ -2644,15 +2754,15 @@ function parseDotenvLine(line) {
|
|
|
2644
2754
|
return { key, value };
|
|
2645
2755
|
}
|
|
2646
2756
|
async function parse2(serviceDir) {
|
|
2647
|
-
const entries = await
|
|
2757
|
+
const entries = await fs12.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2648
2758
|
const configs = [];
|
|
2649
2759
|
const seen = /* @__PURE__ */ new Set();
|
|
2650
2760
|
for (const entry of entries) {
|
|
2651
2761
|
if (!entry.isFile()) continue;
|
|
2652
2762
|
const match = isConfigFile(entry.name);
|
|
2653
2763
|
if (!match.match || match.fileType !== "env") continue;
|
|
2654
|
-
const filePath =
|
|
2655
|
-
const content = await
|
|
2764
|
+
const filePath = path13.join(serviceDir, entry.name);
|
|
2765
|
+
const content = await fs12.readFile(filePath, "utf8");
|
|
2656
2766
|
for (const line of content.split("\n")) {
|
|
2657
2767
|
const parsed = parseDotenvLine(line);
|
|
2658
2768
|
if (!parsed) continue;
|
|
@@ -2670,9 +2780,9 @@ async function parse2(serviceDir) {
|
|
|
2670
2780
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2671
2781
|
|
|
2672
2782
|
// src/extract/databases/prisma.ts
|
|
2673
|
-
import
|
|
2783
|
+
import path14 from "path";
|
|
2674
2784
|
async function parse3(serviceDir) {
|
|
2675
|
-
const schemaPath =
|
|
2785
|
+
const schemaPath = path14.join(serviceDir, "prisma", "schema.prisma");
|
|
2676
2786
|
const content = await readIfExists(schemaPath);
|
|
2677
2787
|
if (!content) return [];
|
|
2678
2788
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2801,10 +2911,10 @@ async function parse5(serviceDir) {
|
|
|
2801
2911
|
var knexParser = { name: "knex", parse: parse5 };
|
|
2802
2912
|
|
|
2803
2913
|
// src/extract/databases/ormconfig.ts
|
|
2804
|
-
import
|
|
2914
|
+
import path15 from "path";
|
|
2805
2915
|
async function parse6(serviceDir) {
|
|
2806
2916
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2807
|
-
const abs =
|
|
2917
|
+
const abs = path15.join(serviceDir, candidate);
|
|
2808
2918
|
if (!await exists(abs)) continue;
|
|
2809
2919
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2810
2920
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2862,9 +2972,9 @@ async function parse7(serviceDir) {
|
|
|
2862
2972
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2863
2973
|
|
|
2864
2974
|
// src/extract/databases/sequelize.ts
|
|
2865
|
-
import
|
|
2975
|
+
import path16 from "path";
|
|
2866
2976
|
async function parse8(serviceDir) {
|
|
2867
|
-
const configPath =
|
|
2977
|
+
const configPath = path16.join(serviceDir, "config", "config.json");
|
|
2868
2978
|
if (!await exists(configPath)) return [];
|
|
2869
2979
|
const raw = await readJson(configPath);
|
|
2870
2980
|
const out = [];
|
|
@@ -2890,7 +3000,7 @@ async function parse8(serviceDir) {
|
|
|
2890
3000
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2891
3001
|
|
|
2892
3002
|
// src/extract/databases/docker-compose.ts
|
|
2893
|
-
import
|
|
3003
|
+
import path17 from "path";
|
|
2894
3004
|
function portFromService(svc) {
|
|
2895
3005
|
for (const raw of svc.ports ?? []) {
|
|
2896
3006
|
const str = String(raw);
|
|
@@ -2917,7 +3027,7 @@ function databaseFromEnv(svc) {
|
|
|
2917
3027
|
}
|
|
2918
3028
|
async function parse9(serviceDir) {
|
|
2919
3029
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2920
|
-
const abs =
|
|
3030
|
+
const abs = path17.join(serviceDir, name);
|
|
2921
3031
|
if (!await exists(abs)) continue;
|
|
2922
3032
|
const raw = await readYaml(abs);
|
|
2923
3033
|
if (!raw?.services) return [];
|
|
@@ -2959,7 +3069,7 @@ function compatibleDriversFor(engine) {
|
|
|
2959
3069
|
function toDatabaseNode(config) {
|
|
2960
3070
|
return {
|
|
2961
3071
|
id: databaseId2(config.host),
|
|
2962
|
-
type:
|
|
3072
|
+
type: NodeType7.DatabaseNode,
|
|
2963
3073
|
name: config.database || config.host,
|
|
2964
3074
|
engine: config.engine,
|
|
2965
3075
|
engineVersion: config.engineVersion,
|
|
@@ -3089,19 +3199,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3089
3199
|
discoveredVia: mergedDiscoveredVia
|
|
3090
3200
|
});
|
|
3091
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));
|
|
3092
3212
|
const edge = {
|
|
3093
|
-
id: extractedEdgeId2(
|
|
3094
|
-
source:
|
|
3213
|
+
id: extractedEdgeId2(fileNodeId, dbNode.id, EdgeType5.CONNECTS_TO),
|
|
3214
|
+
source: fileNodeId,
|
|
3095
3215
|
target: dbNode.id,
|
|
3096
|
-
type:
|
|
3097
|
-
provenance:
|
|
3098
|
-
confidence:
|
|
3099
|
-
|
|
3100
|
-
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3101
|
-
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3102
|
-
evidence: {
|
|
3103
|
-
file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
|
|
3104
|
-
}
|
|
3216
|
+
type: EdgeType5.CONNECTS_TO,
|
|
3217
|
+
provenance: Provenance3.EXTRACTED,
|
|
3218
|
+
confidence: confidenceForExtracted2("structural"),
|
|
3219
|
+
evidence: { file: evidenceFile }
|
|
3105
3220
|
};
|
|
3106
3221
|
if (!graph.hasEdge(edge.id)) {
|
|
3107
3222
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3126,21 +3241,21 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3126
3241
|
}
|
|
3127
3242
|
|
|
3128
3243
|
// src/extract/configs.ts
|
|
3129
|
-
import { promises as
|
|
3130
|
-
import
|
|
3244
|
+
import { promises as fs13 } from "fs";
|
|
3245
|
+
import path19 from "path";
|
|
3131
3246
|
import {
|
|
3132
|
-
EdgeType as
|
|
3133
|
-
NodeType as
|
|
3134
|
-
Provenance as
|
|
3247
|
+
EdgeType as EdgeType6,
|
|
3248
|
+
NodeType as NodeType8,
|
|
3249
|
+
Provenance as Provenance4,
|
|
3135
3250
|
configId,
|
|
3136
|
-
confidenceForExtracted as
|
|
3251
|
+
confidenceForExtracted as confidenceForExtracted3
|
|
3137
3252
|
} from "@neat.is/types";
|
|
3138
3253
|
async function walkConfigFiles(dir) {
|
|
3139
3254
|
const out = [];
|
|
3140
3255
|
async function walk(current) {
|
|
3141
|
-
const entries = await
|
|
3256
|
+
const entries = await fs13.readdir(current, { withFileTypes: true });
|
|
3142
3257
|
for (const entry of entries) {
|
|
3143
|
-
const full =
|
|
3258
|
+
const full = path19.join(current, entry.name);
|
|
3144
3259
|
if (entry.isDirectory()) {
|
|
3145
3260
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3146
3261
|
if (await isPythonVenvDir(full)) continue;
|
|
@@ -3159,26 +3274,35 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3159
3274
|
for (const service of services) {
|
|
3160
3275
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3161
3276
|
for (const file of configFiles) {
|
|
3162
|
-
const relPath =
|
|
3277
|
+
const relPath = path19.relative(scanPath, file);
|
|
3163
3278
|
const node = {
|
|
3164
3279
|
id: configId(relPath),
|
|
3165
|
-
type:
|
|
3166
|
-
name:
|
|
3280
|
+
type: NodeType8.ConfigNode,
|
|
3281
|
+
name: path19.basename(file),
|
|
3167
3282
|
path: relPath,
|
|
3168
|
-
fileType: isConfigFile(
|
|
3283
|
+
fileType: isConfigFile(path19.basename(file)).fileType
|
|
3169
3284
|
};
|
|
3170
3285
|
if (!graph.hasNode(node.id)) {
|
|
3171
3286
|
graph.addNode(node.id, node);
|
|
3172
3287
|
nodesAdded++;
|
|
3173
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;
|
|
3174
3298
|
const edge = {
|
|
3175
|
-
id: extractedEdgeId2(
|
|
3176
|
-
source:
|
|
3299
|
+
id: extractedEdgeId2(fileNodeId, node.id, EdgeType6.CONFIGURED_BY),
|
|
3300
|
+
source: fileNodeId,
|
|
3177
3301
|
target: node.id,
|
|
3178
|
-
type:
|
|
3179
|
-
provenance:
|
|
3180
|
-
confidence:
|
|
3181
|
-
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("/") }
|
|
3182
3306
|
};
|
|
3183
3307
|
if (!graph.hasEdge(edge.id)) {
|
|
3184
3308
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3209,111 +3333,6 @@ import {
|
|
|
3209
3333
|
confidenceForExtracted as confidenceForExtracted4,
|
|
3210
3334
|
passesExtractedFloor
|
|
3211
3335
|
} from "@neat.is/types";
|
|
3212
|
-
|
|
3213
|
-
// src/extract/calls/shared.ts
|
|
3214
|
-
import { promises as fs13 } from "fs";
|
|
3215
|
-
import path19 from "path";
|
|
3216
|
-
import {
|
|
3217
|
-
EdgeType as EdgeType6,
|
|
3218
|
-
NodeType as NodeType8,
|
|
3219
|
-
Provenance as Provenance4,
|
|
3220
|
-
confidenceForExtracted as confidenceForExtracted3,
|
|
3221
|
-
extractedEdgeId as extractedEdgeId3,
|
|
3222
|
-
fileId as fileId2
|
|
3223
|
-
} from "@neat.is/types";
|
|
3224
|
-
async function walkSourceFiles(dir) {
|
|
3225
|
-
const out = [];
|
|
3226
|
-
async function walk(current) {
|
|
3227
|
-
const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3228
|
-
for (const entry of entries) {
|
|
3229
|
-
const full = path19.join(current, entry.name);
|
|
3230
|
-
if (entry.isDirectory()) {
|
|
3231
|
-
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3232
|
-
if (await isPythonVenvDir(full)) continue;
|
|
3233
|
-
await walk(full);
|
|
3234
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
|
|
3235
|
-
out.push(full);
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
await walk(dir);
|
|
3240
|
-
return out;
|
|
3241
|
-
}
|
|
3242
|
-
async function loadSourceFiles(dir) {
|
|
3243
|
-
const paths = await walkSourceFiles(dir);
|
|
3244
|
-
const out = [];
|
|
3245
|
-
for (const p of paths) {
|
|
3246
|
-
try {
|
|
3247
|
-
const content = await fs13.readFile(p, "utf8");
|
|
3248
|
-
out.push({ path: p, content });
|
|
3249
|
-
} catch {
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
return out;
|
|
3253
|
-
}
|
|
3254
|
-
function lineOf(text, needle) {
|
|
3255
|
-
const idx = text.indexOf(needle);
|
|
3256
|
-
if (idx < 0) return 1;
|
|
3257
|
-
return text.slice(0, idx).split("\n").length;
|
|
3258
|
-
}
|
|
3259
|
-
function snippet(text, line) {
|
|
3260
|
-
const lines = text.split("\n");
|
|
3261
|
-
return (lines[line - 1] ?? "").trim();
|
|
3262
|
-
}
|
|
3263
|
-
function toPosix2(p) {
|
|
3264
|
-
return p.split("\\").join("/");
|
|
3265
|
-
}
|
|
3266
|
-
function languageForPath(relPath) {
|
|
3267
|
-
switch (path19.extname(relPath).toLowerCase()) {
|
|
3268
|
-
case ".py":
|
|
3269
|
-
return "python";
|
|
3270
|
-
case ".ts":
|
|
3271
|
-
case ".tsx":
|
|
3272
|
-
return "typescript";
|
|
3273
|
-
case ".js":
|
|
3274
|
-
case ".jsx":
|
|
3275
|
-
case ".mjs":
|
|
3276
|
-
case ".cjs":
|
|
3277
|
-
return "javascript";
|
|
3278
|
-
default:
|
|
3279
|
-
return void 0;
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
3283
|
-
let nodesAdded = 0;
|
|
3284
|
-
let edgesAdded = 0;
|
|
3285
|
-
const fileNodeId = fileId2(serviceName, relPath);
|
|
3286
|
-
if (!graph.hasNode(fileNodeId)) {
|
|
3287
|
-
const language = languageForPath(relPath);
|
|
3288
|
-
const node = {
|
|
3289
|
-
id: fileNodeId,
|
|
3290
|
-
type: NodeType8.FileNode,
|
|
3291
|
-
service: serviceName,
|
|
3292
|
-
path: relPath,
|
|
3293
|
-
...language ? { language } : {},
|
|
3294
|
-
discoveredVia: "static"
|
|
3295
|
-
};
|
|
3296
|
-
graph.addNode(fileNodeId, node);
|
|
3297
|
-
nodesAdded++;
|
|
3298
|
-
}
|
|
3299
|
-
const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType6.CONTAINS);
|
|
3300
|
-
if (!graph.hasEdge(containsId)) {
|
|
3301
|
-
const edge = {
|
|
3302
|
-
id: containsId,
|
|
3303
|
-
source: serviceNodeId,
|
|
3304
|
-
target: fileNodeId,
|
|
3305
|
-
type: EdgeType6.CONTAINS,
|
|
3306
|
-
provenance: Provenance4.EXTRACTED,
|
|
3307
|
-
confidence: confidenceForExtracted3("structural"),
|
|
3308
|
-
evidence: { file: relPath }
|
|
3309
|
-
};
|
|
3310
|
-
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
3311
|
-
edgesAdded++;
|
|
3312
|
-
}
|
|
3313
|
-
return { fileNodeId, nodesAdded, edgesAdded };
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
// src/extract/calls/http.ts
|
|
3317
3336
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3318
3337
|
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3319
3338
|
function isInsideJsxExternalLink(node) {
|
|
@@ -3593,6 +3612,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
|
|
|
3593
3612
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3594
3613
|
function isLikelyAddress(value) {
|
|
3595
3614
|
if (!value) return false;
|
|
3615
|
+
if (/\s/.test(value) || value.startsWith("{")) return false;
|
|
3596
3616
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3597
3617
|
}
|
|
3598
3618
|
function normaliseForMatch(s) {
|
|
@@ -3898,17 +3918,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3898
3918
|
graph.addNode(node.id, node);
|
|
3899
3919
|
nodesAdded++;
|
|
3900
3920
|
}
|
|
3901
|
-
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);
|
|
3902
3931
|
if (!graph.hasEdge(edgeId)) {
|
|
3903
3932
|
const edge = {
|
|
3904
3933
|
id: edgeId,
|
|
3905
|
-
source:
|
|
3934
|
+
source: fileNodeId,
|
|
3906
3935
|
target: node.id,
|
|
3907
3936
|
type: EdgeType10.RUNS_ON,
|
|
3908
3937
|
provenance: Provenance8.EXTRACTED,
|
|
3909
3938
|
confidence: confidenceForExtracted7("structural"),
|
|
3910
3939
|
evidence: {
|
|
3911
|
-
file: path26.relative(scanPath, dockerfilePath)
|
|
3940
|
+
file: toPosix2(path26.relative(scanPath, dockerfilePath))
|
|
3912
3941
|
}
|
|
3913
3942
|
};
|
|
3914
3943
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4868,6 +4897,319 @@ import Fastify from "fastify";
|
|
|
4868
4897
|
import cors from "@fastify/cors";
|
|
4869
4898
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
4870
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
|
+
|
|
4871
5213
|
// src/streaming.ts
|
|
4872
5214
|
var SSE_HEARTBEAT_MS = 3e4;
|
|
4873
5215
|
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
@@ -5324,6 +5666,105 @@ function registerRoutes(scope, ctx) {
|
|
|
5324
5666
|
violations
|
|
5325
5667
|
};
|
|
5326
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
|
+
});
|
|
5327
5768
|
}
|
|
5328
5769
|
async function buildApi(opts) {
|
|
5329
5770
|
const app = Fastify({ logger: false });
|
|
@@ -5469,6 +5910,8 @@ export {
|
|
|
5469
5910
|
saveGraphToDisk,
|
|
5470
5911
|
loadGraphFromDisk,
|
|
5471
5912
|
startPersistLoop,
|
|
5913
|
+
detectPackageManager,
|
|
5914
|
+
runPackageManagerInstall,
|
|
5472
5915
|
loadSnapshotForDiff,
|
|
5473
5916
|
computeGraphDiff,
|
|
5474
5917
|
pathsForProject,
|
|
@@ -5488,4 +5931,4 @@ export {
|
|
|
5488
5931
|
removeProject,
|
|
5489
5932
|
buildApi
|
|
5490
5933
|
};
|
|
5491
|
-
//# sourceMappingURL=chunk-
|
|
5934
|
+
//# sourceMappingURL=chunk-WDG4QEFO.js.map
|