@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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readDaemonRecord
4
- } from "./chunk-GXWBMJDJ.js";
4
+ } from "./chunk-ZV7GHZ2D.js";
5
5
  import {
6
6
  buildSearchIndex
7
7
  } from "./chunk-XOOCA5T7.js";
@@ -52,7 +52,7 @@ import {
52
52
  signalDaemonStop,
53
53
  startPersistLoop,
54
54
  startStalenessLoop
55
- } from "./chunk-T3X6XJPU.js";
55
+ } from "./chunk-Q5EDVWKZ.js";
56
56
  import {
57
57
  startOtelGrpcReceiver
58
58
  } from "./chunk-MTXF77TN.js";
package/dist/index.cjs CHANGED
@@ -5349,7 +5349,9 @@ async function loadGraphFromDisk(graph, outPath) {
5349
5349
  graph.clear();
5350
5350
  graph.import(payload.graph);
5351
5351
  }
5352
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5352
+ function startPersistLoop(graph, outPath, opts = {}) {
5353
+ const intervalMs = opts.intervalMs ?? 6e4;
5354
+ const exitOnSignal = opts.exitOnSignal ?? true;
5353
5355
  let stopped = false;
5354
5356
  const tick = async () => {
5355
5357
  if (stopped) return;
@@ -5362,7 +5364,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5362
5364
  const interval = setInterval(() => {
5363
5365
  void tick();
5364
5366
  }, intervalMs);
5365
- const onSignal = (signal) => {
5367
+ const onSignal = exitOnSignal ? (signal) => {
5366
5368
  void (async () => {
5367
5369
  try {
5368
5370
  await saveGraphToDisk(graph, outPath);
@@ -5372,14 +5374,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5372
5374
  process.exit(0);
5373
5375
  }
5374
5376
  })();
5375
- };
5376
- process.on("SIGTERM", onSignal);
5377
- process.on("SIGINT", onSignal);
5377
+ } : null;
5378
+ if (onSignal) {
5379
+ process.on("SIGTERM", onSignal);
5380
+ process.on("SIGINT", onSignal);
5381
+ }
5378
5382
  return () => {
5379
5383
  stopped = true;
5380
5384
  clearInterval(interval);
5381
- process.off("SIGTERM", onSignal);
5382
- process.off("SIGINT", onSignal);
5385
+ if (onSignal) {
5386
+ process.off("SIGTERM", onSignal);
5387
+ process.off("SIGINT", onSignal);
5388
+ }
5383
5389
  };
5384
5390
  }
5385
5391
 
@@ -6430,12 +6436,16 @@ function serializeGraph(graph) {
6430
6436
  });
6431
6437
  return { nodes, edges };
6432
6438
  }
