@neat.is/core 0.4.0 → 0.4.3
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-N6RPINEJ.js → chunk-CB2UK4EH.js} +138 -41
- package/dist/chunk-CB2UK4EH.js.map +1 -0
- package/dist/{chunk-IXIFJKMM.js → chunk-D5PIJFBE.js} +2 -2
- package/dist/{chunk-4V23KYOP.js → chunk-KYRIQIPG.js} +17 -9
- package/dist/chunk-KYRIQIPG.js.map +1 -0
- package/dist/{chunk-UPW4CMOH.js → chunk-NTQHMXWE.js} +122 -46
- package/dist/chunk-NTQHMXWE.js.map +1 -0
- package/dist/cli.cjs +351 -121
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +235 -81
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +267 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +276 -102
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-GGZHR7VM.js → otel-grpc-QTX2YQJZ.js} +3 -3
- package/dist/server.cjs +120 -44
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-4V23KYOP.js.map +0 -1
- package/dist/chunk-N6RPINEJ.js.map +0 -1
- package/dist/chunk-UPW4CMOH.js.map +0 -1
- /package/dist/{chunk-IXIFJKMM.js.map → chunk-D5PIJFBE.js.map} +0 -0
- /package/dist/{otel-grpc-GGZHR7VM.js.map → otel-grpc-QTX2YQJZ.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
|
|
|
57
57
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
58
|
const publicRead = opts.publicRead === true;
|
|
59
59
|
app.addHook("preHandler", (req, reply, done) => {
|
|
60
|
-
const
|
|
60
|
+
const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
61
61
|
for (const suffix of suffixes) {
|
|
62
|
-
if (
|
|
62
|
+
if (path38 === suffix || path38.endsWith(suffix)) {
|
|
63
63
|
done();
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -1023,19 +1023,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1023
1023
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1024
1024
|
let best = { path: [start], edges: [] };
|
|
1025
1025
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1026
|
-
function step(node,
|
|
1027
|
-
if (
|
|
1028
|
-
best = { path: [...
|
|
1026
|
+
function step(node, path38, edges) {
|
|
1027
|
+
if (path38.length > best.path.length) {
|
|
1028
|
+
best = { path: [...path38], edges: [...edges] };
|
|
1029
1029
|
}
|
|
1030
|
-
if (
|
|
1030
|
+
if (path38.length - 1 >= maxDepth) return;
|
|
1031
1031
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1032
1032
|
for (const [srcId, edge] of incoming) {
|
|
1033
1033
|
if (visited.has(srcId)) continue;
|
|
1034
1034
|
visited.add(srcId);
|
|
1035
|
-
|
|
1035
|
+
path38.push(srcId);
|
|
1036
1036
|
edges.push(edge);
|
|
1037
|
-
step(srcId,
|
|
1038
|
-
|
|
1037
|
+
step(srcId, path38, edges);
|
|
1038
|
+
path38.pop();
|
|
1039
1039
|
edges.pop();
|
|
1040
1040
|
visited.delete(srcId);
|
|
1041
1041
|
}
|
|
@@ -2119,8 +2119,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2119
2119
|
".turbo",
|
|
2120
2120
|
"dist",
|
|
2121
2121
|
"build",
|
|
2122
|
-
".next"
|
|
2122
|
+
".next",
|
|
2123
|
+
// Python virtualenv shapes (issue #344). Walking into a venv pulls in the
|
|
2124
|
+
// entire CPython stdlib + every installed package as if it were first-party
|
|
2125
|
+
// service code — 20k+ files, none of which the user wrote. The shape names
|
|
2126
|
+
// cover the three common venv tools (`venv` / `python -m venv`,
|
|
2127
|
+
// `virtualenv`) and the tox + PEP 582 conventions.
|
|
2128
|
+
".venv",
|
|
2129
|
+
"venv",
|
|
2130
|
+
"__pypackages__",
|
|
2131
|
+
".tox",
|
|
2132
|
+
// `site-packages` shows up nested inside venvs that don't carry one of the
|
|
2133
|
+
// outer names (system Python on macOS, in-place pyenv layouts). Listing it
|
|
2134
|
+
// here means we stop the walk at the boundary even when the wrapper dir
|
|
2135
|
+
// wasn't recognisable.
|
|
2136
|
+
"site-packages"
|
|
2123
2137
|
]);
|
|
2138
|
+
var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
|
|
2139
|
+
async function isPythonVenvDir(dir) {
|
|
2140
|
+
const cached = PYVENV_MARKER_CACHE.get(dir);
|
|
2141
|
+
if (cached !== void 0) return cached;
|
|
2142
|
+
try {
|
|
2143
|
+
const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
|
|
2144
|
+
const ok = stat.isFile();
|
|
2145
|
+
PYVENV_MARKER_CACHE.set(dir, ok);
|
|
2146
|
+
return ok;
|
|
2147
|
+
} catch {
|
|
2148
|
+
PYVENV_MARKER_CACHE.set(dir, false);
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2124
2152
|
function isConfigFile(name) {
|
|
2125
2153
|
const ext = import_node_path4.default.extname(name);
|
|
2126
2154
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
@@ -2457,6 +2485,7 @@ async function walkDirs(start, scanPath, options, visit) {
|
|
|
2457
2485
|
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2458
2486
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2459
2487
|
}
|
|
2488
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2460
2489
|
await visit(child);
|
|
2461
2490
|
await recurse(child, depth + 1);
|
|
2462
2491
|
}
|
|
@@ -2735,7 +2764,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
|
2735
2764
|
for (const entry of entries) {
|
|
2736
2765
|
if (entry.isDirectory()) {
|
|
2737
2766
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2738
|
-
|
|
2767
|
+
const child = import_node_path9.default.join(start, entry.name);
|
|
2768
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2769
|
+
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
2739
2770
|
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
|
|
2740
2771
|
out.push(import_node_path9.default.join(start, entry.name));
|
|
2741
2772
|
}
|
|
@@ -3417,7 +3448,9 @@ async function walkConfigFiles(dir) {
|
|
|
3417
3448
|
for (const entry of entries) {
|
|
3418
3449
|
const full = import_node_path18.default.join(current, entry.name);
|
|
3419
3450
|
if (entry.isDirectory()) {
|
|
3420
|
-
if (
|
|
3451
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3452
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3453
|
+
await walk(full);
|
|
3421
3454
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
3422
3455
|
out.push(full);
|
|
3423
3456
|
}
|
|
@@ -3485,7 +3518,9 @@ async function walkSourceFiles(dir) {
|
|
|
3485
3518
|
for (const entry of entries) {
|
|
3486
3519
|
const full = import_node_path19.default.join(current, entry.name);
|
|
3487
3520
|
if (entry.isDirectory()) {
|
|
3488
|
-
if (
|
|
3521
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3522
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3523
|
+
await walk(full);
|
|
3489
3524
|
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
|
|
3490
3525
|
out.push(full);
|
|
3491
3526
|
}
|
|
@@ -4121,7 +4156,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4121
4156
|
for (const entry of entries) {
|
|
4122
4157
|
if (entry.isDirectory()) {
|
|
4123
4158
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
4124
|
-
|
|
4159
|
+
const child = import_node_path27.default.join(start, entry.name);
|
|
4160
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4161
|
+
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
4125
4162
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
4126
4163
|
out.push(import_node_path27.default.join(start, entry.name));
|
|
4127
4164
|
}
|
|
@@ -4169,7 +4206,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
4169
4206
|
for (const entry of entries) {
|
|
4170
4207
|
if (entry.isDirectory()) {
|
|
4171
4208
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4172
|
-
|
|
4209
|
+
const child = import_node_path28.default.join(start, entry.name);
|
|
4210
|
+
if (await isPythonVenvDir(child)) continue;
|
|
4211
|
+
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
4173
4212
|
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
|
|
4174
4213
|
out.push(import_node_path28.default.join(start, entry.name));
|
|
4175
4214
|
}
|
|
@@ -5028,10 +5067,19 @@ function projectFromReq(req) {
|
|
|
5028
5067
|
const params = req.params;
|
|
5029
5068
|
return params.project ?? DEFAULT_PROJECT;
|
|
5030
5069
|
}
|
|
5031
|
-
function resolveProject(registry, req, reply) {
|
|
5070
|
+
function resolveProject(registry, req, reply, bootstrap) {
|
|
5032
5071
|
const name = projectFromReq(req);
|
|
5033
5072
|
const ctx = registry.get(name);
|
|
5034
5073
|
if (!ctx) {
|
|
5074
|
+
const phase = bootstrap?.status(name);
|
|
5075
|
+
if (phase === "bootstrapping") {
|
|
5076
|
+
void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
|
|
5077
|
+
return null;
|
|
5078
|
+
}
|
|
5079
|
+
if (phase === "broken") {
|
|
5080
|
+
void reply.code(503).send({ ready: false, project: name, status: "broken" });
|
|
5081
|
+
return null;
|
|
5082
|
+
}
|
|
5035
5083
|
void reply.code(404).send({ error: "project not found", project: name });
|
|
5036
5084
|
return null;
|
|
5037
5085
|
}
|
|
@@ -5061,36 +5109,38 @@ function buildLegacyRegistry(opts) {
|
|
|
5061
5109
|
function registerRoutes(scope, ctx) {
|
|
5062
5110
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
5063
5111
|
scope.get("/events", (req, reply) => {
|
|
5064
|
-
const proj = resolveProject(registry, req, reply);
|
|
5112
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5065
5113
|
if (!proj) return;
|
|
5066
5114
|
handleSse(req, reply, { project: proj.name });
|
|
5067
5115
|
});
|
|
5068
|
-
scope
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5116
|
+
if (ctx.scope === "project") {
|
|
5117
|
+
scope.get("/health", async (req, reply) => {
|
|
5118
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5119
|
+
if (!proj) return;
|
|
5120
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5121
|
+
return {
|
|
5122
|
+
ok: true,
|
|
5123
|
+
project: proj.name,
|
|
5124
|
+
uptimeMs,
|
|
5125
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
5126
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
5127
|
+
// the canonical triple and lets the extras pass through.
|
|
5128
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
5129
|
+
nodeCount: proj.graph.order,
|
|
5130
|
+
edgeCount: proj.graph.size,
|
|
5131
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
5132
|
+
};
|
|
5133
|
+
});
|
|
5134
|
+
}
|
|
5085
5135
|
scope.get("/graph", async (req, reply) => {
|
|
5086
|
-
const proj = resolveProject(registry, req, reply);
|
|
5136
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5087
5137
|
if (!proj) return;
|
|
5088
5138
|
return serializeGraph(proj.graph);
|
|
5089
5139
|
});
|
|
5090
5140
|
scope.get(
|
|
5091
5141
|
"/graph/node/:id",
|
|
5092
5142
|
async (req, reply) => {
|
|
5093
|
-
const proj = resolveProject(registry, req, reply);
|
|
5143
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5094
5144
|
if (!proj) return;
|
|
5095
5145
|
const { id } = req.params;
|
|
5096
5146
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -5102,7 +5152,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5102
5152
|
scope.get(
|
|
5103
5153
|
"/graph/edges/:id",
|
|
5104
5154
|
async (req, reply) => {
|
|
5105
|
-
const proj = resolveProject(registry, req, reply);
|
|
5155
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5106
5156
|
if (!proj) return;
|
|
5107
5157
|
const { id } = req.params;
|
|
5108
5158
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -5114,7 +5164,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5114
5164
|
}
|
|
5115
5165
|
);
|
|
5116
5166
|
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
5117
|
-
const proj = resolveProject(registry, req, reply);
|
|
5167
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5118
5168
|
if (!proj) return;
|
|
5119
5169
|
const { nodeId } = req.params;
|
|
5120
5170
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5129,7 +5179,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5129
5179
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
5130
5180
|
});
|
|
5131
5181
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
5132
|
-
const proj = resolveProject(registry, req, reply);
|
|
5182
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5133
5183
|
if (!proj) return;
|
|
5134
5184
|
let typeFilter;
|
|
5135
5185
|
if (req.query.type) {
|
|
@@ -5164,7 +5214,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5164
5214
|
});
|
|
5165
5215
|
});
|
|
5166
5216
|
scope.get("/incidents", async (req, reply) => {
|
|
5167
|
-
const proj = resolveProject(registry, req, reply);
|
|
5217
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5168
5218
|
if (!proj) return;
|
|
5169
5219
|
const epath = errorsPathFor(proj);
|
|
5170
5220
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -5176,7 +5226,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5176
5226
|
return { count: sliced.length, total, events: sliced };
|
|
5177
5227
|
});
|
|
5178
5228
|
scope.get("/stale-events", async (req, reply) => {
|
|
5179
|
-
const proj = resolveProject(registry, req, reply);
|
|
5229
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5180
5230
|
if (!proj) return;
|
|
5181
5231
|
const spath = staleEventsPathFor(proj);
|
|
5182
5232
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -5191,7 +5241,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5191
5241
|
scope.get(
|
|
5192
5242
|
"/incidents/:nodeId",
|
|
5193
5243
|
async (req, reply) => {
|
|
5194
|
-
const proj = resolveProject(registry, req, reply);
|
|
5244
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5195
5245
|
if (!proj) return;
|
|
5196
5246
|
const { nodeId } = req.params;
|
|
5197
5247
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5207,7 +5257,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5207
5257
|
}
|
|
5208
5258
|
);
|
|
5209
5259
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
5210
|
-
const proj = resolveProject(registry, req, reply);
|
|
5260
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5211
5261
|
if (!proj) return;
|
|
5212
5262
|
const { nodeId } = req.params;
|
|
5213
5263
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5227,7 +5277,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5227
5277
|
return result;
|
|
5228
5278
|
});
|
|
5229
5279
|
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
5230
|
-
const proj = resolveProject(registry, req, reply);
|
|
5280
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5231
5281
|
if (!proj) return;
|
|
5232
5282
|
const { nodeId } = req.params;
|
|
5233
5283
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -5240,7 +5290,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5240
5290
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
5241
5291
|
});
|
|
5242
5292
|
scope.get("/search", async (req, reply) => {
|
|
5243
|
-
const proj = resolveProject(registry, req, reply);
|
|
5293
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5244
5294
|
if (!proj) return;
|
|
5245
5295
|
const raw = (req.query.q ?? "").trim();
|
|
5246
5296
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -5271,7 +5321,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5271
5321
|
scope.get(
|
|
5272
5322
|
"/graph/diff",
|
|
5273
5323
|
async (req, reply) => {
|
|
5274
|
-
const proj = resolveProject(registry, req, reply);
|
|
5324
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5275
5325
|
if (!proj) return;
|
|
5276
5326
|
const against = req.query.against;
|
|
5277
5327
|
if (!against) {
|
|
@@ -5286,7 +5336,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5286
5336
|
}
|
|
5287
5337
|
);
|
|
5288
5338
|
scope.post("/snapshot", async (req, reply) => {
|
|
5289
|
-
const proj = resolveProject(registry, req, reply);
|
|
5339
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5290
5340
|
if (!proj) return;
|
|
5291
5341
|
const body = req.body;
|
|
5292
5342
|
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
@@ -5315,7 +5365,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5315
5365
|
}
|
|
5316
5366
|
});
|
|
5317
5367
|
scope.post("/graph/scan", async (req, reply) => {
|
|
5318
|
-
const proj = resolveProject(registry, req, reply);
|
|
5368
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5319
5369
|
if (!proj) return;
|
|
5320
5370
|
if (!proj.scanPath) {
|
|
5321
5371
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -5331,7 +5381,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5331
5381
|
};
|
|
5332
5382
|
});
|
|
5333
5383
|
scope.get("/policies", async (req, reply) => {
|
|
5334
|
-
const proj = resolveProject(registry, req, reply);
|
|
5384
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5335
5385
|
if (!proj) return;
|
|
5336
5386
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
5337
5387
|
if (!policyPath) {
|
|
@@ -5348,7 +5398,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5348
5398
|
}
|
|
5349
5399
|
});
|
|
5350
5400
|
scope.get("/policies/violations", async (req, reply) => {
|
|
5351
|
-
const proj = resolveProject(registry, req, reply);
|
|
5401
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5352
5402
|
if (!proj) return;
|
|
5353
5403
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5354
5404
|
let violations = await log.readAll();
|
|
@@ -5368,7 +5418,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5368
5418
|
return { violations };
|
|
5369
5419
|
});
|
|
5370
5420
|
scope.post("/policies/check", async (req, reply) => {
|
|
5371
|
-
const proj = resolveProject(registry, req, reply);
|
|
5421
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5372
5422
|
if (!proj) return;
|
|
5373
5423
|
const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5374
5424
|
if (!parsed.success) {
|
|
@@ -5439,10 +5489,36 @@ async function buildApi(opts) {
|
|
|
5439
5489
|
const routeCtx = {
|
|
5440
5490
|
registry,
|
|
5441
5491
|
startedAt,
|
|
5492
|
+
scope: "root",
|
|
5442
5493
|
errorsPathFor,
|
|
5443
5494
|
staleEventsPathFor,
|
|
5444
|
-
policyFilePathFor
|
|
5495
|
+
policyFilePathFor,
|
|
5496
|
+
bootstrap: opts.bootstrap
|
|
5445
5497
|
};
|
|
5498
|
+
app.get("/health", async () => {
|
|
5499
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5500
|
+
const bootstrapList = opts.bootstrap?.list() ?? [];
|
|
5501
|
+
const byName = new Map(bootstrapList.map((p) => [p.name, p]));
|
|
5502
|
+
const names = /* @__PURE__ */ new Set([
|
|
5503
|
+
...registry.list(),
|
|
5504
|
+
...bootstrapList.map((p) => p.name)
|
|
5505
|
+
]);
|
|
5506
|
+
const projects = [...names].sort().map((name) => {
|
|
5507
|
+
const proj = registry.get(name);
|
|
5508
|
+
const tracked = byName.get(name);
|
|
5509
|
+
return {
|
|
5510
|
+
name,
|
|
5511
|
+
nodeCount: proj?.graph.order ?? 0,
|
|
5512
|
+
edgeCount: proj?.graph.size ?? 0,
|
|
5513
|
+
...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
|
|
5514
|
+
};
|
|
5515
|
+
});
|
|
5516
|
+
return {
|
|
5517
|
+
ok: true,
|
|
5518
|
+
uptimeMs,
|
|
5519
|
+
projects
|
|
5520
|
+
};
|
|
5521
|
+
});
|
|
5446
5522
|
app.get("/projects", async (_req, reply) => {
|
|
5447
5523
|
try {
|
|
5448
5524
|
return await listProjects();
|
|
@@ -5470,7 +5546,7 @@ async function buildApi(opts) {
|
|
|
5470
5546
|
registerRoutes(app, routeCtx);
|
|
5471
5547
|
await app.register(
|
|
5472
5548
|
async (scope) => {
|
|
5473
|
-
registerRoutes(scope, routeCtx);
|
|
5549
|
+
registerRoutes(scope, { ...routeCtx, scope: "project" });
|
|
5474
5550
|
},
|
|
5475
5551
|
{ prefix: "/projects/:project" }
|
|
5476
5552
|
);
|
|
@@ -5483,16 +5559,39 @@ init_otel_grpc();
|
|
|
5483
5559
|
|
|
5484
5560
|
// src/daemon.ts
|
|
5485
5561
|
init_cjs_shims();
|
|
5486
|
-
var
|
|
5487
|
-
var
|
|
5562
|
+
var import_node_fs22 = require("fs");
|
|
5563
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5488
5564
|
init_otel();
|
|
5489
5565
|
init_auth();
|
|
5566
|
+
|
|
5567
|
+
// src/unrouted.ts
|
|
5568
|
+
init_cjs_shims();
|
|
5569
|
+
var import_node_fs21 = require("fs");
|
|
5570
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5571
|
+
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
5572
|
+
return {
|
|
5573
|
+
timestamp: now.toISOString(),
|
|
5574
|
+
reason: "no-project-match",
|
|
5575
|
+
service_name: serviceName ?? null,
|
|
5576
|
+
traceId: traceId ?? null
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
async function appendUnroutedSpan(neatHome2, record) {
|
|
5580
|
+
const target = import_node_path36.default.join(neatHome2, "errors.ndjson");
|
|
5581
|
+
await import_node_fs21.promises.mkdir(neatHome2, { recursive: true });
|
|
5582
|
+
await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
5583
|
+
}
|
|
5584
|
+
function unroutedErrorsPath(neatHome2) {
|
|
5585
|
+
return import_node_path36.default.join(neatHome2, "errors.ndjson");
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
// src/daemon.ts
|
|
5490
5589
|
function neatHomeFor(opts) {
|
|
5491
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5590
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
|
|
5492
5591
|
const env = process.env.NEAT_HOME;
|
|
5493
|
-
if (env && env.length > 0) return
|
|
5592
|
+
if (env && env.length > 0) return import_node_path37.default.resolve(env);
|
|
5494
5593
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5495
|
-
return
|
|
5594
|
+
return import_node_path37.default.join(home, ".neat");
|
|
5496
5595
|
}
|
|
5497
5596
|
function routeSpanToProject(serviceName, projects) {
|
|
5498
5597
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -5527,9 +5626,9 @@ function isTokenContained(needle, haystack) {
|
|
|
5527
5626
|
return tokens.includes(needle);
|
|
5528
5627
|
}
|
|
5529
5628
|
async function bootstrapProject(entry) {
|
|
5530
|
-
const paths = pathsForProject(entry.name,
|
|
5629
|
+
const paths = pathsForProject(entry.name, import_node_path37.default.join(entry.path, "neat-out"));
|
|
5531
5630
|
try {
|
|
5532
|
-
const stat = await
|
|
5631
|
+
const stat = await import_node_fs22.promises.stat(entry.path);
|
|
5533
5632
|
if (!stat.isDirectory()) {
|
|
5534
5633
|
throw new Error(`registered path ${entry.path} is not a directory`);
|
|
5535
5634
|
}
|
|
@@ -5584,27 +5683,30 @@ function resolveOtlpPort(opts) {
|
|
|
5584
5683
|
}
|
|
5585
5684
|
return 4318;
|
|
5586
5685
|
}
|
|
5587
|
-
function resolveHost(opts) {
|
|
5686
|
+
function resolveHost(opts, authTokenSet) {
|
|
5588
5687
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
5589
5688
|
const env = process.env.HOST;
|
|
5590
5689
|
if (env && env.length > 0) return env;
|
|
5690
|
+
if (!authTokenSet) return "127.0.0.1";
|
|
5591
5691
|
return "0.0.0.0";
|
|
5592
5692
|
}
|
|
5593
5693
|
async function startDaemon(opts = {}) {
|
|
5594
5694
|
const home = neatHomeFor(opts);
|
|
5595
5695
|
const regPath = registryPath();
|
|
5596
5696
|
try {
|
|
5597
|
-
await
|
|
5697
|
+
await import_node_fs22.promises.access(regPath);
|
|
5598
5698
|
} catch {
|
|
5599
5699
|
throw new Error(
|
|
5600
5700
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
5601
5701
|
);
|
|
5602
5702
|
}
|
|
5603
|
-
const pidPath =
|
|
5703
|
+
const pidPath = import_node_path37.default.join(home, "neatd.pid");
|
|
5604
5704
|
await writeAtomically(pidPath, `${process.pid}
|
|
5605
5705
|
`);
|
|
5606
5706
|
const slots = /* @__PURE__ */ new Map();
|
|
5607
5707
|
const registry = new Projects();
|
|
5708
|
+
const bootstrapStatus = /* @__PURE__ */ new Map();
|
|
5709
|
+
const bootstrapStartedAt = /* @__PURE__ */ new Map();
|
|
5608
5710
|
const DROP_WARN_INTERVAL_MS = 6e4;
|
|
5609
5711
|
const lastDropWarnAt = /* @__PURE__ */ new Map();
|
|
5610
5712
|
function warnDroppedSpan(project, reason) {
|
|
@@ -5616,6 +5718,22 @@ async function startDaemon(opts = {}) {
|
|
|
5616
5718
|
`[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
|
|
5617
5719
|
);
|
|
5618
5720
|
}
|
|
5721
|
+
const unroutedPath = unroutedErrorsPath(home);
|
|
5722
|
+
const lastUnroutedWarnAt = /* @__PURE__ */ new Map();
|
|
5723
|
+
async function recordUnroutedSpan(serviceName, traceId) {
|
|
5724
|
+
const key = serviceName ?? "<missing>";
|
|
5725
|
+
const now = Date.now();
|
|
5726
|
+
try {
|
|
5727
|
+
await appendUnroutedSpan(home, buildUnroutedSpanRecord(serviceName, traceId, new Date(now)));
|
|
5728
|
+
} catch {
|
|
5729
|
+
}
|
|
5730
|
+
const prev = lastUnroutedWarnAt.get(key) ?? 0;
|
|
5731
|
+
if (now - prev < DROP_WARN_INTERVAL_MS) return;
|
|
5732
|
+
lastUnroutedWarnAt.set(key, now);
|
|
5733
|
+
console.warn(
|
|
5734
|
+
`[neatd] dropping span \u2014 service.name "${key}" matches no registered project and no \`default\` project exists. See ${unroutedPath}.`
|
|
5735
|
+
);
|
|
5736
|
+
}
|
|
5619
5737
|
function upsertRegistryFromSlot(slot) {
|
|
5620
5738
|
if (slot.status !== "active") return;
|
|
5621
5739
|
registry.set(slot.entry.name, {
|
|
@@ -5644,34 +5762,43 @@ async function startDaemon(opts = {}) {
|
|
|
5644
5762
|
return slots.get(entry.name);
|
|
5645
5763
|
}
|
|
5646
5764
|
}
|
|
5765
|
+
async function bootstrapOne(entry) {
|
|
5766
|
+
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
5767
|
+
bootstrapStartedAt.set(entry.name, Date.now());
|
|
5768
|
+
try {
|
|
5769
|
+
const slot = await bootstrapProject(entry);
|
|
5770
|
+
slots.set(entry.name, slot);
|
|
5771
|
+
upsertRegistryFromSlot(slot);
|
|
5772
|
+
bootstrapStatus.set(entry.name, slot.status === "broken" ? "broken" : "active");
|
|
5773
|
+
if (slot.status === "broken") {
|
|
5774
|
+
console.warn(`neatd: project "${entry.name}" broken \u2014 ${slot.errorReason}`);
|
|
5775
|
+
} else {
|
|
5776
|
+
console.log(`neatd: project "${entry.name}" active (${entry.path})`);
|
|
5777
|
+
}
|
|
5778
|
+
} catch (err) {
|
|
5779
|
+
bootstrapStatus.set(entry.name, "broken");
|
|
5780
|
+
console.warn(
|
|
5781
|
+
`neatd: project "${entry.name}" failed to bootstrap \u2014 ${err.message}`
|
|
5782
|
+
);
|
|
5783
|
+
await setStatus(entry.name, "broken").catch(() => {
|
|
5784
|
+
});
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5647
5787
|
async function loadAll() {
|
|
5648
5788
|
const projects = await listProjects();
|
|
5649
5789
|
const seen = /* @__PURE__ */ new Set();
|
|
5790
|
+
const pending = [];
|
|
5650
5791
|
for (const entry of projects) {
|
|
5651
5792
|
seen.add(entry.name);
|
|
5652
5793
|
const existing = slots.get(entry.name);
|
|
5653
5794
|
if (existing) {
|
|
5654
5795
|
if (existing.status === "broken") {
|
|
5655
|
-
|
|
5796
|
+
pending.push(tryRecoverSlot(entry).then(() => {
|
|
5797
|
+
}));
|
|
5656
5798
|
}
|
|
5657
5799
|
continue;
|
|
5658
5800
|
}
|
|
5659
|
-
|
|
5660
|
-
const slot = await bootstrapProject(entry);
|
|
5661
|
-
slots.set(entry.name, slot);
|
|
5662
|
-
upsertRegistryFromSlot(slot);
|
|
5663
|
-
if (slot.status === "broken") {
|
|
5664
|
-
console.warn(`neatd: project "${entry.name}" broken \u2014 ${slot.errorReason}`);
|
|
5665
|
-
} else {
|
|
5666
|
-
console.log(`neatd: project "${entry.name}" active (${entry.path})`);
|
|
5667
|
-
}
|
|
5668
|
-
} catch (err) {
|
|
5669
|
-
console.warn(
|
|
5670
|
-
`neatd: project "${entry.name}" failed to bootstrap \u2014 ${err.message}`
|
|
5671
|
-
);
|
|
5672
|
-
await setStatus(entry.name, "broken").catch(() => {
|
|
5673
|
-
});
|
|
5674
|
-
}
|
|
5801
|
+
pending.push(bootstrapOne(entry));
|
|
5675
5802
|
}
|
|
5676
5803
|
for (const [name, slot] of [...slots.entries()]) {
|
|
5677
5804
|
if (seen.has(name)) continue;
|
|
@@ -5680,27 +5807,45 @@ async function startDaemon(opts = {}) {
|
|
|
5680
5807
|
} catch {
|
|
5681
5808
|
}
|
|
5682
5809
|
slots.delete(name);
|
|
5810
|
+
bootstrapStatus.delete(name);
|
|
5811
|
+
bootstrapStartedAt.delete(name);
|
|
5683
5812
|
console.log(`neatd: project "${name}" removed from registry \u2014 stopped`);
|
|
5684
5813
|
}
|
|
5814
|
+
await Promise.allSettled(pending);
|
|
5815
|
+
}
|
|
5816
|
+
const initialEntries = await listProjects().catch(() => []);
|
|
5817
|
+
for (const entry of initialEntries) {
|
|
5818
|
+
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
5819
|
+
bootstrapStartedAt.set(entry.name, Date.now());
|
|
5685
5820
|
}
|
|
5686
|
-
await loadAll();
|
|
5687
5821
|
const bind = opts.bindListeners !== false;
|
|
5688
5822
|
let restApp = null;
|
|
5689
5823
|
let otlpApp = null;
|
|
5690
5824
|
let restAddress = "";
|
|
5691
5825
|
let otlpAddress = "";
|
|
5692
5826
|
if (bind) {
|
|
5693
|
-
const
|
|
5827
|
+
const auth = readAuthEnv();
|
|
5828
|
+
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
5694
5829
|
const restPort = resolveRestPort(opts);
|
|
5695
5830
|
const otlpPort = resolveOtlpPort(opts);
|
|
5696
|
-
const auth = readAuthEnv();
|
|
5697
5831
|
assertBindAuthority(host, auth.authToken);
|
|
5698
5832
|
try {
|
|
5699
5833
|
restApp = await buildApi({
|
|
5700
5834
|
projects: registry,
|
|
5701
5835
|
authToken: auth.authToken,
|
|
5702
5836
|
trustProxy: auth.trustProxy,
|
|
5703
|
-
publicRead: auth.publicRead
|
|
5837
|
+
publicRead: auth.publicRead,
|
|
5838
|
+
bootstrap: {
|
|
5839
|
+
status: (name) => bootstrapStatus.get(name),
|
|
5840
|
+
list: () => {
|
|
5841
|
+
const now = Date.now();
|
|
5842
|
+
return [...bootstrapStatus.entries()].map(([name, status2]) => ({
|
|
5843
|
+
name,
|
|
5844
|
+
status: status2,
|
|
5845
|
+
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
5846
|
+
}));
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5704
5849
|
});
|
|
5705
5850
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
5706
5851
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
@@ -5713,17 +5858,20 @@ async function startDaemon(opts = {}) {
|
|
|
5713
5858
|
}
|
|
5714
5859
|
if (restApp) await restApp.close().catch(() => {
|
|
5715
5860
|
});
|
|
5716
|
-
await
|
|
5861
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5717
5862
|
});
|
|
5718
5863
|
throw new Error(
|
|
5719
5864
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
5720
5865
|
);
|
|
5721
5866
|
}
|
|
5722
|
-
async function resolveTargetSlot(serviceName) {
|
|
5867
|
+
async function resolveTargetSlot(serviceName, traceId) {
|
|
5723
5868
|
const liveEntries = await listProjects().catch(() => []);
|
|
5724
5869
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
5725
5870
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5726
|
-
if (!slot)
|
|
5871
|
+
if (!slot) {
|
|
5872
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
5873
|
+
return null;
|
|
5874
|
+
}
|
|
5727
5875
|
if (slot.status === "broken") {
|
|
5728
5876
|
const entry = liveEntries.find((e) => e.name === slot.entry.name);
|
|
5729
5877
|
if (entry) {
|
|
@@ -5741,7 +5889,7 @@ async function startDaemon(opts = {}) {
|
|
|
5741
5889
|
authToken: auth.otelToken,
|
|
5742
5890
|
trustProxy: auth.trustProxy,
|
|
5743
5891
|
onSpan: async (span) => {
|
|
5744
|
-
const slot = await resolveTargetSlot(span.service);
|
|
5892
|
+
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
5745
5893
|
if (!slot) return;
|
|
5746
5894
|
await handleSpan(
|
|
5747
5895
|
{
|
|
@@ -5755,7 +5903,7 @@ async function startDaemon(opts = {}) {
|
|
|
5755
5903
|
);
|
|
5756
5904
|
},
|
|
5757
5905
|
onErrorSpanSync: async (span) => {
|
|
5758
|
-
const slot = await resolveTargetSlot(span.service);
|
|
5906
|
+
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
5759
5907
|
if (!slot) return;
|
|
5760
5908
|
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5761
5909
|
}
|
|
@@ -5773,14 +5921,17 @@ async function startDaemon(opts = {}) {
|
|
|
5773
5921
|
});
|
|
5774
5922
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
5775
5923
|
});
|
|
5776
|
-
await
|
|
5924
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5777
5925
|
});
|
|
5778
5926
|
throw new Error(
|
|
5779
5927
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
5780
5928
|
);
|
|
5781
5929
|
}
|
|
5782
5930
|
}
|
|
5783
|
-
|
|
5931
|
+
const initialBootstrap = loadAll().catch((err) => {
|
|
5932
|
+
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
5933
|
+
});
|
|
5934
|
+
let reloading = initialBootstrap;
|
|
5784
5935
|
const reload = async () => {
|
|
5785
5936
|
if (reloading) return reloading;
|
|
5786
5937
|
reloading = (async () => {
|
|
@@ -5792,6 +5943,20 @@ async function startDaemon(opts = {}) {
|
|
|
5792
5943
|
})();
|
|
5793
5944
|
return reloading;
|
|
5794
5945
|
};
|
|
5946
|
+
void initialBootstrap.finally(() => {
|
|
5947
|
+
if (reloading === initialBootstrap) reloading = null;
|
|
5948
|
+
});
|
|
5949
|
+
const tracker = {
|
|
5950
|
+
status: (name) => bootstrapStatus.get(name),
|
|
5951
|
+
list: () => {
|
|
5952
|
+
const now = Date.now();
|
|
5953
|
+
return [...bootstrapStatus.entries()].map(([name, status2]) => ({
|
|
5954
|
+
name,
|
|
5955
|
+
status: status2,
|
|
5956
|
+
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
5957
|
+
}));
|
|
5958
|
+
}
|
|
5959
|
+
};
|
|
5795
5960
|
const sighupHandler = () => {
|
|
5796
5961
|
void reload().catch((err) => {
|
|
5797
5962
|
console.warn(`neatd: SIGHUP reload failed \u2014 ${err.message}`);
|
|
@@ -5813,10 +5978,19 @@ async function startDaemon(opts = {}) {
|
|
|
5813
5978
|
} catch {
|
|
5814
5979
|
}
|
|
5815
5980
|
}
|
|
5816
|
-
await
|
|
5981
|
+
await import_node_fs22.promises.unlink(pidPath).catch(() => {
|
|
5817
5982
|
});
|
|
5818
5983
|
};
|
|
5819
|
-
return {
|
|
5984
|
+
return {
|
|
5985
|
+
slots,
|
|
5986
|
+
reload,
|
|
5987
|
+
stop,
|
|
5988
|
+
pidPath,
|
|
5989
|
+
restAddress,
|
|
5990
|
+
otlpAddress,
|
|
5991
|
+
bootstrap: tracker,
|
|
5992
|
+
initialBootstrap
|
|
5993
|
+
};
|
|
5820
5994
|
}
|
|
5821
5995
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5822
5996
|
0 && (module.exports = {
|