@neat.is/core 0.4.2 → 0.4.4

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-G3YGPWJL.js";
45
45
  import {
46
46
  startOtelGrpcReceiver
47
- } from "./chunk-D5PIJFBE.js";
47
+ } from "./chunk-YJOA7BBF.js";
48
48
  import {
49
49
  __dirname,
50
50
  __require,
51
51
  buildOtelReceiver
52
- } from "./chunk-KYRIQIPG.js";
52
+ } from "./chunk-ZVNP3ZDH.js";
53
53
 
54
54
  // src/cli.ts
55
55
  import path8 from "path";
@@ -694,45 +694,31 @@ 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
- }
697
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
698
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
699
+
705
700
  require('@opentelemetry/auto-instrumentations-node/register')
706
701
  `;
707
702
  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'
713
-
714
- const here = path.dirname(fileURLToPath(import.meta.url))
715
- dotenv.config({ path: path.join(here, '.env.neat') })
703
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
704
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
716
705
 
717
706
  await import('@opentelemetry/auto-instrumentations-node/register')
718
707
  `;
719
708
  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'
725
-
726
- const here = path.dirname(fileURLToPath(import.meta.url))
727
- dotenv.config({ path: path.join(here, '.env.neat') })
709
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
710
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
728
711
 
729
712
  await import('@opentelemetry/auto-instrumentations-node/register')
730
713
  `;
731
- function renderEnvNeat(projectName) {
714
+ function renderNodeOtelInit(template, serviceName, projectName) {
715
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
716
+ }
717
+ function renderEnvNeat(serviceName, projectName) {
732
718
  return [
733
719
  "# Generated by `neat init --apply` (ADR-069).",
734
- `OTEL_SERVICE_NAME=${projectName}`,
735
- "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
720
+ `OTEL_SERVICE_NAME=${serviceName}`,
721
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
736
722
  ""
737
723
  ].join("\n");
738
724
  }
@@ -752,54 +738,34 @@ export async function register() {
752
738
  }
753
739
  `;
754
740
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
755
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
756
- // the OTel SDK starts so the exporter target and service name are in scope.
757
- import { fileURLToPath } from 'node:url'
758
- import path from 'node:path'
759
- import dotenv from 'dotenv'
741
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
742
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
743
+
760
744
  import { NodeSDK } from '@opentelemetry/sdk-node'
761
745
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
762
746
 
763
- const here = path.dirname(fileURLToPath(import.meta.url))
764
- dotenv.config({ path: path.join(here, '.env.neat') })
765
-
766
- const sdk = new NodeSDK({
767
- serviceName: process.env.OTEL_SERVICE_NAME,
768
- instrumentations: [getNodeAutoInstrumentations()],
769
- })
770
- sdk.start()
747
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
771
748
  `;
772
749
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
773
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
774
- // the OTel SDK starts so the exporter target and service name are in scope.
775
- import { fileURLToPath } from 'node:url'
776
- import path from 'node:path'
777
- import dotenv from 'dotenv'
778
- import { NodeSDK } from '@opentelemetry/sdk-node'
779
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
750
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
751
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
780
752
 
781
- const here = path.dirname(fileURLToPath(import.meta.url))
782
- dotenv.config({ path: path.join(here, '.env.neat') })
753
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
754
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
783
755
 
784
- const sdk = new NodeSDK({
785
- serviceName: process.env.OTEL_SERVICE_NAME,
786
- instrumentations: [getNodeAutoInstrumentations()],
787
- })
788
- sdk.start()
756
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
789
757
  `;
758
+ function renderNextInstrumentationNode(template, serviceName, projectName) {
759
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
760
+ }
790
761
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
791
762
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
792
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
793
- // the OTel SDK starts so the exporter target and service name are in scope.
794
- import { fileURLToPath } from 'node:url'
795
- import path from 'node:path'
796
- import dotenv from 'dotenv'
763
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
764
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
765
+
797
766
  import { NodeSDK } from '@opentelemetry/sdk-node'
798
767
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
799
768
 
