@neat.is/core 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -41,6 +41,43 @@ var init_cjs_shims = __esm({
41
41
  }
42
42
  });
43
43
 
44
+ // src/auth.ts
45
+ function mountBearerAuth(app, opts) {
46
+ if (!opts.token || opts.token.length === 0) return;
47
+ if (opts.trustProxy) return;
48
+ const expected = Buffer.from(opts.token, "utf8");
49
+ const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
50
+ app.addHook("preHandler", (req, reply, done) => {
51
+ const path44 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
52
+ for (const suffix of suffixes) {
53
+ if (path44 === suffix || path44.endsWith(suffix)) {
54
+ done();
55
+ return;
56
+ }
57
+ }
58
+ const header = req.headers.authorization;
59
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
60
+ void reply.code(401).send({ error: "unauthorized" });
61
+ return;
62
+ }
63
+ const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
64
+ if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
65
+ void reply.code(401).send({ error: "unauthorized" });
66
+ return;
67
+ }
68
+ done();
69
+ });
70
+ }
71
+ var import_node_crypto, DEFAULT_UNAUTH_SUFFIXES;
72
+ var init_auth = __esm({
73
+ "src/auth.ts"() {
74
+ "use strict";
75
+ init_cjs_shims();
76
+ import_node_crypto = require("crypto");
77
+ DEFAULT_UNAUTH_SUFFIXES = ["/health", "/healthz", "/readyz"];
78
+ }
79
+ });
80
+
44
81
  // src/otel-grpc.ts
45
82
  var otel_grpc_exports = {};
