@neat.is/core 0.4.3 → 0.4.5

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,15 +41,15 @@ import {
41
41
  setStatus,
42
42
  startPersistLoop,
43
43
  startStalenessLoop
44
- } from "./chunk-NTQHMXWE.js";
44
+ } from "./chunk-RBWL4HRB.js";
45
45
  import {
46
46
  startOtelGrpcReceiver
47
- } from "./chunk-D5PIJFBE.js";
47
+ } from "./chunk-3QCRUEQD.js";
48
48
  import {
49
49
  __dirname,
50
50
  __require,
51
51
  buildOtelReceiver
52
- } from "./chunk-KYRIQIPG.js";
52
+ } from "./chunk-HVF4S7J3.js";
53
53
 
54
54
  // src/cli.ts
55
55
  import path8 from "path";
@@ -694,45 +694,49 @@ import path4 from "path";
694
694
  // src/installers/templates.ts
695
695
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
696
696
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
697
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
698
- // the auto-instrumentation hook attaches. Configure via the env file.
699
- const path = require('node:path')
700
- try {
701
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
702
- } catch (err) {
703
- // dotenv unavailable \u2014 fall through to process.env.
704
- }
705
- require('@opentelemetry/auto-instrumentations-node/register')
697
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
698
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
699
+
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()
706
706
  `;
707
707
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
708
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
709
- // the auto-instrumentation hook attaches. Configure via the env file.
710
- import { fileURLToPath } from 'node:url'
711
- import path from 'node:path'
712
- import dotenv from 'dotenv'
708
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
709
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
713
710
 
714
- const here = path.dirname(fileURLToPath(import.meta.url))
715
- dotenv.config({ path: path.join(here, '.env.neat') })
711
+ import { NodeSDK } from '@opentelemetry/sdk-node'
712
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
716
713
 
717
- await import('@opentelemetry/auto-instrumentations-node/register')
714
+ const instrumentations = [getNodeAutoInstrumentations()]
715
+ __INSTRUMENTATION_BLOCK__
716
+ new NodeSDK({ instrumentations }).start()
718
717
  `;
719
718
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
720
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
721
- // the auto-instrumentation hook attaches. Configure via the env file.
722
- import { fileURLToPath } from 'node:url'
723
- import path from 'node:path'
724
- import dotenv from 'dotenv'
719
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
720
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
725
721
 
726
- const here = path.dirname(fileURLToPath(import.meta.url))
727
- dotenv.config({ path: path.join(here, '.env.neat') })
722
+ import { NodeSDK } from '@opentelemetry/sdk-node'
723
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
728
724
 
729
- await import('@opentelemetry/auto-instrumentations-node/register')
725
+ const instrumentations = [getNodeAutoInstrumentations()]
726
+ __INSTRUMENTATION_BLOCK__
727
+ new NodeSDK({ instrumentations }).start()
730
728
  `;
731
- function renderEnvNeat(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);
734
+ }
735
+ function renderEnvNeat(serviceName, projectName) {
732
736
  return [
733
737
  "# Generated by `neat init --apply` (ADR-069).",
734
- `OTEL_SERVICE_NAME=${projectName}`,
735
- "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
738
+ `OTEL_SERVICE_NAME=${serviceName}`,
739
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
736
740
  ""
737
741
  ].join("\n");
738
742
  }
@@ -752,39 +756,41 @@ export async function register() {
752
756
  }
753
757
  `;
754
758
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
755
- process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
756
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
759
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
760
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
757
761
 
758
762
  import { NodeSDK } from '@opentelemetry/sdk-node'
759
763
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
760
764
 
761
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
765
+ const instrumentations = [getNodeAutoInstrumentations()]
766
+ __INSTRUMENTATION_BLOCK__
767
+ new NodeSDK({ instrumentations }).start()
762
768
  `;
763
769
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
764
- process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
765
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
770
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
771
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
766
772
 
767
773
  const { NodeSDK } = require('@opentelemetry/sdk-node')
768
774
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
769
775
 
770
- new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
776
+ const instrumentations = [getNodeAutoInstrumentations()]
777
+ __INSTRUMENTATION_BLOCK__
778
+ new NodeSDK({ instrumentations }).start()
771
779
  `;