800
- const here = path.dirname(fileURLToPath(import.meta.url))
801
- dotenv.config({ path: path.join(here, '.env.neat') })
802
-
803
769
  const sdk = new NodeSDK({
804
770
  serviceName: process.env.OTEL_SERVICE_NAME,
805
771
  instrumentations: [getNodeAutoInstrumentations()],
@@ -807,14 +773,9 @@ const sdk = new NodeSDK({
807
773
  sdk.start()
808
774
  `;
809
775
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
810
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
811
- // the OTel SDK starts so the exporter target and service name are in scope.
812
- const path = require('node:path')
813
- try {
814
- require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
815
- } catch (err) {
816
- // dotenv unavailable \u2014 fall through to process.env.
817
- }
776
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
777
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
778
+
818
779
  const { NodeSDK } = require('@opentelemetry/sdk-node')
819
780
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
820
781
 
@@ -824,6 +785,9 @@ const sdk = new NodeSDK({
824
785
  })
825
786
  sdk.start()
826
787
  `;
788
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
789
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
790
+ }
827
791
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
828
792
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
829
793
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -886,11 +850,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
886
850
  var SDK_PACKAGES = [
887
851
  { name: "@opentelemetry/api", version: "^1.9.0" },
888
852
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
889
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
890
- // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
891
- // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
892
- // before the auto-instrumentation hook attaches.
893
- { name: "dotenv", version: "^16.4.5" }
853
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
894
854
  ];
895
855
  var OTEL_ENV = {
896
856
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
@@ -898,13 +858,36 @@ var OTEL_ENV = {
898
858
  // patch render: it documents the key/value the user can inspect in the
899
859
  // generated .env.neat.
900
860
  file: null,
901
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
902
- value: "http://localhost:4318"
861
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
862
+ value: "http://localhost:4318/projects/<project>/v1/traces"
903
863
  };
904
- function envServiceName(pkg, serviceDir, project) {
864
+ function serviceNodeName(pkg, serviceDir) {
865
+ return pkg.name ?? path4.basename(serviceDir);
866
+ }
867
+ function projectToken(pkg, serviceDir, project) {
905
868
  if (project && project.length > 0) return project;
906
869
  return pkg.name ?? path4.basename(serviceDir);
907
870
  }
871
+ async function readJsonFile(p) {
872
+ try {
873
+ const raw = await fs4.readFile(p, "utf8");
874
+ return JSON.parse(raw);
875
+ } catch {
876
+ return null;
877
+ }
878
+ }
879
+ async function detectRuntimeKind(pkgRoot, pkg) {
880
+ const deps = allDeps(pkg);
881
+ if ("react-native" in deps || "expo" in deps) return "react-native";
882
+ const appJson = await readJsonFile(path4.join(pkgRoot, "app.json"));
883
+ if (appJson && typeof appJson === "object" && "expo" in appJson) {
884
+ return "react-native";
885
+ }
886
+ 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) {
887
+ return "browser-bundle";
888
+ }
889
+ return "node";
890
+ }
908
891
  async function readPackageJson(serviceDir) {
909
892
  try {
910
893
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -1129,14 +1112,25 @@ function lineIsOtelInjection(line) {
1129
1112
  if (trimmed.length === 0) return false;
1130
1113
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
1131
1114
  }
1115
+ async function detectsSrcLayout(serviceDir) {
1116
+ const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
1117
+ exists(path4.join(serviceDir, "src", "app")),
1118
+ exists(path4.join(serviceDir, "src", "pages")),
1119
+ exists(path4.join(serviceDir, "app")),
1120
+ exists(path4.join(serviceDir, "pages"))
1121
+ ]);
1122
+ return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
1123
+ }
1132
1124
  async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
1133
1125
  const useTs = await isTypeScriptProject(serviceDir);
1134
- const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
1126
+ const srcLayout = await detectsSrcLayout(serviceDir);
1127
+ const baseDir = srcLayout ? path4.join(serviceDir, "src") : serviceDir;
1128
+ const instrumentationFile = path4.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
1135
1129
  const instrumentationNodeFile = path4.join(
1136
- serviceDir,
1130
+ baseDir,
1137
1131
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
1138
1132
  );
1139
- const envNeatFile = path4.join(serviceDir, ".env.neat");
1133
+ const envNeatFile = path4.join(baseDir, ".env.neat");
1140
1134
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1141
1135
  const dependencyEdits = [];
1142
1136
  for (const sdk of SDK_PACKAGES) {
@@ -1148,6 +1142,8 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1148
1142
  version: sdk.version
1149
1143
  });
1150
1144
  }
1145
+ const svcName = serviceNodeName(pkg, serviceDir);
1146
+ const projectName = projectToken(pkg, serviceDir, project);
1151
1147
  const generatedFiles = [];
1152
1148
  if (!await exists(instrumentationFile)) {
1153
1149
  generatedFiles.push({
@@ -1159,14 +1155,18 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1159
1155
  if (!await exists(instrumentationNodeFile)) {
1160
1156
  generatedFiles.push({
1161
1157
  file: instrumentationNodeFile,
1162
- contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1158
+ contents: renderNextInstrumentationNode(
1159
+ useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1160
+ svcName,
1161
+ projectName
1162
+ ),
1163
1163
  skipIfExists: true
1164
1164
  });
1165
1165
  }
1166
1166
  if (!await exists(envNeatFile)) {
1167
1167
  generatedFiles.push({
1168
1168
  file: envNeatFile,
1169
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1169
+ contents: renderEnvNeat(svcName, projectName),
1170
1170
  skipIfExists: true
1171
1171
  });
1172
1172
  }
@@ -1227,11 +1227,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1227
1227
  if (!await exists(envNeatFile)) {
1228
1228
  generatedFiles.push({
1229
1229
  file: envNeatFile,
1230
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1230
+ contents: renderEnvNeat(
1231
+ serviceNodeName(pkg, serviceDir),
1232
+ projectToken(pkg, serviceDir, project)
1233
+ ),
1231
1234
  skipIfExists: true
1232
1235
  });
1233
1236
  }
1234
1237
  }
1238
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
1239
+ return renderFrameworkOtelInit(
1240
+ template,
1241
+ serviceNodeName(pkg, serviceDir),
1242
+ projectToken(pkg, serviceDir, project)
1243
+ );
1244
+ }
1235
1245
  function fileImportsOtelHook(raw, specifiers) {
1236
1246
  const lines = raw.split(/\r?\n/);
1237
1247
  for (const line of lines) {
@@ -1257,7 +1267,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1257
1267
  if (!await exists(otelServerFile)) {
1258
1268
  generatedFiles.push({
1259
1269
  file: otelServerFile,
1260
- contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1270
+ contents: renderFrameworkOtelInitForPkg(
1271
+ useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1272
+ pkg,
1273
+ serviceDir,
1274
+ project
1275
+ ),
1261
1276
  skipIfExists: true
1262
1277
  });
1263
1278
  }
@@ -1311,7 +1326,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
1311
1326
  if (!await exists(otelInitFile)) {
1312
1327
  generatedFiles.push({
1313
1328
  file: otelInitFile,
1314
- contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1329
+ contents: renderFrameworkOtelInitForPkg(
1330
+ useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1331
+ pkg,
1332
+ serviceDir,
1333
+ project
1334
+ ),
1315
1335
  skipIfExists: true
1316
1336
  });
1317
1337
  }
@@ -1374,7 +1394,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1374
1394
  if (!await exists(otelInitFile)) {
1375
1395
  generatedFiles.push({
1376
1396
  file: otelInitFile,
1377
- contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1397
+ contents: renderFrameworkOtelInitForPkg(
1398
+ useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1399
+ pkg,
1400
+ serviceDir,
1401
+ project
1402
+ ),
1378
1403
  skipIfExists: true
1379
1404
  });
1380
1405
  }
@@ -1430,7 +1455,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
1430
1455
  if (!await exists(otelInitFile)) {
1431
1456
  generatedFiles.push({
1432
1457
  file: otelInitFile,
1433
- contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1458
+ contents: renderFrameworkOtelInitForPkg(
1459
+ useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1460
+ pkg,
1461
+ serviceDir,
1462
+ project
1463
+ ),
1434
1464
  skipIfExists: true
1435
1465
  });
1436
1466
  }
@@ -1522,6 +1552,10 @@ async function plan(serviceDir, opts) {
1522
1552
  return planAstro(serviceDir, pkg, manifestPath, project);
1523
1553
  }
1524
1554
  }
1555
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
1556
+ if (runtimeKind !== "node") {
1557
+ return { ...empty, runtimeKind };
1558
+ }
1525
1559
  const entryFile = await resolveEntry(serviceDir, pkg);
1526
1560
  if (!entryFile) {
1527
1561
  return { ...empty, libOnly: true };
@@ -1556,18 +1590,20 @@ async function plan(serviceDir, opts) {
1556
1590
  } catch {
1557
1591
  return { ...empty, libOnly: true };
1558
1592
  }
1593
+ const svcName = serviceNodeName(pkg, serviceDir);
1594
+ const projectName = projectToken(pkg, serviceDir, project);
1559
1595
  const generatedFiles = [];
1560
1596
  if (!await exists(otelInitFile)) {
1561
1597
  generatedFiles.push({
1562
1598
  file: otelInitFile,
1563
- contents: otelInitContents(flavor),
1599
+ contents: renderNodeOtelInit(otelInitContents(flavor), svcName, projectName),
1564
1600
  skipIfExists: true
1565
1601
  });
1566
1602
  }
1567
1603
  if (!await exists(envNeatFile)) {
1568
1604
  generatedFiles.push({
1569
1605
  file: envNeatFile,
1570
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1606
+ contents: renderEnvNeat(svcName, projectName),
1571
1607
  skipIfExists: true
1572
1608
  });
1573
1609
  }
@@ -1592,9 +1628,13 @@ function isAllowedWritePath(serviceDir, target) {
1592
1628
  if (base === "package.json") return true;
1593
1629
  if (base === ".env.neat") return true;
1594
1630
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1595
- if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1596
- if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1597
1631
  const relPosix = rel.split(path4.sep).join("/");
1632
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
1633
+ if (relPosix === base) return true;
1634
+ if (relPosix === `src/${base}`) return true;
1635
+ return false;
1636
+ }
1637
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1598
1638
  if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
1599
1639
  if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
1600
1640
  if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
@@ -1620,6 +1660,22 @@ async function apply(installPlan) {
1620
1660
  writtenFiles: []
1621
1661
  };
1622
1662
  }
1663
+ if (installPlan.runtimeKind === "browser-bundle") {
1664
+ return {
1665
+ serviceDir,
1666
+ outcome: "browser-bundle",
1667
+ reason: "browser bundle; Node OTel SDK cannot run here",
1668
+ writtenFiles: []
1669
+ };
1670
+ }
1671
+ if (installPlan.runtimeKind === "react-native") {
1672
+ return {
1673
+ serviceDir,
1674
+ outcome: "react-native",
1675
+ reason: "React Native / Expo target; Node OTel SDK cannot run here",
1676
+ writtenFiles: []
1677
+ };
1678
+ }
1623
1679
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
1624
1680
  return {
1625
1681
  serviceDir,
@@ -2103,11 +2159,13 @@ async function applyInstallersOver(services, project) {
2103
2159
  let instrumented = 0;
2104
2160
  let already = 0;
2105
2161
  let libOnly = 0;
2162
+ let browserBundle = 0;
2163
+ let reactNative = 0;
2106
2164
  for (const svc of services) {
2107
2165
  const installer = await pickInstaller(svc.dir);
2108
2166
  if (!installer) continue;
2109
2167
  const plan3 = await installer.plan(svc.dir, { project });
2110
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
2168
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
2111
2169
  already++;
2112
2170
  continue;
2113
2171
  }
@@ -2115,8 +2173,15 @@ async function applyInstallersOver(services, project) {
2115
2173
  if (outcome.outcome === "instrumented") instrumented++;
2116
2174
  else if (outcome.outcome === "already-instrumented") already++;
2117
2175
  else if (outcome.outcome === "lib-only") libOnly++;
2176
+ else if (outcome.outcome === "browser-bundle") {
2177
+ browserBundle++;
2178
+ console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
2179
+ } else if (outcome.outcome === "react-native") {
2180
+ reactNative++;
2181
+ console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
2182
+ }
2118
2183
  }
2119
- return { instrumented, alreadyInstrumented: already, libOnly };
2184
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
2120
2185
  }
2121
2186
  async function promptYesNo(question) {
2122
2187
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2320,13 +2385,15 @@ async function runOrchestrator(opts) {
2320
2385
  if (gi.action !== "unchanged") {
2321
2386
  console.log(`${gi.action} .gitignore (neat-out/)`);
2322
2387
  }
2388
+ let currentProjectName = opts.project;
2323
2389
  try {
2324
- await addProject({
2390
+ const entry2 = await addProject({
2325
2391
  name: opts.project,
2326
2392
  path: opts.scanPath,
2327
2393
  languages,
2328
2394
  status: "active"
2329
2395
  });
2396
+ currentProjectName = entry2.name;
2330
2397
  } catch (err) {
2331
2398
  if (!(err instanceof ProjectNameCollisionError)) throw err;
2332
2399
  console.error(`neat: ${err.message}`);
@@ -2334,6 +2401,20 @@ async function runOrchestrator(opts) {
2334
2401
  result.exitCode = 1;
2335
2402
  return result;
2336
2403
  }
2404
+ const siblings = await listProjects();
2405
+ const paused = [];
2406
+ for (const p of siblings) {
2407
+ if (p.name !== currentProjectName && p.status === "active") {
2408
+ await setStatus(p.name, "paused");
2409
+ paused.push(p.name);
2410
+ }
2411
+ }
2412
+ if (paused.length > 0) {
2413
+ const plural = paused.length === 1 ? "" : "s";
2414
+ console.log(
2415
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
2416
+ );
2417
+ }
2337
2418
  if (!runApply) {
2338
2419
  result.steps.apply.skipped = true;
2339
2420
  console.log("skipped instrumentation (--no-instrument)");
@@ -3004,7 +3085,7 @@ async function runSync(opts) {
3004
3085
  snapshotPath = target;
3005
3086
  }
3006
3087
  const skipApply = opts.dryRun || opts.noInstrument;
3007
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
3088
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name);
3008
3089
  const warnings = [];
3009
3090
  let daemonState = "skipped";
3010
3091
  let exitCode = 0;
@@ -3352,7 +3433,7 @@ async function buildPatchSections(services, project) {
3352
3433
  const installer = await pickInstaller(svc.dir);
3353
3434
  if (!installer) continue;
3354
3435
  const plan3 = await installer.plan(svc.dir, { project });
3355
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
3436
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
3356
3437
  sections.push({ installer: installer.name, plan: plan3 });
3357
3438
  }
3358
3439
  return sections;
@@ -3396,13 +3477,15 @@ async function runInit(opts) {
3396
3477
  written.push(gitignoreResult.file);
3397
3478
  }
3398
3479
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
3480
+ let currentProjectName = opts.project;
3399
3481
  try {
3400
- await addProject({
3482
+ const entry2 = await addProject({
3401
3483
  name: opts.project,
3402
3484
  path: opts.scanPath,
3403
3485
  languages,
3404
3486
  status: "active"
3405
3487
  });
3488
+ currentProjectName = entry2.name;
3406
3489
  } catch (err) {
3407
3490
  if (err instanceof ProjectNameCollisionError) {
3408
3491
  console.error(`neat init: ${err.message}`);
@@ -3411,11 +3494,27 @@ async function runInit(opts) {
3411
3494
  }
3412
3495
  throw err;
3413
3496
  }
3497
+ const siblings = await listProjects();
3498
+ const paused = [];
3499
+ for (const p of siblings) {
3500
+ if (p.name !== currentProjectName && p.status === "active") {
3501
+ await setStatus(p.name, "paused");
3502
+ paused.push(p.name);
3503
+ }
3504
+ }
3505
+ if (paused.length > 0) {
3506
+ const plural = paused.length === 1 ? "" : "s";
3507
+ console.log(
3508
+ `neat: paused ${paused.length} sibling project${plural}; run \`neat resume <name>\` to bring one back active.`
3509
+ );
3510
+ }
3414
3511
  if (!opts.noInstall) {
3415
3512
  if (opts.apply) {
3416
3513
  let instrumented = 0;
3417
3514
  let alreadyInstrumented = 0;
3418
3515
  let libOnly = 0;
3516
+ let browserBundle = 0;
3517
+ let reactNative = 0;
3419
3518
  for (const section of sections) {
3420
3519
  const installer = INSTALLERS.find((i) => i.name === section.installer);
3421
3520
  if (!installer) continue;
@@ -3427,13 +3526,24 @@ async function runInit(opts) {
3427
3526
  alreadyInstrumented++;
3428
3527
  } else if (outcome.outcome === "lib-only") {
3429
3528
  libOnly++;
3529
+ } else if (outcome.outcome === "browser-bundle") {
3530
+ browserBundle++;
3531
+ console.log(`skipping ${section.plan.serviceDir}: browser bundle; browser-OTel support lands in a future release.`);
3532
+ } else if (outcome.outcome === "react-native") {
3533
+ reactNative++;
3534
+ console.log(`skipping ${section.plan.serviceDir}: React Native target; browser-OTel support lands in a future release.`);
3430
3535
  }
3431
3536
  }
3432
3537
  if (sections.length > 0) {
3433
3538
  console.log("");
3434
- console.log(
3435
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
3436
- );
3539
+ const parts = [
3540
+ `instrumented ${instrumented}`,
3541
+ `already-instrumented ${alreadyInstrumented}`,
3542
+ `lib-only ${libOnly}`
3543
+ ];
3544
+ if (browserBundle > 0) parts.push(`browser-bundle ${browserBundle}`);
3545
+ if (reactNative > 0) parts.push(`react-native ${reactNative}`);
3546
+ console.log(`apply: ${parts.join(", ")}`);
3437
3547
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3438
3548
  }
3439
3549
  } else {
@@ -3923,7 +4033,7 @@ async function runQueryVerb(cmd, parsed) {
3923
4033
  }
3924
4034
  }
3925
4035
  var entry = process.argv[1] ?? "";
3926
- if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
4036
+ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat") || entry.endsWith("/neat.is")) {
3927
4037
  main().catch((err) => {
3928
4038
  console.error(err);
3929
4039
  process.exit(1);