@neat.is/core 0.3.8 → 0.4.0
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-7TYESDAI.js → chunk-4V23KYOP.js} +33 -4
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-IXIFJKMM.js} +2 -2
- package/dist/{chunk-LQ3JFBTX.js → chunk-N6RPINEJ.js} +5 -4
- package/dist/{chunk-LQ3JFBTX.js.map → chunk-N6RPINEJ.js.map} +1 -1
- package/dist/{chunk-CZ3T6TE2.js → chunk-UPW4CMOH.js} +120 -28
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +916 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +765 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +153 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +153 -31
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +282 -160
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-IXIFJKMM.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
loadPolicyFile,
|
|
32
32
|
makeErrorSpanWriter,
|
|
33
33
|
makeSpanHandler,
|
|
34
|
+
normalizeProjectPath,
|
|
34
35
|
pathsForProject,
|
|
35
36
|
promoteFrontierNodes,
|
|
36
37
|
removeProject,
|
|
@@ -40,17 +41,17 @@ import {
|
|
|
40
41
|
setStatus,
|
|
41
42
|
startPersistLoop,
|
|
42
43
|
startStalenessLoop
|
|
43
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-UPW4CMOH.js";
|
|
44
45
|
import {
|
|
45
46
|
startOtelGrpcReceiver
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-IXIFJKMM.js";
|
|
47
48
|
import {
|
|
48
49
|
__require,
|
|
49
50
|
buildOtelReceiver
|
|
50
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-4V23KYOP.js";
|
|
51
52
|
|
|
52
53
|
// src/cli.ts
|
|
53
|
-
import
|
|
54
|
+
import path8 from "path";
|
|
54
55
|
import { promises as fs7 } from "fs";
|
|
55
56
|
|
|
56
57
|
// src/gitignore.ts
|
|
@@ -771,6 +772,100 @@ const sdk = new NodeSDK({
|
|
|
771
772
|
})
|
|
772
773
|
sdk.start()
|
|
773
774
|
`;
|
|
775
|
+
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
776
|
+
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'
|
|
782
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
783
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
784
|
+
|
|
785
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
786
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
787
|
+
|
|
788
|
+
const sdk = new NodeSDK({
|
|
789
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
790
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
791
|
+
})
|
|
792
|
+
sdk.start()
|
|
793
|
+
`;
|
|
794
|
+
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
|
+
}
|
|
803
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
804
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
805
|
+
|
|
806
|
+
const sdk = new NodeSDK({
|
|
807
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
808
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
809
|
+
})
|
|
810
|
+
sdk.start()
|
|
811
|
+
`;
|
|
812
|
+
var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
813
|
+
var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
814
|
+
var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
815
|
+
var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
816
|
+
var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
817
|
+
import './otel-init'
|
|
818
|
+
|
|
819
|
+
import type { Handle } from '@sveltejs/kit'
|
|
820
|
+
|
|
821
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
822
|
+
return resolve(event)
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
826
|
+
import './otel-init'
|
|
827
|
+
|
|
828
|
+
/** @type {import('@sveltejs/kit').Handle} */
|
|
829
|
+
export const handle = async ({ event, resolve }) => {
|
|
830
|
+
return resolve(event)
|
|
831
|
+
}
|
|
832
|
+
`;
|
|
833
|
+
var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
834
|
+
var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
835
|
+
var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
836
|
+
import './otel-init'
|
|
837
|
+
|
|
838
|
+
export default defineNitroPlugin(() => {
|
|
839
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
840
|
+
})
|
|
841
|
+
`;
|
|
842
|
+
var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
843
|
+
require('./otel-init')
|
|
844
|
+
|
|
845
|
+
module.exports = defineNitroPlugin(() => {
|
|
846
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
847
|
+
})
|
|
848
|
+
`;
|
|
849
|
+
var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
850
|
+
var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
851
|
+
var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
852
|
+
import './otel-init'
|
|
853
|
+
|
|
854
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
855
|
+
|
|
856
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
857
|
+
return next()
|
|
858
|
+
})
|
|
859
|
+
`;
|
|
860
|
+
var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
861
|
+
import './otel-init'
|
|
862
|
+
|
|
863
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
864
|
+
|
|
865
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
866
|
+
return next()
|
|
867
|
+
})
|
|
868
|
+
`;
|
|
774
869
|
|
|
775
870
|
// src/installers/javascript.ts
|
|
776
871
|
var SDK_PACKAGES = [
|
|
@@ -822,6 +917,71 @@ async function findNextConfig(serviceDir) {
|
|
|
822
917
|
function hasNextDependency(pkg) {
|
|
823
918
|
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
824
919
|
}
|
|
920
|
+
function allDeps(pkg) {
|
|
921
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
922
|
+
}
|
|
923
|
+
var REMIX_ENTRY_CANDIDATES = [
|
|
924
|
+
"app/entry.server.ts",
|
|
925
|
+
"app/entry.server.tsx",
|
|
926
|
+
"app/entry.server.js",
|
|
927
|
+
"app/entry.server.jsx"
|
|
928
|
+
];
|
|
929
|
+
function hasRemixDependency(pkg) {
|
|
930
|
+
const deps = allDeps(pkg);
|
|
931
|
+
if ("remix" in deps) return true;
|
|
932
|
+
for (const name of Object.keys(deps)) {
|
|
933
|
+
if (name.startsWith("@remix-run/")) return true;
|
|
934
|
+
}
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
async function findRemixEntry(serviceDir) {
|
|
938
|
+
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
939
|
+
const candidate = path4.join(serviceDir, rel);
|
|
940
|
+
if (await exists(candidate)) return candidate;
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
|
|
945
|
+
var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
|
|
946
|
+
function hasSvelteKitDependency(pkg) {
|
|
947
|
+
return "@sveltejs/kit" in allDeps(pkg);
|
|
948
|
+
}
|
|
949
|
+
async function findSvelteKitHooks(serviceDir) {
|
|
950
|
+
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
951
|
+
const candidate = path4.join(serviceDir, rel);
|
|
952
|
+
if (await exists(candidate)) return candidate;
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
async function findSvelteKitConfig(serviceDir) {
|
|
957
|
+
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
958
|
+
const candidate = path4.join(serviceDir, rel);
|
|
959
|
+
if (await exists(candidate)) return candidate;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
964
|
+
function hasNuxtDependency(pkg) {
|
|
965
|
+
return "nuxt" in allDeps(pkg);
|
|
966
|
+
}
|
|
967
|
+
async function findNuxtConfig(serviceDir) {
|
|
968
|
+
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
969
|
+
const candidate = path4.join(serviceDir, name);
|
|
970
|
+
if (await exists(candidate)) return candidate;
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
|
|
975
|
+
function hasAstroDependency(pkg) {
|
|
976
|
+
return "astro" in allDeps(pkg);
|
|
977
|
+
}
|
|
978
|
+
async function findAstroConfig(serviceDir) {
|
|
979
|
+
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
980
|
+
const candidate = path4.join(serviceDir, name);
|
|
981
|
+
if (await exists(candidate)) return candidate;
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
825
985
|
function parseNextMajor(range) {
|
|
826
986
|
if (!range) return null;
|
|
827
987
|
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
@@ -1029,6 +1189,276 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
1029
1189
|
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
1030
1190
|
};
|
|
1031
1191
|
}
|
|
1192
|
+
function buildDependencyEdits(pkg, manifestPath) {
|
|
1193
|
+
const existingDeps = allDeps(pkg);
|
|
1194
|
+
const edits = [];
|
|
1195
|
+
for (const sdk of SDK_PACKAGES) {
|
|
1196
|
+
if (sdk.name in existingDeps) continue;
|
|
1197
|
+
edits.push({
|
|
1198
|
+
file: manifestPath,
|
|
1199
|
+
kind: "add",
|
|
1200
|
+
name: sdk.name,
|
|
1201
|
+
version: sdk.version
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
return edits;
|
|
1205
|
+
}
|
|
1206
|
+
async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
|
|
1207
|
+
const envNeatFile = path4.join(serviceDir, ".env.neat");
|
|
1208
|
+
if (!await exists(envNeatFile)) {
|
|
1209
|
+
generatedFiles.push({
|
|
1210
|
+
file: envNeatFile,
|
|
1211
|
+
contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
|
|
1212
|
+
skipIfExists: true
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function fileImportsOtelHook(raw, specifiers) {
|
|
1217
|
+
const lines = raw.split(/\r?\n/);
|
|
1218
|
+
for (const line of lines) {
|
|
1219
|
+
const trimmed = line.trim();
|
|
1220
|
+
for (const spec of specifiers) {
|
|
1221
|
+
const escaped = spec.replace(/\./g, "\\.");
|
|
1222
|
+
const pattern = new RegExp(
|
|
1223
|
+
`(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
|
|
1224
|
+
);
|
|
1225
|
+
if (pattern.test(trimmed)) return true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
|
|
1231
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1232
|
+
const otelServerFile = path4.join(
|
|
1233
|
+
serviceDir,
|
|
1234
|
+
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
1235
|
+
);
|
|
1236
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1237
|
+
const generatedFiles = [];
|
|
1238
|
+
if (!await exists(otelServerFile)) {
|
|
1239
|
+
generatedFiles.push({
|
|
1240
|
+
file: otelServerFile,
|
|
1241
|
+
contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
|
|
1242
|
+
skipIfExists: true
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1246
|
+
const entrypointEdits = [];
|
|
1247
|
+
try {
|
|
1248
|
+
const raw = await fs4.readFile(entryFile, "utf8");
|
|
1249
|
+
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
1250
|
+
const lines = raw.split(/\r?\n/);
|
|
1251
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1252
|
+
entrypointEdits.push({
|
|
1253
|
+
file: entryFile,
|
|
1254
|
+
before: firstReal,
|
|
1255
|
+
after: "import './otel.server'"
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
} catch {
|
|
1259
|
+
}
|
|
1260
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1261
|
+
if (empty) {
|
|
1262
|
+
return {
|
|
1263
|
+
language: "javascript",
|
|
1264
|
+
serviceDir,
|
|
1265
|
+
dependencyEdits: [],
|
|
1266
|
+
entrypointEdits: [],
|
|
1267
|
+
envEdits: [],
|
|
1268
|
+
generatedFiles: [],
|
|
1269
|
+
framework: "remix"
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
language: "javascript",
|
|
1274
|
+
serviceDir,
|
|
1275
|
+
dependencyEdits,
|
|
1276
|
+
entrypointEdits,
|
|
1277
|
+
envEdits: [OTEL_ENV],
|
|
1278
|
+
generatedFiles,
|
|
1279
|
+
framework: "remix"
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
|
|
1283
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1284
|
+
const otelInitFile = path4.join(
|
|
1285
|
+
serviceDir,
|
|
1286
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1287
|
+
);
|
|
1288
|
+
const resolvedHooksFile = hooksFile ?? path4.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
1289
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1290
|
+
const generatedFiles = [];
|
|
1291
|
+
const entrypointEdits = [];
|
|
1292
|
+
if (!await exists(otelInitFile)) {
|
|
1293
|
+
generatedFiles.push({
|
|
1294
|
+
file: otelInitFile,
|
|
1295
|
+
contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
|
|
1296
|
+
skipIfExists: true
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1300
|
+
if (hooksFile === null) {
|
|
1301
|
+
generatedFiles.push({
|
|
1302
|
+
file: resolvedHooksFile,
|
|
1303
|
+
contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
|
|
1304
|
+
skipIfExists: true
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
try {
|
|
1308
|
+
const raw = await fs4.readFile(hooksFile, "utf8");
|
|
1309
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1310
|
+
const lines = raw.split(/\r?\n/);
|
|
1311
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1312
|
+
entrypointEdits.push({
|
|
1313
|
+
file: hooksFile,
|
|
1314
|
+
before: firstReal,
|
|
1315
|
+
after: "import './otel-init'"
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1322
|
+
if (empty) {
|
|
1323
|
+
return {
|
|
1324
|
+
language: "javascript",
|
|
1325
|
+
serviceDir,
|
|
1326
|
+
dependencyEdits: [],
|
|
1327
|
+
entrypointEdits: [],
|
|
1328
|
+
envEdits: [],
|
|
1329
|
+
generatedFiles: [],
|
|
1330
|
+
framework: "sveltekit"
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
language: "javascript",
|
|
1335
|
+
serviceDir,
|
|
1336
|
+
dependencyEdits,
|
|
1337
|
+
entrypointEdits,
|
|
1338
|
+
envEdits: [OTEL_ENV],
|
|
1339
|
+
generatedFiles,
|
|
1340
|
+
framework: "sveltekit"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async function planNuxt(serviceDir, pkg, manifestPath) {
|
|
1344
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1345
|
+
const otelPluginFile = path4.join(
|
|
1346
|
+
serviceDir,
|
|
1347
|
+
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
1348
|
+
);
|
|
1349
|
+
const otelInitFile = path4.join(
|
|
1350
|
+
serviceDir,
|
|
1351
|
+
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
1352
|
+
);
|
|
1353
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1354
|
+
const generatedFiles = [];
|
|
1355
|
+
if (!await exists(otelInitFile)) {
|
|
1356
|
+
generatedFiles.push({
|
|
1357
|
+
file: otelInitFile,
|
|
1358
|
+
contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
|
|
1359
|
+
skipIfExists: true
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
if (!await exists(otelPluginFile)) {
|
|
1363
|
+
generatedFiles.push({
|
|
1364
|
+
file: otelPluginFile,
|
|
1365
|
+
contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
|
|
1366
|
+
skipIfExists: true
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1370
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
|
|
1371
|
+
if (empty) {
|
|
1372
|
+
return {
|
|
1373
|
+
language: "javascript",
|
|
1374
|
+
serviceDir,
|
|
1375
|
+
dependencyEdits: [],
|
|
1376
|
+
entrypointEdits: [],
|
|
1377
|
+
envEdits: [],
|
|
1378
|
+
generatedFiles: [],
|
|
1379
|
+
framework: "nuxt"
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return {
|
|
1383
|
+
language: "javascript",
|
|
1384
|
+
serviceDir,
|
|
1385
|
+
dependencyEdits,
|
|
1386
|
+
entrypointEdits: [],
|
|
1387
|
+
envEdits: [OTEL_ENV],
|
|
1388
|
+
generatedFiles,
|
|
1389
|
+
framework: "nuxt"
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
1393
|
+
async function findAstroMiddleware(serviceDir) {
|
|
1394
|
+
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
1395
|
+
const candidate = path4.join(serviceDir, rel);
|
|
1396
|
+
if (await exists(candidate)) return candidate;
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
async function planAstro(serviceDir, pkg, manifestPath) {
|
|
1401
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1402
|
+
const otelInitFile = path4.join(
|
|
1403
|
+
serviceDir,
|
|
1404
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1405
|
+
);
|
|
1406
|
+
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
1407
|
+
const middlewareFile = existingMiddleware ?? path4.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
1408
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1409
|
+
const generatedFiles = [];
|
|
1410
|
+
const entrypointEdits = [];
|
|
1411
|
+
if (!await exists(otelInitFile)) {
|
|
1412
|
+
generatedFiles.push({
|
|
1413
|
+
file: otelInitFile,
|
|
1414
|
+
contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
|
|
1415
|
+
skipIfExists: true
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1419
|
+
if (existingMiddleware === null) {
|
|
1420
|
+
generatedFiles.push({
|
|
1421
|
+
file: middlewareFile,
|
|
1422
|
+
contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
|
|
1423
|
+
skipIfExists: true
|
|
1424
|
+
});
|
|
1425
|
+
} else {
|
|
1426
|
+
try {
|
|
1427
|
+
const raw = await fs4.readFile(existingMiddleware, "utf8");
|
|
1428
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1429
|
+
const lines = raw.split(/\r?\n/);
|
|
1430
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1431
|
+
entrypointEdits.push({
|
|
1432
|
+
file: existingMiddleware,
|
|
1433
|
+
before: firstReal,
|
|
1434
|
+
after: "import './otel-init'"
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1441
|
+
if (empty) {
|
|
1442
|
+
return {
|
|
1443
|
+
language: "javascript",
|
|
1444
|
+
serviceDir,
|
|
1445
|
+
dependencyEdits: [],
|
|
1446
|
+
entrypointEdits: [],
|
|
1447
|
+
envEdits: [],
|
|
1448
|
+
generatedFiles: [],
|
|
1449
|
+
framework: "astro"
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
language: "javascript",
|
|
1454
|
+
serviceDir,
|
|
1455
|
+
dependencyEdits,
|
|
1456
|
+
entrypointEdits,
|
|
1457
|
+
envEdits: [OTEL_ENV],
|
|
1458
|
+
generatedFiles,
|
|
1459
|
+
framework: "astro"
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1032
1462
|
async function plan(serviceDir) {
|
|
1033
1463
|
const pkg = await readPackageJson(serviceDir);
|
|
1034
1464
|
const manifestPath = path4.join(serviceDir, "package.json");
|
|
@@ -1047,6 +1477,31 @@ async function plan(serviceDir) {
|
|
|
1047
1477
|
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
1048
1478
|
}
|
|
1049
1479
|
}
|
|
1480
|
+
if (hasRemixDependency(pkg)) {
|
|
1481
|
+
const remixEntry = await findRemixEntry(serviceDir);
|
|
1482
|
+
if (remixEntry) {
|
|
1483
|
+
return planRemix(serviceDir, pkg, manifestPath, remixEntry);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (hasSvelteKitDependency(pkg)) {
|
|
1487
|
+
const hooks = await findSvelteKitHooks(serviceDir);
|
|
1488
|
+
const config = await findSvelteKitConfig(serviceDir);
|
|
1489
|
+
if (hooks || config) {
|
|
1490
|
+
return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (hasNuxtDependency(pkg)) {
|
|
1494
|
+
const nuxtConfig = await findNuxtConfig(serviceDir);
|
|
1495
|
+
if (nuxtConfig) {
|
|
1496
|
+
return planNuxt(serviceDir, pkg, manifestPath);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (hasAstroDependency(pkg)) {
|
|
1500
|
+
const astroConfig = await findAstroConfig(serviceDir);
|
|
1501
|
+
if (astroConfig) {
|
|
1502
|
+
return planAstro(serviceDir, pkg, manifestPath);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1050
1505
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
1051
1506
|
if (!entryFile) {
|
|
1052
1507
|
return { ...empty, libOnly: true };
|
|
@@ -1119,9 +1574,18 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
1119
1574
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1120
1575
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1121
1576
|
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
1577
|
+
const relPosix = rel.split(path4.sep).join("/");
|
|
1578
|
+
if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
|
|
1579
|
+
if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
|
|
1580
|
+
if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
|
|
1581
|
+
if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
|
|
1582
|
+
if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
|
|
1583
|
+
if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
|
|
1584
|
+
if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
|
|
1122
1585
|
return false;
|
|
1123
1586
|
}
|
|
1124
1587
|
async function writeAtomic(file, contents) {
|
|
1588
|
+
await fs4.mkdir(path4.dirname(file), { recursive: true });
|
|
1125
1589
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1126
1590
|
await fs4.writeFile(tmp, contents, "utf8");
|
|
1127
1591
|
await fs4.rename(tmp, file);
|
|
@@ -1591,6 +2055,49 @@ import http from "http";
|
|
|
1591
2055
|
import path6 from "path";
|
|
1592
2056
|
import { spawn as spawn2 } from "child_process";
|
|
1593
2057
|
import readline from "readline";
|
|
2058
|
+
async function extractAndPersist(opts) {
|
|
2059
|
+
const services = await discoverServices(opts.scanPath);
|
|
2060
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
2061
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
2062
|
+
resetGraph(graphKey);
|
|
2063
|
+
const graph = getGraph(graphKey);
|
|
2064
|
+
const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
|
|
2065
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
2066
|
+
errorsPath: projectPaths.errorsPath
|
|
2067
|
+
});
|
|
2068
|
+
if (!opts.dryRun) {
|
|
2069
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
graph,
|
|
2073
|
+
graphKey,
|
|
2074
|
+
services,
|
|
2075
|
+
languages,
|
|
2076
|
+
nodesAdded: extraction.nodesAdded,
|
|
2077
|
+
edgesAdded: extraction.edgesAdded,
|
|
2078
|
+
snapshotPath: projectPaths.snapshotPath,
|
|
2079
|
+
errorsPath: projectPaths.errorsPath
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
async function applyInstallersOver(services) {
|
|
2083
|
+
let instrumented = 0;
|
|
2084
|
+
let already = 0;
|
|
2085
|
+
let libOnly = 0;
|
|
2086
|
+
for (const svc of services) {
|
|
2087
|
+
const installer = await pickInstaller(svc.dir);
|
|
2088
|
+
if (!installer) continue;
|
|
2089
|
+
const plan3 = await installer.plan(svc.dir);
|
|
2090
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
2091
|
+
already++;
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const outcome = await installer.apply(plan3);
|
|
2095
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
2096
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
2097
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
2098
|
+
}
|
|
2099
|
+
return { instrumented, alreadyInstrumented: already, libOnly };
|
|
2100
|
+
}
|
|
1594
2101
|
async function promptYesNo(question) {
|
|
1595
2102
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1596
2103
|
return new Promise((resolve) => {
|
|
@@ -1685,24 +2192,21 @@ async function runOrchestrator(opts) {
|
|
|
1685
2192
|
}
|
|
1686
2193
|
console.log(`neat: ${opts.scanPath}`);
|
|
1687
2194
|
console.log("");
|
|
1688
|
-
const
|
|
1689
|
-
|
|
2195
|
+
const persisted = await extractAndPersist({
|
|
2196
|
+
scanPath: opts.scanPath,
|
|
2197
|
+
project: opts.project,
|
|
2198
|
+
projectExplicit: opts.projectExplicit
|
|
2199
|
+
});
|
|
2200
|
+
const { graph, services, languages } = persisted;
|
|
1690
2201
|
result.steps.discovery = { services: services.length, languages };
|
|
1691
2202
|
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
1692
2203
|
let runApply = !opts.noInstrument;
|
|
1693
2204
|
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
1694
2205
|
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
1695
2206
|
}
|
|
1696
|
-
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
1697
|
-
resetGraph(graphKey);
|
|
1698
|
-
const graph = getGraph(graphKey);
|
|
1699
|
-
const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
|
|
1700
|
-
const errorsPath = projectPaths.errorsPath;
|
|
1701
|
-
const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
1702
|
-
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
1703
2207
|
result.steps.extraction = {
|
|
1704
|
-
nodesAdded:
|
|
1705
|
-
edgesAdded:
|
|
2208
|
+
nodesAdded: persisted.nodesAdded,
|
|
2209
|
+
edgesAdded: persisted.edgesAdded
|
|
1706
2210
|
};
|
|
1707
2211
|
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
1708
2212
|
result.steps.gitignore = gi.action;
|
|
@@ -1727,24 +2231,11 @@ async function runOrchestrator(opts) {
|
|
|
1727
2231
|
result.steps.apply.skipped = true;
|
|
1728
2232
|
console.log("skipped instrumentation (--no-instrument)");
|
|
1729
2233
|
} else {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (!installer) continue;
|
|
1736
|
-
const plan3 = await installer.plan(svc.dir);
|
|
1737
|
-
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
1738
|
-
already++;
|
|
1739
|
-
continue;
|
|
1740
|
-
}
|
|
1741
|
-
const outcome = await installer.apply(plan3);
|
|
1742
|
-
if (outcome.outcome === "instrumented") instrumented++;
|
|
1743
|
-
else if (outcome.outcome === "already-instrumented") already++;
|
|
1744
|
-
else if (outcome.outcome === "lib-only") libOnly++;
|
|
1745
|
-
}
|
|
1746
|
-
result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
|
|
1747
|
-
console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
|
|
2234
|
+
const tally = await applyInstallersOver(services);
|
|
2235
|
+
result.steps.apply = { ...tally, skipped: false };
|
|
2236
|
+
console.log(
|
|
2237
|
+
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
2238
|
+
);
|
|
1748
2239
|
}
|
|
1749
2240
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
1750
2241
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -1793,8 +2284,8 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
1793
2284
|
console.log(`dashboard: ${dashboardUrl}`);
|
|
1794
2285
|
}
|
|
1795
2286
|
|
|
1796
|
-
// src/cli.ts
|
|
1797
|
-
import
|
|
2287
|
+
// src/cli-verbs.ts
|
|
2288
|
+
import path7 from "path";
|
|
1798
2289
|
|
|
1799
2290
|
// src/cli-client.ts
|
|
1800
2291
|
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
@@ -1814,13 +2305,16 @@ var TransportError = class extends Error {
|
|
|
1814
2305
|
this.name = "TransportError";
|
|
1815
2306
|
}
|
|
1816
2307
|
};
|
|
1817
|
-
function createHttpClient(baseUrl) {
|
|
2308
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
1818
2309
|
const root = baseUrl.replace(/\/$/, "");
|
|
2310
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
1819
2311
|
return {
|
|
1820
|
-
async get(
|
|
2312
|
+
async get(path9) {
|
|
1821
2313
|
let res;
|
|
1822
2314
|
try {
|
|
1823
|
-
res = await fetch(`${root}${
|
|
2315
|
+
res = await fetch(`${root}${path9}`, {
|
|
2316
|
+
headers: { ...authHeader }
|
|
2317
|
+
});
|
|
1824
2318
|
} catch (err) {
|
|
1825
2319
|
throw new TransportError(
|
|
1826
2320
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -1830,18 +2324,18 @@ function createHttpClient(baseUrl) {
|
|
|
1830
2324
|
const body = await res.text().catch(() => "");
|
|
1831
2325
|
throw new HttpError(
|
|
1832
2326
|
res.status,
|
|
1833
|
-
`${res.status} ${res.statusText} on GET ${
|
|
2327
|
+
`${res.status} ${res.statusText} on GET ${path9}: ${body}`,
|
|
1834
2328
|
body
|
|
1835
2329
|
);
|
|
1836
2330
|
}
|
|
1837
2331
|
return await res.json();
|
|
1838
2332
|
},
|
|
1839
|
-
async post(
|
|
2333
|
+
async post(path9, body) {
|
|
1840
2334
|
let res;
|
|
1841
2335
|
try {
|
|
1842
|
-
res = await fetch(`${root}${
|
|
2336
|
+
res = await fetch(`${root}${path9}`, {
|
|
1843
2337
|
method: "POST",
|
|
1844
|
-
headers: { "content-type": "application/json" },
|
|
2338
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
1845
2339
|
body: JSON.stringify(body)
|
|
1846
2340
|
});
|
|
1847
2341
|
} catch (err) {
|
|
@@ -1853,7 +2347,7 @@ function createHttpClient(baseUrl) {
|
|
|
1853
2347
|
const text = await res.text().catch(() => "");
|
|
1854
2348
|
throw new HttpError(
|
|
1855
2349
|
res.status,
|
|
1856
|
-
`${res.status} ${res.statusText} on POST ${
|
|
2350
|
+
`${res.status} ${res.statusText} on POST ${path9}: ${text}`,
|
|
1857
2351
|
text
|
|
1858
2352
|
);
|
|
1859
2353
|
}
|
|
@@ -1867,12 +2361,12 @@ function projectPath(project, suffix) {
|
|
|
1867
2361
|
}
|
|
1868
2362
|
async function runRootCause(client, input) {
|
|
1869
2363
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
1870
|
-
const
|
|
2364
|
+
const path9 = projectPath(
|
|
1871
2365
|
input.project,
|
|
1872
2366
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
1873
2367
|
);
|
|
1874
2368
|
try {
|
|
1875
|
-
const result = await client.get(
|
|
2369
|
+
const result = await client.get(path9);
|
|
1876
2370
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
1877
2371
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
1878
2372
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -1898,12 +2392,12 @@ async function runRootCause(client, input) {
|
|
|
1898
2392
|
}
|
|
1899
2393
|
async function runBlastRadius(client, input) {
|
|
1900
2394
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
1901
|
-
const
|
|
2395
|
+
const path9 = projectPath(
|
|
1902
2396
|
input.project,
|
|
1903
2397
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
1904
2398
|
);
|
|
1905
2399
|
try {
|
|
1906
|
-
const result = await client.get(
|
|
2400
|
+
const result = await client.get(path9);
|
|
1907
2401
|
if (result.totalAffected === 0) {
|
|
1908
2402
|
return {
|
|
1909
2403
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -1937,12 +2431,12 @@ function formatBlastEntry(n) {
|
|
|
1937
2431
|
}
|
|
1938
2432
|
async function runDependencies(client, input) {
|
|
1939
2433
|
const depth = input.depth ?? 3;
|
|
1940
|
-
const
|
|
2434
|
+
const path9 = projectPath(
|
|
1941
2435
|
input.project,
|
|
1942
2436
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
1943
2437
|
);
|
|
1944
2438
|
try {
|
|
1945
|
-
const result = await client.get(
|
|
2439
|
+
const result = await client.get(path9);
|
|
1946
2440
|
if (result.total === 0) {
|
|
1947
2441
|
return {
|
|
1948
2442
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -2023,9 +2517,9 @@ function formatDuration(ms) {
|
|
|
2023
2517
|
return `${Math.round(h / 24)}d`;
|
|
2024
2518
|
}
|
|
2025
2519
|
async function runIncidents(client, input) {
|
|
2026
|
-
const
|
|
2520
|
+
const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
2027
2521
|
try {
|
|
2028
|
-
const body = await client.get(
|
|
2522
|
+
const body = await client.get(path9);
|
|
2029
2523
|
const events = body.events;
|
|
2030
2524
|
if (events.length === 0) {
|
|
2031
2525
|
return {
|
|
@@ -2291,8 +2785,181 @@ function exitCodeForError(err) {
|
|
|
2291
2785
|
if (err instanceof HttpError) return 1;
|
|
2292
2786
|
return 1;
|
|
2293
2787
|
}
|
|
2788
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
2789
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
2790
|
+
}
|
|
2791
|
+
async function pushSnapshotToRemote(input) {
|
|
2792
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
2793
|
+
if (typeof client.post !== "function") {
|
|
2794
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
2795
|
+
}
|
|
2796
|
+
return client.post(
|
|
2797
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
2798
|
+
{ snapshot: input.snapshot }
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/cli-verbs.ts
|
|
2803
|
+
async function resolveProjectEntry(opts) {
|
|
2804
|
+
const entries = await listProjects();
|
|
2805
|
+
if (opts.project) {
|
|
2806
|
+
const match = entries.find((e) => e.name === opts.project);
|
|
2807
|
+
return match ?? null;
|
|
2808
|
+
}
|
|
2809
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2810
|
+
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
2811
|
+
for (const entry2 of entries) {
|
|
2812
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
|
|
2813
|
+
return entry2;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
async function checkDaemonHealth2(baseUrl) {
|
|
2819
|
+
try {
|
|
2820
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
2821
|
+
signal: AbortSignal.timeout(1500)
|
|
2822
|
+
});
|
|
2823
|
+
return res.ok;
|
|
2824
|
+
} catch {
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function snapshotForGraph(persisted) {
|
|
2829
|
+
return {
|
|
2830
|
+
schemaVersion: 3,
|
|
2831
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2832
|
+
graph: persisted.graph.export()
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
function emitResult(result, json) {
|
|
2836
|
+
if (json) {
|
|
2837
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
|
|
2841
|
+
console.log(
|
|
2842
|
+
`${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
|
|
2843
|
+
);
|
|
2844
|
+
if (!result.apply.skipped) {
|
|
2845
|
+
const { instrumented, alreadyInstrumented, libOnly } = result.apply;
|
|
2846
|
+
console.log(
|
|
2847
|
+
`instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
2848
|
+
);
|
|
2849
|
+
} else {
|
|
2850
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
2851
|
+
}
|
|
2852
|
+
for (const warn of result.warnings) console.error(warn);
|
|
2853
|
+
}
|
|
2854
|
+
async function runSync(opts) {
|
|
2855
|
+
const entry2 = await resolveProjectEntry(opts);
|
|
2856
|
+
if (!entry2) {
|
|
2857
|
+
const target = opts.project ?? opts.cwd ?? process.cwd();
|
|
2858
|
+
console.error(
|
|
2859
|
+
`neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
|
|
2860
|
+
);
|
|
2861
|
+
return {
|
|
2862
|
+
exitCode: 1,
|
|
2863
|
+
project: opts.project ?? "",
|
|
2864
|
+
scanPath: target,
|
|
2865
|
+
nodesAdded: 0,
|
|
2866
|
+
edgesAdded: 0,
|
|
2867
|
+
snapshotPath: null,
|
|
2868
|
+
mode: opts.to ? "remote" : "local",
|
|
2869
|
+
daemon: "skipped",
|
|
2870
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
|
|
2871
|
+
warnings: []
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
const persisted = await extractAndPersist({
|
|
2875
|
+
scanPath: entry2.path,
|
|
2876
|
+
project: entry2.name,
|
|
2877
|
+
projectExplicit: true,
|
|
2878
|
+
dryRun: true
|
|
2879
|
+
});
|
|
2880
|
+
let snapshotPath = null;
|
|
2881
|
+
if (!opts.dryRun) {
|
|
2882
|
+
const target = persisted.snapshotPath;
|
|
2883
|
+
await saveGraphToDisk(persisted.graph, target);
|
|
2884
|
+
snapshotPath = target;
|
|
2885
|
+
}
|
|
2886
|
+
const skipApply = opts.dryRun || opts.noInstrument;
|
|
2887
|
+
const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
|
|
2888
|
+
const warnings = [];
|
|
2889
|
+
let daemonState = "skipped";
|
|
2890
|
+
let exitCode = 0;
|
|
2891
|
+
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
2892
|
+
if (!opts.dryRun) {
|
|
2893
|
+
const snapshot = snapshotForGraph(persisted);
|
|
2894
|
+
if (opts.to) {
|
|
2895
|
+
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
2896
|
+
try {
|
|
2897
|
+
await pushSnapshotToRemote({
|
|
2898
|
+
baseUrl: opts.to,
|
|
2899
|
+
token,
|
|
2900
|
+
project: entry2.name,
|
|
2901
|
+
snapshot
|
|
2902
|
+
});
|
|
2903
|
+
daemonState = "remote-ok";
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
if (err instanceof HttpError) {
|
|
2906
|
+
console.error(`neat sync: ${err.message}`);
|
|
2907
|
+
exitCode = 1;
|
|
2908
|
+
} else if (err instanceof TransportError) {
|
|
2909
|
+
console.error(`neat sync: ${err.message}`);
|
|
2910
|
+
exitCode = 3;
|
|
2911
|
+
} else {
|
|
2912
|
+
console.error(`neat sync: ${err.message}`);
|
|
2913
|
+
exitCode = 1;
|
|
2914
|
+
}
|
|
2915
|
+
daemonState = "skipped";
|
|
2916
|
+
}
|
|
2917
|
+
} else {
|
|
2918
|
+
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
2919
|
+
const healthy = await checkDaemonHealth2(daemonUrl);
|
|
2920
|
+
if (healthy) {
|
|
2921
|
+
try {
|
|
2922
|
+
await pushSnapshotToRemote({
|
|
2923
|
+
baseUrl: daemonUrl,
|
|
2924
|
+
token: process.env.NEAT_AUTH_TOKEN,
|
|
2925
|
+
project: entry2.name,
|
|
2926
|
+
snapshot
|
|
2927
|
+
});
|
|
2928
|
+
daemonState = "reloaded";
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
warnings.push(
|
|
2931
|
+
`neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
|
|
2932
|
+
);
|
|
2933
|
+
daemonState = "down";
|
|
2934
|
+
exitCode = 2;
|
|
2935
|
+
}
|
|
2936
|
+
} else {
|
|
2937
|
+
warnings.push(
|
|
2938
|
+
"neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
|
|
2939
|
+
);
|
|
2940
|
+
daemonState = "down";
|
|
2941
|
+
exitCode = 2;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const result = {
|
|
2946
|
+
exitCode,
|
|
2947
|
+
project: entry2.name,
|
|
2948
|
+
scanPath: entry2.path,
|
|
2949
|
+
nodesAdded: persisted.nodesAdded,
|
|
2950
|
+
edgesAdded: persisted.edgesAdded,
|
|
2951
|
+
snapshotPath,
|
|
2952
|
+
mode,
|
|
2953
|
+
daemon: daemonState,
|
|
2954
|
+
apply: { ...applyTally, skipped: skipApply },
|
|
2955
|
+
warnings
|
|
2956
|
+
};
|
|
2957
|
+
emitResult(result, opts.json);
|
|
2958
|
+
return result;
|
|
2959
|
+
}
|
|
2294
2960
|
|
|
2295
2961
|
// src/cli.ts
|
|
2962
|
+
import { DivergenceTypeSchema } from "@neat.is/types";
|
|
2296
2963
|
function usage() {
|
|
2297
2964
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
2298
2965
|
console.log("");
|
|
@@ -2320,6 +2987,15 @@ function usage() {
|
|
|
2320
2987
|
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
2321
2988
|
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
2322
2989
|
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
2990
|
+
console.log(" sync Re-run discovery, extraction, and SDK apply against the");
|
|
2991
|
+
console.log(" registered project, then notify the running daemon.");
|
|
2992
|
+
console.log(" Flags:");
|
|
2993
|
+
console.log(" --project <name> target a registered project by name");
|
|
2994
|
+
console.log(" --to <url> push the snapshot to a remote daemon");
|
|
2995
|
+
console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
|
|
2996
|
+
console.log(" --dry-run run extraction in-memory; do not write");
|
|
2997
|
+
console.log(" --no-instrument skip the SDK install apply step");
|
|
2998
|
+
console.log(" --json emit the delta summary as JSON");
|
|
2323
2999
|
console.log("");
|
|
2324
3000
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
2325
3001
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -2373,7 +3049,9 @@ var STRING_FLAGS = [
|
|
|
2373
3049
|
["--error-id", "errorId"],
|
|
2374
3050
|
["--hypothetical-action", "hypotheticalAction"],
|
|
2375
3051
|
["--type", "type"],
|
|
2376
|
-
["--min-confidence", "minConfidence"]
|
|
3052
|
+
["--min-confidence", "minConfidence"],
|
|
3053
|
+
["--to", "to"],
|
|
3054
|
+
["--token", "token"]
|
|
2377
3055
|
];
|
|
2378
3056
|
function parseArgs(rest) {
|
|
2379
3057
|
const positional = [];
|
|
@@ -2398,6 +3076,8 @@ function parseArgs(rest) {
|
|
|
2398
3076
|
hypotheticalAction: null,
|
|
2399
3077
|
type: null,
|
|
2400
3078
|
minConfidence: null,
|
|
3079
|
+
to: null,
|
|
3080
|
+
token: null,
|
|
2401
3081
|
positional: []
|
|
2402
3082
|
};
|
|
2403
3083
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -2544,12 +3224,12 @@ async function runInit(opts) {
|
|
|
2544
3224
|
printDiscoveryReport(opts, services);
|
|
2545
3225
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
2546
3226
|
const patch = renderPatch(sections);
|
|
2547
|
-
const patchPath =
|
|
3227
|
+
const patchPath = path8.join(opts.scanPath, "neat.patch");
|
|
2548
3228
|
if (opts.dryRun) {
|
|
2549
3229
|
await fs7.writeFile(patchPath, patch, "utf8");
|
|
2550
3230
|
written.push(patchPath);
|
|
2551
3231
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
2552
|
-
const gitignorePath =
|
|
3232
|
+
const gitignorePath = path8.join(opts.scanPath, ".gitignore");
|
|
2553
3233
|
const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
|
|
2554
3234
|
const verb = gitignoreExists ? "append" : "create";
|
|
2555
3235
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
@@ -2561,9 +3241,9 @@ async function runInit(opts) {
|
|
|
2561
3241
|
const graph = getGraph(graphKey);
|
|
2562
3242
|
const projectPaths = pathsForProject(
|
|
2563
3243
|
graphKey,
|
|
2564
|
-
|
|
3244
|
+
path8.join(opts.scanPath, "neat-out")
|
|
2565
3245
|
);
|
|
2566
|
-
const errorsPath =
|
|
3246
|
+
const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
|
|
2567
3247
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
2568
3248
|
await saveGraphToDisk(graph, opts.outPath);
|
|
2569
3249
|
written.push(opts.outPath);
|
|
@@ -2653,9 +3333,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
2653
3333
|
};
|
|
2654
3334
|
function claudeConfigPath() {
|
|
2655
3335
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
2656
|
-
if (override && override.length > 0) return
|
|
3336
|
+
if (override && override.length > 0) return path8.resolve(override);
|
|
2657
3337
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2658
|
-
return
|
|
3338
|
+
return path8.join(home, ".claude.json");
|
|
2659
3339
|
}
|
|
2660
3340
|
async function runSkill(opts) {
|
|
2661
3341
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -2679,7 +3359,7 @@ async function runSkill(opts) {
|
|
|
2679
3359
|
...existing,
|
|
2680
3360
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
2681
3361
|
};
|
|
2682
|
-
await fs7.mkdir(
|
|
3362
|
+
await fs7.mkdir(path8.dirname(target), { recursive: true });
|
|
2683
3363
|
await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2684
3364
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
2685
3365
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
@@ -2714,12 +3394,12 @@ async function main() {
|
|
|
2714
3394
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
2715
3395
|
process.exit(2);
|
|
2716
3396
|
}
|
|
2717
|
-
const scanPath =
|
|
3397
|
+
const scanPath = path8.resolve(target);
|
|
2718
3398
|
const projectExplicit = parsed.project !== null;
|
|
2719
|
-
const projectName = projectExplicit ? project :
|
|
3399
|
+
const projectName = projectExplicit ? project : path8.basename(scanPath);
|
|
2720
3400
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
2721
|
-
const fallback = pathsForProject(projectKey,
|
|
2722
|
-
const outPath =
|
|
3401
|
+
const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
|
|
3402
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
2723
3403
|
const result = await runInit({
|
|
2724
3404
|
scanPath,
|
|
2725
3405
|
outPath,
|
|
@@ -2740,21 +3420,21 @@ async function main() {
|
|
|
2740
3420
|
usage();
|
|
2741
3421
|
process.exit(2);
|
|
2742
3422
|
}
|
|
2743
|
-
const scanPath =
|
|
3423
|
+
const scanPath = path8.resolve(target);
|
|
2744
3424
|
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
2745
3425
|
if (!stat || !stat.isDirectory()) {
|
|
2746
3426
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
2747
3427
|
process.exit(2);
|
|
2748
3428
|
}
|
|
2749
|
-
const projectPaths = pathsForProject(project,
|
|
2750
|
-
const outPath =
|
|
2751
|
-
const errorsPath =
|
|
2752
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
3429
|
+
const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
|
|
3430
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
3431
|
+
const errorsPath = path8.resolve(
|
|
3432
|
+
process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
|
|
2753
3433
|
);
|
|
2754
|
-
const staleEventsPath =
|
|
2755
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
3434
|
+
const staleEventsPath = path8.resolve(
|
|
3435
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
|
|
2756
3436
|
);
|
|
2757
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
3437
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
2758
3438
|
const handle = await startWatch(getGraph(project), {
|
|
2759
3439
|
scanPath,
|
|
2760
3440
|
outPath,
|
|
@@ -2873,6 +3553,18 @@ async function main() {
|
|
|
2873
3553
|
console.log(` ${artifact.startCommand}`);
|
|
2874
3554
|
return;
|
|
2875
3555
|
}
|
|
3556
|
+
if (cmd === "sync") {
|
|
3557
|
+
const result = await runSync({
|
|
3558
|
+
...parsed.project ? { project: parsed.project } : {},
|
|
3559
|
+
...parsed.to ? { to: parsed.to } : {},
|
|
3560
|
+
...parsed.token ? { token: parsed.token } : {},
|
|
3561
|
+
dryRun: parsed.dryRun,
|
|
3562
|
+
noInstrument: parsed.noInstrument,
|
|
3563
|
+
json: parsed.json
|
|
3564
|
+
});
|
|
3565
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
2876
3568
|
if (QUERY_VERBS.has(cmd)) {
|
|
2877
3569
|
const code = await runQueryVerb(cmd, parsed);
|
|
2878
3570
|
if (code !== 0) process.exit(code);
|
|
@@ -2888,11 +3580,11 @@ async function main() {
|
|
|
2888
3580
|
process.exit(1);
|
|
2889
3581
|
}
|
|
2890
3582
|
async function tryOrchestrator(cmd, parsed) {
|
|
2891
|
-
const scanPath =
|
|
3583
|
+
const scanPath = path8.resolve(cmd);
|
|
2892
3584
|
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
2893
3585
|
if (!stat || !stat.isDirectory()) return null;
|
|
2894
3586
|
const projectExplicit = parsed.project !== null;
|
|
2895
|
-
const projectName = projectExplicit ? parsed.project :
|
|
3587
|
+
const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
|
|
2896
3588
|
const result = await runOrchestrator({
|
|
2897
3589
|
scanPath,
|
|
2898
3590
|
project: projectName,
|