@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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-6CO7C4IU.js";
4
+ } from "./chunk-BGPWBRLU.js";
5
5
 
6
6
  // src/graph.ts
7
7
  import GraphDefault from "graphology";
@@ -4980,7 +4980,9 @@ async function loadGraphFromDisk(graph, outPath) {
4980
4980
  graph.clear();
4981
4981
  graph.import(payload.graph);
4982
4982
  }
4983
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4983
+ function startPersistLoop(graph, outPath, opts = {}) {
4984
+ const intervalMs = opts.intervalMs ?? 6e4;
4985
+ const exitOnSignal = opts.exitOnSignal ?? true;
4984
4986
  let stopped = false;
4985
4987
  const tick = async () => {
4986
4988
  if (stopped) return;
@@ -4993,7 +4995,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4993
4995
  const interval = setInterval(() => {
4994
4996
  void tick();
4995
4997
  }, intervalMs);
4996
- const onSignal = (signal) => {
4998
+ const onSignal = exitOnSignal ? (signal) => {
4997
4999
  void (async () => {
4998
5000
  try {
4999
5001
  await saveGraphToDisk(graph, outPath);
@@ -5003,14 +5005,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5003
5005
  process.exit(0);
5004
5006
  }
5005
5007
  })();
5006
- };
5007
- process.on("SIGTERM", onSignal);
5008
- process.on("SIGINT", onSignal);
5008
+ } : null;
5009
+ if (onSignal) {
5010
+ process.on("SIGTERM", onSignal);
5011
+ process.on("SIGINT", onSignal);
5012
+ }
5009
5013
  return () => {
5010
5014
  stopped = true;
5011
5015
  clearInterval(interval);
5012
- process.off("SIGTERM", onSignal);
5013
- process.off("SIGINT", onSignal);
5016
+ if (onSignal) {
5017
+ process.off("SIGTERM", onSignal);
5018
+ process.off("SIGINT", onSignal);
5019
+ }
5014
5020
  };
5015
5021
  }
5016
5022
 
@@ -5168,6 +5174,111 @@ function registryLockPath() {
5168
5174
  function daemonPidPath() {
5169
5175
  return path36.join(neatHome(), "neatd.pid");
5170
5176
  }
5177
+ function daemonsDir() {
5178
+ return path36.join(neatHome(), "daemons");
5179
+ }
5180
+ function isFiniteInt(v) {
5181
+ return typeof v === "number" && Number.isFinite(v);
5182
+ }
5183
+ function parseDaemonRecord(raw) {
5184
+ let obj;
5185
+ try {
5186
+ obj = JSON.parse(raw);
5187
+ } catch {
5188
+ return void 0;
5189
+ }
5190
+ if (typeof obj !== "object" || obj === null) return void 0;
5191
+ const r = obj;
5192
+ const ports = r.ports;
5193
+ if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
5194
+ return void 0;
5195
+ }
5196
+ return {
5197
+ project: r.project,
5198
+ projectPath: r.projectPath,
5199
+ pid: r.pid,
5200
+ status: r.status,
5201
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
5202
+ startedAt: r.startedAt,
5203
+ neatVersion: r.neatVersion
5204
+ };
5205
+ }
5206
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
5207
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5208
+ const dir = daemonsDir();
5209
+ let names;
5210
+ try {
5211
+ names = await fs20.readdir(dir);
5212
+ } catch (err) {
5213
+ if (err.code === "ENOENT") return [];
5214
+ throw err;
5215
+ }
5216
+ const out = [];
5217
+ for (const name of names) {
5218
+ if (!name.endsWith(".json")) continue;
5219
+ const file = path36.join(dir, name);
5220
+ let raw;
5221
+ try {
5222
+ raw = await fs20.readFile(file, "utf8");
5223
+ } catch {
5224
+ continue;
5225
+ }
5226
+ const record = parseDaemonRecord(raw);
5227
+ if (!record) continue;
5228
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
5229
+ out.push({ record, live, source: file });
5230
+ }
5231
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
5232
+ return out;
5233
+ }
5234
+ async function removeDaemonRecord(source) {
5235
+ await fs20.unlink(source).catch(() => {
5236
+ });
5237
+ }
5238
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
5239
+ const discovered = await discoverDaemons(probe);
5240
+ const byPath = /* @__PURE__ */ new Map();
5241
+ for (const d of discovered) {
5242
+ const key = await normalizeProjectPath(d.record.projectPath);
5243
+ byPath.set(key, {
5244
+ project: d.record.project,
5245
+ projectPath: d.record.projectPath,
5246
+ state: d.live ? "running" : "stopped",
5247
+ ports: d.record.ports,
5248
+ pid: d.record.pid
5249
+ });
5250
+ }
5251
+ let legacy = [];
5252
+ try {
5253
+ legacy = (await readRegistry()).projects;
5254
+ } catch {
5255
+ legacy = [];
5256
+ }
5257
+ for (const entry of legacy) {
5258
+ const key = await normalizeProjectPath(entry.path);
5259
+ if (byPath.has(key)) continue;
5260
+ byPath.set(key, {
5261
+ project: entry.name,
5262
+ projectPath: entry.path,
5263
+ state: "registered",
5264
+ registryStatus: entry.status
5265
+ });
5266
+ }
5267
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
5268
+ }
5269
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
5270
+ const discovered = await discoverDaemons(probe);
5271
+ return discovered.find((d) => d.record.project === name);
5272
+ }
5273
+ function signalDaemonStop(pid) {
5274
+ try {
5275
+ process.kill(pid, "SIGTERM");
5276
+ return true;
5277
+ } catch (err) {
5278
+ if (err.code === "ESRCH") return false;
5279
+ throw err;
5280
+ }
5281
+ }
5171
5282
  function isPidAliveDefault(pid) {
5172
5283
  try {
5173
5284
  process.kill(pid, 0);
@@ -5821,12 +5932,16 @@ function serializeGraph(graph) {
5821
5932
  });
5822
5933
  return { nodes, edges };
5823
5934
  }
