@neat.is/core 0.4.4 → 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
@@ -41,19 +41,19 @@ import {
41
41
  setStatus,
42
42
  startPersistLoop,
43
43
  startStalenessLoop
44
- } from "./chunk-G3YGPWJL.js";
44
+ } from "./chunk-RBWL4HRB.js";
45
45
  import {
46
46
  startOtelGrpcReceiver
47
- } from "./chunk-YJOA7BBF.js";
47
+ } from "./chunk-3QCRUEQD.js";
48
48
  import {
49
49
  __dirname,
50
50
  __require,
51
51
  buildOtelReceiver
52
- } from "./chunk-ZVNP3ZDH.js";
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
@@ -697,22 +697,40 @@ var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
697
697
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
698
698
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
699
699
 
700
- require('@opentelemetry/auto-instrumentations-node/register')
700
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
701
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
702
+
703
+ const instrumentations = [getNodeAutoInstrumentations()]
704
+ __INSTRUMENTATION_BLOCK__
705
+ new NodeSDK({ instrumentations }).start()
701
706
  `;
702
707
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
703
708
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
704
709
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
705
710
 
706
- await import('@opentelemetry/auto-instrumentations-node/register')
711
+ import { NodeSDK } from '@opentelemetry/sdk-node'
712
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
713
+
714
+ const instrumentations = [getNodeAutoInstrumentations()]
715
+ __INSTRUMENTATION_BLOCK__
716
+ new NodeSDK({ instrumentations }).start()
707
717
  `;
708
718
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
709
719
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
710
720
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
711
721
 
712
- await import('@opentelemetry/auto-instrumentations-node/register')
722
+ import { NodeSDK } from '@opentelemetry/sdk-node'
723
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
724
+
725
+ const instrumentations = [getNodeAutoInstrumentations()]
726
+ __INSTRUMENTATION_BLOCK__
727
+ new NodeSDK({ instrumentations }).start()
713
728
  `;
714
- function renderNodeOtelInit(template, serviceName, projectName) {
715
- return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
729
+ function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
730
+ const block = registrations.length === 0 ? "" : `
731
+ ${registrations.join("\n")}
732
+ `;
733
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
716
734
  }
717
735
  function renderEnvNeat(serviceName, projectName) {
718
736
  return [
@@ -744,7 +762,9 @@ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projec
744
762
  import { NodeSDK } from '@opentelemetry/sdk-node'
745
763
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
746
764
 
747
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
765
+ const instrumentations = [getNodeAutoInstrumentations()]
766
+ __INSTRUMENTATION_BLOCK__
767
+ new NodeSDK({ instrumentations }).start()
748
768
  `;
749
769
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
750
770
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
@@ -753,10 +773,15 @@ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projec
753
773
  const { NodeSDK } = require('@opentelemetry/sdk-node')
754
774
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
755
775
 
