@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/neatd.cjs
CHANGED
|
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
|
|
|
473
473
|
for (const s of spans) projectQueue.push({ project, span: s });
|
|
474
474
|
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
475
475
|
};
|
|
476
|
+
const offersProjectRouting = opts.onProjectSpan !== void 0;
|
|
476
477
|
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
477
478
|
function warnLegacyEndpoint(serviceName) {
|
|
479
|
+
if (!offersProjectRouting) return;
|
|
478
480
|
if (legacyEndpointWarned.has(serviceName)) return;
|
|
479
481
|
legacyEndpointWarned.add(serviceName);
|
|
480
482
|
console.warn(
|
|
@@ -592,14 +594,15 @@ var init_otel = __esm({
|
|
|
592
594
|
|
|
593
595
|
// src/neatd.ts
|
|
594
596
|
init_cjs_shims();
|
|
595
|
-
var
|
|
597
|
+
var import_node_fs27 = require("fs");
|
|
596
598
|
var import_node_path44 = __toESM(require("path"), 1);
|
|
597
|
-
var
|
|
599
|
+
var import_node_module2 = require("module");
|
|
598
600
|
|
|
599
601
|
// src/daemon.ts
|
|
600
602
|
init_cjs_shims();
|
|
601
603
|
var import_node_fs25 = require("fs");
|
|
602
604
|
var import_node_path42 = __toESM(require("path"), 1);
|
|
605
|
+
var import_node_module = require("module");
|
|
603
606
|
|
|
604
607
|
// src/graph.ts
|
|
605
608
|
init_cjs_shims();
|
|
@@ -5208,7 +5211,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
5208
5211
|
graph.clear();
|
|
5209
5212
|
graph.import(payload.graph);
|
|
5210
5213
|
}
|
|
5211
|
-
function startPersistLoop(graph, outPath,
|
|
5214
|
+
function startPersistLoop(graph, outPath, opts = {}) {
|
|
5215
|
+
const intervalMs = opts.intervalMs ?? 6e4;
|
|
5216
|
+
const exitOnSignal = opts.exitOnSignal ?? true;
|
|
5212
5217
|
let stopped = false;
|
|
5213
5218
|
const tick = async () => {
|
|
5214
5219
|
if (stopped) return;
|
|
@@ -5221,7 +5226,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5221
5226
|
const interval = setInterval(() => {
|
|
5222
5227
|
void tick();
|
|
5223
5228
|
}, intervalMs);
|
|
5224
|
-
const onSignal = (signal) => {
|
|
5229
|
+
const onSignal = exitOnSignal ? (signal) => {
|
|
5225
5230
|
void (async () => {
|
|
5226
5231
|
try {
|
|
5227
5232
|
await saveGraphToDisk(graph, outPath);
|
|
@@ -5231,14 +5236,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5231
5236
|
process.exit(0);
|
|
5232
5237
|
}
|
|
5233
5238
|
})();
|
|
5234
|
-
};
|
|
5235
|
-
|
|
5236
|
-
|
|
5239
|
+
} : null;
|
|
5240
|
+
if (onSignal) {
|
|
5241
|
+
process.on("SIGTERM", onSignal);
|
|
5242
|
+
process.on("SIGINT", onSignal);
|
|
5243
|
+
}
|
|
5237
5244
|
return () => {
|
|
5238
5245
|
stopped = true;
|
|
5239
5246
|
clearInterval(interval);
|
|
5240
|
-
|
|
5241
|
-
|
|
5247
|
+
if (onSignal) {
|
|
5248
|
+
process.off("SIGTERM", onSignal);
|
|
5249
|
+
process.off("SIGINT", onSignal);
|
|
5250
|
+
}
|
|
5242
5251
|
};
|
|
5243
5252
|
}
|
|
5244
5253
|
|
|
@@ -6231,12 +6240,16 @@ function serializeGraph(graph) {
|
|
|
6231
6240
|
});
|
|
6232
6241
|
return { nodes, edges };
|
|
6233
6242
|
}
|
|
6234
|
-
function projectFromReq(req2) {
|
|
6243
|
+
function projectFromReq(req2, singleProject) {
|
|
6235
6244
|
const params = req2.params;
|
|
6236
|
-
|
|
6245
|
+
const named = params.project;
|
|
6246
|
+
if (singleProject) {
|
|
6247
|
+
return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
|
|
6248
|
+
}
|
|
6249
|
+
return named ?? DEFAULT_PROJECT;
|
|
6237
6250
|
}
|
|
6238
|
-
function resolveProject(registry, req2, reply, bootstrap) {
|
|
6239
|
-
const name = projectFromReq(req2);
|
|
6251
|
+
function resolveProject(registry, req2, reply, bootstrap, singleProject) {
|
|
6252
|
+
const name = projectFromReq(req2, singleProject);
|
|
6240
6253
|
const ctx = registry.get(name);
|
|
6241
6254
|
if (!ctx) {
|
|
6242
6255
|
const phase = bootstrap?.status(name);
|
|
@@ -6277,13 +6290,13 @@ function buildLegacyRegistry(opts) {
|
|
|
6277
6290
|
function registerRoutes(scope, ctx) {
|
|
6278
6291
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
6279
6292
|
scope.get("/events", (req2, reply) => {
|
|
6280
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6293
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6281
6294
|
if (!proj) return;
|
|
6282
6295
|
handleSse(req2, reply, { project: proj.name });
|
|
6283
6296
|
});
|
|
6284
6297
|
if (ctx.scope === "project") {
|
|
6285
6298
|
scope.get("/health", async (req2, reply) => {
|
|
6286
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6299
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6287
6300
|
if (!proj) return;
|
|
6288
6301
|
const uptimeMs = Date.now() - startedAt;
|
|
6289
6302
|
return {
|
|
@@ -6301,14 +6314,14 @@ function registerRoutes(scope, ctx) {
|
|
|
6301
6314
|
});
|
|
6302
6315
|
}
|
|
6303
6316
|
scope.get("/graph", async (req2, reply) => {
|
|
6304
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6317
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6305
6318
|
if (!proj) return;
|
|
6306
6319
|
return serializeGraph(proj.graph);
|
|
6307
6320
|
});
|
|
6308
6321
|
scope.get(
|
|
6309
6322
|
"/graph/node/:id",
|
|
6310
6323
|
async (req2, reply) => {
|
|
6311
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6324
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6312
6325
|
if (!proj) return;
|
|
6313
6326
|
const { id } = req2.params;
|
|
6314
6327
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6320,7 +6333,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6320
6333
|
scope.get(
|
|
6321
6334
|
"/graph/edges/:id",
|
|
6322
6335
|
async (req2, reply) => {
|
|
6323
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6336
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6324
6337
|
if (!proj) return;
|
|
6325
6338
|
const { id } = req2.params;
|
|
6326
6339
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -6332,7 +6345,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6332
6345
|
}
|
|
6333
6346
|
);
|
|
6334
6347
|
scope.get("/graph/dependencies/:nodeId", async (req2, reply) => {
|
|
6335
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6348
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6336
6349
|
if (!proj) return;
|
|
6337
6350
|
const { nodeId } = req2.params;
|
|
6338
6351
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6347,7 +6360,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6347
6360
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6348
6361
|
});
|
|
6349
6362
|
scope.get("/graph/divergences", async (req2, reply) => {
|
|
6350
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6363
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6351
6364
|
if (!proj) return;
|
|
6352
6365
|
let typeFilter;
|
|
6353
6366
|
if (req2.query.type) {
|
|
@@ -6382,7 +6395,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6382
6395
|
});
|
|
6383
6396
|
});
|
|
6384
6397
|
scope.get("/incidents", async (req2, reply) => {
|
|
6385
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6398
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6386
6399
|
if (!proj) return;
|
|
6387
6400
|
const epath = errorsPathFor(proj);
|
|
6388
6401
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -6394,7 +6407,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6394
6407
|
return { count: sliced.length, total, events: sliced };
|
|
6395
6408
|
});
|
|
6396
6409
|
scope.get("/stale-events", async (req2, reply) => {
|
|
6397
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6410
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6398
6411
|
if (!proj) return;
|
|
6399
6412
|
const spath = staleEventsPathFor(proj);
|
|
6400
6413
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -6409,7 +6422,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6409
6422
|
scope.get(
|
|
6410
6423
|
"/incidents/:nodeId",
|
|
6411
6424
|
async (req2, reply) => {
|
|
6412
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6425
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6413
6426
|
if (!proj) return;
|
|
6414
6427
|
const { nodeId } = req2.params;
|
|
6415
6428
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6425,7 +6438,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6425
6438
|
}
|
|
6426
6439
|
);
|
|
6427
6440
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
6428
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6441
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6429
6442
|
if (!proj) return;
|
|
6430
6443
|
const { nodeId } = req2.params;
|
|
6431
6444
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6445,7 +6458,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6445
6458
|
return result;
|
|
6446
6459
|
});
|
|
6447
6460
|
scope.get("/graph/blast-radius/:nodeId", async (req2, reply) => {
|
|
6448
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6461
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6449
6462
|
if (!proj) return;
|
|
6450
6463
|
const { nodeId } = req2.params;
|
|
6451
6464
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -6458,7 +6471,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6458
6471
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
6459
6472
|
});
|
|
6460
6473
|
scope.get("/search", async (req2, reply) => {
|
|
6461
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6474
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6462
6475
|
if (!proj) return;
|
|
6463
6476
|
const raw = (req2.query.q ?? "").trim();
|
|
6464
6477
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -6489,7 +6502,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6489
6502
|
scope.get(
|
|
6490
6503
|
"/graph/diff",
|
|
6491
6504
|
async (req2, reply) => {
|
|
6492
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6505
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6493
6506
|
if (!proj) return;
|
|
6494
6507
|
const against = req2.query.against;
|
|
6495
6508
|
if (!against) {
|
|
@@ -6504,7 +6517,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6504
6517
|
}
|
|
6505
6518
|
);
|
|
6506
6519
|
scope.post("/snapshot", async (req2, reply) => {
|
|
6507
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6520
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6508
6521
|
if (!proj) return;
|
|
6509
6522
|
const body = req2.body;
|
|
6510
6523
|
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
@@ -6533,7 +6546,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6533
6546
|
}
|
|
6534
6547
|
});
|
|
6535
6548
|
scope.post("/graph/scan", async (req2, reply) => {
|
|
6536
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6549
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6537
6550
|
if (!proj) return;
|
|
6538
6551
|
if (!proj.scanPath) {
|
|
6539
6552
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6549,7 +6562,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6549
6562
|
};
|
|
6550
6563
|
});
|
|
6551
6564
|
scope.get("/policies", async (req2, reply) => {
|
|
6552
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6565
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6553
6566
|
if (!proj) return;
|
|
6554
6567
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
6555
6568
|
if (!policyPath) {
|
|
@@ -6566,7 +6579,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6566
6579
|
}
|
|
6567
6580
|
});
|
|
6568
6581
|
scope.get("/policies/violations", async (req2, reply) => {
|
|
6569
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6582
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6570
6583
|
if (!proj) return;
|
|
6571
6584
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6572
6585
|
let violations = await log.readAll();
|
|
@@ -6586,7 +6599,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6586
6599
|
return { violations };
|
|
6587
6600
|
});
|
|
6588
6601
|
scope.post("/policies/check", async (req2, reply) => {
|
|
6589
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6602
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6590
6603
|
if (!proj) return;
|
|
6591
6604
|
const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
6592
6605
|
if (!parsed.success) {
|
|
@@ -6622,7 +6635,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6622
6635
|
};
|
|
6623
6636
|
});
|
|
6624
6637
|
scope.get("/extend/list-uninstrumented", async (req2, reply) => {
|
|
6625
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6638
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6626
6639
|
if (!proj) return;
|
|
6627
6640
|
if (!proj.scanPath) {
|
|
6628
6641
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6635,7 +6648,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6635
6648
|
}
|
|
6636
6649
|
});
|
|
6637
6650
|
scope.get("/extend/lookup", async (req2, reply) => {
|
|
6638
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6651
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6639
6652
|
if (!proj) return;
|
|
6640
6653
|
const { library, version } = req2.query;
|
|
6641
6654
|
if (!library) {
|
|
@@ -6648,7 +6661,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6648
6661
|
return result;
|
|
6649
6662
|
});
|
|
6650
6663
|
scope.get("/extend/describe", async (req2, reply) => {
|
|
6651
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6664
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6652
6665
|
if (!proj) return;
|
|
6653
6666
|
if (!proj.scanPath) {
|
|
6654
6667
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6661,7 +6674,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6661
6674
|
}
|
|
6662
6675
|
});
|
|
6663
6676
|
scope.post("/extend/apply", async (req2, reply) => {
|
|
6664
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6677
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6665
6678
|
if (!proj) return;
|
|
6666
6679
|
if (!proj.scanPath) {
|
|
6667
6680
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6681,7 +6694,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6681
6694
|
}
|
|
6682
6695
|
});
|
|
6683
6696
|
scope.post("/extend/dry-run", async (req2, reply) => {
|
|
6684
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6697
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6685
6698
|
if (!proj) return;
|
|
6686
6699
|
if (!proj.scanPath) {
|
|
6687
6700
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6701,7 +6714,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6701
6714
|
}
|
|
6702
6715
|
});
|
|
6703
6716
|
scope.post("/extend/rollback", async (req2, reply) => {
|
|
6704
|
-
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6717
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
6705
6718
|
if (!proj) return;
|
|
6706
6719
|
if (!proj.scanPath) {
|
|
6707
6720
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -6764,7 +6777,8 @@ async function buildApi(opts) {
|
|
|
6764
6777
|
errorsPathFor,
|
|
6765
6778
|
staleEventsPathFor,
|
|
6766
6779
|
policyFilePathFor,
|
|
6767
|
-
bootstrap: opts.bootstrap
|
|
6780
|
+
bootstrap: opts.bootstrap,
|
|
6781
|
+
singleProject: opts.singleProject?.name
|
|
6768
6782
|
};
|
|
6769
6783
|
app.get("/health", async () => {
|
|
6770
6784
|
const uptimeMs = Date.now() - startedAt;
|
|
@@ -6787,10 +6801,29 @@ async function buildApi(opts) {
|
|
|
6787
6801
|
return {
|
|
6788
6802
|
ok: true,
|
|
6789
6803
|
uptimeMs,
|
|
6804
|
+
// ADR-096 §7 — a per-project daemon carries its project at the top level
|
|
6805
|
+
// so a spawn can confirm a daemon found on a reused port is serving THIS
|
|
6806
|
+
// project before reusing it. The legacy multi-project daemon omits it and
|
|
6807
|
+
// is matched against the `projects` array instead.
|
|
6808
|
+
...opts.singleProject ? { project: opts.singleProject.name } : {},
|
|
6790
6809
|
projects
|
|
6791
6810
|
};
|
|
6792
6811
|
});
|
|
6793
6812
|
app.get("/projects", async (_req, reply) => {
|
|
6813
|
+
if (opts.singleProject) {
|
|
6814
|
+
const phase = opts.bootstrap?.status(opts.singleProject.name);
|
|
6815
|
+
const entry2 = {
|
|
6816
|
+
name: opts.singleProject.name,
|
|
6817
|
+
path: opts.singleProject.path,
|
|
6818
|
+
registeredAt: new Date(startedAt).toISOString(),
|
|
6819
|
+
languages: [],
|
|
6820
|
+
// The daemon is serving this project, so it's active unless its
|
|
6821
|
+
// bootstrap broke. ('bootstrapping' still reads as active — the project
|
|
6822
|
+
// is the one to land on; its graph route answers 503 until ready.)
|
|
6823
|
+
status: phase === "broken" ? "broken" : "active"
|
|
6824
|
+
};
|
|
6825
|
+
return [entry2];
|
|
6826
|
+
}
|
|
6794
6827
|
try {
|
|
6795
6828
|
return await listProjects();
|
|
6796
6829
|
} catch (err) {
|
|
@@ -6850,6 +6883,59 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
6850
6883
|
}
|
|
6851
6884
|
|
|
6852
6885
|
// src/daemon.ts
|
|
6886
|
+
function daemonJsonPath(scanPath) {
|
|
6887
|
+
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
6888
|
+
}
|
|
6889
|
+
function daemonsDiscoveryDir(home) {
|
|
6890
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
6891
|
+
return import_node_path42.default.join(base, "daemons");
|
|
6892
|
+
}
|
|
6893
|
+
function daemonDiscoveryPath(project, home) {
|
|
6894
|
+
return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
6895
|
+
}
|
|
6896
|
+
function sanitizeDiscoveryName(project) {
|
|
6897
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
6898
|
+
}
|
|
6899
|
+
function neatHomeFromEnv() {
|
|
6900
|
+
const env = process.env.NEAT_HOME;
|
|
6901
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
6902
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6903
|
+
return import_node_path42.default.join(home, ".neat");
|
|
6904
|
+
}
|
|
6905
|
+
function resolveNeatVersion() {
|
|
6906
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
6907
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
6908
|
+
}
|
|
6909
|
+
try {
|
|
6910
|
+
const req2 = (0, import_node_module.createRequire)(importMetaUrl);
|
|
6911
|
+
const pkg = req2("../package.json");
|
|
6912
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
6913
|
+
} catch {
|
|
6914
|
+
return "0.0.0";
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
async function writeDaemonRecord(record, home) {
|
|
6918
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
6919
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
6920
|
+
try {
|
|
6921
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
6922
|
+
} catch (err) {
|
|
6923
|
+
console.warn(
|
|
6924
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
6925
|
+
);
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
async function clearDaemonRecord(record, home) {
|
|
6929
|
+
try {
|
|
6930
|
+
const stopped = { ...record, status: "stopped" };
|
|
6931
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
6932
|
+
} catch {
|
|
6933
|
+
}
|
|
6934
|
+
try {
|
|
6935
|
+
await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
6936
|
+
} catch {
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6853
6939
|
function teardownSlot(slot) {
|
|
6854
6940
|
try {
|
|
6855
6941
|
slot.stopPersist();
|
|
@@ -6931,7 +7017,7 @@ async function bootstrapProject(entry2) {
|
|
|
6931
7017
|
const detachEvents = attachGraphToEventBus(graph, { project: entry2.name });
|
|
6932
7018
|
try {
|
|
6933
7019
|
await extractFromDirectory(graph, entry2.path);
|
|
6934
|
-
const stopPersist = startPersistLoop(graph, outPath);
|
|
7020
|
+
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
6935
7021
|
await touchLastSeen(entry2.name).catch(() => {
|
|
6936
7022
|
});
|
|
6937
7023
|
return {
|
|
@@ -6966,6 +7052,23 @@ function resolveOtlpPort(opts) {
|
|
|
6966
7052
|
}
|
|
6967
7053
|
return 4318;
|
|
6968
7054
|
}
|
|
7055
|
+
function resolveWebPort() {
|
|
7056
|
+
const env = process.env.NEAT_WEB_PORT;
|
|
7057
|
+
if (env && env.length > 0) {
|
|
7058
|
+
const n = Number.parseInt(env, 10);
|
|
7059
|
+
if (Number.isFinite(n)) return n;
|
|
7060
|
+
}
|
|
7061
|
+
return 6328;
|
|
7062
|
+
}
|
|
7063
|
+
function portFromListenAddress(address, fallback) {
|
|
7064
|
+
try {
|
|
7065
|
+
const port = new URL(address).port;
|
|
7066
|
+
const n = Number.parseInt(port, 10);
|
|
7067
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
7068
|
+
} catch {
|
|
7069
|
+
}
|
|
7070
|
+
return fallback;
|
|
7071
|
+
}
|
|
6969
7072
|
function resolveHost(opts, authTokenSet) {
|
|
6970
7073
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
6971
7074
|
const env = process.env.HOST;
|
|
@@ -6976,13 +7079,24 @@ function resolveHost(opts, authTokenSet) {
|
|
|
6976
7079
|
async function startDaemon(opts = {}) {
|
|
6977
7080
|
const home = neatHomeFor(opts);
|
|
6978
7081
|
const regPath = registryPath();
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
7082
|
+
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
7083
|
+
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
7084
|
+
const singleProject = projectArg;
|
|
7085
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
|
|
7086
|
+
if (singleProject && !singleProjectPath) {
|
|
6982
7087
|
throw new Error(
|
|
6983
|
-
`neatd:
|
|
7088
|
+
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
6984
7089
|
);
|
|
6985
7090
|
}
|
|
7091
|
+
if (!singleProject) {
|
|
7092
|
+
try {
|
|
7093
|
+
await import_node_fs25.promises.access(regPath);
|
|
7094
|
+
} catch {
|
|
7095
|
+
throw new Error(
|
|
7096
|
+
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
7097
|
+
);
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
6986
7100
|
const pidPath = import_node_path42.default.join(home, "neatd.pid");
|
|
6987
7101
|
await writeAtomically(pidPath, `${process.pid}
|
|
6988
7102
|
`);
|
|
@@ -7071,21 +7185,37 @@ async function startDaemon(opts = {}) {
|
|
|
7071
7185
|
});
|
|
7072
7186
|
}
|
|
7073
7187
|
}
|
|
7188
|
+
async function enumerateProjects() {
|
|
7189
|
+
if (singleProject && singleProjectPath) {
|
|
7190
|
+
return [
|
|
7191
|
+
{
|
|
7192
|
+
name: singleProject,
|
|
7193
|
+
path: singleProjectPath,
|
|
7194
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7195
|
+
languages: [],
|
|
7196
|
+
status: "active"
|
|
7197
|
+
}
|
|
7198
|
+
];
|
|
7199
|
+
}
|
|
7200
|
+
return listProjects();
|
|
7201
|
+
}
|
|
7074
7202
|
async function loadAll() {
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7203
|
+
if (!singleProject) {
|
|
7204
|
+
try {
|
|
7205
|
+
const pruned = await pruneRegistry();
|
|
7206
|
+
for (const entry2 of pruned) {
|
|
7207
|
+
console.log(
|
|
7208
|
+
`neatd: pruned project "${entry2.name}" \u2014 registered path ${entry2.path} is gone`
|
|
7209
|
+
);
|
|
7210
|
+
slots.delete(entry2.name);
|
|
7211
|
+
bootstrapStatus.delete(entry2.name);
|
|
7212
|
+
bootstrapStartedAt.delete(entry2.name);
|
|
7213
|
+
}
|
|
7214
|
+
} catch (err) {
|
|
7215
|
+
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7084
7216
|
}
|
|
7085
|
-
} catch (err) {
|
|
7086
|
-
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7087
7217
|
}
|
|
7088
|
-
const projects = await
|
|
7218
|
+
const projects = await enumerateProjects();
|
|
7089
7219
|
const seen = /* @__PURE__ */ new Set();
|
|
7090
7220
|
const pending = [];
|
|
7091
7221
|
for (const entry2 of projects) {
|
|
@@ -7110,7 +7240,7 @@ async function startDaemon(opts = {}) {
|
|
|
7110
7240
|
}
|
|
7111
7241
|
await Promise.allSettled(pending);
|
|
7112
7242
|
}
|
|
7113
|
-
const initialEntries = await
|
|
7243
|
+
const initialEntries = await enumerateProjects().catch(() => []);
|
|
7114
7244
|
for (const entry2 of initialEntries) {
|
|
7115
7245
|
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
7116
7246
|
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
@@ -7120,6 +7250,7 @@ async function startDaemon(opts = {}) {
|
|
|
7120
7250
|
let otlpApp = null;
|
|
7121
7251
|
let restAddress = "";
|
|
7122
7252
|
let otlpAddress = "";
|
|
7253
|
+
let daemonRecord = null;
|
|
7123
7254
|
if (bind) {
|
|
7124
7255
|
const auth = readAuthEnv();
|
|
7125
7256
|
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
@@ -7142,7 +7273,13 @@ async function startDaemon(opts = {}) {
|
|
|
7142
7273
|
elapsedMs: now - (bootstrapStartedAt.get(name) ?? now)
|
|
7143
7274
|
}));
|
|
7144
7275
|
}
|
|
7145
|
-
}
|
|
7276
|
+
},
|
|
7277
|
+
// ADR-096 §4/§5/§7 — hand the daemon's identity to buildApi so the REST
|
|
7278
|
+
// surface reflects "the daemon is the project": `GET /projects` reports
|
|
7279
|
+
// only this project (the dashboard pins to it), and the daemon-wide
|
|
7280
|
+
// `/health` carries it at the top level for the spawn-reuse identity
|
|
7281
|
+
// check. Absent for the legacy multi-project daemon.
|
|
7282
|
+
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7146
7283
|
});
|
|
7147
7284
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7148
7285
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
@@ -7159,6 +7296,25 @@ async function startDaemon(opts = {}) {
|
|
|
7159
7296
|
);
|
|
7160
7297
|
}
|
|
7161
7298
|
async function resolveTargetSlot(serviceName, traceId) {
|
|
7299
|
+
if (singleProject) {
|
|
7300
|
+
let slot2 = slots.get(singleProject);
|
|
7301
|
+
if (!slot2) {
|
|
7302
|
+
slot2 = await tryRecoverSlot({
|
|
7303
|
+
name: singleProject,
|
|
7304
|
+
path: singleProjectPath,
|
|
7305
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7306
|
+
languages: [],
|
|
7307
|
+
status: "active"
|
|
7308
|
+
});
|
|
7309
|
+
} else if (slot2.status === "broken") {
|
|
7310
|
+
slot2 = await tryRecoverSlot(slot2.entry);
|
|
7311
|
+
}
|
|
7312
|
+
if (!slot2 || slot2.status !== "active") {
|
|
7313
|
+
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7314
|
+
return null;
|
|
7315
|
+
}
|
|
7316
|
+
return slot2;
|
|
7317
|
+
}
|
|
7162
7318
|
const liveEntries = await listProjects().catch(() => []);
|
|
7163
7319
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
7164
7320
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
@@ -7261,6 +7417,42 @@ async function startDaemon(opts = {}) {
|
|
|
7261
7417
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
7262
7418
|
);
|
|
7263
7419
|
}
|
|
7420
|
+
if (singleProject && singleProjectPath) {
|
|
7421
|
+
const ports = {
|
|
7422
|
+
rest: portFromListenAddress(restAddress, restPort),
|
|
7423
|
+
otlp: portFromListenAddress(otlpAddress, otlpPort),
|
|
7424
|
+
// The daemon doesn't bind the web port itself (neatd spawns the web
|
|
7425
|
+
// child); it records the allocated value passed through so the
|
|
7426
|
+
// dashboard and `neat ps` agree on where to look.
|
|
7427
|
+
web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
|
|
7428
|
+
};
|
|
7429
|
+
daemonRecord = {
|
|
7430
|
+
project: singleProject,
|
|
7431
|
+
projectPath: singleProjectPath,
|
|
7432
|
+
pid: process.pid,
|
|
7433
|
+
status: "running",
|
|
7434
|
+
ports,
|
|
7435
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7436
|
+
neatVersion: resolveNeatVersion()
|
|
7437
|
+
};
|
|
7438
|
+
try {
|
|
7439
|
+
await writeDaemonRecord(daemonRecord, home);
|
|
7440
|
+
console.log(
|
|
7441
|
+
`neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
|
|
7442
|
+
);
|
|
7443
|
+
} catch (err) {
|
|
7444
|
+
for (const slot of slots.values()) teardownSlot(slot);
|
|
7445
|
+
if (restApp) await restApp.close().catch(() => {
|
|
7446
|
+
});
|
|
7447
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
7448
|
+
});
|
|
7449
|
+
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7450
|
+
});
|
|
7451
|
+
throw new Error(
|
|
7452
|
+
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
7453
|
+
);
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7264
7456
|
}
|
|
7265
7457
|
const initialBootstrap = loadAll().catch((err) => {
|
|
7266
7458
|
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
@@ -7300,7 +7492,7 @@ async function startDaemon(opts = {}) {
|
|
|
7300
7492
|
const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
|
|
7301
7493
|
let registryWatcher = null;
|
|
7302
7494
|
let reloadTimer = null;
|
|
7303
|
-
try {
|
|
7495
|
+
if (!singleProject) try {
|
|
7304
7496
|
const regDir = import_node_path42.default.dirname(regPath);
|
|
7305
7497
|
const regBase = import_node_path42.default.basename(regPath);
|
|
7306
7498
|
registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
|
|
@@ -7340,9 +7532,18 @@ async function startDaemon(opts = {}) {
|
|
|
7340
7532
|
});
|
|
7341
7533
|
if (restApp) await restApp.close().catch(() => {
|
|
7342
7534
|
});
|
|
7535
|
+
for (const slot of slots.values()) {
|
|
7536
|
+
if (slot.status === "active" && slot.outPath) {
|
|
7537
|
+
await saveGraphToDisk(slot.graph, slot.outPath).catch(() => {
|
|
7538
|
+
});
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7343
7541
|
for (const slot of slots.values()) {
|
|
7344
7542
|
teardownSlot(slot);
|
|
7345
7543
|
}
|
|
7544
|
+
if (daemonRecord) {
|
|
7545
|
+
await clearDaemonRecord(daemonRecord, home);
|
|
7546
|
+
}
|
|
7346
7547
|
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7347
7548
|
});
|
|
7348
7549
|
};
|
|
@@ -7354,7 +7555,8 @@ async function startDaemon(opts = {}) {
|
|
|
7354
7555
|
restAddress,
|
|
7355
7556
|
otlpAddress,
|
|
7356
7557
|
bootstrap: tracker,
|
|
7357
|
-
initialBootstrap
|
|
7558
|
+
initialBootstrap,
|
|
7559
|
+
daemonRecord
|
|
7358
7560
|
};
|
|
7359
7561
|
}
|
|
7360
7562
|
|
|
@@ -7364,9 +7566,40 @@ init_auth();
|
|
|
7364
7566
|
// src/web-spawn.ts
|
|
7365
7567
|
init_cjs_shims();
|
|
7366
7568
|
var import_node_child_process2 = require("child_process");
|
|
7569
|
+
var import_node_fs26 = require("fs");
|
|
7367
7570
|
var import_node_net = __toESM(require("net"), 1);
|
|
7368
7571
|
var import_node_path43 = __toESM(require("path"), 1);
|
|
7369
7572
|
var DEFAULT_WEB_PORT = 6328;
|
|
7573
|
+
var DEFAULT_REST_PORT = 8080;
|
|
7574
|
+
function asValidPort(value) {
|
|
7575
|
+
const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
7576
|
+
return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
|
|
7577
|
+
}
|
|
7578
|
+
function projectRoot() {
|
|
7579
|
+
const fromEnv = process.env.NEAT_SCAN_PATH;
|
|
7580
|
+
return import_node_path43.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
|
|
7581
|
+
}
|
|
7582
|
+
async function readDaemonPorts(root) {
|
|
7583
|
+
try {
|
|
7584
|
+
const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(root, "neat-out", "daemon.json"), "utf8");
|
|
7585
|
+
const parsed = JSON.parse(raw);
|
|
7586
|
+
const ports = parsed?.ports ?? {};
|
|
7587
|
+
return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
|
|
7588
|
+
} catch {
|
|
7589
|
+
return { web: null, rest: null };
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
function resolveWebPorts(args) {
|
|
7593
|
+
const { daemonWeb, daemonRest, webPortEnv, apiUrlEnv, restPortArg } = args;
|
|
7594
|
+
const portFromEnv = webPortEnv && webPortEnv.length > 0 ? Number.parseInt(webPortEnv, 10) : null;
|
|
7595
|
+
if (portFromEnv !== null && (!Number.isFinite(portFromEnv) || portFromEnv <= 0 || portFromEnv > 65535)) {
|
|
7596
|
+
throw new Error(`neatd: invalid NEAT_WEB_PORT="${webPortEnv}"`);
|
|
7597
|
+
}
|
|
7598
|
+
const webPort = daemonWeb ?? portFromEnv ?? DEFAULT_WEB_PORT;
|
|
7599
|
+
const restPort = daemonRest ?? asValidPort(restPortArg) ?? DEFAULT_REST_PORT;
|
|
7600
|
+
const apiUrl = apiUrlEnv ?? `http://localhost:${restPort}`;
|
|
7601
|
+
return { webPort, apiUrl };
|
|
7602
|
+
}
|
|
7370
7603
|
async function assertPortFree(port) {
|
|
7371
7604
|
await new Promise((resolve, reject) => {
|
|
7372
7605
|
const tester = import_node_net.default.createServer();
|
|
@@ -7398,42 +7631,105 @@ function resolveWebPackageDir() {
|
|
|
7398
7631
|
function resolveStandaloneServerEntry(webDir) {
|
|
7399
7632
|
return import_node_path43.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
7400
7633
|
}
|
|
7401
|
-
async function
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7634
|
+
async function pickInternalPort() {
|
|
7635
|
+
return new Promise((resolve, reject) => {
|
|
7636
|
+
const srv = import_node_net.default.createServer();
|
|
7637
|
+
srv.once("error", reject);
|
|
7638
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
7639
|
+
const addr = srv.address();
|
|
7640
|
+
if (addr && typeof addr === "object") {
|
|
7641
|
+
const port = addr.port;
|
|
7642
|
+
srv.close(() => resolve(port));
|
|
7643
|
+
} else {
|
|
7644
|
+
srv.close(() => reject(new Error("neatd: could not pick an internal web port")));
|
|
7645
|
+
}
|
|
7646
|
+
});
|
|
7647
|
+
});
|
|
7648
|
+
}
|
|
7649
|
+
async function spawnWebUI(restPort, opts = {}) {
|
|
7650
|
+
const root = projectRoot();
|
|
7651
|
+
const fromDaemon = await readDaemonPorts(root);
|
|
7652
|
+
const { webPort: port, apiUrl } = resolveWebPorts({
|
|
7653
|
+
daemonWeb: fromDaemon.web,
|
|
7654
|
+
daemonRest: fromDaemon.rest,
|
|
7655
|
+
webPortEnv: process.env.NEAT_WEB_PORT,
|
|
7656
|
+
apiUrlEnv: process.env.NEAT_API_URL,
|
|
7657
|
+
restPortArg: restPort
|
|
7658
|
+
});
|
|
7407
7659
|
await assertPortFree(port);
|
|
7408
|
-
const
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7660
|
+
const serverEntry = opts.serverEntry ?? resolveStandaloneServerEntry(resolveWebPackageDir());
|
|
7661
|
+
if (!opts.skipBuildCheck) {
|
|
7662
|
+
try {
|
|
7663
|
+
require.resolve(serverEntry);
|
|
7664
|
+
} catch {
|
|
7665
|
+
throw new Error(
|
|
7666
|
+
`neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
|
|
7667
|
+
);
|
|
7668
|
+
}
|
|
7416
7669
|
}
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7670
|
+
let child = null;
|
|
7671
|
+
let internalPort = 0;
|
|
7672
|
+
let starting = null;
|
|
7673
|
+
let stopped = false;
|
|
7674
|
+
function ensureStarted() {
|
|
7675
|
+
if (starting) return starting;
|
|
7676
|
+
starting = (async () => {
|
|
7677
|
+
internalPort = await pickInternalPort();
|
|
7678
|
+
const env = {
|
|
7679
|
+
...process.env,
|
|
7680
|
+
PORT: String(internalPort),
|
|
7681
|
+
HOSTNAME: "127.0.0.1",
|
|
7682
|
+
NEAT_API_URL: apiUrl
|
|
7683
|
+
};
|
|
7684
|
+
child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
|
|
7685
|
+
cwd: import_node_path43.default.dirname(serverEntry),
|
|
7686
|
+
env,
|
|
7687
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
7688
|
+
detached: false
|
|
7689
|
+
});
|
|
7690
|
+
child.on("error", (err) => {
|
|
7691
|
+
console.error(`neatd: web UI spawn error \u2014 ${err.message}`);
|
|
7692
|
+
});
|
|
7693
|
+
await waitForListening(internalPort);
|
|
7694
|
+
console.log(`neatd: web UI ready on http://localhost:${port}`);
|
|
7695
|
+
})();
|
|
7696
|
+
return starting;
|
|
7697
|
+
}
|
|
7698
|
+
const liveSockets = /* @__PURE__ */ new Set();
|
|
7699
|
+
const front = import_node_net.default.createServer((socket) => {
|
|
7700
|
+
liveSockets.add(socket);
|
|
7701
|
+
socket.on("close", () => liveSockets.delete(socket));
|
|
7702
|
+
socket.on("error", () => socket.destroy());
|
|
7703
|
+
ensureStarted().then(() => {
|
|
7704
|
+
if (stopped) {
|
|
7705
|
+
socket.destroy();
|
|
7706
|
+
return;
|
|
7707
|
+
}
|
|
7708
|
+
const upstream = import_node_net.default.connect(internalPort, "127.0.0.1");
|
|
7709
|
+
upstream.on("error", () => socket.destroy());
|
|
7710
|
+
socket.pipe(upstream);
|
|
7711
|
+
upstream.pipe(socket);
|
|
7712
|
+
socket.on("close", () => upstream.destroy());
|
|
7713
|
+
upstream.on("close", () => socket.destroy());
|
|
7714
|
+
}).catch((err) => {
|
|
7715
|
+
console.error(`neatd: web UI failed to start \u2014 ${err.message}`);
|
|
7716
|
+
socket.destroy();
|
|
7717
|
+
});
|
|
7428
7718
|
});
|
|
7429
|
-
|
|
7430
|
-
|
|
7719
|
+
await new Promise((resolve, reject) => {
|
|
7720
|
+
front.once("error", reject);
|
|
7721
|
+
front.listen(port, "127.0.0.1", () => resolve());
|
|
7431
7722
|
});
|
|
7432
|
-
console.log(`neatd: web UI listening on http://localhost:${port}`);
|
|
7433
|
-
let stopped = false;
|
|
7723
|
+
console.log(`neatd: web UI listening on http://localhost:${port} (starts on first open)`);
|
|
7434
7724
|
async function stop() {
|
|
7435
|
-
if (stopped
|
|
7725
|
+
if (stopped) return;
|
|
7436
7726
|
stopped = true;
|
|
7727
|
+
await new Promise((resolve) => {
|
|
7728
|
+
front.close(() => resolve());
|
|
7729
|
+
for (const socket of liveSockets) socket.destroy();
|
|
7730
|
+
liveSockets.clear();
|
|
7731
|
+
});
|
|
7732
|
+
if (!child || !child.pid) return;
|
|
7437
7733
|
try {
|
|
7438
7734
|
child.kill("SIGTERM");
|
|
7439
7735
|
} catch {
|
|
@@ -7441,18 +7737,46 @@ async function spawnWebUI(restPort) {
|
|
|
7441
7737
|
await new Promise((resolve) => {
|
|
7442
7738
|
const t = setTimeout(() => {
|
|
7443
7739
|
try {
|
|
7444
|
-
child
|
|
7740
|
+
child?.kill("SIGKILL");
|
|
7445
7741
|
} catch {
|
|
7446
7742
|
}
|
|
7447
7743
|
resolve();
|
|
7448
7744
|
}, 3e3);
|
|
7449
|
-
child
|
|
7745
|
+
child?.once("exit", () => {
|
|
7450
7746
|
clearTimeout(t);
|
|
7451
7747
|
resolve();
|
|
7452
7748
|
});
|
|
7453
7749
|
});
|
|
7454
7750
|
}
|
|
7455
|
-
return {
|
|
7751
|
+
return {
|
|
7752
|
+
get child() {
|
|
7753
|
+
return child;
|
|
7754
|
+
},
|
|
7755
|
+
port,
|
|
7756
|
+
started: () => child !== null,
|
|
7757
|
+
stop
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
async function waitForListening(port, timeoutMs = 15e3) {
|
|
7761
|
+
const deadline = Date.now() + timeoutMs;
|
|
7762
|
+
for (; ; ) {
|
|
7763
|
+
const ok = await new Promise((resolve) => {
|
|
7764
|
+
const s = import_node_net.default.connect(port, "127.0.0.1");
|
|
7765
|
+
s.once("connect", () => {
|
|
7766
|
+
s.destroy();
|
|
7767
|
+
resolve(true);
|
|
7768
|
+
});
|
|
7769
|
+
s.once("error", () => {
|
|
7770
|
+
s.destroy();
|
|
7771
|
+
resolve(false);
|
|
7772
|
+
});
|
|
7773
|
+
});
|
|
7774
|
+
if (ok) return;
|
|
7775
|
+
if (Date.now() > deadline) {
|
|
7776
|
+
throw new Error(`neatd: web UI did not start listening on :${port} within ${timeoutMs}ms`);
|
|
7777
|
+
}
|
|
7778
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
7779
|
+
}
|
|
7456
7780
|
}
|
|
7457
7781
|
|
|
7458
7782
|
// src/version-skew.ts
|
|
@@ -7525,7 +7849,7 @@ function localVersion() {
|
|
|
7525
7849
|
return process.env.NEAT_LOCAL_VERSION;
|
|
7526
7850
|
}
|
|
7527
7851
|
try {
|
|
7528
|
-
const req2 = (0,
|
|
7852
|
+
const req2 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
7529
7853
|
const pkg = req2("../package.json");
|
|
7530
7854
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
7531
7855
|
} catch {
|
|
@@ -7541,7 +7865,7 @@ function neatHome2() {
|
|
|
7541
7865
|
}
|
|
7542
7866
|
async function readPid() {
|
|
7543
7867
|
try {
|
|
7544
|
-
const raw = await
|
|
7868
|
+
const raw = await import_node_fs27.promises.readFile(import_node_path44.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
7545
7869
|
const n = Number.parseInt(raw.trim(), 10);
|
|
7546
7870
|
return Number.isFinite(n) ? n : null;
|
|
7547
7871
|
} catch {
|
|
@@ -7555,10 +7879,19 @@ function restPortFromEnv() {
|
|
|
7555
7879
|
const raw = process.env.PORT;
|
|
7556
7880
|
return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
|
|
7557
7881
|
}
|
|
7882
|
+
function webPortFromEnv() {
|
|
7883
|
+
const raw = process.env.NEAT_WEB_PORT;
|
|
7884
|
+
if (!raw || raw.length === 0) return void 0;
|
|
7885
|
+
const n = Number.parseInt(raw, 10);
|
|
7886
|
+
return Number.isFinite(n) ? n : void 0;
|
|
7887
|
+
}
|
|
7558
7888
|
async function cmdStart() {
|
|
7889
|
+
const project = process.env.NEAT_PROJECT;
|
|
7890
|
+
const projectPath = process.env.NEAT_PROJECT_PATH;
|
|
7891
|
+
const startOpts = project && project.length > 0 ? { project, projectPath, webPort: webPortFromEnv() } : {};
|
|
7559
7892
|
let handle;
|
|
7560
7893
|
try {
|
|
7561
|
-
handle = await startDaemon();
|
|
7894
|
+
handle = await startDaemon(startOpts);
|
|
7562
7895
|
} catch (err) {
|
|
7563
7896
|
if (err instanceof BindAuthorityError) {
|
|
7564
7897
|
console.error(`neatd: ${err.message}`);
|