@neat.is/core 0.3.7 → 0.4.0

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,54 @@ 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
+ const publicRead = opts.publicRead === true;
51
+ app.addHook("preHandler", (req, reply, done) => {
52
+ const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
53
+ for (const suffix of suffixes) {
54
+ if (path45 === suffix || path45.endsWith(suffix)) {
55
+ done();
56
+ return;
57
+ }
58
+ }
59
+ if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
60
+ done();
61
+ return;
62
+ }
63
+ const header = req.headers.authorization;
64
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
65
+ void reply.code(401).send({ error: "unauthorized" });
66
+ return;
67
+ }
68
+ const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
69
+ if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
70
+ void reply.code(401).send({ error: "unauthorized" });
71
+ return;
72
+ }
73
+ done();
74
+ });
75
+ }
76
+ var import_node_crypto, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
77
+ var init_auth = __esm({
78
+ "src/auth.ts"() {
79
+ "use strict";
80
+ init_cjs_shims();
81
+ import_node_crypto = require("crypto");
82
+ PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
83
+ DEFAULT_UNAUTH_SUFFIXES = [
84
+ "/health",
85
+ "/healthz",
86
+ "/readyz",
87
+ "/api/config"
88
+ ];
89
+ }
90
+ });
91
+
44
92
  // src/otel-grpc.ts
45
93
  var otel_grpc_exports = {};
46
94
  __export(otel_grpc_exports, {
@@ -101,8 +149,8 @@ function reshapeGrpcRequest(req) {
101
149
  };
102
150
  }
103
151
  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");
152
+ const here = import_node_path35.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
153
+ return import_node_path35.default.resolve(here, "..", "proto");
106
154
  }
