@neat.is/core 0.3.7 → 0.4.0
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-75IBA4UL.js → chunk-4V23KYOP.js} +98 -2
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-IXIFJKMM.js} +16 -2
- package/dist/chunk-IXIFJKMM.js.map +1 -0
- package/dist/{chunk-EDHOCFOG.js → chunk-N6RPINEJ.js} +16 -5
- package/dist/chunk-N6RPINEJ.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-UPW4CMOH.js} +368 -270
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +2009 -421
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +1607 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +259 -29
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +387 -161
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +26 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- package/dist/chunk-ZU2RQRCN.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -41,6 +41,54 @@ var init_cjs_shims = __esm({
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// src/auth.ts
|
|
45
|
+
function mountBearerAuth(app, opts) {
|
|
46
|
+
if (!opts.token || opts.token.length === 0) return;
|
|
47
|
+
if (opts.trustProxy) return;
|
|
48
|
+
const expected = Buffer.from(opts.token, "utf8");
|
|
49
|
+
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
50
|
+
const publicRead = opts.publicRead === true;
|
|
51
|
+
app.addHook("preHandler", (req, reply, done) => {
|
|
52
|
+
const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
53
|
+
for (const suffix of suffixes) {
|
|
54
|
+
if (path45 === suffix || path45.endsWith(suffix)) {
|
|
55
|
+
done();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
60
|
+
done();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const header = req.headers.authorization;
|
|
64
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
65
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
69
|
+
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
70
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
done();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
var import_node_crypto, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
|
|
77
|
+
var init_auth = __esm({
|
|
78
|
+
"src/auth.ts"() {
|
|
79
|
+
"use strict";
|
|
80
|
+
init_cjs_shims();
|
|
81
|
+
import_node_crypto = require("crypto");
|
|
82
|
+
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
83
|
+
DEFAULT_UNAUTH_SUFFIXES = [
|
|
84
|
+
"/health",
|
|
85
|
+
"/healthz",
|
|
86
|
+
"/readyz",
|
|
87
|
+
"/api/config"
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
44
92
|
// src/otel-grpc.ts
|
|
45
93
|
var otel_grpc_exports = {};
|
|
46
94
|
__export(otel_grpc_exports, {
|
|
@@ -101,8 +149,8 @@ function reshapeGrpcRequest(req) {
|
|
|
101
149
|
};
|
|
102
150
|
}
|
|
103
151
|
function resolveProtoRoot() {
|
|
104
|
-
const here =
|
|
105
|
-
return
|
|
152
|
+
const here = import_node_path35.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
153
|
+
return import_node_path35.default.resolve(here, "..", "proto");
|
|
106
154
|
}
|
|
107
155
|
function loadTraceService() {
|
|
108
156
|
const protoRoot = resolveProtoRoot();
|
|
@@ -123,8 +171,21 @@ function loadTraceService() {
|
|
|
123
171
|
async function startOtelGrpcReceiver(opts) {
|
|
124
172
|
const server = new grpc.Server();
|
|
125
173
|
const service = loadTraceService();
|
|
174
|
+
const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
|
|
175
|
+
const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
|
|
126
176
|
server.addService(service, {
|
|
127
177
|
Export: (call, callback) => {
|
|
178
|
+
if (requiresAuth) {
|
|
179
|
+
const meta = call.metadata.get("authorization");
|
|
180
|
+
const got = meta.length > 0 ? String(meta[0]) : "";
|
|
181
|
+
const a = Buffer.from(got, "utf8");
|
|
182
|
+
const b = Buffer.from(expectedHeader, "utf8");
|
|
183
|
+
const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
184
|
+
if (!ok) {
|
|
185
|
+
callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
128
189
|
void (async () => {
|
|
129
190
|
try {
|
|
130
191
|
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
@@ -157,13 +218,14 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
157
218
|
})
|
|
158
219
|
};
|
|
159
220
|
}
|
|
160
|
-
var import_node_url,
|
|
221
|
+
var import_node_url, import_node_path35, import_node_crypto2, grpc, protoLoader;
|
|
161
222
|
var init_otel_grpc = __esm({
|
|
162
223
|
"src/otel-grpc.ts"() {
|
|
163
224
|
"use strict";
|
|
164
225
|
init_cjs_shims();
|
|
165
226
|
import_node_url = require("url");
|
|
166
|
-
|
|
227
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
228
|
+
import_node_crypto2 = require("crypto");
|
|
167
229
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
168
230
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
169
231
|
init_otel();
|
|
@@ -226,6 +288,15 @@ function isoFromUnixNano(nanos) {
|
|
|
226
288
|
return void 0;
|
|
227
289
|
}
|
|
228
290
|
}
|
|
291
|
+
function pickEnv(spanAttrs, resourceAttrs) {
|
|
292
|
+
for (const attrs of [spanAttrs, resourceAttrs]) {
|
|
293
|
+
for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
|
|
294
|
+
const v = attrs[key];
|
|
295
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return ENV_FALLBACK;
|
|
299
|
+
}
|
|
229
300
|
function parseOtlpRequest(body) {
|
|
230
301
|
const out = [];
|
|
231
302
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -245,6 +316,7 @@ function parseOtlpRequest(body) {
|
|
|
245
316
|
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
246
317
|
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
247
318
|
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
319
|
+
env: pickEnv(attrs, resourceAttrs),
|
|
248
320
|
attributes: attrs,
|
|
249
321
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
250
322
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
@@ -259,10 +331,10 @@ function parseOtlpRequest(body) {
|
|
|
259
331
|
return out;
|
|
260
332
|
}
|
|
261
333
|
function loadProtoRoot() {
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
334
|
+
const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
335
|
+
const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
|
|
264
336
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
337
|
+
root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
|
|
266
338
|
root.loadSync(
|
|
267
339
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
340
|
{ keepCase: true }
|
|
@@ -304,6 +376,7 @@ async function buildOtelReceiver(opts) {
|
|
|
304
376
|
logger: false,
|
|
305
377
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
306
378
|
});
|
|
379
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
307
380
|
const queue = [];
|
|
308
381
|
let draining = false;
|
|
309
382
|
let drainPromise = Promise.resolve();
|
|
@@ -382,15 +455,19 @@ async function buildOtelReceiver(opts) {
|
|
|
382
455
|
};
|
|
383
456
|
return decorated;
|
|
384
457
|
}
|
|
385
|
-
var
|
|
458
|
+
var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
386
459
|
var init_otel = __esm({
|
|
387
460
|
"src/otel.ts"() {
|
|
388
461
|
"use strict";
|
|
389
462
|
init_cjs_shims();
|
|
390
|
-
|
|
463
|
+
import_node_path36 = __toESM(require("path"), 1);
|
|
391
464
|
import_node_url2 = require("url");
|
|
392
465
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
393
466
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
467
|
+
init_auth();
|
|
468
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
469
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
470
|
+
ENV_FALLBACK = "unknown";
|
|
394
471
|
exportTraceServiceRequestType = null;
|
|
395
472
|
exportTraceServiceResponseType = null;
|
|
396
473
|
cachedProtobufResponseBody = null;
|
|
@@ -409,8 +486,8 @@ __export(cli_exports, {
|
|
|
409
486
|
});
|
|
410
487
|
module.exports = __toCommonJS(cli_exports);
|
|
411
488
|
init_cjs_shims();
|
|
412
|
-
var
|
|
413
|
-
var
|
|
489
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
490
|
+
var import_node_fs28 = require("fs");
|
|
414
491
|
|
|
415
492
|
// src/graph.ts
|
|
416
493
|
init_cjs_shims();
|
|
@@ -920,19 +997,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
920
997
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
921
998
|
let best = { path: [start], edges: [] };
|
|
922
999
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
923
|
-
function step(node,
|
|
924
|
-
if (
|
|
925
|
-
best = { path: [...
|
|
1000
|
+
function step(node, path45, edges) {
|
|
1001
|
+
if (path45.length > best.path.length) {
|
|
1002
|
+
best = { path: [...path45], edges: [...edges] };
|
|
926
1003
|
}
|
|
927
|
-
if (
|
|
1004
|
+
if (path45.length - 1 >= maxDepth) return;
|
|
928
1005
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
929
1006
|
for (const [srcId, edge] of incoming) {
|
|
930
1007
|
if (visited.has(srcId)) continue;
|
|
931
1008
|
visited.add(srcId);
|
|
932
|
-
|
|
1009
|
+
path45.push(srcId);
|
|
933
1010
|
edges.push(edge);
|
|
934
|
-
step(srcId,
|
|
935
|
-
|
|
1011
|
+
step(srcId, path45, edges);
|
|
1012
|
+
path45.pop();
|
|
936
1013
|
edges.pop();
|
|
937
1014
|
visited.delete(srcId);
|
|
938
1015
|
}
|
|
@@ -1505,52 +1582,64 @@ function cacheSpanService(span, now) {
|
|
|
1505
1582
|
if (!span.traceId || !span.spanId) return;
|
|
1506
1583
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1507
1584
|
parentSpanCache.delete(key);
|
|
1508
|
-
parentSpanCache.set(key, {
|
|
1585
|
+
parentSpanCache.set(key, {
|
|
1586
|
+
service: span.service,
|
|
1587
|
+
env: span.env ?? "unknown",
|
|
1588
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1589
|
+
});
|
|
1509
1590
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1510
1591
|
const oldest = parentSpanCache.keys().next().value;
|
|
1511
1592
|
if (!oldest) break;
|
|
1512
1593
|
parentSpanCache.delete(oldest);
|
|
1513
1594
|
}
|
|
1514
1595
|
}
|
|
1515
|
-
function
|
|
1596
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1516
1597
|
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1517
1598
|
if (!entry2) return null;
|
|
1518
1599
|
if (entry2.expiresAt <= now) {
|
|
1519
1600
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1520
1601
|
return null;
|
|
1521
1602
|
}
|
|
1522
|
-
return entry2.service;
|
|
1603
|
+
return { service: entry2.service, env: entry2.env };
|
|
1523
1604
|
}
|
|
1524
|
-
function resolveServiceId(graph, host) {
|
|
1525
|
-
const
|
|
1526
|
-
if (graph.hasNode(
|
|
1527
|
-
|
|
1605
|
+
function resolveServiceId(graph, host, env) {
|
|
1606
|
+
const envTagged = (0, import_types3.serviceId)(host, env);
|
|
1607
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1608
|
+
const envLess = (0, import_types3.serviceId)(host);
|
|
1609
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1610
|
+
let sameEnv = null;
|
|
1611
|
+
let envLessMatch = null;
|
|
1612
|
+
let anyMatch = null;
|
|
1528
1613
|
graph.forEachNode((id, attrs) => {
|
|
1529
|
-
if (
|
|
1614
|
+
if (sameEnv) return;
|
|
1530
1615
|
const a = attrs;
|
|
1531
1616
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1532
|
-
|
|
1533
|
-
|
|
1617
|
+
const matchesByName = a.name === host;
|
|
1618
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1619
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1620
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1621
|
+
if (nodeEnv === env) {
|
|
1622
|
+
sameEnv = id;
|
|
1534
1623
|
return;
|
|
1535
1624
|
}
|
|
1536
|
-
if (
|
|
1537
|
-
|
|
1538
|
-
}
|
|
1625
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1626
|
+
else if (!anyMatch) anyMatch = id;
|
|
1539
1627
|
});
|
|
1540
|
-
return
|
|
1628
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1541
1629
|
}
|
|
1542
1630
|
function frontierIdFor(host) {
|
|
1543
1631
|
return (0, import_types3.frontierId)(host);
|
|
1544
1632
|
}
|
|
1545
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1546
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1633
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1634
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1547
1635
|
if (graph.hasNode(id)) return id;
|
|
1548
1636
|
const node = {
|
|
1549
1637
|
id,
|
|
1550
1638
|
type: import_types3.NodeType.ServiceNode,
|
|
1551
1639
|
name: serviceName,
|
|
1552
1640
|
language: "unknown",
|
|
1553
|
-
discoveredVia: "otel"
|
|
1641
|
+
discoveredVia: "otel",
|
|
1642
|
+
...env !== "unknown" ? { env } : {}
|
|
1554
1643
|
};
|
|
1555
1644
|
graph.addNode(id, node);
|
|
1556
1645
|
return id;
|
|
@@ -1696,7 +1785,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1696
1785
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1697
1786
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1698
1787
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1699
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1788
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1700
1789
|
};
|
|
1701
1790
|
}
|
|
1702
1791
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1710,7 +1799,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1710
1799
|
async function handleSpan(ctx, span) {
|
|
1711
1800
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1712
1801
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1713
|
-
const
|
|
1802
|
+
const env = span.env ?? "unknown";
|
|
1803
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1714
1804
|
const isError = span.statusCode === 2;
|
|
1715
1805
|
cacheSpanService(span, nowMs);
|
|
1716
1806
|
let affectedNode = sourceId;
|
|
@@ -1733,7 +1823,7 @@ async function handleSpan(ctx, span) {
|
|
|
1733
1823
|
const host = pickAddress(span);
|
|
1734
1824
|
let resolvedViaAddress = false;
|
|
1735
1825
|
if (host && host !== span.service) {
|
|
1736
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1826
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1737
1827
|
if (targetId && targetId !== sourceId) {
|
|
1738
1828
|
upsertObservedEdge(
|
|
1739
1829
|
ctx.graph,
|
|
@@ -1760,9 +1850,9 @@ async function handleSpan(ctx, span) {
|
|
|
1760
1850
|
}
|
|
1761
1851
|
}
|
|
1762
1852
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1763
|
-
const
|
|
1764
|
-
if (
|
|
1765
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
1853
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1854
|
+
if (parent && parent.service !== span.service) {
|
|
1855
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1766
1856
|
upsertObservedEdge(
|
|
1767
1857
|
ctx.graph,
|
|
1768
1858
|
import_types3.EdgeType.CALLS,
|
|
@@ -1958,6 +2048,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
1958
2048
|
throw err;
|
|
1959
2049
|
}
|
|
1960
2050
|
}
|
|
2051
|
+
function mergeSnapshot(graph, snapshot) {
|
|
2052
|
+
const exported = snapshot.graph;
|
|
2053
|
+
let nodesAdded = 0;
|
|
2054
|
+
let edgesAdded = 0;
|
|
2055
|
+
for (const node of exported.nodes ?? []) {
|
|
2056
|
+
if (graph.hasNode(node.key)) continue;
|
|
2057
|
+
if (!node.attributes) continue;
|
|
2058
|
+
graph.addNode(node.key, node.attributes);
|
|
2059
|
+
nodesAdded++;
|
|
2060
|
+
}
|
|
2061
|
+
for (const edge of exported.edges ?? []) {
|
|
2062
|
+
const attrs = edge.attributes;
|
|
2063
|
+
if (!attrs) continue;
|
|
2064
|
+
const id = edge.key ?? attrs.id;
|
|
2065
|
+
if (!id) continue;
|
|
2066
|
+
if (graph.hasEdge(id)) continue;
|
|
2067
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
2068
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
2069
|
+
edgesAdded++;
|
|
2070
|
+
}
|
|
2071
|
+
return { nodesAdded, edgesAdded };
|
|
2072
|
+
}
|
|
1961
2073
|
|
|
1962
2074
|
// src/extract/services.ts
|
|
1963
2075
|
init_cjs_shims();
|
|
@@ -2366,6 +2478,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2366
2478
|
}
|
|
2367
2479
|
return [...found];
|
|
2368
2480
|
}
|
|
2481
|
+
function detectJsFramework(pkg) {
|
|
2482
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2483
|
+
if (deps["next"] !== void 0) return "next";
|
|
2484
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2485
|
+
for (const k of Object.keys(deps)) {
|
|
2486
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2487
|
+
}
|
|
2488
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2489
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2490
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2491
|
+
return void 0;
|
|
2492
|
+
}
|
|
2369
2493
|
async function discoverNodeService(scanPath, dir) {
|
|
2370
2494
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2371
2495
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2377,6 +2501,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2377
2501
|
return null;
|
|
2378
2502
|
}
|
|
2379
2503
|
if (!pkg.name) return null;
|
|
2504
|
+
const framework = detectJsFramework(pkg);
|
|
2380
2505
|
const node = {
|
|
2381
2506
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2382
2507
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2385,7 +2510,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2385
2510
|
version: pkg.version,
|
|
2386
2511
|
dependencies: pkg.dependencies ?? {},
|
|
2387
2512
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2388
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2513
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2514
|
+
...framework ? { framework } : {}
|
|
2389
2515
|
};
|
|
2390
2516
|
return { pkg, dir, node };
|
|
2391
2517
|
}
|
|
@@ -4175,128 +4301,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4175
4301
|
return result;
|
|
4176
4302
|
}
|
|
4177
4303
|
|
|
4178
|
-
// src/persist.ts
|
|
4179
|
-
init_cjs_shims();
|
|
4180
|
-
var import_node_fs18 = require("fs");
|
|
4181
|
-
var import_node_path31 = __toESM(require("path"), 1);
|
|
4182
|
-
var import_types19 = require("@neat.is/types");
|
|
4183
|
-
var SCHEMA_VERSION = 3;
|
|
4184
|
-
function migrateV1ToV2(payload) {
|
|
4185
|
-
const nodes = payload.graph.nodes;
|
|
4186
|
-
if (Array.isArray(nodes)) {
|
|
4187
|
-
for (const node of nodes) {
|
|
4188
|
-
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4189
|
-
delete node.attributes.pgDriverVersion;
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
return { ...payload, schemaVersion: 2 };
|
|
4194
|
-
}
|
|
4195
|
-
function migrateV2ToV3(payload) {
|
|
4196
|
-
const edges = payload.graph.edges;
|
|
4197
|
-
if (Array.isArray(edges)) {
|
|
4198
|
-
for (const edge of edges) {
|
|
4199
|
-
const attrs = edge.attributes;
|
|
4200
|
-
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4201
|
-
attrs.provenance = import_types19.Provenance.OBSERVED;
|
|
4202
|
-
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4203
|
-
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4204
|
-
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4205
|
-
if (type && source && target) {
|
|
4206
|
-
const newId = (0, import_types19.observedEdgeId)(source, target, type);
|
|
4207
|
-
attrs.id = newId;
|
|
4208
|
-
if (edge.key) edge.key = newId;
|
|
4209
|
-
}
|
|
4210
|
-
}
|
|
4211
|
-
}
|
|
4212
|
-
return { ...payload, schemaVersion: 3 };
|
|
4213
|
-
}
|
|
4214
|
-
async function ensureDir(filePath) {
|
|
4215
|
-
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4216
|
-
}
|
|
4217
|
-
async function saveGraphToDisk(graph, outPath) {
|
|
4218
|
-
await ensureDir(outPath);
|
|
4219
|
-
const payload = {
|
|
4220
|
-
schemaVersion: SCHEMA_VERSION,
|
|
4221
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4222
|
-
graph: graph.export()
|
|
4223
|
-
};
|
|
4224
|
-
const tmp = `${outPath}.tmp`;
|
|
4225
|
-
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4226
|
-
await import_node_fs18.promises.rename(tmp, outPath);
|
|
4227
|
-
}
|
|
4228
|
-
async function loadGraphFromDisk(graph, outPath) {
|
|
4229
|
-
let raw;
|
|
4230
|
-
try {
|
|
4231
|
-
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
4232
|
-
} catch (err) {
|
|
4233
|
-
if (err.code === "ENOENT") return;
|
|
4234
|
-
throw err;
|
|
4235
|
-
}
|
|
4236
|
-
let payload = JSON.parse(raw);
|
|
4237
|
-
if (payload.schemaVersion === 1) {
|
|
4238
|
-
payload = migrateV1ToV2(payload);
|
|
4239
|
-
}
|
|
4240
|
-
if (payload.schemaVersion === 2) {
|
|
4241
|
-
payload = migrateV2ToV3(payload);
|
|
4242
|
-
}
|
|
4243
|
-
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4244
|
-
throw new Error(
|
|
4245
|
-
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4246
|
-
);
|
|
4247
|
-
}
|
|
4248
|
-
graph.clear();
|
|
4249
|
-
graph.import(payload.graph);
|
|
4250
|
-
}
|
|
4251
|
-
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4252
|
-
let stopped = false;
|
|
4253
|
-
const tick = async () => {
|
|
4254
|
-
if (stopped) return;
|
|
4255
|
-
try {
|
|
4256
|
-
await saveGraphToDisk(graph, outPath);
|
|
4257
|
-
} catch (err) {
|
|
4258
|
-
console.error("persist: periodic save failed", err);
|
|
4259
|
-
}
|
|
4260
|
-
};
|
|
4261
|
-
const interval = setInterval(() => {
|
|
4262
|
-
void tick();
|
|
4263
|
-
}, intervalMs);
|
|
4264
|
-
const onSignal = (signal) => {
|
|
4265
|
-
void (async () => {
|
|
4266
|
-
try {
|
|
4267
|
-
await saveGraphToDisk(graph, outPath);
|
|
4268
|
-
} catch (err) {
|
|
4269
|
-
console.error(`persist: ${signal} save failed`, err);
|
|
4270
|
-
} finally {
|
|
4271
|
-
process.exit(0);
|
|
4272
|
-
}
|
|
4273
|
-
})();
|
|
4274
|
-
};
|
|
4275
|
-
process.on("SIGTERM", onSignal);
|
|
4276
|
-
process.on("SIGINT", onSignal);
|
|
4277
|
-
return () => {
|
|
4278
|
-
stopped = true;
|
|
4279
|
-
clearInterval(interval);
|
|
4280
|
-
process.off("SIGTERM", onSignal);
|
|
4281
|
-
process.off("SIGINT", onSignal);
|
|
4282
|
-
};
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
// src/watch.ts
|
|
4286
|
-
init_cjs_shims();
|
|
4287
|
-
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
4288
|
-
var import_node_path37 = __toESM(require("path"), 1);
|
|
4289
|
-
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4290
|
-
|
|
4291
|
-
// src/api.ts
|
|
4292
|
-
init_cjs_shims();
|
|
4293
|
-
var import_fastify = __toESM(require("fastify"), 1);
|
|
4294
|
-
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4295
|
-
var import_types22 = require("@neat.is/types");
|
|
4296
|
-
|
|
4297
4304
|
// src/divergences.ts
|
|
4298
4305
|
init_cjs_shims();
|
|
4299
|
-
var
|
|
4306
|
+
var import_types19 = require("@neat.is/types");
|
|
4300
4307
|
function bucketKey(source, target, type) {
|
|
4301
4308
|
return `${type}|${source}|${target}`;
|
|
4302
4309
|
}
|
|
@@ -4304,22 +4311,22 @@ function bucketEdges(graph) {
|
|
|
4304
4311
|
const buckets = /* @__PURE__ */ new Map();
|
|
4305
4312
|
graph.forEachEdge((id, attrs) => {
|
|
4306
4313
|
const e = attrs;
|
|
4307
|
-
const parsed = (0,
|
|
4314
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
4308
4315
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4309
4316
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4310
4317
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4311
4318
|
switch (provenance) {
|
|
4312
|
-
case
|
|
4319
|
+
case import_types19.Provenance.EXTRACTED:
|
|
4313
4320
|
cur.extracted = e;
|
|
4314
4321
|
break;
|
|
4315
|
-
case
|
|
4322
|
+
case import_types19.Provenance.OBSERVED:
|
|
4316
4323
|
cur.observed = e;
|
|
4317
4324
|
break;
|
|
4318
|
-
case
|
|
4325
|
+
case import_types19.Provenance.INFERRED:
|
|
4319
4326
|
cur.inferred = e;
|
|
4320
4327
|
break;
|
|
4321
4328
|
default:
|
|
4322
|
-
if (e.provenance ===
|
|
4329
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
4323
4330
|
}
|
|
4324
4331
|
buckets.set(key, cur);
|
|
4325
4332
|
});
|
|
@@ -4328,7 +4335,7 @@ function bucketEdges(graph) {
|
|
|
4328
4335
|
function nodeIsFrontier(graph, nodeId) {
|
|
4329
4336
|
if (!graph.hasNode(nodeId)) return false;
|
|
4330
4337
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4331
|
-
return attrs.type ===
|
|
4338
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
4332
4339
|
}
|
|
4333
4340
|
function clampConfidence(n) {
|
|
4334
4341
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4389,7 +4396,7 @@ function declaredHostFor(svc) {
|
|
|
4389
4396
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4390
4397
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4391
4398
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4392
|
-
if (e.type ===
|
|
4399
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4393
4400
|
return true;
|
|
4394
4401
|
}
|
|
4395
4402
|
}
|
|
@@ -4402,10 +4409,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4402
4409
|
const out = [];
|
|
4403
4410
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4404
4411
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4405
|
-
if (edge.type !==
|
|
4406
|
-
if (edge.provenance !==
|
|
4412
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4413
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4407
4414
|
const target = graph.getNodeAttributes(edge.target);
|
|
4408
|
-
if (target.type !==
|
|
4415
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4409
4416
|
const observedHost = target.host?.trim();
|
|
4410
4417
|
if (!observedHost) continue;
|
|
4411
4418
|
if (observedHost === declaredHost) continue;
|
|
@@ -4427,10 +4434,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4427
4434
|
const deps = svc.dependencies ?? {};
|
|
4428
4435
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4429
4436
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4430
|
-
if (edge.type !==
|
|
4431
|
-
if (edge.provenance !==
|
|
4437
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4438
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4432
4439
|
const target = graph.getNodeAttributes(edge.target);
|
|
4433
|
-
if (target.type !==
|
|
4440
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4434
4441
|
for (const pair of compatPairs()) {
|
|
4435
4442
|
if (pair.engine !== target.engine) continue;
|
|
4436
4443
|
const declared = deps[pair.driver];
|
|
@@ -4491,7 +4498,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4491
4498
|
}
|
|
4492
4499
|
graph.forEachNode((nodeId, attrs) => {
|
|
4493
4500
|
const n = attrs;
|
|
4494
|
-
if (n.type !==
|
|
4501
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4495
4502
|
const svc = n;
|
|
4496
4503
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4497
4504
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4524,71 +4531,328 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4524
4531
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4525
4532
|
return a.target.localeCompare(b.target);
|
|
4526
4533
|
});
|
|
4527
|
-
return
|
|
4534
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4528
4535
|
divergences: filtered,
|
|
4529
4536
|
totalAffected: filtered.length,
|
|
4530
4537
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4531
4538
|
});
|
|
4532
4539
|
}
|
|
4533
4540
|
|
|
4534
|
-
// src/
|
|
4541
|
+
// src/persist.ts
|
|
4535
4542
|
init_cjs_shims();
|
|
4536
|
-
var
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4543
|
+
var import_node_fs18 = require("fs");
|
|
4544
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4545
|
+
var import_types20 = require("@neat.is/types");
|
|
4546
|
+
var SCHEMA_VERSION = 4;
|
|
4547
|
+
function migrateV1ToV2(payload) {
|
|
4548
|
+
const nodes = payload.graph.nodes;
|
|
4549
|
+
if (Array.isArray(nodes)) {
|
|
4550
|
+
for (const node of nodes) {
|
|
4551
|
+
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4552
|
+
delete node.attributes.pgDriverVersion;
|
|
4553
|
+
}
|
|
4542
4554
|
}
|
|
4543
|
-
return await res.json();
|
|
4544
4555
|
}
|
|
4545
|
-
|
|
4546
|
-
return JSON.parse(raw);
|
|
4556
|
+
return { ...payload, schemaVersion: 2 };
|
|
4547
4557
|
}
|
|
4548
|
-
function
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4558
|
+
function migrateV3ToV4(payload) {
|
|
4559
|
+
return { ...payload, schemaVersion: 4 };
|
|
4560
|
+
}
|
|
4561
|
+
function migrateV2ToV3(payload) {
|
|
4562
|
+
const edges = payload.graph.edges;
|
|
4563
|
+
if (Array.isArray(edges)) {
|
|
4564
|
+
for (const edge of edges) {
|
|
4565
|
+
const attrs = edge.attributes;
|
|
4566
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4567
|
+
attrs.provenance = import_types20.Provenance.OBSERVED;
|
|
4568
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4569
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4570
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4571
|
+
if (type && source && target) {
|
|
4572
|
+
const newId = (0, import_types20.observedEdgeId)(source, target, type);
|
|
4573
|
+
attrs.id = newId;
|
|
4574
|
+
if (edge.key) edge.key = newId;
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4555
4577
|
}
|
|
4556
|
-
return
|
|
4578
|
+
return { ...payload, schemaVersion: 3 };
|
|
4557
4579
|
}
|
|
4558
|
-
function
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
const
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
current: { exportedAt: currentExportedAt },
|
|
4568
|
-
added: { nodes: [], edges: [] },
|
|
4569
|
-
removed: { nodes: [], edges: [] },
|
|
4570
|
-
changed: { nodes: [], edges: [] }
|
|
4580
|
+
async function ensureDir(filePath) {
|
|
4581
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4582
|
+
}
|
|
4583
|
+
async function saveGraphToDisk(graph, outPath) {
|
|
4584
|
+
await ensureDir(outPath);
|
|
4585
|
+
const payload = {
|
|
4586
|
+
schemaVersion: SCHEMA_VERSION,
|
|
4587
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4588
|
+
graph: graph.export()
|
|
4571
4589
|
};
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4590
|
+
const tmp = `${outPath}.tmp`;
|
|
4591
|
+
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4592
|
+
await import_node_fs18.promises.rename(tmp, outPath);
|
|
4593
|
+
}
|
|
4594
|
+
async function loadGraphFromDisk(graph, outPath) {
|
|
4595
|
+
let raw;
|
|
4596
|
+
try {
|
|
4597
|
+
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
4598
|
+
} catch (err) {
|
|
4599
|
+
if (err.code === "ENOENT") return;
|
|
4600
|
+
throw err;
|
|
4579
4601
|
}
|
|
4580
|
-
|
|
4581
|
-
|
|
4602
|
+
let payload = JSON.parse(raw);
|
|
4603
|
+
if (payload.schemaVersion === 1) {
|
|
4604
|
+
payload = migrateV1ToV2(payload);
|
|
4582
4605
|
}
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
if (!before) {
|
|
4586
|
-
result.added.edges.push(after);
|
|
4587
|
-
} else if (!shallowEqual(before, after)) {
|
|
4588
|
-
result.changed.edges.push({ id, before, after });
|
|
4589
|
-
}
|
|
4606
|
+
if (payload.schemaVersion === 2) {
|
|
4607
|
+
payload = migrateV2ToV3(payload);
|
|
4590
4608
|
}
|
|
4591
|
-
|
|
4609
|
+
if (payload.schemaVersion === 3) {
|
|
4610
|
+
payload = migrateV3ToV4(payload);
|
|
4611
|
+
}
|
|
4612
|
+
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4613
|
+
throw new Error(
|
|
4614
|
+
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
graph.clear();
|
|
4618
|
+
graph.import(payload.graph);
|
|
4619
|
+
}
|
|
4620
|
+
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4621
|
+
let stopped = false;
|
|
4622
|
+
const tick = async () => {
|
|
4623
|
+
if (stopped) return;
|
|
4624
|
+
try {
|
|
4625
|
+
await saveGraphToDisk(graph, outPath);
|
|
4626
|
+
} catch (err) {
|
|
4627
|
+
console.error("persist: periodic save failed", err);
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
const interval = setInterval(() => {
|
|
4631
|
+
void tick();
|
|
4632
|
+
}, intervalMs);
|
|
4633
|
+
const onSignal = (signal) => {
|
|
4634
|
+
void (async () => {
|
|
4635
|
+
try {
|
|
4636
|
+
await saveGraphToDisk(graph, outPath);
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
console.error(`persist: ${signal} save failed`, err);
|
|
4639
|
+
} finally {
|
|
4640
|
+
process.exit(0);
|
|
4641
|
+
}
|
|
4642
|
+
})();
|
|
4643
|
+
};
|
|
4644
|
+
process.on("SIGTERM", onSignal);
|
|
4645
|
+
process.on("SIGINT", onSignal);
|
|
4646
|
+
return () => {
|
|
4647
|
+
stopped = true;
|
|
4648
|
+
clearInterval(interval);
|
|
4649
|
+
process.off("SIGTERM", onSignal);
|
|
4650
|
+
process.off("SIGINT", onSignal);
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/gitignore.ts
|
|
4655
|
+
init_cjs_shims();
|
|
4656
|
+
var import_node_fs19 = require("fs");
|
|
4657
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4658
|
+
var NEAT_OUT_LINE = "neat-out/";
|
|
4659
|
+
var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
|
|
4660
|
+
function isNeatOutLine(line) {
|
|
4661
|
+
const trimmed = line.trim();
|
|
4662
|
+
return trimmed === "neat-out/" || trimmed === "neat-out";
|
|
4663
|
+
}
|
|
4664
|
+
async function ensureNeatOutIgnored(projectDir) {
|
|
4665
|
+
const file = import_node_path32.default.join(projectDir, ".gitignore");
|
|
4666
|
+
let existing = null;
|
|
4667
|
+
try {
|
|
4668
|
+
existing = await import_node_fs19.promises.readFile(file, "utf8");
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
if (err.code !== "ENOENT") throw err;
|
|
4671
|
+
}
|
|
4672
|
+
if (existing === null) {
|
|
4673
|
+
await import_node_fs19.promises.writeFile(file, `${NEAT_HEADER}
|
|
4674
|
+
${NEAT_OUT_LINE}
|
|
4675
|
+
`, "utf8");
|
|
4676
|
+
return { action: "created", file };
|
|
4677
|
+
}
|
|
4678
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
4679
|
+
if (isNeatOutLine(line)) return { action: "unchanged", file };
|
|
4680
|
+
}
|
|
4681
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4682
|
+
const appended = `${needsLeadingNewline ? "\n" : ""}
|
|
4683
|
+
${NEAT_HEADER}
|
|
4684
|
+
${NEAT_OUT_LINE}
|
|
4685
|
+
`;
|
|
4686
|
+
await import_node_fs19.promises.writeFile(file, existing + appended, "utf8");
|
|
4687
|
+
return { action: "added", file };
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
// src/summary.ts
|
|
4691
|
+
init_cjs_shims();
|
|
4692
|
+
var import_types21 = require("@neat.is/types");
|
|
4693
|
+
function renderOtelEnvBlock() {
|
|
4694
|
+
return [
|
|
4695
|
+
"for prod OTel routing, set these in your deploy platform's env:",
|
|
4696
|
+
" OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
|
|
4697
|
+
" OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
|
|
4698
|
+
].join("\n");
|
|
4699
|
+
}
|
|
4700
|
+
function findIncompatServices(nodes) {
|
|
4701
|
+
return nodes.filter(
|
|
4702
|
+
(n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
function servicesWithoutObserved(nodes, edges) {
|
|
4706
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4707
|
+
for (const e of edges) {
|
|
4708
|
+
if (e.provenance === import_types21.Provenance.OBSERVED) {
|
|
4709
|
+
seen.add(e.source);
|
|
4710
|
+
seen.add(e.target);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
return nodes.filter(
|
|
4714
|
+
(n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
|
|
4715
|
+
);
|
|
4716
|
+
}
|
|
4717
|
+
function formatDivergence(d) {
|
|
4718
|
+
const conf = d.confidence.toFixed(2);
|
|
4719
|
+
return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
4720
|
+
}
|
|
4721
|
+
function renderValueForwardSummary(input) {
|
|
4722
|
+
const { graph, divergences, verbose } = input;
|
|
4723
|
+
const nodes = [];
|
|
4724
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
4725
|
+
const edges = [];
|
|
4726
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
4727
|
+
const lines = [];
|
|
4728
|
+
lines.push("=== neat: findings ===");
|
|
4729
|
+
lines.push("");
|
|
4730
|
+
const incompatServices = findIncompatServices(nodes);
|
|
4731
|
+
const totalIncompats = incompatServices.reduce(
|
|
4732
|
+
(acc, s) => acc + (s.incompatibilities?.length ?? 0),
|
|
4733
|
+
0
|
|
4734
|
+
);
|
|
4735
|
+
lines.push(`compat violations: ${totalIncompats}`);
|
|
4736
|
+
for (const svc of incompatServices) {
|
|
4737
|
+
for (const inc of svc.incompatibilities ?? []) {
|
|
4738
|
+
const detail = formatIncompat(inc);
|
|
4739
|
+
lines.push(` ${svc.name}: ${detail}`);
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
lines.push("");
|
|
4743
|
+
const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
4744
|
+
lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
|
|
4745
|
+
for (const d of top) lines.push(formatDivergence(d));
|
|
4746
|
+
lines.push("");
|
|
4747
|
+
const noObserved = servicesWithoutObserved(nodes, edges);
|
|
4748
|
+
if (noObserved.length > 0) {
|
|
4749
|
+
lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
|
|
4750
|
+
for (const svc of noObserved) lines.push(` ${svc.name}`);
|
|
4751
|
+
lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
|
|
4752
|
+
lines.push("");
|
|
4753
|
+
}
|
|
4754
|
+
lines.push(renderOtelEnvBlock());
|
|
4755
|
+
lines.push("");
|
|
4756
|
+
if (verbose) {
|
|
4757
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4758
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
4759
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
4760
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
4761
|
+
lines.push("=== graph (verbose) ===");
|
|
4762
|
+
lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
4763
|
+
lines.push("nodes:");
|
|
4764
|
+
for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
4765
|
+
lines.push("edges:");
|
|
4766
|
+
for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
4767
|
+
lines.push("");
|
|
4768
|
+
}
|
|
4769
|
+
return lines.join("\n");
|
|
4770
|
+
}
|
|
4771
|
+
function formatIncompat(inc) {
|
|
4772
|
+
if (inc.kind === "node-engine") {
|
|
4773
|
+
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
4774
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
4775
|
+
}
|
|
4776
|
+
if (inc.kind === "package-conflict") {
|
|
4777
|
+
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
4778
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
4779
|
+
}
|
|
4780
|
+
if (inc.kind === "deprecated-api") {
|
|
4781
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
4782
|
+
}
|
|
4783
|
+
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
// src/watch.ts
|
|
4787
|
+
init_cjs_shims();
|
|
4788
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
4789
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
4790
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4791
|
+
|
|
4792
|
+
// src/api.ts
|
|
4793
|
+
init_cjs_shims();
|
|
4794
|
+
var import_fastify = __toESM(require("fastify"), 1);
|
|
4795
|
+
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4796
|
+
var import_types23 = require("@neat.is/types");
|
|
4797
|
+
|
|
4798
|
+
// src/diff.ts
|
|
4799
|
+
init_cjs_shims();
|
|
4800
|
+
var import_node_fs20 = require("fs");
|
|
4801
|
+
async function loadSnapshotForDiff(target) {
|
|
4802
|
+
if (/^https?:\/\//i.test(target)) {
|
|
4803
|
+
const res = await fetch(target);
|
|
4804
|
+
if (!res.ok) {
|
|
4805
|
+
throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
|
|
4806
|
+
}
|
|
4807
|
+
return await res.json();
|
|
4808
|
+
}
|
|
4809
|
+
const raw = await import_node_fs20.promises.readFile(target, "utf8");
|
|
4810
|
+
return JSON.parse(raw);
|
|
4811
|
+
}
|
|
4812
|
+
function indexEntries(entries) {
|
|
4813
|
+
const m = /* @__PURE__ */ new Map();
|
|
4814
|
+
if (!entries) return m;
|
|
4815
|
+
for (const entry2 of entries) {
|
|
4816
|
+
const id = entry2.attributes?.id ?? entry2.key;
|
|
4817
|
+
if (!id) continue;
|
|
4818
|
+
m.set(id, entry2.attributes);
|
|
4819
|
+
}
|
|
4820
|
+
return m;
|
|
4821
|
+
}
|
|
4822
|
+
function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4823
|
+
const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
|
|
4824
|
+
const baseEdges = indexEntries(baseSnapshot.graph?.edges);
|
|
4825
|
+
const liveNodes = /* @__PURE__ */ new Map();
|
|
4826
|
+
liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
|
|
4827
|
+
const liveEdges = /* @__PURE__ */ new Map();
|
|
4828
|
+
liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
|
|
4829
|
+
const result = {
|
|
4830
|
+
base: { exportedAt: baseSnapshot.exportedAt },
|
|
4831
|
+
current: { exportedAt: currentExportedAt },
|
|
4832
|
+
added: { nodes: [], edges: [] },
|
|
4833
|
+
removed: { nodes: [], edges: [] },
|
|
4834
|
+
changed: { nodes: [], edges: [] }
|
|
4835
|
+
};
|
|
4836
|
+
for (const [id, after] of liveNodes) {
|
|
4837
|
+
const before = baseNodes.get(id);
|
|
4838
|
+
if (!before) {
|
|
4839
|
+
result.added.nodes.push(after);
|
|
4840
|
+
} else if (!shallowEqual(before, after)) {
|
|
4841
|
+
result.changed.nodes.push({ id, before, after });
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
for (const [id, before] of baseNodes) {
|
|
4845
|
+
if (!liveNodes.has(id)) result.removed.nodes.push(before);
|
|
4846
|
+
}
|
|
4847
|
+
for (const [id, after] of liveEdges) {
|
|
4848
|
+
const before = baseEdges.get(id);
|
|
4849
|
+
if (!before) {
|
|
4850
|
+
result.added.edges.push(after);
|
|
4851
|
+
} else if (!shallowEqual(before, after)) {
|
|
4852
|
+
result.changed.edges.push({ id, before, after });
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
for (const [id, before] of baseEdges) {
|
|
4592
4856
|
if (!liveEdges.has(id)) result.removed.edges.push(before);
|
|
4593
4857
|
}
|
|
4594
4858
|
return result;
|
|
@@ -4610,23 +4874,23 @@ function canonicalJson(value) {
|
|
|
4610
4874
|
|
|
4611
4875
|
// src/projects.ts
|
|
4612
4876
|
init_cjs_shims();
|
|
4613
|
-
var
|
|
4877
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4614
4878
|
function pathsForProject(project, baseDir) {
|
|
4615
4879
|
if (project === DEFAULT_PROJECT) {
|
|
4616
4880
|
return {
|
|
4617
|
-
snapshotPath:
|
|
4618
|
-
errorsPath:
|
|
4619
|
-
staleEventsPath:
|
|
4620
|
-
embeddingsCachePath:
|
|
4621
|
-
policyViolationsPath:
|
|
4881
|
+
snapshotPath: import_node_path33.default.join(baseDir, "graph.json"),
|
|
4882
|
+
errorsPath: import_node_path33.default.join(baseDir, "errors.ndjson"),
|
|
4883
|
+
staleEventsPath: import_node_path33.default.join(baseDir, "stale-events.ndjson"),
|
|
4884
|
+
embeddingsCachePath: import_node_path33.default.join(baseDir, "embeddings.json"),
|
|
4885
|
+
policyViolationsPath: import_node_path33.default.join(baseDir, "policy-violations.ndjson")
|
|
4622
4886
|
};
|
|
4623
4887
|
}
|
|
4624
4888
|
return {
|
|
4625
|
-
snapshotPath:
|
|
4626
|
-
errorsPath:
|
|
4627
|
-
staleEventsPath:
|
|
4628
|
-
embeddingsCachePath:
|
|
4629
|
-
policyViolationsPath:
|
|
4889
|
+
snapshotPath: import_node_path33.default.join(baseDir, `${project}.json`),
|
|
4890
|
+
errorsPath: import_node_path33.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4891
|
+
staleEventsPath: import_node_path33.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4892
|
+
embeddingsCachePath: import_node_path33.default.join(baseDir, `embeddings.${project}.json`),
|
|
4893
|
+
policyViolationsPath: import_node_path33.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4630
4894
|
};
|
|
4631
4895
|
}
|
|
4632
4896
|
var Projects = class {
|
|
@@ -4662,49 +4926,49 @@ var Projects = class {
|
|
|
4662
4926
|
|
|
4663
4927
|
// src/registry.ts
|
|
4664
4928
|
init_cjs_shims();
|
|
4665
|
-
var
|
|
4929
|
+
var import_node_fs21 = require("fs");
|
|
4666
4930
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4667
|
-
var
|
|
4668
|
-
var
|
|
4931
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
4932
|
+
var import_types22 = require("@neat.is/types");
|
|
4669
4933
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4670
4934
|
var LOCK_RETRY_MS = 50;
|
|
4671
4935
|
function neatHome() {
|
|
4672
4936
|
const override = process.env.NEAT_HOME;
|
|
4673
|
-
if (override && override.length > 0) return
|
|
4674
|
-
return
|
|
4937
|
+
if (override && override.length > 0) return import_node_path34.default.resolve(override);
|
|
4938
|
+
return import_node_path34.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4675
4939
|
}
|
|
4676
4940
|
function registryPath() {
|
|
4677
|
-
return
|
|
4941
|
+
return import_node_path34.default.join(neatHome(), "projects.json");
|
|
4678
4942
|
}
|
|
4679
4943
|
function registryLockPath() {
|
|
4680
|
-
return
|
|
4944
|
+
return import_node_path34.default.join(neatHome(), "projects.json.lock");
|
|
4681
4945
|
}
|
|
4682
4946
|
async function normalizeProjectPath(input) {
|
|
4683
|
-
const resolved =
|
|
4947
|
+
const resolved = import_node_path34.default.resolve(input);
|
|
4684
4948
|
try {
|
|
4685
|
-
return await
|
|
4949
|
+
return await import_node_fs21.promises.realpath(resolved);
|
|
4686
4950
|
} catch {
|
|
4687
4951
|
return resolved;
|
|
4688
4952
|
}
|
|
4689
4953
|
}
|
|
4690
4954
|
async function writeAtomically(target, contents) {
|
|
4691
|
-
await
|
|
4955
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(target), { recursive: true });
|
|
4692
4956
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4693
|
-
const fd = await
|
|
4957
|
+
const fd = await import_node_fs21.promises.open(tmp, "w");
|
|
4694
4958
|
try {
|
|
4695
4959
|
await fd.writeFile(contents, "utf8");
|
|
4696
4960
|
await fd.sync();
|
|
4697
4961
|
} finally {
|
|
4698
4962
|
await fd.close();
|
|
4699
4963
|
}
|
|
4700
|
-
await
|
|
4964
|
+
await import_node_fs21.promises.rename(tmp, target);
|
|
4701
4965
|
}
|
|
4702
4966
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4703
4967
|
const deadline = Date.now() + timeoutMs;
|
|
4704
|
-
await
|
|
4968
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(lockPath), { recursive: true });
|
|
4705
4969
|
while (true) {
|
|
4706
4970
|
try {
|
|
4707
|
-
const fd = await
|
|
4971
|
+
const fd = await import_node_fs21.promises.open(lockPath, "wx");
|
|
4708
4972
|
await fd.close();
|
|
4709
4973
|
return;
|
|
4710
4974
|
} catch (err) {
|
|
@@ -4720,7 +4984,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4720
4984
|
}
|
|
4721
4985
|
}
|
|
4722
4986
|
async function releaseLock(lockPath) {
|
|
4723
|
-
await
|
|
4987
|
+
await import_node_fs21.promises.unlink(lockPath).catch(() => {
|
|
4724
4988
|
});
|
|
4725
4989
|
}
|
|
4726
4990
|
async function withLock(fn) {
|
|
@@ -4736,7 +5000,7 @@ async function readRegistry() {
|
|
|
4736
5000
|
const file = registryPath();
|
|
4737
5001
|
let raw;
|
|
4738
5002
|
try {
|
|
4739
|
-
raw = await
|
|
5003
|
+
raw = await import_node_fs21.promises.readFile(file, "utf8");
|
|
4740
5004
|
} catch (err) {
|
|
4741
5005
|
if (err.code === "ENOENT") {
|
|
4742
5006
|
return { version: 1, projects: [] };
|
|
@@ -4744,10 +5008,10 @@ async function readRegistry() {
|
|
|
4744
5008
|
throw err;
|
|
4745
5009
|
}
|
|
4746
5010
|
const parsed = JSON.parse(raw);
|
|
4747
|
-
return
|
|
5011
|
+
return import_types22.RegistryFileSchema.parse(parsed);
|
|
4748
5012
|
}
|
|
4749
5013
|
async function writeRegistry(reg) {
|
|
4750
|
-
const validated =
|
|
5014
|
+
const validated = import_types22.RegistryFileSchema.parse(reg);
|
|
4751
5015
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4752
5016
|
}
|
|
4753
5017
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4875,6 +5139,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4875
5139
|
}
|
|
4876
5140
|
|
|
4877
5141
|
// src/api.ts
|
|
5142
|
+
init_auth();
|
|
4878
5143
|
function serializeGraph(graph) {
|
|
4879
5144
|
const nodes = [];
|
|
4880
5145
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4998,11 +5263,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4998
5263
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4999
5264
|
const parsed = [];
|
|
5000
5265
|
for (const c of candidates) {
|
|
5001
|
-
const r =
|
|
5266
|
+
const r = import_types23.DivergenceTypeSchema.safeParse(c);
|
|
5002
5267
|
if (!r.success) {
|
|
5003
5268
|
return reply.code(400).send({
|
|
5004
5269
|
error: `unknown divergence type "${c}"`,
|
|
5005
|
-
allowed:
|
|
5270
|
+
allowed: import_types23.DivergenceTypeSchema.options
|
|
5006
5271
|
});
|
|
5007
5272
|
}
|
|
5008
5273
|
parsed.push(r.data);
|
|
@@ -5147,6 +5412,35 @@ function registerRoutes(scope, ctx) {
|
|
|
5147
5412
|
}
|
|
5148
5413
|
}
|
|
5149
5414
|
);
|
|
5415
|
+
scope.post("/snapshot", async (req, reply) => {
|
|
5416
|
+
const proj = resolveProject(registry, req, reply);
|
|
5417
|
+
if (!proj) return;
|
|
5418
|
+
const body = req.body;
|
|
5419
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5420
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5421
|
+
}
|
|
5422
|
+
const snap = body.snapshot;
|
|
5423
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5424
|
+
return reply.code(400).send({
|
|
5425
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
try {
|
|
5429
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5430
|
+
return {
|
|
5431
|
+
project: proj.name,
|
|
5432
|
+
nodesAdded: result.nodesAdded,
|
|
5433
|
+
edgesAdded: result.edgesAdded,
|
|
5434
|
+
nodeCount: proj.graph.order,
|
|
5435
|
+
edgeCount: proj.graph.size
|
|
5436
|
+
};
|
|
5437
|
+
} catch (err) {
|
|
5438
|
+
return reply.code(400).send({
|
|
5439
|
+
error: "snapshot merge failed",
|
|
5440
|
+
details: err.message
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
});
|
|
5150
5444
|
scope.post("/graph/scan", async (req, reply) => {
|
|
5151
5445
|
const proj = resolveProject(registry, req, reply);
|
|
5152
5446
|
if (!proj) return;
|
|
@@ -5186,7 +5480,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5186
5480
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5187
5481
|
let violations = await log.readAll();
|
|
5188
5482
|
if (req.query.severity) {
|
|
5189
|
-
const sev =
|
|
5483
|
+
const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5190
5484
|
if (!sev.success) {
|
|
5191
5485
|
return reply.code(400).send({
|
|
5192
5486
|
error: "invalid severity",
|
|
@@ -5203,7 +5497,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5203
5497
|
scope.post("/policies/check", async (req, reply) => {
|
|
5204
5498
|
const proj = resolveProject(registry, req, reply);
|
|
5205
5499
|
if (!proj) return;
|
|
5206
|
-
const parsed =
|
|
5500
|
+
const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5207
5501
|
if (!parsed.success) {
|
|
5208
5502
|
return reply.code(400).send({
|
|
5209
5503
|
error: "invalid /policies/check body",
|
|
@@ -5240,6 +5534,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5240
5534
|
async function buildApi(opts) {
|
|
5241
5535
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5242
5536
|
await app.register(import_cors.default, { origin: true });
|
|
5537
|
+
mountBearerAuth(app, {
|
|
5538
|
+
token: opts.authToken,
|
|
5539
|
+
trustProxy: opts.trustProxy,
|
|
5540
|
+
publicRead: opts.publicRead
|
|
5541
|
+
});
|
|
5542
|
+
app.get("/api/config", async () => ({
|
|
5543
|
+
publicRead: opts.publicRead === true,
|
|
5544
|
+
authProxy: opts.trustProxy === true
|
|
5545
|
+
}));
|
|
5243
5546
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5244
5547
|
const registry = buildLegacyRegistry(opts);
|
|
5245
5548
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5307,9 +5610,9 @@ init_otel_grpc();
|
|
|
5307
5610
|
|
|
5308
5611
|
// src/search.ts
|
|
5309
5612
|
init_cjs_shims();
|
|
5310
|
-
var
|
|
5311
|
-
var
|
|
5312
|
-
var
|
|
5613
|
+
var import_node_fs22 = require("fs");
|
|
5614
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5615
|
+
var import_node_crypto3 = require("crypto");
|
|
5313
5616
|
var DEFAULT_LIMIT = 10;
|
|
5314
5617
|
var NOMIC_DIM = 768;
|
|
5315
5618
|
var MINI_LM_DIM = 384;
|
|
@@ -5349,7 +5652,7 @@ function embedText(node) {
|
|
|
5349
5652
|
return parts.join(" ");
|
|
5350
5653
|
}
|
|
5351
5654
|
function attrsHash(node) {
|
|
5352
|
-
return (0,
|
|
5655
|
+
return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
5353
5656
|
}
|
|
5354
5657
|
function cosine(a, b) {
|
|
5355
5658
|
if (a.length !== b.length) return 0;
|
|
@@ -5438,7 +5741,7 @@ async function pickEmbedder() {
|
|
|
5438
5741
|
}
|
|
5439
5742
|
async function readCache(cachePath) {
|
|
5440
5743
|
try {
|
|
5441
|
-
const raw = await
|
|
5744
|
+
const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
|
|
5442
5745
|
const parsed = JSON.parse(raw);
|
|
5443
5746
|
if (parsed.version !== 1) return null;
|
|
5444
5747
|
return parsed;
|
|
@@ -5447,8 +5750,8 @@ async function readCache(cachePath) {
|
|
|
5447
5750
|
}
|
|
5448
5751
|
}
|
|
5449
5752
|
async function writeCache(cachePath, cache) {
|
|
5450
|
-
await
|
|
5451
|
-
await
|
|
5753
|
+
await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(cachePath), { recursive: true });
|
|
5754
|
+
await import_node_fs22.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
5452
5755
|
}
|
|
5453
5756
|
var VectorIndex = class {
|
|
5454
5757
|
constructor(embedder, cachePath) {
|
|
@@ -5603,8 +5906,8 @@ var ALL_PHASES = [
|
|
|
5603
5906
|
];
|
|
5604
5907
|
function classifyChange(relPath) {
|
|
5605
5908
|
const phases = /* @__PURE__ */ new Set();
|
|
5606
|
-
const base =
|
|
5607
|
-
const segments = relPath.split(
|
|
5909
|
+
const base = import_node_path38.default.basename(relPath).toLowerCase();
|
|
5910
|
+
const segments = relPath.split(import_node_path38.default.sep).map((s) => s.toLowerCase());
|
|
5608
5911
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
5609
5912
|
phases.add("services");
|
|
5610
5913
|
phases.add("aliases");
|
|
@@ -5699,16 +6002,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
5699
6002
|
if (count >= limit) return;
|
|
5700
6003
|
let entries;
|
|
5701
6004
|
try {
|
|
5702
|
-
entries =
|
|
6005
|
+
entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
|
|
5703
6006
|
} catch {
|
|
5704
6007
|
return;
|
|
5705
6008
|
}
|
|
5706
6009
|
for (const e of entries) {
|
|
5707
6010
|
if (count >= limit) return;
|
|
5708
6011
|
if (!e.isDirectory()) continue;
|
|
5709
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
6012
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path38.default.join(dir, e.name) + import_node_path38.default.sep))) continue;
|
|
5710
6013
|
count++;
|
|
5711
|
-
if (depth < 2) visit(
|
|
6014
|
+
if (depth < 2) visit(import_node_path38.default.join(dir, e.name), depth + 1);
|
|
5712
6015
|
}
|
|
5713
6016
|
};
|
|
5714
6017
|
visit(scanPath, 0);
|
|
@@ -5726,8 +6029,8 @@ async function startWatch(graph, opts) {
|
|
|
5726
6029
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
5727
6030
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
5728
6031
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5729
|
-
const policyFilePath =
|
|
5730
|
-
const policyViolationsPath =
|
|
6032
|
+
const policyFilePath = import_node_path38.default.join(opts.scanPath, "policy.json");
|
|
6033
|
+
const policyViolationsPath = import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
5731
6034
|
let policies = [];
|
|
5732
6035
|
try {
|
|
5733
6036
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -5776,7 +6079,7 @@ async function startWatch(graph, opts) {
|
|
|
5776
6079
|
const host = opts.host ?? "0.0.0.0";
|
|
5777
6080
|
const port = opts.port ?? 8080;
|
|
5778
6081
|
const otelPort = opts.otelPort ?? 4318;
|
|
5779
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
6082
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
|
|
5780
6083
|
let searchIndex;
|
|
5781
6084
|
try {
|
|
5782
6085
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -5794,7 +6097,7 @@ async function startWatch(graph, opts) {
|
|
|
5794
6097
|
// Paths are derived from the explicit options the watch caller passes
|
|
5795
6098
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
5796
6099
|
// fields so the registry shape is complete.
|
|
5797
|
-
...pathsForProject(projectName,
|
|
6100
|
+
...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
|
|
5798
6101
|
snapshotPath: opts.outPath,
|
|
5799
6102
|
errorsPath: opts.errorsPath,
|
|
5800
6103
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5879,9 +6182,9 @@ async function startWatch(graph, opts) {
|
|
|
5879
6182
|
};
|
|
5880
6183
|
const onPath = (absPath) => {
|
|
5881
6184
|
if (shouldIgnore(absPath)) return;
|
|
5882
|
-
const rel =
|
|
6185
|
+
const rel = import_node_path38.default.relative(opts.scanPath, absPath);
|
|
5883
6186
|
if (!rel || rel.startsWith("..")) return;
|
|
5884
|
-
pendingPaths.add(rel.split(
|
|
6187
|
+
pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
|
|
5885
6188
|
const phases = classifyChange(rel);
|
|
5886
6189
|
if (phases.size === 0) {
|
|
5887
6190
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5933,13 +6236,158 @@ async function startWatch(graph, opts) {
|
|
|
5933
6236
|
return { api, stop };
|
|
5934
6237
|
}
|
|
5935
6238
|
|
|
6239
|
+
// src/deploy/detect.ts
|
|
6240
|
+
init_cjs_shims();
|
|
6241
|
+
var import_node_fs24 = require("fs");
|
|
6242
|
+
var import_node_path39 = __toESM(require("path"), 1);
|
|
6243
|
+
var import_node_child_process = require("child_process");
|
|
6244
|
+
var import_node_crypto4 = require("crypto");
|
|
6245
|
+
function generateToken() {
|
|
6246
|
+
return (0, import_node_crypto4.randomBytes)(32).toString("base64url");
|
|
6247
|
+
}
|
|
6248
|
+
async function probeBinary(binary, arg) {
|
|
6249
|
+
return new Promise((resolve) => {
|
|
6250
|
+
const child = (0, import_node_child_process.spawn)(binary, [arg], { stdio: "ignore" });
|
|
6251
|
+
const timer = setTimeout(() => {
|
|
6252
|
+
child.kill("SIGKILL");
|
|
6253
|
+
resolve(false);
|
|
6254
|
+
}, 2e3);
|
|
6255
|
+
child.once("error", () => {
|
|
6256
|
+
clearTimeout(timer);
|
|
6257
|
+
resolve(false);
|
|
6258
|
+
});
|
|
6259
|
+
child.once("exit", (code) => {
|
|
6260
|
+
clearTimeout(timer);
|
|
6261
|
+
resolve(code === 0);
|
|
6262
|
+
});
|
|
6263
|
+
});
|
|
6264
|
+
}
|
|
6265
|
+
async function detectSubstrate(opts = {}) {
|
|
6266
|
+
const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
|
|
6267
|
+
const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
|
|
6268
|
+
if (await hasDocker()) return "docker-compose";
|
|
6269
|
+
if (await hasSystemd()) return "systemd";
|
|
6270
|
+
return "docker-run";
|
|
6271
|
+
}
|
|
6272
|
+
var IMAGE = "ghcr.io/neat-technologies/neat:latest";
|
|
6273
|
+
function emitDockerCompose(cwd) {
|
|
6274
|
+
return [
|
|
6275
|
+
"services:",
|
|
6276
|
+
" neat:",
|
|
6277
|
+
` image: ${IMAGE}`,
|
|
6278
|
+
" restart: unless-stopped",
|
|
6279
|
+
" environment:",
|
|
6280
|
+
" NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
|
|
6281
|
+
" ports:",
|
|
6282
|
+
' - "8080:8080"',
|
|
6283
|
+
' - "4318:4318"',
|
|
6284
|
+
' - "6328:6328"',
|
|
6285
|
+
" volumes:",
|
|
6286
|
+
` - ${cwd}:/workspace`,
|
|
6287
|
+
" - ./neat-data:/neat-out",
|
|
6288
|
+
""
|
|
6289
|
+
].join("\n");
|
|
6290
|
+
}
|
|
6291
|
+
function emitSystemdUnit(cwd) {
|
|
6292
|
+
return [
|
|
6293
|
+
"# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
|
|
6294
|
+
"# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
|
|
6295
|
+
"[Unit]",
|
|
6296
|
+
"Description=NEAT daemon",
|
|
6297
|
+
"After=network-online.target",
|
|
6298
|
+
"Wants=network-online.target",
|
|
6299
|
+
"",
|
|
6300
|
+
"[Service]",
|
|
6301
|
+
"Type=simple",
|
|
6302
|
+
"ExecStart=/usr/local/bin/neatd start --foreground",
|
|
6303
|
+
`WorkingDirectory=${cwd}`,
|
|
6304
|
+
"EnvironmentFile=/etc/neat/neatd.env",
|
|
6305
|
+
"Restart=always",
|
|
6306
|
+
"RestartSec=5",
|
|
6307
|
+
"",
|
|
6308
|
+
"[Install]",
|
|
6309
|
+
"WantedBy=multi-user.target",
|
|
6310
|
+
""
|
|
6311
|
+
].join("\n");
|
|
6312
|
+
}
|
|
6313
|
+
function emitDockerRunSnippet() {
|
|
6314
|
+
return [
|
|
6315
|
+
"#!/usr/bin/env bash",
|
|
6316
|
+
"set -euo pipefail",
|
|
6317
|
+
"",
|
|
6318
|
+
"# Generate a fresh token once, then store it where your secrets live.",
|
|
6319
|
+
': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
|
|
6320
|
+
"",
|
|
6321
|
+
`docker run -d --name neat \\`,
|
|
6322
|
+
' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
|
|
6323
|
+
" -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
|
|
6324
|
+
' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
|
|
6325
|
+
` ${IMAGE}`,
|
|
6326
|
+
""
|
|
6327
|
+
].join("\n");
|
|
6328
|
+
}
|
|
6329
|
+
function renderOtelEnvBlock2(token, host = "<host>") {
|
|
6330
|
+
return [
|
|
6331
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
|
|
6332
|
+
`OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
|
|
6333
|
+
"OTEL_SERVICE_NAME=<service>"
|
|
6334
|
+
].join("\n");
|
|
6335
|
+
}
|
|
6336
|
+
async function runDeploy(opts = {}) {
|
|
6337
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
6338
|
+
const substrate = await detectSubstrate(opts);
|
|
6339
|
+
const token = generateToken();
|
|
6340
|
+
switch (substrate) {
|
|
6341
|
+
case "docker-compose": {
|
|
6342
|
+
const artifactPath = import_node_path39.default.join(cwd, "docker-compose.neat.yml");
|
|
6343
|
+
const contents = emitDockerCompose(cwd);
|
|
6344
|
+
await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
|
|
6345
|
+
return {
|
|
6346
|
+
substrate,
|
|
6347
|
+
artifactPath,
|
|
6348
|
+
token,
|
|
6349
|
+
contents,
|
|
6350
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path39.default.basename(artifactPath)} up -d`
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
6353
|
+
case "systemd": {
|
|
6354
|
+
const artifactPath = import_node_path39.default.join(cwd, "neat.service");
|
|
6355
|
+
const contents = emitSystemdUnit(cwd);
|
|
6356
|
+
await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
|
|
6357
|
+
return {
|
|
6358
|
+
substrate,
|
|
6359
|
+
artifactPath,
|
|
6360
|
+
token,
|
|
6361
|
+
contents,
|
|
6362
|
+
startCommand: [
|
|
6363
|
+
`sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
|
|
6364
|
+
`sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
|
|
6365
|
+
"sudo systemctl daemon-reload && sudo systemctl enable --now neat"
|
|
6366
|
+
].join(" && ")
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
case "docker-run":
|
|
6370
|
+
default: {
|
|
6371
|
+
const contents = emitDockerRunSnippet();
|
|
6372
|
+
return {
|
|
6373
|
+
substrate: "docker-run",
|
|
6374
|
+
token,
|
|
6375
|
+
contents,
|
|
6376
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
|
|
6377
|
+
${contents}EOF
|
|
6378
|
+
)`
|
|
6379
|
+
};
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
|
|
5936
6384
|
// src/installers/index.ts
|
|
5937
6385
|
init_cjs_shims();
|
|
5938
6386
|
|
|
5939
6387
|
// src/installers/javascript.ts
|
|
5940
6388
|
init_cjs_shims();
|
|
5941
|
-
var
|
|
5942
|
-
var
|
|
6389
|
+
var import_node_fs25 = require("fs");
|
|
6390
|
+
var import_node_path40 = __toESM(require("path"), 1);
|
|
5943
6391
|
|
|
5944
6392
|
// src/installers/templates.ts
|
|
5945
6393
|
init_cjs_shims();
|
|
@@ -5987,6 +6435,151 @@ function renderEnvNeat(serviceName) {
|
|
|
5987
6435
|
""
|
|
5988
6436
|
].join("\n");
|
|
5989
6437
|
}
|
|
6438
|
+
var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
|
|
6439
|
+
var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6440
|
+
export async function register() {
|
|
6441
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
6442
|
+
await import('./instrumentation.node')
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
`;
|
|
6446
|
+
var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6447
|
+
export async function register() {
|
|
6448
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
6449
|
+
await import('./instrumentation.node')
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
`;
|
|
6453
|
+
var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6454
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6455
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6456
|
+
import { fileURLToPath } from 'node:url'
|
|
6457
|
+
import path from 'node:path'
|
|
6458
|
+
import dotenv from 'dotenv'
|
|
6459
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6460
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6461
|
+
|
|
6462
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6463
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6464
|
+
|
|
6465
|
+
const sdk = new NodeSDK({
|
|
6466
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6467
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6468
|
+
})
|
|
6469
|
+
sdk.start()
|
|
6470
|
+
`;
|
|
6471
|
+
var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6472
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6473
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6474
|
+
import { fileURLToPath } from 'node:url'
|
|
6475
|
+
import path from 'node:path'
|
|
6476
|
+
import dotenv from 'dotenv'
|
|
6477
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6478
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6479
|
+
|
|
6480
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6481
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6482
|
+
|
|
6483
|
+
const sdk = new NodeSDK({
|
|
6484
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6485
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6486
|
+
})
|
|
6487
|
+
sdk.start()
|
|
6488
|
+
`;
|
|
6489
|
+
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
6490
|
+
var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6491
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6492
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6493
|
+
import { fileURLToPath } from 'node:url'
|
|
6494
|
+
import path from 'node:path'
|
|
6495
|
+
import dotenv from 'dotenv'
|
|
6496
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6497
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6498
|
+
|
|
6499
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6500
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6501
|
+
|
|
6502
|
+
const sdk = new NodeSDK({
|
|
6503
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6504
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6505
|
+
})
|
|
6506
|
+
sdk.start()
|
|
6507
|
+
`;
|
|
6508
|
+
var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6509
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6510
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6511
|
+
const path = require('node:path')
|
|
6512
|
+
try {
|
|
6513
|
+
require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
|
|
6514
|
+
} catch (err) {
|
|
6515
|
+
// dotenv unavailable \u2014 fall through to process.env.
|
|
6516
|
+
}
|
|
6517
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
6518
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
6519
|
+
|
|
6520
|
+
const sdk = new NodeSDK({
|
|
6521
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6522
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6523
|
+
})
|
|
6524
|
+
sdk.start()
|
|
6525
|
+
`;
|
|
6526
|
+
var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6527
|
+
var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6528
|
+
var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6529
|
+
var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6530
|
+
var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6531
|
+
import './otel-init'
|
|
6532
|
+
|
|
6533
|
+
import type { Handle } from '@sveltejs/kit'
|
|
6534
|
+
|
|
6535
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
6536
|
+
return resolve(event)
|
|
6537
|
+
}
|
|
6538
|
+
`;
|
|
6539
|
+
var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6540
|
+
import './otel-init'
|
|
6541
|
+
|
|
6542
|
+
/** @type {import('@sveltejs/kit').Handle} */
|
|
6543
|
+
export const handle = async ({ event, resolve }) => {
|
|
6544
|
+
return resolve(event)
|
|
6545
|
+
}
|
|
6546
|
+
`;
|
|
6547
|
+
var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6548
|
+
var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6549
|
+
var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6550
|
+
import './otel-init'
|
|
6551
|
+
|
|
6552
|
+
export default defineNitroPlugin(() => {
|
|
6553
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
6554
|
+
})
|
|
6555
|
+
`;
|
|
6556
|
+
var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6557
|
+
require('./otel-init')
|
|
6558
|
+
|
|
6559
|
+
module.exports = defineNitroPlugin(() => {
|
|
6560
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
6561
|
+
})
|
|
6562
|
+
`;
|
|
6563
|
+
var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6564
|
+
var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6565
|
+
var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6566
|
+
import './otel-init'
|
|
6567
|
+
|
|
6568
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
6569
|
+
|
|
6570
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
6571
|
+
return next()
|
|
6572
|
+
})
|
|
6573
|
+
`;
|
|
6574
|
+
var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6575
|
+
import './otel-init'
|
|
6576
|
+
|
|
6577
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
6578
|
+
|
|
6579
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
6580
|
+
return next()
|
|
6581
|
+
})
|
|
6582
|
+
`;
|
|
5990
6583
|
|
|
5991
6584
|
// src/installers/javascript.ts
|
|
5992
6585
|
var SDK_PACKAGES = [
|
|
@@ -6009,7 +6602,7 @@ var OTEL_ENV = {
|
|
|
6009
6602
|
};
|
|
6010
6603
|
async function readPackageJson(serviceDir) {
|
|
6011
6604
|
try {
|
|
6012
|
-
const raw = await
|
|
6605
|
+
const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
|
|
6013
6606
|
return JSON.parse(raw);
|
|
6014
6607
|
} catch {
|
|
6015
6608
|
return null;
|
|
@@ -6017,7 +6610,7 @@ async function readPackageJson(serviceDir) {
|
|
|
6017
6610
|
}
|
|
6018
6611
|
async function exists2(p) {
|
|
6019
6612
|
try {
|
|
6020
|
-
await
|
|
6613
|
+
await import_node_fs25.promises.stat(p);
|
|
6021
6614
|
return true;
|
|
6022
6615
|
} catch {
|
|
6023
6616
|
return false;
|
|
@@ -6027,6 +6620,93 @@ async function detect(serviceDir) {
|
|
|
6027
6620
|
const pkg = await readPackageJson(serviceDir);
|
|
6028
6621
|
return pkg !== null && typeof pkg.name === "string";
|
|
6029
6622
|
}
|
|
6623
|
+
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
6624
|
+
async function findNextConfig(serviceDir) {
|
|
6625
|
+
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
6626
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6627
|
+
if (await exists2(candidate)) return candidate;
|
|
6628
|
+
}
|
|
6629
|
+
return null;
|
|
6630
|
+
}
|
|
6631
|
+
function hasNextDependency(pkg) {
|
|
6632
|
+
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
6633
|
+
}
|
|
6634
|
+
function allDeps(pkg) {
|
|
6635
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6636
|
+
}
|
|
6637
|
+
var REMIX_ENTRY_CANDIDATES = [
|
|
6638
|
+
"app/entry.server.ts",
|
|
6639
|
+
"app/entry.server.tsx",
|
|
6640
|
+
"app/entry.server.js",
|
|
6641
|
+
"app/entry.server.jsx"
|
|
6642
|
+
];
|
|
6643
|
+
function hasRemixDependency(pkg) {
|
|
6644
|
+
const deps = allDeps(pkg);
|
|
6645
|
+
if ("remix" in deps) return true;
|
|
6646
|
+
for (const name of Object.keys(deps)) {
|
|
6647
|
+
if (name.startsWith("@remix-run/")) return true;
|
|
6648
|
+
}
|
|
6649
|
+
return false;
|
|
6650
|
+
}
|
|
6651
|
+
async function findRemixEntry(serviceDir) {
|
|
6652
|
+
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
6653
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6654
|
+
if (await exists2(candidate)) return candidate;
|
|
6655
|
+
}
|
|
6656
|
+
return null;
|
|
6657
|
+
}
|
|
6658
|
+
var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
|
|
6659
|
+
var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
|
|
6660
|
+
function hasSvelteKitDependency(pkg) {
|
|
6661
|
+
return "@sveltejs/kit" in allDeps(pkg);
|
|
6662
|
+
}
|
|
6663
|
+
async function findSvelteKitHooks(serviceDir) {
|
|
6664
|
+
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
6665
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6666
|
+
if (await exists2(candidate)) return candidate;
|
|
6667
|
+
}
|
|
6668
|
+
return null;
|
|
6669
|
+
}
|
|
6670
|
+
async function findSvelteKitConfig(serviceDir) {
|
|
6671
|
+
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
6672
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6673
|
+
if (await exists2(candidate)) return candidate;
|
|
6674
|
+
}
|
|
6675
|
+
return null;
|
|
6676
|
+
}
|
|
6677
|
+
var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
6678
|
+
function hasNuxtDependency(pkg) {
|
|
6679
|
+
return "nuxt" in allDeps(pkg);
|
|
6680
|
+
}
|
|
6681
|
+
async function findNuxtConfig(serviceDir) {
|
|
6682
|
+
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
6683
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6684
|
+
if (await exists2(candidate)) return candidate;
|
|
6685
|
+
}
|
|
6686
|
+
return null;
|
|
6687
|
+
}
|
|
6688
|
+
var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
|
|
6689
|
+
function hasAstroDependency(pkg) {
|
|
6690
|
+
return "astro" in allDeps(pkg);
|
|
6691
|
+
}
|
|
6692
|
+
async function findAstroConfig(serviceDir) {
|
|
6693
|
+
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
6694
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6695
|
+
if (await exists2(candidate)) return candidate;
|
|
6696
|
+
}
|
|
6697
|
+
return null;
|
|
6698
|
+
}
|
|
6699
|
+
function parseNextMajor(range) {
|
|
6700
|
+
if (!range) return null;
|
|
6701
|
+
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
6702
|
+
const match = cleaned.match(/^(\d+)/);
|
|
6703
|
+
if (!match) return null;
|
|
6704
|
+
const n = Number(match[1]);
|
|
6705
|
+
return Number.isFinite(n) ? n : null;
|
|
6706
|
+
}
|
|
6707
|
+
async function isTypeScriptProject(serviceDir) {
|
|
6708
|
+
return exists2(import_node_path40.default.join(serviceDir, "tsconfig.json"));
|
|
6709
|
+
}
|
|
6030
6710
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
6031
6711
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
6032
6712
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
@@ -6071,7 +6751,7 @@ function entryFromScript(script) {
|
|
|
6071
6751
|
}
|
|
6072
6752
|
async function resolveEntry(serviceDir, pkg) {
|
|
6073
6753
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
6074
|
-
const candidate =
|
|
6754
|
+
const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
|
|
6075
6755
|
if (await exists2(candidate)) return candidate;
|
|
6076
6756
|
}
|
|
6077
6757
|
if (pkg.bin) {
|
|
@@ -6085,36 +6765,36 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
6085
6765
|
if (typeof first === "string") binEntry = first;
|
|
6086
6766
|
}
|
|
6087
6767
|
if (binEntry) {
|
|
6088
|
-
const candidate =
|
|
6768
|
+
const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
|
|
6089
6769
|
if (await exists2(candidate)) return candidate;
|
|
6090
6770
|
}
|
|
6091
6771
|
}
|
|
6092
6772
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
6093
6773
|
if (startEntry) {
|
|
6094
|
-
const candidate =
|
|
6774
|
+
const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
|
|
6095
6775
|
if (await exists2(candidate)) return candidate;
|
|
6096
6776
|
}
|
|
6097
6777
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
6098
6778
|
if (devEntry) {
|
|
6099
|
-
const candidate =
|
|
6779
|
+
const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
|
|
6100
6780
|
if (await exists2(candidate)) return candidate;
|
|
6101
6781
|
}
|
|
6102
6782
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
6103
|
-
const candidate =
|
|
6783
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6104
6784
|
if (await exists2(candidate)) return candidate;
|
|
6105
6785
|
}
|
|
6106
6786
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
6107
|
-
const candidate =
|
|
6787
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6108
6788
|
if (await exists2(candidate)) return candidate;
|
|
6109
6789
|
}
|
|
6110
6790
|
for (const name of INDEX_CANDIDATES) {
|
|
6111
|
-
const candidate =
|
|
6791
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6112
6792
|
if (await exists2(candidate)) return candidate;
|
|
6113
6793
|
}
|
|
6114
6794
|
return null;
|
|
6115
6795
|
}
|
|
6116
6796
|
function dispatchEntry(entryFile, pkg) {
|
|
6117
|
-
const ext =
|
|
6797
|
+
const ext = import_node_path40.default.extname(entryFile).toLowerCase();
|
|
6118
6798
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
6119
6799
|
if (ext === ".mjs") return "esm";
|
|
6120
6800
|
if (ext === ".cjs") return "cjs";
|
|
@@ -6131,9 +6811,9 @@ function otelInitContents(flavor) {
|
|
|
6131
6811
|
return OTEL_INIT_CJS;
|
|
6132
6812
|
}
|
|
6133
6813
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
6134
|
-
let rel =
|
|
6814
|
+
let rel = import_node_path40.default.relative(import_node_path40.default.dirname(entryFile), otelInitFile);
|
|
6135
6815
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
6136
|
-
rel = rel.split(
|
|
6816
|
+
rel = rel.split(import_node_path40.default.sep).join("/");
|
|
6137
6817
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
6138
6818
|
if (flavor === "esm") return `import '${rel}'`;
|
|
6139
6819
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -6144,9 +6824,358 @@ function lineIsOtelInjection(line) {
|
|
|
6144
6824
|
if (trimmed.length === 0) return false;
|
|
6145
6825
|
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
6146
6826
|
}
|
|
6827
|
+
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
6828
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
6829
|
+
const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
6830
|
+
const instrumentationNodeFile = import_node_path40.default.join(
|
|
6831
|
+
serviceDir,
|
|
6832
|
+
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
6833
|
+
);
|
|
6834
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
6835
|
+
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6836
|
+
const dependencyEdits = [];
|
|
6837
|
+
for (const sdk of SDK_PACKAGES) {
|
|
6838
|
+
if (sdk.name in existingDeps) continue;
|
|
6839
|
+
dependencyEdits.push({
|
|
6840
|
+
file: manifestPath,
|
|
6841
|
+
kind: "add",
|
|
6842
|
+
name: sdk.name,
|
|
6843
|
+
version: sdk.version
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
const generatedFiles = [];
|
|
6847
|
+
if (!await exists2(instrumentationFile)) {
|
|
6848
|
+
generatedFiles.push({
|
|
6849
|
+
file: instrumentationFile,
|
|
6850
|
+
contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
|
|
6851
|
+
skipIfExists: true
|
|
6852
|
+
});
|
|
6853
|
+
}
|
|
6854
|
+
if (!await exists2(instrumentationNodeFile)) {
|
|
6855
|
+
generatedFiles.push({
|
|
6856
|
+
file: instrumentationNodeFile,
|
|
6857
|
+
contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
|
|
6858
|
+
skipIfExists: true
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
if (!await exists2(envNeatFile)) {
|
|
6862
|
+
generatedFiles.push({
|
|
6863
|
+
file: envNeatFile,
|
|
6864
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6865
|
+
skipIfExists: true
|
|
6866
|
+
});
|
|
6867
|
+
}
|
|
6868
|
+
let nextConfigEdit;
|
|
6869
|
+
const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
6870
|
+
const nextMajor = parseNextMajor(nextRange);
|
|
6871
|
+
if (nextMajor !== null && nextMajor < 15) {
|
|
6872
|
+
try {
|
|
6873
|
+
const raw = await import_node_fs25.promises.readFile(nextConfigPath, "utf8");
|
|
6874
|
+
if (!raw.includes("instrumentationHook")) {
|
|
6875
|
+
nextConfigEdit = {
|
|
6876
|
+
file: nextConfigPath,
|
|
6877
|
+
reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
} catch {
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
|
|
6884
|
+
if (empty) {
|
|
6885
|
+
return {
|
|
6886
|
+
language: "javascript",
|
|
6887
|
+
serviceDir,
|
|
6888
|
+
dependencyEdits: [],
|
|
6889
|
+
entrypointEdits: [],
|
|
6890
|
+
envEdits: [],
|
|
6891
|
+
generatedFiles: [],
|
|
6892
|
+
framework: "next"
|
|
6893
|
+
};
|
|
6894
|
+
}
|
|
6895
|
+
return {
|
|
6896
|
+
language: "javascript",
|
|
6897
|
+
serviceDir,
|
|
6898
|
+
dependencyEdits,
|
|
6899
|
+
entrypointEdits: [],
|
|
6900
|
+
envEdits: [OTEL_ENV],
|
|
6901
|
+
generatedFiles,
|
|
6902
|
+
framework: "next",
|
|
6903
|
+
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
6904
|
+
};
|
|
6905
|
+
}
|
|
6906
|
+
function buildDependencyEdits(pkg, manifestPath) {
|
|
6907
|
+
const existingDeps = allDeps(pkg);
|
|
6908
|
+
const edits = [];
|
|
6909
|
+
for (const sdk of SDK_PACKAGES) {
|
|
6910
|
+
if (sdk.name in existingDeps) continue;
|
|
6911
|
+
edits.push({
|
|
6912
|
+
file: manifestPath,
|
|
6913
|
+
kind: "add",
|
|
6914
|
+
name: sdk.name,
|
|
6915
|
+
version: sdk.version
|
|
6916
|
+
});
|
|
6917
|
+
}
|
|
6918
|
+
return edits;
|
|
6919
|
+
}
|
|
6920
|
+
async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
|
|
6921
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
6922
|
+
if (!await exists2(envNeatFile)) {
|
|
6923
|
+
generatedFiles.push({
|
|
6924
|
+
file: envNeatFile,
|
|
6925
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6926
|
+
skipIfExists: true
|
|
6927
|
+
});
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
function fileImportsOtelHook(raw, specifiers) {
|
|
6931
|
+
const lines = raw.split(/\r?\n/);
|
|
6932
|
+
for (const line of lines) {
|
|
6933
|
+
const trimmed = line.trim();
|
|
6934
|
+
for (const spec of specifiers) {
|
|
6935
|
+
const escaped = spec.replace(/\./g, "\\.");
|
|
6936
|
+
const pattern = new RegExp(
|
|
6937
|
+
`(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
|
|
6938
|
+
);
|
|
6939
|
+
if (pattern.test(trimmed)) return true;
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
return false;
|
|
6943
|
+
}
|
|
6944
|
+
async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
|
|
6945
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
6946
|
+
const otelServerFile = import_node_path40.default.join(
|
|
6947
|
+
serviceDir,
|
|
6948
|
+
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
6949
|
+
);
|
|
6950
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
6951
|
+
const generatedFiles = [];
|
|
6952
|
+
if (!await exists2(otelServerFile)) {
|
|
6953
|
+
generatedFiles.push({
|
|
6954
|
+
file: otelServerFile,
|
|
6955
|
+
contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
|
|
6956
|
+
skipIfExists: true
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
6960
|
+
const entrypointEdits = [];
|
|
6961
|
+
try {
|
|
6962
|
+
const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
|
|
6963
|
+
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
6964
|
+
const lines = raw.split(/\r?\n/);
|
|
6965
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
6966
|
+
entrypointEdits.push({
|
|
6967
|
+
file: entryFile,
|
|
6968
|
+
before: firstReal,
|
|
6969
|
+
after: "import './otel.server'"
|
|
6970
|
+
});
|
|
6971
|
+
}
|
|
6972
|
+
} catch {
|
|
6973
|
+
}
|
|
6974
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
6975
|
+
if (empty) {
|
|
6976
|
+
return {
|
|
6977
|
+
language: "javascript",
|
|
6978
|
+
serviceDir,
|
|
6979
|
+
dependencyEdits: [],
|
|
6980
|
+
entrypointEdits: [],
|
|
6981
|
+
envEdits: [],
|
|
6982
|
+
generatedFiles: [],
|
|
6983
|
+
framework: "remix"
|
|
6984
|
+
};
|
|
6985
|
+
}
|
|
6986
|
+
return {
|
|
6987
|
+
language: "javascript",
|
|
6988
|
+
serviceDir,
|
|
6989
|
+
dependencyEdits,
|
|
6990
|
+
entrypointEdits,
|
|
6991
|
+
envEdits: [OTEL_ENV],
|
|
6992
|
+
generatedFiles,
|
|
6993
|
+
framework: "remix"
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
|
|
6997
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
6998
|
+
const otelInitFile = import_node_path40.default.join(
|
|
6999
|
+
serviceDir,
|
|
7000
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
7001
|
+
);
|
|
7002
|
+
const resolvedHooksFile = hooksFile ?? import_node_path40.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
7003
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7004
|
+
const generatedFiles = [];
|
|
7005
|
+
const entrypointEdits = [];
|
|
7006
|
+
if (!await exists2(otelInitFile)) {
|
|
7007
|
+
generatedFiles.push({
|
|
7008
|
+
file: otelInitFile,
|
|
7009
|
+
contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
|
|
7010
|
+
skipIfExists: true
|
|
7011
|
+
});
|
|
7012
|
+
}
|
|
7013
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
7014
|
+
if (hooksFile === null) {
|
|
7015
|
+
generatedFiles.push({
|
|
7016
|
+
file: resolvedHooksFile,
|
|
7017
|
+
contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
|
|
7018
|
+
skipIfExists: true
|
|
7019
|
+
});
|
|
7020
|
+
} else {
|
|
7021
|
+
try {
|
|
7022
|
+
const raw = await import_node_fs25.promises.readFile(hooksFile, "utf8");
|
|
7023
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
7024
|
+
const lines = raw.split(/\r?\n/);
|
|
7025
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
7026
|
+
entrypointEdits.push({
|
|
7027
|
+
file: hooksFile,
|
|
7028
|
+
before: firstReal,
|
|
7029
|
+
after: "import './otel-init'"
|
|
7030
|
+
});
|
|
7031
|
+
}
|
|
7032
|
+
} catch {
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
7036
|
+
if (empty) {
|
|
7037
|
+
return {
|
|
7038
|
+
language: "javascript",
|
|
7039
|
+
serviceDir,
|
|
7040
|
+
dependencyEdits: [],
|
|
7041
|
+
entrypointEdits: [],
|
|
7042
|
+
envEdits: [],
|
|
7043
|
+
generatedFiles: [],
|
|
7044
|
+
framework: "sveltekit"
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
7047
|
+
return {
|
|
7048
|
+
language: "javascript",
|
|
7049
|
+
serviceDir,
|
|
7050
|
+
dependencyEdits,
|
|
7051
|
+
entrypointEdits,
|
|
7052
|
+
envEdits: [OTEL_ENV],
|
|
7053
|
+
generatedFiles,
|
|
7054
|
+
framework: "sveltekit"
|
|
7055
|
+
};
|
|
7056
|
+
}
|
|
7057
|
+
async function planNuxt(serviceDir, pkg, manifestPath) {
|
|
7058
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7059
|
+
const otelPluginFile = import_node_path40.default.join(
|
|
7060
|
+
serviceDir,
|
|
7061
|
+
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
7062
|
+
);
|
|
7063
|
+
const otelInitFile = import_node_path40.default.join(
|
|
7064
|
+
serviceDir,
|
|
7065
|
+
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
7066
|
+
);
|
|
7067
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7068
|
+
const generatedFiles = [];
|
|
7069
|
+
if (!await exists2(otelInitFile)) {
|
|
7070
|
+
generatedFiles.push({
|
|
7071
|
+
file: otelInitFile,
|
|
7072
|
+
contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
|
|
7073
|
+
skipIfExists: true
|
|
7074
|
+
});
|
|
7075
|
+
}
|
|
7076
|
+
if (!await exists2(otelPluginFile)) {
|
|
7077
|
+
generatedFiles.push({
|
|
7078
|
+
file: otelPluginFile,
|
|
7079
|
+
contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
|
|
7080
|
+
skipIfExists: true
|
|
7081
|
+
});
|
|
7082
|
+
}
|
|
7083
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
7084
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
|
|
7085
|
+
if (empty) {
|
|
7086
|
+
return {
|
|
7087
|
+
language: "javascript",
|
|
7088
|
+
serviceDir,
|
|
7089
|
+
dependencyEdits: [],
|
|
7090
|
+
entrypointEdits: [],
|
|
7091
|
+
envEdits: [],
|
|
7092
|
+
generatedFiles: [],
|
|
7093
|
+
framework: "nuxt"
|
|
7094
|
+
};
|
|
7095
|
+
}
|
|
7096
|
+
return {
|
|
7097
|
+
language: "javascript",
|
|
7098
|
+
serviceDir,
|
|
7099
|
+
dependencyEdits,
|
|
7100
|
+
entrypointEdits: [],
|
|
7101
|
+
envEdits: [OTEL_ENV],
|
|
7102
|
+
generatedFiles,
|
|
7103
|
+
framework: "nuxt"
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
7106
|
+
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
7107
|
+
async function findAstroMiddleware(serviceDir) {
|
|
7108
|
+
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
7109
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
7110
|
+
if (await exists2(candidate)) return candidate;
|
|
7111
|
+
}
|
|
7112
|
+
return null;
|
|
7113
|
+
}
|
|
7114
|
+
async function planAstro(serviceDir, pkg, manifestPath) {
|
|
7115
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7116
|
+
const otelInitFile = import_node_path40.default.join(
|
|
7117
|
+
serviceDir,
|
|
7118
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
7119
|
+
);
|
|
7120
|
+
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
7121
|
+
const middlewareFile = existingMiddleware ?? import_node_path40.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
7122
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7123
|
+
const generatedFiles = [];
|
|
7124
|
+
const entrypointEdits = [];
|
|
7125
|
+
if (!await exists2(otelInitFile)) {
|
|
7126
|
+
generatedFiles.push({
|
|
7127
|
+
file: otelInitFile,
|
|
7128
|
+
contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
|
|
7129
|
+
skipIfExists: true
|
|
7130
|
+
});
|
|
7131
|
+
}
|
|
7132
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
7133
|
+
if (existingMiddleware === null) {
|
|
7134
|
+
generatedFiles.push({
|
|
7135
|
+
file: middlewareFile,
|
|
7136
|
+
contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
|
|
7137
|
+
skipIfExists: true
|
|
7138
|
+
});
|
|
7139
|
+
} else {
|
|
7140
|
+
try {
|
|
7141
|
+
const raw = await import_node_fs25.promises.readFile(existingMiddleware, "utf8");
|
|
7142
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
7143
|
+
const lines = raw.split(/\r?\n/);
|
|
7144
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
7145
|
+
entrypointEdits.push({
|
|
7146
|
+
file: existingMiddleware,
|
|
7147
|
+
before: firstReal,
|
|
7148
|
+
after: "import './otel-init'"
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
} catch {
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
7155
|
+
if (empty) {
|
|
7156
|
+
return {
|
|
7157
|
+
language: "javascript",
|
|
7158
|
+
serviceDir,
|
|
7159
|
+
dependencyEdits: [],
|
|
7160
|
+
entrypointEdits: [],
|
|
7161
|
+
envEdits: [],
|
|
7162
|
+
generatedFiles: [],
|
|
7163
|
+
framework: "astro"
|
|
7164
|
+
};
|
|
7165
|
+
}
|
|
7166
|
+
return {
|
|
7167
|
+
language: "javascript",
|
|
7168
|
+
serviceDir,
|
|
7169
|
+
dependencyEdits,
|
|
7170
|
+
entrypointEdits,
|
|
7171
|
+
envEdits: [OTEL_ENV],
|
|
7172
|
+
generatedFiles,
|
|
7173
|
+
framework: "astro"
|
|
7174
|
+
};
|
|
7175
|
+
}
|
|
6147
7176
|
async function plan(serviceDir) {
|
|
6148
7177
|
const pkg = await readPackageJson(serviceDir);
|
|
6149
|
-
const manifestPath =
|
|
7178
|
+
const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
|
|
6150
7179
|
const empty = {
|
|
6151
7180
|
language: "javascript",
|
|
6152
7181
|
serviceDir,
|
|
@@ -6156,13 +7185,44 @@ async function plan(serviceDir) {
|
|
|
6156
7185
|
generatedFiles: []
|
|
6157
7186
|
};
|
|
6158
7187
|
if (!pkg) return empty;
|
|
7188
|
+
if (hasNextDependency(pkg)) {
|
|
7189
|
+
const nextConfig = await findNextConfig(serviceDir);
|
|
7190
|
+
if (nextConfig) {
|
|
7191
|
+
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
if (hasRemixDependency(pkg)) {
|
|
7195
|
+
const remixEntry = await findRemixEntry(serviceDir);
|
|
7196
|
+
if (remixEntry) {
|
|
7197
|
+
return planRemix(serviceDir, pkg, manifestPath, remixEntry);
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
if (hasSvelteKitDependency(pkg)) {
|
|
7201
|
+
const hooks = await findSvelteKitHooks(serviceDir);
|
|
7202
|
+
const config = await findSvelteKitConfig(serviceDir);
|
|
7203
|
+
if (hooks || config) {
|
|
7204
|
+
return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
if (hasNuxtDependency(pkg)) {
|
|
7208
|
+
const nuxtConfig = await findNuxtConfig(serviceDir);
|
|
7209
|
+
if (nuxtConfig) {
|
|
7210
|
+
return planNuxt(serviceDir, pkg, manifestPath);
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
if (hasAstroDependency(pkg)) {
|
|
7214
|
+
const astroConfig = await findAstroConfig(serviceDir);
|
|
7215
|
+
if (astroConfig) {
|
|
7216
|
+
return planAstro(serviceDir, pkg, manifestPath);
|
|
7217
|
+
}
|
|
7218
|
+
}
|
|
6159
7219
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
6160
7220
|
if (!entryFile) {
|
|
6161
7221
|
return { ...empty, libOnly: true };
|
|
6162
7222
|
}
|
|
6163
7223
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
6164
|
-
const otelInitFile =
|
|
6165
|
-
const envNeatFile =
|
|
7224
|
+
const otelInitFile = import_node_path40.default.join(import_node_path40.default.dirname(entryFile), otelInitFilename(flavor));
|
|
7225
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
6166
7226
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6167
7227
|
const dependencyEdits = [];
|
|
6168
7228
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -6176,7 +7236,7 @@ async function plan(serviceDir) {
|
|
|
6176
7236
|
}
|
|
6177
7237
|
const entrypointEdits = [];
|
|
6178
7238
|
try {
|
|
6179
|
-
const raw = await
|
|
7239
|
+
const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
|
|
6180
7240
|
const lines = raw.split(/\r?\n/);
|
|
6181
7241
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
6182
7242
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -6201,7 +7261,7 @@ async function plan(serviceDir) {
|
|
|
6201
7261
|
if (!await exists2(envNeatFile)) {
|
|
6202
7262
|
generatedFiles.push({
|
|
6203
7263
|
file: envNeatFile,
|
|
6204
|
-
contents: renderEnvNeat(pkg.name ??
|
|
7264
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6205
7265
|
skipIfExists: true
|
|
6206
7266
|
});
|
|
6207
7267
|
}
|
|
@@ -6220,18 +7280,29 @@ async function plan(serviceDir) {
|
|
|
6220
7280
|
};
|
|
6221
7281
|
}
|
|
6222
7282
|
function isAllowedWritePath(serviceDir, target) {
|
|
6223
|
-
const rel =
|
|
7283
|
+
const rel = import_node_path40.default.relative(serviceDir, target);
|
|
6224
7284
|
if (rel.startsWith("..")) return false;
|
|
6225
|
-
const base =
|
|
7285
|
+
const base = import_node_path40.default.basename(target);
|
|
6226
7286
|
if (base === "package.json") return true;
|
|
6227
7287
|
if (base === ".env.neat") return true;
|
|
6228
7288
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
7289
|
+
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
7290
|
+
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
7291
|
+
const relPosix = rel.split(import_node_path40.default.sep).join("/");
|
|
7292
|
+
if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
|
|
7293
|
+
if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
|
|
7294
|
+
if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
|
|
7295
|
+
if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
|
|
7296
|
+
if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
|
|
7297
|
+
if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
|
|
7298
|
+
if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
|
|
6229
7299
|
return false;
|
|
6230
7300
|
}
|
|
6231
7301
|
async function writeAtomic(file, contents) {
|
|
7302
|
+
await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
|
|
6232
7303
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6233
|
-
await
|
|
6234
|
-
await
|
|
7304
|
+
await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
|
|
7305
|
+
await import_node_fs25.promises.rename(tmp, file);
|
|
6235
7306
|
}
|
|
6236
7307
|
async function apply(installPlan) {
|
|
6237
7308
|
const { serviceDir } = installPlan;
|
|
@@ -6243,7 +7314,7 @@ async function apply(installPlan) {
|
|
|
6243
7314
|
writtenFiles: []
|
|
6244
7315
|
};
|
|
6245
7316
|
}
|
|
6246
|
-
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
|
|
7317
|
+
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
|
|
6247
7318
|
return {
|
|
6248
7319
|
serviceDir,
|
|
6249
7320
|
outcome: "already-instrumented",
|
|
@@ -6254,6 +7325,7 @@ async function apply(installPlan) {
|
|
|
6254
7325
|
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
6255
7326
|
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
6256
7327
|
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
7328
|
+
if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
|
|
6257
7329
|
for (const target of allTargets) {
|
|
6258
7330
|
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
6259
7331
|
if (isEntryEdit) continue;
|
|
@@ -6268,7 +7340,7 @@ async function apply(installPlan) {
|
|
|
6268
7340
|
for (const target of allTargets) {
|
|
6269
7341
|
if (await exists2(target)) {
|
|
6270
7342
|
try {
|
|
6271
|
-
originals.set(target, await
|
|
7343
|
+
originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
|
|
6272
7344
|
} catch {
|
|
6273
7345
|
}
|
|
6274
7346
|
}
|
|
@@ -6323,6 +7395,17 @@ async function apply(installPlan) {
|
|
|
6323
7395
|
await writeAtomic(ep.file, newRaw);
|
|
6324
7396
|
writtenFiles.push(ep.file);
|
|
6325
7397
|
}
|
|
7398
|
+
if (installPlan.nextConfigEdit) {
|
|
7399
|
+
const target = installPlan.nextConfigEdit.file;
|
|
7400
|
+
const raw = originals.get(target);
|
|
7401
|
+
if (raw !== void 0 && !raw.includes("instrumentationHook")) {
|
|
7402
|
+
const updated = injectInstrumentationHook(raw);
|
|
7403
|
+
if (updated !== null) {
|
|
7404
|
+
await writeAtomic(target, updated);
|
|
7405
|
+
writtenFiles.push(target);
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
6326
7409
|
} catch (err) {
|
|
6327
7410
|
await rollback(installPlan, originals, createdFiles);
|
|
6328
7411
|
throw err;
|
|
@@ -6338,14 +7421,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6338
7421
|
const removed = [];
|
|
6339
7422
|
for (const [file, raw] of originals.entries()) {
|
|
6340
7423
|
try {
|
|
6341
|
-
await
|
|
7424
|
+
await import_node_fs25.promises.writeFile(file, raw, "utf8");
|
|
6342
7425
|
restored.push(file);
|
|
6343
7426
|
} catch {
|
|
6344
7427
|
}
|
|
6345
7428
|
}
|
|
6346
7429
|
for (const file of createdFiles) {
|
|
6347
7430
|
try {
|
|
6348
|
-
await
|
|
7431
|
+
await import_node_fs25.promises.unlink(file);
|
|
6349
7432
|
removed.push(file);
|
|
6350
7433
|
} catch {
|
|
6351
7434
|
}
|
|
@@ -6360,8 +7443,26 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6360
7443
|
...removed.map((f) => `removed: ${f}`),
|
|
6361
7444
|
""
|
|
6362
7445
|
];
|
|
6363
|
-
const rollbackPath =
|
|
6364
|
-
await
|
|
7446
|
+
const rollbackPath = import_node_path40.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
7447
|
+
await import_node_fs25.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
7448
|
+
}
|
|
7449
|
+
function injectInstrumentationHook(raw) {
|
|
7450
|
+
if (raw.includes("instrumentationHook")) return raw;
|
|
7451
|
+
const anchors = [
|
|
7452
|
+
{ pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
|
|
7453
|
+
{ pattern: /(export\s+default\s*\{)/, label: "esm-default" },
|
|
7454
|
+
{ pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
|
|
7455
|
+
];
|
|
7456
|
+
for (const { pattern } of anchors) {
|
|
7457
|
+
const match = pattern.exec(raw);
|
|
7458
|
+
if (!match) continue;
|
|
7459
|
+
const insertAfter = match.index + match[0].length;
|
|
7460
|
+
const before = raw.slice(0, insertAfter);
|
|
7461
|
+
const after = raw.slice(insertAfter);
|
|
7462
|
+
const injection = "\n experimental: { instrumentationHook: true },";
|
|
7463
|
+
return `${before}${injection}${after}`;
|
|
7464
|
+
}
|
|
7465
|
+
return null;
|
|
6365
7466
|
}
|
|
6366
7467
|
var javascriptInstaller = {
|
|
6367
7468
|
name: "javascript",
|
|
@@ -6372,8 +7473,8 @@ var javascriptInstaller = {
|
|
|
6372
7473
|
|
|
6373
7474
|
// src/installers/python.ts
|
|
6374
7475
|
init_cjs_shims();
|
|
6375
|
-
var
|
|
6376
|
-
var
|
|
7476
|
+
var import_node_fs26 = require("fs");
|
|
7477
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
6377
7478
|
var SDK_PACKAGES2 = [
|
|
6378
7479
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
6379
7480
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -6385,7 +7486,7 @@ var OTEL_ENV2 = {
|
|
|
6385
7486
|
};
|
|
6386
7487
|
async function exists3(p) {
|
|
6387
7488
|
try {
|
|
6388
|
-
await
|
|
7489
|
+
await import_node_fs26.promises.stat(p);
|
|
6389
7490
|
return true;
|
|
6390
7491
|
} catch {
|
|
6391
7492
|
return false;
|
|
@@ -6394,7 +7495,7 @@ async function exists3(p) {
|
|
|
6394
7495
|
async function detect2(serviceDir) {
|
|
6395
7496
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
6396
7497
|
for (const m of markers) {
|
|
6397
|
-
if (await exists3(
|
|
7498
|
+
if (await exists3(import_node_path41.default.join(serviceDir, m))) return true;
|
|
6398
7499
|
}
|
|
6399
7500
|
return false;
|
|
6400
7501
|
}
|
|
@@ -6404,9 +7505,9 @@ function reqPackageName(line) {
|
|
|
6404
7505
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
6405
7506
|
}
|
|
6406
7507
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
6407
|
-
const file =
|
|
7508
|
+
const file = import_node_path41.default.join(serviceDir, "requirements.txt");
|
|
6408
7509
|
if (!await exists3(file)) return null;
|
|
6409
|
-
const raw = await
|
|
7510
|
+
const raw = await import_node_fs26.promises.readFile(file, "utf8");
|
|
6410
7511
|
const presentNames = new Set(
|
|
6411
7512
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
6412
7513
|
);
|
|
@@ -6414,9 +7515,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
6414
7515
|
return { manifest: file, missing: [...missing] };
|
|
6415
7516
|
}
|
|
6416
7517
|
async function planProcfileEdits(serviceDir) {
|
|
6417
|
-
const procfile =
|
|
7518
|
+
const procfile = import_node_path41.default.join(serviceDir, "Procfile");
|
|
6418
7519
|
if (!await exists3(procfile)) return [];
|
|
6419
|
-
const raw = await
|
|
7520
|
+
const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
|
|
6420
7521
|
const edits = [];
|
|
6421
7522
|
for (const line of raw.split(/\r?\n/)) {
|
|
6422
7523
|
if (line.length === 0) continue;
|
|
@@ -6468,8 +7569,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
6468
7569
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
6469
7570
|
`;
|
|
6470
7571
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
6471
|
-
await
|
|
6472
|
-
await
|
|
7572
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
7573
|
+
await import_node_fs26.promises.rename(tmp, manifest);
|
|
6473
7574
|
}
|
|
6474
7575
|
async function applyProcfile(procfile, edits, original) {
|
|
6475
7576
|
let next = original;
|
|
@@ -6478,8 +7579,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
6478
7579
|
next = next.replace(e.before, e.after);
|
|
6479
7580
|
}
|
|
6480
7581
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
6481
|
-
await
|
|
6482
|
-
await
|
|
7582
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
7583
|
+
await import_node_fs26.promises.rename(tmp, procfile);
|
|
6483
7584
|
}
|
|
6484
7585
|
async function apply2(installPlan) {
|
|
6485
7586
|
const { serviceDir } = installPlan;
|
|
@@ -6492,7 +7593,7 @@ async function apply2(installPlan) {
|
|
|
6492
7593
|
const originals = /* @__PURE__ */ new Map();
|
|
6493
7594
|
for (const file of touched) {
|
|
6494
7595
|
try {
|
|
6495
|
-
originals.set(file, await
|
|
7596
|
+
originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
|
|
6496
7597
|
} catch {
|
|
6497
7598
|
}
|
|
6498
7599
|
}
|
|
@@ -6503,7 +7604,7 @@ async function apply2(installPlan) {
|
|
|
6503
7604
|
if (raw === void 0) {
|
|
6504
7605
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
6505
7606
|
}
|
|
6506
|
-
const base =
|
|
7607
|
+
const base = import_node_path41.default.basename(file);
|
|
6507
7608
|
if (base === "requirements.txt") {
|
|
6508
7609
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
6509
7610
|
if (edits.length > 0) {
|
|
@@ -6528,7 +7629,7 @@ async function rollback2(installPlan, originals) {
|
|
|
6528
7629
|
const restored = [];
|
|
6529
7630
|
for (const [file, raw] of originals.entries()) {
|
|
6530
7631
|
try {
|
|
6531
|
-
await
|
|
7632
|
+
await import_node_fs26.promises.writeFile(file, raw, "utf8");
|
|
6532
7633
|
restored.push(file);
|
|
6533
7634
|
} catch {
|
|
6534
7635
|
}
|
|
@@ -6542,8 +7643,8 @@ async function rollback2(installPlan, originals) {
|
|
|
6542
7643
|
...restored.map((f) => `restored: ${f}`),
|
|
6543
7644
|
""
|
|
6544
7645
|
];
|
|
6545
|
-
const rollbackPath =
|
|
6546
|
-
await
|
|
7646
|
+
const rollbackPath = import_node_path41.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
7647
|
+
await import_node_fs26.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
6547
7648
|
}
|
|
6548
7649
|
var pythonInstaller = {
|
|
6549
7650
|
name: "python",
|
|
@@ -6555,7 +7656,7 @@ var pythonInstaller = {
|
|
|
6555
7656
|
// src/installers/shared.ts
|
|
6556
7657
|
init_cjs_shims();
|
|
6557
7658
|
function isEmptyPlan(plan3) {
|
|
6558
|
-
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
|
|
7659
|
+
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
|
|
6559
7660
|
}
|
|
6560
7661
|
|
|
6561
7662
|
// src/installers/index.ts
|
|
@@ -6654,16 +7755,259 @@ function renderPatch(sections) {
|
|
|
6654
7755
|
}
|
|
6655
7756
|
lines.push("");
|
|
6656
7757
|
}
|
|
7758
|
+
if (plan3.nextConfigEdit) {
|
|
7759
|
+
lines.push("### next.config (framework flag)");
|
|
7760
|
+
lines.push(`--- ${plan3.nextConfigEdit.file}`);
|
|
7761
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
|
|
7762
|
+
lines.push("");
|
|
7763
|
+
}
|
|
6657
7764
|
}
|
|
6658
7765
|
return lines.join("\n");
|
|
6659
7766
|
}
|
|
6660
7767
|
|
|
6661
|
-
// src/
|
|
6662
|
-
|
|
7768
|
+
// src/orchestrator.ts
|
|
7769
|
+
init_cjs_shims();
|
|
7770
|
+
var import_node_fs27 = require("fs");
|
|
7771
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
7772
|
+
var import_node_path42 = __toESM(require("path"), 1);
|
|
7773
|
+
var import_node_child_process2 = require("child_process");
|
|
7774
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
7775
|
+
async function extractAndPersist(opts) {
|
|
7776
|
+
const services = await discoverServices(opts.scanPath);
|
|
7777
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7778
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
7779
|
+
resetGraph(graphKey);
|
|
7780
|
+
const graph = getGraph(graphKey);
|
|
7781
|
+
const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
|
|
7782
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
7783
|
+
errorsPath: projectPaths.errorsPath
|
|
7784
|
+
});
|
|
7785
|
+
if (!opts.dryRun) {
|
|
7786
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
7787
|
+
}
|
|
7788
|
+
return {
|
|
7789
|
+
graph,
|
|
7790
|
+
graphKey,
|
|
7791
|
+
services,
|
|
7792
|
+
languages,
|
|
7793
|
+
nodesAdded: extraction.nodesAdded,
|
|
7794
|
+
edgesAdded: extraction.edgesAdded,
|
|
7795
|
+
snapshotPath: projectPaths.snapshotPath,
|
|
7796
|
+
errorsPath: projectPaths.errorsPath
|
|
7797
|
+
};
|
|
7798
|
+
}
|
|
7799
|
+
async function applyInstallersOver(services) {
|
|
7800
|
+
let instrumented = 0;
|
|
7801
|
+
let already = 0;
|
|
7802
|
+
let libOnly = 0;
|
|
7803
|
+
for (const svc of services) {
|
|
7804
|
+
const installer = await pickInstaller(svc.dir);
|
|
7805
|
+
if (!installer) continue;
|
|
7806
|
+
const plan3 = await installer.plan(svc.dir);
|
|
7807
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
7808
|
+
already++;
|
|
7809
|
+
continue;
|
|
7810
|
+
}
|
|
7811
|
+
const outcome = await installer.apply(plan3);
|
|
7812
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
7813
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
7814
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
7815
|
+
}
|
|
7816
|
+
return { instrumented, alreadyInstrumented: already, libOnly };
|
|
7817
|
+
}
|
|
7818
|
+
async function promptYesNo(question) {
|
|
7819
|
+
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
7820
|
+
return new Promise((resolve) => {
|
|
7821
|
+
rl.question(`${question} [Y/n] `, (answer) => {
|
|
7822
|
+
rl.close();
|
|
7823
|
+
const trimmed = answer.trim().toLowerCase();
|
|
7824
|
+
resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
7825
|
+
});
|
|
7826
|
+
});
|
|
7827
|
+
}
|
|
7828
|
+
var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
|
|
7829
|
+
async function checkDaemonHealth(restPort) {
|
|
7830
|
+
return new Promise((resolve) => {
|
|
7831
|
+
const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
|
|
7832
|
+
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
7833
|
+
res.resume();
|
|
7834
|
+
resolve(ok);
|
|
7835
|
+
});
|
|
7836
|
+
req.on("error", () => resolve(false));
|
|
7837
|
+
req.setTimeout(1e3, () => {
|
|
7838
|
+
req.destroy();
|
|
7839
|
+
resolve(false);
|
|
7840
|
+
});
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
7844
|
+
const deadline = Date.now() + timeoutMs;
|
|
7845
|
+
while (Date.now() < deadline) {
|
|
7846
|
+
if (await checkDaemonHealth(restPort)) return true;
|
|
7847
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
7848
|
+
}
|
|
7849
|
+
return false;
|
|
7850
|
+
}
|
|
7851
|
+
function spawnDaemonDetached() {
|
|
7852
|
+
const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
|
|
7853
|
+
const candidates = [
|
|
7854
|
+
import_node_path42.default.join(here, "neatd.cjs"),
|
|
7855
|
+
import_node_path42.default.join(here, "neatd.js")
|
|
7856
|
+
];
|
|
7857
|
+
let entry2 = null;
|
|
7858
|
+
const fsSync = require("fs");
|
|
7859
|
+
for (const c of candidates) {
|
|
7860
|
+
try {
|
|
7861
|
+
fsSync.accessSync(c);
|
|
7862
|
+
entry2 = c;
|
|
7863
|
+
break;
|
|
7864
|
+
} catch {
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
if (!entry2) {
|
|
7868
|
+
throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
|
|
7869
|
+
}
|
|
7870
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
|
|
7871
|
+
detached: true,
|
|
7872
|
+
stdio: "ignore",
|
|
7873
|
+
env: process.env
|
|
7874
|
+
});
|
|
7875
|
+
child.unref();
|
|
7876
|
+
}
|
|
7877
|
+
function openBrowser(url) {
|
|
7878
|
+
if (!process.stdout.isTTY) return "failed";
|
|
7879
|
+
const platform = process.platform;
|
|
7880
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
7881
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
7882
|
+
try {
|
|
7883
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
7884
|
+
child.on("error", () => {
|
|
7885
|
+
});
|
|
7886
|
+
child.unref();
|
|
7887
|
+
return "opened";
|
|
7888
|
+
} catch {
|
|
7889
|
+
return "failed";
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
async function runOrchestrator(opts) {
|
|
7893
|
+
const result = {
|
|
7894
|
+
exitCode: 0,
|
|
7895
|
+
steps: {
|
|
7896
|
+
discovery: { services: 0, languages: [] },
|
|
7897
|
+
extraction: { nodesAdded: 0, edgesAdded: 0 },
|
|
7898
|
+
gitignore: "unchanged",
|
|
7899
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
|
|
7900
|
+
daemon: "skipped",
|
|
7901
|
+
browser: "skipped"
|
|
7902
|
+
}
|
|
7903
|
+
};
|
|
7904
|
+
const stat = await import_node_fs27.promises.stat(opts.scanPath).catch(() => null);
|
|
7905
|
+
if (!stat || !stat.isDirectory()) {
|
|
7906
|
+
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
7907
|
+
result.exitCode = 2;
|
|
7908
|
+
return result;
|
|
7909
|
+
}
|
|
7910
|
+
console.log(`neat: ${opts.scanPath}`);
|
|
7911
|
+
console.log("");
|
|
7912
|
+
const persisted = await extractAndPersist({
|
|
7913
|
+
scanPath: opts.scanPath,
|
|
7914
|
+
project: opts.project,
|
|
7915
|
+
projectExplicit: opts.projectExplicit
|
|
7916
|
+
});
|
|
7917
|
+
const { graph, services, languages } = persisted;
|
|
7918
|
+
result.steps.discovery = { services: services.length, languages };
|
|
7919
|
+
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
7920
|
+
let runApply = !opts.noInstrument;
|
|
7921
|
+
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
7922
|
+
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
7923
|
+
}
|
|
7924
|
+
result.steps.extraction = {
|
|
7925
|
+
nodesAdded: persisted.nodesAdded,
|
|
7926
|
+
edgesAdded: persisted.edgesAdded
|
|
7927
|
+
};
|
|
7928
|
+
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
7929
|
+
result.steps.gitignore = gi.action;
|
|
7930
|
+
if (gi.action !== "unchanged") {
|
|
7931
|
+
console.log(`${gi.action} .gitignore (neat-out/)`);
|
|
7932
|
+
}
|
|
7933
|
+
try {
|
|
7934
|
+
await addProject({
|
|
7935
|
+
name: opts.project,
|
|
7936
|
+
path: opts.scanPath,
|
|
7937
|
+
languages,
|
|
7938
|
+
status: "active"
|
|
7939
|
+
});
|
|
7940
|
+
} catch (err) {
|
|
7941
|
+
if (!(err instanceof ProjectNameCollisionError)) throw err;
|
|
7942
|
+
console.error(`neat: ${err.message}`);
|
|
7943
|
+
console.error("pass --project <other-name> to register under a different name.");
|
|
7944
|
+
result.exitCode = 1;
|
|
7945
|
+
return result;
|
|
7946
|
+
}
|
|
7947
|
+
if (!runApply) {
|
|
7948
|
+
result.steps.apply.skipped = true;
|
|
7949
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
7950
|
+
} else {
|
|
7951
|
+
const tally = await applyInstallersOver(services);
|
|
7952
|
+
result.steps.apply = { ...tally, skipped: false };
|
|
7953
|
+
console.log(
|
|
7954
|
+
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
7955
|
+
);
|
|
7956
|
+
}
|
|
7957
|
+
const restPort = Number(process.env.PORT ?? 8080);
|
|
7958
|
+
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
7959
|
+
if (await checkDaemonHealth(restPort)) {
|
|
7960
|
+
result.steps.daemon = "already-running";
|
|
7961
|
+
} else {
|
|
7962
|
+
try {
|
|
7963
|
+
spawnDaemonDetached();
|
|
7964
|
+
} catch (err) {
|
|
7965
|
+
console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
|
|
7966
|
+
result.exitCode = 1;
|
|
7967
|
+
return result;
|
|
7968
|
+
}
|
|
7969
|
+
const ready = await waitForDaemonReady(restPort, timeoutMs);
|
|
7970
|
+
result.steps.daemon = ready ? "spawned" : "timed-out";
|
|
7971
|
+
if (!ready) {
|
|
7972
|
+
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
7973
|
+
result.exitCode = 1;
|
|
7974
|
+
return result;
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
|
|
7978
|
+
if (opts.noOpen || !process.stdout.isTTY) {
|
|
7979
|
+
result.steps.browser = "skipped";
|
|
7980
|
+
} else {
|
|
7981
|
+
result.steps.browser = openBrowser(dashboardUrl);
|
|
7982
|
+
}
|
|
7983
|
+
printSummary(result, graph, dashboardUrl);
|
|
7984
|
+
return result;
|
|
7985
|
+
}
|
|
7986
|
+
function printSummary(result, graph, dashboardUrl) {
|
|
7987
|
+
const nodes = [];
|
|
7988
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
7989
|
+
const edges = [];
|
|
7990
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
7991
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
7992
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
7993
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
7994
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
7995
|
+
console.log("");
|
|
7996
|
+
console.log("=== summary ===");
|
|
7997
|
+
console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
|
|
7998
|
+
for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
7999
|
+
for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
8000
|
+
console.log("");
|
|
8001
|
+
console.log(`dashboard: ${dashboardUrl}`);
|
|
8002
|
+
}
|
|
8003
|
+
|
|
8004
|
+
// src/cli-verbs.ts
|
|
8005
|
+
init_cjs_shims();
|
|
8006
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
6663
8007
|
|
|
6664
8008
|
// src/cli-client.ts
|
|
6665
8009
|
init_cjs_shims();
|
|
6666
|
-
var
|
|
8010
|
+
var import_types24 = require("@neat.is/types");
|
|
6667
8011
|
var HttpError = class extends Error {
|
|
6668
8012
|
constructor(status2, message, responseBody = "") {
|
|
6669
8013
|
super(message);
|
|
@@ -6680,13 +8024,16 @@ var TransportError = class extends Error {
|
|
|
6680
8024
|
this.name = "TransportError";
|
|
6681
8025
|
}
|
|
6682
8026
|
};
|
|
6683
|
-
function createHttpClient(baseUrl) {
|
|
8027
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
6684
8028
|
const root = baseUrl.replace(/\/$/, "");
|
|
8029
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
6685
8030
|
return {
|
|
6686
|
-
async get(
|
|
8031
|
+
async get(path45) {
|
|
6687
8032
|
let res;
|
|
6688
8033
|
try {
|
|
6689
|
-
res = await fetch(`${root}${
|
|
8034
|
+
res = await fetch(`${root}${path45}`, {
|
|
8035
|
+
headers: { ...authHeader }
|
|
8036
|
+
});
|
|
6690
8037
|
} catch (err) {
|
|
6691
8038
|
throw new TransportError(
|
|
6692
8039
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -6696,18 +8043,18 @@ function createHttpClient(baseUrl) {
|
|
|
6696
8043
|
const body = await res.text().catch(() => "");
|
|
6697
8044
|
throw new HttpError(
|
|
6698
8045
|
res.status,
|
|
6699
|
-
`${res.status} ${res.statusText} on GET ${
|
|
8046
|
+
`${res.status} ${res.statusText} on GET ${path45}: ${body}`,
|
|
6700
8047
|
body
|
|
6701
8048
|
);
|
|
6702
8049
|
}
|
|
6703
8050
|
return await res.json();
|
|
6704
8051
|
},
|
|
6705
|
-
async post(
|
|
8052
|
+
async post(path45, body) {
|
|
6706
8053
|
let res;
|
|
6707
8054
|
try {
|
|
6708
|
-
res = await fetch(`${root}${
|
|
8055
|
+
res = await fetch(`${root}${path45}`, {
|
|
6709
8056
|
method: "POST",
|
|
6710
|
-
headers: { "content-type": "application/json" },
|
|
8057
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
6711
8058
|
body: JSON.stringify(body)
|
|
6712
8059
|
});
|
|
6713
8060
|
} catch (err) {
|
|
@@ -6719,7 +8066,7 @@ function createHttpClient(baseUrl) {
|
|
|
6719
8066
|
const text = await res.text().catch(() => "");
|
|
6720
8067
|
throw new HttpError(
|
|
6721
8068
|
res.status,
|
|
6722
|
-
`${res.status} ${res.statusText} on POST ${
|
|
8069
|
+
`${res.status} ${res.statusText} on POST ${path45}: ${text}`,
|
|
6723
8070
|
text
|
|
6724
8071
|
);
|
|
6725
8072
|
}
|
|
@@ -6733,12 +8080,12 @@ function projectPath(project, suffix) {
|
|
|
6733
8080
|
}
|
|
6734
8081
|
async function runRootCause(client, input) {
|
|
6735
8082
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
6736
|
-
const
|
|
8083
|
+
const path45 = projectPath(
|
|
6737
8084
|
input.project,
|
|
6738
8085
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
6739
8086
|
);
|
|
6740
8087
|
try {
|
|
6741
|
-
const result = await client.get(
|
|
8088
|
+
const result = await client.get(path45);
|
|
6742
8089
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
6743
8090
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
6744
8091
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -6764,12 +8111,12 @@ async function runRootCause(client, input) {
|
|
|
6764
8111
|
}
|
|
6765
8112
|
async function runBlastRadius(client, input) {
|
|
6766
8113
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
6767
|
-
const
|
|
8114
|
+
const path45 = projectPath(
|
|
6768
8115
|
input.project,
|
|
6769
8116
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
6770
8117
|
);
|
|
6771
8118
|
try {
|
|
6772
|
-
const result = await client.get(
|
|
8119
|
+
const result = await client.get(path45);
|
|
6773
8120
|
if (result.totalAffected === 0) {
|
|
6774
8121
|
return {
|
|
6775
8122
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -6798,17 +8145,17 @@ async function runBlastRadius(client, input) {
|
|
|
6798
8145
|
}
|
|
6799
8146
|
}
|
|
6800
8147
|
function formatBlastEntry(n) {
|
|
6801
|
-
const tag = n.edgeProvenance ===
|
|
8148
|
+
const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
6802
8149
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
6803
8150
|
}
|
|
6804
8151
|
async function runDependencies(client, input) {
|
|
6805
8152
|
const depth = input.depth ?? 3;
|
|
6806
|
-
const
|
|
8153
|
+
const path45 = projectPath(
|
|
6807
8154
|
input.project,
|
|
6808
8155
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6809
8156
|
);
|
|
6810
8157
|
try {
|
|
6811
|
-
const result = await client.get(
|
|
8158
|
+
const result = await client.get(path45);
|
|
6812
8159
|
if (result.total === 0) {
|
|
6813
8160
|
return {
|
|
6814
8161
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -6844,9 +8191,9 @@ async function runObservedDependencies(client, input) {
|
|
|
6844
8191
|
const edges = await client.get(
|
|
6845
8192
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
6846
8193
|
);
|
|
6847
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
8194
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
|
|
6848
8195
|
if (observed.length === 0) {
|
|
6849
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
8196
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
|
|
6850
8197
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
6851
8198
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
6852
8199
|
}
|
|
@@ -6854,7 +8201,7 @@ async function runObservedDependencies(client, input) {
|
|
|
6854
8201
|
return {
|
|
6855
8202
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
6856
8203
|
block: blockLines.join("\n"),
|
|
6857
|
-
provenance:
|
|
8204
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6858
8205
|
};
|
|
6859
8206
|
} catch (err) {
|
|
6860
8207
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -6889,9 +8236,9 @@ function formatDuration(ms) {
|
|
|
6889
8236
|
return `${Math.round(h / 24)}d`;
|
|
6890
8237
|
}
|
|
6891
8238
|
async function runIncidents(client, input) {
|
|
6892
|
-
const
|
|
8239
|
+
const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6893
8240
|
try {
|
|
6894
|
-
const body = await client.get(
|
|
8241
|
+
const body = await client.get(path45);
|
|
6895
8242
|
const events = body.events;
|
|
6896
8243
|
if (events.length === 0) {
|
|
6897
8244
|
return {
|
|
@@ -6908,7 +8255,7 @@ async function runIncidents(client, input) {
|
|
|
6908
8255
|
return {
|
|
6909
8256
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
6910
8257
|
block: blockLines.join("\n"),
|
|
6911
|
-
provenance:
|
|
8258
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6912
8259
|
};
|
|
6913
8260
|
} catch (err) {
|
|
6914
8261
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -7017,7 +8364,7 @@ async function runStaleEdges(client, input) {
|
|
|
7017
8364
|
return {
|
|
7018
8365
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
7019
8366
|
block: blockLines.join("\n"),
|
|
7020
|
-
provenance:
|
|
8367
|
+
provenance: import_types24.Provenance.STALE
|
|
7021
8368
|
};
|
|
7022
8369
|
}
|
|
7023
8370
|
async function runPolicies(client, input) {
|
|
@@ -7157,8 +8504,181 @@ function exitCodeForError(err) {
|
|
|
7157
8504
|
if (err instanceof HttpError) return 1;
|
|
7158
8505
|
return 1;
|
|
7159
8506
|
}
|
|
8507
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
8508
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
8509
|
+
}
|
|
8510
|
+
async function pushSnapshotToRemote(input) {
|
|
8511
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
8512
|
+
if (typeof client.post !== "function") {
|
|
8513
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
8514
|
+
}
|
|
8515
|
+
return client.post(
|
|
8516
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
8517
|
+
{ snapshot: input.snapshot }
|
|
8518
|
+
);
|
|
8519
|
+
}
|
|
8520
|
+
|
|
8521
|
+
// src/cli-verbs.ts
|
|
8522
|
+
async function resolveProjectEntry(opts) {
|
|
8523
|
+
const entries = await listProjects();
|
|
8524
|
+
if (opts.project) {
|
|
8525
|
+
const match = entries.find((e) => e.name === opts.project);
|
|
8526
|
+
return match ?? null;
|
|
8527
|
+
}
|
|
8528
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
8529
|
+
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
8530
|
+
for (const entry2 of entries) {
|
|
8531
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
|
|
8532
|
+
return entry2;
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
return null;
|
|
8536
|
+
}
|
|
8537
|
+
async function checkDaemonHealth2(baseUrl) {
|
|
8538
|
+
try {
|
|
8539
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
8540
|
+
signal: AbortSignal.timeout(1500)
|
|
8541
|
+
});
|
|
8542
|
+
return res.ok;
|
|
8543
|
+
} catch {
|
|
8544
|
+
return false;
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
function snapshotForGraph(persisted) {
|
|
8548
|
+
return {
|
|
8549
|
+
schemaVersion: 3,
|
|
8550
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8551
|
+
graph: persisted.graph.export()
|
|
8552
|
+
};
|
|
8553
|
+
}
|
|
8554
|
+
function emitResult(result, json) {
|
|
8555
|
+
if (json) {
|
|
8556
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
8557
|
+
return;
|
|
8558
|
+
}
|
|
8559
|
+
const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
|
|
8560
|
+
console.log(
|
|
8561
|
+
`${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
|
|
8562
|
+
);
|
|
8563
|
+
if (!result.apply.skipped) {
|
|
8564
|
+
const { instrumented, alreadyInstrumented, libOnly } = result.apply;
|
|
8565
|
+
console.log(
|
|
8566
|
+
`instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
8567
|
+
);
|
|
8568
|
+
} else {
|
|
8569
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
8570
|
+
}
|
|
8571
|
+
for (const warn of result.warnings) console.error(warn);
|
|
8572
|
+
}
|
|
8573
|
+
async function runSync(opts) {
|
|
8574
|
+
const entry2 = await resolveProjectEntry(opts);
|
|
8575
|
+
if (!entry2) {
|
|
8576
|
+
const target = opts.project ?? opts.cwd ?? process.cwd();
|
|
8577
|
+
console.error(
|
|
8578
|
+
`neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
|
|
8579
|
+
);
|
|
8580
|
+
return {
|
|
8581
|
+
exitCode: 1,
|
|
8582
|
+
project: opts.project ?? "",
|
|
8583
|
+
scanPath: target,
|
|
8584
|
+
nodesAdded: 0,
|
|
8585
|
+
edgesAdded: 0,
|
|
8586
|
+
snapshotPath: null,
|
|
8587
|
+
mode: opts.to ? "remote" : "local",
|
|
8588
|
+
daemon: "skipped",
|
|
8589
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
|
|
8590
|
+
warnings: []
|
|
8591
|
+
};
|
|
8592
|
+
}
|
|
8593
|
+
const persisted = await extractAndPersist({
|
|
8594
|
+
scanPath: entry2.path,
|
|
8595
|
+
project: entry2.name,
|
|
8596
|
+
projectExplicit: true,
|
|
8597
|
+
dryRun: true
|
|
8598
|
+
});
|
|
8599
|
+
let snapshotPath = null;
|
|
8600
|
+
if (!opts.dryRun) {
|
|
8601
|
+
const target = persisted.snapshotPath;
|
|
8602
|
+
await saveGraphToDisk(persisted.graph, target);
|
|
8603
|
+
snapshotPath = target;
|
|
8604
|
+
}
|
|
8605
|
+
const skipApply = opts.dryRun || opts.noInstrument;
|
|
8606
|
+
const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
|
|
8607
|
+
const warnings = [];
|
|
8608
|
+
let daemonState = "skipped";
|
|
8609
|
+
let exitCode = 0;
|
|
8610
|
+
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
8611
|
+
if (!opts.dryRun) {
|
|
8612
|
+
const snapshot = snapshotForGraph(persisted);
|
|
8613
|
+
if (opts.to) {
|
|
8614
|
+
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
8615
|
+
try {
|
|
8616
|
+
await pushSnapshotToRemote({
|
|
8617
|
+
baseUrl: opts.to,
|
|
8618
|
+
token,
|
|
8619
|
+
project: entry2.name,
|
|
8620
|
+
snapshot
|
|
8621
|
+
});
|
|
8622
|
+
daemonState = "remote-ok";
|
|
8623
|
+
} catch (err) {
|
|
8624
|
+
if (err instanceof HttpError) {
|
|
8625
|
+
console.error(`neat sync: ${err.message}`);
|
|
8626
|
+
exitCode = 1;
|
|
8627
|
+
} else if (err instanceof TransportError) {
|
|
8628
|
+
console.error(`neat sync: ${err.message}`);
|
|
8629
|
+
exitCode = 3;
|
|
8630
|
+
} else {
|
|
8631
|
+
console.error(`neat sync: ${err.message}`);
|
|
8632
|
+
exitCode = 1;
|
|
8633
|
+
}
|
|
8634
|
+
daemonState = "skipped";
|
|
8635
|
+
}
|
|
8636
|
+
} else {
|
|
8637
|
+
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
8638
|
+
const healthy = await checkDaemonHealth2(daemonUrl);
|
|
8639
|
+
if (healthy) {
|
|
8640
|
+
try {
|
|
8641
|
+
await pushSnapshotToRemote({
|
|
8642
|
+
baseUrl: daemonUrl,
|
|
8643
|
+
token: process.env.NEAT_AUTH_TOKEN,
|
|
8644
|
+
project: entry2.name,
|
|
8645
|
+
snapshot
|
|
8646
|
+
});
|
|
8647
|
+
daemonState = "reloaded";
|
|
8648
|
+
} catch (err) {
|
|
8649
|
+
warnings.push(
|
|
8650
|
+
`neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
|
|
8651
|
+
);
|
|
8652
|
+
daemonState = "down";
|
|
8653
|
+
exitCode = 2;
|
|
8654
|
+
}
|
|
8655
|
+
} else {
|
|
8656
|
+
warnings.push(
|
|
8657
|
+
"neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
|
|
8658
|
+
);
|
|
8659
|
+
daemonState = "down";
|
|
8660
|
+
exitCode = 2;
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
const result = {
|
|
8665
|
+
exitCode,
|
|
8666
|
+
project: entry2.name,
|
|
8667
|
+
scanPath: entry2.path,
|
|
8668
|
+
nodesAdded: persisted.nodesAdded,
|
|
8669
|
+
edgesAdded: persisted.edgesAdded,
|
|
8670
|
+
snapshotPath,
|
|
8671
|
+
mode,
|
|
8672
|
+
daemon: daemonState,
|
|
8673
|
+
apply: { ...applyTally, skipped: skipApply },
|
|
8674
|
+
warnings
|
|
8675
|
+
};
|
|
8676
|
+
emitResult(result, opts.json);
|
|
8677
|
+
return result;
|
|
8678
|
+
}
|
|
7160
8679
|
|
|
7161
8680
|
// src/cli.ts
|
|
8681
|
+
var import_types25 = require("@neat.is/types");
|
|
7162
8682
|
function usage() {
|
|
7163
8683
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
7164
8684
|
console.log("");
|
|
@@ -7183,6 +8703,18 @@ function usage() {
|
|
|
7183
8703
|
console.log(" Flags:");
|
|
7184
8704
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
7185
8705
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
8706
|
+
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
8707
|
+
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
8708
|
+
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
8709
|
+
console.log(" sync Re-run discovery, extraction, and SDK apply against the");
|
|
8710
|
+
console.log(" registered project, then notify the running daemon.");
|
|
8711
|
+
console.log(" Flags:");
|
|
8712
|
+
console.log(" --project <name> target a registered project by name");
|
|
8713
|
+
console.log(" --to <url> push the snapshot to a remote daemon");
|
|
8714
|
+
console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
|
|
8715
|
+
console.log(" --dry-run run extraction in-memory; do not write");
|
|
8716
|
+
console.log(" --no-instrument skip the SDK install apply step");
|
|
8717
|
+
console.log(" --json emit the delta summary as JSON");
|
|
7186
8718
|
console.log("");
|
|
7187
8719
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7188
8720
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -7236,7 +8768,9 @@ var STRING_FLAGS = [
|
|
|
7236
8768
|
["--error-id", "errorId"],
|
|
7237
8769
|
["--hypothetical-action", "hypotheticalAction"],
|
|
7238
8770
|
["--type", "type"],
|
|
7239
|
-
["--min-confidence", "minConfidence"]
|
|
8771
|
+
["--min-confidence", "minConfidence"],
|
|
8772
|
+
["--to", "to"],
|
|
8773
|
+
["--token", "token"]
|
|
7240
8774
|
];
|
|
7241
8775
|
function parseArgs(rest) {
|
|
7242
8776
|
const positional = [];
|
|
@@ -7245,6 +8779,10 @@ function parseArgs(rest) {
|
|
|
7245
8779
|
apply: false,
|
|
7246
8780
|
dryRun: false,
|
|
7247
8781
|
noInstall: false,
|
|
8782
|
+
noInstrument: false,
|
|
8783
|
+
noOpen: false,
|
|
8784
|
+
yes: false,
|
|
8785
|
+
verbose: false,
|
|
7248
8786
|
printConfig: false,
|
|
7249
8787
|
json: false,
|
|
7250
8788
|
depth: null,
|
|
@@ -7257,6 +8795,8 @@ function parseArgs(rest) {
|
|
|
7257
8795
|
hypotheticalAction: null,
|
|
7258
8796
|
type: null,
|
|
7259
8797
|
minConfidence: null,
|
|
8798
|
+
to: null,
|
|
8799
|
+
token: null,
|
|
7260
8800
|
positional: []
|
|
7261
8801
|
};
|
|
7262
8802
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -7273,6 +8813,22 @@ function parseArgs(rest) {
|
|
|
7273
8813
|
out.noInstall = true;
|
|
7274
8814
|
continue;
|
|
7275
8815
|
}
|
|
8816
|
+
if (arg === "--no-instrument") {
|
|
8817
|
+
out.noInstrument = true;
|
|
8818
|
+
continue;
|
|
8819
|
+
}
|
|
8820
|
+
if (arg === "--no-open") {
|
|
8821
|
+
out.noOpen = true;
|
|
8822
|
+
continue;
|
|
8823
|
+
}
|
|
8824
|
+
if (arg === "--yes" || arg === "-y") {
|
|
8825
|
+
out.yes = true;
|
|
8826
|
+
continue;
|
|
8827
|
+
}
|
|
8828
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
8829
|
+
out.verbose = true;
|
|
8830
|
+
continue;
|
|
8831
|
+
}
|
|
7276
8832
|
if (arg === "--print-config") {
|
|
7277
8833
|
out.printConfig = true;
|
|
7278
8834
|
continue;
|
|
@@ -7328,34 +8884,6 @@ function assignFlag(out, field, value) {
|
|
|
7328
8884
|
;
|
|
7329
8885
|
out[field] = value;
|
|
7330
8886
|
}
|
|
7331
|
-
function summarise(nodes, edges) {
|
|
7332
|
-
const byNode = /* @__PURE__ */ new Map();
|
|
7333
|
-
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
7334
|
-
const byEdge = /* @__PURE__ */ new Map();
|
|
7335
|
-
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
7336
|
-
const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7337
|
-
const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7338
|
-
return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
|
|
7339
|
-
}
|
|
7340
|
-
function formatIncompat(inc) {
|
|
7341
|
-
if (inc.kind === "node-engine") {
|
|
7342
|
-
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
7343
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
7344
|
-
}
|
|
7345
|
-
if (inc.kind === "package-conflict") {
|
|
7346
|
-
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
7347
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
7348
|
-
}
|
|
7349
|
-
if (inc.kind === "deprecated-api") {
|
|
7350
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
7351
|
-
}
|
|
7352
|
-
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
7353
|
-
}
|
|
7354
|
-
function findIncompatibilities(nodes) {
|
|
7355
|
-
return nodes.filter(
|
|
7356
|
-
(n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
7357
|
-
);
|
|
7358
|
-
}
|
|
7359
8887
|
function printBanner() {
|
|
7360
8888
|
console.log("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557");
|
|
7361
8889
|
console.log("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D");
|
|
@@ -7406,7 +8934,7 @@ async function buildPatchSections(services) {
|
|
|
7406
8934
|
}
|
|
7407
8935
|
async function runInit(opts) {
|
|
7408
8936
|
const written = [];
|
|
7409
|
-
const stat = await
|
|
8937
|
+
const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
|
|
7410
8938
|
if (!stat || !stat.isDirectory()) {
|
|
7411
8939
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
7412
8940
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -7415,11 +8943,15 @@ async function runInit(opts) {
|
|
|
7415
8943
|
printDiscoveryReport(opts, services);
|
|
7416
8944
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
7417
8945
|
const patch = renderPatch(sections);
|
|
7418
|
-
const patchPath =
|
|
8946
|
+
const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
|
|
7419
8947
|
if (opts.dryRun) {
|
|
7420
|
-
await
|
|
8948
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7421
8949
|
written.push(patchPath);
|
|
7422
8950
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8951
|
+
const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
|
|
8952
|
+
const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8953
|
+
const verb = gitignoreExists ? "append" : "create";
|
|
8954
|
+
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
7423
8955
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
7424
8956
|
return { exitCode: 0, writtenFiles: written };
|
|
7425
8957
|
}
|
|
@@ -7428,12 +8960,16 @@ async function runInit(opts) {
|
|
|
7428
8960
|
const graph = getGraph(graphKey);
|
|
7429
8961
|
const projectPaths = pathsForProject(
|
|
7430
8962
|
graphKey,
|
|
7431
|
-
|
|
8963
|
+
import_node_path44.default.join(opts.scanPath, "neat-out")
|
|
7432
8964
|
);
|
|
7433
|
-
const errorsPath =
|
|
8965
|
+
const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
|
|
7434
8966
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7435
8967
|
await saveGraphToDisk(graph, opts.outPath);
|
|
7436
8968
|
written.push(opts.outPath);
|
|
8969
|
+
const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
|
|
8970
|
+
if (gitignoreResult.action !== "unchanged") {
|
|
8971
|
+
written.push(gitignoreResult.file);
|
|
8972
|
+
}
|
|
7437
8973
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7438
8974
|
try {
|
|
7439
8975
|
await addProject({
|
|
@@ -7476,35 +9012,27 @@ async function runInit(opts) {
|
|
|
7476
9012
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
7477
9013
|
}
|
|
7478
9014
|
} else {
|
|
7479
|
-
await
|
|
9015
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7480
9016
|
written.push(patchPath);
|
|
7481
9017
|
}
|
|
7482
9018
|
}
|
|
7483
|
-
const nodes = [];
|
|
7484
|
-
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
7485
|
-
const edges = [];
|
|
7486
|
-
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
7487
9019
|
console.log("");
|
|
7488
|
-
console.log("=== neat init: summary ===");
|
|
7489
9020
|
console.log(`snapshot: ${opts.outPath}`);
|
|
7490
9021
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
7491
|
-
console.log(
|
|
7492
|
-
|
|
9022
|
+
console.log("");
|
|
9023
|
+
const divergenceResult = computeDivergences(graph);
|
|
9024
|
+
console.log(
|
|
9025
|
+
renderValueForwardSummary({
|
|
9026
|
+
graph,
|
|
9027
|
+
divergences: divergenceResult.divergences,
|
|
9028
|
+
verbose: opts.verbose
|
|
9029
|
+
})
|
|
9030
|
+
);
|
|
7493
9031
|
console.log(formatExtractionBanner(result.extractionErrors));
|
|
7494
9032
|
if (result.extractionErrors > 0) {
|
|
7495
9033
|
console.log(`errors: ${errorsPath}`);
|
|
7496
9034
|
}
|
|
7497
9035
|
console.log(formatPrecisionFloorBanner(result.extractedDropped));
|
|
7498
|
-
const incompatibilities = findIncompatibilities(nodes);
|
|
7499
|
-
if (incompatibilities.length > 0) {
|
|
7500
|
-
console.log("");
|
|
7501
|
-
console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
|
|
7502
|
-
for (const svc of incompatibilities) {
|
|
7503
|
-
for (const inc of svc.incompatibilities ?? []) {
|
|
7504
|
-
console.log(` ${svc.name}: ${formatIncompat(inc)}`);
|
|
7505
|
-
}
|
|
7506
|
-
}
|
|
7507
|
-
}
|
|
7508
9036
|
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
7509
9037
|
return { exitCode: 4, writtenFiles: written };
|
|
7510
9038
|
}
|
|
@@ -7524,9 +9052,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
7524
9052
|
};
|
|
7525
9053
|
function claudeConfigPath() {
|
|
7526
9054
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
7527
|
-
if (override && override.length > 0) return
|
|
9055
|
+
if (override && override.length > 0) return import_node_path44.default.resolve(override);
|
|
7528
9056
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7529
|
-
return
|
|
9057
|
+
return import_node_path44.default.join(home, ".claude.json");
|
|
7530
9058
|
}
|
|
7531
9059
|
async function runSkill(opts) {
|
|
7532
9060
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -7538,7 +9066,7 @@ async function runSkill(opts) {
|
|
|
7538
9066
|
const target = claudeConfigPath();
|
|
7539
9067
|
let existing = {};
|
|
7540
9068
|
try {
|
|
7541
|
-
existing = JSON.parse(await
|
|
9069
|
+
existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
|
|
7542
9070
|
} catch (err) {
|
|
7543
9071
|
if (err.code !== "ENOENT") {
|
|
7544
9072
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -7550,8 +9078,8 @@ async function runSkill(opts) {
|
|
|
7550
9078
|
...existing,
|
|
7551
9079
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
7552
9080
|
};
|
|
7553
|
-
await
|
|
7554
|
-
await
|
|
9081
|
+
await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
|
|
9082
|
+
await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
7555
9083
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
7556
9084
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
7557
9085
|
return { exitCode: 0 };
|
|
@@ -7585,12 +9113,12 @@ async function main() {
|
|
|
7585
9113
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
7586
9114
|
process.exit(2);
|
|
7587
9115
|
}
|
|
7588
|
-
const scanPath =
|
|
9116
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
7589
9117
|
const projectExplicit = parsed.project !== null;
|
|
7590
|
-
const projectName = projectExplicit ? project :
|
|
9118
|
+
const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
|
|
7591
9119
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
7592
|
-
const fallback = pathsForProject(projectKey,
|
|
7593
|
-
const outPath =
|
|
9120
|
+
const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
|
|
9121
|
+
const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
7594
9122
|
const result = await runInit({
|
|
7595
9123
|
scanPath,
|
|
7596
9124
|
outPath,
|
|
@@ -7598,7 +9126,8 @@ async function main() {
|
|
|
7598
9126
|
projectExplicit,
|
|
7599
9127
|
apply: apply3,
|
|
7600
9128
|
dryRun,
|
|
7601
|
-
noInstall
|
|
9129
|
+
noInstall,
|
|
9130
|
+
verbose: parsed.verbose
|
|
7602
9131
|
});
|
|
7603
9132
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
7604
9133
|
return;
|
|
@@ -7610,21 +9139,21 @@ async function main() {
|
|
|
7610
9139
|
usage();
|
|
7611
9140
|
process.exit(2);
|
|
7612
9141
|
}
|
|
7613
|
-
const scanPath =
|
|
7614
|
-
const stat = await
|
|
9142
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
9143
|
+
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
7615
9144
|
if (!stat || !stat.isDirectory()) {
|
|
7616
9145
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
7617
9146
|
process.exit(2);
|
|
7618
9147
|
}
|
|
7619
|
-
const projectPaths = pathsForProject(project,
|
|
7620
|
-
const outPath =
|
|
7621
|
-
const errorsPath =
|
|
7622
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
9148
|
+
const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
|
|
9149
|
+
const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
9150
|
+
const errorsPath = import_node_path44.default.resolve(
|
|
9151
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
|
|
7623
9152
|
);
|
|
7624
|
-
const staleEventsPath =
|
|
7625
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
9153
|
+
const staleEventsPath = import_node_path44.default.resolve(
|
|
9154
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.staleEventsPath))
|
|
7626
9155
|
);
|
|
7627
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
9156
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
7628
9157
|
const handle = await startWatch(getGraph(project), {
|
|
7629
9158
|
scanPath,
|
|
7630
9159
|
outPath,
|
|
@@ -7717,15 +9246,74 @@ async function main() {
|
|
|
7717
9246
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
7718
9247
|
return;
|
|
7719
9248
|
}
|
|
9249
|
+
if (cmd === "deploy") {
|
|
9250
|
+
const artifact = await runDeploy();
|
|
9251
|
+
const block = renderOtelEnvBlock2(artifact.token);
|
|
9252
|
+
console.log();
|
|
9253
|
+
console.log(`Substrate detected: ${artifact.substrate}`);
|
|
9254
|
+
if (artifact.artifactPath) {
|
|
9255
|
+
console.log(`Artifact written: ${artifact.artifactPath}`);
|
|
9256
|
+
} else {
|
|
9257
|
+
console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
|
|
9258
|
+
console.log();
|
|
9259
|
+
console.log(artifact.contents);
|
|
9260
|
+
}
|
|
9261
|
+
console.log();
|
|
9262
|
+
console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
|
|
9263
|
+
console.log(` ${artifact.token}`);
|
|
9264
|
+
console.log();
|
|
9265
|
+
console.log("For your application's deploy platform, set these env vars:");
|
|
9266
|
+
console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
9267
|
+
console.log();
|
|
9268
|
+
console.log("Once NEAT is running, your dashboard will be at:");
|
|
9269
|
+
console.log(" https://<host>:6328");
|
|
9270
|
+
console.log();
|
|
9271
|
+
console.log("To start NEAT, run:");
|
|
9272
|
+
console.log(` ${artifact.startCommand}`);
|
|
9273
|
+
return;
|
|
9274
|
+
}
|
|
9275
|
+
if (cmd === "sync") {
|
|
9276
|
+
const result = await runSync({
|
|
9277
|
+
...parsed.project ? { project: parsed.project } : {},
|
|
9278
|
+
...parsed.to ? { to: parsed.to } : {},
|
|
9279
|
+
...parsed.token ? { token: parsed.token } : {},
|
|
9280
|
+
dryRun: parsed.dryRun,
|
|
9281
|
+
noInstrument: parsed.noInstrument,
|
|
9282
|
+
json: parsed.json
|
|
9283
|
+
});
|
|
9284
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9285
|
+
return;
|
|
9286
|
+
}
|
|
7720
9287
|
if (QUERY_VERBS.has(cmd)) {
|
|
7721
9288
|
const code = await runQueryVerb(cmd, parsed);
|
|
7722
9289
|
if (code !== 0) process.exit(code);
|
|
7723
9290
|
return;
|
|
7724
9291
|
}
|
|
9292
|
+
const orchestratorCode = await tryOrchestrator(cmd, parsed);
|
|
9293
|
+
if (orchestratorCode !== null) {
|
|
9294
|
+
if (orchestratorCode !== 0) process.exit(orchestratorCode);
|
|
9295
|
+
return;
|
|
9296
|
+
}
|
|
7725
9297
|
console.error(`neat: unknown command "${cmd}"`);
|
|
7726
9298
|
usage();
|
|
7727
9299
|
process.exit(1);
|
|
7728
9300
|
}
|
|
9301
|
+
async function tryOrchestrator(cmd, parsed) {
|
|
9302
|
+
const scanPath = import_node_path44.default.resolve(cmd);
|
|
9303
|
+
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
9304
|
+
if (!stat || !stat.isDirectory()) return null;
|
|
9305
|
+
const projectExplicit = parsed.project !== null;
|
|
9306
|
+
const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
|
|
9307
|
+
const result = await runOrchestrator({
|
|
9308
|
+
scanPath,
|
|
9309
|
+
project: projectName,
|
|
9310
|
+
projectExplicit,
|
|
9311
|
+
noInstrument: parsed.noInstrument,
|
|
9312
|
+
noOpen: parsed.noOpen,
|
|
9313
|
+
yes: parsed.yes
|
|
9314
|
+
});
|
|
9315
|
+
return result.exitCode;
|
|
9316
|
+
}
|
|
7729
9317
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
7730
9318
|
"root-cause",
|
|
7731
9319
|
"blast-radius",
|
|
@@ -7865,10 +9453,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
7865
9453
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7866
9454
|
const out = [];
|
|
7867
9455
|
for (const p of parts) {
|
|
7868
|
-
const r =
|
|
9456
|
+
const r = import_types25.DivergenceTypeSchema.safeParse(p);
|
|
7869
9457
|
if (!r.success) {
|
|
7870
9458
|
console.error(
|
|
7871
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
9459
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
|
|
7872
9460
|
);
|
|
7873
9461
|
return 2;
|
|
7874
9462
|
}
|