@neat.is/core 0.3.8 → 0.4.2
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-LQ3JFBTX.js → chunk-CB2UK4EH.js} +139 -41
- package/dist/chunk-CB2UK4EH.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-D5PIJFBE.js} +2 -2
- package/dist/{chunk-7TYESDAI.js → chunk-KYRIQIPG.js} +48 -11
- package/dist/chunk-KYRIQIPG.js.map +1 -0
- package/dist/{chunk-CZ3T6TE2.js → chunk-NTQHMXWE.js} +239 -71
- package/dist/chunk-NTQHMXWE.js.map +1 -0
- package/dist/cli.cjs +1209 -176
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +4 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +940 -99
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +418 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +427 -131
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-QTX2YQJZ.js} +3 -3
- package/dist/server.cjs +401 -203
- 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-LQ3JFBTX.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-D5PIJFBE.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-QTX2YQJZ.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,18 +41,20 @@ import {
|
|
|
40
41
|
setStatus,
|
|
41
42
|
startPersistLoop,
|
|
42
43
|
startStalenessLoop
|
|
43
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-NTQHMXWE.js";
|
|
44
45
|
import {
|
|
45
46
|
startOtelGrpcReceiver
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-D5PIJFBE.js";
|
|
47
48
|
import {
|
|
49
|
+
__dirname,
|
|
48
50
|
__require,
|
|
49
51
|
buildOtelReceiver
|
|
50
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-KYRIQIPG.js";
|
|
51
53
|
|
|
52
54
|
// src/cli.ts
|
|
53
|
-
import
|
|
54
|
-
import { promises as fs7 } from "fs";
|
|
55
|
+
import path8 from "path";
|
|
56
|
+
import { promises as fs7, readFileSync } from "fs";
|
|
57
|
+
import { fileURLToPath } from "url";
|
|
55
58
|
|
|
56
59
|
// src/gitignore.ts
|
|
57
60
|
import { promises as fs } from "fs";
|
|
@@ -271,6 +274,14 @@ var IGNORED_WATCH_GLOBS = [
|
|
|
271
274
|
"**/.turbo/**",
|
|
272
275
|
"**/.next/**",
|
|
273
276
|
"**/neat-out/**",
|
|
277
|
+
// Python venv shapes (issue #344). chokidar opens one watch handle per
|
|
278
|
+
// descended dir; a CPython venv carries 20k+ files and trivially blows
|
|
279
|
+
// through the macOS kqueue cap before extraction even runs.
|
|
280
|
+
"**/.venv/**",
|
|
281
|
+
"**/venv/**",
|
|
282
|
+
"**/__pypackages__/**",
|
|
283
|
+
"**/.tox/**",
|
|
284
|
+
"**/site-packages/**",
|
|
274
285
|
"**/.DS_Store"
|
|
275
286
|
];
|
|
276
287
|
var IGNORED_WATCH_PATHS = [
|
|
@@ -281,6 +292,11 @@ var IGNORED_WATCH_PATHS = [
|
|
|
281
292
|
/(?:^|[\\/])\.turbo[\\/]/,
|
|
282
293
|
/(?:^|[\\/])\.next[\\/]/,
|
|
283
294
|
/(?:^|[\\/])neat-out[\\/]/,
|
|
295
|
+
/(?:^|[\\/])\.venv[\\/]/,
|
|
296
|
+
/(?:^|[\\/])venv[\\/]/,
|
|
297
|
+
/(?:^|[\\/])__pypackages__[\\/]/,
|
|
298
|
+
/(?:^|[\\/])\.tox[\\/]/,
|
|
299
|
+
/(?:^|[\\/])site-packages[\\/]/,
|
|
284
300
|
/[\\/]?\.DS_Store$/
|
|
285
301
|
];
|
|
286
302
|
function shouldIgnore(absPath) {
|
|
@@ -712,10 +728,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
|
|
|
712
728
|
|
|
713
729
|
await import('@opentelemetry/auto-instrumentations-node/register')
|
|
714
730
|
`;
|
|
715
|
-
function renderEnvNeat(
|
|
731
|
+
function renderEnvNeat(projectName) {
|
|
716
732
|
return [
|
|
717
733
|
"# Generated by `neat init --apply` (ADR-069).",
|
|
718
|
-
`OTEL_SERVICE_NAME=${
|
|
734
|
+
`OTEL_SERVICE_NAME=${projectName}`,
|
|
719
735
|
"OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
|
|
720
736
|
""
|
|
721
737
|
].join("\n");
|
|
@@ -771,6 +787,100 @@ const sdk = new NodeSDK({
|
|
|
771
787
|
})
|
|
772
788
|
sdk.start()
|
|
773
789
|
`;
|
|
790
|
+
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
791
|
+
var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
792
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
793
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
794
|
+
import { fileURLToPath } from 'node:url'
|
|
795
|
+
import path from 'node:path'
|
|
796
|
+
import dotenv from 'dotenv'
|
|
797
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
798
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
799
|
+
|
|
800
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
801
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
802
|
+
|
|
803
|
+
const sdk = new NodeSDK({
|
|
804
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
805
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
806
|
+
})
|
|
807
|
+
sdk.start()
|
|
808
|
+
`;
|
|
809
|
+
var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
810
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
811
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
812
|
+
const path = require('node:path')
|
|
813
|
+
try {
|
|
814
|
+
require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
|
|
815
|
+
} catch (err) {
|
|
816
|
+
// dotenv unavailable \u2014 fall through to process.env.
|
|
817
|
+
}
|
|
818
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
819
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
820
|
+
|
|
821
|
+
const sdk = new NodeSDK({
|
|
822
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
823
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
824
|
+
})
|
|
825
|
+
sdk.start()
|
|
826
|
+
`;
|
|
827
|
+
var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
828
|
+
var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
829
|
+
var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
830
|
+
var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
831
|
+
var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
832
|
+
import './otel-init'
|
|
833
|
+
|
|
834
|
+
import type { Handle } from '@sveltejs/kit'
|
|
835
|
+
|
|
836
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
837
|
+
return resolve(event)
|
|
838
|
+
}
|
|
839
|
+
`;
|
|
840
|
+
var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
841
|
+
import './otel-init'
|
|
842
|
+
|
|
843
|
+
/** @type {import('@sveltejs/kit').Handle} */
|
|
844
|
+
export const handle = async ({ event, resolve }) => {
|
|
845
|
+
return resolve(event)
|
|
846
|
+
}
|
|
847
|
+
`;
|
|
848
|
+
var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
849
|
+
var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
850
|
+
var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
851
|
+
import './otel-init'
|
|
852
|
+
|
|
853
|
+
export default defineNitroPlugin(() => {
|
|
854
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
855
|
+
})
|
|
856
|
+
`;
|
|
857
|
+
var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
858
|
+
require('./otel-init')
|
|
859
|
+
|
|
860
|
+
module.exports = defineNitroPlugin(() => {
|
|
861
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
862
|
+
})
|
|
863
|
+
`;
|
|
864
|
+
var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
865
|
+
var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
866
|
+
var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
867
|
+
import './otel-init'
|
|
868
|
+
|
|
869
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
870
|
+
|
|
871
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
872
|
+
return next()
|
|
873
|
+
})
|
|
874
|
+
`;
|
|
875
|
+
var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
876
|
+
import './otel-init'
|
|
877
|
+
|
|
878
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
879
|
+
|
|
880
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
881
|
+
return next()
|
|
882
|
+
})
|
|
883
|
+
`;
|
|
774
884
|
|
|
775
885
|
// src/installers/javascript.ts
|
|
776
886
|
var SDK_PACKAGES = [
|
|
@@ -791,6 +901,10 @@ var OTEL_ENV = {
|
|
|
791
901
|
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
792
902
|
value: "http://localhost:4318"
|
|
793
903
|
};
|
|
904
|
+
function envServiceName(pkg, serviceDir, project) {
|
|
905
|
+
if (project && project.length > 0) return project;
|
|
906
|
+
return pkg.name ?? path4.basename(serviceDir);
|
|
907
|
+
}
|
|
794
908
|
async function readPackageJson(serviceDir) {
|
|
795
909
|
try {
|
|
796
910
|
const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
|
|
@@ -822,6 +936,71 @@ async function findNextConfig(serviceDir) {
|
|
|
822
936
|
function hasNextDependency(pkg) {
|
|
823
937
|
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
824
938
|
}
|
|
939
|
+
function allDeps(pkg) {
|
|
940
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
941
|
+
}
|
|
942
|
+
var REMIX_ENTRY_CANDIDATES = [
|
|
943
|
+
"app/entry.server.ts",
|
|
944
|
+
"app/entry.server.tsx",
|
|
945
|
+
"app/entry.server.js",
|
|
946
|
+
"app/entry.server.jsx"
|
|
947
|
+
];
|
|
948
|
+
function hasRemixDependency(pkg) {
|
|
949
|
+
const deps = allDeps(pkg);
|
|
950
|
+
if ("remix" in deps) return true;
|
|
951
|
+
for (const name of Object.keys(deps)) {
|
|
952
|
+
if (name.startsWith("@remix-run/")) return true;
|
|
953
|
+
}
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
async function findRemixEntry(serviceDir) {
|
|
957
|
+
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
958
|
+
const candidate = path4.join(serviceDir, rel);
|
|
959
|
+
if (await exists(candidate)) return candidate;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
|
|
964
|
+
var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
|
|
965
|
+
function hasSvelteKitDependency(pkg) {
|
|
966
|
+
return "@sveltejs/kit" in allDeps(pkg);
|
|
967
|
+
}
|
|
968
|
+
async function findSvelteKitHooks(serviceDir) {
|
|
969
|
+
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
970
|
+
const candidate = path4.join(serviceDir, rel);
|
|
971
|
+
if (await exists(candidate)) return candidate;
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
async function findSvelteKitConfig(serviceDir) {
|
|
976
|
+
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
977
|
+
const candidate = path4.join(serviceDir, rel);
|
|
978
|
+
if (await exists(candidate)) return candidate;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
983
|
+
function hasNuxtDependency(pkg) {
|
|
984
|
+
return "nuxt" in allDeps(pkg);
|
|
985
|
+
}
|
|
986
|
+
async function findNuxtConfig(serviceDir) {
|
|
987
|
+
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
988
|
+
const candidate = path4.join(serviceDir, name);
|
|
989
|
+
if (await exists(candidate)) return candidate;
|
|
990
|
+
}
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
|
|
994
|
+
function hasAstroDependency(pkg) {
|
|
995
|
+
return "astro" in allDeps(pkg);
|
|
996
|
+
}
|
|
997
|
+
async function findAstroConfig(serviceDir) {
|
|
998
|
+
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
999
|
+
const candidate = path4.join(serviceDir, name);
|
|
1000
|
+
if (await exists(candidate)) return candidate;
|
|
1001
|
+
}
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
825
1004
|
function parseNextMajor(range) {
|
|
826
1005
|
if (!range) return null;
|
|
827
1006
|
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
@@ -950,7 +1129,7 @@ function lineIsOtelInjection(line) {
|
|
|
950
1129
|
if (trimmed.length === 0) return false;
|
|
951
1130
|
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
952
1131
|
}
|
|
953
|
-
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
1132
|
+
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
|
|
954
1133
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
955
1134
|
const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
956
1135
|
const instrumentationNodeFile = path4.join(
|
|
@@ -987,7 +1166,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
987
1166
|
if (!await exists(envNeatFile)) {
|
|
988
1167
|
generatedFiles.push({
|
|
989
1168
|
file: envNeatFile,
|
|
990
|
-
contents: renderEnvNeat(pkg
|
|
1169
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
991
1170
|
skipIfExists: true
|
|
992
1171
|
});
|
|
993
1172
|
}
|
|
@@ -1029,9 +1208,280 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
|
1029
1208
|
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
1030
1209
|
};
|
|
1031
1210
|
}
|
|
1032
|
-
|
|
1211
|
+
function buildDependencyEdits(pkg, manifestPath) {
|
|
1212
|
+
const existingDeps = allDeps(pkg);
|
|
1213
|
+
const edits = [];
|
|
1214
|
+
for (const sdk of SDK_PACKAGES) {
|
|
1215
|
+
if (sdk.name in existingDeps) continue;
|
|
1216
|
+
edits.push({
|
|
1217
|
+
file: manifestPath,
|
|
1218
|
+
kind: "add",
|
|
1219
|
+
name: sdk.name,
|
|
1220
|
+
version: sdk.version
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return edits;
|
|
1224
|
+
}
|
|
1225
|
+
async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
1226
|
+
const envNeatFile = path4.join(serviceDir, ".env.neat");
|
|
1227
|
+
if (!await exists(envNeatFile)) {
|
|
1228
|
+
generatedFiles.push({
|
|
1229
|
+
file: envNeatFile,
|
|
1230
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
1231
|
+
skipIfExists: true
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function fileImportsOtelHook(raw, specifiers) {
|
|
1236
|
+
const lines = raw.split(/\r?\n/);
|
|
1237
|
+
for (const line of lines) {
|
|
1238
|
+
const trimmed = line.trim();
|
|
1239
|
+
for (const spec of specifiers) {
|
|
1240
|
+
const escaped = spec.replace(/\./g, "\\.");
|
|
1241
|
+
const pattern = new RegExp(
|
|
1242
|
+
`(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
|
|
1243
|
+
);
|
|
1244
|
+
if (pattern.test(trimmed)) return true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
1250
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1251
|
+
const otelServerFile = path4.join(
|
|
1252
|
+
serviceDir,
|
|
1253
|
+
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
1254
|
+
);
|
|
1255
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1256
|
+
const generatedFiles = [];
|
|
1257
|
+
if (!await exists(otelServerFile)) {
|
|
1258
|
+
generatedFiles.push({
|
|
1259
|
+
file: otelServerFile,
|
|
1260
|
+
contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
|
|
1261
|
+
skipIfExists: true
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
1265
|
+
const entrypointEdits = [];
|
|
1266
|
+
try {
|
|
1267
|
+
const raw = await fs4.readFile(entryFile, "utf8");
|
|
1268
|
+
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
1269
|
+
const lines = raw.split(/\r?\n/);
|
|
1270
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1271
|
+
entrypointEdits.push({
|
|
1272
|
+
file: entryFile,
|
|
1273
|
+
before: firstReal,
|
|
1274
|
+
after: "import './otel.server'"
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1280
|
+
if (empty) {
|
|
1281
|
+
return {
|
|
1282
|
+
language: "javascript",
|
|
1283
|
+
serviceDir,
|
|
1284
|
+
dependencyEdits: [],
|
|
1285
|
+
entrypointEdits: [],
|
|
1286
|
+
envEdits: [],
|
|
1287
|
+
generatedFiles: [],
|
|
1288
|
+
framework: "remix"
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
language: "javascript",
|
|
1293
|
+
serviceDir,
|
|
1294
|
+
dependencyEdits,
|
|
1295
|
+
entrypointEdits,
|
|
1296
|
+
envEdits: [OTEL_ENV],
|
|
1297
|
+
generatedFiles,
|
|
1298
|
+
framework: "remix"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
|
|
1302
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1303
|
+
const otelInitFile = path4.join(
|
|
1304
|
+
serviceDir,
|
|
1305
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1306
|
+
);
|
|
1307
|
+
const resolvedHooksFile = hooksFile ?? path4.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
1308
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1309
|
+
const generatedFiles = [];
|
|
1310
|
+
const entrypointEdits = [];
|
|
1311
|
+
if (!await exists(otelInitFile)) {
|
|
1312
|
+
generatedFiles.push({
|
|
1313
|
+
file: otelInitFile,
|
|
1314
|
+
contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
|
|
1315
|
+
skipIfExists: true
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
1319
|
+
if (hooksFile === null) {
|
|
1320
|
+
generatedFiles.push({
|
|
1321
|
+
file: resolvedHooksFile,
|
|
1322
|
+
contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
|
|
1323
|
+
skipIfExists: true
|
|
1324
|
+
});
|
|
1325
|
+
} else {
|
|
1326
|
+
try {
|
|
1327
|
+
const raw = await fs4.readFile(hooksFile, "utf8");
|
|
1328
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1329
|
+
const lines = raw.split(/\r?\n/);
|
|
1330
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1331
|
+
entrypointEdits.push({
|
|
1332
|
+
file: hooksFile,
|
|
1333
|
+
before: firstReal,
|
|
1334
|
+
after: "import './otel-init'"
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1341
|
+
if (empty) {
|
|
1342
|
+
return {
|
|
1343
|
+
language: "javascript",
|
|
1344
|
+
serviceDir,
|
|
1345
|
+
dependencyEdits: [],
|
|
1346
|
+
entrypointEdits: [],
|
|
1347
|
+
envEdits: [],
|
|
1348
|
+
generatedFiles: [],
|
|
1349
|
+
framework: "sveltekit"
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
language: "javascript",
|
|
1354
|
+
serviceDir,
|
|
1355
|
+
dependencyEdits,
|
|
1356
|
+
entrypointEdits,
|
|
1357
|
+
envEdits: [OTEL_ENV],
|
|
1358
|
+
generatedFiles,
|
|
1359
|
+
framework: "sveltekit"
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
1363
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1364
|
+
const otelPluginFile = path4.join(
|
|
1365
|
+
serviceDir,
|
|
1366
|
+
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
1367
|
+
);
|
|
1368
|
+
const otelInitFile = path4.join(
|
|
1369
|
+
serviceDir,
|
|
1370
|
+
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
1371
|
+
);
|
|
1372
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1373
|
+
const generatedFiles = [];
|
|
1374
|
+
if (!await exists(otelInitFile)) {
|
|
1375
|
+
generatedFiles.push({
|
|
1376
|
+
file: otelInitFile,
|
|
1377
|
+
contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
|
|
1378
|
+
skipIfExists: true
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
if (!await exists(otelPluginFile)) {
|
|
1382
|
+
generatedFiles.push({
|
|
1383
|
+
file: otelPluginFile,
|
|
1384
|
+
contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
|
|
1385
|
+
skipIfExists: true
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
1389
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
|
|
1390
|
+
if (empty) {
|
|
1391
|
+
return {
|
|
1392
|
+
language: "javascript",
|
|
1393
|
+
serviceDir,
|
|
1394
|
+
dependencyEdits: [],
|
|
1395
|
+
entrypointEdits: [],
|
|
1396
|
+
envEdits: [],
|
|
1397
|
+
generatedFiles: [],
|
|
1398
|
+
framework: "nuxt"
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
language: "javascript",
|
|
1403
|
+
serviceDir,
|
|
1404
|
+
dependencyEdits,
|
|
1405
|
+
entrypointEdits: [],
|
|
1406
|
+
envEdits: [OTEL_ENV],
|
|
1407
|
+
generatedFiles,
|
|
1408
|
+
framework: "nuxt"
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
1412
|
+
async function findAstroMiddleware(serviceDir) {
|
|
1413
|
+
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
1414
|
+
const candidate = path4.join(serviceDir, rel);
|
|
1415
|
+
if (await exists(candidate)) return candidate;
|
|
1416
|
+
}
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
1420
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1421
|
+
const otelInitFile = path4.join(
|
|
1422
|
+
serviceDir,
|
|
1423
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1424
|
+
);
|
|
1425
|
+
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
1426
|
+
const middlewareFile = existingMiddleware ?? path4.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
1427
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1428
|
+
const generatedFiles = [];
|
|
1429
|
+
const entrypointEdits = [];
|
|
1430
|
+
if (!await exists(otelInitFile)) {
|
|
1431
|
+
generatedFiles.push({
|
|
1432
|
+
file: otelInitFile,
|
|
1433
|
+
contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
|
|
1434
|
+
skipIfExists: true
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
1438
|
+
if (existingMiddleware === null) {
|
|
1439
|
+
generatedFiles.push({
|
|
1440
|
+
file: middlewareFile,
|
|
1441
|
+
contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
|
|
1442
|
+
skipIfExists: true
|
|
1443
|
+
});
|
|
1444
|
+
} else {
|
|
1445
|
+
try {
|
|
1446
|
+
const raw = await fs4.readFile(existingMiddleware, "utf8");
|
|
1447
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1448
|
+
const lines = raw.split(/\r?\n/);
|
|
1449
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1450
|
+
entrypointEdits.push({
|
|
1451
|
+
file: existingMiddleware,
|
|
1452
|
+
before: firstReal,
|
|
1453
|
+
after: "import './otel-init'"
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1460
|
+
if (empty) {
|
|
1461
|
+
return {
|
|
1462
|
+
language: "javascript",
|
|
1463
|
+
serviceDir,
|
|
1464
|
+
dependencyEdits: [],
|
|
1465
|
+
entrypointEdits: [],
|
|
1466
|
+
envEdits: [],
|
|
1467
|
+
generatedFiles: [],
|
|
1468
|
+
framework: "astro"
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
language: "javascript",
|
|
1473
|
+
serviceDir,
|
|
1474
|
+
dependencyEdits,
|
|
1475
|
+
entrypointEdits,
|
|
1476
|
+
envEdits: [OTEL_ENV],
|
|
1477
|
+
generatedFiles,
|
|
1478
|
+
framework: "astro"
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
async function plan(serviceDir, opts) {
|
|
1033
1482
|
const pkg = await readPackageJson(serviceDir);
|
|
1034
1483
|
const manifestPath = path4.join(serviceDir, "package.json");
|
|
1484
|
+
const project = opts?.project;
|
|
1035
1485
|
const empty = {
|
|
1036
1486
|
language: "javascript",
|
|
1037
1487
|
serviceDir,
|
|
@@ -1044,7 +1494,32 @@ async function plan(serviceDir) {
|
|
|
1044
1494
|
if (hasNextDependency(pkg)) {
|
|
1045
1495
|
const nextConfig = await findNextConfig(serviceDir);
|
|
1046
1496
|
if (nextConfig) {
|
|
1047
|
-
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
1497
|
+
return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (hasRemixDependency(pkg)) {
|
|
1501
|
+
const remixEntry = await findRemixEntry(serviceDir);
|
|
1502
|
+
if (remixEntry) {
|
|
1503
|
+
return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (hasSvelteKitDependency(pkg)) {
|
|
1507
|
+
const hooks = await findSvelteKitHooks(serviceDir);
|
|
1508
|
+
const config = await findSvelteKitConfig(serviceDir);
|
|
1509
|
+
if (hooks || config) {
|
|
1510
|
+
return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (hasNuxtDependency(pkg)) {
|
|
1514
|
+
const nuxtConfig = await findNuxtConfig(serviceDir);
|
|
1515
|
+
if (nuxtConfig) {
|
|
1516
|
+
return planNuxt(serviceDir, pkg, manifestPath, project);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (hasAstroDependency(pkg)) {
|
|
1520
|
+
const astroConfig = await findAstroConfig(serviceDir);
|
|
1521
|
+
if (astroConfig) {
|
|
1522
|
+
return planAstro(serviceDir, pkg, manifestPath, project);
|
|
1048
1523
|
}
|
|
1049
1524
|
}
|
|
1050
1525
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
@@ -1092,7 +1567,7 @@ async function plan(serviceDir) {
|
|
|
1092
1567
|
if (!await exists(envNeatFile)) {
|
|
1093
1568
|
generatedFiles.push({
|
|
1094
1569
|
file: envNeatFile,
|
|
1095
|
-
contents: renderEnvNeat(pkg
|
|
1570
|
+
contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
|
|
1096
1571
|
skipIfExists: true
|
|
1097
1572
|
});
|
|
1098
1573
|
}
|
|
@@ -1119,9 +1594,18 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
1119
1594
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1120
1595
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1121
1596
|
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
1597
|
+
const relPosix = rel.split(path4.sep).join("/");
|
|
1598
|
+
if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
|
|
1599
|
+
if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
|
|
1600
|
+
if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
|
|
1601
|
+
if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
|
|
1602
|
+
if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
|
|
1603
|
+
if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
|
|
1604
|
+
if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
|
|
1122
1605
|
return false;
|
|
1123
1606
|
}
|
|
1124
1607
|
async function writeAtomic(file, contents) {
|
|
1608
|
+
await fs4.mkdir(path4.dirname(file), { recursive: true });
|
|
1125
1609
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1126
1610
|
await fs4.writeFile(tmp, contents, "utf8");
|
|
1127
1611
|
await fs4.rename(tmp, file);
|
|
@@ -1591,6 +2075,49 @@ import http from "http";
|
|
|
1591
2075
|
import path6 from "path";
|
|
1592
2076
|
import { spawn as spawn2 } from "child_process";
|
|
1593
2077
|
import readline from "readline";
|
|
2078
|
+
async function extractAndPersist(opts) {
|
|
2079
|
+
const services = await discoverServices(opts.scanPath);
|
|
2080
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
2081
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
2082
|
+
resetGraph(graphKey);
|
|
2083
|
+
const graph = getGraph(graphKey);
|
|
2084
|
+
const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
|
|
2085
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
2086
|
+
errorsPath: projectPaths.errorsPath
|
|
2087
|
+
});
|
|
2088
|
+
if (!opts.dryRun) {
|
|
2089
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
2090
|
+
}
|
|
2091
|
+
return {
|
|
2092
|
+
graph,
|
|
2093
|
+
graphKey,
|
|
2094
|
+
services,
|
|
2095
|
+
languages,
|
|
2096
|
+
nodesAdded: extraction.nodesAdded,
|
|
2097
|
+
edgesAdded: extraction.edgesAdded,
|
|
2098
|
+
snapshotPath: projectPaths.snapshotPath,
|
|
2099
|
+
errorsPath: projectPaths.errorsPath
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async function applyInstallersOver(services, project) {
|
|
2103
|
+
let instrumented = 0;
|
|
2104
|
+
let already = 0;
|
|
2105
|
+
let libOnly = 0;
|
|
2106
|
+
for (const svc of services) {
|
|
2107
|
+
const installer = await pickInstaller(svc.dir);
|
|
2108
|
+
if (!installer) continue;
|
|
2109
|
+
const plan3 = await installer.plan(svc.dir, { project });
|
|
2110
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
2111
|
+
already++;
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
const outcome = await installer.apply(plan3);
|
|
2115
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
2116
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
2117
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
2118
|
+
}
|
|
2119
|
+
return { instrumented, alreadyInstrumented: already, libOnly };
|
|
2120
|
+
}
|
|
1594
2121
|
async function promptYesNo(question) {
|
|
1595
2122
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1596
2123
|
return new Promise((resolve) => {
|
|
@@ -1601,28 +2128,106 @@ async function promptYesNo(question) {
|
|
|
1601
2128
|
});
|
|
1602
2129
|
});
|
|
1603
2130
|
}
|
|
1604
|
-
var DEFAULT_DAEMON_READY_TIMEOUT_MS =
|
|
1605
|
-
|
|
2131
|
+
var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
|
|
2132
|
+
var PROBE_INTERVAL_MS = 500;
|
|
2133
|
+
async function fetchDaemonHealth(restPort) {
|
|
1606
2134
|
return new Promise((resolve) => {
|
|
1607
2135
|
const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
|
|
1608
2136
|
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
1609
|
-
|
|
1610
|
-
|
|
2137
|
+
if (!ok) {
|
|
2138
|
+
res.resume();
|
|
2139
|
+
resolve(null);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
let body = "";
|
|
2143
|
+
res.setEncoding("utf8");
|
|
2144
|
+
res.on("data", (chunk) => {
|
|
2145
|
+
body += chunk;
|
|
2146
|
+
});
|
|
2147
|
+
res.on("end", () => {
|
|
2148
|
+
try {
|
|
2149
|
+
resolve(JSON.parse(body));
|
|
2150
|
+
} catch {
|
|
2151
|
+
resolve({ ok: true });
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
1611
2154
|
});
|
|
1612
|
-
req.on("error", () => resolve(
|
|
2155
|
+
req.on("error", () => resolve(null));
|
|
1613
2156
|
req.setTimeout(1e3, () => {
|
|
1614
2157
|
req.destroy();
|
|
1615
|
-
resolve(
|
|
2158
|
+
resolve(null);
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
async function checkDaemonHealth(restPort) {
|
|
2163
|
+
const body = await fetchDaemonHealth(restPort);
|
|
2164
|
+
return body !== null;
|
|
2165
|
+
}
|
|
2166
|
+
async function probeProjectHealth(restPort, name) {
|
|
2167
|
+
return new Promise((resolve) => {
|
|
2168
|
+
const req = http.get(
|
|
2169
|
+
`http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
|
|
2170
|
+
(res) => {
|
|
2171
|
+
const code = res.statusCode ?? 0;
|
|
2172
|
+
res.resume();
|
|
2173
|
+
if (code >= 200 && code < 300) resolve("active");
|
|
2174
|
+
else resolve("bootstrapping");
|
|
2175
|
+
}
|
|
2176
|
+
);
|
|
2177
|
+
req.on("error", () => resolve("bootstrapping"));
|
|
2178
|
+
req.setTimeout(1e3, () => {
|
|
2179
|
+
req.destroy();
|
|
2180
|
+
resolve("bootstrapping");
|
|
1616
2181
|
});
|
|
1617
2182
|
});
|
|
1618
2183
|
}
|
|
2184
|
+
async function snapshotProjectStatus(restPort, body) {
|
|
2185
|
+
if (body.projects && body.projects.length > 0) {
|
|
2186
|
+
return body.projects.map((p) => ({
|
|
2187
|
+
name: p.name,
|
|
2188
|
+
status: p.status ?? "active"
|
|
2189
|
+
}));
|
|
2190
|
+
}
|
|
2191
|
+
const entries = await listProjects().catch(() => []);
|
|
2192
|
+
if (entries.length === 0) return [];
|
|
2193
|
+
return Promise.all(
|
|
2194
|
+
entries.map(async (entry2) => ({
|
|
2195
|
+
name: entry2.name,
|
|
2196
|
+
status: await probeProjectHealth(restPort, entry2.name)
|
|
2197
|
+
}))
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
1619
2200
|
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
1620
2201
|
const deadline = Date.now() + timeoutMs;
|
|
2202
|
+
let lastBootstrapping = [];
|
|
1621
2203
|
while (Date.now() < deadline) {
|
|
1622
|
-
|
|
1623
|
-
|
|
2204
|
+
const body = await fetchDaemonHealth(restPort);
|
|
2205
|
+
if (body !== null) {
|
|
2206
|
+
const projects2 = await snapshotProjectStatus(restPort, body);
|
|
2207
|
+
const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
|
|
2208
|
+
const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
|
|
2209
|
+
if (bootstrapping.length === 0) {
|
|
2210
|
+
return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
|
|
2211
|
+
}
|
|
2212
|
+
const key = bootstrapping.slice().sort().join(",");
|
|
2213
|
+
const prevKey = lastBootstrapping.slice().sort().join(",");
|
|
2214
|
+
if (key !== prevKey) {
|
|
2215
|
+
const plural = bootstrapping.length === 1 ? "" : "s";
|
|
2216
|
+
console.log(
|
|
2217
|
+
`neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
|
|
2218
|
+
);
|
|
2219
|
+
lastBootstrapping = bootstrapping;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
1624
2223
|
}
|
|
1625
|
-
|
|
2224
|
+
const final = await fetchDaemonHealth(restPort);
|
|
2225
|
+
const projects = final ? await snapshotProjectStatus(restPort, final) : [];
|
|
2226
|
+
return {
|
|
2227
|
+
ready: false,
|
|
2228
|
+
brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
|
|
2229
|
+
stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
|
|
2230
|
+
};
|
|
1626
2231
|
}
|
|
1627
2232
|
function spawnDaemonDetached() {
|
|
1628
2233
|
const here = path6.dirname(new URL(import.meta.url).pathname);
|
|
@@ -1643,12 +2248,21 @@ function spawnDaemonDetached() {
|
|
|
1643
2248
|
if (!entry2) {
|
|
1644
2249
|
throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
|
|
1645
2250
|
}
|
|
2251
|
+
const env = { ...process.env };
|
|
2252
|
+
const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
|
|
2253
|
+
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
2254
|
+
env.HOST = "127.0.0.1";
|
|
2255
|
+
}
|
|
1646
2256
|
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
1647
2257
|
detached: true,
|
|
1648
|
-
|
|
1649
|
-
|
|
2258
|
+
// stderr inherits the orchestrator's fd so the daemon's
|
|
2259
|
+
// `BindAuthorityError` message lands in front of the operator instead
|
|
2260
|
+
// of being swallowed (issue #341).
|
|
2261
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
2262
|
+
env
|
|
1650
2263
|
});
|
|
1651
2264
|
child.unref();
|
|
2265
|
+
return child;
|
|
1652
2266
|
}
|
|
1653
2267
|
function openBrowser(url) {
|
|
1654
2268
|
if (!process.stdout.isTTY) return "failed";
|
|
@@ -1685,24 +2299,21 @@ async function runOrchestrator(opts) {
|
|
|
1685
2299
|
}
|
|
1686
2300
|
console.log(`neat: ${opts.scanPath}`);
|
|
1687
2301
|
console.log("");
|
|
1688
|
-
const
|
|
1689
|
-
|
|
2302
|
+
const persisted = await extractAndPersist({
|
|
2303
|
+
scanPath: opts.scanPath,
|
|
2304
|
+
project: opts.project,
|
|
2305
|
+
projectExplicit: opts.projectExplicit
|
|
2306
|
+
});
|
|
2307
|
+
const { graph, services, languages } = persisted;
|
|
1690
2308
|
result.steps.discovery = { services: services.length, languages };
|
|
1691
2309
|
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
1692
2310
|
let runApply = !opts.noInstrument;
|
|
1693
2311
|
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
1694
2312
|
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
1695
2313
|
}
|
|
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
2314
|
result.steps.extraction = {
|
|
1704
|
-
nodesAdded:
|
|
1705
|
-
edgesAdded:
|
|
2315
|
+
nodesAdded: persisted.nodesAdded,
|
|
2316
|
+
edgesAdded: persisted.edgesAdded
|
|
1706
2317
|
};
|
|
1707
2318
|
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
1708
2319
|
result.steps.gitignore = gi.action;
|
|
@@ -1727,24 +2338,11 @@ async function runOrchestrator(opts) {
|
|
|
1727
2338
|
result.steps.apply.skipped = true;
|
|
1728
2339
|
console.log("skipped instrumentation (--no-instrument)");
|
|
1729
2340
|
} 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}`);
|
|
2341
|
+
const tally = await applyInstallersOver(services, opts.project);
|
|
2342
|
+
result.steps.apply = { ...tally, skipped: false };
|
|
2343
|
+
console.log(
|
|
2344
|
+
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
2345
|
+
);
|
|
1748
2346
|
}
|
|
1749
2347
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
1750
2348
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -1759,12 +2357,25 @@ async function runOrchestrator(opts) {
|
|
|
1759
2357
|
return result;
|
|
1760
2358
|
}
|
|
1761
2359
|
const ready = await waitForDaemonReady(restPort, timeoutMs);
|
|
1762
|
-
result.steps.daemon = ready ? "spawned" : "timed-out";
|
|
1763
|
-
if (!ready) {
|
|
2360
|
+
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
2361
|
+
if (!ready.ready) {
|
|
1764
2362
|
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
2363
|
+
if (ready.stillBootstrapping.length > 0) {
|
|
2364
|
+
console.error(
|
|
2365
|
+
`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
if (ready.brokenProjects.length > 0) {
|
|
2369
|
+
console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
|
|
2370
|
+
}
|
|
1765
2371
|
result.exitCode = 1;
|
|
1766
2372
|
return result;
|
|
1767
2373
|
}
|
|
2374
|
+
if (ready.brokenProjects.length > 0) {
|
|
2375
|
+
console.warn(
|
|
2376
|
+
`neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
1768
2379
|
}
|
|
1769
2380
|
const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
|
|
1770
2381
|
if (opts.noOpen || !process.stdout.isTTY) {
|
|
@@ -1793,8 +2404,8 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
1793
2404
|
console.log(`dashboard: ${dashboardUrl}`);
|
|
1794
2405
|
}
|
|
1795
2406
|
|
|
1796
|
-
// src/cli.ts
|
|
1797
|
-
import
|
|
2407
|
+
// src/cli-verbs.ts
|
|
2408
|
+
import path7 from "path";
|
|
1798
2409
|
|
|
1799
2410
|
// src/cli-client.ts
|
|
1800
2411
|
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
@@ -1814,13 +2425,16 @@ var TransportError = class extends Error {
|
|
|
1814
2425
|
this.name = "TransportError";
|
|
1815
2426
|
}
|
|
1816
2427
|
};
|
|
1817
|
-
function createHttpClient(baseUrl) {
|
|
2428
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
1818
2429
|
const root = baseUrl.replace(/\/$/, "");
|
|
2430
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
1819
2431
|
return {
|
|
1820
|
-
async get(
|
|
2432
|
+
async get(path9) {
|
|
1821
2433
|
let res;
|
|
1822
2434
|
try {
|
|
1823
|
-
res = await fetch(`${root}${
|
|
2435
|
+
res = await fetch(`${root}${path9}`, {
|
|
2436
|
+
headers: { ...authHeader }
|
|
2437
|
+
});
|
|
1824
2438
|
} catch (err) {
|
|
1825
2439
|
throw new TransportError(
|
|
1826
2440
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -1830,18 +2444,18 @@ function createHttpClient(baseUrl) {
|
|
|
1830
2444
|
const body = await res.text().catch(() => "");
|
|
1831
2445
|
throw new HttpError(
|
|
1832
2446
|
res.status,
|
|
1833
|
-
`${res.status} ${res.statusText} on GET ${
|
|
2447
|
+
`${res.status} ${res.statusText} on GET ${path9}: ${body}`,
|
|
1834
2448
|
body
|
|
1835
2449
|
);
|
|
1836
2450
|
}
|
|
1837
2451
|
return await res.json();
|
|
1838
2452
|
},
|
|
1839
|
-
async post(
|
|
2453
|
+
async post(path9, body) {
|
|
1840
2454
|
let res;
|
|
1841
2455
|
try {
|
|
1842
|
-
res = await fetch(`${root}${
|
|
2456
|
+
res = await fetch(`${root}${path9}`, {
|
|
1843
2457
|
method: "POST",
|
|
1844
|
-
headers: { "content-type": "application/json" },
|
|
2458
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
1845
2459
|
body: JSON.stringify(body)
|
|
1846
2460
|
});
|
|
1847
2461
|
} catch (err) {
|
|
@@ -1853,7 +2467,7 @@ function createHttpClient(baseUrl) {
|
|
|
1853
2467
|
const text = await res.text().catch(() => "");
|
|
1854
2468
|
throw new HttpError(
|
|
1855
2469
|
res.status,
|
|
1856
|
-
`${res.status} ${res.statusText} on POST ${
|
|
2470
|
+
`${res.status} ${res.statusText} on POST ${path9}: ${text}`,
|
|
1857
2471
|
text
|
|
1858
2472
|
);
|
|
1859
2473
|
}
|
|
@@ -1867,12 +2481,12 @@ function projectPath(project, suffix) {
|
|
|
1867
2481
|
}
|
|
1868
2482
|
async function runRootCause(client, input) {
|
|
1869
2483
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
1870
|
-
const
|
|
2484
|
+
const path9 = projectPath(
|
|
1871
2485
|
input.project,
|
|
1872
2486
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
1873
2487
|
);
|
|
1874
2488
|
try {
|
|
1875
|
-
const result = await client.get(
|
|
2489
|
+
const result = await client.get(path9);
|
|
1876
2490
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
1877
2491
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
1878
2492
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -1898,12 +2512,12 @@ async function runRootCause(client, input) {
|
|
|
1898
2512
|
}
|
|
1899
2513
|
async function runBlastRadius(client, input) {
|
|
1900
2514
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
1901
|
-
const
|
|
2515
|
+
const path9 = projectPath(
|
|
1902
2516
|
input.project,
|
|
1903
2517
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
1904
2518
|
);
|
|
1905
2519
|
try {
|
|
1906
|
-
const result = await client.get(
|
|
2520
|
+
const result = await client.get(path9);
|
|
1907
2521
|
if (result.totalAffected === 0) {
|
|
1908
2522
|
return {
|
|
1909
2523
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -1937,12 +2551,12 @@ function formatBlastEntry(n) {
|
|
|
1937
2551
|
}
|
|
1938
2552
|
async function runDependencies(client, input) {
|
|
1939
2553
|
const depth = input.depth ?? 3;
|
|
1940
|
-
const
|
|
2554
|
+
const path9 = projectPath(
|
|
1941
2555
|
input.project,
|
|
1942
2556
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
1943
2557
|
);
|
|
1944
2558
|
try {
|
|
1945
|
-
const result = await client.get(
|
|
2559
|
+
const result = await client.get(path9);
|
|
1946
2560
|
if (result.total === 0) {
|
|
1947
2561
|
return {
|
|
1948
2562
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -2023,9 +2637,9 @@ function formatDuration(ms) {
|
|
|
2023
2637
|
return `${Math.round(h / 24)}d`;
|
|
2024
2638
|
}
|
|
2025
2639
|
async function runIncidents(client, input) {
|
|
2026
|
-
const
|
|
2640
|
+
const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
2027
2641
|
try {
|
|
2028
|
-
const body = await client.get(
|
|
2642
|
+
const body = await client.get(path9);
|
|
2029
2643
|
const events = body.events;
|
|
2030
2644
|
if (events.length === 0) {
|
|
2031
2645
|
return {
|
|
@@ -2291,8 +2905,181 @@ function exitCodeForError(err) {
|
|
|
2291
2905
|
if (err instanceof HttpError) return 1;
|
|
2292
2906
|
return 1;
|
|
2293
2907
|
}
|
|
2908
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
2909
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
2910
|
+
}
|
|
2911
|
+
async function pushSnapshotToRemote(input) {
|
|
2912
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
2913
|
+
if (typeof client.post !== "function") {
|
|
2914
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
2915
|
+
}
|
|
2916
|
+
return client.post(
|
|
2917
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
2918
|
+
{ snapshot: input.snapshot }
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// src/cli-verbs.ts
|
|
2923
|
+
async function resolveProjectEntry(opts) {
|
|
2924
|
+
const entries = await listProjects();
|
|
2925
|
+
if (opts.project) {
|
|
2926
|
+
const match = entries.find((e) => e.name === opts.project);
|
|
2927
|
+
return match ?? null;
|
|
2928
|
+
}
|
|
2929
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2930
|
+
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
2931
|
+
for (const entry2 of entries) {
|
|
2932
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
|
|
2933
|
+
return entry2;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
async function checkDaemonHealth2(baseUrl) {
|
|
2939
|
+
try {
|
|
2940
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
2941
|
+
signal: AbortSignal.timeout(1500)
|
|
2942
|
+
});
|
|
2943
|
+
return res.ok;
|
|
2944
|
+
} catch {
|
|
2945
|
+
return false;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
function snapshotForGraph(persisted) {
|
|
2949
|
+
return {
|
|
2950
|
+
schemaVersion: 3,
|
|
2951
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2952
|
+
graph: persisted.graph.export()
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
function emitResult(result, json) {
|
|
2956
|
+
if (json) {
|
|
2957
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
|
|
2961
|
+
console.log(
|
|
2962
|
+
`${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
|
|
2963
|
+
);
|
|
2964
|
+
if (!result.apply.skipped) {
|
|
2965
|
+
const { instrumented, alreadyInstrumented, libOnly } = result.apply;
|
|
2966
|
+
console.log(
|
|
2967
|
+
`instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
2968
|
+
);
|
|
2969
|
+
} else {
|
|
2970
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
2971
|
+
}
|
|
2972
|
+
for (const warn of result.warnings) console.error(warn);
|
|
2973
|
+
}
|
|
2974
|
+
async function runSync(opts) {
|
|
2975
|
+
const entry2 = await resolveProjectEntry(opts);
|
|
2976
|
+
if (!entry2) {
|
|
2977
|
+
const target = opts.project ?? opts.cwd ?? process.cwd();
|
|
2978
|
+
console.error(
|
|
2979
|
+
`neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
|
|
2980
|
+
);
|
|
2981
|
+
return {
|
|
2982
|
+
exitCode: 1,
|
|
2983
|
+
project: opts.project ?? "",
|
|
2984
|
+
scanPath: target,
|
|
2985
|
+
nodesAdded: 0,
|
|
2986
|
+
edgesAdded: 0,
|
|
2987
|
+
snapshotPath: null,
|
|
2988
|
+
mode: opts.to ? "remote" : "local",
|
|
2989
|
+
daemon: "skipped",
|
|
2990
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
|
|
2991
|
+
warnings: []
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
const persisted = await extractAndPersist({
|
|
2995
|
+
scanPath: entry2.path,
|
|
2996
|
+
project: entry2.name,
|
|
2997
|
+
projectExplicit: true,
|
|
2998
|
+
dryRun: true
|
|
2999
|
+
});
|
|
3000
|
+
let snapshotPath = null;
|
|
3001
|
+
if (!opts.dryRun) {
|
|
3002
|
+
const target = persisted.snapshotPath;
|
|
3003
|
+
await saveGraphToDisk(persisted.graph, target);
|
|
3004
|
+
snapshotPath = target;
|
|
3005
|
+
}
|
|
3006
|
+
const skipApply = opts.dryRun || opts.noInstrument;
|
|
3007
|
+
const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
|
|
3008
|
+
const warnings = [];
|
|
3009
|
+
let daemonState = "skipped";
|
|
3010
|
+
let exitCode = 0;
|
|
3011
|
+
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
3012
|
+
if (!opts.dryRun) {
|
|
3013
|
+
const snapshot = snapshotForGraph(persisted);
|
|
3014
|
+
if (opts.to) {
|
|
3015
|
+
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
3016
|
+
try {
|
|
3017
|
+
await pushSnapshotToRemote({
|
|
3018
|
+
baseUrl: opts.to,
|
|
3019
|
+
token,
|
|
3020
|
+
project: entry2.name,
|
|
3021
|
+
snapshot
|
|
3022
|
+
});
|
|
3023
|
+
daemonState = "remote-ok";
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
if (err instanceof HttpError) {
|
|
3026
|
+
console.error(`neat sync: ${err.message}`);
|
|
3027
|
+
exitCode = 1;
|
|
3028
|
+
} else if (err instanceof TransportError) {
|
|
3029
|
+
console.error(`neat sync: ${err.message}`);
|
|
3030
|
+
exitCode = 3;
|
|
3031
|
+
} else {
|
|
3032
|
+
console.error(`neat sync: ${err.message}`);
|
|
3033
|
+
exitCode = 1;
|
|
3034
|
+
}
|
|
3035
|
+
daemonState = "skipped";
|
|
3036
|
+
}
|
|
3037
|
+
} else {
|
|
3038
|
+
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
3039
|
+
const healthy = await checkDaemonHealth2(daemonUrl);
|
|
3040
|
+
if (healthy) {
|
|
3041
|
+
try {
|
|
3042
|
+
await pushSnapshotToRemote({
|
|
3043
|
+
baseUrl: daemonUrl,
|
|
3044
|
+
token: process.env.NEAT_AUTH_TOKEN,
|
|
3045
|
+
project: entry2.name,
|
|
3046
|
+
snapshot
|
|
3047
|
+
});
|
|
3048
|
+
daemonState = "reloaded";
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
warnings.push(
|
|
3051
|
+
`neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
|
|
3052
|
+
);
|
|
3053
|
+
daemonState = "down";
|
|
3054
|
+
exitCode = 2;
|
|
3055
|
+
}
|
|
3056
|
+
} else {
|
|
3057
|
+
warnings.push(
|
|
3058
|
+
"neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
|
|
3059
|
+
);
|
|
3060
|
+
daemonState = "down";
|
|
3061
|
+
exitCode = 2;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const result = {
|
|
3066
|
+
exitCode,
|
|
3067
|
+
project: entry2.name,
|
|
3068
|
+
scanPath: entry2.path,
|
|
3069
|
+
nodesAdded: persisted.nodesAdded,
|
|
3070
|
+
edgesAdded: persisted.edgesAdded,
|
|
3071
|
+
snapshotPath,
|
|
3072
|
+
mode,
|
|
3073
|
+
daemon: daemonState,
|
|
3074
|
+
apply: { ...applyTally, skipped: skipApply },
|
|
3075
|
+
warnings
|
|
3076
|
+
};
|
|
3077
|
+
emitResult(result, opts.json);
|
|
3078
|
+
return result;
|
|
3079
|
+
}
|
|
2294
3080
|
|
|
2295
3081
|
// src/cli.ts
|
|
3082
|
+
import { DivergenceTypeSchema } from "@neat.is/types";
|
|
2296
3083
|
function usage() {
|
|
2297
3084
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
2298
3085
|
console.log("");
|
|
@@ -2313,6 +3100,8 @@ function usage() {
|
|
|
2313
3100
|
console.log(" uninstall <name>");
|
|
2314
3101
|
console.log(" Remove a project from the registry. Does not touch");
|
|
2315
3102
|
console.log(" neat-out/, policy.json, or any user file.");
|
|
3103
|
+
console.log(" version Print the installed @neat.is/core version and exit.");
|
|
3104
|
+
console.log(" Aliases: --version, -v.");
|
|
2316
3105
|
console.log(" skill Install or print the Claude Code MCP drop-in.");
|
|
2317
3106
|
console.log(" Flags:");
|
|
2318
3107
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
@@ -2320,6 +3109,15 @@ function usage() {
|
|
|
2320
3109
|
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
2321
3110
|
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
2322
3111
|
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
3112
|
+
console.log(" sync Re-run discovery, extraction, and SDK apply against the");
|
|
3113
|
+
console.log(" registered project, then notify the running daemon.");
|
|
3114
|
+
console.log(" Flags:");
|
|
3115
|
+
console.log(" --project <name> target a registered project by name");
|
|
3116
|
+
console.log(" --to <url> push the snapshot to a remote daemon");
|
|
3117
|
+
console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
|
|
3118
|
+
console.log(" --dry-run run extraction in-memory; do not write");
|
|
3119
|
+
console.log(" --no-instrument skip the SDK install apply step");
|
|
3120
|
+
console.log(" --json emit the delta summary as JSON");
|
|
2323
3121
|
console.log("");
|
|
2324
3122
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
2325
3123
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -2373,7 +3171,9 @@ var STRING_FLAGS = [
|
|
|
2373
3171
|
["--error-id", "errorId"],
|
|
2374
3172
|
["--hypothetical-action", "hypotheticalAction"],
|
|
2375
3173
|
["--type", "type"],
|
|
2376
|
-
["--min-confidence", "minConfidence"]
|
|
3174
|
+
["--min-confidence", "minConfidence"],
|
|
3175
|
+
["--to", "to"],
|
|
3176
|
+
["--token", "token"]
|
|
2377
3177
|
];
|
|
2378
3178
|
function parseArgs(rest) {
|
|
2379
3179
|
const positional = [];
|
|
@@ -2398,6 +3198,8 @@ function parseArgs(rest) {
|
|
|
2398
3198
|
hypotheticalAction: null,
|
|
2399
3199
|
type: null,
|
|
2400
3200
|
minConfidence: null,
|
|
3201
|
+
to: null,
|
|
3202
|
+
token: null,
|
|
2401
3203
|
positional: []
|
|
2402
3204
|
};
|
|
2403
3205
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -2426,7 +3228,7 @@ function parseArgs(rest) {
|
|
|
2426
3228
|
out.yes = true;
|
|
2427
3229
|
continue;
|
|
2428
3230
|
}
|
|
2429
|
-
if (arg === "--verbose"
|
|
3231
|
+
if (arg === "--verbose") {
|
|
2430
3232
|
out.verbose = true;
|
|
2431
3233
|
continue;
|
|
2432
3234
|
}
|
|
@@ -2485,6 +3287,28 @@ function assignFlag(out, field, value) {
|
|
|
2485
3287
|
;
|
|
2486
3288
|
out[field] = value;
|
|
2487
3289
|
}
|
|
3290
|
+
function readPackageVersion() {
|
|
3291
|
+
const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
|
|
3292
|
+
const candidates = [
|
|
3293
|
+
path8.resolve(here, "../package.json"),
|
|
3294
|
+
path8.resolve(here, "../../package.json")
|
|
3295
|
+
];
|
|
3296
|
+
for (const candidate of candidates) {
|
|
3297
|
+
try {
|
|
3298
|
+
const raw = readFileSync(candidate, "utf8");
|
|
3299
|
+
const parsed = JSON.parse(raw);
|
|
3300
|
+
if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
|
|
3301
|
+
return parsed.version;
|
|
3302
|
+
}
|
|
3303
|
+
} catch {
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return "unknown";
|
|
3307
|
+
}
|
|
3308
|
+
function printVersion() {
|
|
3309
|
+
process.stdout.write(`${readPackageVersion()}
|
|
3310
|
+
`);
|
|
3311
|
+
}
|
|
2488
3312
|
function printBanner() {
|
|
2489
3313
|
console.log("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557");
|
|
2490
3314
|
console.log("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D");
|
|
@@ -2494,7 +3318,7 @@ function printBanner() {
|
|
|
2494
3318
|
console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
|
|
2495
3319
|
console.log("");
|
|
2496
3320
|
console.log(" Network Expressive Architecting Tool");
|
|
2497
|
-
console.log(" neat.is \xB7 v0.
|
|
3321
|
+
console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
|
|
2498
3322
|
console.log("");
|
|
2499
3323
|
}
|
|
2500
3324
|
function printDiscoveryReport(opts, services) {
|
|
@@ -2522,12 +3346,12 @@ function printDiscoveryReport(opts, services) {
|
|
|
2522
3346
|
}
|
|
2523
3347
|
console.log("");
|
|
2524
3348
|
}
|
|
2525
|
-
async function buildPatchSections(services) {
|
|
3349
|
+
async function buildPatchSections(services, project) {
|
|
2526
3350
|
const sections = [];
|
|
2527
3351
|
for (const svc of services) {
|
|
2528
3352
|
const installer = await pickInstaller(svc.dir);
|
|
2529
3353
|
if (!installer) continue;
|
|
2530
|
-
const plan3 = await installer.plan(svc.dir);
|
|
3354
|
+
const plan3 = await installer.plan(svc.dir, { project });
|
|
2531
3355
|
if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
|
|
2532
3356
|
sections.push({ installer: installer.name, plan: plan3 });
|
|
2533
3357
|
}
|
|
@@ -2542,14 +3366,14 @@ async function runInit(opts) {
|
|
|
2542
3366
|
}
|
|
2543
3367
|
const services = await discoverServices(opts.scanPath);
|
|
2544
3368
|
printDiscoveryReport(opts, services);
|
|
2545
|
-
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
3369
|
+
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
2546
3370
|
const patch = renderPatch(sections);
|
|
2547
|
-
const patchPath =
|
|
3371
|
+
const patchPath = path8.join(opts.scanPath, "neat.patch");
|
|
2548
3372
|
if (opts.dryRun) {
|
|
2549
3373
|
await fs7.writeFile(patchPath, patch, "utf8");
|
|
2550
3374
|
written.push(patchPath);
|
|
2551
3375
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
2552
|
-
const gitignorePath =
|
|
3376
|
+
const gitignorePath = path8.join(opts.scanPath, ".gitignore");
|
|
2553
3377
|
const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
|
|
2554
3378
|
const verb = gitignoreExists ? "append" : "create";
|
|
2555
3379
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
@@ -2561,9 +3385,9 @@ async function runInit(opts) {
|
|
|
2561
3385
|
const graph = getGraph(graphKey);
|
|
2562
3386
|
const projectPaths = pathsForProject(
|
|
2563
3387
|
graphKey,
|
|
2564
|
-
|
|
3388
|
+
path8.join(opts.scanPath, "neat-out")
|
|
2565
3389
|
);
|
|
2566
|
-
const errorsPath =
|
|
3390
|
+
const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
|
|
2567
3391
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
2568
3392
|
await saveGraphToDisk(graph, opts.outPath);
|
|
2569
3393
|
written.push(opts.outPath);
|
|
@@ -2653,9 +3477,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
2653
3477
|
};
|
|
2654
3478
|
function claudeConfigPath() {
|
|
2655
3479
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
2656
|
-
if (override && override.length > 0) return
|
|
3480
|
+
if (override && override.length > 0) return path8.resolve(override);
|
|
2657
3481
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2658
|
-
return
|
|
3482
|
+
return path8.join(home, ".claude.json");
|
|
2659
3483
|
}
|
|
2660
3484
|
async function runSkill(opts) {
|
|
2661
3485
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -2679,7 +3503,7 @@ async function runSkill(opts) {
|
|
|
2679
3503
|
...existing,
|
|
2680
3504
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
2681
3505
|
};
|
|
2682
|
-
await fs7.mkdir(
|
|
3506
|
+
await fs7.mkdir(path8.dirname(target), { recursive: true });
|
|
2683
3507
|
await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2684
3508
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
2685
3509
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
@@ -2700,6 +3524,10 @@ async function main() {
|
|
|
2700
3524
|
usage();
|
|
2701
3525
|
process.exit(0);
|
|
2702
3526
|
}
|
|
3527
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
3528
|
+
printVersion();
|
|
3529
|
+
process.exit(0);
|
|
3530
|
+
}
|
|
2703
3531
|
const parsed = parseArgs(rest);
|
|
2704
3532
|
const { positional, apply: apply3, dryRun, noInstall } = parsed;
|
|
2705
3533
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
@@ -2714,12 +3542,12 @@ async function main() {
|
|
|
2714
3542
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
2715
3543
|
process.exit(2);
|
|
2716
3544
|
}
|
|
2717
|
-
const scanPath =
|
|
3545
|
+
const scanPath = path8.resolve(target);
|
|
2718
3546
|
const projectExplicit = parsed.project !== null;
|
|
2719
|
-
const projectName = projectExplicit ? project :
|
|
3547
|
+
const projectName = projectExplicit ? project : path8.basename(scanPath);
|
|
2720
3548
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
2721
|
-
const fallback = pathsForProject(projectKey,
|
|
2722
|
-
const outPath =
|
|
3549
|
+
const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
|
|
3550
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
2723
3551
|
const result = await runInit({
|
|
2724
3552
|
scanPath,
|
|
2725
3553
|
outPath,
|
|
@@ -2740,21 +3568,21 @@ async function main() {
|
|
|
2740
3568
|
usage();
|
|
2741
3569
|
process.exit(2);
|
|
2742
3570
|
}
|
|
2743
|
-
const scanPath =
|
|
3571
|
+
const scanPath = path8.resolve(target);
|
|
2744
3572
|
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
2745
3573
|
if (!stat || !stat.isDirectory()) {
|
|
2746
3574
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
2747
3575
|
process.exit(2);
|
|
2748
3576
|
}
|
|
2749
|
-
const projectPaths = pathsForProject(project,
|
|
2750
|
-
const outPath =
|
|
2751
|
-
const errorsPath =
|
|
2752
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
3577
|
+
const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
|
|
3578
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
3579
|
+
const errorsPath = path8.resolve(
|
|
3580
|
+
process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
|
|
2753
3581
|
);
|
|
2754
|
-
const staleEventsPath =
|
|
2755
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
3582
|
+
const staleEventsPath = path8.resolve(
|
|
3583
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
|
|
2756
3584
|
);
|
|
2757
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
3585
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
2758
3586
|
const handle = await startWatch(getGraph(project), {
|
|
2759
3587
|
scanPath,
|
|
2760
3588
|
outPath,
|
|
@@ -2873,6 +3701,18 @@ async function main() {
|
|
|
2873
3701
|
console.log(` ${artifact.startCommand}`);
|
|
2874
3702
|
return;
|
|
2875
3703
|
}
|
|
3704
|
+
if (cmd === "sync") {
|
|
3705
|
+
const result = await runSync({
|
|
3706
|
+
...parsed.project ? { project: parsed.project } : {},
|
|
3707
|
+
...parsed.to ? { to: parsed.to } : {},
|
|
3708
|
+
...parsed.token ? { token: parsed.token } : {},
|
|
3709
|
+
dryRun: parsed.dryRun,
|
|
3710
|
+
noInstrument: parsed.noInstrument,
|
|
3711
|
+
json: parsed.json
|
|
3712
|
+
});
|
|
3713
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
2876
3716
|
if (QUERY_VERBS.has(cmd)) {
|
|
2877
3717
|
const code = await runQueryVerb(cmd, parsed);
|
|
2878
3718
|
if (code !== 0) process.exit(code);
|
|
@@ -2888,11 +3728,11 @@ async function main() {
|
|
|
2888
3728
|
process.exit(1);
|
|
2889
3729
|
}
|
|
2890
3730
|
async function tryOrchestrator(cmd, parsed) {
|
|
2891
|
-
const scanPath =
|
|
3731
|
+
const scanPath = path8.resolve(cmd);
|
|
2892
3732
|
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
2893
3733
|
if (!stat || !stat.isDirectory()) return null;
|
|
2894
3734
|
const projectExplicit = parsed.project !== null;
|
|
2895
|
-
const projectName = projectExplicit ? parsed.project :
|
|
3735
|
+
const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
|
|
2896
3736
|
const result = await runOrchestrator({
|
|
2897
3737
|
scanPath,
|
|
2898
3738
|
project: projectName,
|
|
@@ -3093,6 +3933,7 @@ export {
|
|
|
3093
3933
|
CLAUDE_SKILL_CONFIG,
|
|
3094
3934
|
QUERY_VERBS,
|
|
3095
3935
|
parseArgs,
|
|
3936
|
+
readPackageVersion,
|
|
3096
3937
|
runInit,
|
|
3097
3938
|
runQueryVerb,
|
|
3098
3939
|
runSkill
|