@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/server.cjs CHANGED
@@ -472,8 +472,10 @@ async function buildOtelReceiver(opts) {
472
472
  for (const s of spans) projectQueue.push({ project, span: s });
473
473
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
474
474
  };
475
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
475
476
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
476
477
  function warnLegacyEndpoint(serviceName) {
478
+ if (!offersProjectRouting) return;
477
479
  if (legacyEndpointWarned.has(serviceName)) return;
478
480
  legacyEndpointWarned.add(serviceName);
479
481
  console.warn(
@@ -5849,7 +5851,9 @@ async function loadGraphFromDisk(graph, outPath) {
5849
5851
  graph.clear();
5850
5852
  graph.import(payload.graph);
5851
5853
  }
5852
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5854
+ function startPersistLoop(graph, outPath, opts = {}) {
5855
+ const intervalMs = opts.intervalMs ?? 6e4;
5856
+ const exitOnSignal = opts.exitOnSignal ?? true;
5853
5857
  let stopped = false;
5854
5858
  const tick = async () => {
5855
5859
  if (stopped) return;
@@ -5862,7 +5866,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5862
5866
  const interval = setInterval(() => {
5863
5867
  void tick();
5864
5868
  }, intervalMs);
5865
- const onSignal = (signal) => {
5869
+ const onSignal = exitOnSignal ? (signal) => {
5866
5870
  void (async () => {
5867
5871
  try {
5868
5872
  await saveGraphToDisk(graph, outPath);
@@ -5872,14 +5876,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5872
5876
  process.exit(0);
5873
5877
  }
5874
5878
  })();
5875
- };
5876
- process.on("SIGTERM", onSignal);
5877
- process.on("SIGINT", onSignal);
5879
+ } : null;
5880
+ if (onSignal) {
5881
+ process.on("SIGTERM", onSignal);
5882
+ process.on("SIGINT", onSignal);
5883
+ }
5878
5884
  return () => {
5879
5885
  stopped = true;
5880
5886
  clearInterval(interval);
5881
- process.off("SIGTERM", onSignal);
5882
- process.off("SIGINT", onSignal);
5887
+ if (onSignal) {
5888
+ process.off("SIGTERM", onSignal);
5889
+ process.off("SIGINT", onSignal);
5890
+ }
5883
5891
  };
5884
5892
  }
5885
5893
 
@@ -6047,12 +6055,16 @@ function serializeGraph(graph) {
6047
6055
  });
6048
6056
  return { nodes, edges };
6049
6057
  }
6050
- function projectFromReq(req) {
6058
+ function projectFromReq(req, singleProject) {
6051
6059
  const params = req.params;
6052
- return params.project ?? DEFAULT_PROJECT;
6060
+ const named = params.project;
6061
+ if (singleProject) {
6062
+ return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
6063
+ }
6064
+ return named ?? DEFAULT_PROJECT;
6053
6065
  }
6054
- function resolveProject(registry, req, reply, bootstrap) {
6055
- const name = projectFromReq(req);
6066
+ function resolveProject(registry, req, reply, bootstrap, singleProject) {
6067
+ const name = projectFromReq(req, singleProject);
6056
6068
  const ctx = registry.get(name);
6057
6069
  if (!ctx) {
6058
6070
  const phase = bootstrap?.status(name);
@@ -6093,13 +6105,13 @@ function buildLegacyRegistry(opts) {
6093
6105
  function registerRoutes(scope, ctx) {
6094
6106
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
6095
6107
  scope.get("/events", (req, reply) => {
6096
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6108
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6097
6109
  if (!proj) return;
6098
6110
  handleSse(req, reply, { project: proj.name });
6099
6111
  });
6100
6112
  if (ctx.scope === "project") {
6101
6113
  scope.get("/health", async (req, reply) => {
6102
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6114
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6103
6115
  if (!proj) return;
6104
6116
  const uptimeMs = Date.now() - startedAt;
6105
6117
  return {
@@ -6117,14 +6129,14 @@ function registerRoutes(scope, ctx) {
6117
6129
  });
6118
6130
  }
6119
6131
  scope.get("/graph", async (req, reply) => {
6120
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6132
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6121
6133
  if (!proj) return;
6122
6134
  return serializeGraph(proj.graph);
6123
6135
  });
6124
6136
  scope.get(
6125
6137
  "/graph/node/:id",
6126
6138
  async (req, reply) => {
6127
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6139
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6128
6140
  if (!proj) return;
6129
6141
  const { id } = req.params;
6130
6142
  if (!proj.graph.hasNode(id)) {
@@ -6136,7 +6148,7 @@ function registerRoutes(scope, ctx) {
6136
6148
  scope.get(
6137
6149
  "/graph/edges/:id",
6138
6150
  async (req, reply) => {
6139
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6151
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6140
6152
  if (!proj) return;
6141
6153
  const { id } = req.params;
6142
6154
  if (!proj.graph.hasNode(id)) {
@@ -6148,7 +6160,7 @@ function registerRoutes(scope, ctx) {
6148
6160
  }
6149
6161
  );
6150
6162
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
6151
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6163
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6152
6164
  if (!proj) return;
6153
6165
  const { nodeId } = req.params;
6154
6166
  if (!proj.graph.hasNode(nodeId)) {
@@ -6163,7 +6175,7 @@ function registerRoutes(scope, ctx) {
6163
6175
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6164
6176
  });
6165
6177
  scope.get("/graph/divergences", async (req, reply) => {
6166
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6178
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6167
6179
  if (!proj) return;
6168
6180
  let typeFilter;
6169
6181
  if (req.query.type) {
@@ -6198,7 +6210,7 @@ function registerRoutes(scope, ctx) {
6198
6210
  });
6199
6211
  });
6200
6212
  scope.get("/incidents", async (req, reply) => {
6201
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6213
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6202
6214
  if (!proj) return;
6203
6215
  const epath = errorsPathFor(proj);
6204
6216
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -6210,7 +6222,7 @@ function registerRoutes(scope, ctx) {
6210
6222
  return { count: sliced.length, total, events: sliced };
6211
6223
  });
6212
6224
  scope.get("/stale-events", async (req, reply) => {
6213
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6225
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6214
6226
  if (!proj) return;
6215
6227
  const spath = staleEventsPathFor(proj);
6216
6228
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -6225,7 +6237,7 @@ function registerRoutes(scope, ctx) {
6225
6237
  scope.get(
6226
6238
  "/incidents/:nodeId",
6227
6239
  async (req, reply) => {
6228
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6240
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6229
6241
  if (!proj) return;
6230
6242
  const { nodeId } = req.params;
6231
6243
  if (!proj.graph.hasNode(nodeId)) {
@@ -6241,7 +6253,7 @@ function registerRoutes(scope, ctx) {
6241
6253
  }
6242
6254
  );
6243
6255
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6244
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6256
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6245
6257
  if (!proj) return;
6246
6258
  const { nodeId } = req.params;
6247
6259
  if (!proj.graph.hasNode(nodeId)) {
@@ -6261,7 +6273,7 @@ function registerRoutes(scope, ctx) {
6261
6273
  return result;
6262
6274
  });
6263
6275
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
6264
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6276
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6265
6277
  if (!proj) return;
6266
6278
  const { nodeId } = req.params;
6267
6279
  if (!proj.graph.hasNode(nodeId)) {
@@ -6274,7 +6286,7 @@ function registerRoutes(scope, ctx) {
6274
6286
  return getBlastRadius(proj.graph, nodeId, depth);
6275
6287
  });
6276
6288
  scope.get("/search", async (req, reply) => {
6277
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6289
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6278
6290
  if (!proj) return;
6279
6291
  const raw = (req.query.q ?? "").trim();
6280
6292
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -6305,7 +6317,7 @@ function registerRoutes(scope, ctx) {
6305
6317
  scope.get(
6306
6318
  "/graph/diff",
6307
6319
  async (req, reply) => {
6308
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6320
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6309
6321
  if (!proj) return;
6310
6322
  const against = req.query.against;
6311
6323
  if (!against) {
@@ -6320,7 +6332,7 @@ function registerRoutes(scope, ctx) {
6320
6332
  }
6321
6333
  );
6322
6334
  scope.post("/snapshot", async (req, reply) => {
6323
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6335
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6324
6336
  if (!proj) return;
6325
6337
  const body = req.body;
6326
6338
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -6349,7 +6361,7 @@ function registerRoutes(scope, ctx) {
6349
6361
  }
6350
6362
  });
6351
6363
  scope.post("/graph/scan", async (req, reply) => {
6352
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6364
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6353
6365
  if (!proj) return;
6354
6366
  if (!proj.scanPath) {
6355
6367
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6365,7 +6377,7 @@ function registerRoutes(scope, ctx) {
6365
6377
  };
6366
6378
  });
6367
6379
  scope.get("/policies", async (req, reply) => {
6368
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6380
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6369
6381
  if (!proj) return;
6370
6382
  const policyPath = ctx.policyFilePathFor(proj);
6371
6383
  if (!policyPath) {
@@ -6382,7 +6394,7 @@ function registerRoutes(scope, ctx) {
6382
6394
  }
6383
6395
  });
6384
6396
  scope.get("/policies/violations", async (req, reply) => {
6385
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6397
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6386
6398
  if (!proj) return;
6387
6399
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6388
6400
  let violations = await log.readAll();
@@ -6402,7 +6414,7 @@ function registerRoutes(scope, ctx) {
6402
6414
  return { violations };
6403
6415
  });
6404
6416
  scope.post("/policies/check", async (req, reply) => {
6405
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6417
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6406
6418
  if (!proj) return;
6407
6419
  const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6408
6420
  if (!parsed.success) {
@@ -6438,7 +6450,7 @@ function registerRoutes(scope, ctx) {
6438
6450
  };
6439
6451
  });
6440
6452
  scope.get("/extend/list-uninstrumented", async (req, reply) => {
6441
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6453
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6442
6454
  if (!proj) return;
6443
6455
  if (!proj.scanPath) {
6444
6456
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6451,7 +6463,7 @@ function registerRoutes(scope, ctx) {
6451
6463
  }
6452
6464
  });
6453
6465
  scope.get("/extend/lookup", async (req, reply) => {
6454
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6466
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6455
6467
  if (!proj) return;
6456
6468
  const { library, version } = req.query;
6457
6469
  if (!library) {
@@ -6464,7 +6476,7 @@ function registerRoutes(scope, ctx) {
6464
6476
  return result;
6465
6477
  });
6466
6478
  scope.get("/extend/describe", async (req, reply) => {
6467
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6479
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6468
6480
  if (!proj) return;
6469
6481
  if (!proj.scanPath) {
6470
6482
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6477,7 +6489,7 @@ function registerRoutes(scope, ctx) {
6477
6489
  }
6478
6490
  });
6479
6491
  scope.post("/extend/apply", async (req, reply) => {
6480
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6492
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6481
6493
  if (!proj) return;
6482
6494
  if (!proj.scanPath) {
6483
6495
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6497,7 +6509,7 @@ function registerRoutes(scope, ctx) {
6497
6509
  }
6498
6510
  });
6499
6511
  scope.post("/extend/dry-run", async (req, reply) => {
6500
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6512
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6501
6513
  if (!proj) return;
6502
6514
  if (!proj.scanPath) {
6503
6515
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6517,7 +6529,7 @@ function registerRoutes(scope, ctx) {
6517
6529
  }
6518
6530
  });
6519
6531
  scope.post("/extend/rollback", 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
  if (!proj.scanPath) {
6523
6535
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6580,7 +6592,8 @@ async function buildApi(opts) {
6580
6592
  errorsPathFor,
6581
6593
  staleEventsPathFor,
6582
6594
  policyFilePathFor,
6583
- bootstrap: opts.bootstrap
6595
+ bootstrap: opts.bootstrap,
6596
+ singleProject: opts.singleProject?.name
6584
6597
  };
6585
6598
  app.get("/health", async () => {
6586
6599
  const uptimeMs = Date.now() - startedAt;
@@ -6603,10 +6616,29 @@ async function buildApi(opts) {
6603
6616
  return {
6604
6617
  ok: true,
6605
6618
  uptimeMs,
6619
+ // ADR-096 §7 — a per-project daemon carries its project at the top level
6620
+ // so a spawn can confirm a daemon found on a reused port is serving THIS
6621
+ // project before reusing it. The legacy multi-project daemon omits it and
6622
+ // is matched against the `projects` array instead.
6623
+ ...opts.singleProject ? { project: opts.singleProject.name } : {},
6606
6624
  projects
6607
6625
  };
6608
6626
  });
6609
6627
  app.get("/projects", async (_req, reply) => {
6628
+ if (opts.singleProject) {
6629
+ const phase = opts.bootstrap?.status(opts.singleProject.name);
6630
+ const entry = {
6631
+ name: opts.singleProject.name,
6632
+ path: opts.singleProject.path,
6633
+ registeredAt: new Date(startedAt).toISOString(),
6634
+ languages: [],
6635
+ // The daemon is serving this project, so it's active unless its
6636
+ // bootstrap broke. ('bootstrapping' still reads as active — the project
6637
+ // is the one to land on; its graph route answers 503 until ready.)
6638
+ status: phase === "broken" ? "broken" : "active"
6639
+ };
6640
+ return [entry];
6641
+ }
6610
6642
  try {
6611
6643
  return await listProjects();
6612
6644
  } catch (err) {