@neat.is/core 0.4.11 → 0.4.13

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
@@ -10,6 +10,7 @@ import {
10
10
  addCallEdges,
11
11
  addConfigNodes,
12
12
  addDatabasesAndCompat,
13
+ addImports,
13
14
  addInfra,
14
15
  addProject,
15
16
  addServiceAliases,
@@ -17,6 +18,7 @@ import {
17
18
  attachGraphToEventBus,
18
19
  buildApi,
19
20
  computeDivergences,
21
+ detectPackageManager,
20
22
  discoverServices,
21
23
  emitNeatEvent,
22
24
  ensureCompatLoaded,
@@ -37,11 +39,12 @@ import {
37
39
  removeProject,
38
40
  resetGraph,
39
41
  retireEdgesByFile,
42
+ runPackageManagerInstall,
40
43
  saveGraphToDisk,
41
44
  setStatus,
42
45
  startPersistLoop,
43
46
  startStalenessLoop
44
- } from "./chunk-OJHI33LD.js";
47
+ } from "./chunk-5W7H35JJ.js";
45
48
  import {
46
49
  startOtelGrpcReceiver
47
50
  } from "./chunk-3QCRUEQD.js";
@@ -54,8 +57,8 @@ import {
54
57
  } from "./chunk-HVF4S7J3.js";
55
58
 
56
59
  // src/cli.ts
57
- import path9 from "path";
58
- import { promises as fs8, readFileSync } from "fs";
60
+ import path8 from "path";
61
+ import { promises as fs7, readFileSync } from "fs";
59
62
  import { fileURLToPath } from "url";
60
63
 
61
64
  // src/gitignore.ts
@@ -195,6 +198,7 @@ import chokidar from "chokidar";
195
198
  var ALL_PHASES = [
196
199
  "services",
197
200
  "aliases",
201
+ "imports",
198
202
  "databases",
199
203
  "configs",
200
204
  "calls",
@@ -218,6 +222,7 @@ function classifyChange(relPath) {
218
222
  phases.add("aliases");
219
223
  }
220
224
  if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
225
+ phases.add("imports");
221
226
  phases.add("calls");
222
227
  }
223
228
  if (/\.ya?ml$/.test(base) && !/^docker-compose.*\.ya?ml$/.test(base)) {
@@ -239,6 +244,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
239
244
  if (phases.has("aliases")) {
240
245
  await addServiceAliases(graph, scanPath, services);
241
246
  }
247
+ if (phases.has("imports")) {
248
+ const r = await addImports(graph, services);
249
+ nodesAdded += r.nodesAdded;
250
+ edgesAdded += r.edgesAdded;
251
+ }
242
252
  if (phases.has("databases")) {
243
253
  const r = await addDatabasesAndCompat(graph, services, scanPath);
244
254
  nodesAdded += r.nodesAdded;
@@ -696,6 +706,7 @@ ${contents}EOF
696
706
  // src/installers/javascript.ts
697
707
  import { promises as fs4 } from "fs";
698
708
  import path4 from "path";
709
+ import semver from "semver";
699
710
 
700
711
  // src/installers/templates.ts
701
712
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
@@ -1255,6 +1266,13 @@ async function detectRuntimeKind(pkgRoot, pkg) {
1255
1266
  if (await exists(path4.join(pkgRoot, "vite.config.js")) || await exists(path4.join(pkgRoot, "vite.config.ts")) || await exists(path4.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
1256
1267
  return "browser-bundle";
1257
1268
  }
1269
+ if (await exists(path4.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
1270
+ if (await exists(path4.join(pkgRoot, "bun.lockb"))) return "bun";
1271
+ if (await exists(path4.join(pkgRoot, "deno.json")) || await exists(path4.join(pkgRoot, "deno.lock"))) {
1272
+ return "deno";
1273
+ }
1274
+ const engines = pkg.engines ?? {};
1275
+ if ("electron" in engines) return "electron";
1258
1276
  return "node";
1259
1277
  }
1260
1278
  async function readPackageJson(serviceDir) {
@@ -1294,6 +1312,12 @@ async function detect(serviceDir) {
1294
1312
  const pkg = await readPackageJson(serviceDir);
1295
1313
  return pkg !== null && typeof pkg.name === "string";
1296
1314
  }
1315
+ function needsVersionUpgrade(installed, expected) {
1316
+ return !semver.satisfies(
1317
+ semver.minVersion(installed)?.version ?? installed,
1318
+ expected
1319
+ ) && !semver.intersects(installed, expected);
1320
+ }
1297
1321
  var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
1298
1322
  async function findNextConfig(serviceDir) {
1299
1323
  for (const name of NEXT_CONFIG_CANDIDATES) {
@@ -1520,23 +1544,23 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1520
1544
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1521
1545
  const dependencyEdits = [];
1522
1546
  for (const sdk of SDK_PACKAGES) {
1523
- if (sdk.name in existingDeps) continue;
1524
- dependencyEdits.push({
1525
- file: manifestPath,
1526
- kind: "add",
1527
- name: sdk.name,
1528
- version: sdk.version
1529
- });
1547
+ if (sdk.name in existingDeps) {
1548
+ if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
1549
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
1550
+ }
1551
+ continue;
1552
+ }
1553
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
1530
1554
  }
1531
1555
  const nonBundled = detectNonBundledInstrumentations(pkg);
1532
1556
  for (const inst of nonBundled) {
1533
- if (inst.pkg in existingDeps) continue;
1534
- dependencyEdits.push({
1535
- file: manifestPath,
1536
- kind: "add",
1537
- name: inst.pkg,
1538
- version: inst.version
1539
- });
1557
+ if (inst.pkg in existingDeps) {
1558
+ if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
1559
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
1560
+ }
1561
+ continue;
1562
+ }
1563
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
1540
1564
  }
1541
1565
  const svcName = serviceNodeName(pkg, serviceDir);
1542
1566
  const projectName = projectToken(pkg, serviceDir, project);
@@ -1982,23 +2006,23 @@ async function plan(serviceDir, opts) {
1982
2006
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1983
2007
  const dependencyEdits = [];
1984
2008
  for (const sdk of SDK_PACKAGES) {
1985
- if (sdk.name in existingDeps) continue;
1986
- dependencyEdits.push({
1987
- file: manifestPath,
1988
- kind: "add",
1989
- name: sdk.name,
1990
- version: sdk.version
1991
- });
2009
+ if (sdk.name in existingDeps) {
2010
+ if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
2011
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
2012
+ }
2013
+ continue;
2014
+ }
2015
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
1992
2016
  }
1993
2017
  const nonBundled = detectNonBundledInstrumentations(pkg);
1994
2018
  for (const inst of nonBundled) {
1995
- if (inst.pkg in existingDeps) continue;
1996
- dependencyEdits.push({
1997
- file: manifestPath,
1998
- kind: "add",
1999
- name: inst.pkg,
2000
- version: inst.version
2001
- });
2019
+ if (inst.pkg in existingDeps) {
2020
+ if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
2021
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
2022
+ }
2023
+ continue;
2024
+ }
2025
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
2002
2026
  }
2003
2027
  const entrypointEdits = [];
2004
2028
  try {
@@ -2101,6 +2125,28 @@ async function apply(installPlan) {
2101
2125
  writtenFiles: []
2102
2126
  };
2103
2127
  }
2128
+ if (installPlan.runtimeKind === "bun") {
2129
+ return { serviceDir, outcome: "bun", reason: "Bun runtime; use BYO-OTel", writtenFiles: [] };
2130
+ }
2131
+ if (installPlan.runtimeKind === "deno") {
2132
+ return { serviceDir, outcome: "deno", reason: "Deno runtime; use BYO-OTel", writtenFiles: [] };
2133
+ }
2134
+ if (installPlan.runtimeKind === "cloudflare-workers") {
2135
+ return {
2136
+ serviceDir,
2137
+ outcome: "cloudflare-workers",
2138
+ reason: "Cloudflare Workers runtime; use BYO-OTel",
2139
+ writtenFiles: []
2140
+ };
2141
+ }
2142
+ if (installPlan.runtimeKind === "electron") {
2143
+ return {
2144
+ serviceDir,
2145
+ outcome: "electron",
2146
+ reason: "Electron; use BYO-OTel",
2147
+ writtenFiles: []
2148
+ };
2149
+ }
2104
2150
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
2105
2151
  return {
2106
2152
  serviceDir,
@@ -2151,6 +2197,8 @@ async function apply(installPlan) {
2151
2197
  if (!(dep.name in (pkg.dependencies ?? {}))) {
2152
2198
  pkg.dependencies[dep.name] = dep.version;
2153
2199
  }
2200
+ } else if (dep.kind === "upgrade") {
2201
+ pkg.dependencies[dep.name] = dep.version;
2154
2202
  } else {
2155
2203
  delete pkg.dependencies[dep.name];
2156
2204
  }
@@ -2551,99 +2599,19 @@ function renderPatch(sections) {
2551
2599
  }
2552
2600
 
2553
2601
  // src/orchestrator.ts
2554
- import { promises as fs7 } from "fs";
2602
+ import { promises as fs6 } from "fs";
2555
2603
  import http from "http";
2556
2604
  import net from "net";
2557
- import path7 from "path";
2558
- import { spawn as spawn3 } from "child_process";
2559
- import readline from "readline";
2560
-
2561
- // src/installers/package-manager.ts
2562
- import { promises as fs6 } from "fs";
2563
2605
  import path6 from "path";
2564
2606
  import { spawn as spawn2 } from "child_process";
2565
- var LOCKFILE_PRIORITY = [
2566
- { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
2567
- { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
2568
- { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
2569
- {
2570
- lockfile: "package-lock.json",
2571
- pm: "npm",
2572
- args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
2573
- }
2574
- ];
2575
- var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
2576
- async function exists3(p) {
2577
- try {
2578
- await fs6.access(p);
2579
- return true;
2580
- } catch {
2581
- return false;
2582
- }
2583
- }
2584
- async function detectPackageManager(serviceDir) {
2585
- let dir = path6.resolve(serviceDir);
2586
- const stops = /* @__PURE__ */ new Set();
2587
- for (let i = 0; i < 64; i++) {
2588
- if (stops.has(dir)) break;
2589
- stops.add(dir);
2590
- for (const candidate of LOCKFILE_PRIORITY) {
2591
- const lockPath = path6.join(dir, candidate.lockfile);
2592
- if (await exists3(lockPath)) {
2593
- return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
2594
- }
2595
- }
2596
- const parent = path6.dirname(dir);
2597
- if (parent === dir) break;
2598
- dir = parent;
2599
- }
2600
- return { pm: "npm", cwd: path6.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
2601
- }
2602
- async function runPackageManagerInstall(cmd) {
2603
- return new Promise((resolve) => {
2604
- const child = spawn2(cmd.pm, cmd.args, {
2605
- cwd: cmd.cwd,
2606
- // Inherit PATH + HOME so the user's installed managers resolve.
2607
- env: process.env,
2608
- // `false` keeps the parent in control of cleanup if the orchestrator
2609
- // exits before install finishes. Cross-platform-safe.
2610
- shell: false,
2611
- stdio: ["ignore", "ignore", "pipe"]
2612
- });
2613
- let stderr = "";
2614
- child.stderr?.on("data", (chunk) => {
2615
- stderr += chunk.toString("utf8");
2616
- });
2617
- child.on("error", (err) => {
2618
- resolve({
2619
- pm: cmd.pm,
2620
- cwd: cmd.cwd,
2621
- args: cmd.args,
2622
- exitCode: 127,
2623
- stderr: stderr + `
2624
- ${err.message}`
2625
- });
2626
- });
2627
- child.on("close", (code) => {
2628
- resolve({
2629
- pm: cmd.pm,
2630
- cwd: cmd.cwd,
2631
- args: cmd.args,
2632
- exitCode: code ?? 1,
2633
- stderr: stderr.trim()
2634
- });
2635
- });
2636
- });
2637
- }
2638
-
2639
- // src/orchestrator.ts
2607
+ import readline from "readline";
2640
2608
  async function extractAndPersist(opts) {
2641
2609
  const services = await discoverServices(opts.scanPath);
2642
2610
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
2643
2611
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2644
2612
  resetGraph(graphKey);
2645
2613
  const graph = getGraph(graphKey);
2646
- const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
2614
+ const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2647
2615
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
2648
2616
  errorsPath: projectPaths.errorsPath
2649
2617
  });
@@ -2669,6 +2637,10 @@ async function applyInstallersOver(services, project, options = {}) {
2669
2637
  let libOnly = 0;
2670
2638
  let browserBundle = 0;
2671
2639
  let reactNative = 0;
2640
+ let bun = 0;
2641
+ let deno = 0;
2642
+ let cloudflareWorkers = 0;
2643
+ let electron = 0;
2672
2644
  const installPlans = /* @__PURE__ */ new Map();
2673
2645
  for (const svc of services) {
2674
2646
  const installer = await pickInstaller(svc.dir);
@@ -2693,7 +2665,59 @@ async function applyInstallersOver(services, project, options = {}) {
2693
2665
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
2694
2666
  } else if (outcome.outcome === "react-native") {
2695
2667
  reactNative++;
2696
- console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2668
+ const svcName = path6.basename(svc.dir);
2669
+ console.log(
2670
+ `neat: ${svc.dir} detected as React Native / Expo
2671
+ The installer doesn't cover this runtime deterministically.
2672
+ Configure your OTel binding to send spans to:
2673
+ http://localhost:4318/projects/${project}/v1/traces
2674
+ Set OTEL_SERVICE_NAME=${svcName}
2675
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2676
+ );
2677
+ } else if (outcome.outcome === "bun") {
2678
+ bun++;
2679
+ const svcName = path6.basename(svc.dir);
2680
+ console.log(
2681
+ `neat: ${svc.dir} detected as Bun
2682
+ The installer doesn't cover this runtime deterministically.
2683
+ Configure your OTel binding to send spans to:
2684
+ http://localhost:4318/projects/${project}/v1/traces
2685
+ Set OTEL_SERVICE_NAME=${svcName}
2686
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2687
+ );
2688
+ } else if (outcome.outcome === "deno") {
2689
+ deno++;
2690
+ const svcName = path6.basename(svc.dir);
2691
+ console.log(
2692
+ `neat: ${svc.dir} detected as Deno
2693
+ The installer doesn't cover this runtime deterministically.
2694
+ Configure your OTel binding to send spans to:
2695
+ http://localhost:4318/projects/${project}/v1/traces
2696
+ Set OTEL_SERVICE_NAME=${svcName}
2697
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2698
+ );
2699
+ } else if (outcome.outcome === "cloudflare-workers") {
2700
+ cloudflareWorkers++;
2701
+ const svcName = path6.basename(svc.dir);
2702
+ console.log(
2703
+ `neat: ${svc.dir} detected as Cloudflare Workers
2704
+ The installer doesn't cover this runtime deterministically.
2705
+ Configure your OTel binding to send spans to:
2706
+ http://localhost:4318/projects/${project}/v1/traces
2707
+ Set OTEL_SERVICE_NAME=${svcName}
2708
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2709
+ );
2710
+ } else if (outcome.outcome === "electron") {
2711
+ electron++;
2712
+ const svcName = path6.basename(svc.dir);
2713
+ console.log(
2714
+ `neat: ${svc.dir} detected as Electron
2715
+ The installer doesn't cover this runtime deterministically.
2716
+ Configure your OTel binding to send spans to:
2717
+ http://localhost:4318/projects/${project}/v1/traces
2718
+ Set OTEL_SERVICE_NAME=${svcName}
2719
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2720
+ );
2697
2721
  }
2698
2722
  }
2699
2723
  const packageManagerInstalls = [];
@@ -2718,6 +2742,10 @@ async function applyInstallersOver(services, project, options = {}) {
2718
2742
  libOnly,
2719
2743
  browserBundle,
2720
2744
  reactNative,
2745
+ bun,
2746
+ deno,
2747
+ cloudflareWorkers,
2748
+ electron,
2721
2749
  packageManagerInstalls
2722
2750
  };
2723
2751
  }
@@ -2855,10 +2883,10 @@ function formatPortCollisionMessage(port) {
2855
2883
  ];
2856
2884
  }
2857
2885
  function spawnDaemonDetached() {
2858
- const here = path7.dirname(new URL(import.meta.url).pathname);
2886
+ const here = path6.dirname(new URL(import.meta.url).pathname);
2859
2887
  const candidates = [
2860
- path7.join(here, "neatd.cjs"),
2861
- path7.join(here, "neatd.js")
2888
+ path6.join(here, "neatd.cjs"),
2889
+ path6.join(here, "neatd.js")
2862
2890
  ];
2863
2891
  let entry2 = null;
2864
2892
  const fsSync = __require("fs");
@@ -2878,7 +2906,7 @@ function spawnDaemonDetached() {
2878
2906
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2879
2907
  env.HOST = "127.0.0.1";
2880
2908
  }
2881
- const child = spawn3(process.execPath, [entry2, "start"], {
2909
+ const child = spawn2(process.execPath, [entry2, "start"], {
2882
2910
  detached: true,
2883
2911
  // stderr inherits the orchestrator's fd so the daemon's
2884
2912
  // `BindAuthorityError` message lands in front of the operator instead
@@ -2895,7 +2923,7 @@ function openBrowser(url) {
2895
2923
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2896
2924
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
2897
2925
  try {
2898
- const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
2926
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2899
2927
  child.on("error", () => {
2900
2928
  });
2901
2929
  child.unref();
@@ -2916,7 +2944,7 @@ async function runOrchestrator(opts) {
2916
2944
  browser: "skipped"
2917
2945
  }
2918
2946
  };
2919
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
2947
+ const stat = await fs6.stat(opts.scanPath).catch(() => null);
2920
2948
  if (!stat || !stat.isDirectory()) {
2921
2949
  console.error(`neat: ${opts.scanPath} is not a directory`);
2922
2950
  result.exitCode = 2;
@@ -3058,7 +3086,7 @@ function printSummary(result, graph, dashboardUrl) {
3058
3086
  }
3059
3087
 
3060
3088
  // src/cli-verbs.ts
3061
- import path8 from "path";
3089
+ import path7 from "path";
3062
3090
 
3063
3091
  // src/cli-client.ts
3064
3092
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -3086,10 +3114,10 @@ function createHttpClient(baseUrl, bearerToken) {
3086
3114
  const root = baseUrl.replace(/\/$/, "");
3087
3115
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
3088
3116
  return {
3089
- async get(path10) {
3117
+ async get(path9) {
3090
3118
  let res;
3091
3119
  try {
3092
- res = await fetch(`${root}${path10}`, {
3120
+ res = await fetch(`${root}${path9}`, {
3093
3121
  headers: { ...authHeader }
3094
3122
  });
3095
3123
  } catch (err) {
@@ -3101,16 +3129,16 @@ function createHttpClient(baseUrl, bearerToken) {
3101
3129
  const body = await res.text().catch(() => "");
3102
3130
  throw new HttpError(
3103
3131
  res.status,
3104
- `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
3132
+ `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
3105
3133
  body
3106
3134
  );
3107
3135
  }
3108
3136
  return await res.json();
3109
3137
  },
3110
- async post(path10, body) {
3138
+ async post(path9, body) {
3111
3139
  let res;
3112
3140
  try {
3113
- res = await fetch(`${root}${path10}`, {
3141
+ res = await fetch(`${root}${path9}`, {
3114
3142
  method: "POST",
3115
3143
  headers: { "content-type": "application/json", ...authHeader },
3116
3144
  body: JSON.stringify(body)
@@ -3124,7 +3152,7 @@ function createHttpClient(baseUrl, bearerToken) {
3124
3152
  const text = await res.text().catch(() => "");
3125
3153
  throw new HttpError(
3126
3154
  res.status,
3127
- `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
3155
+ `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
3128
3156
  text
3129
3157
  );
3130
3158
  }
@@ -3138,12 +3166,12 @@ function projectPath(project, suffix) {
3138
3166
  }
3139
3167
  async function runRootCause(client, input) {
3140
3168
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
3141
- const path10 = projectPath(
3169
+ const path9 = projectPath(
3142
3170
  input.project,
3143
3171
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
3144
3172
  );
3145
3173
  try {
3146
- const result = await client.get(path10);
3174
+ const result = await client.get(path9);
3147
3175
  const arrowPath = result.traversalPath.join(" \u2190 ");
3148
3176
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
3149
3177
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -3169,12 +3197,12 @@ async function runRootCause(client, input) {
3169
3197
  }
3170
3198
  async function runBlastRadius(client, input) {
3171
3199
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
3172
- const path10 = projectPath(
3200
+ const path9 = projectPath(
3173
3201
  input.project,
3174
3202
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
3175
3203
  );
3176
3204
  try {
3177
- const result = await client.get(path10);
3205
+ const result = await client.get(path9);
3178
3206
  if (result.totalAffected === 0) {
3179
3207
  return {
3180
3208
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -3208,12 +3236,12 @@ function formatBlastEntry(n) {
3208
3236
  }
3209
3237
  async function runDependencies(client, input) {
3210
3238
  const depth = input.depth ?? 3;
3211
- const path10 = projectPath(
3239
+ const path9 = projectPath(
3212
3240
  input.project,
3213
3241
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
3214
3242
  );
3215
3243
  try {
3216
- const result = await client.get(path10);
3244
+ const result = await client.get(path9);
3217
3245
  if (result.total === 0) {
3218
3246
  return {
3219
3247
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -3294,9 +3322,9 @@ function formatDuration(ms) {
3294
3322
  return `${Math.round(h / 24)}d`;
3295
3323
  }
3296
3324
  async function runIncidents(client, input) {
3297
- const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
3325
+ const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
3298
3326
  try {
3299
- const body = await client.get(path10);
3327
+ const body = await client.get(path9);
3300
3328
  const events = body.events;
3301
3329
  if (events.length === 0) {
3302
3330
  return {
@@ -3586,7 +3614,7 @@ async function resolveProjectEntry(opts) {
3586
3614
  const cwd = opts.cwd ?? process.cwd();
3587
3615
  const resolvedCwd = await normalizeProjectPath(cwd);
3588
3616
  for (const entry2 of entries) {
3589
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
3617
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
3590
3618
  return entry2;
3591
3619
  }
3592
3620
  }
@@ -3946,10 +3974,10 @@ function assignFlag(out, field, value) {
3946
3974
  out[field] = value;
3947
3975
  }
3948
3976
  function readPackageVersion() {
3949
- const here = typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath(import.meta.url));
3977
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3950
3978
  const candidates = [
3951
- path9.resolve(here, "../package.json"),
3952
- path9.resolve(here, "../../package.json")
3979
+ path8.resolve(here, "../package.json"),
3980
+ path8.resolve(here, "../../package.json")
3953
3981
  ];
3954
3982
  for (const candidate of candidates) {
3955
3983
  try {
@@ -4017,7 +4045,7 @@ async function buildPatchSections(services, project) {
4017
4045
  }
4018
4046
  async function runInit(opts) {
4019
4047
  const written = [];
4020
- const stat = await fs8.stat(opts.scanPath).catch(() => null);
4048
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
4021
4049
  if (!stat || !stat.isDirectory()) {
4022
4050
  console.error(`neat init: ${opts.scanPath} is not a directory`);
4023
4051
  return { exitCode: 2, writtenFiles: written };
@@ -4026,13 +4054,13 @@ async function runInit(opts) {
4026
4054
  printDiscoveryReport(opts, services);
4027
4055
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
4028
4056
  const patch = renderPatch(sections);
4029
- const patchPath = path9.join(opts.scanPath, "neat.patch");
4057
+ const patchPath = path8.join(opts.scanPath, "neat.patch");
4030
4058
  if (opts.dryRun) {
4031
- await fs8.writeFile(patchPath, patch, "utf8");
4059
+ await fs7.writeFile(patchPath, patch, "utf8");
4032
4060
  written.push(patchPath);
4033
4061
  console.log(`dry-run: patch written to ${patchPath}`);
4034
- const gitignorePath = path9.join(opts.scanPath, ".gitignore");
4035
- const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
4062
+ const gitignorePath = path8.join(opts.scanPath, ".gitignore");
4063
+ const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
4036
4064
  const verb = gitignoreExists ? "append" : "create";
4037
4065
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
4038
4066
  console.log("rerun without --dry-run to register and snapshot.");
@@ -4043,9 +4071,9 @@ async function runInit(opts) {
4043
4071
  const graph = getGraph(graphKey);
4044
4072
  const projectPaths = pathsForProject(
4045
4073
  graphKey,
4046
- path9.join(opts.scanPath, "neat-out")
4074
+ path8.join(opts.scanPath, "neat-out")
4047
4075
  );
4048
- const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
4076
+ const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
4049
4077
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
4050
4078
  await saveGraphToDisk(graph, opts.outPath);
4051
4079
  written.push(opts.outPath);
@@ -4124,7 +4152,7 @@ async function runInit(opts) {
4124
4152
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
4125
4153
  }
4126
4154
  } else {
4127
- await fs8.writeFile(patchPath, patch, "utf8");
4155
+ await fs7.writeFile(patchPath, patch, "utf8");
4128
4156
  written.push(patchPath);
4129
4157
  }
4130
4158
  }
@@ -4164,9 +4192,9 @@ var CLAUDE_SKILL_CONFIG = {
4164
4192
  };
4165
4193
  function claudeConfigPath() {
4166
4194
  const override = process.env.NEAT_CLAUDE_CONFIG;
4167
- if (override && override.length > 0) return path9.resolve(override);
4195
+ if (override && override.length > 0) return path8.resolve(override);
4168
4196
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4169
- return path9.join(home, ".claude.json");
4197
+ return path8.join(home, ".claude.json");
4170
4198
  }
4171
4199
  async function runSkill(opts) {
4172
4200
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -4178,7 +4206,7 @@ async function runSkill(opts) {
4178
4206
  const target = claudeConfigPath();
4179
4207
  let existing = {};
4180
4208
  try {
4181
- existing = JSON.parse(await fs8.readFile(target, "utf8"));
4209
+ existing = JSON.parse(await fs7.readFile(target, "utf8"));
4182
4210
  } catch (err) {
4183
4211
  if (err.code !== "ENOENT") {
4184
4212
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -4190,8 +4218,8 @@ async function runSkill(opts) {
4190
4218
  ...existing,
4191
4219
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
4192
4220
  };
4193
- await fs8.mkdir(path9.dirname(target), { recursive: true });
4194
- await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
4221
+ await fs7.mkdir(path8.dirname(target), { recursive: true });
4222
+ await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
4195
4223
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
4196
4224
  console.log("restart Claude Code to pick up the new MCP server.");
4197
4225
  return { exitCode: 0 };
@@ -4229,12 +4257,12 @@ async function main() {
4229
4257
  console.error("neat init: --apply and --dry-run are mutually exclusive");
4230
4258
  process.exit(2);
4231
4259
  }
4232
- const scanPath = path9.resolve(target);
4260
+ const scanPath = path8.resolve(target);
4233
4261
  const projectExplicit = parsed.project !== null;
4234
- const projectName = projectExplicit ? project : path9.basename(scanPath);
4262
+ const projectName = projectExplicit ? project : path8.basename(scanPath);
4235
4263
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
4236
- const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
4237
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
4264
+ const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
4265
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
4238
4266
  const result = await runInit({
4239
4267
  scanPath,
4240
4268
  outPath,
@@ -4255,21 +4283,21 @@ async function main() {
4255
4283
  usage();
4256
4284
  process.exit(2);
4257
4285
  }
4258
- const scanPath = path9.resolve(target);
4259
- const stat = await fs8.stat(scanPath).catch(() => null);
4286
+ const scanPath = path8.resolve(target);
4287
+ const stat = await fs7.stat(scanPath).catch(() => null);
4260
4288
  if (!stat || !stat.isDirectory()) {
4261
4289
  console.error(`neat watch: ${scanPath} is not a directory`);
4262
4290
  process.exit(2);
4263
4291
  }
4264
- const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
4265
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
4266
- const errorsPath = path9.resolve(
4267
- process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
4292
+ const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
4293
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
4294
+ const errorsPath = path8.resolve(
4295
+ process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
4268
4296
  );
4269
- const staleEventsPath = path9.resolve(
4270
- process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
4297
+ const staleEventsPath = path8.resolve(
4298
+ process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
4271
4299
  );
4272
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
4300
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
4273
4301
  const handle = await startWatch(getGraph(project), {
4274
4302
  scanPath,
4275
4303
  outPath,
@@ -4415,11 +4443,11 @@ async function main() {
4415
4443
  process.exit(1);
4416
4444
  }
4417
4445
  async function tryOrchestrator(cmd, parsed) {
4418
- const scanPath = path9.resolve(cmd);
4419
- const stat = await fs8.stat(scanPath).catch(() => null);
4446
+ const scanPath = path8.resolve(cmd);
4447
+ const stat = await fs7.stat(scanPath).catch(() => null);
4420
4448
  if (!stat || !stat.isDirectory()) return null;
4421
4449
  const projectExplicit = parsed.project !== null;
4422
- const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
4450
+ const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4423
4451
  const result = await runOrchestrator({
4424
4452
  scanPath,
4425
4453
  project: projectName,