@neat.is/core 0.4.11 → 0.4.12

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
@@ -17,6 +17,7 @@ import {
17
17
  attachGraphToEventBus,
18
18
  buildApi,
19
19
  computeDivergences,
20
+ detectPackageManager,
20
21
  discoverServices,
21
22
  emitNeatEvent,
22
23
  ensureCompatLoaded,
@@ -37,11 +38,12 @@ import {
37
38
  removeProject,
38
39
  resetGraph,
39
40
  retireEdgesByFile,
41
+ runPackageManagerInstall,
40
42
  saveGraphToDisk,
41
43
  setStatus,
42
44
  startPersistLoop,
43
45
  startStalenessLoop
44
- } from "./chunk-OJHI33LD.js";
46
+ } from "./chunk-WDG4QEFO.js";
45
47
  import {
46
48
  startOtelGrpcReceiver
47
49
  } from "./chunk-3QCRUEQD.js";
@@ -54,8 +56,8 @@ import {
54
56
  } from "./chunk-HVF4S7J3.js";
55
57
 
56
58
  // src/cli.ts
57
- import path9 from "path";
58
- import { promises as fs8, readFileSync } from "fs";
59
+ import path8 from "path";
60
+ import { promises as fs7, readFileSync } from "fs";
59
61
  import { fileURLToPath } from "url";
60
62
 
61
63
  // src/gitignore.ts
@@ -696,6 +698,7 @@ ${contents}EOF
696
698
  // src/installers/javascript.ts
697
699
  import { promises as fs4 } from "fs";
698
700
  import path4 from "path";
701
+ import semver from "semver";
699
702
 
700
703
  // src/installers/templates.ts
701
704
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
@@ -1255,6 +1258,13 @@ async function detectRuntimeKind(pkgRoot, pkg) {
1255
1258
  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
1259
  return "browser-bundle";
1257
1260
  }
1261
+ if (await exists(path4.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
1262
+ if (await exists(path4.join(pkgRoot, "bun.lockb"))) return "bun";
1263
+ if (await exists(path4.join(pkgRoot, "deno.json")) || await exists(path4.join(pkgRoot, "deno.lock"))) {
1264
+ return "deno";
1265
+ }
1266
+ const engines = pkg.engines ?? {};
1267
+ if ("electron" in engines) return "electron";
1258
1268
  return "node";
1259
1269
  }
1260
1270
  async function readPackageJson(serviceDir) {
@@ -1294,6 +1304,12 @@ async function detect(serviceDir) {
1294
1304
  const pkg = await readPackageJson(serviceDir);
1295
1305
  return pkg !== null && typeof pkg.name === "string";
1296
1306
  }
1307
+ function needsVersionUpgrade(installed, expected) {
1308
+ return !semver.satisfies(
1309
+ semver.minVersion(installed)?.version ?? installed,
1310
+ expected
1311
+ ) && !semver.intersects(installed, expected);
1312
+ }
1297
1313
  var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
1298
1314
  async function findNextConfig(serviceDir) {
1299
1315
  for (const name of NEXT_CONFIG_CANDIDATES) {
@@ -1520,23 +1536,23 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1520
1536
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1521
1537
  const dependencyEdits = [];
1522
1538
  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
- });
1539
+ if (sdk.name in existingDeps) {
1540
+ if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
1541
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
1542
+ }
1543
+ continue;
1544
+ }
1545
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
1530
1546
  }
1531
1547
  const nonBundled = detectNonBundledInstrumentations(pkg);
1532
1548
  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
- });
1549
+ if (inst.pkg in existingDeps) {
1550
+ if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
1551
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
1552
+ }
1553
+ continue;
1554
+ }
1555
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
1540
1556
  }
1541
1557
  const svcName = serviceNodeName(pkg, serviceDir);
1542
1558
  const projectName = projectToken(pkg, serviceDir, project);
@@ -1982,23 +1998,23 @@ async function plan(serviceDir, opts) {
1982
1998
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1983
1999
  const dependencyEdits = [];
1984
2000
  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
- });
2001
+ if (sdk.name in existingDeps) {
2002
+ if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
2003
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
2004
+ }
2005
+ continue;
2006
+ }
2007
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
1992
2008
  }