756
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
776
+ const instrumentations = [getNodeAutoInstrumentations()]
777
+ __INSTRUMENTATION_BLOCK__
778
+ new NodeSDK({ instrumentations }).start()
757
779
  `;
758
- function renderNextInstrumentationNode(template, serviceName, projectName) {
759
- return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
780
+ function renderNextInstrumentationNode(template, serviceName, projectName, registrations = []) {
781
+ const block = registrations.length === 0 ? "" : `
782
+ ${registrations.join("\n")}
783
+ `;
784
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
760
785
  }
761
786
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
762
787
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
@@ -852,6 +877,25 @@ var SDK_PACKAGES = [
852
877
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
853
878
  { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
854
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
+ }
885
+ function detectNonBundledInstrumentations(pkg) {
886
+ const deps = allDeps(pkg);
887
+ const out = [];
888
+ if ("@prisma/client" in deps) {
889
+ const prismaMajor = getMajor(deps["@prisma/client"]);
890
+ const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
891
+ out.push({
892
+ pkg: "@prisma/instrumentation",
893
+ version: prismaInstrVersion,
894
+ registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
895
+ });
896
+ }
897
+ return out;
898
+ }
855
899
  var OTEL_ENV = {
856
900
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
857
901
  // by the apply phase). The envEdits surface stays for the dry-run
@@ -1142,8 +1186,19 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1142
1186
  version: sdk.version
1143
1187
  });
1144
1188
  }
1189
+ const nonBundled = detectNonBundledInstrumentations(pkg);
1190
+ for (const inst of nonBundled) {
1191
+ if (inst.pkg in existingDeps) continue;
1192
+ dependencyEdits.push({
1193
+ file: manifestPath,
1194
+ kind: "add",
1195
+ name: inst.pkg,
1196
+ version: inst.version
1197
+ });
1198
+ }
1145
1199
  const svcName = serviceNodeName(pkg, serviceDir);
1146
1200
  const projectName = projectToken(pkg, serviceDir, project);
1201
+ const registrations = nonBundled.map((i) => i.registration);
1147
1202
  const generatedFiles = [];
1148
1203
  if (!await exists(instrumentationFile)) {
1149
1204
  generatedFiles.push({
@@ -1158,7 +1213,8 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1158
1213
  contents: renderNextInstrumentationNode(
1159
1214
  useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1160
1215
  svcName,
1161
- projectName
1216
+ projectName,
1217
+ registrations
1162
1218
  ),
1163
1219
  skipIfExists: true
1164
1220
  });
@@ -1508,55 +1564,73 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
1508
1564
  framework: "astro"
1509
1565
  };
1510
1566
  }
1511
- async function plan(serviceDir, opts) {
1512
- const pkg = await readPackageJson(serviceDir);
1513
- const manifestPath = path4.join(serviceDir, "package.json");
1514
- const project = opts?.project;
1515
- const empty = {
1516
- language: "javascript",
1517
- serviceDir,
1518
- dependencyEdits: [],
1519
- entrypointEdits: [],
1520
- envEdits: [],
1521
- generatedFiles: []
1522
- };
1523
- if (!pkg) return empty;
1567
+ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
1524
1568
  if (hasNextDependency(pkg)) {
1525
1569
  const nextConfig = await findNextConfig(serviceDir);
1526
1570
  if (nextConfig) {
1527
- return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1571
+ return () => planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1528
1572
  }
1529
1573
  }
1530
1574
  if (hasRemixDependency(pkg)) {
1531
1575
  const remixEntry = await findRemixEntry(serviceDir);
1532
1576
  if (remixEntry) {
1533
- return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1577
+ return () => planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1534
1578
  }
1535
1579
  }
1536
1580
  if (hasSvelteKitDependency(pkg)) {
1537
1581
  const hooks = await findSvelteKitHooks(serviceDir);
1538
1582
  const config = await findSvelteKitConfig(serviceDir);
1539
1583
  if (hooks || config) {
1540
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1584
+ return () => planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1541
1585
  }
1542
1586
  }
1543
1587
  if (hasNuxtDependency(pkg)) {
1544
1588
  const nuxtConfig = await findNuxtConfig(serviceDir);
1545
1589
  if (nuxtConfig) {
1546
- return planNuxt(serviceDir, pkg, manifestPath, project);
1590
+ return () => planNuxt(serviceDir, pkg, manifestPath, project);
1547
1591
  }
1548
1592
  }
1549
1593
  if (hasAstroDependency(pkg)) {
1550
1594
  const astroConfig = await findAstroConfig(serviceDir);
1551
1595
  if (astroConfig) {
1552
- return planAstro(serviceDir, pkg, manifestPath, project);
1596
+ return () => planAstro(serviceDir, pkg, manifestPath, project);
1597
+ }
1598
+ }
1599
+ return null;
1600
+ }
1601
+ async function plan(serviceDir, opts) {
1602
+ const pkg = await readPackageJson(serviceDir);
1603
+ const manifestPath = path4.join(serviceDir, "package.json");
1604
+ const project = opts?.project;
1605
+ const empty = {
1606
+ language: "javascript",
1607
+ serviceDir,
1608
+ dependencyEdits: [],
1609
+ entrypointEdits: [],
1610
+ envEdits: [],
1611
+ generatedFiles: []
1612
+ };
1613
+ if (!pkg) return empty;
1614
+ const frameworkDispatch = await findFrameworkDispatch(
1615
+ serviceDir,
1616
+ pkg,
1617
+ manifestPath,
1618
+ project
1619
+ );
1620
+ let entryFile = null;
1621
+ if (!frameworkDispatch) {
1622
+ entryFile = await resolveEntry(serviceDir, pkg);
1623
+ if (!entryFile) {
1624
+ return { ...empty, libOnly: true };
1553
1625
  }
1554
1626
  }
1627
+ if (frameworkDispatch) {
1628
+ return frameworkDispatch();
1629
+ }
1555
1630
  const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
1556
1631
  if (runtimeKind !== "node") {
1557
1632
  return { ...empty, runtimeKind };
1558
1633
  }
1559
- const entryFile = await resolveEntry(serviceDir, pkg);
1560
1634
  if (!entryFile) {
1561
1635
  return { ...empty, libOnly: true };
1562
1636
  }
@@ -1574,6 +1648,16 @@ async function plan(serviceDir, opts) {
1574
1648
  version: sdk.version
1575
1649
  });
1576
1650
  }
1651
+ const nonBundled = detectNonBundledInstrumentations(pkg);
1652
+ for (const inst of nonBundled) {
1653
+ if (inst.pkg in existingDeps) continue;
1654
+ dependencyEdits.push({
1655
+ file: manifestPath,
1656
+ kind: "add",
1657
+ name: inst.pkg,
1658
+ version: inst.version
1659
+ });
1660
+ }
1577
1661
  const entrypointEdits = [];
1578
1662
  try {
1579
1663
  const raw = await fs4.readFile(entryFile, "utf8");
@@ -1592,11 +1676,17 @@ async function plan(serviceDir, opts) {
1592
1676
  }
1593
1677
  const svcName = serviceNodeName(pkg, serviceDir);
1594
1678
  const projectName = projectToken(pkg, serviceDir, project);
1679
+ const registrations = nonBundled.map((i) => i.registration);
1595
1680
  const generatedFiles = [];
1596
1681
  if (!await exists(otelInitFile)) {
1597
1682
  generatedFiles.push({
1598
1683
  file: otelInitFile,
1599
- contents: renderNodeOtelInit(otelInitContents(flavor), svcName, projectName),
1684
+ contents: renderNodeOtelInit(
1685
+ otelInitContents(flavor),
1686
+ svcName,
1687
+ projectName,
1688
+ registrations
1689
+ ),
1600
1690
  skipIfExists: true
1601
1691
  });
1602
1692
  }
@@ -2126,18 +2216,99 @@ function renderPatch(sections) {
2126
2216
  }
2127
2217
 
2128
2218
  // src/orchestrator.ts
2129
- import { promises as fs6 } from "fs";
2219
+ import { promises as fs7 } from "fs";
2130
2220
  import http from "http";
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";
2131
2228
  import path6 from "path";
2132
2229
  import { spawn as spawn2 } from "child_process";
2133
- 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
2134
2305
  async function extractAndPersist(opts) {
2135
2306
  const services = await discoverServices(opts.scanPath);
2136
2307
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
2137
2308
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2138
2309
  resetGraph(graphKey);
2139
2310
  const graph = getGraph(graphKey);
2140
- const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2311
+ const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
2141
2312
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
2142
2313
  errorsPath: projectPaths.errorsPath
2143
2314
  });
@@ -2155,12 +2326,15 @@ async function extractAndPersist(opts) {
2155
2326
  errorsPath: projectPaths.errorsPath
2156
2327
  };
2157
2328
  }
2158
- async function applyInstallersOver(services, project) {
2329
+ async function applyInstallersOver(services, project, options = {}) {
2330
+ const resolveManager = options.resolveManager ?? detectPackageManager;
2331
+ const runInstall = options.runInstall ?? runPackageManagerInstall;
2159
2332
  let instrumented = 0;
2160
2333
  let already = 0;
2161
2334
  let libOnly = 0;
2162
2335
  let browserBundle = 0;
2163
2336
  let reactNative = 0;
2337
+ const installPlans = /* @__PURE__ */ new Map();
2164
2338
  for (const svc of services) {
2165
2339
  const installer = await pickInstaller(svc.dir);
2166
2340
  if (!installer) continue;
@@ -2170,8 +2344,14 @@ async function applyInstallersOver(services, project) {
2170
2344
  continue;
2171
2345
  }
2172
2346
  const outcome = await installer.apply(plan3);
2173
- if (outcome.outcome === "instrumented") instrumented++;
2174
- 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++;
2175
2355
  else if (outcome.outcome === "lib-only") libOnly++;
2176
2356
  else if (outcome.outcome === "browser-bundle") {
2177
2357
  browserBundle++;
@@ -2181,7 +2361,30 @@ async function applyInstallersOver(services, project) {
2181
2361
  console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2182
2362
  }
2183
2363
  }
2184
- 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
+ };
2185
2388
  }
2186
2389
  async function promptYesNo(question) {
2187
2390
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2294,11 +2497,33 @@ async function waitForDaemonReady(restPort, timeoutMs) {
2294
2497
  stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
2295
2498
  };
2296
2499
  }
2500
+ var NEAT_PORTS = [8080, 4318, 6328];
2501
+ async function isPortFree(port) {
2502
+ return new Promise((resolve) => {
2503
+ const server = net.createServer();
2504
+ server.once("error", () => resolve(false));
2505
+ server.once("listening", () => server.close(() => resolve(true)));
2506
+ server.listen(port, "127.0.0.1");
2507
+ });
2508
+ }
2509
+ async function probePortsFree() {
2510
+ for (const port of NEAT_PORTS) {
2511
+ if (!await isPortFree(port)) return { free: false, held: port };
2512
+ }
2513
+ return { free: true };
2514
+ }
2515
+ function formatPortCollisionMessage(port) {
2516
+ return [
2517
+ `neat: port ${port} is in use; the NEAT daemon needs it.`,
2518
+ ` run \`neatd stop\` to release the previous daemon, or`,
2519
+ ` \`lsof -i :${port}\` to find the holding process.`
2520
+ ];
2521
+ }
2297
2522
  function spawnDaemonDetached() {
2298
- const here = path6.dirname(new URL(import.meta.url).pathname);
2523
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2299
2524
  const candidates = [
2300
- path6.join(here, "neatd.cjs"),
2301
- path6.join(here, "neatd.js")
2525
+ path7.join(here, "neatd.cjs"),
2526
+ path7.join(here, "neatd.js")
2302
2527
  ];
2303
2528
  let entry2 = null;
2304
2529
  const fsSync = __require("fs");
@@ -2318,7 +2543,7 @@ function spawnDaemonDetached() {
2318
2543
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2319
2544
  env.HOST = "127.0.0.1";
2320
2545
  }
2321
- const child = spawn2(process.execPath, [entry2, "start"], {
2546
+ const child = spawn3(process.execPath, [entry2, "start"], {
2322
2547
  detached: true,
2323
2548
  // stderr inherits the orchestrator's fd so the daemon's
2324
2549
  // `BindAuthorityError` message lands in front of the operator instead
@@ -2335,7 +2560,7 @@ function openBrowser(url) {
2335
2560
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2336
2561
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
2337
2562
  try {
2338
- const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2563
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
2339
2564
  child.on("error", () => {
2340
2565
  });
2341
2566
  child.unref();
@@ -2356,7 +2581,7 @@ async function runOrchestrator(opts) {
2356
2581
  browser: "skipped"
2357
2582
  }
2358
2583
  };
2359
- const stat = await fs6.stat(opts.scanPath).catch(() => null);
2584
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
2360
2585
  if (!stat || !stat.isDirectory()) {
2361
2586
  console.error(`neat: ${opts.scanPath} is not a directory`);
2362
2587
  result.exitCode = 2;
@@ -2424,12 +2649,24 @@ async function runOrchestrator(opts) {
2424
2649
  console.log(
2425
2650
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
2426
2651
  );
2652
+ const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
2653
+ if (failedInstalls.length > 0) {
2654
+ result.exitCode = 1;
2655
+ }
2427
2656
  }
2428
2657
  const restPort = Number(process.env.PORT ?? 8080);
2429
2658
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
2430
2659
  if (await checkDaemonHealth(restPort)) {
2431
2660
  result.steps.daemon = "already-running";
2432
2661
  } else {
2662
+ const probe = await probePortsFree();
2663
+ if (!probe.free) {
2664
+ for (const line of formatPortCollisionMessage(probe.held)) {
2665
+ console.error(line);
2666
+ }
2667
+ result.exitCode = 3;
2668
+ return result;
2669
+ }
2433
2670
  try {
2434
2671
  spawnDaemonDetached();
2435
2672
  } catch (err) {
@@ -2486,7 +2723,7 @@ function printSummary(result, graph, dashboardUrl) {
2486
2723
  }
2487
2724
 
2488
2725
  // src/cli-verbs.ts
2489
- import path7 from "path";
2726
+ import path8 from "path";
2490
2727
 
2491
2728
  // src/cli-client.ts
2492
2729
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -2510,10 +2747,10 @@ function createHttpClient(baseUrl, bearerToken) {
2510
2747
  const root = baseUrl.replace(/\/$/, "");
2511
2748
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
2512
2749
  return {
2513
- async get(path9) {
2750
+ async get(path10) {
2514
2751
  let res;
2515
2752
  try {
2516
- res = await fetch(`${root}${path9}`, {
2753
+ res = await fetch(`${root}${path10}`, {
2517
2754
  headers: { ...authHeader }
2518
2755
  });
2519
2756
  } catch (err) {
@@ -2525,16 +2762,16 @@ function createHttpClient(baseUrl, bearerToken) {
2525
2762
  const body = await res.text().catch(() => "");
2526
2763
  throw new HttpError(
2527
2764
  res.status,
2528
- `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
2765
+ `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
2529
2766
  body
2530
2767
  );
2531
2768
  }
2532
2769
  return await res.json();
2533
2770
  },
2534
- async post(path9, body) {
2771
+ async post(path10, body) {
2535
2772
  let res;
2536
2773
  try {
2537
- res = await fetch(`${root}${path9}`, {
2774
+ res = await fetch(`${root}${path10}`, {
2538
2775
  method: "POST",
2539
2776
  headers: { "content-type": "application/json", ...authHeader },
2540
2777
  body: JSON.stringify(body)
@@ -2548,7 +2785,7 @@ function createHttpClient(baseUrl, bearerToken) {
2548
2785
  const text = await res.text().catch(() => "");
2549
2786
  throw new HttpError(
2550
2787
  res.status,
2551
- `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
2788
+ `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
2552
2789
  text
2553
2790
  );
2554
2791
  }
@@ -2562,12 +2799,12 @@ function projectPath(project, suffix) {
2562
2799
  }
2563
2800
  async function runRootCause(client, input) {
2564
2801
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
2565
- const path9 = projectPath(
2802
+ const path10 = projectPath(
2566
2803
  input.project,
2567
2804
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
2568
2805
  );
2569
2806
  try {
2570
- const result = await client.get(path9);
2807
+ const result = await client.get(path10);
2571
2808
  const arrowPath = result.traversalPath.join(" \u2190 ");
2572
2809
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
2573
2810
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -2593,12 +2830,12 @@ async function runRootCause(client, input) {
2593
2830
  }
2594
2831
  async function runBlastRadius(client, input) {
2595
2832
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
2596
- const path9 = projectPath(
2833
+ const path10 = projectPath(
2597
2834
  input.project,
2598
2835
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
2599
2836
  );
2600
2837
  try {
2601
- const result = await client.get(path9);
2838
+ const result = await client.get(path10);
2602
2839
  if (result.totalAffected === 0) {
2603
2840
  return {
2604
2841
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -2632,12 +2869,12 @@ function formatBlastEntry(n) {
2632
2869
  }
2633
2870
  async function runDependencies(client, input) {
2634
2871
  const depth = input.depth ?? 3;
2635
- const path9 = projectPath(
2872
+ const path10 = projectPath(
2636
2873
  input.project,
2637
2874
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
2638
2875
  );
2639
2876
  try {
2640
- const result = await client.get(path9);
2877
+ const result = await client.get(path10);
2641
2878
  if (result.total === 0) {
2642
2879
  return {
2643
2880
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -2718,9 +2955,9 @@ function formatDuration(ms) {
2718
2955
  return `${Math.round(h / 24)}d`;
2719
2956
  }
2720
2957
  async function runIncidents(client, input) {
2721
- 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");
2722
2959
  try {
2723
- const body = await client.get(path9);
2960
+ const body = await client.get(path10);
2724
2961
  const events = body.events;
2725
2962
  if (events.length === 0) {
2726
2963
  return {
@@ -3010,7 +3247,7 @@ async function resolveProjectEntry(opts) {
3010
3247
  const cwd = opts.cwd ?? process.cwd();
3011
3248
  const resolvedCwd = await normalizeProjectPath(cwd);
3012
3249
  for (const entry2 of entries) {
3013
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
3250
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
3014
3251
  return entry2;
3015
3252
  }
3016
3253
  }
@@ -3235,7 +3472,8 @@ function usage() {
3235
3472
  console.log(" 0 success");
3236
3473
  console.log(" 1 server error (4xx/5xx body printed to stderr)");
3237
3474
  console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
3238
- console.log(" 3 daemon not reachable (connection refused / timeout)");
3475
+ console.log(" 3 environmental \u2014 daemon unreachable, or one of ports 8080 / 4318 / 6328");
3476
+ console.log(" is held by another process when `neat <path>` tries to spawn the daemon");
3239
3477
  console.log("");
3240
3478
  console.log("environment:");
3241
3479
  console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
@@ -3369,10 +3607,10 @@ function assignFlag(out, field, value) {
3369
3607
  out[field] = value;
3370
3608
  }
3371
3609
  function readPackageVersion() {
3372
- 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));
3373
3611
  const candidates = [
3374
- path8.resolve(here, "../package.json"),
3375
- path8.resolve(here, "../../package.json")
3612
+ path9.resolve(here, "../package.json"),
3613
+ path9.resolve(here, "../../package.json")
3376
3614
  ];
3377
3615
  for (const candidate of candidates) {
3378
3616
  try {
@@ -3440,7 +3678,7 @@ async function buildPatchSections(services, project) {
3440
3678
  }
3441
3679
  async function runInit(opts) {
3442
3680
  const written = [];
3443
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
3681
+ const stat = await fs8.stat(opts.scanPath).catch(() => null);
3444
3682
  if (!stat || !stat.isDirectory()) {
3445
3683
  console.error(`neat init: ${opts.scanPath} is not a directory`);
3446
3684
  return { exitCode: 2, writtenFiles: written };
@@ -3449,13 +3687,13 @@ async function runInit(opts) {
3449
3687
  printDiscoveryReport(opts, services);
3450
3688
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
3451
3689
  const patch = renderPatch(sections);
3452
- const patchPath = path8.join(opts.scanPath, "neat.patch");
3690
+ const patchPath = path9.join(opts.scanPath, "neat.patch");
3453
3691
  if (opts.dryRun) {
3454
- await fs7.writeFile(patchPath, patch, "utf8");
3692
+ await fs8.writeFile(patchPath, patch, "utf8");
3455
3693
  written.push(patchPath);
3456
3694
  console.log(`dry-run: patch written to ${patchPath}`);
3457
- const gitignorePath = path8.join(opts.scanPath, ".gitignore");
3458
- 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);
3459
3697
  const verb = gitignoreExists ? "append" : "create";
3460
3698
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
3461
3699
  console.log("rerun without --dry-run to register and snapshot.");
@@ -3466,9 +3704,9 @@ async function runInit(opts) {
3466
3704
  const graph = getGraph(graphKey);
3467
3705
  const projectPaths = pathsForProject(
3468
3706
  graphKey,
3469
- path8.join(opts.scanPath, "neat-out")
3707
+ path9.join(opts.scanPath, "neat-out")
3470
3708
  );
3471
- 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));
3472
3710
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
3473
3711
  await saveGraphToDisk(graph, opts.outPath);
3474
3712
  written.push(opts.outPath);
@@ -3547,7 +3785,7 @@ async function runInit(opts) {
3547
3785
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3548
3786
  }
3549
3787
  } else {
3550
- await fs7.writeFile(patchPath, patch, "utf8");
3788
+ await fs8.writeFile(patchPath, patch, "utf8");
3551
3789
  written.push(patchPath);
3552
3790
  }
3553
3791
  }
@@ -3587,9 +3825,9 @@ var CLAUDE_SKILL_CONFIG = {
3587
3825
  };
3588
3826
  function claudeConfigPath() {
3589
3827
  const override = process.env.NEAT_CLAUDE_CONFIG;
3590
- if (override && override.length > 0) return path8.resolve(override);
3828
+ if (override && override.length > 0) return path9.resolve(override);
3591
3829
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3592
- return path8.join(home, ".claude.json");
3830
+ return path9.join(home, ".claude.json");
3593
3831
  }
3594
3832
  async function runSkill(opts) {
3595
3833
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -3601,7 +3839,7 @@ async function runSkill(opts) {
3601
3839
  const target = claudeConfigPath();
3602
3840
  let existing = {};
3603
3841
  try {
3604
- existing = JSON.parse(await fs7.readFile(target, "utf8"));
3842
+ existing = JSON.parse(await fs8.readFile(target, "utf8"));
3605
3843
  } catch (err) {
3606
3844
  if (err.code !== "ENOENT") {
3607
3845
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -3613,8 +3851,8 @@ async function runSkill(opts) {
3613
3851
  ...existing,
3614
3852
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
3615
3853
  };
3616
- await fs7.mkdir(path8.dirname(target), { recursive: true });
3617
- 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");
3618
3856
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
3619
3857
  console.log("restart Claude Code to pick up the new MCP server.");
3620
3858
  return { exitCode: 0 };
@@ -3652,12 +3890,12 @@ async function main() {
3652
3890
  console.error("neat init: --apply and --dry-run are mutually exclusive");
3653
3891
  process.exit(2);
3654
3892
  }
3655
- const scanPath = path8.resolve(target);
3893
+ const scanPath = path9.resolve(target);
3656
3894
  const projectExplicit = parsed.project !== null;
3657
- const projectName = projectExplicit ? project : path8.basename(scanPath);
3895
+ const projectName = projectExplicit ? project : path9.basename(scanPath);
3658
3896
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
3659
- const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
3660
- 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);
3661
3899
  const result = await runInit({
3662
3900
  scanPath,
3663
3901
  outPath,
@@ -3678,21 +3916,21 @@ async function main() {
3678
3916
  usage();
3679
3917
  process.exit(2);
3680
3918
  }
3681
- const scanPath = path8.resolve(target);
3682
- const stat = await fs7.stat(scanPath).catch(() => null);
3919
+ const scanPath = path9.resolve(target);
3920
+ const stat = await fs8.stat(scanPath).catch(() => null);
3683
3921
  if (!stat || !stat.isDirectory()) {
3684
3922
  console.error(`neat watch: ${scanPath} is not a directory`);
3685
3923
  process.exit(2);
3686
3924
  }
3687
- const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
3688
- const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
3689
- const errorsPath = path8.resolve(
3690
- 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))
3691
3929
  );
3692
- const staleEventsPath = path8.resolve(
3693
- 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))
3694
3932
  );
3695
- 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;
3696
3934
  const handle = await startWatch(getGraph(project), {
3697
3935
  scanPath,
3698
3936
  outPath,
@@ -3838,11 +4076,11 @@ async function main() {
3838
4076
  process.exit(1);
3839
4077
  }
3840
4078
  async function tryOrchestrator(cmd, parsed) {
3841
- const scanPath = path8.resolve(cmd);
3842
- const stat = await fs7.stat(scanPath).catch(() => null);
4079
+ const scanPath = path9.resolve(cmd);
4080
+ const stat = await fs8.stat(scanPath).catch(() => null);
3843
4081
  if (!stat || !stat.isDirectory()) return null;
3844
4082
  const projectExplicit = parsed.project !== null;
3845
- const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4083
+ const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
3846
4084
  const result = await runOrchestrator({
3847
4085
  scanPath,
3848
4086
  project: projectName,