@neat.is/core 0.4.3 → 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,8 +738,8 @@ export async function register() {
752
738
  }
753
739
  `;
754
740
  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'
741
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
742
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
757
743
 
758
744
  import { NodeSDK } from '@opentelemetry/sdk-node'
759
745
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
@@ -761,30 +747,25 @@ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentation
761
747
  new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
762
748
  `;
763
749
  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'
750
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
751
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
766
752
 
767
753
  const { NodeSDK } = require('@opentelemetry/sdk-node')
768
754
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
769
755
 
770
756
  new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
771
757
  `;
772
- function renderNextInstrumentationNode(template, projectName) {
773
- return template.replace(/__PROJECT_NAME__/g, projectName);
758
+ function renderNextInstrumentationNode(template, serviceName, projectName) {
759
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
774
760
  }
775
761
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
776
762
  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'
763
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
764
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
765
+
782
766
  import { NodeSDK } from '@opentelemetry/sdk-node'
783
767
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
784
768
 
785
- const here = path.dirname(fileURLToPath(import.meta.url))
786
- dotenv.config({ path: path.join(here, '.env.neat') })
787
-
788
769
  const sdk = new NodeSDK({
789
770
  serviceName: process.env.OTEL_SERVICE_NAME,
790
771
  instrumentations: [getNodeAutoInstrumentations()],
@@ -792,14 +773,9 @@ const sdk = new NodeSDK({
792
773
  sdk.start()
793
774
  `;
794
775
  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
- }
776
+ process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
777
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
778
+
803
779
  const { NodeSDK } = require('@opentelemetry/sdk-node')
804
780
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
805
781
 
@@ -809,6 +785,9 @@ const sdk = new NodeSDK({
809
785
  })
810
786
  sdk.start()
