@neat.is/core 0.4.17 → 0.4.19
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-6CO7C4IU.js → chunk-BGPWBRLU.js} +4 -2
- package/dist/{chunk-6CO7C4IU.js.map → chunk-BGPWBRLU.js.map} +1 -1
- package/dist/{chunk-GHPHVXYM.js → chunk-MTXF77TN.js} +2 -2
- package/dist/{chunk-LUDSPX5N.js → chunk-Q5EDVWKZ.js} +178 -39
- package/dist/{chunk-LUDSPX5N.js.map → chunk-Q5EDVWKZ.js.map} +1 -1
- package/dist/{chunk-CEDXXMGO.js → chunk-ZV7GHZ2D.js} +208 -24
- package/dist/chunk-ZV7GHZ2D.js.map +1 -0
- package/dist/cli.cjs +520 -172
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +256 -70
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +260 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -14
- package/dist/index.d.ts +52 -14
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +429 -96
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +168 -37
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-QAISVAY5.js → otel-grpc-I67ONCRM.js} +3 -3
- package/dist/server.cjs +69 -37
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-CEDXXMGO.js.map +0 -1
- /package/dist/{chunk-GHPHVXYM.js.map → chunk-MTXF77TN.js.map} +0 -0
- /package/dist/{otel-grpc-QAISVAY5.js.map → otel-grpc-I67ONCRM.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
|
|
|
58
58
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
59
59
|
const publicRead = opts.publicRead === true;
|
|
60
60
|
app.addHook("preHandler", (req, reply, done) => {
|
|
61
|
-
const
|
|
61
|
+
const path53 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
62
62
|
for (const suffix of suffixes) {
|
|
63
|
-
if (
|
|
63
|
+
if (path53 === suffix || path53.endsWith(suffix)) {
|
|
64
64
|
done();
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
@@ -474,8 +474,10 @@ async function buildOtelReceiver(opts) {
|
|
|
474
474
|
for (const s of spans) projectQueue.push({ project, span: s });
|
|
475
475
|
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
476
476
|
};
|
|
477
|
+
const offersProjectRouting = opts.onProjectSpan !== void 0;
|
|
477
478
|
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
478
479
|
function warnLegacyEndpoint(serviceName) {
|
|
480
|
+
if (!offersProjectRouting) return;
|
|
479
481
|
if (legacyEndpointWarned.has(serviceName)) return;
|
|
480
482
|
legacyEndpointWarned.add(serviceName);
|
|
481
483
|
console.warn(
|
|
@@ -611,8 +613,8 @@ __export(cli_exports, {
|
|
|
611
613
|
});
|
|
612
614
|
module.exports = __toCommonJS(cli_exports);
|
|
613
615
|
init_cjs_shims();
|
|
614
|
-
var
|
|
615
|
-
var
|
|
616
|
+
var import_node_path52 = __toESM(require("path"), 1);
|
|
617
|
+
var import_node_fs34 = require("fs");
|
|
616
618
|
|
|
617
619
|
// src/banner.ts
|
|
618
620
|
init_cjs_shims();
|
|
@@ -1177,19 +1179,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1177
1179
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1178
1180
|
let best = { path: [start], edges: [] };
|
|
1179
1181
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1180
|
-
function step(node,
|
|
1181
|
-
if (
|
|
1182
|
-
best = { path: [...
|
|
1182
|
+
function step(node, path53, edges) {
|
|
1183
|
+
if (path53.length > best.path.length) {
|
|
1184
|
+
best = { path: [...path53], edges: [...edges] };
|
|
1183
1185
|
}
|
|
1184
|
-
if (
|
|
1186
|
+
if (path53.length - 1 >= maxDepth) return;
|
|
1185
1187
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1186
1188
|
for (const [srcId, edge] of incoming) {
|
|
1187
1189
|
if (visited.has(srcId)) continue;
|
|
1188
1190
|
visited.add(srcId);
|
|
1189
|
-
|
|
1191
|
+
path53.push(srcId);
|
|
1190
1192
|
edges.push(edge);
|
|
1191
|
-
step(srcId,
|
|
1192
|
-
|
|
1193
|
+
step(srcId, path53, edges);
|
|
1194
|
+
path53.pop();
|
|
1193
1195
|
edges.pop();
|
|
1194
1196
|
visited.delete(srcId);
|
|
1195
1197
|
}
|
|
@@ -5615,7 +5617,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
5615
5617
|
graph.clear();
|
|
5616
5618
|
graph.import(payload.graph);
|
|
5617
5619
|
}
|
|
5618
|
-
function startPersistLoop(graph, outPath,
|
|
5620
|
+
function startPersistLoop(graph, outPath, opts = {}) {
|
|
5621
|
+
const intervalMs = opts.intervalMs ?? 6e4;
|
|
5622
|
+
const exitOnSignal = opts.exitOnSignal ?? true;
|
|
5619
5623
|
let stopped = false;
|
|
5620
5624
|
const tick = async () => {
|
|
5621
5625
|
if (stopped) return;
|
|
@@ -5628,7 +5632,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5628
5632
|
const interval = setInterval(() => {
|
|
5629
5633
|
void tick();
|
|
5630
5634
|
}, intervalMs);
|
|
5631
|
-
const onSignal = (signal) => {
|
|
5635
|
+
const onSignal = exitOnSignal ? (signal) => {
|
|
5632
5636
|
void (async () => {
|
|
5633
5637
|
try {
|
|
5634
5638
|
await saveGraphToDisk(graph, outPath);
|
|
@@ -5638,14 +5642,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5638
5642
|
process.exit(0);
|
|
5639
5643
|
}
|
|
5640
5644
|
})();
|
|
5641
|
-
};
|
|
5642
|
-
|
|
5643
|
-
|
|
5645
|
+
} : null;
|
|
5646
|
+
if (onSignal) {
|
|
5647
|
+
process.on("SIGTERM", onSignal);
|
|
5648
|
+
process.on("SIGINT", onSignal);
|
|
5649
|
+
}
|
|
5644
5650
|
return () => {
|
|
5645
5651
|
stopped = true;
|
|
5646
5652
|
clearInterval(interval);
|
|
5647
|
-
|
|
5648
|
-
|
|
5653
|
+
if (onSignal) {
|
|
5654
|
+
process.off("SIGTERM", onSignal);
|
|
5655
|
+
process.off("SIGINT", onSignal);
|
|
5656
|
+
}
|
|
5649
5657
|
};
|
|
5650
5658
|
}
|
|
5651
5659
|
|
|
@@ -6259,6 +6267,111 @@ function registryLockPath() {
|
|
|
6259
6267
|
function daemonPidPath() {
|
|
6260
6268
|
return import_node_path40.default.join(neatHome(), "neatd.pid");
|
|
6261
6269
|
}
|
|
6270
|
+
function daemonsDir() {
|
|
6271
|
+
return import_node_path40.default.join(neatHome(), "daemons");
|
|
6272
|
+
}
|
|
6273
|
+
function isFiniteInt(v) {
|
|
6274
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
6275
|
+
}
|
|
6276
|
+
function parseDaemonRecord(raw) {
|
|
6277
|
+
let obj;
|
|
6278
|
+
try {
|
|
6279
|
+
obj = JSON.parse(raw);
|
|
6280
|
+
} catch {
|
|
6281
|
+
return void 0;
|
|
6282
|
+
}
|
|
6283
|
+
if (typeof obj !== "object" || obj === null) return void 0;
|
|
6284
|
+
const r = obj;
|
|
6285
|
+
const ports = r.ports;
|
|
6286
|
+
if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
|
|
6287
|
+
return void 0;
|
|
6288
|
+
}
|
|
6289
|
+
return {
|
|
6290
|
+
project: r.project,
|
|
6291
|
+
projectPath: r.projectPath,
|
|
6292
|
+
pid: r.pid,
|
|
6293
|
+
status: r.status,
|
|
6294
|
+
ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
|
|
6295
|
+
startedAt: r.startedAt,
|
|
6296
|
+
neatVersion: r.neatVersion
|
|
6297
|
+
};
|
|
6298
|
+
}
|
|
6299
|
+
var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
|
|
6300
|
+
async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
6301
|
+
const dir = daemonsDir();
|
|
6302
|
+
let names;
|
|
6303
|
+
try {
|
|
6304
|
+
names = await import_node_fs25.promises.readdir(dir);
|
|
6305
|
+
} catch (err) {
|
|
6306
|
+
if (err.code === "ENOENT") return [];
|
|
6307
|
+
throw err;
|
|
6308
|
+
}
|
|
6309
|
+
const out = [];
|
|
6310
|
+
for (const name of names) {
|
|
6311
|
+
if (!name.endsWith(".json")) continue;
|
|
6312
|
+
const file = import_node_path40.default.join(dir, name);
|
|
6313
|
+
let raw;
|
|
6314
|
+
try {
|
|
6315
|
+
raw = await import_node_fs25.promises.readFile(file, "utf8");
|
|
6316
|
+
} catch {
|
|
6317
|
+
continue;
|
|
6318
|
+
}
|
|
6319
|
+
const record = parseDaemonRecord(raw);
|
|
6320
|
+
if (!record) continue;
|
|
6321
|
+
const live = record.status === "running" && probe.isPidAlive(record.pid);
|
|
6322
|
+
out.push({ record, live, source: file });
|
|
6323
|
+
}
|
|
6324
|
+
out.sort((a, b) => a.record.project.localeCompare(b.record.project));
|
|
6325
|
+
return out;
|
|
6326
|
+
}
|
|
6327
|
+
async function removeDaemonRecord(source) {
|
|
6328
|
+
await import_node_fs25.promises.unlink(source).catch(() => {
|
|
6329
|
+
});
|
|
6330
|
+
}
|
|
6331
|
+
async function listMachineProjects(probe = defaultDiscoveryProbe) {
|
|
6332
|
+
const discovered = await discoverDaemons(probe);
|
|
6333
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
6334
|
+
for (const d of discovered) {
|
|
6335
|
+
const key = await normalizeProjectPath(d.record.projectPath);
|
|
6336
|
+
byPath.set(key, {
|
|
6337
|
+
project: d.record.project,
|
|
6338
|
+
projectPath: d.record.projectPath,
|
|
6339
|
+
state: d.live ? "running" : "stopped",
|
|
6340
|
+
ports: d.record.ports,
|
|
6341
|
+
pid: d.record.pid
|
|
6342
|
+
});
|
|
6343
|
+
}
|
|
6344
|
+
let legacy = [];
|
|
6345
|
+
try {
|
|
6346
|
+
legacy = (await readRegistry()).projects;
|
|
6347
|
+
} catch {
|
|
6348
|
+
legacy = [];
|
|
6349
|
+
}
|
|
6350
|
+
for (const entry2 of legacy) {
|
|
6351
|
+
const key = await normalizeProjectPath(entry2.path);
|
|
6352
|
+
if (byPath.has(key)) continue;
|
|
6353
|
+
byPath.set(key, {
|
|
6354
|
+
project: entry2.name,
|
|
6355
|
+
projectPath: entry2.path,
|
|
6356
|
+
state: "registered",
|
|
6357
|
+
registryStatus: entry2.status
|
|
6358
|
+
});
|
|
6359
|
+
}
|
|
6360
|
+
return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
|
|
6361
|
+
}
|
|
6362
|
+
async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
|
|
6363
|
+
const discovered = await discoverDaemons(probe);
|
|
6364
|
+
return discovered.find((d) => d.record.project === name);
|
|
6365
|
+
}
|
|
6366
|
+
function signalDaemonStop(pid) {
|
|
6367
|
+
try {
|
|
6368
|
+
process.kill(pid, "SIGTERM");
|
|
6369
|
+
return true;
|
|
6370
|
+
} catch (err) {
|
|
6371
|
+
if (err.code === "ESRCH") return false;
|
|
6372
|
+
throw err;
|
|
6373
|
+
}
|
|
6374
|
+
}
|
|
6262
6375
|
function isPidAliveDefault(pid) {
|
|
6263
6376
|
try {
|
|
6264
6377
|
process.kill(pid, 0);
|
|
@@ -6587,12 +6700,16 @@ function serializeGraph(graph) {
|
|
|
6587
6700
|
});
|
|
6588
6701
|
return { nodes, edges };
|
|
6589
6702
|
}
|
|
6590
|
-
function projectFromReq(req) {
|
|
6703
|
+
function projectFromReq(req, singleProject) {
|
|
6591
6704
|
const params = req.params;
|
|
6592
|
-
|
|
6705
|
+
const named = params.project;
|
|
6706
|
+
if (singleProject) {
|
|
6707
|
+
return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
|
|
6708
|
+
}
|
|
6709
|
+
return named ?? DEFAULT_PROJECT;
|
|
6593
6710
|
}
|
|
6594
|
-
function resolveProject(registry, req, reply, bootstrap) {
|
|
6595
|
-
const name = projectFromReq(req);
|
|
6711
|
+
function resolveProject(registry, req, reply, bootstrap, singleProject) {
|
|
6712
|
+
const name = projectFromReq(req, singleProject);
|
|
6596
6713
|
const ctx = registry.get(name);
|
|
6597
6714
|
if (!ctx) {
|
|
6598
6715
|
const phase = bootstrap?.status(name);
|
|
@@ -6633,13 +6750,13 @@ function buildLegacyRegistry(opts) {
|
|
|
6633
6750
|
function registerRoutes(scope, ctx) {
|
|
6634
6751
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
6635
6752
|
scope.get("/events", (req, reply) => {
|
|
6636
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6753
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6637
6754
|
if (!proj) return;
|
|
6638
6755
|
handleSse(req, reply, { project: proj.name });
|
|
6639
6756
|
});
|
|
6640
6757
|
if (ctx.scope === "project") {
|
|
6641
6758
|
scope.get("/health", async (req, reply) => {
|
|
6642
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6759
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6643
6760
|
if (!proj) return;
|
|
6644
6761
|
const uptimeMs = Date.now() - startedAt;
|
|
6645
6762
|
return {
|
|
@@ -6657,14 +6774,14 @@ function registerRoutes(scope, ctx) {
|
|
|
6657
6774
|
});
|
|
6658
6775
|
}
|
|
6659
6776
|
scope.get("/graph", async (req, reply) => {
|
|
6660
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6777
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6661
6778
|
if (!proj) return;
|
|
6662
6779
|
return serializeGraph(proj.graph);
|
|
6663
6780
|
});
|
|
6664
6781
|
scope.get(
|
|
6665
6782
|
"/graph/node/:id",
|
|
6666
6783
|
async (req, reply) => {
|
|
6667
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6784
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6668
6785
|
if (!proj) return;
|
|
6669
6786
|
const { id } = req.params;
|
|
6670
6787
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6676,7 +6793,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6676
6793
|
scope.get(
|
|
6677
6794
|
"/graph/edges/:id",
|
|
6678
6795
|
async (req, reply) => {
|
|
6679
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6796
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6680
6797
|
if (!proj) return;
|
|
6681
6798
|
const { id } = req.params;
|
|
6682
6799
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6688,7 +6805,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6688
6805
|
}
|
|
6689
6806
|
);
|
|
6690
6807
|
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
6691
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6808
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6692
6809
|
if (!proj) return;
|
|
6693
6810
|
const { nodeId } = req.params;
|
|
6694
6811
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6703,7 +6820,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6703
6820
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6704
6821
|
});
|
|
6705
6822
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6706
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6823
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6707
6824
|
if (!proj) return;
|
|
6708
6825
|
let typeFilter;
|
|
6709
6826
|
if (req.query.type) {
|
|
@@ -6738,7 +6855,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6738
6855
|
});
|
|
6739
6856
|
});
|
|
6740
6857
|
scope.get("/incidents", async (req, reply) => {
|
|
6741
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6858
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6742
6859
|
if (!proj) return;
|
|
6743
6860
|
const epath = errorsPathFor(proj);
|
|
6744
6861
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -6750,7 +6867,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6750
6867
|
return { count: sliced.length, total, events: sliced };
|
|
6751
6868
|
});
|
|
6752
6869
|
scope.get("/stale-events", async (req, reply) => {
|
|
6753
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6870
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6754
6871
|
if (!proj) return;
|
|
6755
6872
|
const spath = staleEventsPathFor(proj);
|
|
6756
6873
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -6765,7 +6882,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6765
6882
|
scope.get(
|
|
6766
6883
|
"/incidents/:nodeId",
|
|
6767
6884
|
async (req, reply) => {
|
|
6768
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6885
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6769
6886
|
if (!proj) return;
|
|
6770
6887
|
const { nodeId } = req.params;
|
|
6771
6888
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6781,7 +6898,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6781
6898
|
}
|
|
6782
6899
|
);
|
|
6783
6900
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6784
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6901
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6785
6902
|
if (!proj) return;
|
|
6786
6903
|
const { nodeId } = req.params;
|
|
6787
6904
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6801,7 +6918,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6801
6918
|
return result;
|
|
6802
6919
|
});
|
|
6803
6920
|
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
6804
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6921
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6805
6922
|
if (!proj) return;
|
|
6806
6923
|
const { nodeId } = req.params;
|
|
6807
6924
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6814,7 +6931,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6814
6931
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
6815
6932
|
});
|
|
6816
6933
|
scope.get("/search", async (req, reply) => {
|
|
6817
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6934
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6818
6935
|
if (!proj) return;
|
|
6819
6936
|
const raw = (req.query.q ?? "").trim();
|
|
6820
6937
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -6845,7 +6962,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6845
6962
|
scope.get(
|
|
6846
6963
|
"/graph/diff",
|
|
6847
6964
|
async (req, reply) => {
|
|
6848
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6965
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6849
6966
|
if (!proj) return;
|
|
6850
6967
|
const against = req.query.against;
|
|
6851
6968
|
if (!against) {
|
|
@@ -6860,7 +6977,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6860
6977
|
}
|
|
6861
6978
|
);
|
|
6862
6979
|
scope.post("/snapshot", async (req, reply) => {
|
|
6863
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6980
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6864
6981
|
if (!proj) return;
|
|
6865
6982
|
const body = req.body;
|
|
6866
6983
|
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
@@ -6889,7 +7006,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6889
7006
|
}
|
|
6890
7007
|
});
|
|
6891
7008
|
scope.post("/graph/scan", async (req, reply) => {
|
|
6892
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7009
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6893
7010
|
if (!proj) return;
|
|
6894
7011
|
if (!proj.scanPath) {
|
|
6895
7012
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6905,7 +7022,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6905
7022
|
};
|
|
6906
7023
|
});
|
|
6907
7024
|
scope.get("/policies", async (req, reply) => {
|
|
6908
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7025
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6909
7026
|
if (!proj) return;
|
|
6910
7027
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
6911
7028
|
if (!policyPath) {
|
|
@@ -6922,7 +7039,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6922
7039
|
}
|
|
6923
7040
|
});
|
|
6924
7041
|
scope.get("/policies/violations", async (req, reply) => {
|
|
6925
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7042
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6926
7043
|
if (!proj) return;
|
|
6927
7044
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6928
7045
|
let violations = await log.readAll();
|
|
@@ -6942,7 +7059,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6942
7059
|
return { violations };
|
|
6943
7060
|
});
|
|
6944
7061
|
scope.post("/policies/check", async (req, reply) => {
|
|
6945
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7062
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6946
7063
|
if (!proj) return;
|
|
6947
7064
|
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6948
7065
|
if (!parsed.success) {
|
|
@@ -6978,7 +7095,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6978
7095
|
};
|
|
6979
7096
|
});
|
|
6980
7097
|
scope.get("/extend/list-uninstrumented", async (req, reply) => {
|
|
6981
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7098
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6982
7099
|
if (!proj) return;
|
|
6983
7100
|
if (!proj.scanPath) {
|
|
6984
7101
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6991,7 +7108,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6991
7108
|
}
|
|
6992
7109
|
});
|
|
6993
7110
|
scope.get("/extend/lookup", async (req, reply) => {
|
|
6994
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7111
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6995
7112
|
if (!proj) return;
|
|
6996
7113
|
const { library, version } = req.query;
|
|
6997
7114
|
if (!library) {
|
|
@@ -7004,7 +7121,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7004
7121
|
return result;
|
|
7005
7122
|
});
|
|
7006
7123
|
scope.get("/extend/describe", async (req, reply) => {
|
|
7007
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7124
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7008
7125
|
if (!proj) return;
|
|
7009
7126
|
if (!proj.scanPath) {
|
|
7010
7127
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -7017,7 +7134,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7017
7134
|
}
|
|
7018
7135
|
});
|
|
7019
7136
|
scope.post("/extend/apply", async (req, reply) => {
|
|
7020
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7137
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7021
7138
|
if (!proj) return;
|
|
7022
7139
|
if (!proj.scanPath) {
|
|
7023
7140
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -7037,7 +7154,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7037
7154
|
}
|
|
7038
7155
|
});
|
|
7039
7156
|
scope.post("/extend/dry-run", async (req, reply) => {
|
|
7040
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7157
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7041
7158
|
if (!proj) return;
|
|
7042
7159
|
if (!proj.scanPath) {
|
|
7043
7160
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -7057,7 +7174,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7057
7174
|
}
|
|
7058
7175
|
});
|
|
7059
7176
|
scope.post("/extend/rollback", async (req, reply) => {
|
|
7060
|
-
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
7177
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7061
7178
|
if (!proj) return;
|
|
7062
7179
|
if (!proj.scanPath) {
|
|
7063
7180
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -7120,7 +7237,8 @@ async function buildApi(opts) {
|
|
|
7120
7237
|
errorsPathFor,
|
|
7121
7238
|
staleEventsPathFor,
|
|
7122
7239
|
policyFilePathFor,
|
|
7123
|
-
bootstrap: opts.bootstrap
|
|
7240
|
+
bootstrap: opts.bootstrap,
|
|
7241
|
+
singleProject: opts.singleProject?.name
|
|
7124
7242
|
};
|
|
7125
7243
|
app.get("/health", async () => {
|
|
7126
7244
|
const uptimeMs = Date.now() - startedAt;
|
|
@@ -7143,10 +7261,29 @@ async function buildApi(opts) {
|
|
|
7143
7261
|
return {
|
|
7144
7262
|
ok: true,
|
|
7145
7263
|
uptimeMs,
|
|
7264
|
+
// ADR-096 §7 — a per-project daemon carries its project at the top level
|
|
7265
|
+
// so a spawn can confirm a daemon found on a reused port is serving THIS
|
|
7266
|
+
// project before reusing it. The legacy multi-project daemon omits it and
|
|
7267
|
+
// is matched against the `projects` array instead.
|
|
7268
|
+
...opts.singleProject ? { project: opts.singleProject.name } : {},
|
|
7146
7269
|
projects
|
|
7147
7270
|
};
|
|
7148
7271
|
});
|
|
7149
7272
|
app.get("/projects", async (_req, reply) => {
|
|
7273
|
+
if (opts.singleProject) {
|
|
7274
|
+
const phase = opts.bootstrap?.status(opts.singleProject.name);
|
|
7275
|
+
const entry2 = {
|
|
7276
|
+
name: opts.singleProject.name,
|
|
7277
|
+
path: opts.singleProject.path,
|
|
7278
|
+
registeredAt: new Date(startedAt).toISOString(),
|
|
7279
|
+
languages: [],
|
|
7280
|
+
// The daemon is serving this project, so it's active unless its
|
|
7281
|
+
// bootstrap broke. ('bootstrapping' still reads as active — the project
|
|
7282
|
+
// is the one to land on; its graph route answers 503 until ready.)
|
|
7283
|
+
status: phase === "broken" ? "broken" : "active"
|
|
7284
|
+
};
|
|
7285
|
+
return [entry2];
|
|
7286
|
+
}
|
|
7150
7287
|
try {
|
|
7151
7288
|
return await listProjects();
|
|
7152
7289
|
} catch (err) {
|
|
@@ -7994,9 +8131,50 @@ var import_semver2 = __toESM(require("semver"), 1);
|
|
|
7994
8131
|
// src/installers/templates.ts
|
|
7995
8132
|
init_cjs_shims();
|
|
7996
8133
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
7997
|
-
var OTEL_INIT_STAMP = "// neat-template-version:
|
|
8134
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
|
|
7998
8135
|
var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
|
|
7999
8136
|
var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
|
|
8137
|
+
var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
|
|
8138
|
+
try {
|
|
8139
|
+
if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
|
|
8140
|
+
const __neatFs = require('node:fs')
|
|
8141
|
+
const __neatPath = require('node:path')
|
|
8142
|
+
let __neatDir = process.cwd()
|
|
8143
|
+
for (let __i = 0; __i < 8; __i++) {
|
|
8144
|
+
try {
|
|
8145
|
+
const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
|
|
8146
|
+
if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
|
|
8147
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
|
|
8148
|
+
break
|
|
8149
|
+
}
|
|
8150
|
+
} catch (_e) {}
|
|
8151
|
+
const __parent = __neatPath.dirname(__neatDir)
|
|
8152
|
+
if (__parent === __neatDir) break
|
|
8153
|
+
__neatDir = __parent
|
|
8154
|
+
}
|
|
8155
|
+
} catch (_e) {}
|
|
8156
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
|
|
8157
|
+
})()`;
|
|
8158
|
+
var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
|
|
8159
|
+
var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
|
|
8160
|
+
try {
|
|
8161
|
+
if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
|
|
8162
|
+
let __neatDir = process.cwd()
|
|
8163
|
+
for (let __i = 0; __i < 8; __i++) {
|
|
8164
|
+
try {
|
|
8165
|
+
const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
|
|
8166
|
+
if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
|
|
8167
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
|
|
8168
|
+
break
|
|
8169
|
+
}
|
|
8170
|
+
} catch (_e) {}
|
|
8171
|
+
const __parent = __neatDirname(__neatDir)
|
|
8172
|
+
if (__parent === __neatDir) break
|
|
8173
|
+
__neatDir = __parent
|
|
8174
|
+
}
|
|
8175
|
+
} catch (_e) {}
|
|
8176
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
|
|
8177
|
+
})()`;
|
|
8000
8178
|
function neatCaptureSource(ts) {
|
|
8001
8179
|
const spanT = ts ? ": any" : "";
|
|
8002
8180
|
const strOpt = ts ? ": string | undefined" : "";
|
|
@@ -8285,7 +8463,7 @@ try {
|
|
|
8285
8463
|
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
8286
8464
|
${OTEL_INIT_STAMP}
|
|
8287
8465
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8288
|
-
|
|
8466
|
+
${OTEL_ENDPOINT_RESOLVER_CJS}
|
|
8289
8467
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8290
8468
|
${OTEL_OTLP_HEADERS_JS}
|
|
8291
8469
|
|
|
@@ -8303,8 +8481,9 @@ ${neatWireCaptureSource(false)}
|
|
|
8303
8481
|
`;
|
|
8304
8482
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
8305
8483
|
${OTEL_INIT_STAMP}
|
|
8484
|
+
${OTEL_ESM_NODE_IMPORTS}
|
|
8306
8485
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8307
|
-
|
|
8486
|
+
${OTEL_ENDPOINT_RESOLVER_ESM}
|
|
8308
8487
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8309
8488
|
${OTEL_OTLP_HEADERS_JS}
|
|
8310
8489
|
|
|
@@ -8327,8 +8506,9 @@ ${OTEL_INIT_STAMP}
|
|
|
8327
8506
|
// strict user tsconfig would reject; suppressing type-checking here keeps the
|
|
8328
8507
|
// generated file from breaking the host project's \`tsc\` gate (#427) without
|
|
8329
8508
|
// constraining the runtime logic. The file is regenerated, never hand-edited.
|
|
8509
|
+
${OTEL_ESM_NODE_IMPORTS}
|
|
8330
8510
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8331
|
-
|
|
8511
|
+
${OTEL_ENDPOINT_RESOLVER_ESM}
|
|
8332
8512
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8333
8513
|
${OTEL_OTLP_HEADERS_JS}
|
|
8334
8514
|
|
|
@@ -8350,11 +8530,14 @@ ${registrations.join("\n")}
|
|
|
8350
8530
|
`;
|
|
8351
8531
|
return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
|
|
8352
8532
|
}
|
|
8353
|
-
function renderEnvNeat(serviceName,
|
|
8533
|
+
function renderEnvNeat(serviceName, _projectName) {
|
|
8354
8534
|
return [
|
|
8355
8535
|
"# Generated by `neat init --apply` (ADR-069).",
|
|
8356
8536
|
`OTEL_SERVICE_NAME=${serviceName}`,
|
|
8357
|
-
|
|
8537
|
+
"# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
|
|
8538
|
+
"# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
|
|
8539
|
+
"# canonical default the daemon takes when its first-choice OTLP port is free.",
|
|
8540
|
+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
|
|
8358
8541
|
"OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
|
|
8359
8542
|
"# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
|
|
8360
8543
|
"# NEAT_OTEL_TOKEN=",
|
|
@@ -8377,8 +8560,9 @@ export async function register() {
|
|
|
8377
8560
|
}
|
|
8378
8561
|
`;
|
|
8379
8562
|
var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
8563
|
+
${OTEL_ESM_NODE_IMPORTS}
|
|
8380
8564
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8381
|
-
|
|
8565
|
+
${OTEL_ENDPOINT_RESOLVER_ESM}
|
|
8382
8566
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8383
8567
|
${OTEL_OTLP_HEADERS_JS}
|
|
8384
8568
|
|
|
@@ -8391,7 +8575,7 @@ new NodeSDK({ instrumentations }).start()
|
|
|
8391
8575
|
`;
|
|
8392
8576
|
var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
8393
8577
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8394
|
-
|
|
8578
|
+
${OTEL_ENDPOINT_RESOLVER_CJS}
|
|
8395
8579
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8396
8580
|
${OTEL_OTLP_HEADERS_JS}
|
|
8397
8581
|
|
|
@@ -8410,8 +8594,9 @@ ${registrations.join("\n")}
|
|
|
8410
8594
|
}
|
|
8411
8595
|
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
8412
8596
|
var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
8597
|
+
${OTEL_ESM_NODE_IMPORTS}
|
|
8413
8598
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8414
|
-
|
|
8599
|
+
${OTEL_ENDPOINT_RESOLVER_ESM}
|
|
8415
8600
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8416
8601
|
${OTEL_OTLP_HEADERS_JS}
|
|
8417
8602
|
|
|
@@ -8426,7 +8611,7 @@ sdk.start()
|
|
|
8426
8611
|
`;
|
|
8427
8612
|
var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
8428
8613
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
8429
|
-
|
|
8614
|
+
${OTEL_ENDPOINT_RESOLVER_CJS}
|
|
8430
8615
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
8431
8616
|
${OTEL_OTLP_HEADERS_JS}
|
|
8432
8617
|
|
|
@@ -9895,19 +10080,51 @@ function renderPatch(sections) {
|
|
|
9895
10080
|
|
|
9896
10081
|
// src/orchestrator.ts
|
|
9897
10082
|
init_cjs_shims();
|
|
9898
|
-
var
|
|
10083
|
+
var import_node_fs33 = require("fs");
|
|
9899
10084
|
var import_node_http = __toESM(require("http"), 1);
|
|
9900
10085
|
var import_node_net = __toESM(require("net"), 1);
|
|
9901
|
-
var
|
|
10086
|
+
var import_node_path50 = __toESM(require("path"), 1);
|
|
9902
10087
|
var import_node_child_process3 = require("child_process");
|
|
9903
10088
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
10089
|
+
|
|
10090
|
+
// src/daemon.ts
|
|
10091
|
+
init_cjs_shims();
|
|
10092
|
+
var import_node_fs32 = require("fs");
|
|
10093
|
+
var import_node_path49 = __toESM(require("path"), 1);
|
|
10094
|
+
var import_node_module = require("module");
|
|
10095
|
+
init_otel();
|
|
10096
|
+
init_auth();
|
|
10097
|
+
|
|
10098
|
+
// src/unrouted.ts
|
|
10099
|
+
init_cjs_shims();
|
|
10100
|
+
var import_node_fs31 = require("fs");
|
|
10101
|
+
var import_node_path48 = __toESM(require("path"), 1);
|
|
10102
|
+
|
|
10103
|
+
// src/daemon.ts
|
|
10104
|
+
function daemonJsonPath(scanPath) {
|
|
10105
|
+
return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
|
|
10106
|
+
}
|
|
10107
|
+
async function readDaemonRecord(scanPath) {
|
|
10108
|
+
try {
|
|
10109
|
+
const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
10110
|
+
const parsed = JSON.parse(raw);
|
|
10111
|
+
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
10112
|
+
return parsed;
|
|
10113
|
+
}
|
|
10114
|
+
return null;
|
|
10115
|
+
} catch {
|
|
10116
|
+
return null;
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
10119
|
+
|
|
10120
|
+
// src/orchestrator.ts
|
|
9904
10121
|
async function extractAndPersist(opts) {
|
|
9905
10122
|
const services = await discoverServices(opts.scanPath);
|
|
9906
10123
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
9907
10124
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
9908
10125
|
resetGraph(graphKey);
|
|
9909
10126
|
const graph = getGraph(graphKey);
|
|
9910
|
-
const projectPaths = pathsForProject(graphKey,
|
|
10127
|
+
const projectPaths = pathsForProject(graphKey, import_node_path50.default.join(opts.scanPath, "neat-out"));
|
|
9911
10128
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
9912
10129
|
errorsPath: projectPaths.errorsPath
|
|
9913
10130
|
});
|
|
@@ -9961,7 +10178,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
9961
10178
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
9962
10179
|
} else if (outcome.outcome === "react-native") {
|
|
9963
10180
|
reactNative++;
|
|
9964
|
-
const svcName =
|
|
10181
|
+
const svcName = import_node_path50.default.basename(svc.dir);
|
|
9965
10182
|
console.log(
|
|
9966
10183
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
9967
10184
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -9972,7 +10189,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
9972
10189
|
);
|
|
9973
10190
|
} else if (outcome.outcome === "bun") {
|
|
9974
10191
|
bun++;
|
|
9975
|
-
const svcName =
|
|
10192
|
+
const svcName = import_node_path50.default.basename(svc.dir);
|
|
9976
10193
|
console.log(
|
|
9977
10194
|
`neat: ${svc.dir} detected as Bun
|
|
9978
10195
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -9983,7 +10200,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
9983
10200
|
);
|
|
9984
10201
|
} else if (outcome.outcome === "deno") {
|
|
9985
10202
|
deno++;
|
|
9986
|
-
const svcName =
|
|
10203
|
+
const svcName = import_node_path50.default.basename(svc.dir);
|
|
9987
10204
|
console.log(
|
|
9988
10205
|
`neat: ${svc.dir} detected as Deno
|
|
9989
10206
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -9994,7 +10211,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
9994
10211
|
);
|
|
9995
10212
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
9996
10213
|
cloudflareWorkers++;
|
|
9997
|
-
const svcName =
|
|
10214
|
+
const svcName = import_node_path50.default.basename(svc.dir);
|
|
9998
10215
|
console.log(
|
|
9999
10216
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
10000
10217
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -10005,7 +10222,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
10005
10222
|
);
|
|
10006
10223
|
} else if (outcome.outcome === "electron") {
|
|
10007
10224
|
electron++;
|
|
10008
|
-
const svcName =
|
|
10225
|
+
const svcName = import_node_path50.default.basename(svc.dir);
|
|
10009
10226
|
console.log(
|
|
10010
10227
|
`neat: ${svc.dir} detected as Electron
|
|
10011
10228
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -10086,10 +10303,6 @@ async function fetchDaemonHealth(restPort) {
|
|
|
10086
10303
|
});
|
|
10087
10304
|
});
|
|
10088
10305
|
}
|
|
10089
|
-
async function checkDaemonHealth(restPort) {
|
|
10090
|
-
const body = await fetchDaemonHealth(restPort);
|
|
10091
|
-
return body !== null;
|
|
10092
|
-
}
|
|
10093
10306
|
async function probeProjectHealth(restPort, name) {
|
|
10094
10307
|
return new Promise((resolve) => {
|
|
10095
10308
|
const req = import_node_http.default.get(
|
|
@@ -10165,12 +10378,6 @@ async function isPortFree(port) {
|
|
|
10165
10378
|
server.listen(port, "127.0.0.1");
|
|
10166
10379
|
});
|
|
10167
10380
|
}
|
|
10168
|
-
async function probePortsFree() {
|
|
10169
|
-
for (const port of NEAT_PORTS) {
|
|
10170
|
-
if (!await isPortFree(port)) return { free: false, held: port };
|
|
10171
|
-
}
|
|
10172
|
-
return { free: true };
|
|
10173
|
-
}
|
|
10174
10381
|
function formatPortCollisionMessage(port) {
|
|
10175
10382
|
return [
|
|
10176
10383
|
`neat: port ${port} is in use; the NEAT daemon needs it.`,
|
|
@@ -10178,11 +10385,82 @@ function formatPortCollisionMessage(port) {
|
|
|
10178
10385
|
` \`lsof -i :${port}\` to find the holding process.`
|
|
10179
10386
|
];
|
|
10180
10387
|
}
|
|
10181
|
-
|
|
10182
|
-
|
|
10388
|
+
var PORT_ALLOCATION_ATTEMPTS = 8;
|
|
10389
|
+
var PORT_STRIDE = 1;
|
|
10390
|
+
async function tripleFree(ports) {
|
|
10391
|
+
for (const p of [ports.rest, ports.otlp, ports.web]) {
|
|
10392
|
+
if (!await isPortFree(p)) return false;
|
|
10393
|
+
}
|
|
10394
|
+
return true;
|
|
10395
|
+
}
|
|
10396
|
+
async function allocatePorts() {
|
|
10397
|
+
const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
|
|
10398
|
+
for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
|
|
10399
|
+
const candidate = {
|
|
10400
|
+
rest: baseRest + i * PORT_STRIDE,
|
|
10401
|
+
otlp: baseOtlp + i * PORT_STRIDE,
|
|
10402
|
+
web: baseWeb + i * PORT_STRIDE
|
|
10403
|
+
};
|
|
10404
|
+
if (await tripleFree(candidate)) return candidate;
|
|
10405
|
+
}
|
|
10406
|
+
return null;
|
|
10407
|
+
}
|
|
10408
|
+
async function persistedPortsFor(scanPath) {
|
|
10409
|
+
const record = await readDaemonRecord(scanPath);
|
|
10410
|
+
if (!record) return null;
|
|
10411
|
+
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
10412
|
+
}
|
|
10413
|
+
async function acquireSpawnLock(scanPath) {
|
|
10414
|
+
const lockPath = import_node_path50.default.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
10415
|
+
await import_node_fs33.promises.mkdir(import_node_path50.default.dirname(lockPath), { recursive: true });
|
|
10416
|
+
const STALE_LOCK_MS = 6e4;
|
|
10417
|
+
try {
|
|
10418
|
+
const fd = await import_node_fs33.promises.open(lockPath, "wx");
|
|
10419
|
+
await fd.writeFile(`${process.pid}
|
|
10420
|
+
`, "utf8");
|
|
10421
|
+
await fd.close();
|
|
10422
|
+
return async () => {
|
|
10423
|
+
await import_node_fs33.promises.unlink(lockPath).catch(() => {
|
|
10424
|
+
});
|
|
10425
|
+
};
|
|
10426
|
+
} catch (err) {
|
|
10427
|
+
if (err.code !== "EEXIST") return null;
|
|
10428
|
+
try {
|
|
10429
|
+
const stat = await import_node_fs33.promises.stat(lockPath);
|
|
10430
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
10431
|
+
await import_node_fs33.promises.unlink(lockPath).catch(() => {
|
|
10432
|
+
});
|
|
10433
|
+
return acquireSpawnLock(scanPath);
|
|
10434
|
+
}
|
|
10435
|
+
} catch {
|
|
10436
|
+
return acquireSpawnLock(scanPath);
|
|
10437
|
+
}
|
|
10438
|
+
return null;
|
|
10439
|
+
}
|
|
10440
|
+
}
|
|
10441
|
+
async function waitForPeerDaemon(restPort, project, timeoutMs) {
|
|
10442
|
+
const deadline = Date.now() + timeoutMs;
|
|
10443
|
+
while (Date.now() < deadline) {
|
|
10444
|
+
if (await healthIsForProject(restPort, project)) return true;
|
|
10445
|
+
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
10446
|
+
}
|
|
10447
|
+
return healthIsForProject(restPort, project);
|
|
10448
|
+
}
|
|
10449
|
+
async function healthIsForProject(restPort, project) {
|
|
10450
|
+
const body = await fetchDaemonHealth(restPort);
|
|
10451
|
+
if (body === null) return false;
|
|
10452
|
+
const named = body.project;
|
|
10453
|
+
if (typeof named === "string") return named === project;
|
|
10454
|
+
if (Array.isArray(body.projects)) {
|
|
10455
|
+
return body.projects.some((p) => p.name === project);
|
|
10456
|
+
}
|
|
10457
|
+
return false;
|
|
10458
|
+
}
|
|
10459
|
+
function spawnDaemonDetached(spec) {
|
|
10460
|
+
const here = import_node_path50.default.dirname(new URL(importMetaUrl).pathname);
|
|
10183
10461
|
const candidates = [
|
|
10184
|
-
|
|
10185
|
-
|
|
10462
|
+
import_node_path50.default.join(here, "neatd.cjs"),
|
|
10463
|
+
import_node_path50.default.join(here, "neatd.js")
|
|
10186
10464
|
];
|
|
10187
10465
|
let entry2 = null;
|
|
10188
10466
|
const fsSync = require("fs");
|
|
@@ -10202,6 +10480,13 @@ function spawnDaemonDetached() {
|
|
|
10202
10480
|
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
10203
10481
|
env.HOST = "127.0.0.1";
|
|
10204
10482
|
}
|
|
10483
|
+
if (spec) {
|
|
10484
|
+
env.NEAT_PROJECT = spec.project;
|
|
10485
|
+
env.NEAT_PROJECT_PATH = spec.projectPath;
|
|
10486
|
+
env.PORT = String(spec.ports.rest);
|
|
10487
|
+
env.OTEL_PORT = String(spec.ports.otlp);
|
|
10488
|
+
env.NEAT_WEB_PORT = String(spec.ports.web);
|
|
10489
|
+
}
|
|
10205
10490
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
|
|
10206
10491
|
detached: true,
|
|
10207
10492
|
// stderr inherits the orchestrator's fd so the daemon's
|
|
@@ -10240,7 +10525,7 @@ async function runOrchestrator(opts) {
|
|
|
10240
10525
|
browser: "skipped"
|
|
10241
10526
|
}
|
|
10242
10527
|
};
|
|
10243
|
-
const stat = await
|
|
10528
|
+
const stat = await import_node_fs33.promises.stat(opts.scanPath).catch(() => null);
|
|
10244
10529
|
if (!stat || !stat.isDirectory()) {
|
|
10245
10530
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
10246
10531
|
result.exitCode = 2;
|
|
@@ -10321,48 +10606,75 @@ async function runOrchestrator(opts) {
|
|
|
10321
10606
|
result.exitCode = 1;
|
|
10322
10607
|
}
|
|
10323
10608
|
}
|
|
10324
|
-
const restPort = Number(process.env.PORT ?? 8080);
|
|
10325
10609
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
10326
|
-
|
|
10610
|
+
const persistedPorts = await persistedPortsFor(opts.scanPath);
|
|
10611
|
+
let allocated = null;
|
|
10612
|
+
if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
|
|
10327
10613
|
result.steps.daemon = "already-running";
|
|
10614
|
+
allocated = persistedPorts;
|
|
10328
10615
|
} else {
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10616
|
+
if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
|
|
10617
|
+
allocated = persistedPorts;
|
|
10618
|
+
} else {
|
|
10619
|
+
allocated = await allocatePorts();
|
|
10620
|
+
}
|
|
10621
|
+
if (!allocated) {
|
|
10622
|
+
for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
|
|
10332
10623
|
console.error(line);
|
|
10333
10624
|
}
|
|
10334
10625
|
result.exitCode = 3;
|
|
10335
10626
|
return result;
|
|
10336
10627
|
}
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
if (!ready.ready) {
|
|
10347
|
-
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
10348
|
-
if (ready.stillBootstrapping.length > 0) {
|
|
10349
|
-
console.error(
|
|
10350
|
-
`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
|
|
10351
|
-
);
|
|
10628
|
+
const release = await acquireSpawnLock(opts.scanPath);
|
|
10629
|
+
if (!release) {
|
|
10630
|
+
const reused = await waitForPeerDaemon(allocated.rest, currentProjectName, timeoutMs);
|
|
10631
|
+
if (reused) {
|
|
10632
|
+
result.steps.daemon = "already-running";
|
|
10633
|
+
} else {
|
|
10634
|
+
console.error("neat: another `neat` is spawning this project but its daemon did not come up in time");
|
|
10635
|
+
result.exitCode = 1;
|
|
10636
|
+
return result;
|
|
10352
10637
|
}
|
|
10353
|
-
|
|
10354
|
-
|
|
10638
|
+
} else {
|
|
10639
|
+
try {
|
|
10640
|
+
if (await healthIsForProject(allocated.rest, currentProjectName)) {
|
|
10641
|
+
result.steps.daemon = "already-running";
|
|
10642
|
+
} else {
|
|
10643
|
+
spawnDaemonDetached({
|
|
10644
|
+
project: currentProjectName,
|
|
10645
|
+
projectPath: opts.scanPath,
|
|
10646
|
+
ports: allocated
|
|
10647
|
+
});
|
|
10648
|
+
const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
|
|
10649
|
+
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
10650
|
+
if (!ready.ready) {
|
|
10651
|
+
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
10652
|
+
if (ready.stillBootstrapping.length > 0) {
|
|
10653
|
+
console.error(`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`);
|
|
10654
|
+
}
|
|
10655
|
+
if (ready.brokenProjects.length > 0) {
|
|
10656
|
+
console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
|
|
10657
|
+
}
|
|
10658
|
+
result.exitCode = 1;
|
|
10659
|
+
return result;
|
|
10660
|
+
}
|
|
10661
|
+
if (ready.brokenProjects.length > 0) {
|
|
10662
|
+
console.warn(
|
|
10663
|
+
`neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
|
|
10664
|
+
);
|
|
10665
|
+
}
|
|
10666
|
+
}
|
|
10667
|
+
} catch (err) {
|
|
10668
|
+
console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
|
|
10669
|
+
result.exitCode = 1;
|
|
10670
|
+
return result;
|
|
10671
|
+
} finally {
|
|
10672
|
+
await release();
|
|
10355
10673
|
}
|
|
10356
|
-
result.exitCode = 1;
|
|
10357
|
-
return result;
|
|
10358
|
-
}
|
|
10359
|
-
if (ready.brokenProjects.length > 0) {
|
|
10360
|
-
console.warn(
|
|
10361
|
-
`neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
|
|
10362
|
-
);
|
|
10363
10674
|
}
|
|
10364
10675
|
}
|
|
10365
|
-
const
|
|
10676
|
+
const webPort = allocated?.web ?? NEAT_PORTS[2];
|
|
10677
|
+
const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
|
|
10366
10678
|
if (opts.noOpen || !process.stdout.isTTY) {
|
|
10367
10679
|
result.steps.browser = "skipped";
|
|
10368
10680
|
} else {
|
|
@@ -10397,7 +10709,7 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
10397
10709
|
|
|
10398
10710
|
// src/cli-verbs.ts
|
|
10399
10711
|
init_cjs_shims();
|
|
10400
|
-
var
|
|
10712
|
+
var import_node_path51 = __toESM(require("path"), 1);
|
|
10401
10713
|
|
|
10402
10714
|
// src/cli-client.ts
|
|
10403
10715
|
init_cjs_shims();
|
|
@@ -10426,10 +10738,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
10426
10738
|
const root = baseUrl.replace(/\/$/, "");
|
|
10427
10739
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
10428
10740
|
return {
|
|
10429
|
-
async get(
|
|
10741
|
+
async get(path53) {
|
|
10430
10742
|
let res;
|
|
10431
10743
|
try {
|
|
10432
|
-
res = await fetch(`${root}${
|
|
10744
|
+
res = await fetch(`${root}${path53}`, {
|
|
10433
10745
|
headers: { ...authHeader }
|
|
10434
10746
|
});
|
|
10435
10747
|
} catch (err) {
|
|
@@ -10441,16 +10753,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
10441
10753
|
const body = await res.text().catch(() => "");
|
|
10442
10754
|
throw new HttpError(
|
|
10443
10755
|
res.status,
|
|
10444
|
-
`${res.status} ${res.statusText} on GET ${
|
|
10756
|
+
`${res.status} ${res.statusText} on GET ${path53}: ${body}`,
|
|
10445
10757
|
body
|
|
10446
10758
|
);
|
|
10447
10759
|
}
|
|
10448
10760
|
return await res.json();
|
|
10449
10761
|
},
|
|
10450
|
-
async post(
|
|
10762
|
+
async post(path53, body) {
|
|
10451
10763
|
let res;
|
|
10452
10764
|
try {
|
|
10453
|
-
res = await fetch(`${root}${
|
|
10765
|
+
res = await fetch(`${root}${path53}`, {
|
|
10454
10766
|
method: "POST",
|
|
10455
10767
|
headers: { "content-type": "application/json", ...authHeader },
|
|
10456
10768
|
body: JSON.stringify(body)
|
|
@@ -10464,7 +10776,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
10464
10776
|
const text = await res.text().catch(() => "");
|
|
10465
10777
|
throw new HttpError(
|
|
10466
10778
|
res.status,
|
|
10467
|
-
`${res.status} ${res.statusText} on POST ${
|
|
10779
|
+
`${res.status} ${res.statusText} on POST ${path53}: ${text}`,
|
|
10468
10780
|
text
|
|
10469
10781
|
);
|
|
10470
10782
|
}
|
|
@@ -10478,12 +10790,12 @@ function projectPath(project, suffix) {
|
|
|
10478
10790
|
}
|
|
10479
10791
|
async function runRootCause(client, input) {
|
|
10480
10792
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
10481
|
-
const
|
|
10793
|
+
const path53 = projectPath(
|
|
10482
10794
|
input.project,
|
|
10483
10795
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
10484
10796
|
);
|
|
10485
10797
|
try {
|
|
10486
|
-
const result = await client.get(
|
|
10798
|
+
const result = await client.get(path53);
|
|
10487
10799
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
10488
10800
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
10489
10801
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -10509,12 +10821,12 @@ async function runRootCause(client, input) {
|
|
|
10509
10821
|
}
|
|
10510
10822
|
async function runBlastRadius(client, input) {
|
|
10511
10823
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
10512
|
-
const
|
|
10824
|
+
const path53 = projectPath(
|
|
10513
10825
|
input.project,
|
|
10514
10826
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
10515
10827
|
);
|
|
10516
10828
|
try {
|
|
10517
|
-
const result = await client.get(
|
|
10829
|
+
const result = await client.get(path53);
|
|
10518
10830
|
if (result.totalAffected === 0) {
|
|
10519
10831
|
return {
|
|
10520
10832
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -10548,12 +10860,12 @@ function formatBlastEntry(n) {
|
|
|
10548
10860
|
}
|
|
10549
10861
|
async function runDependencies(client, input) {
|
|
10550
10862
|
const depth = input.depth ?? 3;
|
|
10551
|
-
const
|
|
10863
|
+
const path53 = projectPath(
|
|
10552
10864
|
input.project,
|
|
10553
10865
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
10554
10866
|
);
|
|
10555
10867
|
try {
|
|
10556
|
-
const result = await client.get(
|
|
10868
|
+
const result = await client.get(path53);
|
|
10557
10869
|
if (result.total === 0) {
|
|
10558
10870
|
return {
|
|
10559
10871
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -10634,9 +10946,9 @@ function formatDuration(ms) {
|
|
|
10634
10946
|
return `${Math.round(h / 24)}d`;
|
|
10635
10947
|
}
|
|
10636
10948
|
async function runIncidents(client, input) {
|
|
10637
|
-
const
|
|
10949
|
+
const path53 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
10638
10950
|
try {
|
|
10639
|
-
const body = await client.get(
|
|
10951
|
+
const body = await client.get(path53);
|
|
10640
10952
|
const events = body.events;
|
|
10641
10953
|
if (events.length === 0) {
|
|
10642
10954
|
return {
|
|
@@ -10926,13 +11238,13 @@ async function resolveProjectEntry(opts) {
|
|
|
10926
11238
|
const cwd = opts.cwd ?? process.cwd();
|
|
10927
11239
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
10928
11240
|
for (const entry2 of entries) {
|
|
10929
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
11241
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path51.default.sep}`)) {
|
|
10930
11242
|
return entry2;
|
|
10931
11243
|
}
|
|
10932
11244
|
}
|
|
10933
11245
|
return null;
|
|
10934
11246
|
}
|
|
10935
|
-
async function
|
|
11247
|
+
async function checkDaemonHealth(baseUrl) {
|
|
10936
11248
|
try {
|
|
10937
11249
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
10938
11250
|
signal: AbortSignal.timeout(1500)
|
|
@@ -11033,7 +11345,7 @@ async function runSync(opts) {
|
|
|
11033
11345
|
}
|
|
11034
11346
|
} else {
|
|
11035
11347
|
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
11036
|
-
const healthy = await
|
|
11348
|
+
const healthy = await checkDaemonHealth(daemonUrl);
|
|
11037
11349
|
if (healthy) {
|
|
11038
11350
|
try {
|
|
11039
11351
|
await pushSnapshotToRemote({
|
|
@@ -11107,11 +11419,15 @@ function usage() {
|
|
|
11107
11419
|
console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
|
|
11108
11420
|
console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
|
|
11109
11421
|
console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
|
|
11110
|
-
console.log(" list
|
|
11111
|
-
console.log("
|
|
11112
|
-
console.log("
|
|
11422
|
+
console.log(" list Report the daemons running on this machine (alias: ps).");
|
|
11423
|
+
console.log(" Reads ~/.neat/daemons/ and folds in any registered");
|
|
11424
|
+
console.log(" project no daemon has self-described yet.");
|
|
11425
|
+
console.log(" ps Alias of list.");
|
|
11426
|
+
console.log(" pause <name> Stop a project's daemon until it is started again.");
|
|
11427
|
+
console.log(" resume <name> Bring a paused project back; for a stopped daemon, re-run");
|
|
11428
|
+
console.log(" `neat <path>` to start it.");
|
|
11113
11429
|
console.log(" uninstall <name>");
|
|
11114
|
-
console.log("
|
|
11430
|
+
console.log(" Stop a project's daemon and retire it. Does not touch");
|
|
11115
11431
|
console.log(" neat-out/, policy.json, or any user file.");
|
|
11116
11432
|
console.log(" prune Drop registry entries whose path is gone from disk.");
|
|
11117
11433
|
console.log(" Flags: --json emit the removed list as JSON");
|
|
@@ -11308,6 +11624,14 @@ function printVersion() {
|
|
|
11308
11624
|
process.stdout.write(`${readPackageVersion()}
|
|
11309
11625
|
`);
|
|
11310
11626
|
}
|
|
11627
|
+
function formatMachineProjectRow(r) {
|
|
11628
|
+
if (r.ports) {
|
|
11629
|
+
const where = r.pid !== void 0 ? ` pid=${r.pid}` : "";
|
|
11630
|
+
return `${r.project} ${r.state} rest=${r.ports.rest} otlp=${r.ports.otlp} web=${r.ports.web} ${r.projectPath}${where}`;
|
|
11631
|
+
}
|
|
11632
|
+
const status2 = r.registryStatus ? ` (${r.registryStatus})` : "";
|
|
11633
|
+
return `${r.project} ${r.state}${status2} ${r.projectPath}`;
|
|
11634
|
+
}
|
|
11311
11635
|
function printDiscoveryReport(opts, services) {
|
|
11312
11636
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
11313
11637
|
const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
|
|
@@ -11346,7 +11670,7 @@ async function buildPatchSections(services, project) {
|
|
|
11346
11670
|
}
|
|
11347
11671
|
async function runInit(opts) {
|
|
11348
11672
|
const written = [];
|
|
11349
|
-
const stat = await
|
|
11673
|
+
const stat = await import_node_fs34.promises.stat(opts.scanPath).catch(() => null);
|
|
11350
11674
|
if (!stat || !stat.isDirectory()) {
|
|
11351
11675
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
11352
11676
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -11355,13 +11679,13 @@ async function runInit(opts) {
|
|
|
11355
11679
|
printDiscoveryReport(opts, services);
|
|
11356
11680
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
11357
11681
|
const patch = renderPatch(sections);
|
|
11358
|
-
const patchPath =
|
|
11682
|
+
const patchPath = import_node_path52.default.join(opts.scanPath, "neat.patch");
|
|
11359
11683
|
if (opts.dryRun) {
|
|
11360
|
-
await
|
|
11684
|
+
await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
|
|
11361
11685
|
written.push(patchPath);
|
|
11362
11686
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
11363
|
-
const gitignorePath =
|
|
11364
|
-
const gitignoreExists = await
|
|
11687
|
+
const gitignorePath = import_node_path52.default.join(opts.scanPath, ".gitignore");
|
|
11688
|
+
const gitignoreExists = await import_node_fs34.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
11365
11689
|
const verb = gitignoreExists ? "append" : "create";
|
|
11366
11690
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
11367
11691
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -11372,9 +11696,9 @@ async function runInit(opts) {
|
|
|
11372
11696
|
const graph = getGraph(graphKey);
|
|
11373
11697
|
const projectPaths = pathsForProject(
|
|
11374
11698
|
graphKey,
|
|
11375
|
-
|
|
11699
|
+
import_node_path52.default.join(opts.scanPath, "neat-out")
|
|
11376
11700
|
);
|
|
11377
|
-
const errorsPath =
|
|
11701
|
+
const errorsPath = import_node_path52.default.join(import_node_path52.default.dirname(opts.outPath), import_node_path52.default.basename(projectPaths.errorsPath));
|
|
11378
11702
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
11379
11703
|
await saveGraphToDisk(graph, opts.outPath);
|
|
11380
11704
|
written.push(opts.outPath);
|
|
@@ -11453,7 +11777,7 @@ async function runInit(opts) {
|
|
|
11453
11777
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
11454
11778
|
}
|
|
11455
11779
|
} else {
|
|
11456
|
-
await
|
|
11780
|
+
await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
|
|
11457
11781
|
written.push(patchPath);
|
|
11458
11782
|
}
|
|
11459
11783
|
}
|
|
@@ -11493,9 +11817,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
11493
11817
|
};
|
|
11494
11818
|
function claudeConfigPath() {
|
|
11495
11819
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
11496
|
-
if (override && override.length > 0) return
|
|
11820
|
+
if (override && override.length > 0) return import_node_path52.default.resolve(override);
|
|
11497
11821
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
11498
|
-
return
|
|
11822
|
+
return import_node_path52.default.join(home, ".claude.json");
|
|
11499
11823
|
}
|
|
11500
11824
|
async function runSkill(opts) {
|
|
11501
11825
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -11507,7 +11831,7 @@ async function runSkill(opts) {
|
|
|
11507
11831
|
const target = claudeConfigPath();
|
|
11508
11832
|
let existing = {};
|
|
11509
11833
|
try {
|
|
11510
|
-
existing = JSON.parse(await
|
|
11834
|
+
existing = JSON.parse(await import_node_fs34.promises.readFile(target, "utf8"));
|
|
11511
11835
|
} catch (err) {
|
|
11512
11836
|
if (err.code !== "ENOENT") {
|
|
11513
11837
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -11519,8 +11843,8 @@ async function runSkill(opts) {
|
|
|
11519
11843
|
...existing,
|
|
11520
11844
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
11521
11845
|
};
|
|
11522
|
-
await
|
|
11523
|
-
await
|
|
11846
|
+
await import_node_fs34.promises.mkdir(import_node_path52.default.dirname(target), { recursive: true });
|
|
11847
|
+
await import_node_fs34.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11524
11848
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
11525
11849
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
11526
11850
|
return { exitCode: 0 };
|
|
@@ -11569,12 +11893,12 @@ async function main() {
|
|
|
11569
11893
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
11570
11894
|
process.exit(2);
|
|
11571
11895
|
}
|
|
11572
|
-
const scanPath =
|
|
11896
|
+
const scanPath = import_node_path52.default.resolve(target);
|
|
11573
11897
|
const projectExplicit = parsed.project !== null;
|
|
11574
|
-
const projectName = projectExplicit ? project :
|
|
11898
|
+
const projectName = projectExplicit ? project : import_node_path52.default.basename(scanPath);
|
|
11575
11899
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
11576
|
-
const fallback = pathsForProject(projectKey,
|
|
11577
|
-
const outPath =
|
|
11900
|
+
const fallback = pathsForProject(projectKey, import_node_path52.default.join(scanPath, "neat-out")).snapshotPath;
|
|
11901
|
+
const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
11578
11902
|
const result = await runInit({
|
|
11579
11903
|
scanPath,
|
|
11580
11904
|
outPath,
|
|
@@ -11595,21 +11919,21 @@ async function main() {
|
|
|
11595
11919
|
usage();
|
|
11596
11920
|
process.exit(2);
|
|
11597
11921
|
}
|
|
11598
|
-
const scanPath =
|
|
11599
|
-
const stat = await
|
|
11922
|
+
const scanPath = import_node_path52.default.resolve(target);
|
|
11923
|
+
const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
|
|
11600
11924
|
if (!stat || !stat.isDirectory()) {
|
|
11601
11925
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
11602
11926
|
process.exit(2);
|
|
11603
11927
|
}
|
|
11604
|
-
const projectPaths = pathsForProject(project,
|
|
11605
|
-
const outPath =
|
|
11606
|
-
const errorsPath =
|
|
11607
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
11928
|
+
const projectPaths = pathsForProject(project, import_node_path52.default.join(scanPath, "neat-out"));
|
|
11929
|
+
const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
11930
|
+
const errorsPath = import_node_path52.default.resolve(
|
|
11931
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.errorsPath))
|
|
11608
11932
|
);
|
|
11609
|
-
const staleEventsPath =
|
|
11610
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
11933
|
+
const staleEventsPath = import_node_path52.default.resolve(
|
|
11934
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.staleEventsPath))
|
|
11611
11935
|
);
|
|
11612
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
11936
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path52.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
11613
11937
|
const handle = await startWatch(getGraph(project), {
|
|
11614
11938
|
scanPath,
|
|
11615
11939
|
outPath,
|
|
@@ -11636,16 +11960,14 @@ async function main() {
|
|
|
11636
11960
|
process.on("SIGINT", shutdown);
|
|
11637
11961
|
return;
|
|
11638
11962
|
}
|
|
11639
|
-
if (cmd === "list") {
|
|
11640
|
-
const
|
|
11641
|
-
if (
|
|
11642
|
-
console.log("no projects registered. run `neat init <path>` to register one.");
|
|
11963
|
+
if (cmd === "list" || cmd === "ps") {
|
|
11964
|
+
const rows = await listMachineProjects();
|
|
11965
|
+
if (rows.length === 0) {
|
|
11966
|
+
console.log("no daemons running and no projects registered. run `neat init <path>` to register one.");
|
|
11643
11967
|
return;
|
|
11644
11968
|
}
|
|
11645
|
-
for (const
|
|
11646
|
-
|
|
11647
|
-
const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
|
|
11648
|
-
console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
|
|
11969
|
+
for (const r of rows) {
|
|
11970
|
+
console.log(formatMachineProjectRow(r));
|
|
11649
11971
|
}
|
|
11650
11972
|
return;
|
|
11651
11973
|
}
|
|
@@ -11656,6 +11978,15 @@ async function main() {
|
|
|
11656
11978
|
usage();
|
|
11657
11979
|
process.exit(2);
|
|
11658
11980
|
}
|
|
11981
|
+
const daemon = await findDaemonByProject(name);
|
|
11982
|
+
if (daemon) {
|
|
11983
|
+
if (daemon.live && signalDaemonStop(daemon.record.pid)) {
|
|
11984
|
+
console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 stopped daemon pid ${daemon.record.pid}`);
|
|
11985
|
+
} else {
|
|
11986
|
+
console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 daemon was not running`);
|
|
11987
|
+
}
|
|
11988
|
+
return;
|
|
11989
|
+
}
|
|
11659
11990
|
try {
|
|
11660
11991
|
const entry2 = await setStatus(name, "paused");
|
|
11661
11992
|
console.log(`paused: ${entry2.name} (${entry2.path})`);
|
|
@@ -11672,6 +12003,15 @@ async function main() {
|
|
|
11672
12003
|
usage();
|
|
11673
12004
|
process.exit(2);
|
|
11674
12005
|
}
|
|
12006
|
+
const daemon = await findDaemonByProject(name);
|
|
12007
|
+
if (daemon) {
|
|
12008
|
+
if (daemon.live) {
|
|
12009
|
+
console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 daemon already running`);
|
|
12010
|
+
} else {
|
|
12011
|
+
console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 run \`neat ${daemon.record.projectPath}\` to start its daemon again`);
|
|
12012
|
+
}
|
|
12013
|
+
return;
|
|
12014
|
+
}
|
|
11675
12015
|
try {
|
|
11676
12016
|
const entry2 = await setStatus(name, "active");
|
|
11677
12017
|
console.log(`resumed: ${entry2.name} (${entry2.path})`);
|
|
@@ -11693,12 +12033,20 @@ async function main() {
|
|
|
11693
12033
|
usage();
|
|
11694
12034
|
process.exit(2);
|
|
11695
12035
|
}
|
|
12036
|
+
const daemon = await findDaemonByProject(name);
|
|
11696
12037
|
const removed = await removeProject(name);
|
|
11697
|
-
if (!removed) {
|
|
12038
|
+
if (!daemon && !removed) {
|
|
11698
12039
|
console.error(`neat uninstall: no project named "${name}"`);
|
|
11699
12040
|
process.exit(1);
|
|
11700
12041
|
}
|
|
11701
|
-
|
|
12042
|
+
const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
|
|
12043
|
+
if (daemon) {
|
|
12044
|
+
if (daemon.live && signalDaemonStop(daemon.record.pid)) {
|
|
12045
|
+
console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
|
|
12046
|
+
}
|
|
12047
|
+
await removeDaemonRecord(daemon.source);
|
|
12048
|
+
}
|
|
12049
|
+
console.log(`unregistered: ${name} (${projectPath2})`);
|
|
11702
12050
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
11703
12051
|
return;
|
|
11704
12052
|
}
|
|
@@ -11768,11 +12116,11 @@ async function main() {
|
|
|
11768
12116
|
process.exit(1);
|
|
11769
12117
|
}
|
|
11770
12118
|
async function tryOrchestrator(cmd, parsed) {
|
|
11771
|
-
const scanPath =
|
|
11772
|
-
const stat = await
|
|
12119
|
+
const scanPath = import_node_path52.default.resolve(cmd);
|
|
12120
|
+
const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
|
|
11773
12121
|
if (!stat || !stat.isDirectory()) return null;
|
|
11774
12122
|
const projectExplicit = parsed.project !== null;
|
|
11775
|
-
const projectName = projectExplicit ? parsed.project :
|
|
12123
|
+
const projectName = projectExplicit ? parsed.project : import_node_path52.default.basename(scanPath);
|
|
11776
12124
|
const result = await runOrchestrator({
|
|
11777
12125
|
scanPath,
|
|
11778
12126
|
project: projectName,
|