@neat.is/core 0.4.18 → 0.4.19
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-T3X6XJPU.js → chunk-Q5EDVWKZ.js} +68 -38
- package/dist/{chunk-T3X6XJPU.js.map → chunk-Q5EDVWKZ.js.map} +1 -1
- package/dist/{chunk-GXWBMJDJ.js → chunk-ZV7GHZ2D.js} +17 -18
- package/dist/chunk-ZV7GHZ2D.js.map +1 -0
- package/dist/cli.cjs +67 -37
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +81 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +89 -54
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +10 -3
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +67 -37
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-GXWBMJDJ.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -63,7 +63,11 @@ declare function compatPairs(): readonly CompatPair[];
|
|
|
63
63
|
|
|
64
64
|
declare function saveGraphToDisk(graph: NeatGraph, outPath: string): Promise<void>;
|
|
65
65
|
declare function loadGraphFromDisk(graph: NeatGraph, outPath: string): Promise<void>;
|
|
66
|
-
|
|
66
|
+
interface PersistLoopOptions {
|
|
67
|
+
intervalMs?: number;
|
|
68
|
+
exitOnSignal?: boolean;
|
|
69
|
+
}
|
|
70
|
+
declare function startPersistLoop(graph: NeatGraph, outPath: string, opts?: PersistLoopOptions): () => void;
|
|
67
71
|
|
|
68
72
|
interface ScoredNode {
|
|
69
73
|
node: GraphNode;
|
|
@@ -125,6 +129,10 @@ interface BuildApiOptions {
|
|
|
125
129
|
elapsedMs: number;
|
|
126
130
|
}>;
|
|
127
131
|
};
|
|
132
|
+
singleProject?: {
|
|
133
|
+
name: string;
|
|
134
|
+
path: string;
|
|
135
|
+
};
|
|
128
136
|
}
|
|
129
137
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
130
138
|
|
package/dist/index.d.ts
CHANGED
|
@@ -63,7 +63,11 @@ declare function compatPairs(): readonly CompatPair[];
|
|
|
63
63
|
|
|
64
64
|
declare function saveGraphToDisk(graph: NeatGraph, outPath: string): Promise<void>;
|
|
65
65
|
declare function loadGraphFromDisk(graph: NeatGraph, outPath: string): Promise<void>;
|
|
66
|
-
|
|
66
|
+
interface PersistLoopOptions {
|
|
67
|
+
intervalMs?: number;
|
|
68
|
+
exitOnSignal?: boolean;
|
|
69
|
+
}
|
|
70
|
+
declare function startPersistLoop(graph: NeatGraph, outPath: string, opts?: PersistLoopOptions): () => void;
|
|
67
71
|
|
|
68
72
|
interface ScoredNode {
|
|
69
73
|
node: GraphNode;
|
|
@@ -125,6 +129,10 @@ interface BuildApiOptions {
|
|
|
125
129
|
elapsedMs: number;
|
|
126
130
|
}>;
|
|
127
131
|
};
|
|
132
|
+
singleProject?: {
|
|
133
|
+
name: string;
|
|
134
|
+
path: string;
|
|
135
|
+
};
|
|
128
136
|
}
|
|
129
137
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
130
138
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZV7GHZ2D.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-Q5EDVWKZ.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
43
|
} from "./chunk-MTXF77TN.js";
|
package/dist/neatd.cjs
CHANGED
|
@@ -5211,7 +5211,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
5211
5211
|
graph.clear();
|
|
5212
5212
|
graph.import(payload.graph);
|
|
5213
5213
|
}
|
|
5214
|
-
function startPersistLoop(graph, outPath,
|
|
5214
|
+
function startPersistLoop(graph, outPath, opts = {}) {
|
|
5215
|
+
const intervalMs = opts.intervalMs ?? 6e4;
|
|
5216
|
+
const exitOnSignal = opts.exitOnSignal ?? true;
|
|
5215
5217
|
let stopped = false;
|
|
5216
5218
|
const tick = async () => {
|
|
5217
5219
|
if (stopped) return;
|
|
@@ -5224,7 +5226,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5224
5226
|
const interval = setInterval(() => {
|
|
5225
5227
|
void tick();
|
|
5226
5228
|
}, intervalMs);
|
|
5227
|
-
const onSignal = (signal) => {
|
|
5229
|
+
const onSignal = exitOnSignal ? (signal) => {
|
|
5228
5230
|
void (async () => {
|
|
5229
5231
|
try {
|
|
5230
5232
|
await saveGraphToDisk(graph, outPath);
|
|
@@ -5234,14 +5236,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5234
5236
|
process.exit(0);
|
|
5235
5237
|
}
|
|
5236
5238
|
})();
|
|
5237
|
-
};
|
|
5238
|
-
|
|
5239
|
-
|
|
5239
|
+
} : null;
|
|
5240
|
+
if (onSignal) {
|
|
5241
|
+
process.on("SIGTERM", onSignal);
|
|
5242
|
+
process.on("SIGINT", onSignal);
|
|
5243
|
+
}
|
|
5240
5244
|
return () => {
|
|
5241
5245
|
stopped = true;
|
|
5242
5246
|
clearInterval(interval);
|
|
5243
|
-
|
|
5244
|
-
|
|
5247
|
+
if (onSignal) {
|
|
5248
|
+
process.off("SIGTERM", onSignal);
|
|
5249
|
+
process.off("SIGINT", onSignal);
|
|
5250
|
+
}
|
|
5245
5251
|
};
|
|
5246
5252
|
}
|
|
5247
5253
|
|
|
@@ -6234,12 +6240,16 @@ function serializeGraph(graph) {
|
|
|
6234
6240
|
});
|
|
6235
6241
|
return { nodes, edges };
|
|
6236
6242
|
}
|
|
6237
|
-
function projectFromReq(req2) {
|
|
6243
|
+
function projectFromReq(req2, singleProject) {
|
|
6238
6244
|
const params = req2.params;
|
|
6239
|
-
|
|
6245
|
+
const named = params.project;
|
|
6246
|
+
if (singleProject) {
|
|
6247
|
+
return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
|
|
6248
|
+
}
|
|
6249
|
+
return named ?? DEFAULT_PROJECT;
|
|
6240
6250
|
}
|
|
6241
|
-
function resolveProject(registry, req2, reply, bootstrap) {
|
|
6242
|
-
const name = projectFromReq(req2);
|
|
6251
|
+
function resolveProject(registry, req2, reply, bootstrap, singleProject) {
|
|
6252
|
+
const name = projectFromReq(req2, singleProject);
|
|
6243
6253
|
const ctx = registry.get(name);
|
|
6244
6254
|
if (!ctx) {
|
|
6245
6255
|
const phase = bootstrap?.status(name);
|
|
@@ -6280,13 +6290,13 @@ function buildLegacyRegistry(opts) {
|
|
|
6280
6290
|
function registerRoutes(scope, ctx) {
|
|
6281
6291
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
6282
6292
|
scope.get("/events", (req2, reply) => {
|
|
6283
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6293
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6284
6294
|
if (!proj) return;
|
|
6285
6295
|
handleSse(req2, reply, { project: proj.name });
|
|
6286
6296
|
});
|
|
6287
6297
|
if (ctx.scope === "project") {
|
|
6288
6298
|
scope.get("/health", async (req2, reply) => {
|
|
6289
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6299
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6290
6300
|
if (!proj) return;
|
|
6291
6301
|
const uptimeMs = Date.now() - startedAt;
|
|
6292
6302
|
return {
|
|
@@ -6304,14 +6314,14 @@ function registerRoutes(scope, ctx) {
|
|
|
6304
6314
|
});
|
|
6305
6315
|
}
|
|
6306
6316
|
scope.get("/graph", async (req2, reply) => {
|
|
6307
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6317
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6308
6318
|
if (!proj) return;
|
|
6309
6319
|
return serializeGraph(proj.graph);
|
|
6310
6320
|
});
|
|
6311
6321
|
scope.get(
|
|
6312
6322
|
"/graph/node/:id",
|
|
6313
6323
|
async (req2, reply) => {
|
|
6314
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6324
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6315
6325
|
if (!proj) return;
|
|
6316
6326
|
const { id } = req2.params;
|
|
6317
6327
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6323,7 +6333,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6323
6333
|
scope.get(
|
|
6324
6334
|
"/graph/edges/:id",
|
|
6325
6335
|
async (req2, reply) => {
|
|
6326
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6336
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6327
6337
|
if (!proj) return;
|
|
6328
6338
|
const { id } = req2.params;
|
|
6329
6339
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6335,7 +6345,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6335
6345
|
}
|
|
6336
6346
|
);
|
|
6337
6347
|
scope.get("/graph/dependencies/:nodeId", async (req2, reply) => {
|
|
6338
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6348
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6339
6349
|
if (!proj) return;
|
|
6340
6350
|
const { nodeId } = req2.params;
|
|
6341
6351
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6350,7 +6360,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6350
6360
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6351
6361
|
});
|
|
6352
6362
|
scope.get("/graph/divergences", async (req2, reply) => {
|
|
6353
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6363
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6354
6364
|
if (!proj) return;
|
|
6355
6365
|
let typeFilter;
|
|
6356
6366
|
if (req2.query.type) {
|
|
@@ -6385,7 +6395,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6385
6395
|
});
|
|
6386
6396
|
});
|
|
6387
6397
|
scope.get("/incidents", async (req2, reply) => {
|
|
6388
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6398
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6389
6399
|
if (!proj) return;
|
|
6390
6400
|
const epath = errorsPathFor(proj);
|
|
6391
6401
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -6397,7 +6407,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6397
6407
|
return { count: sliced.length, total, events: sliced };
|
|
6398
6408
|
});
|
|
6399
6409
|
scope.get("/stale-events", async (req2, reply) => {
|
|
6400
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6410
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6401
6411
|
if (!proj) return;
|
|
6402
6412
|
const spath = staleEventsPathFor(proj);
|
|
6403
6413
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -6412,7 +6422,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6412
6422
|
scope.get(
|
|
6413
6423
|
"/incidents/:nodeId",
|
|
6414
6424
|
async (req2, reply) => {
|
|
6415
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6425
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6416
6426
|
if (!proj) return;
|
|
6417
6427
|
const { nodeId } = req2.params;
|
|
6418
6428
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6428,7 +6438,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6428
6438
|
}
|
|
6429
6439
|
);
|
|
6430
6440
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
6431
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6441
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6432
6442
|
if (!proj) return;
|
|
6433
6443
|
const { nodeId } = req2.params;
|
|
6434
6444
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6448,7 +6458,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6448
6458
|
return result;
|
|
6449
6459
|
});
|
|
6450
6460
|
scope.get("/graph/blast-radius/:nodeId", async (req2, reply) => {
|
|
6451
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6461
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6452
6462
|
if (!proj) return;
|
|
6453
6463
|
const { nodeId } = req2.params;
|
|
6454
6464
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6461,7 +6471,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6461
6471
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
6462
6472
|
});
|
|
6463
6473
|
scope.get("/search", async (req2, reply) => {
|
|
6464
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6474
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6465
6475
|
if (!proj) return;
|
|
6466
6476
|
const raw = (req2.query.q ?? "").trim();
|
|
6467
6477
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -6492,7 +6502,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6492
6502
|
scope.get(
|
|
6493
6503
|
"/graph/diff",
|
|
6494
6504
|
async (req2, reply) => {
|
|
6495
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6505
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6496
6506
|
if (!proj) return;
|
|
6497
6507
|
const against = req2.query.against;
|
|
6498
6508
|
if (!against) {
|
|
@@ -6507,7 +6517,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6507
6517
|
}
|
|
6508
6518
|
);
|
|
6509
6519
|
scope.post("/snapshot", async (req2, reply) => {
|
|
6510
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6520
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6511
6521
|
if (!proj) return;
|
|
6512
6522
|
const body = req2.body;
|
|
6513
6523
|
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
@@ -6536,7 +6546,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6536
6546
|
}
|
|
6537
6547
|
});
|
|
6538
6548
|
scope.post("/graph/scan", async (req2, reply) => {
|
|
6539
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6549
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6540
6550
|
if (!proj) return;
|
|
6541
6551
|
if (!proj.scanPath) {
|
|
6542
6552
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6552,7 +6562,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6552
6562
|
};
|
|
6553
6563
|
});
|
|
6554
6564
|
scope.get("/policies", async (req2, reply) => {
|
|
6555
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6565
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6556
6566
|
if (!proj) return;
|
|
6557
6567
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
6558
6568
|
if (!policyPath) {
|
|
@@ -6569,7 +6579,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6569
6579
|
}
|
|
6570
6580
|
});
|
|
6571
6581
|
scope.get("/policies/violations", async (req2, reply) => {
|
|
6572
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6582
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6573
6583
|
if (!proj) return;
|
|
6574
6584
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6575
6585
|
let violations = await log.readAll();
|
|
@@ -6589,7 +6599,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6589
6599
|
return { violations };
|
|
6590
6600
|
});
|
|
6591
6601
|
scope.post("/policies/check", async (req2, reply) => {
|
|
6592
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6602
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6593
6603
|
if (!proj) return;
|
|
6594
6604
|
const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
6595
6605
|
if (!parsed.success) {
|
|
@@ -6625,7 +6635,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6625
6635
|
};
|
|
6626
6636
|
});
|
|
6627
6637
|
scope.get("/extend/list-uninstrumented", async (req2, reply) => {
|
|
6628
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6638
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6629
6639
|
if (!proj) return;
|
|
6630
6640
|
if (!proj.scanPath) {
|
|
6631
6641
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6638,7 +6648,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6638
6648
|
}
|
|
6639
6649
|
});
|
|
6640
6650
|
scope.get("/extend/lookup", async (req2, reply) => {
|
|
6641
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6651
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6642
6652
|
if (!proj) return;
|
|
6643
6653
|
const { library, version } = req2.query;
|
|
6644
6654
|
if (!library) {
|
|
@@ -6651,7 +6661,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6651
6661
|
return result;
|
|
6652
6662
|
});
|
|
6653
6663
|
scope.get("/extend/describe", async (req2, reply) => {
|
|
6654
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6664
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6655
6665
|
if (!proj) return;
|
|
6656
6666
|
if (!proj.scanPath) {
|
|
6657
6667
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6664,7 +6674,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6664
6674
|
}
|
|
6665
6675
|
});
|
|
6666
6676
|
scope.post("/extend/apply", async (req2, reply) => {
|
|
6667
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6677
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6668
6678
|
if (!proj) return;
|
|
6669
6679
|
if (!proj.scanPath) {
|
|
6670
6680
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6684,7 +6694,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6684
6694
|
}
|
|
6685
6695
|
});
|
|
6686
6696
|
scope.post("/extend/dry-run", async (req2, reply) => {
|
|
6687
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6697
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6688
6698
|
if (!proj) return;
|
|
6689
6699
|
if (!proj.scanPath) {
|
|
6690
6700
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6704,7 +6714,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6704
6714
|
}
|
|
6705
6715
|
});
|
|
6706
6716
|
scope.post("/extend/rollback", async (req2, reply) => {
|
|
6707
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6717
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6708
6718
|
if (!proj) return;
|
|
6709
6719
|
if (!proj.scanPath) {
|
|
6710
6720
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6767,7 +6777,8 @@ async function buildApi(opts) {
|
|
|
6767
6777
|
errorsPathFor,
|
|
6768
6778
|
staleEventsPathFor,
|
|
6769
6779
|
policyFilePathFor,
|
|
6770
|
-
bootstrap: opts.bootstrap
|
|
6780
|
+
bootstrap: opts.bootstrap,
|
|
6781
|
+
singleProject: opts.singleProject?.name
|
|
6771
6782
|
};
|
|
6772
6783
|
app.get("/health", async () => {
|
|
6773
6784
|
const uptimeMs = Date.now() - startedAt;
|
|
@@ -6790,10 +6801,29 @@ async function buildApi(opts) {
|
|
|
6790
6801
|
return {
|
|
6791
6802
|
ok: true,
|
|
6792
6803
|
uptimeMs,
|
|
6804
|
+
// ADR-096 §7 — a per-project daemon carries its project at the top level
|
|
6805
|
+
// so a spawn can confirm a daemon found on a reused port is serving THIS
|
|
6806
|
+
// project before reusing it. The legacy multi-project daemon omits it and
|
|
6807
|
+
// is matched against the `projects` array instead.
|
|
6808
|
+
...opts.singleProject ? { project: opts.singleProject.name } : {},
|
|
6793
6809
|
projects
|
|
6794
6810
|
};
|
|
6795
6811
|
});
|
|
6796
6812
|
app.get("/projects", async (_req, reply) => {
|
|
6813
|
+
if (opts.singleProject) {
|
|
6814
|
+
const phase = opts.bootstrap?.status(opts.singleProject.name);
|
|
6815
|
+
const entry2 = {
|
|
6816
|
+
name: opts.singleProject.name,
|
|
6817
|
+
path: opts.singleProject.path,
|
|
6818
|
+
registeredAt: new Date(startedAt).toISOString(),
|
|
6819
|
+
languages: [],
|
|
6820
|
+
// The daemon is serving this project, so it's active unless its
|
|
6821
|
+
// bootstrap broke. ('bootstrapping' still reads as active — the project
|
|
6822
|
+
// is the one to land on; its graph route answers 503 until ready.)
|
|
6823
|
+
status: phase === "broken" ? "broken" : "active"
|
|
6824
|
+
};
|
|
6825
|
+
return [entry2];
|
|
6826
|
+
}
|
|
6797
6827
|
try {
|
|
6798
6828
|
return await listProjects();
|
|
6799
6829
|
} catch (err) {
|
|
@@ -6987,7 +7017,7 @@ async function bootstrapProject(entry2) {
|
|
|
6987
7017
|
const detachEvents = attachGraphToEventBus(graph, { project: entry2.name });
|
|
6988
7018
|
try {
|
|
6989
7019
|
await extractFromDirectory(graph, entry2.path);
|
|
6990
|
-
const stopPersist = startPersistLoop(graph, outPath);
|
|
7020
|
+
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
6991
7021
|
await touchLastSeen(entry2.name).catch(() => {
|
|
6992
7022
|
});
|
|
6993
7023
|
return {
|
|
@@ -7243,22 +7273,14 @@ async function startDaemon(opts = {}) {
|
|
|
7243
7273
|
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
7244
7274
|
}));
|
|
7245
7275
|
}
|
|
7246
|
-
}
|
|
7276
|
+
},
|
|
7277
|
+
// ADR-096 §4/§5/§7 — hand the daemon's identity to buildApi so the REST
|
|
7278
|
+
// surface reflects "the daemon is the project": `GET /projects` reports
|
|
7279
|
+
// only this project (the dashboard pins to it), and the daemon-wide
|
|
7280
|
+
// `/health` carries it at the top level for the spawn-reuse identity
|
|
7281
|
+
// check. Absent for the legacy multi-project daemon.
|
|
7282
|
+
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7247
7283
|
});
|
|
7248
|
-
if (singleProject) {
|
|
7249
|
-
restApp.addHook("onSend", async (req2, _reply, payload) => {
|
|
7250
|
-
if (req2.url.split("?")[0] !== "/health") return payload;
|
|
7251
|
-
if (typeof payload !== "string") return payload;
|
|
7252
|
-
try {
|
|
7253
|
-
const body = JSON.parse(payload);
|
|
7254
|
-
if (typeof body !== "object" || body === null) return payload;
|
|
7255
|
-
body.project = singleProject;
|
|
7256
|
-
return JSON.stringify(body);
|
|
7257
|
-
} catch {
|
|
7258
|
-
return payload;
|
|
7259
|
-
}
|
|
7260
|
-
});
|
|
7261
|
-
}
|
|
7262
7284
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7263
7285
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
7264
7286
|
} catch (err) {
|
|
@@ -7510,6 +7532,12 @@ async function startDaemon(opts = {}) {
|
|
|
7510
7532
|
});
|
|
7511
7533
|
if (restApp) await restApp.close().catch(() => {
|
|
7512
7534
|
});
|
|
7535
|
+
for (const slot of slots.values()) {
|
|
7536
|
+
if (slot.status === "active" && slot.outPath) {
|
|
7537
|
+
await saveGraphToDisk(slot.graph, slot.outPath).catch(() => {
|
|
7538
|
+
});
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7513
7541
|
for (const slot of slots.values()) {
|
|
7514
7542
|
teardownSlot(slot);
|
|
7515
7543
|
}
|
|
@@ -7667,7 +7695,10 @@ async function spawnWebUI(restPort, opts = {}) {
|
|
|
7667
7695
|
})();
|
|
7668
7696
|
return starting;
|
|
7669
7697
|
}
|
|
7698
|
+
const liveSockets = /* @__PURE__ */ new Set();
|
|
7670
7699
|
const front = import_node_net.default.createServer((socket) => {
|
|
7700
|
+
liveSockets.add(socket);
|
|
7701
|
+
socket.on("close", () => liveSockets.delete(socket));
|
|
7671
7702
|
socket.on("error", () => socket.destroy());
|
|
7672
7703
|
ensureStarted().then(() => {
|
|
7673
7704
|
if (stopped) {
|
|
@@ -7693,7 +7724,11 @@ async function spawnWebUI(restPort, opts = {}) {
|
|
|
7693
7724
|
async function stop() {
|
|
7694
7725
|
if (stopped) return;
|
|
7695
7726
|
stopped = true;
|
|
7696
|
-
await new Promise((resolve) =>
|
|
7727
|
+
await new Promise((resolve) => {
|
|
7728
|
+
front.close(() => resolve());
|
|
7729
|
+
for (const socket of liveSockets) socket.destroy();
|
|
7730
|
+
liveSockets.clear();
|
|
7731
|
+
});
|
|
7697
7732
|
if (!child || !child.pid) return;
|
|
7698
7733
|
try {
|
|
7699
7734
|
child.kill("SIGTERM");
|