6433
- function projectFromReq(req) {
6439
+ function projectFromReq(req, singleProject) {
6434
6440
  const params = req.params;
6435
- return params.project ?? DEFAULT_PROJECT;
6441
+ const named = params.project;
6442
+ if (singleProject) {
6443
+ return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
6444
+ }
6445
+ return named ?? DEFAULT_PROJECT;
6436
6446
  }
6437
- function resolveProject(registry, req, reply, bootstrap) {
6438
- const name = projectFromReq(req);
6447
+ function resolveProject(registry, req, reply, bootstrap, singleProject) {
6448
+ const name = projectFromReq(req, singleProject);
6439
6449
  const ctx = registry.get(name);
6440
6450
  if (!ctx) {
6441
6451
  const phase = bootstrap?.status(name);
@@ -6476,13 +6486,13 @@ function buildLegacyRegistry(opts) {
6476
6486
  function registerRoutes(scope, ctx) {
6477
6487
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
6478
6488
  scope.get("/events", (req, reply) => {
6479
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6489
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6480
6490
  if (!proj) return;
6481
6491
  handleSse(req, reply, { project: proj.name });
6482
6492
  });
6483
6493
  if (ctx.scope === "project") {
6484
6494
  scope.get("/health", async (req, reply) => {
6485
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6495
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6486
6496
  if (!proj) return;
6487
6497
  const uptimeMs = Date.now() - startedAt;
6488
6498
  return {
@@ -6500,14 +6510,14 @@ function registerRoutes(scope, ctx) {
6500
6510
  });
6501
6511
  }
6502
6512
  scope.get("/graph", async (req, reply) => {
6503
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6513
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6504
6514
  if (!proj) return;
6505
6515
  return serializeGraph(proj.graph);
6506
6516
  });
6507
6517
  scope.get(
6508
6518
  "/graph/node/:id",
6509
6519
  async (req, reply) => {
6510
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6520
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6511
6521
  if (!proj) return;
6512
6522
  const { id } = req.params;
6513
6523
  if (!proj.graph.hasNode(id)) {
@@ -6519,7 +6529,7 @@ function registerRoutes(scope, ctx) {
6519
6529
  scope.get(
6520
6530
  "/graph/edges/:id",
6521
6531
  async (req, reply) => {
6522
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6532
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6523
6533
  if (!proj) return;
6524
6534
  const { id } = req.params;
6525
6535
  if (!proj.graph.hasNode(id)) {
@@ -6531,7 +6541,7 @@ function registerRoutes(scope, ctx) {
6531
6541
  }
6532
6542
  );
6533
6543
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
6534
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6544
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6535
6545
  if (!proj) return;
6536
6546
  const { nodeId } = req.params;
6537
6547
  if (!proj.graph.hasNode(nodeId)) {
@@ -6546,7 +6556,7 @@ function registerRoutes(scope, ctx) {
6546
6556
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6547
6557
  });
6548
6558
  scope.get("/graph/divergences", async (req, reply) => {
6549
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6559
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6550
6560
  if (!proj) return;
6551
6561
  let typeFilter;
6552
6562
  if (req.query.type) {
@@ -6581,7 +6591,7 @@ function registerRoutes(scope, ctx) {
6581
6591
  });
6582
6592
  });
6583
6593
  scope.get("/incidents", async (req, reply) => {
6584
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6594
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6585
6595
  if (!proj) return;
6586
6596
  const epath = errorsPathFor(proj);
6587
6597
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -6593,7 +6603,7 @@ function registerRoutes(scope, ctx) {
6593
6603
  return { count: sliced.length, total, events: sliced };
6594
6604
  });
6595
6605
  scope.get("/stale-events", async (req, reply) => {
6596
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6606
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6597
6607
  if (!proj) return;
6598
6608
  const spath = staleEventsPathFor(proj);
6599
6609
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -6608,7 +6618,7 @@ function registerRoutes(scope, ctx) {
6608
6618
  scope.get(
6609
6619
  "/incidents/:nodeId",
6610
6620
  async (req, reply) => {
6611
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6621
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6612
6622
  if (!proj) return;
6613
6623
  const { nodeId } = req.params;
6614
6624
  if (!proj.graph.hasNode(nodeId)) {
@@ -6624,7 +6634,7 @@ function registerRoutes(scope, ctx) {
6624
6634
  }
6625
6635
  );
6626
6636
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6627
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6637
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6628
6638
  if (!proj) return;
6629
6639
  const { nodeId } = req.params;
6630
6640
  if (!proj.graph.hasNode(nodeId)) {
@@ -6644,7 +6654,7 @@ function registerRoutes(scope, ctx) {
6644
6654
  return result;
6645
6655
  });
6646
6656
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
6647
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6657
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6648
6658
  if (!proj) return;
6649
6659
  const { nodeId } = req.params;
6650
6660
  if (!proj.graph.hasNode(nodeId)) {
@@ -6657,7 +6667,7 @@ function registerRoutes(scope, ctx) {
6657
6667
  return getBlastRadius(proj.graph, nodeId, depth);
6658
6668
  });
6659
6669
  scope.get("/search", async (req, reply) => {
6660
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6670
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6661
6671
  if (!proj) return;
6662
6672
  const raw = (req.query.q ?? "").trim();
6663
6673
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -6688,7 +6698,7 @@ function registerRoutes(scope, ctx) {
6688
6698
  scope.get(
6689
6699
  "/graph/diff",
6690
6700
  async (req, reply) => {
6691
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6701
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6692
6702
  if (!proj) return;
6693
6703
  const against = req.query.against;
6694
6704
  if (!against) {
@@ -6703,7 +6713,7 @@ function registerRoutes(scope, ctx) {
6703
6713
  }
6704
6714
  );
6705
6715
  scope.post("/snapshot", async (req, reply) => {
6706
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6716
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6707
6717
  if (!proj) return;
6708
6718
  const body = req.body;
6709
6719
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -6732,7 +6742,7 @@ function registerRoutes(scope, ctx) {
6732
6742
  }
6733
6743
  });
6734
6744
  scope.post("/graph/scan", async (req, reply) => {
6735
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6745
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6736
6746
  if (!proj) return;
6737
6747
  if (!proj.scanPath) {
6738
6748
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6748,7 +6758,7 @@ function registerRoutes(scope, ctx) {
6748
6758
  };
6749
6759
  });
6750
6760
  scope.get("/policies", async (req, reply) => {
6751
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6761
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6752
6762
  if (!proj) return;
6753
6763
  const policyPath = ctx.policyFilePathFor(proj);
6754
6764
  if (!policyPath) {
@@ -6765,7 +6775,7 @@ function registerRoutes(scope, ctx) {
6765
6775
  }
6766
6776
  });
6767
6777
  scope.get("/policies/violations", async (req, reply) => {
6768
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6778
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6769
6779
  if (!proj) return;
6770
6780
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6771
6781
  let violations = await log.readAll();
@@ -6785,7 +6795,7 @@ function registerRoutes(scope, ctx) {
6785
6795
  return { violations };
6786
6796
  });
6787
6797
  scope.post("/policies/check", async (req, reply) => {
6788
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6798
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6789
6799
  if (!proj) return;
6790
6800
  const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6791
6801
  if (!parsed.success) {
@@ -6821,7 +6831,7 @@ function registerRoutes(scope, ctx) {
6821
6831
  };
6822
6832
  });
6823
6833
  scope.get("/extend/list-uninstrumented", async (req, reply) => {
6824
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6834
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6825
6835
  if (!proj) return;
6826
6836
  if (!proj.scanPath) {
6827
6837
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6834,7 +6844,7 @@ function registerRoutes(scope, ctx) {
6834
6844
  }
6835
6845
  });
6836
6846
  scope.get("/extend/lookup", async (req, reply) => {
6837
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6847
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6838
6848
  if (!proj) return;
6839
6849
  const { library, version } = req.query;
6840
6850
  if (!library) {
@@ -6847,7 +6857,7 @@ function registerRoutes(scope, ctx) {
6847
6857
  return result;
6848
6858
  });
6849
6859
  scope.get("/extend/describe", async (req, reply) => {
6850
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6860
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6851
6861
  if (!proj) return;
6852
6862
  if (!proj.scanPath) {
6853
6863
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6860,7 +6870,7 @@ function registerRoutes(scope, ctx) {
6860
6870
  }
6861
6871
  });
6862
6872
  scope.post("/extend/apply", async (req, reply) => {
6863
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6873
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6864
6874
  if (!proj) return;
6865
6875
  if (!proj.scanPath) {
6866
6876
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6880,7 +6890,7 @@ function registerRoutes(scope, ctx) {
6880
6890
  }
6881
6891
  });
6882
6892
  scope.post("/extend/dry-run", async (req, reply) => {
6883
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6893
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6884
6894
  if (!proj) return;
6885
6895
  if (!proj.scanPath) {
6886
6896
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6900,7 +6910,7 @@ function registerRoutes(scope, ctx) {
6900
6910
  }
6901
6911
  });
6902
6912
  scope.post("/extend/rollback", async (req, reply) => {
6903
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6913
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6904
6914
  if (!proj) return;
6905
6915
  if (!proj.scanPath) {
6906
6916
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6963,7 +6973,8 @@ async function buildApi(opts) {
6963
6973
  errorsPathFor,
6964
6974
  staleEventsPathFor,
6965
6975
  policyFilePathFor,
6966
- bootstrap: opts.bootstrap
6976
+ bootstrap: opts.bootstrap,
6977
+ singleProject: opts.singleProject?.name
6967
6978
  };
6968
6979
  app.get("/health", async () => {
6969
6980
  const uptimeMs = Date.now() - startedAt;
@@ -6986,10 +6997,29 @@ async function buildApi(opts) {
6986
6997
  return {
6987
6998
  ok: true,
6988
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 } : {},
6989
7005
  projects
6990
7006
  };
6991
7007
  });
6992
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
+ }
6993
7023
  try {
6994
7024
  return await listProjects();
6995
7025
  } catch (err) {
@@ -7191,7 +7221,7 @@ async function bootstrapProject(entry) {
7191
7221
  const detachEvents = attachGraphToEventBus(graph, { project: entry.name });
7192
7222
  try {
7193
7223
  await extractFromDirectory(graph, entry.path);
7194
- const stopPersist = startPersistLoop(graph, outPath);
7224
+ const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
7195
7225
  await touchLastSeen(entry.name).catch(() => {
7196
7226
  });
7197
7227
  return {
@@ -7447,22 +7477,14 @@ async function startDaemon(opts = {}) {
7447
7477
  elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
7448
7478
  }));
7449
7479
  }
7450
- }
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
7451
7487
  });
7452
- if (singleProject) {
7453
- restApp.addHook("onSend", async (req, _reply, payload) => {
7454
- if (req.url.split("?")[0] !== "/health") return payload;
7455
- if (typeof payload !== "string") return payload;
7456
- try {
7457
- const body = JSON.parse(payload);
7458
- if (typeof body !== "object" || body === null) return payload;
7459
- body.project = singleProject;
7460
- return JSON.stringify(body);
7461
- } catch {
7462
- return payload;
7463
- }
7464
- });
7465
- }
7466
7488
  restAddress = await restApp.listen({ port: restPort, host });
7467
7489
  console.log(`neatd: REST listening on ${restAddress}`);
7468
7490
  } catch (err) {
@@ -7714,6 +7736,12 @@ async function startDaemon(opts = {}) {
7714
7736
  });
7715
7737
  if (restApp) await restApp.close().catch(() => {
7716
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
+ }
7717
7745
  for (const slot of slots.values()) {
7718
7746
  teardownSlot(slot);
7719
7747
  }