@neat.is/core 0.4.9 → 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
|
@@ -655,8 +655,9 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
// src/ingest.ts
|
|
658
|
-
import { promises as fs3 } from "fs";
|
|
658
|
+
import { promises as fs3, existsSync, readFileSync } from "fs";
|
|
659
659
|
import path3 from "path";
|
|
660
|
+
import * as sourceMapJs from "source-map-js";
|
|
660
661
|
|
|
661
662
|
// src/policy.ts
|
|
662
663
|
import { promises as fs2 } from "fs";
|
|
@@ -1103,6 +1104,14 @@ function warnUnidentifiedSpan(project) {
|
|
|
1103
1104
|
`[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
|
|
1104
1105
|
);
|
|
1105
1106
|
}
|
|
1107
|
+
var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
|
|
1108
|
+
function warnNoSourceMaps(serviceName) {
|
|
1109
|
+
if (noSourceMapWarnedServices.has(serviceName)) return;
|
|
1110
|
+
noSourceMapWarnedServices.add(serviceName);
|
|
1111
|
+
console.warn(
|
|
1112
|
+
`[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.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1106
1115
|
function pickAttr(span, ...keys) {
|
|
1107
1116
|
for (const k of keys) {
|
|
1108
1117
|
const v = span.attributes[k];
|
|
@@ -1145,8 +1154,13 @@ function languageForExt(relPath) {
|
|
|
1145
1154
|
return void 0;
|
|
1146
1155
|
}
|
|
1147
1156
|
}
|
|
1148
|
-
function relPathForRuntimeFile(filepath, serviceNode) {
|
|
1157
|
+
function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
|
|
1149
1158
|
let p = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1159
|
+
if (scanPath && scanPath.length > 0) {
|
|
1160
|
+
const absRoot = toPosix(path3.resolve(scanPath, serviceNode?.repoPath ?? ""));
|
|
1161
|
+
const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
|
|
1162
|
+
if (p.startsWith(anchor)) return p.slice(anchor.length);
|
|
1163
|
+
}
|
|
1150
1164
|
const root = serviceNode?.repoPath;
|
|
1151
1165
|
if (root && root !== "." && root.length > 0) {
|
|
1152
1166
|
const rootPosix = toPosix(root);
|
|
@@ -1163,16 +1177,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
|
|
|
1163
1177
|
p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
|
|
1164
1178
|
return p.length > 0 ? p : null;
|
|
1165
1179
|
}
|
|
1166
|
-
|
|
1180
|
+
var sourceMapCache = /* @__PURE__ */ new Map();
|
|
1181
|
+
function resolveDistToSrc(absFilepath, line) {
|
|
1182
|
+
if (!absFilepath.endsWith(".js")) return null;
|
|
1183
|
+
let entry = sourceMapCache.get(absFilepath);
|
|
1184
|
+
if (entry === void 0) {
|
|
1185
|
+
entry = null;
|
|
1186
|
+
const mapPath = `${absFilepath}.map`;
|
|
1187
|
+
try {
|
|
1188
|
+
if (existsSync(mapPath)) {
|
|
1189
|
+
const raw = JSON.parse(readFileSync(mapPath, "utf8"));
|
|
1190
|
+
const consumer = new sourceMapJs.SourceMapConsumer(raw);
|
|
1191
|
+
entry = { consumer, dir: path3.dirname(mapPath) };
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
entry = null;
|
|
1195
|
+
}
|
|
1196
|
+
sourceMapCache.set(absFilepath, entry);
|
|
1197
|
+
}
|
|
1198
|
+
if (!entry) return null;
|
|
1199
|
+
try {
|
|
1200
|
+
const pos = entry.consumer.originalPositionFor({
|
|
1201
|
+
line: line !== void 0 && Number.isFinite(line) ? line : 1,
|
|
1202
|
+
column: 0
|
|
1203
|
+
});
|
|
1204
|
+
if (!pos || !pos.source) return null;
|
|
1205
|
+
const root = entry.consumer.sourceRoot ?? "";
|
|
1206
|
+
const resolved = path3.resolve(entry.dir, root, pos.source);
|
|
1207
|
+
return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
|
|
1208
|
+
} catch {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
1167
1213
|
const filepath = span.attributes[CODE_FILEPATH_ATTR];
|
|
1168
1214
|
if (typeof filepath !== "string" || filepath.length === 0) return null;
|
|
1169
|
-
const relPath = relPathForRuntimeFile(filepath, serviceNode);
|
|
1170
|
-
if (!relPath) return null;
|
|
1171
1215
|
const linenoRaw = span.attributes[CODE_LINENO_ATTR];
|
|
1172
|
-
|
|
1216
|
+
let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1217
|
+
const abs = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1218
|
+
const resolved = resolveDistToSrc(abs, line);
|
|
1219
|
+
let effectivePath = filepath;
|
|
1220
|
+
let originalRelPath;
|
|
1221
|
+
if (resolved) {
|
|
1222
|
+
originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
|
|
1223
|
+
effectivePath = resolved.filepath;
|
|
1224
|
+
if (resolved.line !== void 0) line = resolved.line;
|
|
1225
|
+
}
|
|
1226
|
+
const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
|
|
1227
|
+
if (!relPath) return null;
|
|
1228
|
+
if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
|
|
1229
|
+
warnNoSourceMaps(serviceNode.name);
|
|
1230
|
+
}
|
|
1173
1231
|
const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
|
|
1174
1232
|
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1175
|
-
return {
|
|
1233
|
+
return {
|
|
1234
|
+
relPath,
|
|
1235
|
+
...line !== void 0 ? { line } : {},
|
|
1236
|
+
...fn ? { fn } : {},
|
|
1237
|
+
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1238
|
+
};
|
|
1176
1239
|
}
|
|
1177
1240
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1178
1241
|
const fileNodeId = fileId(serviceName, callSite.relPath);
|
|
@@ -1184,6 +1247,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1184
1247
|
service: serviceName,
|
|
1185
1248
|
path: callSite.relPath,
|
|
1186
1249
|
...language ? { language } : {},
|
|
1250
|
+
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1187
1251
|
discoveredVia: "otel"
|
|
1188
1252
|
};
|
|
1189
1253
|
graph.addNode(fileNodeId, node);
|
|
@@ -1209,6 +1273,12 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1209
1273
|
}
|
|
1210
1274
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1211
1275
|
var STITCH_MAX_DEPTH = 2;
|
|
1276
|
+
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1277
|
+
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1278
|
+
function spanMintsObservedEdge(kind) {
|
|
1279
|
+
if (kind === void 0 || kind === 0) return true;
|
|
1280
|
+
return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
|
|
1281
|
+
}
|
|
1212
1282
|
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
1213
1283
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1214
1284
|
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
@@ -1444,12 +1514,13 @@ async function handleSpan(ctx, span) {
|
|
|
1444
1514
|
const isError = span.statusCode === 2;
|
|
1445
1515
|
cacheSpanService(span, nowMs);
|
|
1446
1516
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1447
|
-
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
1517
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1448
1518
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1449
1519
|
let affectedNode = sourceId;
|
|
1520
|
+
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1450
1521
|
if (span.dbSystem) {
|
|
1451
1522
|
const host = pickAddress(span);
|
|
1452
|
-
if (host) {
|
|
1523
|
+
if (mintsFromCallerSide && host) {
|
|
1453
1524
|
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
1454
1525
|
const targetId = databaseId(host);
|
|
1455
1526
|
const result = upsertObservedEdge(
|
|
@@ -1465,7 +1536,7 @@ async function handleSpan(ctx, span) {
|
|
|
1465
1536
|
} else {
|
|
1466
1537
|
const host = pickAddress(span);
|
|
1467
1538
|
let resolvedViaAddress = false;
|
|
1468
|
-
if (host && host !== span.service) {
|
|
1539
|
+
if (mintsFromCallerSide && host && host !== span.service) {
|
|
1469
1540
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1470
1541
|
if (targetId && targetId !== sourceId) {
|
|
1471
1542
|
upsertObservedEdge(
|
|
@@ -3270,8 +3341,14 @@ function collectStringLiterals(node, out) {
|
|
|
3270
3341
|
if (child) collectStringLiterals(child, out);
|
|
3271
3342
|
}
|
|
3272
3343
|
}
|
|
3344
|
+
var PARSE_CHUNK = 16384;
|
|
3345
|
+
function parseSource(parser, source) {
|
|
3346
|
+
return parser.parse(
|
|
3347
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3273
3350
|
function callsFromSource(source, parser, knownHosts) {
|
|
3274
|
-
const tree = parser
|
|
3351
|
+
const tree = parseSource(parser, source);
|
|
3275
3352
|
const literals = [];
|
|
3276
3353
|
collectStringLiterals(tree.rootNode, literals);
|
|
3277
3354
|
const out = [];
|
|
@@ -3952,7 +4029,7 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3952
4029
|
import path30 from "path";
|
|
3953
4030
|
|
|
3954
4031
|
// src/extract/retire.ts
|
|
3955
|
-
import { existsSync } from "fs";
|
|
4032
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3956
4033
|
import path29 from "path";
|
|
3957
4034
|
import { NodeType as NodeType11, Provenance as Provenance9 } from "@neat.is/types";
|
|
3958
4035
|
function dropOrphanedFileNodes(graph) {
|
|
@@ -3988,10 +4065,10 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
3988
4065
|
const evidenceFile = edge.evidence?.file;
|
|
3989
4066
|
if (!evidenceFile) return;
|
|
3990
4067
|
if (path29.isAbsolute(evidenceFile)) {
|
|
3991
|
-
if (!
|
|
4068
|
+
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
3992
4069
|
return;
|
|
3993
4070
|
}
|
|
3994
|
-
const found = bases.some((base) =>
|
|
4071
|
+
const found = bases.some((base) => existsSync2(path29.join(base, evidenceFile)));
|
|
3995
4072
|
if (!found) toDrop.push(id);
|
|
3996
4073
|
});
|
|
3997
4074
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -4567,6 +4644,66 @@ function registryPath() {
|
|
|
4567
4644
|
function registryLockPath() {
|
|
4568
4645
|
return path33.join(neatHome(), "projects.json.lock");
|
|
4569
4646
|
}
|
|
4647
|
+
function daemonPidPath() {
|
|
4648
|
+
return path33.join(neatHome(), "neatd.pid");
|
|
4649
|
+
}
|
|
4650
|
+
function isPidAliveDefault(pid) {
|
|
4651
|
+
try {
|
|
4652
|
+
process.kill(pid, 0);
|
|
4653
|
+
return true;
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
return err.code === "EPERM";
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
async function readPidFile(file) {
|
|
4659
|
+
try {
|
|
4660
|
+
const raw = await fs19.readFile(file, "utf8");
|
|
4661
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
4662
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
4663
|
+
} catch {
|
|
4664
|
+
return void 0;
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
var defaultLockHolderProbe = {
|
|
4668
|
+
isPidAlive: isPidAliveDefault,
|
|
4669
|
+
daemonPidFromFile: () => readPidFile(daemonPidPath()),
|
|
4670
|
+
async daemonResponds() {
|
|
4671
|
+
const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
4672
|
+
try {
|
|
4673
|
+
await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
|
|
4674
|
+
return true;
|
|
4675
|
+
} catch {
|
|
4676
|
+
return false;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
};
|
|
4680
|
+
async function readLockPid(lockPath) {
|
|
4681
|
+
return readPidFile(lockPath);
|
|
4682
|
+
}
|
|
4683
|
+
async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
|
|
4684
|
+
const lockPid = await readLockPid(lockPath);
|
|
4685
|
+
const daemonPid = await probe.daemonPidFromFile();
|
|
4686
|
+
if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
|
|
4687
|
+
// Either confirms a live daemon is in the picture (the second guards
|
|
4688
|
+
// against a stale pidfile whose PID got reused).
|
|
4689
|
+
(daemonPid === lockPid || await probe.daemonResponds())) {
|
|
4690
|
+
return { kind: "daemon", pid: daemonPid };
|
|
4691
|
+
}
|
|
4692
|
+
if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
|
|
4693
|
+
return { kind: "command", pid: lockPid };
|
|
4694
|
+
}
|
|
4695
|
+
return { kind: "stale" };
|
|
4696
|
+
}
|
|
4697
|
+
function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
4698
|
+
switch (holder.kind) {
|
|
4699
|
+
case "daemon":
|
|
4700
|
+
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\`.`;
|
|
4701
|
+
case "command":
|
|
4702
|
+
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.`;
|
|
4703
|
+
case "stale":
|
|
4704
|
+
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.`;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4570
4707
|
async function normalizeProjectPath(input) {
|
|
4571
4708
|
const resolved = path33.resolve(input);
|
|
4572
4709
|
try {
|
|
@@ -4587,21 +4724,31 @@ async function writeAtomically(target, contents) {
|
|
|
4587
4724
|
}
|
|
4588
4725
|
await fs19.rename(tmp, target);
|
|
4589
4726
|
}
|
|
4590
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4727
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
4591
4728
|
const deadline = Date.now() + timeoutMs;
|
|
4592
4729
|
await fs19.mkdir(path33.dirname(lockPath), { recursive: true });
|
|
4730
|
+
let probedHolder = false;
|
|
4593
4731
|
while (true) {
|
|
4594
4732
|
try {
|
|
4595
4733
|
const fd = await fs19.open(lockPath, "wx");
|
|
4596
|
-
|
|
4734
|
+
try {
|
|
4735
|
+
await fd.writeFile(`${process.pid}
|
|
4736
|
+
`, "utf8");
|
|
4737
|
+
} finally {
|
|
4738
|
+
await fd.close();
|
|
4739
|
+
}
|
|
4597
4740
|
return;
|
|
4598
4741
|
} catch (err) {
|
|
4599
4742
|
const code = err.code;
|
|
4600
4743
|
if (code !== "EEXIST") throw err;
|
|
4744
|
+
if (!probedHolder) {
|
|
4745
|
+
probedHolder = true;
|
|
4746
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
4747
|
+
if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
4748
|
+
}
|
|
4601
4749
|
if (Date.now() >= deadline) {
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
);
|
|
4750
|
+
const holder = await classifyLockHolder(lockPath, probe);
|
|
4751
|
+
throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
4605
4752
|
}
|
|
4606
4753
|
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4607
4754
|
}
|
|
@@ -5341,4 +5488,4 @@ export {
|
|
|
5341
5488
|
removeProject,
|
|
5342
5489
|
buildApi
|
|
5343
5490
|
};
|
|
5344
|
-
//# sourceMappingURL=chunk-
|
|
5491
|
+
//# sourceMappingURL=chunk-OJHI33LD.js.map
|