@neat.is/core 0.3.7 → 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();
@@ -259,10 +310,10 @@ function parseOtlpRequest(body) {
259
310
  return out;
260
311
  }
261
312
  function loadProtoRoot() {
262
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
313
+ const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
314
+ const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
264
315
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
316
+ root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
266
317
  root.loadSync(
267
318
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
319
  { keepCase: true }
@@ -304,6 +355,7 @@ async function buildOtelReceiver(opts) {
304
355
  logger: false,
305
356
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
306
357
  });
358
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
307
359
  const queue = [];
308
360
  let draining = false;
309
361
  let drainPromise = Promise.resolve();
@@ -382,15 +434,16 @@ async function buildOtelReceiver(opts) {
382
434
  };
383
435
  return decorated;
384
436
  }
385
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
437
+ var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
386
438
  var init_otel = __esm({
387
439
  "src/otel.ts"() {
388
440
  "use strict";
389
441
  init_cjs_shims();
390
- import_node_path35 = __toESM(require("path"), 1);
442
+ import_node_path36 = __toESM(require("path"), 1);
391
443
  import_node_url2 = require("url");
392
444
  import_fastify2 = __toESM(require("fastify"), 1);
393
445
  import_protobufjs = __toESM(require("protobufjs"), 1);
446
+ init_auth();
394
447
  exportTraceServiceRequestType = null;
395
448
  exportTraceServiceResponseType = null;
396
449
  cachedProtobufResponseBody = null;
@@ -409,8 +462,8 @@ __export(cli_exports, {
409
462
  });
410
463
  module.exports = __toCommonJS(cli_exports);
411
464
  init_cjs_shims();
412
- var import_node_path40 = __toESM(require("path"), 1);
413
- var import_node_fs25 = require("fs");
465
+ var import_node_path43 = __toESM(require("path"), 1);
466
+ var import_node_fs28 = require("fs");
414
467
 
415
468
  // src/graph.ts
416
469
  init_cjs_shims();
@@ -920,19 +973,19 @@ function confidenceFromMix(edges, now = Date.now()) {
920
973
  function longestIncomingWalk(graph, start, maxDepth) {
921
974
  let best = { path: [start], edges: [] };
922
975
  const visited = /* @__PURE__ */ new Set([start]);
923
- function step(node, path41, edges) {
924
- if (path41.length > best.path.length) {
925
- best = { path: [...path41], edges: [...edges] };
976
+ function step(node, path44, edges) {
977
+ if (path44.length > best.path.length) {
978
+ best = { path: [...path44], edges: [...edges] };
926
979
  }
927
- if (path41.length - 1 >= maxDepth) return;
980
+ if (path44.length - 1 >= maxDepth) return;
928
981
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
929
982
  for (const [srcId, edge] of incoming) {
930
983
  if (visited.has(srcId)) continue;
931
984
  visited.add(srcId);
932
- path41.push(srcId);
985
+ path44.push(srcId);
933
986
  edges.push(edge);
934
- step(srcId, path41, edges);
935
- path41.pop();
987
+ step(srcId, path44, edges);
988
+ path44.pop();
936
989
  edges.pop();
937
990
  visited.delete(srcId);
938
991
  }
@@ -4175,128 +4228,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4175
4228
  return result;
4176
4229
  }
4177
4230
 
4178
- // src/persist.ts
4179
- init_cjs_shims();
4180
- var import_node_fs18 = require("fs");
4181
- var import_node_path31 = __toESM(require("path"), 1);
4182
- var import_types19 = require("@neat.is/types");
4183
- var SCHEMA_VERSION = 3;
4184
- function migrateV1ToV2(payload) {
4185
- const nodes = payload.graph.nodes;
4186
- if (Array.isArray(nodes)) {
4187
- for (const node of nodes) {
4188
- if (node.attributes && "pgDriverVersion" in node.attributes) {
4189
- delete node.attributes.pgDriverVersion;
4190
- }
4191
- }
4192
- }
4193
- return { ...payload, schemaVersion: 2 };
4194
- }
4195
- function migrateV2ToV3(payload) {
4196
- const edges = payload.graph.edges;
4197
- if (Array.isArray(edges)) {
4198
- for (const edge of edges) {
4199
- const attrs = edge.attributes;
4200
- if (!attrs || attrs.provenance !== "FRONTIER") continue;
4201
- attrs.provenance = import_types19.Provenance.OBSERVED;
4202
- const type = typeof attrs.type === "string" ? attrs.type : void 0;
4203
- const source = typeof attrs.source === "string" ? attrs.source : void 0;
4204
- const target = typeof attrs.target === "string" ? attrs.target : void 0;
4205
- if (type && source && target) {
4206
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4207
- attrs.id = newId;
4208
- if (edge.key) edge.key = newId;
4209
- }
4210
- }
4211
- }
4212
- return { ...payload, schemaVersion: 3 };
4213
- }
4214
- async function ensureDir(filePath) {
4215
- await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4216
- }
4217
- async function saveGraphToDisk(graph, outPath) {
4218
- await ensureDir(outPath);
4219
- const payload = {
4220
- schemaVersion: SCHEMA_VERSION,
4221
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4222
- graph: graph.export()
4223
- };
4224
- const tmp = `${outPath}.tmp`;
4225
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4226
- await import_node_fs18.promises.rename(tmp, outPath);
4227
- }
4228
- async function loadGraphFromDisk(graph, outPath) {
4229
- let raw;
4230
- try {
4231
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4232
- } catch (err) {
4233
- if (err.code === "ENOENT") return;
4234
- throw err;
4235
- }
4236
- let payload = JSON.parse(raw);
4237
- if (payload.schemaVersion === 1) {
4238
- payload = migrateV1ToV2(payload);
4239
- }
4240
- if (payload.schemaVersion === 2) {
4241
- payload = migrateV2ToV3(payload);
4242
- }
4243
- if (payload.schemaVersion !== SCHEMA_VERSION) {
4244
- throw new Error(
4245
- `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
4246
- );
4247
- }
4248
- graph.clear();
4249
- graph.import(payload.graph);
4250
- }
4251
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4252
- let stopped = false;
4253
- const tick = async () => {
4254
- if (stopped) return;
4255
- try {
4256
- await saveGraphToDisk(graph, outPath);
4257
- } catch (err) {
4258
- console.error("persist: periodic save failed", err);
4259
- }
4260
- };
4261
- const interval = setInterval(() => {
4262
- void tick();
4263
- }, intervalMs);
4264
- const onSignal = (signal) => {
4265
- void (async () => {
4266
- try {
4267
- await saveGraphToDisk(graph, outPath);
4268
- } catch (err) {
4269
- console.error(`persist: ${signal} save failed`, err);
4270
- } finally {
4271
- process.exit(0);
4272
- }
4273
- })();
4274
- };
4275
- process.on("SIGTERM", onSignal);
4276
- process.on("SIGINT", onSignal);
4277
- return () => {
4278
- stopped = true;
4279
- clearInterval(interval);
4280
- process.off("SIGTERM", onSignal);
4281
- process.off("SIGINT", onSignal);
4282
- };
4283
- }
4284
-
4285
- // src/watch.ts
4286
- init_cjs_shims();
4287
- var import_node_fs22 = __toESM(require("fs"), 1);
4288
- var import_node_path37 = __toESM(require("path"), 1);
4289
- var import_chokidar = __toESM(require("chokidar"), 1);
4290
-
4291
- // src/api.ts
4292
- init_cjs_shims();
4293
- var import_fastify = __toESM(require("fastify"), 1);
4294
- var import_cors = __toESM(require("@fastify/cors"), 1);
4295
- var import_types22 = require("@neat.is/types");
4296
-
4297
4231
  // src/divergences.ts
4298
4232
  init_cjs_shims();
4299
- var import_types20 = require("@neat.is/types");
4233
+ var import_types19 = require("@neat.is/types");
4300
4234
  function bucketKey(source, target, type) {
4301
4235
  return `${type}|${source}|${target}`;
4302
4236
  }
@@ -4304,22 +4238,22 @@ function bucketEdges(graph) {
4304
4238
  const buckets = /* @__PURE__ */ new Map();
4305
4239
  graph.forEachEdge((id, attrs) => {
4306
4240
  const e = attrs;
4307
- const parsed = (0, import_types20.parseEdgeId)(id);
4241
+ const parsed = (0, import_types19.parseEdgeId)(id);
4308
4242
  const provenance = parsed?.provenance ?? e.provenance;
4309
4243
  const key = bucketKey(e.source, e.target, e.type);
4310
4244
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4311
4245
  switch (provenance) {
4312
- case import_types20.Provenance.EXTRACTED:
4246
+ case import_types19.Provenance.EXTRACTED:
4313
4247
  cur.extracted = e;
4314
4248
  break;
4315
- case import_types20.Provenance.OBSERVED:
4249
+ case import_types19.Provenance.OBSERVED:
4316
4250
  cur.observed = e;
4317
4251
  break;
4318
- case import_types20.Provenance.INFERRED:
4252
+ case import_types19.Provenance.INFERRED:
4319
4253
  cur.inferred = e;
4320
4254
  break;
4321
4255
  default:
4322
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4256
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4323
4257
  }
4324
4258
  buckets.set(key, cur);
4325
4259
  });
@@ -4328,7 +4262,7 @@ function bucketEdges(graph) {
4328
4262
  function nodeIsFrontier(graph, nodeId) {
4329
4263
  if (!graph.hasNode(nodeId)) return false;
4330
4264
  const attrs = graph.getNodeAttributes(nodeId);
4331
- return attrs.type === import_types20.NodeType.FrontierNode;
4265
+ return attrs.type === import_types19.NodeType.FrontierNode;
4332
4266
  }
4333
4267
  function clampConfidence(n) {
4334
4268
  if (!Number.isFinite(n)) return 0;
@@ -4389,7 +4323,7 @@ function declaredHostFor(svc) {
4389
4323
  function hasExtractedConfiguredBy(graph, svcId) {
4390
4324
  for (const edgeId of graph.outboundEdges(svcId)) {
4391
4325
  const e = graph.getEdgeAttributes(edgeId);
4392
- 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) {
4393
4327
  return true;
4394
4328
  }
4395
4329
  }
@@ -4402,10 +4336,10 @@ function detectHostMismatch(graph, svcId, svc) {
4402
4336
  const out = [];
4403
4337
  for (const edgeId of graph.outboundEdges(svcId)) {
4404
4338
  const edge = graph.getEdgeAttributes(edgeId);
4405
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4406
- 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;
4407
4341
  const target = graph.getNodeAttributes(edge.target);
4408
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4342
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4409
4343
  const observedHost = target.host?.trim();
4410
4344
  if (!observedHost) continue;
4411
4345
  if (observedHost === declaredHost) continue;
@@ -4427,10 +4361,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4427
4361
  const deps = svc.dependencies ?? {};
4428
4362
  for (const edgeId of graph.outboundEdges(svcId)) {
4429
4363
  const edge = graph.getEdgeAttributes(edgeId);
4430
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4431
- 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;
4432
4366
  const target = graph.getNodeAttributes(edge.target);
4433
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4367
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4434
4368
  for (const pair of compatPairs()) {
4435
4369
  if (pair.engine !== target.engine) continue;
4436
4370
  const declared = deps[pair.driver];
@@ -4491,7 +4425,7 @@ function computeDivergences(graph, opts = {}) {
4491
4425
  }
4492
4426
  graph.forEachNode((nodeId, attrs) => {
4493
4427
  const n = attrs;
4494
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4428
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4495
4429
  const svc = n;
4496
4430
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4497
4431
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4524,85 +4458,336 @@ function computeDivergences(graph, opts = {}) {
4524
4458
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4525
4459
  return a.target.localeCompare(b.target);
4526
4460
  });
4527
- return import_types20.DivergenceResultSchema.parse({
4461
+ return import_types19.DivergenceResultSchema.parse({
4528
4462
  divergences: filtered,
4529
4463
  totalAffected: filtered.length,
4530
4464
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
4531
4465
  });
4532
4466
  }
4533
4467
 
4534
- // src/diff.ts
4468
+ // src/persist.ts
4535
4469
  init_cjs_shims();
4536
- var import_node_fs19 = require("fs");
4537
- async function loadSnapshotForDiff(target) {
4538
- if (/^https?:\/\//i.test(target)) {
4539
- const res = await fetch(target);
4540
- if (!res.ok) {
4541
- 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
+ }
4542
4481
  }
4543
- return await res.json();
4544
4482
  }
4545
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
4546
- return JSON.parse(raw);
4483
+ return { ...payload, schemaVersion: 2 };
4547
4484
  }
4548
- function indexEntries(entries) {
4549
- const m = /* @__PURE__ */ new Map();
4550
- if (!entries) return m;
4551
- for (const entry2 of entries) {
4552
- const id = entry2.attributes?.id ?? entry2.key;
4553
- if (!id) continue;
4554
- 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
+ }
4555
4501
  }
4556
- return m;
4502
+ return { ...payload, schemaVersion: 3 };
4557
4503
  }
4558
- function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4559
- const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
4560
- const baseEdges = indexEntries(baseSnapshot.graph?.edges);
4561
- const liveNodes = /* @__PURE__ */ new Map();
4562
- liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
4563
- const liveEdges = /* @__PURE__ */ new Map();
4564
- liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
4565
- const result = {
4566
- base: { exportedAt: baseSnapshot.exportedAt },
4567
- current: { exportedAt: currentExportedAt },
4568
- added: { nodes: [], edges: [] },
4569
- removed: { nodes: [], edges: [] },
4570
- 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()
4571
4513
  };
4572
- for (const [id, after] of liveNodes) {
4573
- const before = baseNodes.get(id);
4574
- if (!before) {
4575
- result.added.nodes.push(after);
4576
- } else if (!shallowEqual(before, after)) {
4577
- result.changed.nodes.push({ id, before, after });
4578
- }
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;
4579
4525
  }
4580
- for (const [id, before] of baseNodes) {
4581
- if (!liveNodes.has(id)) result.removed.nodes.push(before);
4526
+ let payload = JSON.parse(raw);
4527
+ if (payload.schemaVersion === 1) {
4528
+ payload = migrateV1ToV2(payload);
4582
4529
  }
4583
- for (const [id, after] of liveEdges) {
4584
- const before = baseEdges.get(id);
4585
- if (!before) {
4586
- result.added.edges.push(after);
4587
- } else if (!shallowEqual(before, after)) {
4588
- result.changed.edges.push({ id, before, after });
4589
- }
4530
+ if (payload.schemaVersion === 2) {
4531
+ payload = migrateV2ToV3(payload);
4590
4532
  }
4591
- for (const [id, before] of baseEdges) {
4592
- 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
+ );
4593
4537
  }
4594
- return result;
4595
- }
4596
- function shallowEqual(a, b) {
4597
- return canonicalJson(a) === canonicalJson(b);
4538
+ graph.clear();
4539
+ graph.import(payload.graph);
4598
4540
  }
4599
- function canonicalJson(value) {
4600
- return JSON.stringify(value, (_key, v) => {
4601
- if (v && typeof v === "object" && !Array.isArray(v)) {
4602
- return Object.keys(v).sort().reduce((acc, k) => {
4603
- acc[k] = v[k];
4604
- return acc;
4605
- }, {});
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) {
4782
+ return canonicalJson(a) === canonicalJson(b);
4783
+ }
4784
+ function canonicalJson(value) {
4785
+ return JSON.stringify(value, (_key, v) => {
4786
+ if (v && typeof v === "object" && !Array.isArray(v)) {
4787
+ return Object.keys(v).sort().reduce((acc, k) => {
4788
+ acc[k] = v[k];
4789
+ return acc;
4790
+ }, {});
4606
4791
  }
4607
4792
  return v;
4608
4793
  });
@@ -4610,23 +4795,23 @@ function canonicalJson(value) {
4610
4795
 
4611
4796
  // src/projects.ts
4612
4797
  init_cjs_shims();
4613
- var import_node_path32 = __toESM(require("path"), 1);
4798
+ var import_node_path33 = __toESM(require("path"), 1);
4614
4799
  function pathsForProject(project, baseDir) {
4615
4800
  if (project === DEFAULT_PROJECT) {
4616
4801
  return {
4617
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4618
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4619
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4620
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4621
- 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")
4622
4807
  };
4623
4808
  }
4624
4809
  return {
4625
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4626
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4627
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4628
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4629
- 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`)
4630
4815
  };
4631
4816
  }
4632
4817
  var Projects = class {
@@ -4662,49 +4847,49 @@ var Projects = class {
4662
4847
 
4663
4848
  // src/registry.ts
4664
4849
  init_cjs_shims();
4665
- var import_node_fs20 = require("fs");
4850
+ var import_node_fs21 = require("fs");
4666
4851
  var import_node_os2 = __toESM(require("os"), 1);
4667
- var import_node_path33 = __toESM(require("path"), 1);
4668
- var import_types21 = require("@neat.is/types");
4852
+ var import_node_path34 = __toESM(require("path"), 1);
4853
+ var import_types22 = require("@neat.is/types");
4669
4854
  var LOCK_TIMEOUT_MS = 5e3;
4670
4855
  var LOCK_RETRY_MS = 50;
4671
4856
  function neatHome() {
4672
4857
  const override = process.env.NEAT_HOME;
4673
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
4674
- 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");
4675
4860
  }
4676
4861
  function registryPath() {
4677
- return import_node_path33.default.join(neatHome(), "projects.json");
4862
+ return import_node_path34.default.join(neatHome(), "projects.json");
4678
4863
  }
4679
4864
  function registryLockPath() {
4680
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
4865
+ return import_node_path34.default.join(neatHome(), "projects.json.lock");
4681
4866
  }
4682
4867
  async function normalizeProjectPath(input) {
4683
- const resolved = import_node_path33.default.resolve(input);
4868
+ const resolved = import_node_path34.default.resolve(input);
4684
4869
  try {
4685
- return await import_node_fs20.promises.realpath(resolved);
4870
+ return await import_node_fs21.promises.realpath(resolved);
4686
4871
  } catch {
4687
4872
  return resolved;
4688
4873
  }
4689
4874
  }
4690
4875
  async function writeAtomically(target, contents) {
4691
- 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 });
4692
4877
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4693
- const fd = await import_node_fs20.promises.open(tmp, "w");
4878
+ const fd = await import_node_fs21.promises.open(tmp, "w");
4694
4879
  try {
4695
4880
  await fd.writeFile(contents, "utf8");
4696
4881
  await fd.sync();
4697
4882
  } finally {
4698
4883
  await fd.close();
4699
4884
  }
4700
- await import_node_fs20.promises.rename(tmp, target);
4885
+ await import_node_fs21.promises.rename(tmp, target);
4701
4886
  }
4702
4887
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4703
4888
  const deadline = Date.now() + timeoutMs;
4704
- 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 });
4705
4890
  while (true) {
4706
4891
  try {
4707
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
4892
+ const fd = await import_node_fs21.promises.open(lockPath, "wx");
4708
4893
  await fd.close();
4709
4894
  return;
4710
4895
  } catch (err) {
@@ -4720,7 +4905,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4720
4905
  }
4721
4906
  }
4722
4907
  async function releaseLock(lockPath) {
4723
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
4908
+ await import_node_fs21.promises.unlink(lockPath).catch(() => {
4724
4909
  });
4725
4910
  }
4726
4911
  async function withLock(fn) {
@@ -4736,7 +4921,7 @@ async function readRegistry() {
4736
4921
  const file = registryPath();
4737
4922
  let raw;
4738
4923
  try {
4739
- raw = await import_node_fs20.promises.readFile(file, "utf8");
4924
+ raw = await import_node_fs21.promises.readFile(file, "utf8");
4740
4925
  } catch (err) {
4741
4926
  if (err.code === "ENOENT") {
4742
4927
  return { version: 1, projects: [] };
@@ -4744,10 +4929,10 @@ async function readRegistry() {
4744
4929
  throw err;
4745
4930
  }
4746
4931
  const parsed = JSON.parse(raw);
4747
- return import_types21.RegistryFileSchema.parse(parsed);
4932
+ return import_types22.RegistryFileSchema.parse(parsed);
4748
4933
  }
4749
4934
  async function writeRegistry(reg) {
4750
- const validated = import_types21.RegistryFileSchema.parse(reg);
4935
+ const validated = import_types22.RegistryFileSchema.parse(reg);
4751
4936
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4752
4937
  }
4753
4938
  var ProjectNameCollisionError = class extends Error {
@@ -4875,6 +5060,7 @@ data: ${JSON.stringify(envelope.payload)}
4875
5060
  }
4876
5061
 
4877
5062
  // src/api.ts
5063
+ init_auth();
4878
5064
  function serializeGraph(graph) {
4879
5065
  const nodes = [];
4880
5066
  graph.forEachNode((_id, attrs) => {
@@ -4998,11 +5184,11 @@ function registerRoutes(scope, ctx) {
4998
5184
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4999
5185
  const parsed = [];
5000
5186
  for (const c of candidates) {
5001
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5187
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5002
5188
  if (!r.success) {
5003
5189
  return reply.code(400).send({
5004
5190
  error: `unknown divergence type "${c}"`,
5005
- allowed: import_types22.DivergenceTypeSchema.options
5191
+ allowed: import_types23.DivergenceTypeSchema.options
5006
5192
  });
5007
5193
  }
5008
5194
  parsed.push(r.data);
@@ -5186,7 +5372,7 @@ function registerRoutes(scope, ctx) {
5186
5372
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5187
5373
  let violations = await log.readAll();
5188
5374
  if (req.query.severity) {
5189
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5375
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5190
5376
  if (!sev.success) {
5191
5377
  return reply.code(400).send({
5192
5378
  error: "invalid severity",
@@ -5203,7 +5389,7 @@ function registerRoutes(scope, ctx) {
5203
5389
  scope.post("/policies/check", async (req, reply) => {
5204
5390
  const proj = resolveProject(registry, req, reply);
5205
5391
  if (!proj) return;
5206
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5392
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5207
5393
  if (!parsed.success) {
5208
5394
  return reply.code(400).send({
5209
5395
  error: "invalid /policies/check body",
@@ -5240,6 +5426,7 @@ function registerRoutes(scope, ctx) {
5240
5426
  async function buildApi(opts) {
5241
5427
  const app = (0, import_fastify.default)({ logger: false });
5242
5428
  await app.register(import_cors.default, { origin: true });
5429
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5243
5430
  const startedAt = opts.startedAt ?? Date.now();
5244
5431
  const registry = buildLegacyRegistry(opts);
5245
5432
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5307,9 +5494,9 @@ init_otel_grpc();
5307
5494
 
5308
5495
  // src/search.ts
5309
5496
  init_cjs_shims();
5310
- var import_node_fs21 = require("fs");
5311
- var import_node_path36 = __toESM(require("path"), 1);
5312
- 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");
5313
5500
  var DEFAULT_LIMIT = 10;
5314
5501
  var NOMIC_DIM = 768;
5315
5502
  var MINI_LM_DIM = 384;
@@ -5349,7 +5536,7 @@ function embedText(node) {
5349
5536
  return parts.join(" ");
5350
5537
  }
5351
5538
  function attrsHash(node) {
5352
- 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);
5353
5540
  }
5354
5541
  function cosine(a, b) {
5355
5542
  if (a.length !== b.length) return 0;
@@ -5438,7 +5625,7 @@ async function pickEmbedder() {
5438
5625
  }
5439
5626
  async function readCache(cachePath) {
5440
5627
  try {
5441
- const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
5628
+ const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
5442
5629
  const parsed = JSON.parse(raw);
5443
5630
  if (parsed.version !== 1) return null;
5444
5631
  return parsed;
@@ -5447,8 +5634,8 @@ async function readCache(cachePath) {
5447
5634
  }
5448
5635
  }
5449
5636
  async function writeCache(cachePath, cache) {
5450
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
5451
- 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));
5452
5639
  }
5453
5640
  var VectorIndex = class {
5454
5641
  constructor(embedder, cachePath) {
@@ -5603,8 +5790,8 @@ var ALL_PHASES = [
5603
5790
  ];
5604
5791
  function classifyChange(relPath) {
5605
5792
  const phases = /* @__PURE__ */ new Set();
5606
- const base = import_node_path37.default.basename(relPath).toLowerCase();
5607
- 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());
5608
5795
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
5609
5796
  phases.add("services");
5610
5797
  phases.add("aliases");
@@ -5699,16 +5886,16 @@ function countWatchableDirs(scanPath, limit) {
5699
5886
  if (count >= limit) return;
5700
5887
  let entries;
5701
5888
  try {
5702
- entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
5889
+ entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
5703
5890
  } catch {
5704
5891
  return;
5705
5892
  }
5706
5893
  for (const e of entries) {
5707
5894
  if (count >= limit) return;
5708
5895
  if (!e.isDirectory()) continue;
5709
- 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;
5710
5897
  count++;
5711
- 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);
5712
5899
  }
5713
5900
  };
5714
5901
  visit(scanPath, 0);
@@ -5726,8 +5913,8 @@ async function startWatch(graph, opts) {
5726
5913
  const projectName = opts.project ?? DEFAULT_PROJECT;
5727
5914
  await loadGraphFromDisk(graph, opts.outPath);
5728
5915
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
5729
- const policyFilePath = import_node_path37.default.join(opts.scanPath, "policy.json");
5730
- 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");
5731
5918
  let policies = [];
5732
5919
  try {
5733
5920
  policies = await loadPolicyFile(policyFilePath);
@@ -5776,7 +5963,7 @@ async function startWatch(graph, opts) {
5776
5963
  const host = opts.host ?? "0.0.0.0";
5777
5964
  const port = opts.port ?? 8080;
5778
5965
  const otelPort = opts.otelPort ?? 4318;
5779
- 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");
5780
5967
  let searchIndex;
5781
5968
  try {
5782
5969
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -5794,7 +5981,7 @@ async function startWatch(graph, opts) {
5794
5981
  // Paths are derived from the explicit options the watch caller passes
5795
5982
  // — pathsForProject is only used to fill in the embeddings/snapshot
5796
5983
  // fields so the registry shape is complete.
5797
- ...pathsForProject(projectName, import_node_path37.default.dirname(opts.outPath)),
5984
+ ...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
5798
5985
  snapshotPath: opts.outPath,
5799
5986
  errorsPath: opts.errorsPath,
5800
5987
  staleEventsPath: opts.staleEventsPath
@@ -5879,9 +6066,9 @@ async function startWatch(graph, opts) {
5879
6066
  };
5880
6067
  const onPath = (absPath) => {
5881
6068
  if (shouldIgnore(absPath)) return;
5882
- const rel = import_node_path37.default.relative(opts.scanPath, absPath);
6069
+ const rel = import_node_path38.default.relative(opts.scanPath, absPath);
5883
6070
  if (!rel || rel.startsWith("..")) return;
5884
- pendingPaths.add(rel.split(import_node_path37.default.sep).join("/"));
6071
+ pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
5885
6072
  const phases = classifyChange(rel);
5886
6073
  if (phases.size === 0) {
5887
6074
  for (const p of ALL_PHASES) pending.add(p);
@@ -5933,13 +6120,158 @@ async function startWatch(graph, opts) {
5933
6120
  return { api, stop };
5934
6121
  }
5935
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
+
5936
6268
  // src/installers/index.ts
5937
6269
  init_cjs_shims();
5938
6270
 
5939
6271
  // src/installers/javascript.ts
5940
6272
  init_cjs_shims();
5941
- var import_node_fs23 = require("fs");
5942
- var import_node_path38 = __toESM(require("path"), 1);
6273
+ var import_node_fs25 = require("fs");
6274
+ var import_node_path40 = __toESM(require("path"), 1);
5943
6275
 
5944
6276
  // src/installers/templates.ts
5945
6277
  init_cjs_shims();
@@ -5987,6 +6319,57 @@ function renderEnvNeat(serviceName) {
5987
6319
  ""
5988
6320
  ].join("\n");
5989
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
+ `;
5990
6373
 
5991
6374
  // src/installers/javascript.ts
5992
6375
  var SDK_PACKAGES = [
@@ -6009,7 +6392,7 @@ var OTEL_ENV = {
6009
6392
  };
6010
6393
  async function readPackageJson(serviceDir) {
6011
6394
  try {
6012
- 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");
6013
6396
  return JSON.parse(raw);
6014
6397
  } catch {
6015
6398
  return null;
@@ -6017,7 +6400,7 @@ async function readPackageJson(serviceDir) {
6017
6400
  }
6018
6401
  async function exists2(p) {
6019
6402
  try {
6020
- await import_node_fs23.promises.stat(p);
6403
+ await import_node_fs25.promises.stat(p);
6021
6404
  return true;
6022
6405
  } catch {
6023
6406
  return false;
@@ -6027,6 +6410,28 @@ async function detect(serviceDir) {
6027
6410
  const pkg = await readPackageJson(serviceDir);
6028
6411
  return pkg !== null && typeof pkg.name === "string";
6029
6412
  }
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
+ }
6030
6435
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
6031
6436
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
6032
6437
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
@@ -6071,7 +6476,7 @@ function entryFromScript(script) {
6071
6476
  }
6072
6477
  async function resolveEntry(serviceDir, pkg) {
6073
6478
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
6074
- const candidate = import_node_path38.default.resolve(serviceDir, pkg.main);
6479
+ const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
6075
6480
  if (await exists2(candidate)) return candidate;
6076
6481
  }
6077
6482
  if (pkg.bin) {
@@ -6085,36 +6490,36 @@ async function resolveEntry(serviceDir, pkg) {
6085
6490
  if (typeof first === "string") binEntry = first;
6086
6491
  }
6087
6492
  if (binEntry) {
6088
- const candidate = import_node_path38.default.resolve(serviceDir, binEntry);
6493
+ const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
6089
6494
  if (await exists2(candidate)) return candidate;
6090
6495
  }
6091
6496
  }
6092
6497
  const startEntry = entryFromScript(pkg.scripts?.start);
6093
6498
  if (startEntry) {
6094
- const candidate = import_node_path38.default.resolve(serviceDir, startEntry);
6499
+ const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
6095
6500
  if (await exists2(candidate)) return candidate;
6096
6501
  }
6097
6502
  const devEntry = entryFromScript(pkg.scripts?.dev);
6098
6503
  if (devEntry) {
6099
- const candidate = import_node_path38.default.resolve(serviceDir, devEntry);
6504
+ const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
6100
6505
  if (await exists2(candidate)) return candidate;
6101
6506
  }
6102
6507
  for (const rel of SRC_INDEX_CANDIDATES) {
6103
- const candidate = import_node_path38.default.join(serviceDir, rel);
6508
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6104
6509
  if (await exists2(candidate)) return candidate;
6105
6510
  }
6106
6511
  for (const rel of SRC_NAMED_CANDIDATES) {
6107
- const candidate = import_node_path38.default.join(serviceDir, rel);
6512
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6108
6513
  if (await exists2(candidate)) return candidate;
6109
6514
  }
6110
6515
  for (const name of INDEX_CANDIDATES) {
6111
- const candidate = import_node_path38.default.join(serviceDir, name);
6516
+ const candidate = import_node_path40.default.join(serviceDir, name);
6112
6517
  if (await exists2(candidate)) return candidate;
6113
6518
  }
6114
6519
  return null;
6115
6520
  }
6116
6521
  function dispatchEntry(entryFile, pkg) {
6117
- const ext = import_node_path38.default.extname(entryFile).toLowerCase();
6522
+ const ext = import_node_path40.default.extname(entryFile).toLowerCase();
6118
6523
  if (ext === ".ts" || ext === ".tsx") return "ts";
6119
6524
  if (ext === ".mjs") return "esm";
6120
6525
  if (ext === ".cjs") return "cjs";
@@ -6131,9 +6536,9 @@ function otelInitContents(flavor) {
6131
6536
  return OTEL_INIT_CJS;
6132
6537
  }
6133
6538
  function injectionLine(flavor, entryFile, otelInitFile) {
6134
- 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);
6135
6540
  if (!rel.startsWith(".")) rel = `./${rel}`;
6136
- rel = rel.split(import_node_path38.default.sep).join("/");
6541
+ rel = rel.split(import_node_path40.default.sep).join("/");
6137
6542
  if (flavor === "cjs") return `require('${rel}')`;
6138
6543
  if (flavor === "esm") return `import '${rel}'`;
6139
6544
  const tsRel = rel.replace(/\.ts$/, "");
@@ -6144,9 +6549,88 @@ function lineIsOtelInjection(line) {
6144
6549
  if (trimmed.length === 0) return false;
6145
6550
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6146
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
+ }
6147
6631
  async function plan(serviceDir) {
6148
6632
  const pkg = await readPackageJson(serviceDir);
6149
- const manifestPath = import_node_path38.default.join(serviceDir, "package.json");
6633
+ const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
6150
6634
  const empty = {
6151
6635
  language: "javascript",
6152
6636
  serviceDir,
@@ -6156,13 +6640,19 @@ async function plan(serviceDir) {
6156
6640
  generatedFiles: []
6157
6641
  };
6158
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
+ }
6159
6649
  const entryFile = await resolveEntry(serviceDir, pkg);
6160
6650
  if (!entryFile) {
6161
6651
  return { ...empty, libOnly: true };
6162
6652
  }
6163
6653
  const flavor = dispatchEntry(entryFile, pkg);
6164
- const otelInitFile = import_node_path38.default.join(import_node_path38.default.dirname(entryFile), otelInitFilename(flavor));
6165
- 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");
6166
6656
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6167
6657
  const dependencyEdits = [];
6168
6658
  for (const sdk of SDK_PACKAGES) {
@@ -6176,7 +6666,7 @@ async function plan(serviceDir) {
6176
6666
  }
6177
6667
  const entrypointEdits = [];
6178
6668
  try {
6179
- const raw = await import_node_fs23.promises.readFile(entryFile, "utf8");
6669
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
6180
6670
  const lines = raw.split(/\r?\n/);
6181
6671
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6182
6672
  if (!lineIsOtelInjection(firstReal)) {
@@ -6201,7 +6691,7 @@ async function plan(serviceDir) {
6201
6691
  if (!await exists2(envNeatFile)) {
6202
6692
  generatedFiles.push({
6203
6693
  file: envNeatFile,
6204
- contents: renderEnvNeat(pkg.name ?? import_node_path38.default.basename(serviceDir)),
6694
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6205
6695
  skipIfExists: true
6206
6696
  });
6207
6697
  }
@@ -6220,18 +6710,20 @@ async function plan(serviceDir) {
6220
6710
  };
6221
6711
  }
6222
6712
  function isAllowedWritePath(serviceDir, target) {
6223
- const rel = import_node_path38.default.relative(serviceDir, target);
6713
+ const rel = import_node_path40.default.relative(serviceDir, target);
6224
6714
  if (rel.startsWith("..")) return false;
6225
- const base = import_node_path38.default.basename(target);
6715
+ const base = import_node_path40.default.basename(target);
6226
6716
  if (base === "package.json") return true;
6227
6717
  if (base === ".env.neat") return true;
6228
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;
6229
6721
  return false;
6230
6722
  }
6231
6723
  async function writeAtomic(file, contents) {
6232
6724
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6233
- await import_node_fs23.promises.writeFile(tmp, contents, "utf8");
6234
- 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);
6235
6727
  }
6236
6728
  async function apply(installPlan) {
6237
6729
  const { serviceDir } = installPlan;
@@ -6243,7 +6735,7 @@ async function apply(installPlan) {
6243
6735
  writtenFiles: []
6244
6736
  };
6245
6737
  }
6246
- 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) {
6247
6739
  return {
6248
6740
  serviceDir,
6249
6741
  outcome: "already-instrumented",
@@ -6254,6 +6746,7 @@ async function apply(installPlan) {
6254
6746
  for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
6255
6747
  for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
6256
6748
  for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
6749
+ if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
6257
6750
  for (const target of allTargets) {
6258
6751
  const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
6259
6752
  if (isEntryEdit) continue;
@@ -6268,7 +6761,7 @@ async function apply(installPlan) {
6268
6761
  for (const target of allTargets) {
6269
6762
  if (await exists2(target)) {
6270
6763
  try {
6271
- originals.set(target, await import_node_fs23.promises.readFile(target, "utf8"));
6764
+ originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
6272
6765
  } catch {
6273
6766
  }
6274
6767
  }
@@ -6323,6 +6816,17 @@ async function apply(installPlan) {
6323
6816
  await writeAtomic(ep.file, newRaw);
6324
6817
  writtenFiles.push(ep.file);
6325
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
+ }
6326
6830
  } catch (err) {
6327
6831
  await rollback(installPlan, originals, createdFiles);
6328
6832
  throw err;
@@ -6338,14 +6842,14 @@ async function rollback(installPlan, originals, createdFiles) {
6338
6842
  const removed = [];
6339
6843
  for (const [file, raw] of originals.entries()) {
6340
6844
  try {
6341
- await import_node_fs23.promises.writeFile(file, raw, "utf8");
6845
+ await import_node_fs25.promises.writeFile(file, raw, "utf8");
6342
6846
  restored.push(file);
6343
6847
  } catch {
6344
6848
  }
6345
6849
  }
6346
6850
  for (const file of createdFiles) {
6347
6851
  try {
6348
- await import_node_fs23.promises.unlink(file);
6852
+ await import_node_fs25.promises.unlink(file);
6349
6853
  removed.push(file);
6350
6854
  } catch {
6351
6855
  }
@@ -6360,8 +6864,26 @@ async function rollback(installPlan, originals, createdFiles) {
6360
6864
  ...removed.map((f) => `removed: ${f}`),
6361
6865
  ""
6362
6866
  ];
6363
- const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
6364
- 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;
6365
6887
  }
6366
6888
  var javascriptInstaller = {
6367
6889
  name: "javascript",
@@ -6372,8 +6894,8 @@ var javascriptInstaller = {
6372
6894
 
6373
6895
  // src/installers/python.ts
6374
6896
  init_cjs_shims();
6375
- var import_node_fs24 = require("fs");
6376
- var import_node_path39 = __toESM(require("path"), 1);
6897
+ var import_node_fs26 = require("fs");
6898
+ var import_node_path41 = __toESM(require("path"), 1);
6377
6899
  var SDK_PACKAGES2 = [
6378
6900
  { name: "opentelemetry-distro", version: ">=0.49b0" },
6379
6901
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -6385,7 +6907,7 @@ var OTEL_ENV2 = {
6385
6907
  };
6386
6908
  async function exists3(p) {
6387
6909
  try {
6388
- await import_node_fs24.promises.stat(p);
6910
+ await import_node_fs26.promises.stat(p);
6389
6911
  return true;
6390
6912
  } catch {
6391
6913
  return false;
@@ -6394,7 +6916,7 @@ async function exists3(p) {
6394
6916
  async function detect2(serviceDir) {
6395
6917
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
6396
6918
  for (const m of markers) {
6397
- 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;
6398
6920
  }
6399
6921
  return false;
6400
6922
  }
@@ -6404,9 +6926,9 @@ function reqPackageName(line) {
6404
6926
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
6405
6927
  }
6406
6928
  async function planRequirementsTxtEdits(serviceDir) {
6407
- const file = import_node_path39.default.join(serviceDir, "requirements.txt");
6929
+ const file = import_node_path41.default.join(serviceDir, "requirements.txt");
6408
6930
  if (!await exists3(file)) return null;
6409
- const raw = await import_node_fs24.promises.readFile(file, "utf8");
6931
+ const raw = await import_node_fs26.promises.readFile(file, "utf8");
6410
6932
  const presentNames = new Set(
6411
6933
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
6412
6934
  );
@@ -6414,9 +6936,9 @@ async function planRequirementsTxtEdits(serviceDir) {
6414
6936
  return { manifest: file, missing: [...missing] };
6415
6937
  }
6416
6938
  async function planProcfileEdits(serviceDir) {
6417
- const procfile = import_node_path39.default.join(serviceDir, "Procfile");
6939
+ const procfile = import_node_path41.default.join(serviceDir, "Procfile");
6418
6940
  if (!await exists3(procfile)) return [];
6419
- const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
6941
+ const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
6420
6942
  const edits = [];
6421
6943
  for (const line of raw.split(/\r?\n/)) {
6422
6944
  if (line.length === 0) continue;
@@ -6468,8 +6990,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
6468
6990
  const next = `${original}${trailing}${newlines.join("\n")}
6469
6991
  `;
6470
6992
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
6471
- await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6472
- 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);
6473
6995
  }
6474
6996
  async function applyProcfile(procfile, edits, original) {
6475
6997
  let next = original;
@@ -6478,8 +7000,8 @@ async function applyProcfile(procfile, edits, original) {
6478
7000
  next = next.replace(e.before, e.after);
6479
7001
  }
6480
7002
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
6481
- await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6482
- 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);
6483
7005
  }
6484
7006
  async function apply2(installPlan) {
6485
7007
  const { serviceDir } = installPlan;
@@ -6492,7 +7014,7 @@ async function apply2(installPlan) {
6492
7014
  const originals = /* @__PURE__ */ new Map();
6493
7015
  for (const file of touched) {
6494
7016
  try {
6495
- originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
7017
+ originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
6496
7018
  } catch {
6497
7019
  }
6498
7020
  }
@@ -6503,7 +7025,7 @@ async function apply2(installPlan) {
6503
7025
  if (raw === void 0) {
6504
7026
  throw new Error(`python installer: cannot read ${file} during apply`);
6505
7027
  }
6506
- const base = import_node_path39.default.basename(file);
7028
+ const base = import_node_path41.default.basename(file);
6507
7029
  if (base === "requirements.txt") {
6508
7030
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
6509
7031
  if (edits.length > 0) {
@@ -6528,7 +7050,7 @@ async function rollback2(installPlan, originals) {
6528
7050
  const restored = [];
6529
7051
  for (const [file, raw] of originals.entries()) {
6530
7052
  try {
6531
- await import_node_fs24.promises.writeFile(file, raw, "utf8");
7053
+ await import_node_fs26.promises.writeFile(file, raw, "utf8");
6532
7054
  restored.push(file);
6533
7055
  } catch {
6534
7056
  }
@@ -6542,8 +7064,8 @@ async function rollback2(installPlan, originals) {
6542
7064
  ...restored.map((f) => `restored: ${f}`),
6543
7065
  ""
6544
7066
  ];
6545
- const rollbackPath = import_node_path39.default.join(installPlan.serviceDir, "neat-rollback.patch");
6546
- 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");
6547
7069
  }
6548
7070
  var pythonInstaller = {
6549
7071
  name: "python",
@@ -6555,7 +7077,7 @@ var pythonInstaller = {
6555
7077
  // src/installers/shared.ts
6556
7078
  init_cjs_shims();
6557
7079
  function isEmptyPlan(plan3) {
6558
- 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;
6559
7081
  }
6560
7082
 
6561
7083
  // src/installers/index.ts
@@ -6654,16 +7176,231 @@ function renderPatch(sections) {
6654
7176
  }
6655
7177
  lines.push("");
6656
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
+ }
6657
7185
  }
6658
7186
  return lines.join("\n");
6659
7187
  }
6660
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
+
6661
7398
  // src/cli.ts
6662
- var import_types24 = require("@neat.is/types");
7399
+ var import_types25 = require("@neat.is/types");
6663
7400
 
6664
7401
  // src/cli-client.ts
6665
7402
  init_cjs_shims();
6666
- var import_types23 = require("@neat.is/types");
7403
+ var import_types24 = require("@neat.is/types");
6667
7404
  var HttpError = class extends Error {
6668
7405
  constructor(status2, message, responseBody = "") {
6669
7406
  super(message);
@@ -6683,10 +7420,10 @@ var TransportError = class extends Error {
6683
7420
  function createHttpClient(baseUrl) {
6684
7421
  const root = baseUrl.replace(/\/$/, "");
6685
7422
  return {
6686
- async get(path41) {
7423
+ async get(path44) {
6687
7424
  let res;
6688
7425
  try {
6689
- res = await fetch(`${root}${path41}`);
7426
+ res = await fetch(`${root}${path44}`);
6690
7427
  } catch (err) {
6691
7428
  throw new TransportError(
6692
7429
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -6696,16 +7433,16 @@ function createHttpClient(baseUrl) {
6696
7433
  const body = await res.text().catch(() => "");
6697
7434
  throw new HttpError(
6698
7435
  res.status,
6699
- `${res.status} ${res.statusText} on GET ${path41}: ${body}`,
7436
+ `${res.status} ${res.statusText} on GET ${path44}: ${body}`,
6700
7437
  body
6701
7438
  );
6702
7439
  }
6703
7440
  return await res.json();
6704
7441
  },
6705
- async post(path41, body) {
7442
+ async post(path44, body) {
6706
7443
  let res;
6707
7444
  try {
6708
- res = await fetch(`${root}${path41}`, {
7445
+ res = await fetch(`${root}${path44}`, {
6709
7446
  method: "POST",
6710
7447
  headers: { "content-type": "application/json" },
6711
7448
  body: JSON.stringify(body)
@@ -6719,7 +7456,7 @@ function createHttpClient(baseUrl) {
6719
7456
  const text = await res.text().catch(() => "");
6720
7457
  throw new HttpError(
6721
7458
  res.status,
6722
- `${res.status} ${res.statusText} on POST ${path41}: ${text}`,
7459
+ `${res.status} ${res.statusText} on POST ${path44}: ${text}`,
6723
7460
  text
6724
7461
  );
6725
7462
  }
@@ -6733,12 +7470,12 @@ function projectPath(project, suffix) {
6733
7470
  }
6734
7471
  async function runRootCause(client, input) {
6735
7472
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
6736
- const path41 = projectPath(
7473
+ const path44 = projectPath(
6737
7474
  input.project,
6738
7475
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
6739
7476
  );
6740
7477
  try {
6741
- const result = await client.get(path41);
7478
+ const result = await client.get(path44);
6742
7479
  const arrowPath = result.traversalPath.join(" \u2190 ");
6743
7480
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
6744
7481
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -6764,12 +7501,12 @@ async function runRootCause(client, input) {
6764
7501
  }
6765
7502
  async function runBlastRadius(client, input) {
6766
7503
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
6767
- const path41 = projectPath(
7504
+ const path44 = projectPath(
6768
7505
  input.project,
6769
7506
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6770
7507
  );
6771
7508
  try {
6772
- const result = await client.get(path41);
7509
+ const result = await client.get(path44);
6773
7510
  if (result.totalAffected === 0) {
6774
7511
  return {
6775
7512
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -6798,17 +7535,17 @@ async function runBlastRadius(client, input) {
6798
7535
  }
6799
7536
  }
6800
7537
  function formatBlastEntry(n) {
6801
- 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]" : "";
6802
7539
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
6803
7540
  }
6804
7541
  async function runDependencies(client, input) {
6805
7542
  const depth = input.depth ?? 3;
6806
- const path41 = projectPath(
7543
+ const path44 = projectPath(
6807
7544
  input.project,
6808
7545
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6809
7546
  );
6810
7547
  try {
6811
- const result = await client.get(path41);
7548
+ const result = await client.get(path44);
6812
7549
  if (result.total === 0) {
6813
7550
  return {
6814
7551
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -6844,9 +7581,9 @@ async function runObservedDependencies(client, input) {
6844
7581
  const edges = await client.get(
6845
7582
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
6846
7583
  );
6847
- 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);
6848
7585
  if (observed.length === 0) {
6849
- 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);
6850
7587
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
6851
7588
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
6852
7589
  }
@@ -6854,7 +7591,7 @@ async function runObservedDependencies(client, input) {
6854
7591
  return {
6855
7592
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
6856
7593
  block: blockLines.join("\n"),
6857
- provenance: import_types23.Provenance.OBSERVED
7594
+ provenance: import_types24.Provenance.OBSERVED
6858
7595
  };
6859
7596
  } catch (err) {
6860
7597
  if (err instanceof HttpError && err.status === 404) {
@@ -6889,9 +7626,9 @@ function formatDuration(ms) {
6889
7626
  return `${Math.round(h / 24)}d`;
6890
7627
  }
6891
7628
  async function runIncidents(client, input) {
6892
- 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");
6893
7630
  try {
6894
- const body = await client.get(path41);
7631
+ const body = await client.get(path44);
6895
7632
  const events = body.events;
6896
7633
  if (events.length === 0) {
6897
7634
  return {
@@ -6908,7 +7645,7 @@ async function runIncidents(client, input) {
6908
7645
  return {
6909
7646
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6910
7647
  block: blockLines.join("\n"),
6911
- provenance: import_types23.Provenance.OBSERVED
7648
+ provenance: import_types24.Provenance.OBSERVED
6912
7649
  };
6913
7650
  } catch (err) {
6914
7651
  if (err instanceof HttpError && err.status === 404) {
@@ -7017,7 +7754,7 @@ async function runStaleEdges(client, input) {
7017
7754
  return {
7018
7755
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
7019
7756
  block: blockLines.join("\n"),
7020
- provenance: import_types23.Provenance.STALE
7757
+ provenance: import_types24.Provenance.STALE
7021
7758
  };
7022
7759
  }
7023
7760
  async function runPolicies(client, input) {
@@ -7183,6 +7920,9 @@ function usage() {
7183
7920
  console.log(" Flags:");
7184
7921
  console.log(" --print-config print the JSON snippet to stdout");
7185
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.");
7186
7926
  console.log("");
7187
7927
  console.log("query commands (mirror the MCP tools, ADR-050):");
7188
7928
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -7245,6 +7985,10 @@ function parseArgs(rest) {
7245
7985
  apply: false,
7246
7986
  dryRun: false,
7247
7987
  noInstall: false,
7988
+ noInstrument: false,
7989
+ noOpen: false,
7990
+ yes: false,
7991
+ verbose: false,
7248
7992
  printConfig: false,
7249
7993
  json: false,
7250
7994
  depth: null,
@@ -7273,6 +8017,22 @@ function parseArgs(rest) {
7273
8017
  out.noInstall = true;
7274
8018
  continue;
7275
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
+ }
7276
8036
  if (arg === "--print-config") {
7277
8037
  out.printConfig = true;
7278
8038
  continue;
@@ -7328,34 +8088,6 @@ function assignFlag(out, field, value) {
7328
8088
  ;
7329
8089
  out[field] = value;
7330
8090
  }
7331
- function summarise(nodes, edges) {
7332
- const byNode = /* @__PURE__ */ new Map();
7333
- for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
7334
- const byEdge = /* @__PURE__ */ new Map();
7335
- for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
7336
- const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
7337
- const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
7338
- return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
7339
- }
7340
- function formatIncompat(inc) {
7341
- if (inc.kind === "node-engine") {
7342
- const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
7343
- return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
7344
- }
7345
- if (inc.kind === "package-conflict") {
7346
- const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
7347
- return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
7348
- }
7349
- if (inc.kind === "deprecated-api") {
7350
- return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
7351
- }
7352
- return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
7353
- }
7354
- function findIncompatibilities(nodes) {
7355
- return nodes.filter(
7356
- (n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
7357
- );
7358
- }
7359
8091
  function printBanner() {
7360
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");
7361
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");
@@ -7406,7 +8138,7 @@ async function buildPatchSections(services) {
7406
8138
  }
7407
8139
  async function runInit(opts) {
7408
8140
  const written = [];
7409
- 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);
7410
8142
  if (!stat || !stat.isDirectory()) {
7411
8143
  console.error(`neat init: ${opts.scanPath} is not a directory`);
7412
8144
  return { exitCode: 2, writtenFiles: written };
@@ -7415,11 +8147,15 @@ async function runInit(opts) {
7415
8147
  printDiscoveryReport(opts, services);
7416
8148
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
7417
8149
  const patch = renderPatch(sections);
7418
- const patchPath = import_node_path40.default.join(opts.scanPath, "neat.patch");
8150
+ const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
7419
8151
  if (opts.dryRun) {
7420
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
8152
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7421
8153
  written.push(patchPath);
7422
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/)`);
7423
8159
  console.log("rerun without --dry-run to register and snapshot.");
7424
8160
  return { exitCode: 0, writtenFiles: written };
7425
8161
  }
@@ -7428,12 +8164,16 @@ async function runInit(opts) {
7428
8164
  const graph = getGraph(graphKey);
7429
8165
  const projectPaths = pathsForProject(
7430
8166
  graphKey,
7431
- import_node_path40.default.join(opts.scanPath, "neat-out")
8167
+ import_node_path43.default.join(opts.scanPath, "neat-out")
7432
8168
  );
7433
- 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));
7434
8170
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7435
8171
  await saveGraphToDisk(graph, opts.outPath);
7436
8172
  written.push(opts.outPath);
8173
+ const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
8174
+ if (gitignoreResult.action !== "unchanged") {
8175
+ written.push(gitignoreResult.file);
8176
+ }
7437
8177
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
7438
8178
  try {
7439
8179
  await addProject({
@@ -7476,35 +8216,27 @@ async function runInit(opts) {
7476
8216
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7477
8217
  }
7478
8218
  } else {
7479
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
8219
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7480
8220
  written.push(patchPath);
7481
8221
  }
7482
8222
  }
7483
- const nodes = [];
7484
- graph.forEachNode((_id, attrs) => nodes.push(attrs));
7485
- const edges = [];
7486
- graph.forEachEdge((_id, attrs) => edges.push(attrs));
7487
8223
  console.log("");
7488
- console.log("=== neat init: summary ===");
7489
8224
  console.log(`snapshot: ${opts.outPath}`);
7490
8225
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
7491
- console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
7492
- 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
+ );
7493
8235
  console.log(formatExtractionBanner(result.extractionErrors));
7494
8236
  if (result.extractionErrors > 0) {
7495
8237
  console.log(`errors: ${errorsPath}`);
7496
8238
  }
7497
8239
  console.log(formatPrecisionFloorBanner(result.extractedDropped));
7498
- const incompatibilities = findIncompatibilities(nodes);
7499
- if (incompatibilities.length > 0) {
7500
- console.log("");
7501
- console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
7502
- for (const svc of incompatibilities) {
7503
- for (const inc of svc.incompatibilities ?? []) {
7504
- console.log(` ${svc.name}: ${formatIncompat(inc)}`);
7505
- }
7506
- }
7507
- }
7508
8240
  if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
7509
8241
  return { exitCode: 4, writtenFiles: written };
7510
8242
  }
@@ -7524,9 +8256,9 @@ var CLAUDE_SKILL_CONFIG = {
7524
8256
  };
7525
8257
  function claudeConfigPath() {
7526
8258
  const override = process.env.NEAT_CLAUDE_CONFIG;
7527
- 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);
7528
8260
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7529
- return import_node_path40.default.join(home, ".claude.json");
8261
+ return import_node_path43.default.join(home, ".claude.json");
7530
8262
  }
7531
8263
  async function runSkill(opts) {
7532
8264
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -7538,7 +8270,7 @@ async function runSkill(opts) {
7538
8270
  const target = claudeConfigPath();
7539
8271
  let existing = {};
7540
8272
  try {
7541
- existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
8273
+ existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
7542
8274
  } catch (err) {
7543
8275
  if (err.code !== "ENOENT") {
7544
8276
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -7550,8 +8282,8 @@ async function runSkill(opts) {
7550
8282
  ...existing,
7551
8283
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
7552
8284
  };
7553
- await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(target), { recursive: true });
7554
- 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");
7555
8287
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
7556
8288
  console.log("restart Claude Code to pick up the new MCP server.");
7557
8289
  return { exitCode: 0 };
@@ -7585,12 +8317,12 @@ async function main() {
7585
8317
  console.error("neat init: --apply and --dry-run are mutually exclusive");
7586
8318
  process.exit(2);
7587
8319
  }
7588
- const scanPath = import_node_path40.default.resolve(target);
8320
+ const scanPath = import_node_path43.default.resolve(target);
7589
8321
  const projectExplicit = parsed.project !== null;
7590
- const projectName = projectExplicit ? project : import_node_path40.default.basename(scanPath);
8322
+ const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
7591
8323
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
7592
- const fallback = pathsForProject(projectKey, import_node_path40.default.join(scanPath, "neat-out")).snapshotPath;
7593
- 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);
7594
8326
  const result = await runInit({
7595
8327
  scanPath,
7596
8328
  outPath,
@@ -7598,7 +8330,8 @@ async function main() {
7598
8330
  projectExplicit,
7599
8331
  apply: apply3,
7600
8332
  dryRun,
7601
- noInstall
8333
+ noInstall,
8334
+ verbose: parsed.verbose
7602
8335
  });
7603
8336
  if (result.exitCode !== 0) process.exit(result.exitCode);
7604
8337
  return;
@@ -7610,21 +8343,21 @@ async function main() {
7610
8343
  usage();
7611
8344
  process.exit(2);
7612
8345
  }
7613
- const scanPath = import_node_path40.default.resolve(target);
7614
- 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);
7615
8348
  if (!stat || !stat.isDirectory()) {
7616
8349
  console.error(`neat watch: ${scanPath} is not a directory`);
7617
8350
  process.exit(2);
7618
8351
  }
7619
- const projectPaths = pathsForProject(project, import_node_path40.default.join(scanPath, "neat-out"));
7620
- const outPath = import_node_path40.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
7621
- const errorsPath = import_node_path40.default.resolve(
7622
- 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))
7623
8356
  );
7624
- const staleEventsPath = import_node_path40.default.resolve(
7625
- 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))
7626
8359
  );
7627
- 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;
7628
8361
  const handle = await startWatch(getGraph(project), {
7629
8362
  scanPath,
7630
8363
  outPath,
@@ -7717,15 +8450,62 @@ async function main() {
7717
8450
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
7718
8451
  return;
7719
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
+ }
7720
8479
  if (QUERY_VERBS.has(cmd)) {
7721
8480
  const code = await runQueryVerb(cmd, parsed);
7722
8481
  if (code !== 0) process.exit(code);
7723
8482
  return;
7724
8483
  }
8484
+ const orchestratorCode = await tryOrchestrator(cmd, parsed);
8485
+ if (orchestratorCode !== null) {
8486
+ if (orchestratorCode !== 0) process.exit(orchestratorCode);
8487
+ return;
8488
+ }
7725
8489
  console.error(`neat: unknown command "${cmd}"`);
7726
8490
  usage();
7727
8491
  process.exit(1);
7728
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
+ }
7729
8509
  var QUERY_VERBS = /* @__PURE__ */ new Set([
7730
8510
  "root-cause",
7731
8511
  "blast-radius",
@@ -7865,10 +8645,10 @@ async function runQueryVerb(cmd, parsed) {
7865
8645
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7866
8646
  const out = [];
7867
8647
  for (const p of parts) {
7868
- const r = import_types24.DivergenceTypeSchema.safeParse(p);
8648
+ const r = import_types25.DivergenceTypeSchema.safeParse(p);
7869
8649
  if (!r.success) {
7870
8650
  console.error(
7871
- `neat divergences: unknown --type "${p}". allowed: ${import_types24.DivergenceTypeSchema.options.join(", ")}`
8651
+ `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
7872
8652
  );
7873
8653
  return 2;
7874
8654
  }