1993
2009
  const nonBundled = detectNonBundledInstrumentations(pkg);
1994
2010
  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
- });
2011
+ if (inst.pkg in existingDeps) {
2012
+ if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
2013
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
2014
+ }
2015
+ continue;
2016
+ }
2017
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
2002
2018
  }
2003
2019
  const entrypointEdits = [];
2004
2020
  try {
@@ -2101,6 +2117,28 @@ async function apply(installPlan) {
2101
2117
  writtenFiles: []
2102
2118
  };
2103
2119
  }
2120
+ if (installPlan.runtimeKind === "bun") {
2121
+ return { serviceDir, outcome: "bun", reason: "Bun runtime; use BYO-OTel", writtenFiles: [] };
2122
+ }
2123
+ if (installPlan.runtimeKind === "deno") {
2124
+ return { serviceDir, outcome: "deno", reason: "Deno runtime; use BYO-OTel", writtenFiles: [] };
2125
+ }
2126
+ if (installPlan.runtimeKind === "cloudflare-workers") {
2127
+ return {
2128
+ serviceDir,
2129
+ outcome: "cloudflare-workers",
2130
+ reason: "Cloudflare Workers runtime; use BYO-OTel",
2131
+ writtenFiles: []
2132
+ };
2133
+ }
2134
+ if (installPlan.runtimeKind === "electron") {
2135
+ return {
2136
+ serviceDir,
2137
+ outcome: "electron",
2138
+ reason: "Electron; use BYO-OTel",
2139
+ writtenFiles: []
2140
+ };
2141
+ }
2104
2142
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
2105
2143
  return {
2106
2144
  serviceDir,
@@ -2151,6 +2189,8 @@ async function apply(installPlan) {
2151
2189
  if (!(dep.name in (pkg.dependencies ?? {}))) {
2152
2190
  pkg.dependencies[dep.name] = dep.version;
2153
2191
  }
2192
+ } else if (dep.kind === "upgrade") {
2193
+ pkg.dependencies[dep.name] = dep.version;
2154
2194
  } else {
2155
2195
  delete pkg.dependencies[dep.name];
2156
2196
  }
@@ -2551,99 +2591,19 @@ function renderPatch(sections) {
2551
2591
  }
2552
2592
 
2553
2593
  // src/orchestrator.ts
2554
- import { promises as fs7 } from "fs";
2594
+ import { promises as fs6 } from "fs";
2555
2595
  import http from "http";
2556
2596
  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
2597
  import path6 from "path";
2564
2598
  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
2599
+ import readline from "readline";
2640
2600
  async function extractAndPersist(opts) {
2641
2601
  const services = await discoverServices(opts.scanPath);
2642
2602
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
2643
2603
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2644
2604
  resetGraph(graphKey);
2645
2605
  const graph = getGraph(graphKey);
2646
- const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
2606
+ const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2647
2607
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
2648
2608
  errorsPath: projectPaths.errorsPath
2649
2609
  });
