@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/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
- declare function startPersistLoop(graph: NeatGraph, outPath: string, intervalMs?: number): () => void;
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
- declare function startPersistLoop(graph: NeatGraph, outPath: string, intervalMs?: number): () => void;
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-GXWBMJDJ.js";
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-T3X6XJPU.js";
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, intervalMs = 6e4) {
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
- process.on("SIGTERM", onSignal);
5239
- process.on("SIGINT", onSignal);
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
- process.off("SIGTERM", onSignal);
5244
- process.off("SIGINT", onSignal);
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
- return params.project ?? DEFAULT_PROJECT;
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) => front.close(() => 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");