@neat.is/core 0.4.5 → 0.4.7
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-RBWL4HRB.js → chunk-7FTK47JQ.js} +277 -103
- package/dist/chunk-7FTK47JQ.js.map +1 -0
- package/dist/{chunk-6GXBAR3M.js → chunk-LS6NS72S.js} +37 -3
- package/dist/chunk-LS6NS72S.js.map +1 -0
- package/dist/cli.cjs +600 -219
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +294 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +314 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +314 -115
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +263 -98
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-6GXBAR3M.js.map +0 -1
- package/dist/chunk-RBWL4HRB.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -49,9 +49,9 @@ function mountBearerAuth(app, opts) {
|
|
|
49
49
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
50
50
|
const publicRead = opts.publicRead === true;
|
|
51
51
|
app.addHook("preHandler", (req, reply, done) => {
|
|
52
|
-
const
|
|
52
|
+
const path46 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
53
53
|
for (const suffix of suffixes) {
|
|
54
|
-
if (
|
|
54
|
+
if (path46 === suffix || path46.endsWith(suffix)) {
|
|
55
55
|
done();
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -562,8 +562,8 @@ __export(cli_exports, {
|
|
|
562
562
|
});
|
|
563
563
|
module.exports = __toCommonJS(cli_exports);
|
|
564
564
|
init_cjs_shims();
|
|
565
|
-
var
|
|
566
|
-
var
|
|
565
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
566
|
+
var import_node_fs29 = require("fs");
|
|
567
567
|
var import_node_url3 = require("url");
|
|
568
568
|
|
|
569
569
|
// src/graph.ts
|
|
@@ -1074,19 +1074,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1074
1074
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1075
1075
|
let best = { path: [start], edges: [] };
|
|
1076
1076
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1077
|
-
function step(node,
|
|
1078
|
-
if (
|
|
1079
|
-
best = { path: [...
|
|
1077
|
+
function step(node, path46, edges) {
|
|
1078
|
+
if (path46.length > best.path.length) {
|
|
1079
|
+
best = { path: [...path46], edges: [...edges] };
|
|
1080
1080
|
}
|
|
1081
|
-
if (
|
|
1081
|
+
if (path46.length - 1 >= maxDepth) return;
|
|
1082
1082
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1083
1083
|
for (const [srcId, edge] of incoming) {
|
|
1084
1084
|
if (visited.has(srcId)) continue;
|
|
1085
1085
|
visited.add(srcId);
|
|
1086
|
-
|
|
1086
|
+
path46.push(srcId);
|
|
1087
1087
|
edges.push(edge);
|
|
1088
|
-
step(srcId,
|
|
1089
|
-
|
|
1088
|
+
step(srcId, path46, edges);
|
|
1089
|
+
path46.pop();
|
|
1090
1090
|
edges.pop();
|
|
1091
1091
|
visited.delete(srcId);
|
|
1092
1092
|
}
|
|
@@ -1649,6 +1649,86 @@ function hostFromUrl(u) {
|
|
|
1649
1649
|
function pickAddress(span) {
|
|
1650
1650
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1651
1651
|
}
|
|
1652
|
+
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1653
|
+
var CODE_LINENO_ATTR = "code.lineno";
|
|
1654
|
+
var CODE_FUNCTION_ATTR = "code.function";
|
|
1655
|
+
function toPosix(p) {
|
|
1656
|
+
return p.split("\\").join("/");
|
|
1657
|
+
}
|
|
1658
|
+
function languageForExt(relPath) {
|
|
1659
|
+
const dot = relPath.lastIndexOf(".");
|
|
1660
|
+
if (dot === -1) return void 0;
|
|
1661
|
+
switch (relPath.slice(dot).toLowerCase()) {
|
|
1662
|
+
case ".py":
|
|
1663
|
+
return "python";
|
|
1664
|
+
case ".ts":
|
|
1665
|
+
case ".tsx":
|
|
1666
|
+
return "typescript";
|
|
1667
|
+
case ".js":
|
|
1668
|
+
case ".jsx":
|
|
1669
|
+
case ".mjs":
|
|
1670
|
+
case ".cjs":
|
|
1671
|
+
return "javascript";
|
|
1672
|
+
default:
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function relPathForRuntimeFile(filepath, serviceNode) {
|
|
1677
|
+
let p = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1678
|
+
const root = serviceNode?.repoPath;
|
|
1679
|
+
if (root && root !== "." && root.length > 0) {
|
|
1680
|
+
const rootPosix = toPosix(root);
|
|
1681
|
+
const anchor = `/${rootPosix}/`;
|
|
1682
|
+
const idx = p.lastIndexOf(anchor);
|
|
1683
|
+
if (idx !== -1) return p.slice(idx + anchor.length);
|
|
1684
|
+
const base = rootPosix.split("/").filter(Boolean).pop();
|
|
1685
|
+
if (base) {
|
|
1686
|
+
const baseAnchor = `/${base}/`;
|
|
1687
|
+
const bidx = p.lastIndexOf(baseAnchor);
|
|
1688
|
+
if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
|
|
1692
|
+
return p.length > 0 ? p : null;
|
|
1693
|
+
}
|
|
1694
|
+
function callSiteFromSpan(span, serviceNode) {
|
|
1695
|
+
const filepath = span.attributes[CODE_FILEPATH_ATTR];
|
|
1696
|
+
if (typeof filepath !== "string" || filepath.length === 0) return null;
|
|
1697
|
+
const relPath = relPathForRuntimeFile(filepath, serviceNode);
|
|
1698
|
+
if (!relPath) return null;
|
|
1699
|
+
const linenoRaw = span.attributes[CODE_LINENO_ATTR];
|
|
1700
|
+
const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1701
|
+
const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
|
|
1702
|
+
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1703
|
+
return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
|
|
1704
|
+
}
|
|
1705
|
+
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1706
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
|
|
1707
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
1708
|
+
const language = languageForExt(callSite.relPath);
|
|
1709
|
+
const node = {
|
|
1710
|
+
id: fileNodeId,
|
|
1711
|
+
type: import_types3.NodeType.FileNode,
|
|
1712
|
+
service: serviceName,
|
|
1713
|
+
path: callSite.relPath,
|
|
1714
|
+
...language ? { language } : {},
|
|
1715
|
+
discoveredVia: "otel"
|
|
1716
|
+
};
|
|
1717
|
+
graph.addNode(fileNodeId, node);
|
|
1718
|
+
}
|
|
1719
|
+
const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
|
|
1720
|
+
if (!graph.hasEdge(containsId)) {
|
|
1721
|
+
const edge = {
|
|
1722
|
+
id: containsId,
|
|
1723
|
+
source: serviceNodeId,
|
|
1724
|
+
target: fileNodeId,
|
|
1725
|
+
type: import_types3.EdgeType.CONTAINS,
|
|
1726
|
+
provenance: import_types3.Provenance.OBSERVED
|
|
1727
|
+
};
|
|
1728
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
1729
|
+
}
|
|
1730
|
+
return fileNodeId;
|
|
1731
|
+
}
|
|
1652
1732
|
function makeObservedEdgeId(type, source, target) {
|
|
1653
1733
|
return (0, import_types3.observedEdgeId)(source, target, type);
|
|
1654
1734
|
}
|
|
@@ -1891,6 +1971,9 @@ async function handleSpan(ctx, span) {
|
|
|
1891
1971
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1892
1972
|
const isError = span.statusCode === 2;
|
|
1893
1973
|
cacheSpanService(span, nowMs);
|
|
1974
|
+
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1975
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
1976
|
+
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1894
1977
|
let affectedNode = sourceId;
|
|
1895
1978
|
if (span.dbSystem) {
|
|
1896
1979
|
const host = pickAddress(span);
|
|
@@ -1900,7 +1983,7 @@ async function handleSpan(ctx, span) {
|
|
|
1900
1983
|
const result = upsertObservedEdge(
|
|
1901
1984
|
ctx.graph,
|
|
1902
1985
|
import_types3.EdgeType.CONNECTS_TO,
|
|
1903
|
-
|
|
1986
|
+
observedSource(),
|
|
1904
1987
|
targetId,
|
|
1905
1988
|
ts,
|
|
1906
1989
|
isError
|
|
@@ -1916,7 +1999,7 @@ async function handleSpan(ctx, span) {
|
|
|
1916
1999
|
upsertObservedEdge(
|
|
1917
2000
|
ctx.graph,
|
|
1918
2001
|
import_types3.EdgeType.CALLS,
|
|
1919
|
-
|
|
2002
|
+
observedSource(),
|
|
1920
2003
|
targetId,
|
|
1921
2004
|
ts,
|
|
1922
2005
|
isError
|
|
@@ -1928,7 +2011,7 @@ async function handleSpan(ctx, span) {
|
|
|
1928
2011
|
upsertObservedEdge(
|
|
1929
2012
|
ctx.graph,
|
|
1930
2013
|
import_types3.EdgeType.CALLS,
|
|
1931
|
-
|
|
2014
|
+
observedSource(),
|
|
1932
2015
|
frontierNodeId,
|
|
1933
2016
|
ts,
|
|
1934
2017
|
isError
|
|
@@ -3571,7 +3654,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3571
3654
|
|
|
3572
3655
|
// src/extract/calls/index.ts
|
|
3573
3656
|
init_cjs_shims();
|
|
3574
|
-
var
|
|
3657
|
+
var import_types15 = require("@neat.is/types");
|
|
3575
3658
|
|
|
3576
3659
|
// src/extract/calls/http.ts
|
|
3577
3660
|
init_cjs_shims();
|
|
@@ -3579,12 +3662,13 @@ var import_node_path20 = __toESM(require("path"), 1);
|
|
|
3579
3662
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3580
3663
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3581
3664
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
3582
|
-
var
|
|
3665
|
+
var import_types10 = require("@neat.is/types");
|
|
3583
3666
|
|
|
3584
3667
|
// src/extract/calls/shared.ts
|
|
3585
3668
|
init_cjs_shims();
|
|
3586
3669
|
var import_node_fs13 = require("fs");
|
|
3587
3670
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
3671
|
+
var import_types9 = require("@neat.is/types");
|
|
3588
3672
|
async function walkSourceFiles(dir) {
|
|
3589
3673
|
const out = [];
|
|
3590
3674
|
async function walk(current) {
|
|
@@ -3624,6 +3708,58 @@ function snippet(text, line) {
|
|
|
3624
3708
|
const lines = text.split("\n");
|
|
3625
3709
|
return (lines[line - 1] ?? "").trim();
|
|
3626
3710
|
}
|
|
3711
|
+
function toPosix2(p) {
|
|
3712
|
+
return p.split("\\").join("/");
|
|
3713
|
+
}
|
|
3714
|
+
function languageForPath(relPath) {
|
|
3715
|
+
switch (import_node_path19.default.extname(relPath).toLowerCase()) {
|
|
3716
|
+
case ".py":
|
|
3717
|
+
return "python";
|
|
3718
|
+
case ".ts":
|
|
3719
|
+
case ".tsx":
|
|
3720
|
+
return "typescript";
|
|
3721
|
+
case ".js":
|
|
3722
|
+
case ".jsx":
|
|
3723
|
+
case ".mjs":
|
|
3724
|
+
case ".cjs":
|
|
3725
|
+
return "javascript";
|
|
3726
|
+
default:
|
|
3727
|
+
return void 0;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
3731
|
+
let nodesAdded = 0;
|
|
3732
|
+
let edgesAdded = 0;
|
|
3733
|
+
const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
|
|
3734
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
3735
|
+
const language = languageForPath(relPath);
|
|
3736
|
+
const node = {
|
|
3737
|
+
id: fileNodeId,
|
|
3738
|
+
type: import_types9.NodeType.FileNode,
|
|
3739
|
+
service: serviceName,
|
|
3740
|
+
path: relPath,
|
|
3741
|
+
...language ? { language } : {},
|
|
3742
|
+
discoveredVia: "static"
|
|
3743
|
+
};
|
|
3744
|
+
graph.addNode(fileNodeId, node);
|
|
3745
|
+
nodesAdded++;
|
|
3746
|
+
}
|
|
3747
|
+
const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
|
|
3748
|
+
if (!graph.hasEdge(containsId)) {
|
|
3749
|
+
const edge = {
|
|
3750
|
+
id: containsId,
|
|
3751
|
+
source: serviceNodeId,
|
|
3752
|
+
target: fileNodeId,
|
|
3753
|
+
type: import_types9.EdgeType.CONTAINS,
|
|
3754
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
3755
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
3756
|
+
evidence: { file: relPath }
|
|
3757
|
+
};
|
|
3758
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
3759
|
+
edgesAdded++;
|
|
3760
|
+
}
|
|
3761
|
+
return { fileNodeId, nodesAdded, edgesAdded };
|
|
3762
|
+
}
|
|
3627
3763
|
|
|
3628
3764
|
// src/extract/calls/http.ts
|
|
3629
3765
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
@@ -3657,16 +3793,16 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3657
3793
|
const tree = parser.parse(source);
|
|
3658
3794
|
const literals = [];
|
|
3659
3795
|
collectStringLiterals(tree.rootNode, literals);
|
|
3660
|
-
const
|
|
3796
|
+
const out = [];
|
|
3661
3797
|
for (const lit of literals) {
|
|
3662
3798
|
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3663
3799
|
for (const host of knownHosts) {
|
|
3664
3800
|
if (urlMatchesHost(lit.text, host)) {
|
|
3665
|
-
|
|
3801
|
+
out.push({ host, line: lit.node.startPosition.row + 1 });
|
|
3666
3802
|
}
|
|
3667
3803
|
}
|
|
3668
3804
|
}
|
|
3669
|
-
return
|
|
3805
|
+
return out;
|
|
3670
3806
|
}
|
|
3671
3807
|
function makeJsParser() {
|
|
3672
3808
|
const p = new import_tree_sitter.default();
|
|
@@ -3689,71 +3825,78 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3689
3825
|
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3690
3826
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3691
3827
|
}
|
|
3828
|
+
let nodesAdded = 0;
|
|
3692
3829
|
let edgesAdded = 0;
|
|
3693
3830
|
for (const service of services) {
|
|
3694
3831
|
const files = await loadSourceFiles(service.dir);
|
|
3695
|
-
const
|
|
3832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3696
3833
|
for (const file of files) {
|
|
3697
3834
|
if (isTestPath(file.path)) continue;
|
|
3698
3835
|
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3699
|
-
let
|
|
3836
|
+
let sites;
|
|
3700
3837
|
try {
|
|
3701
|
-
|
|
3838
|
+
sites = callsFromSource(file.content, parser, knownHosts);
|
|
3702
3839
|
} catch (err) {
|
|
3703
3840
|
recordExtractionError("http call extraction", file.path, err);
|
|
3704
3841
|
continue;
|
|
3705
3842
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3843
|
+
if (sites.length === 0) continue;
|
|
3844
|
+
const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
|
|
3845
|
+
for (const site of sites) {
|
|
3846
|
+
const targetId = hostToNodeId.get(site.host);
|
|
3708
3847
|
if (!targetId || targetId === service.node.id) continue;
|
|
3709
|
-
|
|
3710
|
-
|
|
3848
|
+
const dedupKey = `${relFile}|${targetId}`;
|
|
3849
|
+
if (seen.has(dedupKey)) continue;
|
|
3850
|
+
seen.add(dedupKey);
|
|
3851
|
+
const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
|
|
3852
|
+
const ev = {
|
|
3853
|
+
file: relFile,
|
|
3854
|
+
line: site.line,
|
|
3855
|
+
snippet: snippet(file.content, site.line)
|
|
3856
|
+
};
|
|
3857
|
+
if (!(0, import_types10.passesExtractedFloor)(confidence)) {
|
|
3858
|
+
noteExtractedDropped({
|
|
3859
|
+
source: service.node.id,
|
|
3860
|
+
target: targetId,
|
|
3861
|
+
type: import_types10.EdgeType.CALLS,
|
|
3862
|
+
confidence,
|
|
3863
|
+
confidenceKind: "hostname-shape-match",
|
|
3864
|
+
evidence: ev
|
|
3865
|
+
});
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
3869
|
+
graph,
|
|
3870
|
+
service.pkg.name,
|
|
3871
|
+
service.node.id,
|
|
3872
|
+
relFile
|
|
3873
|
+
);
|
|
3874
|
+
nodesAdded += n;
|
|
3875
|
+
edgesAdded += e;
|
|
3876
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
|
|
3877
|
+
if (!graph.hasEdge(edgeId)) {
|
|
3878
|
+
const edge = {
|
|
3879
|
+
id: edgeId,
|
|
3880
|
+
source: fileNodeId,
|
|
3881
|
+
target: targetId,
|
|
3882
|
+
type: import_types10.EdgeType.CALLS,
|
|
3883
|
+
provenance: import_types10.Provenance.EXTRACTED,
|
|
3884
|
+
confidence,
|
|
3885
|
+
evidence: ev
|
|
3886
|
+
};
|
|
3887
|
+
graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
|
|
3888
|
+
edgesAdded++;
|
|
3711
3889
|
}
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3715
|
-
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3716
|
-
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3717
|
-
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3718
|
-
const ev = {
|
|
3719
|
-
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3720
|
-
line,
|
|
3721
|
-
snippet: snippet(fileContent, line)
|
|
3722
|
-
};
|
|
3723
|
-
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3724
|
-
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3725
|
-
noteExtractedDropped({
|
|
3726
|
-
source: service.node.id,
|
|
3727
|
-
target: targetId,
|
|
3728
|
-
type: import_types9.EdgeType.CALLS,
|
|
3729
|
-
confidence,
|
|
3730
|
-
confidenceKind: "hostname-shape-match",
|
|
3731
|
-
evidence: ev
|
|
3732
|
-
});
|
|
3733
|
-
continue;
|
|
3734
|
-
}
|
|
3735
|
-
const edge = {
|
|
3736
|
-
id: edgeId,
|
|
3737
|
-
source: service.node.id,
|
|
3738
|
-
target: targetId,
|
|
3739
|
-
type: import_types9.EdgeType.CALLS,
|
|
3740
|
-
provenance: import_types9.Provenance.EXTRACTED,
|
|
3741
|
-
confidence,
|
|
3742
|
-
evidence: ev
|
|
3743
|
-
};
|
|
3744
|
-
if (!graph.hasEdge(edge.id)) {
|
|
3745
|
-
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
3746
|
-
edgesAdded++;
|
|
3747
3890
|
}
|
|
3748
3891
|
}
|
|
3749
3892
|
}
|
|
3750
|
-
return edgesAdded;
|
|
3893
|
+
return { nodesAdded, edgesAdded };
|
|
3751
3894
|
}
|
|
3752
3895
|
|
|
3753
3896
|
// src/extract/calls/kafka.ts
|
|
3754
3897
|
init_cjs_shims();
|
|
3755
3898
|
var import_node_path21 = __toESM(require("path"), 1);
|
|
3756
|
-
var
|
|
3899
|
+
var import_types11 = require("@neat.is/types");
|
|
3757
3900
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3758
3901
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
3759
3902
|
function findAll(re, text) {
|
|
@@ -3774,7 +3917,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3774
3917
|
seen.add(key);
|
|
3775
3918
|
const line = lineOf(file.content, topic);
|
|
3776
3919
|
out.push({
|
|
3777
|
-
infraId: (0,
|
|
3920
|
+
infraId: (0, import_types11.infraId)("kafka-topic", topic),
|
|
3778
3921
|
name: topic,
|
|
3779
3922
|
kind: "kafka-topic",
|
|
3780
3923
|
edgeType,
|
|
@@ -3797,7 +3940,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3797
3940
|
// src/extract/calls/redis.ts
|
|
3798
3941
|
init_cjs_shims();
|
|
3799
3942
|
var import_node_path22 = __toESM(require("path"), 1);
|
|
3800
|
-
var
|
|
3943
|
+
var import_types12 = require("@neat.is/types");
|
|
3801
3944
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3802
3945
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
3803
3946
|
const out = [];
|
|
@@ -3810,7 +3953,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3810
3953
|
seen.add(host);
|
|
3811
3954
|
const line = lineOf(file.content, host);
|
|
3812
3955
|
out.push({
|
|
3813
|
-
infraId: (0,
|
|
3956
|
+
infraId: (0, import_types12.infraId)("redis", host),
|
|
3814
3957
|
name: host,
|
|
3815
3958
|
kind: "redis",
|
|
3816
3959
|
edgeType: "CALLS",
|
|
@@ -3831,7 +3974,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3831
3974
|
// src/extract/calls/aws.ts
|
|
3832
3975
|
init_cjs_shims();
|
|
3833
3976
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
3834
|
-
var
|
|
3977
|
+
var import_types13 = require("@neat.is/types");
|
|
3835
3978
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3836
3979
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3837
3980
|
function hasMarker(text, markers) {
|
|
@@ -3855,7 +3998,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3855
3998
|
seen.add(key);
|
|
3856
3999
|
const line = lineOf(file.content, name);
|
|
3857
4000
|
out.push({
|
|
3858
|
-
infraId: (0,
|
|
4001
|
+
infraId: (0, import_types13.infraId)(kind, name),
|
|
3859
4002
|
name,
|
|
3860
4003
|
kind,
|
|
3861
4004
|
edgeType: "CALLS",
|
|
@@ -3890,7 +4033,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3890
4033
|
// src/extract/calls/grpc.ts
|
|
3891
4034
|
init_cjs_shims();
|
|
3892
4035
|
var import_node_path24 = __toESM(require("path"), 1);
|
|
3893
|
-
var
|
|
4036
|
+
var import_types14 = require("@neat.is/types");
|
|
3894
4037
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3895
4038
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3896
4039
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
@@ -3938,7 +4081,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3938
4081
|
const { kind } = classified;
|
|
3939
4082
|
const line = lineOf(file.content, m[0]);
|
|
3940
4083
|
out.push({
|
|
3941
|
-
infraId: (0,
|
|
4084
|
+
infraId: (0, import_types14.infraId)(kind, name),
|
|
3942
4085
|
name,
|
|
3943
4086
|
kind,
|
|
3944
4087
|
edgeType: "CALLS",
|
|
@@ -3960,11 +4103,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3960
4103
|
function edgeTypeFromEndpoint(ep) {
|
|
3961
4104
|
switch (ep.edgeType) {
|
|
3962
4105
|
case "PUBLISHES_TO":
|
|
3963
|
-
return
|
|
4106
|
+
return import_types15.EdgeType.PUBLISHES_TO;
|
|
3964
4107
|
case "CONSUMES_FROM":
|
|
3965
|
-
return
|
|
4108
|
+
return import_types15.EdgeType.CONSUMES_FROM;
|
|
3966
4109
|
default:
|
|
3967
|
-
return
|
|
4110
|
+
return import_types15.EdgeType.CALLS;
|
|
3968
4111
|
}
|
|
3969
4112
|
}
|
|
3970
4113
|
function isAwsKind(kind) {
|
|
@@ -3991,7 +4134,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3991
4134
|
if (!graph.hasNode(ep.infraId)) {
|
|
3992
4135
|
const node = {
|
|
3993
4136
|
id: ep.infraId,
|
|
3994
|
-
type:
|
|
4137
|
+
type: import_types15.NodeType.InfraNode,
|
|
3995
4138
|
name: ep.name,
|
|
3996
4139
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3997
4140
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -4003,11 +4146,8 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4003
4146
|
nodesAdded++;
|
|
4004
4147
|
}
|
|
4005
4148
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
4006
|
-
const
|
|
4007
|
-
if (
|
|
4008
|
-
seenEdges.add(edgeId);
|
|
4009
|
-
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
4010
|
-
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
4149
|
+
const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
|
|
4150
|
+
if (!(0, import_types15.passesExtractedFloor)(confidence)) {
|
|
4011
4151
|
noteExtractedDropped({
|
|
4012
4152
|
source: service.node.id,
|
|
4013
4153
|
target: ep.infraId,
|
|
@@ -4018,13 +4158,25 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4018
4158
|
});
|
|
4019
4159
|
continue;
|
|
4020
4160
|
}
|
|
4161
|
+
const relFile = toPosix2(ep.evidence.file);
|
|
4162
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
4163
|
+
graph,
|
|
4164
|
+
service.pkg.name,
|
|
4165
|
+
service.node.id,
|
|
4166
|
+
relFile
|
|
4167
|
+
);
|
|
4168
|
+
nodesAdded += n;
|
|
4169
|
+
edgesAdded += e;
|
|
4170
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
|
|
4171
|
+
if (seenEdges.has(edgeId)) continue;
|
|
4172
|
+
seenEdges.add(edgeId);
|
|
4021
4173
|
if (!graph.hasEdge(edgeId)) {
|
|
4022
4174
|
const edge = {
|
|
4023
4175
|
id: edgeId,
|
|
4024
|
-
source:
|
|
4176
|
+
source: fileNodeId,
|
|
4025
4177
|
target: ep.infraId,
|
|
4026
4178
|
type: edgeType,
|
|
4027
|
-
provenance:
|
|
4179
|
+
provenance: import_types15.Provenance.EXTRACTED,
|
|
4028
4180
|
confidence,
|
|
4029
4181
|
evidence: ep.evidence
|
|
4030
4182
|
};
|
|
@@ -4036,11 +4188,11 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4036
4188
|
return { nodesAdded, edgesAdded };
|
|
4037
4189
|
}
|
|
4038
4190
|
async function addCallEdges(graph, services) {
|
|
4039
|
-
const
|
|
4191
|
+
const http2 = await addHttpCallEdges(graph, services);
|
|
4040
4192
|
const ext = await addExternalEndpointEdges(graph, services);
|
|
4041
4193
|
return {
|
|
4042
|
-
nodesAdded: ext.nodesAdded,
|
|
4043
|
-
edgesAdded:
|
|
4194
|
+
nodesAdded: http2.nodesAdded + ext.nodesAdded,
|
|
4195
|
+
edgesAdded: http2.edgesAdded + ext.edgesAdded
|
|
4044
4196
|
};
|
|
4045
4197
|
}
|
|
4046
4198
|
|
|
@@ -4050,15 +4202,15 @@ init_cjs_shims();
|
|
|
4050
4202
|
// src/extract/infra/docker-compose.ts
|
|
4051
4203
|
init_cjs_shims();
|
|
4052
4204
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
4053
|
-
var
|
|
4205
|
+
var import_types17 = require("@neat.is/types");
|
|
4054
4206
|
|
|
4055
4207
|
// src/extract/infra/shared.ts
|
|
4056
4208
|
init_cjs_shims();
|
|
4057
|
-
var
|
|
4209
|
+
var import_types16 = require("@neat.is/types");
|
|
4058
4210
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
4059
4211
|
return {
|
|
4060
|
-
id: (0,
|
|
4061
|
-
type:
|
|
4212
|
+
id: (0, import_types16.infraId)(kind, name),
|
|
4213
|
+
type: import_types16.NodeType.InfraNode,
|
|
4062
4214
|
name,
|
|
4063
4215
|
provider,
|
|
4064
4216
|
kind,
|
|
@@ -4137,15 +4289,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4137
4289
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
4138
4290
|
const targetId = composeNameToNodeId.get(dep);
|
|
4139
4291
|
if (!targetId) continue;
|
|
4140
|
-
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId,
|
|
4292
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
|
|
4141
4293
|
if (graph.hasEdge(edgeId)) continue;
|
|
4142
4294
|
const edge = {
|
|
4143
4295
|
id: edgeId,
|
|
4144
4296
|
source: sourceId,
|
|
4145
4297
|
target: targetId,
|
|
4146
|
-
type:
|
|
4147
|
-
provenance:
|
|
4148
|
-
confidence: (0,
|
|
4298
|
+
type: import_types17.EdgeType.DEPENDS_ON,
|
|
4299
|
+
provenance: import_types17.Provenance.EXTRACTED,
|
|
4300
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
4149
4301
|
evidence: { file: evidenceFile }
|
|
4150
4302
|
};
|
|
4151
4303
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4159,7 +4311,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4159
4311
|
init_cjs_shims();
|
|
4160
4312
|
var import_node_path26 = __toESM(require("path"), 1);
|
|
4161
4313
|
var import_node_fs14 = require("fs");
|
|
4162
|
-
var
|
|
4314
|
+
var import_types18 = require("@neat.is/types");
|
|
4163
4315
|
function runtimeImage(content) {
|
|
4164
4316
|
const lines = content.split("\n");
|
|
4165
4317
|
let last = null;
|
|
@@ -4198,15 +4350,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4198
4350
|
graph.addNode(node.id, node);
|
|
4199
4351
|
nodesAdded++;
|
|
4200
4352
|
}
|
|
4201
|
-
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id,
|
|
4353
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
|
|
4202
4354
|
if (!graph.hasEdge(edgeId)) {
|
|
4203
4355
|
const edge = {
|
|
4204
4356
|
id: edgeId,
|
|
4205
4357
|
source: service.node.id,
|
|
4206
4358
|
target: node.id,
|
|
4207
|
-
type:
|
|
4208
|
-
provenance:
|
|
4209
|
-
confidence: (0,
|
|
4359
|
+
type: import_types18.EdgeType.RUNS_ON,
|
|
4360
|
+
provenance: import_types18.Provenance.EXTRACTED,
|
|
4361
|
+
confidence: (0, import_types18.confidenceForExtracted)("structural"),
|
|
4210
4362
|
evidence: {
|
|
4211
4363
|
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
4212
4364
|
}
|
|
@@ -4334,17 +4486,29 @@ var import_node_path30 = __toESM(require("path"), 1);
|
|
|
4334
4486
|
init_cjs_shims();
|
|
4335
4487
|
var import_node_fs17 = require("fs");
|
|
4336
4488
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
4337
|
-
var
|
|
4489
|
+
var import_types19 = require("@neat.is/types");
|
|
4490
|
+
function dropOrphanedFileNodes(graph) {
|
|
4491
|
+
const orphans = [];
|
|
4492
|
+
graph.forEachNode((id, attrs) => {
|
|
4493
|
+
if (attrs.type !== import_types19.NodeType.FileNode) return;
|
|
4494
|
+
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
4495
|
+
orphans.push(id);
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
for (const id of orphans) graph.dropNode(id);
|
|
4499
|
+
return orphans.length;
|
|
4500
|
+
}
|
|
4338
4501
|
function retireEdgesByFile(graph, file) {
|
|
4339
4502
|
const normalized = file.split("\\").join("/");
|
|
4340
4503
|
const toDrop = [];
|
|
4341
4504
|
graph.forEachEdge((id, attrs) => {
|
|
4342
4505
|
const edge = attrs;
|
|
4343
|
-
if (edge.provenance !==
|
|
4506
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4344
4507
|
if (!edge.evidence?.file) return;
|
|
4345
4508
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4346
4509
|
});
|
|
4347
4510
|
for (const id of toDrop) graph.dropEdge(id);
|
|
4511
|
+
dropOrphanedFileNodes(graph);
|
|
4348
4512
|
return toDrop.length;
|
|
4349
4513
|
}
|
|
4350
4514
|
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
@@ -4352,7 +4516,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4352
4516
|
const bases = [scanPath, ...serviceDirs];
|
|
4353
4517
|
graph.forEachEdge((id, attrs) => {
|
|
4354
4518
|
const edge = attrs;
|
|
4355
|
-
if (edge.provenance !==
|
|
4519
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4356
4520
|
const evidenceFile = edge.evidence?.file;
|
|
4357
4521
|
if (!evidenceFile) return;
|
|
4358
4522
|
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
@@ -4363,6 +4527,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4363
4527
|
if (!found) toDrop.push(id);
|
|
4364
4528
|
});
|
|
4365
4529
|
for (const id of toDrop) graph.dropEdge(id);
|
|
4530
|
+
dropOrphanedFileNodes(graph);
|
|
4366
4531
|
return toDrop.length;
|
|
4367
4532
|
}
|
|
4368
4533
|
|
|
@@ -4430,7 +4595,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4430
4595
|
|
|
4431
4596
|
// src/divergences.ts
|
|
4432
4597
|
init_cjs_shims();
|
|
4433
|
-
var
|
|
4598
|
+
var import_types20 = require("@neat.is/types");
|
|
4434
4599
|
function bucketKey(source, target, type) {
|
|
4435
4600
|
return `${type}|${source}|${target}`;
|
|
4436
4601
|
}
|
|
@@ -4438,22 +4603,22 @@ function bucketEdges(graph) {
|
|
|
4438
4603
|
const buckets = /* @__PURE__ */ new Map();
|
|
4439
4604
|
graph.forEachEdge((id, attrs) => {
|
|
4440
4605
|
const e = attrs;
|
|
4441
|
-
const parsed = (0,
|
|
4606
|
+
const parsed = (0, import_types20.parseEdgeId)(id);
|
|
4442
4607
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4443
4608
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4444
4609
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4445
4610
|
switch (provenance) {
|
|
4446
|
-
case
|
|
4611
|
+
case import_types20.Provenance.EXTRACTED:
|
|
4447
4612
|
cur.extracted = e;
|
|
4448
4613
|
break;
|
|
4449
|
-
case
|
|
4614
|
+
case import_types20.Provenance.OBSERVED:
|
|
4450
4615
|
cur.observed = e;
|
|
4451
4616
|
break;
|
|
4452
|
-
case
|
|
4617
|
+
case import_types20.Provenance.INFERRED:
|
|
4453
4618
|
cur.inferred = e;
|
|
4454
4619
|
break;
|
|
4455
4620
|
default:
|
|
4456
|
-
if (e.provenance ===
|
|
4621
|
+
if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
|
|
4457
4622
|
}
|
|
4458
4623
|
buckets.set(key, cur);
|
|
4459
4624
|
});
|
|
@@ -4462,7 +4627,7 @@ function bucketEdges(graph) {
|
|
|
4462
4627
|
function nodeIsFrontier(graph, nodeId) {
|
|
4463
4628
|
if (!graph.hasNode(nodeId)) return false;
|
|
4464
4629
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4465
|
-
return attrs.type ===
|
|
4630
|
+
return attrs.type === import_types20.NodeType.FrontierNode;
|
|
4466
4631
|
}
|
|
4467
4632
|
function clampConfidence(n) {
|
|
4468
4633
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4483,6 +4648,7 @@ function gradedConfidence(edge) {
|
|
|
4483
4648
|
}
|
|
4484
4649
|
function detectMissingDivergences(graph, bucket) {
|
|
4485
4650
|
const out = [];
|
|
4651
|
+
if (bucket.type === import_types20.EdgeType.CONTAINS) return out;
|
|
4486
4652
|
if (bucket.extracted && !bucket.observed) {
|
|
4487
4653
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4488
4654
|
out.push({
|
|
@@ -4523,7 +4689,7 @@ function declaredHostFor(svc) {
|
|
|
4523
4689
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4524
4690
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4525
4691
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4526
|
-
if (e.type ===
|
|
4692
|
+
if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
|
|
4527
4693
|
return true;
|
|
4528
4694
|
}
|
|
4529
4695
|
}
|
|
@@ -4536,10 +4702,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4536
4702
|
const out = [];
|
|
4537
4703
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4538
4704
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4539
|
-
if (edge.type !==
|
|
4540
|
-
if (edge.provenance !==
|
|
4705
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4706
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4541
4707
|
const target = graph.getNodeAttributes(edge.target);
|
|
4542
|
-
if (target.type !==
|
|
4708
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4543
4709
|
const observedHost = target.host?.trim();
|
|
4544
4710
|
if (!observedHost) continue;
|
|
4545
4711
|
if (observedHost === declaredHost) continue;
|
|
@@ -4561,10 +4727,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4561
4727
|
const deps = svc.dependencies ?? {};
|
|
4562
4728
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4563
4729
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4564
|
-
if (edge.type !==
|
|
4565
|
-
if (edge.provenance !==
|
|
4730
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4731
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4566
4732
|
const target = graph.getNodeAttributes(edge.target);
|
|
4567
|
-
if (target.type !==
|
|
4733
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4568
4734
|
for (const pair of compatPairs()) {
|
|
4569
4735
|
if (pair.engine !== target.engine) continue;
|
|
4570
4736
|
const declared = deps[pair.driver];
|
|
@@ -4625,7 +4791,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4625
4791
|
}
|
|
4626
4792
|
graph.forEachNode((nodeId, attrs) => {
|
|
4627
4793
|
const n = attrs;
|
|
4628
|
-
if (n.type !==
|
|
4794
|
+
if (n.type !== import_types20.NodeType.ServiceNode) return;
|
|
4629
4795
|
const svc = n;
|
|
4630
4796
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4631
4797
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4658,7 +4824,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4658
4824
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4659
4825
|
return a.target.localeCompare(b.target);
|
|
4660
4826
|
});
|
|
4661
|
-
return
|
|
4827
|
+
return import_types20.DivergenceResultSchema.parse({
|
|
4662
4828
|
divergences: filtered,
|
|
4663
4829
|
totalAffected: filtered.length,
|
|
4664
4830
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4669,7 +4835,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4669
4835
|
init_cjs_shims();
|
|
4670
4836
|
var import_node_fs18 = require("fs");
|
|
4671
4837
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4672
|
-
var
|
|
4838
|
+
var import_types21 = require("@neat.is/types");
|
|
4673
4839
|
var SCHEMA_VERSION = 4;
|
|
4674
4840
|
function migrateV1ToV2(payload) {
|
|
4675
4841
|
const nodes = payload.graph.nodes;
|
|
@@ -4691,12 +4857,12 @@ function migrateV2ToV3(payload) {
|
|
|
4691
4857
|
for (const edge of edges) {
|
|
4692
4858
|
const attrs = edge.attributes;
|
|
4693
4859
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4694
|
-
attrs.provenance =
|
|
4860
|
+
attrs.provenance = import_types21.Provenance.OBSERVED;
|
|
4695
4861
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4696
4862
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4697
4863
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4698
4864
|
if (type && source && target) {
|
|
4699
|
-
const newId = (0,
|
|
4865
|
+
const newId = (0, import_types21.observedEdgeId)(source, target, type);
|
|
4700
4866
|
attrs.id = newId;
|
|
4701
4867
|
if (edge.key) edge.key = newId;
|
|
4702
4868
|
}
|
|
@@ -4816,7 +4982,7 @@ ${NEAT_OUT_LINE}
|
|
|
4816
4982
|
|
|
4817
4983
|
// src/summary.ts
|
|
4818
4984
|
init_cjs_shims();
|
|
4819
|
-
var
|
|
4985
|
+
var import_types22 = require("@neat.is/types");
|
|
4820
4986
|
function renderOtelEnvBlock() {
|
|
4821
4987
|
return [
|
|
4822
4988
|
"for prod OTel routing, set these in your deploy platform's env:",
|
|
@@ -4826,19 +4992,19 @@ function renderOtelEnvBlock() {
|
|
|
4826
4992
|
}
|
|
4827
4993
|
function findIncompatServices(nodes) {
|
|
4828
4994
|
return nodes.filter(
|
|
4829
|
-
(n) => n.type ===
|
|
4995
|
+
(n) => n.type === import_types22.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
4830
4996
|
);
|
|
4831
4997
|
}
|
|
4832
4998
|
function servicesWithoutObserved(nodes, edges) {
|
|
4833
4999
|
const seen = /* @__PURE__ */ new Set();
|
|
4834
5000
|
for (const e of edges) {
|
|
4835
|
-
if (e.provenance ===
|
|
5001
|
+
if (e.provenance === import_types22.Provenance.OBSERVED) {
|
|
4836
5002
|
seen.add(e.source);
|
|
4837
5003
|
seen.add(e.target);
|
|
4838
5004
|
}
|
|
4839
5005
|
}
|
|
4840
5006
|
return nodes.filter(
|
|
4841
|
-
(n) => n.type ===
|
|
5007
|
+
(n) => n.type === import_types22.NodeType.ServiceNode && !seen.has(n.id)
|
|
4842
5008
|
);
|
|
4843
5009
|
}
|
|
4844
5010
|
function formatDivergence(d) {
|
|
@@ -4920,7 +5086,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
4920
5086
|
init_cjs_shims();
|
|
4921
5087
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
4922
5088
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4923
|
-
var
|
|
5089
|
+
var import_types24 = require("@neat.is/types");
|
|
4924
5090
|
|
|
4925
5091
|
// src/diff.ts
|
|
4926
5092
|
init_cjs_shims();
|
|
@@ -5056,7 +5222,7 @@ init_cjs_shims();
|
|
|
5056
5222
|
var import_node_fs21 = require("fs");
|
|
5057
5223
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
5058
5224
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5059
|
-
var
|
|
5225
|
+
var import_types23 = require("@neat.is/types");
|
|
5060
5226
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5061
5227
|
var LOCK_RETRY_MS = 50;
|
|
5062
5228
|
function neatHome() {
|
|
@@ -5135,10 +5301,10 @@ async function readRegistry() {
|
|
|
5135
5301
|
throw err;
|
|
5136
5302
|
}
|
|
5137
5303
|
const parsed = JSON.parse(raw);
|
|
5138
|
-
return
|
|
5304
|
+
return import_types23.RegistryFileSchema.parse(parsed);
|
|
5139
5305
|
}
|
|
5140
5306
|
async function writeRegistry(reg) {
|
|
5141
|
-
const validated =
|
|
5307
|
+
const validated = import_types23.RegistryFileSchema.parse(reg);
|
|
5142
5308
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
5143
5309
|
}
|
|
5144
5310
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -5401,11 +5567,11 @@ function registerRoutes(scope, ctx) {
|
|
|
5401
5567
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5402
5568
|
const parsed = [];
|
|
5403
5569
|
for (const c of candidates) {
|
|
5404
|
-
const r =
|
|
5570
|
+
const r = import_types24.DivergenceTypeSchema.safeParse(c);
|
|
5405
5571
|
if (!r.success) {
|
|
5406
5572
|
return reply.code(400).send({
|
|
5407
5573
|
error: `unknown divergence type "${c}"`,
|
|
5408
|
-
allowed:
|
|
5574
|
+
allowed: import_types24.DivergenceTypeSchema.options
|
|
5409
5575
|
});
|
|
5410
5576
|
}
|
|
5411
5577
|
parsed.push(r.data);
|
|
@@ -5618,7 +5784,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5618
5784
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5619
5785
|
let violations = await log.readAll();
|
|
5620
5786
|
if (req.query.severity) {
|
|
5621
|
-
const sev =
|
|
5787
|
+
const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5622
5788
|
if (!sev.success) {
|
|
5623
5789
|
return reply.code(400).send({
|
|
5624
5790
|
error: "invalid severity",
|
|
@@ -5635,7 +5801,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5635
5801
|
scope.post("/policies/check", async (req, reply) => {
|
|
5636
5802
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5637
5803
|
if (!proj) return;
|
|
5638
|
-
const parsed =
|
|
5804
|
+
const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5639
5805
|
if (!parsed.success) {
|
|
5640
5806
|
return reply.code(400).send({
|
|
5641
5807
|
error: "invalid /policies/check body",
|
|
@@ -6569,38 +6735,119 @@ var import_node_path40 = __toESM(require("path"), 1);
|
|
|
6569
6735
|
// src/installers/templates.ts
|
|
6570
6736
|
init_cjs_shims();
|
|
6571
6737
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
6738
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
|
|
6739
|
+
function neatCallsiteProcessorSource(ts) {
|
|
6740
|
+
const stackT = ts ? ": string" : "";
|
|
6741
|
+
const spanT = ts ? ": any" : "";
|
|
6742
|
+
const strOpt = ts ? ": string | undefined" : "";
|
|
6743
|
+
return `function __neatPickUserFrame(stack${stackT}) {
|
|
6744
|
+
const lines = String(stack || '').split('\\n')
|
|
6745
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6746
|
+
const raw = lines[i].trim()
|
|
6747
|
+
if (raw.indexOf('at ') !== 0) continue
|
|
6748
|
+
if (raw.indexOf('node_modules') !== -1) continue
|
|
6749
|
+
if (raw.indexOf('@opentelemetry') !== -1) continue
|
|
6750
|
+
if (raw.indexOf('node:') !== -1) continue
|
|
6751
|
+
if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
|
|
6752
|
+
const bodyText = raw.slice(3)
|
|
6753
|
+
const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
|
|
6754
|
+
if (!loc) continue
|
|
6755
|
+
const paren = bodyText.lastIndexOf('(')
|
|
6756
|
+
let filepath${strOpt}
|
|
6757
|
+
let fn${strOpt}
|
|
6758
|
+
if (paren !== -1) {
|
|
6759
|
+
fn = bodyText.slice(0, paren).trim()
|
|
6760
|
+
if (fn.indexOf('async ') === 0) fn = fn.slice(6)
|
|
6761
|
+
if (fn.indexOf('new ') === 0) fn = fn.slice(4)
|
|
6762
|
+
filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
|
|
6763
|
+
} else {
|
|
6764
|
+
filepath = bodyText.slice(0, bodyText.length - loc[0].length)
|
|
6765
|
+
}
|
|
6766
|
+
if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
|
|
6767
|
+
if (!filepath) continue
|
|
6768
|
+
return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
|
|
6769
|
+
}
|
|
6770
|
+
return null
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
class NeatCallSiteSpanProcessor {
|
|
6774
|
+
onStart(span${spanT}) {
|
|
6775
|
+
if (!span || (span.kind !== 2 && span.kind !== 3)) return
|
|
6776
|
+
const frame = __neatPickUserFrame(new Error().stack)
|
|
6777
|
+
if (!frame) return
|
|
6778
|
+
span.setAttribute('code.filepath', frame.filepath)
|
|
6779
|
+
if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
|
|
6780
|
+
if (frame.function) span.setAttribute('code.function', frame.function)
|
|
6781
|
+
}
|
|
6782
|
+
onEnd() {}
|
|
6783
|
+
forceFlush() { return Promise.resolve() }
|
|
6784
|
+
shutdown() { return Promise.resolve() }
|
|
6785
|
+
}`;
|
|
6786
|
+
}
|
|
6787
|
+
var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
|
|
6788
|
+
var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
|
|
6789
|
+
function neatRegisterCallsiteSource(ts) {
|
|
6790
|
+
const providerT = ts ? ": any" : "";
|
|
6791
|
+
return `try {
|
|
6792
|
+
const provider${providerT} = trace.getTracerProvider()
|
|
6793
|
+
const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
|
|
6794
|
+
if (delegate && typeof delegate.addSpanProcessor === 'function') {
|
|
6795
|
+
delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
|
|
6796
|
+
}
|
|
6797
|
+
} catch {
|
|
6798
|
+
// capture is best-effort; span export is unaffected
|
|
6799
|
+
}`;
|
|
6800
|
+
}
|
|
6572
6801
|
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
6802
|
+
${OTEL_INIT_STAMP}
|
|
6573
6803
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6574
6804
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6575
6805
|
|
|
6576
6806
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
6577
6807
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
6808
|
+
const { trace } = require('@opentelemetry/api')
|
|
6809
|
+
|
|
6810
|
+
${CALLSITE_PROCESSOR_JS}
|
|
6578
6811
|
|
|
6579
6812
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6580
6813
|
__INSTRUMENTATION_BLOCK__
|
|
6581
|
-
new NodeSDK({ instrumentations })
|
|
6814
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6815
|
+
sdk.start()
|
|
6816
|
+
${neatRegisterCallsiteSource(false)}
|
|
6582
6817
|
`;
|
|
6583
6818
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
6819
|
+
${OTEL_INIT_STAMP}
|
|
6584
6820
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6585
6821
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6586
6822
|
|
|
6587
6823
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6588
6824
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6825
|
+
import { trace } from '@opentelemetry/api'
|
|
6826
|
+
|
|
6827
|
+
${CALLSITE_PROCESSOR_JS}
|
|
6589
6828
|
|
|
6590
6829
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6591
6830
|
__INSTRUMENTATION_BLOCK__
|
|
6592
|
-
new NodeSDK({ instrumentations })
|
|
6831
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6832
|
+
sdk.start()
|
|
6833
|
+
${neatRegisterCallsiteSource(false)}
|
|
6593
6834
|
`;
|
|
6594
6835
|
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
6836
|
+
${OTEL_INIT_STAMP}
|
|
6595
6837
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6596
6838
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6597
6839
|
|
|
6598
6840
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6599
6841
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6842
|
+
import { trace } from '@opentelemetry/api'
|
|
6843
|
+
|
|
6844
|
+
${CALLSITE_PROCESSOR_TS}
|
|
6600
6845
|
|
|
6601
6846
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6602
6847
|
__INSTRUMENTATION_BLOCK__
|
|
6603
|
-
new NodeSDK({ instrumentations })
|
|
6848
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6849
|
+
sdk.start()
|
|
6850
|
+
${neatRegisterCallsiteSource(true)}
|
|
6604
6851
|
`;
|
|
6605
6852
|
function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
|
|
6606
6853
|
const block = registrations.length === 0 ? "" : `
|
|
@@ -6753,13 +7000,20 @@ var SDK_PACKAGES = [
|
|
|
6753
7000
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
6754
7001
|
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
6755
7002
|
];
|
|
7003
|
+
function getMajor(versionRange) {
|
|
7004
|
+
if (!versionRange) return 0;
|
|
7005
|
+
const match = versionRange.match(/(\d+)/);
|
|
7006
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
7007
|
+
}
|
|
6756
7008
|
function detectNonBundledInstrumentations(pkg) {
|
|
6757
7009
|
const deps = allDeps(pkg);
|
|
6758
7010
|
const out = [];
|
|
6759
7011
|
if ("@prisma/client" in deps) {
|
|
7012
|
+
const prismaMajor = getMajor(deps["@prisma/client"]);
|
|
7013
|
+
const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
|
|
6760
7014
|
out.push({
|
|
6761
7015
|
pkg: "@prisma/instrumentation",
|
|
6762
|
-
version:
|
|
7016
|
+
version: prismaInstrVersion,
|
|
6763
7017
|
registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
|
|
6764
7018
|
});
|
|
6765
7019
|
}
|
|
@@ -6817,6 +7071,23 @@ async function exists2(p) {
|
|
|
6817
7071
|
return false;
|
|
6818
7072
|
}
|
|
6819
7073
|
}
|
|
7074
|
+
async function readFileMaybe(p) {
|
|
7075
|
+
try {
|
|
7076
|
+
return await import_node_fs25.promises.readFile(p, "utf8");
|
|
7077
|
+
} catch {
|
|
7078
|
+
return null;
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
async function planOtelInitGeneration(file, contents) {
|
|
7082
|
+
const existing = await readFileMaybe(file);
|
|
7083
|
+
if (existing === null) {
|
|
7084
|
+
return { file, contents, skipIfExists: true };
|
|
7085
|
+
}
|
|
7086
|
+
if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
|
|
7087
|
+
return { file, contents, skipIfExists: false };
|
|
7088
|
+
}
|
|
7089
|
+
return null;
|
|
7090
|
+
}
|
|
6820
7091
|
async function detect(serviceDir) {
|
|
6821
7092
|
const pkg = await readPackageJson(serviceDir);
|
|
6822
7093
|
return pkg !== null && typeof pkg.name === "string";
|
|
@@ -7547,18 +7818,11 @@ async function plan(serviceDir, opts) {
|
|
|
7547
7818
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
7548
7819
|
const registrations = nonBundled.map((i) => i.registration);
|
|
7549
7820
|
const generatedFiles = [];
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
svcName,
|
|
7556
|
-
projectName,
|
|
7557
|
-
registrations
|
|
7558
|
-
),
|
|
7559
|
-
skipIfExists: true
|
|
7560
|
-
});
|
|
7561
|
-
}
|
|
7821
|
+
const otelInitGen = await planOtelInitGeneration(
|
|
7822
|
+
otelInitFile,
|
|
7823
|
+
renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
|
|
7824
|
+
);
|
|
7825
|
+
if (otelInitGen) generatedFiles.push(otelInitGen);
|
|
7562
7826
|
if (!await exists2(envNeatFile)) {
|
|
7563
7827
|
generatedFiles.push({
|
|
7564
7828
|
file: envNeatFile,
|
|
@@ -8088,19 +8352,100 @@ function renderPatch(sections) {
|
|
|
8088
8352
|
|
|
8089
8353
|
// src/orchestrator.ts
|
|
8090
8354
|
init_cjs_shims();
|
|
8091
|
-
var
|
|
8355
|
+
var import_node_fs28 = require("fs");
|
|
8092
8356
|
var import_node_http = __toESM(require("http"), 1);
|
|
8093
8357
|
var import_node_net = __toESM(require("net"), 1);
|
|
8358
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
8359
|
+
var import_node_child_process3 = require("child_process");
|
|
8360
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
8361
|
+
|
|
8362
|
+
// src/installers/package-manager.ts
|
|
8363
|
+
init_cjs_shims();
|
|
8364
|
+
var import_node_fs27 = require("fs");
|
|
8094
8365
|
var import_node_path42 = __toESM(require("path"), 1);
|
|
8095
8366
|
var import_node_child_process2 = require("child_process");
|
|
8096
|
-
var
|
|
8367
|
+
var LOCKFILE_PRIORITY = [
|
|
8368
|
+
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
8369
|
+
{ lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
|
|
8370
|
+
{ lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
|
|
8371
|
+
{
|
|
8372
|
+
lockfile: "package-lock.json",
|
|
8373
|
+
pm: "npm",
|
|
8374
|
+
args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
|
|
8375
|
+
}
|
|
8376
|
+
];
|
|
8377
|
+
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
8378
|
+
async function exists4(p) {
|
|
8379
|
+
try {
|
|
8380
|
+
await import_node_fs27.promises.access(p);
|
|
8381
|
+
return true;
|
|
8382
|
+
} catch {
|
|
8383
|
+
return false;
|
|
8384
|
+
}
|
|
8385
|
+
}
|
|
8386
|
+
async function detectPackageManager(serviceDir) {
|
|
8387
|
+
let dir = import_node_path42.default.resolve(serviceDir);
|
|
8388
|
+
const stops = /* @__PURE__ */ new Set();
|
|
8389
|
+
for (let i = 0; i < 64; i++) {
|
|
8390
|
+
if (stops.has(dir)) break;
|
|
8391
|
+
stops.add(dir);
|
|
8392
|
+
for (const candidate of LOCKFILE_PRIORITY) {
|
|
8393
|
+
const lockPath = import_node_path42.default.join(dir, candidate.lockfile);
|
|
8394
|
+
if (await exists4(lockPath)) {
|
|
8395
|
+
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
const parent = import_node_path42.default.dirname(dir);
|
|
8399
|
+
if (parent === dir) break;
|
|
8400
|
+
dir = parent;
|
|
8401
|
+
}
|
|
8402
|
+
return { pm: "npm", cwd: import_node_path42.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
8403
|
+
}
|
|
8404
|
+
async function runPackageManagerInstall(cmd) {
|
|
8405
|
+
return new Promise((resolve) => {
|
|
8406
|
+
const child = (0, import_node_child_process2.spawn)(cmd.pm, cmd.args, {
|
|
8407
|
+
cwd: cmd.cwd,
|
|
8408
|
+
// Inherit PATH + HOME so the user's installed managers resolve.
|
|
8409
|
+
env: process.env,
|
|
8410
|
+
// `false` keeps the parent in control of cleanup if the orchestrator
|
|
8411
|
+
// exits before install finishes. Cross-platform-safe.
|
|
8412
|
+
shell: false,
|
|
8413
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
8414
|
+
});
|
|
8415
|
+
let stderr = "";
|
|
8416
|
+
child.stderr?.on("data", (chunk) => {
|
|
8417
|
+
stderr += chunk.toString("utf8");
|
|
8418
|
+
});
|
|
8419
|
+
child.on("error", (err) => {
|
|
8420
|
+
resolve({
|
|
8421
|
+
pm: cmd.pm,
|
|
8422
|
+
cwd: cmd.cwd,
|
|
8423
|
+
args: cmd.args,
|
|
8424
|
+
exitCode: 127,
|
|
8425
|
+
stderr: stderr + `
|
|
8426
|
+
${err.message}`
|
|
8427
|
+
});
|
|
8428
|
+
});
|
|
8429
|
+
child.on("close", (code) => {
|
|
8430
|
+
resolve({
|
|
8431
|
+
pm: cmd.pm,
|
|
8432
|
+
cwd: cmd.cwd,
|
|
8433
|
+
args: cmd.args,
|
|
8434
|
+
exitCode: code ?? 1,
|
|
8435
|
+
stderr: stderr.trim()
|
|
8436
|
+
});
|
|
8437
|
+
});
|
|
8438
|
+
});
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
// src/orchestrator.ts
|
|
8097
8442
|
async function extractAndPersist(opts) {
|
|
8098
8443
|
const services = await discoverServices(opts.scanPath);
|
|
8099
8444
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
8100
8445
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
8101
8446
|
resetGraph(graphKey);
|
|
8102
8447
|
const graph = getGraph(graphKey);
|
|
8103
|
-
const projectPaths = pathsForProject(graphKey,
|
|
8448
|
+
const projectPaths = pathsForProject(graphKey, import_node_path43.default.join(opts.scanPath, "neat-out"));
|
|
8104
8449
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
8105
8450
|
errorsPath: projectPaths.errorsPath
|
|
8106
8451
|
});
|
|
@@ -8118,12 +8463,15 @@ async function extractAndPersist(opts) {
|
|
|
8118
8463
|
errorsPath: projectPaths.errorsPath
|
|
8119
8464
|
};
|
|
8120
8465
|
}
|
|
8121
|
-
async function applyInstallersOver(services, project) {
|
|
8466
|
+
async function applyInstallersOver(services, project, options = {}) {
|
|
8467
|
+
const resolveManager = options.resolveManager ?? detectPackageManager;
|
|
8468
|
+
const runInstall = options.runInstall ?? runPackageManagerInstall;
|
|
8122
8469
|
let instrumented = 0;
|
|
8123
8470
|
let already = 0;
|
|
8124
8471
|
let libOnly = 0;
|
|
8125
8472
|
let browserBundle = 0;
|
|
8126
8473
|
let reactNative = 0;
|
|
8474
|
+
const installPlans = /* @__PURE__ */ new Map();
|
|
8127
8475
|
for (const svc of services) {
|
|
8128
8476
|
const installer = await pickInstaller(svc.dir);
|
|
8129
8477
|
if (!installer) continue;
|
|
@@ -8133,8 +8481,14 @@ async function applyInstallersOver(services, project) {
|
|
|
8133
8481
|
continue;
|
|
8134
8482
|
}
|
|
8135
8483
|
const outcome = await installer.apply(plan3);
|
|
8136
|
-
if (outcome.outcome === "instrumented")
|
|
8137
|
-
|
|
8484
|
+
if (outcome.outcome === "instrumented") {
|
|
8485
|
+
instrumented++;
|
|
8486
|
+
if (plan3.dependencyEdits.length > 0) {
|
|
8487
|
+
const cmd = await resolveManager(svc.dir);
|
|
8488
|
+
const key = `${cmd.pm}:${cmd.cwd}`;
|
|
8489
|
+
if (!installPlans.has(key)) installPlans.set(key, cmd);
|
|
8490
|
+
}
|
|
8491
|
+
} else if (outcome.outcome === "already-instrumented") already++;
|
|
8138
8492
|
else if (outcome.outcome === "lib-only") libOnly++;
|
|
8139
8493
|
else if (outcome.outcome === "browser-bundle") {
|
|
8140
8494
|
browserBundle++;
|
|
@@ -8144,7 +8498,30 @@ async function applyInstallersOver(services, project) {
|
|
|
8144
8498
|
console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
|
|
8145
8499
|
}
|
|
8146
8500
|
}
|
|
8147
|
-
|
|
8501
|
+
const packageManagerInstalls = [];
|
|
8502
|
+
for (const cmd of installPlans.values()) {
|
|
8503
|
+
console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
|
|
8504
|
+
const result = await runInstall(cmd);
|
|
8505
|
+
packageManagerInstalls.push(result);
|
|
8506
|
+
if (result.exitCode !== 0) {
|
|
8507
|
+
console.error(
|
|
8508
|
+
`neat: ${cmd.pm} install failed in ${cmd.cwd} (exit ${result.exitCode}); run it manually to surface the error.`
|
|
8509
|
+
);
|
|
8510
|
+
if (result.stderr.length > 0) {
|
|
8511
|
+
for (const line of result.stderr.split(/\r?\n/).slice(0, 20)) {
|
|
8512
|
+
console.error(` ${line}`);
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
return {
|
|
8518
|
+
instrumented,
|
|
8519
|
+
alreadyInstrumented: already,
|
|
8520
|
+
libOnly,
|
|
8521
|
+
browserBundle,
|
|
8522
|
+
reactNative,
|
|
8523
|
+
packageManagerInstalls
|
|
8524
|
+
};
|
|
8148
8525
|
}
|
|
8149
8526
|
async function promptYesNo(question) {
|
|
8150
8527
|
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -8280,10 +8657,10 @@ function formatPortCollisionMessage(port) {
|
|
|
8280
8657
|
];
|
|
8281
8658
|
}
|
|
8282
8659
|
function spawnDaemonDetached() {
|
|
8283
|
-
const here =
|
|
8660
|
+
const here = import_node_path43.default.dirname(new URL(importMetaUrl).pathname);
|
|
8284
8661
|
const candidates = [
|
|
8285
|
-
|
|
8286
|
-
|
|
8662
|
+
import_node_path43.default.join(here, "neatd.cjs"),
|
|
8663
|
+
import_node_path43.default.join(here, "neatd.js")
|
|
8287
8664
|
];
|
|
8288
8665
|
let entry2 = null;
|
|
8289
8666
|
const fsSync = require("fs");
|
|
@@ -8303,7 +8680,7 @@ function spawnDaemonDetached() {
|
|
|
8303
8680
|
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
8304
8681
|
env.HOST = "127.0.0.1";
|
|
8305
8682
|
}
|
|
8306
|
-
const child = (0,
|
|
8683
|
+
const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
|
|
8307
8684
|
detached: true,
|
|
8308
8685
|
// stderr inherits the orchestrator's fd so the daemon's
|
|
8309
8686
|
// `BindAuthorityError` message lands in front of the operator instead
|
|
@@ -8320,7 +8697,7 @@ function openBrowser(url) {
|
|
|
8320
8697
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
8321
8698
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
8322
8699
|
try {
|
|
8323
|
-
const child = (0,
|
|
8700
|
+
const child = (0, import_node_child_process3.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
8324
8701
|
child.on("error", () => {
|
|
8325
8702
|
});
|
|
8326
8703
|
child.unref();
|
|
@@ -8341,7 +8718,7 @@ async function runOrchestrator(opts) {
|
|
|
8341
8718
|
browser: "skipped"
|
|
8342
8719
|
}
|
|
8343
8720
|
};
|
|
8344
|
-
const stat = await
|
|
8721
|
+
const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
|
|
8345
8722
|
if (!stat || !stat.isDirectory()) {
|
|
8346
8723
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
8347
8724
|
result.exitCode = 2;
|
|
@@ -8409,6 +8786,10 @@ async function runOrchestrator(opts) {
|
|
|
8409
8786
|
console.log(
|
|
8410
8787
|
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
8411
8788
|
);
|
|
8789
|
+
const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
|
|
8790
|
+
if (failedInstalls.length > 0) {
|
|
8791
|
+
result.exitCode = 1;
|
|
8792
|
+
}
|
|
8412
8793
|
}
|
|
8413
8794
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
8414
8795
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -8480,11 +8861,11 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
8480
8861
|
|
|
8481
8862
|
// src/cli-verbs.ts
|
|
8482
8863
|
init_cjs_shims();
|
|
8483
|
-
var
|
|
8864
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
8484
8865
|
|
|
8485
8866
|
// src/cli-client.ts
|
|
8486
8867
|
init_cjs_shims();
|
|
8487
|
-
var
|
|
8868
|
+
var import_types25 = require("@neat.is/types");
|
|
8488
8869
|
var HttpError = class extends Error {
|
|
8489
8870
|
constructor(status2, message, responseBody = "") {
|
|
8490
8871
|
super(message);
|
|
@@ -8505,10 +8886,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
8505
8886
|
const root = baseUrl.replace(/\/$/, "");
|
|
8506
8887
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
8507
8888
|
return {
|
|
8508
|
-
async get(
|
|
8889
|
+
async get(path46) {
|
|
8509
8890
|
let res;
|
|
8510
8891
|
try {
|
|
8511
|
-
res = await fetch(`${root}${
|
|
8892
|
+
res = await fetch(`${root}${path46}`, {
|
|
8512
8893
|
headers: { ...authHeader }
|
|
8513
8894
|
});
|
|
8514
8895
|
} catch (err) {
|
|
@@ -8520,16 +8901,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
8520
8901
|
const body = await res.text().catch(() => "");
|
|
8521
8902
|
throw new HttpError(
|
|
8522
8903
|
res.status,
|
|
8523
|
-
`${res.status} ${res.statusText} on GET ${
|
|
8904
|
+
`${res.status} ${res.statusText} on GET ${path46}: ${body}`,
|
|
8524
8905
|
body
|
|
8525
8906
|
);
|
|
8526
8907
|
}
|
|
8527
8908
|
return await res.json();
|
|
8528
8909
|
},
|
|
8529
|
-
async post(
|
|
8910
|
+
async post(path46, body) {
|
|
8530
8911
|
let res;
|
|
8531
8912
|
try {
|
|
8532
|
-
res = await fetch(`${root}${
|
|
8913
|
+
res = await fetch(`${root}${path46}`, {
|
|
8533
8914
|
method: "POST",
|
|
8534
8915
|
headers: { "content-type": "application/json", ...authHeader },
|
|
8535
8916
|
body: JSON.stringify(body)
|
|
@@ -8543,7 +8924,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
8543
8924
|
const text = await res.text().catch(() => "");
|
|
8544
8925
|
throw new HttpError(
|
|
8545
8926
|
res.status,
|
|
8546
|
-
`${res.status} ${res.statusText} on POST ${
|
|
8927
|
+
`${res.status} ${res.statusText} on POST ${path46}: ${text}`,
|
|
8547
8928
|
text
|
|
8548
8929
|
);
|
|
8549
8930
|
}
|
|
@@ -8557,12 +8938,12 @@ function projectPath(project, suffix) {
|
|
|
8557
8938
|
}
|
|
8558
8939
|
async function runRootCause(client, input) {
|
|
8559
8940
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
8560
|
-
const
|
|
8941
|
+
const path46 = projectPath(
|
|
8561
8942
|
input.project,
|
|
8562
8943
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
8563
8944
|
);
|
|
8564
8945
|
try {
|
|
8565
|
-
const result = await client.get(
|
|
8946
|
+
const result = await client.get(path46);
|
|
8566
8947
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
8567
8948
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
8568
8949
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -8588,12 +8969,12 @@ async function runRootCause(client, input) {
|
|
|
8588
8969
|
}
|
|
8589
8970
|
async function runBlastRadius(client, input) {
|
|
8590
8971
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
8591
|
-
const
|
|
8972
|
+
const path46 = projectPath(
|
|
8592
8973
|
input.project,
|
|
8593
8974
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
8594
8975
|
);
|
|
8595
8976
|
try {
|
|
8596
|
-
const result = await client.get(
|
|
8977
|
+
const result = await client.get(path46);
|
|
8597
8978
|
if (result.totalAffected === 0) {
|
|
8598
8979
|
return {
|
|
8599
8980
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -8622,17 +9003,17 @@ async function runBlastRadius(client, input) {
|
|
|
8622
9003
|
}
|
|
8623
9004
|
}
|
|
8624
9005
|
function formatBlastEntry(n) {
|
|
8625
|
-
const tag = n.edgeProvenance ===
|
|
9006
|
+
const tag = n.edgeProvenance === import_types25.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
8626
9007
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
8627
9008
|
}
|
|
8628
9009
|
async function runDependencies(client, input) {
|
|
8629
9010
|
const depth = input.depth ?? 3;
|
|
8630
|
-
const
|
|
9011
|
+
const path46 = projectPath(
|
|
8631
9012
|
input.project,
|
|
8632
9013
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
8633
9014
|
);
|
|
8634
9015
|
try {
|
|
8635
|
-
const result = await client.get(
|
|
9016
|
+
const result = await client.get(path46);
|
|
8636
9017
|
if (result.total === 0) {
|
|
8637
9018
|
return {
|
|
8638
9019
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -8668,9 +9049,9 @@ async function runObservedDependencies(client, input) {
|
|
|
8668
9049
|
const edges = await client.get(
|
|
8669
9050
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
8670
9051
|
);
|
|
8671
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
9052
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types25.Provenance.OBSERVED);
|
|
8672
9053
|
if (observed.length === 0) {
|
|
8673
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
9054
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types25.Provenance.EXTRACTED);
|
|
8674
9055
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
8675
9056
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
8676
9057
|
}
|
|
@@ -8678,7 +9059,7 @@ async function runObservedDependencies(client, input) {
|
|
|
8678
9059
|
return {
|
|
8679
9060
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
8680
9061
|
block: blockLines.join("\n"),
|
|
8681
|
-
provenance:
|
|
9062
|
+
provenance: import_types25.Provenance.OBSERVED
|
|
8682
9063
|
};
|
|
8683
9064
|
} catch (err) {
|
|
8684
9065
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -8713,9 +9094,9 @@ function formatDuration(ms) {
|
|
|
8713
9094
|
return `${Math.round(h / 24)}d`;
|
|
8714
9095
|
}
|
|
8715
9096
|
async function runIncidents(client, input) {
|
|
8716
|
-
const
|
|
9097
|
+
const path46 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
8717
9098
|
try {
|
|
8718
|
-
const body = await client.get(
|
|
9099
|
+
const body = await client.get(path46);
|
|
8719
9100
|
const events = body.events;
|
|
8720
9101
|
if (events.length === 0) {
|
|
8721
9102
|
return {
|
|
@@ -8732,7 +9113,7 @@ async function runIncidents(client, input) {
|
|
|
8732
9113
|
return {
|
|
8733
9114
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
8734
9115
|
block: blockLines.join("\n"),
|
|
8735
|
-
provenance:
|
|
9116
|
+
provenance: import_types25.Provenance.OBSERVED
|
|
8736
9117
|
};
|
|
8737
9118
|
} catch (err) {
|
|
8738
9119
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -8841,7 +9222,7 @@ async function runStaleEdges(client, input) {
|
|
|
8841
9222
|
return {
|
|
8842
9223
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
8843
9224
|
block: blockLines.join("\n"),
|
|
8844
|
-
provenance:
|
|
9225
|
+
provenance: import_types25.Provenance.STALE
|
|
8845
9226
|
};
|
|
8846
9227
|
}
|
|
8847
9228
|
async function runPolicies(client, input) {
|
|
@@ -9005,7 +9386,7 @@ async function resolveProjectEntry(opts) {
|
|
|
9005
9386
|
const cwd = opts.cwd ?? process.cwd();
|
|
9006
9387
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
9007
9388
|
for (const entry2 of entries) {
|
|
9008
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
9389
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path44.default.sep}`)) {
|
|
9009
9390
|
return entry2;
|
|
9010
9391
|
}
|
|
9011
9392
|
}
|
|
@@ -9155,7 +9536,7 @@ async function runSync(opts) {
|
|
|
9155
9536
|
}
|
|
9156
9537
|
|
|
9157
9538
|
// src/cli.ts
|
|
9158
|
-
var
|
|
9539
|
+
var import_types26 = require("@neat.is/types");
|
|
9159
9540
|
function usage() {
|
|
9160
9541
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
9161
9542
|
console.log("");
|
|
@@ -9365,14 +9746,14 @@ function assignFlag(out, field, value) {
|
|
|
9365
9746
|
out[field] = value;
|
|
9366
9747
|
}
|
|
9367
9748
|
function readPackageVersion() {
|
|
9368
|
-
const here = typeof __dirname !== "undefined" ? __dirname :
|
|
9749
|
+
const here = typeof __dirname !== "undefined" ? __dirname : import_node_path45.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
|
|
9369
9750
|
const candidates = [
|
|
9370
|
-
|
|
9371
|
-
|
|
9751
|
+
import_node_path45.default.resolve(here, "../package.json"),
|
|
9752
|
+
import_node_path45.default.resolve(here, "../../package.json")
|
|
9372
9753
|
];
|
|
9373
9754
|
for (const candidate of candidates) {
|
|
9374
9755
|
try {
|
|
9375
|
-
const raw = (0,
|
|
9756
|
+
const raw = (0, import_node_fs29.readFileSync)(candidate, "utf8");
|
|
9376
9757
|
const parsed = JSON.parse(raw);
|
|
9377
9758
|
if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
|
|
9378
9759
|
return parsed.version;
|
|
@@ -9436,7 +9817,7 @@ async function buildPatchSections(services, project) {
|
|
|
9436
9817
|
}
|
|
9437
9818
|
async function runInit(opts) {
|
|
9438
9819
|
const written = [];
|
|
9439
|
-
const stat = await
|
|
9820
|
+
const stat = await import_node_fs29.promises.stat(opts.scanPath).catch(() => null);
|
|
9440
9821
|
if (!stat || !stat.isDirectory()) {
|
|
9441
9822
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
9442
9823
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -9445,13 +9826,13 @@ async function runInit(opts) {
|
|
|
9445
9826
|
printDiscoveryReport(opts, services);
|
|
9446
9827
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
9447
9828
|
const patch = renderPatch(sections);
|
|
9448
|
-
const patchPath =
|
|
9829
|
+
const patchPath = import_node_path45.default.join(opts.scanPath, "neat.patch");
|
|
9449
9830
|
if (opts.dryRun) {
|
|
9450
|
-
await
|
|
9831
|
+
await import_node_fs29.promises.writeFile(patchPath, patch, "utf8");
|
|
9451
9832
|
written.push(patchPath);
|
|
9452
9833
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
9453
|
-
const gitignorePath =
|
|
9454
|
-
const gitignoreExists = await
|
|
9834
|
+
const gitignorePath = import_node_path45.default.join(opts.scanPath, ".gitignore");
|
|
9835
|
+
const gitignoreExists = await import_node_fs29.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
9455
9836
|
const verb = gitignoreExists ? "append" : "create";
|
|
9456
9837
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
9457
9838
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -9462,9 +9843,9 @@ async function runInit(opts) {
|
|
|
9462
9843
|
const graph = getGraph(graphKey);
|
|
9463
9844
|
const projectPaths = pathsForProject(
|
|
9464
9845
|
graphKey,
|
|
9465
|
-
|
|
9846
|
+
import_node_path45.default.join(opts.scanPath, "neat-out")
|
|
9466
9847
|
);
|
|
9467
|
-
const errorsPath =
|
|
9848
|
+
const errorsPath = import_node_path45.default.join(import_node_path45.default.dirname(opts.outPath), import_node_path45.default.basename(projectPaths.errorsPath));
|
|
9468
9849
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
9469
9850
|
await saveGraphToDisk(graph, opts.outPath);
|
|
9470
9851
|
written.push(opts.outPath);
|
|
@@ -9543,7 +9924,7 @@ async function runInit(opts) {
|
|
|
9543
9924
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
9544
9925
|
}
|
|
9545
9926
|
} else {
|
|
9546
|
-
await
|
|
9927
|
+
await import_node_fs29.promises.writeFile(patchPath, patch, "utf8");
|
|
9547
9928
|
written.push(patchPath);
|
|
9548
9929
|
}
|
|
9549
9930
|
}
|
|
@@ -9583,9 +9964,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
9583
9964
|
};
|
|
9584
9965
|
function claudeConfigPath() {
|
|
9585
9966
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
9586
|
-
if (override && override.length > 0) return
|
|
9967
|
+
if (override && override.length > 0) return import_node_path45.default.resolve(override);
|
|
9587
9968
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
9588
|
-
return
|
|
9969
|
+
return import_node_path45.default.join(home, ".claude.json");
|
|
9589
9970
|
}
|
|
9590
9971
|
async function runSkill(opts) {
|
|
9591
9972
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -9597,7 +9978,7 @@ async function runSkill(opts) {
|
|
|
9597
9978
|
const target = claudeConfigPath();
|
|
9598
9979
|
let existing = {};
|
|
9599
9980
|
try {
|
|
9600
|
-
existing = JSON.parse(await
|
|
9981
|
+
existing = JSON.parse(await import_node_fs29.promises.readFile(target, "utf8"));
|
|
9601
9982
|
} catch (err) {
|
|
9602
9983
|
if (err.code !== "ENOENT") {
|
|
9603
9984
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -9609,8 +9990,8 @@ async function runSkill(opts) {
|
|
|
9609
9990
|
...existing,
|
|
9610
9991
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
9611
9992
|
};
|
|
9612
|
-
await
|
|
9613
|
-
await
|
|
9993
|
+
await import_node_fs29.promises.mkdir(import_node_path45.default.dirname(target), { recursive: true });
|
|
9994
|
+
await import_node_fs29.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
9614
9995
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
9615
9996
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
9616
9997
|
return { exitCode: 0 };
|
|
@@ -9648,12 +10029,12 @@ async function main() {
|
|
|
9648
10029
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
9649
10030
|
process.exit(2);
|
|
9650
10031
|
}
|
|
9651
|
-
const scanPath =
|
|
10032
|
+
const scanPath = import_node_path45.default.resolve(target);
|
|
9652
10033
|
const projectExplicit = parsed.project !== null;
|
|
9653
|
-
const projectName = projectExplicit ? project :
|
|
10034
|
+
const projectName = projectExplicit ? project : import_node_path45.default.basename(scanPath);
|
|
9654
10035
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
9655
|
-
const fallback = pathsForProject(projectKey,
|
|
9656
|
-
const outPath =
|
|
10036
|
+
const fallback = pathsForProject(projectKey, import_node_path45.default.join(scanPath, "neat-out")).snapshotPath;
|
|
10037
|
+
const outPath = import_node_path45.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
9657
10038
|
const result = await runInit({
|
|
9658
10039
|
scanPath,
|
|
9659
10040
|
outPath,
|
|
@@ -9674,21 +10055,21 @@ async function main() {
|
|
|
9674
10055
|
usage();
|
|
9675
10056
|
process.exit(2);
|
|
9676
10057
|
}
|
|
9677
|
-
const scanPath =
|
|
9678
|
-
const stat = await
|
|
10058
|
+
const scanPath = import_node_path45.default.resolve(target);
|
|
10059
|
+
const stat = await import_node_fs29.promises.stat(scanPath).catch(() => null);
|
|
9679
10060
|
if (!stat || !stat.isDirectory()) {
|
|
9680
10061
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
9681
10062
|
process.exit(2);
|
|
9682
10063
|
}
|
|
9683
|
-
const projectPaths = pathsForProject(project,
|
|
9684
|
-
const outPath =
|
|
9685
|
-
const errorsPath =
|
|
9686
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
10064
|
+
const projectPaths = pathsForProject(project, import_node_path45.default.join(scanPath, "neat-out"));
|
|
10065
|
+
const outPath = import_node_path45.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
10066
|
+
const errorsPath = import_node_path45.default.resolve(
|
|
10067
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path45.default.join(import_node_path45.default.dirname(outPath), import_node_path45.default.basename(projectPaths.errorsPath))
|
|
9687
10068
|
);
|
|
9688
|
-
const staleEventsPath =
|
|
9689
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
10069
|
+
const staleEventsPath = import_node_path45.default.resolve(
|
|
10070
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path45.default.join(import_node_path45.default.dirname(outPath), import_node_path45.default.basename(projectPaths.staleEventsPath))
|
|
9690
10071
|
);
|
|
9691
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
10072
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path45.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
9692
10073
|
const handle = await startWatch(getGraph(project), {
|
|
9693
10074
|
scanPath,
|
|
9694
10075
|
outPath,
|
|
@@ -9834,11 +10215,11 @@ async function main() {
|
|
|
9834
10215
|
process.exit(1);
|
|
9835
10216
|
}
|
|
9836
10217
|
async function tryOrchestrator(cmd, parsed) {
|
|
9837
|
-
const scanPath =
|
|
9838
|
-
const stat = await
|
|
10218
|
+
const scanPath = import_node_path45.default.resolve(cmd);
|
|
10219
|
+
const stat = await import_node_fs29.promises.stat(scanPath).catch(() => null);
|
|
9839
10220
|
if (!stat || !stat.isDirectory()) return null;
|
|
9840
10221
|
const projectExplicit = parsed.project !== null;
|
|
9841
|
-
const projectName = projectExplicit ? parsed.project :
|
|
10222
|
+
const projectName = projectExplicit ? parsed.project : import_node_path45.default.basename(scanPath);
|
|
9842
10223
|
const result = await runOrchestrator({
|
|
9843
10224
|
scanPath,
|
|
9844
10225
|
project: projectName,
|
|
@@ -9988,10 +10369,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
9988
10369
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9989
10370
|
const out = [];
|
|
9990
10371
|
for (const p of parts) {
|
|
9991
|
-
const r =
|
|
10372
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(p);
|
|
9992
10373
|
if (!r.success) {
|
|
9993
10374
|
console.error(
|
|
9994
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
10375
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types26.DivergenceTypeSchema.options.join(", ")}`
|
|
9995
10376
|
);
|
|
9996
10377
|
return 2;
|
|
9997
10378
|
}
|