@neat.is/core 0.3.6 → 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-4ASCXBZF.js → chunk-7TYESDAI.js} +101 -5
- 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-YHQYHFI3.js → chunk-LQ3JFBTX.js} +99 -17
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1264 -396
- 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 +935 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +228 -19
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +143 -10
- 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-4ASCXBZF.js.map +0 -1
- package/dist/chunk-G3PDTGOW.js.map +0 -1
- package/dist/chunk-YHQYHFI3.js.map +0 -1
- /package/dist/{otel-grpc-J4O2SIBZ.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();
|
|
@@ -258,21 +309,41 @@ function parseOtlpRequest(body) {
|
|
|
258
309
|
}
|
|
259
310
|
return out;
|
|
260
311
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
312
|
+
function loadProtoRoot() {
|
|
313
|
+
const here = import_node_path36.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
314
|
+
const protoRoot = import_node_path36.default.resolve(here, "..", "proto");
|
|
265
315
|
const root = new import_protobufjs.default.Root();
|
|
266
|
-
root.resolvePath = (_origin, target) =>
|
|
316
|
+
root.resolvePath = (_origin, target) => import_node_path36.default.resolve(protoRoot, target);
|
|
267
317
|
root.loadSync(
|
|
268
318
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
269
319
|
{ keepCase: true }
|
|
270
320
|
);
|
|
321
|
+
return root;
|
|
322
|
+
}
|
|
323
|
+
function loadProtobufDecoder() {
|
|
324
|
+
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
325
|
+
const root = loadProtoRoot();
|
|
271
326
|
exportTraceServiceRequestType = root.lookupType(
|
|
272
327
|
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
273
328
|
);
|
|
274
329
|
return exportTraceServiceRequestType;
|
|
275
330
|
}
|
|
331
|
+
function loadProtobufResponseEncoder() {
|
|
332
|
+
if (exportTraceServiceResponseType) return exportTraceServiceResponseType;
|
|
333
|
+
const root = loadProtoRoot();
|
|
334
|
+
exportTraceServiceResponseType = root.lookupType(
|
|
335
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse"
|
|
336
|
+
);
|
|
337
|
+
return exportTraceServiceResponseType;
|
|
338
|
+
}
|
|
339
|
+
function encodeProtobufResponseBody() {
|
|
340
|
+
if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
|
|
341
|
+
const Type = loadProtobufResponseEncoder();
|
|
342
|
+
const msg = Type.create({});
|
|
343
|
+
const encoded = Type.encode(msg).finish();
|
|
344
|
+
cachedProtobufResponseBody = Buffer.from(encoded);
|
|
345
|
+
return cachedProtobufResponseBody;
|
|
346
|
+
}
|
|
276
347
|
async function decodeProtobufBody(buf) {
|
|
277
348
|
const Type = loadProtobufDecoder();
|
|
278
349
|
const decoded = Type.decode(buf).toJSON();
|
|
@@ -284,6 +355,7 @@ async function buildOtelReceiver(opts) {
|
|
|
284
355
|
logger: false,
|
|
285
356
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
286
357
|
});
|
|
358
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
287
359
|
const queue = [];
|
|
288
360
|
let draining = false;
|
|
289
361
|
let drainPromise = Promise.resolve();
|
|
@@ -319,7 +391,9 @@ async function buildOtelReceiver(opts) {
|
|
|
319
391
|
app.post("/v1/traces", async (req, reply) => {
|
|
320
392
|
const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
321
393
|
let body;
|
|
394
|
+
let responseFlavor;
|
|
322
395
|
if (ct === "application/x-protobuf") {
|
|
396
|
+
responseFlavor = "protobuf";
|
|
323
397
|
try {
|
|
324
398
|
body = await decodeProtobufBody(req.body);
|
|
325
399
|
} catch (err) {
|
|
@@ -328,6 +402,7 @@ async function buildOtelReceiver(opts) {
|
|
|
328
402
|
});
|
|
329
403
|
}
|
|
330
404
|
} else if (!ct || ct === "application/json") {
|
|
405
|
+
responseFlavor = "json";
|
|
331
406
|
body = req.body ?? {};
|
|
332
407
|
} else {
|
|
333
408
|
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
@@ -345,7 +420,11 @@ async function buildOtelReceiver(opts) {
|
|
|
345
420
|
}
|
|
346
421
|
}
|
|
347
422
|
enqueue(spans);
|
|
348
|
-
|
|
423
|
+
if (responseFlavor === "protobuf") {
|
|
424
|
+
const buf = encodeProtobufResponseBody();
|
|
425
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
426
|
+
}
|
|
427
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
349
428
|
});
|
|
350
429
|
const decorated = app;
|
|
351
430
|
decorated.flushPending = async () => {
|
|
@@ -355,16 +434,19 @@ async function buildOtelReceiver(opts) {
|
|
|
355
434
|
};
|
|
356
435
|
return decorated;
|
|
357
436
|
}
|
|
358
|
-
var
|
|
437
|
+
var import_node_path36, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
359
438
|
var init_otel = __esm({
|
|
360
439
|
"src/otel.ts"() {
|
|
361
440
|
"use strict";
|
|
362
441
|
init_cjs_shims();
|
|
363
|
-
|
|
442
|
+
import_node_path36 = __toESM(require("path"), 1);
|
|
364
443
|
import_node_url2 = require("url");
|
|
365
444
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
366
445
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
446
|
+
init_auth();
|
|
367
447
|
exportTraceServiceRequestType = null;
|
|
448
|
+
exportTraceServiceResponseType = null;
|
|
449
|
+
cachedProtobufResponseBody = null;
|
|
368
450
|
}
|
|
369
451
|
});
|
|
370
452
|
|
|
@@ -380,8 +462,8 @@ __export(cli_exports, {
|
|
|
380
462
|
});
|
|
381
463
|
module.exports = __toCommonJS(cli_exports);
|
|
382
464
|
init_cjs_shims();
|
|
383
|
-
var
|
|
384
|
-
var
|
|
465
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
466
|
+
var import_node_fs28 = require("fs");
|
|
385
467
|
|
|
386
468
|
// src/graph.ts
|
|
387
469
|
init_cjs_shims();
|
|
@@ -891,19 +973,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
891
973
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
892
974
|
let best = { path: [start], edges: [] };
|
|
893
975
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
894
|
-
function step(node,
|
|
895
|
-
if (
|
|
896
|
-
best = { path: [...
|
|
976
|
+
function step(node, path44, edges) {
|
|
977
|
+
if (path44.length > best.path.length) {
|
|
978
|
+
best = { path: [...path44], edges: [...edges] };
|
|
897
979
|
}
|
|
898
|
-
if (
|
|
980
|
+
if (path44.length - 1 >= maxDepth) return;
|
|
899
981
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
900
982
|
for (const [srcId, edge] of incoming) {
|
|
901
983
|
if (visited.has(srcId)) continue;
|
|
902
984
|
visited.add(srcId);
|
|
903
|
-
|
|
985
|
+
path44.push(srcId);
|
|
904
986
|
edges.push(edge);
|
|
905
|
-
step(srcId,
|
|
906
|
-
|
|
987
|
+
step(srcId, path44, edges);
|
|
988
|
+
path44.pop();
|
|
907
989
|
edges.pop();
|
|
908
990
|
visited.delete(srcId);
|
|
909
991
|
}
|
|
@@ -4146,128 +4228,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4146
4228
|
return result;
|
|
4147
4229
|
}
|
|
4148
4230
|
|
|
4149
|
-
// src/persist.ts
|
|
4150
|
-
init_cjs_shims();
|
|
4151
|
-
var import_node_fs18 = require("fs");
|
|
4152
|
-
var import_node_path31 = __toESM(require("path"), 1);
|
|
4153
|
-
var import_types19 = require("@neat.is/types");
|
|
4154
|
-
var SCHEMA_VERSION = 3;
|
|
4155
|
-
function migrateV1ToV2(payload) {
|
|
4156
|
-
const nodes = payload.graph.nodes;
|
|
4157
|
-
if (Array.isArray(nodes)) {
|
|
4158
|
-
for (const node of nodes) {
|
|
4159
|
-
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
4160
|
-
delete node.attributes.pgDriverVersion;
|
|
4161
|
-
}
|
|
4162
|
-
}
|
|
4163
|
-
}
|
|
4164
|
-
return { ...payload, schemaVersion: 2 };
|
|
4165
|
-
}
|
|
4166
|
-
function migrateV2ToV3(payload) {
|
|
4167
|
-
const edges = payload.graph.edges;
|
|
4168
|
-
if (Array.isArray(edges)) {
|
|
4169
|
-
for (const edge of edges) {
|
|
4170
|
-
const attrs = edge.attributes;
|
|
4171
|
-
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4172
|
-
attrs.provenance = import_types19.Provenance.OBSERVED;
|
|
4173
|
-
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4174
|
-
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4175
|
-
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4176
|
-
if (type && source && target) {
|
|
4177
|
-
const newId = (0, import_types19.observedEdgeId)(source, target, type);
|
|
4178
|
-
attrs.id = newId;
|
|
4179
|
-
if (edge.key) edge.key = newId;
|
|
4180
|
-
}
|
|
4181
|
-
}
|
|
4182
|
-
}
|
|
4183
|
-
return { ...payload, schemaVersion: 3 };
|
|
4184
|
-
}
|
|
4185
|
-
async function ensureDir(filePath) {
|
|
4186
|
-
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4187
|
-
}
|
|
4188
|
-
async function saveGraphToDisk(graph, outPath) {
|
|
4189
|
-
await ensureDir(outPath);
|
|
4190
|
-
const payload = {
|
|
4191
|
-
schemaVersion: SCHEMA_VERSION,
|
|
4192
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4193
|
-
graph: graph.export()
|
|
4194
|
-
};
|
|
4195
|
-
const tmp = `${outPath}.tmp`;
|
|
4196
|
-
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4197
|
-
await import_node_fs18.promises.rename(tmp, outPath);
|
|
4198
|
-
}
|
|
4199
|
-
async function loadGraphFromDisk(graph, outPath) {
|
|
4200
|
-
let raw;
|
|
4201
|
-
try {
|
|
4202
|
-
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
4203
|
-
} catch (err) {
|
|
4204
|
-
if (err.code === "ENOENT") return;
|
|
4205
|
-
throw err;
|
|
4206
|
-
}
|
|
4207
|
-
let payload = JSON.parse(raw);
|
|
4208
|
-
if (payload.schemaVersion === 1) {
|
|
4209
|
-
payload = migrateV1ToV2(payload);
|
|
4210
|
-
}
|
|
4211
|
-
if (payload.schemaVersion === 2) {
|
|
4212
|
-
payload = migrateV2ToV3(payload);
|
|
4213
|
-
}
|
|
4214
|
-
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4215
|
-
throw new Error(
|
|
4216
|
-
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4217
|
-
);
|
|
4218
|
-
}
|
|
4219
|
-
graph.clear();
|
|
4220
|
-
graph.import(payload.graph);
|
|
4221
|
-
}
|
|
4222
|
-
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
4223
|
-
let stopped = false;
|
|
4224
|
-
const tick = async () => {
|
|
4225
|
-
if (stopped) return;
|
|
4226
|
-
try {
|
|
4227
|
-
await saveGraphToDisk(graph, outPath);
|
|
4228
|
-
} catch (err) {
|
|
4229
|
-
console.error("persist: periodic save failed", err);
|
|
4230
|
-
}
|
|
4231
|
-
};
|
|
4232
|
-
const interval = setInterval(() => {
|
|
4233
|
-
void tick();
|
|
4234
|
-
}, intervalMs);
|
|
4235
|
-
const onSignal = (signal) => {
|
|
4236
|
-
void (async () => {
|
|
4237
|
-
try {
|
|
4238
|
-
await saveGraphToDisk(graph, outPath);
|
|
4239
|
-
} catch (err) {
|
|
4240
|
-
console.error(`persist: ${signal} save failed`, err);
|
|
4241
|
-
} finally {
|
|
4242
|
-
process.exit(0);
|
|
4243
|
-
}
|
|
4244
|
-
})();
|
|
4245
|
-
};
|
|
4246
|
-
process.on("SIGTERM", onSignal);
|
|
4247
|
-
process.on("SIGINT", onSignal);
|
|
4248
|
-
return () => {
|
|
4249
|
-
stopped = true;
|
|
4250
|
-
clearInterval(interval);
|
|
4251
|
-
process.off("SIGTERM", onSignal);
|
|
4252
|
-
process.off("SIGINT", onSignal);
|
|
4253
|
-
};
|
|
4254
|
-
}
|
|
4255
|
-
|
|
4256
|
-
// src/watch.ts
|
|
4257
|
-
init_cjs_shims();
|
|
4258
|
-
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
4259
|
-
var import_node_path37 = __toESM(require("path"), 1);
|
|
4260
|
-
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4261
|
-
|
|
4262
|
-
// src/api.ts
|
|
4263
|
-
init_cjs_shims();
|
|
4264
|
-
var import_fastify = __toESM(require("fastify"), 1);
|
|
4265
|
-
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4266
|
-
var import_types22 = require("@neat.is/types");
|
|
4267
|
-
|
|
4268
4231
|
// src/divergences.ts
|
|
4269
4232
|
init_cjs_shims();
|
|
4270
|
-
var
|
|
4233
|
+
var import_types19 = require("@neat.is/types");
|
|
4271
4234
|
function bucketKey(source, target, type) {
|
|
4272
4235
|
return `${type}|${source}|${target}`;
|
|
4273
4236
|
}
|
|
@@ -4275,22 +4238,22 @@ function bucketEdges(graph) {
|
|
|
4275
4238
|
const buckets = /* @__PURE__ */ new Map();
|
|
4276
4239
|
graph.forEachEdge((id, attrs) => {
|
|
4277
4240
|
const e = attrs;
|
|
4278
|
-
const parsed = (0,
|
|
4241
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
4279
4242
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4280
4243
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4281
4244
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4282
4245
|
switch (provenance) {
|
|
4283
|
-
case
|
|
4246
|
+
case import_types19.Provenance.EXTRACTED:
|
|
4284
4247
|
cur.extracted = e;
|
|
4285
4248
|
break;
|
|
4286
|
-
case
|
|
4249
|
+
case import_types19.Provenance.OBSERVED:
|
|
4287
4250
|
cur.observed = e;
|
|
4288
4251
|
break;
|
|
4289
|
-
case
|
|
4252
|
+
case import_types19.Provenance.INFERRED:
|
|
4290
4253
|
cur.inferred = e;
|
|
4291
4254
|
break;
|
|
4292
4255
|
default:
|
|
4293
|
-
if (e.provenance ===
|
|
4256
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
4294
4257
|
}
|
|
4295
4258
|
buckets.set(key, cur);
|
|
4296
4259
|
});
|
|
@@ -4299,7 +4262,7 @@ function bucketEdges(graph) {
|
|
|
4299
4262
|
function nodeIsFrontier(graph, nodeId) {
|
|
4300
4263
|
if (!graph.hasNode(nodeId)) return false;
|
|
4301
4264
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4302
|
-
return attrs.type ===
|
|
4265
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
4303
4266
|
}
|
|
4304
4267
|
function clampConfidence(n) {
|
|
4305
4268
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4360,7 +4323,7 @@ function declaredHostFor(svc) {
|
|
|
4360
4323
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4361
4324
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4362
4325
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4363
|
-
if (e.type ===
|
|
4326
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4364
4327
|
return true;
|
|
4365
4328
|
}
|
|
4366
4329
|
}
|
|
@@ -4373,10 +4336,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4373
4336
|
const out = [];
|
|
4374
4337
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4375
4338
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4376
|
-
if (edge.type !==
|
|
4377
|
-
if (edge.provenance !==
|
|
4339
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4340
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4378
4341
|
const target = graph.getNodeAttributes(edge.target);
|
|
4379
|
-
if (target.type !==
|
|
4342
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4380
4343
|
const observedHost = target.host?.trim();
|
|
4381
4344
|
if (!observedHost) continue;
|
|
4382
4345
|
if (observedHost === declaredHost) continue;
|
|
@@ -4398,10 +4361,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4398
4361
|
const deps = svc.dependencies ?? {};
|
|
4399
4362
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4400
4363
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4401
|
-
if (edge.type !==
|
|
4402
|
-
if (edge.provenance !==
|
|
4364
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4365
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4403
4366
|
const target = graph.getNodeAttributes(edge.target);
|
|
4404
|
-
if (target.type !==
|
|
4367
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4405
4368
|
for (const pair of compatPairs()) {
|
|
4406
4369
|
if (pair.engine !== target.engine) continue;
|
|
4407
4370
|
const declared = deps[pair.driver];
|
|
@@ -4462,7 +4425,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4462
4425
|
}
|
|
4463
4426
|
graph.forEachNode((nodeId, attrs) => {
|
|
4464
4427
|
const n = attrs;
|
|
4465
|
-
if (n.type !==
|
|
4428
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4466
4429
|
const svc = n;
|
|
4467
4430
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4468
4431
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4495,76 +4458,327 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4495
4458
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4496
4459
|
return a.target.localeCompare(b.target);
|
|
4497
4460
|
});
|
|
4498
|
-
return
|
|
4461
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4499
4462
|
divergences: filtered,
|
|
4500
4463
|
totalAffected: filtered.length,
|
|
4501
4464
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4502
4465
|
});
|
|
4503
4466
|
}
|
|
4504
4467
|
|
|
4505
|
-
// src/
|
|
4468
|
+
// src/persist.ts
|
|
4506
4469
|
init_cjs_shims();
|
|
4507
|
-
var
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
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
|
+
}
|
|
4513
4481
|
}
|
|
4514
|
-
return await res.json();
|
|
4515
4482
|
}
|
|
4516
|
-
|
|
4517
|
-
return JSON.parse(raw);
|
|
4483
|
+
return { ...payload, schemaVersion: 2 };
|
|
4518
4484
|
}
|
|
4519
|
-
function
|
|
4520
|
-
const
|
|
4521
|
-
if (
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
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
|
+
}
|
|
4526
4501
|
}
|
|
4527
|
-
return
|
|
4502
|
+
return { ...payload, schemaVersion: 3 };
|
|
4528
4503
|
}
|
|
4529
|
-
function
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
const
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
current: { exportedAt: currentExportedAt },
|
|
4539
|
-
added: { nodes: [], edges: [] },
|
|
4540
|
-
removed: { nodes: [], edges: [] },
|
|
4541
|
-
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()
|
|
4542
4513
|
};
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
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;
|
|
4550
4525
|
}
|
|
4551
|
-
|
|
4552
|
-
|
|
4526
|
+
let payload = JSON.parse(raw);
|
|
4527
|
+
if (payload.schemaVersion === 1) {
|
|
4528
|
+
payload = migrateV1ToV2(payload);
|
|
4553
4529
|
}
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
if (!before) {
|
|
4557
|
-
result.added.edges.push(after);
|
|
4558
|
-
} else if (!shallowEqual(before, after)) {
|
|
4559
|
-
result.changed.edges.push({ id, before, after });
|
|
4560
|
-
}
|
|
4530
|
+
if (payload.schemaVersion === 2) {
|
|
4531
|
+
payload = migrateV2ToV3(payload);
|
|
4561
4532
|
}
|
|
4562
|
-
|
|
4563
|
-
|
|
4533
|
+
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4534
|
+
throw new Error(
|
|
4535
|
+
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4536
|
+
);
|
|
4564
4537
|
}
|
|
4565
|
-
|
|
4538
|
+
graph.clear();
|
|
4539
|
+
graph.import(payload.graph);
|
|
4566
4540
|
}
|
|
4567
|
-
function
|
|
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) {
|
|
4568
4782
|
return canonicalJson(a) === canonicalJson(b);
|
|
4569
4783
|
}
|
|
4570
4784
|
function canonicalJson(value) {
|
|
@@ -4581,23 +4795,23 @@ function canonicalJson(value) {
|
|
|
4581
4795
|
|
|
4582
4796
|
// src/projects.ts
|
|
4583
4797
|
init_cjs_shims();
|
|
4584
|
-
var
|
|
4798
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4585
4799
|
function pathsForProject(project, baseDir) {
|
|
4586
4800
|
if (project === DEFAULT_PROJECT) {
|
|
4587
4801
|
return {
|
|
4588
|
-
snapshotPath:
|
|
4589
|
-
errorsPath:
|
|
4590
|
-
staleEventsPath:
|
|
4591
|
-
embeddingsCachePath:
|
|
4592
|
-
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")
|
|
4593
4807
|
};
|
|
4594
4808
|
}
|
|
4595
4809
|
return {
|
|
4596
|
-
snapshotPath:
|
|
4597
|
-
errorsPath:
|
|
4598
|
-
staleEventsPath:
|
|
4599
|
-
embeddingsCachePath:
|
|
4600
|
-
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`)
|
|
4601
4815
|
};
|
|
4602
4816
|
}
|
|
4603
4817
|
var Projects = class {
|
|
@@ -4633,49 +4847,49 @@ var Projects = class {
|
|
|
4633
4847
|
|
|
4634
4848
|
// src/registry.ts
|
|
4635
4849
|
init_cjs_shims();
|
|
4636
|
-
var
|
|
4850
|
+
var import_node_fs21 = require("fs");
|
|
4637
4851
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4638
|
-
var
|
|
4639
|
-
var
|
|
4852
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
4853
|
+
var import_types22 = require("@neat.is/types");
|
|
4640
4854
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4641
4855
|
var LOCK_RETRY_MS = 50;
|
|
4642
4856
|
function neatHome() {
|
|
4643
4857
|
const override = process.env.NEAT_HOME;
|
|
4644
|
-
if (override && override.length > 0) return
|
|
4645
|
-
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");
|
|
4646
4860
|
}
|
|
4647
4861
|
function registryPath() {
|
|
4648
|
-
return
|
|
4862
|
+
return import_node_path34.default.join(neatHome(), "projects.json");
|
|
4649
4863
|
}
|
|
4650
4864
|
function registryLockPath() {
|
|
4651
|
-
return
|
|
4865
|
+
return import_node_path34.default.join(neatHome(), "projects.json.lock");
|
|
4652
4866
|
}
|
|
4653
4867
|
async function normalizeProjectPath(input) {
|
|
4654
|
-
const resolved =
|
|
4868
|
+
const resolved = import_node_path34.default.resolve(input);
|
|
4655
4869
|
try {
|
|
4656
|
-
return await
|
|
4870
|
+
return await import_node_fs21.promises.realpath(resolved);
|
|
4657
4871
|
} catch {
|
|
4658
4872
|
return resolved;
|
|
4659
4873
|
}
|
|
4660
4874
|
}
|
|
4661
4875
|
async function writeAtomically(target, contents) {
|
|
4662
|
-
await
|
|
4876
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(target), { recursive: true });
|
|
4663
4877
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4664
|
-
const fd = await
|
|
4878
|
+
const fd = await import_node_fs21.promises.open(tmp, "w");
|
|
4665
4879
|
try {
|
|
4666
4880
|
await fd.writeFile(contents, "utf8");
|
|
4667
4881
|
await fd.sync();
|
|
4668
4882
|
} finally {
|
|
4669
4883
|
await fd.close();
|
|
4670
4884
|
}
|
|
4671
|
-
await
|
|
4885
|
+
await import_node_fs21.promises.rename(tmp, target);
|
|
4672
4886
|
}
|
|
4673
4887
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4674
4888
|
const deadline = Date.now() + timeoutMs;
|
|
4675
|
-
await
|
|
4889
|
+
await import_node_fs21.promises.mkdir(import_node_path34.default.dirname(lockPath), { recursive: true });
|
|
4676
4890
|
while (true) {
|
|
4677
4891
|
try {
|
|
4678
|
-
const fd = await
|
|
4892
|
+
const fd = await import_node_fs21.promises.open(lockPath, "wx");
|
|
4679
4893
|
await fd.close();
|
|
4680
4894
|
return;
|
|
4681
4895
|
} catch (err) {
|
|
@@ -4691,7 +4905,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4691
4905
|
}
|
|
4692
4906
|
}
|
|
4693
4907
|
async function releaseLock(lockPath) {
|
|
4694
|
-
await
|
|
4908
|
+
await import_node_fs21.promises.unlink(lockPath).catch(() => {
|
|
4695
4909
|
});
|
|
4696
4910
|
}
|
|
4697
4911
|
async function withLock(fn) {
|
|
@@ -4707,7 +4921,7 @@ async function readRegistry() {
|
|
|
4707
4921
|
const file = registryPath();
|
|
4708
4922
|
let raw;
|
|
4709
4923
|
try {
|
|
4710
|
-
raw = await
|
|
4924
|
+
raw = await import_node_fs21.promises.readFile(file, "utf8");
|
|
4711
4925
|
} catch (err) {
|
|
4712
4926
|
if (err.code === "ENOENT") {
|
|
4713
4927
|
return { version: 1, projects: [] };
|
|
@@ -4715,10 +4929,10 @@ async function readRegistry() {
|
|
|
4715
4929
|
throw err;
|
|
4716
4930
|
}
|
|
4717
4931
|
const parsed = JSON.parse(raw);
|
|
4718
|
-
return
|
|
4932
|
+
return import_types22.RegistryFileSchema.parse(parsed);
|
|
4719
4933
|
}
|
|
4720
4934
|
async function writeRegistry(reg) {
|
|
4721
|
-
const validated =
|
|
4935
|
+
const validated = import_types22.RegistryFileSchema.parse(reg);
|
|
4722
4936
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4723
4937
|
}
|
|
4724
4938
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4846,6 +5060,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4846
5060
|
}
|
|
4847
5061
|
|
|
4848
5062
|
// src/api.ts
|
|
5063
|
+
init_auth();
|
|
4849
5064
|
function serializeGraph(graph) {
|
|
4850
5065
|
const nodes = [];
|
|
4851
5066
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4969,11 +5184,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4969
5184
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4970
5185
|
const parsed = [];
|
|
4971
5186
|
for (const c of candidates) {
|
|
4972
|
-
const r =
|
|
5187
|
+
const r = import_types23.DivergenceTypeSchema.safeParse(c);
|
|
4973
5188
|
if (!r.success) {
|
|
4974
5189
|
return reply.code(400).send({
|
|
4975
5190
|
error: `unknown divergence type "${c}"`,
|
|
4976
|
-
allowed:
|
|
5191
|
+
allowed: import_types23.DivergenceTypeSchema.options
|
|
4977
5192
|
});
|
|
4978
5193
|
}
|
|
4979
5194
|
parsed.push(r.data);
|
|
@@ -5157,7 +5372,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5157
5372
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5158
5373
|
let violations = await log.readAll();
|
|
5159
5374
|
if (req.query.severity) {
|
|
5160
|
-
const sev =
|
|
5375
|
+
const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5161
5376
|
if (!sev.success) {
|
|
5162
5377
|
return reply.code(400).send({
|
|
5163
5378
|
error: "invalid severity",
|
|
@@ -5174,7 +5389,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5174
5389
|
scope.post("/policies/check", async (req, reply) => {
|
|
5175
5390
|
const proj = resolveProject(registry, req, reply);
|
|
5176
5391
|
if (!proj) return;
|
|
5177
|
-
const parsed =
|
|
5392
|
+
const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5178
5393
|
if (!parsed.success) {
|
|
5179
5394
|
return reply.code(400).send({
|
|
5180
5395
|
error: "invalid /policies/check body",
|
|
@@ -5211,6 +5426,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5211
5426
|
async function buildApi(opts) {
|
|
5212
5427
|
const app = (0, import_fastify.default)({ logger: false });
|
|
5213
5428
|
await app.register(import_cors.default, { origin: true });
|
|
5429
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
5214
5430
|
const startedAt = opts.startedAt ?? Date.now();
|
|
5215
5431
|
const registry = buildLegacyRegistry(opts);
|
|
5216
5432
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5278,9 +5494,9 @@ init_otel_grpc();
|
|
|
5278
5494
|
|
|
5279
5495
|
// src/search.ts
|
|
5280
5496
|
init_cjs_shims();
|
|
5281
|
-
var
|
|
5282
|
-
var
|
|
5283
|
-
var
|
|
5497
|
+
var import_node_fs22 = require("fs");
|
|
5498
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5499
|
+
var import_node_crypto3 = require("crypto");
|
|
5284
5500
|
var DEFAULT_LIMIT = 10;
|
|
5285
5501
|
var NOMIC_DIM = 768;
|
|
5286
5502
|
var MINI_LM_DIM = 384;
|
|
@@ -5320,7 +5536,7 @@ function embedText(node) {
|
|
|
5320
5536
|
return parts.join(" ");
|
|
5321
5537
|
}
|
|
5322
5538
|
function attrsHash(node) {
|
|
5323
|
-
return (0,
|
|
5539
|
+
return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
5324
5540
|
}
|
|
5325
5541
|
function cosine(a, b) {
|
|
5326
5542
|
if (a.length !== b.length) return 0;
|
|
@@ -5409,7 +5625,7 @@ async function pickEmbedder() {
|
|
|
5409
5625
|
}
|
|
5410
5626
|
async function readCache(cachePath) {
|
|
5411
5627
|
try {
|
|
5412
|
-
const raw = await
|
|
5628
|
+
const raw = await import_node_fs22.promises.readFile(cachePath, "utf8");
|
|
5413
5629
|
const parsed = JSON.parse(raw);
|
|
5414
5630
|
if (parsed.version !== 1) return null;
|
|
5415
5631
|
return parsed;
|
|
@@ -5418,8 +5634,8 @@ async function readCache(cachePath) {
|
|
|
5418
5634
|
}
|
|
5419
5635
|
}
|
|
5420
5636
|
async function writeCache(cachePath, cache) {
|
|
5421
|
-
await
|
|
5422
|
-
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));
|
|
5423
5639
|
}
|
|
5424
5640
|
var VectorIndex = class {
|
|
5425
5641
|
constructor(embedder, cachePath) {
|
|
@@ -5574,8 +5790,8 @@ var ALL_PHASES = [
|
|
|
5574
5790
|
];
|
|
5575
5791
|
function classifyChange(relPath) {
|
|
5576
5792
|
const phases = /* @__PURE__ */ new Set();
|
|
5577
|
-
const base =
|
|
5578
|
-
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());
|
|
5579
5795
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
5580
5796
|
phases.add("services");
|
|
5581
5797
|
phases.add("aliases");
|
|
@@ -5670,16 +5886,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
5670
5886
|
if (count >= limit) return;
|
|
5671
5887
|
let entries;
|
|
5672
5888
|
try {
|
|
5673
|
-
entries =
|
|
5889
|
+
entries = import_node_fs23.default.readdirSync(dir, { withFileTypes: true });
|
|
5674
5890
|
} catch {
|
|
5675
5891
|
return;
|
|
5676
5892
|
}
|
|
5677
5893
|
for (const e of entries) {
|
|
5678
5894
|
if (count >= limit) return;
|
|
5679
5895
|
if (!e.isDirectory()) continue;
|
|
5680
|
-
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;
|
|
5681
5897
|
count++;
|
|
5682
|
-
if (depth < 2) visit(
|
|
5898
|
+
if (depth < 2) visit(import_node_path38.default.join(dir, e.name), depth + 1);
|
|
5683
5899
|
}
|
|
5684
5900
|
};
|
|
5685
5901
|
visit(scanPath, 0);
|
|
@@ -5697,8 +5913,8 @@ async function startWatch(graph, opts) {
|
|
|
5697
5913
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
5698
5914
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
5699
5915
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5700
|
-
const policyFilePath =
|
|
5701
|
-
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");
|
|
5702
5918
|
let policies = [];
|
|
5703
5919
|
try {
|
|
5704
5920
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -5747,7 +5963,7 @@ async function startWatch(graph, opts) {
|
|
|
5747
5963
|
const host = opts.host ?? "0.0.0.0";
|
|
5748
5964
|
const port = opts.port ?? 8080;
|
|
5749
5965
|
const otelPort = opts.otelPort ?? 4318;
|
|
5750
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5966
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
|
|
5751
5967
|
let searchIndex;
|
|
5752
5968
|
try {
|
|
5753
5969
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -5765,7 +5981,7 @@ async function startWatch(graph, opts) {
|
|
|
5765
5981
|
// Paths are derived from the explicit options the watch caller passes
|
|
5766
5982
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
5767
5983
|
// fields so the registry shape is complete.
|
|
5768
|
-
...pathsForProject(projectName,
|
|
5984
|
+
...pathsForProject(projectName, import_node_path38.default.dirname(opts.outPath)),
|
|
5769
5985
|
snapshotPath: opts.outPath,
|
|
5770
5986
|
errorsPath: opts.errorsPath,
|
|
5771
5987
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5850,9 +6066,9 @@ async function startWatch(graph, opts) {
|
|
|
5850
6066
|
};
|
|
5851
6067
|
const onPath = (absPath) => {
|
|
5852
6068
|
if (shouldIgnore(absPath)) return;
|
|
5853
|
-
const rel =
|
|
6069
|
+
const rel = import_node_path38.default.relative(opts.scanPath, absPath);
|
|
5854
6070
|
if (!rel || rel.startsWith("..")) return;
|
|
5855
|
-
pendingPaths.add(rel.split(
|
|
6071
|
+
pendingPaths.add(rel.split(import_node_path38.default.sep).join("/"));
|
|
5856
6072
|
const phases = classifyChange(rel);
|
|
5857
6073
|
if (phases.size === 0) {
|
|
5858
6074
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5904,13 +6120,158 @@ async function startWatch(graph, opts) {
|
|
|
5904
6120
|
return { api, stop };
|
|
5905
6121
|
}
|
|
5906
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
|
+
|
|
5907
6268
|
// src/installers/index.ts
|
|
5908
6269
|
init_cjs_shims();
|
|
5909
6270
|
|
|
5910
6271
|
// src/installers/javascript.ts
|
|
5911
6272
|
init_cjs_shims();
|
|
5912
|
-
var
|
|
5913
|
-
var
|
|
6273
|
+
var import_node_fs25 = require("fs");
|
|
6274
|
+
var import_node_path40 = __toESM(require("path"), 1);
|
|
5914
6275
|
|
|
5915
6276
|
// src/installers/templates.ts
|
|
5916
6277
|
init_cjs_shims();
|
|
@@ -5958,6 +6319,57 @@ function renderEnvNeat(serviceName) {
|
|
|
5958
6319
|
""
|
|
5959
6320
|
].join("\n");
|
|
5960
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
|
+
`;
|
|
5961
6373
|
|
|
5962
6374
|
// src/installers/javascript.ts
|
|
5963
6375
|
var SDK_PACKAGES = [
|
|
@@ -5980,7 +6392,7 @@ var OTEL_ENV = {
|
|
|
5980
6392
|
};
|
|
5981
6393
|
async function readPackageJson(serviceDir) {
|
|
5982
6394
|
try {
|
|
5983
|
-
const raw = await
|
|
6395
|
+
const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
|
|
5984
6396
|
return JSON.parse(raw);
|
|
5985
6397
|
} catch {
|
|
5986
6398
|
return null;
|
|
@@ -5988,7 +6400,7 @@ async function readPackageJson(serviceDir) {
|
|
|
5988
6400
|
}
|
|
5989
6401
|
async function exists2(p) {
|
|
5990
6402
|
try {
|
|
5991
|
-
await
|
|
6403
|
+
await import_node_fs25.promises.stat(p);
|
|
5992
6404
|
return true;
|
|
5993
6405
|
} catch {
|
|
5994
6406
|
return false;
|
|
@@ -5998,10 +6410,73 @@ async function detect(serviceDir) {
|
|
|
5998
6410
|
const pkg = await readPackageJson(serviceDir);
|
|
5999
6411
|
return pkg !== null && typeof pkg.name === "string";
|
|
6000
6412
|
}
|
|
6001
|
-
var
|
|
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
|
+
}
|
|
6435
|
+
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
6436
|
+
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
6437
|
+
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
6438
|
+
var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
|
|
6439
|
+
(name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
|
|
6440
|
+
);
|
|
6441
|
+
var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
|
|
6442
|
+
"node",
|
|
6443
|
+
"ts-node",
|
|
6444
|
+
"tsx",
|
|
6445
|
+
"ts-node-dev",
|
|
6446
|
+
"nodemon",
|
|
6447
|
+
"npx",
|
|
6448
|
+
"pnpm",
|
|
6449
|
+
"yarn",
|
|
6450
|
+
"npm",
|
|
6451
|
+
"cross-env",
|
|
6452
|
+
"dotenv",
|
|
6453
|
+
"--"
|
|
6454
|
+
]);
|
|
6455
|
+
function looksLikeEntryPath(token) {
|
|
6456
|
+
if (token.length === 0) return false;
|
|
6457
|
+
if (token.startsWith("-")) return false;
|
|
6458
|
+
if (token.includes("=")) return false;
|
|
6459
|
+
if (token.includes("/")) return true;
|
|
6460
|
+
return /\.(?:m?[jt]sx?|c[jt]s)$/.test(token);
|
|
6461
|
+
}
|
|
6462
|
+
function scriptHasShellChain(script) {
|
|
6463
|
+
return /(?:&&|\|\||;|\|(?!\|))/.test(script);
|
|
6464
|
+
}
|
|
6465
|
+
function entryFromScript(script) {
|
|
6466
|
+
if (!script) return void 0;
|
|
6467
|
+
if (scriptHasShellChain(script)) return void 0;
|
|
6468
|
+
const tokens = script.split(/\s+/).filter((t) => t.length > 0);
|
|
6469
|
+
for (const token of tokens) {
|
|
6470
|
+
const lower = token.toLowerCase();
|
|
6471
|
+
if (SCRIPT_LAUNCHERS.has(lower)) continue;
|
|
6472
|
+
const cleaned = token.startsWith("./") ? token.slice(2) : token;
|
|
6473
|
+
if (looksLikeEntryPath(cleaned)) return cleaned;
|
|
6474
|
+
}
|
|
6475
|
+
return void 0;
|
|
6476
|
+
}
|
|
6002
6477
|
async function resolveEntry(serviceDir, pkg) {
|
|
6003
6478
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
6004
|
-
const candidate =
|
|
6479
|
+
const candidate = import_node_path40.default.resolve(serviceDir, pkg.main);
|
|
6005
6480
|
if (await exists2(candidate)) return candidate;
|
|
6006
6481
|
}
|
|
6007
6482
|
if (pkg.bin) {
|
|
@@ -6015,18 +6490,36 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
6015
6490
|
if (typeof first === "string") binEntry = first;
|
|
6016
6491
|
}
|
|
6017
6492
|
if (binEntry) {
|
|
6018
|
-
const candidate =
|
|
6493
|
+
const candidate = import_node_path40.default.resolve(serviceDir, binEntry);
|
|
6019
6494
|
if (await exists2(candidate)) return candidate;
|
|
6020
6495
|
}
|
|
6021
6496
|
}
|
|
6497
|
+
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
6498
|
+
if (startEntry) {
|
|
6499
|
+
const candidate = import_node_path40.default.resolve(serviceDir, startEntry);
|
|
6500
|
+
if (await exists2(candidate)) return candidate;
|
|
6501
|
+
}
|
|
6502
|
+
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
6503
|
+
if (devEntry) {
|
|
6504
|
+
const candidate = import_node_path40.default.resolve(serviceDir, devEntry);
|
|
6505
|
+
if (await exists2(candidate)) return candidate;
|
|
6506
|
+
}
|
|
6507
|
+
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
6508
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6509
|
+
if (await exists2(candidate)) return candidate;
|
|
6510
|
+
}
|
|
6511
|
+
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
6512
|
+
const candidate = import_node_path40.default.join(serviceDir, rel);
|
|
6513
|
+
if (await exists2(candidate)) return candidate;
|
|
6514
|
+
}
|
|
6022
6515
|
for (const name of INDEX_CANDIDATES) {
|
|
6023
|
-
const candidate =
|
|
6516
|
+
const candidate = import_node_path40.default.join(serviceDir, name);
|
|
6024
6517
|
if (await exists2(candidate)) return candidate;
|
|
6025
6518
|
}
|
|
6026
6519
|
return null;
|
|
6027
6520
|
}
|
|
6028
6521
|
function dispatchEntry(entryFile, pkg) {
|
|
6029
|
-
const ext =
|
|
6522
|
+
const ext = import_node_path40.default.extname(entryFile).toLowerCase();
|
|
6030
6523
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
6031
6524
|
if (ext === ".mjs") return "esm";
|
|
6032
6525
|
if (ext === ".cjs") return "cjs";
|
|
@@ -6043,9 +6536,9 @@ function otelInitContents(flavor) {
|
|
|
6043
6536
|
return OTEL_INIT_CJS;
|
|
6044
6537
|
}
|
|
6045
6538
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
6046
|
-
let rel =
|
|
6539
|
+
let rel = import_node_path40.default.relative(import_node_path40.default.dirname(entryFile), otelInitFile);
|
|
6047
6540
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
6048
|
-
rel = rel.split(
|
|
6541
|
+
rel = rel.split(import_node_path40.default.sep).join("/");
|
|
6049
6542
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
6050
6543
|
if (flavor === "esm") return `import '${rel}'`;
|
|
6051
6544
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -6056,9 +6549,88 @@ function lineIsOtelInjection(line) {
|
|
|
6056
6549
|
if (trimmed.length === 0) return false;
|
|
6057
6550
|
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
6058
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
|
+
}
|
|
6059
6631
|
async function plan(serviceDir) {
|
|
6060
6632
|
const pkg = await readPackageJson(serviceDir);
|
|
6061
|
-
const manifestPath =
|
|
6633
|
+
const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
|
|
6062
6634
|
const empty = {
|
|
6063
6635
|
language: "javascript",
|
|
6064
6636
|
serviceDir,
|
|
@@ -6068,13 +6640,19 @@ async function plan(serviceDir) {
|
|
|
6068
6640
|
generatedFiles: []
|
|
6069
6641
|
};
|
|
6070
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
|
+
}
|
|
6071
6649
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
6072
6650
|
if (!entryFile) {
|
|
6073
6651
|
return { ...empty, libOnly: true };
|
|
6074
6652
|
}
|
|
6075
6653
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
6076
|
-
const otelInitFile =
|
|
6077
|
-
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");
|
|
6078
6656
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
6079
6657
|
const dependencyEdits = [];
|
|
6080
6658
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -6088,7 +6666,7 @@ async function plan(serviceDir) {
|
|
|
6088
6666
|
}
|
|
6089
6667
|
const entrypointEdits = [];
|
|
6090
6668
|
try {
|
|
6091
|
-
const raw = await
|
|
6669
|
+
const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
|
|
6092
6670
|
const lines = raw.split(/\r?\n/);
|
|
6093
6671
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
6094
6672
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -6113,7 +6691,7 @@ async function plan(serviceDir) {
|
|
|
6113
6691
|
if (!await exists2(envNeatFile)) {
|
|
6114
6692
|
generatedFiles.push({
|
|
6115
6693
|
file: envNeatFile,
|
|
6116
|
-
contents: renderEnvNeat(pkg.name ??
|
|
6694
|
+
contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
|
|
6117
6695
|
skipIfExists: true
|
|
6118
6696
|
});
|
|
6119
6697
|
}
|
|
@@ -6132,18 +6710,20 @@ async function plan(serviceDir) {
|
|
|
6132
6710
|
};
|
|
6133
6711
|
}
|
|
6134
6712
|
function isAllowedWritePath(serviceDir, target) {
|
|
6135
|
-
const rel =
|
|
6713
|
+
const rel = import_node_path40.default.relative(serviceDir, target);
|
|
6136
6714
|
if (rel.startsWith("..")) return false;
|
|
6137
|
-
const base =
|
|
6715
|
+
const base = import_node_path40.default.basename(target);
|
|
6138
6716
|
if (base === "package.json") return true;
|
|
6139
6717
|
if (base === ".env.neat") return true;
|
|
6140
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;
|
|
6141
6721
|
return false;
|
|
6142
6722
|
}
|
|
6143
6723
|
async function writeAtomic(file, contents) {
|
|
6144
6724
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
6145
|
-
await
|
|
6146
|
-
await
|
|
6725
|
+
await import_node_fs25.promises.writeFile(tmp, contents, "utf8");
|
|
6726
|
+
await import_node_fs25.promises.rename(tmp, file);
|
|
6147
6727
|
}
|
|
6148
6728
|
async function apply(installPlan) {
|
|
6149
6729
|
const { serviceDir } = installPlan;
|
|
@@ -6155,7 +6735,7 @@ async function apply(installPlan) {
|
|
|
6155
6735
|
writtenFiles: []
|
|
6156
6736
|
};
|
|
6157
6737
|
}
|
|
6158
|
-
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) {
|
|
6159
6739
|
return {
|
|
6160
6740
|
serviceDir,
|
|
6161
6741
|
outcome: "already-instrumented",
|
|
@@ -6166,6 +6746,7 @@ async function apply(installPlan) {
|
|
|
6166
6746
|
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
6167
6747
|
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
6168
6748
|
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
6749
|
+
if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
|
|
6169
6750
|
for (const target of allTargets) {
|
|
6170
6751
|
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
6171
6752
|
if (isEntryEdit) continue;
|
|
@@ -6180,7 +6761,7 @@ async function apply(installPlan) {
|
|
|
6180
6761
|
for (const target of allTargets) {
|
|
6181
6762
|
if (await exists2(target)) {
|
|
6182
6763
|
try {
|
|
6183
|
-
originals.set(target, await
|
|
6764
|
+
originals.set(target, await import_node_fs25.promises.readFile(target, "utf8"));
|
|
6184
6765
|
} catch {
|
|
6185
6766
|
}
|
|
6186
6767
|
}
|
|
@@ -6235,6 +6816,17 @@ async function apply(installPlan) {
|
|
|
6235
6816
|
await writeAtomic(ep.file, newRaw);
|
|
6236
6817
|
writtenFiles.push(ep.file);
|
|
6237
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
|
+
}
|
|
6238
6830
|
} catch (err) {
|
|
6239
6831
|
await rollback(installPlan, originals, createdFiles);
|
|
6240
6832
|
throw err;
|
|
@@ -6250,14 +6842,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6250
6842
|
const removed = [];
|
|
6251
6843
|
for (const [file, raw] of originals.entries()) {
|
|
6252
6844
|
try {
|
|
6253
|
-
await
|
|
6845
|
+
await import_node_fs25.promises.writeFile(file, raw, "utf8");
|
|
6254
6846
|
restored.push(file);
|
|
6255
6847
|
} catch {
|
|
6256
6848
|
}
|
|
6257
6849
|
}
|
|
6258
6850
|
for (const file of createdFiles) {
|
|
6259
6851
|
try {
|
|
6260
|
-
await
|
|
6852
|
+
await import_node_fs25.promises.unlink(file);
|
|
6261
6853
|
removed.push(file);
|
|
6262
6854
|
} catch {
|
|
6263
6855
|
}
|
|
@@ -6272,8 +6864,26 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
6272
6864
|
...removed.map((f) => `removed: ${f}`),
|
|
6273
6865
|
""
|
|
6274
6866
|
];
|
|
6275
|
-
const rollbackPath =
|
|
6276
|
-
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;
|
|
6277
6887
|
}
|
|
6278
6888
|
var javascriptInstaller = {
|
|
6279
6889
|
name: "javascript",
|
|
@@ -6284,8 +6894,8 @@ var javascriptInstaller = {
|
|
|
6284
6894
|
|
|
6285
6895
|
// src/installers/python.ts
|
|
6286
6896
|
init_cjs_shims();
|
|
6287
|
-
var
|
|
6288
|
-
var
|
|
6897
|
+
var import_node_fs26 = require("fs");
|
|
6898
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
6289
6899
|
var SDK_PACKAGES2 = [
|
|
6290
6900
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
6291
6901
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -6297,7 +6907,7 @@ var OTEL_ENV2 = {
|
|
|
6297
6907
|
};
|
|
6298
6908
|
async function exists3(p) {
|
|
6299
6909
|
try {
|
|
6300
|
-
await
|
|
6910
|
+
await import_node_fs26.promises.stat(p);
|
|
6301
6911
|
return true;
|
|
6302
6912
|
} catch {
|
|
6303
6913
|
return false;
|
|
@@ -6306,7 +6916,7 @@ async function exists3(p) {
|
|
|
6306
6916
|
async function detect2(serviceDir) {
|
|
6307
6917
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
6308
6918
|
for (const m of markers) {
|
|
6309
|
-
if (await exists3(
|
|
6919
|
+
if (await exists3(import_node_path41.default.join(serviceDir, m))) return true;
|
|
6310
6920
|
}
|
|
6311
6921
|
return false;
|
|
6312
6922
|
}
|
|
@@ -6316,9 +6926,9 @@ function reqPackageName(line) {
|
|
|
6316
6926
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
6317
6927
|
}
|
|
6318
6928
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
6319
|
-
const file =
|
|
6929
|
+
const file = import_node_path41.default.join(serviceDir, "requirements.txt");
|
|
6320
6930
|
if (!await exists3(file)) return null;
|
|
6321
|
-
const raw = await
|
|
6931
|
+
const raw = await import_node_fs26.promises.readFile(file, "utf8");
|
|
6322
6932
|
const presentNames = new Set(
|
|
6323
6933
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
6324
6934
|
);
|
|
@@ -6326,9 +6936,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
6326
6936
|
return { manifest: file, missing: [...missing] };
|
|
6327
6937
|
}
|
|
6328
6938
|
async function planProcfileEdits(serviceDir) {
|
|
6329
|
-
const procfile =
|
|
6939
|
+
const procfile = import_node_path41.default.join(serviceDir, "Procfile");
|
|
6330
6940
|
if (!await exists3(procfile)) return [];
|
|
6331
|
-
const raw = await
|
|
6941
|
+
const raw = await import_node_fs26.promises.readFile(procfile, "utf8");
|
|
6332
6942
|
const edits = [];
|
|
6333
6943
|
for (const line of raw.split(/\r?\n/)) {
|
|
6334
6944
|
if (line.length === 0) continue;
|
|
@@ -6380,8 +6990,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
6380
6990
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
6381
6991
|
`;
|
|
6382
6992
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
6383
|
-
await
|
|
6384
|
-
await
|
|
6993
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
6994
|
+
await import_node_fs26.promises.rename(tmp, manifest);
|
|
6385
6995
|
}
|
|
6386
6996
|
async function applyProcfile(procfile, edits, original) {
|
|
6387
6997
|
let next = original;
|
|
@@ -6390,8 +7000,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
6390
7000
|
next = next.replace(e.before, e.after);
|
|
6391
7001
|
}
|
|
6392
7002
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
6393
|
-
await
|
|
6394
|
-
await
|
|
7003
|
+
await import_node_fs26.promises.writeFile(tmp, next, "utf8");
|
|
7004
|
+
await import_node_fs26.promises.rename(tmp, procfile);
|
|
6395
7005
|
}
|
|
6396
7006
|
async function apply2(installPlan) {
|
|
6397
7007
|
const { serviceDir } = installPlan;
|
|
@@ -6404,7 +7014,7 @@ async function apply2(installPlan) {
|
|
|
6404
7014
|
const originals = /* @__PURE__ */ new Map();
|
|
6405
7015
|
for (const file of touched) {
|
|
6406
7016
|
try {
|
|
6407
|
-
originals.set(file, await
|
|
7017
|
+
originals.set(file, await import_node_fs26.promises.readFile(file, "utf8"));
|
|
6408
7018
|
} catch {
|
|
6409
7019
|
}
|
|
6410
7020
|
}
|
|
@@ -6415,7 +7025,7 @@ async function apply2(installPlan) {
|
|
|
6415
7025
|
if (raw === void 0) {
|
|
6416
7026
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
6417
7027
|
}
|
|
6418
|
-
const base =
|
|
7028
|
+
const base = import_node_path41.default.basename(file);
|
|
6419
7029
|
if (base === "requirements.txt") {
|
|
6420
7030
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
6421
7031
|
if (edits.length > 0) {
|
|
@@ -6440,7 +7050,7 @@ async function rollback2(installPlan, originals) {
|
|
|
6440
7050
|
const restored = [];
|
|
6441
7051
|
for (const [file, raw] of originals.entries()) {
|
|
6442
7052
|
try {
|
|
6443
|
-
await
|
|
7053
|
+
await import_node_fs26.promises.writeFile(file, raw, "utf8");
|
|
6444
7054
|
restored.push(file);
|
|
6445
7055
|
} catch {
|
|
6446
7056
|
}
|
|
@@ -6454,8 +7064,8 @@ async function rollback2(installPlan, originals) {
|
|
|
6454
7064
|
...restored.map((f) => `restored: ${f}`),
|
|
6455
7065
|
""
|
|
6456
7066
|
];
|
|
6457
|
-
const rollbackPath =
|
|
6458
|
-
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");
|
|
6459
7069
|
}
|
|
6460
7070
|
var pythonInstaller = {
|
|
6461
7071
|
name: "python",
|
|
@@ -6467,7 +7077,7 @@ var pythonInstaller = {
|
|
|
6467
7077
|
// src/installers/shared.ts
|
|
6468
7078
|
init_cjs_shims();
|
|
6469
7079
|
function isEmptyPlan(plan3) {
|
|
6470
|
-
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;
|
|
6471
7081
|
}
|
|
6472
7082
|
|
|
6473
7083
|
// src/installers/index.ts
|
|
@@ -6566,16 +7176,231 @@ function renderPatch(sections) {
|
|
|
6566
7176
|
}
|
|
6567
7177
|
lines.push("");
|
|
6568
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
|
+
}
|
|
6569
7185
|
}
|
|
6570
7186
|
return lines.join("\n");
|
|
6571
7187
|
}
|
|
6572
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
|
+
|
|
6573
7398
|
// src/cli.ts
|
|
6574
|
-
var
|
|
7399
|
+
var import_types25 = require("@neat.is/types");
|
|
6575
7400
|
|
|
6576
7401
|
// src/cli-client.ts
|
|
6577
7402
|
init_cjs_shims();
|
|
6578
|
-
var
|
|
7403
|
+
var import_types24 = require("@neat.is/types");
|
|
6579
7404
|
var HttpError = class extends Error {
|
|
6580
7405
|
constructor(status2, message, responseBody = "") {
|
|
6581
7406
|
super(message);
|
|
@@ -6595,10 +7420,10 @@ var TransportError = class extends Error {
|
|
|
6595
7420
|
function createHttpClient(baseUrl) {
|
|
6596
7421
|
const root = baseUrl.replace(/\/$/, "");
|
|
6597
7422
|
return {
|
|
6598
|
-
async get(
|
|
7423
|
+
async get(path44) {
|
|
6599
7424
|
let res;
|
|
6600
7425
|
try {
|
|
6601
|
-
res = await fetch(`${root}${
|
|
7426
|
+
res = await fetch(`${root}${path44}`);
|
|
6602
7427
|
} catch (err) {
|
|
6603
7428
|
throw new TransportError(
|
|
6604
7429
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -6608,16 +7433,16 @@ function createHttpClient(baseUrl) {
|
|
|
6608
7433
|
const body = await res.text().catch(() => "");
|
|
6609
7434
|
throw new HttpError(
|
|
6610
7435
|
res.status,
|
|
6611
|
-
`${res.status} ${res.statusText} on GET ${
|
|
7436
|
+
`${res.status} ${res.statusText} on GET ${path44}: ${body}`,
|
|
6612
7437
|
body
|
|
6613
7438
|
);
|
|
6614
7439
|
}
|
|
6615
7440
|
return await res.json();
|
|
6616
7441
|
},
|
|
6617
|
-
async post(
|
|
7442
|
+
async post(path44, body) {
|
|
6618
7443
|
let res;
|
|
6619
7444
|
try {
|
|
6620
|
-
res = await fetch(`${root}${
|
|
7445
|
+
res = await fetch(`${root}${path44}`, {
|
|
6621
7446
|
method: "POST",
|
|
6622
7447
|
headers: { "content-type": "application/json" },
|
|
6623
7448
|
body: JSON.stringify(body)
|
|
@@ -6631,7 +7456,7 @@ function createHttpClient(baseUrl) {
|
|
|
6631
7456
|
const text = await res.text().catch(() => "");
|
|
6632
7457
|
throw new HttpError(
|
|
6633
7458
|
res.status,
|
|
6634
|
-
`${res.status} ${res.statusText} on POST ${
|
|
7459
|
+
`${res.status} ${res.statusText} on POST ${path44}: ${text}`,
|
|
6635
7460
|
text
|
|
6636
7461
|
);
|
|
6637
7462
|
}
|
|
@@ -6645,12 +7470,12 @@ function projectPath(project, suffix) {
|
|
|
6645
7470
|
}
|
|
6646
7471
|
async function runRootCause(client, input) {
|
|
6647
7472
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
6648
|
-
const
|
|
7473
|
+
const path44 = projectPath(
|
|
6649
7474
|
input.project,
|
|
6650
7475
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
6651
7476
|
);
|
|
6652
7477
|
try {
|
|
6653
|
-
const result = await client.get(
|
|
7478
|
+
const result = await client.get(path44);
|
|
6654
7479
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
6655
7480
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
6656
7481
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -6676,12 +7501,12 @@ async function runRootCause(client, input) {
|
|
|
6676
7501
|
}
|
|
6677
7502
|
async function runBlastRadius(client, input) {
|
|
6678
7503
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
6679
|
-
const
|
|
7504
|
+
const path44 = projectPath(
|
|
6680
7505
|
input.project,
|
|
6681
7506
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
6682
7507
|
);
|
|
6683
7508
|
try {
|
|
6684
|
-
const result = await client.get(
|
|
7509
|
+
const result = await client.get(path44);
|
|
6685
7510
|
if (result.totalAffected === 0) {
|
|
6686
7511
|
return {
|
|
6687
7512
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -6710,17 +7535,17 @@ async function runBlastRadius(client, input) {
|
|
|
6710
7535
|
}
|
|
6711
7536
|
}
|
|
6712
7537
|
function formatBlastEntry(n) {
|
|
6713
|
-
const tag = n.edgeProvenance ===
|
|
7538
|
+
const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
6714
7539
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
6715
7540
|
}
|
|
6716
7541
|
async function runDependencies(client, input) {
|
|
6717
7542
|
const depth = input.depth ?? 3;
|
|
6718
|
-
const
|
|
7543
|
+
const path44 = projectPath(
|
|
6719
7544
|
input.project,
|
|
6720
7545
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6721
7546
|
);
|
|
6722
7547
|
try {
|
|
6723
|
-
const result = await client.get(
|
|
7548
|
+
const result = await client.get(path44);
|
|
6724
7549
|
if (result.total === 0) {
|
|
6725
7550
|
return {
|
|
6726
7551
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -6756,9 +7581,9 @@ async function runObservedDependencies(client, input) {
|
|
|
6756
7581
|
const edges = await client.get(
|
|
6757
7582
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
6758
7583
|
);
|
|
6759
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
7584
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
|
|
6760
7585
|
if (observed.length === 0) {
|
|
6761
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
7586
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
|
|
6762
7587
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
6763
7588
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
6764
7589
|
}
|
|
@@ -6766,7 +7591,7 @@ async function runObservedDependencies(client, input) {
|
|
|
6766
7591
|
return {
|
|
6767
7592
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
6768
7593
|
block: blockLines.join("\n"),
|
|
6769
|
-
provenance:
|
|
7594
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6770
7595
|
};
|
|
6771
7596
|
} catch (err) {
|
|
6772
7597
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -6801,9 +7626,9 @@ function formatDuration(ms) {
|
|
|
6801
7626
|
return `${Math.round(h / 24)}d`;
|
|
6802
7627
|
}
|
|
6803
7628
|
async function runIncidents(client, input) {
|
|
6804
|
-
const
|
|
7629
|
+
const path44 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6805
7630
|
try {
|
|
6806
|
-
const body = await client.get(
|
|
7631
|
+
const body = await client.get(path44);
|
|
6807
7632
|
const events = body.events;
|
|
6808
7633
|
if (events.length === 0) {
|
|
6809
7634
|
return {
|
|
@@ -6820,7 +7645,7 @@ async function runIncidents(client, input) {
|
|
|
6820
7645
|
return {
|
|
6821
7646
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
6822
7647
|
block: blockLines.join("\n"),
|
|
6823
|
-
provenance:
|
|
7648
|
+
provenance: import_types24.Provenance.OBSERVED
|
|
6824
7649
|
};
|
|
6825
7650
|
} catch (err) {
|
|
6826
7651
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -6929,7 +7754,7 @@ async function runStaleEdges(client, input) {
|
|
|
6929
7754
|
return {
|
|
6930
7755
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
6931
7756
|
block: blockLines.join("\n"),
|
|
6932
|
-
provenance:
|
|
7757
|
+
provenance: import_types24.Provenance.STALE
|
|
6933
7758
|
};
|
|
6934
7759
|
}
|
|
6935
7760
|
async function runPolicies(client, input) {
|
|
@@ -7095,6 +7920,9 @@ function usage() {
|
|
|
7095
7920
|
console.log(" Flags:");
|
|
7096
7921
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
7097
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.");
|
|
7098
7926
|
console.log("");
|
|
7099
7927
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7100
7928
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -7157,6 +7985,10 @@ function parseArgs(rest) {
|
|
|
7157
7985
|
apply: false,
|
|
7158
7986
|
dryRun: false,
|
|
7159
7987
|
noInstall: false,
|
|
7988
|
+
noInstrument: false,
|
|
7989
|
+
noOpen: false,
|
|
7990
|
+
yes: false,
|
|
7991
|
+
verbose: false,
|
|
7160
7992
|
printConfig: false,
|
|
7161
7993
|
json: false,
|
|
7162
7994
|
depth: null,
|
|
@@ -7185,6 +8017,22 @@ function parseArgs(rest) {
|
|
|
7185
8017
|
out.noInstall = true;
|
|
7186
8018
|
continue;
|
|
7187
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
|
+
}
|
|
7188
8036
|
if (arg === "--print-config") {
|
|
7189
8037
|
out.printConfig = true;
|
|
7190
8038
|
continue;
|
|
@@ -7240,34 +8088,6 @@ function assignFlag(out, field, value) {
|
|
|
7240
8088
|
;
|
|
7241
8089
|
out[field] = value;
|
|
7242
8090
|
}
|
|
7243
|
-
function summarise(nodes, edges) {
|
|
7244
|
-
const byNode = /* @__PURE__ */ new Map();
|
|
7245
|
-
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
7246
|
-
const byEdge = /* @__PURE__ */ new Map();
|
|
7247
|
-
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
7248
|
-
const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7249
|
-
const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
|
|
7250
|
-
return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
|
|
7251
|
-
}
|
|
7252
|
-
function formatIncompat(inc) {
|
|
7253
|
-
if (inc.kind === "node-engine") {
|
|
7254
|
-
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
7255
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
7256
|
-
}
|
|
7257
|
-
if (inc.kind === "package-conflict") {
|
|
7258
|
-
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
7259
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
7260
|
-
}
|
|
7261
|
-
if (inc.kind === "deprecated-api") {
|
|
7262
|
-
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
7263
|
-
}
|
|
7264
|
-
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
7265
|
-
}
|
|
7266
|
-
function findIncompatibilities(nodes) {
|
|
7267
|
-
return nodes.filter(
|
|
7268
|
-
(n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
7269
|
-
);
|
|
7270
|
-
}
|
|
7271
8091
|
function printBanner() {
|
|
7272
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");
|
|
7273
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");
|
|
@@ -7318,7 +8138,7 @@ async function buildPatchSections(services) {
|
|
|
7318
8138
|
}
|
|
7319
8139
|
async function runInit(opts) {
|
|
7320
8140
|
const written = [];
|
|
7321
|
-
const stat = await
|
|
8141
|
+
const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
|
|
7322
8142
|
if (!stat || !stat.isDirectory()) {
|
|
7323
8143
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
7324
8144
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -7327,11 +8147,15 @@ async function runInit(opts) {
|
|
|
7327
8147
|
printDiscoveryReport(opts, services);
|
|
7328
8148
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
7329
8149
|
const patch = renderPatch(sections);
|
|
7330
|
-
const patchPath =
|
|
8150
|
+
const patchPath = import_node_path43.default.join(opts.scanPath, "neat.patch");
|
|
7331
8151
|
if (opts.dryRun) {
|
|
7332
|
-
await
|
|
8152
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7333
8153
|
written.push(patchPath);
|
|
7334
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/)`);
|
|
7335
8159
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
7336
8160
|
return { exitCode: 0, writtenFiles: written };
|
|
7337
8161
|
}
|
|
@@ -7340,12 +8164,16 @@ async function runInit(opts) {
|
|
|
7340
8164
|
const graph = getGraph(graphKey);
|
|
7341
8165
|
const projectPaths = pathsForProject(
|
|
7342
8166
|
graphKey,
|
|
7343
|
-
|
|
8167
|
+
import_node_path43.default.join(opts.scanPath, "neat-out")
|
|
7344
8168
|
);
|
|
7345
|
-
const errorsPath =
|
|
8169
|
+
const errorsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), import_node_path43.default.basename(projectPaths.errorsPath));
|
|
7346
8170
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7347
8171
|
await saveGraphToDisk(graph, opts.outPath);
|
|
7348
8172
|
written.push(opts.outPath);
|
|
8173
|
+
const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
|
|
8174
|
+
if (gitignoreResult.action !== "unchanged") {
|
|
8175
|
+
written.push(gitignoreResult.file);
|
|
8176
|
+
}
|
|
7349
8177
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
7350
8178
|
try {
|
|
7351
8179
|
await addProject({
|
|
@@ -7388,35 +8216,27 @@ async function runInit(opts) {
|
|
|
7388
8216
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
7389
8217
|
}
|
|
7390
8218
|
} else {
|
|
7391
|
-
await
|
|
8219
|
+
await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
|
|
7392
8220
|
written.push(patchPath);
|
|
7393
8221
|
}
|
|
7394
8222
|
}
|
|
7395
|
-
const nodes = [];
|
|
7396
|
-
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
7397
|
-
const edges = [];
|
|
7398
|
-
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
7399
8223
|
console.log("");
|
|
7400
|
-
console.log("=== neat init: summary ===");
|
|
7401
8224
|
console.log(`snapshot: ${opts.outPath}`);
|
|
7402
8225
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
7403
|
-
console.log(
|
|
7404
|
-
|
|
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
|
+
);
|
|
7405
8235
|
console.log(formatExtractionBanner(result.extractionErrors));
|
|
7406
8236
|
if (result.extractionErrors > 0) {
|
|
7407
8237
|
console.log(`errors: ${errorsPath}`);
|
|
7408
8238
|
}
|
|
7409
8239
|
console.log(formatPrecisionFloorBanner(result.extractedDropped));
|
|
7410
|
-
const incompatibilities = findIncompatibilities(nodes);
|
|
7411
|
-
if (incompatibilities.length > 0) {
|
|
7412
|
-
console.log("");
|
|
7413
|
-
console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
|
|
7414
|
-
for (const svc of incompatibilities) {
|
|
7415
|
-
for (const inc of svc.incompatibilities ?? []) {
|
|
7416
|
-
console.log(` ${svc.name}: ${formatIncompat(inc)}`);
|
|
7417
|
-
}
|
|
7418
|
-
}
|
|
7419
|
-
}
|
|
7420
8240
|
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
7421
8241
|
return { exitCode: 4, writtenFiles: written };
|
|
7422
8242
|
}
|
|
@@ -7436,9 +8256,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
7436
8256
|
};
|
|
7437
8257
|
function claudeConfigPath() {
|
|
7438
8258
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
7439
|
-
if (override && override.length > 0) return
|
|
8259
|
+
if (override && override.length > 0) return import_node_path43.default.resolve(override);
|
|
7440
8260
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7441
|
-
return
|
|
8261
|
+
return import_node_path43.default.join(home, ".claude.json");
|
|
7442
8262
|
}
|
|
7443
8263
|
async function runSkill(opts) {
|
|
7444
8264
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -7450,7 +8270,7 @@ async function runSkill(opts) {
|
|
|
7450
8270
|
const target = claudeConfigPath();
|
|
7451
8271
|
let existing = {};
|
|
7452
8272
|
try {
|
|
7453
|
-
existing = JSON.parse(await
|
|
8273
|
+
existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
|
|
7454
8274
|
} catch (err) {
|
|
7455
8275
|
if (err.code !== "ENOENT") {
|
|
7456
8276
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -7462,8 +8282,8 @@ async function runSkill(opts) {
|
|
|
7462
8282
|
...existing,
|
|
7463
8283
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
7464
8284
|
};
|
|
7465
|
-
await
|
|
7466
|
-
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");
|
|
7467
8287
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
7468
8288
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
7469
8289
|
return { exitCode: 0 };
|
|
@@ -7497,12 +8317,12 @@ async function main() {
|
|
|
7497
8317
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
7498
8318
|
process.exit(2);
|
|
7499
8319
|
}
|
|
7500
|
-
const scanPath =
|
|
8320
|
+
const scanPath = import_node_path43.default.resolve(target);
|
|
7501
8321
|
const projectExplicit = parsed.project !== null;
|
|
7502
|
-
const projectName = projectExplicit ? project :
|
|
8322
|
+
const projectName = projectExplicit ? project : import_node_path43.default.basename(scanPath);
|
|
7503
8323
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
7504
|
-
const fallback = pathsForProject(projectKey,
|
|
7505
|
-
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);
|
|
7506
8326
|
const result = await runInit({
|
|
7507
8327
|
scanPath,
|
|
7508
8328
|
outPath,
|
|
@@ -7510,7 +8330,8 @@ async function main() {
|
|
|
7510
8330
|
projectExplicit,
|
|
7511
8331
|
apply: apply3,
|
|
7512
8332
|
dryRun,
|
|
7513
|
-
noInstall
|
|
8333
|
+
noInstall,
|
|
8334
|
+
verbose: parsed.verbose
|
|
7514
8335
|
});
|
|
7515
8336
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
7516
8337
|
return;
|
|
@@ -7522,21 +8343,21 @@ async function main() {
|
|
|
7522
8343
|
usage();
|
|
7523
8344
|
process.exit(2);
|
|
7524
8345
|
}
|
|
7525
|
-
const scanPath =
|
|
7526
|
-
const stat = await
|
|
8346
|
+
const scanPath = import_node_path43.default.resolve(target);
|
|
8347
|
+
const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
|
|
7527
8348
|
if (!stat || !stat.isDirectory()) {
|
|
7528
8349
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
7529
8350
|
process.exit(2);
|
|
7530
8351
|
}
|
|
7531
|
-
const projectPaths = pathsForProject(project,
|
|
7532
|
-
const outPath =
|
|
7533
|
-
const errorsPath =
|
|
7534
|
-
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))
|
|
7535
8356
|
);
|
|
7536
|
-
const staleEventsPath =
|
|
7537
|
-
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))
|
|
7538
8359
|
);
|
|
7539
|
-
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;
|
|
7540
8361
|
const handle = await startWatch(getGraph(project), {
|
|
7541
8362
|
scanPath,
|
|
7542
8363
|
outPath,
|
|
@@ -7629,15 +8450,62 @@ async function main() {
|
|
|
7629
8450
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
7630
8451
|
return;
|
|
7631
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
|
+
}
|
|
7632
8479
|
if (QUERY_VERBS.has(cmd)) {
|
|
7633
8480
|
const code = await runQueryVerb(cmd, parsed);
|
|
7634
8481
|
if (code !== 0) process.exit(code);
|
|
7635
8482
|
return;
|
|
7636
8483
|
}
|
|
8484
|
+
const orchestratorCode = await tryOrchestrator(cmd, parsed);
|
|
8485
|
+
if (orchestratorCode !== null) {
|
|
8486
|
+
if (orchestratorCode !== 0) process.exit(orchestratorCode);
|
|
8487
|
+
return;
|
|
8488
|
+
}
|
|
7637
8489
|
console.error(`neat: unknown command "${cmd}"`);
|
|
7638
8490
|
usage();
|
|
7639
8491
|
process.exit(1);
|
|
7640
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
|
+
}
|
|
7641
8509
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
7642
8510
|
"root-cause",
|
|
7643
8511
|
"blast-radius",
|
|
@@ -7777,10 +8645,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
7777
8645
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7778
8646
|
const out = [];
|
|
7779
8647
|
for (const p of parts) {
|
|
7780
|
-
const r =
|
|
8648
|
+
const r = import_types25.DivergenceTypeSchema.safeParse(p);
|
|
7781
8649
|
if (!r.success) {
|
|
7782
8650
|
console.error(
|
|
7783
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
8651
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
|
|
7784
8652
|
);
|
|
7785
8653
|
return 2;
|
|
7786
8654
|
}
|