@neat.is/core 0.4.5 → 0.4.7

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.js CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  setStatus,
42
42
  startPersistLoop,
43
43
  startStalenessLoop
44
- } from "./chunk-RBWL4HRB.js";
44
+ } from "./chunk-7FTK47JQ.js";
45
45
  import {
46
46
  startOtelGrpcReceiver
47
47
  } from "./chunk-3QCRUEQD.js";
@@ -52,8 +52,8 @@ import {
52
52
  } from "./chunk-HVF4S7J3.js";
53
53
 
54
54
  // src/cli.ts
55
- import path8 from "path";
56
- import { promises as fs7, readFileSync } from "fs";
55
+ import path9 from "path";
56
+ import { promises as fs8, readFileSync } from "fs";
57
57
  import { fileURLToPath } from "url";
58
58
 
59
59
  // src/gitignore.ts
@@ -693,38 +693,119 @@ import path4 from "path";
693
693
 
694
694
  // src/installers/templates.ts
695
695
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
696
+ var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
697
+ function neatCallsiteProcessorSource(ts) {
698
+ const stackT = ts ? ": string" : "";
699
+ const spanT = ts ? ": any" : "";
700
+ const strOpt = ts ? ": string | undefined" : "";
701
+ return `function __neatPickUserFrame(stack${stackT}) {
702
+ const lines = String(stack || '').split('\\n')
703
+ for (let i = 0; i < lines.length; i++) {
704
+ const raw = lines[i].trim()
705
+ if (raw.indexOf('at ') !== 0) continue
706
+ if (raw.indexOf('node_modules') !== -1) continue
707
+ if (raw.indexOf('@opentelemetry') !== -1) continue
708
+ if (raw.indexOf('node:') !== -1) continue
709
+ if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
710
+ const bodyText = raw.slice(3)
711
+ const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
712
+ if (!loc) continue
713
+ const paren = bodyText.lastIndexOf('(')
714
+ let filepath${strOpt}
715
+ let fn${strOpt}
716
+ if (paren !== -1) {
717
+ fn = bodyText.slice(0, paren).trim()
718
+ if (fn.indexOf('async ') === 0) fn = fn.slice(6)
719
+ if (fn.indexOf('new ') === 0) fn = fn.slice(4)
720
+ filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
721
+ } else {
722
+ filepath = bodyText.slice(0, bodyText.length - loc[0].length)
723
+ }
724
+ if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
725
+ if (!filepath) continue
726
+ return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
727
+ }
728
+ return null
729
+ }
730
+
731
+ class NeatCallSiteSpanProcessor {
732
+ onStart(span${spanT}) {
733
+ if (!span || (span.kind !== 2 && span.kind !== 3)) return
734
+ const frame = __neatPickUserFrame(new Error().stack)
735
+ if (!frame) return
736
+ span.setAttribute('code.filepath', frame.filepath)
737
+ if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
738
+ if (frame.function) span.setAttribute('code.function', frame.function)
739
+ }
740
+ onEnd() {}
741
+ forceFlush() { return Promise.resolve() }
742
+ shutdown() { return Promise.resolve() }
743
+ }`;
744
+ }
745
+ var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
746
+ var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
747
+ function neatRegisterCallsiteSource(ts) {
748
+ const providerT = ts ? ": any" : "";
749
+ return `try {
750
+ const provider${providerT} = trace.getTracerProvider()
751
+ const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
752
+ if (delegate && typeof delegate.addSpanProcessor === 'function') {
753
+ delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
754
+ }
755
+ } catch {
756
+ // capture is best-effort; span export is unaffected
757
+ }`;
758
+ }
696
759
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
760
+ ${OTEL_INIT_STAMP}
697
761
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
698
762
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
699
763
 
700
764
  const { NodeSDK } = require('@opentelemetry/sdk-node')
701
765
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
766
+ const { trace } = require('@opentelemetry/api')
767
+
768
+ ${CALLSITE_PROCESSOR_JS}
702
769
 
703
770
  const instrumentations = [getNodeAutoInstrumentations()]
704
771
  __INSTRUMENTATION_BLOCK__
