@neat.is/core 0.3.8 → 0.4.2

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
@@ -47,14 +47,19 @@ function mountBearerAuth(app, opts) {
47
47
  if (opts.trustProxy) return;
48
48
  const expected = Buffer.from(opts.token, "utf8");
49
49
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
50
+ const publicRead = opts.publicRead === true;
50
51
  app.addHook("preHandler", (req, reply, done) => {
51
- const path44 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
52
+ const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
52
53
  for (const suffix of suffixes) {
53
- if (path44 === suffix || path44.endsWith(suffix)) {
54
+ if (path45 === suffix || path45.endsWith(suffix)) {
54
55
  done();
55
56
  return;
56
57
  }
57
58
  }
59
+ if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
60
+ done();
61
+ return;
62
+ }
58
63
  const header = req.headers.authorization;
59
64
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
60
65
  void reply.code(401).send({ error: "unauthorized" });
@@ -68,13 +73,19 @@ function mountBearerAuth(app, opts) {
68
73
  done();
69
74
  });
70
75
  }
71
- var import_node_crypto, DEFAULT_UNAUTH_SUFFIXES;
76
+ var import_node_crypto, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
72
77
  var init_auth = __esm({
73
78
  "src/auth.ts"() {
74
79
  "use strict";
75
80
  init_cjs_shims();
76
81
  import_node_crypto = require("crypto");
77
- DEFAULT_UNAUTH_SUFFIXES = ["/health", "/healthz", "/readyz"];
82
+ PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
83
+ DEFAULT_UNAUTH_SUFFIXES = [
84
+ "/health",
85
+ "/healthz",
86
+ "/readyz",
87
+ "/api/config"
88
+ ];
78
89
  }
79
90
  });
80
91
 
@@ -277,6 +288,15 @@ function isoFromUnixNano(nanos) {
277
288
  return void 0;
278
289
  }
279
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
+ }
280
300
  function parseOtlpRequest(body) {
281
301
  const out = [];
282
302
  for (const rs of body.resourceSpans ?? []) {
@@ -296,6 +316,7 @@ function parseOtlpRequest(body) {
296
316
  endTimeUnixNano: span.endTimeUnixNano ?? "0",
297
317
  startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
298
318
  durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
319
+ env: pickEnv(attrs, resourceAttrs),
299
320
  attributes: attrs,
300
321
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
301
322
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
@@ -434,7 +455,7 @@ async function buildOtelReceiver(opts) {
434
455
  };
435
456
  return decorated;
436
457
  }
437
- var import_node_path36, 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;
438
459
  var init_otel = __esm({
439
460
  "src/otel.ts"() {
440
461
  "use strict";
@@ -444,6 +465,9 @@ var init_otel = __esm({
444
465
  import_fastify2 = __toESM(require("fastify"), 1);
445
466
  import_protobufjs = __toESM(require("protobufjs"), 1);
446
467
  init_auth();
468
+ ENV_ATTR_CANONICAL = "deployment.environment.name";
469
+ ENV_ATTR_COMPAT = "deployment.environment";
470
+ ENV_FALLBACK = "unknown";
447
471
  exportTraceServiceRequestType = null;
448
472
  exportTraceServiceResponseType = null;
449
473
  cachedProtobufResponseBody = null;
@@ -456,14 +480,16 @@ __export(cli_exports, {
456
480
  CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
457
481
  QUERY_VERBS: () => QUERY_VERBS,
458
482
  parseArgs: () => parseArgs,
483
+ readPackageVersion: () => readPackageVersion,
459
484
  runInit: () => runInit,
460
485
  runQueryVerb: () => runQueryVerb,
461
486
  runSkill: () => runSkill
462
487
  });
463
488
  module.exports = __toCommonJS(cli_exports);
464
489
  init_cjs_shims();
465
- var import_node_path43 = __toESM(require("path"), 1);
490
+ var import_node_path44 = __toESM(require("path"), 1);
466
491
  var import_node_fs28 = require("fs");
492
+ var import_node_url3 = require("url");
467
493
 
468
494
  // src/graph.ts
469
495
  init_cjs_shims();
@@ -973,19 +999,19 @@ function confidenceFromMix(edges, now = Date.now()) {
973
999
  function longestIncomingWalk(graph, start, maxDepth) {
974
1000
  let best = { path: [start], edges: [] };
975
1001
  const visited = /* @__PURE__ */ new Set([start]);
976
- function step(node, path44, edges) {
977
- if (path44.length > best.path.length) {
978
- best = { path: [...path44], edges: [...edges] };
1002
+ function step(node, path45, edges) {
1003
+ if (path45.length > best.path.length) {
1004
+ best = { path: [...path45], edges: [...edges] };
979
1005
  }
980
- if (path44.length - 1 >= maxDepth) return;
1006
+ if (path45.length - 1 >= maxDepth) return;
981
1007
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
982
1008
  for (const [srcId, edge] of incoming) {
983
1009
  if (visited.has(srcId)) continue;
984
1010
  visited.add(srcId);
985
- path44.push(srcId);
1011
+ path45.push(srcId);
986
1012
  edges.push(edge);
987
- step(srcId, path44, edges);
988
- path44.pop();
1013
+ step(srcId, path45, edges);
1014
+ path45.pop();
989
1015
  edges.pop();
990
1016
  visited.delete(srcId);
991
1017
  }
@@ -1558,52 +1584,64 @@ function cacheSpanService(span, now) {
1558
1584
  if (!span.traceId || !span.spanId) return;
1559
1585
  const key = parentSpanKey(span.traceId, span.spanId);
1560
1586
  parentSpanCache.delete(key);
1561
- parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1587
+ parentSpanCache.set(key, {
1588
+ service: span.service,
1589
+ env: span.env ?? "unknown",
1590
+ expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1591
+ });
1562
1592
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1563
1593
  const oldest = parentSpanCache.keys().next().value;
1564
1594
  if (!oldest) break;
1565
1595
  parentSpanCache.delete(oldest);
1566
1596
  }
1567
1597
  }
1568
- function lookupParentSpanService(traceId, parentSpanId, now) {
1598
+ function lookupParentSpan(traceId, parentSpanId, now) {
1569
1599
  const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1570
1600
  if (!entry2) return null;
1571
1601
  if (entry2.expiresAt <= now) {
1572
1602
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1573
1603
  return null;
1574
1604
  }
1575
- return entry2.service;
1605
+ return { service: entry2.service, env: entry2.env };
1576
1606
  }
1577
- function resolveServiceId(graph, host) {
1578
- const direct = (0, import_types3.serviceId)(host);
1579
- if (graph.hasNode(direct)) return direct;
1580
- let found = null;
1607
+ function resolveServiceId(graph, host, env) {
1608
+ const envTagged = (0, import_types3.serviceId)(host, env);
1609
+ if (graph.hasNode(envTagged)) return envTagged;
1610
+ const envLess = (0, import_types3.serviceId)(host);
1611
+ if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
1612
+ let sameEnv = null;
1613
+ let envLessMatch = null;
1614
+ let anyMatch = null;
1581
1615
  graph.forEachNode((id, attrs) => {
1582
- if (found) return;
1616
+ if (sameEnv) return;
1583
1617
  const a = attrs;
1584
1618
  if (a.type !== import_types3.NodeType.ServiceNode) return;
1585
- if (a.name === host) {
1586
- found = id;
1619
+ const matchesByName = a.name === host;
1620
+ const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1621
+ if (!matchesByName && !matchesByAlias) return;
1622
+ const nodeEnv = a.env ?? "unknown";
1623
+ if (nodeEnv === env) {
1624
+ sameEnv = id;
1587
1625
  return;
1588
1626
  }
1589
- if (a.aliases && a.aliases.includes(host)) {
1590
- found = id;
1591
- }
1627
+ if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
1628
+ else if (!anyMatch) anyMatch = id;
1592
1629
  });
1593
- return found;
1630
+ return sameEnv ?? envLessMatch ?? anyMatch;
1594
1631
  }
1595
1632
  function frontierIdFor(host) {
1596
1633
  return (0, import_types3.frontierId)(host);
1597
1634
  }
1598
- function ensureServiceNode(graph, serviceName) {
1599
- const id = (0, import_types3.serviceId)(serviceName);
1635
+ function ensureServiceNode(graph, serviceName, env) {
1636
+ const id = (0, import_types3.serviceId)(serviceName, env);
1600
1637
  if (graph.hasNode(id)) return id;
1601
1638
  const node = {
1602
1639
  id,
1603
1640
  type: import_types3.NodeType.ServiceNode,
1604
1641
  name: serviceName,
1605
1642
  language: "unknown",
1606
- discoveredVia: "otel"
1643
+ discoveredVia: "otel",
1644
+ ...env !== "unknown" ? { env } : {}
1607
1645
  };
1608
1646
  graph.addNode(id, node);
1609
1647
  return id;
@@ -1749,7 +1787,7 @@ function buildErrorEventForReceiver(span) {
1749
1787
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1750
1788
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1751
1789
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1752
- affectedNode: (0, import_types3.serviceId)(span.service)
1790
+ affectedNode: (0, import_types3.serviceId)(span.service, span.env)
1753
1791
  };
1754
1792
  }
1755
1793
  function makeErrorSpanWriter(errorsPath) {
@@ -1763,7 +1801,8 @@ function makeErrorSpanWriter(errorsPath) {
1763
1801
  async function handleSpan(ctx, span) {
1764
1802
  const ts = span.startTimeIso ?? nowIso(ctx);
1765
1803
  const nowMs = ctx.now ? ctx.now() : Date.now();
1766
- const sourceId = ensureServiceNode(ctx.graph, span.service);
1804
+ const env = span.env ?? "unknown";
1805
+ const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1767
1806
  const isError = span.statusCode === 2;
1768
1807
  cacheSpanService(span, nowMs);
1769
1808
  let affectedNode = sourceId;
@@ -1786,7 +1825,7 @@ async function handleSpan(ctx, span) {
1786
1825
  const host = pickAddress(span);
1787
1826
  let resolvedViaAddress = false;
1788
1827
  if (host && host !== span.service) {
1789
- const targetId = resolveServiceId(ctx.graph, host);
1828
+ const targetId = resolveServiceId(ctx.graph, host, env);
1790
1829
  if (targetId && targetId !== sourceId) {
1791
1830
  upsertObservedEdge(
1792
1831
  ctx.graph,
@@ -1813,9 +1852,9 @@ async function handleSpan(ctx, span) {
1813
1852
  }
1814
1853
  }
1815
1854
  if (!resolvedViaAddress && span.parentSpanId) {
1816
- const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
1817
- if (parentService && parentService !== span.service) {
1818
- const parentId = ensureServiceNode(ctx.graph, parentService);
1855
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
1856
+ if (parent && parent.service !== span.service) {
1857
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1819
1858
  upsertObservedEdge(
1820
1859
  ctx.graph,
1821
1860
  import_types3.EdgeType.CALLS,
@@ -2011,6 +2050,28 @@ async function readErrorEvents(errorsPath) {
2011
2050
  throw err;
2012
2051
  }
2013
2052
  }
2053
+ function mergeSnapshot(graph, snapshot) {
2054
+ const exported = snapshot.graph;
2055
+ let nodesAdded = 0;
2056
+ let edgesAdded = 0;
2057
+ for (const node of exported.nodes ?? []) {
2058
+ if (graph.hasNode(node.key)) continue;
2059
+ if (!node.attributes) continue;
2060
+ graph.addNode(node.key, node.attributes);
2061
+ nodesAdded++;
2062
+ }
2063
+ for (const edge of exported.edges ?? []) {
2064
+ const attrs = edge.attributes;
2065
+ if (!attrs) continue;
2066
+ const id = edge.key ?? attrs.id;
2067
+ if (!id) continue;
2068
+ if (graph.hasEdge(id)) continue;
2069
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2070
+ graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
2071
+ edgesAdded++;
2072
+ }
2073
+ return { nodesAdded, edgesAdded };
2074
+ }
2014
2075
 
2015
2076
  // src/extract/services.ts
2016
2077
  init_cjs_shims();
@@ -2034,8 +2095,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2034
2095
  ".turbo",
2035
2096
  "dist",
2036
2097
  "build",
2037
- ".next"
2098
+ ".next",
2099
+ // Python virtualenv shapes (issue #344). Walking into a venv pulls in the
2100
+ // entire CPython stdlib + every installed package as if it were first-party
2101
+ // service code — 20k+ files, none of which the user wrote. The shape names
2102
+ // cover the three common venv tools (`venv` / `python -m venv`,
2103
+ // `virtualenv`) and the tox + PEP 582 conventions.
2104
+ ".venv",
2105
+ "venv",
2106
+ "__pypackages__",
2107
+ ".tox",
2108
+ // `site-packages` shows up nested inside venvs that don't carry one of the
2109
+ // outer names (system Python on macOS, in-place pyenv layouts). Listing it
2110
+ // here means we stop the walk at the boundary even when the wrapper dir
2111
+ // wasn't recognisable.
2112
+ "site-packages"
2038
2113
  ]);
2114
+ var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
2115
+ async function isPythonVenvDir(dir) {
2116
+ const cached = PYVENV_MARKER_CACHE.get(dir);
2117
+ if (cached !== void 0) return cached;
2118
+ try {
2119
+ const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
2120
+ const ok = stat.isFile();
2121
+ PYVENV_MARKER_CACHE.set(dir, ok);
2122
+ return ok;
2123
+ } catch {
2124
+ PYVENV_MARKER_CACHE.set(dir, false);
2125
+ return false;
2126
+ }
2127
+ }
2039
2128
  function isConfigFile(name) {
2040
2129
  const ext = import_node_path4.default.extname(name);
2041
2130
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
@@ -2384,6 +2473,7 @@ async function walkDirs(start, scanPath, options, visit) {
2384
2473
  const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2385
2474
  if (rel && options.ig.ignores(rel + "/")) continue;
2386
2475
  }
2476
+ if (await isPythonVenvDir(child)) continue;
2387
2477
  await visit(child);
2388
2478
  await recurse(child, depth + 1);
2389
2479
  }
@@ -2419,6 +2509,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2419
2509
  }
2420
2510
  return [...found];
2421
2511
  }
2512
+ function detectJsFramework(pkg) {
2513
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2514
+ if (deps["next"] !== void 0) return "next";
2515
+ if (deps["remix"] !== void 0) return "remix";
2516
+ for (const k of Object.keys(deps)) {
2517
+ if (k.startsWith("@remix-run/")) return "remix";
2518
+ }
2519
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
2520
+ if (deps["nuxt"] !== void 0) return "nuxt";
2521
+ if (deps["astro"] !== void 0) return "astro";
2522
+ return void 0;
2523
+ }
2422
2524
  async function discoverNodeService(scanPath, dir) {
2423
2525
  const pkgPath = import_node_path8.default.join(dir, "package.json");
2424
2526
  if (!await exists(pkgPath)) return null;
@@ -2430,6 +2532,7 @@ async function discoverNodeService(scanPath, dir) {
2430
2532
  return null;
2431
2533
  }
2432
2534
  if (!pkg.name) return null;
2535
+ const framework = detectJsFramework(pkg);
2433
2536
  const node = {
2434
2537
  id: (0, import_types5.serviceId)(pkg.name),
2435
2538
  type: import_types5.NodeType.ServiceNode,
@@ -2438,7 +2541,8 @@ async function discoverNodeService(scanPath, dir) {
2438
2541
  version: pkg.version,
2439
2542
  dependencies: pkg.dependencies ?? {},
2440
2543
  repoPath: import_node_path8.default.relative(scanPath, dir),
2441
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2544
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2545
+ ...framework ? { framework } : {}
2442
2546
  };
2443
2547
  return { pkg, dir, node };
2444
2548
  }
@@ -2648,7 +2752,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
2648
2752
  for (const entry2 of entries) {
2649
2753
  if (entry2.isDirectory()) {
2650
2754
  if (IGNORED_DIRS.has(entry2.name)) continue;
2651
- out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2755
+ const child = import_node_path9.default.join(start, entry2.name);
2756
+ if (await isPythonVenvDir(child)) continue;
2757
+ out.push(...await walkYamlFiles(child, depth + 1, max));
2652
2758
  } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2653
2759
  out.push(import_node_path9.default.join(start, entry2.name));
2654
2760
  }
@@ -3330,7 +3436,9 @@ async function walkConfigFiles(dir) {
3330
3436
  for (const entry2 of entries) {
3331
3437
  const full = import_node_path18.default.join(current, entry2.name);
3332
3438
  if (entry2.isDirectory()) {
3333
- if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3439
+ if (IGNORED_DIRS.has(entry2.name)) continue;
3440
+ if (await isPythonVenvDir(full)) continue;
3441
+ await walk(full);
3334
3442
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
3335
3443
  out.push(full);
3336
3444
  }
@@ -3398,7 +3506,9 @@ async function walkSourceFiles(dir) {
3398
3506
  for (const entry2 of entries) {
3399
3507
  const full = import_node_path19.default.join(current, entry2.name);
3400
3508
  if (entry2.isDirectory()) {
3401
- if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3509
+ if (IGNORED_DIRS.has(entry2.name)) continue;
3510
+ if (await isPythonVenvDir(full)) continue;
3511
+ await walk(full);
3402
3512
  } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3403
3513
  out.push(full);
3404
3514
  }
@@ -4034,7 +4144,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4034
4144
  for (const entry2 of entries) {
4035
4145
  if (entry2.isDirectory()) {
4036
4146
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4037
- out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
4147
+ const child = import_node_path27.default.join(start, entry2.name);
4148
+ if (await isPythonVenvDir(child)) continue;
4149
+ out.push(...await walkTfFiles(child, depth + 1, max));
4038
4150
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4039
4151
  out.push(import_node_path27.default.join(start, entry2.name));
4040
4152
  }
@@ -4082,7 +4194,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4082
4194
  for (const entry2 of entries) {
4083
4195
  if (entry2.isDirectory()) {
4084
4196
  if (IGNORED_DIRS.has(entry2.name)) continue;
4085
- out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
4197
+ const child = import_node_path28.default.join(start, entry2.name);
4198
+ if (await isPythonVenvDir(child)) continue;
4199
+ out.push(...await walkYamlFiles2(child, depth + 1, max));
4086
4200
  } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
4087
4201
  out.push(import_node_path28.default.join(start, entry2.name));
4088
4202
  }
@@ -4470,7 +4584,7 @@ init_cjs_shims();
4470
4584
  var import_node_fs18 = require("fs");
4471
4585
  var import_node_path31 = __toESM(require("path"), 1);
4472
4586
  var import_types20 = require("@neat.is/types");
4473
- var SCHEMA_VERSION = 3;
4587
+ var SCHEMA_VERSION = 4;
4474
4588
  function migrateV1ToV2(payload) {
4475
4589
  const nodes = payload.graph.nodes;
4476
4590
  if (Array.isArray(nodes)) {
@@ -4482,6 +4596,9 @@ function migrateV1ToV2(payload) {
4482
4596
  }
4483
4597
  return { ...payload, schemaVersion: 2 };
4484
4598
  }
4599
+ function migrateV3ToV4(payload) {
4600
+ return { ...payload, schemaVersion: 4 };
4601
+ }
4485
4602
  function migrateV2ToV3(payload) {
4486
4603
  const edges = payload.graph.edges;
4487
4604
  if (Array.isArray(edges)) {
@@ -4530,6 +4647,9 @@ async function loadGraphFromDisk(graph, outPath) {
4530
4647
  if (payload.schemaVersion === 2) {
4531
4648
  payload = migrateV2ToV3(payload);
4532
4649
  }
4650
+ if (payload.schemaVersion === 3) {
4651
+ payload = migrateV3ToV4(payload);
4652
+ }
4533
4653
  if (payload.schemaVersion !== SCHEMA_VERSION) {
4534
4654
  throw new Error(
4535
4655
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -5076,10 +5196,19 @@ function projectFromReq(req) {
5076
5196
  const params = req.params;
5077
5197
  return params.project ?? DEFAULT_PROJECT;
5078
5198
  }
5079
- function resolveProject(registry, req, reply) {
5199
+ function resolveProject(registry, req, reply, bootstrap) {
5080
5200
  const name = projectFromReq(req);
5081
5201
  const ctx = registry.get(name);
5082
5202
  if (!ctx) {
5203
+ const phase = bootstrap?.status(name);
5204
+ if (phase === "bootstrapping") {
5205
+ void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
5206
+ return null;
5207
+ }
5208
+ if (phase === "broken") {
5209
+ void reply.code(503).send({ ready: false, project: name, status: "broken" });
5210
+ return null;
5211
+ }
5083
5212
  void reply.code(404).send({ error: "project not found", project: name });
5084
5213
  return null;
5085
5214
  }
@@ -5109,36 +5238,38 @@ function buildLegacyRegistry(opts) {
5109
5238
  function registerRoutes(scope, ctx) {
5110
5239
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
5111
5240
  scope.get("/events", (req, reply) => {
5112
- const proj = resolveProject(registry, req, reply);
5241
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5113
5242
  if (!proj) return;
5114
5243
  handleSse(req, reply, { project: proj.name });
5115
5244
  });
5116
- scope.get("/health", async (req, reply) => {
5117
- const proj = resolveProject(registry, req, reply);
5118
- if (!proj) return;
5119
- const uptimeMs = Date.now() - startedAt;
5120
- return {
5121
- ok: true,
5122
- project: proj.name,
5123
- uptimeMs,
5124
- // Legacy fields kept additively. The web shell's StatusBar reads
5125
- // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5126
- // the canonical triple and lets the extras pass through.
5127
- uptime: Math.floor(uptimeMs / 1e3),
5128
- nodeCount: proj.graph.order,
5129
- edgeCount: proj.graph.size,
5130
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5131
- };
5132
- });
5245
+ if (ctx.scope === "project") {
5246
+ scope.get("/health", async (req, reply) => {
5247
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5248
+ if (!proj) return;
5249
+ const uptimeMs = Date.now() - startedAt;
5250
+ return {
5251
+ ok: true,
5252
+ project: proj.name,
5253
+ uptimeMs,
5254
+ // Legacy fields kept additively. The web shell's StatusBar reads
5255
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5256
+ // the canonical triple and lets the extras pass through.
5257
+ uptime: Math.floor(uptimeMs / 1e3),
5258
+ nodeCount: proj.graph.order,
5259
+ edgeCount: proj.graph.size,
5260
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5261
+ };
5262
+ });
5263
+ }
5133
5264
  scope.get("/graph", async (req, reply) => {
5134
- const proj = resolveProject(registry, req, reply);
5265
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5135
5266
  if (!proj) return;
5136
5267
  return serializeGraph(proj.graph);
5137
5268
  });
5138
5269
  scope.get(
5139
5270
  "/graph/node/:id",
5140
5271
  async (req, reply) => {
5141
- const proj = resolveProject(registry, req, reply);
5272
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5142
5273
  if (!proj) return;
5143
5274
  const { id } = req.params;
5144
5275
  if (!proj.graph.hasNode(id)) {
@@ -5150,7 +5281,7 @@ function registerRoutes(scope, ctx) {
5150
5281
  scope.get(
5151
5282
  "/graph/edges/:id",
5152
5283
  async (req, reply) => {
5153
- const proj = resolveProject(registry, req, reply);
5284
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5154
5285
  if (!proj) return;
5155
5286
  const { id } = req.params;
5156
5287
  if (!proj.graph.hasNode(id)) {
@@ -5162,7 +5293,7 @@ function registerRoutes(scope, ctx) {
5162
5293
  }
5163
5294
  );
5164
5295
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
5165
- const proj = resolveProject(registry, req, reply);
5296
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5166
5297
  if (!proj) return;
5167
5298
  const { nodeId } = req.params;
5168
5299
  if (!proj.graph.hasNode(nodeId)) {
@@ -5177,7 +5308,7 @@ function registerRoutes(scope, ctx) {
5177
5308
  return getTransitiveDependencies(proj.graph, nodeId, depth);
5178
5309
  });
5179
5310
  scope.get("/graph/divergences", async (req, reply) => {
5180
- const proj = resolveProject(registry, req, reply);
5311
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5181
5312
  if (!proj) return;
5182
5313
  let typeFilter;
5183
5314
  if (req.query.type) {
@@ -5212,7 +5343,7 @@ function registerRoutes(scope, ctx) {
5212
5343
  });
5213
5344
  });
5214
5345
  scope.get("/incidents", async (req, reply) => {
5215
- const proj = resolveProject(registry, req, reply);
5346
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5216
5347
  if (!proj) return;
5217
5348
  const epath = errorsPathFor(proj);
5218
5349
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -5224,7 +5355,7 @@ function registerRoutes(scope, ctx) {
5224
5355
  return { count: sliced.length, total, events: sliced };
5225
5356
  });
5226
5357
  scope.get("/stale-events", async (req, reply) => {
5227
- const proj = resolveProject(registry, req, reply);
5358
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5228
5359
  if (!proj) return;
5229
5360
  const spath = staleEventsPathFor(proj);
5230
5361
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -5239,7 +5370,7 @@ function registerRoutes(scope, ctx) {
5239
5370
  scope.get(
5240
5371
  "/incidents/:nodeId",
5241
5372
  async (req, reply) => {
5242
- const proj = resolveProject(registry, req, reply);
5373
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5243
5374
  if (!proj) return;
5244
5375
  const { nodeId } = req.params;
5245
5376
  if (!proj.graph.hasNode(nodeId)) {
@@ -5255,7 +5386,7 @@ function registerRoutes(scope, ctx) {
5255
5386
  }
5256
5387
  );
5257
5388
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
5258
- const proj = resolveProject(registry, req, reply);
5389
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5259
5390
  if (!proj) return;
5260
5391
  const { nodeId } = req.params;
5261
5392
  if (!proj.graph.hasNode(nodeId)) {
@@ -5275,7 +5406,7 @@ function registerRoutes(scope, ctx) {
5275
5406
  return result;
5276
5407
  });
5277
5408
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
5278
- const proj = resolveProject(registry, req, reply);
5409
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5279
5410
  if (!proj) return;
5280
5411
  const { nodeId } = req.params;
5281
5412
  if (!proj.graph.hasNode(nodeId)) {
@@ -5288,7 +5419,7 @@ function registerRoutes(scope, ctx) {
5288
5419
  return getBlastRadius(proj.graph, nodeId, depth);
5289
5420
  });
5290
5421
  scope.get("/search", async (req, reply) => {
5291
- const proj = resolveProject(registry, req, reply);
5422
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5292
5423
  if (!proj) return;
5293
5424
  const raw = (req.query.q ?? "").trim();
5294
5425
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -5319,7 +5450,7 @@ function registerRoutes(scope, ctx) {
5319
5450
  scope.get(
5320
5451
  "/graph/diff",
5321
5452
  async (req, reply) => {
5322
- const proj = resolveProject(registry, req, reply);
5453
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5323
5454
  if (!proj) return;
5324
5455
  const against = req.query.against;
5325
5456
  if (!against) {
@@ -5333,8 +5464,37 @@ function registerRoutes(scope, ctx) {
5333
5464
  }
5334
5465
  }
5335
5466
  );
5467
+ scope.post("/snapshot", async (req, reply) => {
5468
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5469
+ if (!proj) return;
5470
+ const body = req.body;
5471
+ if (!body || typeof body !== "object" || !body.snapshot) {
5472
+ return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
5473
+ }
5474
+ const snap = body.snapshot;
5475
+ if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
5476
+ return reply.code(400).send({
5477
+ error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
5478
+ });
5479
+ }
5480
+ try {
5481
+ const result = mergeSnapshot(proj.graph, snap);
5482
+ return {
5483
+ project: proj.name,
5484
+ nodesAdded: result.nodesAdded,
5485
+ edgesAdded: result.edgesAdded,
5486
+ nodeCount: proj.graph.order,
5487
+ edgeCount: proj.graph.size
5488
+ };
5489
+ } catch (err) {
5490
+ return reply.code(400).send({
5491
+ error: "snapshot merge failed",
5492
+ details: err.message
5493
+ });
5494
+ }
5495
+ });
5336
5496
  scope.post("/graph/scan", async (req, reply) => {
5337
- const proj = resolveProject(registry, req, reply);
5497
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5338
5498
  if (!proj) return;
5339
5499
  if (!proj.scanPath) {
5340
5500
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -5350,7 +5510,7 @@ function registerRoutes(scope, ctx) {
5350
5510
  };
5351
5511
  });
5352
5512
  scope.get("/policies", async (req, reply) => {
5353
- const proj = resolveProject(registry, req, reply);
5513
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5354
5514
  if (!proj) return;
5355
5515
  const policyPath = ctx.policyFilePathFor(proj);
5356
5516
  if (!policyPath) {
@@ -5367,7 +5527,7 @@ function registerRoutes(scope, ctx) {
5367
5527
  }
5368
5528
  });
5369
5529
  scope.get("/policies/violations", async (req, reply) => {
5370
- const proj = resolveProject(registry, req, reply);
5530
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5371
5531
  if (!proj) return;
5372
5532
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5373
5533
  let violations = await log.readAll();
@@ -5387,7 +5547,7 @@ function registerRoutes(scope, ctx) {
5387
5547
  return { violations };
5388
5548
  });
5389
5549
  scope.post("/policies/check", async (req, reply) => {
5390
- const proj = resolveProject(registry, req, reply);
5550
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5391
5551
  if (!proj) return;
5392
5552
  const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5393
5553
  if (!parsed.success) {
@@ -5426,7 +5586,15 @@ function registerRoutes(scope, ctx) {
5426
5586
  async function buildApi(opts) {
5427
5587
  const app = (0, import_fastify.default)({ logger: false });
5428
5588
  await app.register(import_cors.default, { origin: true });
5429
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5589
+ mountBearerAuth(app, {
5590
+ token: opts.authToken,
5591
+ trustProxy: opts.trustProxy,
5592
+ publicRead: opts.publicRead
5593
+ });
5594
+ app.get("/api/config", async () => ({
5595
+ publicRead: opts.publicRead === true,
5596
+ authProxy: opts.trustProxy === true
5597
+ }));
5430
5598
  const startedAt = opts.startedAt ?? Date.now();
5431
5599
  const registry = buildLegacyRegistry(opts);
5432
5600
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5450,10 +5618,36 @@ async function buildApi(opts) {
5450
5618
  const routeCtx = {
5451
5619
  registry,
5452
5620
  startedAt,
5621
+ scope: "root",
5453
5622
  errorsPathFor,
5454
5623
  staleEventsPathFor,
5455
- policyFilePathFor
5624
+ policyFilePathFor,
5625
+ bootstrap: opts.bootstrap
5456
5626
  };
5627
+ app.get("/health", async () => {
5628
+ const uptimeMs = Date.now() - startedAt;
5629
+ const bootstrapList = opts.bootstrap?.list() ?? [];
5630
+ const byName = new Map(bootstrapList.map((p) => [p.name, p]));
5631
+ const names = /* @__PURE__ */ new Set([
5632
+ ...registry.list(),
5633
+ ...bootstrapList.map((p) => p.name)
5634
+ ]);
5635
+ const projects = [...names].sort().map((name) => {
5636
+ const proj = registry.get(name);
5637
+ const tracked = byName.get(name);
5638
+ return {
5639
+ name,
5640
+ nodeCount: proj?.graph.order ?? 0,
5641
+ edgeCount: proj?.graph.size ?? 0,
5642
+ ...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
5643
+ };
5644
+ });
5645
+ return {
5646
+ ok: true,
5647
+ uptimeMs,
5648
+ projects
5649
+ };
5650
+ });
5457
5651
  app.get("/projects", async (_req, reply) => {
5458
5652
  try {
5459
5653
  return await listProjects();
@@ -5481,7 +5675,7 @@ async function buildApi(opts) {
5481
5675
  registerRoutes(app, routeCtx);
5482
5676
  await app.register(
5483
5677
  async (scope) => {
5484
- registerRoutes(scope, routeCtx);
5678
+ registerRoutes(scope, { ...routeCtx, scope: "project" });
5485
5679
  },
5486
5680
  { prefix: "/projects/:project" }
5487
5681
  );
@@ -5864,6 +6058,14 @@ var IGNORED_WATCH_GLOBS = [
5864
6058
  "**/.turbo/**",
5865
6059
  "**/.next/**",
5866
6060
  "**/neat-out/**",
6061
+ // Python venv shapes (issue #344). chokidar opens one watch handle per
6062
+ // descended dir; a CPython venv carries 20k+ files and trivially blows
6063
+ // through the macOS kqueue cap before extraction even runs.
6064
+ "**/.venv/**",
6065
+ "**/venv/**",
6066
+ "**/__pypackages__/**",
6067
+ "**/.tox/**",
6068
+ "**/site-packages/**",
5867
6069
  "**/.DS_Store"
5868
6070
  ];
5869
6071
  var IGNORED_WATCH_PATHS = [
@@ -5874,6 +6076,11 @@ var IGNORED_WATCH_PATHS = [
5874
6076
  /(?:^|[\\/])\.turbo[\\/]/,
5875
6077
  /(?:^|[\\/])\.next[\\/]/,
5876
6078
  /(?:^|[\\/])neat-out[\\/]/,
6079
+ /(?:^|[\\/])\.venv[\\/]/,
6080
+ /(?:^|[\\/])venv[\\/]/,
6081
+ /(?:^|[\\/])__pypackages__[\\/]/,
6082
+ /(?:^|[\\/])\.tox[\\/]/,
6083
+ /(?:^|[\\/])site-packages[\\/]/,
5877
6084
  /[\\/]?\.DS_Store$/
5878
6085
  ];
5879
6086
  function shouldIgnore(absPath) {
@@ -6311,10 +6518,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
6311
6518
 
6312
6519
  await import('@opentelemetry/auto-instrumentations-node/register')
6313
6520
  `;
6314
- function renderEnvNeat(serviceName) {
6521
+ function renderEnvNeat(projectName) {
6315
6522
  return [
6316
6523
  "# Generated by `neat init --apply` (ADR-069).",
6317
- `OTEL_SERVICE_NAME=${serviceName}`,
6524
+ `OTEL_SERVICE_NAME=${projectName}`,
6318
6525
  "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
6319
6526
  ""
6320
6527
  ].join("\n");
@@ -6370,6 +6577,100 @@ const sdk = new NodeSDK({
6370
6577
  })
6371
6578
  sdk.start()
6372
6579
  `;
6580
+ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6581
+ var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6582
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6583
+ // the OTel SDK starts so the exporter target and service name are in scope.
6584
+ import { fileURLToPath } from 'node:url'
6585
+ import path from 'node:path'
6586
+ import dotenv from 'dotenv'
6587
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6588
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6589
+
6590
+ const here = path.dirname(fileURLToPath(import.meta.url))
6591
+ dotenv.config({ path: path.join(here, '.env.neat') })
6592
+
6593
+ const sdk = new NodeSDK({
6594
+ serviceName: process.env.OTEL_SERVICE_NAME,
6595
+ instrumentations: [getNodeAutoInstrumentations()],
6596
+ })
6597
+ sdk.start()
6598
+ `;
6599
+ var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6600
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6601
+ // the OTel SDK starts so the exporter target and service name are in scope.
6602
+ const path = require('node:path')
6603
+ try {
6604
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6605
+ } catch (err) {
6606
+ // dotenv unavailable \u2014 fall through to process.env.
6607
+ }
6608
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6609
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6610
+
6611
+ const sdk = new NodeSDK({
6612
+ serviceName: process.env.OTEL_SERVICE_NAME,
6613
+ instrumentations: [getNodeAutoInstrumentations()],
6614
+ })
6615
+ sdk.start()
6616
+ `;
6617
+ var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6618
+ var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6619
+ var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6620
+ var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6621
+ var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6622
+ import './otel-init'
6623
+
6624
+ import type { Handle } from '@sveltejs/kit'
6625
+
6626
+ export const handle: Handle = async ({ event, resolve }) => {
6627
+ return resolve(event)
6628
+ }
6629
+ `;
6630
+ var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6631
+ import './otel-init'
6632
+
6633
+ /** @type {import('@sveltejs/kit').Handle} */
6634
+ export const handle = async ({ event, resolve }) => {
6635
+ return resolve(event)
6636
+ }
6637
+ `;
6638
+ var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6639
+ var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6640
+ var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6641
+ import './otel-init'
6642
+
6643
+ export default defineNitroPlugin(() => {
6644
+ // OTel SDK is initialised at module-load via the side-effect import above.
6645
+ })
6646
+ `;
6647
+ var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6648
+ require('./otel-init')
6649
+
6650
+ module.exports = defineNitroPlugin(() => {
6651
+ // OTel SDK is initialised at module-load via the side-effect import above.
6652
+ })
6653
+ `;
6654
+ var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6655
+ var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6656
+ var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6657
+ import './otel-init'
6658
+
6659
+ import { defineMiddleware } from 'astro:middleware'
6660
+
6661
+ export const onRequest = defineMiddleware(async (context, next) => {
6662
+ return next()
6663
+ })
6664
+ `;
6665
+ var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6666
+ import './otel-init'
6667
+
6668
+ import { defineMiddleware } from 'astro:middleware'
6669
+
6670
+ export const onRequest = defineMiddleware(async (context, next) => {
6671
+ return next()
6672
+ })
6673
+ `;
6373
6674
 
6374
6675
  // src/installers/javascript.ts
6375
6676
  var SDK_PACKAGES = [
@@ -6390,6 +6691,10 @@ var OTEL_ENV = {
6390
6691
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6391
6692
  value: "http://localhost:4318"
6392
6693
  };
6694
+ function envServiceName(pkg, serviceDir, project) {
6695
+ if (project && project.length > 0) return project;
6696
+ return pkg.name ?? import_node_path40.default.basename(serviceDir);
6697
+ }
6393
6698
  async function readPackageJson(serviceDir) {
6394
6699
  try {
6395
6700
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6421,6 +6726,71 @@ async function findNextConfig(serviceDir) {
6421
6726
  function hasNextDependency(pkg) {
6422
6727
  return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
6423
6728
  }
6729
+ function allDeps(pkg) {
6730
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6731
+ }
6732
+ var REMIX_ENTRY_CANDIDATES = [
6733
+ "app/entry.server.ts",
6734
+ "app/entry.server.tsx",
6735
+ "app/entry.server.js",
6736
+ "app/entry.server.jsx"
6737
+ ];
6738
+ function hasRemixDependency(pkg) {
6739
+ const deps = allDeps(pkg);
6740
+ if ("remix" in deps) return true;
6741
+ for (const name of Object.keys(deps)) {
6742
+ if (name.startsWith("@remix-run/")) return true;
6743
+ }
6744
+ return false;
6745
+ }
6746
+ async function findRemixEntry(serviceDir) {
6747
+ for (const rel of REMIX_ENTRY_CANDIDATES) {
6748
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6749
+ if (await exists2(candidate)) return candidate;
6750
+ }
6751
+ return null;
6752
+ }
6753
+ var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
6754
+ var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
6755
+ function hasSvelteKitDependency(pkg) {
6756
+ return "@sveltejs/kit" in allDeps(pkg);
6757
+ }
6758
+ async function findSvelteKitHooks(serviceDir) {
6759
+ for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
6760
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6761
+ if (await exists2(candidate)) return candidate;
6762
+ }
6763
+ return null;
6764
+ }
6765
+ async function findSvelteKitConfig(serviceDir) {
6766
+ for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
6767
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6768
+ if (await exists2(candidate)) return candidate;
6769
+ }
6770
+ return null;
6771
+ }
6772
+ var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
6773
+ function hasNuxtDependency(pkg) {
6774
+ return "nuxt" in allDeps(pkg);
6775
+ }
6776
+ async function findNuxtConfig(serviceDir) {
6777
+ for (const name of NUXT_CONFIG_CANDIDATES) {
6778
+ const candidate = import_node_path40.default.join(serviceDir, name);
6779
+ if (await exists2(candidate)) return candidate;
6780
+ }
6781
+ return null;
6782
+ }
6783
+ var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
6784
+ function hasAstroDependency(pkg) {
6785
+ return "astro" in allDeps(pkg);
6786
+ }
6787
+ async function findAstroConfig(serviceDir) {
6788
+ for (const name of ASTRO_CONFIG_CANDIDATES) {
6789
+ const candidate = import_node_path40.default.join(serviceDir, name);
6790
+ if (await exists2(candidate)) return candidate;
6791
+ }
6792
+ return null;
6793
+ }
6424
6794
  function parseNextMajor(range) {
6425
6795
  if (!range) return null;
6426
6796
  const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
@@ -6549,7 +6919,7 @@ function lineIsOtelInjection(line) {
6549
6919
  if (trimmed.length === 0) return false;
6550
6920
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6551
6921
  }
6552
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6922
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
6553
6923
  const useTs = await isTypeScriptProject(serviceDir);
6554
6924
  const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6555
6925
  const instrumentationNodeFile = import_node_path40.default.join(
@@ -6586,7 +6956,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6586
6956
  if (!await exists2(envNeatFile)) {
6587
6957
  generatedFiles.push({
6588
6958
  file: envNeatFile,
6589
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6959
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
6590
6960
  skipIfExists: true
6591
6961
  });
6592
6962
  }
@@ -6628,9 +6998,280 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6628
6998
  ...nextConfigEdit ? { nextConfigEdit } : {}
6629
6999
  };
6630
7000
  }
6631
- async function plan(serviceDir) {
7001
+ function buildDependencyEdits(pkg, manifestPath) {
7002
+ const existingDeps = allDeps(pkg);
7003
+ const edits = [];
7004
+ for (const sdk of SDK_PACKAGES) {
7005
+ if (sdk.name in existingDeps) continue;
7006
+ edits.push({
7007
+ file: manifestPath,
7008
+ kind: "add",
7009
+ name: sdk.name,
7010
+ version: sdk.version
7011
+ });
7012
+ }
7013
+ return edits;
7014
+ }
7015
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
7016
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
7017
+ if (!await exists2(envNeatFile)) {
7018
+ generatedFiles.push({
7019
+ file: envNeatFile,
7020
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7021
+ skipIfExists: true
7022
+ });
7023
+ }
7024
+ }
7025
+ function fileImportsOtelHook(raw, specifiers) {
7026
+ const lines = raw.split(/\r?\n/);
7027
+ for (const line of lines) {
7028
+ const trimmed = line.trim();
7029
+ for (const spec of specifiers) {
7030
+ const escaped = spec.replace(/\./g, "\\.");
7031
+ const pattern = new RegExp(
7032
+ `(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
7033
+ );
7034
+ if (pattern.test(trimmed)) return true;
7035
+ }
7036
+ }
7037
+ return false;
7038
+ }
7039
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
7040
+ const useTs = await isTypeScriptProject(serviceDir);
7041
+ const otelServerFile = import_node_path40.default.join(
7042
+ serviceDir,
7043
+ useTs ? "app/otel.server.ts" : "app/otel.server.js"
7044
+ );
7045
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7046
+ const generatedFiles = [];
7047
+ if (!await exists2(otelServerFile)) {
7048
+ generatedFiles.push({
7049
+ file: otelServerFile,
7050
+ contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
7051
+ skipIfExists: true
7052
+ });
7053
+ }
7054
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7055
+ const entrypointEdits = [];
7056
+ try {
7057
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
7058
+ if (!fileImportsOtelHook(raw, ["./otel.server"])) {
7059
+ const lines = raw.split(/\r?\n/);
7060
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7061
+ entrypointEdits.push({
7062
+ file: entryFile,
7063
+ before: firstReal,
7064
+ after: "import './otel.server'"
7065
+ });
7066
+ }
7067
+ } catch {
7068
+ }
7069
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7070
+ if (empty) {
7071
+ return {
7072
+ language: "javascript",
7073
+ serviceDir,
7074
+ dependencyEdits: [],
7075
+ entrypointEdits: [],
7076
+ envEdits: [],
7077
+ generatedFiles: [],
7078
+ framework: "remix"
7079
+ };
7080
+ }
7081
+ return {
7082
+ language: "javascript",
7083
+ serviceDir,
7084
+ dependencyEdits,
7085
+ entrypointEdits,
7086
+ envEdits: [OTEL_ENV],
7087
+ generatedFiles,
7088
+ framework: "remix"
7089
+ };
7090
+ }
7091
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
7092
+ const useTs = await isTypeScriptProject(serviceDir);
7093
+ const otelInitFile = import_node_path40.default.join(
7094
+ serviceDir,
7095
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7096
+ );
7097
+ const resolvedHooksFile = hooksFile ?? import_node_path40.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
7098
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7099
+ const generatedFiles = [];
7100
+ const entrypointEdits = [];
7101
+ if (!await exists2(otelInitFile)) {
7102
+ generatedFiles.push({
7103
+ file: otelInitFile,
7104
+ contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7105
+ skipIfExists: true
7106
+ });
7107
+ }
7108
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7109
+ if (hooksFile === null) {
7110
+ generatedFiles.push({
7111
+ file: resolvedHooksFile,
7112
+ contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
7113
+ skipIfExists: true
7114
+ });
7115
+ } else {
7116
+ try {
7117
+ const raw = await import_node_fs25.promises.readFile(hooksFile, "utf8");
7118
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7119
+ const lines = raw.split(/\r?\n/);
7120
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7121
+ entrypointEdits.push({
7122
+ file: hooksFile,
7123
+ before: firstReal,
7124
+ after: "import './otel-init'"
7125
+ });
7126
+ }
7127
+ } catch {
7128
+ }
7129
+ }
7130
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7131
+ if (empty) {
7132
+ return {
7133
+ language: "javascript",
7134
+ serviceDir,
7135
+ dependencyEdits: [],
7136
+ entrypointEdits: [],
7137
+ envEdits: [],
7138
+ generatedFiles: [],
7139
+ framework: "sveltekit"
7140
+ };
7141
+ }
7142
+ return {
7143
+ language: "javascript",
7144
+ serviceDir,
7145
+ dependencyEdits,
7146
+ entrypointEdits,
7147
+ envEdits: [OTEL_ENV],
7148
+ generatedFiles,
7149
+ framework: "sveltekit"
7150
+ };
7151
+ }
7152
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7153
+ const useTs = await isTypeScriptProject(serviceDir);
7154
+ const otelPluginFile = import_node_path40.default.join(
7155
+ serviceDir,
7156
+ useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
7157
+ );
7158
+ const otelInitFile = import_node_path40.default.join(
7159
+ serviceDir,
7160
+ useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
7161
+ );
7162
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7163
+ const generatedFiles = [];
7164
+ if (!await exists2(otelInitFile)) {
7165
+ generatedFiles.push({
7166
+ file: otelInitFile,
7167
+ contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7168
+ skipIfExists: true
7169
+ });
7170
+ }
7171
+ if (!await exists2(otelPluginFile)) {
7172
+ generatedFiles.push({
7173
+ file: otelPluginFile,
7174
+ contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
7175
+ skipIfExists: true
7176
+ });
7177
+ }
7178
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7179
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
7180
+ if (empty) {
7181
+ return {
7182
+ language: "javascript",
7183
+ serviceDir,
7184
+ dependencyEdits: [],
7185
+ entrypointEdits: [],
7186
+ envEdits: [],
7187
+ generatedFiles: [],
7188
+ framework: "nuxt"
7189
+ };
7190
+ }
7191
+ return {
7192
+ language: "javascript",
7193
+ serviceDir,
7194
+ dependencyEdits,
7195
+ entrypointEdits: [],
7196
+ envEdits: [OTEL_ENV],
7197
+ generatedFiles,
7198
+ framework: "nuxt"
7199
+ };
7200
+ }
7201
+ var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
7202
+ async function findAstroMiddleware(serviceDir) {
7203
+ for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
7204
+ const candidate = import_node_path40.default.join(serviceDir, rel);
7205
+ if (await exists2(candidate)) return candidate;
7206
+ }
7207
+ return null;
7208
+ }
7209
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
7210
+ const useTs = await isTypeScriptProject(serviceDir);
7211
+ const otelInitFile = import_node_path40.default.join(
7212
+ serviceDir,
7213
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7214
+ );
7215
+ const existingMiddleware = await findAstroMiddleware(serviceDir);
7216
+ const middlewareFile = existingMiddleware ?? import_node_path40.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
7217
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7218
+ const generatedFiles = [];
7219
+ const entrypointEdits = [];
7220
+ if (!await exists2(otelInitFile)) {
7221
+ generatedFiles.push({
7222
+ file: otelInitFile,
7223
+ contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7224
+ skipIfExists: true
7225
+ });
7226
+ }
7227
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7228
+ if (existingMiddleware === null) {
7229
+ generatedFiles.push({
7230
+ file: middlewareFile,
7231
+ contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
7232
+ skipIfExists: true
7233
+ });
7234
+ } else {
7235
+ try {
7236
+ const raw = await import_node_fs25.promises.readFile(existingMiddleware, "utf8");
7237
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7238
+ const lines = raw.split(/\r?\n/);
7239
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7240
+ entrypointEdits.push({
7241
+ file: existingMiddleware,
7242
+ before: firstReal,
7243
+ after: "import './otel-init'"
7244
+ });
7245
+ }
7246
+ } catch {
7247
+ }
7248
+ }
7249
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7250
+ if (empty) {
7251
+ return {
7252
+ language: "javascript",
7253
+ serviceDir,
7254
+ dependencyEdits: [],
7255
+ entrypointEdits: [],
7256
+ envEdits: [],
7257
+ generatedFiles: [],
7258
+ framework: "astro"
7259
+ };
7260
+ }
7261
+ return {
7262
+ language: "javascript",
7263
+ serviceDir,
7264
+ dependencyEdits,
7265
+ entrypointEdits,
7266
+ envEdits: [OTEL_ENV],
7267
+ generatedFiles,
7268
+ framework: "astro"
7269
+ };
7270
+ }
7271
+ async function plan(serviceDir, opts) {
6632
7272
  const pkg = await readPackageJson(serviceDir);
6633
7273
  const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
7274
+ const project = opts?.project;
6634
7275
  const empty = {
6635
7276
  language: "javascript",
6636
7277
  serviceDir,
@@ -6643,7 +7284,32 @@ async function plan(serviceDir) {
6643
7284
  if (hasNextDependency(pkg)) {
6644
7285
  const nextConfig = await findNextConfig(serviceDir);
6645
7286
  if (nextConfig) {
6646
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
7287
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
7288
+ }
7289
+ }
7290
+ if (hasRemixDependency(pkg)) {
7291
+ const remixEntry = await findRemixEntry(serviceDir);
7292
+ if (remixEntry) {
7293
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
7294
+ }
7295
+ }
7296
+ if (hasSvelteKitDependency(pkg)) {
7297
+ const hooks = await findSvelteKitHooks(serviceDir);
7298
+ const config = await findSvelteKitConfig(serviceDir);
7299
+ if (hooks || config) {
7300
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
7301
+ }
7302
+ }
7303
+ if (hasNuxtDependency(pkg)) {
7304
+ const nuxtConfig = await findNuxtConfig(serviceDir);
7305
+ if (nuxtConfig) {
7306
+ return planNuxt(serviceDir, pkg, manifestPath, project);
7307
+ }
7308
+ }
7309
+ if (hasAstroDependency(pkg)) {
7310
+ const astroConfig = await findAstroConfig(serviceDir);
7311
+ if (astroConfig) {
7312
+ return planAstro(serviceDir, pkg, manifestPath, project);
6647
7313
  }
6648
7314
  }
6649
7315
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -6691,7 +7357,7 @@ async function plan(serviceDir) {
6691
7357
  if (!await exists2(envNeatFile)) {
6692
7358
  generatedFiles.push({
6693
7359
  file: envNeatFile,
6694
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
7360
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
6695
7361
  skipIfExists: true
6696
7362
  });
6697
7363
  }
@@ -6718,9 +7384,18 @@ function isAllowedWritePath(serviceDir, target) {
6718
7384
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6719
7385
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6720
7386
  if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7387
+ const relPosix = rel.split(import_node_path40.default.sep).join("/");
7388
+ if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
7389
+ if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
7390
+ if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
7391
+ if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
7392
+ if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
7393
+ if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
7394
+ if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
6721
7395
  return false;
6722
7396
  }
6723
7397
  async function writeAtomic(file, contents) {
7398
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
6724
7399
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6725
7400
  await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
6726
7401
  await import_node_fs25.promises.rename(tmp, file);
@@ -7193,6 +7868,49 @@ var import_node_http = __toESM(require("http"), 1);
7193
7868
  var import_node_path42 = __toESM(require("path"), 1);
7194
7869
  var import_node_child_process2 = require("child_process");
7195
7870
  var import_node_readline = __toESM(require("readline"), 1);
7871
+ async function extractAndPersist(opts) {
7872
+ const services = await discoverServices(opts.scanPath);
7873
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
7874
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
7875
+ resetGraph(graphKey);
7876
+ const graph = getGraph(graphKey);
7877
+ const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
7878
+ const extraction = await extractFromDirectory(graph, opts.scanPath, {
7879
+ errorsPath: projectPaths.errorsPath
7880
+ });
7881
+ if (!opts.dryRun) {
7882
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
7883
+ }
7884
+ return {
7885
+ graph,
7886
+ graphKey,
7887
+ services,
7888
+ languages,
7889
+ nodesAdded: extraction.nodesAdded,
7890
+ edgesAdded: extraction.edgesAdded,
7891
+ snapshotPath: projectPaths.snapshotPath,
7892
+ errorsPath: projectPaths.errorsPath
7893
+ };
7894
+ }
7895
+ async function applyInstallersOver(services, project) {
7896
+ let instrumented = 0;
7897
+ let already = 0;
7898
+ let libOnly = 0;
7899
+ for (const svc of services) {
7900
+ const installer = await pickInstaller(svc.dir);
7901
+ if (!installer) continue;
7902
+ const plan3 = await installer.plan(svc.dir, { project });
7903
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
7904
+ already++;
7905
+ continue;
7906
+ }
7907
+ const outcome = await installer.apply(plan3);
7908
+ if (outcome.outcome === "instrumented") instrumented++;
7909
+ else if (outcome.outcome === "already-instrumented") already++;
7910
+ else if (outcome.outcome === "lib-only") libOnly++;
7911
+ }
7912
+ return { instrumented, alreadyInstrumented: already, libOnly };
7913
+ }
7196
7914
  async function promptYesNo(question) {
7197
7915
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
7198
7916
  return new Promise((resolve) => {
@@ -7203,28 +7921,106 @@ async function promptYesNo(question) {
7203
7921
  });
7204
7922
  });
7205
7923
  }
7206
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
7207
- async function checkDaemonHealth(restPort) {
7924
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
7925
+ var PROBE_INTERVAL_MS = 500;
7926
+ async function fetchDaemonHealth(restPort) {
7208
7927
  return new Promise((resolve) => {
7209
7928
  const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
7210
7929
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
7211
- res.resume();
7212
- resolve(ok);
7930
+ if (!ok) {
7931
+ res.resume();
7932
+ resolve(null);
7933
+ return;
7934
+ }
7935
+ let body = "";
7936
+ res.setEncoding("utf8");
7937
+ res.on("data", (chunk) => {
7938
+ body += chunk;
7939
+ });
7940
+ res.on("end", () => {
7941
+ try {
7942
+ resolve(JSON.parse(body));
7943
+ } catch {
7944
+ resolve({ ok: true });
7945
+ }
7946
+ });
7213
7947
  });
7214
- req.on("error", () => resolve(false));
7948
+ req.on("error", () => resolve(null));
7215
7949
  req.setTimeout(1e3, () => {
7216
7950
  req.destroy();
7217
- resolve(false);
7951
+ resolve(null);
7218
7952
  });
7219
7953
  });
7220
7954
  }
7955
+ async function checkDaemonHealth(restPort) {
7956
+ const body = await fetchDaemonHealth(restPort);
7957
+ return body !== null;
7958
+ }
7959
+ async function probeProjectHealth(restPort, name) {
7960
+ return new Promise((resolve) => {
7961
+ const req = import_node_http.default.get(
7962
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
7963
+ (res) => {
7964
+ const code = res.statusCode ?? 0;
7965
+ res.resume();
7966
+ if (code >= 200 && code < 300) resolve("active");
7967
+ else resolve("bootstrapping");
7968
+ }
7969
+ );
7970
+ req.on("error", () => resolve("bootstrapping"));
7971
+ req.setTimeout(1e3, () => {
7972
+ req.destroy();
7973
+ resolve("bootstrapping");
7974
+ });
7975
+ });
7976
+ }
7977
+ async function snapshotProjectStatus(restPort, body) {
7978
+ if (body.projects && body.projects.length > 0) {
7979
+ return body.projects.map((p) => ({
7980
+ name: p.name,
7981
+ status: p.status ?? "active"
7982
+ }));
7983
+ }
7984
+ const entries = await listProjects().catch(() => []);
7985
+ if (entries.length === 0) return [];
7986
+ return Promise.all(
7987
+ entries.map(async (entry2) => ({
7988
+ name: entry2.name,
7989
+ status: await probeProjectHealth(restPort, entry2.name)
7990
+ }))
7991
+ );
7992
+ }
7221
7993
  async function waitForDaemonReady(restPort, timeoutMs) {
7222
7994
  const deadline = Date.now() + timeoutMs;
7995
+ let lastBootstrapping = [];
7223
7996
  while (Date.now() < deadline) {
7224
- if (await checkDaemonHealth(restPort)) return true;
7225
- await new Promise((r) => setTimeout(r, 300));
7997
+ const body = await fetchDaemonHealth(restPort);
7998
+ if (body !== null) {
7999
+ const projects2 = await snapshotProjectStatus(restPort, body);
8000
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
8001
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
8002
+ if (bootstrapping.length === 0) {
8003
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
8004
+ }
8005
+ const key = bootstrapping.slice().sort().join(",");
8006
+ const prevKey = lastBootstrapping.slice().sort().join(",");
8007
+ if (key !== prevKey) {
8008
+ const plural = bootstrapping.length === 1 ? "" : "s";
8009
+ console.log(
8010
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
8011
+ );
8012
+ lastBootstrapping = bootstrapping;
8013
+ }
8014
+ }
8015
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
7226
8016
  }
7227
- return false;
8017
+ const final = await fetchDaemonHealth(restPort);
8018
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
8019
+ return {
8020
+ ready: false,
8021
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
8022
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
8023
+ };
7228
8024
  }
7229
8025
  function spawnDaemonDetached() {
7230
8026
  const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
@@ -7245,12 +8041,21 @@ function spawnDaemonDetached() {
7245
8041
  if (!entry2) {
7246
8042
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
7247
8043
  }
8044
+ const env = { ...process.env };
8045
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
8046
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
8047
+ env.HOST = "127.0.0.1";
8048
+ }
7248
8049
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
7249
8050
  detached: true,
7250
- stdio: "ignore",
7251
- env: process.env
8051
+ // stderr inherits the orchestrator's fd so the daemon's
8052
+ // `BindAuthorityError` message lands in front of the operator instead
8053
+ // of being swallowed (issue #341).
8054
+ stdio: ["ignore", "ignore", "inherit"],
8055
+ env
7252
8056
  });
7253
8057
  child.unref();
8058
+ return child;
7254
8059
  }
7255
8060
  function openBrowser(url) {
7256
8061
  if (!process.stdout.isTTY) return "failed";
@@ -7287,24 +8092,21 @@ async function runOrchestrator(opts) {
7287
8092
  }
7288
8093
  console.log(`neat: ${opts.scanPath}`);
7289
8094
  console.log("");
7290
- const services = await discoverServices(opts.scanPath);
7291
- const languages = [...new Set(services.map((s) => s.node.language))].sort();
8095
+ const persisted = await extractAndPersist({
8096
+ scanPath: opts.scanPath,
8097
+ project: opts.project,
8098
+ projectExplicit: opts.projectExplicit
8099
+ });
8100
+ const { graph, services, languages } = persisted;
7292
8101
  result.steps.discovery = { services: services.length, languages };
7293
8102
  console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
7294
8103
  let runApply = !opts.noInstrument;
7295
8104
  if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
7296
8105
  runApply = await promptYesNo("instrument your services and open the dashboard?");
7297
8106
  }
7298
- const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
7299
- resetGraph(graphKey);
7300
- const graph = getGraph(graphKey);
7301
- const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
7302
- const errorsPath = projectPaths.errorsPath;
7303
- const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7304
- await saveGraphToDisk(graph, projectPaths.snapshotPath);
7305
8107
  result.steps.extraction = {
7306
- nodesAdded: extraction.nodesAdded,
7307
- edgesAdded: extraction.edgesAdded
8108
+ nodesAdded: persisted.nodesAdded,
8109
+ edgesAdded: persisted.edgesAdded
7308
8110
  };
7309
8111
  const gi = await ensureNeatOutIgnored(opts.scanPath);
7310
8112
  result.steps.gitignore = gi.action;
@@ -7329,24 +8131,11 @@ async function runOrchestrator(opts) {
7329
8131
  result.steps.apply.skipped = true;
7330
8132
  console.log("skipped instrumentation (--no-instrument)");
7331
8133
  } else {
7332
- let instrumented = 0;
7333
- let already = 0;
7334
- let libOnly = 0;
7335
- for (const svc of services) {
7336
- const installer = await pickInstaller(svc.dir);
7337
- if (!installer) continue;
7338
- const plan3 = await installer.plan(svc.dir);
7339
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
7340
- already++;
7341
- continue;
7342
- }
7343
- const outcome = await installer.apply(plan3);
7344
- if (outcome.outcome === "instrumented") instrumented++;
7345
- else if (outcome.outcome === "already-instrumented") already++;
7346
- else if (outcome.outcome === "lib-only") libOnly++;
7347
- }
7348
- result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
7349
- console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
8134
+ const tally = await applyInstallersOver(services, opts.project);
8135
+ result.steps.apply = { ...tally, skipped: false };
8136
+ console.log(
8137
+ `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
8138
+ );
7350
8139
  }
7351
8140
  const restPort = Number(process.env.PORT ?? 8080);
7352
8141
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -7361,12 +8150,25 @@ async function runOrchestrator(opts) {
7361
8150
  return result;
7362
8151
  }
7363
8152
  const ready = await waitForDaemonReady(restPort, timeoutMs);
7364
- result.steps.daemon = ready ? "spawned" : "timed-out";
7365
- if (!ready) {
8153
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
8154
+ if (!ready.ready) {
7366
8155
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
8156
+ if (ready.stillBootstrapping.length > 0) {
8157
+ console.error(
8158
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
8159
+ );
8160
+ }
8161
+ if (ready.brokenProjects.length > 0) {
8162
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
8163
+ }
7367
8164
  result.exitCode = 1;
7368
8165
  return result;
7369
8166
  }
8167
+ if (ready.brokenProjects.length > 0) {
8168
+ console.warn(
8169
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
8170
+ );
8171
+ }
7370
8172
  }
7371
8173
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
7372
8174
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -7395,8 +8197,9 @@ function printSummary(result, graph, dashboardUrl) {
7395
8197
  console.log(`dashboard: ${dashboardUrl}`);
7396
8198
  }
7397
8199
 
7398
- // src/cli.ts
7399
- var import_types25 = require("@neat.is/types");
8200
+ // src/cli-verbs.ts
8201
+ init_cjs_shims();
8202
+ var import_node_path43 = __toESM(require("path"), 1);
7400
8203
 
7401
8204
  // src/cli-client.ts
7402
8205
  init_cjs_shims();
@@ -7417,13 +8220,16 @@ var TransportError = class extends Error {
7417
8220
  this.name = "TransportError";
7418
8221
  }
7419
8222
  };
7420
- function createHttpClient(baseUrl) {
8223
+ function createHttpClient(baseUrl, bearerToken) {
7421
8224
  const root = baseUrl.replace(/\/$/, "");
8225
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
7422
8226
  return {
7423
- async get(path44) {
8227
+ async get(path45) {
7424
8228
  let res;
7425
8229
  try {
7426
- res = await fetch(`${root}${path44}`);
8230
+ res = await fetch(`${root}${path45}`, {
8231
+ headers: { ...authHeader }
8232
+ });
7427
8233
  } catch (err) {
7428
8234
  throw new TransportError(
7429
8235
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -7433,18 +8239,18 @@ function createHttpClient(baseUrl) {
7433
8239
  const body = await res.text().catch(() => "");
7434
8240
  throw new HttpError(
7435
8241
  res.status,
7436
- `${res.status} ${res.statusText} on GET ${path44}: ${body}`,
8242
+ `${res.status} ${res.statusText} on GET ${path45}: ${body}`,
7437
8243
  body
7438
8244
  );
7439
8245
  }
7440
8246
  return await res.json();
7441
8247
  },
7442
- async post(path44, body) {
8248
+ async post(path45, body) {
7443
8249
  let res;
7444
8250
  try {
7445
- res = await fetch(`${root}${path44}`, {
8251
+ res = await fetch(`${root}${path45}`, {
7446
8252
  method: "POST",
7447
- headers: { "content-type": "application/json" },
8253
+ headers: { "content-type": "application/json", ...authHeader },
7448
8254
  body: JSON.stringify(body)
7449
8255
  });
7450
8256
  } catch (err) {
@@ -7456,7 +8262,7 @@ function createHttpClient(baseUrl) {
7456
8262
  const text = await res.text().catch(() => "");
7457
8263
  throw new HttpError(
7458
8264
  res.status,
7459
- `${res.status} ${res.statusText} on POST ${path44}: ${text}`,
8265
+ `${res.status} ${res.statusText} on POST ${path45}: ${text}`,
7460
8266
  text
7461
8267
  );
7462
8268
  }
@@ -7470,12 +8276,12 @@ function projectPath(project, suffix) {
7470
8276
  }
7471
8277
  async function runRootCause(client, input) {
7472
8278
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
7473
- const path44 = projectPath(
8279
+ const path45 = projectPath(
7474
8280
  input.project,
7475
8281
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
7476
8282
  );
7477
8283
  try {
7478
- const result = await client.get(path44);
8284
+ const result = await client.get(path45);
7479
8285
  const arrowPath = result.traversalPath.join(" \u2190 ");
7480
8286
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
7481
8287
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -7501,12 +8307,12 @@ async function runRootCause(client, input) {
7501
8307
  }
7502
8308
  async function runBlastRadius(client, input) {
7503
8309
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
7504
- const path44 = projectPath(
8310
+ const path45 = projectPath(
7505
8311
  input.project,
7506
8312
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
7507
8313
  );
7508
8314
  try {
7509
- const result = await client.get(path44);
8315
+ const result = await client.get(path45);
7510
8316
  if (result.totalAffected === 0) {
7511
8317
  return {
7512
8318
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -7540,12 +8346,12 @@ function formatBlastEntry(n) {
7540
8346
  }
7541
8347
  async function runDependencies(client, input) {
7542
8348
  const depth = input.depth ?? 3;
7543
- const path44 = projectPath(
8349
+ const path45 = projectPath(
7544
8350
  input.project,
7545
8351
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
7546
8352
  );
7547
8353
  try {
7548
- const result = await client.get(path44);
8354
+ const result = await client.get(path45);
7549
8355
  if (result.total === 0) {
7550
8356
  return {
7551
8357
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -7626,9 +8432,9 @@ function formatDuration(ms) {
7626
8432
  return `${Math.round(h / 24)}d`;
7627
8433
  }
7628
8434
  async function runIncidents(client, input) {
7629
- const path44 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
8435
+ const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
7630
8436
  try {
7631
- const body = await client.get(path44);
8437
+ const body = await client.get(path45);
7632
8438
  const events = body.events;
7633
8439
  if (events.length === 0) {
7634
8440
  return {
@@ -7894,8 +8700,181 @@ function exitCodeForError(err) {
7894
8700
  if (err instanceof HttpError) return 1;
7895
8701
  return 1;
7896
8702
  }
8703
+ function createSnapshotPushClient(baseUrl, token) {
8704
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
8705
+ }
8706
+ async function pushSnapshotToRemote(input) {
8707
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
8708
+ if (typeof client.post !== "function") {
8709
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
8710
+ }
8711
+ return client.post(
8712
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
8713
+ { snapshot: input.snapshot }
8714
+ );
8715
+ }
8716
+
8717
+ // src/cli-verbs.ts
8718
+ async function resolveProjectEntry(opts) {
8719
+ const entries = await listProjects();
8720
+ if (opts.project) {
8721
+ const match = entries.find((e) => e.name === opts.project);
8722
+ return match ?? null;
8723
+ }
8724
+ const cwd = opts.cwd ?? process.cwd();
8725
+ const resolvedCwd = await normalizeProjectPath(cwd);
8726
+ for (const entry2 of entries) {
8727
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
8728
+ return entry2;
8729
+ }
8730
+ }
8731
+ return null;
8732
+ }
8733
+ async function checkDaemonHealth2(baseUrl) {
8734
+ try {
8735
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
8736
+ signal: AbortSignal.timeout(1500)
8737
+ });
8738
+ return res.ok;
8739
+ } catch {
8740
+ return false;
8741
+ }
8742
+ }
8743
+ function snapshotForGraph(persisted) {
8744
+ return {
8745
+ schemaVersion: 3,
8746
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8747
+ graph: persisted.graph.export()
8748
+ };
8749
+ }
8750
+ function emitResult(result, json) {
8751
+ if (json) {
8752
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
8753
+ return;
8754
+ }
8755
+ const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
8756
+ console.log(
8757
+ `${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
8758
+ );
8759
+ if (!result.apply.skipped) {
8760
+ const { instrumented, alreadyInstrumented, libOnly } = result.apply;
8761
+ console.log(
8762
+ `instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
8763
+ );
8764
+ } else {
8765
+ console.log("skipped instrumentation (--no-instrument)");
8766
+ }
8767
+ for (const warn of result.warnings) console.error(warn);
8768
+ }
8769
+ async function runSync(opts) {
8770
+ const entry2 = await resolveProjectEntry(opts);
8771
+ if (!entry2) {
8772
+ const target = opts.project ?? opts.cwd ?? process.cwd();
8773
+ console.error(
8774
+ `neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
8775
+ );
8776
+ return {
8777
+ exitCode: 1,
8778
+ project: opts.project ?? "",
8779
+ scanPath: target,
8780
+ nodesAdded: 0,
8781
+ edgesAdded: 0,
8782
+ snapshotPath: null,
8783
+ mode: opts.to ? "remote" : "local",
8784
+ daemon: "skipped",
8785
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
8786
+ warnings: []
8787
+ };
8788
+ }
8789
+ const persisted = await extractAndPersist({
8790
+ scanPath: entry2.path,
8791
+ project: entry2.name,
8792
+ projectExplicit: true,
8793
+ dryRun: true
8794
+ });
8795
+ let snapshotPath = null;
8796
+ if (!opts.dryRun) {
8797
+ const target = persisted.snapshotPath;
8798
+ await saveGraphToDisk(persisted.graph, target);
8799
+ snapshotPath = target;
8800
+ }
8801
+ const skipApply = opts.dryRun || opts.noInstrument;
8802
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8803
+ const warnings = [];
8804
+ let daemonState = "skipped";
8805
+ let exitCode = 0;
8806
+ const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
8807
+ if (!opts.dryRun) {
8808
+ const snapshot = snapshotForGraph(persisted);
8809
+ if (opts.to) {
8810
+ const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
8811
+ try {
8812
+ await pushSnapshotToRemote({
8813
+ baseUrl: opts.to,
8814
+ token,
8815
+ project: entry2.name,
8816
+ snapshot
8817
+ });
8818
+ daemonState = "remote-ok";
8819
+ } catch (err) {
8820
+ if (err instanceof HttpError) {
8821
+ console.error(`neat sync: ${err.message}`);
8822
+ exitCode = 1;
8823
+ } else if (err instanceof TransportError) {
8824
+ console.error(`neat sync: ${err.message}`);
8825
+ exitCode = 3;
8826
+ } else {
8827
+ console.error(`neat sync: ${err.message}`);
8828
+ exitCode = 1;
8829
+ }
8830
+ daemonState = "skipped";
8831
+ }
8832
+ } else {
8833
+ const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
8834
+ const healthy = await checkDaemonHealth2(daemonUrl);
8835
+ if (healthy) {
8836
+ try {
8837
+ await pushSnapshotToRemote({
8838
+ baseUrl: daemonUrl,
8839
+ token: process.env.NEAT_AUTH_TOKEN,
8840
+ project: entry2.name,
8841
+ snapshot
8842
+ });
8843
+ daemonState = "reloaded";
8844
+ } catch (err) {
8845
+ warnings.push(
8846
+ `neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
8847
+ );
8848
+ daemonState = "down";
8849
+ exitCode = 2;
8850
+ }
8851
+ } else {
8852
+ warnings.push(
8853
+ "neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
8854
+ );
8855
+ daemonState = "down";
8856
+ exitCode = 2;
8857
+ }
8858
+ }
8859
+ }
8860
+ const result = {
8861
+ exitCode,
8862
+ project: entry2.name,
8863
+ scanPath: entry2.path,
8864
+ nodesAdded: persisted.nodesAdded,
8865
+ edgesAdded: persisted.edgesAdded,
8866
+ snapshotPath,
8867
+ mode,
8868
+ daemon: daemonState,
8869
+ apply: { ...applyTally, skipped: skipApply },
8870
+ warnings
8871
+ };
8872
+ emitResult(result, opts.json);
8873
+ return result;
8874
+ }
7897
8875
 
7898
8876
  // src/cli.ts
8877
+ var import_types25 = require("@neat.is/types");
7899
8878
  function usage() {
7900
8879
  console.log("usage: neat <command> [args] [--project <name>]");
7901
8880
  console.log("");
@@ -7916,6 +8895,8 @@ function usage() {
7916
8895
  console.log(" uninstall <name>");
7917
8896
  console.log(" Remove a project from the registry. Does not touch");
7918
8897
  console.log(" neat-out/, policy.json, or any user file.");
8898
+ console.log(" version Print the installed @neat.is/core version and exit.");
8899
+ console.log(" Aliases: --version, -v.");
7919
8900
  console.log(" skill Install or print the Claude Code MCP drop-in.");
7920
8901
  console.log(" Flags:");
7921
8902
  console.log(" --print-config print the JSON snippet to stdout");
@@ -7923,6 +8904,15 @@ function usage() {
7923
8904
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
7924
8905
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
7925
8906
  console.log(" print the OTel env-vars block to paste into your platform.");
8907
+ console.log(" sync Re-run discovery, extraction, and SDK apply against the");
8908
+ console.log(" registered project, then notify the running daemon.");
8909
+ console.log(" Flags:");
8910
+ console.log(" --project <name> target a registered project by name");
8911
+ console.log(" --to <url> push the snapshot to a remote daemon");
8912
+ console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
8913
+ console.log(" --dry-run run extraction in-memory; do not write");
8914
+ console.log(" --no-instrument skip the SDK install apply step");
8915
+ console.log(" --json emit the delta summary as JSON");
7926
8916
  console.log("");
7927
8917
  console.log("query commands (mirror the MCP tools, ADR-050):");
7928
8918
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -7976,7 +8966,9 @@ var STRING_FLAGS = [
7976
8966
  ["--error-id", "errorId"],
7977
8967
  ["--hypothetical-action", "hypotheticalAction"],
7978
8968
  ["--type", "type"],
7979
- ["--min-confidence", "minConfidence"]
8969
+ ["--min-confidence", "minConfidence"],
8970
+ ["--to", "to"],
8971
+ ["--token", "token"]
7980
8972
  ];
7981
8973
  function parseArgs(rest) {
7982
8974
  const positional = [];
@@ -8001,6 +8993,8 @@ function parseArgs(rest) {
8001
8993
  hypotheticalAction: null,
8002
8994
  type: null,
8003
8995
  minConfidence: null,
8996
+ to: null,
8997
+ token: null,
8004
8998
  positional: []
8005
8999
  };
8006
9000
  for (let i = 0; i < rest.length; i++) {
@@ -8029,7 +9023,7 @@ function parseArgs(rest) {
8029
9023
  out.yes = true;
8030
9024
  continue;
8031
9025
  }
8032
- if (arg === "--verbose" || arg === "-v") {
9026
+ if (arg === "--verbose") {
8033
9027
  out.verbose = true;
8034
9028
  continue;
8035
9029
  }
@@ -8088,6 +9082,28 @@ function assignFlag(out, field, value) {
8088
9082
  ;
8089
9083
  out[field] = value;
8090
9084
  }
9085
+ function readPackageVersion() {
9086
+ const here = typeof __dirname !== "undefined" ? __dirname : import_node_path44.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
9087
+ const candidates = [
9088
+ import_node_path44.default.resolve(here, "../package.json"),
9089
+ import_node_path44.default.resolve(here, "../../package.json")
9090
+ ];
9091
+ for (const candidate of candidates) {
9092
+ try {
9093
+ const raw = (0, import_node_fs28.readFileSync)(candidate, "utf8");
9094
+ const parsed = JSON.parse(raw);
9095
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
9096
+ return parsed.version;
9097
+ }
9098
+ } catch {
9099
+ }
9100
+ }
9101
+ return "unknown";
9102
+ }
9103
+ function printVersion() {
9104
+ process.stdout.write(`${readPackageVersion()}
9105
+ `);
9106
+ }
8091
9107
  function printBanner() {
8092
9108
  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");
8093
9109
  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");
@@ -8097,7 +9113,7 @@ function printBanner() {
8097
9113
  console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
8098
9114
  console.log("");
8099
9115
  console.log(" Network Expressive Architecting Tool");
8100
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
9116
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
8101
9117
  console.log("");
8102
9118
  }
8103
9119
  function printDiscoveryReport(opts, services) {
@@ -8125,12 +9141,12 @@ function printDiscoveryReport(opts, services) {
8125
9141
  }
8126
9142
  console.log("");
8127
9143
  }
8128
- async function buildPatchSections(services) {
9144
+ async function buildPatchSections(services, project) {
8129
9145
  const sections = [];
8130
9146
  for (const svc of services) {
8131
9147
  const installer = await pickInstaller(svc.dir);
8132
9148
  if (!installer) continue;
8133
- const plan3 = await installer.plan(svc.dir);
9149
+ const plan3 = await installer.plan(svc.dir, { project });
8134
9150
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
8135
9151
  sections.push({ installer: installer.name, plan: plan3 });
8136
9152
  }
@@ -8145,14 +9161,14 @@ async function runInit(opts) {
8145
9161
  }
8146
9162
  const services = await discoverServices(opts.scanPath);
8147
9163
  printDiscoveryReport(opts, services);
8148
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
9164
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
8149
9165
  const patch = renderPatch(sections);
8150
- const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
9166
+ const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
8151
9167
  if (opts.dryRun) {
8152
9168
  await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
8153
9169
  written.push(patchPath);
8154
9170
  console.log(`dry-run: patch written to ${patchPath}`);
8155
- const gitignorePath = import_node_path43.default.join(opts.scanPath, ".gitignore");
9171
+ const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
8156
9172
  const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
8157
9173
  const verb = gitignoreExists ? "append" : "create";
8158
9174
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
@@ -8164,9 +9180,9 @@ async function runInit(opts) {
8164
9180
  const graph = getGraph(graphKey);
8165
9181
  const projectPaths = pathsForProject(
8166
9182
  graphKey,
8167
- import_node_path43.default.join(opts.scanPath, "neat-out")
9183
+ import_node_path44.default.join(opts.scanPath, "neat-out")
8168
9184
  );
8169
- const errorsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), import_node_path43.default.basename(projectPaths.errorsPath));
9185
+ const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
8170
9186
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
8171
9187
  await saveGraphToDisk(graph, opts.outPath);
8172
9188
  written.push(opts.outPath);
@@ -8256,9 +9272,9 @@ var CLAUDE_SKILL_CONFIG = {
8256
9272
  };
8257
9273
  function claudeConfigPath() {
8258
9274
  const override = process.env.NEAT_CLAUDE_CONFIG;
8259
- if (override && override.length > 0) return import_node_path43.default.resolve(override);
9275
+ if (override && override.length > 0) return import_node_path44.default.resolve(override);
8260
9276
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8261
- return import_node_path43.default.join(home, ".claude.json");
9277
+ return import_node_path44.default.join(home, ".claude.json");
8262
9278
  }
8263
9279
  async function runSkill(opts) {
8264
9280
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -8282,7 +9298,7 @@ async function runSkill(opts) {
8282
9298
  ...existing,
8283
9299
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
8284
9300
  };
8285
- await import_node_fs28.promises.mkdir(import_node_path43.default.dirname(target), { recursive: true });
9301
+ await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
8286
9302
  await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
8287
9303
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
8288
9304
  console.log("restart Claude Code to pick up the new MCP server.");
@@ -8303,6 +9319,10 @@ async function main() {
8303
9319
  usage();
8304
9320
  process.exit(0);
8305
9321
  }
9322
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
9323
+ printVersion();
9324
+ process.exit(0);
9325
+ }
8306
9326
  const parsed = parseArgs(rest);
8307
9327
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
8308
9328
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -8317,12 +9337,12 @@ async function main() {
8317
9337
  console.error("neat init: --apply and --dry-run are mutually exclusive");
8318
9338
  process.exit(2);
8319
9339
  }
8320
- const scanPath = import_node_path43.default.resolve(target);
9340
+ const scanPath = import_node_path44.default.resolve(target);
8321
9341
  const projectExplicit = parsed.project !== null;
8322
- const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
9342
+ const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
8323
9343
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
8324
- const fallback = pathsForProject(projectKey, import_node_path43.default.join(scanPath, "neat-out")).snapshotPath;
8325
- const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
9344
+ const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
9345
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
8326
9346
  const result = await runInit({
8327
9347
  scanPath,
8328
9348
  outPath,
@@ -8343,21 +9363,21 @@ async function main() {
8343
9363
  usage();
8344
9364
  process.exit(2);
8345
9365
  }
8346
- const scanPath = import_node_path43.default.resolve(target);
9366
+ const scanPath = import_node_path44.default.resolve(target);
8347
9367
  const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
8348
9368
  if (!stat || !stat.isDirectory()) {
8349
9369
  console.error(`neat watch: ${scanPath} is not a directory`);
8350
9370
  process.exit(2);
8351
9371
  }
8352
- const projectPaths = pathsForProject(project, import_node_path43.default.join(scanPath, "neat-out"));
8353
- const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
8354
- const errorsPath = import_node_path43.default.resolve(
8355
- process.env.NEAT_ERRORS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.errorsPath))
9372
+ const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
9373
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
9374
+ const errorsPath = import_node_path44.default.resolve(
9375
+ process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
8356
9376
  );
8357
- const staleEventsPath = import_node_path43.default.resolve(
8358
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.staleEventsPath))
9377
+ const staleEventsPath = import_node_path44.default.resolve(
9378
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.staleEventsPath))
8359
9379
  );
8360
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path43.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
9380
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
8361
9381
  const handle = await startWatch(getGraph(project), {
8362
9382
  scanPath,
8363
9383
  outPath,
@@ -8476,6 +9496,18 @@ async function main() {
8476
9496
  console.log(` ${artifact.startCommand}`);
8477
9497
  return;
8478
9498
  }
9499
+ if (cmd === "sync") {
9500
+ const result = await runSync({
9501
+ ...parsed.project ? { project: parsed.project } : {},
9502
+ ...parsed.to ? { to: parsed.to } : {},
9503
+ ...parsed.token ? { token: parsed.token } : {},
9504
+ dryRun: parsed.dryRun,
9505
+ noInstrument: parsed.noInstrument,
9506
+ json: parsed.json
9507
+ });
9508
+ if (result.exitCode !== 0) process.exit(result.exitCode);
9509
+ return;
9510
+ }
8479
9511
  if (QUERY_VERBS.has(cmd)) {
8480
9512
  const code = await runQueryVerb(cmd, parsed);
8481
9513
  if (code !== 0) process.exit(code);
@@ -8491,11 +9523,11 @@ async function main() {
8491
9523
  process.exit(1);
8492
9524
  }
8493
9525
  async function tryOrchestrator(cmd, parsed) {
8494
- const scanPath = import_node_path43.default.resolve(cmd);
9526
+ const scanPath = import_node_path44.default.resolve(cmd);
8495
9527
  const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
8496
9528
  if (!stat || !stat.isDirectory()) return null;
8497
9529
  const projectExplicit = parsed.project !== null;
8498
- const projectName = projectExplicit ? parsed.project : import_node_path43.default.basename(scanPath);
9530
+ const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
8499
9531
  const result = await runOrchestrator({
8500
9532
  scanPath,
8501
9533
  project: projectName,
@@ -8697,6 +9729,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
8697
9729
  CLAUDE_SKILL_CONFIG,
8698
9730
  QUERY_VERBS,
8699
9731
  parseArgs,
9732
+ readPackageVersion,
8700
9733
  runInit,
8701
9734
  runQueryVerb,
8702
9735
  runSkill