@neat.is/core 0.4.0 → 0.4.3

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.cjs CHANGED
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
57
57
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
58
  const publicRead = opts.publicRead === true;
59
59
  app.addHook("preHandler", (req, reply, done) => {
60
- const path37 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path37 === suffix || path37.endsWith(suffix)) {
62
+ if (path38 === suffix || path38.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -1023,19 +1023,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1023
1023
  function longestIncomingWalk(graph, start, maxDepth) {
1024
1024
  let best = { path: [start], edges: [] };
1025
1025
  const visited = /* @__PURE__ */ new Set([start]);
1026
- function step(node, path37, edges) {
1027
- if (path37.length > best.path.length) {
1028
- best = { path: [...path37], edges: [...edges] };
1026
+ function step(node, path38, edges) {
1027
+ if (path38.length > best.path.length) {
1028
+ best = { path: [...path38], edges: [...edges] };
1029
1029
  }
1030
- if (path37.length - 1 >= maxDepth) return;
1030
+ if (path38.length - 1 >= maxDepth) return;
1031
1031
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1032
1032
  for (const [srcId, edge] of incoming) {
1033
1033
  if (visited.has(srcId)) continue;
1034
1034
  visited.add(srcId);
1035
- path37.push(srcId);
1035
+ path38.push(srcId);
1036
1036
  edges.push(edge);
1037
- step(srcId, path37, edges);
1038
- path37.pop();
1037
+ step(srcId, path38, edges);
1038
+ path38.pop();
1039
1039
  edges.pop();
1040
1040
  visited.delete(srcId);
1041
1041
  }
@@ -2119,8 +2119,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2119
2119
  ".turbo",
2120
2120
  "dist",
2121
2121
  "build",
2122
- ".next"
2122
+ ".next",
2123
+ // Python virtualenv shapes (issue #344). Walking into a venv pulls in the
2124
+ // entire CPython stdlib + every installed package as if it were first-party
2125
+ // service code — 20k+ files, none of which the user wrote. The shape names
2126
+ // cover the three common venv tools (`venv` / `python -m venv`,
2127
+ // `virtualenv`) and the tox + PEP 582 conventions.
2128
+ ".venv",
2129
+ "venv",
2130
+ "__pypackages__",
2131
+ ".tox",
2132
+ // `site-packages` shows up nested inside venvs that don't carry one of the
2133
+ // outer names (system Python on macOS, in-place pyenv layouts). Listing it
2134
+ // here means we stop the walk at the boundary even when the wrapper dir
2135
+ // wasn't recognisable.
2136
+ "site-packages"
2123
2137
  ]);
2138
+ var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
2139
+ async function isPythonVenvDir(dir) {
2140
+ const cached = PYVENV_MARKER_CACHE.get(dir);
2141
+ if (cached !== void 0) return cached;
2142
+ try {
2143
+ const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
2144
+ const ok = stat.isFile();
2145
+ PYVENV_MARKER_CACHE.set(dir, ok);
2146
+ return ok;
2147
+ } catch {
2148
+ PYVENV_MARKER_CACHE.set(dir, false);
2149
+ return false;
2150
+ }
2151
+ }
2124
2152
  function isConfigFile(name) {
2125
2153
  const ext = import_node_path4.default.extname(name);
2126
2154
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
@@ -2457,6 +2485,7 @@ async function walkDirs(start, scanPath, options, visit) {
2457
2485
  const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2458
2486
  if (rel && options.ig.ignores(rel + "/")) continue;
2459
2487
  }
2488
+ if (await isPythonVenvDir(child)) continue;
2460
2489
  await visit(child);
2461
2490
  await recurse(child, depth + 1);
2462
2491
  }
@@ -2735,7 +2764,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
2735
2764
  for (const entry of entries) {
2736
2765
  if (entry.isDirectory()) {
2737
2766
  if (IGNORED_DIRS.has(entry.name)) continue;
2738
- out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2767
+ const child = import_node_path9.default.join(start, entry.name);
2768
+ if (await isPythonVenvDir(child)) continue;
2769
+ out.push(...await walkYamlFiles(child, depth + 1, max));
2739
2770
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2740
2771
  out.push(import_node_path9.default.join(start, entry.name));
2741
2772
  }
@@ -3417,7 +3448,9 @@ async function walkConfigFiles(dir) {
3417
3448
  for (const entry of entries) {
3418
3449
  const full = import_node_path18.default.join(current, entry.name);
3419
3450
  if (entry.isDirectory()) {
3420
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3451
+ if (IGNORED_DIRS.has(entry.name)) continue;
3452
+ if (await isPythonVenvDir(full)) continue;
3453
+ await walk(full);
3421
3454
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
3422
3455
  out.push(full);
3423
3456
  }
@@ -3485,7 +3518,9 @@ async function walkSourceFiles(dir) {
3485
3518
  for (const entry of entries) {
3486
3519
  const full = import_node_path19.default.join(current, entry.name);
3487
3520
  if (entry.isDirectory()) {
3488
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3521
+ if (IGNORED_DIRS.has(entry.name)) continue;
3522
+ if (await isPythonVenvDir(full)) continue;
3523
+ await walk(full);
3489
3524
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3490
3525
  out.push(full);
3491
3526
  }
@@ -4121,7 +4156,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4121
4156
  for (const entry of entries) {
4122
4157
  if (entry.isDirectory()) {
4123
4158
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4124
- out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
4159
+ const child = import_node_path27.default.join(start, entry.name);
4160
+ if (await isPythonVenvDir(child)) continue;
4161
+ out.push(...await walkTfFiles(child, depth + 1, max));
4125
4162
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4126
4163
  out.push(import_node_path27.default.join(start, entry.name));
4127
4164
  }
@@ -4169,7 +4206,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4169
4206
  for (const entry of entries) {
4170
4207
  if (entry.isDirectory()) {
4171
4208
  if (IGNORED_DIRS.has(entry.name)) continue;
4172
- out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
4209
+ const child = import_node_path28.default.join(start, entry.name);
4210
+ if (await isPythonVenvDir(child)) continue;
4211
+ out.push(...await walkYamlFiles2(child, depth + 1, max));
4173
4212
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4174
4213
  out.push(import_node_path28.default.join(start, entry.name));
4175
4214
  }
@@ -5028,10 +5067,19 @@ function projectFromReq(req) {
5028
5067
  const params = req.params;
5029
5068
  return params.project ?? DEFAULT_PROJECT;
5030
5069
  }
5031
- function resolveProject(registry, req, reply) {
5070
+ function resolveProject(registry, req, reply, bootstrap) {
5032
5071
  const name = projectFromReq(req);
5033
5072
  const ctx = registry.get(name);
5034
5073
  if (!ctx) {
5074
+ const phase = bootstrap?.status(name);
5075
+ if (phase === "bootstrapping") {
5076
+ void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
5077
+ return null;
5078
+ }
5079
+ if (phase === "broken") {
5080
+ void reply.code(503).send({ ready: false, project: name, status: "broken" });
5081
+ return null;
5082
+ }
5035
5083
  void reply.code(404).send({ error: "project not found", project: name });
5036
5084
  return null;
5037
5085
  }
@@ -5061,36 +5109,38 @@ function buildLegacyRegistry(opts) {
5061
5109
  function registerRoutes(scope, ctx) {
5062
5110
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
5063
5111
  scope.get("/events", (req, reply) => {
5064
- const proj = resolveProject(registry, req, reply);
5112
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5065
5113
  if (!proj) return;
5066
5114
  handleSse(req, reply, { project: proj.name });
5067
5115
  });
5068
- scope.get("/health", async (req, reply) => {
5069
- const proj = resolveProject(registry, req, reply);
5070
- if (!proj) return;
5071
- const uptimeMs = Date.now() - startedAt;
5072
- return {
5073
- ok: true,
5074
- project: proj.name,
5075
- uptimeMs,
5076
- // Legacy fields kept additively. The web shell's StatusBar reads
5077
- // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5078
- // the canonical triple and lets the extras pass through.
5079
- uptime: Math.floor(uptimeMs / 1e3),
5080
- nodeCount: proj.graph.order,
5081
- edgeCount: proj.graph.size,
5082
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5083
- };
5084
- });
5116
+ if (ctx.scope === "project") {
5117
+ scope.get("/health", async (req, reply) => {
5118
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5119
+ if (!proj) return;
5120
+ const uptimeMs = Date.now() - startedAt;
5121
+ return {
5122
+ ok: true,
5123
+ project: proj.name,
5124
+ uptimeMs,
5125
+ // Legacy fields kept additively. The web shell's StatusBar reads
5126
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5127
+ // the canonical triple and lets the extras pass through.
5128
+ uptime: Math.floor(uptimeMs / 1e3),
5129
+ nodeCount: proj.graph.order,
5130
+ edgeCount: proj.graph.size,
5131
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5132
+ };
5133
+ });
5134
+ }
5085
5135
  scope.get("/graph", async (req, reply) => {
5086
- const proj = resolveProject(registry, req, reply);
5136
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5087
5137
  if (!proj) return;
5088
5138
  return serializeGraph(proj.graph);
5089
5139
  });
5090
5140
  scope.get(
5091
5141
  "/graph/node/:id",
5092
5142
  async (req, reply) => {
5093
- const proj = resolveProject(registry, req, reply);
5143
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5094
5144
  if (!proj) return;
5095
5145
  const { id } = req.params;
5096
5146
  if (!proj.graph.hasNode(id)) {
@@ -5102,7 +5152,7 @@ function registerRoutes(scope, ctx) {
5102
5152
  scope.get(
5103
5153
  "/graph/edges/:id",
5104
5154
  async (req, reply) => {
5105
- const proj = resolveProject(registry, req, reply);
5155
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5106
5156
  if (!proj) return;
5107
5157
  const { id } = req.params;
5108
5158
  if (!proj.graph.hasNode(id)) {
@@ -5114,7 +5164,7 @@ function registerRoutes(scope, ctx) {
5114
5164
  }
5115
5165
  );
5116
5166
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
5117
- const proj = resolveProject(registry, req, reply);
5167
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5118
5168
  if (!proj) return;
5119
5169
  const { nodeId } = req.params;
5120
5170
  if (!proj.graph.hasNode(nodeId)) {
@@ -5129,7 +5179,7 @@ function registerRoutes(scope, ctx) {
5129
5179
  return getTransitiveDependencies(proj.graph, nodeId, depth);
5130
5180
  });
5131
5181
  scope.get("/graph/divergences", async (req, reply) => {
5132
- const proj = resolveProject(registry, req, reply);
5182
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5133
5183
  if (!proj) return;
5134
5184
  let typeFilter;
5135
5185
  if (req.query.type) {
@@ -5164,7 +5214,7 @@ function registerRoutes(scope, ctx) {
5164
5214
  });
5165
5215
  });
5166
5216
  scope.get("/incidents", async (req, reply) => {
5167
- const proj = resolveProject(registry, req, reply);
5217
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5168
5218
  if (!proj) return;
5169
5219
  const epath = errorsPathFor(proj);
5170
5220
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -5176,7 +5226,7 @@ function registerRoutes(scope, ctx) {
5176
5226
  return { count: sliced.length, total, events: sliced };
5177
5227
  });
5178
5228
  scope.get("/stale-events", async (req, reply) => {
5179
- const proj = resolveProject(registry, req, reply);
5229
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5180
5230
  if (!proj) return;
5181
5231
  const spath = staleEventsPathFor(proj);
5182
5232
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -5191,7 +5241,7 @@ function registerRoutes(scope, ctx) {
5191
5241
  scope.get(
5192
5242
  "/incidents/:nodeId",
5193
5243
  async (req, reply) => {
5194
- const proj = resolveProject(registry, req, reply);
5244
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5195
5245
  if (!proj) return;
5196
5246
  const { nodeId } = req.params;
5197
5247
  if (!proj.graph.hasNode(nodeId)) {
@@ -5207,7 +5257,7 @@ function registerRoutes(scope, ctx) {
5207
5257
  }
5208
5258
  );
5209
5259
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
5210
- const proj = resolveProject(registry, req, reply);
5260
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5211
5261
  if (!proj) return;
5212
5262
  const { nodeId } = req.params;
5213
5263
  if (!proj.graph.hasNode(nodeId)) {
@@ -5227,7 +5277,7 @@ function registerRoutes(scope, ctx) {
5227
5277
  return result;
5228
5278
  });
5229
5279
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
5230
- const proj = resolveProject(registry, req, reply);
5280
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5231
5281
  if (!proj) return;
5232
5282
  const { nodeId } = req.params;
5233
5283
  if (!proj.graph.hasNode(nodeId)) {
@@ -5240,7 +5290,7 @@ function registerRoutes(scope, ctx) {
5240
5290
  return getBlastRadius(proj.graph, nodeId, depth);
5241
5291
  });
5242
5292
  scope.get("/search", async (req, reply) => {
5243
- const proj = resolveProject(registry, req, reply);
5293
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5244
5294
  if (!proj) return;
5245
5295
  const raw = (req.query.q ?? "").trim();
5246
5296
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -5271,7 +5321,7 @@ function registerRoutes(scope, ctx) {
5271
5321
  scope.get(
5272
5322
  "/graph/diff",
5273
5323
  async (req, reply) => {
5274
- const proj = resolveProject(registry, req, reply);
5324
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5275
5325
  if (!proj) return;
5276
5326
  const against = req.query.against;
5277
5327
  if (!against) {
@@ -5286,7 +5336,7 @@ function registerRoutes(scope, ctx) {
5286
5336
  }
5287
5337
  );
5288
5338
  scope.post("/snapshot", async (req, reply) => {
5289
- const proj = resolveProject(registry, req, reply);
5339
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5290
5340
  if (!proj) return;
5291
5341
  const body = req.body;
5292
5342
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -5315,7 +5365,7 @@ function registerRoutes(scope, ctx) {
5315
5365
  }
5316
5366
  });
5317
5367
  scope.post("/graph/scan", async (req, reply) => {
5318
- const proj = resolveProject(registry, req, reply);
5368
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5319
5369
  if (!proj) return;
5320
5370
  if (!proj.scanPath) {
5321
5371
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -5331,7 +5381,7 @@ function registerRoutes(scope, ctx) {
5331
5381
  };
5332
5382
  });
5333
5383
  scope.get("/policies", async (req, reply) => {
5334
- const proj = resolveProject(registry, req, reply);
5384
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5335
5385
  if (!proj) return;
5336
5386
  const policyPath = ctx.policyFilePathFor(proj);
5337
5387
  if (!policyPath) {
@@ -5348,7 +5398,7 @@ function registerRoutes(scope, ctx) {
5348
5398
  }
5349
5399
  });
5350
5400
  scope.get("/policies/violations", async (req, reply) => {
5351
- const proj = resolveProject(registry, req, reply);
5401
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5352
5402
  if (!proj) return;
5353
5403
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5354
5404
  let violations = await log.readAll();
@@ -5368,7 +5418,7 @@ function registerRoutes(scope, ctx) {
5368
5418
  return { violations };
5369
5419
  });
5370
5420
  scope.post("/policies/check", async (req, reply) => {
5371
- const proj = resolveProject(registry, req, reply);
5421
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5372
5422
  if (!proj) return;
5373
5423
  const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5374
5424
  if (!parsed.success) {
@@ -5439,10 +5489,36 @@ async function buildApi(opts) {
5439
5489
  const routeCtx = {
5440
5490
  registry,
5441
5491
  startedAt,
5492
+ scope: "root",
5442
5493
  errorsPathFor,
5443
5494
  staleEventsPathFor,
5444
- policyFilePathFor
5495
+ policyFilePathFor,
5496
+ bootstrap: opts.bootstrap
5445
5497
  };
5498
+ app.get("/health", async () => {
5499
+ const uptimeMs = Date.now() - startedAt;
5500
+ const bootstrapList = opts.bootstrap?.list() ?? [];
5501
+ const byName = new Map(bootstrapList.map((p) => [p.name, p]));
5502
+ const names = /* @__PURE__ */ new Set([
5503
+ ...registry.list(),
5504
+ ...bootstrapList.map((p) => p.name)
5505
+ ]);
5506
+ const projects = [...names].sort().map((name) => {
5507
+ const proj = registry.get(name);
5508
+ const tracked = byName.get(name);
5509
+ return {
5510
+ name,
5511
+ nodeCount: proj?.graph.order ?? 0,
5512
+ edgeCount: proj?.graph.size ?? 0,
5513
+ ...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
5514
+ };
5515
+ });
5516
+ return {
5517
+ ok: true,
5518
+ uptimeMs,
5519
+ projects
5520
+ };
5521
+ });
5446
5522
  app.get("/projects", async (_req, reply) => {
5447
5523
  try {
5448
5524
  return await listProjects();
@@ -5470,7 +5546,7 @@ async function buildApi(opts) {
5470
5546
  registerRoutes(app, routeCtx);
5471
5547
  await app.register(
5472
5548
  async (scope) => {
5473
- registerRoutes(scope, routeCtx);
5549
+ registerRoutes(scope, { ...routeCtx, scope: "project" });
5474
5550
  },
5475
5551
  { prefix: "/projects/:project" }
5476
5552
  );
@@ -5483,16 +5559,39 @@ init_otel_grpc();
5483
5559
 
5484
5560
  // src/daemon.ts
5485
5561
  init_cjs_shims();
5486
- var import_node_fs21 = require("fs");
5487
- var import_node_path36 = __toESM(require("path"), 1);
5562
+ var import_node_fs22 = require("fs");
5563
+ var import_node_path37 = __toESM(require("path"), 1);
5488
5564
  init_otel();
5489
5565
  init_auth();
5566
+
5567
+ // src/unrouted.ts
5568
+ init_cjs_shims();
5569
+ var import_node_fs21 = require("fs");
5570
+ var import_node_path36 = __toESM(require("path"), 1);
5571
+ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5572
+ return {
5573
+ timestamp: now.toISOString(),
5574
+ reason: "no-project-match",
5575
+ service_name: serviceName ?? null,
5576
+ traceId: traceId ?? null
5577
+ };
5578
+ }
5579
+ async function appendUnroutedSpan(neatHome2, record) {
5580
+ const target = import_node_path36.default.join(neatHome2, "errors.ndjson");
5581
+ await import_node_fs21.promises.mkdir(neatHome2, { recursive: true });
5582
+ await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
5583
+ }
5584
+ function unroutedErrorsPath(neatHome2) {
5585
+ return import_node_path36.default.join(neatHome2, "errors.ndjson");
5586
+ }
5587
+
5588
+ // src/daemon.ts
5490
5589
  function neatHomeFor(opts) {
5491
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
5590
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
5492
5591
  const env = process.env.NEAT_HOME;
5493
- if (env && env.length > 0) return import_node_path36.default.resolve(env);
5592
+ if (env && env.length > 0) return import_node_path37.default.resolve(env);
5494
5593
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5495
- return import_node_path36.default.join(home, ".neat");
5594
+ return import_node_path37.default.join(home, ".neat");
5496
5595
  }
5497
5596
  function routeSpanToProject(serviceName, projects) {
5498
5597
  if (!serviceName) return DEFAULT_PROJECT;
@@ -5527,9 +5626,9 @@ function isTokenContained(needle, haystack) {
5527
5626
  return tokens.includes(needle);
5528
5627
  }
5529
5628
  async function bootstrapProject(entry) {
5530
- const paths = pathsForProject(entry.name, import_node_path36.default.join(entry.path, "neat-out"));
5629
+ const paths = pathsForProject(entry.name, import_node_path37.default.join(entry.path, "neat-out"));
5531
5630
  try {
5532
- const stat = await import_node_fs21.promises.stat(entry.path);
5631
+ const stat = await import_node_fs22.promises.stat(entry.path);
5533
5632
  if (!stat.isDirectory()) {
5534
5633
  throw new Error(`registered path ${entry.path} is not a directory`);
5535
5634
  }
@@ -5584,27 +5683,30 @@ function resolveOtlpPort(opts) {
5584
5683
  }
5585
5684
  return 4318;
5586
5685
  }
5587
- function resolveHost(opts) {
5686
+ function resolveHost(opts, authTokenSet) {
5588
5687
  if (opts.host && opts.host.length > 0) return opts.host;
5589
5688
  const env = process.env.HOST;
5590
5689
  if (env && env.length > 0) return env;
5690
+ if (!authTokenSet) return "127.0.0.1";
5591
5691
  return "0.0.0.0";
5592
5692
  }
5593
5693
  async function startDaemon(opts = {}) {
5594
5694
  const home = neatHomeFor(opts);
5595
5695
  const regPath = registryPath();
5596
5696
  try {
5597
- await import_node_fs21.promises.access(regPath);
5697
+ await import_node_fs22.promises.access(regPath);
5598
5698
  } catch {
5599
5699
  throw new Error(
5600
5700
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5601
5701
  );
5602
5702
  }
5603
- const pidPath = import_node_path36.default.join(home, "neatd.pid");
5703
+ const pidPath = import_node_path37.default.join(home, "neatd.pid");
5604
5704
  await writeAtomically(pidPath, `${process.pid}
5605
5705
  `);
5606
5706
  const slots = /* @__PURE__ */ new Map();
5607
5707
  const registry = new Projects();
5708
+ const bootstrapStatus = /* @__PURE__ */ new Map();
5709
+ const bootstrapStartedAt = /* @__PURE__ */ new Map();
5608
5710
  const DROP_WARN_INTERVAL_MS = 6e4;
5609
5711
  const lastDropWarnAt = /* @__PURE__ */ new Map();
5610
5712
  function warnDroppedSpan(project, reason) {
@@ -5616,6 +5718,22 @@ async function startDaemon(opts = {}) {
5616
5718
  `[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
5617
5719
  );
5618
5720
  }
5721
+ const unroutedPath = unroutedErrorsPath(home);
5722
+ const lastUnroutedWarnAt = /* @__PURE__ */ new Map();
5723
+ async function recordUnroutedSpan(serviceName, traceId) {
5724
+ const key = serviceName ?? "<missing>";
5725
+ const now = Date.now();
5726
+ try {
5727
+ await appendUnroutedSpan(home, buildUnroutedSpanRecord(serviceName, traceId, new Date(now)));
5728
+ } catch {
5729
+ }
5730
+ const prev = lastUnroutedWarnAt.get(key) ?? 0;
5731
+ if (now - prev < DROP_WARN_INTERVAL_MS) return;
5732
+ lastUnroutedWarnAt.set(key, now);
5733
+ console.warn(
5734
+ `[neatd] dropping span \u2014 service.name "${key}" matches no registered project and no \`default\` project exists. See ${unroutedPath}.`
5735
+ );
5736
+ }
5619
5737
  function upsertRegistryFromSlot(slot) {
5620
5738
  if (slot.status !== "active") return;
5621
5739
  registry.set(slot.entry.name, {
@@ -5644,34 +5762,43 @@ async function startDaemon(opts = {}) {
5644
5762
  return slots.get(entry.name);
5645
5763
  }
5646
5764
  }
5765
+ async function bootstrapOne(entry) {
5766
+ bootstrapStatus.set(entry.name, "bootstrapping");
5767
+ bootstrapStartedAt.set(entry.name, Date.now());
5768
+ try {
5769
+ const slot = await bootstrapProject(entry);
5770
+ slots.set(entry.name, slot);
5771
+ upsertRegistryFromSlot(slot);
5772
+ bootstrapStatus.set(entry.name, slot.status === "broken" ? "broken" : "active");
5773
+ if (slot.status === "broken") {
5774
+ console.warn(`neatd: project "${entry.name}" broken \u2014 ${slot.errorReason}`);
5775
+ } else {
5776
+ console.log(`neatd: project "${entry.name}" active (${entry.path})`);
5777
+ }
5778
+ } catch (err) {
5779
+ bootstrapStatus.set(entry.name, "broken");
5780
+ console.warn(
5781
+ `neatd: project "${entry.name}" failed to bootstrap \u2014 ${err.message}`
5782
+ );
5783
+ await setStatus(entry.name, "broken").catch(() => {
5784
+ });
5785
+ }
5786
+ }
5647
5787
  async function loadAll() {
5648
5788
  const projects = await listProjects();
5649
5789
  const seen = /* @__PURE__ */ new Set();
5790
+ const pending = [];
5650
5791
  for (const entry of projects) {
5651
5792
  seen.add(entry.name);
5652
5793
  const existing = slots.get(entry.name);
5653
5794
  if (existing) {
5654
5795
  if (existing.status === "broken") {
5655
- await tryRecoverSlot(entry);
5796
+ pending.push(tryRecoverSlot(entry).then(() => {
5797
+ }));
5656
5798
  }
5657
5799
  continue;
5658
5800
  }
5659
- try {
5660
- const slot = await bootstrapProject(entry);
5661
- slots.set(entry.name, slot);
5662
- upsertRegistryFromSlot(slot);
5663
- if (slot.status === "broken") {
5664
- console.warn(`neatd: project "${entry.name}" broken \u2014 ${slot.errorReason}`);
5665
- } else {
5666
- console.log(`neatd: project "${entry.name}" active (${entry.path})`);
5667
- }
5668
- } catch (err) {
5669
- console.warn(
5670
- `neatd: project "${entry.name}" failed to bootstrap \u2014 ${err.message}`
5671
- );
5672
- await setStatus(entry.name, "broken").catch(() => {
5673
- });
5674
- }
5801
+ pending.push(bootstrapOne(entry));
5675
5802
  }
5676
5803
  for (const [name, slot] of [...slots.entries()]) {
5677
5804
  if (seen.has(name)) continue;
@@ -5680,27 +5807,45 @@ async function startDaemon(opts = {}) {
5680
5807
  } catch {
5681
5808
  }
5682
5809
  slots.delete(name);
5810
+ bootstrapStatus.delete(name);
5811
+ bootstrapStartedAt.delete(name);
5683
5812
  console.log(`neatd: project "${name}" removed from registry \u2014 stopped`);
5684
5813
  }
5814
+ await Promise.allSettled(pending);
5815
+ }
5816
+ const initialEntries = await listProjects().catch(() => []);
5817
+ for (const entry of initialEntries) {
5818
+ bootstrapStatus.set(entry.name, "bootstrapping");
5819
+ bootstrapStartedAt.set(entry.name, Date.now());
5685
5820
  }
5686
- await loadAll();
5687
5821
  const bind = opts.bindListeners !== false;
5688
5822
  let restApp = null;
5689
5823
  let otlpApp = null;
5690
5824
  let restAddress = "";
5691
5825
  let otlpAddress = "";
5692
5826
  if (bind) {
5693
- const host = resolveHost(opts);
5827
+ const auth = readAuthEnv();
5828
+ const host = resolveHost(opts, Boolean(auth.authToken));
5694
5829
  const restPort = resolveRestPort(opts);
5695
5830
  const otlpPort = resolveOtlpPort(opts);
5696
- const auth = readAuthEnv();
5697
5831
  assertBindAuthority(host, auth.authToken);
5698
5832
  try {
5699
5833
  restApp = await buildApi({
5700
5834
  projects: registry,
5701
5835
  authToken: auth.authToken,
5702
5836
  trustProxy: auth.trustProxy,
5703
- publicRead: auth.publicRead
5837
+ publicRead: auth.publicRead,
5838
+ bootstrap: {
5839
+ status: (name) => bootstrapStatus.get(name),
5840
+ list: () => {
5841
+ const now = Date.now();
5842
+ return [...bootstrapStatus.entries()].map(([name, status2]) => ({
5843
+ name,
5844
+ status: status2,
5845
+ elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
5846
+ }));
5847
+ }
5848
+ }
5704
5849
  });
5705
5850
  restAddress = await restApp.listen({ port: restPort, host });
5706
5851
  console.log(`neatd: REST listening on ${restAddress}`);
@@ -5713,17 +5858,20 @@ async function startDaemon(opts = {}) {
5713
5858
  }
5714
5859
  if (restApp) await restApp.close().catch(() => {
5715
5860
  });
5716
- await import_node_fs21.promises.unlink(pidPath).catch(() => {
5861
+ await import_node_fs22.promises.unlink(pidPath).catch(() => {
5717
5862
  });
5718
5863
  throw new Error(
5719
5864
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
5720
5865
  );
5721
5866
  }
5722
- async function resolveTargetSlot(serviceName) {
5867
+ async function resolveTargetSlot(serviceName, traceId) {
5723
5868
  const liveEntries = await listProjects().catch(() => []);
5724
5869
  const target = routeSpanToProject(serviceName, liveEntries);
5725
5870
  let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
5726
- if (!slot) return null;
5871
+ if (!slot) {
5872
+ await recordUnroutedSpan(serviceName, traceId);
5873
+ return null;
5874
+ }
5727
5875
  if (slot.status === "broken") {
5728
5876
  const entry = liveEntries.find((e) => e.name === slot.entry.name);
5729
5877
  if (entry) {
@@ -5741,7 +5889,7 @@ async function startDaemon(opts = {}) {
5741
5889
  authToken: auth.otelToken,
5742
5890
  trustProxy: auth.trustProxy,
5743
5891
  onSpan: async (span) => {
5744
- const slot = await resolveTargetSlot(span.service);
5892
+ const slot = await resolveTargetSlot(span.service, span.traceId);
5745
5893
  if (!slot) return;
5746
5894
  await handleSpan(
5747
5895
  {
@@ -5755,7 +5903,7 @@ async function startDaemon(opts = {}) {
5755
5903
  );
5756
5904
  },
5757
5905
  onErrorSpanSync: async (span) => {
5758
- const slot = await resolveTargetSlot(span.service);
5906
+ const slot = await resolveTargetSlot(span.service, span.traceId);
5759
5907
  if (!slot) return;
5760
5908
  await makeErrorSpanWriter(slot.paths.errorsPath)(span);
5761
5909
  }
@@ -5773,14 +5921,17 @@ async function startDaemon(opts = {}) {
5773
5921
  });
5774
5922
  if (otlpApp) await otlpApp.close().catch(() => {
5775
5923
  });
5776
- await import_node_fs21.promises.unlink(pidPath).catch(() => {
5924
+ await import_node_fs22.promises.unlink(pidPath).catch(() => {
5777
5925
  });
5778
5926
  throw new Error(
5779
5927
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
5780
5928
  );
5781
5929
  }
5782
5930
  }
5783
- let reloading = null;
5931
+ const initialBootstrap = loadAll().catch((err) => {
5932
+ console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
5933
+ });
5934
+ let reloading = initialBootstrap;
5784
5935
  const reload = async () => {
5785
5936
  if (reloading) return reloading;
5786
5937
  reloading = (async () => {
@@ -5792,6 +5943,20 @@ async function startDaemon(opts = {}) {
5792
5943
  })();
5793
5944
  return reloading;
5794
5945
  };
5946
+ void initialBootstrap.finally(() => {
5947
+ if (reloading === initialBootstrap) reloading = null;
5948
+ });
5949
+ const tracker = {
5950
+ status: (name) => bootstrapStatus.get(name),
5951
+ list: () => {
5952
+ const now = Date.now();
5953
+ return [...bootstrapStatus.entries()].map(([name, status2]) => ({
5954
+ name,
5955
+ status: status2,
5956
+ elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
5957
+ }));
5958
+ }
5959
+ };
5795
5960
  const sighupHandler = () => {
5796
5961
  void reload().catch((err) => {
5797
5962
  console.warn(`neatd: SIGHUP reload failed \u2014 ${err.message}`);
@@ -5813,10 +5978,19 @@ async function startDaemon(opts = {}) {
5813
5978
  } catch {
5814
5979
  }
5815
5980
  }
5816
- await import_node_fs21.promises.unlink(pidPath).catch(() => {
5981
+ await import_node_fs22.promises.unlink(pidPath).catch(() => {
5817
5982
  });
5818
5983
  };
5819
- return { slots, reload, stop, pidPath, restAddress, otlpAddress };
5984
+ return {
5985
+ slots,
5986
+ reload,
5987
+ stop,
5988
+ pidPath,
5989
+ restAddress,
5990
+ otlpAddress,
5991
+ bootstrap: tracker,
5992
+ initialBootstrap
5993
+ };
5820
5994
  }
5821
5995
  // Annotate the CommonJS export names for ESM import in node:
5822
5996
  0 && (module.exports = {