@neat.is/core 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -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;
@@ -462,7 +486,7 @@ __export(cli_exports, {
462
486
  });
463
487
  module.exports = __toCommonJS(cli_exports);
464
488
  init_cjs_shims();
465
- var import_node_path43 = __toESM(require("path"), 1);
489
+ var import_node_path44 = __toESM(require("path"), 1);
466
490
  var import_node_fs28 = require("fs");
467
491
 
468
492
  // src/graph.ts
@@ -973,19 +997,19 @@ function confidenceFromMix(edges, now = Date.now()) {
973
997
  function longestIncomingWalk(graph, start, maxDepth) {
974
998
  let best = { path: [start], edges: [] };
975
999
  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] };
1000
+ function step(node, path45, edges) {
1001
+ if (path45.length > best.path.length) {
1002
+ best = { path: [...path45], edges: [...edges] };
979
1003
  }
980
- if (path44.length - 1 >= maxDepth) return;
1004
+ if (path45.length - 1 >= maxDepth) return;
981
1005
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
982
1006
  for (const [srcId, edge] of incoming) {
983
1007
  if (visited.has(srcId)) continue;
984
1008
  visited.add(srcId);
985
- path44.push(srcId);
1009
+ path45.push(srcId);
986
1010
  edges.push(edge);
987
- step(srcId, path44, edges);
988
- path44.pop();
1011
+ step(srcId, path45, edges);
1012
+ path45.pop();
989
1013
  edges.pop();
990
1014
  visited.delete(srcId);
991
1015
  }
@@ -1558,52 +1582,64 @@ function cacheSpanService(span, now) {
1558
1582
  if (!span.traceId || !span.spanId) return;
1559
1583
  const key = parentSpanKey(span.traceId, span.spanId);
1560
1584
  parentSpanCache.delete(key);
1561
- parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1585
+ parentSpanCache.set(key, {
1586
+ service: span.service,
1587
+ env: span.env ?? "unknown",
1588
+ expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1589
+ });
1562
1590
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1563
1591
  const oldest = parentSpanCache.keys().next().value;
1564
1592
  if (!oldest) break;
1565
1593
  parentSpanCache.delete(oldest);
1566
1594
  }
1567
1595
  }