5824
- function projectFromReq(req) {
5935
+ function projectFromReq(req, singleProject) {
5825
5936
  const params = req.params;
5826
- return params.project ?? DEFAULT_PROJECT;
5937
+ const named = params.project;
5938
+ if (singleProject) {
5939
+ return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
5940
+ }
5941
+ return named ?? DEFAULT_PROJECT;
5827
5942
  }
5828
- function resolveProject(registry, req, reply, bootstrap) {
5829
- const name = projectFromReq(req);
5943
+ function resolveProject(registry, req, reply, bootstrap, singleProject) {
5944
+ const name = projectFromReq(req, singleProject);
5830
5945
  const ctx = registry.get(name);
5831
5946
  if (!ctx) {
5832
5947
  const phase = bootstrap?.status(name);
@@ -5867,13 +5982,13 @@ function buildLegacyRegistry(opts) {
5867
5982
  function registerRoutes(scope, ctx) {
5868
5983
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
5869
5984
  scope.get("/events", (req, reply) => {
5870
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5985
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5871
5986
  if (!proj) return;
5872
5987
  handleSse(req, reply, { project: proj.name });
5873
5988
  });
5874
5989
  if (ctx.scope === "project") {
5875
5990
  scope.get("/health", async (req, reply) => {
5876
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5991
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5877
5992
  if (!proj) return;
5878
5993
  const uptimeMs = Date.now() - startedAt;
5879
5994
  return {
@@ -5891,14 +6006,14 @@ function registerRoutes(scope, ctx) {
5891
6006
  });
5892
6007
  }
5893
6008
  scope.get("/graph", async (req, reply) => {
5894
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6009
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5895
6010
  if (!proj) return;
5896
6011
  return serializeGraph(proj.graph);
5897
6012
  });
5898
6013
  scope.get(
5899
6014
  "/graph/node/:id",
5900
6015
  async (req, reply) => {
5901
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6016
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5902
6017
  if (!proj) return;
5903
6018
  const { id } = req.params;
5904
6019
  if (!proj.graph.hasNode(id)) {
@@ -5910,7 +6025,7 @@ function registerRoutes(scope, ctx) {
5910
6025
  scope.get(
5911
6026
  "/graph/edges/:id",
5912
6027
  async (req, reply) => {
5913
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6028
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5914
6029
  if (!proj) return;
5915
6030
  const { id } = req.params;
5916
6031
  if (!proj.graph.hasNode(id)) {
@@ -5922,7 +6037,7 @@ function registerRoutes(scope, ctx) {
5922
6037
  }
5923
6038
  );
5924
6039
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
5925
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6040
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5926
6041
  if (!proj) return;
5927
6042
  const { nodeId } = req.params;
5928
6043
  if (!proj.graph.hasNode(nodeId)) {
@@ -5937,7 +6052,7 @@ function registerRoutes(scope, ctx) {
5937
6052
  return getTransitiveDependencies(proj.graph, nodeId, depth);
5938
6053
  });
5939
6054
  scope.get("/graph/divergences", async (req, reply) => {
5940
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6055
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5941
6056
  if (!proj) return;
5942
6057
  let typeFilter;
5943
6058
  if (req.query.type) {
@@ -5972,7 +6087,7 @@ function registerRoutes(scope, ctx) {
5972
6087
  });
5973
6088
  });
5974
6089
  scope.get("/incidents", async (req, reply) => {
5975
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6090
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5976
6091
  if (!proj) return;
5977
6092
  const epath = errorsPathFor(proj);
5978
6093
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -5984,7 +6099,7 @@ function registerRoutes(scope, ctx) {
5984
6099
  return { count: sliced.length, total, events: sliced };
5985
6100
  });
5986
6101
  scope.get("/stale-events", async (req, reply) => {
5987
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6102
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5988
6103
  if (!proj) return;
5989
6104
  const spath = staleEventsPathFor(proj);
5990
6105
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -5999,7 +6114,7 @@ function registerRoutes(scope, ctx) {
5999
6114
  scope.get(
6000
6115
  "/incidents/:nodeId",
6001
6116
  async (req, reply) => {
6002
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6117
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6003
6118
  if (!proj) return;
6004
6119
  const { nodeId } = req.params;
6005
6120
  if (!proj.graph.hasNode(nodeId)) {
@@ -6015,7 +6130,7 @@ function registerRoutes(scope, ctx) {
6015
6130
  }
6016
6131
  );
6017
6132
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6018
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6133
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6019
6134
  if (!proj) return;
6020
6135
  const { nodeId } = req.params;
6021
6136
  if (!proj.graph.hasNode(nodeId)) {
@@ -6035,7 +6150,7 @@ function registerRoutes(scope, ctx) {
6035
6150
  return result;
6036
6151
  });
6037
6152
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
6038
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6153
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6039
6154
  if (!proj) return;
6040
6155
  const { nodeId } = req.params;
6041
6156
  if (!proj.graph.hasNode(nodeId)) {
@@ -6048,7 +6163,7 @@ function registerRoutes(scope, ctx) {
6048
6163
  return getBlastRadius(proj.graph, nodeId, depth);
6049
6164
  });
6050
6165
  scope.get("/search", async (req, reply) => {
6051
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6166
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6052
6167
  if (!proj) return;
6053
6168
  const raw = (req.query.q ?? "").trim();
6054
6169
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -6079,7 +6194,7 @@ function registerRoutes(scope, ctx) {
6079
6194
  scope.get(
6080
6195
  "/graph/diff",
6081
6196
  async (req, reply) => {
6082
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6197
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6083
6198
  if (!proj) return;
6084
6199
  const against = req.query.against;
6085
6200
  if (!against) {
@@ -6094,7 +6209,7 @@ function registerRoutes(scope, ctx) {
6094
6209
  }
6095
6210
  );
6096
6211
  scope.post("/snapshot", async (req, reply) => {
6097
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6212
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6098
6213
  if (!proj) return;
6099
6214
  const body = req.body;
6100
6215
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -6123,7 +6238,7 @@ function registerRoutes(scope, ctx) {
6123
6238
  }
6124
6239
  });
6125
6240
  scope.post("/graph/scan", async (req, reply) => {
6126
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6241
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6127
6242
  if (!proj) return;
6128
6243
  if (!proj.scanPath) {
6129
6244
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6139,7 +6254,7 @@ function registerRoutes(scope, ctx) {
6139
6254
  };
6140
6255
  });
6141
6256
  scope.get("/policies", async (req, reply) => {
6142
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6257
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6143
6258
  if (!proj) return;
6144
6259
  const policyPath = ctx.policyFilePathFor(proj);
6145
6260
  if (!policyPath) {
@@ -6156,7 +6271,7 @@ function registerRoutes(scope, ctx) {
6156
6271
  }
6157
6272
  });
6158
6273
  scope.get("/policies/violations", async (req, reply) => {
6159
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6274
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6160
6275
  if (!proj) return;
6161
6276
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6162
6277
  let violations = await log.readAll();
@@ -6176,7 +6291,7 @@ function registerRoutes(scope, ctx) {
6176
6291
  return { violations };
6177
6292
  });
6178
6293
  scope.post("/policies/check", async (req, reply) => {
6179
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6294
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6180
6295
  if (!proj) return;
6181
6296
  const parsed = PoliciesCheckBodySchema.safeParse(req.body ?? {});
6182
6297
  if (!parsed.success) {
@@ -6212,7 +6327,7 @@ function registerRoutes(scope, ctx) {
6212
6327
  };
6213
6328
  });
6214
6329
  scope.get("/extend/list-uninstrumented", async (req, reply) => {
6215
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6330
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6216
6331
  if (!proj) return;
6217
6332
  if (!proj.scanPath) {
6218
6333
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6225,7 +6340,7 @@ function registerRoutes(scope, ctx) {
6225
6340
  }
6226
6341
  });
6227
6342
  scope.get("/extend/lookup", async (req, reply) => {
6228
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6343
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6229
6344
  if (!proj) return;
6230
6345
  const { library, version } = req.query;
6231
6346
  if (!library) {
@@ -6238,7 +6353,7 @@ function registerRoutes(scope, ctx) {
6238
6353
  return result;
6239
6354
  });
6240
6355
  scope.get("/extend/describe", async (req, reply) => {
6241
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6356
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6242
6357
  if (!proj) return;
6243
6358
  if (!proj.scanPath) {
6244
6359
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6251,7 +6366,7 @@ function registerRoutes(scope, ctx) {
6251
6366
  }
6252
6367
  });
6253
6368
  scope.post("/extend/apply", async (req, reply) => {
6254
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6369
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6255
6370
  if (!proj) return;
6256
6371
  if (!proj.scanPath) {
6257
6372
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6271,7 +6386,7 @@ function registerRoutes(scope, ctx) {
6271
6386
  }
6272
6387
  });
6273
6388
  scope.post("/extend/dry-run", async (req, reply) => {
6274
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6389
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6275
6390
  if (!proj) return;
6276
6391
  if (!proj.scanPath) {
6277
6392
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6291,7 +6406,7 @@ function registerRoutes(scope, ctx) {
6291
6406
  }
6292
6407
  });
6293
6408
  scope.post("/extend/rollback", async (req, reply) => {
6294
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6409
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6295
6410
  if (!proj) return;
6296
6411
  if (!proj.scanPath) {
6297
6412
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6354,7 +6469,8 @@ async function buildApi(opts) {
6354
6469
  errorsPathFor,
6355
6470
  staleEventsPathFor,
6356
6471
  policyFilePathFor,
6357
- bootstrap: opts.bootstrap
6472
+ bootstrap: opts.bootstrap,
6473
+ singleProject: opts.singleProject?.name
6358
6474
  };
6359
6475
  app.get("/health", async () => {
6360
6476
  const uptimeMs = Date.now() - startedAt;
@@ -6377,10 +6493,29 @@ async function buildApi(opts) {
6377
6493
  return {
6378
6494
  ok: true,
6379
6495
  uptimeMs,
6496
+ // ADR-096 §7 — a per-project daemon carries its project at the top level
6497
+ // so a spawn can confirm a daemon found on a reused port is serving THIS
6498
+ // project before reusing it. The legacy multi-project daemon omits it and
6499
+ // is matched against the `projects` array instead.
6500
+ ...opts.singleProject ? { project: opts.singleProject.name } : {},
6380
6501
  projects
6381
6502
  };
6382
6503
  });
6383
6504
  app.get("/projects", async (_req, reply) => {
6505
+ if (opts.singleProject) {
6506
+ const phase = opts.bootstrap?.status(opts.singleProject.name);
6507
+ const entry = {
6508
+ name: opts.singleProject.name,
6509
+ path: opts.singleProject.path,
6510
+ registeredAt: new Date(startedAt).toISOString(),
6511
+ languages: [],
6512
+ // The daemon is serving this project, so it's active unless its
6513
+ // bootstrap broke. ('bootstrapping' still reads as active — the project
6514
+ // is the one to land on; its graph route answers 503 until ready.)
6515
+ status: phase === "broken" ? "broken" : "active"
6516
+ };
6517
+ return [entry];
6518
+ }
6384
6519
  try {
6385
6520
  return await listProjects();
6386
6521
  } catch (err) {
@@ -6465,6 +6600,10 @@ export {
6465
6600
  parseExtraProjects,
6466
6601
  registryPath,
6467
6602
  registryLockPath,
6603
+ removeDaemonRecord,
6604
+ listMachineProjects,
6605
+ findDaemonByProject,
6606
+ signalDaemonStop,
6468
6607
  normalizeProjectPath,
6469
6608
  writeAtomically,
6470
6609
  readRegistry,
@@ -6478,4 +6617,4 @@ export {
6478
6617
  pruneRegistry,
6479
6618
  buildApi
6480
6619
  };
6481
- //# sourceMappingURL=chunk-LUDSPX5N.js.map
6620
+ //# sourceMappingURL=chunk-Q5EDVWKZ.js.map