@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.cjs
CHANGED
|
@@ -675,6 +675,7 @@ init_cjs_shims();
|
|
|
675
675
|
init_cjs_shims();
|
|
676
676
|
var import_node_fs3 = require("fs");
|
|
677
677
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
678
|
+
var sourceMapJs = __toESM(require("source-map-js"), 1);
|
|
678
679
|
|
|
679
680
|
// src/policy.ts
|
|
680
681
|
init_cjs_shims();
|
|
@@ -1679,6 +1680,14 @@ function warnUnidentifiedSpan(project) {
|
|
|
1679
1680
|
`[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
|
|
1680
1681
|
);
|
|
1681
1682
|
}
|
|
1683
|
+
var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
|
|
1684
|
+
function warnNoSourceMaps(serviceName) {
|
|
1685
|
+
if (noSourceMapWarnedServices.has(serviceName)) return;
|
|
1686
|
+
noSourceMapWarnedServices.add(serviceName);
|
|
1687
|
+
console.warn(
|
|
1688
|
+
`[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.`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1682
1691
|
function pickAttr(span, ...keys) {
|
|
1683
1692
|
for (const k of keys) {
|
|
1684
1693
|
const v = span.attributes[k];
|
|
@@ -1721,8 +1730,13 @@ function languageForExt(relPath) {
|
|
|
1721
1730
|
return void 0;
|
|
1722
1731
|
}
|
|
1723
1732
|
}
|
|
1724
|
-
function relPathForRuntimeFile(filepath, serviceNode) {
|
|
1733
|
+
function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
|
|
1725
1734
|
let p = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1735
|
+
if (scanPath && scanPath.length > 0) {
|
|
1736
|
+
const absRoot = toPosix(import_node_path3.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
|
|
1737
|
+
const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
|
|
1738
|
+
if (p.startsWith(anchor)) return p.slice(anchor.length);
|
|
1739
|
+
}
|
|
1726
1740
|
const root = serviceNode?.repoPath;
|
|
1727
1741
|
if (root && root !== "." && root.length > 0) {
|
|
1728
1742
|
const rootPosix = toPosix(root);
|
|
@@ -1739,16 +1753,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
|
|
|
1739
1753
|
p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
|
|
1740
1754
|
return p.length > 0 ? p : null;
|
|
1741
1755
|
}
|
|
1742
|
-
|
|
1756
|
+
var sourceMapCache = /* @__PURE__ */ new Map();
|
|
1757
|
+
function resolveDistToSrc(absFilepath, line) {
|
|
1758
|
+
if (!absFilepath.endsWith(".js")) return null;
|
|
1759
|
+
let entry = sourceMapCache.get(absFilepath);
|
|
1760
|
+
if (entry === void 0) {
|
|
1761
|
+
entry = null;
|
|
1762
|
+
const mapPath = `${absFilepath}.map`;
|
|
1763
|
+
try {
|
|
1764
|
+
if ((0, import_node_fs3.existsSync)(mapPath)) {
|
|
1765
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(mapPath, "utf8"));
|
|
1766
|
+
const consumer = new sourceMapJs.SourceMapConsumer(raw);
|
|
1767
|
+
entry = { consumer, dir: import_node_path3.default.dirname(mapPath) };
|
|
1768
|
+
}
|
|
1769
|
+
} catch {
|
|
1770
|
+
entry = null;
|
|
1771
|
+
}
|
|
1772
|
+
sourceMapCache.set(absFilepath, entry);
|
|
1773
|
+
}
|
|
1774
|
+
if (!entry) return null;
|
|
1775
|
+
try {
|
|
1776
|
+
const pos = entry.consumer.originalPositionFor({
|
|
1777
|
+
line: line !== void 0 && Number.isFinite(line) ? line : 1,
|
|
1778
|
+
column: 0
|
|
1779
|
+
});
|
|
1780
|
+
if (!pos || !pos.source) return null;
|
|
1781
|
+
const root = entry.consumer.sourceRoot ?? "";
|
|
1782
|
+
const resolved = import_node_path3.default.resolve(entry.dir, root, pos.source);
|
|
1783
|
+
return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
|
|
1784
|
+
} catch {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
1743
1789
|
const filepath = span.attributes[CODE_FILEPATH_ATTR];
|
|
1744
1790
|
if (typeof filepath !== "string" || filepath.length === 0) return null;
|
|
1745
|
-
const relPath = relPathForRuntimeFile(filepath, serviceNode);
|
|
1746
|
-
if (!relPath) return null;
|
|
1747
1791
|
const linenoRaw = span.attributes[CODE_LINENO_ATTR];
|
|
1748
|
-
|
|
1792
|
+
let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1793
|
+
const abs = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1794
|
+
const resolved = resolveDistToSrc(abs, line);
|
|
1795
|
+
let effectivePath = filepath;
|
|
1796
|
+
let originalRelPath;
|
|
1797
|
+
if (resolved) {
|
|
1798
|
+
originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
|
|
1799
|
+
effectivePath = resolved.filepath;
|
|
1800
|
+
if (resolved.line !== void 0) line = resolved.line;
|
|
1801
|
+
}
|
|
1802
|
+
const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
|
|
1803
|
+
if (!relPath) return null;
|
|
1804
|
+
if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
|
|
1805
|
+
warnNoSourceMaps(serviceNode.name);
|
|
1806
|
+
}
|
|
1749
1807
|
const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
|
|
1750
1808
|
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1751
|
-
return {
|
|
1809
|
+
return {
|
|
1810
|
+
relPath,
|
|
1811
|
+
...line !== void 0 ? { line } : {},
|
|
1812
|
+
...fn ? { fn } : {},
|
|
1813
|
+
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1814
|
+
};
|
|
1752
1815
|
}
|
|
1753
1816
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1754
1817
|
const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
|
|
@@ -1760,6 +1823,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1760
1823
|
service: serviceName,
|
|
1761
1824
|
path: callSite.relPath,
|
|
1762
1825
|
...language ? { language } : {},
|
|
1826
|
+
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1763
1827
|
discoveredVia: "otel"
|
|
1764
1828
|
};
|
|
1765
1829
|
graph.addNode(fileNodeId, node);
|
|
@@ -1785,6 +1849,12 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1785
1849
|
}
|
|
1786
1850
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1787
1851
|
var STITCH_MAX_DEPTH = 2;
|
|
1852
|
+
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1853
|
+
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1854
|
+
function spanMintsObservedEdge(kind) {
|
|
1855
|
+
if (kind === void 0 || kind === 0) return true;
|
|
1856
|
+
return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
|
|
1857
|
+
}
|
|
1788
1858
|
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
1789
1859
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1790
1860
|
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
@@ -2020,12 +2090,13 @@ async function handleSpan(ctx, span) {
|
|
|
2020
2090
|
const isError = span.statusCode === 2;
|
|
2021
2091
|
cacheSpanService(span, nowMs);
|
|
2022
2092
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
2023
|
-
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
2093
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2024
2094
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2025
2095
|
let affectedNode = sourceId;
|
|
2096
|
+
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2026
2097
|
if (span.dbSystem) {
|
|
2027
2098
|
const host = pickAddress(span);
|
|
2028
|
-
if (host) {
|
|
2099
|
+
if (mintsFromCallerSide && host) {
|
|
2029
2100
|
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
2030
2101
|
const targetId = (0, import_types3.databaseId)(host);
|
|
2031
2102
|
const result = upsertObservedEdge(
|
|
@@ -2041,7 +2112,7 @@ async function handleSpan(ctx, span) {
|
|
|
2041
2112
|
} else {
|
|
2042
2113
|
const host = pickAddress(span);
|
|
2043
2114
|
let resolvedViaAddress = false;
|
|
2044
|
-
if (host && host !== span.service) {
|
|
2115
|
+
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2045
2116
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2046
2117
|
if (targetId && targetId !== sourceId) {
|
|
2047
2118
|
upsertObservedEdge(
|
|
@@ -3825,8 +3896,14 @@ function collectStringLiterals(node, out) {
|
|
|
3825
3896
|
if (child) collectStringLiterals(child, out);
|
|
3826
3897
|
}
|
|
3827
3898
|
}
|
|
3899
|
+
var PARSE_CHUNK = 16384;
|
|
3900
|
+
function parseSource(parser, source) {
|
|
3901
|
+
return parser.parse(
|
|
3902
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3828
3905
|
function callsFromSource(source, parser, knownHosts) {
|
|
3829
|
-
const tree = parser
|
|
3906
|
+
const tree = parseSource(parser, source);
|
|
3830
3907
|
const literals = [];
|
|
3831
3908
|
collectStringLiterals(tree.rootNode, literals);
|
|
3832
3909
|
const out = [];
|
|
@@ -5121,6 +5198,66 @@ function registryPath() {
|
|
|
5121
5198
|
function registryLockPath() {
|
|
5122
5199
|
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
5123
5200
|
}
|
|
5201
|
+
function daemonPidPath() {
|
|
5202
|
+
return import_node_path33.default.join(neatHome(), "neatd.pid");
|
|
5203
|
+
}
|
|
5204
|
+
function isPidAliveDefault(pid) {
|
|
5205
|
+
try {
|
|
5206
|
+
process.kill(pid, 0);
|
|
5207
|
+
return true;
|
|
5208
|
+
} catch (err) {
|
|
5209
|
+
return err.code === "EPERM";
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
async function readPidFile(file) {
|
|
5213
|
+
try {
|
|
5214
|
+
const raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
5215
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
5216
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
5217
|
+
} catch {
|
|
5218
|
+
return void 0;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
var defaultLockHolderProbe = {
|
|
5222
|
+
isPidAlive: isPidAliveDefault,
|
|
5223
|
+
daemonPidFromFile: () => readPidFile(daemonPidPath()),
|
|
5224
|
+
async daemonResponds() {
|
|
5225
|
+
const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
5226
|
+
try {
|
|
5227
|
+
await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
|
|
5228
|
+
return true;
|
|
5229
|
+
} catch {
|
|
5230
|
+
return false;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
async function readLockPid(lockPath) {
|
|
5235
|
+
return readPidFile(lockPath);
|
|
5236
|
+
}
|
|
5237
|
+
async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
|
|
5238
|
+
const lockPid = await readLockPid(lockPath);
|
|
5239
|
+
const daemonPid = await probe.daemonPidFromFile();
|
|
5240
|
+
if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
|
|
5241
|
+
// Either confirms a live daemon is in the picture (the second guards
|
|
5242
|
+
// against a stale pidfile whose PID got reused).
|
|
5243
|
+
(daemonPid === lockPid || await probe.daemonResponds())) {
|
|
5244
|
+
return { kind: "daemon", pid: daemonPid };
|
|
5245
|
+
}
|
|
5246
|
+
if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
|
|
5247
|
+
return { kind: "command", pid: lockPid };
|
|
5248
|
+
}
|
|
5249
|
+
return { kind: "stale" };
|
|
5250
|
+
}
|
|
5251
|
+
function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
5252
|
+
switch (holder.kind) {
|
|
5253
|
+
case "daemon":
|
|
5254
|
+
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\`.`;
|
|
5255
|
+
case "command":
|
|
5256
|
+
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.`;
|
|
5257
|
+
case "stale":
|
|
5258
|
+
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.`;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5124
5261
|
async function normalizeProjectPath(input) {
|
|
5125
5262
|
const resolved = import_node_path33.default.resolve(input);
|
|
5126
5263
|
try {
|
|
@@ -5141,21 +5278,31 @@ async function writeAtomically(target, contents) {
|
|
|
5141
5278
|
}
|
|
5142
5279
|
await import_node_fs20.promises.rename(tmp, target);
|
|
5143
5280
|
}
|
|
5144
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
5281
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
5145
5282
|
const deadline = Date.now() + timeoutMs;
|
|
5146
5283
|
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
5284
|
+
let probedHolder = false;
|
|
5147
5285
|
while (true) {
|
|
5148
5286
|
try {
|
|
5149
5287
|
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
5150
|
-
|
|
5288
|
+
try {
|
|
5289
|
+
await fd.writeFile(`${process.pid}
|
|
5290
|
+
`, "utf8");
|
|
5291
|
+
} finally {
|
|
5292
|
+
await fd.close();
|
|
5293
|
+
}
|
|
5151
5294
|
return;
|
|
5152
5295
|
} catch (err) {
|
|
5153
5296
|
const code = err.code;
|
|
5154
5297
|
if (code !== "EEXIST") throw err;
|
|
5298
|
+
if (!probedHolder) {
|
|
5299
|
+
probedHolder = true;
|
|
5300
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
5301
|
+
if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
5302
|
+
}
|
|
5155
5303
|
if (Date.now() >= deadline) {
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
);
|
|
5304
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
5305
|
+
throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
5159
5306
|
}
|
|
5160
5307
|
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
5161
5308
|
}
|
|
@@ -6193,6 +6340,7 @@ async function startDaemon(opts = {}) {
|
|
|
6193
6340
|
{
|
|
6194
6341
|
graph: slot.graph,
|
|
6195
6342
|
errorsPath: slot.paths.errorsPath,
|
|
6343
|
+
scanPath: slot.entry.path,
|
|
6196
6344
|
project: slot.entry.name,
|
|
6197
6345
|
// Receiver already wrote the error event synchronously below.
|
|
6198
6346
|
writeErrorEventInline: false
|
|
@@ -6216,6 +6364,7 @@ async function startDaemon(opts = {}) {
|
|
|
6216
6364
|
{
|
|
6217
6365
|
graph: slot.graph,
|
|
6218
6366
|
errorsPath: slot.paths.errorsPath,
|
|
6367
|
+
scanPath: slot.entry.path,
|
|
6219
6368
|
project: slot.entry.name,
|
|
6220
6369
|
writeErrorEventInline: false
|
|
6221
6370
|
},
|