@neat.is/core 0.7.9 → 0.8.0-rc.1
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-N5TPODCX.js → chunk-3PXRQH53.js} +905 -37
- package/dist/chunk-3PXRQH53.js.map +1 -0
- package/dist/{chunk-ILG3SMD5.js → chunk-OJ6BW63H.js} +6 -2
- package/dist/chunk-OJ6BW63H.js.map +1 -0
- package/dist/cli.cjs +1673 -270
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +689 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +922 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +922 -42
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +911 -39
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ILG3SMD5.js.map +0 -1
- package/dist/chunk-N5TPODCX.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -61,8 +61,8 @@ function mountBearerAuth(app, opts) {
|
|
|
61
61
|
]);
|
|
62
62
|
const publicRead = opts.publicRead === true;
|
|
63
63
|
app.addHook("preHandler", (req, reply, done) => {
|
|
64
|
-
const
|
|
65
|
-
if (exactUnauthPaths.has(
|
|
64
|
+
const path84 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
65
|
+
if (exactUnauthPaths.has(path84) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path84)) {
|
|
66
66
|
done();
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
@@ -415,8 +415,8 @@ function websocketChannelPathOf(attrs) {
|
|
|
415
415
|
const v = attrs[key];
|
|
416
416
|
if (typeof v === "string" && v.length > 0) {
|
|
417
417
|
const q = v.indexOf("?");
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
418
|
+
const path84 = q === -1 ? v : v.slice(0, q);
|
|
419
|
+
if (path84.length > 0) return path84;
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
return void 0;
|
|
@@ -771,9 +771,9 @@ __export(cli_exports, {
|
|
|
771
771
|
});
|
|
772
772
|
module.exports = __toCommonJS(cli_exports);
|
|
773
773
|
init_cjs_shims();
|
|
774
|
-
var
|
|
774
|
+
var import_node_path83 = __toESM(require("path"), 1);
|
|
775
775
|
var import_node_os8 = __toESM(require("os"), 1);
|
|
776
|
-
var
|
|
776
|
+
var import_node_fs48 = require("fs");
|
|
777
777
|
|
|
778
778
|
// src/banner.ts
|
|
779
779
|
init_cjs_shims();
|
|
@@ -1341,19 +1341,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1341
1341
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1342
1342
|
let best = { path: [start], edges: [] };
|
|
1343
1343
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1344
|
-
function step(node,
|
|
1345
|
-
if (
|
|
1346
|
-
best = { path: [...
|
|
1344
|
+
function step(node, path84, edges) {
|
|
1345
|
+
if (path84.length > best.path.length) {
|
|
1346
|
+
best = { path: [...path84], edges: [...edges] };
|
|
1347
1347
|
}
|
|
1348
|
-
if (
|
|
1348
|
+
if (path84.length - 1 >= maxDepth) return;
|
|
1349
1349
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1350
1350
|
for (const [srcId, edge] of incoming) {
|
|
1351
1351
|
if (visited.has(srcId)) continue;
|
|
1352
1352
|
visited.add(srcId);
|
|
1353
|
-
|
|
1353
|
+
path84.push(srcId);
|
|
1354
1354
|
edges.push(edge);
|
|
1355
|
-
step(srcId,
|
|
1356
|
-
|
|
1355
|
+
step(srcId, path84, edges);
|
|
1356
|
+
path84.pop();
|
|
1357
1357
|
edges.pop();
|
|
1358
1358
|
visited.delete(srcId);
|
|
1359
1359
|
}
|
|
@@ -1445,7 +1445,7 @@ var rootCauseShapes = {
|
|
|
1445
1445
|
[import_types.NodeType.FileNode]: fileRootCauseShape,
|
|
1446
1446
|
[import_types.NodeType.SymbolNode]: symbolRootCauseShape
|
|
1447
1447
|
};
|
|
1448
|
-
function
|
|
1448
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1449
1449
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1450
1450
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1451
1451
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -1560,26 +1560,26 @@ function dominantFailingCall(graph, serviceId9, visited) {
|
|
|
1560
1560
|
return best;
|
|
1561
1561
|
}
|
|
1562
1562
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1563
|
-
const
|
|
1563
|
+
const path84 = [originServiceId];
|
|
1564
1564
|
const edges = [];
|
|
1565
1565
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1566
1566
|
let current = originServiceId;
|
|
1567
1567
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1568
1568
|
const hop = dominantFailingCall(graph, current, visited);
|
|
1569
1569
|
if (!hop) break;
|
|
1570
|
-
|
|
1570
|
+
path84.push(hop.nextService);
|
|
1571
1571
|
edges.push(hop.edge);
|
|
1572
1572
|
visited.add(hop.nextService);
|
|
1573
1573
|
current = hop.nextService;
|
|
1574
1574
|
}
|
|
1575
1575
|
if (edges.length === 0) return null;
|
|
1576
|
-
return { path:
|
|
1576
|
+
return { path: path84, edges, culprit: current };
|
|
1577
1577
|
}
|
|
1578
1578
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1579
1579
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1580
1580
|
if (!chain) return null;
|
|
1581
1581
|
const culprit = chain.culprit;
|
|
1582
|
-
const
|
|
1582
|
+
const path84 = [...chain.path];
|
|
1583
1583
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1584
1584
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1585
1585
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -1587,14 +1587,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1587
1587
|
if (loc) {
|
|
1588
1588
|
let rootCauseNode = culprit;
|
|
1589
1589
|
if (loc.fileNode) {
|
|
1590
|
-
|
|
1590
|
+
path84.push(loc.fileNode);
|
|
1591
1591
|
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1592
1592
|
rootCauseNode = loc.fileNode;
|
|
1593
1593
|
}
|
|
1594
1594
|
return import_types.RootCauseResultSchema.parse({
|
|
1595
1595
|
rootCauseNode,
|
|
1596
1596
|
rootCauseReason: loc.rootCauseReason,
|
|
1597
|
-
traversalPath:
|
|
1597
|
+
traversalPath: path84,
|
|
1598
1598
|
edgeProvenances,
|
|
1599
1599
|
confidence,
|
|
1600
1600
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -1606,7 +1606,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1606
1606
|
return import_types.RootCauseResultSchema.parse({
|
|
1607
1607
|
rootCauseNode: culprit,
|
|
1608
1608
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1609
|
-
traversalPath:
|
|
1609
|
+
traversalPath: path84,
|
|
1610
1610
|
edgeProvenances,
|
|
1611
1611
|
confidence,
|
|
1612
1612
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -1702,7 +1702,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1702
1702
|
dependencies: [],
|
|
1703
1703
|
observed: false,
|
|
1704
1704
|
inboundObservedCount: 0,
|
|
1705
|
-
hasExtractedOutbound: false
|
|
1705
|
+
hasExtractedOutbound: false,
|
|
1706
|
+
inboundVolume: 0,
|
|
1707
|
+
window: "lifetime"
|
|
1706
1708
|
});
|
|
1707
1709
|
}
|
|
1708
1710
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -1733,11 +1735,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1733
1735
|
}
|
|
1734
1736
|
}
|
|
1735
1737
|
let inboundObservedCount = 0;
|
|
1738
|
+
let inboundVolume = 0;
|
|
1739
|
+
let inboundLastObserved;
|
|
1736
1740
|
for (const tgt of scope) {
|
|
1737
1741
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1738
1742
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1739
1743
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1740
|
-
if (e.provenance === import_types.Provenance.OBSERVED)
|
|
1744
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1745
|
+
inboundObservedCount += 1;
|
|
1746
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
1747
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
1748
|
+
inboundLastObserved = e.lastObserved;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1741
1751
|
}
|
|
1742
1752
|
}
|
|
1743
1753
|
dependencies.sort(
|
|
@@ -1748,7 +1758,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1748
1758
|
dependencies,
|
|
1749
1759
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1750
1760
|
inboundObservedCount,
|
|
1751
|
-
hasExtractedOutbound
|
|
1761
|
+
hasExtractedOutbound,
|
|
1762
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
1763
|
+
inboundVolume,
|
|
1764
|
+
window: "lifetime",
|
|
1765
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
var SATURATION_P95_MS = 1e3;
|
|
1769
|
+
function nodeScope(graph, nodeId) {
|
|
1770
|
+
const scope = [nodeId];
|
|
1771
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
1772
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1773
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1774
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1775
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1776
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1777
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1778
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return scope;
|
|
1782
|
+
}
|
|
1783
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
1784
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
1785
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
1786
|
+
}
|
|
1787
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
1788
|
+
const scope = nodeScope(graph, nodeId);
|
|
1789
|
+
let errorsFromCallers = 0;
|
|
1790
|
+
let inboundVolume = 0;
|
|
1791
|
+
let outboundVolume = 0;
|
|
1792
|
+
let outboundErrors = 0;
|
|
1793
|
+
let latestInboundMs;
|
|
1794
|
+
let latencyP95Ms;
|
|
1795
|
+
let stale = false;
|
|
1796
|
+
for (const n of scope) {
|
|
1797
|
+
if (!graph.hasNode(n)) continue;
|
|
1798
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
1799
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1800
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1801
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1802
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1803
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1804
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
1805
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
1806
|
+
if (e.lastObserved) {
|
|
1807
|
+
const t = Date.parse(e.lastObserved);
|
|
1808
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1812
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1813
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1814
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1815
|
+
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1816
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
1820
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
1821
|
+
return {
|
|
1822
|
+
errorsEmittedHere,
|
|
1823
|
+
errorsFromCallers,
|
|
1824
|
+
callCount: inboundVolume,
|
|
1825
|
+
outboundVolume,
|
|
1826
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
1827
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
1828
|
+
stale
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function isSaturated(ctx) {
|
|
1832
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
1833
|
+
}
|
|
1834
|
+
function classifyNode(ctx) {
|
|
1835
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
1836
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
1837
|
+
return "symptom-only";
|
|
1838
|
+
}
|
|
1839
|
+
return "primary-failure";
|
|
1840
|
+
}
|
|
1841
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1842
|
+
return "unrelated";
|
|
1843
|
+
}
|
|
1844
|
+
function isVictimSeed(ctx) {
|
|
1845
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1846
|
+
}
|
|
1847
|
+
function grainOf(graph, nodeId) {
|
|
1848
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1849
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
1850
|
+
if (t === import_types.NodeType.ServiceNode) return "service";
|
|
1851
|
+
if (t === import_types.NodeType.FileNode) return "file";
|
|
1852
|
+
if (t === import_types.NodeType.SymbolNode) return "symbol";
|
|
1853
|
+
return t;
|
|
1854
|
+
}
|
|
1855
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
1856
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
1857
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
1858
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
1859
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
1860
|
+
while (queue.length > 0) {
|
|
1861
|
+
const frame = queue.shift();
|
|
1862
|
+
if (frame.depth >= maxDepth) continue;
|
|
1863
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1864
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1865
|
+
for (const [nid, edge] of neighbours) {
|
|
1866
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
1867
|
+
if (enqueued.has(nid)) continue;
|
|
1868
|
+
enqueued.add(nid);
|
|
1869
|
+
queue.push({
|
|
1870
|
+
nodeId: nid,
|
|
1871
|
+
depth: frame.depth + 1,
|
|
1872
|
+
nodes: [...frame.nodes, nid],
|
|
1873
|
+
edges: [...frame.edges, edge]
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
var EMPTY_CONTEXT = {
|
|
1880
|
+
errorsEmittedHere: 0,
|
|
1881
|
+
errorsFromCallers: 0,
|
|
1882
|
+
callCount: 0,
|
|
1883
|
+
outboundVolume: 0,
|
|
1884
|
+
stale: false
|
|
1885
|
+
};
|
|
1886
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
1887
|
+
if (!graph.hasNode(nodeId)) {
|
|
1888
|
+
return import_types.ExpandResultSchema.parse({
|
|
1889
|
+
origin: nodeId,
|
|
1890
|
+
direction,
|
|
1891
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
1892
|
+
neighbours: []
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
1896
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
1897
|
+
const neighbours = [];
|
|
1898
|
+
for (const [nid, edge] of best) {
|
|
1899
|
+
if (edge.type === import_types.EdgeType.CONTAINS) continue;
|
|
1900
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
1901
|
+
neighbours.push({
|
|
1902
|
+
node: nid,
|
|
1903
|
+
edgeType: edge.type,
|
|
1904
|
+
provenance: edge.provenance,
|
|
1905
|
+
classification: classifyNode(nctx),
|
|
1906
|
+
context: nctx
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
1910
|
+
return import_types.ExpandResultSchema.parse({
|
|
1911
|
+
origin: nodeId,
|
|
1912
|
+
direction,
|
|
1913
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
1914
|
+
neighbours
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
1918
|
+
const buildPath = (fp) => ({
|
|
1919
|
+
nodes: fp.nodes,
|
|
1920
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
1921
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
1922
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
1923
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
1924
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
1925
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
1926
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
1927
|
+
)
|
|
1928
|
+
});
|
|
1929
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
1930
|
+
return import_types.RelateResultSchema.parse({
|
|
1931
|
+
a,
|
|
1932
|
+
b,
|
|
1933
|
+
related: false,
|
|
1934
|
+
direction: null,
|
|
1935
|
+
paths: [],
|
|
1936
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
1940
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
1941
|
+
if (!down && !up) {
|
|
1942
|
+
return import_types.RelateResultSchema.parse({
|
|
1943
|
+
a,
|
|
1944
|
+
b,
|
|
1945
|
+
related: false,
|
|
1946
|
+
direction: null,
|
|
1947
|
+
paths: [],
|
|
1948
|
+
note: `no path within ${maxDepth} hops`
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
const paths = [];
|
|
1952
|
+
const direction = down ? "a->b" : "b->a";
|
|
1953
|
+
if (down) paths.push(buildPath(down));
|
|
1954
|
+
if (up) paths.push(buildPath(up));
|
|
1955
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
1956
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
1957
|
+
return import_types.RelateResultSchema.parse({
|
|
1958
|
+
a,
|
|
1959
|
+
b,
|
|
1960
|
+
related: true,
|
|
1961
|
+
direction,
|
|
1962
|
+
paths,
|
|
1963
|
+
...grainGap ? { grainGap: true } : {}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
1967
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
1968
|
+
let best = null;
|
|
1969
|
+
for (const nid of upstream) {
|
|
1970
|
+
if (nid === alertNodeId) continue;
|
|
1971
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
1972
|
+
if (ctx.outboundVolume === 0) continue;
|
|
1973
|
+
const isSource = ctx.callCount === 0;
|
|
1974
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
1975
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
1976
|
+
}
|
|
1977
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
1978
|
+
}
|
|
1979
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
1980
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
1981
|
+
if (!legacy) return null;
|
|
1982
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
1983
|
+
if (!navigation) return legacy;
|
|
1984
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
1985
|
+
}
|
|
1986
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
1987
|
+
const seedNode = legacy.rootCauseNode;
|
|
1988
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
1989
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
1990
|
+
const candidates = [];
|
|
1991
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
1992
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
1993
|
+
if (origin) {
|
|
1994
|
+
const path84 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1995
|
+
const originConfidence = confidenceFromMix(path84?.edges ?? [], now);
|
|
1996
|
+
candidates.push({
|
|
1997
|
+
node: origin.node,
|
|
1998
|
+
classification: "primary-failure",
|
|
1999
|
+
reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
|
|
2000
|
+
context: origin.ctx,
|
|
2001
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
2002
|
+
provenance: import_types.Provenance.OBSERVED
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
2006
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
2007
|
+
candidates.push({
|
|
2008
|
+
node: seedNode,
|
|
2009
|
+
classification: "symptom-only",
|
|
2010
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
2011
|
+
context: seedCtx,
|
|
2012
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
2013
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2014
|
+
});
|
|
2015
|
+
} else {
|
|
2016
|
+
candidates.push({
|
|
2017
|
+
node: seedNode,
|
|
2018
|
+
classification: "primary-failure",
|
|
2019
|
+
reason: legacy.rootCauseReason,
|
|
2020
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
2021
|
+
confidence: legacy.confidence,
|
|
2022
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
const top = candidates[0];
|
|
2026
|
+
let traversalPath = legacy.traversalPath;
|
|
2027
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
2028
|
+
if (top.node !== seedNode) {
|
|
2029
|
+
const path84 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
2030
|
+
if (path84) {
|
|
2031
|
+
traversalPath = path84.nodes;
|
|
2032
|
+
edgeProvenances = path84.edges.map((e) => e.provenance);
|
|
2033
|
+
} else {
|
|
2034
|
+
traversalPath = [errorNodeId, top.node];
|
|
2035
|
+
edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return import_types.RootCauseResultSchema.parse({
|
|
2039
|
+
rootCauseNode: top.node,
|
|
2040
|
+
rootCauseReason: top.reason,
|
|
2041
|
+
traversalPath,
|
|
2042
|
+
edgeProvenances,
|
|
2043
|
+
confidence: top.confidence,
|
|
2044
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
2045
|
+
candidates
|
|
1752
2046
|
});
|
|
1753
2047
|
}
|
|
1754
2048
|
|
|
@@ -2274,6 +2568,7 @@ var import_yaml = require("yaml");
|
|
|
2274
2568
|
var import_types3 = require("@neat.is/types");
|
|
2275
2569
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
2276
2570
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2571
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
2277
2572
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2278
2573
|
"node_modules",
|
|
2279
2574
|
".git",
|
|
@@ -2313,6 +2608,7 @@ async function isPythonVenvDir(dir) {
|
|
|
2313
2608
|
function isConfigFile(name) {
|
|
2314
2609
|
const ext = import_node_path4.default.extname(name);
|
|
2315
2610
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2611
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
2316
2612
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2317
2613
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2318
2614
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -3141,8 +3437,8 @@ function chiRoutesFromSource(source, parser) {
|
|
|
3141
3437
|
chiWalk(tree.rootNode, "", out);
|
|
3142
3438
|
return out;
|
|
3143
3439
|
}
|
|
3144
|
-
function stripChiRegex(
|
|
3145
|
-
return
|
|
3440
|
+
function stripChiRegex(path84) {
|
|
3441
|
+
return path84.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
|
|
3146
3442
|
}
|
|
3147
3443
|
function chiWalk(node, prefix, out) {
|
|
3148
3444
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3814,9 +4110,9 @@ function rubyRocketRoute(args) {
|
|
|
3814
4110
|
if (!pair || pair.type !== "pair") continue;
|
|
3815
4111
|
const k = pair.childForFieldName("key");
|
|
3816
4112
|
if (k?.type !== "string") continue;
|
|
3817
|
-
const
|
|
3818
|
-
if (
|
|
3819
|
-
return { path:
|
|
4113
|
+
const path84 = rubyLiteral(k);
|
|
4114
|
+
if (path84 === null) continue;
|
|
4115
|
+
return { path: path84, target: rubyLiteral(pair.childForFieldName("value")) };
|
|
3820
4116
|
}
|
|
3821
4117
|
return null;
|
|
3822
4118
|
}
|
|
@@ -4436,7 +4732,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
|
|
|
4436
4732
|
};
|
|
4437
4733
|
const filePrefix = /* @__PURE__ */ new Map();
|
|
4438
4734
|
const conflicted = /* @__PURE__ */ new Set();
|
|
4439
|
-
const
|
|
4735
|
+
const apply6 = (file, prefix) => {
|
|
4440
4736
|
if (conflicted.has(file)) return;
|
|
4441
4737
|
const existing = filePrefix.get(file);
|
|
4442
4738
|
if (existing === void 0) filePrefix.set(file, prefix);
|
|
@@ -4453,7 +4749,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
|
|
|
4453
4749
|
const info = fileInfo.get(file);
|
|
4454
4750
|
const rv = info?.routerVars.get(name);
|
|
4455
4751
|
if (!info || !rv) return;
|
|
4456
|
-
if (rv.declares && info.appVars.size === 0)
|
|
4752
|
+
if (rv.declares && info.appVars.size === 0) apply6(file, accPrefix);
|
|
4457
4753
|
for (const m of rv.mounts) {
|
|
4458
4754
|
if (!m.target) continue;
|
|
4459
4755
|
const t = resolveTarget(m.target, file);
|
|
@@ -4641,6 +4937,63 @@ function columnIsObserved(col) {
|
|
|
4641
4937
|
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
4642
4938
|
}
|
|
4643
4939
|
|
|
4940
|
+
// src/latency-digest.ts
|
|
4941
|
+
init_cjs_shims();
|
|
4942
|
+
var SUB = 16;
|
|
4943
|
+
var MIN_EXP = -10;
|
|
4944
|
+
var MAX_EXP = 22;
|
|
4945
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4946
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4947
|
+
function latencyBucketIndex(ms) {
|
|
4948
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4949
|
+
const e = Math.floor(Math.log2(ms));
|
|
4950
|
+
if (e < MIN_EXP) return 0;
|
|
4951
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4952
|
+
const base = 2 ** e;
|
|
4953
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4954
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4955
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4956
|
+
}
|
|
4957
|
+
function bucketRepresentativeMs(index) {
|
|
4958
|
+
if (index <= 0) return 0;
|
|
4959
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4960
|
+
const zeroBased = index - 1;
|
|
4961
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4962
|
+
const s = zeroBased % SUB;
|
|
4963
|
+
const base = 2 ** e;
|
|
4964
|
+
const lower = base * (1 + s / SUB);
|
|
4965
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4966
|
+
return (lower + upper) / 2;
|
|
4967
|
+
}
|
|
4968
|
+
function recordLatency(hist, ms) {
|
|
4969
|
+
const key = String(latencyBucketIndex(ms));
|
|
4970
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4971
|
+
return hist;
|
|
4972
|
+
}
|
|
4973
|
+
function quantile(hist, q) {
|
|
4974
|
+
const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
|
|
4975
|
+
let total = 0;
|
|
4976
|
+
for (const [, c] of entries) total += c;
|
|
4977
|
+
if (total === 0) return 0;
|
|
4978
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4979
|
+
let cumulative = 0;
|
|
4980
|
+
for (const [idx, c] of entries) {
|
|
4981
|
+
cumulative += c;
|
|
4982
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4983
|
+
}
|
|
4984
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4985
|
+
}
|
|
4986
|
+
function round(ms) {
|
|
4987
|
+
return Math.round(ms * 100) / 100;
|
|
4988
|
+
}
|
|
4989
|
+
function latencyPercentiles(hist) {
|
|
4990
|
+
if (!hist) return void 0;
|
|
4991
|
+
let total = 0;
|
|
4992
|
+
for (const c of Object.values(hist)) total += c;
|
|
4993
|
+
if (total === 0) return void 0;
|
|
4994
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4644
4997
|
// src/ingest.ts
|
|
4645
4998
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4646
4999
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5336,7 +5689,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5336
5689
|
graph.addNode(id, node);
|
|
5337
5690
|
return id;
|
|
5338
5691
|
}
|
|
5339
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5692
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5340
5693
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5341
5694
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5342
5695
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5344,10 +5697,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5344
5697
|
const existing = graph.getEdgeAttributes(id);
|
|
5345
5698
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5346
5699
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5700
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5701
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5347
5702
|
const newSignal = {
|
|
5348
5703
|
spanCount: newSpanCount,
|
|
5349
5704
|
errorCount: newErrorCount,
|
|
5350
|
-
lastObservedAgeMs: 0
|
|
5705
|
+
lastObservedAgeMs: 0,
|
|
5706
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5707
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5708
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5351
5709
|
};
|
|
5352
5710
|
const updated = {
|
|
5353
5711
|
...existing,
|
|
@@ -5362,10 +5720,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5362
5720
|
graph.replaceEdgeAttributes(id, updated);
|
|
5363
5721
|
return { edge: updated, created: false };
|
|
5364
5722
|
}
|
|
5723
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5724
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5365
5725
|
const signal = {
|
|
5366
5726
|
spanCount: 1,
|
|
5367
5727
|
errorCount: isError ? 1 : 0,
|
|
5368
|
-
lastObservedAgeMs: 0
|
|
5728
|
+
lastObservedAgeMs: 0,
|
|
5729
|
+
...latencyHist ? { latencyHist } : {},
|
|
5730
|
+
...latencyMs ? { latencyMs } : {}
|
|
5369
5731
|
};
|
|
5370
5732
|
const edge = {
|
|
5371
5733
|
id,
|
|
@@ -5429,6 +5791,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
5429
5791
|
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
|
|
5430
5792
|
await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5431
5793
|
}
|
|
5794
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
5795
|
+
const ev = {
|
|
5796
|
+
id: input.id,
|
|
5797
|
+
timestamp: input.timestamp,
|
|
5798
|
+
service: input.service,
|
|
5799
|
+
traceId: input.id,
|
|
5800
|
+
spanId: input.id,
|
|
5801
|
+
errorType: input.errorType,
|
|
5802
|
+
errorMessage: input.errorMessage,
|
|
5803
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
5804
|
+
affectedNode: input.affectedNode
|
|
5805
|
+
};
|
|
5806
|
+
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
|
|
5807
|
+
await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5808
|
+
}
|
|
5432
5809
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
5433
5810
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
5434
5811
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -5575,6 +5952,7 @@ async function handleSpan(ctx, span) {
|
|
|
5575
5952
|
}
|
|
5576
5953
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5577
5954
|
const isError = span.statusCode === 2;
|
|
5955
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5578
5956
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5579
5957
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5580
5958
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5613,7 +5991,8 @@ async function handleSpan(ctx, span) {
|
|
|
5613
5991
|
targetId,
|
|
5614
5992
|
ts,
|
|
5615
5993
|
isError,
|
|
5616
|
-
callSiteEvidence
|
|
5994
|
+
callSiteEvidence,
|
|
5995
|
+
durationMs
|
|
5617
5996
|
);
|
|
5618
5997
|
if (result) affectedNode = targetId;
|
|
5619
5998
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5625,7 +6004,8 @@ async function handleSpan(ctx, span) {
|
|
|
5625
6004
|
collectionId,
|
|
5626
6005
|
ts,
|
|
5627
6006
|
isError,
|
|
5628
|
-
callSiteEvidence
|
|
6007
|
+
callSiteEvidence,
|
|
6008
|
+
durationMs
|
|
5629
6009
|
);
|
|
5630
6010
|
}
|
|
5631
6011
|
if (span.dbTable) {
|
|
@@ -5637,7 +6017,8 @@ async function handleSpan(ctx, span) {
|
|
|
5637
6017
|
tableId,
|
|
5638
6018
|
ts,
|
|
5639
6019
|
isError,
|
|
5640
|
-
callSiteEvidence
|
|
6020
|
+
callSiteEvidence,
|
|
6021
|
+
durationMs
|
|
5641
6022
|
);
|
|
5642
6023
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5643
6024
|
}
|
|
@@ -5656,7 +6037,8 @@ async function handleSpan(ctx, span) {
|
|
|
5656
6037
|
targetId,
|
|
5657
6038
|
ts,
|
|
5658
6039
|
isError,
|
|
5659
|
-
callSiteEvidence
|
|
6040
|
+
callSiteEvidence,
|
|
6041
|
+
durationMs
|
|
5660
6042
|
);
|
|
5661
6043
|
if (result) affectedNode = targetId;
|
|
5662
6044
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5673,7 +6055,8 @@ async function handleSpan(ctx, span) {
|
|
|
5673
6055
|
targetId,
|
|
5674
6056
|
ts,
|
|
5675
6057
|
isError,
|
|
5676
|
-
callSiteEvidence
|
|
6058
|
+
callSiteEvidence,
|
|
6059
|
+
durationMs
|
|
5677
6060
|
);
|
|
5678
6061
|
if (result) affectedNode = targetId;
|
|
5679
6062
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5685,7 +6068,8 @@ async function handleSpan(ctx, span) {
|
|
|
5685
6068
|
targetId,
|
|
5686
6069
|
ts,
|
|
5687
6070
|
isError,
|
|
5688
|
-
callSiteEvidence
|
|
6071
|
+
callSiteEvidence,
|
|
6072
|
+
durationMs
|
|
5689
6073
|
);
|
|
5690
6074
|
if (result) affectedNode = targetId;
|
|
5691
6075
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5701,7 +6085,8 @@ async function handleSpan(ctx, span) {
|
|
|
5701
6085
|
targetId,
|
|
5702
6086
|
ts,
|
|
5703
6087
|
isError,
|
|
5704
|
-
callSiteEvidence
|
|
6088
|
+
callSiteEvidence,
|
|
6089
|
+
durationMs
|
|
5705
6090
|
);
|
|
5706
6091
|
if (result) affectedNode = targetId;
|
|
5707
6092
|
} else {
|
|
@@ -5717,7 +6102,8 @@ async function handleSpan(ctx, span) {
|
|
|
5717
6102
|
targetId,
|
|
5718
6103
|
ts,
|
|
5719
6104
|
isError,
|
|
5720
|
-
callSiteEvidence
|
|
6105
|
+
callSiteEvidence,
|
|
6106
|
+
durationMs
|
|
5721
6107
|
);
|
|
5722
6108
|
affectedNode = targetId;
|
|
5723
6109
|
resolvedViaAddress = true;
|
|
@@ -5730,7 +6116,8 @@ async function handleSpan(ctx, span) {
|
|
|
5730
6116
|
frontierNodeId,
|
|
5731
6117
|
ts,
|
|
5732
6118
|
isError,
|
|
5733
|
-
callSiteEvidence
|
|
6119
|
+
callSiteEvidence,
|
|
6120
|
+
durationMs
|
|
5734
6121
|
);
|
|
5735
6122
|
affectedNode = frontierNodeId;
|
|
5736
6123
|
resolvedViaAddress = true;
|
|
@@ -5756,7 +6143,8 @@ async function handleSpan(ctx, span) {
|
|
|
5756
6143
|
sourceId,
|
|
5757
6144
|
ts,
|
|
5758
6145
|
isError,
|
|
5759
|
-
fallbackEvidence
|
|
6146
|
+
fallbackEvidence,
|
|
6147
|
+
durationMs
|
|
5760
6148
|
);
|
|
5761
6149
|
}
|
|
5762
6150
|
}
|
|
@@ -5770,7 +6158,7 @@ async function handleSpan(ctx, span) {
|
|
|
5770
6158
|
);
|
|
5771
6159
|
if (routeNodeId) {
|
|
5772
6160
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5773
|
-
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
6161
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5774
6162
|
}
|
|
5775
6163
|
}
|
|
5776
6164
|
if (span.statusCode === 2) {
|
|
@@ -11242,8 +11630,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
|
|
|
11242
11630
|
var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
|
|
11243
11631
|
var import_types35 = require("@neat.is/types");
|
|
11244
11632
|
init_otel();
|
|
11245
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
11246
11633
|
var PARSE_CHUNK10 = 16384;
|
|
11634
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
11635
|
+
"Query",
|
|
11636
|
+
"QueryContext",
|
|
11637
|
+
"QueryRow",
|
|
11638
|
+
"QueryRowContext",
|
|
11639
|
+
"Exec",
|
|
11640
|
+
"ExecContext",
|
|
11641
|
+
"Prepare",
|
|
11642
|
+
"PrepareContext"
|
|
11643
|
+
]);
|
|
11644
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
11645
|
+
"Get",
|
|
11646
|
+
"Select",
|
|
11647
|
+
"Queryx",
|
|
11648
|
+
"QueryRowx",
|
|
11649
|
+
"NamedExec",
|
|
11650
|
+
"NamedQuery",
|
|
11651
|
+
"MustExec",
|
|
11652
|
+
"Preparex",
|
|
11653
|
+
"GetContext",
|
|
11654
|
+
"SelectContext"
|
|
11655
|
+
]);
|
|
11656
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
11657
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
11658
|
+
function makeGoParser3() {
|
|
11659
|
+
const p = new import_tree_sitter14.default();
|
|
11660
|
+
p.setLanguage(import_tree_sitter_go3.default);
|
|
11661
|
+
return p;
|
|
11662
|
+
}
|
|
11663
|
+
function parseSource10(parser, source) {
|
|
11664
|
+
return parser.parse(
|
|
11665
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
11666
|
+
);
|
|
11667
|
+
}
|
|
11247
11668
|
function walk7(node, visit) {
|
|
11248
11669
|
visit(node);
|
|
11249
11670
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -11251,25 +11672,54 @@ function walk7(node, visit) {
|
|
|
11251
11672
|
if (child) walk7(child, visit);
|
|
11252
11673
|
}
|
|
11253
11674
|
}
|
|
11675
|
+
function goStringLiteralValue(node) {
|
|
11676
|
+
if (!node) return null;
|
|
11677
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
11678
|
+
const t = node.text;
|
|
11679
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
11680
|
+
}
|
|
11681
|
+
return null;
|
|
11682
|
+
}
|
|
11683
|
+
function goImportsAny(root, names) {
|
|
11684
|
+
let found = false;
|
|
11685
|
+
walk7(root, (node) => {
|
|
11686
|
+
if (found || node.type !== "import_spec") return;
|
|
11687
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
11688
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
11689
|
+
if (value !== null && names.has(value)) found = true;
|
|
11690
|
+
}
|
|
11691
|
+
});
|
|
11692
|
+
return found;
|
|
11693
|
+
}
|
|
11694
|
+
function firstStringLiteralArg(argsNode) {
|
|
11695
|
+
if (!argsNode) return null;
|
|
11696
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
11697
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
11698
|
+
if (value !== null) return value;
|
|
11699
|
+
}
|
|
11700
|
+
return null;
|
|
11701
|
+
}
|
|
11254
11702
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
11255
11703
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
const
|
|
11259
|
-
|
|
11260
|
-
);
|
|
11704
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
11705
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
11706
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
11707
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
11708
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
11261
11709
|
const out = [];
|
|
11262
11710
|
walk7(tree.rootNode, (node) => {
|
|
11263
11711
|
if (node.type !== "call_expression") return;
|
|
11264
11712
|
const fn = node.childForFieldName("function");
|
|
11265
11713
|
if (fn?.type !== "selector_expression") return;
|
|
11266
11714
|
const method = fn.childForFieldName("field")?.text;
|
|
11267
|
-
if (!method
|
|
11268
|
-
const
|
|
11269
|
-
if (!
|
|
11270
|
-
const sql =
|
|
11715
|
+
if (!method) return;
|
|
11716
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
11717
|
+
if (!recognized) return;
|
|
11718
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
11719
|
+
if (sql === null) return;
|
|
11271
11720
|
const table = tableFromSqlStatement(sql);
|
|
11272
11721
|
if (!table) return;
|
|
11722
|
+
const columns = columnsFromSqlStatement(sql);
|
|
11273
11723
|
const line = node.startPosition.row + 1;
|
|
11274
11724
|
out.push({
|
|
11275
11725
|
infraId: (0, import_types35.infraId)("sql-table", table),
|
|
@@ -11277,7 +11727,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
11277
11727
|
kind: "sql-table",
|
|
11278
11728
|
edgeType: "CALLS",
|
|
11279
11729
|
confidenceKind: "verified-call-site",
|
|
11280
|
-
|
|
11730
|
+
...columns.length > 0 ? { columns } : {},
|
|
11731
|
+
evidence: {
|
|
11732
|
+
file: toPosix(import_node_path48.default.relative(serviceDir, file.path)),
|
|
11733
|
+
line,
|
|
11734
|
+
snippet: snippet(file.content, line)
|
|
11735
|
+
}
|
|
11281
11736
|
});
|
|
11282
11737
|
});
|
|
11283
11738
|
return out;
|
|
@@ -11291,12 +11746,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
|
|
|
11291
11746
|
var import_types36 = require("@neat.is/types");
|
|
11292
11747
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
11293
11748
|
var PARSE_CHUNK11 = 16384;
|
|
11294
|
-
function
|
|
11749
|
+
function makeGoParser4() {
|
|
11295
11750
|
const p = new import_tree_sitter15.default();
|
|
11296
11751
|
p.setLanguage(import_tree_sitter_go4.default);
|
|
11297
11752
|
return p;
|
|
11298
11753
|
}
|
|
11299
|
-
function
|
|
11754
|
+
function parseSource11(parser, source) {
|
|
11300
11755
|
return parser.parse(
|
|
11301
11756
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
11302
11757
|
);
|
|
@@ -11720,7 +12175,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
11720
12175
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
11721
12176
|
if (import_node_path49.default.extname(file.path) !== ".go") return [];
|
|
11722
12177
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11723
|
-
const tree =
|
|
12178
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11724
12179
|
const { structs, models, tableFor } = analyze(tree);
|
|
11725
12180
|
const out = [];
|
|
11726
12181
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -11751,7 +12206,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
11751
12206
|
function gormForeignKeys(file, serviceDir) {
|
|
11752
12207
|
if (import_node_path49.default.extname(file.path) !== ".go") return [];
|
|
11753
12208
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11754
|
-
const tree =
|
|
12209
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11755
12210
|
const { structs, models, tableFor } = analyze(tree);
|
|
11756
12211
|
const out = [];
|
|
11757
12212
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13978,7 +14433,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
13978
14433
|
init_cjs_shims();
|
|
13979
14434
|
var import_fastify2 = __toESM(require("fastify"), 1);
|
|
13980
14435
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
13981
|
-
var
|
|
14436
|
+
var import_types86 = require("@neat.is/types");
|
|
13982
14437
|
|
|
13983
14438
|
// src/extend/index.ts
|
|
13984
14439
|
init_cjs_shims();
|
|
@@ -15307,6 +15762,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
15307
15762
|
unresolved++;
|
|
15308
15763
|
continue;
|
|
15309
15764
|
}
|
|
15765
|
+
if (signal.incident) {
|
|
15766
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
15767
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
15768
|
+
unresolved++;
|
|
15769
|
+
continue;
|
|
15770
|
+
}
|
|
15771
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
15772
|
+
id: signal.incident.id,
|
|
15773
|
+
timestamp: signal.incident.timestamp,
|
|
15774
|
+
service: signal.incident.service,
|
|
15775
|
+
errorType: signal.incident.errorType,
|
|
15776
|
+
errorMessage: signal.incident.errorMessage,
|
|
15777
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
15778
|
+
affectedNode: resolved.targetNodeId
|
|
15779
|
+
});
|
|
15780
|
+
continue;
|
|
15781
|
+
}
|
|
15310
15782
|
if (resolved.ensureInfraNode) {
|
|
15311
15783
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
15312
15784
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -15772,10 +16244,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
|
|
|
15772
16244
|
// src/connectors/supabase/map.ts
|
|
15773
16245
|
var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
|
|
15774
16246
|
var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
|
|
15775
|
-
function targetFromRestPath(
|
|
15776
|
-
const rpcMatch = REST_RPC_PATH_RE.exec(
|
|
16247
|
+
function targetFromRestPath(path84) {
|
|
16248
|
+
const rpcMatch = REST_RPC_PATH_RE.exec(path84);
|
|
15777
16249
|
if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
|
|
15778
|
-
const tableMatch = REST_TABLE_PATH_RE.exec(
|
|
16250
|
+
const tableMatch = REST_TABLE_PATH_RE.exec(path84);
|
|
15779
16251
|
if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
|
|
15780
16252
|
return null;
|
|
15781
16253
|
}
|
|
@@ -16379,9 +16851,9 @@ function parseFirebaseTargetName(targetName) {
|
|
|
16379
16851
|
const secondSep = rest.indexOf(FIELD_SEP);
|
|
16380
16852
|
if (secondSep === -1) return null;
|
|
16381
16853
|
const method = rest.slice(0, secondSep);
|
|
16382
|
-
const
|
|
16383
|
-
if (!resourceName || !method || !
|
|
16384
|
-
return { resourceName, method, path:
|
|
16854
|
+
const path84 = rest.slice(secondSep + 1);
|
|
16855
|
+
if (!resourceName || !method || !path84) return null;
|
|
16856
|
+
return { resourceName, method, path: path84 };
|
|
16385
16857
|
}
|
|
16386
16858
|
function resourceNameFor(type, labels) {
|
|
16387
16859
|
if (!labels) return null;
|
|
@@ -16419,14 +16891,14 @@ function mapLogEntryToSignal(entry2) {
|
|
|
16419
16891
|
if (!req) return null;
|
|
16420
16892
|
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
16421
16893
|
const method = req.requestMethod.toUpperCase();
|
|
16422
|
-
const
|
|
16423
|
-
if (
|
|
16894
|
+
const path84 = pathFromRequestUrl(req.requestUrl);
|
|
16895
|
+
if (path84 === null) return null;
|
|
16424
16896
|
const timestamp = entry2.timestamp;
|
|
16425
16897
|
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
16426
16898
|
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
|
|
16427
16899
|
return {
|
|
16428
16900
|
targetKind: resourceType,
|
|
16429
|
-
targetName: packFirebaseTargetName({ resourceName, method, path:
|
|
16901
|
+
targetName: packFirebaseTargetName({ resourceName, method, path: path84 }),
|
|
16430
16902
|
callCount: 1,
|
|
16431
16903
|
errorCount: isError ? 1 : 0,
|
|
16432
16904
|
lastObservedIso: timestamp
|
|
@@ -16633,7 +17105,7 @@ function mapEventToSignal(event) {
|
|
|
16633
17105
|
if (Number.isNaN(observedAt.getTime())) return null;
|
|
16634
17106
|
const statusCode = metadata?.statusCode;
|
|
16635
17107
|
const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
|
|
16636
|
-
const
|
|
17108
|
+
const path84 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
|
|
16637
17109
|
return {
|
|
16638
17110
|
targetKind: CLOUDFLARE_TARGET_KIND,
|
|
16639
17111
|
targetName: scriptName,
|
|
@@ -16641,7 +17113,7 @@ function mapEventToSignal(event) {
|
|
|
16641
17113
|
errorCount: isError ? 1 : 0,
|
|
16642
17114
|
lastObservedIso: observedAt.toISOString(),
|
|
16643
17115
|
method,
|
|
16644
|
-
...
|
|
17116
|
+
...path84 ? { path: path84 } : {},
|
|
16645
17117
|
...typeof statusCode === "number" ? { statusCode } : {},
|
|
16646
17118
|
...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
|
|
16647
17119
|
};
|
|
@@ -16687,8 +17159,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
|
|
|
16687
17159
|
});
|
|
16688
17160
|
return found;
|
|
16689
17161
|
}
|
|
16690
|
-
function findMatchingRouteNode(graph, serviceName, method,
|
|
16691
|
-
const normalizedPath = normalizePathTemplate(
|
|
17162
|
+
function findMatchingRouteNode(graph, serviceName, method, path84) {
|
|
17163
|
+
const normalizedPath = normalizePathTemplate(path84);
|
|
16692
17164
|
let found = null;
|
|
16693
17165
|
graph.forEachNode((id, attrs) => {
|
|
16694
17166
|
if (found) return;
|
|
@@ -16705,10 +17177,10 @@ function createCloudflareResolveTarget(config, graph) {
|
|
|
16705
17177
|
return (signal) => {
|
|
16706
17178
|
if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
|
|
16707
17179
|
const scriptName = signal.targetName;
|
|
16708
|
-
const { method, path:
|
|
17180
|
+
const { method, path: path84 } = signal;
|
|
16709
17181
|
const resolveRouteGrain = (serviceName, wholeFileId) => {
|
|
16710
|
-
if (!method || !
|
|
16711
|
-
return findMatchingRouteNode(graph, serviceName, method,
|
|
17182
|
+
if (!method || !path84) return wholeFileId;
|
|
17183
|
+
return findMatchingRouteNode(graph, serviceName, method, path84) ?? wholeFileId;
|
|
16712
17184
|
};
|
|
16713
17185
|
const mapping = config.workers?.[scriptName];
|
|
16714
17186
|
if (mapping) {
|
|
@@ -17060,9 +17532,9 @@ function parseCloudRunTargetName(targetName) {
|
|
|
17060
17532
|
const secondSep = rest.indexOf(FIELD_SEP2);
|
|
17061
17533
|
if (secondSep === -1) return null;
|
|
17062
17534
|
const method = rest.slice(0, secondSep);
|
|
17063
|
-
const
|
|
17064
|
-
if (!serviceName || !method || !
|
|
17065
|
-
return { serviceName, method, path:
|
|
17535
|
+
const path84 = rest.slice(secondSep + 1);
|
|
17536
|
+
if (!serviceName || !method || !path84) return null;
|
|
17537
|
+
return { serviceName, method, path: path84 };
|
|
17066
17538
|
}
|
|
17067
17539
|
|
|
17068
17540
|
// src/connectors/cloud-run/map.ts
|
|
@@ -17091,14 +17563,14 @@ function mapLogEntryToSignal2(entry2) {
|
|
|
17091
17563
|
if (!req) return null;
|
|
17092
17564
|
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
17093
17565
|
const method = req.requestMethod.toUpperCase();
|
|
17094
|
-
const
|
|
17095
|
-
if (
|
|
17566
|
+
const path84 = pathFromRequestUrl2(req.requestUrl);
|
|
17567
|
+
if (path84 === null) return null;
|
|
17096
17568
|
const timestamp = entry2.timestamp;
|
|
17097
17569
|
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
17098
17570
|
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
|
|
17099
17571
|
return {
|
|
17100
17572
|
targetKind: CLOUD_RUN_TARGET_KIND,
|
|
17101
|
-
targetName: packCloudRunTargetName({ serviceName, method, path:
|
|
17573
|
+
targetName: packCloudRunTargetName({ serviceName, method, path: path84 }),
|
|
17102
17574
|
callCount: 1,
|
|
17103
17575
|
errorCount: isError ? 1 : 0,
|
|
17104
17576
|
lastObservedIso: timestamp
|
|
@@ -17137,14 +17609,14 @@ function createCloudRunResolveTarget(graph, config) {
|
|
|
17137
17609
|
if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
|
|
17138
17610
|
const identity = parseCloudRunTargetName(signal.targetName);
|
|
17139
17611
|
if (!identity) return null;
|
|
17140
|
-
const { serviceName: gcpServiceName, method, path:
|
|
17612
|
+
const { serviceName: gcpServiceName, method, path: path84 } = identity;
|
|
17141
17613
|
const mappedService = config.serviceMap?.[gcpServiceName];
|
|
17142
17614
|
if (mappedService) {
|
|
17143
17615
|
const routeNodeId = findMatchingRouteNode2(
|
|
17144
17616
|
graph,
|
|
17145
17617
|
mappedService,
|
|
17146
17618
|
method,
|
|
17147
|
-
normalizePathTemplate(
|
|
17619
|
+
normalizePathTemplate(path84)
|
|
17148
17620
|
);
|
|
17149
17621
|
if (routeNodeId) {
|
|
17150
17622
|
return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types71.EdgeType.CALLS };
|
|
@@ -17541,6 +18013,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
17541
18013
|
};
|
|
17542
18014
|
}
|
|
17543
18015
|
|
|
18016
|
+
// src/connectors/eas/index.ts
|
|
18017
|
+
init_cjs_shims();
|
|
18018
|
+
|
|
18019
|
+
// src/connectors/eas/client.ts
|
|
18020
|
+
init_cjs_shims();
|
|
18021
|
+
|
|
18022
|
+
// src/connectors/eas/types.ts
|
|
18023
|
+
init_cjs_shims();
|
|
18024
|
+
function readEasCredentials(raw) {
|
|
18025
|
+
const token = raw["token"];
|
|
18026
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
18027
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
18028
|
+
}
|
|
18029
|
+
return { token };
|
|
18030
|
+
}
|
|
18031
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
18032
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
18033
|
+
"SPIN_UP_BUILDER",
|
|
18034
|
+
"PREPARE_CREDENTIALS",
|
|
18035
|
+
"RESTORE_CACHE",
|
|
18036
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
18037
|
+
]);
|
|
18038
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
18039
|
+
function isTransientFailure(err) {
|
|
18040
|
+
if (!err) return false;
|
|
18041
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
18042
|
+
if (phase) {
|
|
18043
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
18044
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
18045
|
+
}
|
|
18046
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
18047
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
18048
|
+
return false;
|
|
18049
|
+
}
|
|
18050
|
+
var FIELD_SEP3 = "\0";
|
|
18051
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
18052
|
+
function packEasTargetName(identity) {
|
|
18053
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
18054
|
+
}
|
|
18055
|
+
function parseEasTargetName(targetName) {
|
|
18056
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
18057
|
+
if (sep === -1) return null;
|
|
18058
|
+
const serviceName = targetName.slice(0, sep);
|
|
18059
|
+
const phase = targetName.slice(sep + 1);
|
|
18060
|
+
if (!serviceName) return null;
|
|
18061
|
+
return { serviceName, phase };
|
|
18062
|
+
}
|
|
18063
|
+
|
|
18064
|
+
// src/connectors/eas/client.ts
|
|
18065
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
18066
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
18067
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
18068
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
18069
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
18070
|
+
var BUILDS_QUERY = `
|
|
18071
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
18072
|
+
app {
|
|
18073
|
+
byId(appId: $appId) {
|
|
18074
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
18075
|
+
id
|
|
18076
|
+
status
|
|
18077
|
+
platform
|
|
18078
|
+
buildProfile
|
|
18079
|
+
gitCommitHash
|
|
18080
|
+
gitCommitMessage
|
|
18081
|
+
gitRef
|
|
18082
|
+
isGitWorkingTreeDirty
|
|
18083
|
+
createdAt
|
|
18084
|
+
completedAt
|
|
18085
|
+
error {
|
|
18086
|
+
buildPhase
|
|
18087
|
+
errorCode
|
|
18088
|
+
message
|
|
18089
|
+
docsUrl
|
|
18090
|
+
}
|
|
18091
|
+
logFileUrls
|
|
18092
|
+
}
|
|
18093
|
+
}
|
|
18094
|
+
}
|
|
18095
|
+
}
|
|
18096
|
+
`;
|
|
18097
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
18098
|
+
const res = await junctionFetch(
|
|
18099
|
+
apiUrl,
|
|
18100
|
+
{
|
|
18101
|
+
method: "POST",
|
|
18102
|
+
headers: {
|
|
18103
|
+
"Content-Type": "application/json",
|
|
18104
|
+
...bearerAuthHeader(token)
|
|
18105
|
+
},
|
|
18106
|
+
body: JSON.stringify({ query, variables })
|
|
18107
|
+
},
|
|
18108
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
18109
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
18110
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
18111
|
+
);
|
|
18112
|
+
if (!res.ok) {
|
|
18113
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
18114
|
+
}
|
|
18115
|
+
const body = await res.json();
|
|
18116
|
+
if (body.errors && body.errors.length > 0) {
|
|
18117
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
18118
|
+
}
|
|
18119
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
18120
|
+
return body.data;
|
|
18121
|
+
}
|
|
18122
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
18123
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
18124
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
18125
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
18126
|
+
const out = [];
|
|
18127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18128
|
+
for (let page = 0; page < maxPages; page++) {
|
|
18129
|
+
const data = await easGraphQL(
|
|
18130
|
+
apiUrl,
|
|
18131
|
+
token,
|
|
18132
|
+
BUILDS_QUERY,
|
|
18133
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
18134
|
+
config.appId,
|
|
18135
|
+
fetchImpl
|
|
18136
|
+
);
|
|
18137
|
+
const builds = data.app?.byId?.builds;
|
|
18138
|
+
if (!Array.isArray(builds)) break;
|
|
18139
|
+
let added = 0;
|
|
18140
|
+
for (const b of builds) {
|
|
18141
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
18142
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
18143
|
+
if (seen.has(b.id)) continue;
|
|
18144
|
+
seen.add(b.id);
|
|
18145
|
+
out.push(b);
|
|
18146
|
+
added++;
|
|
18147
|
+
}
|
|
18148
|
+
if (builds.length < pageSize) break;
|
|
18149
|
+
if (added === 0) break;
|
|
18150
|
+
}
|
|
18151
|
+
return out;
|
|
18152
|
+
}
|
|
18153
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
18154
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
18155
|
+
const doFetch = fetchImpl ?? fetch;
|
|
18156
|
+
const chunks = [];
|
|
18157
|
+
for (const url of logFileUrls) {
|
|
18158
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
18159
|
+
try {
|
|
18160
|
+
const res = await doFetch(url);
|
|
18161
|
+
if (!res.ok) continue;
|
|
18162
|
+
chunks.push(await res.text());
|
|
18163
|
+
} catch {
|
|
18164
|
+
}
|
|
18165
|
+
}
|
|
18166
|
+
const joined = chunks.join("\n");
|
|
18167
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
18168
|
+
}
|
|
18169
|
+
|
|
18170
|
+
// src/connectors/eas/map.ts
|
|
18171
|
+
init_cjs_shims();
|
|
18172
|
+
function buildEventTime(build) {
|
|
18173
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
18174
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
18175
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
18176
|
+
}
|
|
18177
|
+
function incidentMessage2(build) {
|
|
18178
|
+
const err = build.error ?? {};
|
|
18179
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
18180
|
+
const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
|
|
18181
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
18182
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
18183
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
18184
|
+
}
|
|
18185
|
+
return msg;
|
|
18186
|
+
}
|
|
18187
|
+
function incidentAttributes(build) {
|
|
18188
|
+
const attrs = {};
|
|
18189
|
+
const err = build.error ?? {};
|
|
18190
|
+
const put = (k, v) => {
|
|
18191
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
18192
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
18193
|
+
};
|
|
18194
|
+
put("eas.buildId", build.id);
|
|
18195
|
+
put("eas.platform", build.platform ?? void 0);
|
|
18196
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
18197
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
18198
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
18199
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
18200
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
18201
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
18202
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
18203
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
18204
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
18205
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
18206
|
+
}
|
|
18207
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
18208
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
18209
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
18210
|
+
attrs["eas.logs"] = build.logsText;
|
|
18211
|
+
}
|
|
18212
|
+
return attrs;
|
|
18213
|
+
}
|
|
18214
|
+
function mapBuildToSignal(build, serviceName) {
|
|
18215
|
+
if (!build || typeof build !== "object") return null;
|
|
18216
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
18217
|
+
if (!build.error) return null;
|
|
18218
|
+
if (isTransientFailure(build.error)) return null;
|
|
18219
|
+
const timestamp = buildEventTime(build);
|
|
18220
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
18221
|
+
return {
|
|
18222
|
+
targetKind: EAS_TARGET_KIND,
|
|
18223
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
18224
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
18225
|
+
callCount: 0,
|
|
18226
|
+
errorCount: 0,
|
|
18227
|
+
lastObservedIso: timestamp,
|
|
18228
|
+
incident: {
|
|
18229
|
+
id: `eas:build:${build.id}`,
|
|
18230
|
+
timestamp,
|
|
18231
|
+
service: serviceName,
|
|
18232
|
+
errorType: "eas-build-failure",
|
|
18233
|
+
errorMessage: incidentMessage2(build),
|
|
18234
|
+
attributes: incidentAttributes(build)
|
|
18235
|
+
}
|
|
18236
|
+
};
|
|
18237
|
+
}
|
|
18238
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
18239
|
+
const out = [];
|
|
18240
|
+
for (const build of builds) {
|
|
18241
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
18242
|
+
if (signal) out.push(signal);
|
|
18243
|
+
}
|
|
18244
|
+
return out;
|
|
18245
|
+
}
|
|
18246
|
+
|
|
18247
|
+
// src/connectors/eas/resolve.ts
|
|
18248
|
+
init_cjs_shims();
|
|
18249
|
+
var import_types83 = require("@neat.is/types");
|
|
18250
|
+
var NO_ENV2 = "unknown";
|
|
18251
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
18252
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
18253
|
+
"READ_APP_CONFIG",
|
|
18254
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
18255
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
18256
|
+
]);
|
|
18257
|
+
function configBasenamesForPhase(phase) {
|
|
18258
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
18259
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
18260
|
+
return [];
|
|
18261
|
+
}
|
|
18262
|
+
function configNodeService(graph, configNodeId) {
|
|
18263
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
18264
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
18265
|
+
if (edge.type !== import_types83.EdgeType.CONFIGURED_BY) continue;
|
|
18266
|
+
const parsed = (0, import_types83.parseFileId)(edge.source);
|
|
18267
|
+
if (parsed) return parsed.service;
|
|
18268
|
+
}
|
|
18269
|
+
return null;
|
|
18270
|
+
}
|
|
18271
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
18272
|
+
let scoped = null;
|
|
18273
|
+
let anyMatch = null;
|
|
18274
|
+
graph.forEachNode((id, attrs) => {
|
|
18275
|
+
if (scoped) return;
|
|
18276
|
+
const node = attrs;
|
|
18277
|
+
if (node.type !== import_types83.NodeType.ConfigNode) return;
|
|
18278
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
18279
|
+
if (anyMatch === null) anyMatch = id;
|
|
18280
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
18281
|
+
});
|
|
18282
|
+
return scoped ?? anyMatch;
|
|
18283
|
+
}
|
|
18284
|
+
function createEasResolveTarget(graph) {
|
|
18285
|
+
return (signal) => {
|
|
18286
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
18287
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
18288
|
+
if (!identity) return null;
|
|
18289
|
+
const { serviceName, phase } = identity;
|
|
18290
|
+
const basenames = configBasenamesForPhase(phase);
|
|
18291
|
+
if (basenames.length > 0) {
|
|
18292
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
18293
|
+
if (configNodeId) {
|
|
18294
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: import_types83.EdgeType.CALLS };
|
|
18295
|
+
}
|
|
18296
|
+
}
|
|
18297
|
+
return {
|
|
18298
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
18299
|
+
serviceName,
|
|
18300
|
+
edgeType: import_types83.EdgeType.CALLS
|
|
18301
|
+
};
|
|
18302
|
+
};
|
|
18303
|
+
}
|
|
18304
|
+
|
|
18305
|
+
// src/connectors/eas/index.ts
|
|
18306
|
+
function isBuildSince(build, sinceIso) {
|
|
18307
|
+
const t = Date.parse(buildEventTime(build));
|
|
18308
|
+
const s = Date.parse(sinceIso);
|
|
18309
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
18310
|
+
return t > s;
|
|
18311
|
+
}
|
|
18312
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
18313
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
18314
|
+
if (!since) return floor.toISOString();
|
|
18315
|
+
const sinceMs = new Date(since).getTime();
|
|
18316
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
18317
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
18318
|
+
}
|
|
18319
|
+
var EasConnector = class {
|
|
18320
|
+
constructor(config, fetchImpl) {
|
|
18321
|
+
this.config = config;
|
|
18322
|
+
this.fetchImpl = fetchImpl;
|
|
18323
|
+
}
|
|
18324
|
+
config;
|
|
18325
|
+
fetchImpl;
|
|
18326
|
+
provider = "eas";
|
|
18327
|
+
async poll(ctx) {
|
|
18328
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
18329
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
18330
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
18331
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
18332
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
18333
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
18334
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
18335
|
+
for (const build of fresh) {
|
|
18336
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
18337
|
+
}
|
|
18338
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
18339
|
+
}
|
|
18340
|
+
};
|
|
18341
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
18342
|
+
return {
|
|
18343
|
+
connector: new EasConnector(config, fetchImpl),
|
|
18344
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
18345
|
+
};
|
|
18346
|
+
}
|
|
18347
|
+
|
|
17544
18348
|
// src/connectors/registry.ts
|
|
17545
18349
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
17546
18350
|
async function authProbe(input) {
|
|
@@ -17828,6 +18632,41 @@ var PROVIDER_DISPATCH = {
|
|
|
17828
18632
|
...fetchImpl ? { fetchImpl } : {}
|
|
17829
18633
|
});
|
|
17830
18634
|
}
|
|
18635
|
+
},
|
|
18636
|
+
eas: {
|
|
18637
|
+
provider: "eas",
|
|
18638
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
18639
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
18640
|
+
primaryCredentialKey: "token",
|
|
18641
|
+
requiredCredentialFields: ["token"],
|
|
18642
|
+
requiredOptionFields: ["appId"],
|
|
18643
|
+
build(graph, options) {
|
|
18644
|
+
return createEasConnector(graph, options);
|
|
18645
|
+
},
|
|
18646
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
18647
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
18648
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
18649
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
18650
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
18651
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
18652
|
+
// silently at the first poll.
|
|
18653
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
18654
|
+
const cfg = options;
|
|
18655
|
+
const appId = String(cfg.appId ?? "");
|
|
18656
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
18657
|
+
const probeConfig = {
|
|
18658
|
+
appId,
|
|
18659
|
+
pageSize: 1,
|
|
18660
|
+
maxPages: 1,
|
|
18661
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
18662
|
+
};
|
|
18663
|
+
try {
|
|
18664
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
18665
|
+
return { ok: true };
|
|
18666
|
+
} catch (err) {
|
|
18667
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
18668
|
+
}
|
|
18669
|
+
}
|
|
17831
18670
|
}
|
|
17832
18671
|
};
|
|
17833
18672
|
function vercelCredsFrom(credentials) {
|
|
@@ -18040,7 +18879,11 @@ async function startConnectorPolling(input) {
|
|
|
18040
18879
|
const stopFns = all.map(
|
|
18041
18880
|
(registration) => startConnectorPollLoop(
|
|
18042
18881
|
registration.connector,
|
|
18043
|
-
{
|
|
18882
|
+
{
|
|
18883
|
+
projectDir: input.projectDir,
|
|
18884
|
+
credentials: registration.credentials,
|
|
18885
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
18886
|
+
},
|
|
18044
18887
|
input.graph,
|
|
18045
18888
|
registration.resolveTarget,
|
|
18046
18889
|
{ intervalMs: registration.intervalMs, connectorId: registration.id }
|
|
@@ -18258,11 +19101,11 @@ function registerRoutes(scope, ctx) {
|
|
|
18258
19101
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18259
19102
|
const parsed = [];
|
|
18260
19103
|
for (const c of candidates) {
|
|
18261
|
-
const r =
|
|
19104
|
+
const r = import_types86.DivergenceTypeSchema.safeParse(c);
|
|
18262
19105
|
if (!r.success) {
|
|
18263
19106
|
return reply.code(400).send({
|
|
18264
19107
|
error: `unknown divergence type "${c}"`,
|
|
18265
|
-
allowed:
|
|
19108
|
+
allowed: import_types86.DivergenceTypeSchema.options
|
|
18266
19109
|
});
|
|
18267
19110
|
}
|
|
18268
19111
|
parsed.push(r.data);
|
|
@@ -18369,10 +19212,15 @@ function registerRoutes(scope, ctx) {
|
|
|
18369
19212
|
}
|
|
18370
19213
|
const reg = built.registration;
|
|
18371
19214
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
19215
|
+
const incidentsPath = errorsPathFor(proj);
|
|
18372
19216
|
try {
|
|
18373
19217
|
const result = await ctx.runPoll(
|
|
18374
19218
|
reg.connector,
|
|
18375
|
-
{
|
|
19219
|
+
{
|
|
19220
|
+
projectDir: proj.scanPath ?? "",
|
|
19221
|
+
credentials: reg.credentials,
|
|
19222
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
19223
|
+
},
|
|
18376
19224
|
proj.graph,
|
|
18377
19225
|
reg.resolveTarget
|
|
18378
19226
|
);
|
|
@@ -18442,6 +19290,34 @@ function registerRoutes(scope, ctx) {
|
|
|
18442
19290
|
}
|
|
18443
19291
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
18444
19292
|
});
|
|
19293
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
19294
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
19295
|
+
if (!proj) return;
|
|
19296
|
+
const { nodeId } = req.params;
|
|
19297
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
19298
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
19299
|
+
}
|
|
19300
|
+
const direction = req.query.direction;
|
|
19301
|
+
if (direction !== "up" && direction !== "down") {
|
|
19302
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
19303
|
+
}
|
|
19304
|
+
const epath = errorsPathFor(proj);
|
|
19305
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
19306
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
19307
|
+
});
|
|
19308
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
19309
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
19310
|
+
if (!proj) return;
|
|
19311
|
+
const { a, b } = req.query;
|
|
19312
|
+
if (!a || !b) {
|
|
19313
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
19314
|
+
}
|
|
19315
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
19316
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
19317
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
19318
|
+
}
|
|
19319
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
19320
|
+
});
|
|
18445
19321
|
scope.get("/search", async (req, reply) => {
|
|
18446
19322
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18447
19323
|
if (!proj) return;
|
|
@@ -18571,7 +19447,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18571
19447
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
18572
19448
|
let violations = await log.readAll();
|
|
18573
19449
|
if (req.query.severity) {
|
|
18574
|
-
const sev =
|
|
19450
|
+
const sev = import_types86.PolicySeveritySchema.safeParse(req.query.severity);
|
|
18575
19451
|
if (!sev.success) {
|
|
18576
19452
|
return reply.code(400).send({
|
|
18577
19453
|
error: "invalid severity",
|
|
@@ -18610,7 +19486,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18610
19486
|
scope.post("/policies/check", async (req, reply) => {
|
|
18611
19487
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18612
19488
|
if (!proj) return;
|
|
18613
|
-
const parsed =
|
|
19489
|
+
const parsed = import_types86.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
18614
19490
|
if (!parsed.success) {
|
|
18615
19491
|
return reply.code(400).send({
|
|
18616
19492
|
error: "invalid /policies/check body",
|
|
@@ -18943,7 +19819,7 @@ var import_node_fs34 = require("fs");
|
|
|
18943
19819
|
var import_node_path68 = __toESM(require("path"), 1);
|
|
18944
19820
|
|
|
18945
19821
|
// src/daemon.ts
|
|
18946
|
-
var
|
|
19822
|
+
var import_types87 = require("@neat.is/types");
|
|
18947
19823
|
function daemonJsonPath(scanPath) {
|
|
18948
19824
|
return import_node_path69.default.join(scanPath, "neat-out", "daemon.json");
|
|
18949
19825
|
}
|
|
@@ -19601,6 +20477,9 @@ async function startWatch(graph, opts) {
|
|
|
19601
20477
|
project: projectName,
|
|
19602
20478
|
graph,
|
|
19603
20479
|
projectDir: opts.scanPath,
|
|
20480
|
+
// Incident ledger for an incident-emitting connector (ADR-185), same path
|
|
20481
|
+
// `neat watch`'s own error-span writer already uses.
|
|
20482
|
+
errorsPath: opts.errorsPath,
|
|
19604
20483
|
...opts.neatHome ? { home: opts.neatHome } : {},
|
|
19605
20484
|
onSkip: (skipped, reason) => console.warn(
|
|
19606
20485
|
`neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
|
|
@@ -22229,14 +23108,519 @@ async function apply3(installPlan) {
|
|
|
22229
23108
|
await import_node_fs41.promises.writeFile(generated.file, generated.contents, "utf8");
|
|
22230
23109
|
writtenFiles.push(generated.file);
|
|
22231
23110
|
}
|
|
22232
|
-
|
|
23111
|
+
const wroteManifest = writtenFiles.some((f) => f.split(/[\\/]/).pop() === "go.mod");
|
|
23112
|
+
return {
|
|
23113
|
+
serviceDir: installPlan.serviceDir,
|
|
23114
|
+
outcome: writtenFiles.length ? "instrumented" : "already-instrumented",
|
|
23115
|
+
writtenFiles,
|
|
23116
|
+
...wroteManifest ? { followUpInstall: "go mod download" } : {}
|
|
23117
|
+
};
|
|
22233
23118
|
}
|
|
22234
23119
|
var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
|
|
22235
23120
|
|
|
23121
|
+
// src/installers/ruby.ts
|
|
23122
|
+
init_cjs_shims();
|
|
23123
|
+
var import_node_fs42 = require("fs");
|
|
23124
|
+
var import_node_path76 = __toESM(require("path"), 1);
|
|
23125
|
+
var RUBY_MARKERS = [
|
|
23126
|
+
"Gemfile",
|
|
23127
|
+
"Gemfile.lock"
|
|
23128
|
+
];
|
|
23129
|
+
var RUBY_GEMS = [
|
|
23130
|
+
{ name: "opentelemetry-sdk", version: "~> 1.5" },
|
|
23131
|
+
{ name: "opentelemetry-exporter-otlp", version: "~> 0.29" },
|
|
23132
|
+
{ name: "opentelemetry-instrumentation-all", version: "~> 0.62" }
|
|
23133
|
+
];
|
|
23134
|
+
var NEAT_OTEL_STAMP2 = "neat-otel-init v1";
|
|
23135
|
+
var INITIALIZER_REL = import_node_path76.default.join("config", "initializers", "neat_otel.rb");
|
|
23136
|
+
function neatOtelRb(opts = {}) {
|
|
23137
|
+
const service = opts.project ?? "ruby-service";
|
|
23138
|
+
const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
|
|
23139
|
+
return `# ${NEAT_OTEL_STAMP2} \u2014 generated by NEAT. Safe to re-generate; do not edit.
|
|
23140
|
+
# Rails auto-loads this at boot (config/initializers/*). It points the
|
|
23141
|
+
# OpenTelemetry SDK at your NEAT daemon, enables the Ruby auto-instrumentation
|
|
23142
|
+
# set, and installs a span processor that stamps code.file.path /
|
|
23143
|
+
# code.line.number / code.function.name on the CLIENT/PRODUCER spans your app
|
|
23144
|
+
# issues, so NEAT fuses each runtime span onto the source file that made the
|
|
23145
|
+
# call (docs/contracts/file-awareness.md). Absolute paths are emitted here;
|
|
23146
|
+
# ingest anchors them against the service root. If the OpenTelemetry gems are
|
|
23147
|
+
# not installed this file degrades to a no-op rather than breaking boot.
|
|
23148
|
+
|
|
23149
|
+
begin
|
|
23150
|
+
require 'opentelemetry/sdk'
|
|
23151
|
+
require 'opentelemetry/exporter/otlp'
|
|
23152
|
+
require 'opentelemetry/instrumentation/all'
|
|
23153
|
+
_neat_otel_loaded = true
|
|
23154
|
+
rescue LoadError
|
|
23155
|
+
_neat_otel_loaded = false
|
|
23156
|
+
end
|
|
23157
|
+
|
|
23158
|
+
if _neat_otel_loaded && ENV['NEAT_CALLSITE_DISABLED'] != '1'
|
|
23159
|
+
# Walk the Ruby call stack to the first application frame and stamp the stable
|
|
23160
|
+
# OTel source attributes on CLIENT/PRODUCER spans. SERVER spans are created
|
|
23161
|
+
# before the handler runs, so they stay route/service-grained, honestly.
|
|
23162
|
+
class NeatCallSiteSpanProcessor
|
|
23163
|
+
def initialize(root)
|
|
23164
|
+
@root = root.to_s.end_with?(File::SEPARATOR) ? root.to_s : root.to_s + File::SEPARATOR
|
|
23165
|
+
end
|
|
23166
|
+
|
|
23167
|
+
def on_start(span, _parent_context)
|
|
23168
|
+
kind = span.kind
|
|
23169
|
+
return unless kind == OpenTelemetry::Trace::SpanKind::CLIENT ||
|
|
23170
|
+
kind == OpenTelemetry::Trace::SpanKind::PRODUCER
|
|
23171
|
+
caller_locations(1).each do |loc|
|
|
23172
|
+
file = loc.absolute_path || loc.path
|
|
23173
|
+
next if file.nil?
|
|
23174
|
+
next unless file.start_with?(@root)
|
|
23175
|
+
next if file.include?('/vendor/') || file.include?('/.bundle/')
|
|
23176
|
+
next if file.end_with?('neat_otel.rb')
|
|
23177
|
+
span.set_attribute('code.file.path', file)
|
|
23178
|
+
span.set_attribute('code.line.number', loc.lineno)
|
|
23179
|
+
span.set_attribute('code.function.name', loc.label.to_s)
|
|
23180
|
+
break
|
|
23181
|
+
end
|
|
23182
|
+
rescue StandardError
|
|
23183
|
+
# never break the host application
|
|
23184
|
+
end
|
|
23185
|
+
|
|
23186
|
+
def on_finish(_span); end
|
|
23187
|
+
|
|
23188
|
+
def force_flush(timeout: nil)
|
|
23189
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
23190
|
+
end
|
|
23191
|
+
|
|
23192
|
+
def shutdown(timeout: nil)
|
|
23193
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
23194
|
+
end
|
|
23195
|
+
end
|
|
23196
|
+
|
|
23197
|
+
_neat_root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
23198
|
+
_neat_service = ENV.fetch('OTEL_SERVICE_NAME', '${service}')
|
|
23199
|
+
_neat_endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', '${endpoint2}')
|
|
23200
|
+
|
|
23201
|
+
OpenTelemetry::SDK.configure do |c|
|
|
23202
|
+
c.service_name = _neat_service
|
|
23203
|
+
c.use_all
|
|
23204
|
+
c.add_span_processor(
|
|
23205
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
23206
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: _neat_endpoint)
|
|
23207
|
+
)
|
|
23208
|
+
)
|
|
23209
|
+
c.add_span_processor(NeatCallSiteSpanProcessor.new(_neat_root))
|
|
23210
|
+
end
|
|
23211
|
+
end
|
|
23212
|
+
`;
|
|
23213
|
+
}
|
|
23214
|
+
async function exists6(p) {
|
|
23215
|
+
try {
|
|
23216
|
+
await import_node_fs42.promises.stat(p);
|
|
23217
|
+
return true;
|
|
23218
|
+
} catch {
|
|
23219
|
+
return false;
|
|
23220
|
+
}
|
|
23221
|
+
}
|
|
23222
|
+
async function detect4(serviceDir) {
|
|
23223
|
+
for (const marker of RUBY_MARKERS) {
|
|
23224
|
+
if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
|
|
23225
|
+
}
|
|
23226
|
+
return false;
|
|
23227
|
+
}
|
|
23228
|
+
async function isRailsApp(serviceDir, gemfile) {
|
|
23229
|
+
if (gemfile && /^\s*gem\s+['"]rails['"]/m.test(gemfile)) return true;
|
|
23230
|
+
for (const marker of ["config/application.rb", "config/environment.rb", "bin/rails"]) {
|
|
23231
|
+
if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
|
|
23232
|
+
}
|
|
23233
|
+
return false;
|
|
23234
|
+
}
|
|
23235
|
+
function gemPresent(gemfile, name) {
|
|
23236
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23237
|
+
return new RegExp(`^\\s*gem\\s+['"]${escaped}['"]`, "m").test(gemfile);
|
|
23238
|
+
}
|
|
23239
|
+
async function readGemfile(serviceDir) {
|
|
23240
|
+
const file = import_node_path76.default.join(serviceDir, "Gemfile");
|
|
23241
|
+
if (!await exists6(file)) return null;
|
|
23242
|
+
return { file, body: await import_node_fs42.promises.readFile(file, "utf8") };
|
|
23243
|
+
}
|
|
23244
|
+
async function plan4(serviceDir, opts) {
|
|
23245
|
+
const empty = {
|
|
23246
|
+
language: "ruby",
|
|
23247
|
+
serviceDir,
|
|
23248
|
+
dependencyEdits: [],
|
|
23249
|
+
entrypointEdits: [],
|
|
23250
|
+
envEdits: []
|
|
23251
|
+
};
|
|
23252
|
+
const gemfile = await readGemfile(serviceDir);
|
|
23253
|
+
const dependencyEdits = [];
|
|
23254
|
+
if (gemfile) {
|
|
23255
|
+
for (const gem of RUBY_GEMS) {
|
|
23256
|
+
if (!gemPresent(gemfile.body, gem.name)) {
|
|
23257
|
+
dependencyEdits.push({ file: gemfile.file, kind: "add", name: gem.name, version: gem.version });
|
|
23258
|
+
}
|
|
23259
|
+
}
|
|
23260
|
+
}
|
|
23261
|
+
const rails = await isRailsApp(serviceDir, gemfile?.body ?? null);
|
|
23262
|
+
const initializer = import_node_path76.default.join(serviceDir, INITIALIZER_REL);
|
|
23263
|
+
const generatedFiles = [];
|
|
23264
|
+
if (rails && !await exists6(initializer)) {
|
|
23265
|
+
generatedFiles.push({
|
|
23266
|
+
file: initializer,
|
|
23267
|
+
contents: neatOtelRb({ project: opts?.project }),
|
|
23268
|
+
skipIfExists: true
|
|
23269
|
+
});
|
|
23270
|
+
}
|
|
23271
|
+
if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
|
|
23272
|
+
return empty;
|
|
23273
|
+
}
|
|
23274
|
+
const envEdits = [
|
|
23275
|
+
{ file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
|
|
23276
|
+
];
|
|
23277
|
+
return {
|
|
23278
|
+
language: "ruby",
|
|
23279
|
+
serviceDir,
|
|
23280
|
+
dependencyEdits,
|
|
23281
|
+
entrypointEdits: [],
|
|
23282
|
+
envEdits,
|
|
23283
|
+
...generatedFiles.length > 0 ? { generatedFiles } : {}
|
|
23284
|
+
};
|
|
23285
|
+
}
|
|
23286
|
+
async function writeFileAtomic2(file, contents) {
|
|
23287
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
23288
|
+
await import_node_fs42.promises.writeFile(tmp, contents, "utf8");
|
|
23289
|
+
await import_node_fs42.promises.rename(tmp, file);
|
|
23290
|
+
}
|
|
23291
|
+
async function applyGemfile(file, edits, original) {
|
|
23292
|
+
const lines = edits.filter((e) => e.kind === "add").map((e) => `gem '${e.name}', '${e.version}'`);
|
|
23293
|
+
const banner = `
|
|
23294
|
+
# ${NEAT_OTEL_STAMP2} \u2014 OpenTelemetry gems added by NEAT
|
|
23295
|
+
`;
|
|
23296
|
+
const trailing = original.endsWith("\n") ? "" : "\n";
|
|
23297
|
+
await writeFileAtomic2(file, `${original}${trailing}${banner}${lines.join("\n")}
|
|
23298
|
+
`);
|
|
23299
|
+
}
|
|
23300
|
+
async function rollback3(serviceDir, language, originals, created) {
|
|
23301
|
+
const restored = [];
|
|
23302
|
+
for (const [file, raw] of originals.entries()) {
|
|
23303
|
+
try {
|
|
23304
|
+
await import_node_fs42.promises.writeFile(file, raw, "utf8");
|
|
23305
|
+
restored.push(file);
|
|
23306
|
+
} catch {
|
|
23307
|
+
}
|
|
23308
|
+
}
|
|
23309
|
+
const removed = [];
|
|
23310
|
+
for (const file of created) {
|
|
23311
|
+
try {
|
|
23312
|
+
await import_node_fs42.promises.rm(file, { force: true });
|
|
23313
|
+
removed.push(file);
|
|
23314
|
+
} catch {
|
|
23315
|
+
}
|
|
23316
|
+
}
|
|
23317
|
+
const body = [
|
|
23318
|
+
"# neat-rollback.patch",
|
|
23319
|
+
"",
|
|
23320
|
+
`# Generated after a partial apply failure in the ${language} installer.`,
|
|
23321
|
+
"# Files listed below were restored to their pre-apply contents.",
|
|
23322
|
+
"",
|
|
23323
|
+
...restored.map((f) => `restored: ${f}`),
|
|
23324
|
+
...removed.map((f) => `removed: ${f}`),
|
|
23325
|
+
""
|
|
23326
|
+
];
|
|
23327
|
+
await import_node_fs42.promises.writeFile(import_node_path76.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
|
|
23328
|
+
}
|
|
23329
|
+
async function apply4(installPlan) {
|
|
23330
|
+
const { serviceDir } = installPlan;
|
|
23331
|
+
const generatedFiles = installPlan.generatedFiles ?? [];
|
|
23332
|
+
const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
|
|
23333
|
+
if (manifests.size === 0 && generatedFiles.length === 0) {
|
|
23334
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
|
|
23335
|
+
}
|
|
23336
|
+
const originals = /* @__PURE__ */ new Map();
|
|
23337
|
+
for (const file of manifests) {
|
|
23338
|
+
try {
|
|
23339
|
+
originals.set(file, await import_node_fs42.promises.readFile(file, "utf8"));
|
|
23340
|
+
} catch {
|
|
23341
|
+
}
|
|
23342
|
+
}
|
|
23343
|
+
const writtenFiles = [];
|
|
23344
|
+
const created = [];
|
|
23345
|
+
try {
|
|
23346
|
+
for (const gf of generatedFiles) {
|
|
23347
|
+
if (await exists6(gf.file)) continue;
|
|
23348
|
+
await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(gf.file), { recursive: true });
|
|
23349
|
+
await writeFileAtomic2(gf.file, gf.contents);
|
|
23350
|
+
writtenFiles.push(gf.file);
|
|
23351
|
+
created.push(gf.file);
|
|
23352
|
+
}
|
|
23353
|
+
for (const file of manifests) {
|
|
23354
|
+
const raw = originals.get(file);
|
|
23355
|
+
if (raw === void 0) throw new Error(`ruby installer: cannot read ${file} during apply`);
|
|
23356
|
+
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
23357
|
+
if (edits.length > 0) {
|
|
23358
|
+
await applyGemfile(file, edits, raw);
|
|
23359
|
+
writtenFiles.push(file);
|
|
23360
|
+
}
|
|
23361
|
+
}
|
|
23362
|
+
} catch (err) {
|
|
23363
|
+
await rollback3(serviceDir, installPlan.language, originals, created);
|
|
23364
|
+
throw err;
|
|
23365
|
+
}
|
|
23366
|
+
const wroteManifest = writtenFiles.some((f) => import_node_path76.default.basename(f) === "Gemfile");
|
|
23367
|
+
return {
|
|
23368
|
+
serviceDir,
|
|
23369
|
+
outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
|
|
23370
|
+
writtenFiles,
|
|
23371
|
+
...wroteManifest ? { followUpInstall: "bundle install" } : {}
|
|
23372
|
+
};
|
|
23373
|
+
}
|
|
23374
|
+
var rubyInstaller = { name: "ruby", detect: detect4, plan: plan4, apply: apply4 };
|
|
23375
|
+
|
|
23376
|
+
// src/installers/php.ts
|
|
23377
|
+
init_cjs_shims();
|
|
23378
|
+
var import_node_fs43 = require("fs");
|
|
23379
|
+
var import_node_path77 = __toESM(require("path"), 1);
|
|
23380
|
+
var PHP_MARKERS = [
|
|
23381
|
+
"composer.json",
|
|
23382
|
+
"composer.lock"
|
|
23383
|
+
];
|
|
23384
|
+
var NEAT_OTEL_FILENAME2 = "neat_otel.php";
|
|
23385
|
+
var NEAT_OTEL_STAMP3 = "neat-otel-init v1";
|
|
23386
|
+
var LARAVEL_PACKAGE = "open-telemetry/opentelemetry-auto-laravel";
|
|
23387
|
+
var PHP_PACKAGES = [
|
|
23388
|
+
{ name: "open-telemetry/sdk", version: "^1.0" },
|
|
23389
|
+
{ name: "open-telemetry/exporter-otlp", version: "^1.0" },
|
|
23390
|
+
{ name: "php-http/guzzle7-adapter", version: "^1.0" },
|
|
23391
|
+
{ name: LARAVEL_PACKAGE, version: "^0.1" }
|
|
23392
|
+
];
|
|
23393
|
+
var PHP_PECL_CAVEAT = "PHP auto-instrumentation requires the `opentelemetry` PECL extension (`pecl install opentelemetry`, then `extension=opentelemetry.so` in php.ini). NEAT cannot install a PECL extension via composer \u2014 until it is loaded no spans are produced. See neat_otel.php and ADR-186.";
|
|
23394
|
+
function neatOtelPhp(opts = {}) {
|
|
23395
|
+
const service = opts.project ?? "php-service";
|
|
23396
|
+
const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
|
|
23397
|
+
return `<?php
|
|
23398
|
+
// ${NEAT_OTEL_STAMP3} \u2014 generated by NEAT. Safe to re-generate; do not edit.
|
|
23399
|
+
//
|
|
23400
|
+
// REQUIRED SYSTEM STEP \u2014 NEAT CANNOT DO THIS FOR YOU:
|
|
23401
|
+
// PHP OpenTelemetry auto-instrumentation needs the \`opentelemetry\` PECL
|
|
23402
|
+
// extension, a system-level install composer cannot provide:
|
|
23403
|
+
// pecl install opentelemetry
|
|
23404
|
+
// then enable it in your php.ini:
|
|
23405
|
+
// extension=opentelemetry.so
|
|
23406
|
+
// Verify with \`php -m | grep opentelemetry\`. Until the extension is loaded
|
|
23407
|
+
// the Laravel auto-instrumentation hooks never fire and no spans are emitted.
|
|
23408
|
+
//
|
|
23409
|
+
// Wire this file so it runs before the framework boots \u2014 either set
|
|
23410
|
+
// auto_prepend_file = /absolute/path/to/neat_otel.php
|
|
23411
|
+
// in php.ini / .user.ini, or require it at the very top of public/index.php and
|
|
23412
|
+
// artisan. It points the exporter at your NEAT daemon and turns on the SDK
|
|
23413
|
+
// autoloader; the auto-laravel instrumentation then produces route, DB, cache,
|
|
23414
|
+
// and queue spans that fuse onto your extracted routes and Eloquent tables.
|
|
23415
|
+
//
|
|
23416
|
+
// FILE-GRAIN (code.file.path call-site attribution) is a documented follow-up
|
|
23417
|
+
// for PHP \u2014 see ADR-186. Route, table, and service grain land now.
|
|
23418
|
+
|
|
23419
|
+
declare(strict_types=1);
|
|
23420
|
+
|
|
23421
|
+
// Degrade to a no-op when the extension isn't present, so a bare app still
|
|
23422
|
+
// boots (never break the host application).
|
|
23423
|
+
if (!extension_loaded('opentelemetry')) {
|
|
23424
|
+
return;
|
|
23425
|
+
}
|
|
23426
|
+
|
|
23427
|
+
// Point the exporter at NEAT unless the operator already set these. The
|
|
23428
|
+
// endpoint is NEAT's project-scoped traces path (ADR-183).
|
|
23429
|
+
$neat_defaults = [
|
|
23430
|
+
'OTEL_PHP_AUTOLOAD_ENABLED' => 'true',
|
|
23431
|
+
'OTEL_SERVICE_NAME' => '${service}',
|
|
23432
|
+
'OTEL_TRACES_EXPORTER' => 'otlp',
|
|
23433
|
+
'OTEL_EXPORTER_OTLP_PROTOCOL' => 'http/json',
|
|
23434
|
+
'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => '${endpoint2}',
|
|
23435
|
+
'OTEL_PROPAGATORS' => 'baggage,tracecontext',
|
|
23436
|
+
];
|
|
23437
|
+
foreach ($neat_defaults as $neat_key => $neat_value) {
|
|
23438
|
+
if (getenv($neat_key) === false && !isset($_SERVER[$neat_key]) && !isset($_ENV[$neat_key])) {
|
|
23439
|
+
putenv($neat_key . '=' . $neat_value);
|
|
23440
|
+
$_SERVER[$neat_key] = $neat_value;
|
|
23441
|
+
$_ENV[$neat_key] = $neat_value;
|
|
23442
|
+
}
|
|
23443
|
+
}
|
|
23444
|
+
`;
|
|
23445
|
+
}
|
|
23446
|
+
async function exists7(p) {
|
|
23447
|
+
try {
|
|
23448
|
+
await import_node_fs43.promises.stat(p);
|
|
23449
|
+
return true;
|
|
23450
|
+
} catch {
|
|
23451
|
+
return false;
|
|
23452
|
+
}
|
|
23453
|
+
}
|
|
23454
|
+
async function detect5(serviceDir) {
|
|
23455
|
+
for (const marker of PHP_MARKERS) {
|
|
23456
|
+
if (await exists7(import_node_path77.default.join(serviceDir, marker))) return true;
|
|
23457
|
+
}
|
|
23458
|
+
return false;
|
|
23459
|
+
}
|
|
23460
|
+
function readComposerObject(body) {
|
|
23461
|
+
let parsed = null;
|
|
23462
|
+
try {
|
|
23463
|
+
parsed = JSON.parse(body);
|
|
23464
|
+
} catch {
|
|
23465
|
+
parsed = null;
|
|
23466
|
+
}
|
|
23467
|
+
const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
23468
|
+
const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
|
|
23469
|
+
const requireDev = obj["require-dev"] && typeof obj["require-dev"] === "object" && !Array.isArray(obj["require-dev"]) ? obj["require-dev"] : {};
|
|
23470
|
+
return { require: require2, requireDev };
|
|
23471
|
+
}
|
|
23472
|
+
async function plan5(serviceDir, opts) {
|
|
23473
|
+
const empty = {
|
|
23474
|
+
language: "php",
|
|
23475
|
+
serviceDir,
|
|
23476
|
+
dependencyEdits: [],
|
|
23477
|
+
entrypointEdits: [],
|
|
23478
|
+
envEdits: []
|
|
23479
|
+
};
|
|
23480
|
+
const composerPath = import_node_path77.default.join(serviceDir, "composer.json");
|
|
23481
|
+
const hasComposer = await exists7(composerPath);
|
|
23482
|
+
const dependencyEdits = [];
|
|
23483
|
+
if (hasComposer) {
|
|
23484
|
+
const body = await import_node_fs43.promises.readFile(composerPath, "utf8");
|
|
23485
|
+
const { require: require2, requireDev } = readComposerObject(body);
|
|
23486
|
+
const laravel = "laravel/framework" in require2 || "laravel/framework" in requireDev || await exists7(import_node_path77.default.join(serviceDir, "artisan"));
|
|
23487
|
+
const wanted = laravel ? PHP_PACKAGES : PHP_PACKAGES.filter((p) => p.name !== LARAVEL_PACKAGE);
|
|
23488
|
+
for (const pkg of wanted) {
|
|
23489
|
+
if (!(pkg.name in require2)) {
|
|
23490
|
+
dependencyEdits.push({ file: composerPath, kind: "add", name: pkg.name, version: pkg.version });
|
|
23491
|
+
}
|
|
23492
|
+
}
|
|
23493
|
+
}
|
|
23494
|
+
const bootstrap = import_node_path77.default.join(serviceDir, NEAT_OTEL_FILENAME2);
|
|
23495
|
+
const generatedFiles = [];
|
|
23496
|
+
if (hasComposer && !await exists7(bootstrap)) {
|
|
23497
|
+
generatedFiles.push({ file: bootstrap, contents: neatOtelPhp({ project: opts?.project }), skipIfExists: true });
|
|
23498
|
+
}
|
|
23499
|
+
if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
|
|
23500
|
+
return empty;
|
|
23501
|
+
}
|
|
23502
|
+
const envEdits = [
|
|
23503
|
+
{ file: null, key: "OTEL_PHP_AUTOLOAD_ENABLED", value: "true" },
|
|
23504
|
+
{ file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
|
|
23505
|
+
];
|
|
23506
|
+
return {
|
|
23507
|
+
language: "php",
|
|
23508
|
+
serviceDir,
|
|
23509
|
+
dependencyEdits,
|
|
23510
|
+
entrypointEdits: [],
|
|
23511
|
+
envEdits,
|
|
23512
|
+
...generatedFiles.length > 0 ? { generatedFiles } : {}
|
|
23513
|
+
};
|
|
23514
|
+
}
|
|
23515
|
+
async function writeFileAtomic3(file, contents) {
|
|
23516
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
23517
|
+
await import_node_fs43.promises.writeFile(tmp, contents, "utf8");
|
|
23518
|
+
await import_node_fs43.promises.rename(tmp, file);
|
|
23519
|
+
}
|
|
23520
|
+
async function applyComposerJson(file, edits, original) {
|
|
23521
|
+
let parsed;
|
|
23522
|
+
try {
|
|
23523
|
+
parsed = JSON.parse(original);
|
|
23524
|
+
} catch {
|
|
23525
|
+
throw new Error(`php installer: composer.json at ${file} is not valid JSON`);
|
|
23526
|
+
}
|
|
23527
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
23528
|
+
throw new Error(`php installer: composer.json at ${file} is not a JSON object`);
|
|
23529
|
+
}
|
|
23530
|
+
const obj = parsed;
|
|
23531
|
+
const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
|
|
23532
|
+
for (const e of edits) {
|
|
23533
|
+
if (e.kind !== "add") continue;
|
|
23534
|
+
if (!(e.name in require2)) require2[e.name] = e.version;
|
|
23535
|
+
}
|
|
23536
|
+
obj.require = require2;
|
|
23537
|
+
await writeFileAtomic3(file, JSON.stringify(obj, null, 2) + "\n");
|
|
23538
|
+
}
|
|
23539
|
+
async function rollback4(serviceDir, language, originals, created) {
|
|
23540
|
+
const restored = [];
|
|
23541
|
+
for (const [file, raw] of originals.entries()) {
|
|
23542
|
+
try {
|
|
23543
|
+
await import_node_fs43.promises.writeFile(file, raw, "utf8");
|
|
23544
|
+
restored.push(file);
|
|
23545
|
+
} catch {
|
|
23546
|
+
}
|
|
23547
|
+
}
|
|
23548
|
+
const removed = [];
|
|
23549
|
+
for (const file of created) {
|
|
23550
|
+
try {
|
|
23551
|
+
await import_node_fs43.promises.rm(file, { force: true });
|
|
23552
|
+
removed.push(file);
|
|
23553
|
+
} catch {
|
|
23554
|
+
}
|
|
23555
|
+
}
|
|
23556
|
+
const body = [
|
|
23557
|
+
"# neat-rollback.patch",
|
|
23558
|
+
"",
|
|
23559
|
+
`# Generated after a partial apply failure in the ${language} installer.`,
|
|
23560
|
+
"# Files listed below were restored to their pre-apply contents.",
|
|
23561
|
+
"",
|
|
23562
|
+
...restored.map((f) => `restored: ${f}`),
|
|
23563
|
+
...removed.map((f) => `removed: ${f}`),
|
|
23564
|
+
""
|
|
23565
|
+
];
|
|
23566
|
+
await import_node_fs43.promises.writeFile(import_node_path77.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
|
|
23567
|
+
}
|
|
23568
|
+
async function apply5(installPlan) {
|
|
23569
|
+
const { serviceDir } = installPlan;
|
|
23570
|
+
const generatedFiles = installPlan.generatedFiles ?? [];
|
|
23571
|
+
const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
|
|
23572
|
+
if (manifests.size === 0 && generatedFiles.length === 0) {
|
|
23573
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [], reason: PHP_PECL_CAVEAT };
|
|
23574
|
+
}
|
|
23575
|
+
const originals = /* @__PURE__ */ new Map();
|
|
23576
|
+
for (const file of manifests) {
|
|
23577
|
+
try {
|
|
23578
|
+
originals.set(file, await import_node_fs43.promises.readFile(file, "utf8"));
|
|
23579
|
+
} catch {
|
|
23580
|
+
}
|
|
23581
|
+
}
|
|
23582
|
+
const writtenFiles = [];
|
|
23583
|
+
const created = [];
|
|
23584
|
+
try {
|
|
23585
|
+
for (const gf of generatedFiles) {
|
|
23586
|
+
if (await exists7(gf.file)) continue;
|
|
23587
|
+
await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(gf.file), { recursive: true });
|
|
23588
|
+
await writeFileAtomic3(gf.file, gf.contents);
|
|
23589
|
+
writtenFiles.push(gf.file);
|
|
23590
|
+
created.push(gf.file);
|
|
23591
|
+
}
|
|
23592
|
+
for (const file of manifests) {
|
|
23593
|
+
const raw = originals.get(file);
|
|
23594
|
+
if (raw === void 0) throw new Error(`php installer: cannot read ${file} during apply`);
|
|
23595
|
+
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
23596
|
+
if (edits.length > 0) {
|
|
23597
|
+
await applyComposerJson(file, edits, raw);
|
|
23598
|
+
writtenFiles.push(file);
|
|
23599
|
+
}
|
|
23600
|
+
}
|
|
23601
|
+
} catch (err) {
|
|
23602
|
+
await rollback4(serviceDir, installPlan.language, originals, created);
|
|
23603
|
+
throw err;
|
|
23604
|
+
}
|
|
23605
|
+
if (writtenFiles.length > 0) {
|
|
23606
|
+
console.warn(`neat: PHP instrumentation staged in ${import_node_path77.default.basename(serviceDir)}, but a system step remains:
|
|
23607
|
+
${PHP_PECL_CAVEAT}`);
|
|
23608
|
+
}
|
|
23609
|
+
const wroteManifest = writtenFiles.some((f) => import_node_path77.default.basename(f) === "composer.json");
|
|
23610
|
+
return {
|
|
23611
|
+
serviceDir,
|
|
23612
|
+
outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
|
|
23613
|
+
writtenFiles,
|
|
23614
|
+
reason: PHP_PECL_CAVEAT,
|
|
23615
|
+
...wroteManifest ? { followUpInstall: "composer install" } : {}
|
|
23616
|
+
};
|
|
23617
|
+
}
|
|
23618
|
+
var phpInstaller = { name: "php", detect: detect5, plan: plan5, apply: apply5 };
|
|
23619
|
+
|
|
22236
23620
|
// src/installers/shared.ts
|
|
22237
23621
|
init_cjs_shims();
|
|
22238
|
-
function isEmptyPlan(
|
|
22239
|
-
return
|
|
23622
|
+
function isEmptyPlan(plan6) {
|
|
23623
|
+
return plan6.dependencyEdits.length === 0 && plan6.entrypointEdits.length === 0 && plan6.envEdits.length === 0 && (plan6.generatedFiles?.length ?? 0) === 0 && plan6.nextConfigEdit === void 0;
|
|
22240
23624
|
}
|
|
22241
23625
|
|
|
22242
23626
|
// src/installers/index.ts
|
|
@@ -22248,9 +23632,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
|
|
|
22248
23632
|
"Pipfile.lock",
|
|
22249
23633
|
"Gemfile.lock",
|
|
22250
23634
|
"Cargo.lock",
|
|
22251
|
-
"go.sum"
|
|
23635
|
+
"go.sum",
|
|
23636
|
+
"composer.lock"
|
|
22252
23637
|
]);
|
|
22253
|
-
var INSTALLERS = [
|
|
23638
|
+
var INSTALLERS = [
|
|
23639
|
+
javascriptInstaller,
|
|
23640
|
+
pythonInstaller,
|
|
23641
|
+
goInstaller,
|
|
23642
|
+
rubyInstaller,
|
|
23643
|
+
phpInstaller
|
|
23644
|
+
];
|
|
22254
23645
|
async function pickInstaller(serviceDir) {
|
|
22255
23646
|
for (const inst of INSTALLERS) {
|
|
22256
23647
|
if (await inst.detect(serviceDir)) return inst;
|
|
@@ -22265,7 +23656,7 @@ function renderPatch(sections) {
|
|
|
22265
23656
|
"No SDK installers matched the discovered services. Two reasons this",
|
|
22266
23657
|
"normally happens:",
|
|
22267
23658
|
" - the project uses a language NEAT does not yet instrument",
|
|
22268
|
-
" (Java /
|
|
23659
|
+
" (Java / .NET / Rust are out of scope per ADR-047);",
|
|
22269
23660
|
" - the SDK is already installed, so the installer returned an empty",
|
|
22270
23661
|
" plan.",
|
|
22271
23662
|
"",
|
|
@@ -22275,22 +23666,22 @@ function renderPatch(sections) {
|
|
|
22275
23666
|
}
|
|
22276
23667
|
const lines = ["# neat install plan", ""];
|
|
22277
23668
|
for (const section of sections) {
|
|
22278
|
-
const { installer, plan:
|
|
22279
|
-
lines.push(`## ${installer} (${
|
|
23669
|
+
const { installer, plan: plan6 } = section;
|
|
23670
|
+
lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
|
|
22280
23671
|
lines.push("");
|
|
22281
|
-
if (
|
|
23672
|
+
if (plan6.libOnly) {
|
|
22282
23673
|
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
22283
23674
|
lines.push("");
|
|
22284
23675
|
continue;
|
|
22285
23676
|
}
|
|
22286
|
-
if (
|
|
22287
|
-
lines.push(`entry: ${
|
|
23677
|
+
if (plan6.entryFile) {
|
|
23678
|
+
lines.push(`entry: ${plan6.entryFile}`);
|
|
22288
23679
|
lines.push("");
|
|
22289
23680
|
}
|
|
22290
|
-
if (
|
|
23681
|
+
if (plan6.dependencyEdits.length > 0) {
|
|
22291
23682
|
lines.push("### dependencies");
|
|
22292
23683
|
const byFile = /* @__PURE__ */ new Map();
|
|
22293
|
-
for (const dep of
|
|
23684
|
+
for (const dep of plan6.dependencyEdits) {
|
|
22294
23685
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
22295
23686
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
22296
23687
|
throw new Error(
|
|
@@ -22309,9 +23700,9 @@ function renderPatch(sections) {
|
|
|
22309
23700
|
}
|
|
22310
23701
|
lines.push("");
|
|
22311
23702
|
}
|
|
22312
|
-
if (
|
|
23703
|
+
if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
|
|
22313
23704
|
lines.push("### generated files");
|
|
22314
|
-
for (const gen of
|
|
23705
|
+
for (const gen of plan6.generatedFiles) {
|
|
22315
23706
|
lines.push(`--- (new file) ${gen.file}`);
|
|
22316
23707
|
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
22317
23708
|
lines.push(`+ ${ln}`);
|
|
@@ -22319,26 +23710,26 @@ function renderPatch(sections) {
|
|
|
22319
23710
|
}
|
|
22320
23711
|
lines.push("");
|
|
22321
23712
|
}
|
|
22322
|
-
if (
|
|
23713
|
+
if (plan6.entrypointEdits.length > 0) {
|
|
22323
23714
|
lines.push("### entry-point injection");
|
|
22324
|
-
for (const e of
|
|
23715
|
+
for (const e of plan6.entrypointEdits) {
|
|
22325
23716
|
lines.push(`--- ${e.file}`);
|
|
22326
23717
|
lines.push(`+ ${e.after}`);
|
|
22327
23718
|
lines.push(` ${e.before}`);
|
|
22328
23719
|
}
|
|
22329
23720
|
lines.push("");
|
|
22330
23721
|
}
|
|
22331
|
-
if (
|
|
23722
|
+
if (plan6.envEdits.length > 0) {
|
|
22332
23723
|
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
22333
|
-
for (const env of
|
|
23724
|
+
for (const env of plan6.envEdits) {
|
|
22334
23725
|
lines.push(`- ${env.key}=${env.value}`);
|
|
22335
23726
|
}
|
|
22336
23727
|
lines.push("");
|
|
22337
23728
|
}
|
|
22338
|
-
if (
|
|
23729
|
+
if (plan6.nextConfigEdit) {
|
|
22339
23730
|
lines.push("### next.config (framework flag)");
|
|
22340
|
-
lines.push(`--- ${
|
|
22341
|
-
lines.push(`+ experimental: { instrumentationHook: true }, // ${
|
|
23731
|
+
lines.push(`--- ${plan6.nextConfigEdit.file}`);
|
|
23732
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
|
|
22342
23733
|
lines.push("");
|
|
22343
23734
|
}
|
|
22344
23735
|
}
|
|
@@ -22347,10 +23738,10 @@ function renderPatch(sections) {
|
|
|
22347
23738
|
|
|
22348
23739
|
// src/orchestrator.ts
|
|
22349
23740
|
init_cjs_shims();
|
|
22350
|
-
var
|
|
23741
|
+
var import_node_fs44 = require("fs");
|
|
22351
23742
|
var import_node_http = __toESM(require("http"), 1);
|
|
22352
23743
|
var import_node_net = __toESM(require("net"), 1);
|
|
22353
|
-
var
|
|
23744
|
+
var import_node_path78 = __toESM(require("path"), 1);
|
|
22354
23745
|
var import_node_url4 = require("url");
|
|
22355
23746
|
var import_node_child_process3 = require("child_process");
|
|
22356
23747
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
@@ -22360,7 +23751,7 @@ async function extractAndPersist(opts) {
|
|
|
22360
23751
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
22361
23752
|
resetGraph(graphKey);
|
|
22362
23753
|
const graph = getGraph(graphKey);
|
|
22363
|
-
const projectPaths = pathsForProject(graphKey,
|
|
23754
|
+
const projectPaths = pathsForProject(graphKey, import_node_path78.default.join(opts.scanPath, "neat-out"));
|
|
22364
23755
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
22365
23756
|
errorsPath: projectPaths.errorsPath
|
|
22366
23757
|
});
|
|
@@ -22391,28 +23782,34 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22391
23782
|
let cloudflareWorkers = 0;
|
|
22392
23783
|
let electron = 0;
|
|
22393
23784
|
const installPlans = /* @__PURE__ */ new Map();
|
|
23785
|
+
const dependencyInstructions = /* @__PURE__ */ new Map();
|
|
22394
23786
|
for (const svc of services) {
|
|
22395
23787
|
const installer = await pickInstaller(svc.dir);
|
|
22396
23788
|
if (!installer) continue;
|
|
22397
|
-
const
|
|
22398
|
-
if (isEmptyPlan(
|
|
23789
|
+
const plan6 = await installer.plan(svc.dir, { project });
|
|
23790
|
+
if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
|
|
22399
23791
|
already++;
|
|
22400
23792
|
continue;
|
|
22401
23793
|
}
|
|
22402
|
-
const outcome = await installer.apply(
|
|
23794
|
+
const outcome = await installer.apply(plan6);
|
|
22403
23795
|
if (outcome.outcome === "instrumented") {
|
|
22404
23796
|
instrumented++;
|
|
22405
|
-
if (
|
|
22406
|
-
const
|
|
22407
|
-
|
|
22408
|
-
|
|
23797
|
+
if (plan6.dependencyEdits.length > 0) {
|
|
23798
|
+
const manifest = import_node_path78.default.basename(plan6.dependencyEdits[0].file);
|
|
23799
|
+
if (manifest === "package.json") {
|
|
23800
|
+
const cmd = await resolveManager(svc.dir);
|
|
23801
|
+
const key = `${cmd.pm}:${cmd.cwd}`;
|
|
23802
|
+
if (!installPlans.has(key)) installPlans.set(key, cmd);
|
|
23803
|
+
} else if (outcome.followUpInstall) {
|
|
23804
|
+
dependencyInstructions.set(svc.dir, outcome.followUpInstall);
|
|
23805
|
+
}
|
|
22409
23806
|
}
|
|
22410
23807
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
22411
23808
|
else if (outcome.outcome === "lib-only") {
|
|
22412
23809
|
libOnly++;
|
|
22413
23810
|
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
22414
23811
|
if (appDeps.length > 0) {
|
|
22415
|
-
const svcName =
|
|
23812
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22416
23813
|
const list = appDeps.join(", ");
|
|
22417
23814
|
console.warn(
|
|
22418
23815
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
@@ -22425,7 +23822,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22425
23822
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
22426
23823
|
} else if (outcome.outcome === "react-native") {
|
|
22427
23824
|
reactNative++;
|
|
22428
|
-
const svcName =
|
|
23825
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22429
23826
|
console.log(
|
|
22430
23827
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
22431
23828
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -22436,7 +23833,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22436
23833
|
);
|
|
22437
23834
|
} else if (outcome.outcome === "bun") {
|
|
22438
23835
|
bun++;
|
|
22439
|
-
const svcName =
|
|
23836
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22440
23837
|
console.log(
|
|
22441
23838
|
`neat: ${svc.dir} detected as Bun
|
|
22442
23839
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -22447,7 +23844,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22447
23844
|
);
|
|
22448
23845
|
} else if (outcome.outcome === "deno") {
|
|
22449
23846
|
deno++;
|
|
22450
|
-
const svcName =
|
|
23847
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22451
23848
|
console.log(
|
|
22452
23849
|
`neat: ${svc.dir} detected as Deno
|
|
22453
23850
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -22458,7 +23855,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22458
23855
|
);
|
|
22459
23856
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
22460
23857
|
cloudflareWorkers++;
|
|
22461
|
-
const svcName =
|
|
23858
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22462
23859
|
console.log(
|
|
22463
23860
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
22464
23861
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -22469,7 +23866,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22469
23866
|
);
|
|
22470
23867
|
} else if (outcome.outcome === "electron") {
|
|
22471
23868
|
electron++;
|
|
22472
|
-
const svcName =
|
|
23869
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22473
23870
|
console.log(
|
|
22474
23871
|
`neat: ${svc.dir} detected as Electron
|
|
22475
23872
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -22482,7 +23879,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22482
23879
|
if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
|
|
22483
23880
|
const gaps = uninstrumentedLibraries(svc.pkg);
|
|
22484
23881
|
if (gaps.length > 0) {
|
|
22485
|
-
const svcName =
|
|
23882
|
+
const svcName = import_node_path78.default.basename(svc.dir);
|
|
22486
23883
|
const list = gaps.join(", ");
|
|
22487
23884
|
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
22488
23885
|
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
@@ -22510,6 +23907,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22510
23907
|
}
|
|
22511
23908
|
}
|
|
22512
23909
|
}
|
|
23910
|
+
for (const [dir, command] of dependencyInstructions) {
|
|
23911
|
+
console.log(
|
|
23912
|
+
`neat: dependencies staged in ${dir}; run \`${command}\` to install them \u2014 NEAT does not run it for you.`
|
|
23913
|
+
);
|
|
23914
|
+
}
|
|
22513
23915
|
return {
|
|
22514
23916
|
instrumented,
|
|
22515
23917
|
alreadyInstrumented: already,
|
|
@@ -22520,7 +23922,8 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
22520
23922
|
deno,
|
|
22521
23923
|
cloudflareWorkers,
|
|
22522
23924
|
electron,
|
|
22523
|
-
packageManagerInstalls
|
|
23925
|
+
packageManagerInstalls,
|
|
23926
|
+
dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
|
|
22524
23927
|
};
|
|
22525
23928
|
}
|
|
22526
23929
|
async function promptYesNo(question) {
|
|
@@ -22688,24 +24091,24 @@ async function persistedPortsFor(scanPath) {
|
|
|
22688
24091
|
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
22689
24092
|
}
|
|
22690
24093
|
async function acquireSpawnLock(scanPath) {
|
|
22691
|
-
const lockPath =
|
|
22692
|
-
await
|
|
24094
|
+
const lockPath = import_node_path78.default.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
24095
|
+
await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(lockPath), { recursive: true });
|
|
22693
24096
|
const STALE_LOCK_MS = 6e4;
|
|
22694
24097
|
try {
|
|
22695
|
-
const fd = await
|
|
24098
|
+
const fd = await import_node_fs44.promises.open(lockPath, "wx");
|
|
22696
24099
|
await fd.writeFile(`${process.pid}
|
|
22697
24100
|
`, "utf8");
|
|
22698
24101
|
await fd.close();
|
|
22699
24102
|
return async () => {
|
|
22700
|
-
await
|
|
24103
|
+
await import_node_fs44.promises.unlink(lockPath).catch(() => {
|
|
22701
24104
|
});
|
|
22702
24105
|
};
|
|
22703
24106
|
} catch (err) {
|
|
22704
24107
|
if (err.code !== "EEXIST") return null;
|
|
22705
24108
|
try {
|
|
22706
|
-
const stat = await
|
|
24109
|
+
const stat = await import_node_fs44.promises.stat(lockPath);
|
|
22707
24110
|
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
22708
|
-
await
|
|
24111
|
+
await import_node_fs44.promises.unlink(lockPath).catch(() => {
|
|
22709
24112
|
});
|
|
22710
24113
|
return acquireSpawnLock(scanPath);
|
|
22711
24114
|
}
|
|
@@ -22734,13 +24137,13 @@ async function healthIsForProject(restPort, project) {
|
|
|
22734
24137
|
return false;
|
|
22735
24138
|
}
|
|
22736
24139
|
function daemonLogPath(projectPath3) {
|
|
22737
|
-
return
|
|
24140
|
+
return import_node_path78.default.join(projectPath3, "neat-out", "daemon.log");
|
|
22738
24141
|
}
|
|
22739
24142
|
function spawnDaemonDetached(spec) {
|
|
22740
|
-
const here =
|
|
24143
|
+
const here = import_node_path78.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
|
|
22741
24144
|
const candidates = [
|
|
22742
|
-
|
|
22743
|
-
|
|
24145
|
+
import_node_path78.default.join(here, "neatd.cjs"),
|
|
24146
|
+
import_node_path78.default.join(here, "neatd.js")
|
|
22744
24147
|
];
|
|
22745
24148
|
let entry2 = null;
|
|
22746
24149
|
const fsSync = require("fs");
|
|
@@ -22770,7 +24173,7 @@ function spawnDaemonDetached(spec) {
|
|
|
22770
24173
|
let logFd = null;
|
|
22771
24174
|
if (spec) {
|
|
22772
24175
|
const logPath = daemonLogPath(spec.projectPath);
|
|
22773
|
-
fsSync.mkdirSync(
|
|
24176
|
+
fsSync.mkdirSync(import_node_path78.default.dirname(logPath), { recursive: true });
|
|
22774
24177
|
logFd = fsSync.openSync(logPath, "a");
|
|
22775
24178
|
}
|
|
22776
24179
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
|
|
@@ -22809,7 +24212,7 @@ async function runOrchestrator(opts) {
|
|
|
22809
24212
|
browser: "skipped"
|
|
22810
24213
|
}
|
|
22811
24214
|
};
|
|
22812
|
-
const stat = await
|
|
24215
|
+
const stat = await import_node_fs44.promises.stat(opts.scanPath).catch(() => null);
|
|
22813
24216
|
if (!stat || !stat.isDirectory()) {
|
|
22814
24217
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
22815
24218
|
result.exitCode = 2;
|
|
@@ -22969,7 +24372,7 @@ async function runOrchestrator(opts) {
|
|
|
22969
24372
|
result.steps.browser = openBrowser(dashboardUrl);
|
|
22970
24373
|
}
|
|
22971
24374
|
const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
|
|
22972
|
-
const daemonLog = daemonRunning ?
|
|
24375
|
+
const daemonLog = daemonRunning ? import_node_path78.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
|
|
22973
24376
|
printSummary(result, graph, dashboardUrl, daemonLog);
|
|
22974
24377
|
return result;
|
|
22975
24378
|
}
|
|
@@ -23428,27 +24831,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
23428
24831
|
|
|
23429
24832
|
// src/hooks-cli.ts
|
|
23430
24833
|
init_cjs_shims();
|
|
23431
|
-
var
|
|
24834
|
+
var import_node_path79 = __toESM(require("path"), 1);
|
|
23432
24835
|
var import_node_os5 = __toESM(require("os"), 1);
|
|
23433
|
-
var
|
|
24836
|
+
var import_node_fs45 = require("fs");
|
|
23434
24837
|
var import_node_url5 = require("url");
|
|
23435
24838
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
23436
24839
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
23437
24840
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
23438
24841
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
23439
24842
|
function moduleDir() {
|
|
23440
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
24843
|
+
return typeof __dirname !== "undefined" ? __dirname : import_node_path79.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
|
|
23441
24844
|
}
|
|
23442
24845
|
async function readSkillAsset(rel) {
|
|
23443
24846
|
const here = moduleDir();
|
|
23444
24847
|
const candidates = [
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
24848
|
+
import_node_path79.default.resolve(here, "../../claude-skill", rel),
|
|
24849
|
+
import_node_path79.default.resolve(here, "../../../claude-skill", rel),
|
|
24850
|
+
import_node_path79.default.resolve(here, "../claude-skill", rel)
|
|
23448
24851
|
];
|
|
23449
24852
|
for (const candidate of candidates) {
|
|
23450
24853
|
try {
|
|
23451
|
-
return await
|
|
24854
|
+
return await import_node_fs45.promises.readFile(candidate, "utf8");
|
|
23452
24855
|
} catch {
|
|
23453
24856
|
}
|
|
23454
24857
|
}
|
|
@@ -23458,17 +24861,17 @@ async function readSkillAsset(rel) {
|
|
|
23458
24861
|
}
|
|
23459
24862
|
function neatHome3() {
|
|
23460
24863
|
const override = process.env.NEAT_HOME;
|
|
23461
|
-
if (override && override.length > 0) return
|
|
23462
|
-
return
|
|
24864
|
+
if (override && override.length > 0) return import_node_path79.default.resolve(override);
|
|
24865
|
+
return import_node_path79.default.join(import_node_os5.default.homedir(), ".neat");
|
|
23463
24866
|
}
|
|
23464
24867
|
function claudeSettingsPath() {
|
|
23465
24868
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
23466
|
-
if (override && override.length > 0) return
|
|
24869
|
+
if (override && override.length > 0) return import_node_path79.default.resolve(override);
|
|
23467
24870
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
|
|
23468
|
-
return
|
|
24871
|
+
return import_node_path79.default.join(home, ".claude", "settings.json");
|
|
23469
24872
|
}
|
|
23470
24873
|
function installedHookPath() {
|
|
23471
|
-
return
|
|
24874
|
+
return import_node_path79.default.join(neatHome3(), "hooks", HOOK_FILENAME);
|
|
23472
24875
|
}
|
|
23473
24876
|
function isNeatSearchEntry(entry2) {
|
|
23474
24877
|
return (entry2.hooks ?? []).some(
|
|
@@ -23501,14 +24904,14 @@ async function runHooks(opts) {
|
|
|
23501
24904
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
23502
24905
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
23503
24906
|
const scriptPath = installedHookPath();
|
|
23504
|
-
await
|
|
23505
|
-
await
|
|
23506
|
-
const guidePath =
|
|
23507
|
-
await
|
|
24907
|
+
await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(scriptPath), { recursive: true });
|
|
24908
|
+
await import_node_fs45.promises.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
24909
|
+
const guidePath = import_node_path79.default.join(neatHome3(), GUIDE_INSTALL_NAME);
|
|
24910
|
+
await import_node_fs45.promises.writeFile(guidePath, guide, "utf8");
|
|
23508
24911
|
const settingsFile = claudeSettingsPath();
|
|
23509
24912
|
let settings = {};
|
|
23510
24913
|
try {
|
|
23511
|
-
settings = JSON.parse(await
|
|
24914
|
+
settings = JSON.parse(await import_node_fs45.promises.readFile(settingsFile, "utf8"));
|
|
23512
24915
|
} catch (err) {
|
|
23513
24916
|
if (err.code !== "ENOENT") {
|
|
23514
24917
|
console.error(
|
|
@@ -23530,8 +24933,8 @@ async function runHooks(opts) {
|
|
|
23530
24933
|
...settings,
|
|
23531
24934
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
23532
24935
|
};
|
|
23533
|
-
await
|
|
23534
|
-
await
|
|
24936
|
+
await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(settingsFile), { recursive: true });
|
|
24937
|
+
await import_node_fs45.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
23535
24938
|
console.log(`neat hooks: installed the search-nudge hook`);
|
|
23536
24939
|
console.log(` script: ${scriptPath}`);
|
|
23537
24940
|
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
@@ -23602,9 +25005,9 @@ async function runHooksCommand(args) {
|
|
|
23602
25005
|
|
|
23603
25006
|
// src/codex-cli.ts
|
|
23604
25007
|
init_cjs_shims();
|
|
23605
|
-
var
|
|
25008
|
+
var import_node_path80 = __toESM(require("path"), 1);
|
|
23606
25009
|
var import_node_os6 = __toESM(require("os"), 1);
|
|
23607
|
-
var
|
|
25010
|
+
var import_node_fs46 = require("fs");
|
|
23608
25011
|
var import_node_util = require("util");
|
|
23609
25012
|
var import_smol_toml5 = require("smol-toml");
|
|
23610
25013
|
var CODEX_MCP_SERVER = {
|
|
@@ -23622,14 +25025,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
|
23622
25025
|
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
23623
25026
|
function codexConfigPath() {
|
|
23624
25027
|
const override = process.env.NEAT_CODEX_CONFIG;
|
|
23625
|
-
if (override && override.length > 0) return
|
|
25028
|
+
if (override && override.length > 0) return import_node_path80.default.resolve(override);
|
|
23626
25029
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
|
|
23627
|
-
return
|
|
25030
|
+
return import_node_path80.default.join(home, ".codex", "config.toml");
|
|
23628
25031
|
}
|
|
23629
25032
|
function agentsFilePath() {
|
|
23630
25033
|
const override = process.env.NEAT_CODEX_AGENTS;
|
|
23631
|
-
if (override && override.length > 0) return
|
|
23632
|
-
return
|
|
25034
|
+
if (override && override.length > 0) return import_node_path80.default.resolve(override);
|
|
25035
|
+
return import_node_path80.default.join(process.cwd(), "AGENTS.md");
|
|
23633
25036
|
}
|
|
23634
25037
|
function isTableHeader(line) {
|
|
23635
25038
|
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
@@ -23763,7 +25166,7 @@ async function runCodex(opts) {
|
|
|
23763
25166
|
const agentsPath = agentsFilePath();
|
|
23764
25167
|
let configRaw = "";
|
|
23765
25168
|
try {
|
|
23766
|
-
configRaw = await
|
|
25169
|
+
configRaw = await import_node_fs46.promises.readFile(configPath, "utf8");
|
|
23767
25170
|
} catch (err) {
|
|
23768
25171
|
if (err.code !== "ENOENT") {
|
|
23769
25172
|
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
@@ -23772,7 +25175,7 @@ async function runCodex(opts) {
|
|
|
23772
25175
|
}
|
|
23773
25176
|
let agentsRaw = "";
|
|
23774
25177
|
try {
|
|
23775
|
-
agentsRaw = await
|
|
25178
|
+
agentsRaw = await import_node_fs46.promises.readFile(agentsPath, "utf8");
|
|
23776
25179
|
} catch (err) {
|
|
23777
25180
|
if (err.code !== "ENOENT") {
|
|
23778
25181
|
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
@@ -23812,15 +25215,15 @@ async function runCodex(opts) {
|
|
|
23812
25215
|
return { exitCode: 0 };
|
|
23813
25216
|
}
|
|
23814
25217
|
if (config.changed) {
|
|
23815
|
-
await
|
|
23816
|
-
await
|
|
25218
|
+
await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(configPath), { recursive: true });
|
|
25219
|
+
await import_node_fs46.promises.writeFile(configPath, config.text, "utf8");
|
|
23817
25220
|
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
23818
25221
|
} else {
|
|
23819
25222
|
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
23820
25223
|
}
|
|
23821
25224
|
if (agents.changed) {
|
|
23822
|
-
await
|
|
23823
|
-
await
|
|
25225
|
+
await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(agentsPath), { recursive: true });
|
|
25226
|
+
await import_node_fs46.promises.writeFile(agentsPath, agents.text, "utf8");
|
|
23824
25227
|
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
23825
25228
|
} else {
|
|
23826
25229
|
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
@@ -23877,9 +25280,9 @@ async function runCodexCommand(args) {
|
|
|
23877
25280
|
|
|
23878
25281
|
// src/editors-cli.ts
|
|
23879
25282
|
init_cjs_shims();
|
|
23880
|
-
var
|
|
25283
|
+
var import_node_path81 = __toESM(require("path"), 1);
|
|
23881
25284
|
var import_node_os7 = __toESM(require("os"), 1);
|
|
23882
|
-
var
|
|
25285
|
+
var import_node_fs47 = require("fs");
|
|
23883
25286
|
var import_node_util2 = require("util");
|
|
23884
25287
|
var jsonc = __toESM(require("jsonc-parser"), 1);
|
|
23885
25288
|
var NEAT_MCP_SERVER = {
|
|
@@ -23903,17 +25306,17 @@ function homeDir() {
|
|
|
23903
25306
|
}
|
|
23904
25307
|
function xdgConfigDir() {
|
|
23905
25308
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
23906
|
-
return xdg && xdg.length > 0 ?
|
|
25309
|
+
return xdg && xdg.length > 0 ? import_node_path81.default.resolve(xdg) : import_node_path81.default.join(homeDir(), ".config");
|
|
23907
25310
|
}
|
|
23908
25311
|
function envOverride(name) {
|
|
23909
25312
|
const v = process.env[name];
|
|
23910
|
-
return v && v.length > 0 ?
|
|
25313
|
+
return v && v.length > 0 ? import_node_path81.default.resolve(v) : void 0;
|
|
23911
25314
|
}
|
|
23912
25315
|
var CURSOR_CLIENT = {
|
|
23913
25316
|
id: "cursor",
|
|
23914
25317
|
label: "Cursor",
|
|
23915
25318
|
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
23916
|
-
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ??
|
|
25319
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path81.default.join(homeDir(), ".cursor", "mcp.json"),
|
|
23917
25320
|
mcpContainerKey: "mcpServers",
|
|
23918
25321
|
format: "json",
|
|
23919
25322
|
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
@@ -23925,7 +25328,7 @@ var DEVIN_CLIENT = {
|
|
|
23925
25328
|
id: "devin",
|
|
23926
25329
|
label: "Devin Desktop (Cascade)",
|
|
23927
25330
|
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
23928
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ??
|
|
25331
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
23929
25332
|
mcpContainerKey: "mcpServers",
|
|
23930
25333
|
format: "json",
|
|
23931
25334
|
rulesFileName: ".windsurfrules"
|
|
@@ -23934,7 +25337,7 @@ var GEMINI_CLIENT = {
|
|
|
23934
25337
|
id: "gemini",
|
|
23935
25338
|
label: "Gemini CLI",
|
|
23936
25339
|
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
23937
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ??
|
|
25340
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path81.default.join(homeDir(), ".gemini", "settings.json"),
|
|
23938
25341
|
mcpContainerKey: "mcpServers",
|
|
23939
25342
|
format: "json",
|
|
23940
25343
|
rulesFileName: "GEMINI.md"
|
|
@@ -23943,7 +25346,7 @@ var QWEN_CLIENT = {
|
|
|
23943
25346
|
id: "qwen",
|
|
23944
25347
|
label: "Qwen Code",
|
|
23945
25348
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
23946
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ??
|
|
25349
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".qwen", "settings.json"),
|
|
23947
25350
|
mcpContainerKey: "mcpServers",
|
|
23948
25351
|
format: "json",
|
|
23949
25352
|
rulesFileName: "QWEN.md"
|
|
@@ -23952,7 +25355,7 @@ var AMAZONQ_CLIENT = {
|
|
|
23952
25355
|
id: "amazonq",
|
|
23953
25356
|
label: "Amazon Q Developer CLI",
|
|
23954
25357
|
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
23955
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ??
|
|
25358
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path81.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
23956
25359
|
mcpContainerKey: "mcpServers",
|
|
23957
25360
|
format: "json"
|
|
23958
25361
|
};
|
|
@@ -23960,7 +25363,7 @@ var ROOCODE_CLIENT = {
|
|
|
23960
25363
|
id: "roocode",
|
|
23961
25364
|
label: "Roo Code",
|
|
23962
25365
|
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
23963
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ??
|
|
25366
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path81.default.join(process.cwd(), ".roo", "mcp.json"),
|
|
23964
25367
|
mcpContainerKey: "mcpServers",
|
|
23965
25368
|
format: "json"
|
|
23966
25369
|
};
|
|
@@ -23973,9 +25376,9 @@ var ZED_CLIENT = {
|
|
|
23973
25376
|
if (override) return override;
|
|
23974
25377
|
if (process.platform === "win32") {
|
|
23975
25378
|
const appData = process.env.APPDATA;
|
|
23976
|
-
if (appData && appData.length > 0) return
|
|
25379
|
+
if (appData && appData.length > 0) return import_node_path81.default.join(appData, "Zed", "settings.json");
|
|
23977
25380
|
}
|
|
23978
|
-
return
|
|
25381
|
+
return import_node_path81.default.join(homeDir(), ".config", "zed", "settings.json");
|
|
23979
25382
|
},
|
|
23980
25383
|
mcpContainerKey: "context_servers",
|
|
23981
25384
|
format: "jsonc",
|
|
@@ -23985,7 +25388,7 @@ var OPENCODE_CLIENT = {
|
|
|
23985
25388
|
id: "opencode",
|
|
23986
25389
|
label: "OpenCode",
|
|
23987
25390
|
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
23988
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ??
|
|
25391
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
23989
25392
|
mcpContainerKey: "mcp",
|
|
23990
25393
|
format: "json",
|
|
23991
25394
|
serverEntry: NEAT_OPENCODE_SERVER,
|
|
@@ -23995,7 +25398,7 @@ var CRUSH_CLIENT = {
|
|
|
23995
25398
|
id: "crush",
|
|
23996
25399
|
label: "Crush",
|
|
23997
25400
|
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
23998
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ??
|
|
25401
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "crush", "crush.json"),
|
|
23999
25402
|
mcpContainerKey: "mcp",
|
|
24000
25403
|
format: "json",
|
|
24001
25404
|
serverEntry: NEAT_CRUSH_SERVER,
|
|
@@ -24058,7 +25461,7 @@ async function planMcp(client, mcpPath) {
|
|
|
24058
25461
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
24059
25462
|
let raw = "";
|
|
24060
25463
|
try {
|
|
24061
|
-
raw = await
|
|
25464
|
+
raw = await import_node_fs47.promises.readFile(mcpPath, "utf8");
|
|
24062
25465
|
} catch (err) {
|
|
24063
25466
|
const e = err;
|
|
24064
25467
|
if (e.code === "ENOENT") {
|
|
@@ -24100,7 +25503,7 @@ async function runEditorInstall(client, opts) {
|
|
|
24100
25503
|
const mcpPath = client.mcpConfigPath();
|
|
24101
25504
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
24102
25505
|
const hasRules = typeof client.rulesFileName === "string";
|
|
24103
|
-
const rulesPath = hasRules ?
|
|
25506
|
+
const rulesPath = hasRules ? import_node_path81.default.join(opts.projectDir, client.rulesFileName) : "";
|
|
24104
25507
|
const mcp = await planMcp(client, mcpPath);
|
|
24105
25508
|
if (mcp === null) return { exitCode: 1 };
|
|
24106
25509
|
let existingRules = "";
|
|
@@ -24109,7 +25512,7 @@ async function runEditorInstall(client, opts) {
|
|
|
24109
25512
|
let block = "";
|
|
24110
25513
|
if (hasRules) {
|
|
24111
25514
|
try {
|
|
24112
|
-
existingRules = await
|
|
25515
|
+
existingRules = await import_node_fs47.promises.readFile(rulesPath, "utf8");
|
|
24113
25516
|
} catch (err) {
|
|
24114
25517
|
if (err.code !== "ENOENT") {
|
|
24115
25518
|
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
@@ -24143,11 +25546,11 @@ async function runEditorInstall(client, opts) {
|
|
|
24143
25546
|
);
|
|
24144
25547
|
return { exitCode: 0 };
|
|
24145
25548
|
}
|
|
24146
|
-
await
|
|
24147
|
-
await
|
|
25549
|
+
await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(mcpPath), { recursive: true });
|
|
25550
|
+
await import_node_fs47.promises.writeFile(mcpPath, mcp.text, "utf8");
|
|
24148
25551
|
if (hasRules) {
|
|
24149
|
-
await
|
|
24150
|
-
await
|
|
25552
|
+
await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(rulesPath), { recursive: true });
|
|
25553
|
+
await import_node_fs47.promises.writeFile(rulesPath, newRules, "utf8");
|
|
24151
25554
|
}
|
|
24152
25555
|
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
24153
25556
|
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
@@ -24183,11 +25586,11 @@ function usage3(client) {
|
|
|
24183
25586
|
}
|
|
24184
25587
|
async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
24185
25588
|
const client = CLIENTS[clientId];
|
|
24186
|
-
let
|
|
25589
|
+
let apply6 = false;
|
|
24187
25590
|
for (const arg of args) {
|
|
24188
25591
|
switch (arg) {
|
|
24189
25592
|
case "--apply":
|
|
24190
|
-
|
|
25593
|
+
apply6 = true;
|
|
24191
25594
|
break;
|
|
24192
25595
|
case "-h":
|
|
24193
25596
|
case "--help":
|
|
@@ -24200,7 +25603,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
24200
25603
|
}
|
|
24201
25604
|
}
|
|
24202
25605
|
try {
|
|
24203
|
-
const { exitCode } = await runEditorInstall(client, { apply:
|
|
25606
|
+
const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
|
|
24204
25607
|
return exitCode;
|
|
24205
25608
|
} catch (err) {
|
|
24206
25609
|
console.error(err.message);
|
|
@@ -24210,11 +25613,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
24210
25613
|
|
|
24211
25614
|
// src/monitor.ts
|
|
24212
25615
|
init_cjs_shims();
|
|
24213
|
-
var
|
|
25616
|
+
var import_types89 = require("@neat.is/types");
|
|
24214
25617
|
|
|
24215
25618
|
// src/cli-client.ts
|
|
24216
25619
|
init_cjs_shims();
|
|
24217
|
-
var
|
|
25620
|
+
var import_types88 = require("@neat.is/types");
|
|
24218
25621
|
var HttpError = class extends Error {
|
|
24219
25622
|
constructor(status2, message, responseBody = "") {
|
|
24220
25623
|
super(message);
|
|
@@ -24239,10 +25642,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
24239
25642
|
const root = baseUrl.replace(/\/$/, "");
|
|
24240
25643
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
24241
25644
|
return {
|
|
24242
|
-
async get(
|
|
25645
|
+
async get(path84) {
|
|
24243
25646
|
let res;
|
|
24244
25647
|
try {
|
|
24245
|
-
res = await fetch(`${root}${
|
|
25648
|
+
res = await fetch(`${root}${path84}`, {
|
|
24246
25649
|
headers: { ...authHeader }
|
|
24247
25650
|
});
|
|
24248
25651
|
} catch (err) {
|
|
@@ -24254,16 +25657,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
24254
25657
|
const body = await res.text().catch(() => "");
|
|
24255
25658
|
throw new HttpError(
|
|
24256
25659
|
res.status,
|
|
24257
|
-
`${res.status} ${res.statusText} on GET ${
|
|
25660
|
+
`${res.status} ${res.statusText} on GET ${path84}: ${body}`,
|
|
24258
25661
|
body
|
|
24259
25662
|
);
|
|
24260
25663
|
}
|
|
24261
25664
|
return await res.json();
|
|
24262
25665
|
},
|
|
24263
|
-
async post(
|
|
25666
|
+
async post(path84, body) {
|
|
24264
25667
|
let res;
|
|
24265
25668
|
try {
|
|
24266
|
-
res = await fetch(`${root}${
|
|
25669
|
+
res = await fetch(`${root}${path84}`, {
|
|
24267
25670
|
method: "POST",
|
|
24268
25671
|
headers: { "content-type": "application/json", ...authHeader },
|
|
24269
25672
|
body: JSON.stringify(body)
|
|
@@ -24277,7 +25680,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
24277
25680
|
const text = await res.text().catch(() => "");
|
|
24278
25681
|
throw new HttpError(
|
|
24279
25682
|
res.status,
|
|
24280
|
-
`${res.status} ${res.statusText} on POST ${
|
|
25683
|
+
`${res.status} ${res.statusText} on POST ${path84}: ${text}`,
|
|
24281
25684
|
text
|
|
24282
25685
|
);
|
|
24283
25686
|
}
|
|
@@ -24291,12 +25694,12 @@ function projectPath(project, suffix) {
|
|
|
24291
25694
|
}
|
|
24292
25695
|
async function runRootCause(client, input) {
|
|
24293
25696
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
24294
|
-
const
|
|
25697
|
+
const path84 = projectPath(
|
|
24295
25698
|
input.project,
|
|
24296
25699
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
24297
25700
|
);
|
|
24298
25701
|
try {
|
|
24299
|
-
const result = await client.get(
|
|
25702
|
+
const result = await client.get(path84);
|
|
24300
25703
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
24301
25704
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
24302
25705
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -24322,12 +25725,12 @@ async function runRootCause(client, input) {
|
|
|
24322
25725
|
}
|
|
24323
25726
|
async function runBlastRadius(client, input) {
|
|
24324
25727
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
24325
|
-
const
|
|
25728
|
+
const path84 = projectPath(
|
|
24326
25729
|
input.project,
|
|
24327
25730
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
24328
25731
|
);
|
|
24329
25732
|
try {
|
|
24330
|
-
const result = await client.get(
|
|
25733
|
+
const result = await client.get(path84);
|
|
24331
25734
|
if (result.totalAffected === 0) {
|
|
24332
25735
|
return {
|
|
24333
25736
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -24356,17 +25759,17 @@ async function runBlastRadius(client, input) {
|
|
|
24356
25759
|
}
|
|
24357
25760
|
}
|
|
24358
25761
|
function formatBlastEntry(n) {
|
|
24359
|
-
const tag = n.edgeProvenance ===
|
|
25762
|
+
const tag = n.edgeProvenance === import_types88.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
24360
25763
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
24361
25764
|
}
|
|
24362
25765
|
async function runDependencies(client, input) {
|
|
24363
25766
|
const depth = input.depth ?? 3;
|
|
24364
|
-
const
|
|
25767
|
+
const path84 = projectPath(
|
|
24365
25768
|
input.project,
|
|
24366
25769
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
24367
25770
|
);
|
|
24368
25771
|
try {
|
|
24369
|
-
const result = await client.get(
|
|
25772
|
+
const result = await client.get(path84);
|
|
24370
25773
|
if (result.total === 0) {
|
|
24371
25774
|
return {
|
|
24372
25775
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -24413,7 +25816,7 @@ async function runObservedDependencies(client, input) {
|
|
|
24413
25816
|
if (result.observed) {
|
|
24414
25817
|
return {
|
|
24415
25818
|
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.`,
|
|
24416
|
-
provenance:
|
|
25819
|
+
provenance: import_types88.Provenance.OBSERVED
|
|
24417
25820
|
};
|
|
24418
25821
|
}
|
|
24419
25822
|
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
@@ -24423,7 +25826,7 @@ async function runObservedDependencies(client, input) {
|
|
|
24423
25826
|
return {
|
|
24424
25827
|
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
24425
25828
|
block: blockLines.join("\n"),
|
|
24426
|
-
provenance:
|
|
25829
|
+
provenance: import_types88.Provenance.OBSERVED
|
|
24427
25830
|
};
|
|
24428
25831
|
} catch (err) {
|
|
24429
25832
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -24458,9 +25861,9 @@ function formatDuration(ms) {
|
|
|
24458
25861
|
return `${Math.round(h / 24)}d`;
|
|
24459
25862
|
}
|
|
24460
25863
|
async function runIncidents(client, input) {
|
|
24461
|
-
const
|
|
25864
|
+
const path84 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
24462
25865
|
try {
|
|
24463
|
-
const body = await client.get(
|
|
25866
|
+
const body = await client.get(path84);
|
|
24464
25867
|
const events = body.events;
|
|
24465
25868
|
if (events.length === 0) {
|
|
24466
25869
|
return {
|
|
@@ -24477,7 +25880,7 @@ async function runIncidents(client, input) {
|
|
|
24477
25880
|
return {
|
|
24478
25881
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
24479
25882
|
block: blockLines.join("\n"),
|
|
24480
|
-
provenance:
|
|
25883
|
+
provenance: import_types88.Provenance.OBSERVED
|
|
24481
25884
|
};
|
|
24482
25885
|
} catch (err) {
|
|
24483
25886
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -24586,7 +25989,7 @@ async function runStaleEdges(client, input) {
|
|
|
24586
25989
|
return {
|
|
24587
25990
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
24588
25991
|
block: blockLines.join("\n"),
|
|
24589
|
-
provenance:
|
|
25992
|
+
provenance: import_types88.Provenance.STALE
|
|
24590
25993
|
};
|
|
24591
25994
|
}
|
|
24592
25995
|
async function runPolicies(client, input) {
|
|
@@ -24745,10 +26148,10 @@ async function pushSnapshotToRemote(input) {
|
|
|
24745
26148
|
|
|
24746
26149
|
// src/monitor.ts
|
|
24747
26150
|
var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
24748
|
-
|
|
24749
|
-
|
|
24750
|
-
|
|
24751
|
-
|
|
26151
|
+
import_types89.EdgeType.CALLS,
|
|
26152
|
+
import_types89.EdgeType.CONNECTS_TO,
|
|
26153
|
+
import_types89.EdgeType.PUBLISHES_TO,
|
|
26154
|
+
import_types89.EdgeType.CONSUMES_FROM
|
|
24752
26155
|
]);
|
|
24753
26156
|
function divergenceKey(d) {
|
|
24754
26157
|
const column = "column" in d && d.column ? d.column : "";
|
|
@@ -24793,7 +26196,7 @@ function formatDivergenceLine2(d) {
|
|
|
24793
26196
|
}
|
|
24794
26197
|
}
|
|
24795
26198
|
function formatStaleLine(edgeId) {
|
|
24796
|
-
const parsed = (0,
|
|
26199
|
+
const parsed = (0, import_types89.parseEdgeId)(edgeId);
|
|
24797
26200
|
if (parsed) {
|
|
24798
26201
|
return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
|
|
24799
26202
|
}
|
|
@@ -24806,7 +26209,7 @@ function divergenceJson(d) {
|
|
|
24806
26209
|
return JSON.stringify({ kind: "divergence", ...d });
|
|
24807
26210
|
}
|
|
24808
26211
|
function staleJson(edgeId) {
|
|
24809
|
-
const parsed = (0,
|
|
26212
|
+
const parsed = (0, import_types89.parseEdgeId)(edgeId);
|
|
24810
26213
|
return JSON.stringify({
|
|
24811
26214
|
kind: "stale",
|
|
24812
26215
|
edgeId,
|
|
@@ -24876,7 +26279,7 @@ var MonitorEmitter = class {
|
|
|
24876
26279
|
// ignores non-OBSERVED edges and non-dependency edge types (structural
|
|
24877
26280
|
// ownership), so only real runtime dependencies reach stdout.
|
|
24878
26281
|
emitObservedEdge(edge) {
|
|
24879
|
-
if (edge.provenance !==
|
|
26282
|
+
if (edge.provenance !== import_types89.Provenance.OBSERVED) return false;
|
|
24880
26283
|
if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
|
|
24881
26284
|
const key = `edge|${edge.id}`;
|
|
24882
26285
|
if (this.seen.has(key)) return false;
|
|
@@ -25034,7 +26437,7 @@ async function runMonitor(opts) {
|
|
|
25034
26437
|
case "edge-added": {
|
|
25035
26438
|
const payload = safeParse(frame.data);
|
|
25036
26439
|
const edge = payload?.edge;
|
|
25037
|
-
if (edge && edge.provenance ===
|
|
26440
|
+
if (edge && edge.provenance === import_types89.Provenance.OBSERVED) {
|
|
25038
26441
|
emitter.emitObservedEdge(edge);
|
|
25039
26442
|
divergences.schedule();
|
|
25040
26443
|
}
|
|
@@ -25114,7 +26517,7 @@ function sleep(ms, signal) {
|
|
|
25114
26517
|
|
|
25115
26518
|
// src/cli-verbs.ts
|
|
25116
26519
|
init_cjs_shims();
|
|
25117
|
-
var
|
|
26520
|
+
var import_node_path82 = __toESM(require("path"), 1);
|
|
25118
26521
|
async function resolveProjectEntry(opts) {
|
|
25119
26522
|
const entries = await listProjects();
|
|
25120
26523
|
if (opts.project) {
|
|
@@ -25124,7 +26527,7 @@ async function resolveProjectEntry(opts) {
|
|
|
25124
26527
|
const cwd = opts.cwd ?? process.cwd();
|
|
25125
26528
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
25126
26529
|
for (const entry2 of entries) {
|
|
25127
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
26530
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path82.default.sep}`)) {
|
|
25128
26531
|
return entry2;
|
|
25129
26532
|
}
|
|
25130
26533
|
}
|
|
@@ -25277,7 +26680,7 @@ async function runSync(opts) {
|
|
|
25277
26680
|
}
|
|
25278
26681
|
|
|
25279
26682
|
// src/cli.ts
|
|
25280
|
-
var
|
|
26683
|
+
var import_types90 = require("@neat.is/types");
|
|
25281
26684
|
function isNpxInvocation() {
|
|
25282
26685
|
if (process.env.npm_command === "exec") return true;
|
|
25283
26686
|
const execpath = process.env.npm_execpath ?? "";
|
|
@@ -25631,15 +27034,15 @@ async function buildPatchSections(services, project) {
|
|
|
25631
27034
|
for (const svc of services) {
|
|
25632
27035
|
const installer = await pickInstaller(svc.dir);
|
|
25633
27036
|
if (!installer) continue;
|
|
25634
|
-
const
|
|
25635
|
-
if (isEmptyPlan(
|
|
25636
|
-
sections.push({ installer: installer.name, plan:
|
|
27037
|
+
const plan6 = await installer.plan(svc.dir, { project });
|
|
27038
|
+
if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
|
|
27039
|
+
sections.push({ installer: installer.name, plan: plan6 });
|
|
25637
27040
|
}
|
|
25638
27041
|
return sections;
|
|
25639
27042
|
}
|
|
25640
27043
|
async function runInit(opts) {
|
|
25641
27044
|
const written = [];
|
|
25642
|
-
const stat = await
|
|
27045
|
+
const stat = await import_node_fs48.promises.stat(opts.scanPath).catch(() => null);
|
|
25643
27046
|
if (!stat || !stat.isDirectory()) {
|
|
25644
27047
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
25645
27048
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -25648,13 +27051,13 @@ async function runInit(opts) {
|
|
|
25648
27051
|
printDiscoveryReport(opts, services);
|
|
25649
27052
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
25650
27053
|
const patch = renderPatch(sections);
|
|
25651
|
-
const patchPath =
|
|
27054
|
+
const patchPath = import_node_path83.default.join(opts.scanPath, "neat.patch");
|
|
25652
27055
|
if (opts.dryRun) {
|
|
25653
|
-
await
|
|
27056
|
+
await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
|
|
25654
27057
|
written.push(patchPath);
|
|
25655
27058
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
25656
|
-
const gitignorePath =
|
|
25657
|
-
const gitignoreExists = await
|
|
27059
|
+
const gitignorePath = import_node_path83.default.join(opts.scanPath, ".gitignore");
|
|
27060
|
+
const gitignoreExists = await import_node_fs48.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
25658
27061
|
const verb = gitignoreExists ? "append" : "create";
|
|
25659
27062
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
25660
27063
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -25665,9 +27068,9 @@ async function runInit(opts) {
|
|
|
25665
27068
|
const graph = getGraph(graphKey);
|
|
25666
27069
|
const projectPaths = pathsForProject(
|
|
25667
27070
|
graphKey,
|
|
25668
|
-
|
|
27071
|
+
import_node_path83.default.join(opts.scanPath, "neat-out")
|
|
25669
27072
|
);
|
|
25670
|
-
const errorsPath =
|
|
27073
|
+
const errorsPath = import_node_path83.default.join(import_node_path83.default.dirname(opts.outPath), import_node_path83.default.basename(projectPaths.errorsPath));
|
|
25671
27074
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
25672
27075
|
await saveGraphToDisk(graph, opts.outPath);
|
|
25673
27076
|
written.push(opts.outPath);
|
|
@@ -25746,7 +27149,7 @@ async function runInit(opts) {
|
|
|
25746
27149
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
25747
27150
|
}
|
|
25748
27151
|
} else {
|
|
25749
|
-
await
|
|
27152
|
+
await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
|
|
25750
27153
|
written.push(patchPath);
|
|
25751
27154
|
}
|
|
25752
27155
|
}
|
|
@@ -25786,9 +27189,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
25786
27189
|
};
|
|
25787
27190
|
function claudeConfigPath() {
|
|
25788
27191
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
25789
|
-
if (override && override.length > 0) return
|
|
27192
|
+
if (override && override.length > 0) return import_node_path83.default.resolve(override);
|
|
25790
27193
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
25791
|
-
return
|
|
27194
|
+
return import_node_path83.default.join(home, ".claude.json");
|
|
25792
27195
|
}
|
|
25793
27196
|
async function runSkill(opts) {
|
|
25794
27197
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -25800,7 +27203,7 @@ async function runSkill(opts) {
|
|
|
25800
27203
|
const target = claudeConfigPath();
|
|
25801
27204
|
let existing = {};
|
|
25802
27205
|
try {
|
|
25803
|
-
existing = JSON.parse(await
|
|
27206
|
+
existing = JSON.parse(await import_node_fs48.promises.readFile(target, "utf8"));
|
|
25804
27207
|
} catch (err) {
|
|
25805
27208
|
if (err.code !== "ENOENT") {
|
|
25806
27209
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -25812,8 +27215,8 @@ async function runSkill(opts) {
|
|
|
25812
27215
|
...existing,
|
|
25813
27216
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
25814
27217
|
};
|
|
25815
|
-
await
|
|
25816
|
-
await
|
|
27218
|
+
await import_node_fs48.promises.mkdir(import_node_path83.default.dirname(target), { recursive: true });
|
|
27219
|
+
await import_node_fs48.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
25817
27220
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
25818
27221
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
25819
27222
|
console.log("");
|
|
@@ -25886,7 +27289,7 @@ async function main() {
|
|
|
25886
27289
|
}
|
|
25887
27290
|
const cmd = argvParsed.positional[0];
|
|
25888
27291
|
const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
|
|
25889
|
-
const { positional, apply:
|
|
27292
|
+
const { positional, apply: apply6, dryRun, noInstall } = parsed;
|
|
25890
27293
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
25891
27294
|
if (cmd === "init") {
|
|
25892
27295
|
const target = positional[0];
|
|
@@ -25895,22 +27298,22 @@ async function main() {
|
|
|
25895
27298
|
usage4();
|
|
25896
27299
|
process.exit(2);
|
|
25897
27300
|
}
|
|
25898
|
-
if (
|
|
27301
|
+
if (apply6 && dryRun) {
|
|
25899
27302
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
25900
27303
|
process.exit(2);
|
|
25901
27304
|
}
|
|
25902
|
-
const scanPath =
|
|
27305
|
+
const scanPath = import_node_path83.default.resolve(target);
|
|
25903
27306
|
const projectExplicit = parsed.project !== null;
|
|
25904
|
-
const projectName = projectExplicit ? project :
|
|
27307
|
+
const projectName = projectExplicit ? project : import_node_path83.default.basename(scanPath);
|
|
25905
27308
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
25906
|
-
const fallback = pathsForProject(projectKey,
|
|
25907
|
-
const outPath =
|
|
27309
|
+
const fallback = pathsForProject(projectKey, import_node_path83.default.join(scanPath, "neat-out")).snapshotPath;
|
|
27310
|
+
const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
25908
27311
|
const result = await runInit({
|
|
25909
27312
|
scanPath,
|
|
25910
27313
|
outPath,
|
|
25911
27314
|
project: projectName,
|
|
25912
27315
|
projectExplicit,
|
|
25913
|
-
apply:
|
|
27316
|
+
apply: apply6,
|
|
25914
27317
|
dryRun,
|
|
25915
27318
|
noInstall,
|
|
25916
27319
|
verbose: parsed.verbose
|
|
@@ -25925,21 +27328,21 @@ async function main() {
|
|
|
25925
27328
|
usage4();
|
|
25926
27329
|
process.exit(2);
|
|
25927
27330
|
}
|
|
25928
|
-
const scanPath =
|
|
25929
|
-
const stat = await
|
|
27331
|
+
const scanPath = import_node_path83.default.resolve(target);
|
|
27332
|
+
const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
|
|
25930
27333
|
if (!stat || !stat.isDirectory()) {
|
|
25931
27334
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
25932
27335
|
process.exit(2);
|
|
25933
27336
|
}
|
|
25934
|
-
const projectPaths = pathsForProject(project,
|
|
25935
|
-
const outPath =
|
|
25936
|
-
const errorsPath =
|
|
25937
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
27337
|
+
const projectPaths = pathsForProject(project, import_node_path83.default.join(scanPath, "neat-out"));
|
|
27338
|
+
const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
27339
|
+
const errorsPath = import_node_path83.default.resolve(
|
|
27340
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.errorsPath))
|
|
25938
27341
|
);
|
|
25939
|
-
const staleEventsPath =
|
|
25940
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
27342
|
+
const staleEventsPath = import_node_path83.default.resolve(
|
|
27343
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.staleEventsPath))
|
|
25941
27344
|
);
|
|
25942
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
27345
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path83.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
25943
27346
|
const handle = await startWatch(getGraph(project), {
|
|
25944
27347
|
scanPath,
|
|
25945
27348
|
outPath,
|
|
@@ -25948,7 +27351,7 @@ async function main() {
|
|
|
25948
27351
|
project,
|
|
25949
27352
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
25950
27353
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
25951
|
-
neatHome: process.env.NEAT_HOME ?
|
|
27354
|
+
neatHome: process.env.NEAT_HOME ? import_node_path83.default.resolve(process.env.NEAT_HOME) : import_node_path83.default.join(import_node_os8.default.homedir(), ".neat"),
|
|
25952
27355
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
25953
27356
|
host: process.env.HOST ?? "0.0.0.0",
|
|
25954
27357
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -26130,11 +27533,11 @@ async function main() {
|
|
|
26130
27533
|
process.exit(1);
|
|
26131
27534
|
}
|
|
26132
27535
|
async function tryOrchestrator(cmd, parsed) {
|
|
26133
|
-
const scanPath =
|
|
26134
|
-
const stat = await
|
|
27536
|
+
const scanPath = import_node_path83.default.resolve(cmd);
|
|
27537
|
+
const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
|
|
26135
27538
|
if (!stat || !stat.isDirectory()) return null;
|
|
26136
27539
|
const projectExplicit = parsed.project !== null;
|
|
26137
|
-
const projectName = projectExplicit ? parsed.project :
|
|
27540
|
+
const projectName = projectExplicit ? parsed.project : import_node_path83.default.basename(scanPath);
|
|
26138
27541
|
const result = await runOrchestrator({
|
|
26139
27542
|
scanPath,
|
|
26140
27543
|
project: projectName,
|
|
@@ -26323,10 +27726,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
26323
27726
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
26324
27727
|
const out = [];
|
|
26325
27728
|
for (const p of parts) {
|
|
26326
|
-
const r =
|
|
27729
|
+
const r = import_types90.DivergenceTypeSchema.safeParse(p);
|
|
26327
27730
|
if (!r.success) {
|
|
26328
27731
|
console.error(
|
|
26329
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
27732
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types90.DivergenceTypeSchema.options.join(", ")}`
|
|
26330
27733
|
);
|
|
26331
27734
|
return 2;
|
|
26332
27735
|
}
|