@neat.is/core 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/neatd.cjs CHANGED
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
57
57
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
58
  const publicRead = opts.publicRead === true;
59
59
  app.addHook("preHandler", (req2, reply, done) => {
60
- const path40 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path42 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path40 === suffix || path40.endsWith(suffix)) {
62
+ if (path42 === suffix || path42.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -185,8 +185,8 @@ function reshapeGrpcRequest(req2) {
185
185
  };
186
186
  }
187
187
  function resolveProtoRoot() {
188
- const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
- return import_node_path34.default.resolve(here, "..", "proto");
188
+ const here = import_node_path36.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
+ return import_node_path36.default.resolve(here, "..", "proto");
190
190
  }
191
191
  function loadTraceService() {
192
192
  const protoRoot = resolveProtoRoot();
@@ -254,13 +254,13 @@ async function startOtelGrpcReceiver(opts) {
254
254
  })
255
255
  };
256
256
  }
257
- var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
257
+ var import_node_url, import_node_path36, import_node_crypto2, grpc, protoLoader;
258
258
  var init_otel_grpc = __esm({
259
259
  "src/otel-grpc.ts"() {
260
260
  "use strict";
261
261
  init_cjs_shims();
262
262
  import_node_url = require("url");
263
- import_node_path34 = __toESM(require("path"), 1);
263
+ import_node_path36 = __toESM(require("path"), 1);
264
264
  import_node_crypto2 = require("crypto");
265
265
  grpc = __toESM(require("@grpc/grpc-js"), 1);
266
266
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -370,10 +370,10 @@ function parseOtlpRequest(body) {
370
370
  return out;
371
371
  }
372
372
  function loadProtoRoot() {
373
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
373
+ const here = import_node_path37.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
+ const protoRoot = import_node_path37.default.resolve(here, "..", "proto");
375
375
  const root = new import_protobufjs.default.Root();
376
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
376
+ root.resolvePath = (_origin, target) => import_node_path37.default.resolve(protoRoot, target);
377
377
  root.loadSync(
378
378
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
379
379
  { keepCase: true }
@@ -566,12 +566,12 @@ async function buildOtelReceiver(opts) {
566
566
  };
567
567
  return decorated;
568
568
  }
569
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
569
+ var import_node_path37, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
570
570
  var init_otel = __esm({
571
571
  "src/otel.ts"() {
572
572
  "use strict";
573
573
  init_cjs_shims();
574
- import_node_path35 = __toESM(require("path"), 1);
574
+ import_node_path37 = __toESM(require("path"), 1);
575
575
  import_node_url2 = require("url");
576
576
  import_fastify2 = __toESM(require("fastify"), 1);
577
577
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -587,14 +587,14 @@ var init_otel = __esm({
587
587
 
588
588
  // src/neatd.ts
589
589
  init_cjs_shims();
590
- var import_node_fs23 = require("fs");
591
- var import_node_path39 = __toESM(require("path"), 1);
590
+ var import_node_fs25 = require("fs");
591
+ var import_node_path41 = __toESM(require("path"), 1);
592
592
  var import_node_module = require("module");
593
593
 
594
594
  // src/daemon.ts
595
595
  init_cjs_shims();
596
- var import_node_fs22 = require("fs");
597
- var import_node_path37 = __toESM(require("path"), 1);
596
+ var import_node_fs24 = require("fs");
597
+ var import_node_path39 = __toESM(require("path"), 1);
598
598
 
599
599
  // src/graph.ts
600
600
  init_cjs_shims();
@@ -631,6 +631,7 @@ init_cjs_shims();
631
631
  init_cjs_shims();
632
632
  var import_node_fs3 = require("fs");
633
633
  var import_node_path3 = __toESM(require("path"), 1);
634
+ var sourceMapJs = __toESM(require("source-map-js"), 1);
634
635
 
635
636
  // src/policy.ts
636
637
  init_cjs_shims();
@@ -1072,19 +1073,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1072
1073
  function longestIncomingWalk(graph, start, maxDepth) {
1073
1074
  let best = { path: [start], edges: [] };
1074
1075
  const visited = /* @__PURE__ */ new Set([start]);
1075
- function step(node, path40, edges) {
1076
- if (path40.length > best.path.length) {
1077
- best = { path: [...path40], edges: [...edges] };
1076
+ function step(node, path42, edges) {
1077
+ if (path42.length > best.path.length) {
1078
+ best = { path: [...path42], edges: [...edges] };
1078
1079
  }
1079
- if (path40.length - 1 >= maxDepth) return;
1080
+ if (path42.length - 1 >= maxDepth) return;
1080
1081
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1081
1082
  for (const [srcId, edge] of incoming) {
1082
1083
  if (visited.has(srcId)) continue;
1083
1084
  visited.add(srcId);
1084
- path40.push(srcId);
1085
+ path42.push(srcId);
1085
1086
  edges.push(edge);
1086
- step(srcId, path40, edges);
1087
- path40.pop();
1087
+ step(srcId, path42, edges);
1088
+ path42.pop();
1088
1089
  edges.pop();
1089
1090
  visited.delete(srcId);
1090
1091
  }
@@ -1613,6 +1614,14 @@ function warnUnidentifiedSpan(project) {
1613
1614
  `[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
1614
1615
  );
1615
1616
  }
1617
+ var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
1618
+ function warnNoSourceMaps(serviceName) {
1619
+ if (noSourceMapWarnedServices.has(serviceName)) return;
1620
+ noSourceMapWarnedServices.add(serviceName);
1621
+ console.warn(
1622
+ `[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
1623
+ );
1624
+ }
1616
1625
  function pickAttr(span, ...keys) {
1617
1626
  for (const k of keys) {
1618
1627
  const v = span.attributes[k];
@@ -1655,8 +1664,13 @@ function languageForExt(relPath) {
1655
1664
  return void 0;
1656
1665
  }
1657
1666
  }
1658
- function relPathForRuntimeFile(filepath, serviceNode) {
1667
+ function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
1659
1668
  let p = toPosix(filepath).replace(/^file:\/\//, "");
1669
+ if (scanPath && scanPath.length > 0) {
1670
+ const absRoot = toPosix(import_node_path3.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
1671
+ const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
1672
+ if (p.startsWith(anchor)) return p.slice(anchor.length);
1673
+ }
1660
1674
  const root = serviceNode?.repoPath;
1661
1675
  if (root && root !== "." && root.length > 0) {
1662
1676
  const rootPosix = toPosix(root);
@@ -1673,16 +1687,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
1673
1687
  p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1674
1688
  return p.length > 0 ? p : null;
1675
1689
  }
1676
- function callSiteFromSpan(span, serviceNode) {
1690
+ var sourceMapCache = /* @__PURE__ */ new Map();
1691
+ function resolveDistToSrc(absFilepath, line) {
1692
+ if (!absFilepath.endsWith(".js")) return null;
1693
+ let entry2 = sourceMapCache.get(absFilepath);
1694
+ if (entry2 === void 0) {
1695
+ entry2 = null;
1696
+ const mapPath = `${absFilepath}.map`;
1697
+ try {
1698
+ if ((0, import_node_fs3.existsSync)(mapPath)) {
1699
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(mapPath, "utf8"));
1700
+ const consumer = new sourceMapJs.SourceMapConsumer(raw);
1701
+ entry2 = { consumer, dir: import_node_path3.default.dirname(mapPath) };
1702
+ }
1703
+ } catch {
1704
+ entry2 = null;
1705
+ }
1706
+ sourceMapCache.set(absFilepath, entry2);
1707
+ }
1708
+ if (!entry2) return null;
1709
+ try {
1710
+ const pos = entry2.consumer.originalPositionFor({
1711
+ line: line !== void 0 && Number.isFinite(line) ? line : 1,
1712
+ column: 0
1713
+ });
1714
+ if (!pos || !pos.source) return null;
1715
+ const root = entry2.consumer.sourceRoot ?? "";
1716
+ const resolved = import_node_path3.default.resolve(entry2.dir, root, pos.source);
1717
+ return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ }
1722
+ function callSiteFromSpan(span, serviceNode, scanPath) {
1677
1723
  const filepath = span.attributes[CODE_FILEPATH_ATTR];
1678
1724
  if (typeof filepath !== "string" || filepath.length === 0) return null;
1679
- const relPath = relPathForRuntimeFile(filepath, serviceNode);
1680
- if (!relPath) return null;
1681
1725
  const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1682
- const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1726
+ let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1727
+ const abs = toPosix(filepath).replace(/^file:\/\//, "");
1728
+ const resolved = resolveDistToSrc(abs, line);
1729
+ let effectivePath = filepath;
1730
+ let originalRelPath;
1731
+ if (resolved) {
1732
+ originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
1733
+ effectivePath = resolved.filepath;
1734
+ if (resolved.line !== void 0) line = resolved.line;
1735
+ }
1736
+ const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
1737
+ if (!relPath) return null;
1738
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
1739
+ warnNoSourceMaps(serviceNode.name);
1740
+ }
1683
1741
  const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1684
1742
  const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1685
- return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1743
+ return {
1744
+ relPath,
1745
+ ...line !== void 0 ? { line } : {},
1746
+ ...fn ? { fn } : {},
1747
+ ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1748
+ };
1686
1749
  }
1687
1750
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1688
1751
  const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
@@ -1694,6 +1757,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1694
1757
  service: serviceName,
1695
1758
  path: callSite.relPath,
1696
1759
  ...language ? { language } : {},
1760
+ ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1697
1761
  discoveredVia: "otel"
1698
1762
  };
1699
1763
  graph.addNode(fileNodeId, node);
@@ -1719,6 +1783,12 @@ function makeInferredEdgeId(type, source, target) {
1719
1783
  }
1720
1784
  var INFERRED_CONFIDENCE = 0.6;
1721
1785
  var STITCH_MAX_DEPTH = 2;
1786
+ var WIRE_SPAN_KIND_CLIENT = 3;
1787
+ var WIRE_SPAN_KIND_PRODUCER = 4;
1788
+ function spanMintsObservedEdge(kind) {
1789
+ if (kind === void 0 || kind === 0) return true;
1790
+ return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1791
+ }
1722
1792
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1723
1793
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1724
1794
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -1825,7 +1895,7 @@ function ensureFrontierNode(graph, host, ts) {
1825
1895
  graph.addNode(id, node);
1826
1896
  return id;
1827
1897
  }
1828
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1898
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1829
1899
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1830
1900
  const id = makeObservedEdgeId(type, source, target);
1831
1901
  if (graph.hasEdge(id)) {
@@ -1862,7 +1932,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1862
1932
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1863
1933
  lastObserved: ts,
1864
1934
  callCount: 1,
1865
- signal
1935
+ signal,
1936
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
1937
+ // Only set when code.filepath was present on the span — never fabricated.
1938
+ ...evidence ? { evidence } : {}
1866
1939
  };
1867
1940
  graph.addEdgeWithKey(id, source, target, edge);
1868
1941
  return { edge, created: true };
@@ -1954,12 +2027,14 @@ async function handleSpan(ctx, span) {
1954
2027
  const isError = span.statusCode === 2;
1955
2028
  cacheSpanService(span, nowMs);
1956
2029
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1957
- const callSite = callSiteFromSpan(span, sourceServiceNode);
2030
+ const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1958
2031
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2032
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
1959
2033
  let affectedNode = sourceId;
2034
+ const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1960
2035
  if (span.dbSystem) {
1961
2036
  const host = pickAddress(span);
1962
- if (host) {
2037
+ if (mintsFromCallerSide && host) {
1963
2038
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
1964
2039
  const targetId = (0, import_types3.databaseId)(host);
1965
2040
  const result = upsertObservedEdge(
@@ -1968,14 +2043,15 @@ async function handleSpan(ctx, span) {
1968
2043
  observedSource(),
1969
2044
  targetId,
1970
2045
  ts,
1971
- isError
2046
+ isError,
2047
+ callSiteEvidence
1972
2048
  );
1973
2049
  if (result) affectedNode = targetId;
1974
2050
  }
1975
2051
  } else {
1976
2052
  const host = pickAddress(span);
1977
2053
  let resolvedViaAddress = false;
1978
- if (host && host !== span.service) {
2054
+ if (mintsFromCallerSide && host && host !== span.service) {
1979
2055
  const targetId = resolveServiceId(ctx.graph, host, env);
1980
2056
  if (targetId && targetId !== sourceId) {
1981
2057
  upsertObservedEdge(
@@ -1984,7 +2060,8 @@ async function handleSpan(ctx, span) {
1984
2060
  observedSource(),
1985
2061
  targetId,
1986
2062
  ts,
1987
- isError
2063
+ isError,
2064
+ callSiteEvidence
1988
2065
  );
1989
2066
  affectedNode = targetId;
1990
2067
  resolvedViaAddress = true;
@@ -1996,7 +2073,8 @@ async function handleSpan(ctx, span) {
1996
2073
  observedSource(),
1997
2074
  frontierNodeId,
1998
2075
  ts,
1999
- isError
2076
+ isError,
2077
+ callSiteEvidence
2000
2078
  );
2001
2079
  affectedNode = frontierNodeId;
2002
2080
  resolvedViaAddress = true;
@@ -2874,14 +2952,111 @@ async function addServiceAliases(graph, scanPath, services) {
2874
2952
 
2875
2953
  // src/extract/databases/index.ts
2876
2954
  init_cjs_shims();
2877
- var import_node_path17 = __toESM(require("path"), 1);
2955
+ var import_node_path18 = __toESM(require("path"), 1);
2956
+ var import_types8 = require("@neat.is/types");
2957
+
2958
+ // src/extract/calls/shared.ts
2959
+ init_cjs_shims();
2960
+ var import_node_fs10 = require("fs");
2961
+ var import_node_path10 = __toESM(require("path"), 1);
2878
2962
  var import_types7 = require("@neat.is/types");
2963
+ async function walkSourceFiles(dir) {
2964
+ const out = [];
2965
+ async function walk(current) {
2966
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2967
+ for (const entry2 of entries) {
2968
+ const full = import_node_path10.default.join(current, entry2.name);
2969
+ if (entry2.isDirectory()) {
2970
+ if (IGNORED_DIRS.has(entry2.name)) continue;
2971
+ if (await isPythonVenvDir(full)) continue;
2972
+ await walk(full);
2973
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))) {
2974
+ out.push(full);
2975
+ }
2976
+ }
2977
+ }
2978
+ await walk(dir);
2979
+ return out;
2980
+ }
2981
+ async function loadSourceFiles(dir) {
2982
+ const paths = await walkSourceFiles(dir);
2983
+ const out = [];
2984
+ for (const p of paths) {
2985
+ try {
2986
+ const content = await import_node_fs10.promises.readFile(p, "utf8");
2987
+ out.push({ path: p, content });
2988
+ } catch {
2989
+ }
2990
+ }
2991
+ return out;
2992
+ }
2993
+ function lineOf(text, needle) {
2994
+ const idx = text.indexOf(needle);
2995
+ if (idx < 0) return 1;
2996
+ return text.slice(0, idx).split("\n").length;
2997
+ }
2998
+ function snippet(text, line) {
2999
+ const lines = text.split("\n");
3000
+ return (lines[line - 1] ?? "").trim();
3001
+ }
3002
+ function toPosix2(p) {
3003
+ return p.split("\\").join("/");
3004
+ }
3005
+ function languageForPath(relPath) {
3006
+ switch (import_node_path10.default.extname(relPath).toLowerCase()) {
3007
+ case ".py":
3008
+ return "python";
3009
+ case ".ts":
3010
+ case ".tsx":
3011
+ return "typescript";
3012
+ case ".js":
3013
+ case ".jsx":
3014
+ case ".mjs":
3015
+ case ".cjs":
3016
+ return "javascript";
3017
+ default:
3018
+ return void 0;
3019
+ }
3020
+ }
3021
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3022
+ let nodesAdded = 0;
3023
+ let edgesAdded = 0;
3024
+ const fileNodeId = (0, import_types7.fileId)(serviceName, relPath);
3025
+ if (!graph.hasNode(fileNodeId)) {
3026
+ const language = languageForPath(relPath);
3027
+ const node = {
3028
+ id: fileNodeId,
3029
+ type: import_types7.NodeType.FileNode,
3030
+ service: serviceName,
3031
+ path: relPath,
3032
+ ...language ? { language } : {},
3033
+ discoveredVia: "static"
3034
+ };
3035
+ graph.addNode(fileNodeId, node);
3036
+ nodesAdded++;
3037
+ }
3038
+ const containsId = (0, import_types7.extractedEdgeId)(serviceNodeId, fileNodeId, import_types7.EdgeType.CONTAINS);
3039
+ if (!graph.hasEdge(containsId)) {
3040
+ const edge = {
3041
+ id: containsId,
3042
+ source: serviceNodeId,
3043
+ target: fileNodeId,
3044
+ type: import_types7.EdgeType.CONTAINS,
3045
+ provenance: import_types7.Provenance.EXTRACTED,
3046
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3047
+ evidence: { file: relPath }
3048
+ };
3049
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3050
+ edgesAdded++;
3051
+ }
3052
+ return { fileNodeId, nodesAdded, edgesAdded };
3053
+ }
2879
3054
 
2880
3055
  // src/extract/databases/db-config-yaml.ts
2881
3056
  init_cjs_shims();
2882
- var import_node_path10 = __toESM(require("path"), 1);
3057
+ var import_node_path11 = __toESM(require("path"), 1);
2883
3058
  async function parse(serviceDir) {
2884
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3059
+ const yamlPath = import_node_path11.default.join(serviceDir, "db-config.yaml");
2885
3060
  if (!await exists(yamlPath)) return [];
2886
3061
  const raw = await readYaml(yamlPath);
2887
3062
  return [
@@ -2899,13 +3074,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2899
3074
 
2900
3075
  // src/extract/databases/dotenv.ts
2901
3076
  init_cjs_shims();
2902
- var import_node_fs11 = require("fs");
2903
- var import_node_path12 = __toESM(require("path"), 1);
3077
+ var import_node_fs12 = require("fs");
3078
+ var import_node_path13 = __toESM(require("path"), 1);
2904
3079
 
2905
3080
  // src/extract/databases/shared.ts
2906
3081
  init_cjs_shims();
2907
- var import_node_fs10 = require("fs");
2908
- var import_node_path11 = __toESM(require("path"), 1);
3082
+ var import_node_fs11 = require("fs");
3083
+ var import_node_path12 = __toESM(require("path"), 1);
2909
3084
  function schemeToEngine(scheme) {
2910
3085
  const s = scheme.toLowerCase().split("+")[0];
2911
3086
  switch (s) {
@@ -2944,14 +3119,14 @@ function parseConnectionString(url) {
2944
3119
  }
2945
3120
  async function readIfExists(filePath) {
2946
3121
  try {
2947
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3122
+ return await import_node_fs11.promises.readFile(filePath, "utf8");
2948
3123
  } catch {
2949
3124
  return null;
2950
3125
  }
2951
3126
  }
2952
3127
  async function findFirst(serviceDir, candidates) {
2953
3128
  for (const rel of candidates) {
2954
- const abs = import_node_path11.default.join(serviceDir, rel);
3129
+ const abs = import_node_path12.default.join(serviceDir, rel);
2955
3130
  const content = await readIfExists(abs);
2956
3131
  if (content !== null) return abs;
2957
3132
  }
@@ -3002,15 +3177,15 @@ function parseDotenvLine(line) {
3002
3177
  return { key, value };
3003
3178
  }
3004
3179
  async function parse2(serviceDir) {
3005
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3180
+ const entries = await import_node_fs12.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3006
3181
  const configs = [];
3007
3182
  const seen = /* @__PURE__ */ new Set();
3008
3183
  for (const entry2 of entries) {
3009
3184
  if (!entry2.isFile()) continue;
3010
3185
  const match = isConfigFile(entry2.name);
3011
3186
  if (!match.match || match.fileType !== "env") continue;
3012
- const filePath = import_node_path12.default.join(serviceDir, entry2.name);
3013
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
3187
+ const filePath = import_node_path13.default.join(serviceDir, entry2.name);
3188
+ const content = await import_node_fs12.promises.readFile(filePath, "utf8");
3014
3189
  for (const line of content.split("\n")) {
3015
3190
  const parsed = parseDotenvLine(line);
3016
3191
  if (!parsed) continue;
@@ -3029,9 +3204,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3029
3204
 
3030
3205
  // src/extract/databases/prisma.ts
3031
3206
  init_cjs_shims();
3032
- var import_node_path13 = __toESM(require("path"), 1);
3207
+ var import_node_path14 = __toESM(require("path"), 1);
3033
3208
  async function parse3(serviceDir) {
3034
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3209
+ const schemaPath = import_node_path14.default.join(serviceDir, "prisma", "schema.prisma");
3035
3210
  const content = await readIfExists(schemaPath);
3036
3211
  if (!content) return [];
3037
3212
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3163,10 +3338,10 @@ var knexParser = { name: "knex", parse: parse5 };
3163
3338
 
3164
3339
  // src/extract/databases/ormconfig.ts
3165
3340
  init_cjs_shims();
3166
- var import_node_path14 = __toESM(require("path"), 1);
3341
+ var import_node_path15 = __toESM(require("path"), 1);
3167
3342
  async function parse6(serviceDir) {
3168
3343
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3169
- const abs = import_node_path14.default.join(serviceDir, candidate);
3344
+ const abs = import_node_path15.default.join(serviceDir, candidate);
3170
3345
  if (!await exists(abs)) continue;
3171
3346
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3172
3347
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3226,9 +3401,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3226
3401
 
3227
3402
  // src/extract/databases/sequelize.ts
3228
3403
  init_cjs_shims();
3229
- var import_node_path15 = __toESM(require("path"), 1);
3404
+ var import_node_path16 = __toESM(require("path"), 1);
3230
3405
  async function parse8(serviceDir) {
3231
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3406
+ const configPath = import_node_path16.default.join(serviceDir, "config", "config.json");
3232
3407
  if (!await exists(configPath)) return [];
3233
3408
  const raw = await readJson(configPath);
3234
3409
  const out = [];
@@ -3255,7 +3430,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3255
3430
 
3256
3431
  // src/extract/databases/docker-compose.ts
3257
3432
  init_cjs_shims();
3258
- var import_node_path16 = __toESM(require("path"), 1);
3433
+ var import_node_path17 = __toESM(require("path"), 1);
3259
3434
  function portFromService(svc) {
3260
3435
  for (const raw of svc.ports ?? []) {
3261
3436
  const str = String(raw);
@@ -3282,7 +3457,7 @@ function databaseFromEnv(svc) {
3282
3457
  }
3283
3458
  async function parse9(serviceDir) {
3284
3459
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3285
- const abs = import_node_path16.default.join(serviceDir, name);
3460
+ const abs = import_node_path17.default.join(serviceDir, name);
3286
3461
  if (!await exists(abs)) continue;
3287
3462
  const raw = await readYaml(abs);
3288
3463
  if (!raw?.services) return [];
@@ -3323,8 +3498,8 @@ function compatibleDriversFor(engine) {
3323
3498
  }
3324
3499
  function toDatabaseNode(config) {
3325
3500
  return {
3326
- id: (0, import_types7.databaseId)(config.host),
3327
- type: import_types7.NodeType.DatabaseNode,
3501
+ id: (0, import_types8.databaseId)(config.host),
3502
+ type: import_types8.NodeType.DatabaseNode,
3328
3503
  name: config.database || config.host,
3329
3504
  engine: config.engine,
3330
3505
  engineVersion: config.engineVersion,
@@ -3454,19 +3629,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3454
3629
  discoveredVia: mergedDiscoveredVia
3455
3630
  });
3456
3631
  }
3632
+ const relConfigFile = toPosix2(import_node_path18.default.relative(service.dir, config.sourceFile));
3633
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3634
+ graph,
3635
+ service.pkg.name,
3636
+ service.node.id,
3637
+ relConfigFile
3638
+ );
3639
+ nodesAdded += fn;
3640
+ edgesAdded += fe;
3641
+ const evidenceFile = toPosix2(import_node_path18.default.relative(scanPath, config.sourceFile));
3457
3642
  const edge = {
3458
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3459
- source: service.node.id,
3643
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3644
+ source: fileNodeId,
3460
3645
  target: dbNode.id,
3461
- type: import_types7.EdgeType.CONNECTS_TO,
3462
- provenance: import_types7.Provenance.EXTRACTED,
3463
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3464
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3465
- // Ghost-edge cleanup keys retirement on this; the conditional
3466
- // sourceFile spread that used to live here was a v0.1.x leftover.
3467
- evidence: {
3468
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3469
- }
3646
+ type: import_types8.EdgeType.CONNECTS_TO,
3647
+ provenance: import_types8.Provenance.EXTRACTED,
3648
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3649
+ evidence: { file: evidenceFile }
3470
3650
  };
3471
3651
  if (!graph.hasEdge(edge.id)) {
3472
3652
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3492,15 +3672,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3492
3672
 
3493
3673
  // src/extract/configs.ts
3494
3674
  init_cjs_shims();
3495
- var import_node_fs12 = require("fs");
3496
- var import_node_path18 = __toESM(require("path"), 1);
3497
- var import_types8 = require("@neat.is/types");
3675
+ var import_node_fs13 = require("fs");
3676
+ var import_node_path19 = __toESM(require("path"), 1);
3677
+ var import_types9 = require("@neat.is/types");
3498
3678
  async function walkConfigFiles(dir) {
3499
3679
  const out = [];
3500
3680
  async function walk(current) {
3501
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3681
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true });
3502
3682
  for (const entry2 of entries) {
3503
- const full = import_node_path18.default.join(current, entry2.name);
3683
+ const full = import_node_path19.default.join(current, entry2.name);
3504
3684
  if (entry2.isDirectory()) {
3505
3685
  if (IGNORED_DIRS.has(entry2.name)) continue;
3506
3686
  if (await isPythonVenvDir(full)) continue;
@@ -3519,26 +3699,35 @@ async function addConfigNodes(graph, services, scanPath) {
3519
3699
  for (const service of services) {
3520
3700
  const configFiles = await walkConfigFiles(service.dir);
3521
3701
  for (const file of configFiles) {
3522
- const relPath = import_node_path18.default.relative(scanPath, file);
3702
+ const relPath = import_node_path19.default.relative(scanPath, file);
3523
3703
  const node = {
3524
- id: (0, import_types8.configId)(relPath),
3525
- type: import_types8.NodeType.ConfigNode,
3526
- name: import_node_path18.default.basename(file),
3704
+ id: (0, import_types9.configId)(relPath),
3705
+ type: import_types9.NodeType.ConfigNode,
3706
+ name: import_node_path19.default.basename(file),
3527
3707
  path: relPath,
3528
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3708
+ fileType: isConfigFile(import_node_path19.default.basename(file)).fileType
3529
3709
  };
3530
3710
  if (!graph.hasNode(node.id)) {
3531
3711
  graph.addNode(node.id, node);
3532
3712
  nodesAdded++;
3533
3713
  }
3714
+ const relToService = toPosix2(import_node_path19.default.relative(service.dir, file));
3715
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3716
+ graph,
3717
+ service.pkg.name,
3718
+ service.node.id,
3719
+ relToService
3720
+ );
3721
+ nodesAdded += fn;
3722
+ edgesAdded += fe;
3534
3723
  const edge = {
3535
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3536
- source: service.node.id,
3724
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types9.EdgeType.CONFIGURED_BY),
3725
+ source: fileNodeId,
3537
3726
  target: node.id,
3538
- type: import_types8.EdgeType.CONFIGURED_BY,
3539
- provenance: import_types8.Provenance.EXTRACTED,
3540
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3541
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3727
+ type: import_types9.EdgeType.CONFIGURED_BY,
3728
+ provenance: import_types9.Provenance.EXTRACTED,
3729
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3730
+ evidence: { file: relPath.split(import_node_path19.default.sep).join("/") }
3542
3731
  };
3543
3732
  if (!graph.hasEdge(edge.id)) {
3544
3733
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3560,105 +3749,6 @@ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3560
3749
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3561
3750
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3562
3751
  var import_types10 = require("@neat.is/types");
3563
-
3564
- // src/extract/calls/shared.ts
3565
- init_cjs_shims();
3566
- var import_node_fs13 = require("fs");
3567
- var import_node_path19 = __toESM(require("path"), 1);
3568
- var import_types9 = require("@neat.is/types");
3569
- async function walkSourceFiles(dir) {
3570
- const out = [];
3571
- async function walk(current) {
3572
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3573
- for (const entry2 of entries) {
3574
- const full = import_node_path19.default.join(current, entry2.name);
3575
- if (entry2.isDirectory()) {
3576
- if (IGNORED_DIRS.has(entry2.name)) continue;
3577
- if (await isPythonVenvDir(full)) continue;
3578
- await walk(full);
3579
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3580
- out.push(full);
3581
- }
3582
- }
3583
- }
3584
- await walk(dir);
3585
- return out;
3586
- }
3587
- async function loadSourceFiles(dir) {
3588
- const paths = await walkSourceFiles(dir);
3589
- const out = [];
3590
- for (const p of paths) {
3591
- try {
3592
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3593
- out.push({ path: p, content });
3594
- } catch {
3595
- }
3596
- }
3597
- return out;
3598
- }
3599
- function lineOf(text, needle) {
3600
- const idx = text.indexOf(needle);
3601
- if (idx < 0) return 1;
3602
- return text.slice(0, idx).split("\n").length;
3603
- }
3604
- function snippet(text, line) {
3605
- const lines = text.split("\n");
3606
- return (lines[line - 1] ?? "").trim();
3607
- }
3608
- function toPosix2(p) {
3609
- return p.split("\\").join("/");
3610
- }
3611
- function languageForPath(relPath) {
3612
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3613
- case ".py":
3614
- return "python";
3615
- case ".ts":
3616
- case ".tsx":
3617
- return "typescript";
3618
- case ".js":
3619
- case ".jsx":
3620
- case ".mjs":
3621
- case ".cjs":
3622
- return "javascript";
3623
- default:
3624
- return void 0;
3625
- }
3626
- }
3627
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3628
- let nodesAdded = 0;
3629
- let edgesAdded = 0;
3630
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3631
- if (!graph.hasNode(fileNodeId)) {
3632
- const language = languageForPath(relPath);
3633
- const node = {
3634
- id: fileNodeId,
3635
- type: import_types9.NodeType.FileNode,
3636
- service: serviceName,
3637
- path: relPath,
3638
- ...language ? { language } : {},
3639
- discoveredVia: "static"
3640
- };
3641
- graph.addNode(fileNodeId, node);
3642
- nodesAdded++;
3643
- }
3644
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3645
- if (!graph.hasEdge(containsId)) {
3646
- const edge = {
3647
- id: containsId,
3648
- source: serviceNodeId,
3649
- target: fileNodeId,
3650
- type: import_types9.EdgeType.CONTAINS,
3651
- provenance: import_types9.Provenance.EXTRACTED,
3652
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3653
- evidence: { file: relPath }
3654
- };
3655
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3656
- edgesAdded++;
3657
- }
3658
- return { fileNodeId, nodesAdded, edgesAdded };
3659
- }
3660
-
3661
- // src/extract/calls/http.ts
3662
3752
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3663
3753
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3664
3754
  function isInsideJsxExternalLink(node) {
@@ -3686,8 +3776,14 @@ function collectStringLiterals(node, out) {
3686
3776
  if (child) collectStringLiterals(child, out);
3687
3777
  }
3688
3778
  }
3779
+ var PARSE_CHUNK = 16384;
3780
+ function parseSource(parser, source) {
3781
+ return parser.parse(
3782
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3783
+ );
3784
+ }
3689
3785
  function callsFromSource(source, parser, knownHosts) {
3690
- const tree = parser.parse(source);
3786
+ const tree = parseSource(parser, source);
3691
3787
  const literals = [];
3692
3788
  collectStringLiterals(tree.rootNode, literals);
3693
3789
  const out = [];
@@ -3936,6 +4032,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
3936
4032
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3937
4033
  function isLikelyAddress(value) {
3938
4034
  if (!value) return false;
4035
+ if (/\s/.test(value) || value.startsWith("{")) return false;
3939
4036
  return /:\d{2,5}$/.test(value) || value.includes(".");
3940
4037
  }
3941
4038
  function normaliseForMatch(s) {
@@ -4247,17 +4344,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4247
4344
  graph.addNode(node.id, node);
4248
4345
  nodesAdded++;
4249
4346
  }
4250
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4347
+ const relDockerfile = toPosix2(import_node_path26.default.relative(service.dir, dockerfilePath));
4348
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4349
+ graph,
4350
+ service.pkg.name,
4351
+ service.node.id,
4352
+ relDockerfile
4353
+ );
4354
+ nodesAdded += fn;
4355
+ edgesAdded += fe;
4356
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types18.EdgeType.RUNS_ON);
4251
4357
  if (!graph.hasEdge(edgeId)) {
4252
4358
  const edge = {
4253
4359
  id: edgeId,
4254
- source: service.node.id,
4360
+ source: fileNodeId,
4255
4361
  target: node.id,
4256
4362
  type: import_types18.EdgeType.RUNS_ON,
4257
4363
  provenance: import_types18.Provenance.EXTRACTED,
4258
4364
  confidence: (0, import_types18.confidenceForExtracted)("structural"),
4259
4365
  evidence: {
4260
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4366
+ file: toPosix2(import_node_path26.default.relative(scanPath, dockerfilePath))
4261
4367
  }
4262
4368
  };
4263
4369
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4648,6 +4754,321 @@ var import_fastify = __toESM(require("fastify"), 1);
4648
4754
  var import_cors = __toESM(require("@fastify/cors"), 1);
4649
4755
  var import_types23 = require("@neat.is/types");
4650
4756
 
4757
+ // src/extend/index.ts
4758
+ init_cjs_shims();
4759
+ var import_node_fs20 = require("fs");
4760
+ var import_node_path34 = __toESM(require("path"), 1);
4761
+ var import_node_os2 = __toESM(require("os"), 1);
4762
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
4763
+
4764
+ // src/installers/package-manager.ts
4765
+ init_cjs_shims();
4766
+ var import_node_fs19 = require("fs");
4767
+ var import_node_path33 = __toESM(require("path"), 1);
4768
+ var import_node_child_process = require("child_process");
4769
+ var LOCKFILE_PRIORITY = [
4770
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
4771
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
4772
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
4773
+ {
4774
+ lockfile: "package-lock.json",
4775
+ pm: "npm",
4776
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
4777
+ }
4778
+ ];
4779
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
4780
+ async function exists2(p) {
4781
+ try {
4782
+ await import_node_fs19.promises.access(p);
4783
+ return true;
4784
+ } catch {
4785
+ return false;
4786
+ }
4787
+ }
4788
+ async function detectPackageManager(serviceDir) {
4789
+ let dir = import_node_path33.default.resolve(serviceDir);
4790
+ const stops = /* @__PURE__ */ new Set();
4791
+ for (let i = 0; i < 64; i++) {
4792
+ if (stops.has(dir)) break;
4793
+ stops.add(dir);
4794
+ for (const candidate of LOCKFILE_PRIORITY) {
4795
+ const lockPath = import_node_path33.default.join(dir, candidate.lockfile);
4796
+ if (await exists2(lockPath)) {
4797
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
4798
+ }
4799
+ }
4800
+ const parent = import_node_path33.default.dirname(dir);
4801
+ if (parent === dir) break;
4802
+ dir = parent;
4803
+ }
4804
+ return { pm: "npm", cwd: import_node_path33.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
4805
+ }
4806
+ async function runPackageManagerInstall(cmd) {
4807
+ return new Promise((resolve) => {
4808
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
4809
+ cwd: cmd.cwd,
4810
+ // Inherit PATH + HOME so the user's installed managers resolve.
4811
+ env: process.env,
4812
+ // `false` keeps the parent in control of cleanup if the orchestrator
4813
+ // exits before install finishes. Cross-platform-safe.
4814
+ shell: false,
4815
+ stdio: ["ignore", "ignore", "pipe"]
4816
+ });
4817
+ let stderr = "";
4818
+ child.stderr?.on("data", (chunk) => {
4819
+ stderr += chunk.toString("utf8");
4820
+ });
4821
+ child.on("error", (err) => {
4822
+ resolve({
4823
+ pm: cmd.pm,
4824
+ cwd: cmd.cwd,
4825
+ args: cmd.args,
4826
+ exitCode: 127,
4827
+ stderr: stderr + `
4828
+ ${err.message}`
4829
+ });
4830
+ });
4831
+ child.on("close", (code) => {
4832
+ resolve({
4833
+ pm: cmd.pm,
4834
+ cwd: cmd.cwd,
4835
+ args: cmd.args,
4836
+ exitCode: code ?? 1,
4837
+ stderr: stderr.trim()
4838
+ });
4839
+ });
4840
+ });
4841
+ }
4842
+
4843
+ // src/extend/index.ts
4844
+ async function fileExists(p) {
4845
+ try {
4846
+ await import_node_fs20.promises.access(p);
4847
+ return true;
4848
+ } catch {
4849
+ return false;
4850
+ }
4851
+ }
4852
+ async function readPackageJson(scanPath) {
4853
+ const pkgPath = import_node_path34.default.join(scanPath, "package.json");
4854
+ const raw = await import_node_fs20.promises.readFile(pkgPath, "utf8");
4855
+ return JSON.parse(raw);
4856
+ }
4857
+ async function findHookFiles(scanPath) {
4858
+ const entries = await import_node_fs20.promises.readdir(scanPath);
4859
+ return entries.filter(
4860
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
4861
+ ).sort();
4862
+ }
4863
+ function extendLogPath() {
4864
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path34.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
4865
+ }
4866
+ async function appendExtendLog(entry2) {
4867
+ const logPath = extendLogPath();
4868
+ await import_node_fs20.promises.mkdir(import_node_path34.default.dirname(logPath), { recursive: true });
4869
+ await import_node_fs20.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
4870
+ }
4871
+ function splicedContent(fileContent, snippet2) {
4872
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
4873
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
4874
+ __INSTRUMENTATION_BLOCK__`);
4875
+ }
4876
+ const lines = fileContent.split("\n");
4877
+ let lastPushIdx = -1;
4878
+ for (let i = 0; i < lines.length; i++) {
4879
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
4880
+ }
4881
+ if (lastPushIdx >= 0) {
4882
+ lines.splice(lastPushIdx + 1, 0, snippet2);
4883
+ return lines.join("\n");
4884
+ }
4885
+ for (let i = 0; i < lines.length; i++) {
4886
+ if (lines[i].includes("new NodeSDK(")) {
4887
+ lines.splice(i, 0, snippet2);
4888
+ return lines.join("\n");
4889
+ }
4890
+ }
4891
+ return null;
4892
+ }
4893
+ async function listUninstrumented(ctx) {
4894
+ const pkg = await readPackageJson(ctx.scanPath);
4895
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4896
+ const results = [];
4897
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
4898
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
4899
+ if (!entry2) continue;
4900
+ if (entry2.coverage === "bundled" || entry2.coverage === "http-only") continue;
4901
+ results.push({
4902
+ library,
4903
+ coverage: entry2.coverage,
4904
+ installedVersion,
4905
+ instrumentation_package: entry2.instrumentation_package,
4906
+ package_version: entry2.package_version,
4907
+ registration: entry2.registration,
4908
+ notes: entry2.notes
4909
+ });
4910
+ }
4911
+ return results;
4912
+ }
4913
+ function lookupInstrumentation(library, installedVersion) {
4914
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
4915
+ if (!entry2) return null;
4916
+ return {
4917
+ library: entry2.library,
4918
+ coverage: entry2.coverage,
4919
+ instrumentation_package: entry2.instrumentation_package,
4920
+ package_version: entry2.package_version,
4921
+ registration: entry2.registration,
4922
+ notes: entry2.notes
4923
+ };
4924
+ }
4925
+ async function describeProjectInstrumentation(ctx) {
4926
+ const hookFiles = await findHookFiles(ctx.scanPath);
4927
+ const envNeat = await fileExists(import_node_path34.default.join(ctx.scanPath, ".env.neat"));
4928
+ const registryInstrPackages = new Set(
4929
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
4930
+ );
4931
+ const pkg = await readPackageJson(ctx.scanPath);
4932
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4933
+ const installedDeps = {};
4934
+ for (const [key, version] of Object.entries(allDeps)) {
4935
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
4936
+ installedDeps[key] = version;
4937
+ }
4938
+ }
4939
+ return { hookFiles, envNeat, installedDeps };
4940
+ }
4941
+ async function applyExtension(ctx, args, options) {
4942
+ const hookFiles = await findHookFiles(ctx.scanPath);
4943
+ if (hookFiles.length === 0) {
4944
+ throw new Error(
4945
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
4946
+ );
4947
+ }
4948
+ for (const file of hookFiles) {
4949
+ const content = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, file), "utf8");
4950
+ if (content.includes(args.registration_snippet)) {
4951
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
4952
+ }
4953
+ }
4954
+ const primaryFile = hookFiles[0];
4955
+ const primaryPath = import_node_path34.default.join(ctx.scanPath, primaryFile);
4956
+ const filesTouched = [];
4957
+ const depsAdded = [];
4958
+ const pkgPath = import_node_path34.default.join(ctx.scanPath, "package.json");
4959
+ const pkg = await readPackageJson(ctx.scanPath);
4960
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
4961
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
4962
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
4963
+ filesTouched.push("package.json");
4964
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
4965
+ }
4966
+ const hookContent = await import_node_fs20.promises.readFile(primaryPath, "utf8");
4967
+ const patched = splicedContent(hookContent, args.registration_snippet);
4968
+ if (!patched) {
4969
+ throw new Error(
4970
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
4971
+ );
4972
+ }
4973
+ await import_node_fs20.promises.writeFile(primaryPath, patched, "utf8");
4974
+ filesTouched.push(primaryFile);
4975
+ const cmd = await detectPackageManager(ctx.scanPath);
4976
+ const installer = options?.runInstall ?? runPackageManagerInstall;
4977
+ const install = await installer(cmd);
4978
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
4979
+ await appendExtendLog({
4980
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4981
+ project: ctx.project,
4982
+ library: args.library,
4983
+ instrumentation_package: args.instrumentation_package,
4984
+ version: args.version,
4985
+ registration_snippet: args.registration_snippet,
4986
+ filesTouched,
4987
+ depsAdded,
4988
+ installOutput
4989
+ });
4990
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
4991
+ }
4992
+ async function dryRunExtension(ctx, args) {
4993
+ const hookFiles = await findHookFiles(ctx.scanPath);
4994
+ if (hookFiles.length === 0) {
4995
+ return {
4996
+ library: args.library,
4997
+ filesTouched: [],
4998
+ depsToAdd: [],
4999
+ packageJsonPatch: {},
5000
+ templatePatch: "No hook files found. Run 'neat init' first."
5001
+ };
5002
+ }
5003
+ for (const file of hookFiles) {
5004
+ const content = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, file), "utf8");
5005
+ if (content.includes(args.registration_snippet)) {
5006
+ return {
5007
+ library: args.library,
5008
+ filesTouched: [],
5009
+ depsToAdd: [],
5010
+ packageJsonPatch: {},
5011
+ templatePatch: "Already applied \u2014 no changes would be made."
5012
+ };
5013
+ }
5014
+ }
5015
+ const primaryFile = hookFiles[0];
5016
+ const filesTouched = [];
5017
+ const depsToAdd = [];
5018
+ let packageJsonPatch = {};
5019
+ let templatePatch = "";
5020
+ const pkg = await readPackageJson(ctx.scanPath);
5021
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5022
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5023
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5024
+ filesTouched.push("package.json");
5025
+ }
5026
+ const hookContent = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, primaryFile), "utf8");
5027
+ const patched = splicedContent(hookContent, args.registration_snippet);
5028
+ if (patched) {
5029
+ filesTouched.push(primaryFile);
5030
+ templatePatch = `+ ${args.registration_snippet}`;
5031
+ } else {
5032
+ templatePatch = "Could not find insertion point in hook file.";
5033
+ }
5034
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5035
+ }
5036
+ async function rollbackExtension(ctx, args) {
5037
+ const logPath = extendLogPath();
5038
+ if (!await fileExists(logPath)) {
5039
+ return { undone: false, message: "no apply found for library" };
5040
+ }
5041
+ const raw = await import_node_fs20.promises.readFile(logPath, "utf8");
5042
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5043
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5044
+ if (!match) {
5045
+ return { undone: false, message: "no apply found for library" };
5046
+ }
5047
+ const pkgPath = import_node_path34.default.join(ctx.scanPath, "package.json");
5048
+ if (await fileExists(pkgPath)) {
5049
+ const pkg = await readPackageJson(ctx.scanPath);
5050
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5051
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5052
+ pkg.dependencies = rest;
5053
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5054
+ }
5055
+ }
5056
+ const hookFiles = await findHookFiles(ctx.scanPath);
5057
+ for (const file of hookFiles) {
5058
+ const filePath = import_node_path34.default.join(ctx.scanPath, file);
5059
+ const content = await import_node_fs20.promises.readFile(filePath, "utf8");
5060
+ if (content.includes(match.registration_snippet)) {
5061
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5062
+ await import_node_fs20.promises.writeFile(filePath, filtered, "utf8");
5063
+ break;
5064
+ }
5065
+ }
5066
+ return {
5067
+ undone: true,
5068
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5069
+ };
5070
+ }
5071
+
4651
5072
  // src/divergences.ts
4652
5073
  init_cjs_shims();
4653
5074
  var import_types21 = require("@neat.is/types");
@@ -4888,7 +5309,7 @@ function computeDivergences(graph, opts = {}) {
4888
5309
 
4889
5310
  // src/diff.ts
4890
5311
  init_cjs_shims();
4891
- var import_node_fs19 = require("fs");
5312
+ var import_node_fs21 = require("fs");
4892
5313
  async function loadSnapshotForDiff(target) {
4893
5314
  if (/^https?:\/\//i.test(target)) {
4894
5315
  const res = await fetch(target);
@@ -4897,7 +5318,7 @@ async function loadSnapshotForDiff(target) {
4897
5318
  }
4898
5319
  return await res.json();
4899
5320
  }
4900
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5321
+ const raw = await import_node_fs21.promises.readFile(target, "utf8");
4901
5322
  return JSON.parse(raw);
4902
5323
  }
4903
5324
  function indexEntries(entries) {
@@ -4965,57 +5386,127 @@ function canonicalJson(value) {
4965
5386
 
4966
5387
  // src/registry.ts
4967
5388
  init_cjs_shims();
4968
- var import_node_fs20 = require("fs");
4969
- var import_node_os2 = __toESM(require("os"), 1);
4970
- var import_node_path33 = __toESM(require("path"), 1);
5389
+ var import_node_fs22 = require("fs");
5390
+ var import_node_os3 = __toESM(require("os"), 1);
5391
+ var import_node_path35 = __toESM(require("path"), 1);
4971
5392
  var import_types22 = require("@neat.is/types");
4972
5393
  var LOCK_TIMEOUT_MS = 5e3;
4973
5394
  var LOCK_RETRY_MS = 50;
4974
5395
  function neatHome() {
4975
5396
  const override = process.env.NEAT_HOME;
4976
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
4977
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5397
+ if (override && override.length > 0) return import_node_path35.default.resolve(override);
5398
+ return import_node_path35.default.join(import_node_os3.default.homedir(), ".neat");
4978
5399
  }
4979
5400
  function registryPath() {
4980
- return import_node_path33.default.join(neatHome(), "projects.json");
5401
+ return import_node_path35.default.join(neatHome(), "projects.json");
4981
5402
  }
4982
5403
  function registryLockPath() {
4983
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5404
+ return import_node_path35.default.join(neatHome(), "projects.json.lock");
5405
+ }
5406
+ function daemonPidPath() {
5407
+ return import_node_path35.default.join(neatHome(), "neatd.pid");
5408
+ }
5409
+ function isPidAliveDefault(pid) {
5410
+ try {
5411
+ process.kill(pid, 0);
5412
+ return true;
5413
+ } catch (err) {
5414
+ return err.code === "EPERM";
5415
+ }
5416
+ }
5417
+ async function readPidFile(file) {
5418
+ try {
5419
+ const raw = await import_node_fs22.promises.readFile(file, "utf8");
5420
+ const pid = Number.parseInt(raw.trim(), 10);
5421
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5422
+ } catch {
5423
+ return void 0;
5424
+ }
5425
+ }
5426
+ var defaultLockHolderProbe = {
5427
+ isPidAlive: isPidAliveDefault,
5428
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
5429
+ async daemonResponds() {
5430
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
5431
+ try {
5432
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
5433
+ return true;
5434
+ } catch {
5435
+ return false;
5436
+ }
5437
+ }
5438
+ };
5439
+ async function readLockPid(lockPath) {
5440
+ return readPidFile(lockPath);
5441
+ }
5442
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
5443
+ const lockPid = await readLockPid(lockPath);
5444
+ const daemonPid = await probe.daemonPidFromFile();
5445
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
5446
+ // Either confirms a live daemon is in the picture (the second guards
5447
+ // against a stale pidfile whose PID got reused).
5448
+ (daemonPid === lockPid || await probe.daemonResponds())) {
5449
+ return { kind: "daemon", pid: daemonPid };
5450
+ }
5451
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
5452
+ return { kind: "command", pid: lockPid };
5453
+ }
5454
+ return { kind: "stale" };
5455
+ }
5456
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
5457
+ switch (holder.kind) {
5458
+ case "daemon":
5459
+ return `The neat daemon (pid ${holder.pid}) is holding the registry lock. Register this project through the daemon, or stop neatd and re-run \`neat init\`.`;
5460
+ case "command":
5461
+ return `Another neat command (pid ${holder.pid}) is holding the registry lock. Wait for it to finish, or check \`ps\` if you're not sure what's running.`;
5462
+ case "stale":
5463
+ return `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`;
5464
+ }
4984
5465
  }
4985
5466
  async function writeAtomically(target, contents) {
4986
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
5467
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(target), { recursive: true });
4987
5468
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4988
- const fd = await import_node_fs20.promises.open(tmp, "w");
5469
+ const fd = await import_node_fs22.promises.open(tmp, "w");
4989
5470
  try {
4990
5471
  await fd.writeFile(contents, "utf8");
4991
5472
  await fd.sync();
4992
5473
  } finally {
4993
5474
  await fd.close();
4994
5475
  }
4995
- await import_node_fs20.promises.rename(tmp, target);
5476
+ await import_node_fs22.promises.rename(tmp, target);
4996
5477
  }
4997
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
5478
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
4998
5479
  const deadline = Date.now() + timeoutMs;
4999
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5480
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(lockPath), { recursive: true });
5481
+ let probedHolder = false;
5000
5482
  while (true) {
5001
5483
  try {
5002
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5003
- await fd.close();
5484
+ const fd = await import_node_fs22.promises.open(lockPath, "wx");
5485
+ try {
5486
+ await fd.writeFile(`${process.pid}
5487
+ `, "utf8");
5488
+ } finally {
5489
+ await fd.close();
5490
+ }
5004
5491
  return;
5005
5492
  } catch (err) {
5006
5493
  const code = err.code;
5007
5494
  if (code !== "EEXIST") throw err;
5495
+ if (!probedHolder) {
5496
+ probedHolder = true;
5497
+ const holder = await classifyLockHolder(lockPath, probe);
5498
+ if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5499
+ }
5008
5500
  if (Date.now() >= deadline) {
5009
- throw new Error(
5010
- `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
5011
- );
5501
+ const holder = await classifyLockHolder(lockPath, probe);
5502
+ throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5012
5503
  }
5013
5504
  await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
5014
5505
  }
5015
5506
  }
5016
5507
  }
5017
5508
  async function releaseLock(lockPath) {
5018
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5509
+ await import_node_fs22.promises.unlink(lockPath).catch(() => {
5019
5510
  });
5020
5511
  }
5021
5512
  async function withLock(fn) {
@@ -5031,7 +5522,7 @@ async function readRegistry() {
5031
5522
  const file = registryPath();
5032
5523
  let raw;
5033
5524
  try {
5034
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5525
+ raw = await import_node_fs22.promises.readFile(file, "utf8");
5035
5526
  } catch (err) {
5036
5527
  if (err.code === "ENOENT") {
5037
5528
  return { version: 1, projects: [] };
@@ -5531,6 +6022,105 @@ function registerRoutes(scope, ctx) {
5531
6022
  violations
5532
6023
  };
5533
6024
  });
6025
+ scope.get("/extend/list-uninstrumented", async (req2, reply) => {
6026
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6027
+ if (!proj) return;
6028
+ if (!proj.scanPath) {
6029
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6030
+ }
6031
+ try {
6032
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6033
+ return { libraries: results };
6034
+ } catch (err) {
6035
+ return reply.code(500).send({ error: err.message });
6036
+ }
6037
+ });
6038
+ scope.get("/extend/lookup", async (req2, reply) => {
6039
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6040
+ if (!proj) return;
6041
+ const { library, version } = req2.query;
6042
+ if (!library) {
6043
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6044
+ }
6045
+ const result = lookupInstrumentation(library, version);
6046
+ if (!result) {
6047
+ return reply.code(404).send({ error: "library not found in registry", library });
6048
+ }
6049
+ return result;
6050
+ });
6051
+ scope.get("/extend/describe", async (req2, reply) => {
6052
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6053
+ if (!proj) return;
6054
+ if (!proj.scanPath) {
6055
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6056
+ }
6057
+ try {
6058
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6059
+ return state;
6060
+ } catch (err) {
6061
+ return reply.code(500).send({ error: err.message });
6062
+ }
6063
+ });
6064
+ scope.post("/extend/apply", async (req2, reply) => {
6065
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6066
+ if (!proj) return;
6067
+ if (!proj.scanPath) {
6068
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6069
+ }
6070
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6071
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6072
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6073
+ }
6074
+ try {
6075
+ const result = await applyExtension(
6076
+ { project: proj.name, scanPath: proj.scanPath },
6077
+ { library, instrumentation_package, version, registration_snippet }
6078
+ );
6079
+ return result;
6080
+ } catch (err) {
6081
+ return reply.code(500).send({ error: err.message });
6082
+ }
6083
+ });
6084
+ scope.post("/extend/dry-run", async (req2, reply) => {
6085
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6086
+ if (!proj) return;
6087
+ if (!proj.scanPath) {
6088
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6089
+ }
6090
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6091
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6092
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6093
+ }
6094
+ try {
6095
+ const result = await dryRunExtension(
6096
+ { project: proj.name, scanPath: proj.scanPath },
6097
+ { library, instrumentation_package, version, registration_snippet }
6098
+ );
6099
+ return result;
6100
+ } catch (err) {
6101
+ return reply.code(500).send({ error: err.message });
6102
+ }
6103
+ });
6104
+ scope.post("/extend/rollback", async (req2, reply) => {
6105
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6106
+ if (!proj) return;
6107
+ if (!proj.scanPath) {
6108
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6109
+ }
6110
+ const { library } = req2.body ?? {};
6111
+ if (!library) {
6112
+ return reply.code(400).send({ error: "body must include library" });
6113
+ }
6114
+ try {
6115
+ const result = await rollbackExtension(
6116
+ { project: proj.name, scanPath: proj.scanPath },
6117
+ { library }
6118
+ );
6119
+ return result;
6120
+ } catch (err) {
6121
+ return reply.code(500).send({ error: err.message });
6122
+ }
6123
+ });
5534
6124
  }
5535
6125
  async function buildApi(opts) {
5536
6126
  const app = (0, import_fastify.default)({ logger: false });
@@ -5641,8 +6231,8 @@ init_auth();
5641
6231
 
5642
6232
  // src/unrouted.ts
5643
6233
  init_cjs_shims();
5644
- var import_node_fs21 = require("fs");
5645
- var import_node_path36 = __toESM(require("path"), 1);
6234
+ var import_node_fs23 = require("fs");
6235
+ var import_node_path38 = __toESM(require("path"), 1);
5646
6236
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5647
6237
  return {
5648
6238
  timestamp: now.toISOString(),
@@ -5652,21 +6242,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
5652
6242
  };
5653
6243
  }
5654
6244
  async function appendUnroutedSpan(neatHome3, record) {
5655
- const target = import_node_path36.default.join(neatHome3, "errors.ndjson");
5656
- await import_node_fs21.promises.mkdir(neatHome3, { recursive: true });
5657
- await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6245
+ const target = import_node_path38.default.join(neatHome3, "errors.ndjson");
6246
+ await import_node_fs23.promises.mkdir(neatHome3, { recursive: true });
6247
+ await import_node_fs23.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
5658
6248
  }
5659
6249
  function unroutedErrorsPath(neatHome3) {
5660
- return import_node_path36.default.join(neatHome3, "errors.ndjson");
6250
+ return import_node_path38.default.join(neatHome3, "errors.ndjson");
5661
6251
  }
5662
6252
 
5663
6253
  // src/daemon.ts
5664
6254
  function neatHomeFor(opts) {
5665
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
6255
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path39.default.resolve(opts.neatHome);
5666
6256
  const env = process.env.NEAT_HOME;
5667
- if (env && env.length > 0) return import_node_path37.default.resolve(env);
6257
+ if (env && env.length > 0) return import_node_path39.default.resolve(env);
5668
6258
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5669
- return import_node_path37.default.join(home, ".neat");
6259
+ return import_node_path39.default.join(home, ".neat");
5670
6260
  }
5671
6261
  function routeSpanToProject(serviceName, projects) {
5672
6262
  if (!serviceName) return DEFAULT_PROJECT;
@@ -5701,9 +6291,9 @@ function isTokenContained(needle, haystack) {
5701
6291
  return tokens.includes(needle);
5702
6292
  }
5703
6293
  async function bootstrapProject(entry2) {
5704
- const paths = pathsForProject(entry2.name, import_node_path37.default.join(entry2.path, "neat-out"));
6294
+ const paths = pathsForProject(entry2.name, import_node_path39.default.join(entry2.path, "neat-out"));
5705
6295
  try {
5706
- const stat = await import_node_fs22.promises.stat(entry2.path);
6296
+ const stat = await import_node_fs24.promises.stat(entry2.path);
5707
6297
  if (!stat.isDirectory()) {
5708
6298
  throw new Error(`registered path ${entry2.path} is not a directory`);
5709
6299
  }
@@ -5769,13 +6359,13 @@ async function startDaemon(opts = {}) {
5769
6359
  const home = neatHomeFor(opts);
5770
6360
  const regPath = registryPath();
5771
6361
  try {
5772
- await import_node_fs22.promises.access(regPath);
6362
+ await import_node_fs24.promises.access(regPath);
5773
6363
  } catch {
5774
6364
  throw new Error(
5775
6365
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5776
6366
  );
5777
6367
  }
5778
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6368
+ const pidPath = import_node_path39.default.join(home, "neatd.pid");
5779
6369
  await writeAtomically(pidPath, `${process.pid}
5780
6370
  `);
5781
6371
  const slots = /* @__PURE__ */ new Map();
@@ -5933,7 +6523,7 @@ async function startDaemon(opts = {}) {
5933
6523
  }
5934
6524
  if (restApp) await restApp.close().catch(() => {
5935
6525
  });
5936
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6526
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
5937
6527
  });
5938
6528
  throw new Error(
5939
6529
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -5989,6 +6579,7 @@ async function startDaemon(opts = {}) {
5989
6579
  {
5990
6580
  graph: slot.graph,
5991
6581
  errorsPath: slot.paths.errorsPath,
6582
+ scanPath: slot.entry.path,
5992
6583
  project: slot.entry.name,
5993
6584
  // Receiver already wrote the error event synchronously below.
5994
6585
  writeErrorEventInline: false
@@ -6012,6 +6603,7 @@ async function startDaemon(opts = {}) {
6012
6603
  {
6013
6604
  graph: slot.graph,
6014
6605
  errorsPath: slot.paths.errorsPath,
6606
+ scanPath: slot.entry.path,
6015
6607
  project: slot.entry.name,
6016
6608
  writeErrorEventInline: false
6017
6609
  },
@@ -6037,7 +6629,7 @@ async function startDaemon(opts = {}) {
6037
6629
  });
6038
6630
  if (otlpApp) await otlpApp.close().catch(() => {
6039
6631
  });
6040
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6632
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6041
6633
  });
6042
6634
  throw new Error(
6043
6635
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6083,9 +6675,9 @@ async function startDaemon(opts = {}) {
6083
6675
  let registryWatcher = null;
6084
6676
  let reloadTimer = null;
6085
6677
  try {
6086
- const regDir = import_node_path37.default.dirname(regPath);
6087
- const regBase = import_node_path37.default.basename(regPath);
6088
- registryWatcher = (0, import_node_fs22.watch)(regDir, (_eventType, filename) => {
6678
+ const regDir = import_node_path39.default.dirname(regPath);
6679
+ const regBase = import_node_path39.default.basename(regPath);
6680
+ registryWatcher = (0, import_node_fs24.watch)(regDir, (_eventType, filename) => {
6089
6681
  if (filename !== null && filename !== regBase) return;
6090
6682
  if (reloadTimer) clearTimeout(reloadTimer);
6091
6683
  reloadTimer = setTimeout(() => {
@@ -6128,7 +6720,7 @@ async function startDaemon(opts = {}) {
6128
6720
  } catch {
6129
6721
  }
6130
6722
  }
6131
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6723
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6132
6724
  });
6133
6725
  };
6134
6726
  return {
@@ -6148,9 +6740,9 @@ init_auth();
6148
6740
 
6149
6741
  // src/web-spawn.ts
6150
6742
  init_cjs_shims();
6151
- var import_node_child_process = require("child_process");
6743
+ var import_node_child_process2 = require("child_process");
6152
6744
  var import_node_net = __toESM(require("net"), 1);
6153
- var import_node_path38 = __toESM(require("path"), 1);
6745
+ var import_node_path40 = __toESM(require("path"), 1);
6154
6746
  var DEFAULT_WEB_PORT = 6328;
6155
6747
  async function assertPortFree(port) {
6156
6748
  await new Promise((resolve, reject) => {
@@ -6178,10 +6770,10 @@ function resolveWebPackageDir() {
6178
6770
  eval("require")
6179
6771
  );
6180
6772
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
6181
- return import_node_path38.default.dirname(pkgJsonPath);
6773
+ return import_node_path40.default.dirname(pkgJsonPath);
6182
6774
  }
6183
6775
  function resolveStandaloneServerEntry(webDir) {
6184
- return import_node_path38.default.join(webDir, ".next/standalone/packages/web/server.js");
6776
+ return import_node_path40.default.join(webDir, ".next/standalone/packages/web/server.js");
6185
6777
  }
6186
6778
  async function spawnWebUI(restPort) {
6187
6779
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -6205,8 +6797,8 @@ async function spawnWebUI(restPort) {
6205
6797
  HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
6206
6798
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
6207
6799
  };
6208
- const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
6209
- cwd: import_node_path38.default.dirname(serverEntry),
6800
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
6801
+ cwd: import_node_path40.default.dirname(serverEntry),
6210
6802
  env,
6211
6803
  stdio: ["ignore", "inherit", "inherit"],
6212
6804
  detached: false
@@ -6319,14 +6911,14 @@ function localVersion() {
6319
6911
  }
6320
6912
  function neatHome2() {
6321
6913
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
6322
- return import_node_path39.default.resolve(process.env.NEAT_HOME);
6914
+ return import_node_path41.default.resolve(process.env.NEAT_HOME);
6323
6915
  }
6324
6916
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6325
- return import_node_path39.default.join(home, ".neat");
6917
+ return import_node_path41.default.join(home, ".neat");
6326
6918
  }
6327
6919
  async function readPid() {
6328
6920
  try {
6329
- const raw = await import_node_fs23.promises.readFile(import_node_path39.default.join(neatHome2(), "neatd.pid"), "utf8");
6921
+ const raw = await import_node_fs25.promises.readFile(import_node_path41.default.join(neatHome2(), "neatd.pid"), "utf8");
6330
6922
  const n = Number.parseInt(raw.trim(), 10);
6331
6923
  return Number.isFinite(n) ? n : null;
6332
6924
  } catch {