@neat.is/core 0.4.17 → 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-6CO7C4IU.js → chunk-BGPWBRLU.js} +4 -2
- package/dist/{chunk-6CO7C4IU.js.map → chunk-BGPWBRLU.js.map} +1 -1
- package/dist/{chunk-GHPHVXYM.js → chunk-MTXF77TN.js} +2 -2
- package/dist/{chunk-LUDSPX5N.js → chunk-Q5EDVWKZ.js} +178 -39
- package/dist/{chunk-LUDSPX5N.js.map → chunk-Q5EDVWKZ.js.map} +1 -1
- package/dist/{chunk-CEDXXMGO.js → chunk-ZV7GHZ2D.js} +208 -24
- package/dist/chunk-ZV7GHZ2D.js.map +1 -0
- package/dist/cli.cjs +520 -172
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +256 -70
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +260 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -14
- package/dist/index.d.ts +52 -14
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +429 -96
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +168 -37
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-QAISVAY5.js → otel-grpc-I67ONCRM.js} +3 -3
- package/dist/server.cjs +69 -37
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-CEDXXMGO.js.map +0 -1
- /package/dist/{chunk-GHPHVXYM.js.map → chunk-MTXF77TN.js.map} +0 -0
- /package/dist/{otel-grpc-QAISVAY5.js.map → otel-grpc-I67ONCRM.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
|
|
|
473
473
|
for (const s of spans) projectQueue.push({ project, span: s });
|
|
474
474
|
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
475
475
|
};
|
|
476
|
+
const offersProjectRouting = opts.onProjectSpan !== void 0;
|
|
476
477
|
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
477
478
|
function warnLegacyEndpoint(serviceName) {
|
|
479
|
+
if (!offersProjectRouting) return;
|
|
478
480
|
if (legacyEndpointWarned.has(serviceName)) return;
|
|
479
481
|
legacyEndpointWarned.add(serviceName);
|
|
480
482
|
console.warn(
|
|
@@ -5347,7 +5349,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
5347
5349
|
graph.clear();
|
|
5348
5350
|
graph.import(payload.graph);
|
|
5349
5351
|
}
|
|
5350
|
-
function startPersistLoop(graph, outPath,
|
|
5352
|
+
function startPersistLoop(graph, outPath, opts = {}) {
|
|
5353
|
+
const intervalMs = opts.intervalMs ?? 6e4;
|
|
5354
|
+
const exitOnSignal = opts.exitOnSignal ?? true;
|
|
5351
5355
|
let stopped = false;
|
|
5352
5356
|
const tick = async () => {
|
|
5353
5357
|
if (stopped) return;
|
|
@@ -5360,7 +5364,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5360
5364
|
const interval = setInterval(() => {
|
|
5361
5365
|
void tick();
|
|
5362
5366
|
}, intervalMs);
|
|
5363
|
-
const onSignal = (signal) => {
|
|
5367
|
+
const onSignal = exitOnSignal ? (signal) => {
|
|
5364
5368
|
void (async () => {
|
|
5365
5369
|
try {
|
|
5366
5370
|
await saveGraphToDisk(graph, outPath);
|
|
@@ -5370,14 +5374,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5370
5374
|
process.exit(0);
|
|
5371
5375
|
}
|
|
5372
5376
|
})();
|
|
5373
|
-
};
|
|
5374
|
-
|
|
5375
|
-
|
|
5377
|
+
} : null;
|
|
5378
|
+
if (onSignal) {
|
|
5379
|
+
process.on("SIGTERM", onSignal);
|
|
5380
|
+
process.on("SIGINT", onSignal);
|
|
5381
|
+
}
|
|
5376
5382
|
return () => {
|
|
5377
5383
|
stopped = true;
|
|
5378
5384
|
clearInterval(interval);
|
|
5379
|
-
|
|
5380
|
-
|
|
5385
|
+
if (onSignal) {
|
|
5386
|
+
process.off("SIGTERM", onSignal);
|
|
5387
|
+
process.off("SIGINT", onSignal);
|
|
5388
|
+
}
|
|
5381
5389
|
};
|
|
5382
5390
|
}
|
|
5383
5391
|
|
|
@@ -6428,12 +6436,16 @@ function serializeGraph(graph) {
|
|
|
6428
6436
|
});
|
|
6429
6437
|
return { nodes, edges };
|
|
6430
6438
|
}
|
|
6431
|
-
function projectFromReq(req) {
|
|
6439
|
+
function projectFromReq(req, singleProject) {
|
|
6432
6440
|
const params = req.params;
|
|
6433
|
-
|
|
6441
|
+
const named = params.project;
|
|
6442
|
+
if (singleProject) {
|
|
6443
|
+
return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
|
|
6444
|
+
}
|
|
6445
|
+
return named ?? DEFAULT_PROJECT;
|
|
6434
6446
|
}
|
|
6435
|
-
function resolveProject(registry, req, reply, bootstrap) {
|
|
6436
|
-
const name = projectFromReq(req);
|
|
6447
|
+
function resolveProject(registry, req, reply, bootstrap, singleProject) {
|
|
6448
|
+
const name = projectFromReq(req, singleProject);
|
|
6437
6449
|
const ctx = registry.get(name);
|
|
6438
6450
|
if (!ctx) {
|
|
6439
6451
|
const phase = bootstrap?.status(name);
|
|
@@ -6474,13 +6486,13 @@ function buildLegacyRegistry(opts) {
|
|
|
6474
6486
|
function registerRoutes(scope, ctx) {
|
|
6475
6487
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
6476
6488
|
scope.get("/events", (req, reply) => {
|
|
6477
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6489
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6478
6490
|
if (!proj) return;
|
|
6479
6491
|
handleSse(req, reply, { project: proj.name });
|
|
6480
6492
|
});
|
|
6481
6493
|
if (ctx.scope === "project") {
|
|
6482
6494
|
scope.get("/health", async (req, reply) => {
|
|
6483
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6495
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6484
6496
|
if (!proj) return;
|
|
6485
6497
|
const uptimeMs = Date.now() - startedAt;
|
|
6486
6498
|
return {
|
|
@@ -6498,14 +6510,14 @@ function registerRoutes(scope, ctx) {
|
|
|
6498
6510
|
});
|
|
6499
6511
|
}
|
|
6500
6512
|
scope.get("/graph", async (req, reply) => {
|
|
6501
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6513
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6502
6514
|
if (!proj) return;
|
|
6503
6515
|
return serializeGraph(proj.graph);
|
|
6504
6516
|
});
|
|
6505
6517
|
scope.get(
|
|
6506
6518
|
"/graph/node/:id",
|
|
6507
6519
|
async (req, reply) => {
|
|
6508
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6520
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6509
6521
|
if (!proj) return;
|
|
6510
6522
|
const { id } = req.params;
|
|
6511
6523
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6517,7 +6529,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6517
6529
|
scope.get(
|
|
6518
6530
|
"/graph/edges/:id",
|
|
6519
6531
|
async (req, reply) => {
|
|
6520
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6532
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6521
6533
|
if (!proj) return;
|
|
6522
6534
|
const { id } = req.params;
|
|
6523
6535
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6529,7 +6541,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6529
6541
|
}
|
|
6530
6542
|
);
|
|
6531
6543
|
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
6532
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6544
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6533
6545
|
if (!proj) return;
|
|
6534
6546
|
const { nodeId } = req.params;
|
|
6535
6547
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6544,7 +6556,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6544
6556
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6545
6557
|
});
|
|
6546
6558
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6547
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6559
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6548
6560
|
if (!proj) return;
|
|
6549
6561
|
let typeFilter;
|
|
6550
6562
|
if (req.query.type) {
|
|
@@ -6579,7 +6591,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6579
6591
|
});
|
|
6580
6592
|
});
|
|
6581
6593
|
scope.get("/incidents", async (req, reply) => {
|
|
6582
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6594
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6583
6595
|
if (!proj) return;
|
|
6584
6596
|
const epath = errorsPathFor(proj);
|
|
6585
6597
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -6591,7 +6603,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6591
6603
|
return { count: sliced.length, total, events: sliced };
|
|
6592
6604
|
});
|
|
6593
6605
|
scope.get("/stale-events", async (req, reply) => {
|
|
6594
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6606
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6595
6607
|
if (!proj) return;
|
|
6596
6608
|
const spath = staleEventsPathFor(proj);
|
|
6597
6609
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -6606,7 +6618,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6606
6618
|
scope.get(
|
|
6607
6619
|
"/incidents/:nodeId",
|
|
6608
6620
|
async (req, reply) => {
|
|
6609
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6621
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6610
6622
|
if (!proj) return;
|
|
6611
6623
|
const { nodeId } = req.params;
|
|
6612
6624
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6622,7 +6634,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6622
6634
|
}
|
|
6623
6635
|
);
|
|
6624
6636
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6625
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6637
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6626
6638
|
if (!proj) return;
|
|
6627
6639
|
const { nodeId } = req.params;
|
|
6628
6640
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6642,7 +6654,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6642
6654
|
return result;
|
|
6643
6655
|
});
|
|
6644
6656
|
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
6645
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6657
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6646
6658
|
if (!proj) return;
|
|
6647
6659
|
const { nodeId } = req.params;
|
|
6648
6660
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6655,7 +6667,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6655
6667
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
6656
6668
|
});
|
|
6657
6669
|
scope.get("/search", async (req, reply) => {
|
|
6658
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6670
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6659
6671
|
if (!proj) return;
|
|
6660
6672
|
const raw = (req.query.q ?? "").trim();
|
|
6661
6673
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -6686,7 +6698,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6686
6698
|
scope.get(
|
|
6687
6699
|
"/graph/diff",
|
|
6688
6700
|
async (req, reply) => {
|
|
6689
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6701
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6690
6702
|
if (!proj) return;
|
|
6691
6703
|
const against = req.query.against;
|
|
6692
6704
|
if (!against) {
|
|
@@ -6701,7 +6713,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6701
6713
|
}
|
|
6702
6714
|
);
|
|
6703
6715
|
scope.post("/snapshot", async (req, reply) => {
|
|
6704
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6716
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6705
6717
|
if (!proj) return;
|
|
6706
6718
|
const body = req.body;
|
|
6707
6719
|
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
@@ -6730,7 +6742,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6730
6742
|
}
|
|
6731
6743
|
});
|
|
6732
6744
|
scope.post("/graph/scan", async (req, reply) => {
|
|
6733
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6745
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6734
6746
|
if (!proj) return;
|
|
6735
6747
|
if (!proj.scanPath) {
|
|
6736
6748
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6746,7 +6758,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6746
6758
|
};
|
|
6747
6759
|
});
|
|
6748
6760
|
scope.get("/policies", async (req, reply) => {
|
|
6749
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6761
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6750
6762
|
if (!proj) return;
|
|
6751
6763
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
6752
6764
|
if (!policyPath) {
|
|
@@ -6763,7 +6775,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6763
6775
|
}
|
|
6764
6776
|
});
|
|
6765
6777
|
scope.get("/policies/violations", async (req, reply) => {
|
|
6766
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6778
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6767
6779
|
if (!proj) return;
|
|
6768
6780
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6769
6781
|
let violations = await log.readAll();
|
|
@@ -6783,7 +6795,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6783
6795
|
return { violations };
|
|
6784
6796
|
});
|
|
6785
6797
|
scope.post("/policies/check", async (req, reply) => {
|
|
6786
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6798
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6787
6799
|
if (!proj) return;
|
|
6788
6800
|
const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6789
6801
|
if (!parsed.success) {
|
|
@@ -6819,7 +6831,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6819
6831
|
};
|
|
6820
6832
|
});
|
|
6821
6833
|
scope.get("/extend/list-uninstrumented", async (req, reply) => {
|
|
6822
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6834
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6823
6835
|
if (!proj) return;
|
|
6824
6836
|
if (!proj.scanPath) {
|
|
6825
6837
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6832,7 +6844,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6832
6844
|
}
|
|
6833
6845
|
});
|
|
6834
6846
|
scope.get("/extend/lookup", async (req, reply) => {
|
|
6835
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6847
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6836
6848
|
if (!proj) return;
|
|
6837
6849
|
const { library, version } = req.query;
|
|
6838
6850
|
if (!library) {
|
|
@@ -6845,7 +6857,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6845
6857
|
return result;
|
|
6846
6858
|
});
|
|
6847
6859
|
scope.get("/extend/describe", async (req, reply) => {
|
|
6848
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6860
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6849
6861
|
if (!proj) return;
|
|
6850
6862
|
if (!proj.scanPath) {
|
|
6851
6863
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6858,7 +6870,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6858
6870
|
}
|
|
6859
6871
|
});
|
|
6860
6872
|
scope.post("/extend/apply", async (req, reply) => {
|
|
6861
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6873
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6862
6874
|
if (!proj) return;
|
|
6863
6875
|
if (!proj.scanPath) {
|
|
6864
6876
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6878,7 +6890,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6878
6890
|
}
|
|
6879
6891
|
});
|
|
6880
6892
|
scope.post("/extend/dry-run", async (req, reply) => {
|
|
6881
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6893
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6882
6894
|
if (!proj) return;
|
|
6883
6895
|
if (!proj.scanPath) {
|
|
6884
6896
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6898,7 +6910,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6898
6910
|
}
|
|
6899
6911
|
});
|
|
6900
6912
|
scope.post("/extend/rollback", async (req, reply) => {
|
|
6901
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6913
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6902
6914
|
if (!proj) return;
|
|
6903
6915
|
if (!proj.scanPath) {
|
|
6904
6916
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6961,7 +6973,8 @@ async function buildApi(opts) {
|
|
|
6961
6973
|
errorsPathFor,
|
|
6962
6974
|
staleEventsPathFor,
|
|
6963
6975
|
policyFilePathFor,
|
|
6964
|
-
bootstrap: opts.bootstrap
|
|
6976
|
+
bootstrap: opts.bootstrap,
|
|
6977
|
+
singleProject: opts.singleProject?.name
|
|
6965
6978
|
};
|
|
6966
6979
|
app.get("/health", async () => {
|
|
6967
6980
|
const uptimeMs = Date.now() - startedAt;
|
|
@@ -6984,10 +6997,29 @@ async function buildApi(opts) {
|
|
|
6984
6997
|
return {
|
|
6985
6998
|
ok: true,
|
|
6986
6999
|
uptimeMs,
|
|
7000
|
+
// ADR-096 §7 — a per-project daemon carries its project at the top level
|
|
7001
|
+
// so a spawn can confirm a daemon found on a reused port is serving THIS
|
|
7002
|
+
// project before reusing it. The legacy multi-project daemon omits it and
|
|
7003
|
+
// is matched against the `projects` array instead.
|
|
7004
|
+
...opts.singleProject ? { project: opts.singleProject.name } : {},
|
|
6987
7005
|
projects
|
|
6988
7006
|
};
|
|
6989
7007
|
});
|
|
6990
7008
|
app.get("/projects", async (_req, reply) => {
|
|
7009
|
+
if (opts.singleProject) {
|
|
7010
|
+
const phase = opts.bootstrap?.status(opts.singleProject.name);
|
|
7011
|
+
const entry = {
|
|
7012
|
+
name: opts.singleProject.name,
|
|
7013
|
+
path: opts.singleProject.path,
|
|
7014
|
+
registeredAt: new Date(startedAt).toISOString(),
|
|
7015
|
+
languages: [],
|
|
7016
|
+
// The daemon is serving this project, so it's active unless its
|
|
7017
|
+
// bootstrap broke. ('bootstrapping' still reads as active — the project
|
|
7018
|
+
// is the one to land on; its graph route answers 503 until ready.)
|
|
7019
|
+
status: phase === "broken" ? "broken" : "active"
|
|
7020
|
+
};
|
|
7021
|
+
return [entry];
|
|
7022
|
+
}
|
|
6991
7023
|
try {
|
|
6992
7024
|
return await listProjects();
|
|
6993
7025
|
} catch (err) {
|
|
@@ -7029,6 +7061,7 @@ init_otel_grpc();
|
|
|
7029
7061
|
init_cjs_shims();
|
|
7030
7062
|
var import_node_fs25 = require("fs");
|
|
7031
7063
|
var import_node_path42 = __toESM(require("path"), 1);
|
|
7064
|
+
var import_node_module = require("module");
|
|
7032
7065
|
init_otel();
|
|
7033
7066
|
init_auth();
|
|
7034
7067
|
|
|
@@ -7054,6 +7087,59 @@ function unroutedErrorsPath(neatHome2) {
|
|
|
7054
7087
|
}
|
|
7055
7088
|
|
|
7056
7089
|
// src/daemon.ts
|
|
7090
|
+
function daemonJsonPath(scanPath) {
|
|
7091
|
+
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
7092
|
+
}
|
|
7093
|
+
function daemonsDiscoveryDir(home) {
|
|
7094
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
7095
|
+
return import_node_path42.default.join(base, "daemons");
|
|
7096
|
+
}
|
|
7097
|
+
function daemonDiscoveryPath(project, home) {
|
|
7098
|
+
return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
7099
|
+
}
|
|
7100
|
+
function sanitizeDiscoveryName(project) {
|
|
7101
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
7102
|
+
}
|
|
7103
|
+
function neatHomeFromEnv() {
|
|
7104
|
+
const env = process.env.NEAT_HOME;
|
|
7105
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
7106
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7107
|
+
return import_node_path42.default.join(home, ".neat");
|
|
7108
|
+
}
|
|
7109
|
+
function resolveNeatVersion() {
|
|
7110
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
7111
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
7112
|
+
}
|
|
7113
|
+
try {
|
|
7114
|
+
const req = (0, import_node_module.createRequire)(importMetaUrl);
|
|
7115
|
+
const pkg = req("../package.json");
|
|
7116
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
7117
|
+
} catch {
|
|
7118
|
+
return "0.0.0";
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
async function writeDaemonRecord(record, home) {
|
|
7122
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
7123
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
7124
|
+
try {
|
|
7125
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
7126
|
+
} catch (err) {
|
|
7127
|
+
console.warn(
|
|
7128
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
7129
|
+
);
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
async function clearDaemonRecord(record, home) {
|
|
7133
|
+
try {
|
|
7134
|
+
const stopped = { ...record, status: "stopped" };
|
|
7135
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
7136
|
+
} catch {
|
|
7137
|
+
}
|
|
7138
|
+
try {
|
|
7139
|
+
await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
7140
|
+
} catch {
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7057
7143
|
function teardownSlot(slot) {
|
|
7058
7144
|
try {
|
|
7059
7145
|
slot.stopPersist();
|
|
@@ -7135,7 +7221,7 @@ async function bootstrapProject(entry) {
|
|
|
7135
7221
|
const detachEvents = attachGraphToEventBus(graph, { project: entry.name });
|
|
7136
7222
|
try {
|
|
7137
7223
|
await extractFromDirectory(graph, entry.path);
|
|
7138
|
-
const stopPersist = startPersistLoop(graph, outPath);
|
|
7224
|
+
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
7139
7225
|
await touchLastSeen(entry.name).catch(() => {
|
|
7140
7226
|
});
|
|
7141
7227
|
return {
|
|
@@ -7170,6 +7256,23 @@ function resolveOtlpPort(opts) {
|
|
|
7170
7256
|
}
|
|
7171
7257
|
return 4318;
|
|
7172
7258
|
}
|
|
7259
|
+
function resolveWebPort() {
|
|
7260
|
+
const env = process.env.NEAT_WEB_PORT;
|
|
7261
|
+
if (env && env.length > 0) {
|
|
7262
|
+
const n = Number.parseInt(env, 10);
|
|
7263
|
+
if (Number.isFinite(n)) return n;
|
|
7264
|
+
}
|
|
7265
|
+
return 6328;
|
|
7266
|
+
}
|
|
7267
|
+
function portFromListenAddress(address, fallback) {
|
|
7268
|
+
try {
|
|
7269
|
+
const port = new URL(address).port;
|
|
7270
|
+
const n = Number.parseInt(port, 10);
|
|
7271
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
7272
|
+
} catch {
|
|
7273
|
+
}
|
|
7274
|
+
return fallback;
|
|
7275
|
+
}
|
|
7173
7276
|
function resolveHost(opts, authTokenSet) {
|
|
7174
7277
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
7175
7278
|
const env = process.env.HOST;
|
|
@@ -7180,13 +7283,24 @@ function resolveHost(opts, authTokenSet) {
|
|
|
7180
7283
|
async function startDaemon(opts = {}) {
|
|
7181
7284
|
const home = neatHomeFor(opts);
|
|
7182
7285
|
const regPath = registryPath();
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7286
|
+
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
7287
|
+
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
7288
|
+
const singleProject = projectArg;
|
|
7289
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
|
|
7290
|
+
if (singleProject && !singleProjectPath) {
|
|
7186
7291
|
throw new Error(
|
|
7187
|
-
`neatd:
|
|
7292
|
+
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
7188
7293
|
);
|
|
7189
7294
|
}
|
|
7295
|
+
if (!singleProject) {
|
|
7296
|
+
try {
|
|
7297
|
+
await import_node_fs25.promises.access(regPath);
|
|
7298
|
+
} catch {
|
|
7299
|
+
throw new Error(
|
|
7300
|
+
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
7301
|
+
);
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7190
7304
|
const pidPath = import_node_path42.default.join(home, "neatd.pid");
|
|
7191
7305
|
await writeAtomically(pidPath, `${process.pid}
|
|
7192
7306
|
`);
|
|
@@ -7275,21 +7389,37 @@ async function startDaemon(opts = {}) {
|
|
|
7275
7389
|
});
|
|
7276
7390
|
}
|
|
7277
7391
|
}
|
|
7392
|
+
async function enumerateProjects() {
|
|
7393
|
+
if (singleProject && singleProjectPath) {
|
|
7394
|
+
return [
|
|
7395
|
+
{
|
|
7396
|
+
name: singleProject,
|
|
7397
|
+
path: singleProjectPath,
|
|
7398
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7399
|
+
languages: [],
|
|
7400
|
+
status: "active"
|
|
7401
|
+
}
|
|
7402
|
+
];
|
|
7403
|
+
}
|
|
7404
|
+
return listProjects();
|
|
7405
|
+
}
|
|
7278
7406
|
async function loadAll() {
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7407
|
+
if (!singleProject) {
|
|
7408
|
+
try {
|
|
7409
|
+
const pruned = await pruneRegistry();
|
|
7410
|
+
for (const entry of pruned) {
|
|
7411
|
+
console.log(
|
|
7412
|
+
`neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
|
|
7413
|
+
);
|
|
7414
|
+
slots.delete(entry.name);
|
|
7415
|
+
bootstrapStatus.delete(entry.name);
|
|
7416
|
+
bootstrapStartedAt.delete(entry.name);
|
|
7417
|
+
}
|
|
7418
|
+
} catch (err) {
|
|
7419
|
+
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7288
7420
|
}
|
|
7289
|
-
} catch (err) {
|
|
7290
|
-
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7291
7421
|
}
|
|
7292
|
-
const projects = await
|
|
7422
|
+
const projects = await enumerateProjects();
|
|
7293
7423
|
const seen = /* @__PURE__ */ new Set();
|
|
7294
7424
|
const pending = [];
|
|
7295
7425
|
for (const entry of projects) {
|
|
@@ -7314,7 +7444,7 @@ async function startDaemon(opts = {}) {
|
|
|
7314
7444
|
}
|
|
7315
7445
|
await Promise.allSettled(pending);
|
|
7316
7446
|
}
|
|
7317
|
-
const initialEntries = await
|
|
7447
|
+
const initialEntries = await enumerateProjects().catch(() => []);
|
|
7318
7448
|
for (const entry of initialEntries) {
|
|
7319
7449
|
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
7320
7450
|
bootstrapStartedAt.set(entry.name, Date.now());
|
|
@@ -7324,6 +7454,7 @@ async function startDaemon(opts = {}) {
|
|
|
7324
7454
|
let otlpApp = null;
|
|
7325
7455
|
let restAddress = "";
|
|
7326
7456
|
let otlpAddress = "";
|
|
7457
|
+
let daemonRecord = null;
|
|
7327
7458
|
if (bind) {
|
|
7328
7459
|
const auth = readAuthEnv();
|
|
7329
7460
|
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
@@ -7346,7 +7477,13 @@ async function startDaemon(opts = {}) {
|
|
|
7346
7477
|
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
7347
7478
|
}));
|
|
7348
7479
|
}
|
|
7349
|
-
}
|
|
7480
|
+
},
|
|
7481
|
+
// ADR-096 §4/§5/§7 — hand the daemon's identity to buildApi so the REST
|
|
7482
|
+
// surface reflects "the daemon is the project": `GET /projects` reports
|
|
7483
|
+
// only this project (the dashboard pins to it), and the daemon-wide
|
|
7484
|
+
// `/health` carries it at the top level for the spawn-reuse identity
|
|
7485
|
+
// check. Absent for the legacy multi-project daemon.
|
|
7486
|
+
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7350
7487
|
});
|
|
7351
7488
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7352
7489
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
@@ -7363,6 +7500,25 @@ async function startDaemon(opts = {}) {
|
|
|
7363
7500
|
);
|
|
7364
7501
|
}
|
|
7365
7502
|
async function resolveTargetSlot(serviceName, traceId) {
|
|
7503
|
+
if (singleProject) {
|
|
7504
|
+
let slot2 = slots.get(singleProject);
|
|
7505
|
+
if (!slot2) {
|
|
7506
|
+
slot2 = await tryRecoverSlot({
|
|
7507
|
+
name: singleProject,
|
|
7508
|
+
path: singleProjectPath,
|
|
7509
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7510
|
+
languages: [],
|
|
7511
|
+
status: "active"
|
|
7512
|
+
});
|
|
7513
|
+
} else if (slot2.status === "broken") {
|
|
7514
|
+
slot2 = await tryRecoverSlot(slot2.entry);
|
|
7515
|
+
}
|
|
7516
|
+
if (!slot2 || slot2.status !== "active") {
|
|
7517
|
+
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7518
|
+
return null;
|
|
7519
|
+
}
|
|
7520
|
+
return slot2;
|
|
7521
|
+
}
|
|
7366
7522
|
const liveEntries = await listProjects().catch(() => []);
|
|
7367
7523
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
7368
7524
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
@@ -7465,6 +7621,42 @@ async function startDaemon(opts = {}) {
|
|
|
7465
7621
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
7466
7622
|
);
|
|
7467
7623
|
}
|
|
7624
|
+
if (singleProject && singleProjectPath) {
|
|
7625
|
+
const ports = {
|
|
7626
|
+
rest: portFromListenAddress(restAddress, restPort),
|
|
7627
|
+
otlp: portFromListenAddress(otlpAddress, otlpPort),
|
|
7628
|
+
// The daemon doesn't bind the web port itself (neatd spawns the web
|
|
7629
|
+
// child); it records the allocated value passed through so the
|
|
7630
|
+
// dashboard and `neat ps` agree on where to look.
|
|
7631
|
+
web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
|
|
7632
|
+
};
|
|
7633
|
+
daemonRecord = {
|
|
7634
|
+
project: singleProject,
|
|
7635
|
+
projectPath: singleProjectPath,
|
|
7636
|
+
pid: process.pid,
|
|
7637
|
+
status: "running",
|
|
7638
|
+
ports,
|
|
7639
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7640
|
+
neatVersion: resolveNeatVersion()
|
|
7641
|
+
};
|
|
7642
|
+
try {
|
|
7643
|
+
await writeDaemonRecord(daemonRecord, home);
|
|
7644
|
+
console.log(
|
|
7645
|
+
`neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
|
|
7646
|
+
);
|
|
7647
|
+
} catch (err) {
|
|
7648
|
+
for (const slot of slots.values()) teardownSlot(slot);
|
|
7649
|
+
if (restApp) await restApp.close().catch(() => {
|
|
7650
|
+
});
|
|
7651
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
7652
|
+
});
|
|
7653
|
+
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7654
|
+
});
|
|
7655
|
+
throw new Error(
|
|
7656
|
+
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
7657
|
+
);
|
|
7658
|
+
}
|
|
7659
|
+
}
|
|
7468
7660
|
}
|
|
7469
7661
|
const initialBootstrap = loadAll().catch((err) => {
|
|
7470
7662
|
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
@@ -7504,7 +7696,7 @@ async function startDaemon(opts = {}) {
|
|
|
7504
7696
|
const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
|
|
7505
7697
|
let registryWatcher = null;
|
|
7506
7698
|
let reloadTimer = null;
|
|
7507
|
-
try {
|
|
7699
|
+
if (!singleProject) try {
|
|
7508
7700
|
const regDir = import_node_path42.default.dirname(regPath);
|
|
7509
7701
|
const regBase = import_node_path42.default.basename(regPath);
|
|
7510
7702
|
registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
|
|
@@ -7544,9 +7736,18 @@ async function startDaemon(opts = {}) {
|
|
|
7544
7736
|
});
|
|
7545
7737
|
if (restApp) await restApp.close().catch(() => {
|
|
7546
7738
|
});
|
|
7739
|
+
for (const slot of slots.values()) {
|
|
7740
|
+
if (slot.status === "active" && slot.outPath) {
|
|
7741
|
+
await saveGraphToDisk(slot.graph, slot.outPath).catch(() => {
|
|
7742
|
+
});
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7547
7745
|
for (const slot of slots.values()) {
|
|
7548
7746
|
teardownSlot(slot);
|
|
7549
7747
|
}
|
|
7748
|
+
if (daemonRecord) {
|
|
7749
|
+
await clearDaemonRecord(daemonRecord, home);
|
|
7750
|
+
}
|
|
7550
7751
|
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7551
7752
|
});
|
|
7552
7753
|
};
|
|
@@ -7558,7 +7759,8 @@ async function startDaemon(opts = {}) {
|
|
|
7558
7759
|
restAddress,
|
|
7559
7760
|
otlpAddress,
|
|
7560
7761
|
bootstrap: tracker,
|
|
7561
|
-
initialBootstrap
|
|
7762
|
+
initialBootstrap,
|
|
7763
|
+
daemonRecord
|
|
7562
7764
|
};
|
|
7563
7765
|
}
|
|
7564
7766
|
// Annotate the CommonJS export names for ESM import in node:
|