@neat.is/core 0.4.5 → 0.4.6

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
@@ -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
@@ -877,13 +877,20 @@ var SDK_PACKAGES = [
877
877
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
878
878
  { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
879
879
  ];
880
+ function getMajor(versionRange) {
881
+ if (!versionRange) return 0;
882
+ const match = versionRange.match(/(\d+)/);
883
+ return match ? parseInt(match[1], 10) : 0;
884
+ }
880
885
  function detectNonBundledInstrumentations(pkg) {
881
886
  const deps = allDeps(pkg);
882
887
  const out = [];
883
888
  if ("@prisma/client" in deps) {
889
+ const prismaMajor = getMajor(deps["@prisma/client"]);
890
+ const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
884
891
  out.push({
885
892
  pkg: "@prisma/instrumentation",
886
- version: "^5.0.0",
893
+ version: prismaInstrVersion,
887
894
  registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
888
895
  });
889
896
  }
@@ -2209,19 +2216,99 @@ function renderPatch(sections) {
2209
2216
  }
2210
2217
 
2211
2218
  // src/orchestrator.ts
2212
- import { promises as fs6 } from "fs";
2219
+ import { promises as fs7 } from "fs";
2213
2220
  import http from "http";
2214
2221
  import net from "net";
2222
+ import path7 from "path";
2223
+ import { spawn as spawn3 } from "child_process";
2224
+ import readline from "readline";
2225
+
2226
+ // src/installers/package-manager.ts
2227
+ import { promises as fs6 } from "fs";
2215
2228
  import path6 from "path";
2216
2229
  import { spawn as spawn2 } from "child_process";
2217
- import readline from "readline";
2230
+ var LOCKFILE_PRIORITY = [
2231
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
2232
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
2233
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
2234
+ {
2235
+ lockfile: "package-lock.json",
2236
+ pm: "npm",
2237
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
2238
+ }
2239
+ ];
2240
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
2241
+ async function exists3(p) {
2242
+ try {
2243
+ await fs6.access(p);
2244
+ return true;
2245
+ } catch {
2246
+ return false;
2247
+ }
2248
+ }
2249
+ async function detectPackageManager(serviceDir) {
2250
+ let dir = path6.resolve(serviceDir);
2251
+ const stops = /* @__PURE__ */ new Set();
2252
+ for (let i = 0; i < 64; i++) {
2253
+ if (stops.has(dir)) break;
2254
+ stops.add(dir);
2255
+ for (const candidate of LOCKFILE_PRIORITY) {
2256
+ const lockPath = path6.join(dir, candidate.lockfile);
2257
+ if (await exists3(lockPath)) {
2258
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
2259
+ }
2260
+ }
2261
+ const parent = path6.dirname(dir);
2262
+ if (parent === dir) break;
2263
+ dir = parent;
2264
+ }
2265
+ return { pm: "npm", cwd: path6.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
2266
+ }
2267
+ async function runPackageManagerInstall(cmd) {
2268
+ return new Promise((resolve) => {
2269
+ const child = spawn2(cmd.pm, cmd.args, {
2270
+ cwd: cmd.cwd,
2271
+ // Inherit PATH + HOME so the user's installed managers resolve.
2272
+ env: process.env,
2273
+ // `false` keeps the parent in control of cleanup if the orchestrator
2274
+ // exits before install finishes. Cross-platform-safe.
2275
+ shell: false,
2276
+ stdio: ["ignore", "ignore", "pipe"]
2277
+ });
2278
+ let stderr = "";
2279
+ child.stderr?.on("data", (chunk) => {
2280
+ stderr += chunk.toString("utf8");
2281
+ });
2282
+ child.on("error", (err) => {
2283
+ resolve({
2284
+ pm: cmd.pm,
2285
+ cwd: cmd.cwd,
2286
+ args: cmd.args,
2287
+ exitCode: 127,
2288
+ stderr: stderr + `
2289
+ ${err.message}`
2290
+ });
2291
+ });
2292
+ child.on("close", (code) => {
2293
+ resolve({
2294
+ pm: cmd.pm,
2295
+ cwd: cmd.cwd,
2296
+ args: cmd.args,
2297
+ exitCode: code ?? 1,
2298
+ stderr: stderr.trim()
2299
+ });
2300
+ });
2301
+ });
2302
+ }
2303
+
2304
+ // src/orchestrator.ts
2218
2305
  async function extractAndPersist(opts) {
2219
2306
  const services = await discoverServices(opts.scanPath);
2220
2307
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
2221
2308
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2222
2309
  resetGraph(graphKey);
2223
2310
  const graph = getGraph(graphKey);
2224
- const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2311
+ const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
2225
2312
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
2226
2313
  errorsPath: projectPaths.errorsPath
2227
2314
  });
@@ -2239,12 +2326,15 @@ async function extractAndPersist(opts) {
2239
2326
  errorsPath: projectPaths.errorsPath
2240
2327
  };
2241
2328
  }
2242
- async function applyInstallersOver(services, project) {
2329
+ async function applyInstallersOver(services, project, options = {}) {
2330
+ const resolveManager = options.resolveManager ?? detectPackageManager;
2331
+ const runInstall = options.runInstall ?? runPackageManagerInstall;
2243
2332
  let instrumented = 0;
2244
2333
  let already = 0;
2245
2334
  let libOnly = 0;
2246
2335
  let browserBundle = 0;
2247
2336
  let reactNative = 0;
2337
+ const installPlans = /* @__PURE__ */ new Map();
2248
2338
  for (const svc of services) {
2249
2339
  const installer = await pickInstaller(svc.dir);
2250
2340
  if (!installer) continue;
@@ -2254,8 +2344,14 @@ async function applyInstallersOver(services, project) {
2254
2344
  continue;
2255
2345
  }
2256
2346
  const outcome = await installer.apply(plan3);
2257
- if (outcome.outcome === "instrumented") instrumented++;
2258
- else if (outcome.outcome === "already-instrumented") already++;
2347
+ if (outcome.outcome === "instrumented") {
2348
+ instrumented++;
2349
+ if (plan3.dependencyEdits.length > 0) {
2350
+ const cmd = await resolveManager(svc.dir);
2351
+ const key = `${cmd.pm}:${cmd.cwd}`;
2352
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
2353
+ }
2354
+ } else if (outcome.outcome === "already-instrumented") already++;
2259
2355
  else if (outcome.outcome === "lib-only") libOnly++;
2260
2356
  else if (outcome.outcome === "browser-bundle") {
2261
2357
  browserBundle++;
@@ -2265,7 +2361,30 @@ async function applyInstallersOver(services, project) {
2265
2361
  console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2266
2362
  }
2267
2363
  }
2268
- return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
2364
+ const packageManagerInstalls = [];
2365
+ for (const cmd of installPlans.values()) {
2366
+ console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
2367
+ const result = await runInstall(cmd);
2368
+ packageManagerInstalls.push(result);
2369
+ if (result.exitCode !== 0) {
2370
+ console.error(
2371
+ `neat: ${cmd.pm} install failed in ${cmd.cwd} (exit ${result.exitCode}); run it manually to surface the error.`
2372
+ );
2373
+ if (result.stderr.length > 0) {
2374
+ for (const line of result.stderr.split(/\r?\n/).slice(0, 20)) {
2375
+ console.error(` ${line}`);
2376
+ }
2377
+ }
2378
+ }
2379
+ }
2380
+ return {
2381
+ instrumented,
2382
+ alreadyInstrumented: already,
2383
+ libOnly,
2384
+ browserBundle,
2385
+ reactNative,
2386
+ packageManagerInstalls
2387
+ };
2269
2388
  }
2270
2389
  async function promptYesNo(question) {
2271
2390
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2401,10 +2520,10 @@ function formatPortCollisionMessage(port) {
2401
2520
  ];
2402
2521
  }
2403
2522
  function spawnDaemonDetached() {
2404
- const here = path6.dirname(new URL(import.meta.url).pathname);
2523
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2405
2524
  const candidates = [
2406
- path6.join(here, "neatd.cjs"),
2407
- path6.join(here, "neatd.js")
2525
+ path7.join(here, "neatd.cjs"),
2526
+ path7.join(here, "neatd.js")
2408
2527
  ];
2409
2528
  let entry2 = null;
2410
2529
  const fsSync = __require("fs");
@@ -2424,7 +2543,7 @@ function spawnDaemonDetached() {
2424
2543
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2425
2544
  env.HOST = "127.0.0.1";
2426
2545
  }
2427
- const child = spawn2(process.execPath, [entry2, "start"], {
2546
+ const child = spawn3(process.execPath, [entry2, "start"], {
2428
2547
  detached: true,
2429
2548
  // stderr inherits the orchestrator's fd so the daemon's
2430
2549
  // `BindAuthorityError` message lands in front of the operator instead
@@ -2441,7 +2560,7 @@ function openBrowser(url) {
2441
2560
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2442
2561
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
2443
2562
  try {
2444
- const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2563
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
2445
2564
  child.on("error", () => {
2446
2565
  });
2447
2566
  child.unref();
@@ -2462,7 +2581,7 @@ async function runOrchestrator(opts) {
2462
2581
  browser: "skipped"
2463
2582
  }
2464
2583
  };
2465
- const stat = await fs6.stat(opts.scanPath).catch(() => null);
2584
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
2466
2585
  if (!stat || !stat.isDirectory()) {
2467
2586
  console.error(`neat: ${opts.scanPath} is not a directory`);
2468
2587
  result.exitCode = 2;
@@ -2530,6 +2649,10 @@ async function runOrchestrator(opts) {
2530
2649
  console.log(
2531
2650
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
2532
2651
  );
2652
+ const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
2653
+ if (failedInstalls.length > 0) {
2654
+ result.exitCode = 1;
2655
+ }
2533
2656
  }
2534
2657
  const restPort = Number(process.env.PORT ?? 8080);
2535
2658
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -2600,7 +2723,7 @@ function printSummary(result, graph, dashboardUrl) {
2600
2723
  }
2601
2724
 
2602
2725
  // src/cli-verbs.ts
2603
- import path7 from "path";
2726
+ import path8 from "path";
2604
2727
 
2605
2728
  // src/cli-client.ts
2606
2729
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -2624,10 +2747,10 @@ function createHttpClient(baseUrl, bearerToken) {
2624
2747
  const root = baseUrl.replace(/\/$/, "");
2625
2748
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
2626
2749
  return {
2627
- async get(path9) {
2750
+ async get(path10) {
2628
2751
  let res;
2629
2752
  try {
2630
- res = await fetch(`${root}${path9}`, {
2753
+ res = await fetch(`${root}${path10}`, {
2631
2754
  headers: { ...authHeader }
2632
2755
  });
2633
2756
  } catch (err) {
@@ -2639,16 +2762,16 @@ function createHttpClient(baseUrl, bearerToken) {
2639
2762
  const body = await res.text().catch(() => "");
2640
2763
  throw new HttpError(
2641
2764
  res.status,
2642
- `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
2765
+ `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
2643
2766
  body
2644
2767
  );
2645
2768
  }
2646
2769
  return await res.json();
2647
2770
  },
2648
- async post(path9, body) {
2771
+ async post(path10, body) {
2649
2772
  let res;
2650
2773
  try {
2651
- res = await fetch(`${root}${path9}`, {
2774
+ res = await fetch(`${root}${path10}`, {
2652
2775
  method: "POST",
2653
2776
  headers: { "content-type": "application/json", ...authHeader },
2654
2777
  body: JSON.stringify(body)
@@ -2662,7 +2785,7 @@ function createHttpClient(baseUrl, bearerToken) {
2662
2785
  const text = await res.text().catch(() => "");
2663
2786
  throw new HttpError(
2664
2787
  res.status,
2665
- `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
2788
+ `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
2666
2789
  text
2667
2790
  );
2668
2791
  }
@@ -2676,12 +2799,12 @@ function projectPath(project, suffix) {
2676
2799
  }
2677
2800
  async function runRootCause(client, input) {
2678
2801
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
2679
- const path9 = projectPath(
2802
+ const path10 = projectPath(
2680
2803
  input.project,
2681
2804
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
2682
2805
  );
2683
2806
  try {
2684
- const result = await client.get(path9);
2807
+ const result = await client.get(path10);
2685
2808
  const arrowPath = result.traversalPath.join(" \u2190 ");
2686
2809
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
2687
2810
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -2707,12 +2830,12 @@ async function runRootCause(client, input) {
2707
2830
  }
2708
2831
  async function runBlastRadius(client, input) {
2709
2832
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
2710
- const path9 = projectPath(
2833
+ const path10 = projectPath(
2711
2834
  input.project,
2712
2835
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
2713
2836
  );
2714
2837
  try {
2715
- const result = await client.get(path9);
2838
+ const result = await client.get(path10);
2716
2839
  if (result.totalAffected === 0) {
2717
2840
  return {
2718
2841
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -2746,12 +2869,12 @@ function formatBlastEntry(n) {
2746
2869
  }
2747
2870
  async function runDependencies(client, input) {
2748
2871
  const depth = input.depth ?? 3;
2749
- const path9 = projectPath(
2872
+ const path10 = projectPath(
2750
2873
  input.project,
2751
2874
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
2752
2875
  );
2753
2876
  try {
2754
- const result = await client.get(path9);
2877
+ const result = await client.get(path10);
2755
2878
  if (result.total === 0) {
2756
2879
  return {
2757
2880
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -2832,9 +2955,9 @@ function formatDuration(ms) {
2832
2955
  return `${Math.round(h / 24)}d`;
2833
2956
  }
2834
2957
  async function runIncidents(client, input) {
2835
- const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2958
+ const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2836
2959
  try {
2837
- const body = await client.get(path9);
2960
+ const body = await client.get(path10);
2838
2961
  const events = body.events;
2839
2962
  if (events.length === 0) {
2840
2963
  return {
@@ -3124,7 +3247,7 @@ async function resolveProjectEntry(opts) {
3124
3247
  const cwd = opts.cwd ?? process.cwd();
3125
3248
  const resolvedCwd = await normalizeProjectPath(cwd);
3126
3249
  for (const entry2 of entries) {
3127
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
3250
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
3128
3251
  return entry2;
3129
3252
  }
3130
3253
  }
@@ -3484,10 +3607,10 @@ function assignFlag(out, field, value) {
3484
3607
  out[field] = value;
3485
3608
  }
3486
3609
  function readPackageVersion() {
3487
- const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3610
+ const here = typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath(import.meta.url));
3488
3611
  const candidates = [
3489
- path8.resolve(here, "../package.json"),
3490
- path8.resolve(here, "../../package.json")
3612
+ path9.resolve(here, "../package.json"),
3613
+ path9.resolve(here, "../../package.json")
3491
3614
  ];
3492
3615
  for (const candidate of candidates) {
3493
3616
  try {
@@ -3555,7 +3678,7 @@ async function buildPatchSections(services, project) {
3555
3678
  }
3556
3679
  async function runInit(opts) {
3557
3680
  const written = [];
3558
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
3681
+ const stat = await fs8.stat(opts.scanPath).catch(() => null);
3559
3682
  if (!stat || !stat.isDirectory()) {
3560
3683
  console.error(`neat init: ${opts.scanPath} is not a directory`);
3561
3684
  return { exitCode: 2, writtenFiles: written };
@@ -3564,13 +3687,13 @@ async function runInit(opts) {
3564
3687
  printDiscoveryReport(opts, services);
3565
3688
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
3566
3689
  const patch = renderPatch(sections);
3567
- const patchPath = path8.join(opts.scanPath, "neat.patch");
3690
+ const patchPath = path9.join(opts.scanPath, "neat.patch");
3568
3691
  if (opts.dryRun) {
3569
- await fs7.writeFile(patchPath, patch, "utf8");
3692
+ await fs8.writeFile(patchPath, patch, "utf8");
3570
3693
  written.push(patchPath);
3571
3694
  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);
3695
+ const gitignorePath = path9.join(opts.scanPath, ".gitignore");
3696
+ const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
3574
3697
  const verb = gitignoreExists ? "append" : "create";
3575
3698
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
3576
3699
  console.log("rerun without --dry-run to register and snapshot.");
@@ -3581,9 +3704,9 @@ async function runInit(opts) {
3581
3704
  const graph = getGraph(graphKey);
3582
3705
  const projectPaths = pathsForProject(
3583
3706
  graphKey,
3584
- path8.join(opts.scanPath, "neat-out")
3707
+ path9.join(opts.scanPath, "neat-out")
3585
3708
  );
3586
- const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
3709
+ const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
3587
3710
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
3588
3711
  await saveGraphToDisk(graph, opts.outPath);
3589
3712
  written.push(opts.outPath);
@@ -3662,7 +3785,7 @@ async function runInit(opts) {
3662
3785
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3663
3786
  }
3664
3787
  } else {
3665
- await fs7.writeFile(patchPath, patch, "utf8");
3788
+ await fs8.writeFile(patchPath, patch, "utf8");
3666
3789
  written.push(patchPath);
3667
3790
  }
3668
3791
  }
@@ -3702,9 +3825,9 @@ var CLAUDE_SKILL_CONFIG = {
3702
3825
  };
3703
3826
  function claudeConfigPath() {
3704
3827
  const override = process.env.NEAT_CLAUDE_CONFIG;
3705
- if (override && override.length > 0) return path8.resolve(override);
3828
+ if (override && override.length > 0) return path9.resolve(override);
3706
3829
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3707
- return path8.join(home, ".claude.json");
3830
+ return path9.join(home, ".claude.json");
3708
3831
  }
3709
3832
  async function runSkill(opts) {
3710
3833
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -3716,7 +3839,7 @@ async function runSkill(opts) {
3716
3839
  const target = claudeConfigPath();
3717
3840
  let existing = {};
3718
3841
  try {
3719
- existing = JSON.parse(await fs7.readFile(target, "utf8"));
3842
+ existing = JSON.parse(await fs8.readFile(target, "utf8"));
3720
3843
  } catch (err) {
3721
3844
  if (err.code !== "ENOENT") {
3722
3845
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -3728,8 +3851,8 @@ async function runSkill(opts) {
3728
3851
  ...existing,
3729
3852
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
3730
3853
  };
3731
- await fs7.mkdir(path8.dirname(target), { recursive: true });
3732
- await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
3854
+ await fs8.mkdir(path9.dirname(target), { recursive: true });
3855
+ await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
3733
3856
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
3734
3857
  console.log("restart Claude Code to pick up the new MCP server.");
3735
3858
  return { exitCode: 0 };
@@ -3767,12 +3890,12 @@ async function main() {
3767
3890
  console.error("neat init: --apply and --dry-run are mutually exclusive");
3768
3891
  process.exit(2);
3769
3892
  }
3770
- const scanPath = path8.resolve(target);
3893
+ const scanPath = path9.resolve(target);
3771
3894
  const projectExplicit = parsed.project !== null;
3772
- const projectName = projectExplicit ? project : path8.basename(scanPath);
3895
+ const projectName = projectExplicit ? project : path9.basename(scanPath);
3773
3896
  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);
3897
+ const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
3898
+ const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
3776
3899
  const result = await runInit({
3777
3900
  scanPath,
3778
3901
  outPath,
@@ -3793,21 +3916,21 @@ async function main() {
3793
3916
  usage();
3794
3917
  process.exit(2);
3795
3918
  }
3796
- const scanPath = path8.resolve(target);
3797
- const stat = await fs7.stat(scanPath).catch(() => null);
3919
+ const scanPath = path9.resolve(target);
3920
+ const stat = await fs8.stat(scanPath).catch(() => null);
3798
3921
  if (!stat || !stat.isDirectory()) {
3799
3922
  console.error(`neat watch: ${scanPath} is not a directory`);
3800
3923
  process.exit(2);
3801
3924
  }
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))
3925
+ const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
3926
+ const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
3927
+ const errorsPath = path9.resolve(
3928
+ process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
3806
3929
  );
3807
- const staleEventsPath = path8.resolve(
3808
- process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
3930
+ const staleEventsPath = path9.resolve(
3931
+ process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
3809
3932
  );
3810
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
3933
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
3811
3934
  const handle = await startWatch(getGraph(project), {
3812
3935
  scanPath,
3813
3936
  outPath,
@@ -3953,11 +4076,11 @@ async function main() {
3953
4076
  process.exit(1);
3954
4077
  }
3955
4078
  async function tryOrchestrator(cmd, parsed) {
3956
- const scanPath = path8.resolve(cmd);
3957
- const stat = await fs7.stat(scanPath).catch(() => null);
4079
+ const scanPath = path9.resolve(cmd);
4080
+ const stat = await fs8.stat(scanPath).catch(() => null);
3958
4081
  if (!stat || !stat.isDirectory()) return null;
3959
4082
  const projectExplicit = parsed.project !== null;
3960
- const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4083
+ const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
3961
4084
  const result = await runOrchestrator({
3962
4085
  scanPath,
3963
4086
  project: projectName,