1568
- function lookupParentSpanService(traceId, parentSpanId, now) {
1596
+ function lookupParentSpan(traceId, parentSpanId, now) {
1569
1597
  const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1570
1598
  if (!entry2) return null;
1571
1599
  if (entry2.expiresAt <= now) {
1572
1600
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1573
1601
  return null;
1574
1602
  }
1575
- return entry2.service;
1603
+ return { service: entry2.service, env: entry2.env };
1576
1604
  }
1577
- function resolveServiceId(graph, host) {
1578
- const direct = (0, import_types3.serviceId)(host);
1579
- if (graph.hasNode(direct)) return direct;
1580
- let found = null;
1605
+ function resolveServiceId(graph, host, env) {
1606
+ const envTagged = (0, import_types3.serviceId)(host, env);
1607
+ if (graph.hasNode(envTagged)) return envTagged;
1608
+ const envLess = (0, import_types3.serviceId)(host);
1609
+ if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
1610
+ let sameEnv = null;
1611
+ let envLessMatch = null;
1612
+ let anyMatch = null;
1581
1613
  graph.forEachNode((id, attrs) => {
1582
- if (found) return;
1614
+ if (sameEnv) return;
1583
1615
  const a = attrs;
1584
1616
  if (a.type !== import_types3.NodeType.ServiceNode) return;
1585
- if (a.name === host) {
1586
- found = id;
1617
+ const matchesByName = a.name === host;
1618
+ const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1619
+ if (!matchesByName && !matchesByAlias) return;
1620
+ const nodeEnv = a.env ?? "unknown";
1621
+ if (nodeEnv === env) {
1622
+ sameEnv = id;
1587
1623
  return;
1588
1624
  }
1589
- if (a.aliases && a.aliases.includes(host)) {
1590
- found = id;
1591
- }
1625
+ if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
1626
+ else if (!anyMatch) anyMatch = id;
1592
1627
  });
1593
- return found;
1628
+ return sameEnv ?? envLessMatch ?? anyMatch;
1594
1629
  }
1595
1630
  function frontierIdFor(host) {
1596
1631
  return (0, import_types3.frontierId)(host);
1597
1632
  }
1598
- function ensureServiceNode(graph, serviceName) {
1599
- const id = (0, import_types3.serviceId)(serviceName);
1633
+ function ensureServiceNode(graph, serviceName, env) {
1634
+ const id = (0, import_types3.serviceId)(serviceName, env);
1600
1635
  if (graph.hasNode(id)) return id;
1601
1636
  const node = {
1602
1637
  id,
1603
1638
  type: import_types3.NodeType.ServiceNode,
1604
1639
  name: serviceName,
1605
1640
  language: "unknown",
1606
- discoveredVia: "otel"
1641
+ discoveredVia: "otel",
1642
+ ...env !== "unknown" ? { env } : {}
1607
1643
  };
1608
1644
  graph.addNode(id, node);
1609
1645
  return id;
@@ -1749,7 +1785,7 @@ function buildErrorEventForReceiver(span) {
1749
1785
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1750
1786
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1751
1787
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1752
- affectedNode: (0, import_types3.serviceId)(span.service)
1788
+ affectedNode: (0, import_types3.serviceId)(span.service, span.env)
1753
1789
  };
1754
1790
  }
1755
1791
  function makeErrorSpanWriter(errorsPath) {
@@ -1763,7 +1799,8 @@ function makeErrorSpanWriter(errorsPath) {
1763
1799
  async function handleSpan(ctx, span) {
1764
1800
  const ts = span.startTimeIso ?? nowIso(ctx);
1765
1801
  const nowMs = ctx.now ? ctx.now() : Date.now();
1766
- const sourceId = ensureServiceNode(ctx.graph, span.service);
1802
+ const env = span.env ?? "unknown";
1803
+ const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1767
1804
  const isError = span.statusCode === 2;
1768
1805
  cacheSpanService(span, nowMs);
1769
1806
  let affectedNode = sourceId;
@@ -1786,7 +1823,7 @@ async function handleSpan(ctx, span) {
1786
1823
  const host = pickAddress(span);
1787
1824
  let resolvedViaAddress = false;
1788
1825
  if (host && host !== span.service) {
1789
- const targetId = resolveServiceId(ctx.graph, host);
1826
+ const targetId = resolveServiceId(ctx.graph, host, env);
1790
1827
  if (targetId && targetId !== sourceId) {
1791
1828
  upsertObservedEdge(
1792
1829
  ctx.graph,
@@ -1813,9 +1850,9 @@ async function handleSpan(ctx, span) {
1813
1850
  }
1814
1851
  }
1815
1852
  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);
1853
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
1854
+ if (parent && parent.service !== span.service) {
1855
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1819
1856
  upsertObservedEdge(
1820
1857
  ctx.graph,
1821
1858
  import_types3.EdgeType.CALLS,
@@ -2011,6 +2048,28 @@ async function readErrorEvents(errorsPath) {
2011
2048
  throw err;
2012
2049
  }
2013
2050
  }
2051
+ function mergeSnapshot(graph, snapshot) {
2052
+ const exported = snapshot.graph;
2053
+ let nodesAdded = 0;
2054
+ let edgesAdded = 0;
2055
+ for (const node of exported.nodes ?? []) {
2056
+ if (graph.hasNode(node.key)) continue;
2057
+ if (!node.attributes) continue;
2058
+ graph.addNode(node.key, node.attributes);
2059
+ nodesAdded++;
2060
+ }
2061
+ for (const edge of exported.edges ?? []) {
2062
+ const attrs = edge.attributes;
2063
+ if (!attrs) continue;
2064
+ const id = edge.key ?? attrs.id;
2065
+ if (!id) continue;
2066
+ if (graph.hasEdge(id)) continue;
2067
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2068
+ graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
2069
+ edgesAdded++;
2070
+ }
2071
+ return { nodesAdded, edgesAdded };
2072
+ }
2014
2073
 
2015
2074
  // src/extract/services.ts
2016
2075
  init_cjs_shims();
@@ -2419,6 +2478,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2419
2478
  }
2420
2479
  return [...found];
2421
2480
  }
2481
+ function detectJsFramework(pkg) {
2482
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2483
+ if (deps["next"] !== void 0) return "next";
2484
+ if (deps["remix"] !== void 0) return "remix";
2485
+ for (const k of Object.keys(deps)) {
2486
+ if (k.startsWith("@remix-run/")) return "remix";
2487
+ }
2488
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
2489
+ if (deps["nuxt"] !== void 0) return "nuxt";
2490
+ if (deps["astro"] !== void 0) return "astro";
2491
+ return void 0;
2492
+ }
2422
2493
  async function discoverNodeService(scanPath, dir) {
2423
2494
  const pkgPath = import_node_path8.default.join(dir, "package.json");
2424
2495
  if (!await exists(pkgPath)) return null;
@@ -2430,6 +2501,7 @@ async function discoverNodeService(scanPath, dir) {
2430
2501
  return null;
2431
2502
  }
2432
2503
  if (!pkg.name) return null;
2504
+ const framework = detectJsFramework(pkg);
2433
2505
  const node = {
2434
2506
  id: (0, import_types5.serviceId)(pkg.name),
2435
2507
  type: import_types5.NodeType.ServiceNode,
@@ -2438,7 +2510,8 @@ async function discoverNodeService(scanPath, dir) {
2438
2510
  version: pkg.version,
2439
2511
  dependencies: pkg.dependencies ?? {},
2440
2512
  repoPath: import_node_path8.default.relative(scanPath, dir),
2441
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2513
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2514
+ ...framework ? { framework } : {}
2442
2515
  };
2443
2516
  return { pkg, dir, node };
2444
2517
  }
@@ -4470,7 +4543,7 @@ init_cjs_shims();
4470
4543
  var import_node_fs18 = require("fs");
4471
4544
  var import_node_path31 = __toESM(require("path"), 1);
4472
4545
  var import_types20 = require("@neat.is/types");
4473
- var SCHEMA_VERSION = 3;
4546
+ var SCHEMA_VERSION = 4;
4474
4547
  function migrateV1ToV2(payload) {
4475
4548
  const nodes = payload.graph.nodes;
4476
4549
  if (Array.isArray(nodes)) {
@@ -4482,6 +4555,9 @@ function migrateV1ToV2(payload) {
4482
4555
  }
4483
4556
  return { ...payload, schemaVersion: 2 };
4484
4557
  }
4558
+ function migrateV3ToV4(payload) {
4559
+ return { ...payload, schemaVersion: 4 };
4560
+ }
4485
4561
  function migrateV2ToV3(payload) {
4486
4562
  const edges = payload.graph.edges;
4487
4563
  if (Array.isArray(edges)) {
@@ -4530,6 +4606,9 @@ async function loadGraphFromDisk(graph, outPath) {
4530
4606
  if (payload.schemaVersion === 2) {
4531
4607
  payload = migrateV2ToV3(payload);
4532
4608
  }
4609
+ if (payload.schemaVersion === 3) {
4610
+ payload = migrateV3ToV4(payload);
4611
+ }
4533
4612
  if (payload.schemaVersion !== SCHEMA_VERSION) {
4534
4613
  throw new Error(
4535
4614
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -5333,6 +5412,35 @@ function registerRoutes(scope, ctx) {
5333
5412
  }
5334
5413
  }
5335
5414
  );
5415
+ scope.post("/snapshot", async (req, reply) => {
5416
+ const proj = resolveProject(registry, req, reply);
5417
+ if (!proj) return;
5418
+ const body = req.body;
5419
+ if (!body || typeof body !== "object" || !body.snapshot) {
5420
+ return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
5421
+ }
5422
+ const snap = body.snapshot;
5423
+ if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
5424
+ return reply.code(400).send({
5425
+ error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
5426
+ });
5427
+ }
5428
+ try {
5429
+ const result = mergeSnapshot(proj.graph, snap);
5430
+ return {
5431
+ project: proj.name,
5432
+ nodesAdded: result.nodesAdded,
5433
+ edgesAdded: result.edgesAdded,
5434
+ nodeCount: proj.graph.order,
5435
+ edgeCount: proj.graph.size
5436
+ };
5437
+ } catch (err) {
5438
+ return reply.code(400).send({
5439
+ error: "snapshot merge failed",
5440
+ details: err.message
5441
+ });
5442
+ }
5443
+ });
5336
5444
  scope.post("/graph/scan", async (req, reply) => {
5337
5445
  const proj = resolveProject(registry, req, reply);
5338
5446
  if (!proj) return;
@@ -5426,7 +5534,15 @@ function registerRoutes(scope, ctx) {
5426
5534
  async function buildApi(opts) {
5427
5535
  const app = (0, import_fastify.default)({ logger: false });
5428
5536
  await app.register(import_cors.default, { origin: true });
5429
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5537
+ mountBearerAuth(app, {
5538
+ token: opts.authToken,
5539
+ trustProxy: opts.trustProxy,
5540
+ publicRead: opts.publicRead
5541
+ });
5542
+ app.get("/api/config", async () => ({
5543
+ publicRead: opts.publicRead === true,
5544
+ authProxy: opts.trustProxy === true
5545
+ }));
5430
5546
  const startedAt = opts.startedAt ?? Date.now();
5431
5547
  const registry = buildLegacyRegistry(opts);
5432
5548
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -6370,6 +6486,100 @@ const sdk = new NodeSDK({
6370
6486
  })
6371
6487
  sdk.start()
6372
6488
  `;
6489
+ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6490
+ var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6491
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6492
+ // the OTel SDK starts so the exporter target and service name are in scope.
6493
+ import { fileURLToPath } from 'node:url'
6494
+ import path from 'node:path'
6495
+ import dotenv from 'dotenv'
6496
+ import { NodeSDK } from '@opentelemetry/sdk-node'
6497
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6498
+
6499
+ const here = path.dirname(fileURLToPath(import.meta.url))
6500
+ dotenv.config({ path: path.join(here, '.env.neat') })
6501
+
6502
+ const sdk = new NodeSDK({
6503
+ serviceName: process.env.OTEL_SERVICE_NAME,
6504
+ instrumentations: [getNodeAutoInstrumentations()],
6505
+ })
6506
+ sdk.start()
6507
+ `;
6508
+ var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6509
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6510
+ // the OTel SDK starts so the exporter target and service name are in scope.
6511
+ const path = require('node:path')
6512
+ try {
6513
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
6514
+ } catch (err) {
6515
+ // dotenv unavailable \u2014 fall through to process.env.
6516
+ }
6517
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6518
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6519
+
6520
+ const sdk = new NodeSDK({
6521
+ serviceName: process.env.OTEL_SERVICE_NAME,
6522
+ instrumentations: [getNodeAutoInstrumentations()],
6523
+ })
6524
+ sdk.start()
6525
+ `;
6526
+ var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6527
+ var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6528
+ var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6529
+ var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6530
+ var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6531
+ import './otel-init'
6532
+
6533
+ import type { Handle } from '@sveltejs/kit'
6534
+
6535
+ export const handle: Handle = async ({ event, resolve }) => {
6536
+ return resolve(event)
6537
+ }
6538
+ `;
6539
+ var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6540
+ import './otel-init'
6541
+
6542
+ /** @type {import('@sveltejs/kit').Handle} */
6543
+ export const handle = async ({ event, resolve }) => {
6544
+ return resolve(event)
6545
+ }
6546
+ `;
6547
+ var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6548
+ var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6549
+ var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6550
+ import './otel-init'
6551
+
6552
+ export default defineNitroPlugin(() => {
6553
+ // OTel SDK is initialised at module-load via the side-effect import above.
6554
+ })
6555
+ `;
6556
+ var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6557
+ require('./otel-init')
6558
+
6559
+ module.exports = defineNitroPlugin(() => {
6560
+ // OTel SDK is initialised at module-load via the side-effect import above.
6561
+ })
6562
+ `;
6563
+ var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
6564
+ var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
6565
+ var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
6566
+ import './otel-init'
6567
+
6568
+ import { defineMiddleware } from 'astro:middleware'
6569
+
6570
+ export const onRequest = defineMiddleware(async (context, next) => {
6571
+ return next()
6572
+ })
6573
+ `;
6574
+ var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
6575
+ import './otel-init'
6576
+
6577
+ import { defineMiddleware } from 'astro:middleware'
6578
+
6579
+ export const onRequest = defineMiddleware(async (context, next) => {
6580
+ return next()
6581
+ })
6582
+ `;
6373
6583
 
6374
6584
  // src/installers/javascript.ts
6375
6585
  var SDK_PACKAGES = [
@@ -6421,6 +6631,71 @@ async function findNextConfig(serviceDir) {
6421
6631
  function hasNextDependency(pkg) {
6422
6632
  return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
6423
6633
  }
6634
+ function allDeps(pkg) {
6635
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6636
+ }
6637
+ var REMIX_ENTRY_CANDIDATES = [
6638
+ "app/entry.server.ts",
6639
+ "app/entry.server.tsx",
6640
+ "app/entry.server.js",
6641
+ "app/entry.server.jsx"
6642
+ ];
6643
+ function hasRemixDependency(pkg) {
6644
+ const deps = allDeps(pkg);
6645
+ if ("remix" in deps) return true;
6646
+ for (const name of Object.keys(deps)) {
6647
+ if (name.startsWith("@remix-run/")) return true;
6648
+ }
6649
+ return false;
6650
+ }
6651
+ async function findRemixEntry(serviceDir) {
6652
+ for (const rel of REMIX_ENTRY_CANDIDATES) {
6653
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6654
+ if (await exists2(candidate)) return candidate;
6655
+ }
6656
+ return null;
6657
+ }
6658
+ var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
6659
+ var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
6660
+ function hasSvelteKitDependency(pkg) {
6661
+ return "@sveltejs/kit" in allDeps(pkg);
6662
+ }
6663
+ async function findSvelteKitHooks(serviceDir) {
6664
+ for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
6665
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6666
+ if (await exists2(candidate)) return candidate;
6667
+ }
6668
+ return null;
6669
+ }
6670
+ async function findSvelteKitConfig(serviceDir) {
6671
+ for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
6672
+ const candidate = import_node_path40.default.join(serviceDir, rel);
6673
+ if (await exists2(candidate)) return candidate;
6674
+ }
6675
+ return null;
6676
+ }
6677
+ var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
6678
+ function hasNuxtDependency(pkg) {
6679
+ return "nuxt" in allDeps(pkg);
6680
+ }
6681
+ async function findNuxtConfig(serviceDir) {
6682
+ for (const name of NUXT_CONFIG_CANDIDATES) {
6683
+ const candidate = import_node_path40.default.join(serviceDir, name);
6684
+ if (await exists2(candidate)) return candidate;
6685
+ }
6686
+ return null;
6687
+ }
6688
+ var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
6689
+ function hasAstroDependency(pkg) {
6690
+ return "astro" in allDeps(pkg);
6691
+ }
6692
+ async function findAstroConfig(serviceDir) {
6693
+ for (const name of ASTRO_CONFIG_CANDIDATES) {
6694
+ const candidate = import_node_path40.default.join(serviceDir, name);
6695
+ if (await exists2(candidate)) return candidate;
6696
+ }
6697
+ return null;
6698
+ }
6424
6699
  function parseNextMajor(range) {
6425
6700
  if (!range) return null;
6426
6701
  const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
@@ -6628,6 +6903,276 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6628
6903
  ...nextConfigEdit ? { nextConfigEdit } : {}
6629
6904
  };
6630
6905
  }
6906
+ function buildDependencyEdits(pkg, manifestPath) {
6907
+ const existingDeps = allDeps(pkg);
6908
+ const edits = [];
6909
+ for (const sdk of SDK_PACKAGES) {
6910
+ if (sdk.name in existingDeps) continue;
6911
+ edits.push({
6912
+ file: manifestPath,
6913
+ kind: "add",
6914
+ name: sdk.name,
6915
+ version: sdk.version
6916
+ });
6917
+ }
6918
+ return edits;
6919
+ }
6920
+ async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
6921
+ const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6922
+ if (!await exists2(envNeatFile)) {
6923
+ generatedFiles.push({
6924
+ file: envNeatFile,
6925
+ contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6926
+ skipIfExists: true
6927
+ });
6928
+ }
6929
+ }
6930
+ function fileImportsOtelHook(raw, specifiers) {
6931
+ const lines = raw.split(/\r?\n/);
6932
+ for (const line of lines) {
6933
+ const trimmed = line.trim();
6934
+ for (const spec of specifiers) {
6935
+ const escaped = spec.replace(/\./g, "\\.");
6936
+ const pattern = new RegExp(
6937
+ `(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
6938
+ );
6939
+ if (pattern.test(trimmed)) return true;
6940
+ }
6941
+ }
6942
+ return false;
6943
+ }
6944
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6945
+ const useTs = await isTypeScriptProject(serviceDir);
6946
+ const otelServerFile = import_node_path40.default.join(
6947
+ serviceDir,
6948
+ useTs ? "app/otel.server.ts" : "app/otel.server.js"
6949
+ );
6950
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
6951
+ const generatedFiles = [];
6952
+ if (!await exists2(otelServerFile)) {
6953
+ generatedFiles.push({
6954
+ file: otelServerFile,
6955
+ contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
6956
+ skipIfExists: true
6957
+ });
6958
+ }
6959
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
6960
+ const entrypointEdits = [];
6961
+ try {
6962
+ const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
6963
+ if (!fileImportsOtelHook(raw, ["./otel.server"])) {
6964
+ const lines = raw.split(/\r?\n/);
6965
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6966
+ entrypointEdits.push({
6967
+ file: entryFile,
6968
+ before: firstReal,
6969
+ after: "import './otel.server'"
6970
+ });
6971
+ }
6972
+ } catch {
6973
+ }
6974
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
6975
+ if (empty) {
6976
+ return {
6977
+ language: "javascript",
6978
+ serviceDir,
6979
+ dependencyEdits: [],
6980
+ entrypointEdits: [],
6981
+ envEdits: [],
6982
+ generatedFiles: [],
6983
+ framework: "remix"
6984
+ };
6985
+ }
6986
+ return {
6987
+ language: "javascript",
6988
+ serviceDir,
6989
+ dependencyEdits,
6990
+ entrypointEdits,
6991
+ envEdits: [OTEL_ENV],
6992
+ generatedFiles,
6993
+ framework: "remix"
6994
+ };
6995
+ }
6996
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
6997
+ const useTs = await isTypeScriptProject(serviceDir);
6998
+ const otelInitFile = import_node_path40.default.join(
6999
+ serviceDir,
7000
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7001
+ );
7002
+ const resolvedHooksFile = hooksFile ?? import_node_path40.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
7003
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7004
+ const generatedFiles = [];
7005
+ const entrypointEdits = [];
7006
+ if (!await exists2(otelInitFile)) {
7007
+ generatedFiles.push({
7008
+ file: otelInitFile,
7009
+ contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
7010
+ skipIfExists: true
7011
+ });
7012
+ }
7013
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7014
+ if (hooksFile === null) {
7015
+ generatedFiles.push({
7016
+ file: resolvedHooksFile,
7017
+ contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
7018
+ skipIfExists: true
7019
+ });
7020
+ } else {
7021
+ try {
7022
+ const raw = await import_node_fs25.promises.readFile(hooksFile, "utf8");
7023
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7024
+ const lines = raw.split(/\r?\n/);
7025
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7026
+ entrypointEdits.push({
7027
+ file: hooksFile,
7028
+ before: firstReal,
7029
+ after: "import './otel-init'"
7030
+ });
7031
+ }
7032
+ } catch {
7033
+ }
7034
+ }
7035
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7036
+ if (empty) {
7037
+ return {
7038
+ language: "javascript",
7039
+ serviceDir,
7040
+ dependencyEdits: [],
7041
+ entrypointEdits: [],
7042
+ envEdits: [],
7043
+ generatedFiles: [],
7044
+ framework: "sveltekit"
7045
+ };
7046
+ }
7047
+ return {
7048
+ language: "javascript",
7049
+ serviceDir,
7050
+ dependencyEdits,
7051
+ entrypointEdits,
7052
+ envEdits: [OTEL_ENV],
7053
+ generatedFiles,
7054
+ framework: "sveltekit"
7055
+ };
7056
+ }
7057
+ async function planNuxt(serviceDir, pkg, manifestPath) {
7058
+ const useTs = await isTypeScriptProject(serviceDir);
7059
+ const otelPluginFile = import_node_path40.default.join(
7060
+ serviceDir,
7061
+ useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
7062
+ );
7063
+ const otelInitFile = import_node_path40.default.join(
7064
+ serviceDir,
7065
+ useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
7066
+ );
7067
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7068
+ const generatedFiles = [];
7069
+ if (!await exists2(otelInitFile)) {
7070
+ generatedFiles.push({
7071
+ file: otelInitFile,
7072
+ contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
7073
+ skipIfExists: true
7074
+ });
7075
+ }
7076
+ if (!await exists2(otelPluginFile)) {
7077
+ generatedFiles.push({
7078
+ file: otelPluginFile,
7079
+ contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
7080
+ skipIfExists: true
7081
+ });
7082
+ }
7083
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7084
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
7085
+ if (empty) {
7086
+ return {
7087
+ language: "javascript",
7088
+ serviceDir,
7089
+ dependencyEdits: [],
7090
+ entrypointEdits: [],
7091
+ envEdits: [],
7092
+ generatedFiles: [],
7093
+ framework: "nuxt"
7094
+ };
7095
+ }
7096
+ return {
7097
+ language: "javascript",
7098
+ serviceDir,
7099
+ dependencyEdits,
7100
+ entrypointEdits: [],
7101
+ envEdits: [OTEL_ENV],
7102
+ generatedFiles,
7103
+ framework: "nuxt"
7104
+ };
7105
+ }
7106
+ var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
7107
+ async function findAstroMiddleware(serviceDir) {
7108
+ for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
7109
+ const candidate = import_node_path40.default.join(serviceDir, rel);
7110
+ if (await exists2(candidate)) return candidate;
7111
+ }
7112
+ return null;
7113
+ }
7114
+ async function planAstro(serviceDir, pkg, manifestPath) {
7115
+ const useTs = await isTypeScriptProject(serviceDir);
7116
+ const otelInitFile = import_node_path40.default.join(
7117
+ serviceDir,
7118
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
7119
+ );
7120
+ const existingMiddleware = await findAstroMiddleware(serviceDir);
7121
+ const middlewareFile = existingMiddleware ?? import_node_path40.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
7122
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
7123
+ const generatedFiles = [];
7124
+ const entrypointEdits = [];
7125
+ if (!await exists2(otelInitFile)) {
7126
+ generatedFiles.push({
7127
+ file: otelInitFile,
7128
+ contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
7129
+ skipIfExists: true
7130
+ });
7131
+ }
7132
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
7133
+ if (existingMiddleware === null) {
7134
+ generatedFiles.push({
7135
+ file: middlewareFile,
7136
+ contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
7137
+ skipIfExists: true
7138
+ });
7139
+ } else {
7140
+ try {
7141
+ const raw = await import_node_fs25.promises.readFile(existingMiddleware, "utf8");
7142
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
7143
+ const lines = raw.split(/\r?\n/);
7144
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
7145
+ entrypointEdits.push({
7146
+ file: existingMiddleware,
7147
+ before: firstReal,
7148
+ after: "import './otel-init'"
7149
+ });
7150
+ }
7151
+ } catch {
7152
+ }
7153
+ }
7154
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
7155
+ if (empty) {
7156
+ return {
7157
+ language: "javascript",
7158
+ serviceDir,
7159
+ dependencyEdits: [],
7160
+ entrypointEdits: [],
7161
+ envEdits: [],
7162
+ generatedFiles: [],
7163
+ framework: "astro"
7164
+ };
7165
+ }
7166
+ return {
7167
+ language: "javascript",
7168
+ serviceDir,
7169
+ dependencyEdits,
7170
+ entrypointEdits,
7171
+ envEdits: [OTEL_ENV],
7172
+ generatedFiles,
7173
+ framework: "astro"
7174
+ };
7175
+ }
6631
7176
  async function plan(serviceDir) {
6632
7177
  const pkg = await readPackageJson(serviceDir);
6633
7178
  const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
@@ -6646,6 +7191,31 @@ async function plan(serviceDir) {
6646
7191
  return planNext(serviceDir, pkg, manifestPath, nextConfig);
6647
7192
  }
6648
7193
  }
7194
+ if (hasRemixDependency(pkg)) {
7195
+ const remixEntry = await findRemixEntry(serviceDir);
7196
+ if (remixEntry) {
7197
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry);
7198
+ }
7199
+ }
7200
+ if (hasSvelteKitDependency(pkg)) {
7201
+ const hooks = await findSvelteKitHooks(serviceDir);
7202
+ const config = await findSvelteKitConfig(serviceDir);
7203
+ if (hooks || config) {
7204
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
7205
+ }
7206
+ }
7207
+ if (hasNuxtDependency(pkg)) {
7208
+ const nuxtConfig = await findNuxtConfig(serviceDir);
7209
+ if (nuxtConfig) {
7210
+ return planNuxt(serviceDir, pkg, manifestPath);
7211
+ }
7212
+ }
7213
+ if (hasAstroDependency(pkg)) {
7214
+ const astroConfig = await findAstroConfig(serviceDir);
7215
+ if (astroConfig) {
7216
+ return planAstro(serviceDir, pkg, manifestPath);
7217
+ }
7218
+ }
6649
7219
  const entryFile = await resolveEntry(serviceDir, pkg);
6650
7220
  if (!entryFile) {
6651
7221
  return { ...empty, libOnly: true };
@@ -6718,9 +7288,18 @@ function isAllowedWritePath(serviceDir, target) {
6718
7288
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6719
7289
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6720
7290
  if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7291
+ const relPosix = rel.split(import_node_path40.default.sep).join("/");
7292
+ if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
7293
+ if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
7294
+ if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
7295
+ if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
7296
+ if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
7297
+ if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
7298
+ if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
6721
7299
  return false;
6722
7300
  }
6723
7301
  async function writeAtomic(file, contents) {
7302
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(file), { recursive: true });
6724
7303
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6725
7304
  await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
6726
7305
  await import_node_fs25.promises.rename(tmp, file);
@@ -7193,6 +7772,49 @@ var import_node_http = __toESM(require("http"), 1);
7193
7772
  var import_node_path42 = __toESM(require("path"), 1);
7194
7773
  var import_node_child_process2 = require("child_process");
7195
7774
  var import_node_readline = __toESM(require("readline"), 1);
7775
+ async function extractAndPersist(opts) {
7776
+ const services = await discoverServices(opts.scanPath);
7777
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
7778
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
7779
+ resetGraph(graphKey);
7780
+ const graph = getGraph(graphKey);
7781
+ const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
7782
+ const extraction = await extractFromDirectory(graph, opts.scanPath, {
7783
+ errorsPath: projectPaths.errorsPath
7784
+ });
7785
+ if (!opts.dryRun) {
7786
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
7787
+ }
7788
+ return {
7789
+ graph,
7790
+ graphKey,
7791
+ services,
7792
+ languages,
7793
+ nodesAdded: extraction.nodesAdded,
7794
+ edgesAdded: extraction.edgesAdded,
7795
+ snapshotPath: projectPaths.snapshotPath,
7796
+ errorsPath: projectPaths.errorsPath
7797
+ };
7798
+ }
7799
+ async function applyInstallersOver(services) {
7800
+ let instrumented = 0;
7801
+ let already = 0;
7802
+ let libOnly = 0;
7803
+ for (const svc of services) {
7804
+ const installer = await pickInstaller(svc.dir);
7805
+ if (!installer) continue;
7806
+ const plan3 = await installer.plan(svc.dir);
7807
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
7808
+ already++;
7809
+ continue;
7810
+ }
7811
+ const outcome = await installer.apply(plan3);
7812
+ if (outcome.outcome === "instrumented") instrumented++;
7813
+ else if (outcome.outcome === "already-instrumented") already++;
7814
+ else if (outcome.outcome === "lib-only") libOnly++;
7815
+ }
7816
+ return { instrumented, alreadyInstrumented: already, libOnly };
7817
+ }
7196
7818
  async function promptYesNo(question) {
7197
7819
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
7198
7820
  return new Promise((resolve) => {
@@ -7287,24 +7909,21 @@ async function runOrchestrator(opts) {
7287
7909
  }
7288
7910
  console.log(`neat: ${opts.scanPath}`);
7289
7911
  console.log("");
7290
- const services = await discoverServices(opts.scanPath);
7291
- const languages = [...new Set(services.map((s) => s.node.language))].sort();
7912
+ const persisted = await extractAndPersist({
7913
+ scanPath: opts.scanPath,
7914
+ project: opts.project,
7915
+ projectExplicit: opts.projectExplicit
7916
+ });
7917
+ const { graph, services, languages } = persisted;
7292
7918
  result.steps.discovery = { services: services.length, languages };
7293
7919
  console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
7294
7920
  let runApply = !opts.noInstrument;
7295
7921
  if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
7296
7922
  runApply = await promptYesNo("instrument your services and open the dashboard?");
7297
7923
  }
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
7924
  result.steps.extraction = {
7306
- nodesAdded: extraction.nodesAdded,
7307
- edgesAdded: extraction.edgesAdded
7925
+ nodesAdded: persisted.nodesAdded,
7926
+ edgesAdded: persisted.edgesAdded
7308
7927
  };
7309
7928
  const gi = await ensureNeatOutIgnored(opts.scanPath);
7310
7929
  result.steps.gitignore = gi.action;
@@ -7329,24 +7948,11 @@ async function runOrchestrator(opts) {
7329
7948
  result.steps.apply.skipped = true;
7330
7949
  console.log("skipped instrumentation (--no-instrument)");
7331
7950
  } 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}`);
7951
+ const tally = await applyInstallersOver(services);
7952
+ result.steps.apply = { ...tally, skipped: false };
7953
+ console.log(
7954
+ `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
7955
+ );
7350
7956
  }
7351
7957
  const restPort = Number(process.env.PORT ?? 8080);
7352
7958
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -7395,8 +8001,9 @@ function printSummary(result, graph, dashboardUrl) {
7395
8001
  console.log(`dashboard: ${dashboardUrl}`);
7396
8002
  }
7397
8003
 
7398
- // src/cli.ts
7399
- var import_types25 = require("@neat.is/types");
8004
+ // src/cli-verbs.ts
8005
+ init_cjs_shims();
8006
+ var import_node_path43 = __toESM(require("path"), 1);
7400
8007
 
7401
8008
  // src/cli-client.ts
7402
8009
  init_cjs_shims();
@@ -7417,13 +8024,16 @@ var TransportError = class extends Error {
7417
8024
  this.name = "TransportError";
7418
8025
  }
7419
8026
  };
7420
- function createHttpClient(baseUrl) {
8027
+ function createHttpClient(baseUrl, bearerToken) {
7421
8028
  const root = baseUrl.replace(/\/$/, "");
8029
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
7422
8030
  return {
7423
- async get(path44) {
8031
+ async get(path45) {
7424
8032
  let res;
7425
8033
  try {
7426
- res = await fetch(`${root}${path44}`);
8034
+ res = await fetch(`${root}${path45}`, {
8035
+ headers: { ...authHeader }
8036
+ });
7427
8037
  } catch (err) {
7428
8038
  throw new TransportError(
7429
8039
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -7433,18 +8043,18 @@ function createHttpClient(baseUrl) {
7433
8043
  const body = await res.text().catch(() => "");
7434
8044
  throw new HttpError(
7435
8045
  res.status,
7436
- `${res.status} ${res.statusText} on GET ${path44}: ${body}`,
8046
+ `${res.status} ${res.statusText} on GET ${path45}: ${body}`,
7437
8047
  body
7438
8048
  );
7439
8049
  }
7440
8050
  return await res.json();
7441
8051
  },
7442
- async post(path44, body) {
8052
+ async post(path45, body) {
7443
8053
  let res;
7444
8054
  try {
7445
- res = await fetch(`${root}${path44}`, {
8055
+ res = await fetch(`${root}${path45}`, {
7446
8056
  method: "POST",
7447
- headers: { "content-type": "application/json" },
8057
+ headers: { "content-type": "application/json", ...authHeader },
7448
8058
  body: JSON.stringify(body)
7449
8059
  });
7450
8060
  } catch (err) {
@@ -7456,7 +8066,7 @@ function createHttpClient(baseUrl) {
7456
8066
  const text = await res.text().catch(() => "");
7457
8067
  throw new HttpError(
7458
8068
  res.status,
7459
- `${res.status} ${res.statusText} on POST ${path44}: ${text}`,
8069
+ `${res.status} ${res.statusText} on POST ${path45}: ${text}`,
7460
8070
  text
7461
8071
  );
7462
8072
  }
@@ -7470,12 +8080,12 @@ function projectPath(project, suffix) {
7470
8080
  }
7471
8081
  async function runRootCause(client, input) {
7472
8082
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
7473
- const path44 = projectPath(
8083
+ const path45 = projectPath(
7474
8084
  input.project,
7475
8085
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
7476
8086
  );
7477
8087
  try {
7478
- const result = await client.get(path44);
8088
+ const result = await client.get(path45);
7479
8089
  const arrowPath = result.traversalPath.join(" \u2190 ");
7480
8090
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
7481
8091
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -7501,12 +8111,12 @@ async function runRootCause(client, input) {
7501
8111
  }
7502
8112
  async function runBlastRadius(client, input) {
7503
8113
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
7504
- const path44 = projectPath(
8114
+ const path45 = projectPath(
7505
8115
  input.project,
7506
8116
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
7507
8117
  );
7508
8118
  try {
7509
- const result = await client.get(path44);
8119
+ const result = await client.get(path45);
7510
8120
  if (result.totalAffected === 0) {
7511
8121
  return {
7512
8122
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -7540,12 +8150,12 @@ function formatBlastEntry(n) {
7540
8150
  }
7541
8151
  async function runDependencies(client, input) {
7542
8152
  const depth = input.depth ?? 3;
7543
- const path44 = projectPath(
8153
+ const path45 = projectPath(
7544
8154
  input.project,
7545
8155
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
7546
8156
  );
7547
8157
  try {
7548
- const result = await client.get(path44);
8158
+ const result = await client.get(path45);
7549
8159
  if (result.total === 0) {
7550
8160
  return {
7551
8161
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -7626,9 +8236,9 @@ function formatDuration(ms) {
7626
8236
  return `${Math.round(h / 24)}d`;
7627
8237
  }
7628
8238
  async function runIncidents(client, input) {
7629
- const path44 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
8239
+ const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
7630
8240
  try {
7631
- const body = await client.get(path44);
8241
+ const body = await client.get(path45);
7632
8242
  const events = body.events;
7633
8243
  if (events.length === 0) {
7634
8244
  return {
@@ -7894,8 +8504,181 @@ function exitCodeForError(err) {
7894
8504
  if (err instanceof HttpError) return 1;
7895
8505
  return 1;
7896
8506
  }
8507
+ function createSnapshotPushClient(baseUrl, token) {
8508
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
8509
+ }
8510
+ async function pushSnapshotToRemote(input) {
8511
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
8512
+ if (typeof client.post !== "function") {
8513
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
8514
+ }
8515
+ return client.post(
8516
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
8517
+ { snapshot: input.snapshot }
8518
+ );
8519
+ }
8520
+
8521
+ // src/cli-verbs.ts
8522
+ async function resolveProjectEntry(opts) {
8523
+ const entries = await listProjects();
8524
+ if (opts.project) {
8525
+ const match = entries.find((e) => e.name === opts.project);
8526
+ return match ?? null;
8527
+ }
8528
+ const cwd = opts.cwd ?? process.cwd();
8529
+ const resolvedCwd = await normalizeProjectPath(cwd);
8530
+ for (const entry2 of entries) {
8531
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
8532
+ return entry2;
8533
+ }
8534
+ }
8535
+ return null;
8536
+ }
8537
+ async function checkDaemonHealth2(baseUrl) {
8538
+ try {
8539
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
8540
+ signal: AbortSignal.timeout(1500)
8541
+ });
8542
+ return res.ok;
8543
+ } catch {
8544
+ return false;
8545
+ }
8546
+ }
8547
+ function snapshotForGraph(persisted) {
8548
+ return {
8549
+ schemaVersion: 3,
8550
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8551
+ graph: persisted.graph.export()
8552
+ };
8553
+ }
8554
+ function emitResult(result, json) {
8555
+ if (json) {
8556
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
8557
+ return;
8558
+ }
8559
+ const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
8560
+ console.log(
8561
+ `${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
8562
+ );
8563
+ if (!result.apply.skipped) {
8564
+ const { instrumented, alreadyInstrumented, libOnly } = result.apply;
8565
+ console.log(
8566
+ `instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
8567
+ );
8568
+ } else {
8569
+ console.log("skipped instrumentation (--no-instrument)");
8570
+ }
8571
+ for (const warn of result.warnings) console.error(warn);
8572
+ }
8573
+ async function runSync(opts) {
8574
+ const entry2 = await resolveProjectEntry(opts);
8575
+ if (!entry2) {
8576
+ const target = opts.project ?? opts.cwd ?? process.cwd();
8577
+ console.error(
8578
+ `neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
8579
+ );
8580
+ return {
8581
+ exitCode: 1,
8582
+ project: opts.project ?? "",
8583
+ scanPath: target,
8584
+ nodesAdded: 0,
8585
+ edgesAdded: 0,
8586
+ snapshotPath: null,
8587
+ mode: opts.to ? "remote" : "local",
8588
+ daemon: "skipped",
8589
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
8590
+ warnings: []
8591
+ };
8592
+ }
8593
+ const persisted = await extractAndPersist({
8594
+ scanPath: entry2.path,
8595
+ project: entry2.name,
8596
+ projectExplicit: true,
8597
+ dryRun: true
8598
+ });
8599
+ let snapshotPath = null;
8600
+ if (!opts.dryRun) {
8601
+ const target = persisted.snapshotPath;
8602
+ await saveGraphToDisk(persisted.graph, target);
8603
+ snapshotPath = target;
8604
+ }
8605
+ const skipApply = opts.dryRun || opts.noInstrument;
8606
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
8607
+ const warnings = [];
8608
+ let daemonState = "skipped";
8609
+ let exitCode = 0;
8610
+ const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
8611
+ if (!opts.dryRun) {
8612
+ const snapshot = snapshotForGraph(persisted);
8613
+ if (opts.to) {
8614
+ const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
8615
+ try {
8616
+ await pushSnapshotToRemote({
8617
+ baseUrl: opts.to,
8618
+ token,
8619
+ project: entry2.name,
8620
+ snapshot
8621
+ });
8622
+ daemonState = "remote-ok";
8623
+ } catch (err) {
8624
+ if (err instanceof HttpError) {
8625
+ console.error(`neat sync: ${err.message}`);
8626
+ exitCode = 1;
8627
+ } else if (err instanceof TransportError) {
8628
+ console.error(`neat sync: ${err.message}`);
8629
+ exitCode = 3;
8630
+ } else {
8631
+ console.error(`neat sync: ${err.message}`);
8632
+ exitCode = 1;
8633
+ }
8634
+ daemonState = "skipped";
8635
+ }
8636
+ } else {
8637
+ const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
8638
+ const healthy = await checkDaemonHealth2(daemonUrl);
8639
+ if (healthy) {
8640
+ try {
8641
+ await pushSnapshotToRemote({
8642
+ baseUrl: daemonUrl,
8643
+ token: process.env.NEAT_AUTH_TOKEN,
8644
+ project: entry2.name,
8645
+ snapshot
8646
+ });
8647
+ daemonState = "reloaded";
8648
+ } catch (err) {
8649
+ warnings.push(
8650
+ `neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
8651
+ );
8652
+ daemonState = "down";
8653
+ exitCode = 2;
8654
+ }
8655
+ } else {
8656
+ warnings.push(
8657
+ "neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
8658
+ );
8659
+ daemonState = "down";
8660
+ exitCode = 2;
8661
+ }
8662
+ }
8663
+ }
8664
+ const result = {
8665
+ exitCode,
8666
+ project: entry2.name,
8667
+ scanPath: entry2.path,
8668
+ nodesAdded: persisted.nodesAdded,
8669
+ edgesAdded: persisted.edgesAdded,
8670
+ snapshotPath,
8671
+ mode,
8672
+ daemon: daemonState,
8673
+ apply: { ...applyTally, skipped: skipApply },
8674
+ warnings
8675
+ };
8676
+ emitResult(result, opts.json);
8677
+ return result;
8678
+ }
7897
8679
 
7898
8680
  // src/cli.ts
8681
+ var import_types25 = require("@neat.is/types");
7899
8682
  function usage() {
7900
8683
  console.log("usage: neat <command> [args] [--project <name>]");
7901
8684
  console.log("");
@@ -7923,6 +8706,15 @@ function usage() {
7923
8706
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
7924
8707
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
7925
8708
  console.log(" print the OTel env-vars block to paste into your platform.");
8709
+ console.log(" sync Re-run discovery, extraction, and SDK apply against the");
8710
+ console.log(" registered project, then notify the running daemon.");
8711
+ console.log(" Flags:");
8712
+ console.log(" --project <name> target a registered project by name");
8713
+ console.log(" --to <url> push the snapshot to a remote daemon");
8714
+ console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
8715
+ console.log(" --dry-run run extraction in-memory; do not write");
8716
+ console.log(" --no-instrument skip the SDK install apply step");
8717
+ console.log(" --json emit the delta summary as JSON");
7926
8718
  console.log("");
7927
8719
  console.log("query commands (mirror the MCP tools, ADR-050):");
7928
8720
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -7976,7 +8768,9 @@ var STRING_FLAGS = [
7976
8768
  ["--error-id", "errorId"],
7977
8769
  ["--hypothetical-action", "hypotheticalAction"],
7978
8770
  ["--type", "type"],
7979
- ["--min-confidence", "minConfidence"]
8771
+ ["--min-confidence", "minConfidence"],
8772
+ ["--to", "to"],
8773
+ ["--token", "token"]
7980
8774
  ];
7981
8775
  function parseArgs(rest) {
7982
8776
  const positional = [];
@@ -8001,6 +8795,8 @@ function parseArgs(rest) {
8001
8795
  hypotheticalAction: null,
8002
8796
  type: null,
8003
8797
  minConfidence: null,
8798
+ to: null,
8799
+ token: null,
8004
8800
  positional: []
8005
8801
  };
8006
8802
  for (let i = 0; i < rest.length; i++) {
@@ -8147,12 +8943,12 @@ async function runInit(opts) {
8147
8943
  printDiscoveryReport(opts, services);
8148
8944
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
8149
8945
  const patch = renderPatch(sections);
8150
- const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
8946
+ const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
8151
8947
  if (opts.dryRun) {
8152
8948
  await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
8153
8949
  written.push(patchPath);
8154
8950
  console.log(`dry-run: patch written to ${patchPath}`);
8155
- const gitignorePath = import_node_path43.default.join(opts.scanPath, ".gitignore");
8951
+ const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
8156
8952
  const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
8157
8953
  const verb = gitignoreExists ? "append" : "create";
8158
8954
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
@@ -8164,9 +8960,9 @@ async function runInit(opts) {
8164
8960
  const graph = getGraph(graphKey);
8165
8961
  const projectPaths = pathsForProject(
8166
8962
  graphKey,
8167
- import_node_path43.default.join(opts.scanPath, "neat-out")
8963
+ import_node_path44.default.join(opts.scanPath, "neat-out")
8168
8964
  );
8169
- const errorsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), import_node_path43.default.basename(projectPaths.errorsPath));
8965
+ const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
8170
8966
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
8171
8967
  await saveGraphToDisk(graph, opts.outPath);
8172
8968
  written.push(opts.outPath);
@@ -8256,9 +9052,9 @@ var CLAUDE_SKILL_CONFIG = {
8256
9052
  };
8257
9053
  function claudeConfigPath() {
8258
9054
  const override = process.env.NEAT_CLAUDE_CONFIG;
8259
- if (override && override.length > 0) return import_node_path43.default.resolve(override);
9055
+ if (override && override.length > 0) return import_node_path44.default.resolve(override);
8260
9056
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8261
- return import_node_path43.default.join(home, ".claude.json");
9057
+ return import_node_path44.default.join(home, ".claude.json");
8262
9058
  }
8263
9059
  async function runSkill(opts) {
8264
9060
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -8282,7 +9078,7 @@ async function runSkill(opts) {
8282
9078
  ...existing,
8283
9079
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
8284
9080
  };
8285
- await import_node_fs28.promises.mkdir(import_node_path43.default.dirname(target), { recursive: true });
9081
+ await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
8286
9082
  await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
8287
9083
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
8288
9084
  console.log("restart Claude Code to pick up the new MCP server.");
@@ -8317,12 +9113,12 @@ async function main() {
8317
9113
  console.error("neat init: --apply and --dry-run are mutually exclusive");
8318
9114
  process.exit(2);
8319
9115
  }
8320
- const scanPath = import_node_path43.default.resolve(target);
9116
+ const scanPath = import_node_path44.default.resolve(target);
8321
9117
  const projectExplicit = parsed.project !== null;
8322
- const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
9118
+ const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
8323
9119
  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);
9120
+ const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
9121
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
8326
9122
  const result = await runInit({
8327
9123
  scanPath,
8328
9124
  outPath,
@@ -8343,21 +9139,21 @@ async function main() {
8343
9139
  usage();
8344
9140
  process.exit(2);
8345
9141
  }
8346
- const scanPath = import_node_path43.default.resolve(target);
9142
+ const scanPath = import_node_path44.default.resolve(target);
8347
9143
  const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
8348
9144
  if (!stat || !stat.isDirectory()) {
8349
9145
  console.error(`neat watch: ${scanPath} is not a directory`);
8350
9146
  process.exit(2);
8351
9147
  }
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))
9148
+ const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
9149
+ const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
9150
+ const errorsPath = import_node_path44.default.resolve(
9151
+ process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
8356
9152
  );
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))
9153
+ const staleEventsPath = import_node_path44.default.resolve(
9154
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.staleEventsPath))
8359
9155
  );
8360
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path43.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
9156
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
8361
9157
  const handle = await startWatch(getGraph(project), {
8362
9158
  scanPath,
8363
9159
  outPath,
@@ -8476,6 +9272,18 @@ async function main() {
8476
9272
  console.log(` ${artifact.startCommand}`);
8477
9273
  return;
8478
9274
  }
9275
+ if (cmd === "sync") {
9276
+ const result = await runSync({
9277
+ ...parsed.project ? { project: parsed.project } : {},
9278
+ ...parsed.to ? { to: parsed.to } : {},
9279
+ ...parsed.token ? { token: parsed.token } : {},
9280
+ dryRun: parsed.dryRun,
9281
+ noInstrument: parsed.noInstrument,
9282
+ json: parsed.json
9283
+ });
9284
+ if (result.exitCode !== 0) process.exit(result.exitCode);
9285
+ return;
9286
+ }
8479
9287
  if (QUERY_VERBS.has(cmd)) {
8480
9288
  const code = await runQueryVerb(cmd, parsed);
8481
9289
  if (code !== 0) process.exit(code);
@@ -8491,11 +9299,11 @@ async function main() {
8491
9299
  process.exit(1);
8492
9300
  }
8493
9301
  async function tryOrchestrator(cmd, parsed) {
8494
- const scanPath = import_node_path43.default.resolve(cmd);
9302
+ const scanPath = import_node_path44.default.resolve(cmd);
8495
9303
  const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
8496
9304
  if (!stat || !stat.isDirectory()) return null;
8497
9305
  const projectExplicit = parsed.project !== null;
8498
- const projectName = projectExplicit ? parsed.project : import_node_path43.default.basename(scanPath);
9306
+ const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
8499
9307
  const result = await runOrchestrator({
8500
9308
  scanPath,
8501
9309
  project: projectName,