@neat.is/core 0.3.8 → 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-7TYESDAI.js → chunk-4V23KYOP.js} +33 -4
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-IXIFJKMM.js} +2 -2
- package/dist/{chunk-LQ3JFBTX.js → chunk-N6RPINEJ.js} +5 -4
- package/dist/{chunk-LQ3JFBTX.js.map → chunk-N6RPINEJ.js.map} +1 -1
- package/dist/{chunk-CZ3T6TE2.js → chunk-UPW4CMOH.js} +120 -28
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +916 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +765 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +153 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +153 -31
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +282 -160
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-IXIFJKMM.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -47,14 +47,19 @@ function mountBearerAuth(app, opts) {
|
|
|
47
47
|
if (opts.trustProxy) return;
|
|
48
48
|
const expected = Buffer.from(opts.token, "utf8");
|
|
49
49
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
50
|
+
const publicRead = opts.publicRead === true;
|
|
50
51
|
app.addHook("preHandler", (req, reply, done) => {
|
|
51
|
-
const
|
|
52
|
+
const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
52
53
|
for (const suffix of suffixes) {
|
|
53
|
-
if (
|
|
54
|
+
if (path45 === suffix || path45.endsWith(suffix)) {
|
|
54
55
|
done();
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
60
|
+
done();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
58
63
|
const header = req.headers.authorization;
|
|
59
64
|
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
60
65
|
void reply.code(401).send({ error: "unauthorized" });
|
|
@@ -68,13 +73,19 @@ function mountBearerAuth(app, opts) {
|
|
|
68
73
|
done();
|
|
69
74
|
});
|
|
70
75
|
}
|
|
71
|
-
var import_node_crypto, DEFAULT_UNAUTH_SUFFIXES;
|
|
76
|
+
var import_node_crypto, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
|
|
72
77
|
var init_auth = __esm({
|
|
73
78
|
"src/auth.ts"() {
|
|
74
79
|
"use strict";
|
|
75
80
|
init_cjs_shims();
|
|
76
81
|
import_node_crypto = require("crypto");
|
|
77
|
-
|
|
82
|
+
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
83
|
+
DEFAULT_UNAUTH_SUFFIXES = [
|
|
84
|
+
"/health",
|
|
85
|
+
"/healthz",
|
|
86
|
+
"/readyz",
|
|
87
|
+
"/api/config"
|
|
88
|
+
];
|
|
78
89
|
}
|
|
79
90
|
});
|
|
80
91
|
|
|
@@ -277,6 +288,15 @@ function isoFromUnixNano(nanos) {
|
|
|
277
288
|
return void 0;
|
|
278
289
|
}
|
|
279
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
|
+
}
|
|
280
300
|
function parseOtlpRequest(body) {
|
|
281
301
|
const out = [];
|
|
282
302
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -296,6 +316,7 @@ function parseOtlpRequest(body) {
|
|
|
296
316
|
endTimeUnixNano: span.endTimeUnixNano ?? "0",
|
|
297
317
|
startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
|
|
298
318
|
durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
|
|
319
|
+
env: pickEnv(attrs, resourceAttrs),
|
|
299
320
|
attributes: attrs,
|
|
300
321
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
301
322
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
@@ -434,7 +455,7 @@ async function buildOtelReceiver(opts) {
|
|
|
434
455
|
};
|
|
435
456
|
return decorated;
|
|
436
457
|
}
|
|
437
|
-
var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
458
|
+
var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
438
459
|
var init_otel = __esm({
|
|
439
460
|
"src/otel.ts"() {
|
|
440
461
|
"use strict";
|
|
@@ -444,6 +465,9 @@ var init_otel = __esm({
|
|
|
444
465
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
445
466
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
446
467
|
init_auth();
|
|
468
|
+
ENV_ATTR_CANONICAL = "deployment.environment.name";
|
|
469
|
+
ENV_ATTR_COMPAT = "deployment.environment";
|
|
470
|
+
ENV_FALLBACK = "unknown";
|
|
447
471
|
exportTraceServiceRequestType = null;
|
|
448
472
|
exportTraceServiceResponseType = null;
|
|
449
473
|
cachedProtobufResponseBody = null;
|
|
@@ -462,7 +486,7 @@ __export(cli_exports, {
|
|
|
462
486
|
});
|
|
463
487
|
module.exports = __toCommonJS(cli_exports);
|
|
464
488
|
init_cjs_shims();
|
|
465
|
-
var
|
|
489
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
466
490
|
var import_node_fs28 = require("fs");
|
|
467
491
|
|
|
468
492
|
// src/graph.ts
|
|
@@ -973,19 +997,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
973
997
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
974
998
|
let best = { path: [start], edges: [] };
|
|
975
999
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
976
|
-
function step(node,
|
|
977
|
-
if (
|
|
978
|
-
best = { path: [...
|
|
1000
|
+
function step(node, path45, edges) {
|
|
1001
|
+
if (path45.length > best.path.length) {
|
|
1002
|
+
best = { path: [...path45], edges: [...edges] };
|
|
979
1003
|
}
|
|
980
|
-
if (
|
|
1004
|
+
if (path45.length - 1 >= maxDepth) return;
|
|
981
1005
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
982
1006
|
for (const [srcId, edge] of incoming) {
|
|
983
1007
|
if (visited.has(srcId)) continue;
|
|
984
1008
|
visited.add(srcId);
|
|
985
|
-
|
|
1009
|
+
path45.push(srcId);
|
|
986
1010
|
edges.push(edge);
|
|
987
|
-
step(srcId,
|
|
988
|
-
|
|
1011
|
+
step(srcId, path45, edges);
|
|
1012
|
+
path45.pop();
|
|
989
1013
|
edges.pop();
|
|
990
1014
|
visited.delete(srcId);
|
|
991
1015
|
}
|
|
@@ -1558,52 +1582,64 @@ function cacheSpanService(span, now) {
|
|
|
1558
1582
|
if (!span.traceId || !span.spanId) return;
|
|
1559
1583
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1560
1584
|
parentSpanCache.delete(key);
|
|
1561
|
-
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
|
+
});
|
|
1562
1590
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1563
1591
|
const oldest = parentSpanCache.keys().next().value;
|
|
1564
1592
|
if (!oldest) break;
|
|
1565
1593
|
parentSpanCache.delete(oldest);
|
|
1566
1594
|
}
|
|
1567
1595
|
}
|
|
1568
|
-
function
|
|
1596
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1569
1597
|
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1570
1598
|
if (!entry2) return null;
|
|
1571
1599
|
if (entry2.expiresAt <= now) {
|
|
1572
1600
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1573
1601
|
return null;
|
|
1574
1602
|
}
|
|
1575
|
-
return entry2.service;
|
|
1603
|
+
return { service: entry2.service, env: entry2.env };
|
|
1576
1604
|
}
|
|
1577
|
-
function resolveServiceId(graph, host) {
|
|
1578
|
-
const
|
|
1579
|
-
if (graph.hasNode(
|
|
1580
|
-
|
|
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;
|
|
1581
1613
|
graph.forEachNode((id, attrs) => {
|
|
1582
|
-
if (
|
|
1614
|
+
if (sameEnv) return;
|
|
1583
1615
|
const a = attrs;
|
|
1584
1616
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1585
|
-
|
|
1586
|
-
|
|
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;
|
|
1587
1623
|
return;
|
|
1588
1624
|
}
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1625
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1626
|
+
else if (!anyMatch) anyMatch = id;
|
|
1592
1627
|
});
|
|
1593
|
-
return
|
|
1628
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1594
1629
|
}
|
|
1595
1630
|
function frontierIdFor(host) {
|
|
1596
1631
|
return (0, import_types3.frontierId)(host);
|
|
1597
1632
|
}
|
|
1598
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1599
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1633
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1634
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1600
1635
|
if (graph.hasNode(id)) return id;
|
|
1601
1636
|
const node = {
|
|
1602
1637
|
id,
|
|
1603
1638
|
type: import_types3.NodeType.ServiceNode,
|
|
1604
1639
|
name: serviceName,
|
|
1605
1640
|
language: "unknown",
|
|
1606
|
-
discoveredVia: "otel"
|
|
1641
|
+
discoveredVia: "otel",
|
|
1642
|
+
...env !== "unknown" ? { env } : {}
|
|
1607
1643
|
};
|
|
1608
1644
|
graph.addNode(id, node);
|
|
1609
1645
|
return id;
|
|
@@ -1749,7 +1785,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1749
1785
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1750
1786
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1751
1787
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1752
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1788
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1753
1789
|
};
|
|
1754
1790
|
}
|
|
1755
1791
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1763,7 +1799,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1763
1799
|
async function handleSpan(ctx, span) {
|
|
1764
1800
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1765
1801
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1766
|
-
const
|
|
1802
|
+
const env = span.env ?? "unknown";
|
|
1803
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1767
1804
|
const isError = span.statusCode === 2;
|
|
1768
1805
|
cacheSpanService(span, nowMs);
|
|
1769
1806
|
let affectedNode = sourceId;
|
|
@@ -1786,7 +1823,7 @@ async function handleSpan(ctx, span) {
|
|
|
1786
1823
|
const host = pickAddress(span);
|
|
1787
1824
|
let resolvedViaAddress = false;
|
|
1788
1825
|
if (host && host !== span.service) {
|
|
1789
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1826
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1790
1827
|
if (targetId && targetId !== sourceId) {
|
|
1791
1828
|
upsertObservedEdge(
|
|
1792
1829
|
ctx.graph,
|
|
@@ -1813,9 +1850,9 @@ async function handleSpan(ctx, span) {
|
|
|
1813
1850
|
}
|
|
1814
1851
|
}
|
|
1815
1852
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1816
|
-
const
|
|
1817
|
-
if (
|
|
1818
|
-
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);
|
|
1819
1856
|
upsertObservedEdge(
|
|
1820
1857
|
ctx.graph,
|
|
1821
1858
|
import_types3.EdgeType.CALLS,
|
|
@@ -2011,6 +2048,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
2011
2048
|
throw err;
|
|
2012
2049
|
}
|
|
2013
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
|
+
}
|
|
2014
2073
|
|
|
2015
2074
|
// src/extract/services.ts
|
|
2016
2075
|
init_cjs_shims();
|
|
@@ -2419,6 +2478,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2419
2478
|
}
|
|
2420
2479
|
return [...found];
|
|
2421
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
|
+
}
|
|
2422
2493
|
async function discoverNodeService(scanPath, dir) {
|
|
2423
2494
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2424
2495
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2430,6 +2501,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2430
2501
|
return null;
|
|
2431
2502
|
}
|
|
2432
2503
|
if (!pkg.name) return null;
|
|
2504
|
+
const framework = detectJsFramework(pkg);
|
|
2433
2505
|
const node = {
|
|
2434
2506
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2435
2507
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2438,7 +2510,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2438
2510
|
version: pkg.version,
|
|
2439
2511
|
dependencies: pkg.dependencies ?? {},
|
|
2440
2512
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2441
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2513
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2514
|
+
...framework ? { framework } : {}
|
|
2442
2515
|
};
|
|
2443
2516
|
return { pkg, dir, node };
|
|
2444
2517
|
}
|
|
@@ -4470,7 +4543,7 @@ init_cjs_shims();
|
|
|
4470
4543
|
var import_node_fs18 = require("fs");
|
|
4471
4544
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4472
4545
|
var import_types20 = require("@neat.is/types");
|
|
4473
|
-
var SCHEMA_VERSION =
|
|
4546
|
+
var SCHEMA_VERSION = 4;
|
|
4474
4547
|
function migrateV1ToV2(payload) {
|
|
4475
4548
|
const nodes = payload.graph.nodes;
|
|
4476
4549
|
if (Array.isArray(nodes)) {
|
|
@@ -4482,6 +4555,9 @@ function migrateV1ToV2(payload) {
|
|
|
4482
4555
|
}
|
|
4483
4556
|
return { ...payload, schemaVersion: 2 };
|
|
4484
4557
|
}
|
|
4558
|
+
function migrateV3ToV4(payload) {
|
|
4559
|
+
return { ...payload, schemaVersion: 4 };
|
|
4560
|
+
}
|
|
4485
4561
|
function migrateV2ToV3(payload) {
|
|
4486
4562
|
const edges = payload.graph.edges;
|
|
4487
4563
|
if (Array.isArray(edges)) {
|
|
@@ -4530,6 +4606,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4530
4606
|
if (payload.schemaVersion === 2) {
|
|
4531
4607
|
payload = migrateV2ToV3(payload);
|
|
4532
4608
|
}
|
|
4609
|
+
if (payload.schemaVersion === 3) {
|
|
4610
|
+
payload = migrateV3ToV4(payload);
|
|
4611
|
+
}
|
|
4533
4612
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4534
4613
|
throw new Error(
|
|
4535
4614
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -5333,6 +5412,35 @@ function registerRoutes(scope, ctx) {
|
|
|
5333
5412
|
}
|
|
5334
5413
|
}
|
|
5335
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
|
+
});
|
|
5336
5444
|
scope.post("/graph/scan", async (req, reply) => {
|
|
5337
5445
|
const proj = resolveProject(registry, req, reply);
|
|
5338
5446
|
if (!proj) return;
|
|
@@ -5426,7 +5534,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5426
5534
|
async function buildApi(opts) {
|
|
5427
5535
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5428
5536
|
await app.register(import_cors.default, { origin: true });
|
|
5429
|
-
mountBearerAuth(app, {
|
|
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
|
+
}));
|
|
5430
5546
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5431
5547
|
const registry = buildLegacyRegistry(opts);
|
|
5432
5548
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -6370,6 +6486,100 @@ const sdk = new NodeSDK({
|
|
|
6370
6486
|
})
|
|
6371
6487
|
sdk.start()
|
|
6372
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
|
+
`;
|
|
6373
6583
|
|
|
6374
6584
|
// src/installers/javascript.ts
|
|
6375
6585
|
var SDK_PACKAGES = [
|
|
@@ -6421,6 +6631,71 @@ async function findNextConfig(serviceDir) {
|
|
|
6421
6631
|
function hasNextDependency(pkg) {
|
|
6422
6632
|
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
6423
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
|
+
}
|
|
6424
6699
|
function parseNextMajor(range) {
|
|
6425
6700
|
if (!range) return null;
|
|
6426
6701
|
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
@@ -6628,6 +6903,276 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
6628
6903
|
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
6629
6904
|
};
|
|
6630
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
|
+
}
|
|
6631
7176
|
async function plan(serviceDir) {
|
|
6632
7177
|
const pkg = await readPackageJson(serviceDir);
|
|
6633
7178
|
const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
|
|
@@ -6646,6 +7191,31 @@ async function plan(serviceDir) {
|
|
|
6646
7191
|
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
6647
7192
|
}
|
|
6648
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
|
+
}
|
|
6649
7219
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
6650
7220
|
if (!entryFile) {
|
|
6651
7221
|
return { ...empty, libOnly: true };
|
|
@@ -6718,9 +7288,18 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
6718
7288
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6719
7289
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6720
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;
|
|
6721
7299
|
return false;
|
|
6722
7300
|
}
|
|
6723
7301
|
async function writeAtomic(file, contents) {
|
|
7302
|
+
await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
|
|
6724
7303
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6725
7304
|
await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
|
|
6726
7305
|
await import_node_fs25.promises.rename(tmp, file);
|
|
@@ -7193,6 +7772,49 @@ var import_node_http = __toESM(require("http"), 1);
|
|
|
7193
7772
|
var import_node_path42 = __toESM(require("path"), 1);
|
|
7194
7773
|
var import_node_child_process2 = require("child_process");
|
|
7195
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
|
+
}
|
|
7196
7818
|
async function promptYesNo(question) {
|
|
7197
7819
|
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
7198
7820
|
return new Promise((resolve) => {
|
|
@@ -7287,24 +7909,21 @@ async function runOrchestrator(opts) {
|
|
|
7287
7909
|
}
|
|
7288
7910
|
console.log(`neat: ${opts.scanPath}`);
|
|
7289
7911
|
console.log("");
|
|
7290
|
-
const
|
|
7291
|
-
|
|
7912
|
+
const persisted = await extractAndPersist({
|
|
7913
|
+
scanPath: opts.scanPath,
|
|
7914
|
+
project: opts.project,
|
|
7915
|
+
projectExplicit: opts.projectExplicit
|
|
7916
|
+
});
|
|
7917
|
+
const { graph, services, languages } = persisted;
|
|
7292
7918
|
result.steps.discovery = { services: services.length, languages };
|
|
7293
7919
|
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
7294
7920
|
let runApply = !opts.noInstrument;
|
|
7295
7921
|
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
7296
7922
|
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
7297
7923
|
}
|
|
7298
|
-
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
7299
|
-
resetGraph(graphKey);
|
|
7300
|
-
const graph = getGraph(graphKey);
|
|
7301
|
-
const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
|
|
7302
|
-
const errorsPath = projectPaths.errorsPath;
|
|
7303
|
-
const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7304
|
-
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
7305
7924
|
result.steps.extraction = {
|
|
7306
|
-
nodesAdded:
|
|
7307
|
-
edgesAdded:
|
|
7925
|
+
nodesAdded: persisted.nodesAdded,
|
|
7926
|
+
edgesAdded: persisted.edgesAdded
|
|
7308
7927
|
};
|
|
7309
7928
|
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
7310
7929
|
result.steps.gitignore = gi.action;
|
|
@@ -7329,24 +7948,11 @@ async function runOrchestrator(opts) {
|
|
|
7329
7948
|
result.steps.apply.skipped = true;
|
|
7330
7949
|
console.log("skipped instrumentation (--no-instrument)");
|
|
7331
7950
|
} else {
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
if (!installer) continue;
|
|
7338
|
-
const plan3 = await installer.plan(svc.dir);
|
|
7339
|
-
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
7340
|
-
already++;
|
|
7341
|
-
continue;
|
|
7342
|
-
}
|
|
7343
|
-
const outcome = await installer.apply(plan3);
|
|
7344
|
-
if (outcome.outcome === "instrumented") instrumented++;
|
|
7345
|
-
else if (outcome.outcome === "already-instrumented") already++;
|
|
7346
|
-
else if (outcome.outcome === "lib-only") libOnly++;
|
|
7347
|
-
}
|
|
7348
|
-
result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
|
|
7349
|
-
console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
|
|
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
|
+
);
|
|
7350
7956
|
}
|
|
7351
7957
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
7352
7958
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -7395,8 +8001,9 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
7395
8001
|
console.log(`dashboard: ${dashboardUrl}`);
|
|
7396
8002
|
}
|
|
7397
8003
|
|
|
7398
|
-
// src/cli.ts
|
|
7399
|
-
|
|
8004
|
+
// src/cli-verbs.ts
|
|
8005
|
+
init_cjs_shims();
|
|
8006
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
7400
8007
|
|
|
7401
8008
|
// src/cli-client.ts
|
|
7402
8009
|
init_cjs_shims();
|
|
@@ -7417,13 +8024,16 @@ var TransportError = class extends Error {
|
|
|
7417
8024
|
this.name = "TransportError";
|
|
7418
8025
|
}
|
|
7419
8026
|
};
|
|
7420
|
-
function createHttpClient(baseUrl) {
|
|
8027
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
7421
8028
|
const root = baseUrl.replace(/\/$/, "");
|
|
8029
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
7422
8030
|
return {
|
|
7423
|
-
async get(
|
|
8031
|
+
async get(path45) {
|
|
7424
8032
|
let res;
|
|
7425
8033
|
try {
|
|
7426
|
-
res = await fetch(`${root}${
|
|
8034
|
+
res = await fetch(`${root}${path45}`, {
|
|
8035
|
+
headers: { ...authHeader }
|
|
8036
|
+
});
|
|
7427
8037
|
} catch (err) {
|
|
7428
8038
|
throw new TransportError(
|
|
7429
8039
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -7433,18 +8043,18 @@ function createHttpClient(baseUrl) {
|
|
|
7433
8043
|
const body = await res.text().catch(() => "");
|
|
7434
8044
|
throw new HttpError(
|
|
7435
8045
|
res.status,
|
|
7436
|
-
`${res.status} ${res.statusText} on GET ${
|
|
8046
|
+
`${res.status} ${res.statusText} on GET ${path45}: ${body}`,
|
|
7437
8047
|
body
|
|
7438
8048
|
);
|
|
7439
8049
|
}
|
|
7440
8050
|
return await res.json();
|
|
7441
8051
|
},
|
|
7442
|
-
async post(
|
|
8052
|
+
async post(path45, body) {
|
|
7443
8053
|
let res;
|
|
7444
8054
|
try {
|
|
7445
|
-
res = await fetch(`${root}${
|
|
8055
|
+
res = await fetch(`${root}${path45}`, {
|
|
7446
8056
|
method: "POST",
|
|
7447
|
-
headers: { "content-type": "application/json" },
|
|
8057
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
7448
8058
|
body: JSON.stringify(body)
|
|
7449
8059
|
});
|
|
7450
8060
|
} catch (err) {
|
|
@@ -7456,7 +8066,7 @@ function createHttpClient(baseUrl) {
|
|
|
7456
8066
|
const text = await res.text().catch(() => "");
|
|
7457
8067
|
throw new HttpError(
|
|
7458
8068
|
res.status,
|
|
7459
|
-
`${res.status} ${res.statusText} on POST ${
|
|
8069
|
+
`${res.status} ${res.statusText} on POST ${path45}: ${text}`,
|
|
7460
8070
|
text
|
|
7461
8071
|
);
|
|
7462
8072
|
}
|
|
@@ -7470,12 +8080,12 @@ function projectPath(project, suffix) {
|
|
|
7470
8080
|
}
|
|
7471
8081
|
async function runRootCause(client, input) {
|
|
7472
8082
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
7473
|
-
const
|
|
8083
|
+
const path45 = projectPath(
|
|
7474
8084
|
input.project,
|
|
7475
8085
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
7476
8086
|
);
|
|
7477
8087
|
try {
|
|
7478
|
-
const result = await client.get(
|
|
8088
|
+
const result = await client.get(path45);
|
|
7479
8089
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
7480
8090
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
7481
8091
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -7501,12 +8111,12 @@ async function runRootCause(client, input) {
|
|
|
7501
8111
|
}
|
|
7502
8112
|
async function runBlastRadius(client, input) {
|
|
7503
8113
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
7504
|
-
const
|
|
8114
|
+
const path45 = projectPath(
|
|
7505
8115
|
input.project,
|
|
7506
8116
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
7507
8117
|
);
|
|
7508
8118
|
try {
|
|
7509
|
-
const result = await client.get(
|
|
8119
|
+
const result = await client.get(path45);
|
|
7510
8120
|
if (result.totalAffected === 0) {
|
|
7511
8121
|
return {
|
|
7512
8122
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -7540,12 +8150,12 @@ function formatBlastEntry(n) {
|
|
|
7540
8150
|
}
|
|
7541
8151
|
async function runDependencies(client, input) {
|
|
7542
8152
|
const depth = input.depth ?? 3;
|
|
7543
|
-
const
|
|
8153
|
+
const path45 = projectPath(
|
|
7544
8154
|
input.project,
|
|
7545
8155
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
7546
8156
|
);
|
|
7547
8157
|
try {
|
|
7548
|
-
const result = await client.get(
|
|
8158
|
+
const result = await client.get(path45);
|
|
7549
8159
|
if (result.total === 0) {
|
|
7550
8160
|
return {
|
|
7551
8161
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -7626,9 +8236,9 @@ function formatDuration(ms) {
|
|
|
7626
8236
|
return `${Math.round(h / 24)}d`;
|
|
7627
8237
|
}
|
|
7628
8238
|
async function runIncidents(client, input) {
|
|
7629
|
-
const
|
|
8239
|
+
const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
7630
8240
|
try {
|
|
7631
|
-
const body = await client.get(
|
|
8241
|
+
const body = await client.get(path45);
|
|
7632
8242
|
const events = body.events;
|
|
7633
8243
|
if (events.length === 0) {
|
|
7634
8244
|
return {
|
|
@@ -7894,8 +8504,181 @@ function exitCodeForError(err) {
|
|
|
7894
8504
|
if (err instanceof HttpError) return 1;
|
|
7895
8505
|
return 1;
|
|
7896
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
|
+
}
|
|
7897
8679
|
|
|
7898
8680
|
// src/cli.ts
|
|
8681
|
+
var import_types25 = require("@neat.is/types");
|
|
7899
8682
|
function usage() {
|
|
7900
8683
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
7901
8684
|
console.log("");
|
|
@@ -7923,6 +8706,15 @@ function usage() {
|
|
|
7923
8706
|
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
7924
8707
|
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
7925
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");
|
|
7926
8718
|
console.log("");
|
|
7927
8719
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7928
8720
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -7976,7 +8768,9 @@ var STRING_FLAGS = [
|
|
|
7976
8768
|
["--error-id", "errorId"],
|
|
7977
8769
|
["--hypothetical-action", "hypotheticalAction"],
|
|
7978
8770
|
["--type", "type"],
|
|
7979
|
-
["--min-confidence", "minConfidence"]
|
|
8771
|
+
["--min-confidence", "minConfidence"],
|
|
8772
|
+
["--to", "to"],
|
|
8773
|
+
["--token", "token"]
|
|
7980
8774
|
];
|
|
7981
8775
|
function parseArgs(rest) {
|
|
7982
8776
|
const positional = [];
|
|
@@ -8001,6 +8795,8 @@ function parseArgs(rest) {
|
|
|
8001
8795
|
hypotheticalAction: null,
|
|
8002
8796
|
type: null,
|
|
8003
8797
|
minConfidence: null,
|
|
8798
|
+
to: null,
|
|
8799
|
+
token: null,
|
|
8004
8800
|
positional: []
|
|
8005
8801
|
};
|
|
8006
8802
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -8147,12 +8943,12 @@ async function runInit(opts) {
|
|
|
8147
8943
|
printDiscoveryReport(opts, services);
|
|
8148
8944
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
8149
8945
|
const patch = renderPatch(sections);
|
|
8150
|
-
const patchPath =
|
|
8946
|
+
const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
|
|
8151
8947
|
if (opts.dryRun) {
|
|
8152
8948
|
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
8153
8949
|
written.push(patchPath);
|
|
8154
8950
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8155
|
-
const gitignorePath =
|
|
8951
|
+
const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
|
|
8156
8952
|
const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8157
8953
|
const verb = gitignoreExists ? "append" : "create";
|
|
8158
8954
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
@@ -8164,9 +8960,9 @@ async function runInit(opts) {
|
|
|
8164
8960
|
const graph = getGraph(graphKey);
|
|
8165
8961
|
const projectPaths = pathsForProject(
|
|
8166
8962
|
graphKey,
|
|
8167
|
-
|
|
8963
|
+
import_node_path44.default.join(opts.scanPath, "neat-out")
|
|
8168
8964
|
);
|
|
8169
|
-
const errorsPath =
|
|
8965
|
+
const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
|
|
8170
8966
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
8171
8967
|
await saveGraphToDisk(graph, opts.outPath);
|
|
8172
8968
|
written.push(opts.outPath);
|
|
@@ -8256,9 +9052,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
8256
9052
|
};
|
|
8257
9053
|
function claudeConfigPath() {
|
|
8258
9054
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
8259
|
-
if (override && override.length > 0) return
|
|
9055
|
+
if (override && override.length > 0) return import_node_path44.default.resolve(override);
|
|
8260
9056
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8261
|
-
return
|
|
9057
|
+
return import_node_path44.default.join(home, ".claude.json");
|
|
8262
9058
|
}
|
|
8263
9059
|
async function runSkill(opts) {
|
|
8264
9060
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -8282,7 +9078,7 @@ async function runSkill(opts) {
|
|
|
8282
9078
|
...existing,
|
|
8283
9079
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
8284
9080
|
};
|
|
8285
|
-
await import_node_fs28.promises.mkdir(
|
|
9081
|
+
await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
|
|
8286
9082
|
await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
8287
9083
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
8288
9084
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
@@ -8317,12 +9113,12 @@ async function main() {
|
|
|
8317
9113
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
8318
9114
|
process.exit(2);
|
|
8319
9115
|
}
|
|
8320
|
-
const scanPath =
|
|
9116
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
8321
9117
|
const projectExplicit = parsed.project !== null;
|
|
8322
|
-
const projectName = projectExplicit ? project :
|
|
9118
|
+
const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
|
|
8323
9119
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
8324
|
-
const fallback = pathsForProject(projectKey,
|
|
8325
|
-
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);
|
|
8326
9122
|
const result = await runInit({
|
|
8327
9123
|
scanPath,
|
|
8328
9124
|
outPath,
|
|
@@ -8343,21 +9139,21 @@ async function main() {
|
|
|
8343
9139
|
usage();
|
|
8344
9140
|
process.exit(2);
|
|
8345
9141
|
}
|
|
8346
|
-
const scanPath =
|
|
9142
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
8347
9143
|
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
8348
9144
|
if (!stat || !stat.isDirectory()) {
|
|
8349
9145
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
8350
9146
|
process.exit(2);
|
|
8351
9147
|
}
|
|
8352
|
-
const projectPaths = pathsForProject(project,
|
|
8353
|
-
const outPath =
|
|
8354
|
-
const errorsPath =
|
|
8355
|
-
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))
|
|
8356
9152
|
);
|
|
8357
|
-
const staleEventsPath =
|
|
8358
|
-
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))
|
|
8359
9155
|
);
|
|
8360
|
-
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;
|
|
8361
9157
|
const handle = await startWatch(getGraph(project), {
|
|
8362
9158
|
scanPath,
|
|
8363
9159
|
outPath,
|
|
@@ -8476,6 +9272,18 @@ async function main() {
|
|
|
8476
9272
|
console.log(` ${artifact.startCommand}`);
|
|
8477
9273
|
return;
|
|
8478
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
|
+
}
|
|
8479
9287
|
if (QUERY_VERBS.has(cmd)) {
|
|
8480
9288
|
const code = await runQueryVerb(cmd, parsed);
|
|
8481
9289
|
if (code !== 0) process.exit(code);
|
|
@@ -8491,11 +9299,11 @@ async function main() {
|
|
|
8491
9299
|
process.exit(1);
|
|
8492
9300
|
}
|
|
8493
9301
|
async function tryOrchestrator(cmd, parsed) {
|
|
8494
|
-
const scanPath =
|
|
9302
|
+
const scanPath = import_node_path44.default.resolve(cmd);
|
|
8495
9303
|
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
8496
9304
|
if (!stat || !stat.isDirectory()) return null;
|
|
8497
9305
|
const projectExplicit = parsed.project !== null;
|
|
8498
|
-
const projectName = projectExplicit ? parsed.project :
|
|
9306
|
+
const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
|
|
8499
9307
|
const result = await runOrchestrator({
|
|
8500
9308
|
scanPath,
|
|
8501
9309
|
project: projectName,
|