@neat.is/core 0.3.7 → 0.3.8
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-75IBA4UL.js → chunk-7TYESDAI.js} +69 -2
- package/dist/chunk-7TYESDAI.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-CZ3T6TE2.js} +251 -245
- package/dist/{chunk-ZU2RQRCN.js.map → chunk-CZ3T6TE2.js.map} +1 -1
- package/dist/{chunk-EDHOCFOG.js → chunk-LQ3JFBTX.js} +15 -5
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1185 -405
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +879 -156
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +98 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +111 -3
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +110 -6
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +25 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-S3AENOZ6.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -41,6 +41,43 @@ var init_cjs_shims = __esm({
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// src/auth.ts
|
|
45
|
+
function mountBearerAuth(app, opts) {
|
|
46
|
+
if (!opts.token || opts.token.length === 0) return;
|
|
47
|
+
if (opts.trustProxy) return;
|
|
48
|
+
const expected = Buffer.from(opts.token, "utf8");
|
|
49
|
+
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
50
|
+
app.addHook("preHandler", (req, reply, done) => {
|
|
51
|
+
const path44 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
52
|
+
for (const suffix of suffixes) {
|
|
53
|
+
if (path44 === suffix || path44.endsWith(suffix)) {
|
|
54
|
+
done();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const header = req.headers.authorization;
|
|
59
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
60
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
64
|
+
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
65
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
done();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
var import_node_crypto, DEFAULT_UNAUTH_SUFFIXES;
|
|
72
|
+
var init_auth = __esm({
|
|
73
|
+
"src/auth.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
init_cjs_shims();
|
|
76
|
+
import_node_crypto = require("crypto");
|
|
77
|
+
DEFAULT_UNAUTH_SUFFIXES = ["/health", "/healthz", "/readyz"];
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
44
81
|
// src/otel-grpc.ts
|
|
45
82
|
var otel_grpc_exports = {};
|
|
46
83
|
__export(otel_grpc_exports, {
|
|
@@ -101,8 +138,8 @@ function reshapeGrpcRequest(req) {
|
|
|
101
138
|
};
|
|
102
139
|
}
|
|
103
140
|
function resolveProtoRoot() {
|
|
104
|
-
const here =
|
|
105
|
-
return
|
|
141
|
+
const here = import_node_path35.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
142
|
+
return import_node_path35.default.resolve(here, "..", "proto");
|
|
106
143
|
}
|
|
107
144
|
function loadTraceService() {
|
|
108
145
|
const protoRoot = resolveProtoRoot();
|
|
@@ -123,8 +160,21 @@ function loadTraceService() {
|
|
|
123
160
|
async function startOtelGrpcReceiver(opts) {
|
|
124
161
|
const server = new grpc.Server();
|
|
125
162
|
const service = loadTraceService();
|
|
163
|
+
const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
|
|
164
|
+
const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
|
|
126
165
|
server.addService(service, {
|
|
127
166
|
Export: (call, callback) => {
|
|
167
|
+
if (requiresAuth) {
|
|
168
|
+
const meta = call.metadata.get("authorization");
|
|
169
|
+
const got = meta.length > 0 ? String(meta[0]) : "";
|
|
170
|
+
const a = Buffer.from(got, "utf8");
|
|
171
|
+
const b = Buffer.from(expectedHeader, "utf8");
|
|
172
|
+
const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
173
|
+
if (!ok) {
|
|
174
|
+
callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
128
178
|
void (async () => {
|
|
129
179
|
try {
|
|
130
180
|
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
@@ -157,13 +207,14 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
157
207
|
})
|
|
158
208
|
};
|
|
159
209
|
}
|
|
160
|
-
var import_node_url,
|
|
210
|
+
var import_node_url, import_node_path35, import_node_crypto2, grpc, protoLoader;
|
|
161
211
|
var init_otel_grpc = __esm({
|
|
162
212
|
"src/otel-grpc.ts"() {
|
|
163
213
|
"use strict";
|
|
164
214
|
init_cjs_shims();
|
|
165
215
|
import_node_url = require("url");
|
|
166
|
-
|
|
216
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
217
|
+
import_node_crypto2 = require("crypto");
|
|
167
218
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
168
219
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
169
220
|
init_otel();
|
|
@@ -259,10 +310,10 @@ function parseOtlpRequest(body) {
|
|
|
259
310
|
return out;
|
|
260
311
|
}
|
|
261
312
|
function loadProtoRoot() {
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
313
|
+
const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
314
|
+
const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
|
|
264
315
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
316
|
+
root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
|
|
266
317
|
root.loadSync(
|
|
267
318
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
319
|
{ keepCase: true }
|
|
@@ -304,6 +355,7 @@ async function buildOtelReceiver(opts) {
|
|
|
304
355
|
logger: false,
|
|
305
356
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
306
357
|
});
|
|
358
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
307
359
|
const queue = [];
|
|
308
360
|
let draining = false;
|
|
309
361
|
let drainPromise = Promise.resolve();
|
|
@@ -382,15 +434,16 @@ async function buildOtelReceiver(opts) {
|
|
|
382
434
|
};
|
|
383
435
|
return decorated;
|
|
384
436
|
}
|
|
385
|
-
var
|
|
437
|
+
var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
386
438
|
var init_otel = __esm({
|
|
387
439
|
"src/otel.ts"() {
|
|
388
440
|
"use strict";
|
|
389
441
|
init_cjs_shims();
|
|
390
|
-
|
|
442
|
+
import_node_path36 = __toESM(require("path"), 1);
|
|
391
443
|
import_node_url2 = require("url");
|
|
392
444
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
393
445
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
446
|
+
init_auth();
|
|
394
447
|
exportTraceServiceRequestType = null;
|
|
395
448
|
exportTraceServiceResponseType = null;
|
|
396
449
|
cachedProtobufResponseBody = null;
|
|
@@ -409,8 +462,8 @@ __export(cli_exports, {
|
|
|
409
462
|
});
|
|
410
463
|
module.exports = __toCommonJS(cli_exports);
|
|
411
464
|
init_cjs_shims();
|
|
412
|
-
var
|
|
413
|
-
var
|
|
465
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
466
|
+
var import_node_fs28 = require("fs");
|
|
414
467
|
|
|
415
468
|
// src/graph.ts
|
|
416
469
|
init_cjs_shims();
|
|
@@ -920,19 +973,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
920
973
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
921
974
|
let best = { path: [start], edges: [] };
|
|
922
975
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
923
|
-
function step(node,
|
|
924
|
-
if (
|
|
925
|
-
best = { path: [...
|
|
976
|
+
function step(node, path44, edges) {
|
|
977
|
+
if (path44.length > best.path.length) {
|
|
978
|
+
best = { path: [...path44], edges: [...edges] };
|
|
926
979
|
}
|
|
927
|
-
if (
|
|
980
|
+
if (path44.length - 1 >= maxDepth) return;
|
|
928
981
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
929
982
|
for (const [srcId, edge] of incoming) {
|
|
930
983
|
if (visited.has(srcId)) continue;
|
|
931
984
|
visited.add(srcId);
|
|
932
|
-
|
|
985
|
+
path44.push(srcId);
|
|
933
986
|
edges.push(edge);
|
|
934
|
-
step(srcId,
|
|
935
|
-
|
|
987
|
+
step(srcId, path44, edges);
|
|
988
|
+
path44.pop();
|
|
936
989
|
edges.pop();
|
|
937
990
|
visited.delete(srcId);
|
|
938
991
|
}
|
|
@@ -4175,128 +4228,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4175
4228
|
return result;
|
|
4176
4229
|
}
|
|
4177
4230
|
|
|
4178
|
-
// src/persist.ts
|
|
4179
|
-
init_cjs_shims();
|
|
4180
|
-
var import_node_fs18 = require("fs");
|
|
4181
|
-
var import_node_path31 = __toESM(require("path"), 1);
|
|
4182
|
-
var import_types19 = require("@neat.is/types");
|
|
4183
|
-
var SCHEMA_VERSION = 3;
|
|
4184
|
-
function migrateV1ToV2(payload) {
|
|
4185
|
-
const nodes = payload.graph.nodes;
|
|
4186
|
-
if (Array.isArray(nodes)) {
|
|
4187
|
-
for (const node of nodes) {
|
|
4188
|
-
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4189
|
-
delete node.attributes.pgDriverVersion;
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
return { ...payload, schemaVersion: 2 };
|
|
4194
|
-
}
|
|
4195
|
-
function migrateV2ToV3(payload) {
|
|
4196
|
-
const edges = payload.graph.edges;
|
|
4197
|
-
if (Array.isArray(edges)) {
|
|
4198
|
-
for (const edge of edges) {
|
|
4199
|
-
const attrs = edge.attributes;
|
|
4200
|
-
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4201
|
-
attrs.provenance = import_types19.Provenance.OBSERVED;
|
|
4202
|
-
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4203
|
-
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4204
|
-
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4205
|
-
if (type && source && target) {
|
|
4206
|
-
const newId = (0, import_types19.observedEdgeId)(source, target, type);
|
|
4207
|
-
attrs.id = newId;
|
|
4208
|
-
if (edge.key) edge.key = newId;
|
|
4209
|
-
}
|
|
4210
|
-
}
|
|
4211
|
-
}
|
|
4212
|
-
return { ...payload, schemaVersion: 3 };
|
|
4213
|
-
}
|
|
4214
|
-
async function ensureDir(filePath) {
|
|
4215
|
-
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4216
|
-
}
|
|
4217
|
-
async function saveGraphToDisk(graph, outPath) {
|
|
4218
|
-
await ensureDir(outPath);
|
|
4219
|
-
const payload = {
|
|
4220
|
-
schemaVersion: SCHEMA_VERSION,
|
|
4221
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4222
|
-
graph: graph.export()
|
|
4223
|
-
};
|
|
4224
|
-
const tmp = `${outPath}.tmp`;
|
|
4225
|
-
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4226
|
-
await import_node_fs18.promises.rename(tmp, outPath);
|
|
4227
|
-
}
|
|
4228
|
-
async function loadGraphFromDisk(graph, outPath) {
|
|
4229
|
-
let raw;
|
|
4230
|
-
try {
|
|
4231
|
-
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
4232
|
-
} catch (err) {
|
|
4233
|
-
if (err.code === "ENOENT") return;
|
|
4234
|
-
throw err;
|
|
4235
|
-
}
|
|
4236
|
-
let payload = JSON.parse(raw);
|
|
4237
|
-
if (payload.schemaVersion === 1) {
|
|
4238
|
-
payload = migrateV1ToV2(payload);
|
|
4239
|
-
}
|
|
4240
|
-
if (payload.schemaVersion === 2) {
|
|
4241
|
-
payload = migrateV2ToV3(payload);
|
|
4242
|
-
}
|
|
4243
|
-
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4244
|
-
throw new Error(
|
|
4245
|
-
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4246
|
-
);
|
|
4247
|
-
}
|
|
4248
|
-
graph.clear();
|
|
4249
|
-
graph.import(payload.graph);
|
|
4250
|
-
}
|
|
4251
|
-
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4252
|
-
let stopped = false;
|
|
4253
|
-
const tick = async () => {
|
|
4254
|
-
if (stopped) return;
|
|
4255
|
-
try {
|
|
4256
|
-
await saveGraphToDisk(graph, outPath);
|
|
4257
|
-
} catch (err) {
|
|
4258
|
-
console.error("persist: periodic save failed", err);
|
|
4259
|
-
}
|
|
4260
|
-
};
|
|
4261
|
-
const interval = setInterval(() => {
|
|
4262
|
-
void tick();
|
|
4263
|
-
}, intervalMs);
|
|
4264
|
-
const onSignal = (signal) => {
|
|
4265
|
-
void (async () => {
|
|
4266
|
-
try {
|
|
4267
|
-
await saveGraphToDisk(graph, outPath);
|
|
4268
|
-
} catch (err) {
|
|
4269
|
-
console.error(`persist: ${signal} save failed`, err);
|
|
4270
|
-
} finally {
|
|
4271
|
-
process.exit(0);
|
|
4272
|
-
}
|
|
4273
|
-
})();
|
|
4274
|
-
};
|
|
4275
|
-
process.on("SIGTERM", onSignal);
|
|
4276
|
-
process.on("SIGINT", onSignal);
|
|
4277
|
-
return () => {
|
|
4278
|
-
stopped = true;
|
|
4279
|
-
clearInterval(interval);
|
|
4280
|
-
process.off("SIGTERM", onSignal);
|
|
4281
|
-
process.off("SIGINT", onSignal);
|
|
4282
|
-
};
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
// src/watch.ts
|
|
4286
|
-
init_cjs_shims();
|
|
4287
|
-
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
4288
|
-
var import_node_path37 = __toESM(require("path"), 1);
|
|
4289
|
-
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4290
|
-
|
|
4291
|
-
// src/api.ts
|
|
4292
|
-
init_cjs_shims();
|
|
4293
|
-
var import_fastify = __toESM(require("fastify"), 1);
|
|
4294
|
-
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4295
|
-
var import_types22 = require("@neat.is/types");
|
|
4296
|
-
|
|
4297
4231
|
// src/divergences.ts
|
|
4298
4232
|
init_cjs_shims();
|
|
4299
|
-
var
|
|
4233
|
+
var import_types19 = require("@neat.is/types");
|
|
4300
4234
|
function bucketKey(source, target, type) {
|
|
4301
4235
|
return `${type}|${source}|${target}`;
|
|
4302
4236
|
}
|
|
@@ -4304,22 +4238,22 @@ function bucketEdges(graph) {
|
|
|
4304
4238
|
const buckets = /* @__PURE__ */ new Map();
|
|
4305
4239
|
graph.forEachEdge((id, attrs) => {
|
|
4306
4240
|
const e = attrs;
|
|
4307
|
-
const parsed = (0,
|
|
4241
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
4308
4242
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4309
4243
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4310
4244
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4311
4245
|
switch (provenance) {
|
|
4312
|
-
case
|
|
4246
|
+
case import_types19.Provenance.EXTRACTED:
|
|
4313
4247
|
cur.extracted = e;
|
|
4314
4248
|
break;
|
|
4315
|
-
case
|
|
4249
|
+
case import_types19.Provenance.OBSERVED:
|
|
4316
4250
|
cur.observed = e;
|
|
4317
4251
|
break;
|
|
4318
|
-
case
|
|
4252
|
+
case import_types19.Provenance.INFERRED:
|
|
4319
4253
|
cur.inferred = e;
|
|
4320
4254
|
break;
|
|
4321
4255
|
default:
|
|
4322
|
-
if (e.provenance ===
|
|
4256
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
4323
4257
|
}
|
|
4324
4258
|
buckets.set(key, cur);
|
|
4325
4259
|
});
|
|
@@ -4328,7 +4262,7 @@ function bucketEdges(graph) {
|
|
|
4328
4262
|
function nodeIsFrontier(graph, nodeId) {
|
|
4329
4263
|
if (!graph.hasNode(nodeId)) return false;
|
|
4330
4264
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4331
|
-
return attrs.type ===
|
|
4265
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
4332
4266
|
}
|
|
4333
4267
|
function clampConfidence(n) {
|
|
4334
4268
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4389,7 +4323,7 @@ function declaredHostFor(svc) {
|
|
|
4389
4323
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4390
4324
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4391
4325
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4392
|
-
if (e.type ===
|
|
4326
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4393
4327
|
return true;
|
|
4394
4328
|
}
|
|
4395
4329
|
}
|
|
@@ -4402,10 +4336,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4402
4336
|
const out = [];
|
|
4403
4337
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4404
4338
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4405
|
-
if (edge.type !==
|
|
4406
|
-
if (edge.provenance !==
|
|
4339
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4340
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4407
4341
|
const target = graph.getNodeAttributes(edge.target);
|
|
4408
|
-
if (target.type !==
|
|
4342
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4409
4343
|
const observedHost = target.host?.trim();
|
|
4410
4344
|
if (!observedHost) continue;
|
|
4411
4345
|
if (observedHost === declaredHost) continue;
|
|
@@ -4427,10 +4361,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4427
4361
|
const deps = svc.dependencies ?? {};
|
|
4428
4362
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4429
4363
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4430
|
-
if (edge.type !==
|
|
4431
|
-
if (edge.provenance !==
|
|
4364
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4365
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4432
4366
|
const target = graph.getNodeAttributes(edge.target);
|
|
4433
|
-
if (target.type !==
|
|
4367
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4434
4368
|
for (const pair of compatPairs()) {
|
|
4435
4369
|
if (pair.engine !== target.engine) continue;
|
|
4436
4370
|
const declared = deps[pair.driver];
|
|
@@ -4491,7 +4425,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4491
4425
|
}
|
|
4492
4426
|
graph.forEachNode((nodeId, attrs) => {
|
|
4493
4427
|
const n = attrs;
|
|
4494
|
-
if (n.type !==
|
|
4428
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4495
4429
|
const svc = n;
|
|
4496
4430
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4497
4431
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4524,85 +4458,336 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4524
4458
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4525
4459
|
return a.target.localeCompare(b.target);
|
|
4526
4460
|
});
|
|
4527
|
-
return
|
|
4461
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4528
4462
|
divergences: filtered,
|
|
4529
4463
|
totalAffected: filtered.length,
|
|
4530
4464
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4531
4465
|
});
|
|
4532
4466
|
}
|
|
4533
4467
|
|
|
4534
|
-
// src/
|
|
4468
|
+
// src/persist.ts
|
|
4535
4469
|
init_cjs_shims();
|
|
4536
|
-
var
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4470
|
+
var import_node_fs18 = require("fs");
|
|
4471
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4472
|
+
var import_types20 = require("@neat.is/types");
|
|
4473
|
+
var SCHEMA_VERSION = 3;
|
|
4474
|
+
function migrateV1ToV2(payload) {
|
|
4475
|
+
const nodes = payload.graph.nodes;
|
|
4476
|
+
if (Array.isArray(nodes)) {
|
|
4477
|
+
for (const node of nodes) {
|
|
4478
|
+
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4479
|
+
delete node.attributes.pgDriverVersion;
|
|
4480
|
+
}
|
|
4542
4481
|
}
|
|
4543
|
-
return await res.json();
|
|
4544
4482
|
}
|
|
4545
|
-
|
|
4546
|
-
return JSON.parse(raw);
|
|
4483
|
+
return { ...payload, schemaVersion: 2 };
|
|
4547
4484
|
}
|
|
4548
|
-
function
|
|
4549
|
-
const
|
|
4550
|
-
if (
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4485
|
+
function migrateV2ToV3(payload) {
|
|
4486
|
+
const edges = payload.graph.edges;
|
|
4487
|
+
if (Array.isArray(edges)) {
|
|
4488
|
+
for (const edge of edges) {
|
|
4489
|
+
const attrs = edge.attributes;
|
|
4490
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4491
|
+
attrs.provenance = import_types20.Provenance.OBSERVED;
|
|
4492
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4493
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4494
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4495
|
+
if (type && source && target) {
|
|
4496
|
+
const newId = (0, import_types20.observedEdgeId)(source, target, type);
|
|
4497
|
+
attrs.id = newId;
|
|
4498
|
+
if (edge.key) edge.key = newId;
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4555
4501
|
}
|
|
4556
|
-
return
|
|
4502
|
+
return { ...payload, schemaVersion: 3 };
|
|
4557
4503
|
}
|
|
4558
|
-
function
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
const
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
current: { exportedAt: currentExportedAt },
|
|
4568
|
-
added: { nodes: [], edges: [] },
|
|
4569
|
-
removed: { nodes: [], edges: [] },
|
|
4570
|
-
changed: { nodes: [], edges: [] }
|
|
4504
|
+
async function ensureDir(filePath) {
|
|
4505
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4506
|
+
}
|
|
4507
|
+
async function saveGraphToDisk(graph, outPath) {
|
|
4508
|
+
await ensureDir(outPath);
|
|
4509
|
+
const payload = {
|
|
4510
|
+
schemaVersion: SCHEMA_VERSION,
|
|
4511
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4512
|
+
graph: graph.export()
|
|
4571
4513
|
};
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4514
|
+
const tmp = `${outPath}.tmp`;
|
|
4515
|
+
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4516
|
+
await import_node_fs18.promises.rename(tmp, outPath);
|
|
4517
|
+
}
|
|
4518
|
+
async function loadGraphFromDisk(graph, outPath) {
|
|
4519
|
+
let raw;
|
|
4520
|
+
try {
|
|
4521
|
+
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
4522
|
+
} catch (err) {
|
|
4523
|
+
if (err.code === "ENOENT") return;
|
|
4524
|
+
throw err;
|
|
4579
4525
|
}
|
|
4580
|
-
|
|
4581
|
-
|
|
4526
|
+
let payload = JSON.parse(raw);
|
|
4527
|
+
if (payload.schemaVersion === 1) {
|
|
4528
|
+
payload = migrateV1ToV2(payload);
|
|
4582
4529
|
}
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
if (!before) {
|
|
4586
|
-
result.added.edges.push(after);
|
|
4587
|
-
} else if (!shallowEqual(before, after)) {
|
|
4588
|
-
result.changed.edges.push({ id, before, after });
|
|
4589
|
-
}
|
|
4530
|
+
if (payload.schemaVersion === 2) {
|
|
4531
|
+
payload = migrateV2ToV3(payload);
|
|
4590
4532
|
}
|
|
4591
|
-
|
|
4592
|
-
|
|
4533
|
+
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4534
|
+
throw new Error(
|
|
4535
|
+
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4536
|
+
);
|
|
4593
4537
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
function shallowEqual(a, b) {
|
|
4597
|
-
return canonicalJson(a) === canonicalJson(b);
|
|
4538
|
+
graph.clear();
|
|
4539
|
+
graph.import(payload.graph);
|
|
4598
4540
|
}
|
|
4599
|
-
function
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4541
|
+
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4542
|
+
let stopped = false;
|
|
4543
|
+
const tick = async () => {
|
|
4544
|
+
if (stopped) return;
|
|
4545
|
+
try {
|
|
4546
|
+
await saveGraphToDisk(graph, outPath);
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
console.error("persist: periodic save failed", err);
|
|
4549
|
+
}
|
|
4550
|
+
};
|
|
4551
|
+
const interval = setInterval(() => {
|
|
4552
|
+
void tick();
|
|
4553
|
+
}, intervalMs);
|
|
4554
|
+
const onSignal = (signal) => {
|
|
4555
|
+
void (async () => {
|
|
4556
|
+
try {
|
|
4557
|
+
await saveGraphToDisk(graph, outPath);
|
|
4558
|
+
} catch (err) {
|
|
4559
|
+
console.error(`persist: ${signal} save failed`, err);
|
|
4560
|
+
} finally {
|
|
4561
|
+
process.exit(0);
|
|
4562
|
+
}
|
|
4563
|
+
})();
|
|
4564
|
+
};
|
|
4565
|
+
process.on("SIGTERM", onSignal);
|
|
4566
|
+
process.on("SIGINT", onSignal);
|
|
4567
|
+
return () => {
|
|
4568
|
+
stopped = true;
|
|
4569
|
+
clearInterval(interval);
|
|
4570
|
+
process.off("SIGTERM", onSignal);
|
|
4571
|
+
process.off("SIGINT", onSignal);
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
// src/gitignore.ts
|
|
4576
|
+
init_cjs_shims();
|
|
4577
|
+
var import_node_fs19 = require("fs");
|
|
4578
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4579
|
+
var NEAT_OUT_LINE = "neat-out/";
|
|
4580
|
+
var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
|
|
4581
|
+
function isNeatOutLine(line) {
|
|
4582
|
+
const trimmed = line.trim();
|
|
4583
|
+
return trimmed === "neat-out/" || trimmed === "neat-out";
|
|
4584
|
+
}
|
|
4585
|
+
async function ensureNeatOutIgnored(projectDir) {
|
|
4586
|
+
const file = import_node_path32.default.join(projectDir, ".gitignore");
|
|
4587
|
+
let existing = null;
|
|
4588
|
+
try {
|
|
4589
|
+
existing = await import_node_fs19.promises.readFile(file, "utf8");
|
|
4590
|
+
} catch (err) {
|
|
4591
|
+
if (err.code !== "ENOENT") throw err;
|
|
4592
|
+
}
|
|
4593
|
+
if (existing === null) {
|
|
4594
|
+
await import_node_fs19.promises.writeFile(file, `${NEAT_HEADER}
|
|
4595
|
+
${NEAT_OUT_LINE}
|
|
4596
|
+
`, "utf8");
|
|
4597
|
+
return { action: "created", file };
|
|
4598
|
+
}
|
|
4599
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
4600
|
+
if (isNeatOutLine(line)) return { action: "unchanged", file };
|
|
4601
|
+
}
|
|
4602
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4603
|
+
const appended = `${needsLeadingNewline ? "\n" : ""}
|
|
4604
|
+
${NEAT_HEADER}
|
|
4605
|
+
${NEAT_OUT_LINE}
|
|
4606
|
+
`;
|
|
4607
|
+
await import_node_fs19.promises.writeFile(file, existing + appended, "utf8");
|
|
4608
|
+
return { action: "added", file };
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
// src/summary.ts
|
|
4612
|
+
init_cjs_shims();
|
|
4613
|
+
var import_types21 = require("@neat.is/types");
|
|
4614
|
+
function renderOtelEnvBlock() {
|
|
4615
|
+
return [
|
|
4616
|
+
"for prod OTel routing, set these in your deploy platform's env:",
|
|
4617
|
+
" OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
|
|
4618
|
+
" OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
|
|
4619
|
+
].join("\n");
|
|
4620
|
+
}
|
|
4621
|
+
function findIncompatServices(nodes) {
|
|
4622
|
+
return nodes.filter(
|
|
4623
|
+
(n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
function servicesWithoutObserved(nodes, edges) {
|
|
4627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4628
|
+
for (const e of edges) {
|
|
4629
|
+
if (e.provenance === import_types21.Provenance.OBSERVED) {
|
|
4630
|
+
seen.add(e.source);
|
|
4631
|
+
seen.add(e.target);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
return nodes.filter(
|
|
4635
|
+
(n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
function formatDivergence(d) {
|
|
4639
|
+
const conf = d.confidence.toFixed(2);
|
|
4640
|
+
return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
4641
|
+
}
|
|
4642
|
+
function renderValueForwardSummary(input) {
|
|
4643
|
+
const { graph, divergences, verbose } = input;
|
|
4644
|
+
const nodes = [];
|
|
4645
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
4646
|
+
const edges = [];
|
|
4647
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
4648
|
+
const lines = [];
|
|
4649
|
+
lines.push("=== neat: findings ===");
|
|
4650
|
+
lines.push("");
|
|
4651
|
+
const incompatServices = findIncompatServices(nodes);
|
|
4652
|
+
const totalIncompats = incompatServices.reduce(
|
|
4653
|
+
(acc, s) => acc + (s.incompatibilities?.length ?? 0),
|
|
4654
|
+
0
|
|
4655
|
+
);
|
|
4656
|
+
lines.push(`compat violations: ${totalIncompats}`);
|
|
4657
|
+
for (const svc of incompatServices) {
|
|
4658
|
+
for (const inc of svc.incompatibilities ?? []) {
|
|
4659
|
+
const detail = formatIncompat(inc);
|
|
4660
|
+
lines.push(` ${svc.name}: ${detail}`);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
lines.push("");
|
|
4664
|
+
const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
4665
|
+
lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
|
|
4666
|
+
for (const d of top) lines.push(formatDivergence(d));
|
|
4667
|
+
lines.push("");
|
|
4668
|
+
const noObserved = servicesWithoutObserved(nodes, edges);
|
|
4669
|
+
if (noObserved.length > 0) {
|
|
4670
|
+
lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
|
|
4671
|
+
for (const svc of noObserved) lines.push(` ${svc.name}`);
|
|
4672
|
+
lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
|
|
4673
|
+
lines.push("");
|
|
4674
|
+
}
|
|
4675
|
+
lines.push(renderOtelEnvBlock());
|
|
4676
|
+
lines.push("");
|
|
4677
|
+
if (verbose) {
|
|
4678
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4679
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
4680
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
4681
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
4682
|
+
lines.push("=== graph (verbose) ===");
|
|
4683
|
+
lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
4684
|
+
lines.push("nodes:");
|
|
4685
|
+
for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
4686
|
+
lines.push("edges:");
|
|
4687
|
+
for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
4688
|
+
lines.push("");
|
|
4689
|
+
}
|
|
4690
|
+
return lines.join("\n");
|
|
4691
|
+
}
|
|
4692
|
+
function formatIncompat(inc) {
|
|
4693
|
+
if (inc.kind === "node-engine") {
|
|
4694
|
+
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
4695
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
4696
|
+
}
|
|
4697
|
+
if (inc.kind === "package-conflict") {
|
|
4698
|
+
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
4699
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
4700
|
+
}
|
|
4701
|
+
if (inc.kind === "deprecated-api") {
|
|
4702
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
4703
|
+
}
|
|
4704
|
+
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// src/watch.ts
|
|
4708
|
+
init_cjs_shims();
|
|
4709
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
4710
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
4711
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4712
|
+
|
|
4713
|
+
// src/api.ts
|
|
4714
|
+
init_cjs_shims();
|
|
4715
|
+
var import_fastify = __toESM(require("fastify"), 1);
|
|
4716
|
+
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4717
|
+
var import_types23 = require("@neat.is/types");
|
|
4718
|
+
|
|
4719
|
+
// src/diff.ts
|
|
4720
|
+
init_cjs_shims();
|
|
4721
|
+
var import_node_fs20 = require("fs");
|
|
4722
|
+
async function loadSnapshotForDiff(target) {
|
|
4723
|
+
if (/^https?:\/\//i.test(target)) {
|
|
4724
|
+
const res = await fetch(target);
|
|
4725
|
+
if (!res.ok) {
|
|
4726
|
+
throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
|
|
4727
|
+
}
|
|
4728
|
+
return await res.json();
|
|
4729
|
+
}
|
|
4730
|
+
const raw = await import_node_fs20.promises.readFile(target, "utf8");
|
|
4731
|
+
return JSON.parse(raw);
|
|
4732
|
+
}
|
|
4733
|
+
function indexEntries(entries) {
|
|
4734
|
+
const m = /* @__PURE__ */ new Map();
|
|
4735
|
+
if (!entries) return m;
|
|
4736
|
+
for (const entry2 of entries) {
|
|
4737
|
+
const id = entry2.attributes?.id ?? entry2.key;
|
|
4738
|
+
if (!id) continue;
|
|
4739
|
+
m.set(id, entry2.attributes);
|
|
4740
|
+
}
|
|
4741
|
+
return m;
|
|
4742
|
+
}
|
|
4743
|
+
function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4744
|
+
const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
|
|
4745
|
+
const baseEdges = indexEntries(baseSnapshot.graph?.edges);
|
|
4746
|
+
const liveNodes = /* @__PURE__ */ new Map();
|
|
4747
|
+
liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
|
|
4748
|
+
const liveEdges = /* @__PURE__ */ new Map();
|
|
4749
|
+
liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
|
|
4750
|
+
const result = {
|
|
4751
|
+
base: { exportedAt: baseSnapshot.exportedAt },
|
|
4752
|
+
current: { exportedAt: currentExportedAt },
|
|
4753
|
+
added: { nodes: [], edges: [] },
|
|
4754
|
+
removed: { nodes: [], edges: [] },
|
|
4755
|
+
changed: { nodes: [], edges: [] }
|
|
4756
|
+
};
|
|
4757
|
+
for (const [id, after] of liveNodes) {
|
|
4758
|
+
const before = baseNodes.get(id);
|
|
4759
|
+
if (!before) {
|
|
4760
|
+
result.added.nodes.push(after);
|
|
4761
|
+
} else if (!shallowEqual(before, after)) {
|
|
4762
|
+
result.changed.nodes.push({ id, before, after });
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
for (const [id, before] of baseNodes) {
|
|
4766
|
+
if (!liveNodes.has(id)) result.removed.nodes.push(before);
|
|
4767
|
+
}
|
|
4768
|
+
for (const [id, after] of liveEdges) {
|
|
4769
|
+
const before = baseEdges.get(id);
|
|
4770
|
+
if (!before) {
|
|
4771
|
+
result.added.edges.push(after);
|
|
4772
|
+
} else if (!shallowEqual(before, after)) {
|
|
4773
|
+
result.changed.edges.push({ id, before, after });
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
for (const [id, before] of baseEdges) {
|
|
4777
|
+
if (!liveEdges.has(id)) result.removed.edges.push(before);
|
|
4778
|
+
}
|
|
4779
|
+
return result;
|
|
4780
|
+
}
|
|
4781
|
+
function shallowEqual(a, b) {
|
|
4782
|
+
return canonicalJson(a) === canonicalJson(b);
|
|
4783
|
+
}
|
|
4784
|
+
function canonicalJson(value) {
|
|
4785
|
+
return JSON.stringify(value, (_key, v) => {
|
|
4786
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
4787
|
+
return Object.keys(v).sort().reduce((acc, k) => {
|
|
4788
|
+
acc[k] = v[k];
|
|
4789
|
+
return acc;
|
|
4790
|
+
}, {});
|
|
4606
4791
|
}
|
|
4607
4792
|
return v;
|
|
4608
4793
|
});
|
|
@@ -4610,23 +4795,23 @@ function canonicalJson(value) {
|
|
|
4610
4795
|
|
|
4611
4796
|
// src/projects.ts
|
|
4612
4797
|
init_cjs_shims();
|
|
4613
|
-
var
|
|
4798
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4614
4799
|
function pathsForProject(project, baseDir) {
|
|
4615
4800
|
if (project === DEFAULT_PROJECT) {
|
|
4616
4801
|
return {
|
|
4617
|
-
snapshotPath:
|
|
4618
|
-
errorsPath:
|
|
4619
|
-
staleEventsPath:
|
|
4620
|
-
embeddingsCachePath:
|
|
4621
|
-
policyViolationsPath:
|
|
4802
|
+
snapshotPath: import_node_path33.default.join(baseDir, "graph.json"),
|
|
4803
|
+
errorsPath: import_node_path33.default.join(baseDir, "errors.ndjson"),
|
|
4804
|
+
staleEventsPath: import_node_path33.default.join(baseDir, "stale-events.ndjson"),
|
|
4805
|
+
embeddingsCachePath: import_node_path33.default.join(baseDir, "embeddings.json"),
|
|
4806
|
+
policyViolationsPath: import_node_path33.default.join(baseDir, "policy-violations.ndjson")
|
|
4622
4807
|
};
|
|
4623
4808
|
}
|
|
4624
4809
|
return {
|
|
4625
|
-
snapshotPath:
|
|
4626
|
-
errorsPath:
|
|
4627
|
-
staleEventsPath:
|
|
4628
|
-
embeddingsCachePath:
|
|
4629
|
-
policyViolationsPath:
|
|
4810
|
+
snapshotPath: import_node_path33.default.join(baseDir, `${project}.json`),
|
|
4811
|
+
errorsPath: import_node_path33.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4812
|
+
staleEventsPath: import_node_path33.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4813
|
+
embeddingsCachePath: import_node_path33.default.join(baseDir, `embeddings.${project}.json`),
|
|
4814
|
+
policyViolationsPath: import_node_path33.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4630
4815
|
};
|
|
4631
4816
|
}
|
|
4632
4817
|
var Projects = class {
|
|
@@ -4662,49 +4847,49 @@ var Projects = class {
|
|
|
4662
4847
|
|
|
4663
4848
|
// src/registry.ts
|
|
4664
4849
|
init_cjs_shims();
|
|
4665
|
-
var
|
|
4850
|
+
var import_node_fs21 = require("fs");
|
|
4666
4851
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4667
|
-
var
|
|
4668
|
-
var
|
|
4852
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
4853
|
+
var import_types22 = require("@neat.is/types");
|
|
4669
4854
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4670
4855
|
var LOCK_RETRY_MS = 50;
|
|
4671
4856
|
function neatHome() {
|
|
4672
4857
|
const override = process.env.NEAT_HOME;
|
|
4673
|
-
if (override && override.length > 0) return
|
|
4674
|
-
return
|
|
4858
|
+
if (override && override.length > 0) return import_node_path34.default.resolve(override);
|
|
4859
|
+
return import_node_path34.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4675
4860
|
}
|
|
4676
4861
|
function registryPath() {
|
|
4677
|
-
return
|
|
4862
|
+
return import_node_path34.default.join(neatHome(), "projects.json");
|
|
4678
4863
|
}
|
|
4679
4864
|
function registryLockPath() {
|
|
4680
|
-
return
|
|
4865
|
+
return import_node_path34.default.join(neatHome(), "projects.json.lock");
|
|
4681
4866
|
}
|
|
4682
4867
|
async function normalizeProjectPath(input) {
|
|
4683
|
-
const resolved =
|
|
4868
|
+
const resolved = import_node_path34.default.resolve(input);
|
|
4684
4869
|
try {
|
|
4685
|
-
return await
|
|
4870
|
+
return await import_node_fs21.promises.realpath(resolved);
|
|
4686
4871
|
} catch {
|
|
4687
4872
|
return resolved;
|
|
4688
4873
|
}
|
|
4689
4874
|
}
|
|
4690
4875
|
async function writeAtomically(target, contents) {
|
|
4691
|
-
await
|
|
4876
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(target), { recursive: true });
|
|
4692
4877
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4693
|
-
const fd = await
|
|
4878
|
+
const fd = await import_node_fs21.promises.open(tmp, "w");
|
|
4694
4879
|
try {
|
|
4695
4880
|
await fd.writeFile(contents, "utf8");
|
|
4696
4881
|
await fd.sync();
|
|
4697
4882
|
} finally {
|
|
4698
4883
|
await fd.close();
|
|
4699
4884
|
}
|
|
4700
|
-
await
|
|
4885
|
+
await import_node_fs21.promises.rename(tmp, target);
|
|
4701
4886
|
}
|
|
4702
4887
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4703
4888
|
const deadline = Date.now() + timeoutMs;
|
|
4704
|
-
await
|
|
4889
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(lockPath), { recursive: true });
|
|
4705
4890
|
while (true) {
|
|
4706
4891
|
try {
|
|
4707
|
-
const fd = await
|
|
4892
|
+
const fd = await import_node_fs21.promises.open(lockPath, "wx");
|
|
4708
4893
|
await fd.close();
|
|
4709
4894
|
return;
|
|
4710
4895
|
} catch (err) {
|
|
@@ -4720,7 +4905,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4720
4905
|
}
|
|
4721
4906
|
}
|
|
4722
4907
|
async function releaseLock(lockPath) {
|
|
4723
|
-
await
|
|
4908
|
+
await import_node_fs21.promises.unlink(lockPath).catch(() => {
|
|
4724
4909
|
});
|
|
4725
4910
|
}
|
|
4726
4911
|
async function withLock(fn) {
|
|
@@ -4736,7 +4921,7 @@ async function readRegistry() {
|
|
|
4736
4921
|
const file = registryPath();
|
|
4737
4922
|
let raw;
|
|
4738
4923
|
try {
|
|
4739
|
-
raw = await
|
|
4924
|
+
raw = await import_node_fs21.promises.readFile(file, "utf8");
|
|
4740
4925
|
} catch (err) {
|
|
4741
4926
|
if (err.code === "ENOENT") {
|
|
4742
4927
|
return { version: 1, projects: [] };
|
|
@@ -4744,10 +4929,10 @@ async function readRegistry() {
|
|
|
4744
4929
|
throw err;
|
|
4745
4930
|
}
|
|
4746
4931
|
const parsed = JSON.parse(raw);
|
|
4747
|
-
return
|
|
4932
|
+
return import_types22.RegistryFileSchema.parse(parsed);
|
|
4748
4933
|
}
|
|
4749
4934
|
async function writeRegistry(reg) {
|
|
4750
|
-
const validated =
|
|
4935
|
+
const validated = import_types22.RegistryFileSchema.parse(reg);
|
|
4751
4936
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4752
4937
|
}
|
|
4753
4938
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4875,6 +5060,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4875
5060
|
}
|
|
4876
5061
|
|
|
4877
5062
|
// src/api.ts
|
|
5063
|
+
init_auth();
|
|
4878
5064
|
function serializeGraph(graph) {
|
|
4879
5065
|
const nodes = [];
|
|
4880
5066
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4998,11 +5184,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4998
5184
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4999
5185
|
const parsed = [];
|
|
5000
5186
|
for (const c of candidates) {
|
|
5001
|
-
const r =
|
|
5187
|
+
const r = import_types23.DivergenceTypeSchema.safeParse(c);
|
|
5002
5188
|
if (!r.success) {
|
|
5003
5189
|
return reply.code(400).send({
|
|
5004
5190
|
error: `unknown divergence type "${c}"`,
|
|
5005
|
-
allowed:
|
|
5191
|
+
allowed: import_types23.DivergenceTypeSchema.options
|
|
5006
5192
|
});
|
|
5007
5193
|
}
|
|
5008
5194
|
parsed.push(r.data);
|
|
@@ -5186,7 +5372,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5186
5372
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5187
5373
|
let violations = await log.readAll();
|
|
5188
5374
|
if (req.query.severity) {
|
|
5189
|
-
const sev =
|
|
5375
|
+
const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5190
5376
|
if (!sev.success) {
|
|
5191
5377
|
return reply.code(400).send({
|
|
5192
5378
|
error: "invalid severity",
|
|
@@ -5203,7 +5389,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5203
5389
|
scope.post("/policies/check", async (req, reply) => {
|
|
5204
5390
|
const proj = resolveProject(registry, req, reply);
|
|
5205
5391
|
if (!proj) return;
|
|
5206
|
-
const parsed =
|
|
5392
|
+
const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5207
5393
|
if (!parsed.success) {
|
|
5208
5394
|
return reply.code(400).send({
|
|
5209
5395
|
error: "invalid /policies/check body",
|
|
@@ -5240,6 +5426,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5240
5426
|
async function buildApi(opts) {
|
|
5241
5427
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5242
5428
|
await app.register(import_cors.default, { origin: true });
|
|
5429
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
5243
5430
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5244
5431
|
const registry = buildLegacyRegistry(opts);
|
|
5245
5432
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5307,9 +5494,9 @@ init_otel_grpc();
|
|
|
5307
5494
|
|
|
5308
5495
|
// src/search.ts
|
|
5309
5496
|
init_cjs_shims();
|
|
5310
|
-
var
|
|
5311
|
-
var
|
|
5312
|
-
var
|
|
5497
|
+
var import_node_fs22 = require("fs");
|
|
5498
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5499
|
+
var import_node_crypto3 = require("crypto");
|
|
5313
5500
|
var DEFAULT_LIMIT = 10;
|
|
5314
5501
|
var NOMIC_DIM = 768;
|
|
5315
5502
|
var MINI_LM_DIM = 384;
|
|
@@ -5349,7 +5536,7 @@ function embedText(node) {
|
|
|
5349
5536
|
return parts.join(" ");
|
|
5350
5537
|
}
|
|
5351
5538
|
function attrsHash(node) {
|
|
5352
|
-
return (0,
|
|
5539
|
+
return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
5353
5540
|
}
|
|
5354
5541
|
function cosine(a, b) {
|
|
5355
5542
|
if (a.length !== b.length) return 0;
|
|
@@ -5438,7 +5625,7 @@ async function pickEmbedder() {
|
|
|
5438
5625
|
}
|
|
5439
5626
|
async function readCache(cachePath) {
|
|
5440
5627
|
try {
|
|
5441
|
-
const raw = await
|
|
5628
|
+
const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
|
|
5442
5629
|
const parsed = JSON.parse(raw);
|
|
5443
5630
|
if (parsed.version !== 1) return null;
|
|
5444
5631
|
return parsed;
|
|
@@ -5447,8 +5634,8 @@ async function readCache(cachePath) {
|
|
|
5447
5634
|
}
|
|
5448
5635
|
}
|
|
5449
5636
|
async function writeCache(cachePath, cache) {
|
|
5450
|
-
await
|
|
5451
|
-
await
|
|
5637
|
+
await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(cachePath), { recursive: true });
|
|
5638
|
+
await import_node_fs22.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
5452
5639
|
}
|
|
5453
5640
|
var VectorIndex = class {
|
|
5454
5641
|
constructor(embedder, cachePath) {
|
|
@@ -5603,8 +5790,8 @@ var ALL_PHASES = [
|
|
|
5603
5790
|
];
|
|
5604
5791
|
function classifyChange(relPath) {
|
|
5605
5792
|
const phases = /* @__PURE__ */ new Set();
|
|
5606
|
-
const base =
|
|
5607
|
-
const segments = relPath.split(
|
|
5793
|
+
const base = import_node_path38.default.basename(relPath).toLowerCase();
|
|
5794
|
+
const segments = relPath.split(import_node_path38.default.sep).map((s) => s.toLowerCase());
|
|
5608
5795
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
5609
5796
|
phases.add("services");
|
|
5610
5797
|
phases.add("aliases");
|
|
@@ -5699,16 +5886,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
5699
5886
|
if (count >= limit) return;
|
|
5700
5887
|
let entries;
|
|
5701
5888
|
try {
|
|
5702
|
-
entries =
|
|
5889
|
+
entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
|
|
5703
5890
|
} catch {
|
|
5704
5891
|
return;
|
|
5705
5892
|
}
|
|
5706
5893
|
for (const e of entries) {
|
|
5707
5894
|
if (count >= limit) return;
|
|
5708
5895
|
if (!e.isDirectory()) continue;
|
|
5709
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
5896
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path38.default.join(dir, e.name) + import_node_path38.default.sep))) continue;
|
|
5710
5897
|
count++;
|
|
5711
|
-
if (depth < 2) visit(
|
|
5898
|
+
if (depth < 2) visit(import_node_path38.default.join(dir, e.name), depth + 1);
|
|
5712
5899
|
}
|
|
5713
5900
|
};
|
|
5714
5901
|
visit(scanPath, 0);
|
|
@@ -5726,8 +5913,8 @@ async function startWatch(graph, opts) {
|
|
|
5726
5913
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
5727
5914
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
5728
5915
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5729
|
-
const policyFilePath =
|
|
5730
|
-
const policyViolationsPath =
|
|
5916
|
+
const policyFilePath = import_node_path38.default.join(opts.scanPath, "policy.json");
|
|
5917
|
+
const policyViolationsPath = import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
5731
5918
|
let policies = [];
|
|
5732
5919
|
try {
|
|
5733
5920
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -5776,7 +5963,7 @@ async function startWatch(graph, opts) {
|
|
|
5776
5963
|
const host = opts.host ?? "0.0.0.0";
|
|
5777
5964
|
const port = opts.port ?? 8080;
|
|
5778
5965
|
const otelPort = opts.otelPort ?? 4318;
|
|
5779
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5966
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
|
|
5780
5967
|
let searchIndex;
|
|
5781
5968
|
try {
|
|
5782
5969
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -5794,7 +5981,7 @@ async function startWatch(graph, opts) {
|
|
|
5794
5981
|
// Paths are derived from the explicit options the watch caller passes
|
|
5795
5982
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
5796
5983
|
// fields so the registry shape is complete.
|
|
5797
|
-
...pathsForProject(projectName,
|
|
5984
|
+
...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
|
|
5798
5985
|
snapshotPath: opts.outPath,
|
|
5799
5986
|
errorsPath: opts.errorsPath,
|
|
5800
5987
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5879,9 +6066,9 @@ async function startWatch(graph, opts) {
|
|
|
5879
6066
|
};
|
|
5880
6067
|
const onPath = (absPath) => {
|
|
5881
6068
|
if (shouldIgnore(absPath)) return;
|
|
5882
|
-
const rel =
|
|
6069
|
+
const rel = import_node_path38.default.relative(opts.scanPath, absPath);
|
|
5883
6070
|
if (!rel || rel.startsWith("..")) return;
|
|
5884
|
-
pendingPaths.add(rel.split(
|
|
6071
|
+
pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
|
|
5885
6072
|
const phases = classifyChange(rel);
|
|
5886
6073
|
if (phases.size === 0) {
|
|
5887
6074
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5933,13 +6120,158 @@ async function startWatch(graph, opts) {
|
|
|
5933
6120
|
return { api, stop };
|
|
5934
6121
|
}
|
|
5935
6122
|
|
|
6123
|
+
// src/deploy/detect.ts
|
|
6124
|
+
init_cjs_shims();
|
|
6125
|
+
var import_node_fs24 = require("fs");
|
|
6126
|
+
var import_node_path39 = __toESM(require("path"), 1);
|
|
6127
|
+
var import_node_child_process = require("child_process");
|
|
6128
|
+
var import_node_crypto4 = require("crypto");
|
|
6129
|
+
function generateToken() {
|
|
6130
|
+
return (0, import_node_crypto4.randomBytes)(32).toString("base64url");
|
|
6131
|
+
}
|
|
6132
|
+
async function probeBinary(binary, arg) {
|
|
6133
|
+
return new Promise((resolve) => {
|
|
6134
|
+
const child = (0, import_node_child_process.spawn)(binary, [arg], { stdio: "ignore" });
|
|
6135
|
+
const timer = setTimeout(() => {
|
|
6136
|
+
child.kill("SIGKILL");
|
|
6137
|
+
resolve(false);
|
|
6138
|
+
}, 2e3);
|
|
6139
|
+
child.once("error", () => {
|
|
6140
|
+
clearTimeout(timer);
|
|
6141
|
+
resolve(false);
|
|
6142
|
+
});
|
|
6143
|
+
child.once("exit", (code) => {
|
|
6144
|
+
clearTimeout(timer);
|
|
6145
|
+
resolve(code === 0);
|
|
6146
|
+
});
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
async function detectSubstrate(opts = {}) {
|
|
6150
|
+
const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
|
|
6151
|
+
const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
|
|
6152
|
+
if (await hasDocker()) return "docker-compose";
|
|
6153
|
+
if (await hasSystemd()) return "systemd";
|
|
6154
|
+
return "docker-run";
|
|
6155
|
+
}
|
|
6156
|
+
var IMAGE = "ghcr.io/neat-technologies/neat:latest";
|
|
6157
|
+
function emitDockerCompose(cwd) {
|
|
6158
|
+
return [
|
|
6159
|
+
"services:",
|
|
6160
|
+
" neat:",
|
|
6161
|
+
` image: ${IMAGE}`,
|
|
6162
|
+
" restart: unless-stopped",
|
|
6163
|
+
" environment:",
|
|
6164
|
+
" NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
|
|
6165
|
+
" ports:",
|
|
6166
|
+
' - "8080:8080"',
|
|
6167
|
+
' - "4318:4318"',
|
|
6168
|
+
' - "6328:6328"',
|
|
6169
|
+
" volumes:",
|
|
6170
|
+
` - ${cwd}:/workspace`,
|
|
6171
|
+
" - ./neat-data:/neat-out",
|
|
6172
|
+
""
|
|
6173
|
+
].join("\n");
|
|
6174
|
+
}
|
|
6175
|
+
function emitSystemdUnit(cwd) {
|
|
6176
|
+
return [
|
|
6177
|
+
"# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
|
|
6178
|
+
"# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
|
|
6179
|
+
"[Unit]",
|
|
6180
|
+
"Description=NEAT daemon",
|
|
6181
|
+
"After=network-online.target",
|
|
6182
|
+
"Wants=network-online.target",
|
|
6183
|
+
"",
|
|
6184
|
+
"[Service]",
|
|
6185
|
+
"Type=simple",
|
|
6186
|
+
"ExecStart=/usr/local/bin/neatd start --foreground",
|
|
6187
|
+
`WorkingDirectory=${cwd}`,
|
|
6188
|
+
"EnvironmentFile=/etc/neat/neatd.env",
|
|
6189
|
+
"Restart=always",
|
|
6190
|
+
"RestartSec=5",
|
|
6191
|
+
"",
|
|
6192
|
+
"[Install]",
|
|
6193
|
+
"WantedBy=multi-user.target",
|
|
6194
|
+
""
|
|
6195
|
+
].join("\n");
|
|
6196
|
+
}
|
|
6197
|
+
function emitDockerRunSnippet() {
|
|
6198
|
+
return [
|
|
6199
|
+
"#!/usr/bin/env bash",
|
|
6200
|
+
"set -euo pipefail",
|
|
6201
|
+
"",
|
|
6202
|
+
"# Generate a fresh token once, then store it where your secrets live.",
|
|
6203
|
+
': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
|
|
6204
|
+
"",
|
|
6205
|
+
`docker run -d --name neat \\`,
|
|
6206
|
+
' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
|
|
6207
|
+
" -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
|
|
6208
|
+
' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
|
|
6209
|
+
` ${IMAGE}`,
|
|
6210
|
+
""
|
|
6211
|
+
].join("\n");
|
|
6212
|
+
}
|
|
6213
|
+
function renderOtelEnvBlock2(token, host = "<host>") {
|
|
6214
|
+
return [
|
|
6215
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
|
|
6216
|
+
`OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
|
|
6217
|
+
"OTEL_SERVICE_NAME=<service>"
|
|
6218
|
+
].join("\n");
|
|
6219
|
+
}
|
|
6220
|
+
async function runDeploy(opts = {}) {
|
|
6221
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
6222
|
+
const substrate = await detectSubstrate(opts);
|
|
6223
|
+
const token = generateToken();
|
|
6224
|
+
switch (substrate) {
|
|
6225
|
+
case "docker-compose": {
|
|
6226
|
+
const artifactPath = import_node_path39.default.join(cwd, "docker-compose.neat.yml");
|
|
6227
|
+
const contents = emitDockerCompose(cwd);
|
|
6228
|
+
await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
|
|
6229
|
+
return {
|
|
6230
|
+
substrate,
|
|
6231
|
+
artifactPath,
|
|
6232
|
+
token,
|
|
6233
|
+
contents,
|
|
6234
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path39.default.basename(artifactPath)} up -d`
|
|
6235
|
+
};
|
|
6236
|
+
}
|
|
6237
|
+
case "systemd": {
|
|
6238
|
+
const artifactPath = import_node_path39.default.join(cwd, "neat.service");
|
|
6239
|
+
const contents = emitSystemdUnit(cwd);
|
|
6240
|
+
await import_node_fs24.promises.writeFile(artifactPath, contents, "utf8");
|
|
6241
|
+
return {
|
|
6242
|
+
substrate,
|
|
6243
|
+
artifactPath,
|
|
6244
|
+
token,
|
|
6245
|
+
contents,
|
|
6246
|
+
startCommand: [
|
|
6247
|
+
`sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
|
|
6248
|
+
`sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
|
|
6249
|
+
"sudo systemctl daemon-reload && sudo systemctl enable --now neat"
|
|
6250
|
+
].join(" && ")
|
|
6251
|
+
};
|
|
6252
|
+
}
|
|
6253
|
+
case "docker-run":
|
|
6254
|
+
default: {
|
|
6255
|
+
const contents = emitDockerRunSnippet();
|
|
6256
|
+
return {
|
|
6257
|
+
substrate: "docker-run",
|
|
6258
|
+
token,
|
|
6259
|
+
contents,
|
|
6260
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
|
|
6261
|
+
${contents}EOF
|
|
6262
|
+
)`
|
|
6263
|
+
};
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
|
|
5936
6268
|
// src/installers/index.ts
|
|
5937
6269
|
init_cjs_shims();
|
|
5938
6270
|
|
|
5939
6271
|
// src/installers/javascript.ts
|
|
5940
6272
|
init_cjs_shims();
|
|
5941
|
-
var
|
|
5942
|
-
var
|
|
6273
|
+
var import_node_fs25 = require("fs");
|
|
6274
|
+
var import_node_path40 = __toESM(require("path"), 1);
|
|
5943
6275
|
|
|
5944
6276
|
// src/installers/templates.ts
|
|
5945
6277
|
init_cjs_shims();
|
|
@@ -5987,6 +6319,57 @@ function renderEnvNeat(serviceName) {
|
|
|
5987
6319
|
""
|
|
5988
6320
|
].join("\n");
|
|
5989
6321
|
}
|
|
6322
|
+
var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
|
|
6323
|
+
var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6324
|
+
export async function register() {
|
|
6325
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
6326
|
+
await import('./instrumentation.node')
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
`;
|
|
6330
|
+
var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6331
|
+
export async function register() {
|
|
6332
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
6333
|
+
await import('./instrumentation.node')
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
`;
|
|
6337
|
+
var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6338
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6339
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6340
|
+
import { fileURLToPath } from 'node:url'
|
|
6341
|
+
import path from 'node:path'
|
|
6342
|
+
import dotenv from 'dotenv'
|
|
6343
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6344
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6345
|
+
|
|
6346
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6347
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6348
|
+
|
|
6349
|
+
const sdk = new NodeSDK({
|
|
6350
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6351
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6352
|
+
})
|
|
6353
|
+
sdk.start()
|
|
6354
|
+
`;
|
|
6355
|
+
var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
6356
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
6357
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
6358
|
+
import { fileURLToPath } from 'node:url'
|
|
6359
|
+
import path from 'node:path'
|
|
6360
|
+
import dotenv from 'dotenv'
|
|
6361
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6362
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6363
|
+
|
|
6364
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6365
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
6366
|
+
|
|
6367
|
+
const sdk = new NodeSDK({
|
|
6368
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
6369
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
6370
|
+
})
|
|
6371
|
+
sdk.start()
|
|
6372
|
+
`;
|
|
5990
6373
|
|
|
5991
6374
|
// src/installers/javascript.ts
|
|
5992
6375
|
var SDK_PACKAGES = [
|
|
@@ -6009,7 +6392,7 @@ var OTEL_ENV = {
|
|
|
6009
6392
|
};
|
|
6010
6393
|
async function readPackageJson(serviceDir) {
|
|
6011
6394
|
try {
|
|
6012
|
-
const raw = await
|
|
6395
|
+
const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
|
|
6013
6396
|
return JSON.parse(raw);
|
|
6014
6397
|
} catch {
|
|
6015
6398
|
return null;
|
|
@@ -6017,7 +6400,7 @@ async function readPackageJson(serviceDir) {
|
|
|
6017
6400
|
}
|
|
6018
6401
|
async function exists2(p) {
|
|
6019
6402
|
try {
|
|
6020
|
-
await
|
|
6403
|
+
await import_node_fs25.promises.stat(p);
|
|
6021
6404
|
return true;
|
|
6022
6405
|
} catch {
|
|
6023
6406
|
return false;
|
|
@@ -6027,6 +6410,28 @@ async function detect(serviceDir) {
|
|
|
6027
6410
|
const pkg = await readPackageJson(serviceDir);
|
|
6028
6411
|
return pkg !== null && typeof pkg.name === "string";
|
|
6029
6412
|
}
|
|
6413
|
+
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
6414
|
+
async function findNextConfig(serviceDir) {
|
|
6415
|
+
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
6416
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6417
|
+
if (await exists2(candidate)) return candidate;
|
|
6418
|
+
}
|
|
6419
|
+
return null;
|
|
6420
|
+
}
|
|
6421
|
+
function hasNextDependency(pkg) {
|
|
6422
|
+
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
6423
|
+
}
|
|
6424
|
+
function parseNextMajor(range) {
|
|
6425
|
+
if (!range) return null;
|
|
6426
|
+
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
6427
|
+
const match = cleaned.match(/^(\d+)/);
|
|
6428
|
+
if (!match) return null;
|
|
6429
|
+
const n = Number(match[1]);
|
|
6430
|
+
return Number.isFinite(n) ? n : null;
|
|
6431
|
+
}
|
|
6432
|
+
async function isTypeScriptProject(serviceDir) {
|
|
6433
|
+
return exists2(import_node_path40.default.join(serviceDir, "tsconfig.json"));
|
|
6434
|
+
}
|
|
6030
6435
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
6031
6436
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
6032
6437
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
@@ -6071,7 +6476,7 @@ function entryFromScript(script) {
|
|
|
6071
6476
|
}
|
|
6072
6477
|
async function resolveEntry(serviceDir, pkg) {
|
|
6073
6478
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
6074
|
-
const candidate =
|
|
6479
|
+
const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
|
|
6075
6480
|
if (await exists2(candidate)) return candidate;
|
|
6076
6481
|
}
|
|
6077
6482
|
if (pkg.bin) {
|
|
@@ -6085,36 +6490,36 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
6085
6490
|
if (typeof first === "string") binEntry = first;
|
|
6086
6491
|
}
|
|
6087
6492
|
if (binEntry) {
|
|
6088
|
-
const candidate =
|
|
6493
|
+
const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
|
|
6089
6494
|
if (await exists2(candidate)) return candidate;
|
|
6090
6495
|
}
|
|
6091
6496
|
}
|
|
6092
6497
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
6093
6498
|
if (startEntry) {
|
|
6094
|
-
const candidate =
|
|
6499
|
+
const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
|
|
6095
6500
|
if (await exists2(candidate)) return candidate;
|
|
6096
6501
|
}
|
|
6097
6502
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
6098
6503
|
if (devEntry) {
|
|
6099
|
-
const candidate =
|
|
6504
|
+
const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
|
|
6100
6505
|
if (await exists2(candidate)) return candidate;
|
|
6101
6506
|
}
|
|
6102
6507
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
6103
|
-
const candidate =
|
|
6508
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6104
6509
|
if (await exists2(candidate)) return candidate;
|
|
6105
6510
|
}
|
|
6106
6511
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
6107
|
-
const candidate =
|
|
6512
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6108
6513
|
if (await exists2(candidate)) return candidate;
|
|
6109
6514
|
}
|
|
6110
6515
|
for (const name of INDEX_CANDIDATES) {
|
|
6111
|
-
const candidate =
|
|
6516
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6112
6517
|
if (await exists2(candidate)) return candidate;
|
|
6113
6518
|
}
|
|
6114
6519
|
return null;
|
|
6115
6520
|
}
|
|
6116
6521
|
function dispatchEntry(entryFile, pkg) {
|
|
6117
|
-
const ext =
|
|
6522
|
+
const ext = import_node_path40.default.extname(entryFile).toLowerCase();
|
|
6118
6523
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
6119
6524
|
if (ext === ".mjs") return "esm";
|
|
6120
6525
|
if (ext === ".cjs") return "cjs";
|
|
@@ -6131,9 +6536,9 @@ function otelInitContents(flavor) {
|
|
|
6131
6536
|
return OTEL_INIT_CJS;
|
|
6132
6537
|
}
|
|
6133
6538
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
6134
|
-
let rel =
|
|
6539
|
+
let rel = import_node_path40.default.relative(import_node_path40.default.dirname(entryFile), otelInitFile);
|
|
6135
6540
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
6136
|
-
rel = rel.split(
|
|
6541
|
+
rel = rel.split(import_node_path40.default.sep).join("/");
|
|
6137
6542
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
6138
6543
|
if (flavor === "esm") return `import '${rel}'`;
|
|
6139
6544
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -6144,9 +6549,88 @@ function lineIsOtelInjection(line) {
|
|
|
6144
6549
|
if (trimmed.length === 0) return false;
|
|
6145
6550
|
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
6146
6551
|
}
|
|
6552
|
+
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
6553
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
6554
|
+
const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
6555
|
+
const instrumentationNodeFile = import_node_path40.default.join(
|
|
6556
|
+
serviceDir,
|
|
6557
|
+
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
6558
|
+
);
|
|
6559
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
6560
|
+
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6561
|
+
const dependencyEdits = [];
|
|
6562
|
+
for (const sdk of SDK_PACKAGES) {
|
|
6563
|
+
if (sdk.name in existingDeps) continue;
|
|
6564
|
+
dependencyEdits.push({
|
|
6565
|
+
file: manifestPath,
|
|
6566
|
+
kind: "add",
|
|
6567
|
+
name: sdk.name,
|
|
6568
|
+
version: sdk.version
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
const generatedFiles = [];
|
|
6572
|
+
if (!await exists2(instrumentationFile)) {
|
|
6573
|
+
generatedFiles.push({
|
|
6574
|
+
file: instrumentationFile,
|
|
6575
|
+
contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
|
|
6576
|
+
skipIfExists: true
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
if (!await exists2(instrumentationNodeFile)) {
|
|
6580
|
+
generatedFiles.push({
|
|
6581
|
+
file: instrumentationNodeFile,
|
|
6582
|
+
contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
|
|
6583
|
+
skipIfExists: true
|
|
6584
|
+
});
|
|
6585
|
+
}
|
|
6586
|
+
if (!await exists2(envNeatFile)) {
|
|
6587
|
+
generatedFiles.push({
|
|
6588
|
+
file: envNeatFile,
|
|
6589
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6590
|
+
skipIfExists: true
|
|
6591
|
+
});
|
|
6592
|
+
}
|
|
6593
|
+
let nextConfigEdit;
|
|
6594
|
+
const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
6595
|
+
const nextMajor = parseNextMajor(nextRange);
|
|
6596
|
+
if (nextMajor !== null && nextMajor < 15) {
|
|
6597
|
+
try {
|
|
6598
|
+
const raw = await import_node_fs25.promises.readFile(nextConfigPath, "utf8");
|
|
6599
|
+
if (!raw.includes("instrumentationHook")) {
|
|
6600
|
+
nextConfigEdit = {
|
|
6601
|
+
file: nextConfigPath,
|
|
6602
|
+
reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
} catch {
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
|
|
6609
|
+
if (empty) {
|
|
6610
|
+
return {
|
|
6611
|
+
language: "javascript",
|
|
6612
|
+
serviceDir,
|
|
6613
|
+
dependencyEdits: [],
|
|
6614
|
+
entrypointEdits: [],
|
|
6615
|
+
envEdits: [],
|
|
6616
|
+
generatedFiles: [],
|
|
6617
|
+
framework: "next"
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
return {
|
|
6621
|
+
language: "javascript",
|
|
6622
|
+
serviceDir,
|
|
6623
|
+
dependencyEdits,
|
|
6624
|
+
entrypointEdits: [],
|
|
6625
|
+
envEdits: [OTEL_ENV],
|
|
6626
|
+
generatedFiles,
|
|
6627
|
+
framework: "next",
|
|
6628
|
+
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6147
6631
|
async function plan(serviceDir) {
|
|
6148
6632
|
const pkg = await readPackageJson(serviceDir);
|
|
6149
|
-
const manifestPath =
|
|
6633
|
+
const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
|
|
6150
6634
|
const empty = {
|
|
6151
6635
|
language: "javascript",
|
|
6152
6636
|
serviceDir,
|
|
@@ -6156,13 +6640,19 @@ async function plan(serviceDir) {
|
|
|
6156
6640
|
generatedFiles: []
|
|
6157
6641
|
};
|
|
6158
6642
|
if (!pkg) return empty;
|
|
6643
|
+
if (hasNextDependency(pkg)) {
|
|
6644
|
+
const nextConfig = await findNextConfig(serviceDir);
|
|
6645
|
+
if (nextConfig) {
|
|
6646
|
+
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6159
6649
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
6160
6650
|
if (!entryFile) {
|
|
6161
6651
|
return { ...empty, libOnly: true };
|
|
6162
6652
|
}
|
|
6163
6653
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
6164
|
-
const otelInitFile =
|
|
6165
|
-
const envNeatFile =
|
|
6654
|
+
const otelInitFile = import_node_path40.default.join(import_node_path40.default.dirname(entryFile), otelInitFilename(flavor));
|
|
6655
|
+
const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
|
|
6166
6656
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6167
6657
|
const dependencyEdits = [];
|
|
6168
6658
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -6176,7 +6666,7 @@ async function plan(serviceDir) {
|
|
|
6176
6666
|
}
|
|
6177
6667
|
const entrypointEdits = [];
|
|
6178
6668
|
try {
|
|
6179
|
-
const raw = await
|
|
6669
|
+
const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
|
|
6180
6670
|
const lines = raw.split(/\r?\n/);
|
|
6181
6671
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
6182
6672
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -6201,7 +6691,7 @@ async function plan(serviceDir) {
|
|
|
6201
6691
|
if (!await exists2(envNeatFile)) {
|
|
6202
6692
|
generatedFiles.push({
|
|
6203
6693
|
file: envNeatFile,
|
|
6204
|
-
contents: renderEnvNeat(pkg.name ??
|
|
6694
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6205
6695
|
skipIfExists: true
|
|
6206
6696
|
});
|
|
6207
6697
|
}
|
|
@@ -6220,18 +6710,20 @@ async function plan(serviceDir) {
|
|
|
6220
6710
|
};
|
|
6221
6711
|
}
|
|
6222
6712
|
function isAllowedWritePath(serviceDir, target) {
|
|
6223
|
-
const rel =
|
|
6713
|
+
const rel = import_node_path40.default.relative(serviceDir, target);
|
|
6224
6714
|
if (rel.startsWith("..")) return false;
|
|
6225
|
-
const base =
|
|
6715
|
+
const base = import_node_path40.default.basename(target);
|
|
6226
6716
|
if (base === "package.json") return true;
|
|
6227
6717
|
if (base === ".env.neat") return true;
|
|
6228
6718
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6719
|
+
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
6720
|
+
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
6229
6721
|
return false;
|
|
6230
6722
|
}
|
|
6231
6723
|
async function writeAtomic(file, contents) {
|
|
6232
6724
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6233
|
-
await
|
|
6234
|
-
await
|
|
6725
|
+
await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
|
|
6726
|
+
await import_node_fs25.promises.rename(tmp, file);
|
|
6235
6727
|
}
|
|
6236
6728
|
async function apply(installPlan) {
|
|
6237
6729
|
const { serviceDir } = installPlan;
|
|
@@ -6243,7 +6735,7 @@ async function apply(installPlan) {
|
|
|
6243
6735
|
writtenFiles: []
|
|
6244
6736
|
};
|
|
6245
6737
|
}
|
|
6246
|
-
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
|
|
6738
|
+
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
|
|
6247
6739
|
return {
|
|
6248
6740
|
serviceDir,
|
|
6249
6741
|
outcome: "already-instrumented",
|
|
@@ -6254,6 +6746,7 @@ async function apply(installPlan) {
|
|
|
6254
6746
|
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
6255
6747
|
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
6256
6748
|
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
6749
|
+
if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
|
|
6257
6750
|
for (const target of allTargets) {
|
|
6258
6751
|
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
6259
6752
|
if (isEntryEdit) continue;
|
|
@@ -6268,7 +6761,7 @@ async function apply(installPlan) {
|
|
|
6268
6761
|
for (const target of allTargets) {
|
|
6269
6762
|
if (await exists2(target)) {
|
|
6270
6763
|
try {
|
|
6271
|
-
originals.set(target, await
|
|
6764
|
+
originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
|
|
6272
6765
|
} catch {
|
|
6273
6766
|
}
|
|
6274
6767
|
}
|
|
@@ -6323,6 +6816,17 @@ async function apply(installPlan) {
|
|
|
6323
6816
|
await writeAtomic(ep.file, newRaw);
|
|
6324
6817
|
writtenFiles.push(ep.file);
|
|
6325
6818
|
}
|
|
6819
|
+
if (installPlan.nextConfigEdit) {
|
|
6820
|
+
const target = installPlan.nextConfigEdit.file;
|
|
6821
|
+
const raw = originals.get(target);
|
|
6822
|
+
if (raw !== void 0 && !raw.includes("instrumentationHook")) {
|
|
6823
|
+
const updated = injectInstrumentationHook(raw);
|
|
6824
|
+
if (updated !== null) {
|
|
6825
|
+
await writeAtomic(target, updated);
|
|
6826
|
+
writtenFiles.push(target);
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6326
6830
|
} catch (err) {
|
|
6327
6831
|
await rollback(installPlan, originals, createdFiles);
|
|
6328
6832
|
throw err;
|
|
@@ -6338,14 +6842,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6338
6842
|
const removed = [];
|
|
6339
6843
|
for (const [file, raw] of originals.entries()) {
|
|
6340
6844
|
try {
|
|
6341
|
-
await
|
|
6845
|
+
await import_node_fs25.promises.writeFile(file, raw, "utf8");
|
|
6342
6846
|
restored.push(file);
|
|
6343
6847
|
} catch {
|
|
6344
6848
|
}
|
|
6345
6849
|
}
|
|
6346
6850
|
for (const file of createdFiles) {
|
|
6347
6851
|
try {
|
|
6348
|
-
await
|
|
6852
|
+
await import_node_fs25.promises.unlink(file);
|
|
6349
6853
|
removed.push(file);
|
|
6350
6854
|
} catch {
|
|
6351
6855
|
}
|
|
@@ -6360,8 +6864,26 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6360
6864
|
...removed.map((f) => `removed: ${f}`),
|
|
6361
6865
|
""
|
|
6362
6866
|
];
|
|
6363
|
-
const rollbackPath =
|
|
6364
|
-
await
|
|
6867
|
+
const rollbackPath = import_node_path40.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
6868
|
+
await import_node_fs25.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
6869
|
+
}
|
|
6870
|
+
function injectInstrumentationHook(raw) {
|
|
6871
|
+
if (raw.includes("instrumentationHook")) return raw;
|
|
6872
|
+
const anchors = [
|
|
6873
|
+
{ pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
|
|
6874
|
+
{ pattern: /(export\s+default\s*\{)/, label: "esm-default" },
|
|
6875
|
+
{ pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
|
|
6876
|
+
];
|
|
6877
|
+
for (const { pattern } of anchors) {
|
|
6878
|
+
const match = pattern.exec(raw);
|
|
6879
|
+
if (!match) continue;
|
|
6880
|
+
const insertAfter = match.index + match[0].length;
|
|
6881
|
+
const before = raw.slice(0, insertAfter);
|
|
6882
|
+
const after = raw.slice(insertAfter);
|
|
6883
|
+
const injection = "\n experimental: { instrumentationHook: true },";
|
|
6884
|
+
return `${before}${injection}${after}`;
|
|
6885
|
+
}
|
|
6886
|
+
return null;
|
|
6365
6887
|
}
|
|
6366
6888
|
var javascriptInstaller = {
|
|
6367
6889
|
name: "javascript",
|
|
@@ -6372,8 +6894,8 @@ var javascriptInstaller = {
|
|
|
6372
6894
|
|
|
6373
6895
|
// src/installers/python.ts
|
|
6374
6896
|
init_cjs_shims();
|
|
6375
|
-
var
|
|
6376
|
-
var
|
|
6897
|
+
var import_node_fs26 = require("fs");
|
|
6898
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
6377
6899
|
var SDK_PACKAGES2 = [
|
|
6378
6900
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
6379
6901
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -6385,7 +6907,7 @@ var OTEL_ENV2 = {
|
|
|
6385
6907
|
};
|
|
6386
6908
|
async function exists3(p) {
|
|
6387
6909
|
try {
|
|
6388
|
-
await
|
|
6910
|
+
await import_node_fs26.promises.stat(p);
|
|
6389
6911
|
return true;
|
|
6390
6912
|
} catch {
|
|
6391
6913
|
return false;
|
|
@@ -6394,7 +6916,7 @@ async function exists3(p) {
|
|
|
6394
6916
|
async function detect2(serviceDir) {
|
|
6395
6917
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
6396
6918
|
for (const m of markers) {
|
|
6397
|
-
if (await exists3(
|
|
6919
|
+
if (await exists3(import_node_path41.default.join(serviceDir, m))) return true;
|
|
6398
6920
|
}
|
|
6399
6921
|
return false;
|
|
6400
6922
|
}
|
|
@@ -6404,9 +6926,9 @@ function reqPackageName(line) {
|
|
|
6404
6926
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
6405
6927
|
}
|
|
6406
6928
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
6407
|
-
const file =
|
|
6929
|
+
const file = import_node_path41.default.join(serviceDir, "requirements.txt");
|
|
6408
6930
|
if (!await exists3(file)) return null;
|
|
6409
|
-
const raw = await
|
|
6931
|
+
const raw = await import_node_fs26.promises.readFile(file, "utf8");
|
|
6410
6932
|
const presentNames = new Set(
|
|
6411
6933
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
6412
6934
|
);
|
|
@@ -6414,9 +6936,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
6414
6936
|
return { manifest: file, missing: [...missing] };
|
|
6415
6937
|
}
|
|
6416
6938
|
async function planProcfileEdits(serviceDir) {
|
|
6417
|
-
const procfile =
|
|
6939
|
+
const procfile = import_node_path41.default.join(serviceDir, "Procfile");
|
|
6418
6940
|
if (!await exists3(procfile)) return [];
|
|
6419
|
-
const raw = await
|
|
6941
|
+
const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
|
|
6420
6942
|
const edits = [];
|
|
6421
6943
|
for (const line of raw.split(/\r?\n/)) {
|
|
6422
6944
|
if (line.length === 0) continue;
|
|
@@ -6468,8 +6990,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
6468
6990
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
6469
6991
|
`;
|
|
6470
6992
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
6471
|
-
await
|
|
6472
|
-
await
|
|
6993
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
6994
|
+
await import_node_fs26.promises.rename(tmp, manifest);
|
|
6473
6995
|
}
|
|
6474
6996
|
async function applyProcfile(procfile, edits, original) {
|
|
6475
6997
|
let next = original;
|
|
@@ -6478,8 +7000,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
6478
7000
|
next = next.replace(e.before, e.after);
|
|
6479
7001
|
}
|
|
6480
7002
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
6481
|
-
await
|
|
6482
|
-
await
|
|
7003
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
7004
|
+
await import_node_fs26.promises.rename(tmp, procfile);
|
|
6483
7005
|
}
|
|
6484
7006
|
async function apply2(installPlan) {
|
|
6485
7007
|
const { serviceDir } = installPlan;
|
|
@@ -6492,7 +7014,7 @@ async function apply2(installPlan) {
|
|
|
6492
7014
|
const originals = /* @__PURE__ */ new Map();
|
|
6493
7015
|
for (const file of touched) {
|
|
6494
7016
|
try {
|
|
6495
|
-
originals.set(file, await
|
|
7017
|
+
originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
|
|
6496
7018
|
} catch {
|
|
6497
7019
|
}
|
|
6498
7020
|
}
|
|
@@ -6503,7 +7025,7 @@ async function apply2(installPlan) {
|
|
|
6503
7025
|
if (raw === void 0) {
|
|
6504
7026
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
6505
7027
|
}
|
|
6506
|
-
const base =
|
|
7028
|
+
const base = import_node_path41.default.basename(file);
|
|
6507
7029
|
if (base === "requirements.txt") {
|
|
6508
7030
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
6509
7031
|
if (edits.length > 0) {
|
|
@@ -6528,7 +7050,7 @@ async function rollback2(installPlan, originals) {
|
|
|
6528
7050
|
const restored = [];
|
|
6529
7051
|
for (const [file, raw] of originals.entries()) {
|
|
6530
7052
|
try {
|
|
6531
|
-
await
|
|
7053
|
+
await import_node_fs26.promises.writeFile(file, raw, "utf8");
|
|
6532
7054
|
restored.push(file);
|
|
6533
7055
|
} catch {
|
|
6534
7056
|
}
|
|
@@ -6542,8 +7064,8 @@ async function rollback2(installPlan, originals) {
|
|
|
6542
7064
|
...restored.map((f) => `restored: ${f}`),
|
|
6543
7065
|
""
|
|
6544
7066
|
];
|
|
6545
|
-
const rollbackPath =
|
|
6546
|
-
await
|
|
7067
|
+
const rollbackPath = import_node_path41.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
7068
|
+
await import_node_fs26.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
6547
7069
|
}
|
|
6548
7070
|
var pythonInstaller = {
|
|
6549
7071
|
name: "python",
|
|
@@ -6555,7 +7077,7 @@ var pythonInstaller = {
|
|
|
6555
7077
|
// src/installers/shared.ts
|
|
6556
7078
|
init_cjs_shims();
|
|
6557
7079
|
function isEmptyPlan(plan3) {
|
|
6558
|
-
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
|
|
7080
|
+
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
|
|
6559
7081
|
}
|
|
6560
7082
|
|
|
6561
7083
|
// src/installers/index.ts
|
|
@@ -6654,16 +7176,231 @@ function renderPatch(sections) {
|
|
|
6654
7176
|
}
|
|
6655
7177
|
lines.push("");
|
|
6656
7178
|
}
|
|
7179
|
+
if (plan3.nextConfigEdit) {
|
|
7180
|
+
lines.push("### next.config (framework flag)");
|
|
7181
|
+
lines.push(`--- ${plan3.nextConfigEdit.file}`);
|
|
7182
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
|
|
7183
|
+
lines.push("");
|
|
7184
|
+
}
|
|
6657
7185
|
}
|
|
6658
7186
|
return lines.join("\n");
|
|
6659
7187
|
}
|
|
6660
7188
|
|
|
7189
|
+
// src/orchestrator.ts
|
|
7190
|
+
init_cjs_shims();
|
|
7191
|
+
var import_node_fs27 = require("fs");
|
|
7192
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
7193
|
+
var import_node_path42 = __toESM(require("path"), 1);
|
|
7194
|
+
var import_node_child_process2 = require("child_process");
|
|
7195
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
7196
|
+
async function promptYesNo(question) {
|
|
7197
|
+
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
7198
|
+
return new Promise((resolve) => {
|
|
7199
|
+
rl.question(`${question} [Y/n] `, (answer) => {
|
|
7200
|
+
rl.close();
|
|
7201
|
+
const trimmed = answer.trim().toLowerCase();
|
|
7202
|
+
resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
7203
|
+
});
|
|
7204
|
+
});
|
|
7205
|
+
}
|
|
7206
|
+
var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
|
|
7207
|
+
async function checkDaemonHealth(restPort) {
|
|
7208
|
+
return new Promise((resolve) => {
|
|
7209
|
+
const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
|
|
7210
|
+
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
7211
|
+
res.resume();
|
|
7212
|
+
resolve(ok);
|
|
7213
|
+
});
|
|
7214
|
+
req.on("error", () => resolve(false));
|
|
7215
|
+
req.setTimeout(1e3, () => {
|
|
7216
|
+
req.destroy();
|
|
7217
|
+
resolve(false);
|
|
7218
|
+
});
|
|
7219
|
+
});
|
|
7220
|
+
}
|
|
7221
|
+
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
7222
|
+
const deadline = Date.now() + timeoutMs;
|
|
7223
|
+
while (Date.now() < deadline) {
|
|
7224
|
+
if (await checkDaemonHealth(restPort)) return true;
|
|
7225
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
7226
|
+
}
|
|
7227
|
+
return false;
|
|
7228
|
+
}
|
|
7229
|
+
function spawnDaemonDetached() {
|
|
7230
|
+
const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
|
|
7231
|
+
const candidates = [
|
|
7232
|
+
import_node_path42.default.join(here, "neatd.cjs"),
|
|
7233
|
+
import_node_path42.default.join(here, "neatd.js")
|
|
7234
|
+
];
|
|
7235
|
+
let entry2 = null;
|
|
7236
|
+
const fsSync = require("fs");
|
|
7237
|
+
for (const c of candidates) {
|
|
7238
|
+
try {
|
|
7239
|
+
fsSync.accessSync(c);
|
|
7240
|
+
entry2 = c;
|
|
7241
|
+
break;
|
|
7242
|
+
} catch {
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
if (!entry2) {
|
|
7246
|
+
throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
|
|
7247
|
+
}
|
|
7248
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
|
|
7249
|
+
detached: true,
|
|
7250
|
+
stdio: "ignore",
|
|
7251
|
+
env: process.env
|
|
7252
|
+
});
|
|
7253
|
+
child.unref();
|
|
7254
|
+
}
|
|
7255
|
+
function openBrowser(url) {
|
|
7256
|
+
if (!process.stdout.isTTY) return "failed";
|
|
7257
|
+
const platform = process.platform;
|
|
7258
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
7259
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
7260
|
+
try {
|
|
7261
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
7262
|
+
child.on("error", () => {
|
|
7263
|
+
});
|
|
7264
|
+
child.unref();
|
|
7265
|
+
return "opened";
|
|
7266
|
+
} catch {
|
|
7267
|
+
return "failed";
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
async function runOrchestrator(opts) {
|
|
7271
|
+
const result = {
|
|
7272
|
+
exitCode: 0,
|
|
7273
|
+
steps: {
|
|
7274
|
+
discovery: { services: 0, languages: [] },
|
|
7275
|
+
extraction: { nodesAdded: 0, edgesAdded: 0 },
|
|
7276
|
+
gitignore: "unchanged",
|
|
7277
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
|
|
7278
|
+
daemon: "skipped",
|
|
7279
|
+
browser: "skipped"
|
|
7280
|
+
}
|
|
7281
|
+
};
|
|
7282
|
+
const stat = await import_node_fs27.promises.stat(opts.scanPath).catch(() => null);
|
|
7283
|
+
if (!stat || !stat.isDirectory()) {
|
|
7284
|
+
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
7285
|
+
result.exitCode = 2;
|
|
7286
|
+
return result;
|
|
7287
|
+
}
|
|
7288
|
+
console.log(`neat: ${opts.scanPath}`);
|
|
7289
|
+
console.log("");
|
|
7290
|
+
const services = await discoverServices(opts.scanPath);
|
|
7291
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7292
|
+
result.steps.discovery = { services: services.length, languages };
|
|
7293
|
+
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
7294
|
+
let runApply = !opts.noInstrument;
|
|
7295
|
+
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
7296
|
+
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
7297
|
+
}
|
|
7298
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
7299
|
+
resetGraph(graphKey);
|
|
7300
|
+
const graph = getGraph(graphKey);
|
|
7301
|
+
const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
|
|
7302
|
+
const errorsPath = projectPaths.errorsPath;
|
|
7303
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7304
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
7305
|
+
result.steps.extraction = {
|
|
7306
|
+
nodesAdded: extraction.nodesAdded,
|
|
7307
|
+
edgesAdded: extraction.edgesAdded
|
|
7308
|
+
};
|
|
7309
|
+
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
7310
|
+
result.steps.gitignore = gi.action;
|
|
7311
|
+
if (gi.action !== "unchanged") {
|
|
7312
|
+
console.log(`${gi.action} .gitignore (neat-out/)`);
|
|
7313
|
+
}
|
|
7314
|
+
try {
|
|
7315
|
+
await addProject({
|
|
7316
|
+
name: opts.project,
|
|
7317
|
+
path: opts.scanPath,
|
|
7318
|
+
languages,
|
|
7319
|
+
status: "active"
|
|
7320
|
+
});
|
|
7321
|
+
} catch (err) {
|
|
7322
|
+
if (!(err instanceof ProjectNameCollisionError)) throw err;
|
|
7323
|
+
console.error(`neat: ${err.message}`);
|
|
7324
|
+
console.error("pass --project <other-name> to register under a different name.");
|
|
7325
|
+
result.exitCode = 1;
|
|
7326
|
+
return result;
|
|
7327
|
+
}
|
|
7328
|
+
if (!runApply) {
|
|
7329
|
+
result.steps.apply.skipped = true;
|
|
7330
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
7331
|
+
} else {
|
|
7332
|
+
let instrumented = 0;
|
|
7333
|
+
let already = 0;
|
|
7334
|
+
let libOnly = 0;
|
|
7335
|
+
for (const svc of services) {
|
|
7336
|
+
const installer = await pickInstaller(svc.dir);
|
|
7337
|
+
if (!installer) continue;
|
|
7338
|
+
const plan3 = await installer.plan(svc.dir);
|
|
7339
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
7340
|
+
already++;
|
|
7341
|
+
continue;
|
|
7342
|
+
}
|
|
7343
|
+
const outcome = await installer.apply(plan3);
|
|
7344
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
7345
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
7346
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
7347
|
+
}
|
|
7348
|
+
result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
|
|
7349
|
+
console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
|
|
7350
|
+
}
|
|
7351
|
+
const restPort = Number(process.env.PORT ?? 8080);
|
|
7352
|
+
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
7353
|
+
if (await checkDaemonHealth(restPort)) {
|
|
7354
|
+
result.steps.daemon = "already-running";
|
|
7355
|
+
} else {
|
|
7356
|
+
try {
|
|
7357
|
+
spawnDaemonDetached();
|
|
7358
|
+
} catch (err) {
|
|
7359
|
+
console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
|
|
7360
|
+
result.exitCode = 1;
|
|
7361
|
+
return result;
|
|
7362
|
+
}
|
|
7363
|
+
const ready = await waitForDaemonReady(restPort, timeoutMs);
|
|
7364
|
+
result.steps.daemon = ready ? "spawned" : "timed-out";
|
|
7365
|
+
if (!ready) {
|
|
7366
|
+
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
7367
|
+
result.exitCode = 1;
|
|
7368
|
+
return result;
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7371
|
+
const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
|
|
7372
|
+
if (opts.noOpen || !process.stdout.isTTY) {
|
|
7373
|
+
result.steps.browser = "skipped";
|
|
7374
|
+
} else {
|
|
7375
|
+
result.steps.browser = openBrowser(dashboardUrl);
|
|
7376
|
+
}
|
|
7377
|
+
printSummary(result, graph, dashboardUrl);
|
|
7378
|
+
return result;
|
|
7379
|
+
}
|
|
7380
|
+
function printSummary(result, graph, dashboardUrl) {
|
|
7381
|
+
const nodes = [];
|
|
7382
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
7383
|
+
const edges = [];
|
|
7384
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
7385
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
7386
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
7387
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
7388
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
7389
|
+
console.log("");
|
|
7390
|
+
console.log("=== summary ===");
|
|
7391
|
+
console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
|
|
7392
|
+
for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
7393
|
+
for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
7394
|
+
console.log("");
|
|
7395
|
+
console.log(`dashboard: ${dashboardUrl}`);
|
|
7396
|
+
}
|
|
7397
|
+
|
|
6661
7398
|
// src/cli.ts
|
|
6662
|
-
var
|
|
7399
|
+
var import_types25 = require("@neat.is/types");
|
|
6663
7400
|
|
|
6664
7401
|
// src/cli-client.ts
|
|
6665
7402
|
init_cjs_shims();
|
|
6666
|
-
var
|
|
7403
|
+
var import_types24 = require("@neat.is/types");
|
|
6667
7404
|
var HttpError = class extends Error {
|
|
6668
7405
|
constructor(status2, message, responseBody = "") {
|
|
6669
7406
|
super(message);
|
|
@@ -6683,10 +7420,10 @@ var TransportError = class extends Error {
|
|
|
6683
7420
|
function createHttpClient(baseUrl) {
|
|
6684
7421
|
const root = baseUrl.replace(/\/$/, "");
|
|
6685
7422
|
return {
|
|
6686
|
-
async get(
|
|
7423
|
+
async get(path44) {
|
|
6687
7424
|
let res;
|
|
6688
7425
|
try {
|
|
6689
|
-
res = await fetch(`${root}${
|
|
7426
|
+
res = await fetch(`${root}${path44}`);
|
|
6690
7427
|
} catch (err) {
|
|
6691
7428
|
throw new TransportError(
|
|
6692
7429
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -6696,16 +7433,16 @@ function createHttpClient(baseUrl) {
|
|
|
6696
7433
|
const body = await res.text().catch(() => "");
|
|
6697
7434
|
throw new HttpError(
|
|
6698
7435
|
res.status,
|
|
6699
|
-
`${res.status} ${res.statusText} on GET ${
|
|
7436
|
+
`${res.status} ${res.statusText} on GET ${path44}: ${body}`,
|
|
6700
7437
|
body
|
|
6701
7438
|
);
|
|
6702
7439
|
}
|
|
6703
7440
|
return await res.json();
|
|
6704
7441
|
},
|
|
6705
|
-
async post(
|
|
7442
|
+
async post(path44, body) {
|
|
6706
7443
|
let res;
|
|
6707
7444
|
try {
|
|
6708
|
-
res = await fetch(`${root}${
|
|
7445
|
+
res = await fetch(`${root}${path44}`, {
|
|
6709
7446
|
method: "POST",
|
|
6710
7447
|
headers: { "content-type": "application/json" },
|
|
6711
7448
|
body: JSON.stringify(body)
|
|
@@ -6719,7 +7456,7 @@ function createHttpClient(baseUrl) {
|
|
|
6719
7456
|
const text = await res.text().catch(() => "");
|
|
6720
7457
|
throw new HttpError(
|
|
6721
7458
|
res.status,
|
|
6722
|
-
`${res.status} ${res.statusText} on POST ${
|
|
7459
|
+
`${res.status} ${res.statusText} on POST ${path44}: ${text}`,
|
|
6723
7460
|
text
|
|
6724
7461
|
);
|
|
6725
7462
|
}
|
|
@@ -6733,12 +7470,12 @@ function projectPath(project, suffix) {
|
|
|
6733
7470
|
}
|
|
6734
7471
|
async function runRootCause(client, input) {
|
|
6735
7472
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
6736
|
-
const
|
|
7473
|
+
const path44 = projectPath(
|
|
6737
7474
|
input.project,
|
|
6738
7475
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
6739
7476
|
);
|
|
6740
7477
|
try {
|
|
6741
|
-
const result = await client.get(
|
|
7478
|
+
const result = await client.get(path44);
|
|
6742
7479
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
6743
7480
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
6744
7481
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -6764,12 +7501,12 @@ async function runRootCause(client, input) {
|
|
|
6764
7501
|
}
|
|
6765
7502
|
async function runBlastRadius(client, input) {
|
|
6766
7503
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
6767
|
-
const
|
|
7504
|
+
const path44 = projectPath(
|
|
6768
7505
|
input.project,
|
|
6769
7506
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
6770
7507
|
);
|
|
6771
7508
|
try {
|
|
6772
|
-
const result = await client.get(
|
|
7509
|
+
const result = await client.get(path44);
|
|
6773
7510
|
if (result.totalAffected === 0) {
|
|
6774
7511
|
return {
|
|
6775
7512
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -6798,17 +7535,17 @@ async function runBlastRadius(client, input) {
|
|
|
6798
7535
|
}
|
|
6799
7536
|
}
|
|
6800
7537
|
function formatBlastEntry(n) {
|
|
6801
|
-
const tag = n.edgeProvenance ===
|
|
7538
|
+
const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
6802
7539
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
6803
7540
|
}
|
|
6804
7541
|
async function runDependencies(client, input) {
|
|
6805
7542
|
const depth = input.depth ?? 3;
|
|
6806
|
-
const
|
|
7543
|
+
const path44 = projectPath(
|
|
6807
7544
|
input.project,
|
|
6808
7545
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6809
7546
|
);
|
|
6810
7547
|
try {
|
|
6811
|
-
const result = await client.get(
|
|
7548
|
+
const result = await client.get(path44);
|
|
6812
7549
|
if (result.total === 0) {
|
|
6813
7550
|
return {
|
|
6814
7551
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -6844,9 +7581,9 @@ async function runObservedDependencies(client, input) {
|
|
|
6844
7581
|
const edges = await client.get(
|
|
6845
7582
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
6846
7583
|
);
|
|
6847
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
7584
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
|
|
6848
7585
|
if (observed.length === 0) {
|
|
6849
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
7586
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
|
|
6850
7587
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
6851
7588
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
6852
7589
|
}
|
|
@@ -6854,7 +7591,7 @@ async function runObservedDependencies(client, input) {
|
|
|
6854
7591
|
return {
|
|
6855
7592
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
6856
7593
|
block: blockLines.join("\n"),
|
|
6857
|
-
provenance:
|
|
7594
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6858
7595
|
};
|
|
6859
7596
|
} catch (err) {
|
|
6860
7597
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -6889,9 +7626,9 @@ function formatDuration(ms) {
|
|
|
6889
7626
|
return `${Math.round(h / 24)}d`;
|
|
6890
7627
|
}
|
|
6891
7628
|
async function runIncidents(client, input) {
|
|
6892
|
-
const
|
|
7629
|
+
const path44 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6893
7630
|
try {
|
|
6894
|
-
const body = await client.get(
|
|
7631
|
+
const body = await client.get(path44);
|
|
6895
7632
|
const events = body.events;
|
|
6896
7633
|
if (events.length === 0) {
|
|
6897
7634
|
return {
|
|
@@ -6908,7 +7645,7 @@ async function runIncidents(client, input) {
|
|
|
6908
7645
|
return {
|
|
6909
7646
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
6910
7647
|
block: blockLines.join("\n"),
|
|
6911
|
-
provenance:
|
|
7648
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6912
7649
|
};
|
|
6913
7650
|
} catch (err) {
|
|
6914
7651
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -7017,7 +7754,7 @@ async function runStaleEdges(client, input) {
|
|
|
7017
7754
|
return {
|
|
7018
7755
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
7019
7756
|
block: blockLines.join("\n"),
|
|
7020
|
-
provenance:
|
|
7757
|
+
provenance: import_types24.Provenance.STALE
|
|
7021
7758
|
};
|
|
7022
7759
|
}
|
|
7023
7760
|
async function runPolicies(client, input) {
|
|
@@ -7183,6 +7920,9 @@ function usage() {
|
|
|
7183
7920
|
console.log(" Flags:");
|
|
7184
7921
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
7185
7922
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
7923
|
+
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
7924
|
+
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
7925
|
+
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
7186
7926
|
console.log("");
|
|
7187
7927
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7188
7928
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -7245,6 +7985,10 @@ function parseArgs(rest) {
|
|
|
7245
7985
|
apply: false,
|
|
7246
7986
|
dryRun: false,
|
|
7247
7987
|
noInstall: false,
|
|
7988
|
+
noInstrument: false,
|
|
7989
|
+
noOpen: false,
|
|
7990
|
+
yes: false,
|
|
7991
|
+
verbose: false,
|
|
7248
7992
|
printConfig: false,
|
|
7249
7993
|
json: false,
|
|
7250
7994
|
depth: null,
|
|
@@ -7273,6 +8017,22 @@ function parseArgs(rest) {
|
|
|
7273
8017
|
out.noInstall = true;
|
|
7274
8018
|
continue;
|
|
7275
8019
|
}
|
|
8020
|
+
if (arg === "--no-instrument") {
|
|
8021
|
+
out.noInstrument = true;
|
|
8022
|
+
continue;
|
|
8023
|
+
}
|
|
8024
|
+
if (arg === "--no-open") {
|
|
8025
|
+
out.noOpen = true;
|
|
8026
|
+
continue;
|
|
8027
|
+
}
|
|
8028
|
+
if (arg === "--yes" || arg === "-y") {
|
|
8029
|
+
out.yes = true;
|
|
8030
|
+
continue;
|
|
8031
|
+
}
|
|
8032
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
8033
|
+
out.verbose = true;
|
|
8034
|
+
continue;
|
|
8035
|
+
}
|
|
7276
8036
|
if (arg === "--print-config") {
|
|
7277
8037
|
out.printConfig = true;
|
|
7278
8038
|
continue;
|
|
@@ -7328,34 +8088,6 @@ function assignFlag(out, field, value) {
|
|
|
7328
8088
|
;
|
|
7329
8089
|
out[field] = value;
|
|
7330
8090
|
}
|
|
7331
|
-
function summarise(nodes, edges) {
|
|
7332
|
-
const byNode = /* @__PURE__ */ new Map();
|
|
7333
|
-
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
7334
|
-
const byEdge = /* @__PURE__ */ new Map();
|
|
7335
|
-
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
7336
|
-
const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7337
|
-
const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7338
|
-
return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
|
|
7339
|
-
}
|
|
7340
|
-
function formatIncompat(inc) {
|
|
7341
|
-
if (inc.kind === "node-engine") {
|
|
7342
|
-
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
7343
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
7344
|
-
}
|
|
7345
|
-
if (inc.kind === "package-conflict") {
|
|
7346
|
-
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
7347
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
7348
|
-
}
|
|
7349
|
-
if (inc.kind === "deprecated-api") {
|
|
7350
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
7351
|
-
}
|
|
7352
|
-
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
7353
|
-
}
|
|
7354
|
-
function findIncompatibilities(nodes) {
|
|
7355
|
-
return nodes.filter(
|
|
7356
|
-
(n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
7357
|
-
);
|
|
7358
|
-
}
|
|
7359
8091
|
function printBanner() {
|
|
7360
8092
|
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");
|
|
7361
8093
|
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");
|
|
@@ -7406,7 +8138,7 @@ async function buildPatchSections(services) {
|
|
|
7406
8138
|
}
|
|
7407
8139
|
async function runInit(opts) {
|
|
7408
8140
|
const written = [];
|
|
7409
|
-
const stat = await
|
|
8141
|
+
const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
|
|
7410
8142
|
if (!stat || !stat.isDirectory()) {
|
|
7411
8143
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
7412
8144
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -7415,11 +8147,15 @@ async function runInit(opts) {
|
|
|
7415
8147
|
printDiscoveryReport(opts, services);
|
|
7416
8148
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
7417
8149
|
const patch = renderPatch(sections);
|
|
7418
|
-
const patchPath =
|
|
8150
|
+
const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
|
|
7419
8151
|
if (opts.dryRun) {
|
|
7420
|
-
await
|
|
8152
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7421
8153
|
written.push(patchPath);
|
|
7422
8154
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8155
|
+
const gitignorePath = import_node_path43.default.join(opts.scanPath, ".gitignore");
|
|
8156
|
+
const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8157
|
+
const verb = gitignoreExists ? "append" : "create";
|
|
8158
|
+
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
7423
8159
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
7424
8160
|
return { exitCode: 0, writtenFiles: written };
|
|
7425
8161
|
}
|
|
@@ -7428,12 +8164,16 @@ async function runInit(opts) {
|
|
|
7428
8164
|
const graph = getGraph(graphKey);
|
|
7429
8165
|
const projectPaths = pathsForProject(
|
|
7430
8166
|
graphKey,
|
|
7431
|
-
|
|
8167
|
+
import_node_path43.default.join(opts.scanPath, "neat-out")
|
|
7432
8168
|
);
|
|
7433
|
-
const errorsPath =
|
|
8169
|
+
const errorsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), import_node_path43.default.basename(projectPaths.errorsPath));
|
|
7434
8170
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7435
8171
|
await saveGraphToDisk(graph, opts.outPath);
|
|
7436
8172
|
written.push(opts.outPath);
|
|
8173
|
+
const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
|
|
8174
|
+
if (gitignoreResult.action !== "unchanged") {
|
|
8175
|
+
written.push(gitignoreResult.file);
|
|
8176
|
+
}
|
|
7437
8177
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7438
8178
|
try {
|
|
7439
8179
|
await addProject({
|
|
@@ -7476,35 +8216,27 @@ async function runInit(opts) {
|
|
|
7476
8216
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
7477
8217
|
}
|
|
7478
8218
|
} else {
|
|
7479
|
-
await
|
|
8219
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7480
8220
|
written.push(patchPath);
|
|
7481
8221
|
}
|
|
7482
8222
|
}
|
|
7483
|
-
const nodes = [];
|
|
7484
|
-
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
7485
|
-
const edges = [];
|
|
7486
|
-
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
7487
8223
|
console.log("");
|
|
7488
|
-
console.log("=== neat init: summary ===");
|
|
7489
8224
|
console.log(`snapshot: ${opts.outPath}`);
|
|
7490
8225
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
7491
|
-
console.log(
|
|
7492
|
-
|
|
8226
|
+
console.log("");
|
|
8227
|
+
const divergenceResult = computeDivergences(graph);
|
|
8228
|
+
console.log(
|
|
8229
|
+
renderValueForwardSummary({
|
|
8230
|
+
graph,
|
|
8231
|
+
divergences: divergenceResult.divergences,
|
|
8232
|
+
verbose: opts.verbose
|
|
8233
|
+
})
|
|
8234
|
+
);
|
|
7493
8235
|
console.log(formatExtractionBanner(result.extractionErrors));
|
|
7494
8236
|
if (result.extractionErrors > 0) {
|
|
7495
8237
|
console.log(`errors: ${errorsPath}`);
|
|
7496
8238
|
}
|
|
7497
8239
|
console.log(formatPrecisionFloorBanner(result.extractedDropped));
|
|
7498
|
-
const incompatibilities = findIncompatibilities(nodes);
|
|
7499
|
-
if (incompatibilities.length > 0) {
|
|
7500
|
-
console.log("");
|
|
7501
|
-
console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
|
|
7502
|
-
for (const svc of incompatibilities) {
|
|
7503
|
-
for (const inc of svc.incompatibilities ?? []) {
|
|
7504
|
-
console.log(` ${svc.name}: ${formatIncompat(inc)}`);
|
|
7505
|
-
}
|
|
7506
|
-
}
|
|
7507
|
-
}
|
|
7508
8240
|
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
7509
8241
|
return { exitCode: 4, writtenFiles: written };
|
|
7510
8242
|
}
|
|
@@ -7524,9 +8256,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
7524
8256
|
};
|
|
7525
8257
|
function claudeConfigPath() {
|
|
7526
8258
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
7527
|
-
if (override && override.length > 0) return
|
|
8259
|
+
if (override && override.length > 0) return import_node_path43.default.resolve(override);
|
|
7528
8260
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7529
|
-
return
|
|
8261
|
+
return import_node_path43.default.join(home, ".claude.json");
|
|
7530
8262
|
}
|
|
7531
8263
|
async function runSkill(opts) {
|
|
7532
8264
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -7538,7 +8270,7 @@ async function runSkill(opts) {
|
|
|
7538
8270
|
const target = claudeConfigPath();
|
|
7539
8271
|
let existing = {};
|
|
7540
8272
|
try {
|
|
7541
|
-
existing = JSON.parse(await
|
|
8273
|
+
existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
|
|
7542
8274
|
} catch (err) {
|
|
7543
8275
|
if (err.code !== "ENOENT") {
|
|
7544
8276
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -7550,8 +8282,8 @@ async function runSkill(opts) {
|
|
|
7550
8282
|
...existing,
|
|
7551
8283
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
7552
8284
|
};
|
|
7553
|
-
await
|
|
7554
|
-
await
|
|
8285
|
+
await import_node_fs28.promises.mkdir(import_node_path43.default.dirname(target), { recursive: true });
|
|
8286
|
+
await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
7555
8287
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
7556
8288
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
7557
8289
|
return { exitCode: 0 };
|
|
@@ -7585,12 +8317,12 @@ async function main() {
|
|
|
7585
8317
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
7586
8318
|
process.exit(2);
|
|
7587
8319
|
}
|
|
7588
|
-
const scanPath =
|
|
8320
|
+
const scanPath = import_node_path43.default.resolve(target);
|
|
7589
8321
|
const projectExplicit = parsed.project !== null;
|
|
7590
|
-
const projectName = projectExplicit ? project :
|
|
8322
|
+
const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
|
|
7591
8323
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
7592
|
-
const fallback = pathsForProject(projectKey,
|
|
7593
|
-
const outPath =
|
|
8324
|
+
const fallback = pathsForProject(projectKey, import_node_path43.default.join(scanPath, "neat-out")).snapshotPath;
|
|
8325
|
+
const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
7594
8326
|
const result = await runInit({
|
|
7595
8327
|
scanPath,
|
|
7596
8328
|
outPath,
|
|
@@ -7598,7 +8330,8 @@ async function main() {
|
|
|
7598
8330
|
projectExplicit,
|
|
7599
8331
|
apply: apply3,
|
|
7600
8332
|
dryRun,
|
|
7601
|
-
noInstall
|
|
8333
|
+
noInstall,
|
|
8334
|
+
verbose: parsed.verbose
|
|
7602
8335
|
});
|
|
7603
8336
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
7604
8337
|
return;
|
|
@@ -7610,21 +8343,21 @@ async function main() {
|
|
|
7610
8343
|
usage();
|
|
7611
8344
|
process.exit(2);
|
|
7612
8345
|
}
|
|
7613
|
-
const scanPath =
|
|
7614
|
-
const stat = await
|
|
8346
|
+
const scanPath = import_node_path43.default.resolve(target);
|
|
8347
|
+
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
7615
8348
|
if (!stat || !stat.isDirectory()) {
|
|
7616
8349
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
7617
8350
|
process.exit(2);
|
|
7618
8351
|
}
|
|
7619
|
-
const projectPaths = pathsForProject(project,
|
|
7620
|
-
const outPath =
|
|
7621
|
-
const errorsPath =
|
|
7622
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
8352
|
+
const projectPaths = pathsForProject(project, import_node_path43.default.join(scanPath, "neat-out"));
|
|
8353
|
+
const outPath = import_node_path43.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
8354
|
+
const errorsPath = import_node_path43.default.resolve(
|
|
8355
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.errorsPath))
|
|
7623
8356
|
);
|
|
7624
|
-
const staleEventsPath =
|
|
7625
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
8357
|
+
const staleEventsPath = import_node_path43.default.resolve(
|
|
8358
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path43.default.join(import_node_path43.default.dirname(outPath), import_node_path43.default.basename(projectPaths.staleEventsPath))
|
|
7626
8359
|
);
|
|
7627
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
8360
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path43.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
7628
8361
|
const handle = await startWatch(getGraph(project), {
|
|
7629
8362
|
scanPath,
|
|
7630
8363
|
outPath,
|
|
@@ -7717,15 +8450,62 @@ async function main() {
|
|
|
7717
8450
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
7718
8451
|
return;
|
|
7719
8452
|
}
|
|
8453
|
+
if (cmd === "deploy") {
|
|
8454
|
+
const artifact = await runDeploy();
|
|
8455
|
+
const block = renderOtelEnvBlock2(artifact.token);
|
|
8456
|
+
console.log();
|
|
8457
|
+
console.log(`Substrate detected: ${artifact.substrate}`);
|
|
8458
|
+
if (artifact.artifactPath) {
|
|
8459
|
+
console.log(`Artifact written: ${artifact.artifactPath}`);
|
|
8460
|
+
} else {
|
|
8461
|
+
console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
|
|
8462
|
+
console.log();
|
|
8463
|
+
console.log(artifact.contents);
|
|
8464
|
+
}
|
|
8465
|
+
console.log();
|
|
8466
|
+
console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
|
|
8467
|
+
console.log(` ${artifact.token}`);
|
|
8468
|
+
console.log();
|
|
8469
|
+
console.log("For your application's deploy platform, set these env vars:");
|
|
8470
|
+
console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
8471
|
+
console.log();
|
|
8472
|
+
console.log("Once NEAT is running, your dashboard will be at:");
|
|
8473
|
+
console.log(" https://<host>:6328");
|
|
8474
|
+
console.log();
|
|
8475
|
+
console.log("To start NEAT, run:");
|
|
8476
|
+
console.log(` ${artifact.startCommand}`);
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
7720
8479
|
if (QUERY_VERBS.has(cmd)) {
|
|
7721
8480
|
const code = await runQueryVerb(cmd, parsed);
|
|
7722
8481
|
if (code !== 0) process.exit(code);
|
|
7723
8482
|
return;
|
|
7724
8483
|
}
|
|
8484
|
+
const orchestratorCode = await tryOrchestrator(cmd, parsed);
|
|
8485
|
+
if (orchestratorCode !== null) {
|
|
8486
|
+
if (orchestratorCode !== 0) process.exit(orchestratorCode);
|
|
8487
|
+
return;
|
|
8488
|
+
}
|
|
7725
8489
|
console.error(`neat: unknown command "${cmd}"`);
|
|
7726
8490
|
usage();
|
|
7727
8491
|
process.exit(1);
|
|
7728
8492
|
}
|
|
8493
|
+
async function tryOrchestrator(cmd, parsed) {
|
|
8494
|
+
const scanPath = import_node_path43.default.resolve(cmd);
|
|
8495
|
+
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
8496
|
+
if (!stat || !stat.isDirectory()) return null;
|
|
8497
|
+
const projectExplicit = parsed.project !== null;
|
|
8498
|
+
const projectName = projectExplicit ? parsed.project : import_node_path43.default.basename(scanPath);
|
|
8499
|
+
const result = await runOrchestrator({
|
|
8500
|
+
scanPath,
|
|
8501
|
+
project: projectName,
|
|
8502
|
+
projectExplicit,
|
|
8503
|
+
noInstrument: parsed.noInstrument,
|
|
8504
|
+
noOpen: parsed.noOpen,
|
|
8505
|
+
yes: parsed.yes
|
|
8506
|
+
});
|
|
8507
|
+
return result.exitCode;
|
|
8508
|
+
}
|
|
7729
8509
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
7730
8510
|
"root-cause",
|
|
7731
8511
|
"blast-radius",
|
|
@@ -7865,10 +8645,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
7865
8645
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7866
8646
|
const out = [];
|
|
7867
8647
|
for (const p of parts) {
|
|
7868
|
-
const r =
|
|
8648
|
+
const r = import_types25.DivergenceTypeSchema.safeParse(p);
|
|
7869
8649
|
if (!r.success) {
|
|
7870
8650
|
console.error(
|
|
7871
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
8651
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
|
|
7872
8652
|
);
|
|
7873
8653
|
return 2;
|
|
7874
8654
|
}
|