@neat.is/core 0.4.16 → 0.4.18

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/neatd.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", (req2, reply, done) => {
60
- const path44 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path45 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path44 === suffix || path44.endsWith(suffix)) {
62
+ if (path45 === suffix || path45.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -187,8 +187,8 @@ function reshapeGrpcRequest(req2) {
187
187
  };
188
188
  }
189
189
  function resolveProtoRoot() {
190
- const here = import_node_path38.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
191
- return import_node_path38.default.resolve(here, "..", "proto");
190
+ const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
191
+ return import_node_path39.default.resolve(here, "..", "proto");
192
192
  }
193
193
  function loadTraceService() {
194
194
  const protoRoot = resolveProtoRoot();
@@ -256,13 +256,13 @@ async function startOtelGrpcReceiver(opts) {
256
256
  })
257
257
  };
258
258
  }
259
- var import_node_url, import_node_path38, import_node_crypto2, grpc, protoLoader;
259
+ var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
260
260
  var init_otel_grpc = __esm({
261
261
  "src/otel-grpc.ts"() {
262
262
  "use strict";
263
263
  init_cjs_shims();
264
264
  import_node_url = require("url");
265
- import_node_path38 = __toESM(require("path"), 1);
265
+ import_node_path39 = __toESM(require("path"), 1);
266
266
  import_node_crypto2 = require("crypto");
267
267
  grpc = __toESM(require("@grpc/grpc-js"), 1);
268
268
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -372,10 +372,10 @@ function parseOtlpRequest(body) {
372
372
  return out;
373
373
  }
374
374
  function loadProtoRoot() {
375
- const here = import_node_path39.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
376
- const protoRoot = import_node_path39.default.resolve(here, "..", "proto");
375
+ const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
376
+ const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
377
377
  const root = new import_protobufjs.default.Root();
378
- root.resolvePath = (_origin, target) => import_node_path39.default.resolve(protoRoot, target);
378
+ root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
379
379
  root.loadSync(
380
380
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
381
381
  { keepCase: true }
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
473
473
  for (const s of spans) projectQueue.push({ project, span: s });
474
474
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
475
475
  };
476
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
476
477
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
477
478
  function warnLegacyEndpoint(serviceName) {
479
+ if (!offersProjectRouting) return;
478
480
  if (legacyEndpointWarned.has(serviceName)) return;
479
481
  legacyEndpointWarned.add(serviceName);
480
482
  console.warn(
@@ -571,12 +573,12 @@ async function buildOtelReceiver(opts) {
571
573
  };
572
574
  return decorated;
573
575
  }
574
- var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
576
+ var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
575
577
  var init_otel = __esm({
576
578
  "src/otel.ts"() {
577
579
  "use strict";
578
580
  init_cjs_shims();
579
- import_node_path39 = __toESM(require("path"), 1);
581
+ import_node_path40 = __toESM(require("path"), 1);
580
582
  import_node_url2 = require("url");
581
583
  import_fastify2 = __toESM(require("fastify"), 1);
582
584
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -592,14 +594,15 @@ var init_otel = __esm({
592
594
 
593
595
  // src/neatd.ts
594
596
  init_cjs_shims();
595
- var import_node_fs26 = require("fs");
596
- var import_node_path43 = __toESM(require("path"), 1);
597
- var import_node_module = require("module");
597
+ var import_node_fs27 = require("fs");
598
+ var import_node_path44 = __toESM(require("path"), 1);
599
+ var import_node_module2 = require("module");
598
600
 
599
601
  // src/daemon.ts
600
602
  init_cjs_shims();
601
603
  var import_node_fs25 = require("fs");
602
- var import_node_path41 = __toESM(require("path"), 1);
604
+ var import_node_path42 = __toESM(require("path"), 1);
605
+ var import_node_module = require("module");
603
606
 
604
607
  // src/graph.ts
605
608
  init_cjs_shims();
@@ -1128,19 +1131,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1128
1131
  function longestIncomingWalk(graph, start, maxDepth) {
1129
1132
  let best = { path: [start], edges: [] };
1130
1133
  const visited = /* @__PURE__ */ new Set([start]);
1131
- function step(node, path44, edges) {
1132
- if (path44.length > best.path.length) {
1133
- best = { path: [...path44], edges: [...edges] };
1134
+ function step(node, path45, edges) {
1135
+ if (path45.length > best.path.length) {
1136
+ best = { path: [...path45], edges: [...edges] };
1134
1137
  }
1135
- if (path44.length - 1 >= maxDepth) return;
1138
+ if (path45.length - 1 >= maxDepth) return;
1136
1139
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1137
1140
  for (const [srcId, edge] of incoming) {
1138
1141
  if (visited.has(srcId)) continue;
1139
1142
  visited.add(srcId);
1140
- path44.push(srcId);
1143
+ path45.push(srcId);
1141
1144
  edges.push(edge);
1142
- step(srcId, path44, edges);
1143
- path44.pop();
1145
+ step(srcId, path45, edges);
1146
+ path45.pop();
1144
1147
  edges.pop();
1145
1148
  visited.delete(srcId);
1146
1149
  }
@@ -1658,6 +1661,41 @@ var DEFAULT_STALE_THRESHOLDS = {
1658
1661
  CONFIGURED_BY: DAY_MS,
1659
1662
  RUNS_ON: DAY_MS
1660
1663
  };
1664
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1665
+ threshold: 5,
1666
+ windowMs: 6e4
1667
+ };
1668
+ function loadIncidentThresholdsFromEnv() {
1669
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1670
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1671
+ try {
1672
+ const overrides = JSON.parse(raw);
1673
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1674
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1675
+ merged.threshold = Math.floor(overrides.threshold);
1676
+ }
1677
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1678
+ merged.windowMs = overrides.windowMs;
1679
+ }
1680
+ return merged;
1681
+ } catch (err) {
1682
+ console.warn(
1683
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1684
+ );
1685
+ return DEFAULT_INCIDENT_THRESHOLDS;
1686
+ }
1687
+ }
1688
+ function httpResponseStatus(span) {
1689
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1690
+ const v = span.attributes[key];
1691
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1692
+ if (typeof v === "string") {
1693
+ const n = Number(v);
1694
+ if (Number.isFinite(n)) return n;
1695
+ }
1696
+ }
1697
+ return void 0;
1698
+ }
1661
1699
  function nowIso(ctx) {
1662
1700
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1663
1701
  }
@@ -2071,6 +2109,71 @@ function makeErrorSpanWriter(errorsPath) {
2071
2109
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
2072
2110
  };
2073
2111
  }
2112
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
2113
+ const attrs = sanitizeAttributes(span.attributes);
2114
+ const first = firstTimestamp ?? timestamp;
2115
+ const peer = pickAddress(span);
2116
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
2117
+ const ev = {
2118
+ id: `${span.traceId}:${span.spanId}`,
2119
+ timestamp,
2120
+ service: span.service,
2121
+ traceId: span.traceId,
2122
+ spanId: span.spanId,
2123
+ errorType: "http-failure",
2124
+ errorMessage: message,
2125
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2126
+ affectedNode,
2127
+ httpStatusCode: statusCode,
2128
+ incidentCount: count,
2129
+ firstTimestamp: first,
2130
+ lastTimestamp: timestamp
2131
+ };
2132
+ await appendErrorEvent(ctx, ev);
2133
+ }
2134
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2135
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2136
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
2137
+ const peer = pickAddress(span) ?? span.spanId;
2138
+ const key = `${span.service}->${peer}`;
2139
+ const existing = ctx.burstState.get(key);
2140
+ let state;
2141
+ if (existing && nowMs - existing.lastMs <= windowMs) {
2142
+ existing.count += 1;
2143
+ existing.lastTs = ts;
2144
+ existing.lastMs = nowMs;
2145
+ existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
2146
+ state = existing;
2147
+ } else {
2148
+ state = {
2149
+ count: 1,
2150
+ firstTs: ts,
2151
+ lastTs: ts,
2152
+ lastMs: nowMs,
2153
+ codes: /* @__PURE__ */ new Map([[status2, 1]])
2154
+ };
2155
+ ctx.burstState.set(key, state);
2156
+ }
2157
+ if (state.count < threshold) return;
2158
+ let dominant = status2;
2159
+ let max = 0;
2160
+ for (const [code, n] of state.codes) {
2161
+ if (n > max) {
2162
+ max = n;
2163
+ dominant = code;
2164
+ }
2165
+ }
2166
+ await recordFailingResponseIncident(
2167
+ ctx,
2168
+ span,
2169
+ affectedNode,
2170
+ state.lastTs,
2171
+ dominant,
2172
+ state.count,
2173
+ state.firstTs
2174
+ );
2175
+ ctx.burstState.delete(key);
2176
+ }
2074
2177
  async function handleSpan(ctx, span) {
2075
2178
  const ts = span.startTimeIso ?? nowIso(ctx);
2076
2179
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -2169,6 +2272,14 @@ async function handleSpan(ctx, span) {
2169
2272
  await appendErrorEvent(ctx, ev);
2170
2273
  }
2171
2274
  }
2275
+ if (span.statusCode !== 2) {
2276
+ const status2 = httpResponseStatus(span);
2277
+ if (status2 !== void 0 && status2 >= 500) {
2278
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2279
+ } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2280
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
2281
+ }
2282
+ }
2172
2283
  void affectedNode;
2173
2284
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2174
2285
  }
@@ -4115,7 +4226,7 @@ async function addConfigNodes(graph, services, scanPath) {
4115
4226
 
4116
4227
  // src/extract/calls/index.ts
4117
4228
  init_cjs_shims();
4118
- var import_types16 = require("@neat.is/types");
4229
+ var import_types17 = require("@neat.is/types");
4119
4230
 
4120
4231
  // src/extract/calls/http.ts
4121
4232
  init_cjs_shims();
@@ -4468,15 +4579,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
4468
4579
  return out;
4469
4580
  }
4470
4581
 
4582
+ // src/extract/calls/supabase.ts
4583
+ init_cjs_shims();
4584
+ var import_node_path27 = __toESM(require("path"), 1);
4585
+ var import_types16 = require("@neat.is/types");
4586
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4587
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4588
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4589
+ function hostFromLiteral(literal) {
4590
+ if (!literal) return null;
4591
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4592
+ if (!m) return null;
4593
+ const host = m[1];
4594
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4595
+ return host;
4596
+ }
4597
+ function readImports2(content) {
4598
+ return {
4599
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4600
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4601
+ };
4602
+ }
4603
+ function constructorMatchesImport(name, ctx) {
4604
+ if (name === "createClient") return ctx.hasSupabaseJs;
4605
+ return ctx.hasSupabaseSsr;
4606
+ }
4607
+ function supabaseEndpointsFromFile(file, serviceDir) {
4608
+ const ctx = readImports2(file.content);
4609
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4610
+ const out = [];
4611
+ const seen = /* @__PURE__ */ new Set();
4612
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4613
+ let m;
4614
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4615
+ const ctor = m[1];
4616
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4617
+ const host = hostFromLiteral(m[2]);
4618
+ const name = host ?? "env";
4619
+ if (seen.has(name)) continue;
4620
+ seen.add(name);
4621
+ const line = lineOf(file.content, m[0]);
4622
+ out.push({
4623
+ infraId: (0, import_types16.infraId)("supabase", name),
4624
+ name,
4625
+ kind: "supabase",
4626
+ edgeType: "CALLS",
4627
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4628
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4629
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4630
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4631
+ confidenceKind: "verified-call-site",
4632
+ evidence: {
4633
+ file: import_node_path27.default.relative(serviceDir, file.path),
4634
+ line,
4635
+ snippet: snippet(file.content, line)
4636
+ }
4637
+ });
4638
+ }
4639
+ return out;
4640
+ }
4641
+
4471
4642
  // src/extract/calls/index.ts
4472
4643
  function edgeTypeFromEndpoint(ep) {
4473
4644
  switch (ep.edgeType) {
4474
4645
  case "PUBLISHES_TO":
4475
- return import_types16.EdgeType.PUBLISHES_TO;
4646
+ return import_types17.EdgeType.PUBLISHES_TO;
4476
4647
  case "CONSUMES_FROM":
4477
- return import_types16.EdgeType.CONSUMES_FROM;
4648
+ return import_types17.EdgeType.CONSUMES_FROM;
4478
4649
  default:
4479
- return import_types16.EdgeType.CALLS;
4650
+ return import_types17.EdgeType.CALLS;
4480
4651
  }
4481
4652
  }
4482
4653
  function isAwsKind(kind) {
@@ -4496,6 +4667,7 @@ async function addExternalEndpointEdges(graph, services) {
4496
4667
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4497
4668
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4498
4669
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4670
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4499
4671
  }
4500
4672
  if (endpoints.length === 0) continue;
4501
4673
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4503,7 +4675,7 @@ async function addExternalEndpointEdges(graph, services) {
4503
4675
  if (!graph.hasNode(ep.infraId)) {
4504
4676
  const node = {
4505
4677
  id: ep.infraId,
4506
- type: import_types16.NodeType.InfraNode,
4678
+ type: import_types17.NodeType.InfraNode,
4507
4679
  name: ep.name,
4508
4680
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4509
4681
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4515,7 +4687,7 @@ async function addExternalEndpointEdges(graph, services) {
4515
4687
  nodesAdded++;
4516
4688
  }
4517
4689
  const edgeType = edgeTypeFromEndpoint(ep);
4518
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4690
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4519
4691
  const relFile = toPosix2(ep.evidence.file);
4520
4692
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4521
4693
  graph,
@@ -4525,7 +4697,7 @@ async function addExternalEndpointEdges(graph, services) {
4525
4697
  );
4526
4698
  nodesAdded += n;
4527
4699
  edgesAdded += e;
4528
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4700
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4529
4701
  noteExtractedDropped({
4530
4702
  source: fileNodeId,
4531
4703
  target: ep.infraId,
@@ -4545,7 +4717,7 @@ async function addExternalEndpointEdges(graph, services) {
4545
4717
  source: fileNodeId,
4546
4718
  target: ep.infraId,
4547
4719
  type: edgeType,
4548
- provenance: import_types16.Provenance.EXTRACTED,
4720
+ provenance: import_types17.Provenance.EXTRACTED,
4549
4721
  confidence,
4550
4722
  evidence: ep.evidence
4551
4723
  };
@@ -4570,16 +4742,16 @@ init_cjs_shims();
4570
4742
 
4571
4743
  // src/extract/infra/docker-compose.ts
4572
4744
  init_cjs_shims();
4573
- var import_node_path27 = __toESM(require("path"), 1);
4574
- var import_types18 = require("@neat.is/types");
4745
+ var import_node_path28 = __toESM(require("path"), 1);
4746
+ var import_types19 = require("@neat.is/types");
4575
4747
 
4576
4748
  // src/extract/infra/shared.ts
4577
4749
  init_cjs_shims();
4578
- var import_types17 = require("@neat.is/types");
4750
+ var import_types18 = require("@neat.is/types");
4579
4751
  function makeInfraNode(kind, name, provider = "self", extras) {
4580
4752
  return {
4581
- id: (0, import_types17.infraId)(kind, name),
4582
- type: import_types17.NodeType.InfraNode,
4753
+ id: (0, import_types18.infraId)(kind, name),
4754
+ type: import_types18.NodeType.InfraNode,
4583
4755
  name,
4584
4756
  provider,
4585
4757
  kind,
@@ -4608,7 +4780,7 @@ function dependsOnList(value) {
4608
4780
  }
4609
4781
  function serviceNameToServiceNode(name, services) {
4610
4782
  for (const s of services) {
4611
- if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4783
+ if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
4612
4784
  }
4613
4785
  return null;
4614
4786
  }
@@ -4617,7 +4789,7 @@ async function addComposeInfra(graph, scanPath, services) {
4617
4789
  let edgesAdded = 0;
4618
4790
  let composePath = null;
4619
4791
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4620
- const abs = import_node_path27.default.join(scanPath, name);
4792
+ const abs = import_node_path28.default.join(scanPath, name);
4621
4793
  if (await exists(abs)) {
4622
4794
  composePath = abs;
4623
4795
  break;
@@ -4630,13 +4802,13 @@ async function addComposeInfra(graph, scanPath, services) {
4630
4802
  } catch (err) {
4631
4803
  recordExtractionError(
4632
4804
  "infra docker-compose",
4633
- import_node_path27.default.relative(scanPath, composePath),
4805
+ import_node_path28.default.relative(scanPath, composePath),
4634
4806
  err
4635
4807
  );
4636
4808
  return { nodesAdded, edgesAdded };
4637
4809
  }
4638
4810
  if (!compose?.services) return { nodesAdded, edgesAdded };
4639
- const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4811
+ const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
4640
4812
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4641
4813
  for (const [composeName, svc] of Object.entries(compose.services)) {
4642
4814
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4658,15 +4830,15 @@ async function addComposeInfra(graph, scanPath, services) {
4658
4830
  for (const dep of dependsOnList(svc.depends_on)) {
4659
4831
  const targetId = composeNameToNodeId.get(dep);
4660
4832
  if (!targetId) continue;
4661
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4833
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4662
4834
  if (graph.hasEdge(edgeId)) continue;
4663
4835
  const edge = {
4664
4836
  id: edgeId,
4665
4837
  source: sourceId,
4666
4838
  target: targetId,
4667
- type: import_types18.EdgeType.DEPENDS_ON,
4668
- provenance: import_types18.Provenance.EXTRACTED,
4669
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4839
+ type: import_types19.EdgeType.DEPENDS_ON,
4840
+ provenance: import_types19.Provenance.EXTRACTED,
4841
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4670
4842
  evidence: { file: evidenceFile }
4671
4843
  };
4672
4844
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4678,9 +4850,9 @@ async function addComposeInfra(graph, scanPath, services) {
4678
4850
 
4679
4851
  // src/extract/infra/dockerfile.ts
4680
4852
  init_cjs_shims();
4681
- var import_node_path28 = __toESM(require("path"), 1);
4853
+ var import_node_path29 = __toESM(require("path"), 1);
4682
4854
  var import_node_fs15 = require("fs");
4683
- var import_types19 = require("@neat.is/types");
4855
+ var import_types20 = require("@neat.is/types");
4684
4856
  function runtimeImage(content) {
4685
4857
  const lines = content.split("\n");
4686
4858
  let last = null;
@@ -4699,7 +4871,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4699
4871
  let nodesAdded = 0;
4700
4872
  let edgesAdded = 0;
4701
4873
  for (const service of services) {
4702
- const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
4874
+ const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
4703
4875
  if (!await exists(dockerfilePath)) continue;
4704
4876
  let content;
4705
4877
  try {
@@ -4707,7 +4879,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4707
4879
  } catch (err) {
4708
4880
  recordExtractionError(
4709
4881
  "infra dockerfile",
4710
- import_node_path28.default.relative(scanPath, dockerfilePath),
4882
+ import_node_path29.default.relative(scanPath, dockerfilePath),
4711
4883
  err
4712
4884
  );
4713
4885
  continue;
@@ -4719,7 +4891,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4719
4891
  graph.addNode(node.id, node);
4720
4892
  nodesAdded++;
4721
4893
  }
4722
- const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
4894
+ const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
4723
4895
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4724
4896
  graph,
4725
4897
  service.pkg.name,
@@ -4728,17 +4900,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4728
4900
  );
4729
4901
  nodesAdded += fn;
4730
4902
  edgesAdded += fe;
4731
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
4903
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4732
4904
  if (!graph.hasEdge(edgeId)) {
4733
4905
  const edge = {
4734
4906
  id: edgeId,
4735
4907
  source: fileNodeId,
4736
4908
  target: node.id,
4737
- type: import_types19.EdgeType.RUNS_ON,
4738
- provenance: import_types19.Provenance.EXTRACTED,
4739
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
4909
+ type: import_types20.EdgeType.RUNS_ON,
4910
+ provenance: import_types20.Provenance.EXTRACTED,
4911
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4740
4912
  evidence: {
4741
- file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
4913
+ file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
4742
4914
  }
4743
4915
  };
4744
4916
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4751,7 +4923,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4751
4923
  // src/extract/infra/terraform.ts
4752
4924
  init_cjs_shims();
4753
4925
  var import_node_fs16 = require("fs");
4754
- var import_node_path29 = __toESM(require("path"), 1);
4926
+ var import_node_path30 = __toESM(require("path"), 1);
4755
4927
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4756
4928
  async function walkTfFiles(start, depth = 0, max = 5) {
4757
4929
  if (depth > max) return [];
@@ -4760,11 +4932,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4760
4932
  for (const entry2 of entries) {
4761
4933
  if (entry2.isDirectory()) {
4762
4934
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4763
- const child = import_node_path29.default.join(start, entry2.name);
4935
+ const child = import_node_path30.default.join(start, entry2.name);
4764
4936
  if (await isPythonVenvDir(child)) continue;
4765
4937
  out.push(...await walkTfFiles(child, depth + 1, max));
4766
4938
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4767
- out.push(import_node_path29.default.join(start, entry2.name));
4939
+ out.push(import_node_path30.default.join(start, entry2.name));
4768
4940
  }
4769
4941
  }
4770
4942
  return out;
@@ -4792,7 +4964,7 @@ async function addTerraformResources(graph, scanPath) {
4792
4964
  // src/extract/infra/k8s.ts
4793
4965
  init_cjs_shims();
4794
4966
  var import_node_fs17 = require("fs");
4795
- var import_node_path30 = __toESM(require("path"), 1);
4967
+ var import_node_path31 = __toESM(require("path"), 1);
4796
4968
  var import_yaml3 = require("yaml");
4797
4969
  var K8S_KIND_TO_INFRA_KIND = {
4798
4970
  Service: "k8s-service",
@@ -4810,11 +4982,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4810
4982
  for (const entry2 of entries) {
4811
4983
  if (entry2.isDirectory()) {
4812
4984
  if (IGNORED_DIRS.has(entry2.name)) continue;
4813
- const child = import_node_path30.default.join(start, entry2.name);
4985
+ const child = import_node_path31.default.join(start, entry2.name);
4814
4986
  if (await isPythonVenvDir(child)) continue;
4815
4987
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4816
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry2.name))) {
4817
- out.push(import_node_path30.default.join(start, entry2.name));
4988
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry2.name))) {
4989
+ out.push(import_node_path31.default.join(start, entry2.name));
4818
4990
  }
4819
4991
  }
4820
4992
  return out;
@@ -4858,17 +5030,17 @@ async function addInfra(graph, scanPath, services) {
4858
5030
  }
4859
5031
 
4860
5032
  // src/extract/index.ts
4861
- var import_node_path32 = __toESM(require("path"), 1);
5033
+ var import_node_path33 = __toESM(require("path"), 1);
4862
5034
 
4863
5035
  // src/extract/retire.ts
4864
5036
  init_cjs_shims();
4865
5037
  var import_node_fs18 = require("fs");
4866
- var import_node_path31 = __toESM(require("path"), 1);
4867
- var import_types20 = require("@neat.is/types");
5038
+ var import_node_path32 = __toESM(require("path"), 1);
5039
+ var import_types21 = require("@neat.is/types");
4868
5040
  function dropOrphanedFileNodes(graph) {
4869
5041
  const orphans = [];
4870
5042
  graph.forEachNode((id, attrs) => {
4871
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5043
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
4872
5044
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4873
5045
  orphans.push(id);
4874
5046
  }
@@ -4881,14 +5053,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4881
5053
  const bases = [scanPath, ...serviceDirs];
4882
5054
  graph.forEachEdge((id, attrs) => {
4883
5055
  const edge = attrs;
4884
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5056
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4885
5057
  const evidenceFile = edge.evidence?.file;
4886
5058
  if (!evidenceFile) return;
4887
- if (import_node_path31.default.isAbsolute(evidenceFile)) {
5059
+ if (import_node_path32.default.isAbsolute(evidenceFile)) {
4888
5060
  if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
4889
5061
  return;
4890
5062
  }
4891
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
5063
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
4892
5064
  if (!found) toDrop.push(id);
4893
5065
  });
4894
5066
  for (const id of toDrop) graph.dropEdge(id);
@@ -4928,7 +5100,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4928
5100
  }
4929
5101
  const droppedEntries = drainDroppedExtracted();
4930
5102
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4931
- const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
5103
+ const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
4932
5104
  try {
4933
5105
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4934
5106
  } catch (err) {
@@ -4963,8 +5135,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4963
5135
  // src/persist.ts
4964
5136
  init_cjs_shims();
4965
5137
  var import_node_fs19 = require("fs");
4966
- var import_node_path33 = __toESM(require("path"), 1);
4967
- var import_types21 = require("@neat.is/types");
5138
+ var import_node_path34 = __toESM(require("path"), 1);
5139
+ var import_types22 = require("@neat.is/types");
4968
5140
  var SCHEMA_VERSION = 4;
4969
5141
  function migrateV1ToV2(payload) {
4970
5142
  const nodes = payload.graph.nodes;
@@ -4986,12 +5158,12 @@ function migrateV2ToV3(payload) {
4986
5158
  for (const edge of edges) {
4987
5159
  const attrs = edge.attributes;
4988
5160
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4989
- attrs.provenance = import_types21.Provenance.OBSERVED;
5161
+ attrs.provenance = import_types22.Provenance.OBSERVED;
4990
5162
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4991
5163
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4992
5164
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4993
5165
  if (type && source && target) {
4994
- const newId = (0, import_types21.observedEdgeId)(source, target, type);
5166
+ const newId = (0, import_types22.observedEdgeId)(source, target, type);
4995
5167
  attrs.id = newId;
4996
5168
  if (edge.key) edge.key = newId;
4997
5169
  }
@@ -5000,7 +5172,7 @@ function migrateV2ToV3(payload) {
5000
5172
  return { ...payload, schemaVersion: 3 };
5001
5173
  }
5002
5174
  async function ensureDir(filePath) {
5003
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
5175
+ await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
5004
5176
  }
5005
5177
  async function saveGraphToDisk(graph, outPath) {
5006
5178
  await ensureDir(outPath);
@@ -5075,23 +5247,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5075
5247
 
5076
5248
  // src/projects.ts
5077
5249
  init_cjs_shims();
5078
- var import_node_path34 = __toESM(require("path"), 1);
5250
+ var import_node_path35 = __toESM(require("path"), 1);
5079
5251
  function pathsForProject(project, baseDir) {
5080
5252
  if (project === DEFAULT_PROJECT) {
5081
5253
  return {
5082
- snapshotPath: import_node_path34.default.join(baseDir, "graph.json"),
5083
- errorsPath: import_node_path34.default.join(baseDir, "errors.ndjson"),
5084
- staleEventsPath: import_node_path34.default.join(baseDir, "stale-events.ndjson"),
5085
- embeddingsCachePath: import_node_path34.default.join(baseDir, "embeddings.json"),
5086
- policyViolationsPath: import_node_path34.default.join(baseDir, "policy-violations.ndjson")
5254
+ snapshotPath: import_node_path35.default.join(baseDir, "graph.json"),
5255
+ errorsPath: import_node_path35.default.join(baseDir, "errors.ndjson"),
5256
+ staleEventsPath: import_node_path35.default.join(baseDir, "stale-events.ndjson"),
5257
+ embeddingsCachePath: import_node_path35.default.join(baseDir, "embeddings.json"),
5258
+ policyViolationsPath: import_node_path35.default.join(baseDir, "policy-violations.ndjson")
5087
5259
  };
5088
5260
  }
5089
5261
  return {
5090
- snapshotPath: import_node_path34.default.join(baseDir, `${project}.json`),
5091
- errorsPath: import_node_path34.default.join(baseDir, `errors.${project}.ndjson`),
5092
- staleEventsPath: import_node_path34.default.join(baseDir, `stale-events.${project}.ndjson`),
5093
- embeddingsCachePath: import_node_path34.default.join(baseDir, `embeddings.${project}.json`),
5094
- policyViolationsPath: import_node_path34.default.join(baseDir, `policy-violations.${project}.ndjson`)
5262
+ snapshotPath: import_node_path35.default.join(baseDir, `${project}.json`),
5263
+ errorsPath: import_node_path35.default.join(baseDir, `errors.${project}.ndjson`),
5264
+ staleEventsPath: import_node_path35.default.join(baseDir, `stale-events.${project}.ndjson`),
5265
+ embeddingsCachePath: import_node_path35.default.join(baseDir, `embeddings.${project}.json`),
5266
+ policyViolationsPath: import_node_path35.default.join(baseDir, `policy-violations.${project}.ndjson`)
5095
5267
  };
5096
5268
  }
5097
5269
  var Projects = class {
@@ -5129,19 +5301,19 @@ var Projects = class {
5129
5301
  init_cjs_shims();
5130
5302
  var import_fastify = __toESM(require("fastify"), 1);
5131
5303
  var import_cors = __toESM(require("@fastify/cors"), 1);
5132
- var import_types24 = require("@neat.is/types");
5304
+ var import_types25 = require("@neat.is/types");
5133
5305
 
5134
5306
  // src/extend/index.ts
5135
5307
  init_cjs_shims();
5136
5308
  var import_node_fs21 = require("fs");
5137
- var import_node_path36 = __toESM(require("path"), 1);
5309
+ var import_node_path37 = __toESM(require("path"), 1);
5138
5310
  var import_node_os2 = __toESM(require("os"), 1);
5139
5311
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5140
5312
 
5141
5313
  // src/installers/package-manager.ts
5142
5314
  init_cjs_shims();
5143
5315
  var import_node_fs20 = require("fs");
5144
- var import_node_path35 = __toESM(require("path"), 1);
5316
+ var import_node_path36 = __toESM(require("path"), 1);
5145
5317
  var import_node_child_process = require("child_process");
5146
5318
  var LOCKFILE_PRIORITY = [
5147
5319
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5163,22 +5335,22 @@ async function exists2(p) {
5163
5335
  }
5164
5336
  }
5165
5337
  async function detectPackageManager(serviceDir) {
5166
- let dir = import_node_path35.default.resolve(serviceDir);
5338
+ let dir = import_node_path36.default.resolve(serviceDir);
5167
5339
  const stops = /* @__PURE__ */ new Set();
5168
5340
  for (let i = 0; i < 64; i++) {
5169
5341
  if (stops.has(dir)) break;
5170
5342
  stops.add(dir);
5171
5343
  for (const candidate of LOCKFILE_PRIORITY) {
5172
- const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
5344
+ const lockPath = import_node_path36.default.join(dir, candidate.lockfile);
5173
5345
  if (await exists2(lockPath)) {
5174
5346
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5175
5347
  }
5176
5348
  }
5177
- const parent = import_node_path35.default.dirname(dir);
5349
+ const parent = import_node_path36.default.dirname(dir);
5178
5350
  if (parent === dir) break;
5179
5351
  dir = parent;
5180
5352
  }
5181
- return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5353
+ return { pm: "npm", cwd: import_node_path36.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5182
5354
  }
5183
5355
  async function runPackageManagerInstall(cmd) {
5184
5356
  return new Promise((resolve) => {
@@ -5227,7 +5399,7 @@ async function fileExists2(p) {
5227
5399
  }
5228
5400
  }
5229
5401
  async function readPackageJson(scanPath) {
5230
- const pkgPath = import_node_path36.default.join(scanPath, "package.json");
5402
+ const pkgPath = import_node_path37.default.join(scanPath, "package.json");
5231
5403
  const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5232
5404
  return JSON.parse(raw);
5233
5405
  }
@@ -5238,11 +5410,11 @@ async function findHookFiles(scanPath) {
5238
5410
  ).sort();
5239
5411
  }
5240
5412
  function extendLogPath() {
5241
- return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5413
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path37.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5242
5414
  }
5243
5415
  async function appendExtendLog(entry2) {
5244
5416
  const logPath = extendLogPath();
5245
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
5417
+ await import_node_fs21.promises.mkdir(import_node_path37.default.dirname(logPath), { recursive: true });
5246
5418
  await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
5247
5419
  }
5248
5420
  function splicedContent(fileContent, snippet2) {
@@ -5301,7 +5473,7 @@ function lookupInstrumentation(library, installedVersion) {
5301
5473
  }
5302
5474
  async function describeProjectInstrumentation(ctx) {
5303
5475
  const hookFiles = await findHookFiles(ctx.scanPath);
5304
- const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
5476
+ const envNeat = await fileExists2(import_node_path37.default.join(ctx.scanPath, ".env.neat"));
5305
5477
  const registryInstrPackages = new Set(
5306
5478
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5307
5479
  );
@@ -5323,16 +5495,16 @@ async function applyExtension(ctx, args, options) {
5323
5495
  );
5324
5496
  }
5325
5497
  for (const file of hookFiles) {
5326
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5498
+ const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
5327
5499
  if (content.includes(args.registration_snippet)) {
5328
5500
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5329
5501
  }
5330
5502
  }
5331
5503
  const primaryFile = hookFiles[0];
5332
- const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
5504
+ const primaryPath = import_node_path37.default.join(ctx.scanPath, primaryFile);
5333
5505
  const filesTouched = [];
5334
5506
  const depsAdded = [];
5335
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5507
+ const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
5336
5508
  const pkg = await readPackageJson(ctx.scanPath);
5337
5509
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5338
5510
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5378,7 +5550,7 @@ async function dryRunExtension(ctx, args) {
5378
5550
  };
5379
5551
  }
5380
5552
  for (const file of hookFiles) {
5381
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5553
+ const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
5382
5554
  if (content.includes(args.registration_snippet)) {
5383
5555
  return {
5384
5556
  library: args.library,
@@ -5400,7 +5572,7 @@ async function dryRunExtension(ctx, args) {
5400
5572
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5401
5573
  filesTouched.push("package.json");
5402
5574
  }
5403
- const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
5575
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, primaryFile), "utf8");
5404
5576
  const patched = splicedContent(hookContent, args.registration_snippet);
5405
5577
  if (patched) {
5406
5578
  filesTouched.push(primaryFile);
@@ -5421,7 +5593,7 @@ async function rollbackExtension(ctx, args) {
5421
5593
  if (!match) {
5422
5594
  return { undone: false, message: "no apply found for library" };
5423
5595
  }
5424
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5596
+ const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
5425
5597
  if (await fileExists2(pkgPath)) {
5426
5598
  const pkg = await readPackageJson(ctx.scanPath);
5427
5599
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5432,7 +5604,7 @@ async function rollbackExtension(ctx, args) {
5432
5604
  }
5433
5605
  const hookFiles = await findHookFiles(ctx.scanPath);
5434
5606
  for (const file of hookFiles) {
5435
- const filePath = import_node_path36.default.join(ctx.scanPath, file);
5607
+ const filePath = import_node_path37.default.join(ctx.scanPath, file);
5436
5608
  const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5437
5609
  if (content.includes(match.registration_snippet)) {
5438
5610
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5448,7 +5620,7 @@ async function rollbackExtension(ctx, args) {
5448
5620
 
5449
5621
  // src/divergences.ts
5450
5622
  init_cjs_shims();
5451
- var import_types22 = require("@neat.is/types");
5623
+ var import_types23 = require("@neat.is/types");
5452
5624
  function bucketKey(source, target, type) {
5453
5625
  return `${type}|${source}|${target}`;
5454
5626
  }
@@ -5456,22 +5628,22 @@ function bucketEdges(graph) {
5456
5628
  const buckets = /* @__PURE__ */ new Map();
5457
5629
  graph.forEachEdge((id, attrs) => {
5458
5630
  const e = attrs;
5459
- const parsed = (0, import_types22.parseEdgeId)(id);
5631
+ const parsed = (0, import_types23.parseEdgeId)(id);
5460
5632
  const provenance = parsed?.provenance ?? e.provenance;
5461
5633
  const key = bucketKey(e.source, e.target, e.type);
5462
5634
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5463
5635
  switch (provenance) {
5464
- case import_types22.Provenance.EXTRACTED:
5636
+ case import_types23.Provenance.EXTRACTED:
5465
5637
  cur.extracted = e;
5466
5638
  break;
5467
- case import_types22.Provenance.OBSERVED:
5639
+ case import_types23.Provenance.OBSERVED:
5468
5640
  cur.observed = e;
5469
5641
  break;
5470
- case import_types22.Provenance.INFERRED:
5642
+ case import_types23.Provenance.INFERRED:
5471
5643
  cur.inferred = e;
5472
5644
  break;
5473
5645
  default:
5474
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
5646
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5475
5647
  }
5476
5648
  buckets.set(key, cur);
5477
5649
  });
@@ -5480,7 +5652,7 @@ function bucketEdges(graph) {
5480
5652
  function nodeIsFrontier(graph, nodeId) {
5481
5653
  if (!graph.hasNode(nodeId)) return false;
5482
5654
  const attrs = graph.getNodeAttributes(nodeId);
5483
- return attrs.type === import_types22.NodeType.FrontierNode;
5655
+ return attrs.type === import_types23.NodeType.FrontierNode;
5484
5656
  }
5485
5657
  function clampConfidence(n) {
5486
5658
  if (!Number.isFinite(n)) return 0;
@@ -5501,7 +5673,7 @@ function gradedConfidence(edge) {
5501
5673
  }
5502
5674
  function detectMissingDivergences(graph, bucket) {
5503
5675
  const out = [];
5504
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5676
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5505
5677
  if (bucket.extracted && !bucket.observed) {
5506
5678
  if (!nodeIsFrontier(graph, bucket.target)) {
5507
5679
  out.push({
@@ -5542,7 +5714,7 @@ function declaredHostFor(svc) {
5542
5714
  function hasExtractedConfiguredBy(graph, svcId) {
5543
5715
  for (const edgeId of graph.outboundEdges(svcId)) {
5544
5716
  const e = graph.getEdgeAttributes(edgeId);
5545
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
5717
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5546
5718
  return true;
5547
5719
  }
5548
5720
  }
@@ -5555,10 +5727,10 @@ function detectHostMismatch(graph, svcId, svc) {
5555
5727
  const out = [];
5556
5728
  for (const edgeId of graph.outboundEdges(svcId)) {
5557
5729
  const edge = graph.getEdgeAttributes(edgeId);
5558
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5559
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5730
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5731
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5560
5732
  const target = graph.getNodeAttributes(edge.target);
5561
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5733
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5562
5734
  const observedHost = target.host?.trim();
5563
5735
  if (!observedHost) continue;
5564
5736
  if (observedHost === declaredHost) continue;
@@ -5580,10 +5752,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5580
5752
  const deps = svc.dependencies ?? {};
5581
5753
  for (const edgeId of graph.outboundEdges(svcId)) {
5582
5754
  const edge = graph.getEdgeAttributes(edgeId);
5583
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5584
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5755
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5756
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5585
5757
  const target = graph.getNodeAttributes(edge.target);
5586
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5758
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5587
5759
  for (const pair of compatPairs()) {
5588
5760
  if (pair.engine !== target.engine) continue;
5589
5761
  const declared = deps[pair.driver];
@@ -5644,7 +5816,7 @@ function computeDivergences(graph, opts = {}) {
5644
5816
  }
5645
5817
  graph.forEachNode((nodeId, attrs) => {
5646
5818
  const n = attrs;
5647
- if (n.type !== import_types22.NodeType.ServiceNode) return;
5819
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5648
5820
  const svc = n;
5649
5821
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5650
5822
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5677,7 +5849,7 @@ function computeDivergences(graph, opts = {}) {
5677
5849
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5678
5850
  return a.target.localeCompare(b.target);
5679
5851
  });
5680
- return import_types22.DivergenceResultSchema.parse({
5852
+ return import_types23.DivergenceResultSchema.parse({
5681
5853
  divergences: filtered,
5682
5854
  totalAffected: filtered.length,
5683
5855
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5765,23 +5937,23 @@ function canonicalJson(value) {
5765
5937
  init_cjs_shims();
5766
5938
  var import_node_fs23 = require("fs");
5767
5939
  var import_node_os3 = __toESM(require("os"), 1);
5768
- var import_node_path37 = __toESM(require("path"), 1);
5769
- var import_types23 = require("@neat.is/types");
5940
+ var import_node_path38 = __toESM(require("path"), 1);
5941
+ var import_types24 = require("@neat.is/types");
5770
5942
  var LOCK_TIMEOUT_MS = 5e3;
5771
5943
  var LOCK_RETRY_MS = 50;
5772
5944
  function neatHome() {
5773
5945
  const override = process.env.NEAT_HOME;
5774
- if (override && override.length > 0) return import_node_path37.default.resolve(override);
5775
- return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
5946
+ if (override && override.length > 0) return import_node_path38.default.resolve(override);
5947
+ return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
5776
5948
  }
5777
5949
  function registryPath() {
5778
- return import_node_path37.default.join(neatHome(), "projects.json");
5950
+ return import_node_path38.default.join(neatHome(), "projects.json");
5779
5951
  }
5780
5952
  function registryLockPath() {
5781
- return import_node_path37.default.join(neatHome(), "projects.json.lock");
5953
+ return import_node_path38.default.join(neatHome(), "projects.json.lock");
5782
5954
  }
5783
5955
  function daemonPidPath() {
5784
- return import_node_path37.default.join(neatHome(), "neatd.pid");
5956
+ return import_node_path38.default.join(neatHome(), "neatd.pid");
5785
5957
  }
5786
5958
  function isPidAliveDefault(pid) {
5787
5959
  try {
@@ -5841,7 +6013,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5841
6013
  }
5842
6014
  }
5843
6015
  async function writeAtomically(target, contents) {
5844
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6016
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
5845
6017
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5846
6018
  const fd = await import_node_fs23.promises.open(tmp, "w");
5847
6019
  try {
@@ -5854,7 +6026,7 @@ async function writeAtomically(target, contents) {
5854
6026
  }
5855
6027
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5856
6028
  const deadline = Date.now() + timeoutMs;
5857
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
6029
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
5858
6030
  let probedHolder = false;
5859
6031
  while (true) {
5860
6032
  try {
@@ -5907,10 +6079,10 @@ async function readRegistry() {
5907
6079
  throw err;
5908
6080
  }
5909
6081
  const parsed = JSON.parse(raw);
5910
- return import_types23.RegistryFileSchema.parse(parsed);
6082
+ return import_types24.RegistryFileSchema.parse(parsed);
5911
6083
  }
5912
6084
  async function writeRegistry(reg) {
5913
- const validated = import_types23.RegistryFileSchema.parse(reg);
6085
+ const validated = import_types24.RegistryFileSchema.parse(reg);
5914
6086
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5915
6087
  }
5916
6088
  async function getProject(name) {
@@ -5940,6 +6112,58 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
5940
6112
  await writeRegistry(reg);
5941
6113
  });
5942
6114
  }
6115
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
6116
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
6117
+ function pruneTtlMs() {
6118
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
6119
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
6120
+ const n = Number.parseInt(raw, 10);
6121
+ if (Number.isFinite(n) && n >= 0) return n;
6122
+ console.warn(
6123
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
6124
+ );
6125
+ return DEFAULT_PRUNE_TTL_MS;
6126
+ }
6127
+ async function statPathStatus(p) {
6128
+ try {
6129
+ const stat = await import_node_fs23.promises.stat(p);
6130
+ return stat.isDirectory() ? "present" : "unknown";
6131
+ } catch (err) {
6132
+ return err.code === "ENOENT" ? "gone" : "unknown";
6133
+ }
6134
+ }
6135
+ async function pruneRegistry(opts = {}) {
6136
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
6137
+ const statPath = opts.statPath ?? statPathStatus;
6138
+ const now = opts.now ?? Date.now;
6139
+ return withLock(async () => {
6140
+ const reg = await readRegistry();
6141
+ const removed = [];
6142
+ const kept = [];
6143
+ for (const entry2 of reg.projects) {
6144
+ const status2 = await statPath(entry2.path);
6145
+ if (status2 !== "gone") {
6146
+ kept.push(entry2);
6147
+ continue;
6148
+ }
6149
+ if (ttlMs <= 0) {
6150
+ removed.push(entry2);
6151
+ continue;
6152
+ }
6153
+ const lastSeen = Date.parse(entry2.lastSeenAt ?? entry2.registeredAt);
6154
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
6155
+ if (age > ttlMs) {
6156
+ removed.push(entry2);
6157
+ } else {
6158
+ kept.push(entry2);
6159
+ }
6160
+ }
6161
+ if (removed.length === 0) return [];
6162
+ reg.projects = kept;
6163
+ await writeRegistry(reg);
6164
+ return removed;
6165
+ });
6166
+ }
5943
6167
 
5944
6168
  // src/streaming.ts
5945
6169
  init_cjs_shims();
@@ -5953,6 +6177,7 @@ function handleSse(req2, reply, opts) {
5953
6177
  reply.raw.setHeader("Connection", "keep-alive");
5954
6178
  reply.raw.setHeader("X-Accel-Buffering", "no");
5955
6179
  reply.raw.flushHeaders?.();
6180
+ reply.raw.write(":open\n\n");
5956
6181
  let pending = 0;
5957
6182
  let dropped = false;
5958
6183
  const closeConnection = () => {
@@ -6132,11 +6357,11 @@ function registerRoutes(scope, ctx) {
6132
6357
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6133
6358
  const parsed = [];
6134
6359
  for (const c of candidates) {
6135
- const r = import_types24.DivergenceTypeSchema.safeParse(c);
6360
+ const r = import_types25.DivergenceTypeSchema.safeParse(c);
6136
6361
  if (!r.success) {
6137
6362
  return reply.code(400).send({
6138
6363
  error: `unknown divergence type "${c}"`,
6139
- allowed: import_types24.DivergenceTypeSchema.options
6364
+ allowed: import_types25.DivergenceTypeSchema.options
6140
6365
  });
6141
6366
  }
6142
6367
  parsed.push(r.data);
@@ -6349,7 +6574,7 @@ function registerRoutes(scope, ctx) {
6349
6574
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6350
6575
  let violations = await log.readAll();
6351
6576
  if (req2.query.severity) {
6352
- const sev = import_types24.PolicySeveritySchema.safeParse(req2.query.severity);
6577
+ const sev = import_types25.PolicySeveritySchema.safeParse(req2.query.severity);
6353
6578
  if (!sev.success) {
6354
6579
  return reply.code(400).send({
6355
6580
  error: "invalid severity",
@@ -6366,7 +6591,7 @@ function registerRoutes(scope, ctx) {
6366
6591
  scope.post("/policies/check", async (req2, reply) => {
6367
6592
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6368
6593
  if (!proj) return;
6369
- const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6594
+ const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6370
6595
  if (!parsed.success) {
6371
6596
  return reply.code(400).send({
6372
6597
  error: "invalid /policies/check body",
@@ -6609,7 +6834,7 @@ init_auth();
6609
6834
  // src/unrouted.ts
6610
6835
  init_cjs_shims();
6611
6836
  var import_node_fs24 = require("fs");
6612
- var import_node_path40 = __toESM(require("path"), 1);
6837
+ var import_node_path41 = __toESM(require("path"), 1);
6613
6838
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
6614
6839
  return {
6615
6840
  timestamp: now.toISOString(),
@@ -6619,15 +6844,68 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6619
6844
  };
6620
6845
  }
6621
6846
  async function appendUnroutedSpan(neatHome3, record) {
6622
- const target = import_node_path40.default.join(neatHome3, "errors.ndjson");
6847
+ const target = import_node_path41.default.join(neatHome3, "errors.ndjson");
6623
6848
  await import_node_fs24.promises.mkdir(neatHome3, { recursive: true });
6624
6849
  await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6625
6850
  }
6626
6851
  function unroutedErrorsPath(neatHome3) {
6627
- return import_node_path40.default.join(neatHome3, "errors.ndjson");
6852
+ return import_node_path41.default.join(neatHome3, "errors.ndjson");
6628
6853
  }
6629
6854
 
6630
6855
  // src/daemon.ts
6856
+ function daemonJsonPath(scanPath) {
6857
+ return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
6858
+ }
6859
+ function daemonsDiscoveryDir(home) {
6860
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
6861
+ return import_node_path42.default.join(base, "daemons");
6862
+ }
6863
+ function daemonDiscoveryPath(project, home) {
6864
+ return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
6865
+ }
6866
+ function sanitizeDiscoveryName(project) {
6867
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
6868
+ }
6869
+ function neatHomeFromEnv() {
6870
+ const env = process.env.NEAT_HOME;
6871
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
6872
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6873
+ return import_node_path42.default.join(home, ".neat");
6874
+ }
6875
+ function resolveNeatVersion() {
6876
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
6877
+ return process.env.NEAT_LOCAL_VERSION;
6878
+ }
6879
+ try {
6880
+ const req2 = (0, import_node_module.createRequire)(importMetaUrl);
6881
+ const pkg = req2("../package.json");
6882
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
6883
+ } catch {
6884
+ return "0.0.0";
6885
+ }
6886
+ }
6887
+ async function writeDaemonRecord(record, home) {
6888
+ const body = JSON.stringify(record, null, 2) + "\n";
6889
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
6890
+ try {
6891
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
6892
+ } catch (err) {
6893
+ console.warn(
6894
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
6895
+ );
6896
+ }
6897
+ }
6898
+ async function clearDaemonRecord(record, home) {
6899
+ try {
6900
+ const stopped = { ...record, status: "stopped" };
6901
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
6902
+ } catch {
6903
+ }
6904
+ try {
6905
+ await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
6906
+ } catch {
6907
+ }
6908
+ }
6631
6909
  function teardownSlot(slot) {
6632
6910
  try {
6633
6911
  slot.stopPersist();
@@ -6639,11 +6917,11 @@ function teardownSlot(slot) {
6639
6917
  }
6640
6918
  }
6641
6919
  function neatHomeFor(opts) {
6642
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
6920
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
6643
6921
  const env = process.env.NEAT_HOME;
6644
- if (env && env.length > 0) return import_node_path41.default.resolve(env);
6922
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
6645
6923
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6646
- return import_node_path41.default.join(home, ".neat");
6924
+ return import_node_path42.default.join(home, ".neat");
6647
6925
  }
6648
6926
  function routeSpanToProject(serviceName, projects) {
6649
6927
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6678,7 +6956,7 @@ function isTokenContained(needle, haystack) {
6678
6956
  return tokens.includes(needle);
6679
6957
  }
6680
6958
  async function bootstrapProject(entry2) {
6681
- const paths = pathsForProject(entry2.name, import_node_path41.default.join(entry2.path, "neat-out"));
6959
+ const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
6682
6960
  try {
6683
6961
  const stat = await import_node_fs25.promises.stat(entry2.path);
6684
6962
  if (!stat.isDirectory()) {
@@ -6744,6 +7022,23 @@ function resolveOtlpPort(opts) {
6744
7022
  }
6745
7023
  return 4318;
6746
7024
  }
7025
+ function resolveWebPort() {
7026
+ const env = process.env.NEAT_WEB_PORT;
7027
+ if (env && env.length > 0) {
7028
+ const n = Number.parseInt(env, 10);
7029
+ if (Number.isFinite(n)) return n;
7030
+ }
7031
+ return 6328;
7032
+ }
7033
+ function portFromListenAddress(address, fallback) {
7034
+ try {
7035
+ const port = new URL(address).port;
7036
+ const n = Number.parseInt(port, 10);
7037
+ if (Number.isFinite(n) && n > 0) return n;
7038
+ } catch {
7039
+ }
7040
+ return fallback;
7041
+ }
6747
7042
  function resolveHost(opts, authTokenSet) {
6748
7043
  if (opts.host && opts.host.length > 0) return opts.host;
6749
7044
  const env = process.env.HOST;
@@ -6754,14 +7049,25 @@ function resolveHost(opts, authTokenSet) {
6754
7049
  async function startDaemon(opts = {}) {
6755
7050
  const home = neatHomeFor(opts);
6756
7051
  const regPath = registryPath();
6757
- try {
6758
- await import_node_fs25.promises.access(regPath);
6759
- } catch {
7052
+ const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
7053
+ const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
7054
+ const singleProject = projectArg;
7055
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
7056
+ if (singleProject && !singleProjectPath) {
6760
7057
  throw new Error(
6761
- `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7058
+ `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
6762
7059
  );
6763
7060
  }
6764
- const pidPath = import_node_path41.default.join(home, "neatd.pid");
7061
+ if (!singleProject) {
7062
+ try {
7063
+ await import_node_fs25.promises.access(regPath);
7064
+ } catch {
7065
+ throw new Error(
7066
+ `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7067
+ );
7068
+ }
7069
+ }
7070
+ const pidPath = import_node_path42.default.join(home, "neatd.pid");
6765
7071
  await writeAtomically(pidPath, `${process.pid}
6766
7072
  `);
6767
7073
  const slots = /* @__PURE__ */ new Map();
@@ -6849,8 +7155,37 @@ async function startDaemon(opts = {}) {
6849
7155
  });
6850
7156
  }
6851
7157
  }
7158
+ async function enumerateProjects() {
7159
+ if (singleProject && singleProjectPath) {
7160
+ return [
7161
+ {
7162
+ name: singleProject,
7163
+ path: singleProjectPath,
7164
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7165
+ languages: [],
7166
+ status: "active"
7167
+ }
7168
+ ];
7169
+ }
7170
+ return listProjects();
7171
+ }
6852
7172
  async function loadAll() {
6853
- const projects = await listProjects();
7173
+ if (!singleProject) {
7174
+ try {
7175
+ const pruned = await pruneRegistry();
7176
+ for (const entry2 of pruned) {
7177
+ console.log(
7178
+ `neatd: pruned project "${entry2.name}" \u2014 registered path ${entry2.path} is gone`
7179
+ );
7180
+ slots.delete(entry2.name);
7181
+ bootstrapStatus.delete(entry2.name);
7182
+ bootstrapStartedAt.delete(entry2.name);
7183
+ }
7184
+ } catch (err) {
7185
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7186
+ }
7187
+ }
7188
+ const projects = await enumerateProjects();
6854
7189
  const seen = /* @__PURE__ */ new Set();
6855
7190
  const pending = [];
6856
7191
  for (const entry2 of projects) {
@@ -6875,7 +7210,7 @@ async function startDaemon(opts = {}) {
6875
7210
  }
6876
7211
  await Promise.allSettled(pending);
6877
7212
  }
6878
- const initialEntries = await listProjects().catch(() => []);
7213
+ const initialEntries = await enumerateProjects().catch(() => []);
6879
7214
  for (const entry2 of initialEntries) {
6880
7215
  bootstrapStatus.set(entry2.name, "bootstrapping");
6881
7216
  bootstrapStartedAt.set(entry2.name, Date.now());
@@ -6885,6 +7220,7 @@ async function startDaemon(opts = {}) {
6885
7220
  let otlpApp = null;
6886
7221
  let restAddress = "";
6887
7222
  let otlpAddress = "";
7223
+ let daemonRecord = null;
6888
7224
  if (bind) {
6889
7225
  const auth = readAuthEnv();
6890
7226
  const host = resolveHost(opts, Boolean(auth.authToken));
@@ -6909,6 +7245,20 @@ async function startDaemon(opts = {}) {
6909
7245
  }
6910
7246
  }
6911
7247
  });
7248
+ if (singleProject) {
7249
+ restApp.addHook("onSend", async (req2, _reply, payload) => {
7250
+ if (req2.url.split("?")[0] !== "/health") return payload;
7251
+ if (typeof payload !== "string") return payload;
7252
+ try {
7253
+ const body = JSON.parse(payload);
7254
+ if (typeof body !== "object" || body === null) return payload;
7255
+ body.project = singleProject;
7256
+ return JSON.stringify(body);
7257
+ } catch {
7258
+ return payload;
7259
+ }
7260
+ });
7261
+ }
6912
7262
  restAddress = await restApp.listen({ port: restPort, host });
6913
7263
  console.log(`neatd: REST listening on ${restAddress}`);
6914
7264
  } catch (err) {
@@ -6924,6 +7274,25 @@ async function startDaemon(opts = {}) {
6924
7274
  );
6925
7275
  }
6926
7276
  async function resolveTargetSlot(serviceName, traceId) {
7277
+ if (singleProject) {
7278
+ let slot2 = slots.get(singleProject);
7279
+ if (!slot2) {
7280
+ slot2 = await tryRecoverSlot({
7281
+ name: singleProject,
7282
+ path: singleProjectPath,
7283
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7284
+ languages: [],
7285
+ status: "active"
7286
+ });
7287
+ } else if (slot2.status === "broken") {
7288
+ slot2 = await tryRecoverSlot(slot2.entry);
7289
+ }
7290
+ if (!slot2 || slot2.status !== "active") {
7291
+ warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7292
+ return null;
7293
+ }
7294
+ return slot2;
7295
+ }
6927
7296
  const liveEntries = await listProjects().catch(() => []);
6928
7297
  const target = routeSpanToProject(serviceName, liveEntries);
6929
7298
  let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
@@ -7026,6 +7395,42 @@ async function startDaemon(opts = {}) {
7026
7395
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
7027
7396
  );
7028
7397
  }
7398
+ if (singleProject && singleProjectPath) {
7399
+ const ports = {
7400
+ rest: portFromListenAddress(restAddress, restPort),
7401
+ otlp: portFromListenAddress(otlpAddress, otlpPort),
7402
+ // The daemon doesn't bind the web port itself (neatd spawns the web
7403
+ // child); it records the allocated value passed through so the
7404
+ // dashboard and `neat ps` agree on where to look.
7405
+ web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
7406
+ };
7407
+ daemonRecord = {
7408
+ project: singleProject,
7409
+ projectPath: singleProjectPath,
7410
+ pid: process.pid,
7411
+ status: "running",
7412
+ ports,
7413
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7414
+ neatVersion: resolveNeatVersion()
7415
+ };
7416
+ try {
7417
+ await writeDaemonRecord(daemonRecord, home);
7418
+ console.log(
7419
+ `neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
7420
+ );
7421
+ } catch (err) {
7422
+ for (const slot of slots.values()) teardownSlot(slot);
7423
+ if (restApp) await restApp.close().catch(() => {
7424
+ });
7425
+ if (otlpApp) await otlpApp.close().catch(() => {
7426
+ });
7427
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
7428
+ });
7429
+ throw new Error(
7430
+ `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
7431
+ );
7432
+ }
7433
+ }
7029
7434
  }
7030
7435
  const initialBootstrap = loadAll().catch((err) => {
7031
7436
  console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
@@ -7065,9 +7470,9 @@ async function startDaemon(opts = {}) {
7065
7470
  const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
7066
7471
  let registryWatcher = null;
7067
7472
  let reloadTimer = null;
7068
- try {
7069
- const regDir = import_node_path41.default.dirname(regPath);
7070
- const regBase = import_node_path41.default.basename(regPath);
7473
+ if (!singleProject) try {
7474
+ const regDir = import_node_path42.default.dirname(regPath);
7475
+ const regBase = import_node_path42.default.basename(regPath);
7071
7476
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
7072
7477
  if (filename !== null && filename !== regBase) return;
7073
7478
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -7108,6 +7513,9 @@ async function startDaemon(opts = {}) {
7108
7513
  for (const slot of slots.values()) {
7109
7514
  teardownSlot(slot);
7110
7515
  }
7516
+ if (daemonRecord) {
7517
+ await clearDaemonRecord(daemonRecord, home);
7518
+ }
7111
7519
  await import_node_fs25.promises.unlink(pidPath).catch(() => {
7112
7520
  });
7113
7521
  };
@@ -7119,7 +7527,8 @@ async function startDaemon(opts = {}) {
7119
7527
  restAddress,
7120
7528
  otlpAddress,
7121
7529
  bootstrap: tracker,
7122
- initialBootstrap
7530
+ initialBootstrap,
7531
+ daemonRecord
7123
7532
  };
7124
7533
  }
7125
7534
 
@@ -7129,9 +7538,40 @@ init_auth();
7129
7538
  // src/web-spawn.ts
7130
7539
  init_cjs_shims();
7131
7540
  var import_node_child_process2 = require("child_process");
7541
+ var import_node_fs26 = require("fs");
7132
7542
  var import_node_net = __toESM(require("net"), 1);
7133
- var import_node_path42 = __toESM(require("path"), 1);
7543
+ var import_node_path43 = __toESM(require("path"), 1);
7134
7544
  var DEFAULT_WEB_PORT = 6328;
7545
+ var DEFAULT_REST_PORT = 8080;
7546
+ function asValidPort(value) {
7547
+ const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
7548
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
7549
+ }
7550
+ function projectRoot() {
7551
+ const fromEnv = process.env.NEAT_SCAN_PATH;
7552
+ return import_node_path43.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
7553
+ }
7554
+ async function readDaemonPorts(root) {
7555
+ try {
7556
+ const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(root, "neat-out", "daemon.json"), "utf8");
7557
+ const parsed = JSON.parse(raw);
7558
+ const ports = parsed?.ports ?? {};
7559
+ return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
7560
+ } catch {
7561
+ return { web: null, rest: null };
7562
+ }
7563
+ }
7564
+ function resolveWebPorts(args) {
7565
+ const { daemonWeb, daemonRest, webPortEnv, apiUrlEnv, restPortArg } = args;
7566
+ const portFromEnv = webPortEnv && webPortEnv.length > 0 ? Number.parseInt(webPortEnv, 10) : null;
7567
+ if (portFromEnv !== null && (!Number.isFinite(portFromEnv) || portFromEnv <= 0 || portFromEnv > 65535)) {
7568
+ throw new Error(`neatd: invalid NEAT_WEB_PORT="${webPortEnv}"`);
7569
+ }
7570
+ const webPort = daemonWeb ?? portFromEnv ?? DEFAULT_WEB_PORT;
7571
+ const restPort = daemonRest ?? asValidPort(restPortArg) ?? DEFAULT_REST_PORT;
7572
+ const apiUrl = apiUrlEnv ?? `http://localhost:${restPort}`;
7573
+ return { webPort, apiUrl };
7574
+ }
7135
7575
  async function assertPortFree(port) {
7136
7576
  await new Promise((resolve, reject) => {
7137
7577
  const tester = import_node_net.default.createServer();
@@ -7158,47 +7598,103 @@ function resolveWebPackageDir() {
7158
7598
  eval("require")
7159
7599
  );
7160
7600
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
7161
- return import_node_path42.default.dirname(pkgJsonPath);
7601
+ return import_node_path43.default.dirname(pkgJsonPath);
7162
7602
  }
7163
7603
  function resolveStandaloneServerEntry(webDir) {
7164
- return import_node_path42.default.join(webDir, ".next/standalone/packages/web/server.js");
7604
+ return import_node_path43.default.join(webDir, ".next/standalone/packages/web/server.js");
7605
+ }
7606
+ async function pickInternalPort() {
7607
+ return new Promise((resolve, reject) => {
7608
+ const srv = import_node_net.default.createServer();
7609
+ srv.once("error", reject);
7610
+ srv.listen(0, "127.0.0.1", () => {
7611
+ const addr = srv.address();
7612
+ if (addr && typeof addr === "object") {
7613
+ const port = addr.port;
7614
+ srv.close(() => resolve(port));
7615
+ } else {
7616
+ srv.close(() => reject(new Error("neatd: could not pick an internal web port")));
7617
+ }
7618
+ });
7619
+ });
7165
7620
  }
7166
- async function spawnWebUI(restPort) {
7167
- const portRaw = process.env.NEAT_WEB_PORT;
7168
- const port = portRaw && portRaw.length > 0 ? Number.parseInt(portRaw, 10) : DEFAULT_WEB_PORT;
7169
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
7170
- throw new Error(`neatd: invalid NEAT_WEB_PORT="${portRaw}"`);
7171
- }
7621
+ async function spawnWebUI(restPort, opts = {}) {
7622
+ const root = projectRoot();
7623
+ const fromDaemon = await readDaemonPorts(root);
7624
+ const { webPort: port, apiUrl } = resolveWebPorts({
7625
+ daemonWeb: fromDaemon.web,
7626
+ daemonRest: fromDaemon.rest,
7627
+ webPortEnv: process.env.NEAT_WEB_PORT,
7628
+ apiUrlEnv: process.env.NEAT_API_URL,
7629
+ restPortArg: restPort
7630
+ });
7172
7631
  await assertPortFree(port);
7173
- const cwd = resolveWebPackageDir();
7174
- const serverEntry = resolveStandaloneServerEntry(cwd);
7175
- try {
7176
- require.resolve(serverEntry);
7177
- } catch {
7178
- throw new Error(
7179
- `neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
7180
- );
7632
+ const serverEntry = opts.serverEntry ?? resolveStandaloneServerEntry(resolveWebPackageDir());
7633
+ if (!opts.skipBuildCheck) {
7634
+ try {
7635
+ require.resolve(serverEntry);
7636
+ } catch {
7637
+ throw new Error(
7638
+ `neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
7639
+ );
7640
+ }
7181
7641
  }
7182
- const env = {
7183
- ...process.env,
7184
- PORT: String(port),
7185
- HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
7186
- NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
7187
- };
7188
- const child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
7189
- cwd: import_node_path42.default.dirname(serverEntry),
7190
- env,
7191
- stdio: ["ignore", "inherit", "inherit"],
7192
- detached: false
7642
+ let child = null;
7643
+ let internalPort = 0;
7644
+ let starting = null;
7645
+ let stopped = false;
7646
+ function ensureStarted() {
7647
+ if (starting) return starting;
7648
+ starting = (async () => {
7649
+ internalPort = await pickInternalPort();
7650
+ const env = {
7651
+ ...process.env,
7652
+ PORT: String(internalPort),
7653
+ HOSTNAME: "127.0.0.1",
7654
+ NEAT_API_URL: apiUrl
7655
+ };
7656
+ child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
7657
+ cwd: import_node_path43.default.dirname(serverEntry),
7658
+ env,
7659
+ stdio: ["ignore", "inherit", "inherit"],
7660
+ detached: false
7661
+ });
7662
+ child.on("error", (err) => {
7663
+ console.error(`neatd: web UI spawn error \u2014 ${err.message}`);
7664
+ });
7665
+ await waitForListening(internalPort);
7666
+ console.log(`neatd: web UI ready on http://localhost:${port}`);
7667
+ })();
7668
+ return starting;
7669
+ }
7670
+ const front = import_node_net.default.createServer((socket) => {
7671
+ socket.on("error", () => socket.destroy());
7672
+ ensureStarted().then(() => {
7673
+ if (stopped) {
7674
+ socket.destroy();
7675
+ return;
7676
+ }
7677
+ const upstream = import_node_net.default.connect(internalPort, "127.0.0.1");
7678
+ upstream.on("error", () => socket.destroy());
7679
+ socket.pipe(upstream);
7680
+ upstream.pipe(socket);
7681
+ socket.on("close", () => upstream.destroy());
7682
+ upstream.on("close", () => socket.destroy());
7683
+ }).catch((err) => {
7684
+ console.error(`neatd: web UI failed to start \u2014 ${err.message}`);
7685
+ socket.destroy();
7686
+ });
7193
7687
  });
7194
- child.on("error", (err) => {
7195
- console.error(`neatd: web UI spawn error \u2014 ${err.message}`);
7688
+ await new Promise((resolve, reject) => {
7689
+ front.once("error", reject);
7690
+ front.listen(port, "127.0.0.1", () => resolve());
7196
7691
  });
7197
- console.log(`neatd: web UI listening on http://localhost:${port}`);
7198
- let stopped = false;
7692
+ console.log(`neatd: web UI listening on http://localhost:${port} (starts on first open)`);
7199
7693
  async function stop() {
7200
- if (stopped || !child.pid) return;
7694
+ if (stopped) return;
7201
7695
  stopped = true;
7696
+ await new Promise((resolve) => front.close(() => resolve()));
7697
+ if (!child || !child.pid) return;
7202
7698
  try {
7203
7699
  child.kill("SIGTERM");
7204
7700
  } catch {
@@ -7206,18 +7702,46 @@ async function spawnWebUI(restPort) {
7206
7702
  await new Promise((resolve) => {
7207
7703
  const t = setTimeout(() => {
7208
7704
  try {
7209
- child.kill("SIGKILL");
7705
+ child?.kill("SIGKILL");
7210
7706
  } catch {
7211
7707
  }
7212
7708
  resolve();
7213
7709
  }, 3e3);
7214
- child.once("exit", () => {
7710
+ child?.once("exit", () => {
7215
7711
  clearTimeout(t);
7216
7712
  resolve();
7217
7713
  });
7218
7714
  });
7219
7715
  }
7220
- return { child, port, stop };
7716
+ return {
7717
+ get child() {
7718
+ return child;
7719
+ },
7720
+ port,
7721
+ started: () => child !== null,
7722
+ stop
7723
+ };
7724
+ }
7725
+ async function waitForListening(port, timeoutMs = 15e3) {
7726
+ const deadline = Date.now() + timeoutMs;
7727
+ for (; ; ) {
7728
+ const ok = await new Promise((resolve) => {
7729
+ const s = import_node_net.default.connect(port, "127.0.0.1");
7730
+ s.once("connect", () => {
7731
+ s.destroy();
7732
+ resolve(true);
7733
+ });
7734
+ s.once("error", () => {
7735
+ s.destroy();
7736
+ resolve(false);
7737
+ });
7738
+ });
7739
+ if (ok) return;
7740
+ if (Date.now() > deadline) {
7741
+ throw new Error(`neatd: web UI did not start listening on :${port} within ${timeoutMs}ms`);
7742
+ }
7743
+ await new Promise((r) => setTimeout(r, 150));
7744
+ }
7221
7745
  }
7222
7746
 
7223
7747
  // src/version-skew.ts
@@ -7290,7 +7814,7 @@ function localVersion() {
7290
7814
  return process.env.NEAT_LOCAL_VERSION;
7291
7815
  }
7292
7816
  try {
7293
- const req2 = (0, import_node_module.createRequire)(importMetaUrl);
7817
+ const req2 = (0, import_node_module2.createRequire)(importMetaUrl);
7294
7818
  const pkg = req2("../package.json");
7295
7819
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7296
7820
  } catch {
@@ -7299,14 +7823,14 @@ function localVersion() {
7299
7823
  }
7300
7824
  function neatHome2() {
7301
7825
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
7302
- return import_node_path43.default.resolve(process.env.NEAT_HOME);
7826
+ return import_node_path44.default.resolve(process.env.NEAT_HOME);
7303
7827
  }
7304
7828
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7305
- return import_node_path43.default.join(home, ".neat");
7829
+ return import_node_path44.default.join(home, ".neat");
7306
7830
  }
7307
7831
  async function readPid() {
7308
7832
  try {
7309
- const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(neatHome2(), "neatd.pid"), "utf8");
7833
+ const raw = await import_node_fs27.promises.readFile(import_node_path44.default.join(neatHome2(), "neatd.pid"), "utf8");
7310
7834
  const n = Number.parseInt(raw.trim(), 10);
7311
7835
  return Number.isFinite(n) ? n : null;
7312
7836
  } catch {
@@ -7320,10 +7844,19 @@ function restPortFromEnv() {
7320
7844
  const raw = process.env.PORT;
7321
7845
  return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
7322
7846
  }
7847
+ function webPortFromEnv() {
7848
+ const raw = process.env.NEAT_WEB_PORT;
7849
+ if (!raw || raw.length === 0) return void 0;
7850
+ const n = Number.parseInt(raw, 10);
7851
+ return Number.isFinite(n) ? n : void 0;
7852
+ }
7323
7853
  async function cmdStart() {
7854
+ const project = process.env.NEAT_PROJECT;
7855
+ const projectPath = process.env.NEAT_PROJECT_PATH;
7856
+ const startOpts = project && project.length > 0 ? { project, projectPath, webPort: webPortFromEnv() } : {};
7324
7857
  let handle;
7325
7858
  try {
7326
- handle = await startDaemon();
7859
+ handle = await startDaemon(startOpts);
7327
7860
  } catch (err) {
7328
7861
  if (err instanceof BindAuthorityError) {
7329
7862
  console.error(`neatd: ${err.message}`);