@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/{chunk-D5PIJFBE.js → chunk-3QCRUEQD.js} +2 -2
- package/dist/{chunk-CB2UK4EH.js → chunk-6GXBAR3M.js} +44 -3
- package/dist/{chunk-CB2UK4EH.js.map → chunk-6GXBAR3M.js.map} +1 -1
- package/dist/{chunk-KYRIQIPG.js → chunk-HVF4S7J3.js} +101 -26
- package/dist/chunk-HVF4S7J3.js.map +1 -0
- package/dist/{chunk-NTQHMXWE.js → chunk-RBWL4HRB.js} +13 -2
- package/dist/chunk-RBWL4HRB.js.map +1 -0
- package/dist/cli.cjs +430 -124
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +323 -103
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +151 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +151 -24
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QTX2YQJZ.js → otel-grpc-VSPMP3OS.js} +3 -3
- package/dist/server.cjs +110 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-KYRIQIPG.js.map +0 -1
- package/dist/chunk-NTQHMXWE.js.map +0 -1
- /package/dist/{chunk-D5PIJFBE.js.map → chunk-3QCRUEQD.js.map} +0 -0
- /package/dist/{otel-grpc-QTX2YQJZ.js.map → otel-grpc-VSPMP3OS.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -41,15 +41,15 @@ import {
|
|
|
41
41
|
setStatus,
|
|
42
42
|
startPersistLoop,
|
|
43
43
|
startStalenessLoop
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-RBWL4HRB.js";
|
|
45
45
|
import {
|
|
46
46
|
startOtelGrpcReceiver
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-3QCRUEQD.js";
|
|
48
48
|
import {
|
|
49
49
|
__dirname,
|
|
50
50
|
__require,
|
|
51
51
|
buildOtelReceiver
|
|
52
|
-
} from "./chunk-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
715
|
-
|
|
711
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
712
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
716
713
|
|
|
717
|
-
|
|
714
|
+
const instrumentations = [getNodeAutoInstrumentations()]
|
|
715
|
+
__INSTRUMENTATION_BLOCK__
|
|
716
|
+
new NodeSDK({ instrumentations }).start()
|
|
718
717
|
`;
|
|
719
718
|
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
727
|
-
|
|
722
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
723
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
728
724
|
|
|
729
|
-
|
|
725
|
+
const instrumentations = [getNodeAutoInstrumentations()]
|
|
726
|
+
__INSTRUMENTATION_BLOCK__
|
|
727
|
+
new NodeSDK({ instrumentations }).start()
|
|
730
728
|
`;
|
|
731
|
-
function
|
|
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=${
|
|
735
|
-
|
|
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 ||= '
|
|
756
|
-
process.env.
|
|
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
|
-
|
|
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 ||= '
|
|
765
|
-
process.env.
|
|
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
|
-
|
|
776
|
+
const instrumentations = [getNodeAutoInstrumentations()]
|
|
777
|
+
__INSTRUMENTATION_BLOCK__
|
|
778
|
+
new NodeSDK({ instrumentations }).start()
|
|
771
779
|
`;
|
|
772
|
-
function renderNextInstrumentationNode(template, projectName) {
|
|
773
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
3440
|
-
`
|
|
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 {
|