@@ -2669,6 +2629,10 @@ async function applyInstallersOver(services, project, options = {}) {
2669
2629
  let libOnly = 0;
2670
2630
  let browserBundle = 0;
2671
2631
  let reactNative = 0;
2632
+ let bun = 0;
2633
+ let deno = 0;
2634
+ let cloudflareWorkers = 0;
2635
+ let electron = 0;
2672
2636
  const installPlans = /* @__PURE__ */ new Map();
2673
2637
  for (const svc of services) {
2674
2638
  const installer = await pickInstaller(svc.dir);
@@ -2693,7 +2657,59 @@ async function applyInstallersOver(services, project, options = {}) {
2693
2657
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
2694
2658
  } else if (outcome.outcome === "react-native") {
2695
2659
  reactNative++;
2696
- console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2660
+ const svcName = path6.basename(svc.dir);
2661
+ console.log(
2662
+ `neat: ${svc.dir} detected as React Native / Expo
2663
+ The installer doesn't cover this runtime deterministically.
2664
+ Configure your OTel binding to send spans to:
2665
+ http://localhost:4318/projects/${project}/v1/traces
2666
+ Set OTEL_SERVICE_NAME=${svcName}
2667
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2668
+ );
2669
+ } else if (outcome.outcome === "bun") {
2670
+ bun++;
2671
+ const svcName = path6.basename(svc.dir);
2672
+ console.log(
2673
+ `neat: ${svc.dir} detected as Bun
2674
+ The installer doesn't cover this runtime deterministically.
2675
+ Configure your OTel binding to send spans to:
2676
+ http://localhost:4318/projects/${project}/v1/traces
2677
+ Set OTEL_SERVICE_NAME=${svcName}
2678
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2679
+ );
2680
+ } else if (outcome.outcome === "deno") {
2681
+ deno++;
2682
+ const svcName = path6.basename(svc.dir);
2683
+ console.log(
2684
+ `neat: ${svc.dir} detected as Deno
2685
+ The installer doesn't cover this runtime deterministically.
2686
+ Configure your OTel binding to send spans to:
2687
+ http://localhost:4318/projects/${project}/v1/traces
2688
+ Set OTEL_SERVICE_NAME=${svcName}
2689
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2690
+ );
2691
+ } else if (outcome.outcome === "cloudflare-workers") {
2692
+ cloudflareWorkers++;
2693
+ const svcName = path6.basename(svc.dir);
2694
+ console.log(
2695
+ `neat: ${svc.dir} detected as Cloudflare Workers
2696
+ The installer doesn't cover this runtime deterministically.
2697
+ Configure your OTel binding to send spans to:
2698
+ http://localhost:4318/projects/${project}/v1/traces
2699
+ Set OTEL_SERVICE_NAME=${svcName}
2700
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2701
+ );
2702
+ } else if (outcome.outcome === "electron") {
2703
+ electron++;
2704
+ const svcName = path6.basename(svc.dir);
2705
+ console.log(
2706
+ `neat: ${svc.dir} detected as Electron
2707
+ The installer doesn't cover this runtime deterministically.
2708
+ Configure your OTel binding to send spans to:
2709
+ http://localhost:4318/projects/${project}/v1/traces
2710
+ Set OTEL_SERVICE_NAME=${svcName}
2711
+ See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
2712
+ );
2697
2713
  }
2698
2714
  }
2699
2715
  const packageManagerInstalls = [];
@@ -2718,6 +2734,10 @@ async function applyInstallersOver(services, project, options = {}) {
2718
2734
  libOnly,
2719
2735
  browserBundle,
2720
2736
  reactNative,
2737
+ bun,
2738
+ deno,
2739
+ cloudflareWorkers,
2740
+ electron,
2721
2741
  packageManagerInstalls
2722
2742
  };
2723
2743
  }
@@ -2855,10 +2875,10 @@ function formatPortCollisionMessage(port) {
2855
2875
  ];
2856
2876
  }
2857
2877
  function spawnDaemonDetached() {
2858
- const here = path7.dirname(new URL(import.meta.url).pathname);
2878
+ const here = path6.dirname(new URL(import.meta.url).pathname);
2859
2879
  const candidates = [
2860
- path7.join(here, "neatd.cjs"),
2861
- path7.join(here, "neatd.js")
2880
+ path6.join(here, "neatd.cjs"),
2881
+ path6.join(here, "neatd.js")
2862
2882
  ];
2863
2883
  let entry2 = null;
2864
2884
  const fsSync = __require("fs");
@@ -2878,7 +2898,7 @@ function spawnDaemonDetached() {
2878
2898
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2879
2899
  env.HOST = "127.0.0.1";
2880
2900
  }
2881
- const child = spawn3(process.execPath, [entry2, "start"], {
2901
+ const child = spawn2(process.execPath, [entry2, "start"], {
2882
2902
  detached: true,
2883
2903
  // stderr inherits the orchestrator's fd so the daemon's
2884
2904
  // `BindAuthorityError` message lands in front of the operator instead
@@ -2895,7 +2915,7 @@ function openBrowser(url) {
2895
2915
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2896
2916
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
2897
2917
  try {
2898
- const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
2918
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2899
2919
  child.on("error", () => {
2900
2920
  });
2901
2921
  child.unref();
@@ -2916,7 +2936,7 @@ async function runOrchestrator(opts) {
2916
2936
  browser: "skipped"
2917
2937
  }
2918
2938
  };
2919
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
2939
+ const stat = await fs6.stat(opts.scanPath).catch(() => null);
2920
2940
  if (!stat || !stat.isDirectory()) {
2921
2941
  console.error(`neat: ${opts.scanPath} is not a directory`);
2922
2942
  result.exitCode = 2;
@@ -3058,7 +3078,7 @@ function printSummary(result, graph, dashboardUrl) {
3058
3078
  }
3059
3079
 
3060
3080
  // src/cli-verbs.ts
3061
- import path8 from "path";
3081
+ import path7 from "path";
3062
3082
 
3063
3083
  // src/cli-client.ts
3064
3084
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -3086,10 +3106,10 @@ function createHttpClient(baseUrl, bearerToken) {
3086
3106
  const root = baseUrl.replace(/\/$/, "");
3087
3107
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
3088
3108
  return {
3089
- async get(path10) {
3109
+ async get(path9) {
3090
3110
  let res;
3091
3111
  try {
3092
- res = await fetch(`${root}${path10}`, {
3112
+ res = await fetch(`${root}${path9}`, {
3093
3113
  headers: { ...authHeader }
3094
3114
  });
3095
3115
  } catch (err) {
@@ -3101,16 +3121,16 @@ function createHttpClient(baseUrl, bearerToken) {
3101
3121
  const body = await res.text().catch(() => "");
3102
3122
  throw new HttpError(
3103
3123
  res.status,
3104
- `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
3124
+ `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
3105
3125
  body
3106
3126
  );
3107
3127
  }
3108
3128
  return await res.json();
3109
3129
  },
3110
- async post(path10, body) {
3130
+ async post(path9, body) {
3111
3131
  let res;
3112
3132
  try {
3113
- res = await fetch(`${root}${path10}`, {
3133
+ res = await fetch(`${root}${path9}`, {
3114
3134
  method: "POST",
3115
3135
  headers: { "content-type": "application/json", ...authHeader },
3116
3136
  body: JSON.stringify(body)
@@ -3124,7 +3144,7 @@ function createHttpClient(baseUrl, bearerToken) {
3124
3144
  const text = await res.text().catch(() => "");
3125
3145
  throw new HttpError(
3126
3146
  res.status,
3127
- `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
3147
+ `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
3128
3148
  text
3129
3149
  );
3130
3150
  }
@@ -3138,12 +3158,12 @@ function projectPath(project, suffix) {
3138
3158
  }
3139
3159
  async function runRootCause(client, input) {
3140
3160
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
3141
- const path10 = projectPath(
3161
+ const path9 = projectPath(
3142
3162
  input.project,
3143
3163
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
3144
3164
  );
3145
3165
  try {
3146
- const result = await client.get(path10);
3166
+ const result = await client.get(path9);
3147
3167
  const arrowPath = result.traversalPath.join(" \u2190 ");
3148
3168
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
3149
3169
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -3169,12 +3189,12 @@ async function runRootCause(client, input) {
3169
3189
  }
3170
3190
  async function runBlastRadius(client, input) {
3171
3191
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
3172
- const path10 = projectPath(
3192
+ const path9 = projectPath(
3173
3193
  input.project,
3174
3194
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
3175
3195
  );
3176
3196
  try {
3177
- const result = await client.get(path10);
3197
+ const result = await client.get(path9);
3178
3198
  if (result.totalAffected === 0) {
3179
3199
  return {
3180
3200
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -3208,12 +3228,12 @@ function formatBlastEntry(n) {
3208
3228
  }
3209
3229
  async function runDependencies(client, input) {
3210
3230
  const depth = input.depth ?? 3;
3211
- const path10 = projectPath(
3231
+ const path9 = projectPath(
3212
3232
  input.project,
3213
3233
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
3214
3234
  );
3215
3235
  try {
3216
- const result = await client.get(path10);
3236
+ const result = await client.get(path9);
3217
3237
  if (result.total === 0) {
3218
3238
  return {
3219
3239
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -3294,9 +3314,9 @@ function formatDuration(ms) {
3294
3314
  return `${Math.round(h / 24)}d`;
3295
3315
  }
3296
3316
  async function runIncidents(client, input) {
3297
- const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
3317
+ const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
3298
3318
  try {
3299
- const body = await client.get(path10);
3319
+ const body = await client.get(path9);
3300
3320
  const events = body.events;
3301
3321
  if (events.length === 0) {
3302
3322
  return {
@@ -3586,7 +3606,7 @@ async function resolveProjectEntry(opts) {
3586
3606
  const cwd = opts.cwd ?? process.cwd();
3587
3607
  const resolvedCwd = await normalizeProjectPath(cwd);
3588
3608
  for (const entry2 of entries) {
3589
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
3609
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
3590
3610
  return entry2;
3591
3611
  }
3592
3612
  }
@@ -3946,10 +3966,10 @@ function assignFlag(out, field, value) {
3946
3966
  out[field] = value;
3947
3967
  }
3948
3968
  function readPackageVersion() {
3949
- const here = typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath(import.meta.url));
3969
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3950
3970
  const candidates = [
3951
- path9.resolve(here, "../package.json"),
3952
- path9.resolve(here, "../../package.json")
3971
+ path8.resolve(here, "../package.json"),
3972
+ path8.resolve(here, "../../package.json")
3953
3973
  ];
3954
3974
  for (const candidate of candidates) {
3955
3975
  try {
@@ -4017,7 +4037,7 @@ async function buildPatchSections(services, project) {
4017
4037
  }
4018
4038
  async function runInit(opts) {
4019
4039
  const written = [];
4020
- const stat = await fs8.stat(opts.scanPath).catch(() => null);
4040
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
4021
4041
  if (!stat || !stat.isDirectory()) {
4022
4042
  console.error(`neat init: ${opts.scanPath} is not a directory`);
4023
4043
  return { exitCode: 2, writtenFiles: written };
@@ -4026,13 +4046,13 @@ async function runInit(opts) {
4026
4046
  printDiscoveryReport(opts, services);
4027
4047
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
4028
4048
  const patch = renderPatch(sections);
4029
- const patchPath = path9.join(opts.scanPath, "neat.patch");
4049
+ const patchPath = path8.join(opts.scanPath, "neat.patch");
4030
4050
  if (opts.dryRun) {
4031
- await fs8.writeFile(patchPath, patch, "utf8");
4051
+ await fs7.writeFile(patchPath, patch, "utf8");
4032
4052
  written.push(patchPath);
4033
4053
  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);
4054
+ const gitignorePath = path8.join(opts.scanPath, ".gitignore");
4055
+ const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
4036
4056
  const verb = gitignoreExists ? "append" : "create";
4037
4057
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
4038
4058
  console.log("rerun without --dry-run to register and snapshot.");
@@ -4043,9 +4063,9 @@ async function runInit(opts) {
4043
4063
  const graph = getGraph(graphKey);
4044
4064
  const projectPaths = pathsForProject(
4045
4065
  graphKey,
4046
- path9.join(opts.scanPath, "neat-out")
4066
+ path8.join(opts.scanPath, "neat-out")
4047
4067
  );
4048
- const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
4068
+ const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
4049
4069
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
4050
4070
  await saveGraphToDisk(graph, opts.outPath);
4051
4071
  written.push(opts.outPath);
@@ -4124,7 +4144,7 @@ async function runInit(opts) {
4124
4144
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
4125
4145
  }
4126
4146
  } else {
4127
- await fs8.writeFile(patchPath, patch, "utf8");
4147
+ await fs7.writeFile(patchPath, patch, "utf8");
4128
4148
  written.push(patchPath);
4129
4149
  }
4130
4150
  }
@@ -4164,9 +4184,9 @@ var CLAUDE_SKILL_CONFIG = {
4164
4184
  };
4165
4185
  function claudeConfigPath() {
4166
4186
  const override = process.env.NEAT_CLAUDE_CONFIG;
4167
- if (override && override.length > 0) return path9.resolve(override);
4187
+ if (override && override.length > 0) return path8.resolve(override);
4168
4188
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4169
- return path9.join(home, ".claude.json");
4189
+ return path8.join(home, ".claude.json");
4170
4190
  }
4171
4191
  async function runSkill(opts) {
4172
4192
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -4178,7 +4198,7 @@ async function runSkill(opts) {
4178
4198
  const target = claudeConfigPath();
4179
4199
  let existing = {};
4180
4200
  try {
4181
- existing = JSON.parse(await fs8.readFile(target, "utf8"));
4201
+ existing = JSON.parse(await fs7.readFile(target, "utf8"));
4182
4202
  } catch (err) {
4183
4203
  if (err.code !== "ENOENT") {
4184
4204
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -4190,8 +4210,8 @@ async function runSkill(opts) {
4190
4210
  ...existing,
4191
4211
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
4192
4212
  };
4193
- await fs8.mkdir(path9.dirname(target), { recursive: true });
4194
- await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
4213
+ await fs7.mkdir(path8.dirname(target), { recursive: true });
4214
+ await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
4195
4215
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
4196
4216
  console.log("restart Claude Code to pick up the new MCP server.");
4197
4217
  return { exitCode: 0 };
@@ -4229,12 +4249,12 @@ async function main() {
4229
4249
  console.error("neat init: --apply and --dry-run are mutually exclusive");
4230
4250
  process.exit(2);
4231
4251
  }
4232
- const scanPath = path9.resolve(target);
4252
+ const scanPath = path8.resolve(target);
4233
4253
  const projectExplicit = parsed.project !== null;
4234
- const projectName = projectExplicit ? project : path9.basename(scanPath);
4254
+ const projectName = projectExplicit ? project : path8.basename(scanPath);
4235
4255
  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);
4256
+ const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
4257
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
4238
4258
  const result = await runInit({
4239
4259
  scanPath,
4240
4260
  outPath,
@@ -4255,21 +4275,21 @@ async function main() {
4255
4275
  usage();
4256
4276
  process.exit(2);
4257
4277
  }
4258
- const scanPath = path9.resolve(target);
4259
- const stat = await fs8.stat(scanPath).catch(() => null);
4278
+ const scanPath = path8.resolve(target);
4279
+ const stat = await fs7.stat(scanPath).catch(() => null);
4260
4280
  if (!stat || !stat.isDirectory()) {
4261
4281
  console.error(`neat watch: ${scanPath} is not a directory`);
4262
4282
  process.exit(2);
4263
4283
  }
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))
4284
+ const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
4285
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
4286
+ const errorsPath = path8.resolve(
4287
+ process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
4268
4288
  );
4269
- const staleEventsPath = path9.resolve(
4270
- process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
4289
+ const staleEventsPath = path8.resolve(
4290
+ process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
4271
4291
  );
4272
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
4292
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
4273
4293
  const handle = await startWatch(getGraph(project), {
4274
4294
  scanPath,
4275
4295
  outPath,
@@ -4415,11 +4435,11 @@ async function main() {
4415
4435
  process.exit(1);
4416
4436
  }
4417
4437
  async function tryOrchestrator(cmd, parsed) {
4418
- const scanPath = path9.resolve(cmd);
4419
- const stat = await fs8.stat(scanPath).catch(() => null);
4438
+ const scanPath = path8.resolve(cmd);
4439
+ const stat = await fs7.stat(scanPath).catch(() => null);
4420
4440
  if (!stat || !stat.isDirectory()) return null;
4421
4441
  const projectExplicit = parsed.project !== null;
4422
- const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
4442
+ const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4423
4443
  const result = await runOrchestrator({
4424
4444
  scanPath,
4425
4445
  project: projectName,