705
- new NodeSDK({ instrumentations }).start()
772
+ const sdk = new NodeSDK({ instrumentations })
773
+ sdk.start()
774
+ ${neatRegisterCallsiteSource(false)}
706
775
  `;
707
776
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
777
+ ${OTEL_INIT_STAMP}
708
778
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
709
779
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
710
780
 
711
781
  import { NodeSDK } from '@opentelemetry/sdk-node'
712
782
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
783
+ import { trace } from '@opentelemetry/api'
784
+
785
+ ${CALLSITE_PROCESSOR_JS}
713
786
 
714
787
  const instrumentations = [getNodeAutoInstrumentations()]
715
788
  __INSTRUMENTATION_BLOCK__
716
- new NodeSDK({ instrumentations }).start()
789
+ const sdk = new NodeSDK({ instrumentations })
790
+ sdk.start()
791
+ ${neatRegisterCallsiteSource(false)}
717
792
  `;
718
793
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
794
+ ${OTEL_INIT_STAMP}
719
795
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
720
796
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
721
797
 
722
798
  import { NodeSDK } from '@opentelemetry/sdk-node'
723
799
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
800
+ import { trace } from '@opentelemetry/api'
801
+
802
+ ${CALLSITE_PROCESSOR_TS}
724
803
 
725
804
  const instrumentations = [getNodeAutoInstrumentations()]
726
805
  __INSTRUMENTATION_BLOCK__
727
- new NodeSDK({ instrumentations }).start()
806
+ const sdk = new NodeSDK({ instrumentations })
807
+ sdk.start()
808
+ ${neatRegisterCallsiteSource(true)}
728
809
  `;
729
810
  function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
730
811
  const block = registrations.length === 0 ? "" : `
