@neat.is/core 0.4.10 → 0.4.11
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-J5CEKCTR.js → chunk-OJHI33LD.js} +167 -20
- package/dist/chunk-OJHI33LD.js.map +1 -0
- package/dist/{chunk-RC3CIDZO.js → chunk-W4RNPPB5.js} +4 -2
- package/dist/{chunk-RC3CIDZO.js.map → chunk-W4RNPPB5.js.map} +1 -1
- package/dist/cli.cjs +415 -38
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +254 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +164 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +164 -15
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +87 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-J5CEKCTR.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -233,6 +233,7 @@ declare function startOtelGrpcReceiver(opts: BuildOtelGrpcReceiverOptions & {
|
|
|
233
233
|
interface IngestContext {
|
|
234
234
|
graph: NeatGraph;
|
|
235
235
|
errorsPath: string;
|
|
236
|
+
scanPath?: string;
|
|
236
237
|
project?: string;
|
|
237
238
|
now?: () => number;
|
|
238
239
|
writeErrorEventInline?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -233,6 +233,7 @@ declare function startOtelGrpcReceiver(opts: BuildOtelGrpcReceiverOptions & {
|
|
|
233
233
|
interface IngestContext {
|
|
234
234
|
graph: NeatGraph;
|
|
235
235
|
errorsPath: string;
|
|
236
|
+
scanPath?: string;
|
|
236
237
|
project?: string;
|
|
237
238
|
now?: () => number;
|
|
238
239
|
writeErrorEventInline?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-W4RNPPB5.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-OJHI33LD.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
43
|
} from "./chunk-3QCRUEQD.js";
|
package/dist/neatd.cjs
CHANGED
|
@@ -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();
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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();
|
|
@@ -1954,12 +2024,13 @@ async function handleSpan(ctx, span) {
|
|
|
1954
2024
|
const isError = span.statusCode === 2;
|
|
1955
2025
|
cacheSpanService(span, nowMs);
|
|
1956
2026
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1957
|
-
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
2027
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1958
2028
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1959
2029
|
let affectedNode = sourceId;
|
|
2030
|
+
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1960
2031
|
if (span.dbSystem) {
|
|
1961
2032
|
const host = pickAddress(span);
|
|
1962
|
-
if (host) {
|
|
2033
|
+
if (mintsFromCallerSide && host) {
|
|
1963
2034
|
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
1964
2035
|
const targetId = (0, import_types3.databaseId)(host);
|
|
1965
2036
|
const result = upsertObservedEdge(
|
|
@@ -1975,7 +2046,7 @@ async function handleSpan(ctx, span) {
|
|
|
1975
2046
|
} else {
|
|
1976
2047
|
const host = pickAddress(span);
|
|
1977
2048
|
let resolvedViaAddress = false;
|
|
1978
|
-
if (host && host !== span.service) {
|
|
2049
|
+
if (mintsFromCallerSide && host && host !== span.service) {
|
|
1979
2050
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1980
2051
|
if (targetId && targetId !== sourceId) {
|
|
1981
2052
|
upsertObservedEdge(
|
|
@@ -3686,8 +3757,14 @@ function collectStringLiterals(node, out) {
|
|
|
3686
3757
|
if (child) collectStringLiterals(child, out);
|
|
3687
3758
|
}
|
|
3688
3759
|
}
|
|
3760
|
+
var PARSE_CHUNK = 16384;
|
|
3761
|
+
function parseSource(parser, source) {
|
|
3762
|
+
return parser.parse(
|
|
3763
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3689
3766
|
function callsFromSource(source, parser, knownHosts) {
|
|
3690
|
-
const tree = parser
|
|
3767
|
+
const tree = parseSource(parser, source);
|
|
3691
3768
|
const literals = [];
|
|
3692
3769
|
collectStringLiterals(tree.rootNode, literals);
|
|
3693
3770
|
const out = [];
|
|
@@ -4982,6 +5059,66 @@ function registryPath() {
|
|
|
4982
5059
|
function registryLockPath() {
|
|
4983
5060
|
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
4984
5061
|
}
|
|
5062
|
+
function daemonPidPath() {
|
|
5063
|
+
return import_node_path33.default.join(neatHome(), "neatd.pid");
|
|
5064
|
+
}
|
|
5065
|
+
function isPidAliveDefault(pid) {
|
|
5066
|
+
try {
|
|
5067
|
+
process.kill(pid, 0);
|
|
5068
|
+
return true;
|
|
5069
|
+
} catch (err) {
|
|
5070
|
+
return err.code === "EPERM";
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
async function readPidFile(file) {
|
|
5074
|
+
try {
|
|
5075
|
+
const raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
5076
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
5077
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
5078
|
+
} catch {
|
|
5079
|
+
return void 0;
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
var defaultLockHolderProbe = {
|
|
5083
|
+
isPidAlive: isPidAliveDefault,
|
|
5084
|
+
daemonPidFromFile: () => readPidFile(daemonPidPath()),
|
|
5085
|
+
async daemonResponds() {
|
|
5086
|
+
const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
5087
|
+
try {
|
|
5088
|
+
await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
|
|
5089
|
+
return true;
|
|
5090
|
+
} catch {
|
|
5091
|
+
return false;
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
};
|
|
5095
|
+
async function readLockPid(lockPath) {
|
|
5096
|
+
return readPidFile(lockPath);
|
|
5097
|
+
}
|
|
5098
|
+
async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
|
|
5099
|
+
const lockPid = await readLockPid(lockPath);
|
|
5100
|
+
const daemonPid = await probe.daemonPidFromFile();
|
|
5101
|
+
if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
|
|
5102
|
+
// Either confirms a live daemon is in the picture (the second guards
|
|
5103
|
+
// against a stale pidfile whose PID got reused).
|
|
5104
|
+
(daemonPid === lockPid || await probe.daemonResponds())) {
|
|
5105
|
+
return { kind: "daemon", pid: daemonPid };
|
|
5106
|
+
}
|
|
5107
|
+
if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
|
|
5108
|
+
return { kind: "command", pid: lockPid };
|
|
5109
|
+
}
|
|
5110
|
+
return { kind: "stale" };
|
|
5111
|
+
}
|
|
5112
|
+
function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
5113
|
+
switch (holder.kind) {
|
|
5114
|
+
case "daemon":
|
|
5115
|
+
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\`.`;
|
|
5116
|
+
case "command":
|
|
5117
|
+
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.`;
|
|
5118
|
+
case "stale":
|
|
5119
|
+
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.`;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
4985
5122
|
async function writeAtomically(target, contents) {
|
|
4986
5123
|
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4987
5124
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
@@ -4994,21 +5131,31 @@ async function writeAtomically(target, contents) {
|
|
|
4994
5131
|
}
|
|
4995
5132
|
await import_node_fs20.promises.rename(tmp, target);
|
|
4996
5133
|
}
|
|
4997
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
5134
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
4998
5135
|
const deadline = Date.now() + timeoutMs;
|
|
4999
5136
|
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
5137
|
+
let probedHolder = false;
|
|
5000
5138
|
while (true) {
|
|
5001
5139
|
try {
|
|
5002
5140
|
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
5003
|
-
|
|
5141
|
+
try {
|
|
5142
|
+
await fd.writeFile(`${process.pid}
|
|
5143
|
+
`, "utf8");
|
|
5144
|
+
} finally {
|
|
5145
|
+
await fd.close();
|
|
5146
|
+
}
|
|
5004
5147
|
return;
|
|
5005
5148
|
} catch (err) {
|
|
5006
5149
|
const code = err.code;
|
|
5007
5150
|
if (code !== "EEXIST") throw err;
|
|
5151
|
+
if (!probedHolder) {
|
|
5152
|
+
probedHolder = true;
|
|
5153
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
5154
|
+
if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
5155
|
+
}
|
|
5008
5156
|
if (Date.now() >= deadline) {
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
);
|
|
5157
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
5158
|
+
throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
5012
5159
|
}
|
|
5013
5160
|
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
5014
5161
|
}
|
|
@@ -5989,6 +6136,7 @@ async function startDaemon(opts = {}) {
|
|
|
5989
6136
|
{
|
|
5990
6137
|
graph: slot.graph,
|
|
5991
6138
|
errorsPath: slot.paths.errorsPath,
|
|
6139
|
+
scanPath: slot.entry.path,
|
|
5992
6140
|
project: slot.entry.name,
|
|
5993
6141
|
// Receiver already wrote the error event synchronously below.
|
|
5994
6142
|
writeErrorEventInline: false
|
|
@@ -6012,6 +6160,7 @@ async function startDaemon(opts = {}) {
|
|
|
6012
6160
|
{
|
|
6013
6161
|
graph: slot.graph,
|
|
6014
6162
|
errorsPath: slot.paths.errorsPath,
|
|
6163
|
+
scanPath: slot.entry.path,
|
|
6015
6164
|
project: slot.entry.name,
|
|
6016
6165
|
writeErrorEventInline: false
|
|
6017
6166
|
},
|