@neat.is/core 0.4.20-dev.20260630 → 0.4.23
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-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-62ELQVIP.js → chunk-IABNGQT2.js} +57 -9
- package/dist/chunk-IABNGQT2.js.map +1 -0
- package/dist/{chunk-GNAX2CBF.js → chunk-O25KZNZK.js} +878 -160
- package/dist/chunk-O25KZNZK.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1251 -355
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +903 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +947 -151
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +835 -122
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -42,13 +42,13 @@ var init_cjs_shims = __esm({
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
// src/auth.ts
|
|
45
|
-
function
|
|
45
|
+
function isLoopbackHost2(host) {
|
|
46
46
|
if (!host) return false;
|
|
47
47
|
return LOOPBACK_HOSTS.has(host);
|
|
48
48
|
}
|
|
49
49
|
function assertBindAuthority(host, token) {
|
|
50
50
|
if (token && token.length > 0) return;
|
|
51
|
-
if (
|
|
51
|
+
if (isLoopbackHost2(host)) return;
|
|
52
52
|
throw new BindAuthorityError(host);
|
|
53
53
|
}
|
|
54
54
|
function mountBearerAuth(app, opts) {
|
|
@@ -574,7 +574,23 @@ async function buildOtelReceiver(opts) {
|
|
|
574
574
|
};
|
|
575
575
|
return decorated;
|
|
576
576
|
}
|
|
577
|
-
|
|
577
|
+
async function listenSteppingOtlp(app, requestedPort, host) {
|
|
578
|
+
let port = requestedPort;
|
|
579
|
+
for (let attempt = 0; ; attempt++) {
|
|
580
|
+
try {
|
|
581
|
+
return await app.listen({ port, host });
|
|
582
|
+
} catch (err) {
|
|
583
|
+
const code = err.code;
|
|
584
|
+
const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
|
|
585
|
+
if (!canStep) throw err;
|
|
586
|
+
console.warn(
|
|
587
|
+
`otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
|
|
588
|
+
);
|
|
589
|
+
port += OTLP_STEP_STRIDE;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
var import_node_path42, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
|
|
578
594
|
var init_otel = __esm({
|
|
579
595
|
"src/otel.ts"() {
|
|
580
596
|
"use strict";
|
|
@@ -590,6 +606,8 @@ var init_otel = __esm({
|
|
|
590
606
|
exportTraceServiceRequestType = null;
|
|
591
607
|
exportTraceServiceResponseType = null;
|
|
592
608
|
cachedProtobufResponseBody = null;
|
|
609
|
+
OTLP_STEP_ATTEMPTS = 8;
|
|
610
|
+
OTLP_STEP_STRIDE = 1;
|
|
593
611
|
}
|
|
594
612
|
});
|
|
595
613
|
|
|
@@ -605,6 +623,7 @@ __export(cli_exports, {
|
|
|
605
623
|
parseArgs: () => parseArgs,
|
|
606
624
|
printBanner: () => printBanner,
|
|
607
625
|
readPackageVersion: () => readPackageVersion,
|
|
626
|
+
resolveDaemonUrl: () => resolveDaemonUrl,
|
|
608
627
|
resolveProjectForVerb: () => resolveProjectForVerb,
|
|
609
628
|
runInit: () => runInit,
|
|
610
629
|
runQueryVerb: () => runQueryVerb,
|
|
@@ -1277,22 +1296,164 @@ var rootCauseShapes = {
|
|
|
1277
1296
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1278
1297
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1279
1298
|
};
|
|
1280
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1299
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1281
1300
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1282
1301
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1283
1302
|
const shape = rootCauseShapes[origin.type];
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1303
|
+
if (shape) {
|
|
1304
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1305
|
+
const match = shape(graph, origin, walk);
|
|
1306
|
+
if (match) {
|
|
1307
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1308
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1309
|
+
rootCauseNode: match.rootCauseNode,
|
|
1310
|
+
rootCauseReason: reason,
|
|
1311
|
+
traversalPath: walk.path,
|
|
1312
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1313
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1314
|
+
fixRecommendation: match.fixRecommendation
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1319
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1320
|
+
if (crossService) return crossService;
|
|
1321
|
+
}
|
|
1322
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1323
|
+
}
|
|
1324
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1325
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1326
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1327
|
+
}
|
|
1328
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1329
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1330
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1331
|
+
if (relevant.length === 0) return null;
|
|
1332
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1333
|
+
const attrs = latest.attributes ?? {};
|
|
1334
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1335
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1336
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1337
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1338
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1339
|
+
const count = sameMode.length;
|
|
1340
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1341
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1342
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1343
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1344
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1345
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1346
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1347
|
+
return {
|
|
1348
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1349
|
+
rootCauseReason,
|
|
1350
|
+
...fileNode ? { fileNode } : {},
|
|
1351
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1355
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1356
|
+
if (!loc) return null;
|
|
1357
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1358
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1289
1359
|
return import_types.RootCauseResultSchema.parse({
|
|
1290
|
-
rootCauseNode:
|
|
1291
|
-
rootCauseReason:
|
|
1292
|
-
traversalPath
|
|
1293
|
-
edgeProvenances
|
|
1294
|
-
confidence:
|
|
1295
|
-
fixRecommendation:
|
|
1360
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1361
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1362
|
+
traversalPath,
|
|
1363
|
+
edgeProvenances,
|
|
1364
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1365
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function isFailingCallEdge(e) {
|
|
1369
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1370
|
+
}
|
|
1371
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1372
|
+
const ids = [serviceId3];
|
|
1373
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1374
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1375
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1376
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1377
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1378
|
+
}
|
|
1379
|
+
return ids;
|
|
1380
|
+
}
|
|
1381
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1382
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1383
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1384
|
+
if (ec !== cc) return ec > cc;
|
|
1385
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1386
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1387
|
+
}
|
|
1388
|
+
return id < curId;
|
|
1389
|
+
}
|
|
1390
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1391
|
+
let best = null;
|
|
1392
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1393
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1394
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1395
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1396
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1397
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1398
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1399
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1400
|
+
best = { nextService: owner.id, edge: e };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return best;
|
|
1405
|
+
}
|
|
1406
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1407
|
+
const path53 = [originServiceId];
|
|
1408
|
+
const edges = [];
|
|
1409
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1410
|
+
let current = originServiceId;
|
|
1411
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1412
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1413
|
+
if (!hop) break;
|
|
1414
|
+
path53.push(hop.nextService);
|
|
1415
|
+
edges.push(hop.edge);
|
|
1416
|
+
visited.add(hop.nextService);
|
|
1417
|
+
current = hop.nextService;
|
|
1418
|
+
}
|
|
1419
|
+
if (edges.length === 0) return null;
|
|
1420
|
+
return { path: path53, edges, culprit: current };
|
|
1421
|
+
}
|
|
1422
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1423
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1424
|
+
if (!chain) return null;
|
|
1425
|
+
const culprit = chain.culprit;
|
|
1426
|
+
const path53 = [...chain.path];
|
|
1427
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1428
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1429
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1430
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1431
|
+
if (loc) {
|
|
1432
|
+
let rootCauseNode = culprit;
|
|
1433
|
+
if (loc.fileNode) {
|
|
1434
|
+
path53.push(loc.fileNode);
|
|
1435
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1436
|
+
rootCauseNode = loc.fileNode;
|
|
1437
|
+
}
|
|
1438
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1439
|
+
rootCauseNode,
|
|
1440
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1441
|
+
traversalPath: path53,
|
|
1442
|
+
edgeProvenances,
|
|
1443
|
+
confidence,
|
|
1444
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1448
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1449
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1450
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1451
|
+
rootCauseNode: culprit,
|
|
1452
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1453
|
+
traversalPath: path53,
|
|
1454
|
+
edgeProvenances,
|
|
1455
|
+
confidence,
|
|
1456
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1296
1457
|
});
|
|
1297
1458
|
}
|
|
1298
1459
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1315,14 +1476,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1315
1476
|
});
|
|
1316
1477
|
}
|
|
1317
1478
|
if (frame.distance >= maxDepth) continue;
|
|
1318
|
-
const
|
|
1319
|
-
for (const [
|
|
1320
|
-
if (enqueued.has(
|
|
1321
|
-
enqueued.add(
|
|
1479
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1480
|
+
for (const [srcId, edge] of incoming) {
|
|
1481
|
+
if (enqueued.has(srcId)) continue;
|
|
1482
|
+
enqueued.add(srcId);
|
|
1322
1483
|
queue.push({
|
|
1323
|
-
nodeId:
|
|
1484
|
+
nodeId: srcId,
|
|
1324
1485
|
distance: frame.distance + 1,
|
|
1325
|
-
path: [...frame.path,
|
|
1486
|
+
path: [...frame.path, srcId],
|
|
1326
1487
|
pathEdges: [...frame.pathEdges, edge]
|
|
1327
1488
|
});
|
|
1328
1489
|
}
|
|
@@ -1378,6 +1539,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1378
1539
|
total: dependencies.length
|
|
1379
1540
|
});
|
|
1380
1541
|
}
|
|
1542
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1543
|
+
if (!graph.hasNode(nodeId)) {
|
|
1544
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1545
|
+
origin: nodeId,
|
|
1546
|
+
dependencies: [],
|
|
1547
|
+
observed: false,
|
|
1548
|
+
inboundObservedCount: 0,
|
|
1549
|
+
hasExtractedOutbound: false
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1553
|
+
const scope = [nodeId];
|
|
1554
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1555
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1556
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1557
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1558
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1559
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const dependencies = [];
|
|
1563
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1564
|
+
let hasExtractedOutbound = false;
|
|
1565
|
+
for (const src of scope) {
|
|
1566
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1567
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1568
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1569
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1570
|
+
if (!seenEdge.has(e.id)) {
|
|
1571
|
+
seenEdge.add(e.id);
|
|
1572
|
+
dependencies.push(e);
|
|
1573
|
+
}
|
|
1574
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1575
|
+
hasExtractedOutbound = true;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
let inboundObservedCount = 0;
|
|
1580
|
+
for (const tgt of scope) {
|
|
1581
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1582
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1583
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1584
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
dependencies.sort(
|
|
1588
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1589
|
+
);
|
|
1590
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1591
|
+
origin: nodeId,
|
|
1592
|
+
dependencies,
|
|
1593
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1594
|
+
inboundObservedCount,
|
|
1595
|
+
hasExtractedOutbound
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1381
1598
|
|
|
1382
1599
|
// src/policy.ts
|
|
1383
1600
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -1647,6 +1864,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
1647
1864
|
}
|
|
1648
1865
|
return out;
|
|
1649
1866
|
}
|
|
1867
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1868
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1869
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1870
|
+
const out = [];
|
|
1871
|
+
for (const policy of policies) {
|
|
1872
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1873
|
+
if (!m) continue;
|
|
1874
|
+
out.push({
|
|
1875
|
+
policyId: policy.id,
|
|
1876
|
+
policyName: policy.name,
|
|
1877
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1878
|
+
severity: policy.severity,
|
|
1879
|
+
onViolation: resolveOnViolation(policy),
|
|
1880
|
+
ruleType: policy.rule.type,
|
|
1881
|
+
match: m.match,
|
|
1882
|
+
reason: m.reason
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
return out;
|
|
1886
|
+
}
|
|
1887
|
+
function requiredProvenanceList(required) {
|
|
1888
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1889
|
+
return String(required);
|
|
1890
|
+
}
|
|
1891
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1892
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1893
|
+
for (const edgeId of incident) {
|
|
1894
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1895
|
+
if (e.type !== edgeType) continue;
|
|
1896
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1897
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1902
|
+
let found = null;
|
|
1903
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1904
|
+
if (found !== null) return;
|
|
1905
|
+
if (subjId === nodeId) return;
|
|
1906
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1907
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1908
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1909
|
+
});
|
|
1910
|
+
return found;
|
|
1911
|
+
}
|
|
1912
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1913
|
+
const rule = policy.rule;
|
|
1914
|
+
switch (rule.type) {
|
|
1915
|
+
case "structural": {
|
|
1916
|
+
if (node.type === rule.fromNodeType) {
|
|
1917
|
+
return {
|
|
1918
|
+
match: "subject",
|
|
1919
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
if (node.type === rule.toNodeType) {
|
|
1923
|
+
return {
|
|
1924
|
+
match: "region",
|
|
1925
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
case "ownership": {
|
|
1931
|
+
if (node.type === rule.nodeType) {
|
|
1932
|
+
return {
|
|
1933
|
+
match: "subject",
|
|
1934
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
case "blast-radius": {
|
|
1940
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1941
|
+
if (node.type === rule.nodeType) {
|
|
1942
|
+
return {
|
|
1943
|
+
match: "subject",
|
|
1944
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1948
|
+
if (subject) {
|
|
1949
|
+
return {
|
|
1950
|
+
match: "region",
|
|
1951
|
+
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
case "compatibility": {
|
|
1957
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1958
|
+
if (node.type === import_types2.NodeType.ServiceNode) {
|
|
1959
|
+
return {
|
|
1960
|
+
match: "subject",
|
|
1961
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1965
|
+
if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
|
|
1966
|
+
return {
|
|
1967
|
+
match: "region",
|
|
1968
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
case "provenance": {
|
|
1974
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1975
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1976
|
+
return {
|
|
1977
|
+
match: "subject",
|
|
1978
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1982
|
+
return {
|
|
1983
|
+
match: "region",
|
|
1984
|
+
reason: rule.targetNodeId !== void 0 ? `this node sits on a ${rule.edgeType} edge to ${rule.targetNodeId}, which must carry ${requiredList} provenance` : `this node sits on a ${rule.edgeType} edge, which must carry ${requiredList} provenance`
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1650
1991
|
async function loadPolicyFile(policyPath) {
|
|
1651
1992
|
let raw;
|
|
1652
1993
|
try {
|
|
@@ -1755,9 +2096,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1755
2096
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1756
2097
|
}
|
|
1757
2098
|
}
|
|
1758
|
-
function
|
|
2099
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1759
2100
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1760
|
-
const v =
|
|
2101
|
+
const v = attrs[key];
|
|
1761
2102
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1762
2103
|
if (typeof v === "string") {
|
|
1763
2104
|
const n = Number(v);
|
|
@@ -1766,6 +2107,63 @@ function httpResponseStatus(span) {
|
|
|
1766
2107
|
}
|
|
1767
2108
|
return void 0;
|
|
1768
2109
|
}
|
|
2110
|
+
function httpResponseStatus(span) {
|
|
2111
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2112
|
+
}
|
|
2113
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2114
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2115
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2116
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2117
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2118
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2119
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2120
|
+
if (where) return `error on ${where}`;
|
|
2121
|
+
return void 0;
|
|
2122
|
+
}
|
|
2123
|
+
var GRPC_STATUS_NAMES = {
|
|
2124
|
+
1: "CANCELLED",
|
|
2125
|
+
2: "UNKNOWN",
|
|
2126
|
+
3: "INVALID_ARGUMENT",
|
|
2127
|
+
4: "DEADLINE_EXCEEDED",
|
|
2128
|
+
5: "NOT_FOUND",
|
|
2129
|
+
6: "ALREADY_EXISTS",
|
|
2130
|
+
7: "PERMISSION_DENIED",
|
|
2131
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2132
|
+
9: "FAILED_PRECONDITION",
|
|
2133
|
+
10: "ABORTED",
|
|
2134
|
+
11: "OUT_OF_RANGE",
|
|
2135
|
+
12: "UNIMPLEMENTED",
|
|
2136
|
+
13: "INTERNAL",
|
|
2137
|
+
14: "UNAVAILABLE",
|
|
2138
|
+
15: "DATA_LOSS",
|
|
2139
|
+
16: "UNAUTHENTICATED"
|
|
2140
|
+
};
|
|
2141
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2142
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2143
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2144
|
+
if (typeof v === "string") {
|
|
2145
|
+
const n = Number(v);
|
|
2146
|
+
if (Number.isFinite(n)) return n;
|
|
2147
|
+
}
|
|
2148
|
+
return void 0;
|
|
2149
|
+
}
|
|
2150
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2151
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2152
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2153
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2154
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2155
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2156
|
+
}
|
|
2157
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2158
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2159
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2160
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2161
|
+
}
|
|
2162
|
+
return void 0;
|
|
2163
|
+
}
|
|
2164
|
+
function incidentMessage(span) {
|
|
2165
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2166
|
+
}
|
|
1769
2167
|
function nowIso(ctx) {
|
|
1770
2168
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1771
2169
|
}
|
|
@@ -1785,13 +2183,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1785
2183
|
`[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.`
|
|
1786
2184
|
);
|
|
1787
2185
|
}
|
|
1788
|
-
function
|
|
2186
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1789
2187
|
for (const k of keys) {
|
|
1790
|
-
const v =
|
|
2188
|
+
const v = attrs[k];
|
|
1791
2189
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1792
2190
|
}
|
|
1793
2191
|
return void 0;
|
|
1794
2192
|
}
|
|
2193
|
+
function pickAttr(span, ...keys) {
|
|
2194
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2195
|
+
}
|
|
1795
2196
|
function hostFromUrl(u) {
|
|
1796
2197
|
if (!u) return void 0;
|
|
1797
2198
|
try {
|
|
@@ -1803,6 +2204,10 @@ function hostFromUrl(u) {
|
|
|
1803
2204
|
function pickAddress(span) {
|
|
1804
2205
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1805
2206
|
}
|
|
2207
|
+
function isLoopbackHost(host) {
|
|
2208
|
+
const h = host.toLowerCase();
|
|
2209
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2210
|
+
}
|
|
1806
2211
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1807
2212
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1808
2213
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1910,15 +2315,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1910
2315
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1911
2316
|
};
|
|
1912
2317
|
}
|
|
2318
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2319
|
+
if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
|
|
2320
|
+
let best = null;
|
|
2321
|
+
graph.forEachNode((_id, attrs) => {
|
|
2322
|
+
const a = attrs;
|
|
2323
|
+
if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
|
|
2324
|
+
if (a.discoveredVia === "otel") return;
|
|
2325
|
+
const p = a.path;
|
|
2326
|
+
if (!p) return;
|
|
2327
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2328
|
+
best = p;
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
return best ?? relPath;
|
|
2332
|
+
}
|
|
1913
2333
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1914
|
-
const
|
|
2334
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2335
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
|
|
1915
2336
|
if (!graph.hasNode(fileNodeId)) {
|
|
1916
|
-
const language = languageForExt(
|
|
2337
|
+
const language = languageForExt(relPath);
|
|
1917
2338
|
const node = {
|
|
1918
2339
|
id: fileNodeId,
|
|
1919
2340
|
type: import_types3.NodeType.FileNode,
|
|
1920
2341
|
service: serviceName,
|
|
1921
|
-
path:
|
|
2342
|
+
path: relPath,
|
|
1922
2343
|
...language ? { language } : {},
|
|
1923
2344
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1924
2345
|
discoveredVia: "otel"
|
|
@@ -1946,6 +2367,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1946
2367
|
}
|
|
1947
2368
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1948
2369
|
var STITCH_MAX_DEPTH = 2;
|
|
2370
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2371
|
+
import_types3.EdgeType.CALLS,
|
|
2372
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2373
|
+
import_types3.EdgeType.DEPENDS_ON
|
|
2374
|
+
]);
|
|
1949
2375
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1950
2376
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1951
2377
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2119,6 +2545,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2119
2545
|
for (const edgeId of outbound) {
|
|
2120
2546
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2121
2547
|
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
2548
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2122
2549
|
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2123
2550
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2124
2551
|
if (!visited.has(edge.target)) {
|
|
@@ -2151,6 +2578,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2151
2578
|
await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2152
2579
|
await import_node_fs4.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2153
2580
|
}
|
|
2581
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
2582
|
+
const sid = (0, import_types3.serviceId)(span.service, span.env);
|
|
2583
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
2584
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
2585
|
+
if (callSite) {
|
|
2586
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
2587
|
+
return (0, import_types3.fileId)(span.service, relPath);
|
|
2588
|
+
}
|
|
2589
|
+
return sid;
|
|
2590
|
+
}
|
|
2154
2591
|
function sanitizeAttributes(attrs) {
|
|
2155
2592
|
const out = {};
|
|
2156
2593
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2159,7 +2596,7 @@ function sanitizeAttributes(attrs) {
|
|
|
2159
2596
|
}
|
|
2160
2597
|
return out;
|
|
2161
2598
|
}
|
|
2162
|
-
function buildErrorEventForReceiver(span) {
|
|
2599
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
2163
2600
|
if (span.statusCode !== 2) return null;
|
|
2164
2601
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2165
2602
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -2169,16 +2606,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
2169
2606
|
service: span.service,
|
|
2170
2607
|
traceId: span.traceId,
|
|
2171
2608
|
spanId: span.spanId,
|
|
2172
|
-
errorMessage: span
|
|
2609
|
+
errorMessage: incidentMessage(span),
|
|
2173
2610
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2174
2611
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2175
2612
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2176
|
-
affectedNode: (
|
|
2613
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
2177
2614
|
};
|
|
2178
2615
|
}
|
|
2179
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
2616
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
2180
2617
|
return async (span) => {
|
|
2181
|
-
const ev = buildErrorEventForReceiver(span);
|
|
2618
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
2182
2619
|
if (!ev) return;
|
|
2183
2620
|
await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(errorsPath), { recursive: true });
|
|
2184
2621
|
await import_node_fs4.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -2206,6 +2643,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
2206
2643
|
};
|
|
2207
2644
|
await appendErrorEvent(ctx, ev);
|
|
2208
2645
|
}
|
|
2646
|
+
async function recordExceptionIncident(ctx, span, ts) {
|
|
2647
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
2648
|
+
const ev = {
|
|
2649
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
2650
|
+
timestamp: ts,
|
|
2651
|
+
service: span.service,
|
|
2652
|
+
traceId: span.traceId,
|
|
2653
|
+
spanId: span.spanId,
|
|
2654
|
+
errorMessage: incidentMessage(span),
|
|
2655
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2656
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2657
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2658
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
2659
|
+
};
|
|
2660
|
+
await appendErrorEvent(ctx, ev);
|
|
2661
|
+
}
|
|
2209
2662
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2210
2663
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2211
2664
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -2262,7 +2715,10 @@ async function handleSpan(ctx, span) {
|
|
|
2262
2715
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2263
2716
|
cacheSpanService(span, nowMs, callSite);
|
|
2264
2717
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2265
|
-
const callSiteEvidence = callSite ? {
|
|
2718
|
+
const callSiteEvidence = callSite ? {
|
|
2719
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2720
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2721
|
+
} : void 0;
|
|
2266
2722
|
let affectedNode = sourceId;
|
|
2267
2723
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2268
2724
|
if (span.dbSystem) {
|
|
@@ -2284,7 +2740,7 @@ async function handleSpan(ctx, span) {
|
|
|
2284
2740
|
} else {
|
|
2285
2741
|
const host = pickAddress(span);
|
|
2286
2742
|
let resolvedViaAddress = false;
|
|
2287
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2743
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2288
2744
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2289
2745
|
if (targetId && targetId !== sourceId) {
|
|
2290
2746
|
upsertObservedEdge(
|
|
@@ -2319,7 +2775,11 @@ async function handleSpan(ctx, span) {
|
|
|
2319
2775
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2320
2776
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2321
2777
|
const fallbackEvidence = parent.callSite ? {
|
|
2322
|
-
file:
|
|
2778
|
+
file: reconcileObservedRelPath(
|
|
2779
|
+
ctx.graph,
|
|
2780
|
+
parent.service,
|
|
2781
|
+
parent.callSite.relPath
|
|
2782
|
+
),
|
|
2323
2783
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2324
2784
|
} : void 0;
|
|
2325
2785
|
upsertObservedEdge(
|
|
@@ -2344,7 +2804,7 @@ async function handleSpan(ctx, span) {
|
|
|
2344
2804
|
service: span.service,
|
|
2345
2805
|
traceId: span.traceId,
|
|
2346
2806
|
spanId: span.spanId,
|
|
2347
|
-
errorMessage: span
|
|
2807
|
+
errorMessage: incidentMessage(span),
|
|
2348
2808
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2349
2809
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2350
2810
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2355,7 +2815,9 @@ async function handleSpan(ctx, span) {
|
|
|
2355
2815
|
}
|
|
2356
2816
|
if (span.statusCode !== 2) {
|
|
2357
2817
|
const status2 = httpResponseStatus(span);
|
|
2358
|
-
if (
|
|
2818
|
+
if (span.exception) {
|
|
2819
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
2820
|
+
} else if (status2 !== void 0 && status2 >= 500) {
|
|
2359
2821
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2360
2822
|
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2361
2823
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
@@ -2520,12 +2982,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2520
2982
|
async function readErrorEvents(errorsPath) {
|
|
2521
2983
|
try {
|
|
2522
2984
|
const raw = await import_node_fs4.promises.readFile(errorsPath, "utf8");
|
|
2523
|
-
|
|
2985
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2986
|
+
return dedupeIncidents(events);
|
|
2524
2987
|
} catch (err) {
|
|
2525
2988
|
if (err.code === "ENOENT") return [];
|
|
2526
2989
|
throw err;
|
|
2527
2990
|
}
|
|
2528
2991
|
}
|
|
2992
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2993
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2994
|
+
if (ev.errorType) return false;
|
|
2995
|
+
if (!ev.attributes) return false;
|
|
2996
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2997
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2998
|
+
}
|
|
2999
|
+
function dedupeIncidents(events) {
|
|
3000
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3001
|
+
const once = [];
|
|
3002
|
+
for (const ev of events) {
|
|
3003
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
3004
|
+
if (key === void 0) {
|
|
3005
|
+
once.push(ev);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
if (seen.has(key)) continue;
|
|
3009
|
+
seen.add(key);
|
|
3010
|
+
once.push(ev);
|
|
3011
|
+
}
|
|
3012
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
3013
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
3014
|
+
for (const ev of once) {
|
|
3015
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
3016
|
+
}
|
|
3017
|
+
return once.filter((ev) => {
|
|
3018
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
3019
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
2529
3022
|
function mergeSnapshot(graph, snapshot) {
|
|
2530
3023
|
const exported = snapshot.graph;
|
|
2531
3024
|
let nodesAdded = 0;
|
|
@@ -2606,10 +3099,17 @@ function isConfigFile(name) {
|
|
|
2606
3099
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2607
3100
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2608
3101
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3102
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2609
3103
|
return { match: true, fileType: "env" };
|
|
2610
3104
|
}
|
|
2611
3105
|
return { match: false, fileType: "" };
|
|
2612
3106
|
}
|
|
3107
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3108
|
+
return name === ".env.neat";
|
|
3109
|
+
}
|
|
3110
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3111
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3112
|
+
}
|
|
2613
3113
|
function isTestPath(filePath) {
|
|
2614
3114
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2615
3115
|
const segments = normalised.split("/");
|
|
@@ -3315,7 +3815,9 @@ async function walkSourceFiles(dir) {
|
|
|
3315
3815
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3316
3816
|
if (await isPythonVenvDir(full)) continue;
|
|
3317
3817
|
await walk(full);
|
|
3318
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name))
|
|
3818
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3819
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3820
|
+
!isNeatAuthoredSourceFile(entry2.name)) {
|
|
3319
3821
|
out.push(full);
|
|
3320
3822
|
}
|
|
3321
3823
|
}
|
|
@@ -3485,11 +3987,28 @@ function collectJsImports(node, out) {
|
|
|
3485
3987
|
if (child) collectJsImports(child, out);
|
|
3486
3988
|
}
|
|
3487
3989
|
}
|
|
3990
|
+
function collectImportedNames(node, out) {
|
|
3991
|
+
if (node.type === "aliased_import") {
|
|
3992
|
+
const nameNode = node.childForFieldName("name");
|
|
3993
|
+
if (nameNode) out.push(nameNode.text);
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if (node.type === "dotted_name") {
|
|
3997
|
+
out.push(node.text);
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4001
|
+
const child = node.namedChild(i);
|
|
4002
|
+
if (child) collectImportedNames(child, out);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
3488
4005
|
function collectPyImports(node, out) {
|
|
3489
4006
|
if (node.type === "import_from_statement") {
|
|
3490
4007
|
let level = 0;
|
|
3491
4008
|
let modulePath = "";
|
|
4009
|
+
const names = [];
|
|
3492
4010
|
let pastFrom = false;
|
|
4011
|
+
let pastImport = false;
|
|
3493
4012
|
for (let i = 0; i < node.childCount; i++) {
|
|
3494
4013
|
const child = node.child(i);
|
|
3495
4014
|
if (!child) continue;
|
|
@@ -3497,26 +4016,30 @@ function collectPyImports(node, out) {
|
|
|
3497
4016
|
if (child.type === "from") pastFrom = true;
|
|
3498
4017
|
continue;
|
|
3499
4018
|
}
|
|
3500
|
-
if (
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
if (!rc) continue;
|
|
3505
|
-
if (rc.type === "import_prefix") {
|
|
3506
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3507
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3508
|
-
}
|
|
3509
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4019
|
+
if (!pastImport) {
|
|
4020
|
+
if (child.type === "import") {
|
|
4021
|
+
pastImport = true;
|
|
4022
|
+
continue;
|
|
3510
4023
|
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
4024
|
+
if (child.type === "relative_import") {
|
|
4025
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4026
|
+
const rc = child.child(j);
|
|
4027
|
+
if (!rc) continue;
|
|
4028
|
+
if (rc.type === "import_prefix") {
|
|
4029
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
4030
|
+
if (rc.child(k)?.type === ".") level++;
|
|
4031
|
+
}
|
|
4032
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4033
|
+
}
|
|
4034
|
+
} else if (child.type === "dotted_name") {
|
|
4035
|
+
modulePath = child.text;
|
|
4036
|
+
}
|
|
4037
|
+
continue;
|
|
3516
4038
|
}
|
|
4039
|
+
collectImportedNames(child, names);
|
|
3517
4040
|
}
|
|
3518
4041
|
if (level > 0 || modulePath) {
|
|
3519
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4042
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3520
4043
|
}
|
|
3521
4044
|
}
|
|
3522
4045
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3618,8 +4141,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3618
4141
|
return null;
|
|
3619
4142
|
}
|
|
3620
4143
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3621
|
-
if (!imp.modulePath) return null;
|
|
3622
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3623
4144
|
let baseDir;
|
|
3624
4145
|
if (imp.level > 0) {
|
|
3625
4146
|
baseDir = import_node_path13.default.dirname(importerPath);
|
|
@@ -3627,13 +4148,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3627
4148
|
} else {
|
|
3628
4149
|
baseDir = serviceDir;
|
|
3629
4150
|
}
|
|
3630
|
-
const
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
4151
|
+
const moduleBase = imp.modulePath ? import_node_path13.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4152
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4153
|
+
let needModuleFile = imp.names.length === 0;
|
|
4154
|
+
for (const name of imp.names) {
|
|
4155
|
+
const submoduleFile = import_node_path13.default.join(moduleBase, `${name}.py`);
|
|
4156
|
+
const subpackageInit = import_node_path13.default.join(moduleBase, name, "__init__.py");
|
|
4157
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
4158
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, submoduleFile)));
|
|
4159
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
4160
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, subpackageInit)));
|
|
4161
|
+
} else {
|
|
4162
|
+
needModuleFile = true;
|
|
3634
4163
|
}
|
|
3635
4164
|
}
|
|
3636
|
-
|
|
4165
|
+
if (needModuleFile) {
|
|
4166
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path13.default.join(moduleBase, "__init__.py")] : [import_node_path13.default.join(moduleBase, "__init__.py")];
|
|
4167
|
+
for (const candidate of moduleFileCandidates) {
|
|
4168
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4169
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, candidate)));
|
|
4170
|
+
break;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
return [...resolved];
|
|
3637
4175
|
}
|
|
3638
4176
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3639
4177
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3674,17 +4212,18 @@ async function addImports(graph, services) {
|
|
|
3674
4212
|
continue;
|
|
3675
4213
|
}
|
|
3676
4214
|
for (const imp of pyImports) {
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4215
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4216
|
+
for (const resolved of resolvedPaths) {
|
|
4217
|
+
edgesAdded += emitImportEdge(
|
|
4218
|
+
graph,
|
|
4219
|
+
service.pkg.name,
|
|
4220
|
+
importerFileId,
|
|
4221
|
+
relFile,
|
|
4222
|
+
resolved,
|
|
4223
|
+
imp.line,
|
|
4224
|
+
imp.snippet
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
3688
4227
|
}
|
|
3689
4228
|
continue;
|
|
3690
4229
|
}
|
|
@@ -4320,6 +4859,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4320
4859
|
edgesAdded++;
|
|
4321
4860
|
}
|
|
4322
4861
|
}
|
|
4862
|
+
if (allConfigs.length === 1) {
|
|
4863
|
+
const primary = allConfigs[0];
|
|
4864
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4865
|
+
const relPath = import_node_path21.default.relative(scanPath, primary.sourceFile);
|
|
4866
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4867
|
+
if (!graph.hasNode(cfgId)) {
|
|
4868
|
+
const cfgNode = {
|
|
4869
|
+
id: cfgId,
|
|
4870
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4871
|
+
name: import_node_path21.default.basename(primary.sourceFile),
|
|
4872
|
+
path: relPath,
|
|
4873
|
+
fileType: isConfigFile(import_node_path21.default.basename(primary.sourceFile)).fileType || "config"
|
|
4874
|
+
};
|
|
4875
|
+
graph.addNode(cfgId, cfgNode);
|
|
4876
|
+
nodesAdded++;
|
|
4877
|
+
}
|
|
4878
|
+
const cfgEdge = {
|
|
4879
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4880
|
+
source: service.node.id,
|
|
4881
|
+
target: cfgId,
|
|
4882
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4883
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4884
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4885
|
+
evidence: { file: toPosix2(relPath) }
|
|
4886
|
+
};
|
|
4887
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4888
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4889
|
+
edgesAdded++;
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4323
4892
|
attachIncompatibilities(service, allConfigs);
|
|
4324
4893
|
if (graph.hasNode(service.node.id)) {
|
|
4325
4894
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4331,6 +4900,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4331
4900
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4332
4901
|
delete updated.incompatibilities;
|
|
4333
4902
|
}
|
|
4903
|
+
if (!service.node.dbConnectionTarget) {
|
|
4904
|
+
delete updated.dbConnectionTarget;
|
|
4905
|
+
}
|
|
4334
4906
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4335
4907
|
}
|
|
4336
4908
|
}
|
|
@@ -4508,7 +5080,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4508
5080
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4509
5081
|
if (seen.has(dedupKey)) continue;
|
|
4510
5082
|
seen.add(dedupKey);
|
|
4511
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
5083
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4512
5084
|
const ev = {
|
|
4513
5085
|
file: relFile,
|
|
4514
5086
|
line: site.line,
|
|
@@ -4528,7 +5100,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4528
5100
|
target: targetId,
|
|
4529
5101
|
type: import_types11.EdgeType.CALLS,
|
|
4530
5102
|
confidence,
|
|
4531
|
-
confidenceKind: "
|
|
5103
|
+
confidenceKind: "url-literal-service-target",
|
|
4532
5104
|
evidence: ev
|
|
4533
5105
|
});
|
|
4534
5106
|
continue;
|
|
@@ -5034,19 +5606,29 @@ init_cjs_shims();
|
|
|
5034
5606
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5035
5607
|
var import_node_fs16 = require("fs");
|
|
5036
5608
|
var import_types20 = require("@neat.is/types");
|
|
5037
|
-
function
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5609
|
+
function readDockerfile(content) {
|
|
5610
|
+
let image = null;
|
|
5611
|
+
const ports = [];
|
|
5612
|
+
let cmd = null;
|
|
5613
|
+
let entrypoint = null;
|
|
5614
|
+
for (const raw of content.split("\n")) {
|
|
5041
5615
|
const line = raw.trim();
|
|
5042
5616
|
if (!line || line.startsWith("#")) continue;
|
|
5043
|
-
if (
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
if (
|
|
5047
|
-
|
|
5617
|
+
if (/^from\s+/i.test(line)) {
|
|
5618
|
+
const candidate = line.split(/\s+/)[1];
|
|
5619
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5620
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5621
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5622
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5623
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5624
|
+
}
|
|
5625
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5626
|
+
entrypoint = line;
|
|
5627
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5628
|
+
cmd = line;
|
|
5629
|
+
}
|
|
5048
5630
|
}
|
|
5049
|
-
return
|
|
5631
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5050
5632
|
}
|
|
5051
5633
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5052
5634
|
let nodesAdded = 0;
|
|
@@ -5065,14 +5647,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5065
5647
|
);
|
|
5066
5648
|
continue;
|
|
5067
5649
|
}
|
|
5068
|
-
const
|
|
5069
|
-
if (!image) continue;
|
|
5070
|
-
const node = makeInfraNode("container-image", image);
|
|
5650
|
+
const facts = readDockerfile(content);
|
|
5651
|
+
if (!facts.image) continue;
|
|
5652
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5071
5653
|
if (!graph.hasNode(node.id)) {
|
|
5072
5654
|
graph.addNode(node.id, node);
|
|
5073
5655
|
nodesAdded++;
|
|
5074
5656
|
}
|
|
5075
5657
|
const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
|
|
5658
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath));
|
|
5076
5659
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5077
5660
|
graph,
|
|
5078
5661
|
service.pkg.name,
|
|
@@ -5091,12 +5674,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5091
5674
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
5092
5675
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5093
5676
|
evidence: {
|
|
5094
|
-
file:
|
|
5677
|
+
file: evidenceFile,
|
|
5678
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5095
5679
|
}
|
|
5096
5680
|
};
|
|
5097
5681
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5098
5682
|
edgesAdded++;
|
|
5099
5683
|
}
|
|
5684
|
+
for (const port of facts.ports) {
|
|
5685
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5686
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5687
|
+
graph.addNode(portNode.id, portNode);
|
|
5688
|
+
nodesAdded++;
|
|
5689
|
+
}
|
|
5690
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5691
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5692
|
+
const portEdge = {
|
|
5693
|
+
id: portEdgeId,
|
|
5694
|
+
source: fileNodeId,
|
|
5695
|
+
target: portNode.id,
|
|
5696
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5697
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5698
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5699
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5700
|
+
};
|
|
5701
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5702
|
+
edgesAdded++;
|
|
5703
|
+
}
|
|
5100
5704
|
}
|
|
5101
5705
|
return { nodesAdded, edgesAdded };
|
|
5102
5706
|
}
|
|
@@ -5105,7 +5709,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5105
5709
|
init_cjs_shims();
|
|
5106
5710
|
var import_node_fs17 = require("fs");
|
|
5107
5711
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
5712
|
+
var import_types21 = require("@neat.is/types");
|
|
5108
5713
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5714
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5109
5715
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5110
5716
|
if (depth > max) return [];
|
|
5111
5717
|
const out = [];
|
|
@@ -5122,24 +5728,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5122
5728
|
}
|
|
5123
5729
|
return out;
|
|
5124
5730
|
}
|
|
5731
|
+
function blockBody(content, from) {
|
|
5732
|
+
const open = content.indexOf("{", from);
|
|
5733
|
+
if (open === -1) return null;
|
|
5734
|
+
let depth = 0;
|
|
5735
|
+
for (let i = open; i < content.length; i++) {
|
|
5736
|
+
const ch = content[i];
|
|
5737
|
+
if (ch === "{") depth++;
|
|
5738
|
+
else if (ch === "}") {
|
|
5739
|
+
depth--;
|
|
5740
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
return null;
|
|
5744
|
+
}
|
|
5745
|
+
function lineAt(content, index) {
|
|
5746
|
+
let line = 1;
|
|
5747
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5748
|
+
if (content[i] === "\n") line++;
|
|
5749
|
+
}
|
|
5750
|
+
return line;
|
|
5751
|
+
}
|
|
5125
5752
|
async function addTerraformResources(graph, scanPath) {
|
|
5126
5753
|
let nodesAdded = 0;
|
|
5754
|
+
let edgesAdded = 0;
|
|
5127
5755
|
const files = await walkTfFiles(scanPath);
|
|
5128
5756
|
for (const file of files) {
|
|
5129
5757
|
const content = await import_node_fs17.promises.readFile(file, "utf8");
|
|
5758
|
+
const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, file));
|
|
5759
|
+
const resources = [];
|
|
5760
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5130
5761
|
RESOURCE_RE.lastIndex = 0;
|
|
5131
5762
|
let m;
|
|
5132
5763
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5133
|
-
const
|
|
5764
|
+
const type = m[1];
|
|
5134
5765
|
const name = m[2];
|
|
5135
|
-
const node = makeInfraNode(
|
|
5766
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5136
5767
|
if (!graph.hasNode(node.id)) {
|
|
5137
5768
|
graph.addNode(node.id, node);
|
|
5138
5769
|
nodesAdded++;
|
|
5139
5770
|
}
|
|
5771
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5772
|
+
const resource = {
|
|
5773
|
+
type,
|
|
5774
|
+
name,
|
|
5775
|
+
nodeId: node.id,
|
|
5776
|
+
body: span?.body ?? "",
|
|
5777
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5778
|
+
};
|
|
5779
|
+
resources.push(resource);
|
|
5780
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5781
|
+
}
|
|
5782
|
+
for (const resource of resources) {
|
|
5783
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5784
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5785
|
+
let ref;
|
|
5786
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5787
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5788
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5789
|
+
const target = byKey.get(key);
|
|
5790
|
+
if (!target) continue;
|
|
5791
|
+
if (seen.has(target.nodeId)) continue;
|
|
5792
|
+
seen.add(target.nodeId);
|
|
5793
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5794
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5795
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5796
|
+
const edge = {
|
|
5797
|
+
id: edgeId,
|
|
5798
|
+
source: resource.nodeId,
|
|
5799
|
+
target: target.nodeId,
|
|
5800
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5801
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5802
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5803
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5804
|
+
};
|
|
5805
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5806
|
+
edgesAdded++;
|
|
5807
|
+
}
|
|
5140
5808
|
}
|
|
5141
5809
|
}
|
|
5142
|
-
return { nodesAdded, edgesAdded
|
|
5810
|
+
return { nodesAdded, edgesAdded };
|
|
5143
5811
|
}
|
|
5144
5812
|
|
|
5145
5813
|
// src/extract/infra/k8s.ts
|
|
@@ -5217,11 +5885,11 @@ var import_node_path34 = __toESM(require("path"), 1);
|
|
|
5217
5885
|
init_cjs_shims();
|
|
5218
5886
|
var import_node_fs19 = require("fs");
|
|
5219
5887
|
var import_node_path33 = __toESM(require("path"), 1);
|
|
5220
|
-
var
|
|
5888
|
+
var import_types22 = require("@neat.is/types");
|
|
5221
5889
|
function dropOrphanedFileNodes(graph) {
|
|
5222
5890
|
const orphans = [];
|
|
5223
5891
|
graph.forEachNode((id, attrs) => {
|
|
5224
|
-
if (attrs.type !==
|
|
5892
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5225
5893
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5226
5894
|
orphans.push(id);
|
|
5227
5895
|
}
|
|
@@ -5234,7 +5902,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
5234
5902
|
const toDrop = [];
|
|
5235
5903
|
graph.forEachEdge((id, attrs) => {
|
|
5236
5904
|
const edge = attrs;
|
|
5237
|
-
if (edge.provenance !==
|
|
5905
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5238
5906
|
if (!edge.evidence?.file) return;
|
|
5239
5907
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
5240
5908
|
});
|
|
@@ -5247,7 +5915,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5247
5915
|
const bases = [scanPath, ...serviceDirs];
|
|
5248
5916
|
graph.forEachEdge((id, attrs) => {
|
|
5249
5917
|
const edge = attrs;
|
|
5250
|
-
if (edge.provenance !==
|
|
5918
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5251
5919
|
const evidenceFile = edge.evidence?.file;
|
|
5252
5920
|
if (!evidenceFile) return;
|
|
5253
5921
|
if (import_node_path33.default.isAbsolute(evidenceFile)) {
|
|
@@ -5328,7 +5996,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5328
5996
|
|
|
5329
5997
|
// src/divergences.ts
|
|
5330
5998
|
init_cjs_shims();
|
|
5331
|
-
var
|
|
5999
|
+
var import_types23 = require("@neat.is/types");
|
|
5332
6000
|
function bucketKey(source, target, type) {
|
|
5333
6001
|
return `${type}|${source}|${target}`;
|
|
5334
6002
|
}
|
|
@@ -5336,22 +6004,22 @@ function bucketEdges(graph) {
|
|
|
5336
6004
|
const buckets = /* @__PURE__ */ new Map();
|
|
5337
6005
|
graph.forEachEdge((id, attrs) => {
|
|
5338
6006
|
const e = attrs;
|
|
5339
|
-
const parsed = (0,
|
|
6007
|
+
const parsed = (0, import_types23.parseEdgeId)(id);
|
|
5340
6008
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5341
6009
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5342
6010
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5343
6011
|
switch (provenance) {
|
|
5344
|
-
case
|
|
6012
|
+
case import_types23.Provenance.EXTRACTED:
|
|
5345
6013
|
cur.extracted = e;
|
|
5346
6014
|
break;
|
|
5347
|
-
case
|
|
6015
|
+
case import_types23.Provenance.OBSERVED:
|
|
5348
6016
|
cur.observed = e;
|
|
5349
6017
|
break;
|
|
5350
|
-
case
|
|
6018
|
+
case import_types23.Provenance.INFERRED:
|
|
5351
6019
|
cur.inferred = e;
|
|
5352
6020
|
break;
|
|
5353
6021
|
default:
|
|
5354
|
-
if (e.provenance ===
|
|
6022
|
+
if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
|
|
5355
6023
|
}
|
|
5356
6024
|
buckets.set(key, cur);
|
|
5357
6025
|
});
|
|
@@ -5360,7 +6028,7 @@ function bucketEdges(graph) {
|
|
|
5360
6028
|
function nodeIsFrontier(graph, nodeId) {
|
|
5361
6029
|
if (!graph.hasNode(nodeId)) return false;
|
|
5362
6030
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5363
|
-
return attrs.type ===
|
|
6031
|
+
return attrs.type === import_types23.NodeType.FrontierNode;
|
|
5364
6032
|
}
|
|
5365
6033
|
function clampConfidence(n) {
|
|
5366
6034
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5379,10 +6047,16 @@ function gradedConfidence(edge) {
|
|
|
5379
6047
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5380
6048
|
return clampConfidence(confidenceForEdge(edge));
|
|
5381
6049
|
}
|
|
6050
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6051
|
+
import_types23.EdgeType.CALLS,
|
|
6052
|
+
import_types23.EdgeType.CONNECTS_TO,
|
|
6053
|
+
import_types23.EdgeType.PUBLISHES_TO,
|
|
6054
|
+
import_types23.EdgeType.CONSUMES_FROM
|
|
6055
|
+
]);
|
|
5382
6056
|
function detectMissingDivergences(graph, bucket) {
|
|
5383
6057
|
const out = [];
|
|
5384
|
-
if (bucket.type ===
|
|
5385
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6058
|
+
if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
|
|
6059
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5386
6060
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5387
6061
|
out.push({
|
|
5388
6062
|
type: "missing-observed",
|
|
@@ -5422,7 +6096,7 @@ function declaredHostFor(svc) {
|
|
|
5422
6096
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5423
6097
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5424
6098
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5425
|
-
if (e.type ===
|
|
6099
|
+
if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
|
|
5426
6100
|
return true;
|
|
5427
6101
|
}
|
|
5428
6102
|
}
|
|
@@ -5435,10 +6109,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5435
6109
|
const out = [];
|
|
5436
6110
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5437
6111
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5438
|
-
if (edge.type !==
|
|
5439
|
-
if (edge.provenance !==
|
|
6112
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
6113
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5440
6114
|
const target = graph.getNodeAttributes(edge.target);
|
|
5441
|
-
if (target.type !==
|
|
6115
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5442
6116
|
const observedHost = target.host?.trim();
|
|
5443
6117
|
if (!observedHost) continue;
|
|
5444
6118
|
if (observedHost === declaredHost) continue;
|
|
@@ -5460,10 +6134,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5460
6134
|
const deps = svc.dependencies ?? {};
|
|
5461
6135
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5462
6136
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5463
|
-
if (edge.type !==
|
|
5464
|
-
if (edge.provenance !==
|
|
6137
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
6138
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5465
6139
|
const target = graph.getNodeAttributes(edge.target);
|
|
5466
|
-
if (target.type !==
|
|
6140
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5467
6141
|
for (const pair of compatPairs()) {
|
|
5468
6142
|
if (pair.engine !== target.engine) continue;
|
|
5469
6143
|
const declared = deps[pair.driver];
|
|
@@ -5516,6 +6190,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5516
6190
|
function involvesNode(d, nodeId) {
|
|
5517
6191
|
return d.source === nodeId || d.target === nodeId;
|
|
5518
6192
|
}
|
|
6193
|
+
function suppressHostMismatchHalves(all) {
|
|
6194
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6195
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6196
|
+
for (const d of all) {
|
|
6197
|
+
if (d.type !== "host-mismatch") continue;
|
|
6198
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6199
|
+
declaredHalf.add((0, import_types23.databaseId)(d.extractedHost));
|
|
6200
|
+
}
|
|
6201
|
+
if (observedHalf.size === 0) return all;
|
|
6202
|
+
return all.filter((d) => {
|
|
6203
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6204
|
+
return false;
|
|
6205
|
+
}
|
|
6206
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6207
|
+
return true;
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
5519
6210
|
function computeDivergences(graph, opts = {}) {
|
|
5520
6211
|
const all = [];
|
|
5521
6212
|
const buckets = bucketEdges(graph);
|
|
@@ -5524,12 +6215,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5524
6215
|
}
|
|
5525
6216
|
graph.forEachNode((nodeId, attrs) => {
|
|
5526
6217
|
const n = attrs;
|
|
5527
|
-
if (n.type !==
|
|
6218
|
+
if (n.type !== import_types23.NodeType.ServiceNode) return;
|
|
5528
6219
|
const svc = n;
|
|
5529
6220
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5530
6221
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5531
6222
|
});
|
|
5532
|
-
|
|
6223
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6224
|
+
let filtered = reconciled;
|
|
5533
6225
|
if (opts.type) {
|
|
5534
6226
|
const allowed = opts.type;
|
|
5535
6227
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5557,7 +6249,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5557
6249
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5558
6250
|
return a.target.localeCompare(b.target);
|
|
5559
6251
|
});
|
|
5560
|
-
return
|
|
6252
|
+
return import_types23.DivergenceResultSchema.parse({
|
|
5561
6253
|
divergences: filtered,
|
|
5562
6254
|
totalAffected: filtered.length,
|
|
5563
6255
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5568,7 +6260,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5568
6260
|
init_cjs_shims();
|
|
5569
6261
|
var import_node_fs20 = require("fs");
|
|
5570
6262
|
var import_node_path35 = __toESM(require("path"), 1);
|
|
5571
|
-
var
|
|
6263
|
+
var import_types24 = require("@neat.is/types");
|
|
5572
6264
|
var SCHEMA_VERSION = 4;
|
|
5573
6265
|
function migrateV1ToV2(payload) {
|
|
5574
6266
|
const nodes = payload.graph.nodes;
|
|
@@ -5590,12 +6282,12 @@ function migrateV2ToV3(payload) {
|
|
|
5590
6282
|
for (const edge of edges) {
|
|
5591
6283
|
const attrs = edge.attributes;
|
|
5592
6284
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5593
|
-
attrs.provenance =
|
|
6285
|
+
attrs.provenance = import_types24.Provenance.OBSERVED;
|
|
5594
6286
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5595
6287
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5596
6288
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5597
6289
|
if (type && source && target) {
|
|
5598
|
-
const newId = (0,
|
|
6290
|
+
const newId = (0, import_types24.observedEdgeId)(source, target, type);
|
|
5599
6291
|
attrs.id = newId;
|
|
5600
6292
|
if (edge.key) edge.key = newId;
|
|
5601
6293
|
}
|
|
@@ -5721,7 +6413,7 @@ ${NEAT_OUT_LINE}
|
|
|
5721
6413
|
|
|
5722
6414
|
// src/summary.ts
|
|
5723
6415
|
init_cjs_shims();
|
|
5724
|
-
var
|
|
6416
|
+
var import_types25 = require("@neat.is/types");
|
|
5725
6417
|
function renderOtelEnvBlock() {
|
|
5726
6418
|
return [
|
|
5727
6419
|
"for prod OTel routing, set these in your deploy platform's env:",
|
|
@@ -5731,19 +6423,19 @@ function renderOtelEnvBlock() {
|
|
|
5731
6423
|
}
|
|
5732
6424
|
function findIncompatServices(nodes) {
|
|
5733
6425
|
return nodes.filter(
|
|
5734
|
-
(n) => n.type ===
|
|
6426
|
+
(n) => n.type === import_types25.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
5735
6427
|
);
|
|
5736
6428
|
}
|
|
5737
6429
|
function servicesWithoutObserved(nodes, edges) {
|
|
5738
6430
|
const seen = /* @__PURE__ */ new Set();
|
|
5739
6431
|
for (const e of edges) {
|
|
5740
|
-
if (e.provenance ===
|
|
6432
|
+
if (e.provenance === import_types25.Provenance.OBSERVED) {
|
|
5741
6433
|
seen.add(e.source);
|
|
5742
6434
|
seen.add(e.target);
|
|
5743
6435
|
}
|
|
5744
6436
|
}
|
|
5745
6437
|
return nodes.filter(
|
|
5746
|
-
(n) => n.type ===
|
|
6438
|
+
(n) => n.type === import_types25.NodeType.ServiceNode && !seen.has(n.id)
|
|
5747
6439
|
);
|
|
5748
6440
|
}
|
|
5749
6441
|
function formatDivergence(d) {
|
|
@@ -5817,15 +6509,15 @@ function formatIncompat(inc) {
|
|
|
5817
6509
|
|
|
5818
6510
|
// src/watch.ts
|
|
5819
6511
|
init_cjs_shims();
|
|
5820
|
-
var
|
|
5821
|
-
var
|
|
6512
|
+
var import_node_fs29 = __toESM(require("fs"), 1);
|
|
6513
|
+
var import_node_path46 = __toESM(require("path"), 1);
|
|
5822
6514
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
5823
6515
|
|
|
5824
6516
|
// src/api.ts
|
|
5825
6517
|
init_cjs_shims();
|
|
5826
6518
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5827
6519
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5828
|
-
var
|
|
6520
|
+
var import_types27 = require("@neat.is/types");
|
|
5829
6521
|
|
|
5830
6522
|
// src/extend/index.ts
|
|
5831
6523
|
init_cjs_shims();
|
|
@@ -6276,7 +6968,7 @@ init_cjs_shims();
|
|
|
6276
6968
|
var import_node_fs25 = require("fs");
|
|
6277
6969
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
6278
6970
|
var import_node_path40 = __toESM(require("path"), 1);
|
|
6279
|
-
var
|
|
6971
|
+
var import_types26 = require("@neat.is/types");
|
|
6280
6972
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6281
6973
|
var LOCK_RETRY_MS = 50;
|
|
6282
6974
|
function neatHome() {
|
|
@@ -6530,10 +7222,10 @@ async function readRegistry() {
|
|
|
6530
7222
|
throw err;
|
|
6531
7223
|
}
|
|
6532
7224
|
const parsed = JSON.parse(raw);
|
|
6533
|
-
return
|
|
7225
|
+
return import_types26.RegistryFileSchema.parse(parsed);
|
|
6534
7226
|
}
|
|
6535
7227
|
async function writeRegistry(reg) {
|
|
6536
|
-
const validated =
|
|
7228
|
+
const validated = import_types26.RegistryFileSchema.parse(reg);
|
|
6537
7229
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6538
7230
|
}
|
|
6539
7231
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -6845,6 +7537,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6845
7537
|
}
|
|
6846
7538
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6847
7539
|
});
|
|
7540
|
+
scope.get(
|
|
7541
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7542
|
+
async (req, reply) => {
|
|
7543
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7544
|
+
if (!proj) return;
|
|
7545
|
+
const { nodeId } = req.params;
|
|
7546
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7547
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7548
|
+
}
|
|
7549
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7550
|
+
}
|
|
7551
|
+
);
|
|
6848
7552
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6849
7553
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6850
7554
|
if (!proj) return;
|
|
@@ -6853,11 +7557,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6853
7557
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6854
7558
|
const parsed = [];
|
|
6855
7559
|
for (const c of candidates) {
|
|
6856
|
-
const r =
|
|
7560
|
+
const r = import_types27.DivergenceTypeSchema.safeParse(c);
|
|
6857
7561
|
if (!r.success) {
|
|
6858
7562
|
return reply.code(400).send({
|
|
6859
7563
|
error: `unknown divergence type "${c}"`,
|
|
6860
|
-
allowed:
|
|
7564
|
+
allowed: import_types27.DivergenceTypeSchema.options
|
|
6861
7565
|
});
|
|
6862
7566
|
}
|
|
6863
7567
|
parsed.push(r.data);
|
|
@@ -6905,23 +7609,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6905
7609
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6906
7610
|
return { count: sliced.length, total, events: sliced };
|
|
6907
7611
|
});
|
|
7612
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
7613
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7614
|
+
if (!proj) return;
|
|
7615
|
+
const { nodeId } = req.params;
|
|
7616
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7617
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7618
|
+
return;
|
|
7619
|
+
}
|
|
7620
|
+
const epath = errorsPathFor(proj);
|
|
7621
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7622
|
+
const events = await readErrorEvents(epath);
|
|
7623
|
+
const filtered = events.filter(
|
|
7624
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7625
|
+
);
|
|
7626
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7627
|
+
};
|
|
6908
7628
|
scope.get(
|
|
6909
7629
|
"/incidents/:nodeId",
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6916
|
-
}
|
|
6917
|
-
const epath = errorsPathFor(proj);
|
|
6918
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6919
|
-
const events = await readErrorEvents(epath);
|
|
6920
|
-
const filtered = events.filter(
|
|
6921
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6922
|
-
);
|
|
6923
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6924
|
-
}
|
|
7630
|
+
incidentHistoryHandler
|
|
7631
|
+
);
|
|
7632
|
+
scope.get(
|
|
7633
|
+
"/graph/incident-history/:nodeId",
|
|
7634
|
+
incidentHistoryHandler
|
|
6925
7635
|
);
|
|
6926
7636
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6927
7637
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6930,16 +7640,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6930
7640
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6931
7641
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6932
7642
|
}
|
|
6933
|
-
let errorEvent;
|
|
6934
7643
|
const epath = errorsPathFor(proj);
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
7644
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7645
|
+
let errorEvent;
|
|
7646
|
+
if (req.query.errorId) {
|
|
7647
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6938
7648
|
if (!errorEvent) {
|
|
6939
7649
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6940
7650
|
}
|
|
6941
7651
|
}
|
|
6942
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7652
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6943
7653
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6944
7654
|
return result;
|
|
6945
7655
|
});
|
|
@@ -7070,7 +7780,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7070
7780
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
7071
7781
|
let violations = await log.readAll();
|
|
7072
7782
|
if (req.query.severity) {
|
|
7073
|
-
const sev =
|
|
7783
|
+
const sev = import_types27.PolicySeveritySchema.safeParse(req.query.severity);
|
|
7074
7784
|
if (!sev.success) {
|
|
7075
7785
|
return reply.code(400).send({
|
|
7076
7786
|
error: "invalid severity",
|
|
@@ -7084,10 +7794,32 @@ function registerRoutes(scope, ctx) {
|
|
|
7084
7794
|
}
|
|
7085
7795
|
return { violations };
|
|
7086
7796
|
});
|
|
7797
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7798
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7799
|
+
if (!proj) return;
|
|
7800
|
+
const nodeId = req.query.node;
|
|
7801
|
+
if (!nodeId) {
|
|
7802
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7803
|
+
}
|
|
7804
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7805
|
+
let policies = [];
|
|
7806
|
+
if (policyPath) {
|
|
7807
|
+
try {
|
|
7808
|
+
policies = await loadPolicyFile(policyPath);
|
|
7809
|
+
} catch (err) {
|
|
7810
|
+
return reply.code(400).send({
|
|
7811
|
+
error: "policy.json failed to parse",
|
|
7812
|
+
details: err.message
|
|
7813
|
+
});
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7817
|
+
return { node: nodeId, applicable };
|
|
7818
|
+
});
|
|
7087
7819
|
scope.post("/policies/check", async (req, reply) => {
|
|
7088
7820
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7089
7821
|
if (!proj) return;
|
|
7090
|
-
const parsed =
|
|
7822
|
+
const parsed = import_types27.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
7091
7823
|
if (!parsed.success) {
|
|
7092
7824
|
return reply.code(400).send({
|
|
7093
7825
|
error: "invalid /policies/check body",
|
|
@@ -7346,12 +8078,111 @@ async function buildApi(opts) {
|
|
|
7346
8078
|
// src/watch.ts
|
|
7347
8079
|
init_auth();
|
|
7348
8080
|
init_otel();
|
|
7349
|
-
init_otel_grpc();
|
|
7350
8081
|
|
|
7351
|
-
// src/
|
|
8082
|
+
// src/daemon.ts
|
|
8083
|
+
init_cjs_shims();
|
|
8084
|
+
var import_node_fs27 = require("fs");
|
|
8085
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
8086
|
+
var import_node_module = require("module");
|
|
8087
|
+
init_otel();
|
|
8088
|
+
init_auth();
|
|
8089
|
+
|
|
8090
|
+
// src/unrouted.ts
|
|
7352
8091
|
init_cjs_shims();
|
|
7353
8092
|
var import_node_fs26 = require("fs");
|
|
7354
8093
|
var import_node_path43 = __toESM(require("path"), 1);
|
|
8094
|
+
|
|
8095
|
+
// src/daemon.ts
|
|
8096
|
+
var import_types28 = require("@neat.is/types");
|
|
8097
|
+
function daemonJsonPath(scanPath) {
|
|
8098
|
+
return import_node_path44.default.join(scanPath, "neat-out", "daemon.json");
|
|
8099
|
+
}
|
|
8100
|
+
function daemonsDiscoveryDir(home) {
|
|
8101
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
8102
|
+
return import_node_path44.default.join(base, "daemons");
|
|
8103
|
+
}
|
|
8104
|
+
function daemonDiscoveryPath(project, home) {
|
|
8105
|
+
return import_node_path44.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
8106
|
+
}
|
|
8107
|
+
function sanitizeDiscoveryName(project) {
|
|
8108
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
8109
|
+
}
|
|
8110
|
+
function neatHomeFromEnv() {
|
|
8111
|
+
const env = process.env.NEAT_HOME;
|
|
8112
|
+
if (env && env.length > 0) return import_node_path44.default.resolve(env);
|
|
8113
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8114
|
+
return import_node_path44.default.join(home, ".neat");
|
|
8115
|
+
}
|
|
8116
|
+
async function readDaemonRecord(scanPath) {
|
|
8117
|
+
try {
|
|
8118
|
+
const raw = await import_node_fs27.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
8119
|
+
const parsed = JSON.parse(raw);
|
|
8120
|
+
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
8121
|
+
return parsed;
|
|
8122
|
+
}
|
|
8123
|
+
return null;
|
|
8124
|
+
} catch {
|
|
8125
|
+
return null;
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8128
|
+
function resolveNeatVersion() {
|
|
8129
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
8130
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
8131
|
+
}
|
|
8132
|
+
try {
|
|
8133
|
+
const req = (0, import_node_module.createRequire)(importMetaUrl);
|
|
8134
|
+
const pkg = req("../package.json");
|
|
8135
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
8136
|
+
} catch {
|
|
8137
|
+
return "0.0.0";
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
8140
|
+
async function writeDaemonRecord(record, home) {
|
|
8141
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
8142
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
8143
|
+
try {
|
|
8144
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
8145
|
+
} catch (err) {
|
|
8146
|
+
console.warn(
|
|
8147
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
8148
|
+
);
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
async function clearDaemonRecord(record, home) {
|
|
8152
|
+
try {
|
|
8153
|
+
const stopped = { ...record, status: "stopped" };
|
|
8154
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
8155
|
+
} catch {
|
|
8156
|
+
}
|
|
8157
|
+
try {
|
|
8158
|
+
await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
8159
|
+
} catch {
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
function portFromListenAddress(address, fallback) {
|
|
8163
|
+
try {
|
|
8164
|
+
const port = new URL(address).port;
|
|
8165
|
+
const n = Number.parseInt(port, 10);
|
|
8166
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
8167
|
+
} catch {
|
|
8168
|
+
}
|
|
8169
|
+
return fallback;
|
|
8170
|
+
}
|
|
8171
|
+
function resolveHost(opts, authTokenSet) {
|
|
8172
|
+
if (opts.host && opts.host.length > 0) return opts.host;
|
|
8173
|
+
const env = process.env.HOST;
|
|
8174
|
+
if (env && env.length > 0) return env;
|
|
8175
|
+
if (!authTokenSet) return "127.0.0.1";
|
|
8176
|
+
return "0.0.0.0";
|
|
8177
|
+
}
|
|
8178
|
+
|
|
8179
|
+
// src/watch.ts
|
|
8180
|
+
init_otel_grpc();
|
|
8181
|
+
|
|
8182
|
+
// src/search.ts
|
|
8183
|
+
init_cjs_shims();
|
|
8184
|
+
var import_node_fs28 = require("fs");
|
|
8185
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
7355
8186
|
var import_node_crypto3 = require("crypto");
|
|
7356
8187
|
var DEFAULT_LIMIT = 10;
|
|
7357
8188
|
var NOMIC_DIM = 768;
|
|
@@ -7481,7 +8312,7 @@ async function pickEmbedder() {
|
|
|
7481
8312
|
}
|
|
7482
8313
|
async function readCache(cachePath) {
|
|
7483
8314
|
try {
|
|
7484
|
-
const raw = await
|
|
8315
|
+
const raw = await import_node_fs28.promises.readFile(cachePath, "utf8");
|
|
7485
8316
|
const parsed = JSON.parse(raw);
|
|
7486
8317
|
if (parsed.version !== 1) return null;
|
|
7487
8318
|
return parsed;
|
|
@@ -7490,8 +8321,8 @@ async function readCache(cachePath) {
|
|
|
7490
8321
|
}
|
|
7491
8322
|
}
|
|
7492
8323
|
async function writeCache(cachePath, cache) {
|
|
7493
|
-
await
|
|
7494
|
-
await
|
|
8324
|
+
await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(cachePath), { recursive: true });
|
|
8325
|
+
await import_node_fs28.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
7495
8326
|
}
|
|
7496
8327
|
var VectorIndex = class {
|
|
7497
8328
|
constructor(embedder, cachePath) {
|
|
@@ -7639,6 +8470,7 @@ async function buildSearchIndex(graph, options = {}) {
|
|
|
7639
8470
|
var ALL_PHASES = [
|
|
7640
8471
|
"services",
|
|
7641
8472
|
"aliases",
|
|
8473
|
+
"files",
|
|
7642
8474
|
"imports",
|
|
7643
8475
|
"databases",
|
|
7644
8476
|
"configs",
|
|
@@ -7647,8 +8479,8 @@ var ALL_PHASES = [
|
|
|
7647
8479
|
];
|
|
7648
8480
|
function classifyChange(relPath) {
|
|
7649
8481
|
const phases = /* @__PURE__ */ new Set();
|
|
7650
|
-
const base =
|
|
7651
|
-
const segments = relPath.split(
|
|
8482
|
+
const base = import_node_path46.default.basename(relPath).toLowerCase();
|
|
8483
|
+
const segments = relPath.split(import_node_path46.default.sep).map((s) => s.toLowerCase());
|
|
7652
8484
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
7653
8485
|
phases.add("services");
|
|
7654
8486
|
phases.add("aliases");
|
|
@@ -7663,6 +8495,7 @@ function classifyChange(relPath) {
|
|
|
7663
8495
|
phases.add("aliases");
|
|
7664
8496
|
}
|
|
7665
8497
|
if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
|
|
8498
|
+
phases.add("files");
|
|
7666
8499
|
phases.add("imports");
|
|
7667
8500
|
phases.add("calls");
|
|
7668
8501
|
}
|
|
@@ -7685,6 +8518,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
7685
8518
|
if (phases.has("aliases")) {
|
|
7686
8519
|
await addServiceAliases(graph, scanPath, services);
|
|
7687
8520
|
}
|
|
8521
|
+
if (phases.has("files")) {
|
|
8522
|
+
const r = await addFiles(graph, services);
|
|
8523
|
+
nodesAdded += r.nodesAdded;
|
|
8524
|
+
edgesAdded += r.edgesAdded;
|
|
8525
|
+
}
|
|
7688
8526
|
if (phases.has("imports")) {
|
|
7689
8527
|
const r = await addImports(graph, services);
|
|
7690
8528
|
nodesAdded += r.nodesAdded;
|
|
@@ -7719,6 +8557,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
7719
8557
|
durationMs: Date.now() - started
|
|
7720
8558
|
};
|
|
7721
8559
|
}
|
|
8560
|
+
var DEFAULT_WATCH_WEB_PORT = 6328;
|
|
7722
8561
|
var IGNORED_WATCH_GLOBS = [
|
|
7723
8562
|
"**/node_modules/**",
|
|
7724
8563
|
"**/.git/**",
|
|
@@ -7762,16 +8601,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
7762
8601
|
if (count >= limit) return;
|
|
7763
8602
|
let entries;
|
|
7764
8603
|
try {
|
|
7765
|
-
entries =
|
|
8604
|
+
entries = import_node_fs29.default.readdirSync(dir, { withFileTypes: true });
|
|
7766
8605
|
} catch {
|
|
7767
8606
|
return;
|
|
7768
8607
|
}
|
|
7769
8608
|
for (const e of entries) {
|
|
7770
8609
|
if (count >= limit) return;
|
|
7771
8610
|
if (!e.isDirectory()) continue;
|
|
7772
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
8611
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path46.default.join(dir, e.name) + import_node_path46.default.sep))) continue;
|
|
7773
8612
|
count++;
|
|
7774
|
-
if (depth < 2) visit(
|
|
8613
|
+
if (depth < 2) visit(import_node_path46.default.join(dir, e.name), depth + 1);
|
|
7775
8614
|
}
|
|
7776
8615
|
};
|
|
7777
8616
|
visit(scanPath, 0);
|
|
@@ -7789,8 +8628,8 @@ async function startWatch(graph, opts) {
|
|
|
7789
8628
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
7790
8629
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
7791
8630
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
7792
|
-
const policyFilePath =
|
|
7793
|
-
const policyViolationsPath =
|
|
8631
|
+
const policyFilePath = import_node_path46.default.join(opts.scanPath, "policy.json");
|
|
8632
|
+
const policyViolationsPath = import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
7794
8633
|
let policies = [];
|
|
7795
8634
|
try {
|
|
7796
8635
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -7841,7 +8680,7 @@ async function startWatch(graph, opts) {
|
|
|
7841
8680
|
assertBindAuthority(host, auth.authToken);
|
|
7842
8681
|
const port = opts.port ?? 8080;
|
|
7843
8682
|
const otelPort = opts.otelPort ?? 4318;
|
|
7844
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
8683
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "embeddings.json");
|
|
7845
8684
|
let searchIndex;
|
|
7846
8685
|
try {
|
|
7847
8686
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -7859,7 +8698,7 @@ async function startWatch(graph, opts) {
|
|
|
7859
8698
|
// Paths are derived from the explicit options the watch caller passes
|
|
7860
8699
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
7861
8700
|
// fields so the registry shape is complete.
|
|
7862
|
-
...pathsForProject(projectName,
|
|
8701
|
+
...pathsForProject(projectName, import_node_path46.default.dirname(opts.outPath)),
|
|
7863
8702
|
snapshotPath: opts.outPath,
|
|
7864
8703
|
errorsPath: opts.errorsPath,
|
|
7865
8704
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -7867,7 +8706,7 @@ async function startWatch(graph, opts) {
|
|
|
7867
8706
|
searchIndex
|
|
7868
8707
|
});
|
|
7869
8708
|
const api = await buildApi({ projects: registry });
|
|
7870
|
-
await api.listen({ port, host });
|
|
8709
|
+
const restAddress = await api.listen({ port, host });
|
|
7871
8710
|
console.log(`neat-core listening on http://${host}:${port}`);
|
|
7872
8711
|
console.log(` scan path: ${opts.scanPath} (watching for changes)`);
|
|
7873
8712
|
console.log(` snapshot path: ${opts.outPath}`);
|
|
@@ -7880,10 +8719,41 @@ async function startWatch(graph, opts) {
|
|
|
7880
8719
|
writeErrorEventInline: false,
|
|
7881
8720
|
onPolicyTrigger
|
|
7882
8721
|
});
|
|
7883
|
-
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
|
|
8722
|
+
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
|
|
7884
8723
|
const otelHttp = await buildOtelReceiver({ onSpan, onErrorSpanSync });
|
|
7885
|
-
await otelHttp
|
|
7886
|
-
|
|
8724
|
+
const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
|
|
8725
|
+
const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
|
|
8726
|
+
console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
|
|
8727
|
+
const boundRestPort = portFromListenAddress(restAddress, port);
|
|
8728
|
+
const daemonRecord = {
|
|
8729
|
+
project: projectName,
|
|
8730
|
+
projectPath: opts.scanPath,
|
|
8731
|
+
pid: process.pid,
|
|
8732
|
+
status: "running",
|
|
8733
|
+
ports: {
|
|
8734
|
+
rest: boundRestPort,
|
|
8735
|
+
otlp: boundOtelPort,
|
|
8736
|
+
// watch serves no dashboard of its own; record the canonical web port so
|
|
8737
|
+
// the record shape is valid. Only ports.otlp is load-bearing here.
|
|
8738
|
+
web: DEFAULT_WATCH_WEB_PORT
|
|
8739
|
+
},
|
|
8740
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8741
|
+
neatVersion: resolveNeatVersion()
|
|
8742
|
+
};
|
|
8743
|
+
try {
|
|
8744
|
+
await writeDaemonRecord(daemonRecord);
|
|
8745
|
+
console.log(
|
|
8746
|
+
`neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
|
|
8747
|
+
);
|
|
8748
|
+
} catch (err) {
|
|
8749
|
+
await api.close().catch(() => {
|
|
8750
|
+
});
|
|
8751
|
+
await otelHttp.close().catch(() => {
|
|
8752
|
+
});
|
|
8753
|
+
throw new Error(
|
|
8754
|
+
`neat watch: failed to write daemon.json \u2014 ${err.message}`
|
|
8755
|
+
);
|
|
8756
|
+
}
|
|
7887
8757
|
let grpcReceiver = null;
|
|
7888
8758
|
if (opts.otelGrpc) {
|
|
7889
8759
|
const grpcPort = opts.otelGrpcPort ?? 4317;
|
|
@@ -7946,9 +8816,9 @@ async function startWatch(graph, opts) {
|
|
|
7946
8816
|
};
|
|
7947
8817
|
const onPath = (absPath) => {
|
|
7948
8818
|
if (shouldIgnore(absPath)) return;
|
|
7949
|
-
const rel =
|
|
8819
|
+
const rel = import_node_path46.default.relative(opts.scanPath, absPath);
|
|
7950
8820
|
if (!rel || rel.startsWith("..")) return;
|
|
7951
|
-
pendingPaths.add(rel.split(
|
|
8821
|
+
pendingPaths.add(rel.split(import_node_path46.default.sep).join("/"));
|
|
7952
8822
|
const phases = classifyChange(rel);
|
|
7953
8823
|
if (phases.size === 0) {
|
|
7954
8824
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -7996,14 +8866,16 @@ async function startWatch(graph, opts) {
|
|
|
7996
8866
|
await api.close();
|
|
7997
8867
|
await otelHttp.close();
|
|
7998
8868
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
8869
|
+
await clearDaemonRecord(daemonRecord).catch(() => {
|
|
8870
|
+
});
|
|
7999
8871
|
};
|
|
8000
8872
|
return { api, stop };
|
|
8001
8873
|
}
|
|
8002
8874
|
|
|
8003
8875
|
// src/deploy/detect.ts
|
|
8004
8876
|
init_cjs_shims();
|
|
8005
|
-
var
|
|
8006
|
-
var
|
|
8877
|
+
var import_node_fs30 = require("fs");
|
|
8878
|
+
var import_node_path47 = __toESM(require("path"), 1);
|
|
8007
8879
|
var import_node_child_process2 = require("child_process");
|
|
8008
8880
|
var import_node_crypto4 = require("crypto");
|
|
8009
8881
|
function generateToken() {
|
|
@@ -8103,21 +8975,21 @@ async function runDeploy(opts = {}) {
|
|
|
8103
8975
|
const token = generateToken();
|
|
8104
8976
|
switch (substrate) {
|
|
8105
8977
|
case "docker-compose": {
|
|
8106
|
-
const artifactPath =
|
|
8978
|
+
const artifactPath = import_node_path47.default.join(cwd, "docker-compose.neat.yml");
|
|
8107
8979
|
const contents = emitDockerCompose(cwd);
|
|
8108
|
-
await
|
|
8980
|
+
await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
|
|
8109
8981
|
return {
|
|
8110
8982
|
substrate,
|
|
8111
8983
|
artifactPath,
|
|
8112
8984
|
token,
|
|
8113
8985
|
contents,
|
|
8114
|
-
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${
|
|
8986
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path47.default.basename(artifactPath)} up -d`
|
|
8115
8987
|
};
|
|
8116
8988
|
}
|
|
8117
8989
|
case "systemd": {
|
|
8118
|
-
const artifactPath =
|
|
8990
|
+
const artifactPath = import_node_path47.default.join(cwd, "neat.service");
|
|
8119
8991
|
const contents = emitSystemdUnit(cwd);
|
|
8120
|
-
await
|
|
8992
|
+
await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
|
|
8121
8993
|
return {
|
|
8122
8994
|
substrate,
|
|
8123
8995
|
artifactPath,
|
|
@@ -8150,8 +9022,8 @@ init_cjs_shims();
|
|
|
8150
9022
|
|
|
8151
9023
|
// src/installers/javascript.ts
|
|
8152
9024
|
init_cjs_shims();
|
|
8153
|
-
var
|
|
8154
|
-
var
|
|
9025
|
+
var import_node_fs31 = require("fs");
|
|
9026
|
+
var import_node_path48 = __toESM(require("path"), 1);
|
|
8155
9027
|
var import_semver2 = __toESM(require("semver"), 1);
|
|
8156
9028
|
|
|
8157
9029
|
// src/installers/templates.ts
|
|
@@ -8747,15 +9619,15 @@ var OTEL_ENV = {
|
|
|
8747
9619
|
value: "http://localhost:4318/projects/<project>/v1/traces"
|
|
8748
9620
|
};
|
|
8749
9621
|
function serviceNodeName(pkg, serviceDir) {
|
|
8750
|
-
return pkg.name ??
|
|
9622
|
+
return pkg.name ?? import_node_path48.default.basename(serviceDir);
|
|
8751
9623
|
}
|
|
8752
9624
|
function projectToken(pkg, serviceDir, project) {
|
|
8753
9625
|
if (project && project.length > 0) return project;
|
|
8754
|
-
return pkg.name ??
|
|
9626
|
+
return pkg.name ?? import_node_path48.default.basename(serviceDir);
|
|
8755
9627
|
}
|
|
8756
9628
|
async function readJsonFile(p) {
|
|
8757
9629
|
try {
|
|
8758
|
-
const raw = await
|
|
9630
|
+
const raw = await import_node_fs31.promises.readFile(p, "utf8");
|
|
8759
9631
|
return JSON.parse(raw);
|
|
8760
9632
|
} catch {
|
|
8761
9633
|
return null;
|
|
@@ -8764,16 +9636,16 @@ async function readJsonFile(p) {
|
|
|
8764
9636
|
async function detectRuntimeKind(pkgRoot, pkg) {
|
|
8765
9637
|
const deps = allDeps(pkg);
|
|
8766
9638
|
if ("react-native" in deps || "expo" in deps) return "react-native";
|
|
8767
|
-
const appJson = await readJsonFile(
|
|
9639
|
+
const appJson = await readJsonFile(import_node_path48.default.join(pkgRoot, "app.json"));
|
|
8768
9640
|
if (appJson && typeof appJson === "object" && "expo" in appJson) {
|
|
8769
9641
|
return "react-native";
|
|
8770
9642
|
}
|
|
8771
|
-
if (await exists3(
|
|
9643
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
|
|
8772
9644
|
return "browser-bundle";
|
|
8773
9645
|
}
|
|
8774
|
-
if (await exists3(
|
|
8775
|
-
if (await exists3(
|
|
8776
|
-
if (await exists3(
|
|
9646
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
|
|
9647
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "bun.lockb"))) return "bun";
|
|
9648
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path48.default.join(pkgRoot, "deno.lock"))) {
|
|
8777
9649
|
return "deno";
|
|
8778
9650
|
}
|
|
8779
9651
|
const engines = pkg.engines ?? {};
|
|
@@ -8782,7 +9654,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
|
|
|
8782
9654
|
}
|
|
8783
9655
|
async function readPackageJson2(serviceDir) {
|
|
8784
9656
|
try {
|
|
8785
|
-
const raw = await
|
|
9657
|
+
const raw = await import_node_fs31.promises.readFile(import_node_path48.default.join(serviceDir, "package.json"), "utf8");
|
|
8786
9658
|
return JSON.parse(raw);
|
|
8787
9659
|
} catch {
|
|
8788
9660
|
return null;
|
|
@@ -8790,7 +9662,7 @@ async function readPackageJson2(serviceDir) {
|
|
|
8790
9662
|
}
|
|
8791
9663
|
async function exists3(p) {
|
|
8792
9664
|
try {
|
|
8793
|
-
await
|
|
9665
|
+
await import_node_fs31.promises.stat(p);
|
|
8794
9666
|
return true;
|
|
8795
9667
|
} catch {
|
|
8796
9668
|
return false;
|
|
@@ -8798,7 +9670,7 @@ async function exists3(p) {
|
|
|
8798
9670
|
}
|
|
8799
9671
|
async function readFileMaybe(p) {
|
|
8800
9672
|
try {
|
|
8801
|
-
return await
|
|
9673
|
+
return await import_node_fs31.promises.readFile(p, "utf8");
|
|
8802
9674
|
} catch {
|
|
8803
9675
|
return null;
|
|
8804
9676
|
}
|
|
@@ -8826,7 +9698,7 @@ function needsVersionUpgrade(installed, expected) {
|
|
|
8826
9698
|
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
8827
9699
|
async function findNextConfig(serviceDir) {
|
|
8828
9700
|
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
8829
|
-
const candidate =
|
|
9701
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8830
9702
|
if (await exists3(candidate)) return candidate;
|
|
8831
9703
|
}
|
|
8832
9704
|
return null;
|
|
@@ -8852,9 +9724,23 @@ var WEB_FRAMEWORK_DEPS = [
|
|
|
8852
9724
|
"polka",
|
|
8853
9725
|
"micro"
|
|
8854
9726
|
];
|
|
8855
|
-
|
|
9727
|
+
var WORKER_FRAMEWORK_DEPS = [
|
|
9728
|
+
"bullmq",
|
|
9729
|
+
"bull",
|
|
9730
|
+
"bee-queue",
|
|
9731
|
+
"agenda",
|
|
9732
|
+
"kafkajs",
|
|
9733
|
+
"amqplib",
|
|
9734
|
+
"amqp-connection-manager",
|
|
9735
|
+
"nats",
|
|
9736
|
+
"node-resque",
|
|
9737
|
+
"pg-boss",
|
|
9738
|
+
"graphile-worker"
|
|
9739
|
+
];
|
|
9740
|
+
var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
|
|
9741
|
+
function appFrameworkDependencies(pkg) {
|
|
8856
9742
|
const deps = allDeps(pkg);
|
|
8857
|
-
return
|
|
9743
|
+
return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
|
|
8858
9744
|
}
|
|
8859
9745
|
function uninstrumentedLibraries(pkg) {
|
|
8860
9746
|
const deps = allDeps(pkg);
|
|
@@ -8881,7 +9767,7 @@ function hasRemixDependency(pkg) {
|
|
|
8881
9767
|
}
|
|
8882
9768
|
async function findRemixEntry(serviceDir) {
|
|
8883
9769
|
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
8884
|
-
const candidate =
|
|
9770
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8885
9771
|
if (await exists3(candidate)) return candidate;
|
|
8886
9772
|
}
|
|
8887
9773
|
return null;
|
|
@@ -8893,14 +9779,14 @@ function hasSvelteKitDependency(pkg) {
|
|
|
8893
9779
|
}
|
|
8894
9780
|
async function findSvelteKitHooks(serviceDir) {
|
|
8895
9781
|
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
8896
|
-
const candidate =
|
|
9782
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8897
9783
|
if (await exists3(candidate)) return candidate;
|
|
8898
9784
|
}
|
|
8899
9785
|
return null;
|
|
8900
9786
|
}
|
|
8901
9787
|
async function findSvelteKitConfig(serviceDir) {
|
|
8902
9788
|
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
8903
|
-
const candidate =
|
|
9789
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8904
9790
|
if (await exists3(candidate)) return candidate;
|
|
8905
9791
|
}
|
|
8906
9792
|
return null;
|
|
@@ -8911,7 +9797,7 @@ function hasNuxtDependency(pkg) {
|
|
|
8911
9797
|
}
|
|
8912
9798
|
async function findNuxtConfig(serviceDir) {
|
|
8913
9799
|
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
8914
|
-
const candidate =
|
|
9800
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8915
9801
|
if (await exists3(candidate)) return candidate;
|
|
8916
9802
|
}
|
|
8917
9803
|
return null;
|
|
@@ -8922,7 +9808,7 @@ function hasAstroDependency(pkg) {
|
|
|
8922
9808
|
}
|
|
8923
9809
|
async function findAstroConfig(serviceDir) {
|
|
8924
9810
|
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
8925
|
-
const candidate =
|
|
9811
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8926
9812
|
if (await exists3(candidate)) return candidate;
|
|
8927
9813
|
}
|
|
8928
9814
|
return null;
|
|
@@ -8936,15 +9822,15 @@ function parseNextMajor(range) {
|
|
|
8936
9822
|
return Number.isFinite(n) ? n : null;
|
|
8937
9823
|
}
|
|
8938
9824
|
async function isTypeScriptProject(serviceDir) {
|
|
8939
|
-
return exists3(
|
|
9825
|
+
return exists3(import_node_path48.default.join(serviceDir, "tsconfig.json"));
|
|
8940
9826
|
}
|
|
8941
9827
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
8942
9828
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
8943
9829
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
8944
|
-
var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
|
|
9830
|
+
var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
|
|
8945
9831
|
(name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
|
|
8946
9832
|
);
|
|
8947
|
-
var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
|
|
9833
|
+
var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
|
|
8948
9834
|
(name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
|
|
8949
9835
|
);
|
|
8950
9836
|
var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
|
|
@@ -8985,7 +9871,7 @@ function entryFromScript(script) {
|
|
|
8985
9871
|
}
|
|
8986
9872
|
async function resolveEntry(serviceDir, pkg) {
|
|
8987
9873
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
8988
|
-
const candidate =
|
|
9874
|
+
const candidate = import_node_path48.default.resolve(serviceDir, pkg.main);
|
|
8989
9875
|
if (await exists3(candidate)) return candidate;
|
|
8990
9876
|
}
|
|
8991
9877
|
if (pkg.bin) {
|
|
@@ -8999,40 +9885,40 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
8999
9885
|
if (typeof first === "string") binEntry = first;
|
|
9000
9886
|
}
|
|
9001
9887
|
if (binEntry) {
|
|
9002
|
-
const candidate =
|
|
9888
|
+
const candidate = import_node_path48.default.resolve(serviceDir, binEntry);
|
|
9003
9889
|
if (await exists3(candidate)) return candidate;
|
|
9004
9890
|
}
|
|
9005
9891
|
}
|
|
9006
9892
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
9007
9893
|
if (startEntry) {
|
|
9008
|
-
const candidate =
|
|
9894
|
+
const candidate = import_node_path48.default.resolve(serviceDir, startEntry);
|
|
9009
9895
|
if (await exists3(candidate)) return candidate;
|
|
9010
9896
|
}
|
|
9011
9897
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
9012
9898
|
if (devEntry) {
|
|
9013
|
-
const candidate =
|
|
9899
|
+
const candidate = import_node_path48.default.resolve(serviceDir, devEntry);
|
|
9014
9900
|
if (await exists3(candidate)) return candidate;
|
|
9015
9901
|
}
|
|
9016
9902
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
9017
|
-
const candidate =
|
|
9903
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9018
9904
|
if (await exists3(candidate)) return candidate;
|
|
9019
9905
|
}
|
|
9020
9906
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
9021
|
-
const candidate =
|
|
9907
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9022
9908
|
if (await exists3(candidate)) return candidate;
|
|
9023
9909
|
}
|
|
9024
9910
|
for (const rel of ROOT_NAMED_CANDIDATES) {
|
|
9025
|
-
const candidate =
|
|
9911
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9026
9912
|
if (await exists3(candidate)) return candidate;
|
|
9027
9913
|
}
|
|
9028
9914
|
for (const name of INDEX_CANDIDATES) {
|
|
9029
|
-
const candidate =
|
|
9915
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
9030
9916
|
if (await exists3(candidate)) return candidate;
|
|
9031
9917
|
}
|
|
9032
9918
|
return null;
|
|
9033
9919
|
}
|
|
9034
9920
|
function dispatchEntry(entryFile, pkg) {
|
|
9035
|
-
const ext =
|
|
9921
|
+
const ext = import_node_path48.default.extname(entryFile).toLowerCase();
|
|
9036
9922
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
9037
9923
|
if (ext === ".mjs") return "esm";
|
|
9038
9924
|
if (ext === ".cjs") return "cjs";
|
|
@@ -9049,9 +9935,9 @@ function otelInitContents(flavor) {
|
|
|
9049
9935
|
return OTEL_INIT_CJS;
|
|
9050
9936
|
}
|
|
9051
9937
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
9052
|
-
let rel =
|
|
9938
|
+
let rel = import_node_path48.default.relative(import_node_path48.default.dirname(entryFile), otelInitFile);
|
|
9053
9939
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
9054
|
-
rel = rel.split(
|
|
9940
|
+
rel = rel.split(import_node_path48.default.sep).join("/");
|
|
9055
9941
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
9056
9942
|
if (flavor === "esm") return `import '${rel}'`;
|
|
9057
9943
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -9064,23 +9950,23 @@ function lineIsOtelInjection(line) {
|
|
|
9064
9950
|
}
|
|
9065
9951
|
async function detectsSrcLayout(serviceDir) {
|
|
9066
9952
|
const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
|
|
9067
|
-
exists3(
|
|
9068
|
-
exists3(
|
|
9069
|
-
exists3(
|
|
9070
|
-
exists3(
|
|
9953
|
+
exists3(import_node_path48.default.join(serviceDir, "src", "app")),
|
|
9954
|
+
exists3(import_node_path48.default.join(serviceDir, "src", "pages")),
|
|
9955
|
+
exists3(import_node_path48.default.join(serviceDir, "app")),
|
|
9956
|
+
exists3(import_node_path48.default.join(serviceDir, "pages"))
|
|
9071
9957
|
]);
|
|
9072
9958
|
return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
|
|
9073
9959
|
}
|
|
9074
9960
|
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
|
|
9075
9961
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9076
9962
|
const srcLayout = await detectsSrcLayout(serviceDir);
|
|
9077
|
-
const baseDir = srcLayout ?
|
|
9078
|
-
const instrumentationFile =
|
|
9079
|
-
const instrumentationNodeFile =
|
|
9963
|
+
const baseDir = srcLayout ? import_node_path48.default.join(serviceDir, "src") : serviceDir;
|
|
9964
|
+
const instrumentationFile = import_node_path48.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
9965
|
+
const instrumentationNodeFile = import_node_path48.default.join(
|
|
9080
9966
|
baseDir,
|
|
9081
9967
|
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
9082
9968
|
);
|
|
9083
|
-
const envNeatFile =
|
|
9969
|
+
const envNeatFile = import_node_path48.default.join(baseDir, ".env.neat");
|
|
9084
9970
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9085
9971
|
const dependencyEdits = [];
|
|
9086
9972
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -9137,7 +10023,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
|
|
|
9137
10023
|
const nextMajor = parseNextMajor(nextRange);
|
|
9138
10024
|
if (nextMajor !== null && nextMajor < 15) {
|
|
9139
10025
|
try {
|
|
9140
|
-
const raw = await
|
|
10026
|
+
const raw = await import_node_fs31.promises.readFile(nextConfigPath, "utf8");
|
|
9141
10027
|
if (!raw.includes("instrumentationHook")) {
|
|
9142
10028
|
nextConfigEdit = {
|
|
9143
10029
|
file: nextConfigPath,
|
|
@@ -9185,7 +10071,7 @@ function buildDependencyEdits(pkg, manifestPath) {
|
|
|
9185
10071
|
return edits;
|
|
9186
10072
|
}
|
|
9187
10073
|
async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
9188
|
-
const envNeatFile =
|
|
10074
|
+
const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
|
|
9189
10075
|
if (!await exists3(envNeatFile)) {
|
|
9190
10076
|
generatedFiles.push({
|
|
9191
10077
|
file: envNeatFile,
|
|
@@ -9220,7 +10106,7 @@ function fileImportsOtelHook(raw, specifiers) {
|
|
|
9220
10106
|
}
|
|
9221
10107
|
async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
9222
10108
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9223
|
-
const otelServerFile =
|
|
10109
|
+
const otelServerFile = import_node_path48.default.join(
|
|
9224
10110
|
serviceDir,
|
|
9225
10111
|
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
9226
10112
|
);
|
|
@@ -9241,7 +10127,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
9241
10127
|
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
9242
10128
|
const entrypointEdits = [];
|
|
9243
10129
|
try {
|
|
9244
|
-
const raw = await
|
|
10130
|
+
const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
|
|
9245
10131
|
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
9246
10132
|
const lines = raw.split(/\r?\n/);
|
|
9247
10133
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9277,11 +10163,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
9277
10163
|
}
|
|
9278
10164
|
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
|
|
9279
10165
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9280
|
-
const otelInitFile =
|
|
10166
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9281
10167
|
serviceDir,
|
|
9282
10168
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
9283
10169
|
);
|
|
9284
|
-
const resolvedHooksFile = hooksFile ??
|
|
10170
|
+
const resolvedHooksFile = hooksFile ?? import_node_path48.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
9285
10171
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
9286
10172
|
const generatedFiles = [];
|
|
9287
10173
|
const entrypointEdits = [];
|
|
@@ -9306,7 +10192,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
9306
10192
|
});
|
|
9307
10193
|
} else {
|
|
9308
10194
|
try {
|
|
9309
|
-
const raw = await
|
|
10195
|
+
const raw = await import_node_fs31.promises.readFile(hooksFile, "utf8");
|
|
9310
10196
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
9311
10197
|
const lines = raw.split(/\r?\n/);
|
|
9312
10198
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9343,11 +10229,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
9343
10229
|
}
|
|
9344
10230
|
async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
9345
10231
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9346
|
-
const otelPluginFile =
|
|
10232
|
+
const otelPluginFile = import_node_path48.default.join(
|
|
9347
10233
|
serviceDir,
|
|
9348
10234
|
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
9349
10235
|
);
|
|
9350
|
-
const otelInitFile =
|
|
10236
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9351
10237
|
serviceDir,
|
|
9352
10238
|
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
9353
10239
|
);
|
|
@@ -9398,19 +10284,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
|
9398
10284
|
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
9399
10285
|
async function findAstroMiddleware(serviceDir) {
|
|
9400
10286
|
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
9401
|
-
const candidate =
|
|
10287
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9402
10288
|
if (await exists3(candidate)) return candidate;
|
|
9403
10289
|
}
|
|
9404
10290
|
return null;
|
|
9405
10291
|
}
|
|
9406
10292
|
async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
9407
10293
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9408
|
-
const otelInitFile =
|
|
10294
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9409
10295
|
serviceDir,
|
|
9410
10296
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
9411
10297
|
);
|
|
9412
10298
|
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
9413
|
-
const middlewareFile = existingMiddleware ??
|
|
10299
|
+
const middlewareFile = existingMiddleware ?? import_node_path48.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
9414
10300
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
9415
10301
|
const generatedFiles = [];
|
|
9416
10302
|
const entrypointEdits = [];
|
|
@@ -9435,7 +10321,7 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
|
9435
10321
|
});
|
|
9436
10322
|
} else {
|
|
9437
10323
|
try {
|
|
9438
|
-
const raw = await
|
|
10324
|
+
const raw = await import_node_fs31.promises.readFile(existingMiddleware, "utf8");
|
|
9439
10325
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
9440
10326
|
const lines = raw.split(/\r?\n/);
|
|
9441
10327
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9506,7 +10392,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
|
|
|
9506
10392
|
}
|
|
9507
10393
|
async function plan(serviceDir, opts) {
|
|
9508
10394
|
const pkg = await readPackageJson2(serviceDir);
|
|
9509
|
-
const manifestPath =
|
|
10395
|
+
const manifestPath = import_node_path48.default.join(serviceDir, "package.json");
|
|
9510
10396
|
const project = opts?.project;
|
|
9511
10397
|
const empty = {
|
|
9512
10398
|
language: "javascript",
|
|
@@ -9541,8 +10427,8 @@ async function plan(serviceDir, opts) {
|
|
|
9541
10427
|
return { ...empty, libOnly: true };
|
|
9542
10428
|
}
|
|
9543
10429
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
9544
|
-
const otelInitFile =
|
|
9545
|
-
const envNeatFile =
|
|
10430
|
+
const otelInitFile = import_node_path48.default.join(import_node_path48.default.dirname(entryFile), otelInitFilename(flavor));
|
|
10431
|
+
const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
|
|
9546
10432
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9547
10433
|
const dependencyEdits = [];
|
|
9548
10434
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -9566,7 +10452,7 @@ async function plan(serviceDir, opts) {
|
|
|
9566
10452
|
}
|
|
9567
10453
|
const entrypointEdits = [];
|
|
9568
10454
|
try {
|
|
9569
|
-
const raw = await
|
|
10455
|
+
const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
|
|
9570
10456
|
const lines = raw.split(/\r?\n/);
|
|
9571
10457
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
9572
10458
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -9611,13 +10497,13 @@ async function plan(serviceDir, opts) {
|
|
|
9611
10497
|
};
|
|
9612
10498
|
}
|
|
9613
10499
|
function isAllowedWritePath(serviceDir, target) {
|
|
9614
|
-
const rel =
|
|
10500
|
+
const rel = import_node_path48.default.relative(serviceDir, target);
|
|
9615
10501
|
if (rel.startsWith("..")) return false;
|
|
9616
|
-
const base =
|
|
10502
|
+
const base = import_node_path48.default.basename(target);
|
|
9617
10503
|
if (base === "package.json") return true;
|
|
9618
10504
|
if (base === ".env.neat") return true;
|
|
9619
10505
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
9620
|
-
const relPosix = rel.split(
|
|
10506
|
+
const relPosix = rel.split(import_node_path48.default.sep).join("/");
|
|
9621
10507
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
|
|
9622
10508
|
if (relPosix === base) return true;
|
|
9623
10509
|
if (relPosix === `src/${base}`) return true;
|
|
@@ -9634,10 +10520,10 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
9634
10520
|
return false;
|
|
9635
10521
|
}
|
|
9636
10522
|
async function writeAtomic(file, contents) {
|
|
9637
|
-
await
|
|
10523
|
+
await import_node_fs31.promises.mkdir(import_node_path48.default.dirname(file), { recursive: true });
|
|
9638
10524
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
9639
|
-
await
|
|
9640
|
-
await
|
|
10525
|
+
await import_node_fs31.promises.writeFile(tmp, contents, "utf8");
|
|
10526
|
+
await import_node_fs31.promises.rename(tmp, file);
|
|
9641
10527
|
}
|
|
9642
10528
|
async function apply(installPlan) {
|
|
9643
10529
|
const { serviceDir } = installPlan;
|
|
@@ -9713,7 +10599,7 @@ async function apply(installPlan) {
|
|
|
9713
10599
|
for (const target of allTargets) {
|
|
9714
10600
|
if (await exists3(target)) {
|
|
9715
10601
|
try {
|
|
9716
|
-
originals.set(target, await
|
|
10602
|
+
originals.set(target, await import_node_fs31.promises.readFile(target, "utf8"));
|
|
9717
10603
|
} catch {
|
|
9718
10604
|
}
|
|
9719
10605
|
}
|
|
@@ -9796,14 +10682,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
9796
10682
|
const removed = [];
|
|
9797
10683
|
for (const [file, raw] of originals.entries()) {
|
|
9798
10684
|
try {
|
|
9799
|
-
await
|
|
10685
|
+
await import_node_fs31.promises.writeFile(file, raw, "utf8");
|
|
9800
10686
|
restored.push(file);
|
|
9801
10687
|
} catch {
|
|
9802
10688
|
}
|
|
9803
10689
|
}
|
|
9804
10690
|
for (const file of createdFiles) {
|
|
9805
10691
|
try {
|
|
9806
|
-
await
|
|
10692
|
+
await import_node_fs31.promises.unlink(file);
|
|
9807
10693
|
removed.push(file);
|
|
9808
10694
|
} catch {
|
|
9809
10695
|
}
|
|
@@ -9818,8 +10704,8 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
9818
10704
|
...removed.map((f) => `removed: ${f}`),
|
|
9819
10705
|
""
|
|
9820
10706
|
];
|
|
9821
|
-
const rollbackPath =
|
|
9822
|
-
await
|
|
10707
|
+
const rollbackPath = import_node_path48.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
10708
|
+
await import_node_fs31.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
9823
10709
|
}
|
|
9824
10710
|
function injectInstrumentationHook(raw) {
|
|
9825
10711
|
if (raw.includes("instrumentationHook")) return raw;
|
|
@@ -9848,8 +10734,8 @@ var javascriptInstaller = {
|
|
|
9848
10734
|
|
|
9849
10735
|
// src/installers/python.ts
|
|
9850
10736
|
init_cjs_shims();
|
|
9851
|
-
var
|
|
9852
|
-
var
|
|
10737
|
+
var import_node_fs32 = require("fs");
|
|
10738
|
+
var import_node_path49 = __toESM(require("path"), 1);
|
|
9853
10739
|
var SDK_PACKAGES2 = [
|
|
9854
10740
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
9855
10741
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -9861,7 +10747,7 @@ var OTEL_ENV2 = {
|
|
|
9861
10747
|
};
|
|
9862
10748
|
async function exists4(p) {
|
|
9863
10749
|
try {
|
|
9864
|
-
await
|
|
10750
|
+
await import_node_fs32.promises.stat(p);
|
|
9865
10751
|
return true;
|
|
9866
10752
|
} catch {
|
|
9867
10753
|
return false;
|
|
@@ -9870,7 +10756,7 @@ async function exists4(p) {
|
|
|
9870
10756
|
async function detect2(serviceDir) {
|
|
9871
10757
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
9872
10758
|
for (const m of markers) {
|
|
9873
|
-
if (await exists4(
|
|
10759
|
+
if (await exists4(import_node_path49.default.join(serviceDir, m))) return true;
|
|
9874
10760
|
}
|
|
9875
10761
|
return false;
|
|
9876
10762
|
}
|
|
@@ -9880,9 +10766,9 @@ function reqPackageName(line) {
|
|
|
9880
10766
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
9881
10767
|
}
|
|
9882
10768
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
9883
|
-
const file =
|
|
10769
|
+
const file = import_node_path49.default.join(serviceDir, "requirements.txt");
|
|
9884
10770
|
if (!await exists4(file)) return null;
|
|
9885
|
-
const raw = await
|
|
10771
|
+
const raw = await import_node_fs32.promises.readFile(file, "utf8");
|
|
9886
10772
|
const presentNames = new Set(
|
|
9887
10773
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
9888
10774
|
);
|
|
@@ -9890,9 +10776,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
9890
10776
|
return { manifest: file, missing: [...missing] };
|
|
9891
10777
|
}
|
|
9892
10778
|
async function planProcfileEdits(serviceDir) {
|
|
9893
|
-
const procfile =
|
|
10779
|
+
const procfile = import_node_path49.default.join(serviceDir, "Procfile");
|
|
9894
10780
|
if (!await exists4(procfile)) return [];
|
|
9895
|
-
const raw = await
|
|
10781
|
+
const raw = await import_node_fs32.promises.readFile(procfile, "utf8");
|
|
9896
10782
|
const edits = [];
|
|
9897
10783
|
for (const line of raw.split(/\r?\n/)) {
|
|
9898
10784
|
if (line.length === 0) continue;
|
|
@@ -9944,8 +10830,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
9944
10830
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
9945
10831
|
`;
|
|
9946
10832
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
9947
|
-
await
|
|
9948
|
-
await
|
|
10833
|
+
await import_node_fs32.promises.writeFile(tmp, next, "utf8");
|
|
10834
|
+
await import_node_fs32.promises.rename(tmp, manifest);
|
|
9949
10835
|
}
|
|
9950
10836
|
async function applyProcfile(procfile, edits, original) {
|
|
9951
10837
|
let next = original;
|
|
@@ -9954,8 +10840,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
9954
10840
|
next = next.replace(e.before, e.after);
|
|
9955
10841
|
}
|
|
9956
10842
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
9957
|
-
await
|
|
9958
|
-
await
|
|
10843
|
+
await import_node_fs32.promises.writeFile(tmp, next, "utf8");
|
|
10844
|
+
await import_node_fs32.promises.rename(tmp, procfile);
|
|
9959
10845
|
}
|
|
9960
10846
|
async function apply2(installPlan) {
|
|
9961
10847
|
const { serviceDir } = installPlan;
|
|
@@ -9968,7 +10854,7 @@ async function apply2(installPlan) {
|
|
|
9968
10854
|
const originals = /* @__PURE__ */ new Map();
|
|
9969
10855
|
for (const file of touched) {
|
|
9970
10856
|
try {
|
|
9971
|
-
originals.set(file, await
|
|
10857
|
+
originals.set(file, await import_node_fs32.promises.readFile(file, "utf8"));
|
|
9972
10858
|
} catch {
|
|
9973
10859
|
}
|
|
9974
10860
|
}
|
|
@@ -9979,7 +10865,7 @@ async function apply2(installPlan) {
|
|
|
9979
10865
|
if (raw === void 0) {
|
|
9980
10866
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
9981
10867
|
}
|
|
9982
|
-
const base =
|
|
10868
|
+
const base = import_node_path49.default.basename(file);
|
|
9983
10869
|
if (base === "requirements.txt") {
|
|
9984
10870
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
9985
10871
|
if (edits.length > 0) {
|
|
@@ -10004,7 +10890,7 @@ async function rollback2(installPlan, originals) {
|
|
|
10004
10890
|
const restored = [];
|
|
10005
10891
|
for (const [file, raw] of originals.entries()) {
|
|
10006
10892
|
try {
|
|
10007
|
-
await
|
|
10893
|
+
await import_node_fs32.promises.writeFile(file, raw, "utf8");
|
|
10008
10894
|
restored.push(file);
|
|
10009
10895
|
} catch {
|
|
10010
10896
|
}
|
|
@@ -10018,8 +10904,8 @@ async function rollback2(installPlan, originals) {
|
|
|
10018
10904
|
...restored.map((f) => `restored: ${f}`),
|
|
10019
10905
|
""
|
|
10020
10906
|
];
|
|
10021
|
-
const rollbackPath =
|
|
10022
|
-
await
|
|
10907
|
+
const rollbackPath = import_node_path49.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
10908
|
+
await import_node_fs32.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
10023
10909
|
}
|
|
10024
10910
|
var pythonInstaller = {
|
|
10025
10911
|
name: "python",
|
|
@@ -10148,38 +11034,6 @@ var import_node_net = __toESM(require("net"), 1);
|
|
|
10148
11034
|
var import_node_path50 = __toESM(require("path"), 1);
|
|
10149
11035
|
var import_node_child_process3 = require("child_process");
|
|
10150
11036
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
10151
|
-
|
|
10152
|
-
// src/daemon.ts
|
|
10153
|
-
init_cjs_shims();
|
|
10154
|
-
var import_node_fs32 = require("fs");
|
|
10155
|
-
var import_node_path49 = __toESM(require("path"), 1);
|
|
10156
|
-
var import_node_module = require("module");
|
|
10157
|
-
init_otel();
|
|
10158
|
-
init_auth();
|
|
10159
|
-
|
|
10160
|
-
// src/unrouted.ts
|
|
10161
|
-
init_cjs_shims();
|
|
10162
|
-
var import_node_fs31 = require("fs");
|
|
10163
|
-
var import_node_path48 = __toESM(require("path"), 1);
|
|
10164
|
-
|
|
10165
|
-
// src/daemon.ts
|
|
10166
|
-
function daemonJsonPath(scanPath) {
|
|
10167
|
-
return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
|
|
10168
|
-
}
|
|
10169
|
-
async function readDaemonRecord(scanPath) {
|
|
10170
|
-
try {
|
|
10171
|
-
const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
10172
|
-
const parsed = JSON.parse(raw);
|
|
10173
|
-
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
10174
|
-
return parsed;
|
|
10175
|
-
}
|
|
10176
|
-
return null;
|
|
10177
|
-
} catch {
|
|
10178
|
-
return null;
|
|
10179
|
-
}
|
|
10180
|
-
}
|
|
10181
|
-
|
|
10182
|
-
// src/orchestrator.ts
|
|
10183
11037
|
async function extractAndPersist(opts) {
|
|
10184
11038
|
const services = await discoverServices(opts.scanPath);
|
|
10185
11039
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
@@ -10236,11 +11090,13 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
10236
11090
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
10237
11091
|
else if (outcome.outcome === "lib-only") {
|
|
10238
11092
|
libOnly++;
|
|
10239
|
-
|
|
11093
|
+
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
11094
|
+
if (appDeps.length > 0) {
|
|
10240
11095
|
const svcName = import_node_path50.default.basename(svc.dir);
|
|
11096
|
+
const list = appDeps.join(", ");
|
|
10241
11097
|
console.warn(
|
|
10242
11098
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
10243
|
-
${svc.dir} depends on a
|
|
11099
|
+
${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
|
|
10244
11100
|
Add a "start" script to package.json, or point neat at the entry file directly.`
|
|
10245
11101
|
);
|
|
10246
11102
|
}
|
|
@@ -10308,10 +11164,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
10308
11164
|
if (gaps.length > 0) {
|
|
10309
11165
|
const svcName = import_node_path50.default.basename(svc.dir);
|
|
10310
11166
|
const list = gaps.join(", ");
|
|
10311
|
-
const
|
|
11167
|
+
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
11168
|
+
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
10312
11169
|
console.warn(
|
|
10313
11170
|
`neat: calls to ${list} won't be observed by default in ${svcName}.
|
|
10314
|
-
${
|
|
11171
|
+
${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
|
|
10315
11172
|
Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
|
|
10316
11173
|
);
|
|
10317
11174
|
}
|
|
@@ -10405,29 +11262,22 @@ async function probeProjectHealth(restPort, name) {
|
|
|
10405
11262
|
});
|
|
10406
11263
|
});
|
|
10407
11264
|
}
|
|
10408
|
-
async function snapshotProjectStatus(restPort, body) {
|
|
11265
|
+
async function snapshotProjectStatus(restPort, project, body) {
|
|
10409
11266
|
if (body.projects && body.projects.length > 0) {
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
status: p.status ?? "active"
|
|
10413
|
-
}
|
|
10414
|
-
}
|
|
10415
|
-
|
|
10416
|
-
if (entries.length === 0) return [];
|
|
10417
|
-
return Promise.all(
|
|
10418
|
-
entries.map(async (entry2) => ({
|
|
10419
|
-
name: entry2.name,
|
|
10420
|
-
status: await probeProjectHealth(restPort, entry2.name)
|
|
10421
|
-
}))
|
|
10422
|
-
);
|
|
11267
|
+
const mine = body.projects.filter((p) => p.name === project);
|
|
11268
|
+
if (mine.length > 0) {
|
|
11269
|
+
return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
|
|
11270
|
+
}
|
|
11271
|
+
}
|
|
11272
|
+
return [{ name: project, status: await probeProjectHealth(restPort, project) }];
|
|
10423
11273
|
}
|
|
10424
|
-
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
11274
|
+
async function waitForDaemonReady(restPort, project, timeoutMs) {
|
|
10425
11275
|
const deadline = Date.now() + timeoutMs;
|
|
10426
11276
|
let lastBootstrapping = [];
|
|
10427
11277
|
while (Date.now() < deadline) {
|
|
10428
11278
|
const body = await fetchDaemonHealth(restPort);
|
|
10429
11279
|
if (body !== null) {
|
|
10430
|
-
const projects2 = await snapshotProjectStatus(restPort, body);
|
|
11280
|
+
const projects2 = await snapshotProjectStatus(restPort, project, body);
|
|
10431
11281
|
const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
|
|
10432
11282
|
const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
|
|
10433
11283
|
if (bootstrapping.length === 0) {
|
|
@@ -10446,7 +11296,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
10446
11296
|
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
10447
11297
|
}
|
|
10448
11298
|
const final = await fetchDaemonHealth(restPort);
|
|
10449
|
-
const projects = final ? await snapshotProjectStatus(restPort, final) : [];
|
|
11299
|
+
const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
|
|
10450
11300
|
return {
|
|
10451
11301
|
ready: false,
|
|
10452
11302
|
brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
|
|
@@ -10454,14 +11304,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
10454
11304
|
};
|
|
10455
11305
|
}
|
|
10456
11306
|
var NEAT_PORTS = [8080, 4318, 6328];
|
|
10457
|
-
|
|
11307
|
+
function probeBind(port, host) {
|
|
10458
11308
|
return new Promise((resolve) => {
|
|
10459
11309
|
const server = import_node_net.default.createServer();
|
|
10460
|
-
server.once("error", () =>
|
|
10461
|
-
|
|
10462
|
-
|
|
11310
|
+
server.once("error", (err) => {
|
|
11311
|
+
resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
|
|
11312
|
+
});
|
|
11313
|
+
server.once("listening", () => server.close(() => resolve("free")));
|
|
11314
|
+
server.listen(port, host);
|
|
10463
11315
|
});
|
|
10464
11316
|
}
|
|
11317
|
+
function crossFamilyHost(host) {
|
|
11318
|
+
switch (host) {
|
|
11319
|
+
case "127.0.0.1":
|
|
11320
|
+
case "localhost":
|
|
11321
|
+
return "::1";
|
|
11322
|
+
case "::1":
|
|
11323
|
+
return "127.0.0.1";
|
|
11324
|
+
case "0.0.0.0":
|
|
11325
|
+
return "::";
|
|
11326
|
+
case "::":
|
|
11327
|
+
return "0.0.0.0";
|
|
11328
|
+
default:
|
|
11329
|
+
return null;
|
|
11330
|
+
}
|
|
11331
|
+
}
|
|
11332
|
+
async function isPortFree(port, host = "127.0.0.1") {
|
|
11333
|
+
if (await probeBind(port, host) !== "free") return false;
|
|
11334
|
+
const sibling = crossFamilyHost(host);
|
|
11335
|
+
if (sibling && await probeBind(port, sibling) === "in-use") return false;
|
|
11336
|
+
return true;
|
|
11337
|
+
}
|
|
10465
11338
|
function formatPortCollisionMessage(port) {
|
|
10466
11339
|
return [
|
|
10467
11340
|
`neat: port ${port} is in use; the NEAT daemon needs it.`,
|
|
@@ -10471,13 +11344,13 @@ function formatPortCollisionMessage(port) {
|
|
|
10471
11344
|
}
|
|
10472
11345
|
var PORT_ALLOCATION_ATTEMPTS = 8;
|
|
10473
11346
|
var PORT_STRIDE = 1;
|
|
10474
|
-
async function tripleFree(ports) {
|
|
11347
|
+
async function tripleFree(ports, host = "127.0.0.1") {
|
|
10475
11348
|
for (const p of [ports.rest, ports.otlp, ports.web]) {
|
|
10476
|
-
if (!await isPortFree(p)) return false;
|
|
11349
|
+
if (!await isPortFree(p, host)) return false;
|
|
10477
11350
|
}
|
|
10478
11351
|
return true;
|
|
10479
11352
|
}
|
|
10480
|
-
async function allocatePorts() {
|
|
11353
|
+
async function allocatePorts(host = "127.0.0.1") {
|
|
10481
11354
|
const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
|
|
10482
11355
|
for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
|
|
10483
11356
|
const candidate = {
|
|
@@ -10485,7 +11358,7 @@ async function allocatePorts() {
|
|
|
10485
11358
|
otlp: baseOtlp + i * PORT_STRIDE,
|
|
10486
11359
|
web: baseWeb + i * PORT_STRIDE
|
|
10487
11360
|
};
|
|
10488
|
-
if (await tripleFree(candidate)) return candidate;
|
|
11361
|
+
if (await tripleFree(candidate, host)) return candidate;
|
|
10489
11362
|
}
|
|
10490
11363
|
return null;
|
|
10491
11364
|
}
|
|
@@ -10691,16 +11564,20 @@ async function runOrchestrator(opts) {
|
|
|
10691
11564
|
}
|
|
10692
11565
|
}
|
|
10693
11566
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
11567
|
+
const bindHost = resolveHost(
|
|
11568
|
+
{},
|
|
11569
|
+
typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
|
|
11570
|
+
);
|
|
10694
11571
|
const persistedPorts = await persistedPortsFor(opts.scanPath);
|
|
10695
11572
|
let allocated = null;
|
|
10696
11573
|
if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
|
|
10697
11574
|
result.steps.daemon = "already-running";
|
|
10698
11575
|
allocated = persistedPorts;
|
|
10699
11576
|
} else {
|
|
10700
|
-
if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
|
|
11577
|
+
if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
|
|
10701
11578
|
allocated = persistedPorts;
|
|
10702
11579
|
} else {
|
|
10703
|
-
allocated = await allocatePorts();
|
|
11580
|
+
allocated = await allocatePorts(bindHost);
|
|
10704
11581
|
}
|
|
10705
11582
|
if (!allocated) {
|
|
10706
11583
|
for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
|
|
@@ -10729,7 +11606,7 @@ async function runOrchestrator(opts) {
|
|
|
10729
11606
|
projectPath: opts.scanPath,
|
|
10730
11607
|
ports: allocated
|
|
10731
11608
|
});
|
|
10732
|
-
const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
|
|
11609
|
+
const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
|
|
10733
11610
|
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
10734
11611
|
if (!ready.ready) {
|
|
10735
11612
|
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
@@ -10797,7 +11674,7 @@ var import_node_path51 = __toESM(require("path"), 1);
|
|
|
10797
11674
|
|
|
10798
11675
|
// src/cli-client.ts
|
|
10799
11676
|
init_cjs_shims();
|
|
10800
|
-
var
|
|
11677
|
+
var import_types29 = require("@neat.is/types");
|
|
10801
11678
|
var HttpError = class extends Error {
|
|
10802
11679
|
constructor(status2, message, responseBody = "") {
|
|
10803
11680
|
super(message);
|
|
@@ -10913,7 +11790,7 @@ async function runBlastRadius(client, input) {
|
|
|
10913
11790
|
const result = await client.get(path53);
|
|
10914
11791
|
if (result.totalAffected === 0) {
|
|
10915
11792
|
return {
|
|
10916
|
-
summary: `${result.origin} has no
|
|
11793
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
10917
11794
|
};
|
|
10918
11795
|
}
|
|
10919
11796
|
const sorted = [...result.affectedNodes].sort(
|
|
@@ -10926,7 +11803,7 @@ async function runBlastRadius(client, input) {
|
|
|
10926
11803
|
);
|
|
10927
11804
|
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
10928
11805
|
return {
|
|
10929
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected}
|
|
11806
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
10930
11807
|
block: blockLines.join("\n"),
|
|
10931
11808
|
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
10932
11809
|
provenance: provenances.length ? provenances : void 0
|
|
@@ -10939,7 +11816,7 @@ async function runBlastRadius(client, input) {
|
|
|
10939
11816
|
}
|
|
10940
11817
|
}
|
|
10941
11818
|
function formatBlastEntry(n) {
|
|
10942
|
-
const tag = n.edgeProvenance ===
|
|
11819
|
+
const tag = n.edgeProvenance === import_types29.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
10943
11820
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
10944
11821
|
}
|
|
10945
11822
|
async function runDependencies(client, input) {
|
|
@@ -10980,22 +11857,33 @@ async function runDependencies(client, input) {
|
|
|
10980
11857
|
throw err;
|
|
10981
11858
|
}
|
|
10982
11859
|
}
|
|
11860
|
+
function observedDepLine(nodeId, e) {
|
|
11861
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
11862
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
11863
|
+
}
|
|
10983
11864
|
async function runObservedDependencies(client, input) {
|
|
10984
11865
|
try {
|
|
10985
|
-
const
|
|
10986
|
-
projectPath(
|
|
11866
|
+
const result = await client.get(
|
|
11867
|
+
projectPath(
|
|
11868
|
+
input.project,
|
|
11869
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
11870
|
+
)
|
|
10987
11871
|
);
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
11872
|
+
if (result.dependencies.length === 0) {
|
|
11873
|
+
if (result.observed) {
|
|
11874
|
+
return {
|
|
11875
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
11876
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
11877
|
+
};
|
|
11878
|
+
}
|
|
11879
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
10992
11880
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
10993
11881
|
}
|
|
10994
|
-
const blockLines =
|
|
11882
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
10995
11883
|
return {
|
|
10996
|
-
summary: `${input.nodeId} has ${
|
|
11884
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
10997
11885
|
block: blockLines.join("\n"),
|
|
10998
|
-
provenance:
|
|
11886
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
10999
11887
|
};
|
|
11000
11888
|
} catch (err) {
|
|
11001
11889
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -11049,7 +11937,7 @@ async function runIncidents(client, input) {
|
|
|
11049
11937
|
return {
|
|
11050
11938
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
11051
11939
|
block: blockLines.join("\n"),
|
|
11052
|
-
provenance:
|
|
11940
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
11053
11941
|
};
|
|
11054
11942
|
} catch (err) {
|
|
11055
11943
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -11158,7 +12046,7 @@ async function runStaleEdges(client, input) {
|
|
|
11158
12046
|
return {
|
|
11159
12047
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
11160
12048
|
block: blockLines.join("\n"),
|
|
11161
|
-
provenance:
|
|
12049
|
+
provenance: import_types29.Provenance.STALE
|
|
11162
12050
|
};
|
|
11163
12051
|
}
|
|
11164
12052
|
async function runPolicies(client, input) {
|
|
@@ -11475,7 +12363,7 @@ async function runSync(opts) {
|
|
|
11475
12363
|
}
|
|
11476
12364
|
|
|
11477
12365
|
// src/cli.ts
|
|
11478
|
-
var
|
|
12366
|
+
var import_types30 = require("@neat.is/types");
|
|
11479
12367
|
function isNpxInvocation() {
|
|
11480
12368
|
if (process.env.npm_command === "exec") return true;
|
|
11481
12369
|
const execpath = process.env.npm_execpath ?? "";
|
|
@@ -11540,7 +12428,7 @@ function usage() {
|
|
|
11540
12428
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
11541
12429
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
11542
12430
|
console.log(` example: ${neat} root-cause service:<name>`);
|
|
11543
|
-
console.log(" blast-radius <node-id> BFS
|
|
12431
|
+
console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
|
|
11544
12432
|
console.log(` example: ${neat} blast-radius database:<host>`);
|
|
11545
12433
|
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
11546
12434
|
console.log(" Flags: --depth N (default 3, max 10)");
|
|
@@ -12267,11 +13155,18 @@ Pass --project <name> to choose:
|
|
|
12267
13155
|
${names}`
|
|
12268
13156
|
);
|
|
12269
13157
|
}
|
|
12270
|
-
function resolveDaemonUrl() {
|
|
12271
|
-
|
|
13158
|
+
async function resolveDaemonUrl(project) {
|
|
13159
|
+
const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
|
|
13160
|
+
if (explicit) return explicit;
|
|
13161
|
+
if (project) {
|
|
13162
|
+
const daemon = await findDaemonByProject(project);
|
|
13163
|
+
if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
|
|
13164
|
+
}
|
|
13165
|
+
return "http://localhost:8080";
|
|
12272
13166
|
}
|
|
12273
13167
|
async function runQueryVerb(cmd, parsed) {
|
|
12274
|
-
const
|
|
13168
|
+
const requestedProject = resolveProjectFlag(parsed);
|
|
13169
|
+
const baseUrl = await resolveDaemonUrl(requestedProject);
|
|
12275
13170
|
const client = createHttpClient(baseUrl, resolveAuthToken());
|
|
12276
13171
|
const positional = parsed.positional;
|
|
12277
13172
|
let makeWork;
|
|
@@ -12389,10 +13284,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
12389
13284
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12390
13285
|
const out = [];
|
|
12391
13286
|
for (const p of parts) {
|
|
12392
|
-
const r =
|
|
13287
|
+
const r = import_types30.DivergenceTypeSchema.safeParse(p);
|
|
12393
13288
|
if (!r.success) {
|
|
12394
13289
|
console.error(
|
|
12395
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
13290
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types30.DivergenceTypeSchema.options.join(", ")}`
|
|
12396
13291
|
);
|
|
12397
13292
|
return 2;
|
|
12398
13293
|
}
|
|
@@ -12427,7 +13322,7 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
12427
13322
|
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
12428
13323
|
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
12429
13324
|
} else if (err instanceof TransportError) {
|
|
12430
|
-
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (
|
|
13325
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
|
|
12431
13326
|
} else {
|
|
12432
13327
|
console.error(`neat ${cmd}: ${err.message}`);
|
|
12433
13328
|
}
|
|
@@ -12452,6 +13347,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
12452
13347
|
parseArgs,
|
|
12453
13348
|
printBanner,
|
|
12454
13349
|
readPackageVersion,
|
|
13350
|
+
resolveDaemonUrl,
|
|
12455
13351
|
resolveProjectForVerb,
|
|
12456
13352
|
runInit,
|
|
12457
13353
|
runQueryVerb,
|