811
787
  `;
788
+ function renderFrameworkOtelInit(template, serviceName, projectName) {
789
+ return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName);
790
+ }
812
791
  var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
813
792
  var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
814
793
  var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
@@ -871,11 +850,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
871
850
  var SDK_PACKAGES = [
872
851
  { name: "@opentelemetry/api", version: "^1.9.0" },
873
852
  { 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" }
853
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
879
854
  ];
880
855
  var OTEL_ENV = {
881
856
  // ADR-069 §4 — endpoint moves into the per-package .env.neat (written
@@ -883,13 +858,36 @@ var OTEL_ENV = {
883
858
  // patch render: it documents the key/value the user can inspect in the
884
859
  // generated .env.neat.
885
860
  file: null,
886
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
887
- value: "http://localhost:4318"
861
+ key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
862
+ value: "http://localhost:4318/projects/<project>/v1/traces"
888
863
  };
889
- function envServiceName(pkg, serviceDir, project) {
864
+ function serviceNodeName(pkg, serviceDir) {
865
+ return pkg.name ?? path4.basename(serviceDir);
866
+ }
867
+ function projectToken(pkg, serviceDir, project) {
890
868
  if (project && project.length > 0) return project;
891
869
  return pkg.name ?? path4.basename(serviceDir);
892
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
+ }
893
891
  async function readPackageJson(serviceDir) {
894
892
  try {
895
893
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -1136,7 +1134,6 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1136
1134
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1137
1135
  const dependencyEdits = [];
1138
1136
  for (const sdk of SDK_PACKAGES) {
1139
- if (sdk.name === "dotenv") continue;
1140
1137
  if (sdk.name in existingDeps) continue;
1141
1138
  dependencyEdits.push({
1142
1139
  file: manifestPath,
@@ -1145,7 +1142,8 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1145
1142
  version: sdk.version
1146
1143
  });
1147
1144
  }
1148
- const projectName = envServiceName(pkg, serviceDir, project);
1145
+ const svcName = serviceNodeName(pkg, serviceDir);
1146
+ const projectName = projectToken(pkg, serviceDir, project);
1149
1147
  const generatedFiles = [];
1150
1148
  if (!await exists(instrumentationFile)) {
1151
1149
  generatedFiles.push({
@@ -1159,6 +1157,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1159
1157
  file: instrumentationNodeFile,
1160
1158
  contents: renderNextInstrumentationNode(
1161
1159
  useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1160
+ svcName,
1162
1161
  projectName
1163
1162
  ),
1164
1163
  skipIfExists: true
@@ -1167,7 +1166,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
1167
1166
  if (!await exists(envNeatFile)) {
1168
1167
  generatedFiles.push({
1169
1168
  file: envNeatFile,
1170
- contents: renderEnvNeat(projectName),
1169
+ contents: renderEnvNeat(svcName, projectName),
1171
1170
  skipIfExists: true
1172
1171
  });
1173
1172
  }
@@ -1228,11 +1227,21 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1228
1227
  if (!await exists(envNeatFile)) {
1229
1228
  generatedFiles.push({
1230
1229
  file: envNeatFile,
1231
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1230
+ contents: renderEnvNeat(
1231
+ serviceNodeName(pkg, serviceDir),
1232
+ projectToken(pkg, serviceDir, project)
1233
+ ),
1232
1234
  skipIfExists: true
1233
1235
  });
1234
1236
  }
1235
1237
  }
1238
+ function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
1239
+ return renderFrameworkOtelInit(
1240
+ template,
1241
+ serviceNodeName(pkg, serviceDir),
1242
+ projectToken(pkg, serviceDir, project)
1243
+ );
1244
+ }
1236
1245
  function fileImportsOtelHook(raw, specifiers) {
1237
1246
  const lines = raw.split(/\r?\n/);
1238
1247
  for (const line of lines) {
@@ -1258,7 +1267,12 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1258
1267
  if (!await exists(otelServerFile)) {
1259
1268
  generatedFiles.push({
1260
1269
  file: otelServerFile,
1261
- 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
+ ),
1262
1276
  skipIfExists: true
1263
1277
  });
1264
1278
  }
@@ -1312,7 +1326,12 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
1312
1326
  if (!await exists(otelInitFile)) {
1313
1327
  generatedFiles.push({
1314
1328
  file: otelInitFile,
1315
- 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
+ ),
1316
1335
  skipIfExists: true
1317
1336
  });
1318
1337
  }
@@ -1375,7 +1394,12 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1375
1394
  if (!await exists(otelInitFile)) {
1376
1395
  generatedFiles.push({
1377
1396
  file: otelInitFile,
1378
- 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
+ ),
1379
1403
  skipIfExists: true
1380
1404
  });
1381
1405
  }
@@ -1431,7 +1455,12 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
1431
1455
  if (!await exists(otelInitFile)) {
1432
1456
  generatedFiles.push({
1433
1457
  file: otelInitFile,
1434
- 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
+ ),
1435
1464
  skipIfExists: true
1436
1465
  });
1437
1466
  }
@@ -1523,6 +1552,10 @@ async function plan(serviceDir, opts) {
1523
1552
  return planAstro(serviceDir, pkg, manifestPath, project);
1524
1553
  }
1525
1554
  }
1555
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
1556
+ if (runtimeKind !== "node") {
1557
+ return { ...empty, runtimeKind };
1558
+ }
1526
1559
  const entryFile = await resolveEntry(serviceDir, pkg);
1527
1560
  if (!entryFile) {
1528
1561
  return { ...empty, libOnly: true };
@@ -1557,18 +1590,20 @@ async function plan(serviceDir, opts) {
1557
1590
  } catch {
1558
1591
  return { ...empty, libOnly: true };
1559
1592
  }
1593
+ const svcName = serviceNodeName(pkg, serviceDir);
1594
+ const projectName = projectToken(pkg, serviceDir, project);
1560
1595
  const generatedFiles = [];
1561
1596
  if (!await exists(otelInitFile)) {
1562
1597
  generatedFiles.push({
1563
1598
  file: otelInitFile,
1564
- contents: otelInitContents(flavor),
1599
+ contents: renderNodeOtelInit(otelInitContents(flavor), svcName, projectName),
1565
1600
  skipIfExists: true
1566
1601
  });
1567
1602
  }
1568
1603
  if (!await exists(envNeatFile)) {
1569
1604
  generatedFiles.push({
1570
1605
  file: envNeatFile,
1571
- contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1606
+ contents: renderEnvNeat(svcName, projectName),
1572
1607
  skipIfExists: true
1573
1608
  });
1574
1609
  }
@@ -1625,6 +1660,22 @@ async function apply(installPlan) {
1625
1660
  writtenFiles: []
1626
1661
  };
1627
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
+ }
1628
1679
  if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
1629
1680
  return {
1630
1681
  serviceDir,
@@ -2108,11 +2159,13 @@ async function applyInstallersOver(services, project) {
2108
2159
  let instrumented = 0;
2109
2160
  let already = 0;
2110
2161
  let libOnly = 0;
2162
+ let browserBundle = 0;
2163
+ let reactNative = 0;
2111
2164
  for (const svc of services) {
2112
2165
  const installer = await pickInstaller(svc.dir);
2113
2166
  if (!installer) continue;
2114
2167
  const plan3 = await installer.plan(svc.dir, { project });
2115
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
2168
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) {
2116
2169
  already++;
2117
2170
  continue;
2118
2171
  }
@@ -2120,8 +2173,15 @@ async function applyInstallersOver(services, project) {
2120
2173
  if (outcome.outcome === "instrumented") instrumented++;
2121
2174
  else if (outcome.outcome === "already-instrumented") already++;
2122
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
+ }
2123
2183
  }
2124
- return { instrumented, alreadyInstrumented: already, libOnly };
2184
+ return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
2125
2185
  }
2126
2186
  async function promptYesNo(question) {
2127
2187
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2325,13 +2385,15 @@ async function runOrchestrator(opts) {
2325
2385
  if (gi.action !== "unchanged") {
2326
2386
  console.log(`${gi.action} .gitignore (neat-out/)`);
2327
2387
  }
2388
+ let currentProjectName = opts.project;
2328
2389
  try {
2329
- await addProject({
2390
+ const entry2 = await addProject({
2330
2391
  name: opts.project,
2331
2392
  path: opts.scanPath,
2332
2393
  languages,
2333
2394
  status: "active"
2334
2395
  });
2396
+ currentProjectName = entry2.name;
2335
2397
  } catch (err) {
2336
2398
  if (!(err instanceof ProjectNameCollisionError)) throw err;
2337
2399
  console.error(`neat: ${err.message}`);
@@ -2339,6 +2401,20 @@ async function runOrchestrator(opts) {
2339
2401
  result.exitCode = 1;
2340
2402
  return result;
2341
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
+ }
2342
2418
  if (!runApply) {
2343
2419
  result.steps.apply.skipped = true;
2344
2420
  console.log("skipped instrumentation (--no-instrument)");
@@ -3009,7 +3085,7 @@ async function runSync(opts) {
3009
3085
  snapshotPath = target;
3010
3086
  }
3011
3087
  const skipApply = opts.dryRun || opts.noInstrument;
3012
- 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);
3013
3089
  const warnings = [];
3014
3090
  let daemonState = "skipped";
3015
3091
  let exitCode = 0;
@@ -3357,7 +3433,7 @@ async function buildPatchSections(services, project) {
3357
3433
  const installer = await pickInstaller(svc.dir);
3358
3434
  if (!installer) continue;
3359
3435
  const plan3 = await installer.plan(svc.dir, { project });
3360
- if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
3436
+ if (isEmptyPlan(plan3) && !plan3.libOnly && plan3.runtimeKind === void 0) continue;
3361
3437
  sections.push({ installer: installer.name, plan: plan3 });
3362
3438
  }
3363
3439
  return sections;
@@ -3401,13 +3477,15 @@ async function runInit(opts) {
3401
3477
  written.push(gitignoreResult.file);
3402
3478
  }
3403
3479
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
3480
+ let currentProjectName = opts.project;
3404
3481
  try {
3405
- await addProject({
3482
+ const entry2 = await addProject({
3406
3483
  name: opts.project,
3407
3484
  path: opts.scanPath,
3408
3485
  languages,
3409
3486
  status: "active"
3410
3487
  });
3488
+ currentProjectName = entry2.name;
3411
3489
  } catch (err) {
3412
3490
  if (err instanceof ProjectNameCollisionError) {
3413
3491
  console.error(`neat init: ${err.message}`);
@@ -3416,11 +3494,27 @@ async function runInit(opts) {
3416
3494
  }
3417
3495
  throw err;
3418
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
+ }
3419
3511
  if (!opts.noInstall) {
3420
3512
  if (opts.apply) {
3421
3513
  let instrumented = 0;
3422
3514
  let alreadyInstrumented = 0;
3423
3515
  let libOnly = 0;
3516
+ let browserBundle = 0;
3517
+ let reactNative = 0;
3424
3518
  for (const section of sections) {
3425
3519
  const installer = INSTALLERS.find((i) => i.name === section.installer);
3426
3520
  if (!installer) continue;
@@ -3432,13 +3526,24 @@ async function runInit(opts) {
3432
3526
  alreadyInstrumented++;
3433
3527
  } else if (outcome.outcome === "lib-only") {
3434
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.`);
3435
3535
  }
3436
3536
  }
3437
3537
  if (sections.length > 0) {
3438
3538
  console.log("");
3439
- console.log(
3440
- `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
3441
- );
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(", ")}`);
3442
3547
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
3443
3548
  }
3444
3549
  } else {