107
155
  function loadTraceService() {
108
156
  const protoRoot = resolveProtoRoot();
@@ -123,8 +171,21 @@ function loadTraceService() {
123
171
  async function startOtelGrpcReceiver(opts) {
124
172
  const server = new grpc.Server();
125
173
  const service = loadTraceService();
174
+ const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
175
+ const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
126
176
  server.addService(service, {
127
177
  Export: (call, callback) => {
178
+ if (requiresAuth) {
179
+ const meta = call.metadata.get("authorization");
180
+ const got = meta.length > 0 ? String(meta[0]) : "";
181
+ const a = Buffer.from(got, "utf8");
182
+ const b = Buffer.from(expectedHeader, "utf8");
183
+ const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
184
+ if (!ok) {
185
+ callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
186
+ return;
187
+ }
188
+ }
128
189
  void (async () => {
129
190
  try {
130
191
  const reshaped = reshapeGrpcRequest(call.request ?? {});
@@ -157,13 +218,14 @@ async function startOtelGrpcReceiver(opts) {
157
218
  })
158
219
  };
159
220
  }
160
- var import_node_url, import_node_path34, grpc, protoLoader;
221
+ var import_node_url, import_node_path35, import_node_crypto2, grpc, protoLoader;
161
222
  var init_otel_grpc = __esm({
162
223
  "src/otel-grpc.ts"() {
163
224
  "use strict";
164
225
  init_cjs_shims();
165
226
  import_node_url = require("url");
166
- import_node_path34 = __toESM(require("path"), 1);
227
+ import_node_path35 = __toESM(require("path"), 1);
228
+ import_node_crypto2 = require("crypto");
167
229
  grpc = __toESM(require("@grpc/grpc-js"), 1);
168
230
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
169
231
  init_otel();
@@ -226,6 +288,15 @@ function isoFromUnixNano(nanos) {
226
288
  return void 0;
227
289
  }
228
290
  }
291
+ function pickEnv(spanAttrs, resourceAttrs) {
292
+ for (const attrs of [spanAttrs, resourceAttrs]) {
293
+ for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {
294
+ const v = attrs[key];
295
+ if (typeof v === "string" && v.length > 0) return v;
296
+ }
297
+ }
298
+ return ENV_FALLBACK;
299
+ }
229
300
  function parseOtlpRequest(body) {
230
301
  const out = [];
231
302
  for (const rs of body.resourceSpans ?? []) {
@@ -245,6 +316,7 @@ function parseOtlpRequest(body) {
245
316
  endTimeUnixNano: span.endTimeUnixNano ?? "0",
246
317
  startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
247
318
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
319
+ env: pickEnv(attrs, resourceAttrs),
248
320
  attributes: attrs,
249
321
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
250
322
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
@@ -259,10 +331,10 @@ function parseOtlpRequest(body) {
259
331
  return out;
260
332
  }
261
333
  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");
334
+ const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
335
+ const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
264
336
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
337
+ root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
266
338
  root.loadSync(
267
339
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
340
  { keepCase: true }
@@ -304,6 +376,7 @@ async function buildOtelReceiver(opts) {
304
376
  logger: false,
305
377
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
306
378
  });
379
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
307
380
  const queue = [];
308
381
  let draining = false;
309
382
  let drainPromise = Promise.resolve();
@@ -382,15 +455,19 @@ async function buildOtelReceiver(opts) {
382
455
  };
383
456
  return decorated;
384
457
  }
385
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
458
+ var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
386
459
  var init_otel = __esm({
387
460
  "src/otel.ts"() {
388
461
  "use strict";
389
462
  init_cjs_shims();
390
- import_node_path35 = __toESM(require("path"), 1);
463
+ import_node_path36 = __toESM(require("path"), 1);
391
464
  import_node_url2 = require("url");
392
465
  import_fastify2 = __toESM(require("fastify"), 1);
393
466
  import_protobufjs = __toESM(require("protobufjs"), 1);
467
+ init_auth();
468
+ ENV_ATTR_CANONICAL = "deployment.environment.name";
469
+ ENV_ATTR_COMPAT = "deployment.environment";
470
+ ENV_FALLBACK = "unknown";
394
471
  exportTraceServiceRequestType = null;
395
472
  exportTraceServiceResponseType = null;
396
473
  cachedProtobufResponseBody = null;
@@ -409,8 +486,8 @@ __export(cli_exports, {
409
486
  });
410
487
  module.exports = __toCommonJS(cli_exports);
411
488
  init_cjs_shims();
412
- var import_node_path40 = __toESM(require("path"), 1);
413
- var import_node_fs25 = require("fs");
489
+ var import_node_path44 = __toESM(require("path"), 1);
490
+ var import_node_fs28 = require("fs");
414
491
 
415
492
  // src/graph.ts
416
493
  init_cjs_shims();
@@ -920,19 +997,19 @@ function confidenceFromMix(edges, now = Date.now()) {
920
997
  function longestIncomingWalk(graph, start, maxDepth) {
921
998
  let best = { path: [start], edges: [] };
922
999
  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] };
1000
+ function step(node, path45, edges) {
1001
+ if (path45.length > best.path.length) {
1002
+ best = { path: [...path45], edges: [...edges] };
926
1003
  }
927
- if (path41.length - 1 >= maxDepth) return;
1004
+ if (path45.length - 1 >= maxDepth) return;
928
1005
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
929
1006
  for (const [srcId, edge] of incoming) {
930
1007
  if (visited.has(srcId)) continue;
931
1008
  visited.add(srcId);
932
- path41.push(srcId);
1009
+ path45.push(srcId);
933
1010
  edges.push(edge);
934
- step(srcId, path41, edges);
935
- path41.pop();
1011
+ step(srcId, path45, edges);
1012
+ path45.pop();
936
1013
  edges.pop();
937
1014
  visited.delete(srcId);
938
1015
  }
@@ -1505,52 +1582,64 @@ function cacheSpanService(span, now) {
1505
1582
  if (!span.traceId || !span.spanId) return;
1506
1583
  const key = parentSpanKey(span.traceId, span.spanId);
1507
1584
  parentSpanCache.delete(key);
1508
- parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1585
+ parentSpanCache.set(key, {
1586
+ service: span.service,
1587
+ env: span.env ?? "unknown",
1588
+ expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1589
+ });
1509
1590
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1510
1591
  const oldest = parentSpanCache.keys().next().value;
1511
1592
  if (!oldest) break;
1512
1593
  parentSpanCache.delete(oldest);
1513
1594
  }
1514
1595
  }
1515
- function lookupParentSpanService(traceId, parentSpanId, now) {
1596
+ function lookupParentSpan(traceId, parentSpanId, now) {
1516
1597
  const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1517
1598
  if (!entry2) return null;
1518
1599
  if (entry2.expiresAt <= now) {
1519
1600
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1520
1601
  return null;
1521
1602
  }
1522
- return entry2.service;
1603
+ return { service: entry2.service, env: entry2.env };
1523
1604
  }
1524
- function resolveServiceId(graph, host) {
1525
- const direct = (0, import_types3.serviceId)(host);
1526
- if (graph.hasNode(direct)) return direct;
1527
- let found = null;
1605
+ function resolveServiceId(graph, host, env) {
1606
+ const envTagged = (0, import_types3.serviceId)(host, env);
1607
+ if (graph.hasNode(envTagged)) return envTagged;
1608
+ const envLess = (0, import_types3.serviceId)(host);
1609
+ if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
1610
+ let sameEnv = null;
1611
+ let envLessMatch = null;
1612
+ let anyMatch = null;
1528
1613
  graph.forEachNode((id, attrs) => {
1529
- if (found) return;
1614
+ if (sameEnv) return;
1530
1615
  const a = attrs;
1531
1616
  if (a.type !== import_types3.NodeType.ServiceNode) return;
1532
- if (a.name === host) {
1533
- found = id;
1617
+ const matchesByName = a.name === host;
1618
+ const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1619
+ if (!matchesByName && !matchesByAlias) return;
1620
+ const nodeEnv = a.env ?? "unknown";
1621
+ if (nodeEnv === env) {
1622
+ sameEnv = id;
1534
1623
  return;
1535
1624
  }
1536
- if (a.aliases && a.aliases.includes(host)) {
1537
- found = id;
1538
- }
1625
+ if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
1626
+ else if (!anyMatch) anyMatch = id;
1539
1627
  });
1540
- return found;
1628
+ return sameEnv ?? envLessMatch ?? anyMatch;
1541
1629
  }
1542
1630
  function frontierIdFor(host) {
1543
1631
  return (0, import_types3.frontierId)(host);
1544
1632
  }
1545
- function ensureServiceNode(graph, serviceName) {
1546
- const id = (0, import_types3.serviceId)(serviceName);
1633
+ function ensureServiceNode(graph, serviceName, env) {
1634
+ const id = (0, import_types3.serviceId)(serviceName, env);
1547
1635
  if (graph.hasNode(id)) return id;
1548
1636
  const node = {
1549
1637
  id,
1550
1638
  type: import_types3.NodeType.ServiceNode,
1551
1639
  name: serviceName,
1552
1640
  language: "unknown",
1553
- discoveredVia: "otel"
1641
+ discoveredVia: "otel",
1642
+ ...env !== "unknown" ? { env } : {}
1554
1643
  };
1555
1644
  graph.addNode(id, node);
1556
1645
  return id;
@@ -1696,7 +1785,7 @@ function buildErrorEventForReceiver(span) {
1696
1785
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1697
1786
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1698
1787
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1699
- affectedNode: (0, import_types3.serviceId)(span.service)
1788
+ affectedNode: (0, import_types3.serviceId)(span.service, span.env)
1700
1789
  };
1701
1790
  }
1702
1791
  function makeErrorSpanWriter(errorsPath) {
@@ -1710,7 +1799,8 @@ function makeErrorSpanWriter(errorsPath) {
1710
1799
  async function handleSpan(ctx, span) {
1711
1800
  const ts = span.startTimeIso ?? nowIso(ctx);
1712
1801
  const nowMs = ctx.now ? ctx.now() : Date.now();
1713
- const sourceId = ensureServiceNode(ctx.graph, span.service);
1802
+ const env = span.env ?? "unknown";
1803
+ const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1714
1804
  const isError = span.statusCode === 2;
1715
1805
  cacheSpanService(span, nowMs);
1716
1806
  let affectedNode = sourceId;
@@ -1733,7 +1823,7 @@ async function handleSpan(ctx, span) {
1733
1823
  const host = pickAddress(span);
1734
1824
  let resolvedViaAddress = false;
1735
1825
  if (host && host !== span.service) {
1736
- const targetId = resolveServiceId(ctx.graph, host);
1826
+ const targetId = resolveServiceId(ctx.graph, host, env);
1737
1827
  if (targetId && targetId !== sourceId) {
1738
1828
  upsertObservedEdge(
1739
1829
  ctx.graph,
@@ -1760,9 +1850,9 @@ async function handleSpan(ctx, span) {
1760
1850
  }
1761
1851
  }
1762
1852
  if (!resolvedViaAddress && span.parentSpanId) {
1763
- const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
1764
- if (parentService && parentService !== span.service) {
1765
- const parentId = ensureServiceNode(ctx.graph, parentService);
1853
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
1854
+ if (parent && parent.service !== span.service) {
1855
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1766
1856
  upsertObservedEdge(
1767
1857
  ctx.graph,
1768
1858
  import_types3.EdgeType.CALLS,
@@ -1958,6 +2048,28 @@ async function readErrorEvents(errorsPath) {
1958
2048
  throw err;
1959
2049
  }
1960
2050
  }
2051
+ function mergeSnapshot(graph, snapshot) {
2052
+ const exported = snapshot.graph;
2053
+ let nodesAdded = 0;
2054
+ let edgesAdded = 0;
2055
+ for (const node of exported.nodes ?? []) {
2056
+ if (graph.hasNode(node.key)) continue;
2057
+ if (!node.attributes) continue;
2058
+ graph.addNode(node.key, node.attributes);
2059
+ nodesAdded++;
2060
+ }
2061
+ for (const edge of exported.edges ?? []) {
2062
+ const attrs = edge.attributes;
2063
+ if (!attrs) continue;
2064
+ const id = edge.key ?? attrs.id;
2065
+ if (!id) continue;
2066
+ if (graph.hasEdge(id)) continue;
2067
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2068
+ graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
2069
+ edgesAdded++;
2070
+ }
2071
+ return { nodesAdded, edgesAdded };
2072
+ }
1961
2073
 
1962
2074
  // src/extract/services.ts
1963
2075
  init_cjs_shims();
@@ -2366,6 +2478,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2366
2478
  }
2367
2479
  return [...found];
2368
2480
  }
2481
+ function detectJsFramework(pkg) {
2482
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2483
+ if (deps["next"] !== void 0) return "next";
2484
+ if (deps["remix"] !== void 0) return "remix";
2485
+ for (const k of Object.keys(deps)) {
2486
+ if (k.startsWith("@remix-run/")) return "remix";
2487
+ }
2488
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
2489
+ if (deps["nuxt"] !== void 0) return "nuxt";
2490
+ if (deps["astro"] !== void 0) return "astro";
2491
+ return void 0;
2492
+ }
2369
2493
  async function discoverNodeService(scanPath, dir) {
2370
2494
  const pkgPath = import_node_path8.default.join(dir, "package.json");
2371
2495
  if (!await exists(pkgPath)) return null;
@@ -2377,6 +2501,7 @@ async function discoverNodeService(scanPath, dir) {
2377
2501
  return null;
2378
2502
  }
2379
2503
  if (!pkg.name) return null;
2504
+ const framework = detectJsFramework(pkg);
2380
2505
  const node = {
2381
2506
  id: (0, import_types5.serviceId)(pkg.name),
2382
2507
  type: import_types5.NodeType.ServiceNode,
@@ -2385,7 +2510,8 @@ async function discoverNodeService(scanPath, dir) {
2385
2510
  version: pkg.version,
2386
2511
  dependencies: pkg.dependencies ?? {},
2387
2512
  repoPath: import_node_path8.default.relative(scanPath, dir),
2388
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2513
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2514
+ ...framework ? { framework } : {}
2389
2515
  };
2390
2516
  return { pkg, dir, node };
2391
2517
  }
@@ -4175,128 +4301,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4175
4301
  return result;
4176
4302
  }
4177
4303
 
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
4304
  // src/divergences.ts
4298
4305
  init_cjs_shims();
4299
- var import_types20 = require("@neat.is/types");
4306
+ var import_types19 = require("@neat.is/types");
4300
4307
  function bucketKey(source, target, type) {
4301
4308
  return `${type}|${source}|${target}`;
4302
4309
  }
@@ -4304,22 +4311,22 @@ function bucketEdges(graph) {
4304
4311
  const buckets = /* @__PURE__ */ new Map();
4305
4312
  graph.forEachEdge((id, attrs) => {
4306
4313
  const e = attrs;
4307
- const parsed = (0, import_types20.parseEdgeId)(id);
4314
+ const parsed = (0, import_types19.parseEdgeId)(id);
4308
4315
  const provenance = parsed?.provenance ?? e.provenance;
4309
4316
  const key = bucketKey(e.source, e.target, e.type);
4310
4317
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4311
4318
  switch (provenance) {
4312
- case import_types20.Provenance.EXTRACTED:
4319
+ case import_types19.Provenance.EXTRACTED:
4313
4320
  cur.extracted = e;
4314
4321
  break;
4315
- case import_types20.Provenance.OBSERVED:
4322
+ case import_types19.Provenance.OBSERVED:
4316
4323
  cur.observed = e;
4317
4324
  break;
4318
- case import_types20.Provenance.INFERRED:
4325
+ case import_types19.Provenance.INFERRED:
4319
4326
  cur.inferred = e;
4320
4327
  break;
4321
4328
  default:
4322
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4329
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4323
4330
  }
4324
4331
  buckets.set(key, cur);
4325
4332
  });
@@ -4328,7 +4335,7 @@ function bucketEdges(graph) {
4328
4335
  function nodeIsFrontier(graph, nodeId) {
4329
4336
  if (!graph.hasNode(nodeId)) return false;
4330
4337
  const attrs = graph.getNodeAttributes(nodeId);
4331
- return attrs.type === import_types20.NodeType.FrontierNode;
4338
+ return attrs.type === import_types19.NodeType.FrontierNode;
4332
4339
  }
4333
4340
  function clampConfidence(n) {
4334
4341
  if (!Number.isFinite(n)) return 0;
@@ -4389,7 +4396,7 @@ function declaredHostFor(svc) {
4389
4396
  function hasExtractedConfiguredBy(graph, svcId) {
4390
4397
  for (const edgeId of graph.outboundEdges(svcId)) {
4391
4398
  const e = graph.getEdgeAttributes(edgeId);
4392
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4399
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4393
4400
  return true;
4394
4401
  }
4395
4402
  }
@@ -4402,10 +4409,10 @@ function detectHostMismatch(graph, svcId, svc) {
4402
4409
  const out = [];
4403
4410
  for (const edgeId of graph.outboundEdges(svcId)) {
4404
4411
  const edge = graph.getEdgeAttributes(edgeId);
4405
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4406
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4412
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4413
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4407
4414
  const target = graph.getNodeAttributes(edge.target);
4408
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4415
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4409
4416
  const observedHost = target.host?.trim();
4410
4417
  if (!observedHost) continue;
4411
4418
  if (observedHost === declaredHost) continue;
@@ -4427,10 +4434,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4427
4434
  const deps = svc.dependencies ?? {};
4428
4435
  for (const edgeId of graph.outboundEdges(svcId)) {
4429
4436
  const edge = graph.getEdgeAttributes(edgeId);
4430
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4431
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4437
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4438
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4432
4439
  const target = graph.getNodeAttributes(edge.target);
4433
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4440
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4434
4441
  for (const pair of compatPairs()) {
4435
4442
  if (pair.engine !== target.engine) continue;
4436
4443
  const declared = deps[pair.driver];
@@ -4491,7 +4498,7 @@ function computeDivergences(graph, opts = {}) {
4491
4498
  }
4492
4499
  graph.forEachNode((nodeId, attrs) => {
4493
4500
  const n = attrs;
4494
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4501
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4495
4502
  const svc = n;
4496
4503
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4497
4504
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4524,71 +4531,328 @@ function computeDivergences(graph, opts = {}) {
4524
4531
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4525
4532
  return a.target.localeCompare(b.target);
4526
4533
  });
4527
- return import_types20.DivergenceResultSchema.parse({
4534
+ return import_types19.DivergenceResultSchema.parse({
4528
4535
  divergences: filtered,
4529
4536
  totalAffected: filtered.length,
4530
4537
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
4531
4538
  });
4532
4539
  }
4533
4540
 
4534
- // src/diff.ts
4541
+ // src/persist.ts
4535
4542
  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}`);
4543
+ var import_node_fs18 = require("fs");
4544
+ var import_node_path31 = __toESM(require("path"), 1);
4545
+ var import_types20 = require("@neat.is/types");
4546
+ var SCHEMA_VERSION = 4;
4547
+ function migrateV1ToV2(payload) {
4548
+ const nodes = payload.graph.nodes;
4549
+ if (Array.isArray(nodes)) {
4550
+ for (const node of nodes) {
4551
+ if (node.attributes && "pgDriverVersion" in node.attributes) {
4552
+ delete node.attributes.pgDriverVersion;
4553
+ }
4542
4554
  }
4543
- return await res.json();
4544
4555
  }
4545
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
4546
- return JSON.parse(raw);
4556
+ return { ...payload, schemaVersion: 2 };
4547
4557
  }
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);
4558
+ function migrateV3ToV4(payload) {
4559
+ return { ...payload, schemaVersion: 4 };
4560
+ }
4561
+ function migrateV2ToV3(payload) {
4562
+ const edges = payload.graph.edges;
4563
+ if (Array.isArray(edges)) {
4564
+ for (const edge of edges) {
4565
+ const attrs = edge.attributes;
4566
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
4567
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4568
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
4569
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4570
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4571
+ if (type && source && target) {
4572
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4573
+ attrs.id = newId;
4574
+ if (edge.key) edge.key = newId;
4575
+ }
4576
+ }
4555
4577
  }
4556
- return m;
4578
+ return { ...payload, schemaVersion: 3 };
4557
4579
  }
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: [] }
4580
+ async function ensureDir(filePath) {
4581
+ await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4582
+ }
4583
+ async function saveGraphToDisk(graph, outPath) {
4584
+ await ensureDir(outPath);
4585
+ const payload = {
4586
+ schemaVersion: SCHEMA_VERSION,
4587
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4588
+ graph: graph.export()
4571
4589
  };
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
- }
4590
+ const tmp = `${outPath}.tmp`;
4591
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4592
+ await import_node_fs18.promises.rename(tmp, outPath);
4593
+ }
4594
+ async function loadGraphFromDisk(graph, outPath) {
4595
+ let raw;
4596
+ try {
4597
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4598
+ } catch (err) {
4599
+ if (err.code === "ENOENT") return;
4600
+ throw err;
4579
4601
  }
4580
- for (const [id, before] of baseNodes) {
4581
- if (!liveNodes.has(id)) result.removed.nodes.push(before);
4602
+ let payload = JSON.parse(raw);
4603
+ if (payload.schemaVersion === 1) {
4604
+ payload = migrateV1ToV2(payload);
4582
4605
  }
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
- }
4606
+ if (payload.schemaVersion === 2) {
4607
+ payload = migrateV2ToV3(payload);
4590
4608
  }
4591
- for (const [id, before] of baseEdges) {
4609
+ if (payload.schemaVersion === 3) {
4610
+ payload = migrateV3ToV4(payload);
4611
+ }
4612
+ if (payload.schemaVersion !== SCHEMA_VERSION) {
4613
+ throw new Error(
4614
+ `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
4615
+ );
4616
+ }
4617
+ graph.clear();
4618
+ graph.import(payload.graph);
4619
+ }
4620
+ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4621
+ let stopped = false;
4622
+ const tick = async () => {
4623
+ if (stopped) return;
4624
+ try {
4625
+ await saveGraphToDisk(graph, outPath);
4626
+ } catch (err) {
4627
+ console.error("persist: periodic save failed", err);
4628
+ }
4629
+ };
4630
+ const interval = setInterval(() => {
4631
+ void tick();
4632
+ }, intervalMs);
4633
+ const onSignal = (signal) => {
4634
+ void (async () => {
4635
+ try {
4636
+ await saveGraphToDisk(graph, outPath);
4637
+ } catch (err) {
4638
+ console.error(`persist: ${signal} save failed`, err);
4639
+ } finally {
4640
+ process.exit(0);
4641
+ }
4642
+ })();
4643
+ };
4644
+ process.on("SIGTERM", onSignal);
4645
+ process.on("SIGINT", onSignal);
4646
+ return () => {
4647
+ stopped = true;
4648
+ clearInterval(interval);
4649
+ process.off("SIGTERM", onSignal);
4650
+ process.off("SIGINT", onSignal);
4651
+ };
4652
+ }
4653
+
4654
+ // src/gitignore.ts
4655
+ init_cjs_shims();
4656
+ var import_node_fs19 = require("fs");
4657
+ var import_node_path32 = __toESM(require("path"), 1);
4658
+ var NEAT_OUT_LINE = "neat-out/";
4659
+ var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
4660
+ function isNeatOutLine(line) {
4661
+ const trimmed = line.trim();
4662
+ return trimmed === "neat-out/" || trimmed === "neat-out";
4663
+ }
4664
+ async function ensureNeatOutIgnored(projectDir) {
4665
+ const file = import_node_path32.default.join(projectDir, ".gitignore");
4666
+ let existing = null;
4667
+ try {
4668
+ existing = await import_node_fs19.promises.readFile(file, "utf8");
4669
+ } catch (err) {
4670
+ if (err.code !== "ENOENT") throw err;
4671
+ }
4672
+ if (existing === null) {
4673
+ await import_node_fs19.promises.writeFile(file, `${NEAT_HEADER}
4674
+ ${NEAT_OUT_LINE}
4675
+ `, "utf8");
4676
+ return { action: "created", file };
4677
+ }
4678
+ for (const line of existing.split(/\r?\n/)) {
4679
+ if (isNeatOutLine(line)) return { action: "unchanged", file };
4680
+ }
4681
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
4682
+ const appended = `${needsLeadingNewline ? "\n" : ""}
4683
+ ${NEAT_HEADER}
4684
+ ${NEAT_OUT_LINE}
4685
+ `;
4686
+ await import_node_fs19.promises.writeFile(file, existing + appended, "utf8");
4687
+ return { action: "added", file };
4688
+ }
4689
+
4690
+ // src/summary.ts
4691
+ init_cjs_shims();
4692
+ var import_types21 = require("@neat.is/types");
4693
+ function renderOtelEnvBlock() {
4694
+ return [
4695
+ "for prod OTel routing, set these in your deploy platform's env:",
4696
+ " OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
4697
+ " OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
4698
+ ].join("\n");
4699
+ }
4700
+ function findIncompatServices(nodes) {
4701
+ return nodes.filter(
4702
+ (n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4703
+ );
4704
+ }
4705
+ function servicesWithoutObserved(nodes, edges) {
4706
+ const seen = /* @__PURE__ */ new Set();
4707
+ for (const e of edges) {
4708
+ if (e.provenance === import_types21.Provenance.OBSERVED) {
4709
+ seen.add(e.source);
4710
+ seen.add(e.target);
4711
+ }
4712
+ }
4713
+ return nodes.filter(
4714
+ (n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
4715
+ );
4716
+ }
4717
+ function formatDivergence(d) {
4718
+ const conf = d.confidence.toFixed(2);
4719
+ return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
4720
+ }
4721
+ function renderValueForwardSummary(input) {
4722
+ const { graph, divergences, verbose } = input;
4723
+ const nodes = [];
4724
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
4725
+ const edges = [];
4726
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
4727
+ const lines = [];
4728
+ lines.push("=== neat: findings ===");
4729
+ lines.push("");
4730
+ const incompatServices = findIncompatServices(nodes);
4731
+ const totalIncompats = incompatServices.reduce(
4732
+ (acc, s) => acc + (s.incompatibilities?.length ?? 0),
4733
+ 0
4734
+ );
4735
+ lines.push(`compat violations: ${totalIncompats}`);
4736
+ for (const svc of incompatServices) {
4737
+ for (const inc of svc.incompatibilities ?? []) {
4738
+ const detail = formatIncompat(inc);
4739
+ lines.push(` ${svc.name}: ${detail}`);
4740
+ }
4741
+ }
4742
+ lines.push("");
4743
+ const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
4744
+ lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
4745
+ for (const d of top) lines.push(formatDivergence(d));
4746
+ lines.push("");
4747
+ const noObserved = servicesWithoutObserved(nodes, edges);
4748
+ if (noObserved.length > 0) {
4749
+ lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
4750
+ for (const svc of noObserved) lines.push(` ${svc.name}`);
4751
+ lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
4752
+ lines.push("");
4753
+ }
4754
+ lines.push(renderOtelEnvBlock());
4755
+ lines.push("");
4756
+ if (verbose) {
4757
+ const byNode = /* @__PURE__ */ new Map();
4758
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
4759
+ const byEdge = /* @__PURE__ */ new Map();
4760
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
4761
+ lines.push("=== graph (verbose) ===");
4762
+ lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
4763
+ lines.push("nodes:");
4764
+ for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
4765
+ lines.push("edges:");
4766
+ for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
4767
+ lines.push("");
4768
+ }
4769
+ return lines.join("\n");
4770
+ }
4771
+ function formatIncompat(inc) {
4772
+ if (inc.kind === "node-engine") {
4773
+ const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
4774
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
4775
+ }
4776
+ if (inc.kind === "package-conflict") {
4777
+ const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
4778
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
4779
+ }
4780
+ if (inc.kind === "deprecated-api") {
4781
+ return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
4782
+ }
4783
+ return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
4784
+ }
4785
+
4786
+ // src/watch.ts
4787
+ init_cjs_shims();
4788
+ var import_node_fs23 = __toESM(require("fs"), 1);
4789
+ var import_node_path38 = __toESM(require("path"), 1);
4790
+ var import_chokidar = __toESM(require("chokidar"), 1);
4791
+
4792
+ // src/api.ts
4793
+ init_cjs_shims();
4794
+ var import_fastify = __toESM(require("fastify"), 1);
4795
+ var import_cors = __toESM(require("@fastify/cors"), 1);
4796
+ var import_types23 = require("@neat.is/types");
4797
+
4798
+ // src/diff.ts
4799
+ init_cjs_shims();
4800
+ var import_node_fs20 = require("fs");
4801
+ async function loadSnapshotForDiff(target) {
4802
+ if (/^https?:\/\//i.test(target)) {
4803
+ const res = await fetch(target);
4804
+ if (!res.ok) {
4805
+ throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
4806
+ }
4807
+ return await res.json();
4808
+ }
4809
+ const raw = await import_node_fs20.promises.readFile(target, "utf8");
4810
+ return JSON.parse(raw);
4811
+ }
4812
+ function indexEntries(entries) {
4813
+ const m = /* @__PURE__ */ new Map();
4814
+ if (!entries) return m;
4815
+ for (const entry2 of entries) {
4816
+ const id = entry2.attributes?.id ?? entry2.key;
4817
+ if (!id) continue;
4818
+ m.set(id, entry2.attributes);
4819
+ }
4820
+ return m;
4821
+ }
4822
+ function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4823
+ const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
4824
+ const baseEdges = indexEntries(baseSnapshot.graph?.edges);
4825
+ const liveNodes = /* @__PURE__ */ new Map();
4826
+ liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
4827
+ const liveEdges = /* @__PURE__ */ new Map();
4828
+ liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
4829
+ const result = {
4830
+ base: { exportedAt: baseSnapshot.exportedAt },
4831
+ current: { exportedAt: currentExportedAt },
4832
+ added: { nodes: [], edges: [] },
4833
+ removed: { nodes: [], edges: [] },
4834
+ changed: { nodes: [], edges: [] }
4835
+ };
4836
+ for (const [id, after] of liveNodes) {
4837
+ const before = baseNodes.get(id);
4838
+ if (!before) {
4839
+ result.added.nodes.push(after);
4840
+ } else if (!shallowEqual(before, after)) {
4841
+ result.changed.nodes.push({ id, before, after });
4842
+ }
4843
+ }
4844
+ for (const [id, before] of baseNodes) {
4845
+ if (!liveNodes.has(id)) result.removed.nodes.push(before);
4846
+ }
4847
+ for (const [id, after] of liveEdges) {
4848
+ const before = baseEdges.get(id);
4849
+ if (!before) {
4850
+ result.added.edges.push(after);
4851
+ } else if (!shallowEqual(before, after)) {
4852
+ result.changed.edges.push({ id, before, after });
4853
+ }
4854
+ }
4855
+ for (const [id, before] of baseEdges) {
4592
4856
  if (!liveEdges.has(id)) result.removed.edges.push(before);
4593
4857
  }
4594
4858
  return result;
@@ -4610,23 +4874,23 @@ function canonicalJson(value) {
4610
4874
 
4611
4875
  // src/projects.ts
4612
4876
  init_cjs_shims();
4613
- var import_node_path32 = __toESM(require("path"), 1);
4877
+ var import_node_path33 = __toESM(require("path"), 1);
4614
4878
  function pathsForProject(project, baseDir) {
4615
4879
  if (project === DEFAULT_PROJECT) {
4616
4880
  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")
4881
+ snapshotPath: import_node_path33.default.join(baseDir, "graph.json"),
4882
+ errorsPath: import_node_path33.default.join(baseDir, "errors.ndjson"),
4883
+ staleEventsPath: import_node_path33.default.join(baseDir, "stale-events.ndjson"),
4884
+ embeddingsCachePath: import_node_path33.default.join(baseDir, "embeddings.json"),
4885
+ policyViolationsPath: import_node_path33.default.join(baseDir, "policy-violations.ndjson")
4622
4886
  };
4623
4887
  }
4624
4888
  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`)
4889
+ snapshotPath: import_node_path33.default.join(baseDir, `${project}.json`),
4890
+ errorsPath: import_node_path33.default.join(baseDir, `errors.${project}.ndjson`),
4891
+ staleEventsPath: import_node_path33.default.join(baseDir, `stale-events.${project}.ndjson`),
4892
+ embeddingsCachePath: import_node_path33.default.join(baseDir, `embeddings.${project}.json`),
4893
+ policyViolationsPath: import_node_path33.default.join(baseDir, `policy-violations.${project}.ndjson`)
4630
4894
  };
4631
4895
  }
4632
4896
  var Projects = class {
@@ -4662,49 +4926,49 @@ var Projects = class {
4662
4926
 
4663
4927
  // src/registry.ts
4664
4928
  init_cjs_shims();
4665
- var import_node_fs20 = require("fs");
4929
+ var import_node_fs21 = require("fs");
4666
4930
  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");
4931
+ var import_node_path34 = __toESM(require("path"), 1);
4932
+ var import_types22 = require("@neat.is/types");
4669
4933
  var LOCK_TIMEOUT_MS = 5e3;
4670
4934
  var LOCK_RETRY_MS = 50;
4671
4935
  function neatHome() {
4672
4936
  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");
4937
+ if (override && override.length > 0) return import_node_path34.default.resolve(override);
4938
+ return import_node_path34.default.join(import_node_os2.default.homedir(), ".neat");
4675
4939
  }
4676
4940
  function registryPath() {
4677
- return import_node_path33.default.join(neatHome(), "projects.json");
4941
+ return import_node_path34.default.join(neatHome(), "projects.json");
4678
4942
  }
4679
4943
  function registryLockPath() {
4680
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
4944
+ return import_node_path34.default.join(neatHome(), "projects.json.lock");
4681
4945
  }
4682
4946
  async function normalizeProjectPath(input) {
4683
- const resolved = import_node_path33.default.resolve(input);
4947
+ const resolved = import_node_path34.default.resolve(input);
4684
4948
  try {
4685
- return await import_node_fs20.promises.realpath(resolved);
4949
+ return await import_node_fs21.promises.realpath(resolved);
4686
4950
  } catch {
4687
4951
  return resolved;
4688
4952
  }
4689
4953
  }
4690
4954
  async function writeAtomically(target, contents) {
4691
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4955
+ await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(target), { recursive: true });
4692
4956
  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");
4957
+ const fd = await import_node_fs21.promises.open(tmp, "w");
4694
4958
  try {
4695
4959
  await fd.writeFile(contents, "utf8");
4696
4960
  await fd.sync();
4697
4961
  } finally {
4698
4962
  await fd.close();
4699
4963
  }
4700
- await import_node_fs20.promises.rename(tmp, target);
4964
+ await import_node_fs21.promises.rename(tmp, target);
4701
4965
  }
4702
4966
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4703
4967
  const deadline = Date.now() + timeoutMs;
4704
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4968
+ await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(lockPath), { recursive: true });
4705
4969
  while (true) {
4706
4970
  try {
4707
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
4971
+ const fd = await import_node_fs21.promises.open(lockPath, "wx");
4708
4972
  await fd.close();
4709
4973
  return;
4710
4974
  } catch (err) {
@@ -4720,7 +4984,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4720
4984
  }
4721
4985
  }
4722
4986
  async function releaseLock(lockPath) {
4723
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
4987
+ await import_node_fs21.promises.unlink(lockPath).catch(() => {
4724
4988
  });
4725
4989
  }
4726
4990
  async function withLock(fn) {
@@ -4736,7 +5000,7 @@ async function readRegistry() {
4736
5000
  const file = registryPath();
4737
5001
  let raw;
4738
5002
  try {
4739
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5003
+ raw = await import_node_fs21.promises.readFile(file, "utf8");
4740
5004
  } catch (err) {
4741
5005
  if (err.code === "ENOENT") {
4742
5006
  return { version: 1, projects: [] };
@@ -4744,10 +5008,10 @@ async function readRegistry() {
4744
5008
  throw err;
4745
5009
  }
4746
5010
  const parsed = JSON.parse(raw);
4747
- return import_types21.RegistryFileSchema.parse(parsed);
5011
+ return import_types22.RegistryFileSchema.parse(parsed);
4748
5012
  }
4749
5013
  async function writeRegistry(reg) {
4750
- const validated = import_types21.RegistryFileSchema.parse(reg);
5014
+ const validated = import_types22.RegistryFileSchema.parse(reg);
4751
5015
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4752
5016
  }
4753
5017
  var ProjectNameCollisionError = class extends Error {
@@ -4875,6 +5139,7 @@ data: ${JSON.stringify(envelope.payload)}
4875
5139
  }
4876
5140
 
4877
5141
  // src/api.ts
5142
+ init_auth();
4878
5143
  function serializeGraph(graph) {
4879
5144
  const nodes = [];
4880
5145
  graph.forEachNode((_id, attrs) => {
@@ -4998,11 +5263,11 @@ function registerRoutes(scope, ctx) {
4998
5263
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4999
5264
  const parsed = [];
5000
5265
  for (const c of candidates) {
5001
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5266
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5002
5267
  if (!r.success) {
5003
5268
  return reply.code(400).send({
5004
5269
  error: `unknown divergence type "${c}"`,
5005
- allowed: import_types22.DivergenceTypeSchema.options
5270
+ allowed: import_types23.DivergenceTypeSchema.options
5006
5271
  });
5007
5272
  }
5008
5273
  parsed.push(r.data);
@@ -5147,6 +5412,35 @@ function registerRoutes(scope, ctx) {
5147
5412
  }
5148
5413
  }
5149
5414
  );
5415
+ scope.post("/snapshot", async (req, reply) => {
5416
+ const proj = resolveProject(registry, req, reply);
5417
+ if (!proj) return;
5418
+ const body = req.body;
5419
+ if (!body || typeof body !== "object" || !body.snapshot) {
5420
+ return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
5421
+ }
5422
+ const snap = body.snapshot;
5423
+ if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
5424
+ return reply.code(400).send({
5425
+ error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
5426
+ });
5427
+ }
5428
+ try {
5429
+ const result = mergeSnapshot(proj.graph, snap);
5430
+ return {
5431
+ project: proj.name,
5432
+ nodesAdded: result.nodesAdded,
5433
+ edgesAdded: result.edgesAdded,
5434
+ nodeCount: proj.graph.order,
5435
+ edgeCount: proj.graph.size
5436
+ };
5437
+ } catch (err) {
5438
+ return reply.code(400).send({
5439
+ error: "snapshot merge failed",
5440
+ details: err.message
5441
+ });
5442
+ }
5443
+ });
5150
5444
  scope.post("/graph/scan", async (req, reply) => {
5151
5445
  const proj = resolveProject(registry, req, reply);
5152
5446
  if (!proj) return;
@@ -5186,7 +5480,7 @@ function registerRoutes(scope, ctx) {
5186
5480
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5187
5481
  let violations = await log.readAll();
5188
5482
  if (req.query.severity) {
5189
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5483
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5190
5484
  if (!sev.success) {
5191
5485
  return reply.code(400).send({
5192
5486
  error: "invalid severity",
@@ -5203,7 +5497,7 @@ function registerRoutes(scope, ctx) {
5203
5497
  scope.post("/policies/check", async (req, reply) => {
5204
5498
  const proj = resolveProject(registry, req, reply);
5205
5499
  if (!proj) return;
5206
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5500
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5207
5501
  if (!parsed.success) {
5208
5502
  return reply.code(400).send({
5209
5503
  error: "invalid /policies/check body",
@@ -5240,6 +5534,15 @@ function registerRoutes(scope, ctx) {
5240
5534
  async function buildApi(opts) {
5241
5535
  const app = (0, import_fastify.default)({ logger: false });
5242
5536
  await app.register(import_cors.default, { origin: true });
5537
+ mountBearerAuth(app, {
5538
+ token: opts.authToken,
5539
+ trustProxy: opts.trustProxy,
5540
+ publicRead: opts.publicRead
5541
+ });
5542
+ app.get("/api/config", async () => ({
5543
+ publicRead: opts.publicRead === true,
5544
+ authProxy: opts.trustProxy === true
5545
+ }));
5243
5546
  const startedAt = opts.startedAt ?? Date.now();
5244
5547
  const registry = buildLegacyRegistry(opts);
5245
5548
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5307,9 +5610,9 @@ init_otel_grpc();
5307
5610
 
5308
5611
  // src/search.ts
5309
5612
  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");
5613
+ var import_node_fs22 = require("fs");
5614
+ var import_node_path37 = __toESM(require("path"), 1);
5615
+ var import_node_crypto3 = require("crypto");
5313
5616
  var DEFAULT_LIMIT = 10;
5314
5617
  var NOMIC_DIM = 768;
5315
5618
  var MINI_LM_DIM = 384;
@@ -5349,7 +5652,7 @@ function embedText(node) {
5349
5652
  return parts.join(" ");
5350
5653
  }
5351
5654
  function attrsHash(node) {
5352
- return (0, import_node_crypto.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5655
+ return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
5353
5656
  }
5354
5657
  function cosine(a, b) {
5355
5658
  if (a.length !== b.length) return 0;
@@ -5438,7 +5741,7 @@ async function pickEmbedder() {
5438
5741
  }
5439
5742
  async function readCache(cachePath) {
5440
5743
  try {
5441
- const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
5744
+ const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
5442
5745
  const parsed = JSON.parse(raw);
5443
5746
  if (parsed.version !== 1) return null;
5444
5747
  return parsed;
@@ -5447,8 +5750,8 @@ async function readCache(cachePath) {
5447
5750
  }
5448
5751
  }
5449
5752
  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));
5753
+ await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(cachePath), { recursive: true });
5754
+ await import_node_fs22.promises.writeFile(cachePath, JSON.stringify(cache));
5452
5755
  }
5453
5756
  var VectorIndex = class {
5454
5757
  constructor(embedder, cachePath) {
@@ -5603,8 +5906,8 @@ var ALL_PHASES = [
5603
5906
  ];
5604
5907
  function classifyChange(relPath) {
5605
5908
  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());
5909
+ const base = import_node_path38.default.basename(relPath).toLowerCase();
5910
+ const segments = relPath.split(import_node_path38.default.sep).map((s) => s.toLowerCase());
5608
5911
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
5609
5912
  phases.add("services");
5610
5913
  phases.add("aliases");
@@ -5699,16 +6002,16 @@ function countWatchableDirs(scanPath, limit) {
5699
6002
  if (count >= limit) return;
5700
6003
  let entries;
5701
6004
  try {
5702
- entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
6005
+ entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
5703
6006
  } catch {
5704
6007
  return;
5705
6008
  }
5706
6009
  for (const e of entries) {
5707
6010
  if (count >= limit) return;
5708
6011
  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;
6012
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path38.default.join(dir, e.name) + import_node_path38.default.sep))) continue;
5710
6013
  count++;
5711
- if (depth < 2) visit(import_node_path37.default.join(dir, e.name), depth + 1);
6014
+ if (depth < 2) visit(import_node_path38.default.join(dir, e.name), depth + 1);
5712
6015
  }
5713
6016
  };
5714
6017
  visit(scanPath, 0);
@@ -5726,8 +6029,8 @@ async function startWatch(graph, opts) {
5726
6029
  const projectName = opts.project ?? DEFAULT_PROJECT;
5727
6030
  await loadGraphFromDisk(graph, opts.outPath);
5728
6031
  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");
6032
+ const policyFilePath = import_node_path38.default.join(opts.scanPath, "policy.json");
6033
+ const policyViolationsPath = import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "policy-violations.ndjson");
5731
6034
  let policies = [];
5732
6035
  try {
5733
6036
  policies = await loadPolicyFile(policyFilePath);
@@ -5776,7 +6079,7 @@ async function startWatch(graph, opts) {
5776
6079
  const host = opts.host ?? "0.0.0.0";
5777
6080
  const port = opts.port ?? 8080;
5778
6081
  const otelPort = opts.otelPort ?? 4318;
5779
- const cachePath = opts.embeddingsCachePath ?? import_node_path37.default.join(import_node_path37.default.dirname(opts.outPath), "embeddings.json");
6082
+ const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
5780
6083
  let searchIndex;
5781
6084
  try {
5782
6085
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -5794,7 +6097,7 @@ async function startWatch(graph, opts) {
5794
6097
  // Paths are derived from the explicit options the watch caller passes
5795
6098
  // — pathsForProject is only used to fill in the embeddings/snapshot
5796
6099
  // fields so the registry shape is complete.
5797
- ...pathsForProject(projectName, import_node_path37.default.dirname(opts.outPath)),
6100
+ ...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
5798
6101
  snapshotPath: opts.outPath,
5799
6102
  errorsPath: opts.errorsPath,
5800
6103
  staleEventsPath: opts.staleEventsPath
@@ -5879,9 +6182,9 @@ async function startWatch(graph, opts) {
5879
6182
  };
5880
6183
  const onPath = (absPath) => {
5881
6184
  if (shouldIgnore(absPath)) return;
5882
- const rel = import_node_path37.default.relative(opts.scanPath, absPath);
6185
+ const rel = import_node_path38.default.relative(opts.scanPath, absPath);
5883
6186
  if (!rel || rel.startsWith("..")) return;
5884
- pendingPaths.add(rel.split(import_node_path37.default.sep).join("/"));
6187
+ pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
5885
6188
  const phases = classifyChange(rel);
5886
6189
  if (phases.size === 0) {
5887
6190
  for (const p of ALL_PHASES) pending.add(p);
@@ -5933,13 +6236,158 @@ async function startWatch(graph, opts) {
5933
6236
  return { api, stop };
5934
6237
  }
5935
6238
 
6239
+ // src/deploy/detect.ts
6240
+ init_cjs_shims();
6241
+ var import_node_fs24 = require("fs");
6242
+ var import_node_path39 = __toESM(require("path"), 1);
6243
+ var import_node_child_process = require("child_process");
6244
+ var import_node_crypto4 = require("crypto");
6245
+ function generateToken() {
6246
+ return (0, import_node_crypto4.randomBytes)(32).toString("base64url");
6247
+ }
6248
+ async function probeBinary(binary, arg) {
6249
+ return new Promise((resolve) => {
6250
+ const child = (0, import_node_child_process.spawn)(binary, [arg], { stdio: "ignore" });
6251
+ const timer = setTimeout(() => {
6252
+ child.kill("SIGKILL");
6253
+ resolve(false);
6254
+ }, 2e3);
6255
+ child.once("error", () => {
6256
+ clearTimeout(timer);
6257
+ resolve(false);
6258
+ });
6259
+ child.once("exit", (code) => {
6260
+ clearTimeout(timer);
6261
+ resolve(code === 0);
6262
+ });
6263
+ });
6264
+ }
6265
+ async function detectSubstrate(opts = {}) {
6266
+ const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
6267
+ const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
6268
+ if (await hasDocker()) return "docker-compose";
6269
+ if (await hasSystemd()) return "systemd";
6270
+ return "docker-run";
6271
+ }
6272
+ var IMAGE = "ghcr.io/neat-technologies/neat:latest";
6273
+ function emitDockerCompose(cwd) {
6274
+ return [
6275
+ "services:",
6276
+ " neat:",
6277
+ ` image: ${IMAGE}`,
6278
+ " restart: unless-stopped",
6279
+ " environment:",
6280
+ " NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
6281
+ " ports:",
6282
+ ' - "8080:8080"',
6283
+ ' - "4318:4318"',
6284
+ ' - "6328:6328"',
6285
+ " volumes:",
6286
+ ` - ${cwd}:/workspace`,
6287
+ " - ./neat-data:/neat-out",
6288
+ ""
6289
+ ].join("\n");
6290
+ }
6291
+ function emitSystemdUnit(cwd) {
6292
+ return [
6293
+ "# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
6294
+ "# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
6295
+ "[Unit]",
6296
+ "Description=NEAT daemon",
6297
+ "After=network-online.target",
6298
+ "Wants=network-online.target",
6299
+ "",
6300
+ "[Service]",
6301
+ "Type=simple",
6302
+ "ExecStart=/usr/local/bin/neatd start --foreground",
6303
+ `WorkingDirectory=${cwd}`,
6304
+ "EnvironmentFile=/etc/neat/neatd.env",
6305
+ "Restart=always",
6306
+ "RestartSec=5",
6307
+ "",
6308
+ "[Install]",
6309
+ "WantedBy=multi-user.target",
6310
+ ""
6311
+ ].join("\n");
6312
+ }
6313
+ function emitDockerRunSnippet() {
6314
+ return [
6315
+ "#!/usr/bin/env bash",
6316
+ "set -euo pipefail",
6317
+ "",
6318
+ "# Generate a fresh token once, then store it where your secrets live.",
6319
+ ': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
6320
+ "",
6321
+ `docker run -d --name neat \\`,
6322
+ ' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
6323
+ " -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
6324
+ ' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
6325
+ ` ${IMAGE}`,
6326
+ ""
6327
+ ].join("\n");
6328
+ }
6329
+ function renderOtelEnvBlock2(token, host = "<host>") {
6330
+ return [
6331
+ `OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
6332
+ `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
6333
+ "OTEL_SERVICE_NAME=<service>"
6334
+ ].join("\n");
6335
+ }
6336
+ async function runDeploy(opts = {}) {
6337
+ const cwd = opts.cwd ?? process.cwd();
6338
+ const substrate = await detectSubstrate(opts);
6339
+ const token = generateToken();
6340
+ switch (substrate) {
6341
+ case "docker-compose": {
6342
+ const artifactPath = import_node_path39.default.join(cwd, "docker-compose.neat.yml");
6343
+ const contents = emitDockerCompose(cwd);
6344
+ await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
6345
+ return {
6346
+ substrate,
6347
+ artifactPath,
6348
+ token,
6349
+ contents,
6350
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path39.default.basename(artifactPath)} up -d`
6351
+ };
6352
+ }
6353
+ case "systemd": {
6354
+ const artifactPath = import_node_path39.default.join(cwd, "neat.service");
6355
+ const contents = emitSystemdUnit(cwd);
6356
+ await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
6357
+ return {
6358
+ substrate,
6359
+ artifactPath,
6360
+ token,
6361
+ contents,
6362
+ startCommand: [
6363
+ `sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
6364
+ `sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
6365
+ "sudo systemctl daemon-reload && sudo systemctl enable --now neat"
6366
+ ].join(" && ")
6367
+ };
6368
+ }
6369
+ case "docker-run":
6370
+ default: {
6371
+ const contents = emitDockerRunSnippet();
6372
+ return {
6373
+ substrate: "docker-run",
6374
+ token,
6375
+ contents,
6376
+ startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
6377
+ ${contents}EOF
6378
+ )`
6379
+ };
6380
+ }
6381
+ }
6382
+ }
6383
+
5936
6384
  // src/installers/index.ts
5937
6385
  init_cjs_shims();
5938
6386
 
5939
6387
  // src/installers/javascript.ts
5940
6388
  init_cjs_shims();
5941
- var import_node_fs23 = require("fs");
5942
- var import_node_path38 = __toESM(require("path"), 1);
6389
+ var import_node_fs25 = require("fs");
6390
+ var import_node_path40 = __toESM(require("path"), 1);
5943
6391
 
5944
6392
  // src/installers/templates.ts
5945
6393
  init_cjs_shims();
@@ -5987,6 +6435,151 @@ function renderEnvNeat(serviceName) {
5987
6435
  ""
5988
6436
  ].join("\n");
5989
6437
  }
6438
+ var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
6439
+ var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
6440
+ export async function register() {
6441
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
6442
+ await import('./instrumentation.node')
6443
+ }
6444
+ }
6445
+ `;
6446
+ var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
6447
+ export async function register() {
6448
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
6449
+ await import('./instrumentation.node')
6450
+ }
6451
+ }
6452
+ `;
6453
+ var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6454
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6455
+ // the OTel SDK starts so the exporter target and service name are in scope.
6456
+ import { fileURLToPath } from 'node:url'
6457
+ import path from 'node:path'
6458
+ import dotenv from 'dotenv'
6459
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6460
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6461
+
6462
+ const here = path.dirname(fileURLToPath(import.meta.url))
6463
+ dotenv.config({ path: path.join(here, '.env.neat') })
6464
+
6465
+ const sdk = new NodeSDK({
6466
+ serviceName: process.env.OTEL_SERVICE_NAME,
6467
+ instrumentations: [getNodeAutoInstrumentations()],
6468
+ })
6469
+ sdk.start()
6470
+ `;
6471
+ var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6472
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6473
+ // the OTel SDK starts so the exporter target and service name are in scope.
6474
+ import { fileURLToPath } from 'node:url'
6475
+ import path from 'node:path'
6476
+ import dotenv from 'dotenv'
6477
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6478
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6479
+
6480
+ const here = path.dirname(fileURLToPath(import.meta.url))
6481
+ dotenv.config({ path: path.join(here, '.env.neat') })
6482
+
6483
+ const sdk = new NodeSDK({
6484
+ serviceName: process.env.OTEL_SERVICE_NAME,
6485
+ instrumentations: [getNodeAutoInstrumentations()],
6486
+ })
6487
+ sdk.start()
6488
+ `;
6489
+ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6490
+ var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6491
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6492
+ // the OTel SDK starts so the exporter target and service name are in scope.
6493
+ import { fileURLToPath } from 'node:url'
6494
+ import path from 'node:path'
6495
+ import dotenv from 'dotenv'
6496
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6497
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6498
+
6499
+ const here = path.dirname(fileURLToPath(import.meta.url))
6500
+ dotenv.config({ path: path.join(here, '.env.neat') })
6501
+
6502
+ const sdk = new NodeSDK({
6503
+ serviceName: process.env.OTEL_SERVICE_NAME,
6504
+ instrumentations: [getNodeAutoInstrumentations()],
6505
+ })
6506
+ sdk.start()
6507
+ `;
6508
+ var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6509
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6510
+ // the OTel SDK starts so the exporter target and service name are in scope.
6511
+ const path = require('node:path')
6512
+ try {
6513
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6514
+ } catch (err) {
6515
+ // dotenv unavailable \u2014 fall through to process.env.
6516
+ }
6517
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6518
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6519
+
6520
+ const sdk = new NodeSDK({
6521
+ serviceName: process.env.OTEL_SERVICE_NAME,
6522
+ instrumentations: [getNodeAutoInstrumentations()],
6523
+ })
6524
+ sdk.start()
6525
+ `;
6526
+ var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6527
+ var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6528
+ var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6529
+ var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6530
+ var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6531
+ import './otel-init'
6532
+
6533
+ import type { Handle } from '@sveltejs/kit'
6534
+
6535
+ export const handle: Handle = async ({ event, resolve }) => {
6536
+ return resolve(event)
6537
+ }
6538
+ `;
6539
+ var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6540
+ import './otel-init'
6541
+
6542
+ /** @type {import('@sveltejs/kit').Handle} */
6543
+ export const handle = async ({ event, resolve }) => {
6544
+ return resolve(event)
6545
+ }
6546
+ `;
6547
+ var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6548
+ var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6549
+ var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6550
+ import './otel-init'
6551
+
6552
+ export default defineNitroPlugin(() => {
6553
+ // OTel SDK is initialised at module-load via the side-effect import above.
6554
+ })
6555
+ `;
6556
+ var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6557
+ require('./otel-init')
6558
+
6559
+ module.exports = defineNitroPlugin(() => {
6560
+ // OTel SDK is initialised at module-load via the side-effect import above.
6561
+ })
6562
+ `;
6563
+ var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6564
+ var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6565
+ var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6566
+ import './otel-init'
6567
+
6568
+ import { defineMiddleware } from 'astro:middleware'
6569
+
6570
+ export const onRequest = defineMiddleware(async (context, next) => {
6571
+ return next()
6572
+ })
6573
+ `;
6574
+ var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6575
+ import './otel-init'
6576
+
6577
+ import { defineMiddleware } from 'astro:middleware'
6578
+
6579
+ export const onRequest = defineMiddleware(async (context, next) => {
6580
+ return next()
6581
+ })
6582
+ `;
5990
6583
 
5991
6584
  // src/installers/javascript.ts
5992
6585
  var SDK_PACKAGES = [
@@ -6009,7 +6602,7 @@ var OTEL_ENV = {
6009
6602
  };
6010
6603
  async function readPackageJson(serviceDir) {
6011
6604
  try {
6012
- const raw = await import_node_fs23.promises.readFile(import_node_path38.default.join(serviceDir, "package.json"), "utf8");
6605
+ const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
6013
6606
  return JSON.parse(raw);
6014
6607
  } catch {
6015
6608
  return null;
@@ -6017,7 +6610,7 @@ async function readPackageJson(serviceDir) {
6017
6610
  }
6018
6611
  async function exists2(p) {
6019
6612
  try {
6020
- await import_node_fs23.promises.stat(p);
6613
+ await import_node_fs25.promises.stat(p);
6021
6614
  return true;
6022
6615
  } catch {
6023
6616
  return false;
@@ -6027,6 +6620,93 @@ async function detect(serviceDir) {
6027
6620
  const pkg = await readPackageJson(serviceDir);
6028
6621
  return pkg !== null && typeof pkg.name === "string";
6029
6622
  }
6623
+ var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
6624
+ async function findNextConfig(serviceDir) {
6625
+ for (const name of NEXT_CONFIG_CANDIDATES) {
6626
+ const candidate = import_node_path40.default.join(serviceDir, name);
6627
+ if (await exists2(candidate)) return candidate;
6628
+ }
6629
+ return null;
6630
+ }
6631
+ function hasNextDependency(pkg) {
6632
+ return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
6633
+ }
6634
+ function allDeps(pkg) {
6635
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6636
+ }
6637
+ var REMIX_ENTRY_CANDIDATES = [
6638
+ "app/entry.server.ts",
6639
+ "app/entry.server.tsx",
6640
+ "app/entry.server.js",
6641
+ "app/entry.server.jsx"
6642
+ ];
6643
+ function hasRemixDependency(pkg) {
6644
+ const deps = allDeps(pkg);
6645
+ if ("remix" in deps) return true;
6646
+ for (const name of Object.keys(deps)) {
6647
+ if (name.startsWith("@remix-run/")) return true;
6648
+ }
6649
+ return false;
6650
+ }
6651
+ async function findRemixEntry(serviceDir) {
6652
+ for (const rel of REMIX_ENTRY_CANDIDATES) {
6653
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6654
+ if (await exists2(candidate)) return candidate;
6655
+ }
6656
+ return null;
6657
+ }
6658
+ var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
6659
+ var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
6660
+ function hasSvelteKitDependency(pkg) {
6661
+ return "@sveltejs/kit" in allDeps(pkg);
6662
+ }
6663
+ async function findSvelteKitHooks(serviceDir) {
6664
+ for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
6665
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6666
+ if (await exists2(candidate)) return candidate;
6667
+ }
6668
+ return null;
6669
+ }
6670
+ async function findSvelteKitConfig(serviceDir) {
6671
+ for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
6672
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6673
+ if (await exists2(candidate)) return candidate;
6674
+ }
6675
+ return null;
6676
+ }
6677
+ var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
6678
+ function hasNuxtDependency(pkg) {
6679
+ return "nuxt" in allDeps(pkg);
6680
+ }
6681
+ async function findNuxtConfig(serviceDir) {
6682
+ for (const name of NUXT_CONFIG_CANDIDATES) {
6683
+ const candidate = import_node_path40.default.join(serviceDir, name);
6684
+ if (await exists2(candidate)) return candidate;
6685
+ }
6686
+ return null;
6687
+ }
6688
+ var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
6689
+ function hasAstroDependency(pkg) {
6690
+ return "astro" in allDeps(pkg);
6691
+ }
6692
+ async function findAstroConfig(serviceDir) {
6693
+ for (const name of ASTRO_CONFIG_CANDIDATES) {
6694
+ const candidate = import_node_path40.default.join(serviceDir, name);
6695
+ if (await exists2(candidate)) return candidate;
6696
+ }
6697
+ return null;
6698
+ }
6699
+ function parseNextMajor(range) {
6700
+ if (!range) return null;
6701
+ const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
6702
+ const match = cleaned.match(/^(\d+)/);
6703
+ if (!match) return null;
6704
+ const n = Number(match[1]);
6705
+ return Number.isFinite(n) ? n : null;
6706
+ }
6707
+ async function isTypeScriptProject(serviceDir) {
6708
+ return exists2(import_node_path40.default.join(serviceDir, "tsconfig.json"));
6709
+ }
6030
6710
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
6031
6711
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
6032
6712
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
@@ -6071,7 +6751,7 @@ function entryFromScript(script) {
6071
6751
  }
6072
6752
  async function resolveEntry(serviceDir, pkg) {
6073
6753
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
6074
- const candidate = import_node_path38.default.resolve(serviceDir, pkg.main);
6754
+ const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
6075
6755
  if (await exists2(candidate)) return candidate;
6076
6756
  }
6077
6757
  if (pkg.bin) {
@@ -6085,36 +6765,36 @@ async function resolveEntry(serviceDir, pkg) {
6085
6765
  if (typeof first === "string") binEntry = first;
6086
6766
  }
6087
6767
  if (binEntry) {
6088
- const candidate = import_node_path38.default.resolve(serviceDir, binEntry);
6768
+ const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
6089
6769
  if (await exists2(candidate)) return candidate;
6090
6770
  }
6091
6771
  }
6092
6772
  const startEntry = entryFromScript(pkg.scripts?.start);
6093
6773
  if (startEntry) {
6094
- const candidate = import_node_path38.default.resolve(serviceDir, startEntry);
6774
+ const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
6095
6775
  if (await exists2(candidate)) return candidate;
6096
6776
  }
6097
6777
  const devEntry = entryFromScript(pkg.scripts?.dev);
6098
6778
  if (devEntry) {
6099
- const candidate = import_node_path38.default.resolve(serviceDir, devEntry);
6779
+ const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
6100
6780
  if (await exists2(candidate)) return candidate;
6101
6781
  }
6102
6782
  for (const rel of SRC_INDEX_CANDIDATES) {
6103
- const candidate = import_node_path38.default.join(serviceDir, rel);
6783
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6104
6784
  if (await exists2(candidate)) return candidate;
6105
6785
  }
6106
6786
  for (const rel of SRC_NAMED_CANDIDATES) {
6107
- const candidate = import_node_path38.default.join(serviceDir, rel);
6787
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6108
6788
  if (await exists2(candidate)) return candidate;
6109
6789
  }
6110
6790
  for (const name of INDEX_CANDIDATES) {
6111
- const candidate = import_node_path38.default.join(serviceDir, name);
6791
+ const candidate = import_node_path40.default.join(serviceDir, name);
6112
6792
  if (await exists2(candidate)) return candidate;
6113
6793
  }
6114
6794
  return null;
6115
6795
  }
6116
6796
  function dispatchEntry(entryFile, pkg) {
6117
- const ext = import_node_path38.default.extname(entryFile).toLowerCase();
6797
+ const ext = import_node_path40.default.extname(entryFile).toLowerCase();
6118
6798
  if (ext === ".ts" || ext === ".tsx") return "ts";
6119
6799
  if (ext === ".mjs") return "esm";
6120
6800
  if (ext === ".cjs") return "cjs";
@@ -6131,9 +6811,9 @@ function otelInitContents(flavor) {
6131
6811
  return OTEL_INIT_CJS;
6132
6812
  }
6133
6813
  function injectionLine(flavor, entryFile, otelInitFile) {
6134
- let rel = import_node_path38.default.relative(import_node_path38.default.dirname(entryFile), otelInitFile);
6814
+ let rel = import_node_path40.default.relative(import_node_path40.default.dirname(entryFile), otelInitFile);
6135
6815
  if (!rel.startsWith(".")) rel = `./${rel}`;
6136
- rel = rel.split(import_node_path38.default.sep).join("/");
6816
+ rel = rel.split(import_node_path40.default.sep).join("/");
6137
6817
  if (flavor === "cjs") return `require('${rel}')`;
6138
6818
  if (flavor === "esm") return `import '${rel}'`;
6139
6819
  const tsRel = rel.replace(/\.ts$/, "");
@@ -6144,9 +6824,358 @@ function lineIsOtelInjection(line) {
6144
6824
  if (trimmed.length === 0) return false;
6145
6825
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6146
6826
  }
6827
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6828
+ const useTs = await isTypeScriptProject(serviceDir);
6829
+ const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6830
+ const instrumentationNodeFile = import_node_path40.default.join(
6831
+ serviceDir,
6832
+ useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
6833
+ );
6834
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6835
+ const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6836
+ const dependencyEdits = [];
6837
+ for (const sdk of SDK_PACKAGES) {
6838
+ if (sdk.name in existingDeps) continue;
6839
+ dependencyEdits.push({
6840
+ file: manifestPath,
6841
+ kind: "add",
6842
+ name: sdk.name,
6843
+ version: sdk.version
6844
+ });
6845
+ }
6846
+ const generatedFiles = [];
6847
+ if (!await exists2(instrumentationFile)) {
6848
+ generatedFiles.push({
6849
+ file: instrumentationFile,
6850
+ contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
6851
+ skipIfExists: true
6852
+ });
6853
+ }
6854
+ if (!await exists2(instrumentationNodeFile)) {
6855
+ generatedFiles.push({
6856
+ file: instrumentationNodeFile,
6857
+ contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
6858
+ skipIfExists: true
6859
+ });
6860
+ }
6861
+ if (!await exists2(envNeatFile)) {
6862
+ generatedFiles.push({
6863
+ file: envNeatFile,
6864
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6865
+ skipIfExists: true
6866
+ });
6867
+ }
6868
+ let nextConfigEdit;
6869
+ const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
6870
+ const nextMajor = parseNextMajor(nextRange);
6871
+ if (nextMajor !== null && nextMajor < 15) {
6872
+ try {
6873
+ const raw = await import_node_fs25.promises.readFile(nextConfigPath, "utf8");
6874
+ if (!raw.includes("instrumentationHook")) {
6875
+ nextConfigEdit = {
6876
+ file: nextConfigPath,
6877
+ reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
6878
+ };
6879
+ }
6880
+ } catch {
6881
+ }
6882
+ }
6883
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
6884
+ if (empty) {
6885
+ return {
6886
+ language: "javascript",
6887
+ serviceDir,
6888
+ dependencyEdits: [],
6889
+ entrypointEdits: [],
6890
+ envEdits: [],
6891
+ generatedFiles: [],
6892
+ framework: "next"
6893
+ };
6894
+ }
6895
+ return {
6896
+ language: "javascript",
6897
+ serviceDir,
6898
+ dependencyEdits,
6899
+ entrypointEdits: [],
6900
+ envEdits: [OTEL_ENV],
6901
+ generatedFiles,
6902
+ framework: "next",
6903
+ ...nextConfigEdit ? { nextConfigEdit } : {}
6904
+ };
6905
+ }
6906
+ function buildDependencyEdits(pkg, manifestPath) {
6907
+ const existingDeps = allDeps(pkg);
6908
+ const edits = [];
6909
+ for (const sdk of SDK_PACKAGES) {
6910
+ if (sdk.name in existingDeps) continue;
6911
+ edits.push({
6912
+ file: manifestPath,
6913
+ kind: "add",
6914
+ name: sdk.name,
6915
+ version: sdk.version
6916
+ });
6917
+ }
6918
+ return edits;
6919
+ }
6920
+ async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
6921
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6922
+ if (!await exists2(envNeatFile)) {
6923
+ generatedFiles.push({
6924
+ file: envNeatFile,
6925
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6926
+ skipIfExists: true
6927
+ });
6928
+ }
6929
+ }
6930
+ function fileImportsOtelHook(raw, specifiers) {
6931
+ const lines = raw.split(/\r?\n/);
6932
+ for (const line of lines) {
6933
+ const trimmed = line.trim();
6934
+ for (const spec of specifiers) {
6935
+ const escaped = spec.replace(/\./g, "\\.");
6936
+ const pattern = new RegExp(
6937
+ `(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
6938
+ );
6939
+ if (pattern.test(trimmed)) return true;
6940
+ }
6941
+ }
6942
+ return false;
6943
+ }
6944
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6945
+ const useTs = await isTypeScriptProject(serviceDir);
6946
+ const otelServerFile = import_node_path40.default.join(
6947
+ serviceDir,
6948
+ useTs ? "app/otel.server.ts" : "app/otel.server.js"
6949
+ );
6950
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
6951
+ const generatedFiles = [];
6952
+ if (!await exists2(otelServerFile)) {
6953
+ generatedFiles.push({
6954
+ file: otelServerFile,
6955
+ contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
6956
+ skipIfExists: true
6957
+ });
6958
+ }
6959
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
6960
+ const entrypointEdits = [];
6961
+ try {
6962
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
6963
+ if (!fileImportsOtelHook(raw, ["./otel.server"])) {
6964
+ const lines = raw.split(/\r?\n/);
6965
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6966
+ entrypointEdits.push({
6967
+ file: entryFile,
6968
+ before: firstReal,
6969
+ after: "import './otel.server'"
6970
+ });
6971
+ }
6972
+ } catch {
6973
+ }
6974
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
6975
+ if (empty) {
6976
+ return {
6977
+ language: "javascript",
6978
+ serviceDir,
6979
+ dependencyEdits: [],
6980
+ entrypointEdits: [],
6981
+ envEdits: [],
6982
+ generatedFiles: [],
6983
+ framework: "remix"
6984
+ };
6985
+ }
6986
+ return {
6987
+ language: "javascript",
6988
+ serviceDir,
6989
+ dependencyEdits,
6990
+ entrypointEdits,
6991
+ envEdits: [OTEL_ENV],
6992
+ generatedFiles,
6993
+ framework: "remix"
6994
+ };
6995
+ }
6996
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
6997
+ const useTs = await isTypeScriptProject(serviceDir);
6998
+ const otelInitFile = import_node_path40.default.join(
6999
+ serviceDir,
7000
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7001
+ );
7002
+ const resolvedHooksFile = hooksFile ?? import_node_path40.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
7003
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7004
+ const generatedFiles = [];
7005
+ const entrypointEdits = [];
7006
+ if (!await exists2(otelInitFile)) {
7007
+ generatedFiles.push({
7008
+ file: otelInitFile,
7009
+ contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7010
+ skipIfExists: true
7011
+ });
7012
+ }
7013
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7014
+ if (hooksFile === null) {
7015
+ generatedFiles.push({
7016
+ file: resolvedHooksFile,
7017
+ contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
7018
+ skipIfExists: true
7019
+ });
7020
+ } else {
7021
+ try {
7022
+ const raw = await import_node_fs25.promises.readFile(hooksFile, "utf8");
7023
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7024
+ const lines = raw.split(/\r?\n/);
7025
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7026
+ entrypointEdits.push({
7027
+ file: hooksFile,
7028
+ before: firstReal,
7029
+ after: "import './otel-init'"
7030
+ });
7031
+ }
7032
+ } catch {
7033
+ }
7034
+ }
7035
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7036
+ if (empty) {
7037
+ return {
7038
+ language: "javascript",
7039
+ serviceDir,
7040
+ dependencyEdits: [],
7041
+ entrypointEdits: [],
7042
+ envEdits: [],
7043
+ generatedFiles: [],
7044
+ framework: "sveltekit"
7045
+ };
7046
+ }
7047
+ return {
7048
+ language: "javascript",
7049
+ serviceDir,
7050
+ dependencyEdits,
7051
+ entrypointEdits,
7052
+ envEdits: [OTEL_ENV],
7053
+ generatedFiles,
7054
+ framework: "sveltekit"
7055
+ };
7056
+ }
7057
+ async function planNuxt(serviceDir, pkg, manifestPath) {
7058
+ const useTs = await isTypeScriptProject(serviceDir);
7059
+ const otelPluginFile = import_node_path40.default.join(
7060
+ serviceDir,
7061
+ useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
7062
+ );
7063
+ const otelInitFile = import_node_path40.default.join(
7064
+ serviceDir,
7065
+ useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
7066
+ );
7067
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7068
+ const generatedFiles = [];
7069
+ if (!await exists2(otelInitFile)) {
7070
+ generatedFiles.push({
7071
+ file: otelInitFile,
7072
+ contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7073
+ skipIfExists: true
7074
+ });
7075
+ }
7076
+ if (!await exists2(otelPluginFile)) {
7077
+ generatedFiles.push({
7078
+ file: otelPluginFile,
7079
+ contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
7080
+ skipIfExists: true
7081
+ });
7082
+ }
7083
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7084
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
7085
+ if (empty) {
7086
+ return {
7087
+ language: "javascript",
7088
+ serviceDir,
7089
+ dependencyEdits: [],
7090
+ entrypointEdits: [],
7091
+ envEdits: [],
7092
+ generatedFiles: [],
7093
+ framework: "nuxt"
7094
+ };
7095
+ }
7096
+ return {
7097
+ language: "javascript",
7098
+ serviceDir,
7099
+ dependencyEdits,
7100
+ entrypointEdits: [],
7101
+ envEdits: [OTEL_ENV],
7102
+ generatedFiles,
7103
+ framework: "nuxt"
7104
+ };
7105
+ }
7106
+ var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
7107
+ async function findAstroMiddleware(serviceDir) {
7108
+ for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
7109
+ const candidate = import_node_path40.default.join(serviceDir, rel);
7110
+ if (await exists2(candidate)) return candidate;
7111
+ }
7112
+ return null;
7113
+ }
7114
+ async function planAstro(serviceDir, pkg, manifestPath) {
7115
+ const useTs = await isTypeScriptProject(serviceDir);
7116
+ const otelInitFile = import_node_path40.default.join(
7117
+ serviceDir,
7118
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7119
+ );
7120
+ const existingMiddleware = await findAstroMiddleware(serviceDir);
7121
+ const middlewareFile = existingMiddleware ?? import_node_path40.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
7122
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7123
+ const generatedFiles = [];
7124
+ const entrypointEdits = [];
7125
+ if (!await exists2(otelInitFile)) {
7126
+ generatedFiles.push({
7127
+ file: otelInitFile,
7128
+ contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7129
+ skipIfExists: true
7130
+ });
7131
+ }
7132
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7133
+ if (existingMiddleware === null) {
7134
+ generatedFiles.push({
7135
+ file: middlewareFile,
7136
+ contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
7137
+ skipIfExists: true
7138
+ });
7139
+ } else {
7140
+ try {
7141
+ const raw = await import_node_fs25.promises.readFile(existingMiddleware, "utf8");
7142
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7143
+ const lines = raw.split(/\r?\n/);
7144
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7145
+ entrypointEdits.push({
7146
+ file: existingMiddleware,
7147
+ before: firstReal,
7148
+ after: "import './otel-init'"
7149
+ });
7150
+ }
7151
+ } catch {
7152
+ }
7153
+ }
7154
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7155
+ if (empty) {
7156
+ return {
7157
+ language: "javascript",
7158
+ serviceDir,
7159
+ dependencyEdits: [],
7160
+ entrypointEdits: [],
7161
+ envEdits: [],
7162
+ generatedFiles: [],
7163
+ framework: "astro"
7164
+ };
7165
+ }
7166
+ return {
7167
+ language: "javascript",
7168
+ serviceDir,
7169
+ dependencyEdits,
7170
+ entrypointEdits,
7171
+ envEdits: [OTEL_ENV],
7172
+ generatedFiles,
7173
+ framework: "astro"
7174
+ };
7175
+ }
6147
7176
  async function plan(serviceDir) {
6148
7177
  const pkg = await readPackageJson(serviceDir);
6149
- const manifestPath = import_node_path38.default.join(serviceDir, "package.json");
7178
+ const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
6150
7179
  const empty = {
6151
7180
  language: "javascript",
6152
7181
  serviceDir,
@@ -6156,13 +7185,44 @@ async function plan(serviceDir) {
6156
7185
  generatedFiles: []
6157
7186
  };
6158
7187
  if (!pkg) return empty;
7188
+ if (hasNextDependency(pkg)) {
7189
+ const nextConfig = await findNextConfig(serviceDir);
7190
+ if (nextConfig) {
7191
+ return planNext(serviceDir, pkg, manifestPath, nextConfig);
7192
+ }
7193
+ }
7194
+ if (hasRemixDependency(pkg)) {
7195
+ const remixEntry = await findRemixEntry(serviceDir);
7196
+ if (remixEntry) {
7197
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry);
7198
+ }
7199
+ }
7200
+ if (hasSvelteKitDependency(pkg)) {
7201
+ const hooks = await findSvelteKitHooks(serviceDir);
7202
+ const config = await findSvelteKitConfig(serviceDir);
7203
+ if (hooks || config) {
7204
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
7205
+ }
7206
+ }
7207
+ if (hasNuxtDependency(pkg)) {
7208
+ const nuxtConfig = await findNuxtConfig(serviceDir);
7209
+ if (nuxtConfig) {
7210
+ return planNuxt(serviceDir, pkg, manifestPath);
7211
+ }
7212
+ }
7213
+ if (hasAstroDependency(pkg)) {
7214
+ const astroConfig = await findAstroConfig(serviceDir);
7215
+ if (astroConfig) {
7216
+ return planAstro(serviceDir, pkg, manifestPath);
7217
+ }
7218
+ }
6159
7219
  const entryFile = await resolveEntry(serviceDir, pkg);
6160
7220
  if (!entryFile) {
6161
7221
  return { ...empty, libOnly: true };
6162
7222
  }
6163
7223
  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");
7224
+ const otelInitFile = import_node_path40.default.join(import_node_path40.default.dirname(entryFile), otelInitFilename(flavor));
7225
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6166
7226
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6167
7227
  const dependencyEdits = [];
6168
7228
  for (const sdk of SDK_PACKAGES) {
@@ -6176,7 +7236,7 @@ async function plan(serviceDir) {
6176
7236
  }
6177
7237
  const entrypointEdits = [];
6178
7238
  try {
6179
- const raw = await import_node_fs23.promises.readFile(entryFile, "utf8");
7239
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
6180
7240
  const lines = raw.split(/\r?\n/);
6181
7241
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6182
7242
  if (!lineIsOtelInjection(firstReal)) {
@@ -6201,7 +7261,7 @@ async function plan(serviceDir) {
6201
7261
  if (!await exists2(envNeatFile)) {
6202
7262
  generatedFiles.push({
6203
7263
  file: envNeatFile,
6204
- contents: renderEnvNeat(pkg.name ?? import_node_path38.default.basename(serviceDir)),
7264
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6205
7265
  skipIfExists: true
6206
7266
  });
6207
7267
  }
@@ -6220,18 +7280,29 @@ async function plan(serviceDir) {
6220
7280
  };
6221
7281
  }
6222
7282
  function isAllowedWritePath(serviceDir, target) {
6223
- const rel = import_node_path38.default.relative(serviceDir, target);
7283
+ const rel = import_node_path40.default.relative(serviceDir, target);
6224
7284
  if (rel.startsWith("..")) return false;
6225
- const base = import_node_path38.default.basename(target);
7285
+ const base = import_node_path40.default.basename(target);
6226
7286
  if (base === "package.json") return true;
6227
7287
  if (base === ".env.neat") return true;
6228
7288
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7289
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7290
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7291
+ const relPosix = rel.split(import_node_path40.default.sep).join("/");
7292
+ if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
7293
+ if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
7294
+ if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
7295
+ if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
7296
+ if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
7297
+ if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
7298
+ if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
6229
7299
  return false;
6230
7300
  }
6231
7301
  async function writeAtomic(file, contents) {
7302
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
6232
7303
  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);
7304
+ await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
7305
+ await import_node_fs25.promises.rename(tmp, file);
6235
7306
  }
6236
7307
  async function apply(installPlan) {
6237
7308
  const { serviceDir } = installPlan;
@@ -6243,7 +7314,7 @@ async function apply(installPlan) {
6243
7314
  writtenFiles: []
6244
7315
  };
6245
7316
  }
6246
- if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
7317
+ if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
6247
7318
  return {
6248
7319
  serviceDir,
6249
7320
  outcome: "already-instrumented",
@@ -6254,6 +7325,7 @@ async function apply(installPlan) {
6254
7325
  for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
6255
7326
  for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
6256
7327
  for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
7328
+ if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
6257
7329
  for (const target of allTargets) {
6258
7330
  const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
6259
7331
  if (isEntryEdit) continue;
@@ -6268,7 +7340,7 @@ async function apply(installPlan) {
6268
7340
  for (const target of allTargets) {
6269
7341
  if (await exists2(target)) {
6270
7342
  try {
6271
- originals.set(target, await import_node_fs23.promises.readFile(target, "utf8"));
7343
+ originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
6272
7344
  } catch {
6273
7345
  }
6274
7346
  }
@@ -6323,6 +7395,17 @@ async function apply(installPlan) {
6323
7395
  await writeAtomic(ep.file, newRaw);
6324
7396
  writtenFiles.push(ep.file);
6325
7397
  }
7398
+ if (installPlan.nextConfigEdit) {
7399
+ const target = installPlan.nextConfigEdit.file;
7400
+ const raw = originals.get(target);
7401
+ if (raw !== void 0 && !raw.includes("instrumentationHook")) {
7402
+ const updated = injectInstrumentationHook(raw);
7403
+ if (updated !== null) {
7404
+ await writeAtomic(target, updated);
7405
+ writtenFiles.push(target);
7406
+ }
7407
+ }
7408
+ }
6326
7409
  } catch (err) {
6327
7410
  await rollback(installPlan, originals, createdFiles);
6328
7411
  throw err;
@@ -6338,14 +7421,14 @@ async function rollback(installPlan, originals, createdFiles) {
6338
7421
  const removed = [];
6339
7422
  for (const [file, raw] of originals.entries()) {
6340
7423
  try {
6341
- await import_node_fs23.promises.writeFile(file, raw, "utf8");
7424
+ await import_node_fs25.promises.writeFile(file, raw, "utf8");
6342
7425
  restored.push(file);
6343
7426
  } catch {
6344
7427
  }
6345
7428
  }
6346
7429
  for (const file of createdFiles) {
6347
7430
  try {
6348
- await import_node_fs23.promises.unlink(file);
7431
+ await import_node_fs25.promises.unlink(file);
6349
7432
  removed.push(file);
6350
7433
  } catch {
6351
7434
  }
@@ -6360,8 +7443,26 @@ async function rollback(installPlan, originals, createdFiles) {
6360
7443
  ...removed.map((f) => `removed: ${f}`),
6361
7444
  ""
6362
7445
  ];
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");
7446
+ const rollbackPath = import_node_path40.default.join(installPlan.serviceDir, "neat-rollback.patch");
7447
+ await import_node_fs25.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
7448
+ }
7449
+ function injectInstrumentationHook(raw) {
7450
+ if (raw.includes("instrumentationHook")) return raw;
7451
+ const anchors = [
7452
+ { pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
7453
+ { pattern: /(export\s+default\s*\{)/, label: "esm-default" },
7454
+ { pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
7455
+ ];
7456
+ for (const { pattern } of anchors) {
7457
+ const match = pattern.exec(raw);
7458
+ if (!match) continue;
7459
+ const insertAfter = match.index + match[0].length;
7460
+ const before = raw.slice(0, insertAfter);
7461
+ const after = raw.slice(insertAfter);
7462
+ const injection = "\n experimental: { instrumentationHook: true },";
7463
+ return `${before}${injection}${after}`;
7464
+ }
7465
+ return null;
6365
7466
  }
6366
7467
  var javascriptInstaller = {
6367
7468
  name: "javascript",
@@ -6372,8 +7473,8 @@ var javascriptInstaller = {
6372
7473
 
6373
7474
  // src/installers/python.ts
6374
7475
  init_cjs_shims();
6375
- var import_node_fs24 = require("fs");
6376
- var import_node_path39 = __toESM(require("path"), 1);
7476
+ var import_node_fs26 = require("fs");
7477
+ var import_node_path41 = __toESM(require("path"), 1);
6377
7478
  var SDK_PACKAGES2 = [
6378
7479
  { name: "opentelemetry-distro", version: ">=0.49b0" },
6379
7480
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -6385,7 +7486,7 @@ var OTEL_ENV2 = {
6385
7486
  };
6386
7487
  async function exists3(p) {
6387
7488
  try {
6388
- await import_node_fs24.promises.stat(p);
7489
+ await import_node_fs26.promises.stat(p);
6389
7490
  return true;
6390
7491
  } catch {
6391
7492
  return false;
@@ -6394,7 +7495,7 @@ async function exists3(p) {
6394
7495
  async function detect2(serviceDir) {
6395
7496
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
6396
7497
  for (const m of markers) {
6397
- if (await exists3(import_node_path39.default.join(serviceDir, m))) return true;
7498
+ if (await exists3(import_node_path41.default.join(serviceDir, m))) return true;
6398
7499
  }
6399
7500
  return false;
6400
7501
  }
@@ -6404,9 +7505,9 @@ function reqPackageName(line) {
6404
7505
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
6405
7506
  }
6406
7507
  async function planRequirementsTxtEdits(serviceDir) {
6407
- const file = import_node_path39.default.join(serviceDir, "requirements.txt");
7508
+ const file = import_node_path41.default.join(serviceDir, "requirements.txt");
6408
7509
  if (!await exists3(file)) return null;
6409
- const raw = await import_node_fs24.promises.readFile(file, "utf8");
7510
+ const raw = await import_node_fs26.promises.readFile(file, "utf8");
6410
7511
  const presentNames = new Set(
6411
7512
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
6412
7513
  );
@@ -6414,9 +7515,9 @@ async function planRequirementsTxtEdits(serviceDir) {
6414
7515
  return { manifest: file, missing: [...missing] };
6415
7516
  }
6416
7517
  async function planProcfileEdits(serviceDir) {
6417
- const procfile = import_node_path39.default.join(serviceDir, "Procfile");
7518
+ const procfile = import_node_path41.default.join(serviceDir, "Procfile");
6418
7519
  if (!await exists3(procfile)) return [];
6419
- const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
7520
+ const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
6420
7521
  const edits = [];
6421
7522
  for (const line of raw.split(/\r?\n/)) {
6422
7523
  if (line.length === 0) continue;
@@ -6468,8 +7569,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
6468
7569
  const next = `${original}${trailing}${newlines.join("\n")}
6469
7570
  `;
6470
7571
  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);
7572
+ await import_node_fs26.promises.writeFile(tmp, next, "utf8");
7573
+ await import_node_fs26.promises.rename(tmp, manifest);
6473
7574
  }
6474
7575
  async function applyProcfile(procfile, edits, original) {
6475
7576
  let next = original;
@@ -6478,8 +7579,8 @@ async function applyProcfile(procfile, edits, original) {
6478
7579
  next = next.replace(e.before, e.after);
6479
7580
  }
6480
7581
  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);
7582
+ await import_node_fs26.promises.writeFile(tmp, next, "utf8");
7583
+ await import_node_fs26.promises.rename(tmp, procfile);
6483
7584
  }
6484
7585
  async function apply2(installPlan) {
6485
7586
  const { serviceDir } = installPlan;
@@ -6492,7 +7593,7 @@ async function apply2(installPlan) {
6492
7593
  const originals = /* @__PURE__ */ new Map();
6493
7594
  for (const file of touched) {
6494
7595
  try {
6495
- originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
7596
+ originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
6496
7597
  } catch {
6497
7598
  }
6498
7599
  }
@@ -6503,7 +7604,7 @@ async function apply2(installPlan) {
6503
7604
  if (raw === void 0) {
6504
7605
  throw new Error(`python installer: cannot read ${file} during apply`);
6505
7606
  }
6506
- const base = import_node_path39.default.basename(file);
7607
+ const base = import_node_path41.default.basename(file);
6507
7608
  if (base === "requirements.txt") {
6508
7609
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
6509
7610
  if (edits.length > 0) {
@@ -6528,7 +7629,7 @@ async function rollback2(installPlan, originals) {
6528
7629
  const restored = [];
6529
7630
  for (const [file, raw] of originals.entries()) {
6530
7631
  try {
6531
- await import_node_fs24.promises.writeFile(file, raw, "utf8");
7632
+ await import_node_fs26.promises.writeFile(file, raw, "utf8");
6532
7633
  restored.push(file);
6533
7634
  } catch {
6534
7635
  }
@@ -6542,8 +7643,8 @@ async function rollback2(installPlan, originals) {
6542
7643
  ...restored.map((f) => `restored: ${f}`),
6543
7644
  ""
6544
7645
  ];
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");
7646
+ const rollbackPath = import_node_path41.default.join(installPlan.serviceDir, "neat-rollback.patch");
7647
+ await import_node_fs26.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6547
7648
  }
6548
7649
  var pythonInstaller = {
6549
7650
  name: "python",
@@ -6555,7 +7656,7 @@ var pythonInstaller = {
6555
7656
  // src/installers/shared.ts
6556
7657
  init_cjs_shims();
6557
7658
  function isEmptyPlan(plan3) {
6558
- return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
7659
+ return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
6559
7660
  }
6560
7661
 
6561
7662
  // src/installers/index.ts
@@ -6654,16 +7755,259 @@ function renderPatch(sections) {
6654
7755
  }
6655
7756
  lines.push("");
6656
7757
  }
7758
+ if (plan3.nextConfigEdit) {
7759
+ lines.push("### next.config (framework flag)");
7760
+ lines.push(`--- ${plan3.nextConfigEdit.file}`);
7761
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
7762
+ lines.push("");
7763
+ }
6657
7764
  }
6658
7765
  return lines.join("\n");
6659
7766
  }
6660
7767
 
6661
- // src/cli.ts
6662
- var import_types24 = require("@neat.is/types");
7768
+ // src/orchestrator.ts
7769
+ init_cjs_shims();
7770
+ var import_node_fs27 = require("fs");
7771
+ var import_node_http = __toESM(require("http"), 1);
7772
+ var import_node_path42 = __toESM(require("path"), 1);
7773
+ var import_node_child_process2 = require("child_process");
7774
+ var import_node_readline = __toESM(require("readline"), 1);
7775
+ async function extractAndPersist(opts) {
7776
+ const services = await discoverServices(opts.scanPath);
7777
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
7778
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
7779
+ resetGraph(graphKey);
7780
+ const graph = getGraph(graphKey);
7781
+ const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
7782
+ const extraction = await extractFromDirectory(graph, opts.scanPath, {
7783
+ errorsPath: projectPaths.errorsPath
7784
+ });
7785
+ if (!opts.dryRun) {
7786
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
7787
+ }
7788
+ return {
7789
+ graph,
7790
+ graphKey,
7791
+ services,
7792
+ languages,
7793
+ nodesAdded: extraction.nodesAdded,
7794
+ edgesAdded: extraction.edgesAdded,
7795
+ snapshotPath: projectPaths.snapshotPath,
7796
+ errorsPath: projectPaths.errorsPath
7797
+ };
7798
+ }
7799
+ async function applyInstallersOver(services) {
7800
+ let instrumented = 0;
7801
+ let already = 0;
7802
+ let libOnly = 0;
7803
+ for (const svc of services) {
7804
+ const installer = await pickInstaller(svc.dir);
7805
+ if (!installer) continue;
7806
+ const plan3 = await installer.plan(svc.dir);
7807
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
7808
+ already++;
7809
+ continue;
7810
+ }
7811
+ const outcome = await installer.apply(plan3);
7812
+ if (outcome.outcome === "instrumented") instrumented++;
7813
+ else if (outcome.outcome === "already-instrumented") already++;
7814
+ else if (outcome.outcome === "lib-only") libOnly++;
7815
+ }
7816
+ return { instrumented, alreadyInstrumented: already, libOnly };
7817
+ }
7818
+ async function promptYesNo(question) {
7819
+ const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
7820
+ return new Promise((resolve) => {
7821
+ rl.question(`${question} [Y/n] `, (answer) => {
7822
+ rl.close();
7823
+ const trimmed = answer.trim().toLowerCase();
7824
+ resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
7825
+ });
7826
+ });
7827
+ }
7828
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
7829
+ async function checkDaemonHealth(restPort) {
7830
+ return new Promise((resolve) => {
7831
+ const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
7832
+ const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
7833
+ res.resume();
7834
+ resolve(ok);
7835
+ });
7836
+ req.on("error", () => resolve(false));
7837
+ req.setTimeout(1e3, () => {
7838
+ req.destroy();
7839
+ resolve(false);
7840
+ });
7841
+ });
7842
+ }
7843
+ async function waitForDaemonReady(restPort, timeoutMs) {
7844
+ const deadline = Date.now() + timeoutMs;
7845
+ while (Date.now() < deadline) {
7846
+ if (await checkDaemonHealth(restPort)) return true;
7847
+ await new Promise((r) => setTimeout(r, 300));
7848
+ }
7849
+ return false;
7850
+ }
7851
+ function spawnDaemonDetached() {
7852
+ const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
7853
+ const candidates = [
7854
+ import_node_path42.default.join(here, "neatd.cjs"),
7855
+ import_node_path42.default.join(here, "neatd.js")
7856
+ ];
7857
+ let entry2 = null;
7858
+ const fsSync = require("fs");
7859
+ for (const c of candidates) {
7860
+ try {
7861
+ fsSync.accessSync(c);
7862
+ entry2 = c;
7863
+ break;
7864
+ } catch {
7865
+ }
7866
+ }
7867
+ if (!entry2) {
7868
+ throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
7869
+ }
7870
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
7871
+ detached: true,
7872
+ stdio: "ignore",
7873
+ env: process.env
7874
+ });
7875
+ child.unref();
7876
+ }
7877
+ function openBrowser(url) {
7878
+ if (!process.stdout.isTTY) return "failed";
7879
+ const platform = process.platform;
7880
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
7881
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
7882
+ try {
7883
+ const child = (0, import_node_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
7884
+ child.on("error", () => {
7885
+ });
7886
+ child.unref();
7887
+ return "opened";
7888
+ } catch {
7889
+ return "failed";
7890
+ }
7891
+ }
7892
+ async function runOrchestrator(opts) {
7893
+ const result = {
7894
+ exitCode: 0,
7895
+ steps: {
7896
+ discovery: { services: 0, languages: [] },
7897
+ extraction: { nodesAdded: 0, edgesAdded: 0 },
7898
+ gitignore: "unchanged",
7899
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
7900
+ daemon: "skipped",
7901
+ browser: "skipped"
7902
+ }
7903
+ };
7904
+ const stat = await import_node_fs27.promises.stat(opts.scanPath).catch(() => null);
7905
+ if (!stat || !stat.isDirectory()) {
7906
+ console.error(`neat: ${opts.scanPath} is not a directory`);
7907
+ result.exitCode = 2;
7908
+ return result;
7909
+ }
7910
+ console.log(`neat: ${opts.scanPath}`);
7911
+ console.log("");
7912
+ const persisted = await extractAndPersist({
7913
+ scanPath: opts.scanPath,
7914
+ project: opts.project,
7915
+ projectExplicit: opts.projectExplicit
7916
+ });
7917
+ const { graph, services, languages } = persisted;
7918
+ result.steps.discovery = { services: services.length, languages };
7919
+ console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
7920
+ let runApply = !opts.noInstrument;
7921
+ if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
7922
+ runApply = await promptYesNo("instrument your services and open the dashboard?");
7923
+ }
7924
+ result.steps.extraction = {
7925
+ nodesAdded: persisted.nodesAdded,
7926
+ edgesAdded: persisted.edgesAdded
7927
+ };
7928
+ const gi = await ensureNeatOutIgnored(opts.scanPath);
7929
+ result.steps.gitignore = gi.action;
7930
+ if (gi.action !== "unchanged") {
7931
+ console.log(`${gi.action} .gitignore (neat-out/)`);
7932
+ }
7933
+ try {
7934
+ await addProject({
7935
+ name: opts.project,
7936
+ path: opts.scanPath,
7937
+ languages,
7938
+ status: "active"
7939
+ });
7940
+ } catch (err) {
7941
+ if (!(err instanceof ProjectNameCollisionError)) throw err;
7942
+ console.error(`neat: ${err.message}`);
7943
+ console.error("pass --project <other-name> to register under a different name.");
7944
+ result.exitCode = 1;
7945
+ return result;
7946
+ }
7947
+ if (!runApply) {
7948
+ result.steps.apply.skipped = true;
7949
+ console.log("skipped instrumentation (--no-instrument)");
7950
+ } else {
7951
+ const tally = await applyInstallersOver(services);
7952
+ result.steps.apply = { ...tally, skipped: false };
7953
+ console.log(
7954
+ `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
7955
+ );
7956
+ }
7957
+ const restPort = Number(process.env.PORT ?? 8080);
7958
+ const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
7959
+ if (await checkDaemonHealth(restPort)) {
7960
+ result.steps.daemon = "already-running";
7961
+ } else {
7962
+ try {
7963
+ spawnDaemonDetached();
7964
+ } catch (err) {
7965
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
7966
+ result.exitCode = 1;
7967
+ return result;
7968
+ }
7969
+ const ready = await waitForDaemonReady(restPort, timeoutMs);
7970
+ result.steps.daemon = ready ? "spawned" : "timed-out";
7971
+ if (!ready) {
7972
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
7973
+ result.exitCode = 1;
7974
+ return result;
7975
+ }
7976
+ }
7977
+ const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
7978
+ if (opts.noOpen || !process.stdout.isTTY) {
7979
+ result.steps.browser = "skipped";
7980
+ } else {
7981
+ result.steps.browser = openBrowser(dashboardUrl);
7982
+ }
7983
+ printSummary(result, graph, dashboardUrl);
7984
+ return result;
7985
+ }
7986
+ function printSummary(result, graph, dashboardUrl) {
7987
+ const nodes = [];
7988
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
7989
+ const edges = [];
7990
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
7991
+ const byNode = /* @__PURE__ */ new Map();
7992
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
7993
+ const byEdge = /* @__PURE__ */ new Map();
7994
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
7995
+ console.log("");
7996
+ console.log("=== summary ===");
7997
+ console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
7998
+ for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
7999
+ for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
8000
+ console.log("");
8001
+ console.log(`dashboard: ${dashboardUrl}`);
8002
+ }
8003
+
8004
+ // src/cli-verbs.ts
8005
+ init_cjs_shims();
8006
+ var import_node_path43 = __toESM(require("path"), 1);
6663
8007
 
6664
8008
  // src/cli-client.ts
6665
8009
  init_cjs_shims();
6666
- var import_types23 = require("@neat.is/types");
8010
+ var import_types24 = require("@neat.is/types");
6667
8011
  var HttpError = class extends Error {
6668
8012
  constructor(status2, message, responseBody = "") {
6669
8013
  super(message);
@@ -6680,13 +8024,16 @@ var TransportError = class extends Error {
6680
8024
  this.name = "TransportError";
6681
8025
  }
6682
8026
  };
6683
- function createHttpClient(baseUrl) {
8027
+ function createHttpClient(baseUrl, bearerToken) {
6684
8028
  const root = baseUrl.replace(/\/$/, "");
8029
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
6685
8030
  return {
6686
- async get(path41) {
8031
+ async get(path45) {
6687
8032
  let res;
6688
8033
  try {
6689
- res = await fetch(`${root}${path41}`);
8034
+ res = await fetch(`${root}${path45}`, {
8035
+ headers: { ...authHeader }
8036
+ });
6690
8037
  } catch (err) {
6691
8038
  throw new TransportError(
6692
8039
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -6696,18 +8043,18 @@ function createHttpClient(baseUrl) {
6696
8043
  const body = await res.text().catch(() => "");
6697
8044
  throw new HttpError(
6698
8045
  res.status,
6699
- `${res.status} ${res.statusText} on GET ${path41}: ${body}`,
8046
+ `${res.status} ${res.statusText} on GET ${path45}: ${body}`,
6700
8047
  body
6701
8048
  );
6702
8049
  }
6703
8050
  return await res.json();
6704
8051
  },
6705
- async post(path41, body) {
8052
+ async post(path45, body) {
6706
8053
  let res;
6707
8054
  try {
6708
- res = await fetch(`${root}${path41}`, {
8055
+ res = await fetch(`${root}${path45}`, {
6709
8056
  method: "POST",
6710
- headers: { "content-type": "application/json" },
8057
+ headers: { "content-type": "application/json", ...authHeader },
6711
8058
  body: JSON.stringify(body)
6712
8059
  });
6713
8060
  } catch (err) {
@@ -6719,7 +8066,7 @@ function createHttpClient(baseUrl) {
6719
8066
  const text = await res.text().catch(() => "");
6720
8067
  throw new HttpError(
6721
8068
  res.status,
6722
- `${res.status} ${res.statusText} on POST ${path41}: ${text}`,
8069
+ `${res.status} ${res.statusText} on POST ${path45}: ${text}`,
6723
8070
  text
6724
8071
  );
6725
8072
  }
@@ -6733,12 +8080,12 @@ function projectPath(project, suffix) {
6733
8080
  }
6734
8081
  async function runRootCause(client, input) {
6735
8082
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
6736
- const path41 = projectPath(
8083
+ const path45 = projectPath(
6737
8084
  input.project,
6738
8085
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
6739
8086
  );
6740
8087
  try {
6741
- const result = await client.get(path41);
8088
+ const result = await client.get(path45);
6742
8089
  const arrowPath = result.traversalPath.join(" \u2190 ");
6743
8090
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
6744
8091
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -6764,12 +8111,12 @@ async function runRootCause(client, input) {
6764
8111
  }
6765
8112
  async function runBlastRadius(client, input) {
6766
8113
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
6767
- const path41 = projectPath(
8114
+ const path45 = projectPath(
6768
8115
  input.project,
6769
8116
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6770
8117
  );
6771
8118
  try {
6772
- const result = await client.get(path41);
8119
+ const result = await client.get(path45);
6773
8120
  if (result.totalAffected === 0) {
6774
8121
  return {
6775
8122
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -6798,17 +8145,17 @@ async function runBlastRadius(client, input) {
6798
8145
  }
6799
8146
  }
6800
8147
  function formatBlastEntry(n) {
6801
- const tag = n.edgeProvenance === import_types23.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
8148
+ const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
6802
8149
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
6803
8150
  }
6804
8151
  async function runDependencies(client, input) {
6805
8152
  const depth = input.depth ?? 3;
6806
- const path41 = projectPath(
8153
+ const path45 = projectPath(
6807
8154
  input.project,
6808
8155
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6809
8156
  );
6810
8157
  try {
6811
- const result = await client.get(path41);
8158
+ const result = await client.get(path45);
6812
8159
  if (result.total === 0) {
6813
8160
  return {
6814
8161
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -6844,9 +8191,9 @@ async function runObservedDependencies(client, input) {
6844
8191
  const edges = await client.get(
6845
8192
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
6846
8193
  );
6847
- const observed = edges.outbound.filter((e) => e.provenance === import_types23.Provenance.OBSERVED);
8194
+ const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
6848
8195
  if (observed.length === 0) {
6849
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types23.Provenance.EXTRACTED);
8196
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
6850
8197
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
6851
8198
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
6852
8199
  }
@@ -6854,7 +8201,7 @@ async function runObservedDependencies(client, input) {
6854
8201
  return {
6855
8202
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
6856
8203
  block: blockLines.join("\n"),
6857
- provenance: import_types23.Provenance.OBSERVED
8204
+ provenance: import_types24.Provenance.OBSERVED
6858
8205
  };
6859
8206
  } catch (err) {
6860
8207
  if (err instanceof HttpError && err.status === 404) {
@@ -6889,9 +8236,9 @@ function formatDuration(ms) {
6889
8236
  return `${Math.round(h / 24)}d`;
6890
8237
  }
6891
8238
  async function runIncidents(client, input) {
6892
- const path41 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
8239
+ const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6893
8240
  try {
6894
- const body = await client.get(path41);
8241
+ const body = await client.get(path45);
6895
8242
  const events = body.events;
6896
8243
  if (events.length === 0) {
6897
8244
  return {
@@ -6908,7 +8255,7 @@ async function runIncidents(client, input) {
6908
8255
  return {
6909
8256
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6910
8257
  block: blockLines.join("\n"),
6911
- provenance: import_types23.Provenance.OBSERVED
8258
+ provenance: import_types24.Provenance.OBSERVED
6912
8259
  };
6913
8260
  } catch (err) {
6914
8261
  if (err instanceof HttpError && err.status === 404) {
@@ -7017,7 +8364,7 @@ async function runStaleEdges(client, input) {
7017
8364
  return {
7018
8365
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
7019
8366
  block: blockLines.join("\n"),
7020
- provenance: import_types23.Provenance.STALE
8367
+ provenance: import_types24.Provenance.STALE
7021
8368
  };
7022
8369
  }
7023
8370
  async function runPolicies(client, input) {
@@ -7157,8 +8504,181 @@ function exitCodeForError(err) {
7157
8504
  if (err instanceof HttpError) return 1;
7158
8505
  return 1;
7159
8506
  }
8507
+ function createSnapshotPushClient(baseUrl, token) {
8508
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
8509
+ }
8510
+ async function pushSnapshotToRemote(input) {
8511
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
8512
+ if (typeof client.post !== "function") {
8513
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
8514
+ }
8515
+ return client.post(
8516
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
8517
+ { snapshot: input.snapshot }
8518
+ );
8519
+ }
8520
+
8521
+ // src/cli-verbs.ts
8522
+ async function resolveProjectEntry(opts) {
8523
+ const entries = await listProjects();
8524
+ if (opts.project) {
8525
+ const match = entries.find((e) => e.name === opts.project);
8526
+ return match ?? null;
8527
+ }
8528
+ const cwd = opts.cwd ?? process.cwd();
8529
+ const resolvedCwd = await normalizeProjectPath(cwd);
8530
+ for (const entry2 of entries) {
8531
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
8532
+ return entry2;
8533
+ }
8534
+ }
8535
+ return null;
8536
+ }
8537
+ async function checkDaemonHealth2(baseUrl) {
8538
+ try {
8539
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
8540
+ signal: AbortSignal.timeout(1500)
8541
+ });
8542
+ return res.ok;
8543
+ } catch {
8544
+ return false;
8545
+ }
8546
+ }
8547
+ function snapshotForGraph(persisted) {
8548
+ return {
8549
+ schemaVersion: 3,
8550
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8551
+ graph: persisted.graph.export()
8552
+ };
8553
+ }
8554
+ function emitResult(result, json) {
8555
+ if (json) {
8556
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
8557
+ return;
8558
+ }
8559
+ const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
8560
+ console.log(
8561
+ `${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
8562
+ );
8563
+ if (!result.apply.skipped) {
8564
+ const { instrumented, alreadyInstrumented, libOnly } = result.apply;
8565
+ console.log(
8566
+ `instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
8567
+ );
8568
+ } else {
8569
+ console.log("skipped instrumentation (--no-instrument)");
8570
+ }
8571
+ for (const warn of result.warnings) console.error(warn);
8572
+ }
8573
+ async function runSync(opts) {
8574
+ const entry2 = await resolveProjectEntry(opts);
8575
+ if (!entry2) {
8576
+ const target = opts.project ?? opts.cwd ?? process.cwd();
8577
+ console.error(
8578
+ `neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
8579
+ );
8580
+ return {
8581
+ exitCode: 1,
8582
+ project: opts.project ?? "",
8583
+ scanPath: target,
8584
+ nodesAdded: 0,
8585
+ edgesAdded: 0,
8586
+ snapshotPath: null,
8587
+ mode: opts.to ? "remote" : "local",
8588
+ daemon: "skipped",
8589
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
8590
+ warnings: []
8591
+ };
8592
+ }
8593
+ const persisted = await extractAndPersist({
8594
+ scanPath: entry2.path,
8595
+ project: entry2.name,
8596
+ projectExplicit: true,
8597
+ dryRun: true
8598
+ });
8599
+ let snapshotPath = null;
8600
+ if (!opts.dryRun) {
8601
+ const target = persisted.snapshotPath;
8602
+ await saveGraphToDisk(persisted.graph, target);
8603
+ snapshotPath = target;
8604
+ }
8605
+ const skipApply = opts.dryRun || opts.noInstrument;
8606
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
8607
+ const warnings = [];
8608
+ let daemonState = "skipped";
8609
+ let exitCode = 0;
8610
+ const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
8611
+ if (!opts.dryRun) {
8612
+ const snapshot = snapshotForGraph(persisted);
8613
+ if (opts.to) {
8614
+ const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
8615
+ try {
8616
+ await pushSnapshotToRemote({
8617
+ baseUrl: opts.to,
8618
+ token,
8619
+ project: entry2.name,
8620
+ snapshot
8621
+ });
8622
+ daemonState = "remote-ok";
8623
+ } catch (err) {
8624
+ if (err instanceof HttpError) {
8625
+ console.error(`neat sync: ${err.message}`);
8626
+ exitCode = 1;
8627
+ } else if (err instanceof TransportError) {
8628
+ console.error(`neat sync: ${err.message}`);
8629
+ exitCode = 3;
8630
+ } else {
8631
+ console.error(`neat sync: ${err.message}`);
8632
+ exitCode = 1;
8633
+ }
8634
+ daemonState = "skipped";
8635
+ }
8636
+ } else {
8637
+ const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
8638
+ const healthy = await checkDaemonHealth2(daemonUrl);
8639
+ if (healthy) {
8640
+ try {
8641
+ await pushSnapshotToRemote({
8642
+ baseUrl: daemonUrl,
8643
+ token: process.env.NEAT_AUTH_TOKEN,
8644
+ project: entry2.name,
8645
+ snapshot
8646
+ });
8647
+ daemonState = "reloaded";
8648
+ } catch (err) {
8649
+ warnings.push(
8650
+ `neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
8651
+ );
8652
+ daemonState = "down";
8653
+ exitCode = 2;
8654
+ }
8655
+ } else {
8656
+ warnings.push(
8657
+ "neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
8658
+ );
8659
+ daemonState = "down";
8660
+ exitCode = 2;
8661
+ }
8662
+ }
8663
+ }
8664
+ const result = {
8665
+ exitCode,
8666
+ project: entry2.name,
8667
+ scanPath: entry2.path,
8668
+ nodesAdded: persisted.nodesAdded,
8669
+ edgesAdded: persisted.edgesAdded,
8670
+ snapshotPath,
8671
+ mode,
8672
+ daemon: daemonState,
8673
+ apply: { ...applyTally, skipped: skipApply },
8674
+ warnings
8675
+ };
8676
+ emitResult(result, opts.json);
8677
+ return result;
8678
+ }
7160
8679
 
7161
8680
  // src/cli.ts
8681
+ var import_types25 = require("@neat.is/types");
7162
8682
  function usage() {
7163
8683
  console.log("usage: neat <command> [args] [--project <name>]");
7164
8684
  console.log("");
@@ -7183,6 +8703,18 @@ function usage() {
7183
8703
  console.log(" Flags:");
7184
8704
  console.log(" --print-config print the JSON snippet to stdout");
7185
8705
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
8706
+ console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
8707
+ console.log(" emit a docker-compose / systemd / docker run artifact, and");
8708
+ console.log(" print the OTel env-vars block to paste into your platform.");
8709
+ console.log(" sync Re-run discovery, extraction, and SDK apply against the");
8710
+ console.log(" registered project, then notify the running daemon.");
8711
+ console.log(" Flags:");
8712
+ console.log(" --project <name> target a registered project by name");
8713
+ console.log(" --to <url> push the snapshot to a remote daemon");
8714
+ console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
8715
+ console.log(" --dry-run run extraction in-memory; do not write");
8716
+ console.log(" --no-instrument skip the SDK install apply step");
8717
+ console.log(" --json emit the delta summary as JSON");
7186
8718
  console.log("");
7187
8719
  console.log("query commands (mirror the MCP tools, ADR-050):");
7188
8720
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -7236,7 +8768,9 @@ var STRING_FLAGS = [
7236
8768
  ["--error-id", "errorId"],
7237
8769
  ["--hypothetical-action", "hypotheticalAction"],
7238
8770
  ["--type", "type"],
7239
- ["--min-confidence", "minConfidence"]
8771
+ ["--min-confidence", "minConfidence"],
8772
+ ["--to", "to"],
8773
+ ["--token", "token"]
7240
8774
  ];
7241
8775
  function parseArgs(rest) {
7242
8776
  const positional = [];
@@ -7245,6 +8779,10 @@ function parseArgs(rest) {
7245
8779
  apply: false,
7246
8780
  dryRun: false,
7247
8781
  noInstall: false,
8782
+ noInstrument: false,
8783
+ noOpen: false,
8784
+ yes: false,
8785
+ verbose: false,
7248
8786
  printConfig: false,
7249
8787
  json: false,
7250
8788
  depth: null,
@@ -7257,6 +8795,8 @@ function parseArgs(rest) {
7257
8795
  hypotheticalAction: null,
7258
8796
  type: null,
7259
8797
  minConfidence: null,
8798
+ to: null,
8799
+ token: null,
7260
8800
  positional: []
7261
8801
  };
7262
8802
  for (let i = 0; i < rest.length; i++) {
@@ -7273,6 +8813,22 @@ function parseArgs(rest) {
7273
8813
  out.noInstall = true;
7274
8814
  continue;
7275
8815
  }
8816
+ if (arg === "--no-instrument") {
8817
+ out.noInstrument = true;
8818
+ continue;
8819
+ }
8820
+ if (arg === "--no-open") {
8821
+ out.noOpen = true;
8822
+ continue;
8823
+ }
8824
+ if (arg === "--yes" || arg === "-y") {
8825
+ out.yes = true;
8826
+ continue;
8827
+ }
8828
+ if (arg === "--verbose" || arg === "-v") {
8829
+ out.verbose = true;
8830
+ continue;
8831
+ }
7276
8832
  if (arg === "--print-config") {
7277
8833
  out.printConfig = true;
7278
8834
  continue;
@@ -7328,34 +8884,6 @@ function assignFlag(out, field, value) {
7328
8884
  ;
7329
8885
  out[field] = value;
7330
8886
  }
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
8887
  function printBanner() {
7360
8888
  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
8889
  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 +8934,7 @@ async function buildPatchSections(services) {
7406
8934
  }
7407
8935
  async function runInit(opts) {
7408
8936
  const written = [];
7409
- const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
8937
+ const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
7410
8938
  if (!stat || !stat.isDirectory()) {
7411
8939
  console.error(`neat init: ${opts.scanPath} is not a directory`);
7412
8940
  return { exitCode: 2, writtenFiles: written };
@@ -7415,11 +8943,15 @@ async function runInit(opts) {
7415
8943
  printDiscoveryReport(opts, services);
7416
8944
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
7417
8945
  const patch = renderPatch(sections);
7418
- const patchPath = import_node_path40.default.join(opts.scanPath, "neat.patch");
8946
+ const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
7419
8947
  if (opts.dryRun) {
7420
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
8948
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7421
8949
  written.push(patchPath);
7422
8950
  console.log(`dry-run: patch written to ${patchPath}`);
8951
+ const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
8952
+ const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
8953
+ const verb = gitignoreExists ? "append" : "create";
8954
+ console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
7423
8955
  console.log("rerun without --dry-run to register and snapshot.");
7424
8956
  return { exitCode: 0, writtenFiles: written };
7425
8957
  }
@@ -7428,12 +8960,16 @@ async function runInit(opts) {
7428
8960
  const graph = getGraph(graphKey);
7429
8961
  const projectPaths = pathsForProject(
7430
8962
  graphKey,
7431
- import_node_path40.default.join(opts.scanPath, "neat-out")
8963
+ import_node_path44.default.join(opts.scanPath, "neat-out")
7432
8964
  );
7433
- const errorsPath = import_node_path40.default.join(import_node_path40.default.dirname(opts.outPath), import_node_path40.default.basename(projectPaths.errorsPath));
8965
+ const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
7434
8966
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7435
8967
  await saveGraphToDisk(graph, opts.outPath);
7436
8968
  written.push(opts.outPath);
8969
+ const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
8970
+ if (gitignoreResult.action !== "unchanged") {
8971
+ written.push(gitignoreResult.file);
8972
+ }
7437
8973
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
7438
8974
  try {
7439
8975
  await addProject({
@@ -7476,35 +9012,27 @@ async function runInit(opts) {
7476
9012
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7477
9013
  }
7478
9014
  } else {
7479
- await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
9015
+ await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
7480
9016
  written.push(patchPath);
7481
9017
  }
7482
9018
  }
7483
- const nodes = [];
7484
- graph.forEachNode((_id, attrs) => nodes.push(attrs));
7485
- const edges = [];
7486
- graph.forEachEdge((_id, attrs) => edges.push(attrs));
7487
9019
  console.log("");
7488
- console.log("=== neat init: summary ===");
7489
9020
  console.log(`snapshot: ${opts.outPath}`);
7490
9021
  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));
9022
+ console.log("");
9023
+ const divergenceResult = computeDivergences(graph);
9024
+ console.log(
9025
+ renderValueForwardSummary({
9026
+ graph,
9027
+ divergences: divergenceResult.divergences,
9028
+ verbose: opts.verbose
9029
+ })
9030
+ );
7493
9031
  console.log(formatExtractionBanner(result.extractionErrors));
7494
9032
  if (result.extractionErrors > 0) {
7495
9033
  console.log(`errors: ${errorsPath}`);
7496
9034
  }
7497
9035
  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
9036
  if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
7509
9037
  return { exitCode: 4, writtenFiles: written };
7510
9038
  }
@@ -7524,9 +9052,9 @@ var CLAUDE_SKILL_CONFIG = {
7524
9052
  };
7525
9053
  function claudeConfigPath() {
7526
9054
  const override = process.env.NEAT_CLAUDE_CONFIG;
7527
- if (override && override.length > 0) return import_node_path40.default.resolve(override);
9055
+ if (override && override.length > 0) return import_node_path44.default.resolve(override);
7528
9056
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7529
- return import_node_path40.default.join(home, ".claude.json");
9057
+ return import_node_path44.default.join(home, ".claude.json");
7530
9058
  }
7531
9059
  async function runSkill(opts) {
7532
9060
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -7538,7 +9066,7 @@ async function runSkill(opts) {
7538
9066
  const target = claudeConfigPath();
7539
9067
  let existing = {};
7540
9068
  try {
7541
- existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
9069
+ existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
7542
9070
  } catch (err) {
7543
9071
  if (err.code !== "ENOENT") {
7544
9072
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -7550,8 +9078,8 @@ async function runSkill(opts) {
7550
9078
  ...existing,
7551
9079
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
7552
9080
  };
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");
9081
+ await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
9082
+ await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
7555
9083
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
7556
9084
  console.log("restart Claude Code to pick up the new MCP server.");
7557
9085
  return { exitCode: 0 };
@@ -7585,12 +9113,12 @@ async function main() {
7585
9113
  console.error("neat init: --apply and --dry-run are mutually exclusive");
7586
9114
  process.exit(2);
7587
9115
  }
7588
- const scanPath = import_node_path40.default.resolve(target);
9116
+ const scanPath = import_node_path44.default.resolve(target);
7589
9117
  const projectExplicit = parsed.project !== null;
7590
- const projectName = projectExplicit ? project : import_node_path40.default.basename(scanPath);
9118
+ const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
7591
9119
  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);
9120
+ const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
9121
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
7594
9122
  const result = await runInit({
7595
9123
  scanPath,
7596
9124
  outPath,
@@ -7598,7 +9126,8 @@ async function main() {
7598
9126
  projectExplicit,
7599
9127
  apply: apply3,
7600
9128
  dryRun,
7601
- noInstall
9129
+ noInstall,
9130
+ verbose: parsed.verbose
7602
9131
  });
7603
9132
  if (result.exitCode !== 0) process.exit(result.exitCode);
7604
9133
  return;
@@ -7610,21 +9139,21 @@ async function main() {
7610
9139
  usage();
7611
9140
  process.exit(2);
7612
9141
  }
7613
- const scanPath = import_node_path40.default.resolve(target);
7614
- const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
9142
+ const scanPath = import_node_path44.default.resolve(target);
9143
+ const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
7615
9144
  if (!stat || !stat.isDirectory()) {
7616
9145
  console.error(`neat watch: ${scanPath} is not a directory`);
7617
9146
  process.exit(2);
7618
9147
  }
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))
9148
+ const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
9149
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
9150
+ const errorsPath = import_node_path44.default.resolve(
9151
+ process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
7623
9152
  );
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))
9153
+ const staleEventsPath = import_node_path44.default.resolve(
9154
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.staleEventsPath))
7626
9155
  );
7627
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path40.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
9156
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
7628
9157
  const handle = await startWatch(getGraph(project), {
7629
9158
  scanPath,
7630
9159
  outPath,
@@ -7717,15 +9246,74 @@ async function main() {
7717
9246
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
7718
9247
  return;
7719
9248
  }
9249
+ if (cmd === "deploy") {
9250
+ const artifact = await runDeploy();
9251
+ const block = renderOtelEnvBlock2(artifact.token);
9252
+ console.log();
9253
+ console.log(`Substrate detected: ${artifact.substrate}`);
9254
+ if (artifact.artifactPath) {
9255
+ console.log(`Artifact written: ${artifact.artifactPath}`);
9256
+ } else {
9257
+ console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
9258
+ console.log();
9259
+ console.log(artifact.contents);
9260
+ }
9261
+ console.log();
9262
+ console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
9263
+ console.log(` ${artifact.token}`);
9264
+ console.log();
9265
+ console.log("For your application's deploy platform, set these env vars:");
9266
+ console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
9267
+ console.log();
9268
+ console.log("Once NEAT is running, your dashboard will be at:");
9269
+ console.log(" https://<host>:6328");
9270
+ console.log();
9271
+ console.log("To start NEAT, run:");
9272
+ console.log(` ${artifact.startCommand}`);
9273
+ return;
9274
+ }
9275
+ if (cmd === "sync") {
9276
+ const result = await runSync({
9277
+ ...parsed.project ? { project: parsed.project } : {},
9278
+ ...parsed.to ? { to: parsed.to } : {},
9279
+ ...parsed.token ? { token: parsed.token } : {},
9280
+ dryRun: parsed.dryRun,
9281
+ noInstrument: parsed.noInstrument,
9282
+ json: parsed.json
9283
+ });
9284
+ if (result.exitCode !== 0) process.exit(result.exitCode);
9285
+ return;
9286
+ }
7720
9287
  if (QUERY_VERBS.has(cmd)) {
7721
9288
  const code = await runQueryVerb(cmd, parsed);
7722
9289
  if (code !== 0) process.exit(code);
7723
9290
  return;
7724
9291
  }
9292
+ const orchestratorCode = await tryOrchestrator(cmd, parsed);
9293
+ if (orchestratorCode !== null) {
9294
+ if (orchestratorCode !== 0) process.exit(orchestratorCode);
9295
+ return;
9296
+ }
7725
9297
  console.error(`neat: unknown command "${cmd}"`);
7726
9298
  usage();
7727
9299
  process.exit(1);
7728
9300
  }
9301
+ async function tryOrchestrator(cmd, parsed) {
9302
+ const scanPath = import_node_path44.default.resolve(cmd);
9303
+ const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
9304
+ if (!stat || !stat.isDirectory()) return null;
9305
+ const projectExplicit = parsed.project !== null;
9306
+ const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
9307
+ const result = await runOrchestrator({
9308
+ scanPath,
9309
+ project: projectName,
9310
+ projectExplicit,
9311
+ noInstrument: parsed.noInstrument,
9312
+ noOpen: parsed.noOpen,
9313
+ yes: parsed.yes
9314
+ });
9315
+ return result.exitCode;
9316
+ }
7729
9317
  var QUERY_VERBS = /* @__PURE__ */ new Set([
7730
9318
  "root-cause",
7731
9319
  "blast-radius",
@@ -7865,10 +9453,10 @@ async function runQueryVerb(cmd, parsed) {
7865
9453
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7866
9454
  const out = [];
7867
9455
  for (const p of parts) {
7868
- const r = import_types24.DivergenceTypeSchema.safeParse(p);
9456
+ const r = import_types25.DivergenceTypeSchema.safeParse(p);
7869
9457
  if (!r.success) {
7870
9458
  console.error(
7871
- `neat divergences: unknown --type "${p}". allowed: ${import_types24.DivergenceTypeSchema.options.join(", ")}`
9459
+ `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
7872
9460
  );
7873
9461
  return 2;
7874
9462
  }