@neat.is/core 0.3.8 → 0.4.2
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-LQ3JFBTX.js → chunk-CB2UK4EH.js} +139 -41
- package/dist/chunk-CB2UK4EH.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-D5PIJFBE.js} +2 -2
- package/dist/{chunk-7TYESDAI.js → chunk-KYRIQIPG.js} +48 -11
- package/dist/chunk-KYRIQIPG.js.map +1 -0
- package/dist/{chunk-CZ3T6TE2.js → chunk-NTQHMXWE.js} +239 -71
- package/dist/chunk-NTQHMXWE.js.map +1 -0
- package/dist/cli.cjs +1209 -176
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +4 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +940 -99
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +418 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +427 -131
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-QTX2YQJZ.js} +3 -3
- package/dist/server.cjs +401 -203
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- package/dist/chunk-LQ3JFBTX.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-D5PIJFBE.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-QTX2YQJZ.js.map} +0 -0
package/dist/neatd.cjs
CHANGED
|
@@ -55,14 +55,19 @@ function mountBearerAuth(app, opts) {
|
|
|
55
55
|
if (opts.trustProxy) return;
|
|
56
56
|
const expected = Buffer.from(opts.token, "utf8");
|
|
57
57
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
|
+
const publicRead = opts.publicRead === true;
|
|
58
59
|
app.addHook("preHandler", (req2, reply, done) => {
|
|
59
|
-
const
|
|
60
|
+
const path40 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
60
61
|
for (const suffix of suffixes) {
|
|
61
|
-
if (
|
|
62
|
+
if (path40 === suffix || path40.endsWith(suffix)) {
|
|
62
63
|
done();
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req2.method)) {
|
|
68
|
+
done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
66
71
|
const header = req2.headers.authorization;
|
|
67
72
|
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
68
73
|
void reply.code(401).send({ error: "unauthorized" });
|
|
@@ -76,16 +81,21 @@ function mountBearerAuth(app, opts) {
|
|
|
76
81
|
done();
|
|
77
82
|
});
|
|
78
83
|
}
|
|
84
|
+
function parseBoolEnv(v) {
|
|
85
|
+
if (!v) return false;
|
|
86
|
+
return v === "true" || v === "1";
|
|
87
|
+
}
|
|
79
88
|
function readAuthEnv(env = process.env) {
|
|
80
89
|
const t = env.NEAT_AUTH_TOKEN;
|
|
81
90
|
const ot = env.NEAT_OTEL_TOKEN;
|
|
82
91
|
return {
|
|
83
92
|
authToken: t && t.length > 0 ? t : void 0,
|
|
84
93
|
otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
|
|
85
|
-
trustProxy: env.NEAT_AUTH_PROXY === "true"
|
|
94
|
+
trustProxy: env.NEAT_AUTH_PROXY === "true",
|
|
95
|
+
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
86
96
|
};
|
|
87
97
|
}
|
|
88
|
-
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, DEFAULT_UNAUTH_SUFFIXES;
|
|
98
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
|
|
89
99
|
var init_auth = __esm({
|
|
90
100
|
"src/auth.ts"() {
|
|
91
101
|
"use strict";
|
|
@@ -105,7 +115,13 @@ var init_auth = __esm({
|
|
|
105
115
|
this.name = "BindAuthorityError";
|
|
106
116
|
}
|
|
107
117
|
};
|
|
108
|
-
|
|
118
|
+
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
119
|
+
DEFAULT_UNAUTH_SUFFIXES = [
|
|
120
|
+
"/health",
|
|
121
|
+
"/healthz",
|
|
122
|
+
"/readyz",
|
|
123
|
+
"/api/config"
|
|
124
|
+
];
|
|
109
125
|
}
|
|
110
126
|
});
|
|
111
127
|
|
|
@@ -308,6 +324,15 @@ function isoFromUnixNano(nanos) {
|
|
|
308
324
|
return void 0;
|
|
309
325
|
}
|
|
310
326
|
}
|
|
327
|
+
function pickEnv(spanAttrs, resourceAttrs) {
|
|
328
|
+
for (const attrs of [spanAttrs, resourceAttrs]) {
|
|
329
|
+
for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
|
|
330
|
+
const v = attrs[key];
|
|
331
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return ENV_FALLBACK;
|
|
335
|
+
}
|
|
311
336
|
function parseOtlpRequest(body) {
|
|
312
337
|
const out = [];
|
|
313
338
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -327,6 +352,7 @@ function parseOtlpRequest(body) {
|
|
|
327
352
|
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
328
353
|
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
329
354
|
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
355
|
+
env: pickEnv(attrs, resourceAttrs),
|
|
330
356
|
attributes: attrs,
|
|
331
357
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
332
358
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
@@ -465,7 +491,7 @@ async function buildOtelReceiver(opts) {
|
|
|
465
491
|
};
|
|
466
492
|
return decorated;
|
|
467
493
|
}
|
|
468
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
494
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
469
495
|
var init_otel = __esm({
|
|
470
496
|
"src/otel.ts"() {
|
|
471
497
|
"use strict";
|
|
@@ -475,6 +501,9 @@ var init_otel = __esm({
|
|
|
475
501
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
476
502
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
477
503
|
init_auth();
|
|
504
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
505
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
506
|
+
ENV_FALLBACK = "unknown";
|
|
478
507
|
exportTraceServiceRequestType = null;
|
|
479
508
|
exportTraceServiceResponseType = null;
|
|
480
509
|
cachedProtobufResponseBody = null;
|
|
@@ -483,14 +512,14 @@ var init_otel = __esm({
|
|
|
483
512
|
|
|
484
513
|
// src/neatd.ts
|
|
485
514
|
init_cjs_shims();
|
|
486
|
-
var
|
|
487
|
-
var
|
|
515
|
+
var import_node_fs23 = require("fs");
|
|
516
|
+
var import_node_path39 = __toESM(require("path"), 1);
|
|
488
517
|
var import_node_module = require("module");
|
|
489
518
|
|
|
490
519
|
// src/daemon.ts
|
|
491
520
|
init_cjs_shims();
|
|
492
|
-
var
|
|
493
|
-
var
|
|
521
|
+
var import_node_fs22 = require("fs");
|
|
522
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
494
523
|
|
|
495
524
|
// src/graph.ts
|
|
496
525
|
init_cjs_shims();
|
|
@@ -950,19 +979,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
950
979
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
951
980
|
let best = { path: [start], edges: [] };
|
|
952
981
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
953
|
-
function step(node,
|
|
954
|
-
if (
|
|
955
|
-
best = { path: [...
|
|
982
|
+
function step(node, path40, edges) {
|
|
983
|
+
if (path40.length > best.path.length) {
|
|
984
|
+
best = { path: [...path40], edges: [...edges] };
|
|
956
985
|
}
|
|
957
|
-
if (
|
|
986
|
+
if (path40.length - 1 >= maxDepth) return;
|
|
958
987
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
959
988
|
for (const [srcId, edge] of incoming) {
|
|
960
989
|
if (visited.has(srcId)) continue;
|
|
961
990
|
visited.add(srcId);
|
|
962
|
-
|
|
991
|
+
path40.push(srcId);
|
|
963
992
|
edges.push(edge);
|
|
964
|
-
step(srcId,
|
|
965
|
-
|
|
993
|
+
step(srcId, path40, edges);
|
|
994
|
+
path40.pop();
|
|
966
995
|
edges.pop();
|
|
967
996
|
visited.delete(srcId);
|
|
968
997
|
}
|
|
@@ -1513,52 +1542,64 @@ function cacheSpanService(span, now) {
|
|
|
1513
1542
|
if (!span.traceId || !span.spanId) return;
|
|
1514
1543
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1515
1544
|
parentSpanCache.delete(key);
|
|
1516
|
-
parentSpanCache.set(key, {
|
|
1545
|
+
parentSpanCache.set(key, {
|
|
1546
|
+
service: span.service,
|
|
1547
|
+
env: span.env ?? "unknown",
|
|
1548
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1549
|
+
});
|
|
1517
1550
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1518
1551
|
const oldest = parentSpanCache.keys().next().value;
|
|
1519
1552
|
if (!oldest) break;
|
|
1520
1553
|
parentSpanCache.delete(oldest);
|
|
1521
1554
|
}
|
|
1522
1555
|
}
|
|
1523
|
-
function
|
|
1556
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1524
1557
|
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1525
1558
|
if (!entry2) return null;
|
|
1526
1559
|
if (entry2.expiresAt <= now) {
|
|
1527
1560
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1528
1561
|
return null;
|
|
1529
1562
|
}
|
|
1530
|
-
return entry2.service;
|
|
1563
|
+
return { service: entry2.service, env: entry2.env };
|
|
1531
1564
|
}
|
|
1532
|
-
function resolveServiceId(graph, host) {
|
|
1533
|
-
const
|
|
1534
|
-
if (graph.hasNode(
|
|
1535
|
-
|
|
1565
|
+
function resolveServiceId(graph, host, env) {
|
|
1566
|
+
const envTagged = (0, import_types3.serviceId)(host, env);
|
|
1567
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1568
|
+
const envLess = (0, import_types3.serviceId)(host);
|
|
1569
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1570
|
+
let sameEnv = null;
|
|
1571
|
+
let envLessMatch = null;
|
|
1572
|
+
let anyMatch = null;
|
|
1536
1573
|
graph.forEachNode((id, attrs) => {
|
|
1537
|
-
if (
|
|
1574
|
+
if (sameEnv) return;
|
|
1538
1575
|
const a = attrs;
|
|
1539
1576
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1540
|
-
|
|
1541
|
-
|
|
1577
|
+
const matchesByName = a.name === host;
|
|
1578
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1579
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1580
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1581
|
+
if (nodeEnv === env) {
|
|
1582
|
+
sameEnv = id;
|
|
1542
1583
|
return;
|
|
1543
1584
|
}
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1546
|
-
}
|
|
1585
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1586
|
+
else if (!anyMatch) anyMatch = id;
|
|
1547
1587
|
});
|
|
1548
|
-
return
|
|
1588
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1549
1589
|
}
|
|
1550
1590
|
function frontierIdFor(host) {
|
|
1551
1591
|
return (0, import_types3.frontierId)(host);
|
|
1552
1592
|
}
|
|
1553
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1554
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1593
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1594
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1555
1595
|
if (graph.hasNode(id)) return id;
|
|
1556
1596
|
const node = {
|
|
1557
1597
|
id,
|
|
1558
1598
|
type: import_types3.NodeType.ServiceNode,
|
|
1559
1599
|
name: serviceName,
|
|
1560
1600
|
language: "unknown",
|
|
1561
|
-
discoveredVia: "otel"
|
|
1601
|
+
discoveredVia: "otel",
|
|
1602
|
+
...env !== "unknown" ? { env } : {}
|
|
1562
1603
|
};
|
|
1563
1604
|
graph.addNode(id, node);
|
|
1564
1605
|
return id;
|
|
@@ -1704,7 +1745,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1704
1745
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1705
1746
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1706
1747
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1707
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1748
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1708
1749
|
};
|
|
1709
1750
|
}
|
|
1710
1751
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1718,7 +1759,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1718
1759
|
async function handleSpan(ctx, span) {
|
|
1719
1760
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1720
1761
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1721
|
-
const
|
|
1762
|
+
const env = span.env ?? "unknown";
|
|
1763
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1722
1764
|
const isError = span.statusCode === 2;
|
|
1723
1765
|
cacheSpanService(span, nowMs);
|
|
1724
1766
|
let affectedNode = sourceId;
|
|
@@ -1741,7 +1783,7 @@ async function handleSpan(ctx, span) {
|
|
|
1741
1783
|
const host = pickAddress(span);
|
|
1742
1784
|
let resolvedViaAddress = false;
|
|
1743
1785
|
if (host && host !== span.service) {
|
|
1744
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1786
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1745
1787
|
if (targetId && targetId !== sourceId) {
|
|
1746
1788
|
upsertObservedEdge(
|
|
1747
1789
|
ctx.graph,
|
|
@@ -1768,9 +1810,9 @@ async function handleSpan(ctx, span) {
|
|
|
1768
1810
|
}
|
|
1769
1811
|
}
|
|
1770
1812
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1771
|
-
const
|
|
1772
|
-
if (
|
|
1773
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
1813
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1814
|
+
if (parent && parent.service !== span.service) {
|
|
1815
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1774
1816
|
upsertObservedEdge(
|
|
1775
1817
|
ctx.graph,
|
|
1776
1818
|
import_types3.EdgeType.CALLS,
|
|
@@ -1893,6 +1935,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
1893
1935
|
throw err;
|
|
1894
1936
|
}
|
|
1895
1937
|
}
|
|
1938
|
+
function mergeSnapshot(graph, snapshot) {
|
|
1939
|
+
const exported = snapshot.graph;
|
|
1940
|
+
let nodesAdded = 0;
|
|
1941
|
+
let edgesAdded = 0;
|
|
1942
|
+
for (const node of exported.nodes ?? []) {
|
|
1943
|
+
if (graph.hasNode(node.key)) continue;
|
|
1944
|
+
if (!node.attributes) continue;
|
|
1945
|
+
graph.addNode(node.key, node.attributes);
|
|
1946
|
+
nodesAdded++;
|
|
1947
|
+
}
|
|
1948
|
+
for (const edge of exported.edges ?? []) {
|
|
1949
|
+
const attrs = edge.attributes;
|
|
1950
|
+
if (!attrs) continue;
|
|
1951
|
+
const id = edge.key ?? attrs.id;
|
|
1952
|
+
if (!id) continue;
|
|
1953
|
+
if (graph.hasEdge(id)) continue;
|
|
1954
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
1955
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
1956
|
+
edgesAdded++;
|
|
1957
|
+
}
|
|
1958
|
+
return { nodesAdded, edgesAdded };
|
|
1959
|
+
}
|
|
1896
1960
|
|
|
1897
1961
|
// src/extract/services.ts
|
|
1898
1962
|
init_cjs_shims();
|
|
@@ -1916,8 +1980,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1916
1980
|
".turbo",
|
|
1917
1981
|
"dist",
|
|
1918
1982
|
"build",
|
|
1919
|
-
".next"
|
|
1983
|
+
".next",
|
|
1984
|
+
// Python virtualenv shapes (issue #344). Walking into a venv pulls in the
|
|
1985
|
+
// entire CPython stdlib + every installed package as if it were first-party
|
|
1986
|
+
// service code — 20k+ files, none of which the user wrote. The shape names
|
|
1987
|
+
// cover the three common venv tools (`venv` / `python -m venv`,
|
|
1988
|
+
// `virtualenv`) and the tox + PEP 582 conventions.
|
|
1989
|
+
".venv",
|
|
1990
|
+
"venv",
|
|
1991
|
+
"__pypackages__",
|
|
1992
|
+
".tox",
|
|
1993
|
+
// `site-packages` shows up nested inside venvs that don't carry one of the
|
|
1994
|
+
// outer names (system Python on macOS, in-place pyenv layouts). Listing it
|
|
1995
|
+
// here means we stop the walk at the boundary even when the wrapper dir
|
|
1996
|
+
// wasn't recognisable.
|
|
1997
|
+
"site-packages"
|
|
1920
1998
|
]);
|
|
1999
|
+
var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
|
|
2000
|
+
async function isPythonVenvDir(dir) {
|
|
2001
|
+
const cached = PYVENV_MARKER_CACHE.get(dir);
|
|
2002
|
+
if (cached !== void 0) return cached;
|
|
2003
|
+
try {
|
|
2004
|
+
const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
|
|
2005
|
+
const ok = stat.isFile();
|
|
2006
|
+
PYVENV_MARKER_CACHE.set(dir, ok);
|
|
2007
|
+
return ok;
|
|
2008
|
+
} catch {
|
|
2009
|
+
PYVENV_MARKER_CACHE.set(dir, false);
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1921
2013
|
function isConfigFile(name) {
|
|
1922
2014
|
const ext = import_node_path4.default.extname(name);
|
|
1923
2015
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
@@ -2254,6 +2346,7 @@ async function walkDirs(start, scanPath, options, visit) {
|
|
|
2254
2346
|
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2255
2347
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2256
2348
|
}
|
|
2349
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2257
2350
|
await visit(child);
|
|
2258
2351
|
await recurse(child, depth + 1);
|
|
2259
2352
|
}
|
|
@@ -2289,6 +2382,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2289
2382
|
}
|
|
2290
2383
|
return [...found];
|
|
2291
2384
|
}
|
|
2385
|
+
function detectJsFramework(pkg) {
|
|
2386
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2387
|
+
if (deps["next"] !== void 0) return "next";
|
|
2388
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2389
|
+
for (const k of Object.keys(deps)) {
|
|
2390
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2391
|
+
}
|
|
2392
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2393
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2394
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2395
|
+
return void 0;
|
|
2396
|
+
}
|
|
2292
2397
|
async function discoverNodeService(scanPath, dir) {
|
|
2293
2398
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2294
2399
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2300,6 +2405,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2300
2405
|
return null;
|
|
2301
2406
|
}
|
|
2302
2407
|
if (!pkg.name) return null;
|
|
2408
|
+
const framework = detectJsFramework(pkg);
|
|
2303
2409
|
const node = {
|
|
2304
2410
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2305
2411
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2308,7 +2414,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2308
2414
|
version: pkg.version,
|
|
2309
2415
|
dependencies: pkg.dependencies ?? {},
|
|
2310
2416
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2311
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2417
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2418
|
+
...framework ? { framework } : {}
|
|
2312
2419
|
};
|
|
2313
2420
|
return { pkg, dir, node };
|
|
2314
2421
|
}
|
|
@@ -2518,7 +2625,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
|
2518
2625
|
for (const entry2 of entries) {
|
|
2519
2626
|
if (entry2.isDirectory()) {
|
|
2520
2627
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2521
|
-
|
|
2628
|
+
const child = import_node_path9.default.join(start, entry2.name);
|
|
2629
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2630
|
+
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
2522
2631
|
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
|
|
2523
2632
|
out.push(import_node_path9.default.join(start, entry2.name));
|
|
2524
2633
|
}
|
|
@@ -3200,7 +3309,9 @@ async function walkConfigFiles(dir) {
|
|
|
3200
3309
|
for (const entry2 of entries) {
|
|
3201
3310
|
const full = import_node_path18.default.join(current, entry2.name);
|
|
3202
3311
|
if (entry2.isDirectory()) {
|
|
3203
|
-
if (
|
|
3312
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3313
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3314
|
+
await walk(full);
|
|
3204
3315
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
3205
3316
|
out.push(full);
|
|
3206
3317
|
}
|
|
@@ -3268,7 +3379,9 @@ async function walkSourceFiles(dir) {
|
|
|
3268
3379
|
for (const entry2 of entries) {
|
|
3269
3380
|
const full = import_node_path19.default.join(current, entry2.name);
|
|
3270
3381
|
if (entry2.isDirectory()) {
|
|
3271
|
-
if (
|
|
3382
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3383
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3384
|
+
await walk(full);
|
|
3272
3385
|
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
|
|
3273
3386
|
out.push(full);
|
|
3274
3387
|
}
|
|
@@ -3904,7 +4017,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
3904
4017
|
for (const entry2 of entries) {
|
|
3905
4018
|
if (entry2.isDirectory()) {
|
|
3906
4019
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
3907
|
-
|
|
4020
|
+
const child = import_node_path27.default.join(start, entry2.name);
|
|
4021
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4022
|
+
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
3908
4023
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
3909
4024
|
out.push(import_node_path27.default.join(start, entry2.name));
|
|
3910
4025
|
}
|
|
@@ -3952,7 +4067,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
3952
4067
|
for (const entry2 of entries) {
|
|
3953
4068
|
if (entry2.isDirectory()) {
|
|
3954
4069
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3955
|
-
|
|
4070
|
+
const child = import_node_path28.default.join(start, entry2.name);
|
|
4071
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4072
|
+
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
3956
4073
|
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
|
|
3957
4074
|
out.push(import_node_path28.default.join(start, entry2.name));
|
|
3958
4075
|
}
|
|
@@ -4091,7 +4208,7 @@ init_cjs_shims();
|
|
|
4091
4208
|
var import_node_fs18 = require("fs");
|
|
4092
4209
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4093
4210
|
var import_types19 = require("@neat.is/types");
|
|
4094
|
-
var SCHEMA_VERSION =
|
|
4211
|
+
var SCHEMA_VERSION = 4;
|
|
4095
4212
|
function migrateV1ToV2(payload) {
|
|
4096
4213
|
const nodes = payload.graph.nodes;
|
|
4097
4214
|
if (Array.isArray(nodes)) {
|
|
@@ -4103,6 +4220,9 @@ function migrateV1ToV2(payload) {
|
|
|
4103
4220
|
}
|
|
4104
4221
|
return { ...payload, schemaVersion: 2 };
|
|
4105
4222
|
}
|
|
4223
|
+
function migrateV3ToV4(payload) {
|
|
4224
|
+
return { ...payload, schemaVersion: 4 };
|
|
4225
|
+
}
|
|
4106
4226
|
function migrateV2ToV3(payload) {
|
|
4107
4227
|
const edges = payload.graph.edges;
|
|
4108
4228
|
if (Array.isArray(edges)) {
|
|
@@ -4151,6 +4271,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4151
4271
|
if (payload.schemaVersion === 2) {
|
|
4152
4272
|
payload = migrateV2ToV3(payload);
|
|
4153
4273
|
}
|
|
4274
|
+
if (payload.schemaVersion === 3) {
|
|
4275
|
+
payload = migrateV3ToV4(payload);
|
|
4276
|
+
}
|
|
4154
4277
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4155
4278
|
throw new Error(
|
|
4156
4279
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -4747,10 +4870,19 @@ function projectFromReq(req2) {
|
|
|
4747
4870
|
const params = req2.params;
|
|
4748
4871
|
return params.project ?? DEFAULT_PROJECT;
|
|
4749
4872
|
}
|
|
4750
|
-
function resolveProject(registry, req2, reply) {
|
|
4873
|
+
function resolveProject(registry, req2, reply, bootstrap) {
|
|
4751
4874
|
const name = projectFromReq(req2);
|
|
4752
4875
|
const ctx = registry.get(name);
|
|
4753
4876
|
if (!ctx) {
|
|
4877
|
+
const phase = bootstrap?.status(name);
|
|
4878
|
+
if (phase === "bootstrapping") {
|
|
4879
|
+
void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
|
|
4880
|
+
return null;
|
|
4881
|
+
}
|
|
4882
|
+
if (phase === "broken") {
|
|
4883
|
+
void reply.code(503).send({ ready: false, project: name, status: "broken" });
|
|
4884
|
+
return null;
|
|
4885
|
+
}
|
|
4754
4886
|
void reply.code(404).send({ error: "project not found", project: name });
|
|
4755
4887
|
return null;
|
|
4756
4888
|
}
|
|
@@ -4780,36 +4912,38 @@ function buildLegacyRegistry(opts) {
|
|
|
4780
4912
|
function registerRoutes(scope, ctx) {
|
|
4781
4913
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4782
4914
|
scope.get("/events", (req2, reply) => {
|
|
4783
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4915
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4784
4916
|
if (!proj) return;
|
|
4785
4917
|
handleSse(req2, reply, { project: proj.name });
|
|
4786
4918
|
});
|
|
4787
|
-
scope
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4919
|
+
if (ctx.scope === "project") {
|
|
4920
|
+
scope.get("/health", async (req2, reply) => {
|
|
4921
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4922
|
+
if (!proj) return;
|
|
4923
|
+
const uptimeMs = Date.now() - startedAt;
|
|
4924
|
+
return {
|
|
4925
|
+
ok: true,
|
|
4926
|
+
project: proj.name,
|
|
4927
|
+
uptimeMs,
|
|
4928
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
4929
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
4930
|
+
// the canonical triple and lets the extras pass through.
|
|
4931
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
4932
|
+
nodeCount: proj.graph.order,
|
|
4933
|
+
edgeCount: proj.graph.size,
|
|
4934
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4935
|
+
};
|
|
4936
|
+
});
|
|
4937
|
+
}
|
|
4804
4938
|
scope.get("/graph", async (req2, reply) => {
|
|
4805
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4939
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4806
4940
|
if (!proj) return;
|
|
4807
4941
|
return serializeGraph(proj.graph);
|
|
4808
4942
|
});
|
|
4809
4943
|
scope.get(
|
|
4810
4944
|
"/graph/node/:id",
|
|
4811
4945
|
async (req2, reply) => {
|
|
4812
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4946
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4813
4947
|
if (!proj) return;
|
|
4814
4948
|
const { id } = req2.params;
|
|
4815
4949
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -4821,7 +4955,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4821
4955
|
scope.get(
|
|
4822
4956
|
"/graph/edges/:id",
|
|
4823
4957
|
async (req2, reply) => {
|
|
4824
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4958
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4825
4959
|
if (!proj) return;
|
|
4826
4960
|
const { id } = req2.params;
|
|
4827
4961
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -4833,7 +4967,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4833
4967
|
}
|
|
4834
4968
|
);
|
|
4835
4969
|
scope.get("/graph/dependencies/:nodeId", async (req2, reply) => {
|
|
4836
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4970
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4837
4971
|
if (!proj) return;
|
|
4838
4972
|
const { nodeId } = req2.params;
|
|
4839
4973
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4848,7 +4982,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4848
4982
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
4849
4983
|
});
|
|
4850
4984
|
scope.get("/graph/divergences", async (req2, reply) => {
|
|
4851
|
-
const proj = resolveProject(registry, req2, reply);
|
|
4985
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4852
4986
|
if (!proj) return;
|
|
4853
4987
|
let typeFilter;
|
|
4854
4988
|
if (req2.query.type) {
|
|
@@ -4883,7 +5017,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4883
5017
|
});
|
|
4884
5018
|
});
|
|
4885
5019
|
scope.get("/incidents", async (req2, reply) => {
|
|
4886
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5020
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4887
5021
|
if (!proj) return;
|
|
4888
5022
|
const epath = errorsPathFor(proj);
|
|
4889
5023
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -4895,7 +5029,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4895
5029
|
return { count: sliced.length, total, events: sliced };
|
|
4896
5030
|
});
|
|
4897
5031
|
scope.get("/stale-events", async (req2, reply) => {
|
|
4898
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5032
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4899
5033
|
if (!proj) return;
|
|
4900
5034
|
const spath = staleEventsPathFor(proj);
|
|
4901
5035
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -4910,7 +5044,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4910
5044
|
scope.get(
|
|
4911
5045
|
"/incidents/:nodeId",
|
|
4912
5046
|
async (req2, reply) => {
|
|
4913
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5047
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4914
5048
|
if (!proj) return;
|
|
4915
5049
|
const { nodeId } = req2.params;
|
|
4916
5050
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4926,7 +5060,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4926
5060
|
}
|
|
4927
5061
|
);
|
|
4928
5062
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
4929
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5063
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4930
5064
|
if (!proj) return;
|
|
4931
5065
|
const { nodeId } = req2.params;
|
|
4932
5066
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4946,7 +5080,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4946
5080
|
return result;
|
|
4947
5081
|
});
|
|
4948
5082
|
scope.get("/graph/blast-radius/:nodeId", async (req2, reply) => {
|
|
4949
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5083
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4950
5084
|
if (!proj) return;
|
|
4951
5085
|
const { nodeId } = req2.params;
|
|
4952
5086
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4959,7 +5093,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4959
5093
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
4960
5094
|
});
|
|
4961
5095
|
scope.get("/search", async (req2, reply) => {
|
|
4962
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5096
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4963
5097
|
if (!proj) return;
|
|
4964
5098
|
const raw = (req2.query.q ?? "").trim();
|
|
4965
5099
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -4990,7 +5124,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4990
5124
|
scope.get(
|
|
4991
5125
|
"/graph/diff",
|
|
4992
5126
|
async (req2, reply) => {
|
|
4993
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5127
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
4994
5128
|
if (!proj) return;
|
|
4995
5129
|
const against = req2.query.against;
|
|
4996
5130
|
if (!against) {
|
|
@@ -5004,8 +5138,37 @@ function registerRoutes(scope, ctx) {
|
|
|
5004
5138
|
}
|
|
5005
5139
|
}
|
|
5006
5140
|
);
|
|
5141
|
+
scope.post("/snapshot", async (req2, reply) => {
|
|
5142
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
5143
|
+
if (!proj) return;
|
|
5144
|
+
const body = req2.body;
|
|
5145
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5146
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5147
|
+
}
|
|
5148
|
+
const snap = body.snapshot;
|
|
5149
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5150
|
+
return reply.code(400).send({
|
|
5151
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
try {
|
|
5155
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5156
|
+
return {
|
|
5157
|
+
project: proj.name,
|
|
5158
|
+
nodesAdded: result.nodesAdded,
|
|
5159
|
+
edgesAdded: result.edgesAdded,
|
|
5160
|
+
nodeCount: proj.graph.order,
|
|
5161
|
+
edgeCount: proj.graph.size
|
|
5162
|
+
};
|
|
5163
|
+
} catch (err) {
|
|
5164
|
+
return reply.code(400).send({
|
|
5165
|
+
error: "snapshot merge failed",
|
|
5166
|
+
details: err.message
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
});
|
|
5007
5170
|
scope.post("/graph/scan", async (req2, reply) => {
|
|
5008
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5171
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
5009
5172
|
if (!proj) return;
|
|
5010
5173
|
if (!proj.scanPath) {
|
|
5011
5174
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -5021,7 +5184,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5021
5184
|
};
|
|
5022
5185
|
});
|
|
5023
5186
|
scope.get("/policies", async (req2, reply) => {
|
|
5024
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5187
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
5025
5188
|
if (!proj) return;
|
|
5026
5189
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
5027
5190
|
if (!policyPath) {
|
|
@@ -5038,7 +5201,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5038
5201
|
}
|
|
5039
5202
|
});
|
|
5040
5203
|
scope.get("/policies/violations", async (req2, reply) => {
|
|
5041
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5204
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
5042
5205
|
if (!proj) return;
|
|
5043
5206
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5044
5207
|
let violations = await log.readAll();
|
|
@@ -5058,7 +5221,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5058
5221
|
return { violations };
|
|
5059
5222
|
});
|
|
5060
5223
|
scope.post("/policies/check", async (req2, reply) => {
|
|
5061
|
-
const proj = resolveProject(registry, req2, reply);
|
|
5224
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
5062
5225
|
if (!proj) return;
|
|
5063
5226
|
const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
5064
5227
|
if (!parsed.success) {
|
|
@@ -5097,7 +5260,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5097
5260
|
async function buildApi(opts) {
|
|
5098
5261
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5099
5262
|
await app.register(import_cors.default, { origin: true });
|
|
5100
|
-
mountBearerAuth(app, {
|
|
5263
|
+
mountBearerAuth(app, {
|
|
5264
|
+
token: opts.authToken,
|
|
5265
|
+
trustProxy: opts.trustProxy,
|
|
5266
|
+
publicRead: opts.publicRead
|
|
5267
|
+
});
|
|
5268
|
+
app.get("/api/config", async () => ({
|
|
5269
|
+
publicRead: opts.publicRead === true,
|
|
5270
|
+
authProxy: opts.trustProxy === true
|
|
5271
|
+
}));
|
|
5101
5272
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5102
5273
|
const registry = buildLegacyRegistry(opts);
|
|
5103
5274
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5121,10 +5292,36 @@ async function buildApi(opts) {
|
|
|
5121
5292
|
const routeCtx = {
|
|
5122
5293
|
registry,
|
|
5123
5294
|
startedAt,
|
|
5295
|
+
scope: "root",
|
|
5124
5296
|
errorsPathFor,
|
|
5125
5297
|
staleEventsPathFor,
|
|
5126
|
-
policyFilePathFor
|
|
5298
|
+
policyFilePathFor,
|
|
5299
|
+
bootstrap: opts.bootstrap
|
|
5127
5300
|
};
|
|
5301
|
+
app.get("/health", async () => {
|
|
5302
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5303
|
+
const bootstrapList = opts.bootstrap?.list() ?? [];
|
|
5304
|
+
const byName = new Map(bootstrapList.map((p) => [p.name, p]));
|
|
5305
|
+
const names = /* @__PURE__ */ new Set([
|
|
5306
|
+
...registry.list(),
|
|
5307
|
+
...bootstrapList.map((p) => p.name)
|
|
5308
|
+
]);
|
|
5309
|
+
const projects = [...names].sort().map((name) => {
|
|
5310
|
+
const proj = registry.get(name);
|
|
5311
|
+
const tracked = byName.get(name);
|
|
5312
|
+
return {
|
|
5313
|
+
name,
|
|
5314
|
+
nodeCount: proj?.graph.order ?? 0,
|
|
5315
|
+
edgeCount: proj?.graph.size ?? 0,
|
|
5316
|
+
...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
|
|
5317
|
+
};
|
|
5318
|
+
});
|
|
5319
|
+
return {
|
|
5320
|
+
ok: true,
|
|
5321
|
+
uptimeMs,
|
|
5322
|
+
projects
|
|
5323
|
+
};
|
|
5324
|
+
});
|
|
5128
5325
|
app.get("/projects", async (_req, reply) => {
|
|
5129
5326
|
try {
|
|
5130
5327
|
return await listProjects();
|
|
@@ -5152,7 +5349,7 @@ async function buildApi(opts) {
|
|
|
5152
5349
|
registerRoutes(app, routeCtx);
|
|
5153
5350
|
await app.register(
|
|
5154
5351
|
async (scope) => {
|
|
5155
|
-
registerRoutes(scope, routeCtx);
|
|
5352
|
+
registerRoutes(scope, { ...routeCtx, scope: "project" });
|
|
5156
5353
|
},
|
|
5157
5354
|
{ prefix: "/projects/:project" }
|
|
5158
5355
|
);
|
|
@@ -5162,12 +5359,35 @@ async function buildApi(opts) {
|
|
|
5162
5359
|
// src/daemon.ts
|
|
5163
5360
|
init_otel();
|
|
5164
5361
|
init_auth();
|
|
5362
|
+
|
|
5363
|
+
// src/unrouted.ts
|
|
5364
|
+
init_cjs_shims();
|
|
5365
|
+
var import_node_fs21 = require("fs");
|
|
5366
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5367
|
+
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
5368
|
+
return {
|
|
5369
|
+
timestamp: now.toISOString(),
|
|
5370
|
+
reason: "no-project-match",
|
|
5371
|
+
service_name: serviceName ?? null,
|
|
5372
|
+
traceId: traceId ?? null
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
async function appendUnroutedSpan(neatHome3, record) {
|
|
5376
|
+
const target = import_node_path36.default.join(neatHome3, "errors.ndjson");
|
|
5377
|
+
await import_node_fs21.promises.mkdir(neatHome3, { recursive: true });
|
|
5378
|
+
await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
5379
|
+
}
|
|
5380
|
+
function unroutedErrorsPath(neatHome3) {
|
|
5381
|
+
return import_node_path36.default.join(neatHome3, "errors.ndjson");
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
// src/daemon.ts
|
|
5165
5385
|
function neatHomeFor(opts) {
|
|
5166
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5386
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
|
|
5167
5387
|
const env = process.env.NEAT_HOME;
|
|
5168
|
-
if (env && env.length > 0) return
|
|
5388
|
+
if (env && env.length > 0) return import_node_path37.default.resolve(env);
|
|
5169
5389
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5170
|
-
return
|
|
5390
|
+
return import_node_path37.default.join(home, ".neat");
|
|
5171
5391
|
}
|
|
5172
5392
|
function routeSpanToProject(serviceName, projects) {
|
|
5173
5393
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -5202,9 +5422,9 @@ function isTokenContained(needle, haystack) {
|
|
|
5202
5422
|
return tokens.includes(needle);
|
|
5203
5423
|
}
|
|
5204
5424
|
async function bootstrapProject(entry2) {
|
|
5205
|
-
const paths = pathsForProject(entry2.name,
|
|
5425
|
+
const paths = pathsForProject(entry2.name, import_node_path37.default.join(entry2.path, "neat-out"));
|
|
5206
5426
|
try {
|
|
5207
|
-
const stat = await
|
|
5427
|
+
const stat = await import_node_fs22.promises.stat(entry2.path);
|
|
5208
5428
|
if (!stat.isDirectory()) {
|
|
5209
5429
|
throw new Error(`registered path ${entry2.path} is not a directory`);
|
|
5210
5430
|
}
|
|
@@ -5259,27 +5479,30 @@ function resolveOtlpPort(opts) {
|
|
|
5259
5479
|
}
|
|
5260
5480
|
return 4318;
|
|
5261
5481
|
}
|
|
5262
|
-
function resolveHost(opts) {
|
|
5482
|
+
function resolveHost(opts, authTokenSet) {
|
|
5263
5483
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
5264
5484
|
const env = process.env.HOST;
|
|
5265
5485
|
if (env && env.length > 0) return env;
|
|
5486
|
+
if (!authTokenSet) return "127.0.0.1";
|
|
5266
5487
|
return "0.0.0.0";
|
|
5267
5488
|
}
|
|
5268
5489
|
async function startDaemon(opts = {}) {
|
|
5269
5490
|
const home = neatHomeFor(opts);
|
|
5270
5491
|
const regPath = registryPath();
|
|
5271
5492
|
try {
|
|
5272
|
-
await
|
|
5493
|
+
await import_node_fs22.promises.access(regPath);
|
|
5273
5494
|
} catch {
|
|
5274
5495
|
throw new Error(
|
|
5275
5496
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
5276
5497
|
);
|
|
5277
5498
|
}
|
|
5278
|
-
const pidPath =
|
|
5499
|
+
const pidPath = import_node_path37.default.join(home, "neatd.pid");
|
|
5279
5500
|
await writeAtomically(pidPath, `${process.pid}
|
|
5280
5501
|
`);
|
|
5281
5502
|
const slots = /* @__PURE__ */ new Map();
|
|
5282
5503
|
const registry = new Projects();
|
|
5504
|
+
const bootstrapStatus = /* @__PURE__ */ new Map();
|
|
5505
|
+
const bootstrapStartedAt = /* @__PURE__ */ new Map();
|
|
5283
5506
|
const DROP_WARN_INTERVAL_MS = 6e4;
|
|
5284
5507
|
const lastDropWarnAt = /* @__PURE__ */ new Map();
|
|
5285
5508
|
function warnDroppedSpan(project, reason) {
|
|
@@ -5291,6 +5514,22 @@ async function startDaemon(opts = {}) {
|
|
|
5291
5514
|
`[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
|
|
5292
5515
|
);
|
|
5293
5516
|
}
|
|
5517
|
+
const unroutedPath = unroutedErrorsPath(home);
|
|
5518
|
+
const lastUnroutedWarnAt = /* @__PURE__ */ new Map();
|
|
5519
|
+
async function recordUnroutedSpan(serviceName, traceId) {
|
|
5520
|
+
const key = serviceName ?? "<missing>";
|
|
5521
|
+
const now = Date.now();
|
|
5522
|
+
try {
|
|
5523
|
+
await appendUnroutedSpan(home, buildUnroutedSpanRecord(serviceName, traceId, new Date(now)));
|
|
5524
|
+
} catch {
|
|
5525
|
+
}
|
|
5526
|
+
const prev = lastUnroutedWarnAt.get(key) ?? 0;
|
|
5527
|
+
if (now - prev < DROP_WARN_INTERVAL_MS) return;
|
|
5528
|
+
lastUnroutedWarnAt.set(key, now);
|
|
5529
|
+
console.warn(
|
|
5530
|
+
`[neatd] dropping span \u2014 service.name "${key}" matches no registered project and no \`default\` project exists. See ${unroutedPath}.`
|
|
5531
|
+
);
|
|
5532
|
+
}
|
|
5294
5533
|
function upsertRegistryFromSlot(slot) {
|
|
5295
5534
|
if (slot.status !== "active") return;
|
|
5296
5535
|
registry.set(slot.entry.name, {
|
|
@@ -5319,34 +5558,43 @@ async function startDaemon(opts = {}) {
|
|
|
5319
5558
|
return slots.get(entry2.name);
|
|
5320
5559
|
}
|
|
5321
5560
|
}
|
|
5561
|
+
async function bootstrapOne(entry2) {
|
|
5562
|
+
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
5563
|
+
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
5564
|
+
try {
|
|
5565
|
+
const slot = await bootstrapProject(entry2);
|
|
5566
|
+
slots.set(entry2.name, slot);
|
|
5567
|
+
upsertRegistryFromSlot(slot);
|
|
5568
|
+
bootstrapStatus.set(entry2.name, slot.status === "broken" ? "broken" : "active");
|
|
5569
|
+
if (slot.status === "broken") {
|
|
5570
|
+
console.warn(`neatd: project "${entry2.name}" broken \u2014 ${slot.errorReason}`);
|
|
5571
|
+
} else {
|
|
5572
|
+
console.log(`neatd: project "${entry2.name}" active (${entry2.path})`);
|
|
5573
|
+
}
|
|
5574
|
+
} catch (err) {
|
|
5575
|
+
bootstrapStatus.set(entry2.name, "broken");
|
|
5576
|
+
console.warn(
|
|
5577
|
+
`neatd: project "${entry2.name}" failed to bootstrap \u2014 ${err.message}`
|
|
5578
|
+
);
|
|
5579
|
+
await setStatus(entry2.name, "broken").catch(() => {
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5322
5583
|
async function loadAll() {
|
|
5323
5584
|
const projects = await listProjects();
|
|
5324
5585
|
const seen = /* @__PURE__ */ new Set();
|
|
5586
|
+
const pending = [];
|
|
5325
5587
|
for (const entry2 of projects) {
|
|
5326
5588
|
seen.add(entry2.name);
|
|
5327
5589
|
const existing = slots.get(entry2.name);
|
|
5328
5590
|
if (existing) {
|
|
5329
5591
|
if (existing.status === "broken") {
|
|
5330
|
-
|
|
5592
|
+
pending.push(tryRecoverSlot(entry2).then(() => {
|
|
5593
|
+
}));
|
|
5331
5594
|
}
|
|
5332
5595
|
continue;
|
|
5333
5596
|
}
|
|
5334
|
-
|
|
5335
|
-
const slot = await bootstrapProject(entry2);
|
|
5336
|
-
slots.set(entry2.name, slot);
|
|
5337
|
-
upsertRegistryFromSlot(slot);
|
|
5338
|
-
if (slot.status === "broken") {
|
|
5339
|
-
console.warn(`neatd: project "${entry2.name}" broken \u2014 ${slot.errorReason}`);
|
|
5340
|
-
} else {
|
|
5341
|
-
console.log(`neatd: project "${entry2.name}" active (${entry2.path})`);
|
|
5342
|
-
}
|
|
5343
|
-
} catch (err) {
|
|
5344
|
-
console.warn(
|
|
5345
|
-
`neatd: project "${entry2.name}" failed to bootstrap \u2014 ${err.message}`
|
|
5346
|
-
);
|
|
5347
|
-
await setStatus(entry2.name, "broken").catch(() => {
|
|
5348
|
-
});
|
|
5349
|
-
}
|
|
5597
|
+
pending.push(bootstrapOne(entry2));
|
|
5350
5598
|
}
|
|
5351
5599
|
for (const [name, slot] of [...slots.entries()]) {
|
|
5352
5600
|
if (seen.has(name)) continue;
|
|
@@ -5355,26 +5603,45 @@ async function startDaemon(opts = {}) {
|
|
|
5355
5603
|
} catch {
|
|
5356
5604
|
}
|
|
5357
5605
|
slots.delete(name);
|
|
5606
|
+
bootstrapStatus.delete(name);
|
|
5607
|
+
bootstrapStartedAt.delete(name);
|
|
5358
5608
|
console.log(`neatd: project "${name}" removed from registry \u2014 stopped`);
|
|
5359
5609
|
}
|
|
5610
|
+
await Promise.allSettled(pending);
|
|
5611
|
+
}
|
|
5612
|
+
const initialEntries = await listProjects().catch(() => []);
|
|
5613
|
+
for (const entry2 of initialEntries) {
|
|
5614
|
+
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
5615
|
+
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
5360
5616
|
}
|
|
5361
|
-
await loadAll();
|
|
5362
5617
|
const bind = opts.bindListeners !== false;
|
|
5363
5618
|
let restApp = null;
|
|
5364
5619
|
let otlpApp = null;
|
|
5365
5620
|
let restAddress = "";
|
|
5366
5621
|
let otlpAddress = "";
|
|
5367
5622
|
if (bind) {
|
|
5368
|
-
const
|
|
5623
|
+
const auth = readAuthEnv();
|
|
5624
|
+
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
5369
5625
|
const restPort = resolveRestPort(opts);
|
|
5370
5626
|
const otlpPort = resolveOtlpPort(opts);
|
|
5371
|
-
const auth = readAuthEnv();
|
|
5372
5627
|
assertBindAuthority(host, auth.authToken);
|
|
5373
5628
|
try {
|
|
5374
5629
|
restApp = await buildApi({
|
|
5375
5630
|
projects: registry,
|
|
5376
5631
|
authToken: auth.authToken,
|
|
5377
|
-
trustProxy: auth.trustProxy
|
|
5632
|
+
trustProxy: auth.trustProxy,
|
|
5633
|
+
publicRead: auth.publicRead,
|
|
5634
|
+
bootstrap: {
|
|
5635
|
+
status: (name) => bootstrapStatus.get(name),
|
|
5636
|
+
list: () => {
|
|
5637
|
+
const now = Date.now();
|
|
5638
|
+
return [...bootstrapStatus.entries()].map(([name, status2]) => ({
|
|
5639
|
+
name,
|
|
5640
|
+
status: status2,
|
|
5641
|
+
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
5642
|
+
}));
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5378
5645
|
});
|
|
5379
5646
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
5380
5647
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
@@ -5387,17 +5654,20 @@ async function startDaemon(opts = {}) {
|
|
|
5387
5654
|
}
|
|
5388
5655
|
if (restApp) await restApp.close().catch(() => {
|
|
5389
5656
|
});
|
|
5390
|
-
await
|
|
5657
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5391
5658
|
});
|
|
5392
5659
|
throw new Error(
|
|
5393
5660
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
5394
5661
|
);
|
|
5395
5662
|
}
|
|
5396
|
-
async function resolveTargetSlot(serviceName) {
|
|
5663
|
+
async function resolveTargetSlot(serviceName, traceId) {
|
|
5397
5664
|
const liveEntries = await listProjects().catch(() => []);
|
|
5398
5665
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
5399
5666
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5400
|
-
if (!slot)
|
|
5667
|
+
if (!slot) {
|
|
5668
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
5669
|
+
return null;
|
|
5670
|
+
}
|
|
5401
5671
|
if (slot.status === "broken") {
|
|
5402
5672
|
const entry2 = liveEntries.find((e) => e.name === slot.entry.name);
|
|
5403
5673
|
if (entry2) {
|
|
@@ -5415,7 +5685,7 @@ async function startDaemon(opts = {}) {
|
|
|
5415
5685
|
authToken: auth.otelToken,
|
|
5416
5686
|
trustProxy: auth.trustProxy,
|
|
5417
5687
|
onSpan: async (span) => {
|
|
5418
|
-
const slot = await resolveTargetSlot(span.service);
|
|
5688
|
+
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
5419
5689
|
if (!slot) return;
|
|
5420
5690
|
await handleSpan(
|
|
5421
5691
|
{
|
|
@@ -5429,7 +5699,7 @@ async function startDaemon(opts = {}) {
|
|
|
5429
5699
|
);
|
|
5430
5700
|
},
|
|
5431
5701
|
onErrorSpanSync: async (span) => {
|
|
5432
|
-
const slot = await resolveTargetSlot(span.service);
|
|
5702
|
+
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
5433
5703
|
if (!slot) return;
|
|
5434
5704
|
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5435
5705
|
}
|
|
@@ -5447,14 +5717,17 @@ async function startDaemon(opts = {}) {
|
|
|
5447
5717
|
});
|
|
5448
5718
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
5449
5719
|
});
|
|
5450
|
-
await
|
|
5720
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5451
5721
|
});
|
|
5452
5722
|
throw new Error(
|
|
5453
5723
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
5454
5724
|
);
|
|
5455
5725
|
}
|
|
5456
5726
|
}
|
|
5457
|
-
|
|
5727
|
+
const initialBootstrap = loadAll().catch((err) => {
|
|
5728
|
+
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
5729
|
+
});
|
|
5730
|
+
let reloading = initialBootstrap;
|
|
5458
5731
|
const reload = async () => {
|
|
5459
5732
|
if (reloading) return reloading;
|
|
5460
5733
|
reloading = (async () => {
|
|
@@ -5466,6 +5739,20 @@ async function startDaemon(opts = {}) {
|
|
|
5466
5739
|
})();
|
|
5467
5740
|
return reloading;
|
|
5468
5741
|
};
|
|
5742
|
+
void initialBootstrap.finally(() => {
|
|
5743
|
+
if (reloading === initialBootstrap) reloading = null;
|
|
5744
|
+
});
|
|
5745
|
+
const tracker = {
|
|
5746
|
+
status: (name) => bootstrapStatus.get(name),
|
|
5747
|
+
list: () => {
|
|
5748
|
+
const now = Date.now();
|
|
5749
|
+
return [...bootstrapStatus.entries()].map(([name, status2]) => ({
|
|
5750
|
+
name,
|
|
5751
|
+
status: status2,
|
|
5752
|
+
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
5753
|
+
}));
|
|
5754
|
+
}
|
|
5755
|
+
};
|
|
5469
5756
|
const sighupHandler = () => {
|
|
5470
5757
|
void reload().catch((err) => {
|
|
5471
5758
|
console.warn(`neatd: SIGHUP reload failed \u2014 ${err.message}`);
|
|
@@ -5487,10 +5774,19 @@ async function startDaemon(opts = {}) {
|
|
|
5487
5774
|
} catch {
|
|
5488
5775
|
}
|
|
5489
5776
|
}
|
|
5490
|
-
await
|
|
5777
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5491
5778
|
});
|
|
5492
5779
|
};
|
|
5493
|
-
return {
|
|
5780
|
+
return {
|
|
5781
|
+
slots,
|
|
5782
|
+
reload,
|
|
5783
|
+
stop,
|
|
5784
|
+
pidPath,
|
|
5785
|
+
restAddress,
|
|
5786
|
+
otlpAddress,
|
|
5787
|
+
bootstrap: tracker,
|
|
5788
|
+
initialBootstrap
|
|
5789
|
+
};
|
|
5494
5790
|
}
|
|
5495
5791
|
|
|
5496
5792
|
// src/neatd.ts
|
|
@@ -5500,7 +5796,7 @@ init_auth();
|
|
|
5500
5796
|
init_cjs_shims();
|
|
5501
5797
|
var import_node_child_process = require("child_process");
|
|
5502
5798
|
var import_node_net = __toESM(require("net"), 1);
|
|
5503
|
-
var
|
|
5799
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
5504
5800
|
var DEFAULT_WEB_PORT = 6328;
|
|
5505
5801
|
async function assertPortFree(port) {
|
|
5506
5802
|
await new Promise((resolve, reject) => {
|
|
@@ -5528,10 +5824,10 @@ function resolveWebPackageDir() {
|
|
|
5528
5824
|
eval("require")
|
|
5529
5825
|
);
|
|
5530
5826
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
5531
|
-
return
|
|
5827
|
+
return import_node_path38.default.dirname(pkgJsonPath);
|
|
5532
5828
|
}
|
|
5533
5829
|
function resolveStandaloneServerEntry(webDir) {
|
|
5534
|
-
return
|
|
5830
|
+
return import_node_path38.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
5535
5831
|
}
|
|
5536
5832
|
async function spawnWebUI(restPort) {
|
|
5537
5833
|
const portRaw = process.env.NEAT_WEB_PORT;
|
|
@@ -5556,7 +5852,7 @@ async function spawnWebUI(restPort) {
|
|
|
5556
5852
|
NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
|
|
5557
5853
|
};
|
|
5558
5854
|
const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
|
|
5559
|
-
cwd:
|
|
5855
|
+
cwd: import_node_path38.default.dirname(serverEntry),
|
|
5560
5856
|
env,
|
|
5561
5857
|
stdio: ["ignore", "inherit", "inherit"],
|
|
5562
5858
|
detached: false
|
|
@@ -5669,14 +5965,14 @@ function localVersion() {
|
|
|
5669
5965
|
}
|
|
5670
5966
|
function neatHome2() {
|
|
5671
5967
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
5672
|
-
return
|
|
5968
|
+
return import_node_path39.default.resolve(process.env.NEAT_HOME);
|
|
5673
5969
|
}
|
|
5674
5970
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5675
|
-
return
|
|
5971
|
+
return import_node_path39.default.join(home, ".neat");
|
|
5676
5972
|
}
|
|
5677
5973
|
async function readPid() {
|
|
5678
5974
|
try {
|
|
5679
|
-
const raw = await
|
|
5975
|
+
const raw = await import_node_fs23.promises.readFile(import_node_path39.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
5680
5976
|
const n = Number.parseInt(raw.trim(), 10);
|
|
5681
5977
|
return Number.isFinite(n) ? n : null;
|
|
5682
5978
|
} catch {
|