@neat.is/core 0.3.8 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LQ3JFBTX.js → chunk-CB2UK4EH.js} +139 -41
- package/dist/chunk-CB2UK4EH.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-D5PIJFBE.js} +2 -2
- package/dist/{chunk-7TYESDAI.js → chunk-KYRIQIPG.js} +48 -11
- package/dist/chunk-KYRIQIPG.js.map +1 -0
- package/dist/{chunk-CZ3T6TE2.js → chunk-NTQHMXWE.js} +239 -71
- package/dist/chunk-NTQHMXWE.js.map +1 -0
- package/dist/cli.cjs +1209 -176
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +4 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +940 -99
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +418 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +427 -131
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-QTX2YQJZ.js} +3 -3
- package/dist/server.cjs +401 -203
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- package/dist/chunk-LQ3JFBTX.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-D5PIJFBE.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-QTX2YQJZ.js.map} +0 -0
package/dist/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;
|
|
@@ -456,14 +480,16 @@ __export(cli_exports, {
|
|
|
456
480
|
CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
|
|
457
481
|
QUERY_VERBS: () => QUERY_VERBS,
|
|
458
482
|
parseArgs: () => parseArgs,
|
|
483
|
+
readPackageVersion: () => readPackageVersion,
|
|
459
484
|
runInit: () => runInit,
|
|
460
485
|
runQueryVerb: () => runQueryVerb,
|
|
461
486
|
runSkill: () => runSkill
|
|
462
487
|
});
|
|
463
488
|
module.exports = __toCommonJS(cli_exports);
|
|
464
489
|
init_cjs_shims();
|
|
465
|
-
var
|
|
490
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
466
491
|
var import_node_fs28 = require("fs");
|
|
492
|
+
var import_node_url3 = require("url");
|
|
467
493
|
|
|
468
494
|
// src/graph.ts
|
|
469
495
|
init_cjs_shims();
|
|
@@ -973,19 +999,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
973
999
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
974
1000
|
let best = { path: [start], edges: [] };
|
|
975
1001
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
976
|
-
function step(node,
|
|
977
|
-
if (
|
|
978
|
-
best = { path: [...
|
|
1002
|
+
function step(node, path45, edges) {
|
|
1003
|
+
if (path45.length > best.path.length) {
|
|
1004
|
+
best = { path: [...path45], edges: [...edges] };
|
|
979
1005
|
}
|
|
980
|
-
if (
|
|
1006
|
+
if (path45.length - 1 >= maxDepth) return;
|
|
981
1007
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
982
1008
|
for (const [srcId, edge] of incoming) {
|
|
983
1009
|
if (visited.has(srcId)) continue;
|
|
984
1010
|
visited.add(srcId);
|
|
985
|
-
|
|
1011
|
+
path45.push(srcId);
|
|
986
1012
|
edges.push(edge);
|
|
987
|
-
step(srcId,
|
|
988
|
-
|
|
1013
|
+
step(srcId, path45, edges);
|
|
1014
|
+
path45.pop();
|
|
989
1015
|
edges.pop();
|
|
990
1016
|
visited.delete(srcId);
|
|
991
1017
|
}
|
|
@@ -1558,52 +1584,64 @@ function cacheSpanService(span, now) {
|
|
|
1558
1584
|
if (!span.traceId || !span.spanId) return;
|
|
1559
1585
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1560
1586
|
parentSpanCache.delete(key);
|
|
1561
|
-
parentSpanCache.set(key, {
|
|
1587
|
+
parentSpanCache.set(key, {
|
|
1588
|
+
service: span.service,
|
|
1589
|
+
env: span.env ?? "unknown",
|
|
1590
|
+
expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
|
|
1591
|
+
});
|
|
1562
1592
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1563
1593
|
const oldest = parentSpanCache.keys().next().value;
|
|
1564
1594
|
if (!oldest) break;
|
|
1565
1595
|
parentSpanCache.delete(oldest);
|
|
1566
1596
|
}
|
|
1567
1597
|
}
|
|
1568
|
-
function
|
|
1598
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1569
1599
|
const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1570
1600
|
if (!entry2) return null;
|
|
1571
1601
|
if (entry2.expiresAt <= now) {
|
|
1572
1602
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1573
1603
|
return null;
|
|
1574
1604
|
}
|
|
1575
|
-
return entry2.service;
|
|
1605
|
+
return { service: entry2.service, env: entry2.env };
|
|
1576
1606
|
}
|
|
1577
|
-
function resolveServiceId(graph, host) {
|
|
1578
|
-
const
|
|
1579
|
-
if (graph.hasNode(
|
|
1580
|
-
|
|
1607
|
+
function resolveServiceId(graph, host, env) {
|
|
1608
|
+
const envTagged = (0, import_types3.serviceId)(host, env);
|
|
1609
|
+
if (graph.hasNode(envTagged)) return envTagged;
|
|
1610
|
+
const envLess = (0, import_types3.serviceId)(host);
|
|
1611
|
+
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
1612
|
+
let sameEnv = null;
|
|
1613
|
+
let envLessMatch = null;
|
|
1614
|
+
let anyMatch = null;
|
|
1581
1615
|
graph.forEachNode((id, attrs) => {
|
|
1582
|
-
if (
|
|
1616
|
+
if (sameEnv) return;
|
|
1583
1617
|
const a = attrs;
|
|
1584
1618
|
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1585
|
-
|
|
1586
|
-
|
|
1619
|
+
const matchesByName = a.name === host;
|
|
1620
|
+
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1621
|
+
if (!matchesByName && !matchesByAlias) return;
|
|
1622
|
+
const nodeEnv = a.env ?? "unknown";
|
|
1623
|
+
if (nodeEnv === env) {
|
|
1624
|
+
sameEnv = id;
|
|
1587
1625
|
return;
|
|
1588
1626
|
}
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1627
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1628
|
+
else if (!anyMatch) anyMatch = id;
|
|
1592
1629
|
});
|
|
1593
|
-
return
|
|
1630
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1594
1631
|
}
|
|
1595
1632
|
function frontierIdFor(host) {
|
|
1596
1633
|
return (0, import_types3.frontierId)(host);
|
|
1597
1634
|
}
|
|
1598
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1599
|
-
const id = (0, import_types3.serviceId)(serviceName);
|
|
1635
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1636
|
+
const id = (0, import_types3.serviceId)(serviceName, env);
|
|
1600
1637
|
if (graph.hasNode(id)) return id;
|
|
1601
1638
|
const node = {
|
|
1602
1639
|
id,
|
|
1603
1640
|
type: import_types3.NodeType.ServiceNode,
|
|
1604
1641
|
name: serviceName,
|
|
1605
1642
|
language: "unknown",
|
|
1606
|
-
discoveredVia: "otel"
|
|
1643
|
+
discoveredVia: "otel",
|
|
1644
|
+
...env !== "unknown" ? { env } : {}
|
|
1607
1645
|
};
|
|
1608
1646
|
graph.addNode(id, node);
|
|
1609
1647
|
return id;
|
|
@@ -1749,7 +1787,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1749
1787
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1750
1788
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1751
1789
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1752
|
-
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1790
|
+
affectedNode: (0, import_types3.serviceId)(span.service, span.env)
|
|
1753
1791
|
};
|
|
1754
1792
|
}
|
|
1755
1793
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1763,7 +1801,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1763
1801
|
async function handleSpan(ctx, span) {
|
|
1764
1802
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1765
1803
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1766
|
-
const
|
|
1804
|
+
const env = span.env ?? "unknown";
|
|
1805
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1767
1806
|
const isError = span.statusCode === 2;
|
|
1768
1807
|
cacheSpanService(span, nowMs);
|
|
1769
1808
|
let affectedNode = sourceId;
|
|
@@ -1786,7 +1825,7 @@ async function handleSpan(ctx, span) {
|
|
|
1786
1825
|
const host = pickAddress(span);
|
|
1787
1826
|
let resolvedViaAddress = false;
|
|
1788
1827
|
if (host && host !== span.service) {
|
|
1789
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1828
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1790
1829
|
if (targetId && targetId !== sourceId) {
|
|
1791
1830
|
upsertObservedEdge(
|
|
1792
1831
|
ctx.graph,
|
|
@@ -1813,9 +1852,9 @@ async function handleSpan(ctx, span) {
|
|
|
1813
1852
|
}
|
|
1814
1853
|
}
|
|
1815
1854
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1816
|
-
const
|
|
1817
|
-
if (
|
|
1818
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
1855
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1856
|
+
if (parent && parent.service !== span.service) {
|
|
1857
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1819
1858
|
upsertObservedEdge(
|
|
1820
1859
|
ctx.graph,
|
|
1821
1860
|
import_types3.EdgeType.CALLS,
|
|
@@ -2011,6 +2050,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
2011
2050
|
throw err;
|
|
2012
2051
|
}
|
|
2013
2052
|
}
|
|
2053
|
+
function mergeSnapshot(graph, snapshot) {
|
|
2054
|
+
const exported = snapshot.graph;
|
|
2055
|
+
let nodesAdded = 0;
|
|
2056
|
+
let edgesAdded = 0;
|
|
2057
|
+
for (const node of exported.nodes ?? []) {
|
|
2058
|
+
if (graph.hasNode(node.key)) continue;
|
|
2059
|
+
if (!node.attributes) continue;
|
|
2060
|
+
graph.addNode(node.key, node.attributes);
|
|
2061
|
+
nodesAdded++;
|
|
2062
|
+
}
|
|
2063
|
+
for (const edge of exported.edges ?? []) {
|
|
2064
|
+
const attrs = edge.attributes;
|
|
2065
|
+
if (!attrs) continue;
|
|
2066
|
+
const id = edge.key ?? attrs.id;
|
|
2067
|
+
if (!id) continue;
|
|
2068
|
+
if (graph.hasEdge(id)) continue;
|
|
2069
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
2070
|
+
graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
|
|
2071
|
+
edgesAdded++;
|
|
2072
|
+
}
|
|
2073
|
+
return { nodesAdded, edgesAdded };
|
|
2074
|
+
}
|
|
2014
2075
|
|
|
2015
2076
|
// src/extract/services.ts
|
|
2016
2077
|
init_cjs_shims();
|
|
@@ -2034,8 +2095,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2034
2095
|
".turbo",
|
|
2035
2096
|
"dist",
|
|
2036
2097
|
"build",
|
|
2037
|
-
".next"
|
|
2098
|
+
".next",
|
|
2099
|
+
// Python virtualenv shapes (issue #344). Walking into a venv pulls in the
|
|
2100
|
+
// entire CPython stdlib + every installed package as if it were first-party
|
|
2101
|
+
// service code — 20k+ files, none of which the user wrote. The shape names
|
|
2102
|
+
// cover the three common venv tools (`venv` / `python -m venv`,
|
|
2103
|
+
// `virtualenv`) and the tox + PEP 582 conventions.
|
|
2104
|
+
".venv",
|
|
2105
|
+
"venv",
|
|
2106
|
+
"__pypackages__",
|
|
2107
|
+
".tox",
|
|
2108
|
+
// `site-packages` shows up nested inside venvs that don't carry one of the
|
|
2109
|
+
// outer names (system Python on macOS, in-place pyenv layouts). Listing it
|
|
2110
|
+
// here means we stop the walk at the boundary even when the wrapper dir
|
|
2111
|
+
// wasn't recognisable.
|
|
2112
|
+
"site-packages"
|
|
2038
2113
|
]);
|
|
2114
|
+
var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
|
|
2115
|
+
async function isPythonVenvDir(dir) {
|
|
2116
|
+
const cached = PYVENV_MARKER_CACHE.get(dir);
|
|
2117
|
+
if (cached !== void 0) return cached;
|
|
2118
|
+
try {
|
|
2119
|
+
const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
|
|
2120
|
+
const ok = stat.isFile();
|
|
2121
|
+
PYVENV_MARKER_CACHE.set(dir, ok);
|
|
2122
|
+
return ok;
|
|
2123
|
+
} catch {
|
|
2124
|
+
PYVENV_MARKER_CACHE.set(dir, false);
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2039
2128
|
function isConfigFile(name) {
|
|
2040
2129
|
const ext = import_node_path4.default.extname(name);
|
|
2041
2130
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
@@ -2384,6 +2473,7 @@ async function walkDirs(start, scanPath, options, visit) {
|
|
|
2384
2473
|
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2385
2474
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2386
2475
|
}
|
|
2476
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2387
2477
|
await visit(child);
|
|
2388
2478
|
await recurse(child, depth + 1);
|
|
2389
2479
|
}
|
|
@@ -2419,6 +2509,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2419
2509
|
}
|
|
2420
2510
|
return [...found];
|
|
2421
2511
|
}
|
|
2512
|
+
function detectJsFramework(pkg) {
|
|
2513
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2514
|
+
if (deps["next"] !== void 0) return "next";
|
|
2515
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2516
|
+
for (const k of Object.keys(deps)) {
|
|
2517
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2518
|
+
}
|
|
2519
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2520
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2521
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2522
|
+
return void 0;
|
|
2523
|
+
}
|
|
2422
2524
|
async function discoverNodeService(scanPath, dir) {
|
|
2423
2525
|
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2424
2526
|
if (!await exists(pkgPath)) return null;
|
|
@@ -2430,6 +2532,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2430
2532
|
return null;
|
|
2431
2533
|
}
|
|
2432
2534
|
if (!pkg.name) return null;
|
|
2535
|
+
const framework = detectJsFramework(pkg);
|
|
2433
2536
|
const node = {
|
|
2434
2537
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
2435
2538
|
type: import_types5.NodeType.ServiceNode,
|
|
@@ -2438,7 +2541,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2438
2541
|
version: pkg.version,
|
|
2439
2542
|
dependencies: pkg.dependencies ?? {},
|
|
2440
2543
|
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2441
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2544
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2545
|
+
...framework ? { framework } : {}
|
|
2442
2546
|
};
|
|
2443
2547
|
return { pkg, dir, node };
|
|
2444
2548
|
}
|
|
@@ -2648,7 +2752,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
|
2648
2752
|
for (const entry2 of entries) {
|
|
2649
2753
|
if (entry2.isDirectory()) {
|
|
2650
2754
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2651
|
-
|
|
2755
|
+
const child = import_node_path9.default.join(start, entry2.name);
|
|
2756
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2757
|
+
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
2652
2758
|
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
|
|
2653
2759
|
out.push(import_node_path9.default.join(start, entry2.name));
|
|
2654
2760
|
}
|
|
@@ -3330,7 +3436,9 @@ async function walkConfigFiles(dir) {
|
|
|
3330
3436
|
for (const entry2 of entries) {
|
|
3331
3437
|
const full = import_node_path18.default.join(current, entry2.name);
|
|
3332
3438
|
if (entry2.isDirectory()) {
|
|
3333
|
-
if (
|
|
3439
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3440
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3441
|
+
await walk(full);
|
|
3334
3442
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
3335
3443
|
out.push(full);
|
|
3336
3444
|
}
|
|
@@ -3398,7 +3506,9 @@ async function walkSourceFiles(dir) {
|
|
|
3398
3506
|
for (const entry2 of entries) {
|
|
3399
3507
|
const full = import_node_path19.default.join(current, entry2.name);
|
|
3400
3508
|
if (entry2.isDirectory()) {
|
|
3401
|
-
if (
|
|
3509
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3510
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3511
|
+
await walk(full);
|
|
3402
3512
|
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
|
|
3403
3513
|
out.push(full);
|
|
3404
3514
|
}
|
|
@@ -4034,7 +4144,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4034
4144
|
for (const entry2 of entries) {
|
|
4035
4145
|
if (entry2.isDirectory()) {
|
|
4036
4146
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
4037
|
-
|
|
4147
|
+
const child = import_node_path27.default.join(start, entry2.name);
|
|
4148
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4149
|
+
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
4038
4150
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
4039
4151
|
out.push(import_node_path27.default.join(start, entry2.name));
|
|
4040
4152
|
}
|
|
@@ -4082,7 +4194,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
4082
4194
|
for (const entry2 of entries) {
|
|
4083
4195
|
if (entry2.isDirectory()) {
|
|
4084
4196
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
4085
|
-
|
|
4197
|
+
const child = import_node_path28.default.join(start, entry2.name);
|
|
4198
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4199
|
+
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
4086
4200
|
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
|
|
4087
4201
|
out.push(import_node_path28.default.join(start, entry2.name));
|
|
4088
4202
|
}
|
|
@@ -4470,7 +4584,7 @@ init_cjs_shims();
|
|
|
4470
4584
|
var import_node_fs18 = require("fs");
|
|
4471
4585
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4472
4586
|
var import_types20 = require("@neat.is/types");
|
|
4473
|
-
var SCHEMA_VERSION =
|
|
4587
|
+
var SCHEMA_VERSION = 4;
|
|
4474
4588
|
function migrateV1ToV2(payload) {
|
|
4475
4589
|
const nodes = payload.graph.nodes;
|
|
4476
4590
|
if (Array.isArray(nodes)) {
|
|
@@ -4482,6 +4596,9 @@ function migrateV1ToV2(payload) {
|
|
|
4482
4596
|
}
|
|
4483
4597
|
return { ...payload, schemaVersion: 2 };
|
|
4484
4598
|
}
|
|
4599
|
+
function migrateV3ToV4(payload) {
|
|
4600
|
+
return { ...payload, schemaVersion: 4 };
|
|
4601
|
+
}
|
|
4485
4602
|
function migrateV2ToV3(payload) {
|
|
4486
4603
|
const edges = payload.graph.edges;
|
|
4487
4604
|
if (Array.isArray(edges)) {
|
|
@@ -4530,6 +4647,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4530
4647
|
if (payload.schemaVersion === 2) {
|
|
4531
4648
|
payload = migrateV2ToV3(payload);
|
|
4532
4649
|
}
|
|
4650
|
+
if (payload.schemaVersion === 3) {
|
|
4651
|
+
payload = migrateV3ToV4(payload);
|
|
4652
|
+
}
|
|
4533
4653
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4534
4654
|
throw new Error(
|
|
4535
4655
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -5076,10 +5196,19 @@ function projectFromReq(req) {
|
|
|
5076
5196
|
const params = req.params;
|
|
5077
5197
|
return params.project ?? DEFAULT_PROJECT;
|
|
5078
5198
|
}
|
|
5079
|
-
function resolveProject(registry, req, reply) {
|
|
5199
|
+
function resolveProject(registry, req, reply, bootstrap) {
|
|
5080
5200
|
const name = projectFromReq(req);
|
|
5081
5201
|
const ctx = registry.get(name);
|
|
5082
5202
|
if (!ctx) {
|
|
5203
|
+
const phase = bootstrap?.status(name);
|
|
5204
|
+
if (phase === "bootstrapping") {
|
|
5205
|
+
void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
|
|
5206
|
+
return null;
|
|
5207
|
+
}
|
|
5208
|
+
if (phase === "broken") {
|
|
5209
|
+
void reply.code(503).send({ ready: false, project: name, status: "broken" });
|
|
5210
|
+
return null;
|
|
5211
|
+
}
|
|
5083
5212
|
void reply.code(404).send({ error: "project not found", project: name });
|
|
5084
5213
|
return null;
|
|
5085
5214
|
}
|
|
@@ -5109,36 +5238,38 @@ function buildLegacyRegistry(opts) {
|
|
|
5109
5238
|
function registerRoutes(scope, ctx) {
|
|
5110
5239
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
5111
5240
|
scope.get("/events", (req, reply) => {
|
|
5112
|
-
const proj = resolveProject(registry, req, reply);
|
|
5241
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5113
5242
|
if (!proj) return;
|
|
5114
5243
|
handleSse(req, reply, { project: proj.name });
|
|
5115
5244
|
});
|
|
5116
|
-
scope
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5245
|
+
if (ctx.scope === "project") {
|
|
5246
|
+
scope.get("/health", async (req, reply) => {
|
|
5247
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5248
|
+
if (!proj) return;
|
|
5249
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5250
|
+
return {
|
|
5251
|
+
ok: true,
|
|
5252
|
+
project: proj.name,
|
|
5253
|
+
uptimeMs,
|
|
5254
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
5255
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
5256
|
+
// the canonical triple and lets the extras pass through.
|
|
5257
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
5258
|
+
nodeCount: proj.graph.order,
|
|
5259
|
+
edgeCount: proj.graph.size,
|
|
5260
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
5261
|
+
};
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5133
5264
|
scope.get("/graph", async (req, reply) => {
|
|
5134
|
-
const proj = resolveProject(registry, req, reply);
|
|
5265
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5135
5266
|
if (!proj) return;
|
|
5136
5267
|
return serializeGraph(proj.graph);
|
|
5137
5268
|
});
|
|
5138
5269
|
scope.get(
|
|
5139
5270
|
"/graph/node/:id",
|
|
5140
5271
|
async (req, reply) => {
|
|
5141
|
-
const proj = resolveProject(registry, req, reply);
|
|
5272
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5142
5273
|
if (!proj) return;
|
|
5143
5274
|
const { id } = req.params;
|
|
5144
5275
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -5150,7 +5281,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5150
5281
|
scope.get(
|
|
5151
5282
|
"/graph/edges/:id",
|
|
5152
5283
|
async (req, reply) => {
|
|
5153
|
-
const proj = resolveProject(registry, req, reply);
|
|
5284
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5154
5285
|
if (!proj) return;
|
|
5155
5286
|
const { id } = req.params;
|
|
5156
5287
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -5162,7 +5293,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5162
5293
|
}
|
|
5163
5294
|
);
|
|
5164
5295
|
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
5165
|
-
const proj = resolveProject(registry, req, reply);
|
|
5296
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5166
5297
|
if (!proj) return;
|
|
5167
5298
|
const { nodeId } = req.params;
|
|
5168
5299
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5177,7 +5308,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5177
5308
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
5178
5309
|
});
|
|
5179
5310
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
5180
|
-
const proj = resolveProject(registry, req, reply);
|
|
5311
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5181
5312
|
if (!proj) return;
|
|
5182
5313
|
let typeFilter;
|
|
5183
5314
|
if (req.query.type) {
|
|
@@ -5212,7 +5343,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5212
5343
|
});
|
|
5213
5344
|
});
|
|
5214
5345
|
scope.get("/incidents", async (req, reply) => {
|
|
5215
|
-
const proj = resolveProject(registry, req, reply);
|
|
5346
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5216
5347
|
if (!proj) return;
|
|
5217
5348
|
const epath = errorsPathFor(proj);
|
|
5218
5349
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -5224,7 +5355,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5224
5355
|
return { count: sliced.length, total, events: sliced };
|
|
5225
5356
|
});
|
|
5226
5357
|
scope.get("/stale-events", async (req, reply) => {
|
|
5227
|
-
const proj = resolveProject(registry, req, reply);
|
|
5358
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5228
5359
|
if (!proj) return;
|
|
5229
5360
|
const spath = staleEventsPathFor(proj);
|
|
5230
5361
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -5239,7 +5370,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5239
5370
|
scope.get(
|
|
5240
5371
|
"/incidents/:nodeId",
|
|
5241
5372
|
async (req, reply) => {
|
|
5242
|
-
const proj = resolveProject(registry, req, reply);
|
|
5373
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5243
5374
|
if (!proj) return;
|
|
5244
5375
|
const { nodeId } = req.params;
|
|
5245
5376
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5255,7 +5386,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5255
5386
|
}
|
|
5256
5387
|
);
|
|
5257
5388
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
5258
|
-
const proj = resolveProject(registry, req, reply);
|
|
5389
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5259
5390
|
if (!proj) return;
|
|
5260
5391
|
const { nodeId } = req.params;
|
|
5261
5392
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5275,7 +5406,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5275
5406
|
return result;
|
|
5276
5407
|
});
|
|
5277
5408
|
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
5278
|
-
const proj = resolveProject(registry, req, reply);
|
|
5409
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5279
5410
|
if (!proj) return;
|
|
5280
5411
|
const { nodeId } = req.params;
|
|
5281
5412
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5288,7 +5419,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5288
5419
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
5289
5420
|
});
|
|
5290
5421
|
scope.get("/search", async (req, reply) => {
|
|
5291
|
-
const proj = resolveProject(registry, req, reply);
|
|
5422
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5292
5423
|
if (!proj) return;
|
|
5293
5424
|
const raw = (req.query.q ?? "").trim();
|
|
5294
5425
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -5319,7 +5450,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5319
5450
|
scope.get(
|
|
5320
5451
|
"/graph/diff",
|
|
5321
5452
|
async (req, reply) => {
|
|
5322
|
-
const proj = resolveProject(registry, req, reply);
|
|
5453
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5323
5454
|
if (!proj) return;
|
|
5324
5455
|
const against = req.query.against;
|
|
5325
5456
|
if (!against) {
|
|
@@ -5333,8 +5464,37 @@ function registerRoutes(scope, ctx) {
|
|
|
5333
5464
|
}
|
|
5334
5465
|
}
|
|
5335
5466
|
);
|
|
5467
|
+
scope.post("/snapshot", async (req, reply) => {
|
|
5468
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5469
|
+
if (!proj) return;
|
|
5470
|
+
const body = req.body;
|
|
5471
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
5472
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
5473
|
+
}
|
|
5474
|
+
const snap = body.snapshot;
|
|
5475
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
5476
|
+
return reply.code(400).send({
|
|
5477
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
try {
|
|
5481
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
5482
|
+
return {
|
|
5483
|
+
project: proj.name,
|
|
5484
|
+
nodesAdded: result.nodesAdded,
|
|
5485
|
+
edgesAdded: result.edgesAdded,
|
|
5486
|
+
nodeCount: proj.graph.order,
|
|
5487
|
+
edgeCount: proj.graph.size
|
|
5488
|
+
};
|
|
5489
|
+
} catch (err) {
|
|
5490
|
+
return reply.code(400).send({
|
|
5491
|
+
error: "snapshot merge failed",
|
|
5492
|
+
details: err.message
|
|
5493
|
+
});
|
|
5494
|
+
}
|
|
5495
|
+
});
|
|
5336
5496
|
scope.post("/graph/scan", async (req, reply) => {
|
|
5337
|
-
const proj = resolveProject(registry, req, reply);
|
|
5497
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5338
5498
|
if (!proj) return;
|
|
5339
5499
|
if (!proj.scanPath) {
|
|
5340
5500
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -5350,7 +5510,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5350
5510
|
};
|
|
5351
5511
|
});
|
|
5352
5512
|
scope.get("/policies", async (req, reply) => {
|
|
5353
|
-
const proj = resolveProject(registry, req, reply);
|
|
5513
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5354
5514
|
if (!proj) return;
|
|
5355
5515
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
5356
5516
|
if (!policyPath) {
|
|
@@ -5367,7 +5527,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5367
5527
|
}
|
|
5368
5528
|
});
|
|
5369
5529
|
scope.get("/policies/violations", async (req, reply) => {
|
|
5370
|
-
const proj = resolveProject(registry, req, reply);
|
|
5530
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5371
5531
|
if (!proj) return;
|
|
5372
5532
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5373
5533
|
let violations = await log.readAll();
|
|
@@ -5387,7 +5547,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5387
5547
|
return { violations };
|
|
5388
5548
|
});
|
|
5389
5549
|
scope.post("/policies/check", async (req, reply) => {
|
|
5390
|
-
const proj = resolveProject(registry, req, reply);
|
|
5550
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5391
5551
|
if (!proj) return;
|
|
5392
5552
|
const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5393
5553
|
if (!parsed.success) {
|
|
@@ -5426,7 +5586,15 @@ function registerRoutes(scope, ctx) {
|
|
|
5426
5586
|
async function buildApi(opts) {
|
|
5427
5587
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5428
5588
|
await app.register(import_cors.default, { origin: true });
|
|
5429
|
-
mountBearerAuth(app, {
|
|
5589
|
+
mountBearerAuth(app, {
|
|
5590
|
+
token: opts.authToken,
|
|
5591
|
+
trustProxy: opts.trustProxy,
|
|
5592
|
+
publicRead: opts.publicRead
|
|
5593
|
+
});
|
|
5594
|
+
app.get("/api/config", async () => ({
|
|
5595
|
+
publicRead: opts.publicRead === true,
|
|
5596
|
+
authProxy: opts.trustProxy === true
|
|
5597
|
+
}));
|
|
5430
5598
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5431
5599
|
const registry = buildLegacyRegistry(opts);
|
|
5432
5600
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5450,10 +5618,36 @@ async function buildApi(opts) {
|
|
|
5450
5618
|
const routeCtx = {
|
|
5451
5619
|
registry,
|
|
5452
5620
|
startedAt,
|
|
5621
|
+
scope: "root",
|
|
5453
5622
|
errorsPathFor,
|
|
5454
5623
|
staleEventsPathFor,
|
|
5455
|
-
policyFilePathFor
|
|
5624
|
+
policyFilePathFor,
|
|
5625
|
+
bootstrap: opts.bootstrap
|
|
5456
5626
|
};
|
|
5627
|
+
app.get("/health", async () => {
|
|
5628
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5629
|
+
const bootstrapList = opts.bootstrap?.list() ?? [];
|
|
5630
|
+
const byName = new Map(bootstrapList.map((p) => [p.name, p]));
|
|
5631
|
+
const names = /* @__PURE__ */ new Set([
|
|
5632
|
+
...registry.list(),
|
|
5633
|
+
...bootstrapList.map((p) => p.name)
|
|
5634
|
+
]);
|
|
5635
|
+
const projects = [...names].sort().map((name) => {
|
|
5636
|
+
const proj = registry.get(name);
|
|
5637
|
+
const tracked = byName.get(name);
|
|
5638
|
+
return {
|
|
5639
|
+
name,
|
|
5640
|
+
nodeCount: proj?.graph.order ?? 0,
|
|
5641
|
+
edgeCount: proj?.graph.size ?? 0,
|
|
5642
|
+
...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
|
|
5643
|
+
};
|
|
5644
|
+
});
|
|
5645
|
+
return {
|
|
5646
|
+
ok: true,
|
|
5647
|
+
uptimeMs,
|
|
5648
|
+
projects
|
|
5649
|
+
};
|
|
5650
|
+
});
|
|
5457
5651
|
app.get("/projects", async (_req, reply) => {
|
|
5458
5652
|
try {
|
|
5459
5653
|
return await listProjects();
|
|
@@ -5481,7 +5675,7 @@ async function buildApi(opts) {
|
|
|
5481
5675
|
registerRoutes(app, routeCtx);
|
|
5482
5676
|
await app.register(
|
|
5483
5677
|
async (scope) => {
|
|
5484
|
-
registerRoutes(scope, routeCtx);
|
|
5678
|
+
registerRoutes(scope, { ...routeCtx, scope: "project" });
|
|
5485
5679
|
},
|
|
5486
5680
|
{ prefix: "/projects/:project" }
|
|
5487
5681
|
);
|
|
@@ -5864,6 +6058,14 @@ var IGNORED_WATCH_GLOBS = [
|
|
|
5864
6058
|
"**/.turbo/**",
|
|
5865
6059
|
"**/.next/**",
|
|
5866
6060
|
"**/neat-out/**",
|
|
6061
|
+
// Python venv shapes (issue #344). chokidar opens one watch handle per
|
|
6062
|
+
// descended dir; a CPython venv carries 20k+ files and trivially blows
|
|
6063
|
+
// through the macOS kqueue cap before extraction even runs.
|
|
6064
|
+
"**/.venv/**",
|
|
6065
|
+
"**/venv/**",
|
|
6066
|
+
"**/__pypackages__/**",
|
|
6067
|
+
"**/.tox/**",
|
|
6068
|
+
"**/site-packages/**",
|
|
5867
6069
|
"**/.DS_Store"
|
|
5868
6070
|
];
|
|
5869
6071
|
var IGNORED_WATCH_PATHS = [
|
|
@@ -5874,6 +6076,11 @@ var IGNORED_WATCH_PATHS = [
|
|
|
5874
6076
|
/(?:^|[\\/])\.turbo[\\/]/,
|
|
5875
6077
|
/(?:^|[\\/])\.next[\\/]/,
|
|
5876
6078
|
/(?:^|[\\/])neat-out[\\/]/,
|
|
6079
|
+
/(?:^|[\\/])\.venv[\\/]/,
|
|
6080
|
+
/(?:^|[\\/])venv[\\/]/,
|
|
6081
|
+
/(?:^|[\\/])__pypackages__[\\/]/,
|
|
6082
|
+
/(?:^|[\\/])\.tox[\\/]/,
|
|
6083
|
+
/(?:^|[\\/])site-packages[\\/]/,
|
|
5877
6084
|
/[\\/]?\.DS_Store$/
|
|
5878
6085
|
];
|
|
5879
6086
|
function shouldIgnore(absPath) {
|
|
@@ -6311,10 +6518,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
|
|
|
6311
6518
|
|
|
6312
6519
|
await import('@opentelemetry/auto-instrumentations-node/register')
|
|
6313
6520
|
`;
|
|
6314
|
-
function renderEnvNeat(
|
|
6521
|
+
function renderEnvNeat(projectName) {
|
|
6315
6522
|
return [
|
|
6316
6523
|
"# Generated by `neat init --apply` (ADR-069).",
|
|
6317
|
-
`OTEL_SERVICE_NAME=${
|
|
6524
|
+
`OTEL_SERVICE_NAME=${projectName}`,
|
|
6318
6525
|
"OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
|
|
6319
6526
|
""
|
|
6320
6527
|
].join("\n");
|
|
@@ -6370,6 +6577,100 @@ const sdk = new NodeSDK({
|
|
|
6370
6577
|
})
|
|
6371
6578
|
sdk.start()
|
|
6372
6579
|
`;
|
|
6580
|
+
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
6581
|
+
var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6582
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6583
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6584
|
+
import { fileURLToPath } from 'node:url'
|
|
6585
|
+
import path from 'node:path'
|
|
6586
|
+
import dotenv from 'dotenv'
|
|
6587
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6588
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6589
|
+
|
|
6590
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6591
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6592
|
+
|
|
6593
|
+
const sdk = new NodeSDK({
|
|
6594
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6595
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6596
|
+
})
|
|
6597
|
+
sdk.start()
|
|
6598
|
+
`;
|
|
6599
|
+
var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6600
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6601
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6602
|
+
const path = require('node:path')
|
|
6603
|
+
try {
|
|
6604
|
+
require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
|
|
6605
|
+
} catch (err) {
|
|
6606
|
+
// dotenv unavailable \u2014 fall through to process.env.
|
|
6607
|
+
}
|
|
6608
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
6609
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
6610
|
+
|
|
6611
|
+
const sdk = new NodeSDK({
|
|
6612
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6613
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6614
|
+
})
|
|
6615
|
+
sdk.start()
|
|
6616
|
+
`;
|
|
6617
|
+
var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6618
|
+
var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6619
|
+
var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6620
|
+
var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6621
|
+
var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6622
|
+
import './otel-init'
|
|
6623
|
+
|
|
6624
|
+
import type { Handle } from '@sveltejs/kit'
|
|
6625
|
+
|
|
6626
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
6627
|
+
return resolve(event)
|
|
6628
|
+
}
|
|
6629
|
+
`;
|
|
6630
|
+
var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6631
|
+
import './otel-init'
|
|
6632
|
+
|
|
6633
|
+
/** @type {import('@sveltejs/kit').Handle} */
|
|
6634
|
+
export const handle = async ({ event, resolve }) => {
|
|
6635
|
+
return resolve(event)
|
|
6636
|
+
}
|
|
6637
|
+
`;
|
|
6638
|
+
var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6639
|
+
var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6640
|
+
var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6641
|
+
import './otel-init'
|
|
6642
|
+
|
|
6643
|
+
export default defineNitroPlugin(() => {
|
|
6644
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
6645
|
+
})
|
|
6646
|
+
`;
|
|
6647
|
+
var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6648
|
+
require('./otel-init')
|
|
6649
|
+
|
|
6650
|
+
module.exports = defineNitroPlugin(() => {
|
|
6651
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
6652
|
+
})
|
|
6653
|
+
`;
|
|
6654
|
+
var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
6655
|
+
var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
6656
|
+
var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6657
|
+
import './otel-init'
|
|
6658
|
+
|
|
6659
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
6660
|
+
|
|
6661
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
6662
|
+
return next()
|
|
6663
|
+
})
|
|
6664
|
+
`;
|
|
6665
|
+
var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
6666
|
+
import './otel-init'
|
|
6667
|
+
|
|
6668
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
6669
|
+
|
|
6670
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
6671
|
+
return next()
|
|
6672
|
+
})
|
|
6673
|
+
`;
|
|
6373
6674
|
|
|
6374
6675
|
// src/installers/javascript.ts
|
|
6375
6676
|
var SDK_PACKAGES = [
|
|
@@ -6390,6 +6691,10 @@ var OTEL_ENV = {
|
|
|
6390
6691
|
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
6391
6692
|
value: "http://localhost:4318"
|
|
6392
6693
|
};
|
|
6694
|
+
function envServiceName(pkg, serviceDir, project) {
|
|
6695
|
+
if (project && project.length > 0) return project;
|
|
6696
|
+
return pkg.name ?? import_node_path40.default.basename(serviceDir);
|
|
6697
|
+
}
|
|
6393
6698
|
async function readPackageJson(serviceDir) {
|
|
6394
6699
|
try {
|
|
6395
6700
|
const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
|
|
@@ -6421,6 +6726,71 @@ async function findNextConfig(serviceDir) {
|
|
|
6421
6726
|
function hasNextDependency(pkg) {
|
|
6422
6727
|
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
6423
6728
|
}
|
|
6729
|
+
function allDeps(pkg) {
|
|
6730
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6731
|
+
}
|
|
6732
|
+
var REMIX_ENTRY_CANDIDATES = [
|
|
6733
|
+
"app/entry.server.ts",
|
|
6734
|
+
"app/entry.server.tsx",
|
|
6735
|
+
"app/entry.server.js",
|
|
6736
|
+
"app/entry.server.jsx"
|
|
6737
|
+
];
|
|
6738
|
+
function hasRemixDependency(pkg) {
|
|
6739
|
+
const deps = allDeps(pkg);
|
|
6740
|
+
if ("remix" in deps) return true;
|
|
6741
|
+
for (const name of Object.keys(deps)) {
|
|
6742
|
+
if (name.startsWith("@remix-run/")) return true;
|
|
6743
|
+
}
|
|
6744
|
+
return false;
|
|
6745
|
+
}
|
|
6746
|
+
async function findRemixEntry(serviceDir) {
|
|
6747
|
+
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
6748
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6749
|
+
if (await exists2(candidate)) return candidate;
|
|
6750
|
+
}
|
|
6751
|
+
return null;
|
|
6752
|
+
}
|
|
6753
|
+
var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
|
|
6754
|
+
var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
|
|
6755
|
+
function hasSvelteKitDependency(pkg) {
|
|
6756
|
+
return "@sveltejs/kit" in allDeps(pkg);
|
|
6757
|
+
}
|
|
6758
|
+
async function findSvelteKitHooks(serviceDir) {
|
|
6759
|
+
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
6760
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6761
|
+
if (await exists2(candidate)) return candidate;
|
|
6762
|
+
}
|
|
6763
|
+
return null;
|
|
6764
|
+
}
|
|
6765
|
+
async function findSvelteKitConfig(serviceDir) {
|
|
6766
|
+
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
6767
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6768
|
+
if (await exists2(candidate)) return candidate;
|
|
6769
|
+
}
|
|
6770
|
+
return null;
|
|
6771
|
+
}
|
|
6772
|
+
var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
6773
|
+
function hasNuxtDependency(pkg) {
|
|
6774
|
+
return "nuxt" in allDeps(pkg);
|
|
6775
|
+
}
|
|
6776
|
+
async function findNuxtConfig(serviceDir) {
|
|
6777
|
+
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
6778
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6779
|
+
if (await exists2(candidate)) return candidate;
|
|
6780
|
+
}
|
|
6781
|
+
return null;
|
|
6782
|
+
}
|
|
6783
|
+
var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
|
|
6784
|
+
function hasAstroDependency(pkg) {
|
|
6785
|
+
return "astro" in allDeps(pkg);
|
|
6786
|
+
}
|
|
6787
|
+
async function findAstroConfig(serviceDir) {
|
|
6788
|
+
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
6789
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6790
|
+
if (await exists2(candidate)) return candidate;
|
|
6791
|
+
}
|
|
6792
|
+
return null;
|
|
6793
|
+
}
|
|
6424
6794
|
function parseNextMajor(range) {
|
|
6425
6795
|
if (!range) return null;
|
|
6426
6796
|
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
@@ -6549,7 +6919,7 @@ function lineIsOtelInjection(line) {
|
|
|
6549
6919
|
if (trimmed.length === 0) return false;
|
|
6550
6920
|
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
6551
6921
|
}
|
|
6552
|
-
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
6922
|
+
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
|
|
6553
6923
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
6554
6924
|
const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
6555
6925
|
const instrumentationNodeFile = import_node_path40.default.join(
|
|
@@ -6586,7 +6956,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
6586
6956
|
if (!await exists2(envNeatFile)) {
|
|
6587
6957
|
generatedFiles.push({
|
|
6588
6958
|
file: envNeatFile,
|
|
6589
|
-
contents: renderEnvNeat(pkg
|
|
6959
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
6590
6960
|
skipIfExists: true
|
|
6591
6961
|
});
|
|
6592
6962
|
}
|
|
@@ -6628,9 +6998,280 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
6628
6998
|
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
6629
6999
|
};
|
|
6630
7000
|
}
|
|
6631
|
-
|
|
7001
|
+
function buildDependencyEdits(pkg, manifestPath) {
|
|
7002
|
+
const existingDeps = allDeps(pkg);
|
|
7003
|
+
const edits = [];
|
|
7004
|
+
for (const sdk of SDK_PACKAGES) {
|
|
7005
|
+
if (sdk.name in existingDeps) continue;
|
|
7006
|
+
edits.push({
|
|
7007
|
+
file: manifestPath,
|
|
7008
|
+
kind: "add",
|
|
7009
|
+
name: sdk.name,
|
|
7010
|
+
version: sdk.version
|
|
7011
|
+
});
|
|
7012
|
+
}
|
|
7013
|
+
return edits;
|
|
7014
|
+
}
|
|
7015
|
+
async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
7016
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
7017
|
+
if (!await exists2(envNeatFile)) {
|
|
7018
|
+
generatedFiles.push({
|
|
7019
|
+
file: envNeatFile,
|
|
7020
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
7021
|
+
skipIfExists: true
|
|
7022
|
+
});
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
function fileImportsOtelHook(raw, specifiers) {
|
|
7026
|
+
const lines = raw.split(/\r?\n/);
|
|
7027
|
+
for (const line of lines) {
|
|
7028
|
+
const trimmed = line.trim();
|
|
7029
|
+
for (const spec of specifiers) {
|
|
7030
|
+
const escaped = spec.replace(/\./g, "\\.");
|
|
7031
|
+
const pattern = new RegExp(
|
|
7032
|
+
`(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
|
|
7033
|
+
);
|
|
7034
|
+
if (pattern.test(trimmed)) return true;
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
return false;
|
|
7038
|
+
}
|
|
7039
|
+
async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
7040
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7041
|
+
const otelServerFile = import_node_path40.default.join(
|
|
7042
|
+
serviceDir,
|
|
7043
|
+
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
7044
|
+
);
|
|
7045
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7046
|
+
const generatedFiles = [];
|
|
7047
|
+
if (!await exists2(otelServerFile)) {
|
|
7048
|
+
generatedFiles.push({
|
|
7049
|
+
file: otelServerFile,
|
|
7050
|
+
contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
|
|
7051
|
+
skipIfExists: true
|
|
7052
|
+
});
|
|
7053
|
+
}
|
|
7054
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
7055
|
+
const entrypointEdits = [];
|
|
7056
|
+
try {
|
|
7057
|
+
const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
|
|
7058
|
+
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
7059
|
+
const lines = raw.split(/\r?\n/);
|
|
7060
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
7061
|
+
entrypointEdits.push({
|
|
7062
|
+
file: entryFile,
|
|
7063
|
+
before: firstReal,
|
|
7064
|
+
after: "import './otel.server'"
|
|
7065
|
+
});
|
|
7066
|
+
}
|
|
7067
|
+
} catch {
|
|
7068
|
+
}
|
|
7069
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
7070
|
+
if (empty) {
|
|
7071
|
+
return {
|
|
7072
|
+
language: "javascript",
|
|
7073
|
+
serviceDir,
|
|
7074
|
+
dependencyEdits: [],
|
|
7075
|
+
entrypointEdits: [],
|
|
7076
|
+
envEdits: [],
|
|
7077
|
+
generatedFiles: [],
|
|
7078
|
+
framework: "remix"
|
|
7079
|
+
};
|
|
7080
|
+
}
|
|
7081
|
+
return {
|
|
7082
|
+
language: "javascript",
|
|
7083
|
+
serviceDir,
|
|
7084
|
+
dependencyEdits,
|
|
7085
|
+
entrypointEdits,
|
|
7086
|
+
envEdits: [OTEL_ENV],
|
|
7087
|
+
generatedFiles,
|
|
7088
|
+
framework: "remix"
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
7091
|
+
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
|
|
7092
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7093
|
+
const otelInitFile = import_node_path40.default.join(
|
|
7094
|
+
serviceDir,
|
|
7095
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
7096
|
+
);
|
|
7097
|
+
const resolvedHooksFile = hooksFile ?? import_node_path40.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
7098
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7099
|
+
const generatedFiles = [];
|
|
7100
|
+
const entrypointEdits = [];
|
|
7101
|
+
if (!await exists2(otelInitFile)) {
|
|
7102
|
+
generatedFiles.push({
|
|
7103
|
+
file: otelInitFile,
|
|
7104
|
+
contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
|
|
7105
|
+
skipIfExists: true
|
|
7106
|
+
});
|
|
7107
|
+
}
|
|
7108
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
7109
|
+
if (hooksFile === null) {
|
|
7110
|
+
generatedFiles.push({
|
|
7111
|
+
file: resolvedHooksFile,
|
|
7112
|
+
contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
|
|
7113
|
+
skipIfExists: true
|
|
7114
|
+
});
|
|
7115
|
+
} else {
|
|
7116
|
+
try {
|
|
7117
|
+
const raw = await import_node_fs25.promises.readFile(hooksFile, "utf8");
|
|
7118
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
7119
|
+
const lines = raw.split(/\r?\n/);
|
|
7120
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
7121
|
+
entrypointEdits.push({
|
|
7122
|
+
file: hooksFile,
|
|
7123
|
+
before: firstReal,
|
|
7124
|
+
after: "import './otel-init'"
|
|
7125
|
+
});
|
|
7126
|
+
}
|
|
7127
|
+
} catch {
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
7131
|
+
if (empty) {
|
|
7132
|
+
return {
|
|
7133
|
+
language: "javascript",
|
|
7134
|
+
serviceDir,
|
|
7135
|
+
dependencyEdits: [],
|
|
7136
|
+
entrypointEdits: [],
|
|
7137
|
+
envEdits: [],
|
|
7138
|
+
generatedFiles: [],
|
|
7139
|
+
framework: "sveltekit"
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7142
|
+
return {
|
|
7143
|
+
language: "javascript",
|
|
7144
|
+
serviceDir,
|
|
7145
|
+
dependencyEdits,
|
|
7146
|
+
entrypointEdits,
|
|
7147
|
+
envEdits: [OTEL_ENV],
|
|
7148
|
+
generatedFiles,
|
|
7149
|
+
framework: "sveltekit"
|
|
7150
|
+
};
|
|
7151
|
+
}
|
|
7152
|
+
async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
7153
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7154
|
+
const otelPluginFile = import_node_path40.default.join(
|
|
7155
|
+
serviceDir,
|
|
7156
|
+
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
7157
|
+
);
|
|
7158
|
+
const otelInitFile = import_node_path40.default.join(
|
|
7159
|
+
serviceDir,
|
|
7160
|
+
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
7161
|
+
);
|
|
7162
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7163
|
+
const generatedFiles = [];
|
|
7164
|
+
if (!await exists2(otelInitFile)) {
|
|
7165
|
+
generatedFiles.push({
|
|
7166
|
+
file: otelInitFile,
|
|
7167
|
+
contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
|
|
7168
|
+
skipIfExists: true
|
|
7169
|
+
});
|
|
7170
|
+
}
|
|
7171
|
+
if (!await exists2(otelPluginFile)) {
|
|
7172
|
+
generatedFiles.push({
|
|
7173
|
+
file: otelPluginFile,
|
|
7174
|
+
contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
|
|
7175
|
+
skipIfExists: true
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
7179
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
|
|
7180
|
+
if (empty) {
|
|
7181
|
+
return {
|
|
7182
|
+
language: "javascript",
|
|
7183
|
+
serviceDir,
|
|
7184
|
+
dependencyEdits: [],
|
|
7185
|
+
entrypointEdits: [],
|
|
7186
|
+
envEdits: [],
|
|
7187
|
+
generatedFiles: [],
|
|
7188
|
+
framework: "nuxt"
|
|
7189
|
+
};
|
|
7190
|
+
}
|
|
7191
|
+
return {
|
|
7192
|
+
language: "javascript",
|
|
7193
|
+
serviceDir,
|
|
7194
|
+
dependencyEdits,
|
|
7195
|
+
entrypointEdits: [],
|
|
7196
|
+
envEdits: [OTEL_ENV],
|
|
7197
|
+
generatedFiles,
|
|
7198
|
+
framework: "nuxt"
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
7202
|
+
async function findAstroMiddleware(serviceDir) {
|
|
7203
|
+
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
7204
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
7205
|
+
if (await exists2(candidate)) return candidate;
|
|
7206
|
+
}
|
|
7207
|
+
return null;
|
|
7208
|
+
}
|
|
7209
|
+
async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
7210
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
7211
|
+
const otelInitFile = import_node_path40.default.join(
|
|
7212
|
+
serviceDir,
|
|
7213
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
7214
|
+
);
|
|
7215
|
+
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
7216
|
+
const middlewareFile = existingMiddleware ?? import_node_path40.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
7217
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
7218
|
+
const generatedFiles = [];
|
|
7219
|
+
const entrypointEdits = [];
|
|
7220
|
+
if (!await exists2(otelInitFile)) {
|
|
7221
|
+
generatedFiles.push({
|
|
7222
|
+
file: otelInitFile,
|
|
7223
|
+
contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
|
|
7224
|
+
skipIfExists: true
|
|
7225
|
+
});
|
|
7226
|
+
}
|
|
7227
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
7228
|
+
if (existingMiddleware === null) {
|
|
7229
|
+
generatedFiles.push({
|
|
7230
|
+
file: middlewareFile,
|
|
7231
|
+
contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
|
|
7232
|
+
skipIfExists: true
|
|
7233
|
+
});
|
|
7234
|
+
} else {
|
|
7235
|
+
try {
|
|
7236
|
+
const raw = await import_node_fs25.promises.readFile(existingMiddleware, "utf8");
|
|
7237
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
7238
|
+
const lines = raw.split(/\r?\n/);
|
|
7239
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
7240
|
+
entrypointEdits.push({
|
|
7241
|
+
file: existingMiddleware,
|
|
7242
|
+
before: firstReal,
|
|
7243
|
+
after: "import './otel-init'"
|
|
7244
|
+
});
|
|
7245
|
+
}
|
|
7246
|
+
} catch {
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
7250
|
+
if (empty) {
|
|
7251
|
+
return {
|
|
7252
|
+
language: "javascript",
|
|
7253
|
+
serviceDir,
|
|
7254
|
+
dependencyEdits: [],
|
|
7255
|
+
entrypointEdits: [],
|
|
7256
|
+
envEdits: [],
|
|
7257
|
+
generatedFiles: [],
|
|
7258
|
+
framework: "astro"
|
|
7259
|
+
};
|
|
7260
|
+
}
|
|
7261
|
+
return {
|
|
7262
|
+
language: "javascript",
|
|
7263
|
+
serviceDir,
|
|
7264
|
+
dependencyEdits,
|
|
7265
|
+
entrypointEdits,
|
|
7266
|
+
envEdits: [OTEL_ENV],
|
|
7267
|
+
generatedFiles,
|
|
7268
|
+
framework: "astro"
|
|
7269
|
+
};
|
|
7270
|
+
}
|
|
7271
|
+
async function plan(serviceDir, opts) {
|
|
6632
7272
|
const pkg = await readPackageJson(serviceDir);
|
|
6633
7273
|
const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
|
|
7274
|
+
const project = opts?.project;
|
|
6634
7275
|
const empty = {
|
|
6635
7276
|
language: "javascript",
|
|
6636
7277
|
serviceDir,
|
|
@@ -6643,7 +7284,32 @@ async function plan(serviceDir) {
|
|
|
6643
7284
|
if (hasNextDependency(pkg)) {
|
|
6644
7285
|
const nextConfig = await findNextConfig(serviceDir);
|
|
6645
7286
|
if (nextConfig) {
|
|
6646
|
-
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
7287
|
+
return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
if (hasRemixDependency(pkg)) {
|
|
7291
|
+
const remixEntry = await findRemixEntry(serviceDir);
|
|
7292
|
+
if (remixEntry) {
|
|
7293
|
+
return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
if (hasSvelteKitDependency(pkg)) {
|
|
7297
|
+
const hooks = await findSvelteKitHooks(serviceDir);
|
|
7298
|
+
const config = await findSvelteKitConfig(serviceDir);
|
|
7299
|
+
if (hooks || config) {
|
|
7300
|
+
return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
if (hasNuxtDependency(pkg)) {
|
|
7304
|
+
const nuxtConfig = await findNuxtConfig(serviceDir);
|
|
7305
|
+
if (nuxtConfig) {
|
|
7306
|
+
return planNuxt(serviceDir, pkg, manifestPath, project);
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
if (hasAstroDependency(pkg)) {
|
|
7310
|
+
const astroConfig = await findAstroConfig(serviceDir);
|
|
7311
|
+
if (astroConfig) {
|
|
7312
|
+
return planAstro(serviceDir, pkg, manifestPath, project);
|
|
6647
7313
|
}
|
|
6648
7314
|
}
|
|
6649
7315
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
@@ -6691,7 +7357,7 @@ async function plan(serviceDir) {
|
|
|
6691
7357
|
if (!await exists2(envNeatFile)) {
|
|
6692
7358
|
generatedFiles.push({
|
|
6693
7359
|
file: envNeatFile,
|
|
6694
|
-
contents: renderEnvNeat(pkg
|
|
7360
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
6695
7361
|
skipIfExists: true
|
|
6696
7362
|
});
|
|
6697
7363
|
}
|
|
@@ -6718,9 +7384,18 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
6718
7384
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6719
7385
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6720
7386
|
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
7387
|
+
const relPosix = rel.split(import_node_path40.default.sep).join("/");
|
|
7388
|
+
if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
|
|
7389
|
+
if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
|
|
7390
|
+
if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
|
|
7391
|
+
if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
|
|
7392
|
+
if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
|
|
7393
|
+
if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
|
|
7394
|
+
if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
|
|
6721
7395
|
return false;
|
|
6722
7396
|
}
|
|
6723
7397
|
async function writeAtomic(file, contents) {
|
|
7398
|
+
await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
|
|
6724
7399
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6725
7400
|
await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
|
|
6726
7401
|
await import_node_fs25.promises.rename(tmp, file);
|
|
@@ -7193,6 +7868,49 @@ var import_node_http = __toESM(require("http"), 1);
|
|
|
7193
7868
|
var import_node_path42 = __toESM(require("path"), 1);
|
|
7194
7869
|
var import_node_child_process2 = require("child_process");
|
|
7195
7870
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
7871
|
+
async function extractAndPersist(opts) {
|
|
7872
|
+
const services = await discoverServices(opts.scanPath);
|
|
7873
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7874
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
7875
|
+
resetGraph(graphKey);
|
|
7876
|
+
const graph = getGraph(graphKey);
|
|
7877
|
+
const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
|
|
7878
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
7879
|
+
errorsPath: projectPaths.errorsPath
|
|
7880
|
+
});
|
|
7881
|
+
if (!opts.dryRun) {
|
|
7882
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
7883
|
+
}
|
|
7884
|
+
return {
|
|
7885
|
+
graph,
|
|
7886
|
+
graphKey,
|
|
7887
|
+
services,
|
|
7888
|
+
languages,
|
|
7889
|
+
nodesAdded: extraction.nodesAdded,
|
|
7890
|
+
edgesAdded: extraction.edgesAdded,
|
|
7891
|
+
snapshotPath: projectPaths.snapshotPath,
|
|
7892
|
+
errorsPath: projectPaths.errorsPath
|
|
7893
|
+
};
|
|
7894
|
+
}
|
|
7895
|
+
async function applyInstallersOver(services, project) {
|
|
7896
|
+
let instrumented = 0;
|
|
7897
|
+
let already = 0;
|
|
7898
|
+
let libOnly = 0;
|
|
7899
|
+
for (const svc of services) {
|
|
7900
|
+
const installer = await pickInstaller(svc.dir);
|
|
7901
|
+
if (!installer) continue;
|
|
7902
|
+
const plan3 = await installer.plan(svc.dir, { project });
|
|
7903
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
7904
|
+
already++;
|
|
7905
|
+
continue;
|
|
7906
|
+
}
|
|
7907
|
+
const outcome = await installer.apply(plan3);
|
|
7908
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
7909
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
7910
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
7911
|
+
}
|
|
7912
|
+
return { instrumented, alreadyInstrumented: already, libOnly };
|
|
7913
|
+
}
|
|
7196
7914
|
async function promptYesNo(question) {
|
|
7197
7915
|
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
7198
7916
|
return new Promise((resolve) => {
|
|
@@ -7203,28 +7921,106 @@ async function promptYesNo(question) {
|
|
|
7203
7921
|
});
|
|
7204
7922
|
});
|
|
7205
7923
|
}
|
|
7206
|
-
var DEFAULT_DAEMON_READY_TIMEOUT_MS =
|
|
7207
|
-
|
|
7924
|
+
var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
|
|
7925
|
+
var PROBE_INTERVAL_MS = 500;
|
|
7926
|
+
async function fetchDaemonHealth(restPort) {
|
|
7208
7927
|
return new Promise((resolve) => {
|
|
7209
7928
|
const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
|
|
7210
7929
|
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
7211
|
-
|
|
7212
|
-
|
|
7930
|
+
if (!ok) {
|
|
7931
|
+
res.resume();
|
|
7932
|
+
resolve(null);
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7935
|
+
let body = "";
|
|
7936
|
+
res.setEncoding("utf8");
|
|
7937
|
+
res.on("data", (chunk) => {
|
|
7938
|
+
body += chunk;
|
|
7939
|
+
});
|
|
7940
|
+
res.on("end", () => {
|
|
7941
|
+
try {
|
|
7942
|
+
resolve(JSON.parse(body));
|
|
7943
|
+
} catch {
|
|
7944
|
+
resolve({ ok: true });
|
|
7945
|
+
}
|
|
7946
|
+
});
|
|
7213
7947
|
});
|
|
7214
|
-
req.on("error", () => resolve(
|
|
7948
|
+
req.on("error", () => resolve(null));
|
|
7215
7949
|
req.setTimeout(1e3, () => {
|
|
7216
7950
|
req.destroy();
|
|
7217
|
-
resolve(
|
|
7951
|
+
resolve(null);
|
|
7218
7952
|
});
|
|
7219
7953
|
});
|
|
7220
7954
|
}
|
|
7955
|
+
async function checkDaemonHealth(restPort) {
|
|
7956
|
+
const body = await fetchDaemonHealth(restPort);
|
|
7957
|
+
return body !== null;
|
|
7958
|
+
}
|
|
7959
|
+
async function probeProjectHealth(restPort, name) {
|
|
7960
|
+
return new Promise((resolve) => {
|
|
7961
|
+
const req = import_node_http.default.get(
|
|
7962
|
+
`http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
|
|
7963
|
+
(res) => {
|
|
7964
|
+
const code = res.statusCode ?? 0;
|
|
7965
|
+
res.resume();
|
|
7966
|
+
if (code >= 200 && code < 300) resolve("active");
|
|
7967
|
+
else resolve("bootstrapping");
|
|
7968
|
+
}
|
|
7969
|
+
);
|
|
7970
|
+
req.on("error", () => resolve("bootstrapping"));
|
|
7971
|
+
req.setTimeout(1e3, () => {
|
|
7972
|
+
req.destroy();
|
|
7973
|
+
resolve("bootstrapping");
|
|
7974
|
+
});
|
|
7975
|
+
});
|
|
7976
|
+
}
|
|
7977
|
+
async function snapshotProjectStatus(restPort, body) {
|
|
7978
|
+
if (body.projects && body.projects.length > 0) {
|
|
7979
|
+
return body.projects.map((p) => ({
|
|
7980
|
+
name: p.name,
|
|
7981
|
+
status: p.status ?? "active"
|
|
7982
|
+
}));
|
|
7983
|
+
}
|
|
7984
|
+
const entries = await listProjects().catch(() => []);
|
|
7985
|
+
if (entries.length === 0) return [];
|
|
7986
|
+
return Promise.all(
|
|
7987
|
+
entries.map(async (entry2) => ({
|
|
7988
|
+
name: entry2.name,
|
|
7989
|
+
status: await probeProjectHealth(restPort, entry2.name)
|
|
7990
|
+
}))
|
|
7991
|
+
);
|
|
7992
|
+
}
|
|
7221
7993
|
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
7222
7994
|
const deadline = Date.now() + timeoutMs;
|
|
7995
|
+
let lastBootstrapping = [];
|
|
7223
7996
|
while (Date.now() < deadline) {
|
|
7224
|
-
|
|
7225
|
-
|
|
7997
|
+
const body = await fetchDaemonHealth(restPort);
|
|
7998
|
+
if (body !== null) {
|
|
7999
|
+
const projects2 = await snapshotProjectStatus(restPort, body);
|
|
8000
|
+
const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
|
|
8001
|
+
const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
|
|
8002
|
+
if (bootstrapping.length === 0) {
|
|
8003
|
+
return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
|
|
8004
|
+
}
|
|
8005
|
+
const key = bootstrapping.slice().sort().join(",");
|
|
8006
|
+
const prevKey = lastBootstrapping.slice().sort().join(",");
|
|
8007
|
+
if (key !== prevKey) {
|
|
8008
|
+
const plural = bootstrapping.length === 1 ? "" : "s";
|
|
8009
|
+
console.log(
|
|
8010
|
+
`neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
|
|
8011
|
+
);
|
|
8012
|
+
lastBootstrapping = bootstrapping;
|
|
8013
|
+
}
|
|
8014
|
+
}
|
|
8015
|
+
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
7226
8016
|
}
|
|
7227
|
-
|
|
8017
|
+
const final = await fetchDaemonHealth(restPort);
|
|
8018
|
+
const projects = final ? await snapshotProjectStatus(restPort, final) : [];
|
|
8019
|
+
return {
|
|
8020
|
+
ready: false,
|
|
8021
|
+
brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
|
|
8022
|
+
stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
|
|
8023
|
+
};
|
|
7228
8024
|
}
|
|
7229
8025
|
function spawnDaemonDetached() {
|
|
7230
8026
|
const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
|
|
@@ -7245,12 +8041,21 @@ function spawnDaemonDetached() {
|
|
|
7245
8041
|
if (!entry2) {
|
|
7246
8042
|
throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
|
|
7247
8043
|
}
|
|
8044
|
+
const env = { ...process.env };
|
|
8045
|
+
const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
|
|
8046
|
+
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
8047
|
+
env.HOST = "127.0.0.1";
|
|
8048
|
+
}
|
|
7248
8049
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
|
|
7249
8050
|
detached: true,
|
|
7250
|
-
|
|
7251
|
-
|
|
8051
|
+
// stderr inherits the orchestrator's fd so the daemon's
|
|
8052
|
+
// `BindAuthorityError` message lands in front of the operator instead
|
|
8053
|
+
// of being swallowed (issue #341).
|
|
8054
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
8055
|
+
env
|
|
7252
8056
|
});
|
|
7253
8057
|
child.unref();
|
|
8058
|
+
return child;
|
|
7254
8059
|
}
|
|
7255
8060
|
function openBrowser(url) {
|
|
7256
8061
|
if (!process.stdout.isTTY) return "failed";
|
|
@@ -7287,24 +8092,21 @@ async function runOrchestrator(opts) {
|
|
|
7287
8092
|
}
|
|
7288
8093
|
console.log(`neat: ${opts.scanPath}`);
|
|
7289
8094
|
console.log("");
|
|
7290
|
-
const
|
|
7291
|
-
|
|
8095
|
+
const persisted = await extractAndPersist({
|
|
8096
|
+
scanPath: opts.scanPath,
|
|
8097
|
+
project: opts.project,
|
|
8098
|
+
projectExplicit: opts.projectExplicit
|
|
8099
|
+
});
|
|
8100
|
+
const { graph, services, languages } = persisted;
|
|
7292
8101
|
result.steps.discovery = { services: services.length, languages };
|
|
7293
8102
|
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
7294
8103
|
let runApply = !opts.noInstrument;
|
|
7295
8104
|
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
7296
8105
|
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
7297
8106
|
}
|
|
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
8107
|
result.steps.extraction = {
|
|
7306
|
-
nodesAdded:
|
|
7307
|
-
edgesAdded:
|
|
8108
|
+
nodesAdded: persisted.nodesAdded,
|
|
8109
|
+
edgesAdded: persisted.edgesAdded
|
|
7308
8110
|
};
|
|
7309
8111
|
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
7310
8112
|
result.steps.gitignore = gi.action;
|
|
@@ -7329,24 +8131,11 @@ async function runOrchestrator(opts) {
|
|
|
7329
8131
|
result.steps.apply.skipped = true;
|
|
7330
8132
|
console.log("skipped instrumentation (--no-instrument)");
|
|
7331
8133
|
} 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}`);
|
|
8134
|
+
const tally = await applyInstallersOver(services, opts.project);
|
|
8135
|
+
result.steps.apply = { ...tally, skipped: false };
|
|
8136
|
+
console.log(
|
|
8137
|
+
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
8138
|
+
);
|
|
7350
8139
|
}
|
|
7351
8140
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
7352
8141
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -7361,12 +8150,25 @@ async function runOrchestrator(opts) {
|
|
|
7361
8150
|
return result;
|
|
7362
8151
|
}
|
|
7363
8152
|
const ready = await waitForDaemonReady(restPort, timeoutMs);
|
|
7364
|
-
result.steps.daemon = ready ? "spawned" : "timed-out";
|
|
7365
|
-
if (!ready) {
|
|
8153
|
+
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
8154
|
+
if (!ready.ready) {
|
|
7366
8155
|
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
8156
|
+
if (ready.stillBootstrapping.length > 0) {
|
|
8157
|
+
console.error(
|
|
8158
|
+
`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
|
|
8159
|
+
);
|
|
8160
|
+
}
|
|
8161
|
+
if (ready.brokenProjects.length > 0) {
|
|
8162
|
+
console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
|
|
8163
|
+
}
|
|
7367
8164
|
result.exitCode = 1;
|
|
7368
8165
|
return result;
|
|
7369
8166
|
}
|
|
8167
|
+
if (ready.brokenProjects.length > 0) {
|
|
8168
|
+
console.warn(
|
|
8169
|
+
`neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
|
|
8170
|
+
);
|
|
8171
|
+
}
|
|
7370
8172
|
}
|
|
7371
8173
|
const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
|
|
7372
8174
|
if (opts.noOpen || !process.stdout.isTTY) {
|
|
@@ -7395,8 +8197,9 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
7395
8197
|
console.log(`dashboard: ${dashboardUrl}`);
|
|
7396
8198
|
}
|
|
7397
8199
|
|
|
7398
|
-
// src/cli.ts
|
|
7399
|
-
|
|
8200
|
+
// src/cli-verbs.ts
|
|
8201
|
+
init_cjs_shims();
|
|
8202
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
7400
8203
|
|
|
7401
8204
|
// src/cli-client.ts
|
|
7402
8205
|
init_cjs_shims();
|
|
@@ -7417,13 +8220,16 @@ var TransportError = class extends Error {
|
|
|
7417
8220
|
this.name = "TransportError";
|
|
7418
8221
|
}
|
|
7419
8222
|
};
|
|
7420
|
-
function createHttpClient(baseUrl) {
|
|
8223
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
7421
8224
|
const root = baseUrl.replace(/\/$/, "");
|
|
8225
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
7422
8226
|
return {
|
|
7423
|
-
async get(
|
|
8227
|
+
async get(path45) {
|
|
7424
8228
|
let res;
|
|
7425
8229
|
try {
|
|
7426
|
-
res = await fetch(`${root}${
|
|
8230
|
+
res = await fetch(`${root}${path45}`, {
|
|
8231
|
+
headers: { ...authHeader }
|
|
8232
|
+
});
|
|
7427
8233
|
} catch (err) {
|
|
7428
8234
|
throw new TransportError(
|
|
7429
8235
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -7433,18 +8239,18 @@ function createHttpClient(baseUrl) {
|
|
|
7433
8239
|
const body = await res.text().catch(() => "");
|
|
7434
8240
|
throw new HttpError(
|
|
7435
8241
|
res.status,
|
|
7436
|
-
`${res.status} ${res.statusText} on GET ${
|
|
8242
|
+
`${res.status} ${res.statusText} on GET ${path45}: ${body}`,
|
|
7437
8243
|
body
|
|
7438
8244
|
);
|
|
7439
8245
|
}
|
|
7440
8246
|
return await res.json();
|
|
7441
8247
|
},
|
|
7442
|
-
async post(
|
|
8248
|
+
async post(path45, body) {
|
|
7443
8249
|
let res;
|
|
7444
8250
|
try {
|
|
7445
|
-
res = await fetch(`${root}${
|
|
8251
|
+
res = await fetch(`${root}${path45}`, {
|
|
7446
8252
|
method: "POST",
|
|
7447
|
-
headers: { "content-type": "application/json" },
|
|
8253
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
7448
8254
|
body: JSON.stringify(body)
|
|
7449
8255
|
});
|
|
7450
8256
|
} catch (err) {
|
|
@@ -7456,7 +8262,7 @@ function createHttpClient(baseUrl) {
|
|
|
7456
8262
|
const text = await res.text().catch(() => "");
|
|
7457
8263
|
throw new HttpError(
|
|
7458
8264
|
res.status,
|
|
7459
|
-
`${res.status} ${res.statusText} on POST ${
|
|
8265
|
+
`${res.status} ${res.statusText} on POST ${path45}: ${text}`,
|
|
7460
8266
|
text
|
|
7461
8267
|
);
|
|
7462
8268
|
}
|
|
@@ -7470,12 +8276,12 @@ function projectPath(project, suffix) {
|
|
|
7470
8276
|
}
|
|
7471
8277
|
async function runRootCause(client, input) {
|
|
7472
8278
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
7473
|
-
const
|
|
8279
|
+
const path45 = projectPath(
|
|
7474
8280
|
input.project,
|
|
7475
8281
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
7476
8282
|
);
|
|
7477
8283
|
try {
|
|
7478
|
-
const result = await client.get(
|
|
8284
|
+
const result = await client.get(path45);
|
|
7479
8285
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
7480
8286
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
7481
8287
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -7501,12 +8307,12 @@ async function runRootCause(client, input) {
|
|
|
7501
8307
|
}
|
|
7502
8308
|
async function runBlastRadius(client, input) {
|
|
7503
8309
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
7504
|
-
const
|
|
8310
|
+
const path45 = projectPath(
|
|
7505
8311
|
input.project,
|
|
7506
8312
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
7507
8313
|
);
|
|
7508
8314
|
try {
|
|
7509
|
-
const result = await client.get(
|
|
8315
|
+
const result = await client.get(path45);
|
|
7510
8316
|
if (result.totalAffected === 0) {
|
|
7511
8317
|
return {
|
|
7512
8318
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -7540,12 +8346,12 @@ function formatBlastEntry(n) {
|
|
|
7540
8346
|
}
|
|
7541
8347
|
async function runDependencies(client, input) {
|
|
7542
8348
|
const depth = input.depth ?? 3;
|
|
7543
|
-
const
|
|
8349
|
+
const path45 = projectPath(
|
|
7544
8350
|
input.project,
|
|
7545
8351
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
7546
8352
|
);
|
|
7547
8353
|
try {
|
|
7548
|
-
const result = await client.get(
|
|
8354
|
+
const result = await client.get(path45);
|
|
7549
8355
|
if (result.total === 0) {
|
|
7550
8356
|
return {
|
|
7551
8357
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -7626,9 +8432,9 @@ function formatDuration(ms) {
|
|
|
7626
8432
|
return `${Math.round(h / 24)}d`;
|
|
7627
8433
|
}
|
|
7628
8434
|
async function runIncidents(client, input) {
|
|
7629
|
-
const
|
|
8435
|
+
const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
7630
8436
|
try {
|
|
7631
|
-
const body = await client.get(
|
|
8437
|
+
const body = await client.get(path45);
|
|
7632
8438
|
const events = body.events;
|
|
7633
8439
|
if (events.length === 0) {
|
|
7634
8440
|
return {
|
|
@@ -7894,8 +8700,181 @@ function exitCodeForError(err) {
|
|
|
7894
8700
|
if (err instanceof HttpError) return 1;
|
|
7895
8701
|
return 1;
|
|
7896
8702
|
}
|
|
8703
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
8704
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
8705
|
+
}
|
|
8706
|
+
async function pushSnapshotToRemote(input) {
|
|
8707
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
8708
|
+
if (typeof client.post !== "function") {
|
|
8709
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
8710
|
+
}
|
|
8711
|
+
return client.post(
|
|
8712
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
8713
|
+
{ snapshot: input.snapshot }
|
|
8714
|
+
);
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// src/cli-verbs.ts
|
|
8718
|
+
async function resolveProjectEntry(opts) {
|
|
8719
|
+
const entries = await listProjects();
|
|
8720
|
+
if (opts.project) {
|
|
8721
|
+
const match = entries.find((e) => e.name === opts.project);
|
|
8722
|
+
return match ?? null;
|
|
8723
|
+
}
|
|
8724
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
8725
|
+
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
8726
|
+
for (const entry2 of entries) {
|
|
8727
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
|
|
8728
|
+
return entry2;
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
return null;
|
|
8732
|
+
}
|
|
8733
|
+
async function checkDaemonHealth2(baseUrl) {
|
|
8734
|
+
try {
|
|
8735
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
8736
|
+
signal: AbortSignal.timeout(1500)
|
|
8737
|
+
});
|
|
8738
|
+
return res.ok;
|
|
8739
|
+
} catch {
|
|
8740
|
+
return false;
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
function snapshotForGraph(persisted) {
|
|
8744
|
+
return {
|
|
8745
|
+
schemaVersion: 3,
|
|
8746
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8747
|
+
graph: persisted.graph.export()
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
function emitResult(result, json) {
|
|
8751
|
+
if (json) {
|
|
8752
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
8753
|
+
return;
|
|
8754
|
+
}
|
|
8755
|
+
const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
|
|
8756
|
+
console.log(
|
|
8757
|
+
`${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
|
|
8758
|
+
);
|
|
8759
|
+
if (!result.apply.skipped) {
|
|
8760
|
+
const { instrumented, alreadyInstrumented, libOnly } = result.apply;
|
|
8761
|
+
console.log(
|
|
8762
|
+
`instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
8763
|
+
);
|
|
8764
|
+
} else {
|
|
8765
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
8766
|
+
}
|
|
8767
|
+
for (const warn of result.warnings) console.error(warn);
|
|
8768
|
+
}
|
|
8769
|
+
async function runSync(opts) {
|
|
8770
|
+
const entry2 = await resolveProjectEntry(opts);
|
|
8771
|
+
if (!entry2) {
|
|
8772
|
+
const target = opts.project ?? opts.cwd ?? process.cwd();
|
|
8773
|
+
console.error(
|
|
8774
|
+
`neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
|
|
8775
|
+
);
|
|
8776
|
+
return {
|
|
8777
|
+
exitCode: 1,
|
|
8778
|
+
project: opts.project ?? "",
|
|
8779
|
+
scanPath: target,
|
|
8780
|
+
nodesAdded: 0,
|
|
8781
|
+
edgesAdded: 0,
|
|
8782
|
+
snapshotPath: null,
|
|
8783
|
+
mode: opts.to ? "remote" : "local",
|
|
8784
|
+
daemon: "skipped",
|
|
8785
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
|
|
8786
|
+
warnings: []
|
|
8787
|
+
};
|
|
8788
|
+
}
|
|
8789
|
+
const persisted = await extractAndPersist({
|
|
8790
|
+
scanPath: entry2.path,
|
|
8791
|
+
project: entry2.name,
|
|
8792
|
+
projectExplicit: true,
|
|
8793
|
+
dryRun: true
|
|
8794
|
+
});
|
|
8795
|
+
let snapshotPath = null;
|
|
8796
|
+
if (!opts.dryRun) {
|
|
8797
|
+
const target = persisted.snapshotPath;
|
|
8798
|
+
await saveGraphToDisk(persisted.graph, target);
|
|
8799
|
+
snapshotPath = target;
|
|
8800
|
+
}
|
|
8801
|
+
const skipApply = opts.dryRun || opts.noInstrument;
|
|
8802
|
+
const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
|
|
8803
|
+
const warnings = [];
|
|
8804
|
+
let daemonState = "skipped";
|
|
8805
|
+
let exitCode = 0;
|
|
8806
|
+
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
8807
|
+
if (!opts.dryRun) {
|
|
8808
|
+
const snapshot = snapshotForGraph(persisted);
|
|
8809
|
+
if (opts.to) {
|
|
8810
|
+
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
8811
|
+
try {
|
|
8812
|
+
await pushSnapshotToRemote({
|
|
8813
|
+
baseUrl: opts.to,
|
|
8814
|
+
token,
|
|
8815
|
+
project: entry2.name,
|
|
8816
|
+
snapshot
|
|
8817
|
+
});
|
|
8818
|
+
daemonState = "remote-ok";
|
|
8819
|
+
} catch (err) {
|
|
8820
|
+
if (err instanceof HttpError) {
|
|
8821
|
+
console.error(`neat sync: ${err.message}`);
|
|
8822
|
+
exitCode = 1;
|
|
8823
|
+
} else if (err instanceof TransportError) {
|
|
8824
|
+
console.error(`neat sync: ${err.message}`);
|
|
8825
|
+
exitCode = 3;
|
|
8826
|
+
} else {
|
|
8827
|
+
console.error(`neat sync: ${err.message}`);
|
|
8828
|
+
exitCode = 1;
|
|
8829
|
+
}
|
|
8830
|
+
daemonState = "skipped";
|
|
8831
|
+
}
|
|
8832
|
+
} else {
|
|
8833
|
+
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
8834
|
+
const healthy = await checkDaemonHealth2(daemonUrl);
|
|
8835
|
+
if (healthy) {
|
|
8836
|
+
try {
|
|
8837
|
+
await pushSnapshotToRemote({
|
|
8838
|
+
baseUrl: daemonUrl,
|
|
8839
|
+
token: process.env.NEAT_AUTH_TOKEN,
|
|
8840
|
+
project: entry2.name,
|
|
8841
|
+
snapshot
|
|
8842
|
+
});
|
|
8843
|
+
daemonState = "reloaded";
|
|
8844
|
+
} catch (err) {
|
|
8845
|
+
warnings.push(
|
|
8846
|
+
`neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
|
|
8847
|
+
);
|
|
8848
|
+
daemonState = "down";
|
|
8849
|
+
exitCode = 2;
|
|
8850
|
+
}
|
|
8851
|
+
} else {
|
|
8852
|
+
warnings.push(
|
|
8853
|
+
"neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
|
|
8854
|
+
);
|
|
8855
|
+
daemonState = "down";
|
|
8856
|
+
exitCode = 2;
|
|
8857
|
+
}
|
|
8858
|
+
}
|
|
8859
|
+
}
|
|
8860
|
+
const result = {
|
|
8861
|
+
exitCode,
|
|
8862
|
+
project: entry2.name,
|
|
8863
|
+
scanPath: entry2.path,
|
|
8864
|
+
nodesAdded: persisted.nodesAdded,
|
|
8865
|
+
edgesAdded: persisted.edgesAdded,
|
|
8866
|
+
snapshotPath,
|
|
8867
|
+
mode,
|
|
8868
|
+
daemon: daemonState,
|
|
8869
|
+
apply: { ...applyTally, skipped: skipApply },
|
|
8870
|
+
warnings
|
|
8871
|
+
};
|
|
8872
|
+
emitResult(result, opts.json);
|
|
8873
|
+
return result;
|
|
8874
|
+
}
|
|
7897
8875
|
|
|
7898
8876
|
// src/cli.ts
|
|
8877
|
+
var import_types25 = require("@neat.is/types");
|
|
7899
8878
|
function usage() {
|
|
7900
8879
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
7901
8880
|
console.log("");
|
|
@@ -7916,6 +8895,8 @@ function usage() {
|
|
|
7916
8895
|
console.log(" uninstall <name>");
|
|
7917
8896
|
console.log(" Remove a project from the registry. Does not touch");
|
|
7918
8897
|
console.log(" neat-out/, policy.json, or any user file.");
|
|
8898
|
+
console.log(" version Print the installed @neat.is/core version and exit.");
|
|
8899
|
+
console.log(" Aliases: --version, -v.");
|
|
7919
8900
|
console.log(" skill Install or print the Claude Code MCP drop-in.");
|
|
7920
8901
|
console.log(" Flags:");
|
|
7921
8902
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
@@ -7923,6 +8904,15 @@ function usage() {
|
|
|
7923
8904
|
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
7924
8905
|
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
7925
8906
|
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
8907
|
+
console.log(" sync Re-run discovery, extraction, and SDK apply against the");
|
|
8908
|
+
console.log(" registered project, then notify the running daemon.");
|
|
8909
|
+
console.log(" Flags:");
|
|
8910
|
+
console.log(" --project <name> target a registered project by name");
|
|
8911
|
+
console.log(" --to <url> push the snapshot to a remote daemon");
|
|
8912
|
+
console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
|
|
8913
|
+
console.log(" --dry-run run extraction in-memory; do not write");
|
|
8914
|
+
console.log(" --no-instrument skip the SDK install apply step");
|
|
8915
|
+
console.log(" --json emit the delta summary as JSON");
|
|
7926
8916
|
console.log("");
|
|
7927
8917
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7928
8918
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -7976,7 +8966,9 @@ var STRING_FLAGS = [
|
|
|
7976
8966
|
["--error-id", "errorId"],
|
|
7977
8967
|
["--hypothetical-action", "hypotheticalAction"],
|
|
7978
8968
|
["--type", "type"],
|
|
7979
|
-
["--min-confidence", "minConfidence"]
|
|
8969
|
+
["--min-confidence", "minConfidence"],
|
|
8970
|
+
["--to", "to"],
|
|
8971
|
+
["--token", "token"]
|
|
7980
8972
|
];
|
|
7981
8973
|
function parseArgs(rest) {
|
|
7982
8974
|
const positional = [];
|
|
@@ -8001,6 +8993,8 @@ function parseArgs(rest) {
|
|
|
8001
8993
|
hypotheticalAction: null,
|
|
8002
8994
|
type: null,
|
|
8003
8995
|
minConfidence: null,
|
|
8996
|
+
to: null,
|
|
8997
|
+
token: null,
|
|
8004
8998
|
positional: []
|
|
8005
8999
|
};
|
|
8006
9000
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -8029,7 +9023,7 @@ function parseArgs(rest) {
|
|
|
8029
9023
|
out.yes = true;
|
|
8030
9024
|
continue;
|
|
8031
9025
|
}
|
|
8032
|
-
if (arg === "--verbose"
|
|
9026
|
+
if (arg === "--verbose") {
|
|
8033
9027
|
out.verbose = true;
|
|
8034
9028
|
continue;
|
|
8035
9029
|
}
|
|
@@ -8088,6 +9082,28 @@ function assignFlag(out, field, value) {
|
|
|
8088
9082
|
;
|
|
8089
9083
|
out[field] = value;
|
|
8090
9084
|
}
|
|
9085
|
+
function readPackageVersion() {
|
|
9086
|
+
const here = typeof __dirname !== "undefined" ? __dirname : import_node_path44.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
|
|
9087
|
+
const candidates = [
|
|
9088
|
+
import_node_path44.default.resolve(here, "../package.json"),
|
|
9089
|
+
import_node_path44.default.resolve(here, "../../package.json")
|
|
9090
|
+
];
|
|
9091
|
+
for (const candidate of candidates) {
|
|
9092
|
+
try {
|
|
9093
|
+
const raw = (0, import_node_fs28.readFileSync)(candidate, "utf8");
|
|
9094
|
+
const parsed = JSON.parse(raw);
|
|
9095
|
+
if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
|
|
9096
|
+
return parsed.version;
|
|
9097
|
+
}
|
|
9098
|
+
} catch {
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
return "unknown";
|
|
9102
|
+
}
|
|
9103
|
+
function printVersion() {
|
|
9104
|
+
process.stdout.write(`${readPackageVersion()}
|
|
9105
|
+
`);
|
|
9106
|
+
}
|
|
8091
9107
|
function printBanner() {
|
|
8092
9108
|
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");
|
|
8093
9109
|
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");
|
|
@@ -8097,7 +9113,7 @@ function printBanner() {
|
|
|
8097
9113
|
console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
|
|
8098
9114
|
console.log("");
|
|
8099
9115
|
console.log(" Network Expressive Architecting Tool");
|
|
8100
|
-
console.log(" neat.is \xB7 v0.
|
|
9116
|
+
console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
|
|
8101
9117
|
console.log("");
|
|
8102
9118
|
}
|
|
8103
9119
|
function printDiscoveryReport(opts, services) {
|
|
@@ -8125,12 +9141,12 @@ function printDiscoveryReport(opts, services) {
|
|
|
8125
9141
|
}
|
|
8126
9142
|
console.log("");
|
|
8127
9143
|
}
|
|
8128
|
-
async function buildPatchSections(services) {
|
|
9144
|
+
async function buildPatchSections(services, project) {
|
|
8129
9145
|
const sections = [];
|
|
8130
9146
|
for (const svc of services) {
|
|
8131
9147
|
const installer = await pickInstaller(svc.dir);
|
|
8132
9148
|
if (!installer) continue;
|
|
8133
|
-
const plan3 = await installer.plan(svc.dir);
|
|
9149
|
+
const plan3 = await installer.plan(svc.dir, { project });
|
|
8134
9150
|
if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
|
|
8135
9151
|
sections.push({ installer: installer.name, plan: plan3 });
|
|
8136
9152
|
}
|
|
@@ -8145,14 +9161,14 @@ async function runInit(opts) {
|
|
|
8145
9161
|
}
|
|
8146
9162
|
const services = await discoverServices(opts.scanPath);
|
|
8147
9163
|
printDiscoveryReport(opts, services);
|
|
8148
|
-
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
9164
|
+
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
8149
9165
|
const patch = renderPatch(sections);
|
|
8150
|
-
const patchPath =
|
|
9166
|
+
const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
|
|
8151
9167
|
if (opts.dryRun) {
|
|
8152
9168
|
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
8153
9169
|
written.push(patchPath);
|
|
8154
9170
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8155
|
-
const gitignorePath =
|
|
9171
|
+
const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
|
|
8156
9172
|
const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8157
9173
|
const verb = gitignoreExists ? "append" : "create";
|
|
8158
9174
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
@@ -8164,9 +9180,9 @@ async function runInit(opts) {
|
|
|
8164
9180
|
const graph = getGraph(graphKey);
|
|
8165
9181
|
const projectPaths = pathsForProject(
|
|
8166
9182
|
graphKey,
|
|
8167
|
-
|
|
9183
|
+
import_node_path44.default.join(opts.scanPath, "neat-out")
|
|
8168
9184
|
);
|
|
8169
|
-
const errorsPath =
|
|
9185
|
+
const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
|
|
8170
9186
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
8171
9187
|
await saveGraphToDisk(graph, opts.outPath);
|
|
8172
9188
|
written.push(opts.outPath);
|
|
@@ -8256,9 +9272,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
8256
9272
|
};
|
|
8257
9273
|
function claudeConfigPath() {
|
|
8258
9274
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
8259
|
-
if (override && override.length > 0) return
|
|
9275
|
+
if (override && override.length > 0) return import_node_path44.default.resolve(override);
|
|
8260
9276
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8261
|
-
return
|
|
9277
|
+
return import_node_path44.default.join(home, ".claude.json");
|
|
8262
9278
|
}
|
|
8263
9279
|
async function runSkill(opts) {
|
|
8264
9280
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -8282,7 +9298,7 @@ async function runSkill(opts) {
|
|
|
8282
9298
|
...existing,
|
|
8283
9299
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
8284
9300
|
};
|
|
8285
|
-
await import_node_fs28.promises.mkdir(
|
|
9301
|
+
await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
|
|
8286
9302
|
await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
8287
9303
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
8288
9304
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
@@ -8303,6 +9319,10 @@ async function main() {
|
|
|
8303
9319
|
usage();
|
|
8304
9320
|
process.exit(0);
|
|
8305
9321
|
}
|
|
9322
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
9323
|
+
printVersion();
|
|
9324
|
+
process.exit(0);
|
|
9325
|
+
}
|
|
8306
9326
|
const parsed = parseArgs(rest);
|
|
8307
9327
|
const { positional, apply: apply3, dryRun, noInstall } = parsed;
|
|
8308
9328
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
@@ -8317,12 +9337,12 @@ async function main() {
|
|
|
8317
9337
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
8318
9338
|
process.exit(2);
|
|
8319
9339
|
}
|
|
8320
|
-
const scanPath =
|
|
9340
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
8321
9341
|
const projectExplicit = parsed.project !== null;
|
|
8322
|
-
const projectName = projectExplicit ? project :
|
|
9342
|
+
const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
|
|
8323
9343
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
8324
|
-
const fallback = pathsForProject(projectKey,
|
|
8325
|
-
const outPath =
|
|
9344
|
+
const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
|
|
9345
|
+
const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
8326
9346
|
const result = await runInit({
|
|
8327
9347
|
scanPath,
|
|
8328
9348
|
outPath,
|
|
@@ -8343,21 +9363,21 @@ async function main() {
|
|
|
8343
9363
|
usage();
|
|
8344
9364
|
process.exit(2);
|
|
8345
9365
|
}
|
|
8346
|
-
const scanPath =
|
|
9366
|
+
const scanPath = import_node_path44.default.resolve(target);
|
|
8347
9367
|
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
8348
9368
|
if (!stat || !stat.isDirectory()) {
|
|
8349
9369
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
8350
9370
|
process.exit(2);
|
|
8351
9371
|
}
|
|
8352
|
-
const projectPaths = pathsForProject(project,
|
|
8353
|
-
const outPath =
|
|
8354
|
-
const errorsPath =
|
|
8355
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
9372
|
+
const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
|
|
9373
|
+
const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
9374
|
+
const errorsPath = import_node_path44.default.resolve(
|
|
9375
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
|
|
8356
9376
|
);
|
|
8357
|
-
const staleEventsPath =
|
|
8358
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
9377
|
+
const staleEventsPath = import_node_path44.default.resolve(
|
|
9378
|
+
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
9379
|
);
|
|
8360
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
9380
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
8361
9381
|
const handle = await startWatch(getGraph(project), {
|
|
8362
9382
|
scanPath,
|
|
8363
9383
|
outPath,
|
|
@@ -8476,6 +9496,18 @@ async function main() {
|
|
|
8476
9496
|
console.log(` ${artifact.startCommand}`);
|
|
8477
9497
|
return;
|
|
8478
9498
|
}
|
|
9499
|
+
if (cmd === "sync") {
|
|
9500
|
+
const result = await runSync({
|
|
9501
|
+
...parsed.project ? { project: parsed.project } : {},
|
|
9502
|
+
...parsed.to ? { to: parsed.to } : {},
|
|
9503
|
+
...parsed.token ? { token: parsed.token } : {},
|
|
9504
|
+
dryRun: parsed.dryRun,
|
|
9505
|
+
noInstrument: parsed.noInstrument,
|
|
9506
|
+
json: parsed.json
|
|
9507
|
+
});
|
|
9508
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9509
|
+
return;
|
|
9510
|
+
}
|
|
8479
9511
|
if (QUERY_VERBS.has(cmd)) {
|
|
8480
9512
|
const code = await runQueryVerb(cmd, parsed);
|
|
8481
9513
|
if (code !== 0) process.exit(code);
|
|
@@ -8491,11 +9523,11 @@ async function main() {
|
|
|
8491
9523
|
process.exit(1);
|
|
8492
9524
|
}
|
|
8493
9525
|
async function tryOrchestrator(cmd, parsed) {
|
|
8494
|
-
const scanPath =
|
|
9526
|
+
const scanPath = import_node_path44.default.resolve(cmd);
|
|
8495
9527
|
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
8496
9528
|
if (!stat || !stat.isDirectory()) return null;
|
|
8497
9529
|
const projectExplicit = parsed.project !== null;
|
|
8498
|
-
const projectName = projectExplicit ? parsed.project :
|
|
9530
|
+
const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
|
|
8499
9531
|
const result = await runOrchestrator({
|
|
8500
9532
|
scanPath,
|
|
8501
9533
|
project: projectName,
|
|
@@ -8697,6 +9729,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
8697
9729
|
CLAUDE_SKILL_CONFIG,
|
|
8698
9730
|
QUERY_VERBS,
|
|
8699
9731
|
parseArgs,
|
|
9732
|
+
readPackageVersion,
|
|
8700
9733
|
runInit,
|
|
8701
9734
|
runQueryVerb,
|
|
8702
9735
|
runSkill
|