@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/cli.cjs CHANGED
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
58
58
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
59
59
  const publicRead = opts.publicRead === true;
60
60
  app.addHook("preHandler", (req, reply, done) => {
61
- const path51 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
+ const path53 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
62
62
  for (const suffix of suffixes) {
63
- if (path51 === suffix || path51.endsWith(suffix)) {
63
+ if (path53 === suffix || path53.endsWith(suffix)) {
64
64
  done();
65
65
  return;
66
66
  }
@@ -474,8 +474,10 @@ async function buildOtelReceiver(opts) {
474
474
  for (const s of spans) projectQueue.push({ project, span: s });
475
475
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
476
476
  };
477
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
477
478
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
478
479
  function warnLegacyEndpoint(serviceName) {
480
+ if (!offersProjectRouting) return;
479
481
  if (legacyEndpointWarned.has(serviceName)) return;
480
482
  legacyEndpointWarned.add(serviceName);
481
483
  console.warn(
@@ -611,8 +613,8 @@ __export(cli_exports, {
611
613
  });
612
614
  module.exports = __toCommonJS(cli_exports);
613
615
  init_cjs_shims();
614
- var import_node_path50 = __toESM(require("path"), 1);
615
- var import_node_fs32 = require("fs");
616
+ var import_node_path52 = __toESM(require("path"), 1);
617
+ var import_node_fs34 = require("fs");
616
618
 
617
619
  // src/banner.ts
618
620
  init_cjs_shims();
@@ -1177,19 +1179,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1177
1179
  function longestIncomingWalk(graph, start, maxDepth) {
1178
1180
  let best = { path: [start], edges: [] };
1179
1181
  const visited = /* @__PURE__ */ new Set([start]);
1180
- function step(node, path51, edges) {
1181
- if (path51.length > best.path.length) {
1182
- best = { path: [...path51], edges: [...edges] };
1182
+ function step(node, path53, edges) {
1183
+ if (path53.length > best.path.length) {
1184
+ best = { path: [...path53], edges: [...edges] };
1183
1185
  }
1184
- if (path51.length - 1 >= maxDepth) return;
1186
+ if (path53.length - 1 >= maxDepth) return;
1185
1187
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1186
1188
  for (const [srcId, edge] of incoming) {
1187
1189
  if (visited.has(srcId)) continue;
1188
1190
  visited.add(srcId);
1189
- path51.push(srcId);
1191
+ path53.push(srcId);
1190
1192
  edges.push(edge);
1191
- step(srcId, path51, edges);
1192
- path51.pop();
1193
+ step(srcId, path53, edges);
1194
+ path53.pop();
1193
1195
  edges.pop();
1194
1196
  visited.delete(srcId);
1195
1197
  }
@@ -5615,7 +5617,9 @@ async function loadGraphFromDisk(graph, outPath) {
5615
5617
  graph.clear();
5616
5618
  graph.import(payload.graph);
5617
5619
  }
5618
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5620
+ function startPersistLoop(graph, outPath, opts = {}) {
5621
+ const intervalMs = opts.intervalMs ?? 6e4;
5622
+ const exitOnSignal = opts.exitOnSignal ?? true;
5619
5623
  let stopped = false;
5620
5624
  const tick = async () => {
5621
5625
  if (stopped) return;
@@ -5628,7 +5632,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5628
5632
  const interval = setInterval(() => {
5629
5633
  void tick();
5630
5634
  }, intervalMs);
5631
- const onSignal = (signal) => {
5635
+ const onSignal = exitOnSignal ? (signal) => {
5632
5636
  void (async () => {
5633
5637
  try {
5634
5638
  await saveGraphToDisk(graph, outPath);
@@ -5638,14 +5642,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5638
5642
  process.exit(0);
5639
5643
  }
5640
5644
  })();
5641
- };
5642
- process.on("SIGTERM", onSignal);
5643
- process.on("SIGINT", onSignal);
5645
+ } : null;
5646
+ if (onSignal) {
5647
+ process.on("SIGTERM", onSignal);
5648
+ process.on("SIGINT", onSignal);
5649
+ }
5644
5650
  return () => {
5645
5651
  stopped = true;
5646
5652
  clearInterval(interval);
5647
- process.off("SIGTERM", onSignal);
5648
- process.off("SIGINT", onSignal);
5653
+ if (onSignal) {
5654
+ process.off("SIGTERM", onSignal);
5655
+ process.off("SIGINT", onSignal);
5656
+ }
5649
5657
  };
5650
5658
  }
5651
5659
 
@@ -6259,6 +6267,111 @@ function registryLockPath() {
6259
6267
  function daemonPidPath() {
6260
6268
  return import_node_path40.default.join(neatHome(), "neatd.pid");
6261
6269
  }
6270
+ function daemonsDir() {
6271
+ return import_node_path40.default.join(neatHome(), "daemons");
6272
+ }
6273
+ function isFiniteInt(v) {
6274
+ return typeof v === "number" && Number.isFinite(v);
6275
+ }
6276
+ function parseDaemonRecord(raw) {
6277
+ let obj;
6278
+ try {
6279
+ obj = JSON.parse(raw);
6280
+ } catch {
6281
+ return void 0;
6282
+ }
6283
+ if (typeof obj !== "object" || obj === null) return void 0;
6284
+ const r = obj;
6285
+ const ports = r.ports;
6286
+ 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)) {
6287
+ return void 0;
6288
+ }
6289
+ return {
6290
+ project: r.project,
6291
+ projectPath: r.projectPath,
6292
+ pid: r.pid,
6293
+ status: r.status,
6294
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
6295
+ startedAt: r.startedAt,
6296
+ neatVersion: r.neatVersion
6297
+ };
6298
+ }
6299
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
6300
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
6301
+ const dir = daemonsDir();
6302
+ let names;
6303
+ try {
6304
+ names = await import_node_fs25.promises.readdir(dir);
6305
+ } catch (err) {
6306
+ if (err.code === "ENOENT") return [];
6307
+ throw err;
6308
+ }
6309
+ const out = [];
6310
+ for (const name of names) {
6311
+ if (!name.endsWith(".json")) continue;
6312
+ const file = import_node_path40.default.join(dir, name);
6313
+ let raw;
6314
+ try {
6315
+ raw = await import_node_fs25.promises.readFile(file, "utf8");
6316
+ } catch {
6317
+ continue;
6318
+ }
6319
+ const record = parseDaemonRecord(raw);
6320
+ if (!record) continue;
6321
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
6322
+ out.push({ record, live, source: file });
6323
+ }
6324
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
6325
+ return out;
6326
+ }
6327
+ async function removeDaemonRecord(source) {
6328
+ await import_node_fs25.promises.unlink(source).catch(() => {
6329
+ });
6330
+ }
6331
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
6332
+ const discovered = await discoverDaemons(probe);
6333
+ const byPath = /* @__PURE__ */ new Map();
6334
+ for (const d of discovered) {
6335
+ const key = await normalizeProjectPath(d.record.projectPath);
6336
+ byPath.set(key, {
6337
+ project: d.record.project,
6338
+ projectPath: d.record.projectPath,
6339
+ state: d.live ? "running" : "stopped",
6340
+ ports: d.record.ports,
6341
+ pid: d.record.pid
6342
+ });
6343
+ }
6344
+ let legacy = [];
6345
+ try {
6346
+ legacy = (await readRegistry()).projects;
6347
+ } catch {
6348
+ legacy = [];
6349
+ }
6350
+ for (const entry2 of legacy) {
6351
+ const key = await normalizeProjectPath(entry2.path);
6352
+ if (byPath.has(key)) continue;
6353
+ byPath.set(key, {
6354
+ project: entry2.name,
6355
+ projectPath: entry2.path,
6356
+ state: "registered",
6357
+ registryStatus: entry2.status
6358
+ });
6359
+ }
6360
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
6361
+ }
6362
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
6363
+ const discovered = await discoverDaemons(probe);
6364
+ return discovered.find((d) => d.record.project === name);
6365
+ }
6366
+ function signalDaemonStop(pid) {
6367
+ try {
6368
+ process.kill(pid, "SIGTERM");
6369
+ return true;
6370
+ } catch (err) {
6371
+ if (err.code === "ESRCH") return false;
6372
+ throw err;
6373
+ }
6374
+ }
6262
6375
  function isPidAliveDefault(pid) {
6263
6376
  try {
6264
6377
  process.kill(pid, 0);
@@ -6587,12 +6700,16 @@ function serializeGraph(graph) {
6587
6700
  });
6588
6701
  return { nodes, edges };
6589
6702
  }
6590
- function projectFromReq(req) {
6703
+ function projectFromReq(req, singleProject) {
6591
6704
  const params = req.params;
6592
- return params.project ?? DEFAULT_PROJECT;
6705
+ const named = params.project;
6706
+ if (singleProject) {
6707
+ return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
6708
+ }
6709
+ return named ?? DEFAULT_PROJECT;
6593
6710
  }
6594
- function resolveProject(registry, req, reply, bootstrap) {
6595
- const name = projectFromReq(req);
6711
+ function resolveProject(registry, req, reply, bootstrap, singleProject) {
6712
+ const name = projectFromReq(req, singleProject);
6596
6713
  const ctx = registry.get(name);
6597
6714
  if (!ctx) {
6598
6715
  const phase = bootstrap?.status(name);
@@ -6633,13 +6750,13 @@ function buildLegacyRegistry(opts) {
6633
6750
  function registerRoutes(scope, ctx) {
6634
6751
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
6635
6752
  scope.get("/events", (req, reply) => {
6636
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6753
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6637
6754
  if (!proj) return;
6638
6755
  handleSse(req, reply, { project: proj.name });
6639
6756
  });
6640
6757
  if (ctx.scope === "project") {
6641
6758
  scope.get("/health", async (req, reply) => {
6642
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6759
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6643
6760
  if (!proj) return;
6644
6761
  const uptimeMs = Date.now() - startedAt;
6645
6762
  return {
@@ -6657,14 +6774,14 @@ function registerRoutes(scope, ctx) {
6657
6774
  });
6658
6775
  }
6659
6776
  scope.get("/graph", async (req, reply) => {
6660
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6777
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6661
6778
  if (!proj) return;
6662
6779
  return serializeGraph(proj.graph);
6663
6780
  });
6664
6781
  scope.get(
6665
6782
  "/graph/node/:id",
6666
6783
  async (req, reply) => {
6667
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6784
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6668
6785
  if (!proj) return;
6669
6786
  const { id } = req.params;
6670
6787
  if (!proj.graph.hasNode(id)) {
@@ -6676,7 +6793,7 @@ function registerRoutes(scope, ctx) {
6676
6793
  scope.get(
6677
6794
  "/graph/edges/:id",
6678
6795
  async (req, reply) => {
6679
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6796
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6680
6797
  if (!proj) return;
6681
6798
  const { id } = req.params;
6682
6799
  if (!proj.graph.hasNode(id)) {
@@ -6688,7 +6805,7 @@ function registerRoutes(scope, ctx) {
6688
6805
  }
6689
6806
  );
6690
6807
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
6691
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6808
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6692
6809
  if (!proj) return;
6693
6810
  const { nodeId } = req.params;
6694
6811
  if (!proj.graph.hasNode(nodeId)) {
@@ -6703,7 +6820,7 @@ function registerRoutes(scope, ctx) {
6703
6820
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6704
6821
  });
6705
6822
  scope.get("/graph/divergences", async (req, reply) => {
6706
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6823
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6707
6824
  if (!proj) return;
6708
6825
  let typeFilter;
6709
6826
  if (req.query.type) {
@@ -6738,7 +6855,7 @@ function registerRoutes(scope, ctx) {
6738
6855
  });
6739
6856
  });
6740
6857
  scope.get("/incidents", async (req, reply) => {
6741
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6858
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6742
6859
  if (!proj) return;
6743
6860
  const epath = errorsPathFor(proj);
6744
6861
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -6750,7 +6867,7 @@ function registerRoutes(scope, ctx) {
6750
6867
  return { count: sliced.length, total, events: sliced };
6751
6868
  });
6752
6869
  scope.get("/stale-events", async (req, reply) => {
6753
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6870
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6754
6871
  if (!proj) return;
6755
6872
  const spath = staleEventsPathFor(proj);
6756
6873
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -6765,7 +6882,7 @@ function registerRoutes(scope, ctx) {
6765
6882
  scope.get(
6766
6883
  "/incidents/:nodeId",
6767
6884
  async (req, reply) => {
6768
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6885
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6769
6886
  if (!proj) return;
6770
6887
  const { nodeId } = req.params;
6771
6888
  if (!proj.graph.hasNode(nodeId)) {
@@ -6781,7 +6898,7 @@ function registerRoutes(scope, ctx) {
6781
6898
  }
6782
6899
  );
6783
6900
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6784
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6901
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6785
6902
  if (!proj) return;
6786
6903
  const { nodeId } = req.params;
6787
6904
  if (!proj.graph.hasNode(nodeId)) {
@@ -6801,7 +6918,7 @@ function registerRoutes(scope, ctx) {
6801
6918
  return result;
6802
6919
  });
6803
6920
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
6804
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6921
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6805
6922
  if (!proj) return;
6806
6923
  const { nodeId } = req.params;
6807
6924
  if (!proj.graph.hasNode(nodeId)) {
@@ -6814,7 +6931,7 @@ function registerRoutes(scope, ctx) {
6814
6931
  return getBlastRadius(proj.graph, nodeId, depth);
6815
6932
  });
6816
6933
  scope.get("/search", async (req, reply) => {
6817
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6934
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6818
6935
  if (!proj) return;
6819
6936
  const raw = (req.query.q ?? "").trim();
6820
6937
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -6845,7 +6962,7 @@ function registerRoutes(scope, ctx) {
6845
6962
  scope.get(
6846
6963
  "/graph/diff",
6847
6964
  async (req, reply) => {
6848
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6965
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6849
6966
  if (!proj) return;
6850
6967
  const against = req.query.against;
6851
6968
  if (!against) {
@@ -6860,7 +6977,7 @@ function registerRoutes(scope, ctx) {
6860
6977
  }
6861
6978
  );
6862
6979
  scope.post("/snapshot", async (req, reply) => {
6863
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6980
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6864
6981
  if (!proj) return;
6865
6982
  const body = req.body;
6866
6983
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -6889,7 +7006,7 @@ function registerRoutes(scope, ctx) {
6889
7006
  }
6890
7007
  });
6891
7008
  scope.post("/graph/scan", async (req, reply) => {
6892
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7009
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6893
7010
  if (!proj) return;
6894
7011
  if (!proj.scanPath) {
6895
7012
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6905,7 +7022,7 @@ function registerRoutes(scope, ctx) {
6905
7022
  };
6906
7023
  });
6907
7024
  scope.get("/policies", async (req, reply) => {
6908
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7025
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6909
7026
  if (!proj) return;
6910
7027
  const policyPath = ctx.policyFilePathFor(proj);
6911
7028
  if (!policyPath) {
@@ -6922,7 +7039,7 @@ function registerRoutes(scope, ctx) {
6922
7039
  }
6923
7040
  });
6924
7041
  scope.get("/policies/violations", async (req, reply) => {
6925
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7042
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6926
7043
  if (!proj) return;
6927
7044
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6928
7045
  let violations = await log.readAll();
@@ -6942,7 +7059,7 @@ function registerRoutes(scope, ctx) {
6942
7059
  return { violations };
6943
7060
  });
6944
7061
  scope.post("/policies/check", async (req, reply) => {
6945
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7062
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6946
7063
  if (!proj) return;
6947
7064
  const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6948
7065
  if (!parsed.success) {
@@ -6978,7 +7095,7 @@ function registerRoutes(scope, ctx) {
6978
7095
  };
6979
7096
  });
6980
7097
  scope.get("/extend/list-uninstrumented", async (req, reply) => {
6981
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7098
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6982
7099
  if (!proj) return;
6983
7100
  if (!proj.scanPath) {
6984
7101
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6991,7 +7108,7 @@ function registerRoutes(scope, ctx) {
6991
7108
  }
6992
7109
  });
6993
7110
  scope.get("/extend/lookup", async (req, reply) => {
6994
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7111
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6995
7112
  if (!proj) return;
6996
7113
  const { library, version } = req.query;
6997
7114
  if (!library) {
@@ -7004,7 +7121,7 @@ function registerRoutes(scope, ctx) {
7004
7121
  return result;
7005
7122
  });
7006
7123
  scope.get("/extend/describe", async (req, reply) => {
7007
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7124
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7008
7125
  if (!proj) return;
7009
7126
  if (!proj.scanPath) {
7010
7127
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -7017,7 +7134,7 @@ function registerRoutes(scope, ctx) {
7017
7134
  }
7018
7135
  });
7019
7136
  scope.post("/extend/apply", async (req, reply) => {
7020
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7137
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7021
7138
  if (!proj) return;
7022
7139
  if (!proj.scanPath) {
7023
7140
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -7037,7 +7154,7 @@ function registerRoutes(scope, ctx) {
7037
7154
  }
7038
7155
  });
7039
7156
  scope.post("/extend/dry-run", async (req, reply) => {
7040
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7157
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7041
7158
  if (!proj) return;
7042
7159
  if (!proj.scanPath) {
7043
7160
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -7057,7 +7174,7 @@ function registerRoutes(scope, ctx) {
7057
7174
  }
7058
7175
  });
7059
7176
  scope.post("/extend/rollback", async (req, reply) => {
7060
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
7177
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7061
7178
  if (!proj) return;
7062
7179
  if (!proj.scanPath) {
7063
7180
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -7120,7 +7237,8 @@ async function buildApi(opts) {
7120
7237
  errorsPathFor,
7121
7238
  staleEventsPathFor,
7122
7239
  policyFilePathFor,
7123
- bootstrap: opts.bootstrap
7240
+ bootstrap: opts.bootstrap,
7241
+ singleProject: opts.singleProject?.name
7124
7242
  };
7125
7243
  app.get("/health", async () => {
7126
7244
  const uptimeMs = Date.now() - startedAt;
@@ -7143,10 +7261,29 @@ async function buildApi(opts) {
7143
7261
  return {
7144
7262
  ok: true,
7145
7263
  uptimeMs,
7264
+ // ADR-096 §7 — a per-project daemon carries its project at the top level
7265
+ // so a spawn can confirm a daemon found on a reused port is serving THIS
7266
+ // project before reusing it. The legacy multi-project daemon omits it and
7267
+ // is matched against the `projects` array instead.
7268
+ ...opts.singleProject ? { project: opts.singleProject.name } : {},
7146
7269
  projects
7147
7270
  };
7148
7271
  });
7149
7272
  app.get("/projects", async (_req, reply) => {
7273
+ if (opts.singleProject) {
7274
+ const phase = opts.bootstrap?.status(opts.singleProject.name);
7275
+ const entry2 = {
7276
+ name: opts.singleProject.name,
7277
+ path: opts.singleProject.path,
7278
+ registeredAt: new Date(startedAt).toISOString(),
7279
+ languages: [],
7280
+ // The daemon is serving this project, so it's active unless its
7281
+ // bootstrap broke. ('bootstrapping' still reads as active — the project
7282
+ // is the one to land on; its graph route answers 503 until ready.)
7283
+ status: phase === "broken" ? "broken" : "active"
7284
+ };
7285
+ return [entry2];
7286
+ }
7150
7287
  try {
7151
7288
  return await listProjects();
7152
7289
  } catch (err) {
@@ -7994,9 +8131,50 @@ var import_semver2 = __toESM(require("semver"), 1);
7994
8131
  // src/installers/templates.ts
7995
8132
  init_cjs_shims();
7996
8133
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
7997
- var OTEL_INIT_STAMP = "// neat-template-version: 5 \u2014 layered file-first capture (ADR-090) + http/json protocol pin (#468).";
8134
+ var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
7998
8135
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
7999
8136
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
8137
+ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
8138
+ try {
8139
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8140
+ const __neatFs = require('node:fs')
8141
+ const __neatPath = require('node:path')
8142
+ let __neatDir = process.cwd()
8143
+ for (let __i = 0; __i < 8; __i++) {
8144
+ try {
8145
+ const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8146
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8147
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8148
+ break
8149
+ }
8150
+ } catch (_e) {}
8151
+ const __parent = __neatPath.dirname(__neatDir)
8152
+ if (__parent === __neatDir) break
8153
+ __neatDir = __parent
8154
+ }
8155
+ } catch (_e) {}
8156
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8157
+ })()`;
8158
+ var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
8159
+ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
8160
+ try {
8161
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8162
+ let __neatDir = process.cwd()
8163
+ for (let __i = 0; __i < 8; __i++) {
8164
+ try {
8165
+ const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8166
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8167
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8168
+ break
8169
+ }
8170
+ } catch (_e) {}
8171
+ const __parent = __neatDirname(__neatDir)
8172
+ if (__parent === __neatDir) break
8173
+ __neatDir = __parent
8174
+ }
8175
+ } catch (_e) {}
8176
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8177
+ })()`;
8000
8178
  function neatCaptureSource(ts) {
8001
8179
  const spanT = ts ? ": any" : "";
8002
8180
  const strOpt = ts ? ": string | undefined" : "";
@@ -8285,7 +8463,7 @@ try {
8285
8463
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
8286
8464
  ${OTEL_INIT_STAMP}
8287
8465
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8288
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8466
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8289
8467
  ${OTEL_OTLP_PROTOCOL_JS}
8290
8468
  ${OTEL_OTLP_HEADERS_JS}
8291
8469
 
@@ -8303,8 +8481,9 @@ ${neatWireCaptureSource(false)}
8303
8481
  `;
8304
8482
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
8305
8483
  ${OTEL_INIT_STAMP}
8484
+ ${OTEL_ESM_NODE_IMPORTS}
8306
8485
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8307
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8486
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8308
8487
  ${OTEL_OTLP_PROTOCOL_JS}
8309
8488
  ${OTEL_OTLP_HEADERS_JS}
8310
8489
 
@@ -8327,8 +8506,9 @@ ${OTEL_INIT_STAMP}
8327
8506
  // strict user tsconfig would reject; suppressing type-checking here keeps the
8328
8507
  // generated file from breaking the host project's \`tsc\` gate (#427) without
8329
8508
  // constraining the runtime logic. The file is regenerated, never hand-edited.
8509
+ ${OTEL_ESM_NODE_IMPORTS}
8330
8510
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8331
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8511
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8332
8512
  ${OTEL_OTLP_PROTOCOL_JS}
8333
8513
  ${OTEL_OTLP_HEADERS_JS}
8334
8514
 
@@ -8350,11 +8530,14 @@ ${registrations.join("\n")}
8350
8530
  `;
8351
8531
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
8352
8532
  }
8353
- function renderEnvNeat(serviceName, projectName) {
8533
+ function renderEnvNeat(serviceName, _projectName) {
8354
8534
  return [
8355
8535
  "# Generated by `neat init --apply` (ADR-069).",
8356
8536
  `OTEL_SERVICE_NAME=${serviceName}`,
8357
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
8537
+ "# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
8538
+ "# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
8539
+ "# canonical default the daemon takes when its first-choice OTLP port is free.",
8540
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
8358
8541
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
8359
8542
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
8360
8543
  "# NEAT_OTEL_TOKEN=",
@@ -8377,8 +8560,9 @@ export async function register() {
8377
8560
  }
8378
8561
  `;
8379
8562
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
8563
+ ${OTEL_ESM_NODE_IMPORTS}
8380
8564
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8381
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8565
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8382
8566
  ${OTEL_OTLP_PROTOCOL_JS}
8383
8567
  ${OTEL_OTLP_HEADERS_JS}
8384
8568
 
@@ -8391,7 +8575,7 @@ new NodeSDK({ instrumentations }).start()
8391
8575
  `;
8392
8576
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
8393
8577
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8394
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8578
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8395
8579
  ${OTEL_OTLP_PROTOCOL_JS}
8396
8580
  ${OTEL_OTLP_HEADERS_JS}
8397
8581
 
@@ -8410,8 +8594,9 @@ ${registrations.join("\n")}
8410
8594
  }
8411
8595
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
8412
8596
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8597
+ ${OTEL_ESM_NODE_IMPORTS}
8413
8598
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8414
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8599
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8415
8600
  ${OTEL_OTLP_PROTOCOL_JS}
8416
8601
  ${OTEL_OTLP_HEADERS_JS}
8417
8602
 
@@ -8426,7 +8611,7 @@ sdk.start()
8426
8611
  `;
8427
8612
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8428
8613
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8429
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8614
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8430
8615
  ${OTEL_OTLP_PROTOCOL_JS}
8431
8616
  ${OTEL_OTLP_HEADERS_JS}
8432
8617
 
@@ -9895,19 +10080,51 @@ function renderPatch(sections) {
9895
10080
 
9896
10081
  // src/orchestrator.ts
9897
10082
  init_cjs_shims();
9898
- var import_node_fs31 = require("fs");
10083
+ var import_node_fs33 = require("fs");
9899
10084
  var import_node_http = __toESM(require("http"), 1);
9900
10085
  var import_node_net = __toESM(require("net"), 1);
9901
- var import_node_path48 = __toESM(require("path"), 1);
10086
+ var import_node_path50 = __toESM(require("path"), 1);
9902
10087
  var import_node_child_process3 = require("child_process");
9903
10088
  var import_node_readline = __toESM(require("readline"), 1);
10089
+
10090
+ // src/daemon.ts
10091
+ init_cjs_shims();
10092
+ var import_node_fs32 = require("fs");
10093
+ var import_node_path49 = __toESM(require("path"), 1);
10094
+ var import_node_module = require("module");
10095
+ init_otel();
10096
+ init_auth();
10097
+
10098
+ // src/unrouted.ts
10099
+ init_cjs_shims();
10100
+ var import_node_fs31 = require("fs");
10101
+ var import_node_path48 = __toESM(require("path"), 1);
10102
+
10103
+ // src/daemon.ts
10104
+ function daemonJsonPath(scanPath) {
10105
+ return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
10106
+ }
10107
+ async function readDaemonRecord(scanPath) {
10108
+ try {
10109
+ const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
10110
+ const parsed = JSON.parse(raw);
10111
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
10112
+ return parsed;
10113
+ }
10114
+ return null;
10115
+ } catch {
10116
+ return null;
10117
+ }
10118
+ }
10119
+
10120
+ // src/orchestrator.ts
9904
10121
  async function extractAndPersist(opts) {
9905
10122
  const services = await discoverServices(opts.scanPath);
9906
10123
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9907
10124
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
9908
10125
  resetGraph(graphKey);
9909
10126
  const graph = getGraph(graphKey);
9910
- const projectPaths = pathsForProject(graphKey, import_node_path48.default.join(opts.scanPath, "neat-out"));
10127
+ const projectPaths = pathsForProject(graphKey, import_node_path50.default.join(opts.scanPath, "neat-out"));
9911
10128
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
9912
10129
  errorsPath: projectPaths.errorsPath
9913
10130
  });
@@ -9961,7 +10178,7 @@ async function applyInstallersOver(services, project, options = {}) {
9961
10178
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
9962
10179
  } else if (outcome.outcome === "react-native") {
9963
10180
  reactNative++;
9964
- const svcName = import_node_path48.default.basename(svc.dir);
10181
+ const svcName = import_node_path50.default.basename(svc.dir);
9965
10182
  console.log(
9966
10183
  `neat: ${svc.dir} detected as React Native / Expo
9967
10184
  The installer doesn't cover this runtime deterministically.
@@ -9972,7 +10189,7 @@ async function applyInstallersOver(services, project, options = {}) {
9972
10189
  );
9973
10190
  } else if (outcome.outcome === "bun") {
9974
10191
  bun++;
9975
- const svcName = import_node_path48.default.basename(svc.dir);
10192
+ const svcName = import_node_path50.default.basename(svc.dir);
9976
10193
  console.log(
9977
10194
  `neat: ${svc.dir} detected as Bun
9978
10195
  The installer doesn't cover this runtime deterministically.
@@ -9983,7 +10200,7 @@ async function applyInstallersOver(services, project, options = {}) {
9983
10200
  );
9984
10201
  } else if (outcome.outcome === "deno") {
9985
10202
  deno++;
9986
- const svcName = import_node_path48.default.basename(svc.dir);
10203
+ const svcName = import_node_path50.default.basename(svc.dir);
9987
10204
  console.log(
9988
10205
  `neat: ${svc.dir} detected as Deno
9989
10206
  The installer doesn't cover this runtime deterministically.
@@ -9994,7 +10211,7 @@ async function applyInstallersOver(services, project, options = {}) {
9994
10211
  );
9995
10212
  } else if (outcome.outcome === "cloudflare-workers") {
9996
10213
  cloudflareWorkers++;
9997
- const svcName = import_node_path48.default.basename(svc.dir);
10214
+ const svcName = import_node_path50.default.basename(svc.dir);
9998
10215
  console.log(
9999
10216
  `neat: ${svc.dir} detected as Cloudflare Workers
10000
10217
  The installer doesn't cover this runtime deterministically.
@@ -10005,7 +10222,7 @@ async function applyInstallersOver(services, project, options = {}) {
10005
10222
  );
10006
10223
  } else if (outcome.outcome === "electron") {
10007
10224
  electron++;
10008
- const svcName = import_node_path48.default.basename(svc.dir);
10225
+ const svcName = import_node_path50.default.basename(svc.dir);
10009
10226
  console.log(
10010
10227
  `neat: ${svc.dir} detected as Electron
10011
10228
  The installer doesn't cover this runtime deterministically.
@@ -10086,10 +10303,6 @@ async function fetchDaemonHealth(restPort) {
10086
10303
  });
10087
10304
  });
10088
10305
  }
10089
- async function checkDaemonHealth(restPort) {
10090
- const body = await fetchDaemonHealth(restPort);
10091
- return body !== null;
10092
- }
10093
10306
  async function probeProjectHealth(restPort, name) {
10094
10307
  return new Promise((resolve) => {
10095
10308
  const req = import_node_http.default.get(
@@ -10165,12 +10378,6 @@ async function isPortFree(port) {
10165
10378
  server.listen(port, "127.0.0.1");
10166
10379
  });
10167
10380
  }
10168
- async function probePortsFree() {
10169
- for (const port of NEAT_PORTS) {
10170
- if (!await isPortFree(port)) return { free: false, held: port };
10171
- }
10172
- return { free: true };
10173
- }
10174
10381
  function formatPortCollisionMessage(port) {
10175
10382
  return [
10176
10383
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -10178,11 +10385,82 @@ function formatPortCollisionMessage(port) {
10178
10385
  ` \`lsof -i :${port}\` to find the holding process.`
10179
10386
  ];
10180
10387
  }
10181
- function spawnDaemonDetached() {
10182
- const here = import_node_path48.default.dirname(new URL(importMetaUrl).pathname);
10388
+ var PORT_ALLOCATION_ATTEMPTS = 8;
10389
+ var PORT_STRIDE = 1;
10390
+ async function tripleFree(ports) {
10391
+ for (const p of [ports.rest, ports.otlp, ports.web]) {
10392
+ if (!await isPortFree(p)) return false;
10393
+ }
10394
+ return true;
10395
+ }
10396
+ async function allocatePorts() {
10397
+ const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
10398
+ for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
10399
+ const candidate = {
10400
+ rest: baseRest + i * PORT_STRIDE,
10401
+ otlp: baseOtlp + i * PORT_STRIDE,
10402
+ web: baseWeb + i * PORT_STRIDE
10403
+ };
10404
+ if (await tripleFree(candidate)) return candidate;
10405
+ }
10406
+ return null;
10407
+ }
10408
+ async function persistedPortsFor(scanPath) {
10409
+ const record = await readDaemonRecord(scanPath);
10410
+ if (!record) return null;
10411
+ return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
10412
+ }
10413
+ async function acquireSpawnLock(scanPath) {
10414
+ const lockPath = import_node_path50.default.join(scanPath, "neat-out", "daemon.spawn.lock");
10415
+ await import_node_fs33.promises.mkdir(import_node_path50.default.dirname(lockPath), { recursive: true });
10416
+ const STALE_LOCK_MS = 6e4;
10417
+ try {
10418
+ const fd = await import_node_fs33.promises.open(lockPath, "wx");
10419
+ await fd.writeFile(`${process.pid}
10420
+ `, "utf8");
10421
+ await fd.close();
10422
+ return async () => {
10423
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10424
+ });
10425
+ };
10426
+ } catch (err) {
10427
+ if (err.code !== "EEXIST") return null;
10428
+ try {
10429
+ const stat = await import_node_fs33.promises.stat(lockPath);
10430
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
10431
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10432
+ });
10433
+ return acquireSpawnLock(scanPath);
10434
+ }
10435
+ } catch {
10436
+ return acquireSpawnLock(scanPath);
10437
+ }
10438
+ return null;
10439
+ }
10440
+ }
10441
+ async function waitForPeerDaemon(restPort, project, timeoutMs) {
10442
+ const deadline = Date.now() + timeoutMs;
10443
+ while (Date.now() < deadline) {
10444
+ if (await healthIsForProject(restPort, project)) return true;
10445
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
10446
+ }
10447
+ return healthIsForProject(restPort, project);
10448
+ }
10449
+ async function healthIsForProject(restPort, project) {
10450
+ const body = await fetchDaemonHealth(restPort);
10451
+ if (body === null) return false;
10452
+ const named = body.project;
10453
+ if (typeof named === "string") return named === project;
10454
+ if (Array.isArray(body.projects)) {
10455
+ return body.projects.some((p) => p.name === project);
10456
+ }
10457
+ return false;
10458
+ }
10459
+ function spawnDaemonDetached(spec) {
10460
+ const here = import_node_path50.default.dirname(new URL(importMetaUrl).pathname);
10183
10461
  const candidates = [
10184
- import_node_path48.default.join(here, "neatd.cjs"),
10185
- import_node_path48.default.join(here, "neatd.js")
10462
+ import_node_path50.default.join(here, "neatd.cjs"),
10463
+ import_node_path50.default.join(here, "neatd.js")
10186
10464
  ];
10187
10465
  let entry2 = null;
10188
10466
  const fsSync = require("fs");
@@ -10202,6 +10480,13 @@ function spawnDaemonDetached() {
10202
10480
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
10203
10481
  env.HOST = "127.0.0.1";
10204
10482
  }
10483
+ if (spec) {
10484
+ env.NEAT_PROJECT = spec.project;
10485
+ env.NEAT_PROJECT_PATH = spec.projectPath;
10486
+ env.PORT = String(spec.ports.rest);
10487
+ env.OTEL_PORT = String(spec.ports.otlp);
10488
+ env.NEAT_WEB_PORT = String(spec.ports.web);
10489
+ }
10205
10490
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
10206
10491
  detached: true,
10207
10492
  // stderr inherits the orchestrator's fd so the daemon's
@@ -10240,7 +10525,7 @@ async function runOrchestrator(opts) {
10240
10525
  browser: "skipped"
10241
10526
  }
10242
10527
  };
10243
- const stat = await import_node_fs31.promises.stat(opts.scanPath).catch(() => null);
10528
+ const stat = await import_node_fs33.promises.stat(opts.scanPath).catch(() => null);
10244
10529
  if (!stat || !stat.isDirectory()) {
10245
10530
  console.error(`neat: ${opts.scanPath} is not a directory`);
10246
10531
  result.exitCode = 2;
@@ -10321,48 +10606,75 @@ async function runOrchestrator(opts) {
10321
10606
  result.exitCode = 1;
10322
10607
  }
10323
10608
  }
10324
- const restPort = Number(process.env.PORT ?? 8080);
10325
10609
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
10326
- if (await checkDaemonHealth(restPort)) {
10610
+ const persistedPorts = await persistedPortsFor(opts.scanPath);
10611
+ let allocated = null;
10612
+ if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
10327
10613
  result.steps.daemon = "already-running";
10614
+ allocated = persistedPorts;
10328
10615
  } else {
10329
- const probe = await probePortsFree();
10330
- if (!probe.free) {
10331
- for (const line of formatPortCollisionMessage(probe.held)) {
10616
+ if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
10617
+ allocated = persistedPorts;
10618
+ } else {
10619
+ allocated = await allocatePorts();
10620
+ }
10621
+ if (!allocated) {
10622
+ for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
10332
10623
  console.error(line);
10333
10624
  }
10334
10625
  result.exitCode = 3;
10335
10626
  return result;
10336
10627
  }
10337
- try {
10338
- spawnDaemonDetached();
10339
- } catch (err) {
10340
- console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10341
- result.exitCode = 1;
10342
- return result;
10343
- }
10344
- const ready = await waitForDaemonReady(restPort, timeoutMs);
10345
- result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10346
- if (!ready.ready) {
10347
- console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10348
- if (ready.stillBootstrapping.length > 0) {
10349
- console.error(
10350
- `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
10351
- );
10628
+ const release = await acquireSpawnLock(opts.scanPath);
10629
+ if (!release) {
10630
+ const reused = await waitForPeerDaemon(allocated.rest, currentProjectName, timeoutMs);
10631
+ if (reused) {
10632
+ result.steps.daemon = "already-running";
10633
+ } else {
10634
+ console.error("neat: another `neat` is spawning this project but its daemon did not come up in time");
10635
+ result.exitCode = 1;
10636
+ return result;
10352
10637
  }
10353
- if (ready.brokenProjects.length > 0) {
10354
- console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10638
+ } else {
10639
+ try {
10640
+ if (await healthIsForProject(allocated.rest, currentProjectName)) {
10641
+ result.steps.daemon = "already-running";
10642
+ } else {
10643
+ spawnDaemonDetached({
10644
+ project: currentProjectName,
10645
+ projectPath: opts.scanPath,
10646
+ ports: allocated
10647
+ });
10648
+ const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
10649
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10650
+ if (!ready.ready) {
10651
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10652
+ if (ready.stillBootstrapping.length > 0) {
10653
+ console.error(`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`);
10654
+ }
10655
+ if (ready.brokenProjects.length > 0) {
10656
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10657
+ }
10658
+ result.exitCode = 1;
10659
+ return result;
10660
+ }
10661
+ if (ready.brokenProjects.length > 0) {
10662
+ console.warn(
10663
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10664
+ );
10665
+ }
10666
+ }
10667
+ } catch (err) {
10668
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10669
+ result.exitCode = 1;
10670
+ return result;
10671
+ } finally {
10672
+ await release();
10355
10673
  }
10356
- result.exitCode = 1;
10357
- return result;
10358
- }
10359
- if (ready.brokenProjects.length > 0) {
10360
- console.warn(
10361
- `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10362
- );
10363
10674
  }
10364
10675
  }
10365
- const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
10676
+ const webPort = allocated?.web ?? NEAT_PORTS[2];
10677
+ const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
10366
10678
  if (opts.noOpen || !process.stdout.isTTY) {
10367
10679
  result.steps.browser = "skipped";
10368
10680
  } else {
@@ -10397,7 +10709,7 @@ function printSummary(result, graph, dashboardUrl) {
10397
10709
 
10398
10710
  // src/cli-verbs.ts
10399
10711
  init_cjs_shims();
10400
- var import_node_path49 = __toESM(require("path"), 1);
10712
+ var import_node_path51 = __toESM(require("path"), 1);
10401
10713
 
10402
10714
  // src/cli-client.ts
10403
10715
  init_cjs_shims();
@@ -10426,10 +10738,10 @@ function createHttpClient(baseUrl, bearerToken) {
10426
10738
  const root = baseUrl.replace(/\/$/, "");
10427
10739
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
10428
10740
  return {
10429
- async get(path51) {
10741
+ async get(path53) {
10430
10742
  let res;
10431
10743
  try {
10432
- res = await fetch(`${root}${path51}`, {
10744
+ res = await fetch(`${root}${path53}`, {
10433
10745
  headers: { ...authHeader }
10434
10746
  });
10435
10747
  } catch (err) {
@@ -10441,16 +10753,16 @@ function createHttpClient(baseUrl, bearerToken) {
10441
10753
  const body = await res.text().catch(() => "");
10442
10754
  throw new HttpError(
10443
10755
  res.status,
10444
- `${res.status} ${res.statusText} on GET ${path51}: ${body}`,
10756
+ `${res.status} ${res.statusText} on GET ${path53}: ${body}`,
10445
10757
  body
10446
10758
  );
10447
10759
  }
10448
10760
  return await res.json();
10449
10761
  },
10450
- async post(path51, body) {
10762
+ async post(path53, body) {
10451
10763
  let res;
10452
10764
  try {
10453
- res = await fetch(`${root}${path51}`, {
10765
+ res = await fetch(`${root}${path53}`, {
10454
10766
  method: "POST",
10455
10767
  headers: { "content-type": "application/json", ...authHeader },
10456
10768
  body: JSON.stringify(body)
@@ -10464,7 +10776,7 @@ function createHttpClient(baseUrl, bearerToken) {
10464
10776
  const text = await res.text().catch(() => "");
10465
10777
  throw new HttpError(
10466
10778
  res.status,
10467
- `${res.status} ${res.statusText} on POST ${path51}: ${text}`,
10779
+ `${res.status} ${res.statusText} on POST ${path53}: ${text}`,
10468
10780
  text
10469
10781
  );
10470
10782
  }
@@ -10478,12 +10790,12 @@ function projectPath(project, suffix) {
10478
10790
  }
10479
10791
  async function runRootCause(client, input) {
10480
10792
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
10481
- const path51 = projectPath(
10793
+ const path53 = projectPath(
10482
10794
  input.project,
10483
10795
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
10484
10796
  );
10485
10797
  try {
10486
- const result = await client.get(path51);
10798
+ const result = await client.get(path53);
10487
10799
  const arrowPath = result.traversalPath.join(" \u2190 ");
10488
10800
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
10489
10801
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -10509,12 +10821,12 @@ async function runRootCause(client, input) {
10509
10821
  }
10510
10822
  async function runBlastRadius(client, input) {
10511
10823
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
10512
- const path51 = projectPath(
10824
+ const path53 = projectPath(
10513
10825
  input.project,
10514
10826
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
10515
10827
  );
10516
10828
  try {
10517
- const result = await client.get(path51);
10829
+ const result = await client.get(path53);
10518
10830
  if (result.totalAffected === 0) {
10519
10831
  return {
10520
10832
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -10548,12 +10860,12 @@ function formatBlastEntry(n) {
10548
10860
  }
10549
10861
  async function runDependencies(client, input) {
10550
10862
  const depth = input.depth ?? 3;
10551
- const path51 = projectPath(
10863
+ const path53 = projectPath(
10552
10864
  input.project,
10553
10865
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
10554
10866
  );
10555
10867
  try {
10556
- const result = await client.get(path51);
10868
+ const result = await client.get(path53);
10557
10869
  if (result.total === 0) {
10558
10870
  return {
10559
10871
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -10634,9 +10946,9 @@ function formatDuration(ms) {
10634
10946
  return `${Math.round(h / 24)}d`;
10635
10947
  }
10636
10948
  async function runIncidents(client, input) {
10637
- const path51 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10949
+ const path53 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10638
10950
  try {
10639
- const body = await client.get(path51);
10951
+ const body = await client.get(path53);
10640
10952
  const events = body.events;
10641
10953
  if (events.length === 0) {
10642
10954
  return {
@@ -10926,13 +11238,13 @@ async function resolveProjectEntry(opts) {
10926
11238
  const cwd = opts.cwd ?? process.cwd();
10927
11239
  const resolvedCwd = await normalizeProjectPath(cwd);
10928
11240
  for (const entry2 of entries) {
10929
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path49.default.sep}`)) {
11241
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path51.default.sep}`)) {
10930
11242
  return entry2;
10931
11243
  }
10932
11244
  }
10933
11245
  return null;
10934
11246
  }
10935
- async function checkDaemonHealth2(baseUrl) {
11247
+ async function checkDaemonHealth(baseUrl) {
10936
11248
  try {
10937
11249
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
10938
11250
  signal: AbortSignal.timeout(1500)
@@ -11033,7 +11345,7 @@ async function runSync(opts) {
11033
11345
  }
11034
11346
  } else {
11035
11347
  const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
11036
- const healthy = await checkDaemonHealth2(daemonUrl);
11348
+ const healthy = await checkDaemonHealth(daemonUrl);
11037
11349
  if (healthy) {
11038
11350
  try {
11039
11351
  await pushSnapshotToRemote({
@@ -11107,11 +11419,15 @@ function usage() {
11107
11419
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
11108
11420
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
11109
11421
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
11110
- console.log(" list List every project registered in the machine-level registry.");
11111
- console.log(" pause <name> Mark a project paused \u2014 daemon stops watching until resumed.");
11112
- console.log(" resume <name> Mark a project active again.");
11422
+ console.log(" list Report the daemons running on this machine (alias: ps).");
11423
+ console.log(" Reads ~/.neat/daemons/ and folds in any registered");
11424
+ console.log(" project no daemon has self-described yet.");
11425
+ console.log(" ps Alias of list.");
11426
+ console.log(" pause <name> Stop a project's daemon until it is started again.");
11427
+ console.log(" resume <name> Bring a paused project back; for a stopped daemon, re-run");
11428
+ console.log(" `neat <path>` to start it.");
11113
11429
  console.log(" uninstall <name>");
11114
- console.log(" Remove a project from the registry. Does not touch");
11430
+ console.log(" Stop a project's daemon and retire it. Does not touch");
11115
11431
  console.log(" neat-out/, policy.json, or any user file.");
11116
11432
  console.log(" prune Drop registry entries whose path is gone from disk.");
11117
11433
  console.log(" Flags: --json emit the removed list as JSON");
@@ -11308,6 +11624,14 @@ function printVersion() {
11308
11624
  process.stdout.write(`${readPackageVersion()}
11309
11625
  `);
11310
11626
  }
11627
+ function formatMachineProjectRow(r) {
11628
+ if (r.ports) {
11629
+ const where = r.pid !== void 0 ? ` pid=${r.pid}` : "";
11630
+ return `${r.project} ${r.state} rest=${r.ports.rest} otlp=${r.ports.otlp} web=${r.ports.web} ${r.projectPath}${where}`;
11631
+ }
11632
+ const status2 = r.registryStatus ? ` (${r.registryStatus})` : "";
11633
+ return `${r.project} ${r.state}${status2} ${r.projectPath}`;
11634
+ }
11311
11635
  function printDiscoveryReport(opts, services) {
11312
11636
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
11313
11637
  const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
@@ -11346,7 +11670,7 @@ async function buildPatchSections(services, project) {
11346
11670
  }
11347
11671
  async function runInit(opts) {
11348
11672
  const written = [];
11349
- const stat = await import_node_fs32.promises.stat(opts.scanPath).catch(() => null);
11673
+ const stat = await import_node_fs34.promises.stat(opts.scanPath).catch(() => null);
11350
11674
  if (!stat || !stat.isDirectory()) {
11351
11675
  console.error(`neat init: ${opts.scanPath} is not a directory`);
11352
11676
  return { exitCode: 2, writtenFiles: written };
@@ -11355,13 +11679,13 @@ async function runInit(opts) {
11355
11679
  printDiscoveryReport(opts, services);
11356
11680
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
11357
11681
  const patch = renderPatch(sections);
11358
- const patchPath = import_node_path50.default.join(opts.scanPath, "neat.patch");
11682
+ const patchPath = import_node_path52.default.join(opts.scanPath, "neat.patch");
11359
11683
  if (opts.dryRun) {
11360
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11684
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11361
11685
  written.push(patchPath);
11362
11686
  console.log(`dry-run: patch written to ${patchPath}`);
11363
- const gitignorePath = import_node_path50.default.join(opts.scanPath, ".gitignore");
11364
- const gitignoreExists = await import_node_fs32.promises.stat(gitignorePath).then(() => true).catch(() => false);
11687
+ const gitignorePath = import_node_path52.default.join(opts.scanPath, ".gitignore");
11688
+ const gitignoreExists = await import_node_fs34.promises.stat(gitignorePath).then(() => true).catch(() => false);
11365
11689
  const verb = gitignoreExists ? "append" : "create";
11366
11690
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
11367
11691
  console.log("rerun without --dry-run to register and snapshot.");
@@ -11372,9 +11696,9 @@ async function runInit(opts) {
11372
11696
  const graph = getGraph(graphKey);
11373
11697
  const projectPaths = pathsForProject(
11374
11698
  graphKey,
11375
- import_node_path50.default.join(opts.scanPath, "neat-out")
11699
+ import_node_path52.default.join(opts.scanPath, "neat-out")
11376
11700
  );
11377
- const errorsPath = import_node_path50.default.join(import_node_path50.default.dirname(opts.outPath), import_node_path50.default.basename(projectPaths.errorsPath));
11701
+ const errorsPath = import_node_path52.default.join(import_node_path52.default.dirname(opts.outPath), import_node_path52.default.basename(projectPaths.errorsPath));
11378
11702
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
11379
11703
  await saveGraphToDisk(graph, opts.outPath);
11380
11704
  written.push(opts.outPath);
@@ -11453,7 +11777,7 @@ async function runInit(opts) {
11453
11777
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
11454
11778
  }
11455
11779
  } else {
11456
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11780
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11457
11781
  written.push(patchPath);
11458
11782
  }
11459
11783
  }
@@ -11493,9 +11817,9 @@ var CLAUDE_SKILL_CONFIG = {
11493
11817
  };
11494
11818
  function claudeConfigPath() {
11495
11819
  const override = process.env.NEAT_CLAUDE_CONFIG;
11496
- if (override && override.length > 0) return import_node_path50.default.resolve(override);
11820
+ if (override && override.length > 0) return import_node_path52.default.resolve(override);
11497
11821
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
11498
- return import_node_path50.default.join(home, ".claude.json");
11822
+ return import_node_path52.default.join(home, ".claude.json");
11499
11823
  }
11500
11824
  async function runSkill(opts) {
11501
11825
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -11507,7 +11831,7 @@ async function runSkill(opts) {
11507
11831
  const target = claudeConfigPath();
11508
11832
  let existing = {};
11509
11833
  try {
11510
- existing = JSON.parse(await import_node_fs32.promises.readFile(target, "utf8"));
11834
+ existing = JSON.parse(await import_node_fs34.promises.readFile(target, "utf8"));
11511
11835
  } catch (err) {
11512
11836
  if (err.code !== "ENOENT") {
11513
11837
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -11519,8 +11843,8 @@ async function runSkill(opts) {
11519
11843
  ...existing,
11520
11844
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
11521
11845
  };
11522
- await import_node_fs32.promises.mkdir(import_node_path50.default.dirname(target), { recursive: true });
11523
- await import_node_fs32.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11846
+ await import_node_fs34.promises.mkdir(import_node_path52.default.dirname(target), { recursive: true });
11847
+ await import_node_fs34.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11524
11848
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
11525
11849
  console.log("restart Claude Code to pick up the new MCP server.");
11526
11850
  return { exitCode: 0 };
@@ -11569,12 +11893,12 @@ async function main() {
11569
11893
  console.error("neat init: --apply and --dry-run are mutually exclusive");
11570
11894
  process.exit(2);
11571
11895
  }
11572
- const scanPath = import_node_path50.default.resolve(target);
11896
+ const scanPath = import_node_path52.default.resolve(target);
11573
11897
  const projectExplicit = parsed.project !== null;
11574
- const projectName = projectExplicit ? project : import_node_path50.default.basename(scanPath);
11898
+ const projectName = projectExplicit ? project : import_node_path52.default.basename(scanPath);
11575
11899
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
11576
- const fallback = pathsForProject(projectKey, import_node_path50.default.join(scanPath, "neat-out")).snapshotPath;
11577
- const outPath = import_node_path50.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11900
+ const fallback = pathsForProject(projectKey, import_node_path52.default.join(scanPath, "neat-out")).snapshotPath;
11901
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11578
11902
  const result = await runInit({
11579
11903
  scanPath,
11580
11904
  outPath,
@@ -11595,21 +11919,21 @@ async function main() {
11595
11919
  usage();
11596
11920
  process.exit(2);
11597
11921
  }
11598
- const scanPath = import_node_path50.default.resolve(target);
11599
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
11922
+ const scanPath = import_node_path52.default.resolve(target);
11923
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11600
11924
  if (!stat || !stat.isDirectory()) {
11601
11925
  console.error(`neat watch: ${scanPath} is not a directory`);
11602
11926
  process.exit(2);
11603
11927
  }
11604
- const projectPaths = pathsForProject(project, import_node_path50.default.join(scanPath, "neat-out"));
11605
- const outPath = import_node_path50.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11606
- const errorsPath = import_node_path50.default.resolve(
11607
- process.env.NEAT_ERRORS_PATH ?? import_node_path50.default.join(import_node_path50.default.dirname(outPath), import_node_path50.default.basename(projectPaths.errorsPath))
11928
+ const projectPaths = pathsForProject(project, import_node_path52.default.join(scanPath, "neat-out"));
11929
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11930
+ const errorsPath = import_node_path52.default.resolve(
11931
+ process.env.NEAT_ERRORS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.errorsPath))
11608
11932
  );
11609
- const staleEventsPath = import_node_path50.default.resolve(
11610
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path50.default.join(import_node_path50.default.dirname(outPath), import_node_path50.default.basename(projectPaths.staleEventsPath))
11933
+ const staleEventsPath = import_node_path52.default.resolve(
11934
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.staleEventsPath))
11611
11935
  );
11612
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path50.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11936
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path52.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11613
11937
  const handle = await startWatch(getGraph(project), {
11614
11938
  scanPath,
11615
11939
  outPath,
@@ -11636,16 +11960,14 @@ async function main() {
11636
11960
  process.on("SIGINT", shutdown);
11637
11961
  return;
11638
11962
  }
11639
- if (cmd === "list") {
11640
- const projects = await listProjects();
11641
- if (projects.length === 0) {
11642
- console.log("no projects registered. run `neat init <path>` to register one.");
11963
+ if (cmd === "list" || cmd === "ps") {
11964
+ const rows = await listMachineProjects();
11965
+ if (rows.length === 0) {
11966
+ console.log("no daemons running and no projects registered. run `neat init <path>` to register one.");
11643
11967
  return;
11644
11968
  }
11645
- for (const p of projects) {
11646
- const seen = p.lastSeenAt ? p.lastSeenAt : "never";
11647
- const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
11648
- console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
11969
+ for (const r of rows) {
11970
+ console.log(formatMachineProjectRow(r));
11649
11971
  }
11650
11972
  return;
11651
11973
  }
@@ -11656,6 +11978,15 @@ async function main() {
11656
11978
  usage();
11657
11979
  process.exit(2);
11658
11980
  }
11981
+ const daemon = await findDaemonByProject(name);
11982
+ if (daemon) {
11983
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
11984
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 stopped daemon pid ${daemon.record.pid}`);
11985
+ } else {
11986
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 daemon was not running`);
11987
+ }
11988
+ return;
11989
+ }
11659
11990
  try {
11660
11991
  const entry2 = await setStatus(name, "paused");
11661
11992
  console.log(`paused: ${entry2.name} (${entry2.path})`);
@@ -11672,6 +12003,15 @@ async function main() {
11672
12003
  usage();
11673
12004
  process.exit(2);
11674
12005
  }
12006
+ const daemon = await findDaemonByProject(name);
12007
+ if (daemon) {
12008
+ if (daemon.live) {
12009
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 daemon already running`);
12010
+ } else {
12011
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 run \`neat ${daemon.record.projectPath}\` to start its daemon again`);
12012
+ }
12013
+ return;
12014
+ }
11675
12015
  try {
11676
12016
  const entry2 = await setStatus(name, "active");
11677
12017
  console.log(`resumed: ${entry2.name} (${entry2.path})`);
@@ -11693,12 +12033,20 @@ async function main() {
11693
12033
  usage();
11694
12034
  process.exit(2);
11695
12035
  }
12036
+ const daemon = await findDaemonByProject(name);
11696
12037
  const removed = await removeProject(name);
11697
- if (!removed) {
12038
+ if (!daemon && !removed) {
11698
12039
  console.error(`neat uninstall: no project named "${name}"`);
11699
12040
  process.exit(1);
11700
12041
  }
11701
- console.log(`unregistered: ${removed.name} (${removed.path})`);
12042
+ const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
12043
+ if (daemon) {
12044
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
12045
+ console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
12046
+ }
12047
+ await removeDaemonRecord(daemon.source);
12048
+ }
12049
+ console.log(`unregistered: ${name} (${projectPath2})`);
11702
12050
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
11703
12051
  return;
11704
12052
  }
@@ -11768,11 +12116,11 @@ async function main() {
11768
12116
  process.exit(1);
11769
12117
  }
11770
12118
  async function tryOrchestrator(cmd, parsed) {
11771
- const scanPath = import_node_path50.default.resolve(cmd);
11772
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
12119
+ const scanPath = import_node_path52.default.resolve(cmd);
12120
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11773
12121
  if (!stat || !stat.isDirectory()) return null;
11774
12122
  const projectExplicit = parsed.project !== null;
11775
- const projectName = projectExplicit ? parsed.project : import_node_path50.default.basename(scanPath);
12123
+ const projectName = projectExplicit ? parsed.project : import_node_path52.default.basename(scanPath);
11776
12124
  const result = await runOrchestrator({
11777
12125
  scanPath,
11778
12126
  project: projectName,