@@ -877,13 +958,20 @@ var SDK_PACKAGES = [
877
958
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
878
959
  { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
879
960
  ];
961
+ function getMajor(versionRange) {
962
+ if (!versionRange) return 0;
963
+ const match = versionRange.match(/(\d+)/);
964
+ return match ? parseInt(match[1], 10) : 0;
965
+ }
880
966
  function detectNonBundledInstrumentations(pkg) {
881
967
  const deps = allDeps(pkg);
882
968
  const out = [];
883
969
  if ("@prisma/client" in deps) {
970
+ const prismaMajor = getMajor(deps["@prisma/client"]);
971
+ const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
884
972
  out.push({
885
973
  pkg: "@prisma/instrumentation",
886
- version: "^5.0.0",
974
+ version: prismaInstrVersion,
887
975
  registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
888
976
  });
889
977
  }
@@ -941,6 +1029,23 @@ async function exists(p) {
941
1029
  return false;
942
1030
  }
943
1031
  }
1032
+ async function readFileMaybe(p) {
1033
+ try {
1034
+ return await fs4.readFile(p, "utf8");
1035
+ } catch {
1036
+ return null;
1037
+ }
1038
+ }
1039
+ async function planOtelInitGeneration(file, contents) {
1040
+ const existing = await readFileMaybe(file);
1041
+ if (existing === null) {
1042
+ return { file, contents, skipIfExists: true };
1043
+ }
1044
+ if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
1045
+ return { file, contents, skipIfExists: false };
1046
+ }
1047
+ return null;
1048
+ }
944
1049
  async function detect(serviceDir) {
945
1050
  const pkg = await readPackageJson(serviceDir);
946
1051
  return pkg !== null && typeof pkg.name === "string";
@@ -1671,18 +1776,11 @@ async function plan(serviceDir, opts) {
1671
1776
  const projectName = projectToken(pkg, serviceDir, project);
1672
1777
  const registrations = nonBundled.map((i) => i.registration);
1673
1778
  const generatedFiles = [];
1674
- if (!await exists(otelInitFile)) {
1675
- generatedFiles.push({
1676
- file: otelInitFile,
1677
- contents: renderNodeOtelInit(
1678
- otelInitContents(flavor),
1679
- svcName,
1680
- projectName,
1681
- registrations
1682
- ),
1683
- skipIfExists: true
1684
- });
1685
- }
1779
+ const otelInitGen = await planOtelInitGeneration(
1780
+ otelInitFile,
1781
+ renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
1782
+ );
1783
+ if (otelInitGen) generatedFiles.push(otelInitGen);
1686
1784
  if (!await exists(envNeatFile)) {
1687
1785
  generatedFiles.push({
1688
1786
  file: envNeatFile,
@@ -2209,19 +2307,99 @@ function renderPatch(sections) {
2209
2307
  }
2210
2308
 
2211
2309
  // src/orchestrator.ts
2212
- import { promises as fs6 } from "fs";
2310
+ import { promises as fs7 } from "fs";
2213
2311
  import http from "http";
2214
2312
  import net from "net";
2313
+ import path7 from "path";
2314
+ import { spawn as spawn3 } from "child_process";
2315
+ import readline from "readline";
2316
+
2317
+ // src/installers/package-manager.ts
2318
+ import { promises as fs6 } from "fs";
2215
2319
  import path6 from "path";
2216
2320
  import { spawn as spawn2 } from "child_process";
2217
- import readline from "readline";
2321
+ var LOCKFILE_PRIORITY = [
2322
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
2323
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
2324
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
2325
+ {
2326
+ lockfile: "package-lock.json",
2327
+ pm: "npm",
2328
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
2329
+ }
2330
+ ];
2331
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
2332
+ async function exists3(p) {
2333
+ try {
2334
+ await fs6.access(p);
2335
+ return true;
2336
+ } catch {
2337
+ return false;
2338
+ }
2339
+ }
2340
+ async function detectPackageManager(serviceDir) {
2341
+ let dir = path6.resolve(serviceDir);
2342
+ const stops = /* @__PURE__ */ new Set();
2343
+ for (let i = 0; i < 64; i++) {
2344
+ if (stops.has(dir)) break;
2345
+ stops.add(dir);
2346
+ for (const candidate of LOCKFILE_PRIORITY) {
2347
+ const lockPath = path6.join(dir, candidate.lockfile);
2348
+ if (await exists3(lockPath)) {
2349
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
2350
+ }
2351
+ }
2352
+ const parent = path6.dirname(dir);
2353
+ if (parent === dir) break;
2354
+ dir = parent;
2355
+ }
2356
+ return { pm: "npm", cwd: path6.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
2357
+ }
2358
+ async function runPackageManagerInstall(cmd) {
2359
+ return new Promise((resolve) => {
2360
+ const child = spawn2(cmd.pm, cmd.args, {
2361
+ cwd: cmd.cwd,
2362
+ // Inherit PATH + HOME so the user's installed managers resolve.
2363
+ env: process.env,
2364
+ // `false` keeps the parent in control of cleanup if the orchestrator
2365
+ // exits before install finishes. Cross-platform-safe.
2366
+ shell: false,
2367
+ stdio: ["ignore", "ignore", "pipe"]
2368
+ });
2369
+ let stderr = "";
2370
+ child.stderr?.on("data", (chunk) => {
2371
+ stderr += chunk.toString("utf8");
2372
+ });
2373
+ child.on("error", (err) => {
2374
+ resolve({
2375
+ pm: cmd.pm,
2376
+ cwd: cmd.cwd,
2377
+ args: cmd.args,
2378
+ exitCode: 127,
2379
+ stderr: stderr + `
2380
+ ${err.message}`
2381
+ });
2382
+ });
2383
+ child.on("close", (code) => {
2384
+ resolve({
2385
+ pm: cmd.pm,
2386
+ cwd: cmd.cwd,
2387
+ args: cmd.args,
2388
+ exitCode: code ?? 1,
2389
+ stderr: stderr.trim()
2390
+ });
2391
+ });
2392
+ });
2393
+ }
2394
+
2395
+ // src/orchestrator.ts
2218
2396
  async function extractAndPersist(opts) {
2219
2397
  const services = await discoverServices(opts.scanPath);
2220
2398
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
2221
2399
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2222
2400
  resetGraph(graphKey);
2223
2401
  const graph = getGraph(graphKey);
2224
- const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2402
+ const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
2225
2403
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
2226
2404
  errorsPath: projectPaths.errorsPath
2227
2405
  });
@@ -2239,12 +2417,15 @@ async function extractAndPersist(opts) {
2239
2417
  errorsPath: projectPaths.errorsPath
2240
2418
  };
2241
2419
  }
2242
- async function applyInstallersOver(services, project) {
2420
+ async function applyInstallersOver(services, project, options = {}) {
2421
+ const resolveManager = options.resolveManager ?? detectPackageManager;
2422
+ const runInstall = options.runInstall ?? runPackageManagerInstall;
2243
2423
  let instrumented = 0;
2244
2424
  let already = 0;
2245
2425
  let libOnly = 0;
2246
2426
  let browserBundle = 0;
2247
2427
  let reactNative = 0;
2428
+ const installPlans = /* @__PURE__ */ new Map();
2248
2429
  for (const svc of services) {
2249
2430
  const installer = await pickInstaller(svc.dir);
2250
2431
  if (!installer) continue;
@@ -2254,8 +2435,14 @@ async function applyInstallersOver(services, project) {
2254
2435
  continue;
2255
2436
  }
2256
2437
  const outcome = await installer.apply(plan3);
2257
- if (outcome.outcome === "instrumented") instrumented++;
2258
- else if (outcome.outcome === "already-instrumented") already++;
2438
+ if (outcome.outcome === "instrumented") {
2439
+ instrumented++;
2440
+ if (plan3.dependencyEdits.length > 0) {
2441
+ const cmd = await resolveManager(svc.dir);
2442
+ const key = `${cmd.pm}:${cmd.cwd}`;
2443
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
2444
+ }
2445
+ } else if (outcome.outcome === "already-instrumented") already++;
2259
2446
  else if (outcome.outcome === "lib-only") libOnly++;
2260
2447
  else if (outcome.outcome === "browser-bundle") {
2261
2448
  browserBundle++;
@@ -2265,7 +2452,30 @@ async function applyInstallersOver(services, project) {
2265
2452
  console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2266
2453
  }
2267
2454
  }
2268
- return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
2455
+ const packageManagerInstalls = [];
2456
+ for (const cmd of installPlans.values()) {
2457
+ console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
2458
+ const result = await runInstall(cmd);
2459
+ packageManagerInstalls.push(result);
2460
+ if (result.exitCode !== 0) {
2461
+ console.error(
2462
+ `neat: ${cmd.pm} install failed in ${cmd.cwd} (exit ${result.exitCode}); run it manually to surface the error.`
2463
+ );
2464
+ if (result.stderr.length > 0) {
2465
+ for (const line of result.stderr.split(/\r?\n/).slice(0, 20)) {
2466
+ console.error(` ${line}`);
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ return {
2472
+ instrumented,
2473
+ alreadyInstrumented: already,
2474
+ libOnly,
2475
+ browserBundle,
2476
+ reactNative,
2477
+ packageManagerInstalls
2478
+ };
2269
2479
  }
2270
2480
  async function promptYesNo(question) {
2271
2481
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2401,10 +2611,10 @@ function formatPortCollisionMessage(port) {
2401
2611
  ];
2402
2612
  }
2403
2613
  function spawnDaemonDetached() {
2404
- const here = path6.dirname(new URL(import.meta.url).pathname);
2614
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2405
2615
  const candidates = [
2406
- path6.join(here, "neatd.cjs"),
2407
- path6.join(here, "neatd.js")
2616
+ path7.join(here, "neatd.cjs"),
2617
+ path7.join(here, "neatd.js")
2408
2618
  ];
2409
2619
  let entry2 = null;
2410
2620
  const fsSync = __require("fs");
@@ -2424,7 +2634,7 @@ function spawnDaemonDetached() {
2424
2634
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2425
2635
  env.HOST = "127.0.0.1";
2426
2636
  }
2427
- const child = spawn2(process.execPath, [entry2, "start"], {
2637
+ const child = spawn3(process.execPath, [entry2, "start"], {
2428
2638
  detached: true,
2429
2639
  // stderr inherits the orchestrator's fd so the daemon's
2430
2640
  // `BindAuthorityError` message lands in front of the operator instead
@@ -2441,7 +2651,7 @@ function openBrowser(url) {
2441
2651
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2442
2652
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
2443
2653
  try {
2444
- const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2654
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
2445
2655
  child.on("error", () => {
2446
2656
  });
2447
2657
  child.unref();
@@ -2462,7 +2672,7 @@ async function runOrchestrator(opts) {
2462
2672
  browser: "skipped"
2463
2673
  }
2464
2674
  };
2465
- const stat = await fs6.stat(opts.scanPath).catch(() => null);
2675
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
2466
2676
  if (!stat || !stat.isDirectory()) {
2467
2677
  console.error(`neat: ${opts.scanPath} is not a directory`);
2468
2678
  result.exitCode = 2;
@@ -2530,6 +2740,10 @@ async function runOrchestrator(opts) {
2530
2740
  console.log(
2531
2741
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
2532
2742
  );
2743
+ const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
2744
+ if (failedInstalls.length > 0) {
2745
+ result.exitCode = 1;
2746
+ }
2533
2747
  }
2534
2748
  const restPort = Number(process.env.PORT ?? 8080);
2535
2749
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -2600,7 +2814,7 @@ function printSummary(result, graph, dashboardUrl) {
2600
2814
  }
2601
2815
 
2602
2816
  // src/cli-verbs.ts
2603
- import path7 from "path";
2817
+ import path8 from "path";
2604
2818
 
2605
2819
  // src/cli-client.ts
2606
2820
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -2624,10 +2838,10 @@ function createHttpClient(baseUrl, bearerToken) {
2624
2838
  const root = baseUrl.replace(/\/$/, "");
2625
2839
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
2626
2840
  return {
2627
- async get(path9) {
2841
+ async get(path10) {
2628
2842
  let res;
2629
2843
  try {
2630
- res = await fetch(`${root}${path9}`, {
2844
+ res = await fetch(`${root}${path10}`, {
2631
2845
  headers: { ...authHeader }
2632
2846
  });
2633
2847
  } catch (err) {
@@ -2639,16 +2853,16 @@ function createHttpClient(baseUrl, bearerToken) {
2639
2853
  const body = await res.text().catch(() => "");
2640
2854
  throw new HttpError(
2641
2855
  res.status,
2642
- `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
2856
+ `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
2643
2857
  body
2644
2858
  );
2645
2859
  }
2646
2860
  return await res.json();
2647
2861
  },
2648
- async post(path9, body) {
2862
+ async post(path10, body) {
2649
2863
  let res;
2650
2864
  try {
2651
- res = await fetch(`${root}${path9}`, {
2865
+ res = await fetch(`${root}${path10}`, {
2652
2866
  method: "POST",
2653
2867
  headers: { "content-type": "application/json", ...authHeader },
2654
2868
  body: JSON.stringify(body)
@@ -2662,7 +2876,7 @@ function createHttpClient(baseUrl, bearerToken) {
2662
2876
  const text = await res.text().catch(() => "");
2663
2877
  throw new HttpError(
2664
2878
  res.status,
2665
- `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
2879
+ `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
2666
2880
  text
2667
2881
  );
2668
2882
  }
@@ -2676,12 +2890,12 @@ function projectPath(project, suffix) {
2676
2890
  }
2677
2891
  async function runRootCause(client, input) {
2678
2892
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
2679
- const path9 = projectPath(
2893
+ const path10 = projectPath(
2680
2894
  input.project,
2681
2895
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
2682
2896
  );
2683
2897
  try {
2684
- const result = await client.get(path9);
2898
+ const result = await client.get(path10);
2685
2899
  const arrowPath = result.traversalPath.join(" \u2190 ");
2686
2900
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
2687
2901
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -2707,12 +2921,12 @@ async function runRootCause(client, input) {
2707
2921
  }
2708
2922
  async function runBlastRadius(client, input) {
2709
2923
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
2710
- const path9 = projectPath(
2924
+ const path10 = projectPath(
2711
2925
  input.project,
2712
2926
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
2713
2927
  );
2714
2928
  try {
2715
- const result = await client.get(path9);
2929
+ const result = await client.get(path10);
2716
2930
  if (result.totalAffected === 0) {
2717
2931
  return {
2718
2932
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -2746,12 +2960,12 @@ function formatBlastEntry(n) {
2746
2960
  }
2747
2961
  async function runDependencies(client, input) {
2748
2962
  const depth = input.depth ?? 3;
2749
- const path9 = projectPath(
2963
+ const path10 = projectPath(
2750
2964
  input.project,
2751
2965
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
2752
2966
  );
2753
2967
  try {
2754
- const result = await client.get(path9);
2968
+ const result = await client.get(path10);
2755
2969
  if (result.total === 0) {
2756
2970
  return {
2757
2971
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -2832,9 +3046,9 @@ function formatDuration(ms) {
2832
3046
  return `${Math.round(h / 24)}d`;
2833
3047
  }
2834
3048
  async function runIncidents(client, input) {
2835
- const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
3049
+ const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2836
3050
  try {
2837
- const body = await client.get(path9);
3051
+ const body = await client.get(path10);
2838
3052
  const events = body.events;
2839
3053
  if (events.length === 0) {
2840
3054
  return {
@@ -3124,7 +3338,7 @@ async function resolveProjectEntry(opts) {
3124
3338
  const cwd = opts.cwd ?? process.cwd();
3125
3339
  const resolvedCwd = await normalizeProjectPath(cwd);
3126
3340
  for (const entry2 of entries) {
3127
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
3341
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
3128
3342
  return entry2;
3129
3343
  }
3130
3344
  }
@@ -3484,10 +3698,10 @@ function assignFlag(out, field, value) {
3484
3698
  out[field] = value;
3485
3699
  }
3486
3700
  function readPackageVersion() {
3487
- const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3701
+ const here = typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath(import.meta.url));
3488
3702
  const candidates = [
3489
- path8.resolve(here, "../package.json"),
3490
- path8.resolve(here, "../../package.json")
3703
+ path9.resolve(here, "../package.json"),
3704
+ path9.resolve(here, "../../package.json")
3491
3705
  ];
3492
3706
  for (const candidate of candidates) {
3493
3707
  try {
@@ -3555,7 +3769,7 @@ async function buildPatchSections(services, project) {
3555
3769
  }
3556
3770
  async function runInit(opts) {
3557
3771
  const written = [];
3558
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
3772
+ const stat = await fs8.stat(opts.scanPath).catch(() => null);
3559
3773
  if (!stat || !stat.isDirectory()) {
3560
3774
  console.error(`neat init: ${opts.scanPath} is not a directory`);
3561
3775
  return { exitCode: 2, writtenFiles: written };
@@ -3564,13 +3778,13 @@ async function runInit(opts) {
3564
3778
  printDiscoveryReport(opts, services);
3565
3779
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
3566
3780
  const patch = renderPatch(sections);
3567
- const patchPath = path8.join(opts.scanPath, "neat.patch");
3781
+ const patchPath = path9.join(opts.scanPath, "neat.patch");
3568
3782
  if (opts.dryRun) {
3569
- await fs7.writeFile(patchPath, patch, "utf8");
3783
+ await fs8.writeFile(patchPath, patch, "utf8");
3570
3784
  written.push(patchPath);
3571
3785
  console.log(`dry-run: patch written to ${patchPath}`);
3572
- const gitignorePath = path8.join(opts.scanPath, ".gitignore");
3573
- const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
3786
+ const gitignorePath = path9.join(opts.scanPath, ".gitignore");
3787
+ const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
3574
3788
  const verb = gitignoreExists ? "append" : "create";
3575
3789
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
3576
3790
  console.log("rerun without --dry-run to register and snapshot.");
@@ -3581,9 +3795,9 @@ async function runInit(opts) {
3581
3795
  const graph = getGraph(graphKey);
3582
3796
  const projectPaths = pathsForProject(
3583
3797
  graphKey,
3584
- path8.join(opts.scanPath, "neat-out")
3798
+ path9.join(opts.scanPath, "neat-out")
3585
3799
  );
3586
- const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
3800
+ const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
3587
3801
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
3588
3802
  await saveGraphToDisk(graph, opts.outPath);
3589
3803
  written.push(opts.outPath);
@@ -3662,7 +3876,7 @@ async function runInit(opts) {
3662
3876
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3663
3877
  }
3664
3878
  } else {
3665
- await fs7.writeFile(patchPath, patch, "utf8");
3879
+ await fs8.writeFile(patchPath, patch, "utf8");
3666
3880
  written.push(patchPath);
3667
3881
  }
3668
3882
  }
@@ -3702,9 +3916,9 @@ var CLAUDE_SKILL_CONFIG = {
3702
3916
  };
3703
3917
  function claudeConfigPath() {
3704
3918
  const override = process.env.NEAT_CLAUDE_CONFIG;
3705
- if (override && override.length > 0) return path8.resolve(override);
3919
+ if (override && override.length > 0) return path9.resolve(override);
3706
3920
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3707
- return path8.join(home, ".claude.json");
3921
+ return path9.join(home, ".claude.json");
3708
3922
  }
3709
3923
  async function runSkill(opts) {
3710
3924
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -3716,7 +3930,7 @@ async function runSkill(opts) {
3716
3930
  const target = claudeConfigPath();
3717
3931
  let existing = {};
3718
3932
  try {
3719
- existing = JSON.parse(await fs7.readFile(target, "utf8"));
3933
+ existing = JSON.parse(await fs8.readFile(target, "utf8"));
3720
3934
  } catch (err) {
3721
3935
  if (err.code !== "ENOENT") {
3722
3936
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -3728,8 +3942,8 @@ async function runSkill(opts) {
3728
3942
  ...existing,
3729
3943
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
3730
3944
  };
3731
- await fs7.mkdir(path8.dirname(target), { recursive: true });
3732
- await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
3945
+ await fs8.mkdir(path9.dirname(target), { recursive: true });
3946
+ await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
3733
3947
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
3734
3948
  console.log("restart Claude Code to pick up the new MCP server.");
3735
3949
  return { exitCode: 0 };
@@ -3767,12 +3981,12 @@ async function main() {
3767
3981
  console.error("neat init: --apply and --dry-run are mutually exclusive");
3768
3982
  process.exit(2);
3769
3983
  }
3770
- const scanPath = path8.resolve(target);
3984
+ const scanPath = path9.resolve(target);
3771
3985
  const projectExplicit = parsed.project !== null;
3772
- const projectName = projectExplicit ? project : path8.basename(scanPath);
3986
+ const projectName = projectExplicit ? project : path9.basename(scanPath);
3773
3987
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
3774
- const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
3775
- const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
3988
+ const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
3989
+ const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
3776
3990
  const result = await runInit({
3777
3991
  scanPath,
3778
3992
  outPath,
@@ -3793,21 +4007,21 @@ async function main() {
3793
4007
  usage();
3794
4008
  process.exit(2);
3795
4009
  }
3796
- const scanPath = path8.resolve(target);
3797
- const stat = await fs7.stat(scanPath).catch(() => null);
4010
+ const scanPath = path9.resolve(target);
4011
+ const stat = await fs8.stat(scanPath).catch(() => null);
3798
4012
  if (!stat || !stat.isDirectory()) {
3799
4013
  console.error(`neat watch: ${scanPath} is not a directory`);
3800
4014
  process.exit(2);
3801
4015
  }
3802
- const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
3803
- const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
3804
- const errorsPath = path8.resolve(
3805
- process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
4016
+ const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
4017
+ const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
4018
+ const errorsPath = path9.resolve(
4019
+ process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
3806
4020
  );
3807
- const staleEventsPath = path8.resolve(
3808
- process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
4021
+ const staleEventsPath = path9.resolve(
4022
+ process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
3809
4023
  );
3810
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
4024
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
3811
4025
  const handle = await startWatch(getGraph(project), {
3812
4026
  scanPath,
3813
4027
  outPath,
@@ -3953,11 +4167,11 @@ async function main() {
3953
4167
  process.exit(1);
3954
4168
  }
3955
4169
  async function tryOrchestrator(cmd, parsed) {
3956
- const scanPath = path8.resolve(cmd);
3957
- const stat = await fs7.stat(scanPath).catch(() => null);
4170
+ const scanPath = path9.resolve(cmd);
4171
+ const stat = await fs8.stat(scanPath).catch(() => null);
3958
4172
  if (!stat || !stat.isDirectory()) return null;
3959
4173
  const projectExplicit = parsed.project !== null;
3960
- const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4174
+ const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
3961
4175
  const result = await runOrchestrator({
3962
4176
  scanPath,
3963
4177
  project: projectName,