46
83
  __export(otel_grpc_exports, {
@@ -101,8 +138,8 @@ function reshapeGrpcRequest(req) {
101
138
  };
102
139
  }
103
140
  function resolveProtoRoot() {
104
- const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
- return import_node_path34.default.resolve(here, "..", "proto");
141
+ const here = import_node_path35.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
142
+ return import_node_path35.default.resolve(here, "..", "proto");
106
143
  }
107
144
  function loadTraceService() {
108
145
  const protoRoot = resolveProtoRoot();
@@ -123,8 +160,21 @@ function loadTraceService() {
123
160
  async function startOtelGrpcReceiver(opts) {
124
161
  const server = new grpc.Server();
125
162
  const service = loadTraceService();
163
+ const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
164
+ const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
126
165
  server.addService(service, {
127
166
  Export: (call, callback) => {
167
+ if (requiresAuth) {
168
+ const meta = call.metadata.get("authorization");
169
+ const got = meta.length > 0 ? String(meta[0]) : "";
170
+ const a = Buffer.from(got, "utf8");
171
+ const b = Buffer.from(expectedHeader, "utf8");
172
+ const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
173
+ if (!ok) {
174
+ callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
175
+ return;
176
+ }
177
+ }
128
178
  void (async () => {
129
179
  try {
130
180
  const reshaped = reshapeGrpcRequest(call.request ?? {});
@@ -157,13 +207,14 @@ async function startOtelGrpcReceiver(opts) {
157
207
  })
158
208
  };
159
209
  }
160
- var import_node_url, import_node_path34, grpc, protoLoader;
210
+ var import_node_url, import_node_path35, import_node_crypto2, grpc, protoLoader;
161
211
  var init_otel_grpc = __esm({
162
212
  "src/otel-grpc.ts"() {
163
213
  "use strict";
164
214
  init_cjs_shims();
165
215
  import_node_url = require("url");
166
- import_node_path34 = __toESM(require("path"), 1);
216
+ import_node_path35 = __toESM(require("path"), 1);
217
+ import_node_crypto2 = require("crypto");
167
218
  grpc = __toESM(require("@grpc/grpc-js"), 1);
168
219
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
169
220
  init_otel();
@@ -258,21 +309,41 @@ function parseOtlpRequest(body) {
258
309
  }
259
310
  return out;
260
311
  }
261
- function loadProtobufDecoder() {
262
- if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
263
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
312
+ function loadProtoRoot() {
313
+ const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
314
+ const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
265
315
  const root = new import_protobufjs.default.Root();
266
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
316
+ root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
267
317
  root.loadSync(
268
318
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
269
319
  { keepCase: true }
270
320
  );
321
+ return root;
322
+ }
323
+ function loadProtobufDecoder() {
324
+ if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
325
+ const root = loadProtoRoot();
271
326
  exportTraceServiceRequestType = root.lookupType(
272
327
  "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
273
328
  );
274
329
  return exportTraceServiceRequestType;
275
330
  }
331
+ function loadProtobufResponseEncoder() {
332
+ if (exportTraceServiceResponseType) return exportTraceServiceResponseType;
333
+ const root = loadProtoRoot();
334
+ exportTraceServiceResponseType = root.lookupType(
335
+ "opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse"
336
+ );
337
+ return exportTraceServiceResponseType;
338
+ }
339
+ function encodeProtobufResponseBody() {
340
+ if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
341
+ const Type = loadProtobufResponseEncoder();
342
+ const msg = Type.create({});
343
+ const encoded = Type.encode(msg).finish();
344
+ cachedProtobufResponseBody = Buffer.from(encoded);
345
+ return cachedProtobufResponseBody;
346
+ }
276
347
  async function decodeProtobufBody(buf) {
277
348
  const Type = loadProtobufDecoder();
278
349
  const decoded = Type.decode(buf).toJSON();
@@ -284,6 +355,7 @@ async function buildOtelReceiver(opts) {
284
355
  logger: false,
285
356
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
286
357
  });
358
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
287
359
  const queue = [];
288
360
  let draining = false;
289
361
  let drainPromise = Promise.resolve();
@@ -319,7 +391,9 @@ async function buildOtelReceiver(opts) {
319
391
  app.post("/v1/traces", async (req, reply) => {
320
392
  const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
321
393
  let body;
394
+ let responseFlavor;
322
395
  if (ct === "application/x-protobuf") {
396
+ responseFlavor = "protobuf";
323
397
  try {
324
398
  body = await decodeProtobufBody(req.body);
325
399
  } catch (err) {
@@ -328,6 +402,7 @@ async function buildOtelReceiver(opts) {
328
402
  });
329
403
  }
330
404
  } else if (!ct || ct === "application/json") {
405
+ responseFlavor = "json";
331
406
  body = req.body ?? {};
332
407
  } else {
333
408
  return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
@@ -345,7 +420,11 @@ async function buildOtelReceiver(opts) {
345
420
  }
346
421
  }
347
422
  enqueue(spans);
348
- return reply.code(200).send({ partialSuccess: {} });
423
+ if (responseFlavor === "protobuf") {
424
+ const buf = encodeProtobufResponseBody();
425
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
426
+ }
427
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
349
428
  });
350
429
  const decorated = app;
351
430
  decorated.flushPending = async () => {
@@ -355,16 +434,19 @@ async function buildOtelReceiver(opts) {
355
434
  };
356
435
  return decorated;
357
436
  }
358
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
437
+ var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
359
438
  var init_otel = __esm({
360
439
  "src/otel.ts"() {
361
440
  "use strict";
362
441
  init_cjs_shims();
363
- import_node_path35 = __toESM(require("path"), 1);
442
+ import_node_path36 = __toESM(require("path"), 1);
364
443
  import_node_url2 = require("url");
365
444
  import_fastify2 = __toESM(require("fastify"), 1);
366
445
  import_protobufjs = __toESM(require("protobufjs"), 1);
446
+ init_auth();
367
447
  exportTraceServiceRequestType = null;
448
+ exportTraceServiceResponseType = null;
449
+ cachedProtobufResponseBody = null;
368
450
  }
369
451
  });
370
452
 
@@ -380,8 +462,8 @@ __export(cli_exports, {
380
462
  });
381
463
  module.exports = __toCommonJS(cli_exports);
382
464
  init_cjs_shims();
383
- var import_node_path40 = __toESM(require("path"), 1);
384
- var import_node_fs25 = require("fs");
465
+ var import_node_path43 = __toESM(require("path"), 1);
466
+ var import_node_fs28 = require("fs");
385
467
 
386
468
  // src/graph.ts
387
469
  init_cjs_shims();
@@ -891,19 +973,19 @@ function confidenceFromMix(edges, now = Date.now()) {
891
973
  function longestIncomingWalk(graph, start, maxDepth) {
892
974
  let best = { path: [start], edges: [] };
893
975
  const visited = /* @__PURE__ */ new Set([start]);
894
- function step(node, path41, edges) {
895
- if (path41.length > best.path.length) {
896
- best = { path: [...path41], edges: [...edges] };
976
+ function step(node, path44, edges) {
977
+ if (path44.length > best.path.length) {
978
+ best = { path: [...path44], edges: [...edges] };
897
979
  }
898
- if (path41.length - 1 >= maxDepth) return;
980
+ if (path44.length - 1 >= maxDepth) return;
899
981
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
900
982
  for (const [srcId, edge] of incoming) {
901
983
  if (visited.has(srcId)) continue;
902
984
  visited.add(srcId);
903
- path41.push(srcId);
985
+ path44.push(srcId);
904
986
  edges.push(edge);
905
- step(srcId, path41, edges);
906
- path41.pop();
987
+ step(srcId, path44, edges);
988
+ path44.pop();
907
989
  edges.pop();
908
990
  visited.delete(srcId);
909
991
  }
@@ -4146,128 +4228,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4146
4228
  return result;
4147
4229
  }
4148
4230
 
4149
- // src/persist.ts
4150
- init_cjs_shims();
4151
- var import_node_fs18 = require("fs");
4152
- var import_node_path31 = __toESM(require("path"), 1);
4153
- var import_types19 = require("@neat.is/types");
4154
- var SCHEMA_VERSION = 3;
4155
- function migrateV1ToV2(payload) {
4156
- const nodes = payload.graph.nodes;
4157
- if (Array.isArray(nodes)) {
4158
- for (const node of nodes) {
4159
- if (node.attributes && "pgDriverVersion" in node.attributes) {
4160
- delete node.attributes.pgDriverVersion;
4161
- }
4162
- }
4163
- }
4164
- return { ...payload, schemaVersion: 2 };
4165
- }
4166
- function migrateV2ToV3(payload) {
4167
- const edges = payload.graph.edges;
4168
- if (Array.isArray(edges)) {
4169
- for (const edge of edges) {
4170
- const attrs = edge.attributes;
4171
- if (!attrs || attrs.provenance !== "FRONTIER") continue;
4172
- attrs.provenance = import_types19.Provenance.OBSERVED;
4173
- const type = typeof attrs.type === "string" ? attrs.type : void 0;
4174
- const source = typeof attrs.source === "string" ? attrs.source : void 0;
4175
- const target = typeof attrs.target === "string" ? attrs.target : void 0;
4176
- if (type && source && target) {
4177
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4178
- attrs.id = newId;
4179
- if (edge.key) edge.key = newId;
4180
- }
4181
- }
4182
- }
4183
- return { ...payload, schemaVersion: 3 };
4184
- }
4185
- async function ensureDir(filePath) {
4186
- await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4187
- }
4188
- async function saveGraphToDisk(graph, outPath) {
4189
- await ensureDir(outPath);
4190
- const payload = {
4191
- schemaVersion: SCHEMA_VERSION,
4192
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4193
- graph: graph.export()
4194
- };
4195
- const tmp = `${outPath}.tmp`;
4196
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4197
- await import_node_fs18.promises.rename(tmp, outPath);
4198
- }
4199
- async function loadGraphFromDisk(graph, outPath) {
4200
- let raw;
4201
- try {
4202
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4203
- } catch (err) {
4204
- if (err.code === "ENOENT") return;
4205
- throw err;
4206
- }
4207
- let payload = JSON.parse(raw);
4208
- if (payload.schemaVersion === 1) {
4209
- payload = migrateV1ToV2(payload);
4210
- }
4211
- if (payload.schemaVersion === 2) {
4212
- payload = migrateV2ToV3(payload);
4213
- }
4214
- if (payload.schemaVersion !== SCHEMA_VERSION) {
4215
- throw new Error(
4216
- `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
4217
- );
4218
- }
4219
- graph.clear();
4220
- graph.import(payload.graph);
4221
- }
4222
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4223
- let stopped = false;
4224
- const tick = async () => {
4225
- if (stopped) return;
4226
- try {
4227
- await saveGraphToDisk(graph, outPath);
4228
- } catch (err) {
4229
- console.error("persist: periodic save failed", err);
4230
- }
4231
- };
4232
- const interval = setInterval(() => {
4233
- void tick();
4234
- }, intervalMs);
4235
- const onSignal = (signal) => {
4236
- void (async () => {
4237
- try {
4238
- await saveGraphToDisk(graph, outPath);
4239
- } catch (err) {
4240
- console.error(`persist: ${signal} save failed`, err);
4241
- } finally {
4242
- process.exit(0);
4243
- }
4244
- })();
4245
- };
4246
- process.on("SIGTERM", onSignal);
4247
- process.on("SIGINT", onSignal);
4248
- return () => {
4249
- stopped = true;
4250
- clearInterval(interval);
4251
- process.off("SIGTERM", onSignal);
4252
- process.off("SIGINT", onSignal);
4253
- };
4254
- }
4255
-
4256
- // src/watch.ts
4257
- init_cjs_shims();
4258
- var import_node_fs22 = __toESM(require("fs"), 1);
4259
- var import_node_path37 = __toESM(require("path"), 1);
4260
- var import_chokidar = __toESM(require("chokidar"), 1);
4261
-
4262
- // src/api.ts
4263
- init_cjs_shims();
4264
- var import_fastify = __toESM(require("fastify"), 1);
4265
- var import_cors = __toESM(require("@fastify/cors"), 1);
4266
- var import_types22 = require("@neat.is/types");
4267
-
4268
4231
  // src/divergences.ts
4269
4232
  init_cjs_shims();
4270
- var import_types20 = require("@neat.is/types");
4233
+ var import_types19 = require("@neat.is/types");
4271
4234
  function bucketKey(source, target, type) {
4272
4235
  return `${type}|${source}|${target}`;
4273
4236
  }
@@ -4275,22 +4238,22 @@ function bucketEdges(graph) {
4275
4238
  const buckets = /* @__PURE__ */ new Map();
4276
4239
  graph.forEachEdge((id, attrs) => {
4277
4240
  const e = attrs;
4278
- const parsed = (0, import_types20.parseEdgeId)(id);
4241
+ const parsed = (0, import_types19.parseEdgeId)(id);
4279
4242
  const provenance = parsed?.provenance ?? e.provenance;
4280
4243
  const key = bucketKey(e.source, e.target, e.type);
4281
4244
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4282
4245
  switch (provenance) {
4283
- case import_types20.Provenance.EXTRACTED:
4246
+ case import_types19.Provenance.EXTRACTED:
4284
4247
  cur.extracted = e;
4285
4248
  break;
4286
- case import_types20.Provenance.OBSERVED:
4249
+ case import_types19.Provenance.OBSERVED:
4287
4250
  cur.observed = e;
4288
4251
  break;
4289
- case import_types20.Provenance.INFERRED:
4252
+ case import_types19.Provenance.INFERRED:
4290
4253
  cur.inferred = e;
4291
4254
  break;
4292
4255
  default:
4293
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4256
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4294
4257
  }
4295
4258
  buckets.set(key, cur);
4296
4259
  });
@@ -4299,7 +4262,7 @@ function bucketEdges(graph) {
4299
4262
  function nodeIsFrontier(graph, nodeId) {
4300
4263
  if (!graph.hasNode(nodeId)) return false;
4301
4264
  const attrs = graph.getNodeAttributes(nodeId);
4302
- return attrs.type === import_types20.NodeType.FrontierNode;
4265
+ return attrs.type === import_types19.NodeType.FrontierNode;
4303
4266
  }
4304
4267
  function clampConfidence(n) {
4305
4268
  if (!Number.isFinite(n)) return 0;
@@ -4360,7 +4323,7 @@ function declaredHostFor(svc) {
4360
4323
  function hasExtractedConfiguredBy(graph, svcId) {
4361
4324
  for (const edgeId of graph.outboundEdges(svcId)) {
4362
4325
  const e = graph.getEdgeAttributes(edgeId);
4363
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4326
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4364
4327
  return true;
4365
4328
  }
4366
4329
  }
@@ -4373,10 +4336,10 @@ function detectHostMismatch(graph, svcId, svc) {
4373
4336
  const out = [];
4374
4337
  for (const edgeId of graph.outboundEdges(svcId)) {
4375
4338
  const edge = graph.getEdgeAttributes(edgeId);
4376
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4377
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4339
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4340
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4378
4341
  const target = graph.getNodeAttributes(edge.target);
4379
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4342
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4380
4343
  const observedHost = target.host?.trim();
4381
4344
  if (!observedHost) continue;
4382
4345
  if (observedHost === declaredHost) continue;
@@ -4398,10 +4361,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4398
4361
  const deps = svc.dependencies ?? {};
4399
4362
  for (const edgeId of graph.outboundEdges(svcId)) {
4400
4363
  const edge = graph.getEdgeAttributes(edgeId);
4401
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4402
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4364
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4365
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4403
4366
  const target = graph.getNodeAttributes(edge.target);
4404
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4367
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4405
4368
  for (const pair of compatPairs()) {
4406
4369
  if (pair.engine !== target.engine) continue;
4407
4370
  const declared = deps[pair.driver];
@@ -4462,7 +4425,7 @@ function computeDivergences(graph, opts = {}) {
4462
4425
  }
4463
4426
  graph.forEachNode((nodeId, attrs) => {
4464
4427
  const n = attrs;
4465
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4428
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4466
4429
  const svc = n;
4467
4430
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4468
4431
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4495,76 +4458,327 @@ function computeDivergences(graph, opts = {}) {
4495
4458
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4496
4459
  return a.target.localeCompare(b.target);
4497
4460
  });
4498
- return import_types20.DivergenceResultSchema.parse({
4461
+ return import_types19.DivergenceResultSchema.parse({
4499
4462
  divergences: filtered,
4500
4463
  totalAffected: filtered.length,
4501
4464
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
4502
4465
  });
4503
4466
  }
4504
4467
 
4505
- // src/diff.ts
4468
+ // src/persist.ts
4506
4469
  init_cjs_shims();
4507
- var import_node_fs19 = require("fs");
4508
- async function loadSnapshotForDiff(target) {
4509
- if (/^https?:\/\//i.test(target)) {
4510
- const res = await fetch(target);
4511
- if (!res.ok) {
4512
- throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
4470
+ var import_node_fs18 = require("fs");
4471
+ var import_node_path31 = __toESM(require("path"), 1);
4472
+ var import_types20 = require("@neat.is/types");
4473
+ var SCHEMA_VERSION = 3;
4474
+ function migrateV1ToV2(payload) {
4475
+ const nodes = payload.graph.nodes;
4476
+ if (Array.isArray(nodes)) {
4477
+ for (const node of nodes) {
4478
+ if (node.attributes && "pgDriverVersion" in node.attributes) {
4479
+ delete node.attributes.pgDriverVersion;
4480
+ }
4513
4481
  }
4514
- return await res.json();
4515
4482
  }
4516
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
4517
- return JSON.parse(raw);
4483
+ return { ...payload, schemaVersion: 2 };
4518
4484
  }
4519
- function indexEntries(entries) {
4520
- const m = /* @__PURE__ */ new Map();
4521
- if (!entries) return m;
4522
- for (const entry2 of entries) {
4523
- const id = entry2.attributes?.id ?? entry2.key;
4524
- if (!id) continue;
4525
- m.set(id, entry2.attributes);
4485
+ function migrateV2ToV3(payload) {
4486
+ const edges = payload.graph.edges;
4487
+ if (Array.isArray(edges)) {
4488
+ for (const edge of edges) {
4489
+ const attrs = edge.attributes;
4490
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
4491
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4492
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
4493
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4494
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4495
+ if (type && source && target) {
4496
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4497
+ attrs.id = newId;
4498
+ if (edge.key) edge.key = newId;
4499
+ }
4500
+ }
4526
4501
  }
4527
- return m;
4502
+ return { ...payload, schemaVersion: 3 };
4528
4503
  }
4529
- function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4530
- const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
4531
- const baseEdges = indexEntries(baseSnapshot.graph?.edges);
4532
- const liveNodes = /* @__PURE__ */ new Map();
4533
- liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
4534
- const liveEdges = /* @__PURE__ */ new Map();
4535
- liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
4536
- const result = {
4537
- base: { exportedAt: baseSnapshot.exportedAt },
4538
- current: { exportedAt: currentExportedAt },
4539
- added: { nodes: [], edges: [] },
4540
- removed: { nodes: [], edges: [] },
4541
- changed: { nodes: [], edges: [] }
4504
+ async function ensureDir(filePath) {
4505
+ await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4506
+ }
4507
+ async function saveGraphToDisk(graph, outPath) {
4508
+ await ensureDir(outPath);
4509
+ const payload = {
4510
+ schemaVersion: SCHEMA_VERSION,
4511
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4512
+ graph: graph.export()
4542
4513
  };
4543
- for (const [id, after] of liveNodes) {
4544
- const before = baseNodes.get(id);
4545
- if (!before) {
4546
- result.added.nodes.push(after);
4547
- } else if (!shallowEqual(before, after)) {
4548
- result.changed.nodes.push({ id, before, after });
4549
- }
4514
+ const tmp = `${outPath}.tmp`;
4515
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4516
+ await import_node_fs18.promises.rename(tmp, outPath);
4517
+ }
4518
+ async function loadGraphFromDisk(graph, outPath) {
4519
+ let raw;
4520
+ try {
4521
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4522
+ } catch (err) {
4523
+ if (err.code === "ENOENT") return;
4524
+ throw err;
4550
4525
  }
4551
- for (const [id, before] of baseNodes) {
4552
- if (!liveNodes.has(id)) result.removed.nodes.push(before);
4526
+ let payload = JSON.parse(raw);
4527
+ if (payload.schemaVersion === 1) {
4528
+ payload = migrateV1ToV2(payload);
4553
4529
  }
4554
- for (const [id, after] of liveEdges) {
4555
- const before = baseEdges.get(id);
4556
- if (!before) {
4557
- result.added.edges.push(after);
4558
- } else if (!shallowEqual(before, after)) {
4559
- result.changed.edges.push({ id, before, after });
4560
- }
4530
+ if (payload.schemaVersion === 2) {
4531
+ payload = migrateV2ToV3(payload);
4561
4532
  }
4562
- for (const [id, before] of baseEdges) {
4563
- if (!liveEdges.has(id)) result.removed.edges.push(before);
4533
+ if (payload.schemaVersion !== SCHEMA_VERSION) {
4534
+ throw new Error(
4535
+ `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
4536
+ );
4564
4537
  }
4565
- return result;
4538
+ graph.clear();
4539
+ graph.import(payload.graph);
4566
4540
  }
4567
- function shallowEqual(a, b) {
4541
+ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4542
+ let stopped = false;
4543
+ const tick = async () => {
4544
+ if (stopped) return;
4545
+ try {
4546
+ await saveGraphToDisk(graph, outPath);
4547
+ } catch (err) {
4548
+ console.error("persist: periodic save failed", err);
4549
+ }
4550
+ };
4551
+ const interval = setInterval(() => {
4552
+ void tick();
4553
+ }, intervalMs);
4554
+ const onSignal = (signal) => {
4555
+ void (async () => {
4556
+ try {
4557
+ await saveGraphToDisk(graph, outPath);
4558
+ } catch (err) {
4559
+ console.error(`persist: ${signal} save failed`, err);
4560
+ } finally {
4561
+ process.exit(0);
4562
+ }
4563
+ })();
4564
+ };
4565
+ process.on("SIGTERM", onSignal);
4566
+ process.on("SIGINT", onSignal);
4567
+ return () => {
4568
+ stopped = true;
4569
+ clearInterval(interval);
4570
+ process.off("SIGTERM", onSignal);
4571
+ process.off("SIGINT", onSignal);
4572
+ };
4573
+ }
4574
+
4575
+ // src/gitignore.ts
4576
+ init_cjs_shims();
4577
+ var import_node_fs19 = require("fs");
4578
+ var import_node_path32 = __toESM(require("path"), 1);
4579
+ var NEAT_OUT_LINE = "neat-out/";
4580
+ var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
4581
+ function isNeatOutLine(line) {
4582
+ const trimmed = line.trim();
4583
+ return trimmed === "neat-out/" || trimmed === "neat-out";
4584
+ }
4585
+ async function ensureNeatOutIgnored(projectDir) {
4586
+ const file = import_node_path32.default.join(projectDir, ".gitignore");
4587
+ let existing = null;
4588
+ try {
4589
+ existing = await import_node_fs19.promises.readFile(file, "utf8");
4590
+ } catch (err) {
4591
+ if (err.code !== "ENOENT") throw err;
4592
+ }
4593
+ if (existing === null) {
4594
+ await import_node_fs19.promises.writeFile(file, `${NEAT_HEADER}
4595
+ ${NEAT_OUT_LINE}
4596
+ `, "utf8");
4597
+ return { action: "created", file };
4598
+ }
4599
+ for (const line of existing.split(/\r?\n/)) {
4600
+ if (isNeatOutLine(line)) return { action: "unchanged", file };
4601
+ }
4602
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
4603
+ const appended = `${needsLeadingNewline ? "\n" : ""}
4604
+ ${NEAT_HEADER}
4605
+ ${NEAT_OUT_LINE}
4606
+ `;
4607
+ await import_node_fs19.promises.writeFile(file, existing + appended, "utf8");
4608
+ return { action: "added", file };
4609
+ }
4610
+
4611
+ // src/summary.ts
4612
+ init_cjs_shims();
4613
+ var import_types21 = require("@neat.is/types");
4614
+ function renderOtelEnvBlock() {
4615
+ return [
4616
+ "for prod OTel routing, set these in your deploy platform's env:",
4617
+ " OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
4618
+ " OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
4619
+ ].join("\n");
4620
+ }
4621
+ function findIncompatServices(nodes) {
4622
+ return nodes.filter(
4623
+ (n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4624
+ );
4625
+ }
4626
+ function servicesWithoutObserved(nodes, edges) {
4627
+ const seen = /* @__PURE__ */ new Set();
4628
+ for (const e of edges) {
4629
+ if (e.provenance === import_types21.Provenance.OBSERVED) {
4630
+ seen.add(e.source);
4631
+ seen.add(e.target);
4632
+ }
4633
+ }
4634
+ return nodes.filter(
4635
+ (n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
4636
+ );
4637
+ }
4638
+ function formatDivergence(d) {
4639
+ const conf = d.confidence.toFixed(2);
4640
+ return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
4641
+ }
4642
+ function renderValueForwardSummary(input) {
4643
+ const { graph, divergences, verbose } = input;
4644
+ const nodes = [];
4645
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
4646
+ const edges = [];
4647
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
4648
+ const lines = [];
4649
+ lines.push("=== neat: findings ===");
4650
+ lines.push("");
4651
+ const incompatServices = findIncompatServices(nodes);
4652
+ const totalIncompats = incompatServices.reduce(
4653
+ (acc, s) => acc + (s.incompatibilities?.length ?? 0),
4654
+ 0
4655
+ );
4656
+ lines.push(`compat violations: ${totalIncompats}`);
4657
+ for (const svc of incompatServices) {
4658
+ for (const inc of svc.incompatibilities ?? []) {
4659
+ const detail = formatIncompat(inc);
4660
+ lines.push(` ${svc.name}: ${detail}`);
4661
+ }
4662
+ }
4663
+ lines.push("");
4664
+ const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
4665
+ lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
4666
+ for (const d of top) lines.push(formatDivergence(d));
4667
+ lines.push("");
4668
+ const noObserved = servicesWithoutObserved(nodes, edges);
4669
+ if (noObserved.length > 0) {
4670
+ lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
4671
+ for (const svc of noObserved) lines.push(` ${svc.name}`);
4672
+ lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
4673
+ lines.push("");
4674
+ }
4675
+ lines.push(renderOtelEnvBlock());
4676
+ lines.push("");
4677
+ if (verbose) {
4678
+ const byNode = /* @__PURE__ */ new Map();
4679
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
4680
+ const byEdge = /* @__PURE__ */ new Map();
4681
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
4682
+ lines.push("=== graph (verbose) ===");
4683
+ lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
4684
+ lines.push("nodes:");
4685
+ for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
4686
+ lines.push("edges:");
4687
+ for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
4688
+ lines.push("");
4689
+ }
4690
+ return lines.join("\n");
4691
+ }
4692
+ function formatIncompat(inc) {
4693
+ if (inc.kind === "node-engine") {
4694
+ const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
4695
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
4696
+ }
4697
+ if (inc.kind === "package-conflict") {
4698
+ const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
4699
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
4700
+ }
4701
+ if (inc.kind === "deprecated-api") {
4702
+ return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
4703
+ }
4704
+ return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
4705
+ }
4706
+
4707
+ // src/watch.ts
4708
+ init_cjs_shims();
4709
+ var import_node_fs23 = __toESM(require("fs"), 1);
4710
+ var import_node_path38 = __toESM(require("path"), 1);
4711
+ var import_chokidar = __toESM(require("chokidar"), 1);
4712
+
4713
+ // src/api.ts
4714
+ init_cjs_shims();
4715
+ var import_fastify = __toESM(require("fastify"), 1);
4716
+ var import_cors = __toESM(require("@fastify/cors"), 1);
4717
+ var import_types23 = require("@neat.is/types");
4718
+
4719
+ // src/diff.ts
4720
+ init_cjs_shims();
4721
+ var import_node_fs20 = require("fs");
4722
+ async function loadSnapshotForDiff(target) {
4723
+ if (/^https?:\/\//i.test(target)) {
4724
+ const res = await fetch(target);
4725
+ if (!res.ok) {
4726
+ throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
4727
+ }
4728
+ return await res.json();
4729
+ }
4730
+ const raw = await import_node_fs20.promises.readFile(target, "utf8");
4731
+ return JSON.parse(raw);
4732
+ }
4733
+ function indexEntries(entries) {
4734
+ const m = /* @__PURE__ */ new Map();
4735
+ if (!entries) return m;
4736
+ for (const entry2 of entries) {
4737
+ const id = entry2.attributes?.id ?? entry2.key;
4738
+ if (!id) continue;
4739
+ m.set(id, entry2.attributes);
4740
+ }
4741
+ return m;
4742
+ }
4743
+ function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4744
+ const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
4745
+ const baseEdges = indexEntries(baseSnapshot.graph?.edges);
4746
+ const liveNodes = /* @__PURE__ */ new Map();
4747
+ liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
4748
+ const liveEdges = /* @__PURE__ */ new Map();
4749
+ liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
4750
+ const result = {
4751
+ base: { exportedAt: baseSnapshot.exportedAt },
4752
+ current: { exportedAt: currentExportedAt },
4753
+ added: { nodes: [], edges: [] },
4754
+ removed: { nodes: [], edges: [] },
4755
+ changed: { nodes: [], edges: [] }
4756
+ };
4757
+ for (const [id, after] of liveNodes) {
4758
+ const before = baseNodes.get(id);
4759
+ if (!before) {
4760
+ result.added.nodes.push(after);
4761
+ } else if (!shallowEqual(before, after)) {
4762
+ result.changed.nodes.push({ id, before, after });
4763
+ }
4764
+ }
4765
+ for (const [id, before] of baseNodes) {
4766
+ if (!liveNodes.has(id)) result.removed.nodes.push(before);
4767
+ }
4768
+ for (const [id, after] of liveEdges) {
4769
+ const before = baseEdges.get(id);
4770
+ if (!before) {
4771
+ result.added.edges.push(after);
4772
+ } else if (!shallowEqual(before, after)) {
4773
+ result.changed.edges.push({ id, before, after });
4774
+ }
4775
+ }
4776
+ for (const [id, before] of baseEdges) {
4777
+ if (!liveEdges.has(id)) result.removed.edges.push(before);
4778
+ }
4779
+ return result;
4780
+ }
4781
+ function shallowEqual(a, b) {
4568
4782
  return canonicalJson(a) === canonicalJson(b);
4569
4783
  }
4570
4784
  function canonicalJson(value) {
@@ -4581,23 +4795,23 @@ function canonicalJson(value) {
4581
4795
 
4582
4796
  // src/projects.ts
4583
4797
  init_cjs_shims();
4584
- var import_node_path32 = __toESM(require("path"), 1);
4798
+ var import_node_path33 = __toESM(require("path"), 1);
4585
4799
  function pathsForProject(project, baseDir) {
4586
4800
  if (project === DEFAULT_PROJECT) {
4587
4801
  return {
4588
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4589
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4590
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4591
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4592
- policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
4802
+ snapshotPath: import_node_path33.default.join(baseDir, "graph.json"),
4803
+ errorsPath: import_node_path33.default.join(baseDir, "errors.ndjson"),
4804
+ staleEventsPath: import_node_path33.default.join(baseDir, "stale-events.ndjson"),
4805
+ embeddingsCachePath: import_node_path33.default.join(baseDir, "embeddings.json"),
4806
+ policyViolationsPath: import_node_path33.default.join(baseDir, "policy-violations.ndjson")
4593
4807
  };
4594
4808
  }
4595
4809
  return {
4596
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4597
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4598
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4599
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4600
- policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
4810
+ snapshotPath: import_node_path33.default.join(baseDir, `${project}.json`),
4811
+ errorsPath: import_node_path33.default.join(baseDir, `errors.${project}.ndjson`),
4812
+ staleEventsPath: import_node_path33.default.join(baseDir, `stale-events.${project}.ndjson`),
4813
+ embeddingsCachePath: import_node_path33.default.join(baseDir, `embeddings.${project}.json`),
4814
+ policyViolationsPath: import_node_path33.default.join(baseDir, `policy-violations.${project}.ndjson`)
4601
4815
  };
4602
4816
  }
4603
4817
  var Projects = class {
@@ -4633,49 +4847,49 @@ var Projects = class {
4633
4847
 
4634
4848
  // src/registry.ts
4635
4849
  init_cjs_shims();
4636
- var import_node_fs20 = require("fs");
4850
+ var import_node_fs21 = require("fs");
4637
4851
  var import_node_os2 = __toESM(require("os"), 1);
4638
- var import_node_path33 = __toESM(require("path"), 1);
4639
- var import_types21 = require("@neat.is/types");
4852
+ var import_node_path34 = __toESM(require("path"), 1);
4853
+ var import_types22 = require("@neat.is/types");
4640
4854
  var LOCK_TIMEOUT_MS = 5e3;
4641
4855
  var LOCK_RETRY_MS = 50;
4642
4856
  function neatHome() {
4643
4857
  const override = process.env.NEAT_HOME;
4644
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
4645
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4858
+ if (override && override.length > 0) return import_node_path34.default.resolve(override);
4859
+ return import_node_path34.default.join(import_node_os2.default.homedir(), ".neat");
4646
4860
  }
4647
4861
  function registryPath() {
4648
- return import_node_path33.default.join(neatHome(), "projects.json");
4862
+ return import_node_path34.default.join(neatHome(), "projects.json");
4649
4863
  }
4650
4864
  function registryLockPath() {
4651
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
4865
+ return import_node_path34.default.join(neatHome(), "projects.json.lock");
4652
4866
  }
4653
4867
  async function normalizeProjectPath(input) {
4654
- const resolved = import_node_path33.default.resolve(input);
4868
+ const resolved = import_node_path34.default.resolve(input);
4655
4869
  try {
4656
- return await import_node_fs20.promises.realpath(resolved);
4870
+ return await import_node_fs21.promises.realpath(resolved);
4657
4871
  } catch {
4658
4872
  return resolved;
4659
4873
  }
4660
4874
  }
4661
4875
  async function writeAtomically(target, contents) {
4662
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4876
+ await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(target), { recursive: true });
4663
4877
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4664
- const fd = await import_node_fs20.promises.open(tmp, "w");
4878
+ const fd = await import_node_fs21.promises.open(tmp, "w");
4665
4879
  try {
4666
4880
  await fd.writeFile(contents, "utf8");
4667
4881
  await fd.sync();
4668
4882
  } finally {
4669
4883
  await fd.close();
4670
4884
  }
4671
- await import_node_fs20.promises.rename(tmp, target);
4885
+ await import_node_fs21.promises.rename(tmp, target);
4672
4886
  }
4673
4887
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4674
4888
  const deadline = Date.now() + timeoutMs;
4675
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4889
+ await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(lockPath), { recursive: true });
4676
4890
  while (true) {
4677
4891
  try {
4678
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
4892
+ const fd = await import_node_fs21.promises.open(lockPath, "wx");
4679
4893
  await fd.close();
4680
4894
  return;
4681
4895
  } catch (err) {
@@ -4691,7 +4905,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4691
4905
  }
4692
4906
  }
4693
4907
  async function releaseLock(lockPath) {
4694
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
4908
+ await import_node_fs21.promises.unlink(lockPath).catch(() => {
4695
4909
  });
4696
4910
  }
4697
4911
  async function withLock(fn) {
@@ -4707,7 +4921,7 @@ async function readRegistry() {
4707
4921
  const file = registryPath();
4708
4922
  let raw;
4709
4923
  try {
4710
- raw = await import_node_fs20.promises.readFile(file, "utf8");
4924
+ raw = await import_node_fs21.promises.readFile(file, "utf8");
4711
4925
  } catch (err) {
4712
4926
  if (err.code === "ENOENT") {
4713
4927
  return { version: 1, projects: [] };
@@ -4715,10 +4929,10 @@ async function readRegistry() {
4715
4929
  throw err;
4716
4930
  }
4717
4931
  const parsed = JSON.parse(raw);
4718
- return import_types21.RegistryFileSchema.parse(parsed);
4932
+ return import_types22.RegistryFileSchema.parse(parsed);
4719
4933
  }
4720
4934
  async function writeRegistry(reg) {
4721
- const validated = import_types21.RegistryFileSchema.parse(reg);
4935
+ const validated = import_types22.RegistryFileSchema.parse(reg);
4722
4936
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4723
4937
  }
4724
4938
  var ProjectNameCollisionError = class extends Error {
@@ -4846,6 +5060,7 @@ data: ${JSON.stringify(envelope.payload)}
4846
5060
  }
4847
5061
 
4848
5062
  // src/api.ts
5063
+ init_auth();
4849
5064
  function serializeGraph(graph) {
4850
5065
  const nodes = [];
4851
5066
  graph.forEachNode((_id, attrs) => {
@@ -4969,11 +5184,11 @@ function registerRoutes(scope, ctx) {
4969
5184
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4970
5185
  const parsed = [];
4971
5186
  for (const c of candidates) {
4972
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5187
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
4973
5188
  if (!r.success) {
4974
5189
  return reply.code(400).send({
4975
5190
  error: `unknown divergence type "${c}"`,
4976
- allowed: import_types22.DivergenceTypeSchema.options
5191
+ allowed: import_types23.DivergenceTypeSchema.options
4977
5192
  });
4978
5193
  }
4979
5194
  parsed.push(r.data);
@@ -5157,7 +5372,7 @@ function registerRoutes(scope, ctx) {
5157
5372
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5158
5373
  let violations = await log.readAll();
5159
5374
  if (req.query.severity) {
5160
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5375
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5161
5376
  if (!sev.success) {
5162
5377
  return reply.code(400).send({
5163
5378
  error: "invalid severity",
@@ -5174,7 +5389,7 @@ function registerRoutes(scope, ctx) {
5174
5389
  scope.post("/policies/check", async (req, reply) => {
5175
5390
  const proj = resolveProject(registry, req, reply);
5176
5391
  if (!proj) return;
5177
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5392
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5178
5393
  if (!parsed.success) {
5179
5394
  return reply.code(400).send({
5180
5395
  error: "invalid /policies/check body",
@@ -5211,6 +5426,7 @@ function registerRoutes(scope, ctx) {
5211
5426
  async function buildApi(opts) {
5212
5427
  const app = (0, import_fastify.default)({ logger: false });
5213
5428
  await app.register(import_cors.default, { origin: true });
5429
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5214
5430
  const startedAt = opts.startedAt ?? Date.now();
5215
5431
  const registry = buildLegacyRegistry(opts);
5216
5432
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5278,9 +5494,9 @@ init_otel_grpc();
5278
5494
 
5279
5495
  // src/search.ts
5280
5496
  init_cjs_shims();
5281
- var import_node_fs21 = require("fs");
5282
- var import_node_path36 = __toESM(require("path"), 1);
5283
- var import_node_crypto = require("crypto");
5497
+ var import_node_fs22 = require("fs");
5498
+ var import_node_path37 = __toESM(require("path"), 1);
5499
+ var import_node_crypto3 = require("crypto");
5284
5500
  var DEFAULT_LIMIT = 10;
5285
5501
  var NOMIC_DIM = 768;
5286
5502
  var MINI_LM_DIM = 384;
@@ -5320,7 +5536,7 @@ function embedText(node) {
5320
5536
  return parts.join(" ");
5321
5537
  }
5322
5538
  function attrsHash(node) {
5323
- return (0, import_node_crypto.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5539
+ return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5324
5540
  }
5325
5541
  function cosine(a, b) {
5326
5542
  if (a.length !== b.length) return 0;
@@ -5409,7 +5625,7 @@ async function pickEmbedder() {
5409
5625
  }
5410
5626
  async function readCache(cachePath) {
5411
5627
  try {
5412
- const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
5628
+ const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
5413
5629
  const parsed = JSON.parse(raw);
5414
5630
  if (parsed.version !== 1) return null;
5415
5631
  return parsed;
@@ -5418,8 +5634,8 @@ async function readCache(cachePath) {
5418
5634
  }
5419
5635
  }
5420
5636
  async function writeCache(cachePath, cache) {
5421
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
5422
- await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
5637
+ await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(cachePath), { recursive: true });
5638
+ await import_node_fs22.promises.writeFile(cachePath, JSON.stringify(cache));
5423
5639
  }
5424
5640
  var VectorIndex = class {
5425
5641
  constructor(embedder, cachePath) {
@@ -5574,8 +5790,8 @@ var ALL_PHASES = [
5574
5790
  ];
5575
5791
  function classifyChange(relPath) {
5576
5792
  const phases = /* @__PURE__ */ new Set();
5577
- const base = import_node_path37.default.basename(relPath).toLowerCase();
5578
- const segments = relPath.split(import_node_path37.default.sep).map((s) => s.toLowerCase());
5793
+ const base = import_node_path38.default.basename(relPath).toLowerCase();
5794
+ const segments = relPath.split(import_node_path38.default.sep).map((s) => s.toLowerCase());
5579
5795
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
5580
5796
  phases.add("services");
5581
5797
  phases.add("aliases");
@@ -5670,16 +5886,16 @@ function countWatchableDirs(scanPath, limit) {
5670
5886
  if (count >= limit) return;
5671
5887
  let entries;
5672
5888
  try {
5673
- entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
5889
+ entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
5674
5890
  } catch {
5675
5891
  return;
5676
5892
  }
5677
5893
  for (const e of entries) {
5678
5894
  if (count >= limit) return;
5679
5895
  if (!e.isDirectory()) continue;
5680
- if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path37.default.join(dir, e.name) + import_node_path37.default.sep))) continue;
5896
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path38.default.join(dir, e.name) + import_node_path38.default.sep))) continue;
5681
5897
  count++;
5682
- if (depth < 2) visit(import_node_path37.default.join(dir, e.name), depth + 1);
5898
+ if (depth < 2) visit(import_node_path38.default.join(dir, e.name), depth + 1);
5683
5899
  }
5684
5900
  };
5685
5901
  visit(scanPath, 0);
@@ -5697,8 +5913,8 @@ async function startWatch(graph, opts) {
5697
5913
  const projectName = opts.project ?? DEFAULT_PROJECT;
5698
5914
  await loadGraphFromDisk(graph, opts.outPath);
5699
5915
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
5700
- const policyFilePath = import_node_path37.default.join(opts.scanPath, "policy.json");
5701
- const policyViolationsPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.outPath), "policy-violations.ndjson");
5916
+ const policyFilePath = import_node_path38.default.join(opts.scanPath, "policy.json");
5917
+ const policyViolationsPath = import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "policy-violations.ndjson");
5702
5918
  let policies = [];
5703
5919
  try {
5704
5920
  policies = await loadPolicyFile(policyFilePath);
@@ -5747,7 +5963,7 @@ async function startWatch(graph, opts) {
5747
5963
  const host = opts.host ?? "0.0.0.0";
5748
5964
  const port = opts.port ?? 8080;
5749
5965
  const otelPort = opts.otelPort ?? 4318;
5750
- const cachePath = opts.embeddingsCachePath ?? import_node_path37.default.join(import_node_path37.default.dirname(opts.outPath), "embeddings.json");
5966
+ const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
5751
5967
  let searchIndex;
5752
5968
  try {
5753
5969
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -5765,7 +5981,7 @@ async function startWatch(graph, opts) {
5765
5981
  // Paths are derived from the explicit options the watch caller passes
5766
5982
  // — pathsForProject is only used to fill in the embeddings/snapshot
5767
5983
  // fields so the registry shape is complete.
5768
- ...pathsForProject(projectName, import_node_path37.default.dirname(opts.outPath)),
5984
+ ...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
5769
5985
  snapshotPath: opts.outPath,
5770
5986
  errorsPath: opts.errorsPath,
5771
5987
  staleEventsPath: opts.staleEventsPath
@@ -5850,9 +6066,9 @@ async function startWatch(graph, opts) {
5850
6066
  };
5851
6067
  const onPath = (absPath) => {
5852
6068
  if (shouldIgnore(absPath)) return;
5853
- const rel = import_node_path37.default.relative(opts.scanPath, absPath);
6069
+ const rel = import_node_path38.default.relative(opts.scanPath, absPath);
5854
6070
  if (!rel || rel.startsWith("..")) return;
5855
- pendingPaths.add(rel.split(import_node_path37.default.sep).join("/"));
6071
+ pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
5856
6072
  const phases = classifyChange(rel);
5857
6073
  if (phases.size === 0) {
5858
6074
  for (const p of ALL_PHASES) pending.add(p);
@@ -5904,13 +6120,158 @@ async function startWatch(graph, opts) {
5904
6120
  return { api, stop };
5905
6121
  }
5906
6122
 
6123
+ // src/deploy/detect.ts
6124
+ init_cjs_shims();
6125
+ var import_node_fs24 = require("fs");
6126
+ var import_node_path39 = __toESM(require("path"), 1);
6127
+ var import_node_child_process = require("child_process");
6128
+ var import_node_crypto4 = require("crypto");
6129
+ function generateToken() {
6130
+ return (0, import_node_crypto4.randomBytes)(32).toString("base64url");
6131
+ }
6132
+ async function probeBinary(binary, arg) {
6133
+ return new Promise((resolve) => {
6134
+ const child = (0, import_node_child_process.spawn)(binary, [arg], { stdio: "ignore" });
6135
+ const timer = setTimeout(() => {
6136
+ child.kill("SIGKILL");
6137
+ resolve(false);
6138
+ }, 2e3);
6139
+ child.once("error", () => {
6140
+ clearTimeout(timer);
6141
+ resolve(false);
6142
+ });
6143
+ child.once("exit", (code) => {
6144
+ clearTimeout(timer);
6145
+ resolve(code === 0);
6146
+ });
6147
+ });
6148
+ }
6149
+ async function detectSubstrate(opts = {}) {
6150
+ const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
6151
+ const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
6152
+ if (await hasDocker()) return "docker-compose";
6153
+ if (await hasSystemd()) return "systemd";
6154
+ return "docker-run";
6155
+ }
6156
+ var IMAGE = "ghcr.io/neat-technologies/neat:latest";
6157
+ function emitDockerCompose(cwd) {
6158
+ return [
6159
+ "services:",
6160
+ " neat:",
6161
+ ` image: ${IMAGE}`,
6162
+ " restart: unless-stopped",
6163
+ " environment:",
6164
+ " NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
6165
+ " ports:",
6166
+ ' - "8080:8080"',
6167
+ ' - "4318:4318"',
6168
+ ' - "6328:6328"',
6169
+ " volumes:",
6170
+ ` - ${cwd}:/workspace`,
6171
+ " - ./neat-data:/neat-out",
6172
+ ""
6173
+ ].join("\n");
6174
+ }
6175
+ function emitSystemdUnit(cwd) {
6176
+ return [
6177
+ "# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
6178
+ "# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
6179
+ "[Unit]",
6180
+ "Description=NEAT daemon",
6181
+ "After=network-online.target",
6182
+ "Wants=network-online.target",
6183
+ "",
6184
+ "[Service]",
6185
+ "Type=simple",
6186
+ "ExecStart=/usr/local/bin/neatd start --foreground",
6187
+ `WorkingDirectory=${cwd}`,
6188
+ "EnvironmentFile=/etc/neat/neatd.env",
6189
+ "Restart=always",
6190
+ "RestartSec=5",
6191
+ "",
6192
+ "[Install]",
6193
+ "WantedBy=multi-user.target",
6194
+ ""
6195
+ ].join("\n");
6196
+ }
6197
+ function emitDockerRunSnippet() {
6198
+ return [
6199
+ "#!/usr/bin/env bash",
6200
+ "set -euo pipefail",
6201
+ "",
6202
+ "# Generate a fresh token once, then store it where your secrets live.",
6203
+ ': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
6204
+ "",
6205
+ `docker run -d --name neat \\`,
6206
+ ' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
6207
+ " -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
6208
+ ' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
6209
+ ` ${IMAGE}`,
6210
+ ""
6211
+ ].join("\n");
6212
+ }
6213
+ function renderOtelEnvBlock2(token, host = "<host>") {
6214
+ return [
6215
+ `OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
6216
+ `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
6217
+ "OTEL_SERVICE_NAME=<service>"
6218
+ ].join("\n");
6219
+ }
6220
+ async function runDeploy(opts = {}) {
6221
+ const cwd = opts.cwd ?? process.cwd();
6222
+ const substrate = await detectSubstrate(opts);
6223
+ const token = generateToken();
6224
+ switch (substrate) {
6225
+ case "docker-compose": {
6226
+ const artifactPath = import_node_path39.default.join(cwd, "docker-compose.neat.yml");
6227
+ const contents = emitDockerCompose(cwd);
6228
+ await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
6229
+ return {
6230
+ substrate,
6231
+ artifactPath,
6232
+ token,
6233
+ contents,
6234
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path39.default.basename(artifactPath)} up -d`
6235
+ };
6236
+ }
6237
+ case "systemd": {
6238
+ const artifactPath = import_node_path39.default.join(cwd, "neat.service");
6239
+ const contents = emitSystemdUnit(cwd);
6240
+ await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
6241
+ return {
6242
+ substrate,
6243
+ artifactPath,
6244
+ token,
6245
+ contents,
6246
+ startCommand: [
6247
+ `sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
6248
+ `sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
6249
+ "sudo systemctl daemon-reload && sudo systemctl enable --now neat"
6250
+ ].join(" && ")
6251
+ };
6252
+ }
6253
+ case "docker-run":
6254
+ default: {
6255
+ const contents = emitDockerRunSnippet();
6256
+ return {
6257
+ substrate: "docker-run",
6258
+ token,
6259
+ contents,
6260
+ startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
6261
+ ${contents}EOF
6262
+ )`
6263
+ };
6264
+ }
6265
+ }
6266
+ }
6267
+
5907
6268
  // src/installers/index.ts
5908
6269
  init_cjs_shims();
5909
6270
 
5910
6271
  // src/installers/javascript.ts
5911
6272
  init_cjs_shims();
5912
- var import_node_fs23 = require("fs");
5913
- var import_node_path38 = __toESM(require("path"), 1);
6273
+ var import_node_fs25 = require("fs");
6274
+ var import_node_path40 = __toESM(require("path"), 1);
5914
6275
 
5915
6276
  // src/installers/templates.ts
5916
6277
  init_cjs_shims();
@@ -5958,6 +6319,57 @@ function renderEnvNeat(serviceName) {
5958
6319
  ""
5959
6320
  ].join("\n");
5960
6321
  }
6322
+ var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
6323
+ var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
6324
+ export async function register() {
6325
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
6326
+ await import('./instrumentation.node')
6327
+ }
6328
+ }
6329
+ `;
6330
+ var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
6331
+ export async function register() {
6332
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
6333
+ await import('./instrumentation.node')
6334
+ }
6335
+ }
6336
+ `;
6337
+ var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6338
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6339
+ // the OTel SDK starts so the exporter target and service name are in scope.
6340
+ import { fileURLToPath } from 'node:url'
6341
+ import path from 'node:path'
6342
+ import dotenv from 'dotenv'
6343
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6344
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6345
+
6346
+ const here = path.dirname(fileURLToPath(import.meta.url))
6347
+ dotenv.config({ path: path.join(here, '.env.neat') })
6348
+
6349
+ const sdk = new NodeSDK({
6350
+ serviceName: process.env.OTEL_SERVICE_NAME,
6351
+ instrumentations: [getNodeAutoInstrumentations()],
6352
+ })
6353
+ sdk.start()
6354
+ `;
6355
+ var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6356
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6357
+ // the OTel SDK starts so the exporter target and service name are in scope.
6358
+ import { fileURLToPath } from 'node:url'
6359
+ import path from 'node:path'
6360
+ import dotenv from 'dotenv'
6361
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6362
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6363
+
6364
+ const here = path.dirname(fileURLToPath(import.meta.url))
6365
+ dotenv.config({ path: path.join(here, '.env.neat') })
6366
+
6367
+ const sdk = new NodeSDK({
6368
+ serviceName: process.env.OTEL_SERVICE_NAME,
6369
+ instrumentations: [getNodeAutoInstrumentations()],
6370
+ })
6371
+ sdk.start()
6372
+ `;
5961
6373
 
5962
6374
  // src/installers/javascript.ts
5963
6375
  var SDK_PACKAGES = [
@@ -5980,7 +6392,7 @@ var OTEL_ENV = {
5980
6392
  };
5981
6393
  async function readPackageJson(serviceDir) {
5982
6394
  try {
5983
- const raw = await import_node_fs23.promises.readFile(import_node_path38.default.join(serviceDir, "package.json"), "utf8");
6395
+ const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
5984
6396
  return JSON.parse(raw);
5985
6397
  } catch {
5986
6398
  return null;
@@ -5988,7 +6400,7 @@ async function readPackageJson(serviceDir) {
5988
6400
  }
5989
6401
  async function exists2(p) {
5990
6402
  try {
5991
- await import_node_fs23.promises.stat(p);
6403
+ await import_node_fs25.promises.stat(p);
5992
6404
  return true;
5993
6405
  } catch {
5994
6406
  return false;
@@ -5998,10 +6410,73 @@ async function detect(serviceDir) {
5998
6410
  const pkg = await readPackageJson(serviceDir);
5999
6411
  return pkg !== null && typeof pkg.name === "string";
6000
6412
  }
6001
- var INDEX_CANDIDATES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"];
6413
+ var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
6414
+ async function findNextConfig(serviceDir) {
6415
+ for (const name of NEXT_CONFIG_CANDIDATES) {
6416
+ const candidate = import_node_path40.default.join(serviceDir, name);
6417
+ if (await exists2(candidate)) return candidate;
6418
+ }
6419
+ return null;
6420
+ }
6421
+ function hasNextDependency(pkg) {
6422
+ return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
6423
+ }
6424
+ function parseNextMajor(range) {
6425
+ if (!range) return null;
6426
+ const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
6427
+ const match = cleaned.match(/^(\d+)/);
6428
+ if (!match) return null;
6429
+ const n = Number(match[1]);
6430
+ return Number.isFinite(n) ? n : null;
6431
+ }
6432
+ async function isTypeScriptProject(serviceDir) {
6433
+ return exists2(import_node_path40.default.join(serviceDir, "tsconfig.json"));
6434
+ }
6435
+ var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
6436
+ var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
6437
+ var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
6438
+ var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
6439
+ (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
6440
+ );
6441
+ var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
6442
+ "node",
6443
+ "ts-node",
6444
+ "tsx",
6445
+ "ts-node-dev",
6446
+ "nodemon",
6447
+ "npx",
6448
+ "pnpm",
6449
+ "yarn",
6450
+ "npm",
6451
+ "cross-env",
6452
+ "dotenv",
6453
+ "--"
6454
+ ]);
6455
+ function looksLikeEntryPath(token) {
6456
+ if (token.length === 0) return false;
6457
+ if (token.startsWith("-")) return false;
6458
+ if (token.includes("=")) return false;
6459
+ if (token.includes("/")) return true;
6460
+ return /\.(?:m?[jt]sx?|c[jt]s)$/.test(token);
6461
+ }
6462
+ function scriptHasShellChain(script) {
6463
+ return /(?:&&|\|\||;|\|(?!\|))/.test(script);
6464
+ }
6465
+ function entryFromScript(script) {
6466
+ if (!script) return void 0;
6467
+ if (scriptHasShellChain(script)) return void 0;
6468
+ const tokens = script.split(/\s+/).filter((t) => t.length > 0);
6469
+ for (const token of tokens) {
6470
+ const lower = token.toLowerCase();
6471
+ if (SCRIPT_LAUNCHERS.has(lower)) continue;
6472
+ const cleaned = token.startsWith("./") ? token.slice(2) : token;
6473
+ if (looksLikeEntryPath(cleaned)) return cleaned;
6474
+ }
6475
+ return void 0;
6476
+ }
6002
6477
  async function resolveEntry(serviceDir, pkg) {
6003
6478
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
6004
- const candidate = import_node_path38.default.resolve(serviceDir, pkg.main);
6479
+ const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
6005
6480
  if (await exists2(candidate)) return candidate;
6006
6481
  }
6007
6482
  if (pkg.bin) {
@@ -6015,18 +6490,36 @@ async function resolveEntry(serviceDir, pkg) {
6015
6490
  if (typeof first === "string") binEntry = first;
6016
6491
  }
6017
6492
  if (binEntry) {
6018
- const candidate = import_node_path38.default.resolve(serviceDir, binEntry);
6493
+ const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
6019
6494
  if (await exists2(candidate)) return candidate;
6020
6495
  }
6021
6496
  }
6497
+ const startEntry = entryFromScript(pkg.scripts?.start);
6498
+ if (startEntry) {
6499
+ const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
6500
+ if (await exists2(candidate)) return candidate;
6501
+ }
6502
+ const devEntry = entryFromScript(pkg.scripts?.dev);
6503
+ if (devEntry) {
6504
+ const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
6505
+ if (await exists2(candidate)) return candidate;
6506
+ }
6507
+ for (const rel of SRC_INDEX_CANDIDATES) {
6508
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6509
+ if (await exists2(candidate)) return candidate;
6510
+ }
6511
+ for (const rel of SRC_NAMED_CANDIDATES) {
6512
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6513
+ if (await exists2(candidate)) return candidate;
6514
+ }
6022
6515
  for (const name of INDEX_CANDIDATES) {
6023
- const candidate = import_node_path38.default.join(serviceDir, name);
6516
+ const candidate = import_node_path40.default.join(serviceDir, name);
6024
6517
  if (await exists2(candidate)) return candidate;
6025
6518
  }
6026
6519
  return null;
6027
6520
  }
6028
6521
  function dispatchEntry(entryFile, pkg) {
6029
- const ext = import_node_path38.default.extname(entryFile).toLowerCase();
6522
+ const ext = import_node_path40.default.extname(entryFile).toLowerCase();
6030
6523
  if (ext === ".ts" || ext === ".tsx") return "ts";
6031
6524
  if (ext === ".mjs") return "esm";
6032
6525
  if (ext === ".cjs") return "cjs";
@@ -6043,9 +6536,9 @@ function otelInitContents(flavor) {
6043
6536
  return OTEL_INIT_CJS;
6044
6537
  }
6045
6538
  function injectionLine(flavor, entryFile, otelInitFile) {
6046
- let rel = import_node_path38.default.relative(import_node_path38.default.dirname(entryFile), otelInitFile);
6539
+ let rel = import_node_path40.default.relative(import_node_path40.default.dirname(entryFile), otelInitFile);
6047
6540
  if (!rel.startsWith(".")) rel = `./${rel}`;
6048
- rel = rel.split(import_node_path38.default.sep).join("/");
6541
+ rel = rel.split(import_node_path40.default.sep).join("/");
6049
6542
  if (flavor === "cjs") return `require('${rel}')`;
6050
6543
  if (flavor === "esm") return `import '${rel}'`;
6051
6544
  const tsRel = rel.replace(/\.ts$/, "");
@@ -6056,9 +6549,88 @@ function lineIsOtelInjection(line) {
6056
6549
  if (trimmed.length === 0) return false;
6057
6550
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6058
6551
  }
6552
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6553
+ const useTs = await isTypeScriptProject(serviceDir);
6554
+ const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6555
+ const instrumentationNodeFile = import_node_path40.default.join(
6556
+ serviceDir,
6557
+ useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
6558
+ );
6559
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6560
+ const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6561
+ const dependencyEdits = [];
6562
+ for (const sdk of SDK_PACKAGES) {
6563
+ if (sdk.name in existingDeps) continue;
6564
+ dependencyEdits.push({
6565
+ file: manifestPath,
6566
+ kind: "add",
6567
+ name: sdk.name,
6568
+ version: sdk.version
6569
+ });
6570
+ }
6571
+ const generatedFiles = [];
6572
+ if (!await exists2(instrumentationFile)) {
6573
+ generatedFiles.push({
6574
+ file: instrumentationFile,
6575
+ contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
6576
+ skipIfExists: true
6577
+ });
6578
+ }
6579
+ if (!await exists2(instrumentationNodeFile)) {
6580
+ generatedFiles.push({
6581
+ file: instrumentationNodeFile,
6582
+ contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
6583
+ skipIfExists: true
6584
+ });
6585
+ }
6586
+ if (!await exists2(envNeatFile)) {
6587
+ generatedFiles.push({
6588
+ file: envNeatFile,
6589
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6590
+ skipIfExists: true
6591
+ });
6592
+ }
6593
+ let nextConfigEdit;
6594
+ const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
6595
+ const nextMajor = parseNextMajor(nextRange);
6596
+ if (nextMajor !== null && nextMajor < 15) {
6597
+ try {
6598
+ const raw = await import_node_fs25.promises.readFile(nextConfigPath, "utf8");
6599
+ if (!raw.includes("instrumentationHook")) {
6600
+ nextConfigEdit = {
6601
+ file: nextConfigPath,
6602
+ reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
6603
+ };
6604
+ }
6605
+ } catch {
6606
+ }
6607
+ }
6608
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
6609
+ if (empty) {
6610
+ return {
6611
+ language: "javascript",
6612
+ serviceDir,
6613
+ dependencyEdits: [],
6614
+ entrypointEdits: [],
6615
+ envEdits: [],
6616
+ generatedFiles: [],
6617
+ framework: "next"
6618
+ };
6619
+ }
6620
+ return {
6621
+ language: "javascript",
6622
+ serviceDir,
6623
+ dependencyEdits,
6624
+ entrypointEdits: [],
6625
+ envEdits: [OTEL_ENV],
6626
+ generatedFiles,
6627
+ framework: "next",
6628
+ ...nextConfigEdit ? { nextConfigEdit } : {}
6629
+ };
6630
+ }
6059
6631
  async function plan(serviceDir) {
6060
6632
  const pkg = await readPackageJson(serviceDir);
6061
- const manifestPath = import_node_path38.default.join(serviceDir, "package.json");
6633
+ const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
6062
6634
  const empty = {
6063
6635
  language: "javascript",
6064
6636
  serviceDir,
@@ -6068,13 +6640,19 @@ async function plan(serviceDir) {
6068
6640
  generatedFiles: []
6069
6641
  };
6070
6642
  if (!pkg) return empty;
6643
+ if (hasNextDependency(pkg)) {
6644
+ const nextConfig = await findNextConfig(serviceDir);
6645
+ if (nextConfig) {
6646
+ return planNext(serviceDir, pkg, manifestPath, nextConfig);
6647
+ }
6648
+ }
6071
6649
  const entryFile = await resolveEntry(serviceDir, pkg);
6072
6650
  if (!entryFile) {
6073
6651
  return { ...empty, libOnly: true };
6074
6652
  }
6075
6653
  const flavor = dispatchEntry(entryFile, pkg);
6076
- const otelInitFile = import_node_path38.default.join(import_node_path38.default.dirname(entryFile), otelInitFilename(flavor));
6077
- const envNeatFile = import_node_path38.default.join(serviceDir, ".env.neat");
6654
+ const otelInitFile = import_node_path40.default.join(import_node_path40.default.dirname(entryFile), otelInitFilename(flavor));
6655
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6078
6656
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6079
6657
  const dependencyEdits = [];
6080
6658
  for (const sdk of SDK_PACKAGES) {
@@ -6088,7 +6666,7 @@ async function plan(serviceDir) {
6088
6666
  }
6089
6667
  const entrypointEdits = [];
6090
6668
  try {
6091
- const raw = await import_node_fs23.promises.readFile(entryFile, "utf8");
6669
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
6092
6670
  const lines = raw.split(/\r?\n/);
6093
6671
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6094
6672
  if (!lineIsOtelInjection(firstReal)) {
@@ -6113,7 +6691,7 @@ async function plan(serviceDir) {
6113
6691
  if (!await exists2(envNeatFile)) {
6114
6692
  generatedFiles.push({
6115
6693
  file: envNeatFile,
6116
- contents: renderEnvNeat(pkg.name ?? import_node_path38.default.basename(serviceDir)),
6694
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6117
6695
  skipIfExists: true
6118
6696
  });
6119
6697
  }
@@ -6132,18 +6710,20 @@ async function plan(serviceDir) {
6132
6710
  };
6133
6711
  }
6134
6712
  function isAllowedWritePath(serviceDir, target) {
6135
- const rel = import_node_path38.default.relative(serviceDir, target);
6713
+ const rel = import_node_path40.default.relative(serviceDir, target);
6136
6714
  if (rel.startsWith("..")) return false;
6137
- const base = import_node_path38.default.basename(target);
6715
+ const base = import_node_path40.default.basename(target);
6138
6716
  if (base === "package.json") return true;
6139
6717
  if (base === ".env.neat") return true;
6140
6718
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6719
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6720
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
6141
6721
  return false;
6142
6722
  }
6143
6723
  async function writeAtomic(file, contents) {
6144
6724
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6145
- await import_node_fs23.promises.writeFile(tmp, contents, "utf8");
6146
- await import_node_fs23.promises.rename(tmp, file);
6725
+ await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
6726
+ await import_node_fs25.promises.rename(tmp, file);
6147
6727
  }
6148
6728
  async function apply(installPlan) {
6149
6729
  const { serviceDir } = installPlan;
@@ -6155,7 +6735,7 @@ async function apply(installPlan) {
6155
6735
  writtenFiles: []
6156
6736
  };
6157
6737
  }
6158
- if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
6738
+ if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
6159
6739
  return {
6160
6740
  serviceDir,
6161
6741
  outcome: "already-instrumented",
@@ -6166,6 +6746,7 @@ async function apply(installPlan) {
6166
6746
  for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
6167
6747
  for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
6168
6748
  for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
6749
+ if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
6169
6750
  for (const target of allTargets) {
6170
6751
  const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
6171
6752
  if (isEntryEdit) continue;
@@ -6180,7 +6761,7 @@ async function apply(installPlan) {
6180
6761
  for (const target of allTargets) {
6181
6762
  if (await exists2(target)) {
6182
6763
  try {
6183
- originals.set(target, await import_node_fs23.promises.readFile(target, "utf8"));
6764
+ originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
6184
6765
  } catch {
6185
6766
  }
6186
6767
  }
@@ -6235,6 +6816,17 @@ async function apply(installPlan) {
6235
6816
  await writeAtomic(ep.file, newRaw);
6236
6817
  writtenFiles.push(ep.file);
6237
6818
  }
6819
+ if (installPlan.nextConfigEdit) {
6820
+ const target = installPlan.nextConfigEdit.file;
6821
+ const raw = originals.get(target);
6822
+ if (raw !== void 0 && !raw.includes("instrumentationHook")) {
6823
+ const updated = injectInstrumentationHook(raw);
6824
+ if (updated !== null) {
6825
+ await writeAtomic(target, updated);
6826
+ writtenFiles.push(target);
6827
+ }
6828
+ }
6829
+ }
6238
6830
  } catch (err) {
6239
6831
  await rollback(installPlan, originals, createdFiles);
6240
6832
  throw err;
@@ -6250,14 +6842,14 @@ async function rollback(installPlan, originals, createdFiles) {
6250
6842
  const removed = [];
6251
6843
  for (const [file, raw] of originals.entries()) {
6252
6844
  try {
6253
- await import_node_fs23.promises.writeFile(file, raw, "utf8");
6845
+ await import_node_fs25.promises.writeFile(file, raw, "utf8");
6254
6846
  restored.push(file);
6255
6847
  } catch {
6256
6848
  }
6257
6849
  }
6258
6850
  for (const file of createdFiles) {
6259
6851
  try {
6260
- await import_node_fs23.promises.unlink(file);
6852
+ await import_node_fs25.promises.unlink(file);
6261
6853
  removed.push(file);
6262
6854
  } catch {
6263
6855
  }
@@ -6272,8 +6864,26 @@ async function rollback(installPlan, originals, createdFiles) {
6272
6864
  ...removed.map((f) => `removed: ${f}`),
6273
6865
  ""
6274
6866
  ];
6275
- const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
6276
- await import_node_fs23.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6867
+ const rollbackPath = import_node_path40.default.join(installPlan.serviceDir, "neat-rollback.patch");
6868
+ await import_node_fs25.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6869
+ }
6870
+ function injectInstrumentationHook(raw) {
6871
+ if (raw.includes("instrumentationHook")) return raw;
6872
+ const anchors = [
6873
+ { pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
6874
+ { pattern: /(export\s+default\s*\{)/, label: "esm-default" },
6875
+ { pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
6876
+ ];
6877
+ for (const { pattern } of anchors) {
6878
+ const match = pattern.exec(raw);
6879
+ if (!match) continue;
6880
+ const insertAfter = match.index + match[0].length;
6881
+ const before = raw.slice(0, insertAfter);
6882
+ const after = raw.slice(insertAfter);
6883
+ const injection = "\n experimental: { instrumentationHook: true },";
6884
+ return `${before}${injection}${after}`;
6885
+ }
6886
+ return null;
6277
6887
  }
6278
6888
  var javascriptInstaller = {
6279
6889
  name: "javascript",
@@ -6284,8 +6894,8 @@ var javascriptInstaller = {
6284
6894
 
6285
6895
  // src/installers/python.ts
6286
6896
  init_cjs_shims();
6287
- var import_node_fs24 = require("fs");
6288
- var import_node_path39 = __toESM(require("path"), 1);
6897
+ var import_node_fs26 = require("fs");
6898
+ var import_node_path41 = __toESM(require("path"), 1);
6289
6899
  var SDK_PACKAGES2 = [
6290
6900
  { name: "opentelemetry-distro", version: ">=0.49b0" },
6291
6901
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -6297,7 +6907,7 @@ var OTEL_ENV2 = {
6297
6907
  };
6298
6908
  async function exists3(p) {
6299
6909
  try {
6300
- await import_node_fs24.promises.stat(p);
6910
+ await import_node_fs26.promises.stat(p);
6301
6911
  return true;
6302
6912
  } catch {
6303
6913
  return false;
@@ -6306,7 +6916,7 @@ async function exists3(p) {
6306
6916
  async function detect2(serviceDir) {
6307
6917
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
6308
6918
  for (const m of markers) {
6309
- if (await exists3(import_node_path39.default.join(serviceDir, m))) return true;
6919
+ if (await exists3(import_node_path41.default.join(serviceDir, m))) return true;
6310
6920
  }
6311
6921
  return false;
6312
6922
  }
@@ -6316,9 +6926,9 @@ function reqPackageName(line) {
6316
6926
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
6317
6927
  }
6318
6928
  async function planRequirementsTxtEdits(serviceDir) {
6319
- const file = import_node_path39.default.join(serviceDir, "requirements.txt");
6929
+ const file = import_node_path41.default.join(serviceDir, "requirements.txt");
6320
6930
  if (!await exists3(file)) return null;
6321
- const raw = await import_node_fs24.promises.readFile(file, "utf8");
6931
+ const raw = await import_node_fs26.promises.readFile(file, "utf8");
6322
6932
  const presentNames = new Set(
6323
6933
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
6324
6934
  );
@@ -6326,9 +6936,9 @@ async function planRequirementsTxtEdits(serviceDir) {
6326
6936
  return { manifest: file, missing: [...missing] };
6327
6937
  }
6328
6938
  async function planProcfileEdits(serviceDir) {
6329
- const procfile = import_node_path39.default.join(serviceDir, "Procfile");
6939
+ const procfile = import_node_path41.default.join(serviceDir, "Procfile");
6330
6940
  if (!await exists3(procfile)) return [];
6331
- const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
6941
+ const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
6332
6942
  const edits = [];
6333
6943
  for (const line of raw.split(/\r?\n/)) {
6334
6944
  if (line.length === 0) continue;
@@ -6380,8 +6990,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
6380
6990
  const next = `${original}${trailing}${newlines.join("\n")}
6381
6991
  `;
6382
6992
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
6383
- await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6384
- await import_node_fs24.promises.rename(tmp, manifest);
6993
+ await import_node_fs26.promises.writeFile(tmp, next, "utf8");
6994
+ await import_node_fs26.promises.rename(tmp, manifest);
6385
6995
  }
6386
6996
  async function applyProcfile(procfile, edits, original) {
6387
6997
  let next = original;
@@ -6390,8 +7000,8 @@ async function applyProcfile(procfile, edits, original) {
6390
7000
  next = next.replace(e.before, e.after);
6391
7001
  }
6392
7002
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
6393
- await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6394
- await import_node_fs24.promises.rename(tmp, procfile);
7003
+ await import_node_fs26.promises.writeFile(tmp, next, "utf8");
7004
+ await import_node_fs26.promises.rename(tmp, procfile);
6395
7005
  }
6396
7006
  async function apply2(installPlan) {
6397
7007
  const { serviceDir } = installPlan;
@@ -6404,7 +7014,7 @@ async function apply2(installPlan) {
6404
7014
  const originals = /* @__PURE__ */ new Map();
6405
7015
  for (const file of touched) {
6406
7016
  try {
6407
- originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
7017
+ originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
6408
7018
  } catch {
6409
7019
  }
6410
7020
  }
@@ -6415,7 +7025,7 @@ async function apply2(installPlan) {
6415
7025
  if (raw === void 0) {
6416
7026
  throw new Error(`python installer: cannot read ${file} during apply`);
6417
7027
  }
6418
- const base = import_node_path39.default.basename(file);
7028
+ const base = import_node_path41.default.basename(file);
6419
7029
  if (base === "requirements.txt") {
6420
7030
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
6421
7031
  if (edits.length > 0) {
@@ -6440,7 +7050,7 @@ async function rollback2(installPlan, originals) {
6440
7050
  const restored = [];
6441
7051
  for (const [file, raw] of originals.entries()) {
6442
7052
  try {
6443
- await import_node_fs24.promises.writeFile(file, raw, "utf8");
7053
+ await import_node_fs26.promises.writeFile(file, raw, "utf8");
6444
7054
  restored.push(file);
6445
7055
  } catch {
6446
7056
  }
@@ -6454,8 +7064,8 @@ async function rollback2(installPlan, originals) {
6454
7064
  ...restored.map((f) => `restored: ${f}`),
6455
7065
  ""
6456
7066
  ];
6457
- const rollbackPath = import_node_path39.default.join(installPlan.serviceDir, "neat-rollback.patch");
6458
- await import_node_fs24.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
7067
+ const rollbackPath = import_node_path41.default.join(installPlan.serviceDir, "neat-rollback.patch");
7068
+ await import_node_fs26.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6459
7069
  }
6460
7070
  var pythonInstaller = {
6461
7071
  name: "python",
@@ -6467,7 +7077,7 @@ var pythonInstaller = {
6467
7077
  // src/installers/shared.ts
6468
7078
  init_cjs_shims();
6469
7079
  function isEmptyPlan(plan3) {
6470
- return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
7080
+ return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
6471
7081
  }
6472
7082
 
6473
7083
  // src/installers/index.ts
@@ -6566,16 +7176,231 @@ function renderPatch(sections) {
6566
7176
  }
6567
7177
  lines.push("");
6568
7178
  }
7179
+ if (plan3.nextConfigEdit) {
7180
+ lines.push("### next.config (framework flag)");
7181
+ lines.push(`--- ${plan3.nextConfigEdit.file}`);
7182
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
7183
+ lines.push("");
7184
+ }
6569
7185
  }
6570
7186
  return lines.join("\n");
6571
7187
  }
6572
7188
 
7189
+ // src/orchestrator.ts
7190
+ init_cjs_shims();
7191
+ var import_node_fs27 = require("fs");
7192
+ var import_node_http = __toESM(require("http"), 1);
7193
+ var import_node_path42 = __toESM(require("path"), 1);
7194
+ var import_node_child_process2 = require("child_process");
7195
+ var import_node_readline = __toESM(require("readline"), 1);
7196
+ async function promptYesNo(question) {
7197
+ const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
7198
+ return new Promise((resolve) => {
7199
+ rl.question(`${question} [Y/n] `, (answer) => {
7200
+ rl.close();
7201
+ const trimmed = answer.trim().toLowerCase();
7202
+ resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
7203
+ });
7204
+ });
7205
+ }
7206
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
7207
+ async function checkDaemonHealth(restPort) {
7208
+ return new Promise((resolve) => {
7209
+ const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
7210
+ const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
7211
+ res.resume();
7212
+ resolve(ok);
7213
+ });
7214
+ req.on("error", () => resolve(false));
7215
+ req.setTimeout(1e3, () => {
7216
+ req.destroy();
7217
+ resolve(false);
7218
+ });
7219
+ });
7220
+ }
7221
+ async function waitForDaemonReady(restPort, timeoutMs) {
7222
+ const deadline = Date.now() + timeoutMs;
7223
+ while (Date.now() < deadline) {
7224
+ if (await checkDaemonHealth(restPort)) return true;
7225
+ await new Promise((r) => setTimeout(r, 300));
7226
+ }
7227
+ return false;
7228
+ }
7229
+ function spawnDaemonDetached() {
7230
+ const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
7231
+ const candidates = [
7232
+ import_node_path42.default.join(here, "neatd.cjs"),
7233
+ import_node_path42.default.join(here, "neatd.js")
7234
+ ];
7235
+ let entry2 = null;
7236
+ const fsSync = require("fs");
7237
+ for (const c of candidates) {
7238
+ try {
7239
+ fsSync.accessSync(c);
7240
+ entry2 = c;
7241
+ break;
7242
+ } catch {
7243
+ }
7244
+ }
7245
+ if (!entry2) {
7246
+ throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
7247
+ }
7248
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
7249
+ detached: true,
7250
+ stdio: "ignore",
7251
+ env: process.env
7252
+ });
7253
+ child.unref();
7254
+ }
7255
+ function openBrowser(url) {
7256
+ if (!process.stdout.isTTY) return "failed";
7257
+ const platform = process.platform;
7258
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
7259
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
7260
+ try {
7261
+ const child = (0, import_node_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
7262
+ child.on("error", () => {
7263
+ });
7264
+ child.unref();
7265
+ return "opened";
7266
+ } catch {
7267
+ return "failed";
7268
+ }
7269
+ }
7270
+ async function runOrchestrator(opts) {
7271
+ const result = {
7272
+ exitCode: 0,
7273
+ steps: {
7274
+ discovery: { services: 0, languages: [] },
7275
+ extraction: { nodesAdded: 0, edgesAdded: 0 },
7276
+ gitignore: "unchanged",
7277
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
7278
+ daemon: "skipped",
7279
+ browser: "skipped"
7280
+ }
7281
+ };
7282
+ const stat = await import_node_fs27.promises.stat(opts.scanPath).catch(() => null);
7283
+ if (!stat || !stat.isDirectory()) {
7284
+ console.error(`neat: ${opts.scanPath} is not a directory`);
7285
+ result.exitCode = 2;
7286
+ return result;
7287
+ }
7288
+ console.log(`neat: ${opts.scanPath}`);
7289
+ console.log("");
7290
+ const services = await discoverServices(opts.scanPath);
7291
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
7292
+ result.steps.discovery = { services: services.length, languages };
7293
+ console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
7294
+ let runApply = !opts.noInstrument;
7295
+ if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
7296
+ runApply = await promptYesNo("instrument your services and open the dashboard?");
7297
+ }
7298
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
7299
+ resetGraph(graphKey);
7300
+ const graph = getGraph(graphKey);
7301
+ const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
7302
+ const errorsPath = projectPaths.errorsPath;
7303
+ const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7304
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
7305
+ result.steps.extraction = {
7306
+ nodesAdded: extraction.nodesAdded,
7307
+ edgesAdded: extraction.edgesAdded
7308
+ };
7309
+ const gi = await ensureNeatOutIgnored(opts.scanPath);
7310
+ result.steps.gitignore = gi.action;
7311
+ if (gi.action !== "unchanged") {
7312
+ console.log(`${gi.action} .gitignore (neat-out/)`);
7313
+ }
7314
+ try {
7315
+ await addProject({
7316
+ name: opts.project,
7317
+ path: opts.scanPath,
7318
+ languages,
7319
+ status: "active"
7320
+ });
7321
+ } catch (err) {
7322
+ if (!(err instanceof ProjectNameCollisionError)) throw err;
7323
+ console.error(`neat: ${err.message}`);
7324
+ console.error("pass --project <other-name> to register under a different name.");
7325
+ result.exitCode = 1;
7326
+ return result;
7327
+ }
7328
+ if (!runApply) {
7329
+ result.steps.apply.skipped = true;
7330
+ console.log("skipped instrumentation (--no-instrument)");
7331
+ } else {
7332
+ let instrumented = 0;
7333
+ let already = 0;
7334
+ let libOnly = 0;
7335
+ for (const svc of services) {
7336
+ const installer = await pickInstaller(svc.dir);
7337
+ if (!installer) continue;
7338
+ const plan3 = await installer.plan(svc.dir);
7339
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
7340
+ already++;
7341
+ continue;
7342
+ }
7343
+ const outcome = await installer.apply(plan3);
7344
+ if (outcome.outcome === "instrumented") instrumented++;
7345
+ else if (outcome.outcome === "already-instrumented") already++;
7346
+ else if (outcome.outcome === "lib-only") libOnly++;
7347
+ }
7348
+ result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
7349
+ console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
7350
+ }
7351
+ const restPort = Number(process.env.PORT ?? 8080);
7352
+ const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
7353
+ if (await checkDaemonHealth(restPort)) {
7354
+ result.steps.daemon = "already-running";
7355
+ } else {
7356
+ try {
7357
+ spawnDaemonDetached();
7358
+ } catch (err) {
7359
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
7360
+ result.exitCode = 1;
7361
+ return result;
7362
+ }
7363
+ const ready = await waitForDaemonReady(restPort, timeoutMs);
7364
+ result.steps.daemon = ready ? "spawned" : "timed-out";
7365
+ if (!ready) {
7366
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
7367
+ result.exitCode = 1;
7368
+ return result;
7369
+ }
7370
+ }
7371
+ const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
7372
+ if (opts.noOpen || !process.stdout.isTTY) {
7373
+ result.steps.browser = "skipped";
7374
+ } else {
7375
+ result.steps.browser = openBrowser(dashboardUrl);
7376
+ }
7377
+ printSummary(result, graph, dashboardUrl);
7378
+ return result;
7379
+ }
7380
+ function printSummary(result, graph, dashboardUrl) {
7381
+ const nodes = [];
7382
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
7383
+ const edges = [];
7384
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
7385
+ const byNode = /* @__PURE__ */ new Map();
7386
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
7387
+ const byEdge = /* @__PURE__ */ new Map();
7388
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
7389
+ console.log("");
7390
+ console.log("=== summary ===");
7391
+ console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
7392
+ for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
7393
+ for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
7394
+ console.log("");
7395
+ console.log(`dashboard: ${dashboardUrl}`);
7396
+ }
7397
+
6573
7398
  // src/cli.ts
6574
- var import_types24 = require("@neat.is/types");
7399
+ var import_types25 = require("@neat.is/types");
6575
7400
 
6576
7401
  // src/cli-client.ts
6577
7402
  init_cjs_shims();
6578
- var import_types23 = require("@neat.is/types");
7403
+ var import_types24 = require("@neat.is/types");
6579
7404
  var HttpError = class extends Error {
6580
7405
  constructor(status2, message, responseBody = "") {
6581
7406
  super(message);
@@ -6595,10 +7420,10 @@ var TransportError = class extends Error {
6595
7420
  function createHttpClient(baseUrl) {
6596
7421
  const root = baseUrl.replace(/\/$/, "");
6597
7422
  return {
6598
- async get(path41) {
7423
+ async get(path44) {
6599
7424
  let res;
6600
7425
  try {
6601
- res = await fetch(`${root}${path41}`);
7426
+ res = await fetch(`${root}${path44}`);
6602
7427
  } catch (err) {
6603
7428
  throw new TransportError(
6604
7429
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -6608,16 +7433,16 @@ function createHttpClient(baseUrl) {
6608
7433
  const body = await res.text().catch(() => "");
6609
7434
  throw new HttpError(
6610
7435
  res.status,
6611
- `${res.status} ${res.statusText} on GET ${path41}: ${body}`,
7436
+ `${res.status} ${res.statusText} on GET ${path44}: ${body}`,
6612
7437
  body
6613
7438
  );
6614
7439
  }
6615
7440
  return await res.json();
6616
7441
  },
6617
- async post(path41, body) {
7442
+ async post(path44, body) {
6618
7443
  let res;
6619
7444
  try {
6620
- res = await fetch(`${root}${path41}`, {
7445
+ res = await fetch(`${root}${path44}`, {
6621
7446
  method: "POST",
6622
7447
  headers: { "content-type": "application/json" },
6623
7448
  body: JSON.stringify(body)
@@ -6631,7 +7456,7 @@ function createHttpClient(baseUrl) {
6631
7456
  const text = await res.text().catch(() => "");
6632
7457
  throw new HttpError(
6633
7458
  res.status,
6634
- `${res.status} ${res.statusText} on POST ${path41}: ${text}`,
7459
+ `${res.status} ${res.statusText} on POST ${path44}: ${text}`,
6635
7460
  text
6636
7461
  );
6637
7462
  }
@@ -6645,12 +7470,12 @@ function projectPath(project, suffix) {
6645
7470
  }
6646
7471
  async function runRootCause(client, input) {
6647
7472
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
6648
- const path41 = projectPath(
7473
+ const path44 = projectPath(
6649
7474
  input.project,
6650
7475
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
6651
7476
  );
6652
7477
  try {
6653
- const result = await client.get(path41);
7478
+ const result = await client.get(path44);
6654
7479
  const arrowPath = result.traversalPath.join(" \u2190 ");
6655
7480
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
6656
7481
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -6676,12 +7501,12 @@ async function runRootCause(client, input) {
6676
7501
  }
6677
7502
  async function runBlastRadius(client, input) {
6678
7503
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
6679
- const path41 = projectPath(
7504
+ const path44 = projectPath(
6680
7505
  input.project,
6681
7506
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6682
7507
  );
6683
7508
  try {
6684
- const result = await client.get(path41);
7509
+ const result = await client.get(path44);
6685
7510
  if (result.totalAffected === 0) {
6686
7511
  return {
6687
7512
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -6710,17 +7535,17 @@ async function runBlastRadius(client, input) {
6710
7535
  }
6711
7536
  }
6712
7537
  function formatBlastEntry(n) {
6713
- const tag = n.edgeProvenance === import_types23.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
7538
+ const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
6714
7539
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
6715
7540
  }
6716
7541
  async function runDependencies(client, input) {
6717
7542
  const depth = input.depth ?? 3;
6718
- const path41 = projectPath(
7543
+ const path44 = projectPath(
6719
7544
  input.project,
6720
7545
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6721
7546
  );
6722
7547
  try {
6723
- const result = await client.get(path41);
7548
+ const result = await client.get(path44);
6724
7549
  if (result.total === 0) {
6725
7550
  return {
6726
7551
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -6756,9 +7581,9 @@ async function runObservedDependencies(client, input) {
6756
7581
  const edges = await client.get(
6757
7582
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
6758
7583
  );
6759
- const observed = edges.outbound.filter((e) => e.provenance === import_types23.Provenance.OBSERVED);
7584
+ const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
6760
7585
  if (observed.length === 0) {
6761
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types23.Provenance.EXTRACTED);
7586
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
6762
7587
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
6763
7588
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
6764
7589
  }
@@ -6766,7 +7591,7 @@ async function runObservedDependencies(client, input) {
6766
7591
  return {
6767
7592
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
6768
7593
  block: blockLines.join("\n"),
6769
- provenance: import_types23.Provenance.OBSERVED
7594
+ provenance: import_types24.Provenance.OBSERVED
6770
7595
  };
6771
7596
  } catch (err) {
6772
7597
  if (err instanceof HttpError && err.status === 404) {
@@ -6801,9 +7626,9 @@ function formatDuration(ms) {
6801
7626
  return `${Math.round(h / 24)}d`;
6802
7627
  }
6803
7628
  async function runIncidents(client, input) {
6804
- const path41 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
7629
+ const path44 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6805
7630
  try {
6806
- const body = await client.get(path41);
7631
+ const body = await client.get(path44);
6807
7632
  const events = body.events;
6808
7633
  if (events.length === 0) {
6809
7634
  return {
@@ -6820,7 +7645,7 @@ async function runIncidents(client, input) {
6820
7645
  return {
6821
7646
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6822
7647
  block: blockLines.join("\n"),
6823
- provenance: import_types23.Provenance.OBSERVED
7648
+ provenance: import_types24.Provenance.OBSERVED
6824
7649
  };
6825
7650
  } catch (err) {
6826
7651
  if (err instanceof HttpError && err.status === 404) {
@@ -6929,7 +7754,7 @@ async function runStaleEdges(client, input) {
6929
7754
  return {
6930
7755
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
6931
7756
  block: blockLines.join("\n"),
6932
- provenance: import_types23.Provenance.STALE
7757
+ provenance: import_types24.Provenance.STALE
6933
7758
  };
6934
7759
  }
6935
7760
  async function runPolicies(client, input) {
@@ -7095,6 +7920,9 @@ function usage() {
7095
7920
  console.log(" Flags:");
7096
7921
  console.log(" --print-config print the JSON snippet to stdout");
7097
7922
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
7923
+ console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
7924
+ console.log(" emit a docker-compose / systemd / docker run artifact, and");
7925
+ console.log(" print the OTel env-vars block to paste into your platform.");
7098
7926
  console.log("");
7099
7927
  console.log("query commands (mirror the MCP tools, ADR-050):");
7100
7928
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -7157,6 +7985,10 @@ function parseArgs(rest) {
7157
7985
  apply: false,
7158
7986
  dryRun: false,
7159
7987
  noInstall: false,
7988
+ noInstrument: false,
7989
+ noOpen: false,
7990
+ yes: false,
7991
+ verbose: false,
7160
7992
  printConfig: false,
7161
7993
  json: false,
7162
7994
  depth: null,
@@ -7185,6 +8017,22 @@ function parseArgs(rest) {
7185
8017
  out.noInstall = true;
7186
8018
  continue;
7187
8019
  }
8020
+ if (arg === "--no-instrument") {
8021
+ out.noInstrument = true;
8022
+ continue;
8023
+ }
8024
+ if (arg === "--no-open") {
8025
+ out.noOpen = true;
8026
+ continue;
8027
+ }
8028
+ if (arg === "--yes" || arg === "-y") {
8029
+ out.yes = true;
8030
+ continue;
8031
+ }
8032
+ if (arg === "--verbose" || arg === "-v") {
8033
+ out.verbose = true;
8034
+ continue;
8035
+ }
7188
8036
  if (arg === "--print-config") {
7189
8037
  out.printConfig = true;
7190
8038
  continue;
@@ -7240,34 +8088,6 @@ function assignFlag(out, field, value) {
7240
8088
  ;
7241
8089
  out[field] = value;
7242
8090
  }
7243
- function summarise(nodes, edges) {
7244
- const byNode = /* @__PURE__ */ new Map();
7245
- for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
7246
- const byEdge = /* @__PURE__ */ new Map();
7247
- for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
7248
- const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
7249
- const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
7250
- return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
7251
- }
7252
- function formatIncompat(inc) {
7253
- if (inc.kind === "node-engine") {
7254
- const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
7255
- return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
7256
- }
7257
- if (inc.kind === "package-conflict") {
7258
- const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
7259
- return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
7260
- }
7261
- if (inc.kind === "deprecated-api") {
7262
- return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
7263
- }
7264
- return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
7265
- }
7266
- function findIncompatibilities(nodes) {
7267
- return nodes.filter(
7268
- (n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
7269
- );
7270
- }
7271
8091
  function printBanner() {
7272
8092
  console.log("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557");
7273
8093
  console.log("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D");
@@ -7318,7 +8138,7 @@ async function buildPatchSections(services) {
7318
8138
  }
7319
8139
  async function runInit(opts) {
7320
8140
  const written = [];
7321
- const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
8141
+ const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
7322
8142
  if (!stat || !stat.isDirectory()) {
7323
8143
  console.error(`neat init: ${opts.scanPath} is not a directory`);
7324
8144
  return { exitCode: 2, writtenFiles: written };
@@ -7327,11 +8147,15 @@ async function runInit(opts) {
7327
8147
  printDiscoveryReport(opts, services);
7328
8148
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
7329
8149
  const patch = renderPatch(sections);
7330
- const patchPath = import_node_path40.default.join(opts.scanPath, "neat.patch");
8150
+ const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
7331
8151
  if (opts.dryRun) {
7332
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
8152
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7333
8153
  written.push(patchPath);
7334
8154
  console.log(`dry-run: patch written to ${patchPath}`);
8155
+ const gitignorePath = import_node_path43.default.join(opts.scanPath, ".gitignore");
8156
+ const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
8157
+ const verb = gitignoreExists ? "append" : "create";
8158
+ console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
7335
8159
  console.log("rerun without --dry-run to register and snapshot.");
7336
8160
  return { exitCode: 0, writtenFiles: written };
7337
8161
  }
@@ -7340,12 +8164,16 @@ async function runInit(opts) {
7340
8164
  const graph = getGraph(graphKey);
7341
8165
  const projectPaths = pathsForProject(
7342
8166
  graphKey,
7343
- import_node_path40.default.join(opts.scanPath, "neat-out")
8167
+ import_node_path43.default.join(opts.scanPath, "neat-out")
7344
8168
  );
7345
- const errorsPath = import_node_path40.default.join(import_node_path40.default.dirname(opts.outPath), import_node_path40.default.basename(projectPaths.errorsPath));
8169
+ const errorsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), import_node_path43.default.basename(projectPaths.errorsPath));
7346
8170
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7347
8171
  await saveGraphToDisk(graph, opts.outPath);
7348
8172
  written.push(opts.outPath);
8173
+ const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
8174
+ if (gitignoreResult.action !== "unchanged") {
8175
+ written.push(gitignoreResult.file);
8176
+ }
7349
8177
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
7350
8178
  try {
7351
8179
  await addProject({
@@ -7388,35 +8216,27 @@ async function runInit(opts) {
7388
8216
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7389
8217
  }
7390
8218
  } else {
7391
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
8219
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7392
8220
  written.push(patchPath);
7393
8221
  }
7394
8222
  }
7395
- const nodes = [];
7396
- graph.forEachNode((_id, attrs) => nodes.push(attrs));
7397
- const edges = [];
7398
- graph.forEachEdge((_id, attrs) => edges.push(attrs));
7399
8223
  console.log("");
7400
- console.log("=== neat init: summary ===");
7401
8224
  console.log(`snapshot: ${opts.outPath}`);
7402
8225
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
7403
- console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
7404
- console.log(summarise(nodes, edges));
8226
+ console.log("");
8227
+ const divergenceResult = computeDivergences(graph);
8228
+ console.log(
8229
+ renderValueForwardSummary({
8230
+ graph,
8231
+ divergences: divergenceResult.divergences,
8232
+ verbose: opts.verbose
8233
+ })
8234
+ );
7405
8235
  console.log(formatExtractionBanner(result.extractionErrors));
7406
8236
  if (result.extractionErrors > 0) {
7407
8237
  console.log(`errors: ${errorsPath}`);
7408
8238
  }
7409
8239
  console.log(formatPrecisionFloorBanner(result.extractedDropped));
7410
- const incompatibilities = findIncompatibilities(nodes);
7411
- if (incompatibilities.length > 0) {
7412
- console.log("");
7413
- console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
7414
- for (const svc of incompatibilities) {
7415
- for (const inc of svc.incompatibilities ?? []) {
7416
- console.log(` ${svc.name}: ${formatIncompat(inc)}`);
7417
- }
7418
- }
7419
- }
7420
8240
  if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
7421
8241
  return { exitCode: 4, writtenFiles: written };
7422
8242
  }
@@ -7436,9 +8256,9 @@ var CLAUDE_SKILL_CONFIG = {
7436
8256
  };
7437
8257
  function claudeConfigPath() {
7438
8258
  const override = process.env.NEAT_CLAUDE_CONFIG;
7439
- if (override && override.length > 0) return import_node_path40.default.resolve(override);
8259
+ if (override && override.length > 0) return import_node_path43.default.resolve(override);
7440
8260
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7441
- return import_node_path40.default.join(home, ".claude.json");
8261
+ return import_node_path43.default.join(home, ".claude.json");
7442
8262
  }
7443
8263
  async function runSkill(opts) {
7444
8264
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -7450,7 +8270,7 @@ async function runSkill(opts) {
7450
8270
  const target = claudeConfigPath();
7451
8271
  let existing = {};
7452
8272
  try {
7453
- existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
8273
+ existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
7454
8274
  } catch (err) {
7455
8275
  if (err.code !== "ENOENT") {
7456
8276
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -7462,8 +8282,8 @@ async function runSkill(opts) {
7462
8282
  ...existing,
7463
8283
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
7464
8284
  };
7465
- await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(target), { recursive: true });
7466
- await import_node_fs25.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
8285
+ await import_node_fs28.promises.mkdir(import_node_path43.default.dirname(target), { recursive: true });
8286
+ await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
7467
8287
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
7468
8288
  console.log("restart Claude Code to pick up the new MCP server.");
7469
8289
  return { exitCode: 0 };
@@ -7497,12 +8317,12 @@ async function main() {
7497
8317
  console.error("neat init: --apply and --dry-run are mutually exclusive");
7498
8318
  process.exit(2);
7499
8319
  }
7500
- const scanPath = import_node_path40.default.resolve(target);
8320
+ const scanPath = import_node_path43.default.resolve(target);
7501
8321
  const projectExplicit = parsed.project !== null;
7502
- const projectName = projectExplicit ? project : import_node_path40.default.basename(scanPath);
8322
+ const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
7503
8323
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
7504
- const fallback = pathsForProject(projectKey, import_node_path40.default.join(scanPath, "neat-out")).snapshotPath;
7505
- const outPath = import_node_path40.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
8324
+ const fallback = pathsForProject(projectKey, import_node_path43.default.join(scanPath, "neat-out")).snapshotPath;
8325
+ const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
7506
8326
  const result = await runInit({
7507
8327
  scanPath,
7508
8328
  outPath,
@@ -7510,7 +8330,8 @@ async function main() {
7510
8330
  projectExplicit,
7511
8331
  apply: apply3,
7512
8332
  dryRun,
7513
- noInstall
8333
+ noInstall,
8334
+ verbose: parsed.verbose
7514
8335
  });
7515
8336
  if (result.exitCode !== 0) process.exit(result.exitCode);
7516
8337
  return;
@@ -7522,21 +8343,21 @@ async function main() {
7522
8343
  usage();
7523
8344
  process.exit(2);
7524
8345
  }
7525
- const scanPath = import_node_path40.default.resolve(target);
7526
- const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
8346
+ const scanPath = import_node_path43.default.resolve(target);
8347
+ const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
7527
8348
  if (!stat || !stat.isDirectory()) {
7528
8349
  console.error(`neat watch: ${scanPath} is not a directory`);
7529
8350
  process.exit(2);
7530
8351
  }
7531
- const projectPaths = pathsForProject(project, import_node_path40.default.join(scanPath, "neat-out"));
7532
- const outPath = import_node_path40.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
7533
- const errorsPath = import_node_path40.default.resolve(
7534
- process.env.NEAT_ERRORS_PATH ?? import_node_path40.default.join(import_node_path40.default.dirname(outPath), import_node_path40.default.basename(projectPaths.errorsPath))
8352
+ const projectPaths = pathsForProject(project, import_node_path43.default.join(scanPath, "neat-out"));
8353
+ const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
8354
+ const errorsPath = import_node_path43.default.resolve(
8355
+ process.env.NEAT_ERRORS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.errorsPath))
7535
8356
  );
7536
- const staleEventsPath = import_node_path40.default.resolve(
7537
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path40.default.join(import_node_path40.default.dirname(outPath), import_node_path40.default.basename(projectPaths.staleEventsPath))
8357
+ const staleEventsPath = import_node_path43.default.resolve(
8358
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.staleEventsPath))
7538
8359
  );
7539
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path40.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
8360
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path43.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
7540
8361
  const handle = await startWatch(getGraph(project), {
7541
8362
  scanPath,
7542
8363
  outPath,
@@ -7629,15 +8450,62 @@ async function main() {
7629
8450
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
7630
8451
  return;
7631
8452
  }
8453
+ if (cmd === "deploy") {
8454
+ const artifact = await runDeploy();
8455
+ const block = renderOtelEnvBlock2(artifact.token);
8456
+ console.log();
8457
+ console.log(`Substrate detected: ${artifact.substrate}`);
8458
+ if (artifact.artifactPath) {
8459
+ console.log(`Artifact written: ${artifact.artifactPath}`);
8460
+ } else {
8461
+ console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
8462
+ console.log();
8463
+ console.log(artifact.contents);
8464
+ }
8465
+ console.log();
8466
+ console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
8467
+ console.log(` ${artifact.token}`);
8468
+ console.log();
8469
+ console.log("For your application's deploy platform, set these env vars:");
8470
+ console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
8471
+ console.log();
8472
+ console.log("Once NEAT is running, your dashboard will be at:");
8473
+ console.log(" https://<host>:6328");
8474
+ console.log();
8475
+ console.log("To start NEAT, run:");
8476
+ console.log(` ${artifact.startCommand}`);
8477
+ return;
8478
+ }
7632
8479
  if (QUERY_VERBS.has(cmd)) {
7633
8480
  const code = await runQueryVerb(cmd, parsed);
7634
8481
  if (code !== 0) process.exit(code);
7635
8482
  return;
7636
8483
  }
8484
+ const orchestratorCode = await tryOrchestrator(cmd, parsed);
8485
+ if (orchestratorCode !== null) {
8486
+ if (orchestratorCode !== 0) process.exit(orchestratorCode);
8487
+ return;
8488
+ }
7637
8489
  console.error(`neat: unknown command "${cmd}"`);
7638
8490
  usage();
7639
8491
  process.exit(1);
7640
8492
  }
8493
+ async function tryOrchestrator(cmd, parsed) {
8494
+ const scanPath = import_node_path43.default.resolve(cmd);
8495
+ const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
8496
+ if (!stat || !stat.isDirectory()) return null;
8497
+ const projectExplicit = parsed.project !== null;
8498
+ const projectName = projectExplicit ? parsed.project : import_node_path43.default.basename(scanPath);
8499
+ const result = await runOrchestrator({
8500
+ scanPath,
8501
+ project: projectName,
8502
+ projectExplicit,
8503
+ noInstrument: parsed.noInstrument,
8504
+ noOpen: parsed.noOpen,
8505
+ yes: parsed.yes
8506
+ });
8507
+ return result.exitCode;
8508
+ }
7641
8509
  var QUERY_VERBS = /* @__PURE__ */ new Set([
7642
8510
  "root-cause",
7643
8511
  "blast-radius",
@@ -7777,10 +8645,10 @@ async function runQueryVerb(cmd, parsed) {
7777
8645
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7778
8646
  const out = [];
7779
8647
  for (const p of parts) {
7780
- const r = import_types24.DivergenceTypeSchema.safeParse(p);
8648
+ const r = import_types25.DivergenceTypeSchema.safeParse(p);
7781
8649
  if (!r.success) {
7782
8650
  console.error(
7783
- `neat divergences: unknown --type "${p}". allowed: ${import_types24.DivergenceTypeSchema.options.join(", ")}`
8651
+ `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
7784
8652
  );
7785
8653
  return 2;
7786
8654
  }