772
- function renderNextInstrumentationNode(template, projectName) {
773
- return template.replace(/__PROJECT_NAME__/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);
774
785
  }
775
786
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
776
787
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
777
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
778
- // the OTel SDK starts so the exporter target and service name are in scope.
779
- import { fileURLToPath } from 'node:url'
780
- import path from 'node:path'
781
- import dotenv from 'dotenv'
788
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
789
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
790
+
782
791
  import { NodeSDK } from '@opentelemetry/sdk-node'
783
792
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
784
793
 
785
- const here = path.dirname(fileURLToPath(import.meta.url))
786
- dotenv.config({ path: path.join(here, '.env.neat') })
787
-
788
794
  const sdk = new NodeSDK({
789
795
  serviceName: process.env.OTEL_SERVICE_NAME,
790
796
  instrumentations: [getNodeAutoInstrumentations()],
@@ -792,14 +798,9 @@ const sdk = new NodeSDK({
792
798
  sdk.start()
793
799
  `;
794
800
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
795
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
796
- // the OTel SDK starts so the exporter target and service name are in scope.
797
- const path = require('node:path')
798
- try {
799
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
800
- } catch (err) {
801
- // dotenv unavailable \u2014 fall through to process.env.
802
- }
801
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
802
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
803
+
803
804
  const { NodeSDK } = require('@opentelemetry/sdk-node')
804
805
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
805
806
 
@@ -809,6 +810,9 @@ const sdk = new NodeSDK({
809
810
  })
810
811
  sdk.start()
811
812
  `;
813
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
814
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
815
+ }
812
816
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
813
817
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
814
818
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -871,25 +875,56 @@ export const onRequest = defineMiddleware(async (context, next) => {
871
875
  var SDK_PACKAGES = [
872
876
  { name: "@opentelemetry/api", version: "^1.9.0" },
873
877
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
874
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
875
- // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
876
- // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
877
- // before the auto-instrumentation hook attaches.
878
- { name: "dotenv", version: "^16.4.5" }
878
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
879
879
  ];
880
+ function detectNonBundledInstrumentations(pkg) {
881
+ const deps = allDeps(pkg);
882
+ const out = [];
883
+ if ("@prisma/client" in deps) {
884
+ out.push({
885
+ pkg: "@prisma/instrumentation",
886
+ version: "^5.0.0",
887
+ registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
888
+ });
889
+ }
890
+ return out;
891
+ }
880
892
  var OTEL_ENV = {
881
893
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
882
894
  // by the apply phase). The envEdits surface stays for the dry-run
883
895
  // patch render: it documents the key/value the user can inspect in the
884
896
  // generated .env.neat.
885
897
  file: null,
886
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
887
- value: "http://localhost:4318"
898
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
899
+ value: "http://localhost:4318/projects/<project>/v1/traces"
888
900
  };
889
- function envServiceName(pkg, serviceDir, project) {
901
+ function serviceNodeName(pkg, serviceDir) {
902
+ return pkg.name ?? path4.basename(serviceDir);
903
+ }
904
+ function projectToken(pkg, serviceDir, project) {
890
905
  if (project && project.length > 0) return project;
891
906
  return pkg.name ?? path4.basename(serviceDir);
892
907
  }
908
+ async function readJsonFile(p) {
909
+ try {
910
+ const raw = await fs4.readFile(p, "utf8");
911
+ return JSON.parse(raw);
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+ async function detectRuntimeKind(pkgRoot, pkg) {
917
+ const deps = allDeps(pkg);
918
+ if ("react-native" in deps || "expo" in deps) return "react-native";
919
+ const appJson = await readJsonFile(path4.join(pkgRoot, "app.json"));
920
+ if (appJson && typeof appJson === "object" && "expo" in appJson) {
921
+ return "react-native";
922
+ }
923
+ 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) {
924
+ return "browser-bundle";
925
+ }
926
+ return "node";
927
+ }
893
928
  async function readPackageJson(serviceDir) {
894
929
  try {
895
930
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -1136,7 +1171,6 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1136
1171
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1137
1172
  const dependencyEdits = [];
1138
1173
  for (const sdk of SDK_PACKAGES) {
1139
- if (sdk.name === "dotenv") continue;
1140
1174
  if (sdk.name in existingDeps) continue;
1141
1175
  dependencyEdits.push({
1142
1176
  file: manifestPath,
@@ -1145,7 +1179,19 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1145
1179
  version: sdk.version
1146
1180
  });
1147
1181
  }
1148
- const projectName = envServiceName(pkg, serviceDir, project);
1182
+ const nonBundled = detectNonBundledInstrumentations(pkg);
1183
+ for (const inst of nonBundled) {
1184
+ if (inst.pkg in existingDeps) continue;
1185
+ dependencyEdits.push({
1186
+ file: manifestPath,
1187
+ kind: "add",
1188
+ name: inst.pkg,
1189
+ version: inst.version
1190
+ });
1191
+ }
1192
+ const svcName = serviceNodeName(pkg, serviceDir);
1193
+ const projectName = projectToken(pkg, serviceDir, project);
1194
+ const registrations = nonBundled.map((i) => i.registration);
1149
1195
  const generatedFiles = [];
1150
1196
  if (!await exists(instrumentationFile)) {
1151
1197
  generatedFiles.push({
@@ -1159,7 +1205,9 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1159
1205
  file: instrumentationNodeFile,
1160
1206
  contents: renderNextInstrumentationNode(
1161
1207
  useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1162
- projectName
1208
+ svcName,
1209
+ projectName,
1210
+ registrations
1163
1211
  ),
1164
1212
  skipIfExists: true
1165
1213
  });
@@ -1167,7 +1215,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1167
1215
  if (!await exists(envNeatFile)) {
1168
1216
  generatedFiles.push({
1169
1217
  file: envNeatFile,
1170
- contents: renderEnvNeat(projectName),
1218
+ contents: renderEnvNeat(svcName, projectName),
1171
1219
  skipIfExists: true
1172
1220
  });
1173
1221
  }
@@ -1228,11 +1276,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1228
1276
  if (!await exists(envNeatFile)) {
1229
1277
  generatedFiles.push({
1230
1278
  file: envNeatFile,
1231
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1279
+ contents: renderEnvNeat(
1280
+ serviceNodeName(pkg, serviceDir),
1281
+ projectToken(pkg, serviceDir, project)
1282
+ ),
1232
1283
  skipIfExists: true
1233
1284
  });
1234
1285
  }
1235
1286
  }
1287
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
1288
+ return renderFrameworkOtelInit(
1289
+ template,
1290
+ serviceNodeName(pkg, serviceDir),
1291
+ projectToken(pkg, serviceDir, project)
1292
+ );
1293
+ }
1236
1294
  function fileImportsOtelHook(raw, specifiers) {
1237
1295
  const lines = raw.split(/\r?\n/);
1238
1296
  for (const line of lines) {
@@ -1258,7 +1316,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1258
1316
  if (!await exists(otelServerFile)) {
1259
1317
  generatedFiles.push({
1260
1318
  file: otelServerFile,
1261
- contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1319
+ contents: renderFrameworkOtelInitForPkg(
1320
+ useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1321
+ pkg,
1322
+ serviceDir,
1323
+ project
1324
+ ),
1262
1325
  skipIfExists: true
1263
1326
  });
1264
1327
  }
@@ -1312,7 +1375,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
1312
1375
  if (!await exists(otelInitFile)) {
1313
1376
  generatedFiles.push({
1314
1377
  file: otelInitFile,
1315
- contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1378
+ contents: renderFrameworkOtelInitForPkg(
1379
+ useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1380
+ pkg,
1381
+ serviceDir,
1382
+ project
1383
+ ),
1316
1384
  skipIfExists: true
1317
1385
  });
1318
1386
  }
@@ -1375,7 +1443,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1375
1443
  if (!await exists(otelInitFile)) {
1376
1444
  generatedFiles.push({
1377
1445
  file: otelInitFile,
1378
- contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1446
+ contents: renderFrameworkOtelInitForPkg(
1447
+ useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1448
+ pkg,
1449
+ serviceDir,
1450
+ project
1451
+ ),
1379
1452
  skipIfExists: true
1380
1453
  });
1381
1454
  }
@@ -1431,7 +1504,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
1431
1504
  if (!await exists(otelInitFile)) {
1432
1505
  generatedFiles.push({
1433
1506
  file: otelInitFile,
1434
- contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1507
+ contents: renderFrameworkOtelInitForPkg(
1508
+ useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1509
+ pkg,
1510
+ serviceDir,
1511
+ project
1512
+ ),
1435
1513
  skipIfExists: true
1436
1514
  });
1437
1515
  }
@@ -1479,51 +1557,73 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
1479
1557
  framework: "astro"
1480
1558
  };
1481
1559
  }
1482
- async function plan(serviceDir, opts) {
1483
- const pkg = await readPackageJson(serviceDir);
1484
- const manifestPath = path4.join(serviceDir, "package.json");
1485
- const project = opts?.project;
1486
- const empty = {
1487
- language: "javascript",
1488
- serviceDir,
1489
- dependencyEdits: [],
1490
- entrypointEdits: [],
1491
- envEdits: [],
1492
- generatedFiles: []
1493
- };
1494
- if (!pkg) return empty;
1560
+ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
1495
1561
  if (hasNextDependency(pkg)) {
1496
1562
  const nextConfig = await findNextConfig(serviceDir);
1497
1563
  if (nextConfig) {
1498
- return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1564
+ return () => planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1499
1565
  }
1500
1566
  }
1501
1567
  if (hasRemixDependency(pkg)) {
1502
1568
  const remixEntry = await findRemixEntry(serviceDir);
1503
1569
  if (remixEntry) {
1504
- return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1570
+ return () => planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1505
1571
  }
1506
1572
  }
1507
1573
  if (hasSvelteKitDependency(pkg)) {
1508
1574
  const hooks = await findSvelteKitHooks(serviceDir);
1509
1575
  const config = await findSvelteKitConfig(serviceDir);
1510
1576
  if (hooks || config) {
1511
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1577
+ return () => planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1512
1578
  }
1513
1579
  }
1514
1580
  if (hasNuxtDependency(pkg)) {
1515
1581
  const nuxtConfig = await findNuxtConfig(serviceDir);
1516
1582
  if (nuxtConfig) {
1517
- return planNuxt(serviceDir, pkg, manifestPath, project);
1583
+ return () => planNuxt(serviceDir, pkg, manifestPath, project);
1518
1584
  }
1519
1585
  }
1520
1586
  if (hasAstroDependency(pkg)) {
1521
1587
  const astroConfig = await findAstroConfig(serviceDir);
1522
1588
  if (astroConfig) {
1523
- return planAstro(serviceDir, pkg, manifestPath, project);
1589
+ return () => planAstro(serviceDir, pkg, manifestPath, project);
1524
1590
  }
1525
1591
  }
1526
- const entryFile = await resolveEntry(serviceDir, pkg);
1592
+ return null;
1593
+ }
1594
+ async function plan(serviceDir, opts) {
1595
+ const pkg = await readPackageJson(serviceDir);
1596
+ const manifestPath = path4.join(serviceDir, "package.json");
1597
+ const project = opts?.project;
1598
+ const empty = {
1599
+ language: "javascript",
1600
+ serviceDir,
1601
+ dependencyEdits: [],
1602
+ entrypointEdits: [],
1603
+ envEdits: [],
1604
+ generatedFiles: []
1605
+ };
1606
+ if (!pkg) return empty;
1607
+ const frameworkDispatch = await findFrameworkDispatch(
1608
+ serviceDir,
1609
+ pkg,
1610
+ manifestPath,
1611
+ project
1612
+ );
1613
+ let entryFile = null;
1614
+ if (!frameworkDispatch) {
1615
+ entryFile = await resolveEntry(serviceDir, pkg);
1616
+ if (!entryFile) {
1617
+ return { ...empty, libOnly: true };
1618
+ }
1619
+ }
1620
+ if (frameworkDispatch) {
1621
+ return frameworkDispatch();
1622
+ }
1623
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
1624
+ if (runtimeKind !== "node") {
1625
+ return { ...empty, runtimeKind };
1626
+ }
1527
1627
  if (!entryFile) {
1528
1628
  return { ...empty, libOnly: true };
1529
1629
  }
@@ -1541,6 +1641,16 @@ async function plan(serviceDir, opts) {
1541
1641
  version: sdk.version
1542
1642
  });
1543
1643
  }
1644
+ const nonBundled = detectNonBundledInstrumentations(pkg);
1645
+ for (const inst of nonBundled) {
1646
+ if (inst.pkg in existingDeps) continue;
1647
+ dependencyEdits.push({
1648
+ file: manifestPath,
1649
+ kind: "add",
1650
+ name: inst.pkg,
1651
+ version: inst.version
1652
+ });
1653
+ }
1544
1654
  const entrypointEdits = [];
1545
1655
  try {
1546
1656
  const raw = await fs4.readFile(entryFile, "utf8");
@@ -1557,18 +1667,26 @@ async function plan(serviceDir, opts) {
1557
1667
  } catch {
1558
1668
  return { ...empty, libOnly: true };
1559
1669
  }
1670
+ const svcName = serviceNodeName(pkg, serviceDir);
1671
+ const projectName = projectToken(pkg, serviceDir, project);
1672
+ const registrations = nonBundled.map((i) => i.registration);
1560
1673
  const generatedFiles = [];
1561
1674
  if (!await exists(otelInitFile)) {
1562
1675
  generatedFiles.push({
1563
1676
  file: otelInitFile,
1564
- contents: otelInitContents(flavor),
1677
+ contents: renderNodeOtelInit(
1678
+ otelInitContents(flavor),
1679
+ svcName,
1680
+ projectName,
1681
+ registrations
1682
+ ),
1565
1683
  skipIfExists: true
1566
1684
  });
1567
1685
  }
1568
1686
  if (!await exists(envNeatFile)) {
1569
1687
  generatedFiles.push({
1570
1688
  file: envNeatFile,
1571
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1689
+ contents: renderEnvNeat(svcName, projectName),
1572
1690
  skipIfExists: true
1573
1691
  });
1574
1692
  }
@@ -1625,6 +1743,22 @@ async function apply(installPlan) {
1625
1743
  writtenFiles: []
1626
1744
  };
1627
1745
  }
1746
+ if (installPlan.runtimeKind === "browser-bundle") {
1747
+ return {
1748
+ serviceDir,
1749
+ outcome: "browser-bundle",
1750
+ reason: "browser bundle; Node OTel SDK cannot run here",
1751
+ writtenFiles: []
1752
+ };
1753
+ }
1754
+ if (installPlan.runtimeKind === "react-native") {
1755
+ return {
1756
+ serviceDir,
1757
+ outcome: "react-native",
1758
+ reason: "React Native / Expo target; Node OTel SDK cannot run here",
1759
+ writtenFiles: []
1760
+ };
1761
+ }
1628
1762
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
1629
1763
  return {
1630
1764
  serviceDir,
@@ -2077,6 +2211,7 @@ function renderPatch(sections) {
2077
2211
  // src/orchestrator.ts
2078
2212
  import { promises as fs6 } from "fs";
2079
2213
  import http from "http";
2214
+ import net from "net";
2080
2215
  import path6 from "path";
2081
2216
  import { spawn as spawn2 } from "child_process";
2082
2217
  import readline from "readline";
@@ -2108,11 +2243,13 @@ async function applyInstallersOver(services, project) {
2108
2243
  let instrumented = 0;
2109
2244
  let already = 0;
2110
2245
  let libOnly = 0;
2246
+ let browserBundle = 0;
2247
+ let reactNative = 0;
2111
2248
  for (const svc of services) {
2112
2249
  const installer = await pickInstaller(svc.dir);
2113
2250
  if (!installer) continue;
2114
2251
  const plan3 = await installer.plan(svc.dir, { project });
2115
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
2252
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
2116
2253
  already++;
2117
2254
  continue;
2118
2255
  }
@@ -2120,8 +2257,15 @@ async function applyInstallersOver(services, project) {
2120
2257
  if (outcome.outcome === "instrumented") instrumented++;
2121
2258
  else if (outcome.outcome === "already-instrumented") already++;
2122
2259
  else if (outcome.outcome === "lib-only") libOnly++;
2260
+ else if (outcome.outcome === "browser-bundle") {
2261
+ browserBundle++;
2262
+ console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
2263
+ } else if (outcome.outcome === "react-native") {
2264
+ reactNative++;
2265
+ console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2266
+ }
2123
2267
  }
2124
- return { instrumented, alreadyInstrumented: already, libOnly };
2268
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
2125
2269
  }
2126
2270
  async function promptYesNo(question) {
2127
2271
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2234,6 +2378,28 @@ async function waitForDaemonReady(restPort, timeoutMs) {
2234
2378
  stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
2235
2379
  };
2236
2380
  }
2381
+ var NEAT_PORTS = [8080, 4318, 6328];
2382
+ async function isPortFree(port) {
2383
+ return new Promise((resolve) => {
2384
+ const server = net.createServer();
2385
+ server.once("error", () => resolve(false));
2386
+ server.once("listening", () => server.close(() => resolve(true)));
2387
+ server.listen(port, "127.0.0.1");
2388
+ });
2389
+ }
2390
+ async function probePortsFree() {
2391
+ for (const port of NEAT_PORTS) {
2392
+ if (!await isPortFree(port)) return { free: false, held: port };
2393
+ }
2394
+ return { free: true };
2395
+ }
2396
+ function formatPortCollisionMessage(port) {
2397
+ return [
2398
+ `neat: port ${port} is in use; the NEAT daemon needs it.`,
2399
+ ` run \`neatd stop\` to release the previous daemon, or`,
2400
+ ` \`lsof -i :${port}\` to find the holding process.`
2401
+ ];
2402
+ }
2237
2403
  function spawnDaemonDetached() {
2238
2404
  const here = path6.dirname(new URL(import.meta.url).pathname);
2239
2405
  const candidates = [
@@ -2325,13 +2491,15 @@ async function runOrchestrator(opts) {
2325
2491
  if (gi.action !== "unchanged") {
2326
2492
  console.log(`${gi.action} .gitignore (neat-out/)`);
2327
2493
  }
2494
+ let currentProjectName = opts.project;
2328
2495
  try {
2329
- await addProject({
2496
+ const entry2 = await addProject({
2330
2497
  name: opts.project,
2331
2498
  path: opts.scanPath,
2332
2499
  languages,
2333
2500
  status: "active"
2334
2501
  });
2502
+ currentProjectName = entry2.name;
2335
2503
  } catch (err) {
2336
2504
  if (!(err instanceof ProjectNameCollisionError)) throw err;
2337
2505
  console.error(`neat: ${err.message}`);
@@ -2339,6 +2507,20 @@ async function runOrchestrator(opts) {
2339
2507
  result.exitCode = 1;
2340
2508
  return result;
2341
2509
  }
2510
+ const siblings = await listProjects();
2511
+ const paused = [];
2512
+ for (const p of siblings) {
2513
+ if (p.name !== currentProjectName && p.status === "active") {
2514
+ await setStatus(p.name, "paused");
2515
+ paused.push(p.name);
2516
+ }
2517
+ }
2518
+ if (paused.length > 0) {
2519
+ const plural = paused.length === 1 ? "" : "s";
2520
+ console.log(
2521
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
2522
+ );
2523
+ }
2342
2524
  if (!runApply) {
2343
2525
  result.steps.apply.skipped = true;
2344
2526
  console.log("skipped instrumentation (--no-instrument)");
@@ -2354,6 +2536,14 @@ async function runOrchestrator(opts) {
2354
2536
  if (await checkDaemonHealth(restPort)) {
2355
2537
  result.steps.daemon = "already-running";
2356
2538
  } else {
2539
+ const probe = await probePortsFree();
2540
+ if (!probe.free) {
2541
+ for (const line of formatPortCollisionMessage(probe.held)) {
2542
+ console.error(line);
2543
+ }
2544
+ result.exitCode = 3;
2545
+ return result;
2546
+ }
2357
2547
  try {
2358
2548
  spawnDaemonDetached();
2359
2549
  } catch (err) {
@@ -3009,7 +3199,7 @@ async function runSync(opts) {
3009
3199
  snapshotPath = target;
3010
3200
  }
3011
3201
  const skipApply = opts.dryRun || opts.noInstrument;
3012
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
3202
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name);
3013
3203
  const warnings = [];
3014
3204
  let daemonState = "skipped";
3015
3205
  let exitCode = 0;
@@ -3159,7 +3349,8 @@ function usage() {
3159
3349
  console.log(" 0 success");
3160
3350
  console.log(" 1 server error (4xx/5xx body printed to stderr)");
3161
3351
  console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
3162
- console.log(" 3 daemon not reachable (connection refused / timeout)");
3352
+ console.log(" 3 environmental \u2014 daemon unreachable, or one of ports 8080 / 4318 / 6328");
3353
+ console.log(" is held by another process when `neat <path>` tries to spawn the daemon");
3163
3354
  console.log("");
3164
3355
  console.log("environment:");
3165
3356
  console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
@@ -3357,7 +3548,7 @@ async function buildPatchSections(services, project) {
3357
3548
  const installer = await pickInstaller(svc.dir);
3358
3549
  if (!installer) continue;
3359
3550
  const plan3 = await installer.plan(svc.dir, { project });
3360
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
3551
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
3361
3552
  sections.push({ installer: installer.name, plan: plan3 });
3362
3553
  }
3363
3554
  return sections;
@@ -3401,13 +3592,15 @@ async function runInit(opts) {
3401
3592
  written.push(gitignoreResult.file);
3402
3593
  }
3403
3594
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
3595
+ let currentProjectName = opts.project;
3404
3596
  try {
3405
- await addProject({
3597
+ const entry2 = await addProject({
3406
3598
  name: opts.project,
3407
3599
  path: opts.scanPath,
3408
3600
  languages,
3409
3601
  status: "active"
3410
3602
  });
3603
+ currentProjectName = entry2.name;
3411
3604
  } catch (err) {
3412
3605
  if (err instanceof ProjectNameCollisionError) {
3413
3606
  console.error(`neat init: ${err.message}`);
@@ -3416,11 +3609,27 @@ async function runInit(opts) {
3416
3609
  }
3417
3610
  throw err;
3418
3611
  }
3612
+ const siblings = await listProjects();
3613
+ const paused = [];
3614
+ for (const p of siblings) {
3615
+ if (p.name !== currentProjectName && p.status === "active") {
3616
+ await setStatus(p.name, "paused");
3617
+ paused.push(p.name);
3618
+ }
3619
+ }
3620
+ if (paused.length > 0) {
3621
+ const plural = paused.length === 1 ? "" : "s";
3622
+ console.log(
3623
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
3624
+ );
3625
+ }
3419
3626
  if (!opts.noInstall) {
3420
3627
  if (opts.apply) {
3421
3628
  let instrumented = 0;
3422
3629
  let alreadyInstrumented = 0;
3423
3630
  let libOnly = 0;
3631
+ let browserBundle = 0;
3632
+ let reactNative = 0;
3424
3633
  for (const section of sections) {
3425
3634
  const installer = INSTALLERS.find((i) => i.name === section.installer);
3426
3635
  if (!installer) continue;
@@ -3432,13 +3641,24 @@ async function runInit(opts) {
3432
3641
  alreadyInstrumented++;
3433
3642
  } else if (outcome.outcome === "lib-only") {
3434
3643
  libOnly++;
3644
+ } else if (outcome.outcome === "browser-bundle") {
3645
+ browserBundle++;
3646
+ console.log(`skipping ${section.plan.serviceDir}: browser bundle; browser-OTel support lands in a future release.`);
3647
+ } else if (outcome.outcome === "react-native") {
3648
+ reactNative++;
3649
+ console.log(`skipping ${section.plan.serviceDir}: React Native target; browser-OTel support lands in a future release.`);
3435
3650
  }
3436
3651
  }
3437
3652
  if (sections.length > 0) {
3438
3653
  console.log("");
3439
- console.log(
3440
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
3441
- );
3654
+ const parts = [
3655
+ `instrumented ${instrumented}`,
3656
+ `already-instrumented ${alreadyInstrumented}`,
3657
+ `lib-only ${libOnly}`
3658
+ ];
3659
+ if (browserBundle > 0) parts.push(`browser-bundle ${browserBundle}`);
3660
+ if (reactNative > 0) parts.push(`react-native ${reactNative}`);
3661
+ console.log(`apply: ${parts.join(", ")}`);
3442
3662
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3443
3663
  }
3444
3664
  } else {