@lensmcp/cluster 1.16.16 → 1.16.18
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/executors/gateway/runtime/lens-children.d.ts +4 -0
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -1
- package/executors/gateway/runtime/lens-children.js +95 -7
- package/executors/gateway/runtime/server.d.ts.map +1 -1
- package/executors/gateway/runtime/server.js +9 -8
- package/package.json +3 -3
|
@@ -11,6 +11,10 @@ export interface LensChildren {
|
|
|
11
11
|
* distinct port, append its `lensmcp.local/<key>` route to its fragment. Returns a reaper (kills the
|
|
12
12
|
* child WITHOUT auto-heal respawn) for the daemon to call on unregister — or undefined if no bundle. */
|
|
13
13
|
hostWorkspaceDashboard(fragment: WorkspaceFragment): (() => void) | undefined;
|
|
14
|
+
/** Host a REGISTERED workspace's `lens:true` FRONTEND apps (completes model-A "the daemon hosts
|
|
15
|
+
* everything"): spawn each app's vite (+ capture) in the workspace's OWN root + event bus, on a
|
|
16
|
+
* collision-safe port, and point its route target at that port. Returns a reaper for unregister. */
|
|
17
|
+
hostWorkspaceApps(fragment: WorkspaceFragment): () => void;
|
|
14
18
|
/** Reap all managed children (called from `stop()`). */
|
|
15
19
|
reapAll(): void;
|
|
16
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAQ1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAAC;IAC3D,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CAySvH"}
|
|
@@ -38,6 +38,10 @@ function createLensChildren(rt, obs, options) {
|
|
|
38
38
|
const labeledChildren = new Map();
|
|
39
39
|
const removedLabels = new Set();
|
|
40
40
|
const usedDashboardPorts = new Set();
|
|
41
|
+
// Lens-app vite ports already claimed (the daemon's own apps + every registered workspace's), so a
|
|
42
|
+
// registered workspace whose app declares a port already in use gets offset (vite's --strictPort would
|
|
43
|
+
// otherwise exit-loop). Keyed by the actual bound port.
|
|
44
|
+
const usedAppPorts = new Set();
|
|
41
45
|
// A `SpawnChild` over the gateway's managed-children array — the SAME shape `agent-dev` passes to
|
|
42
46
|
// `spawnLensFrontend`. EVERY gateway-managed child AUTO-HEALS: the dashboard + MCP singletons AND each
|
|
43
47
|
// `lens:true` vite app are restarted when they exit while the gateway is up, with exponential backoff.
|
|
@@ -45,16 +49,19 @@ function createLensChildren(rt, obs, options) {
|
|
|
45
49
|
// and a child that crashes-on-boot backs off to HEAL_MAX_MS rather than spinning. A child that ran
|
|
46
50
|
// healthily (≥ HEAL_HEALTHY_MS) gets a fresh budget, so one that dies after an hour comes back fast. A
|
|
47
51
|
// dead child still NEVER kills the cluster — it simply respawns in the background.
|
|
48
|
-
const spawnManagedChild = (label, bin, args, env, optional = false, respawn
|
|
52
|
+
const spawnManagedChild = (label, bin, args, env, optional = false, respawn,
|
|
53
|
+
// Per-child working dir. The daemon's OWN children run in `rt.root`; a REGISTERED workspace's vite must
|
|
54
|
+
// run in ITS root (so vite loads the right .env / resolves its own context). Threaded through auto-heal.
|
|
55
|
+
cwd) => {
|
|
49
56
|
void optional;
|
|
50
57
|
void respawn; // legacy knobs — auto-heal is now universal (signature kept for compat)
|
|
51
58
|
const spawnedAt = Date.now();
|
|
52
|
-
const child = (0, node_child_process_1.spawn)(bin, args, { cwd: rt.root, env: { ...process.env, ...(env ?? {}) }, stdio: 'inherit' });
|
|
59
|
+
const child = (0, node_child_process_1.spawn)(bin, args, { cwd: cwd ?? rt.root, env: { ...process.env, ...(env ?? {}) }, stdio: 'inherit' });
|
|
53
60
|
lensChildren.push(child);
|
|
54
61
|
labeledChildren.set(label, child); // the CURRENT handle for this label (a per-workspace dashboard reap targets it)
|
|
55
|
-
// Track a `lens:true` app's vite dev server (label 'vite'
|
|
56
|
-
//
|
|
57
|
-
if (label === 'vite')
|
|
62
|
+
// Track a `lens:true` app's vite dev server (label 'vite', or `<ws>:<proj>:vite` for a registered
|
|
63
|
+
// workspace) for the zombie recycle. Its `LENSMCP_PROJECT` names the app; a fresh respawn re-adds here.
|
|
64
|
+
if (label === 'vite' || label.endsWith(':vite'))
|
|
58
65
|
viteChildren.set(child, { project: env?.['LENSMCP_PROJECT'] ?? 'app', spawnedAt });
|
|
59
66
|
child.on('exit', (code, sig) => {
|
|
60
67
|
const idx = lensChildren.indexOf(child);
|
|
@@ -71,7 +78,7 @@ function createLensChildren(rt, obs, options) {
|
|
|
71
78
|
lensHeal.set(label, heal);
|
|
72
79
|
console.warn(`[gateway] lens child ${label} exited (code=${code} sig=${sig}); auto-healing in ${Math.round(delay / 1000)}s (attempt ${heal.attempts}).`);
|
|
73
80
|
const t = setTimeout(() => { if (!rt.stopped())
|
|
74
|
-
spawnManagedChild(label, bin, args, env, optional, respawn); }, delay);
|
|
81
|
+
spawnManagedChild(label, bin, args, env, optional, respawn, cwd); }, delay);
|
|
75
82
|
t.unref?.();
|
|
76
83
|
});
|
|
77
84
|
return child;
|
|
@@ -165,6 +172,7 @@ function createLensChildren(rt, obs, options) {
|
|
|
165
172
|
if (app.started)
|
|
166
173
|
continue;
|
|
167
174
|
app.started = true;
|
|
175
|
+
usedAppPorts.add(app.port); // claim the daemon's own lens-app ports so a registered workspace offsets off them
|
|
168
176
|
try {
|
|
169
177
|
(0, lens_frontend_1.spawnLensFrontend)({
|
|
170
178
|
projectRoot: app.projectRoot,
|
|
@@ -227,6 +235,86 @@ function createLensChildren(rt, obs, options) {
|
|
|
227
235
|
labeledChildren.delete(label);
|
|
228
236
|
};
|
|
229
237
|
};
|
|
238
|
+
// P3d — host a REGISTERED workspace's `lens:true` FRONTEND apps (the model-A completeness gap: the daemon
|
|
239
|
+
// spawned a registered workspace's pods on-demand + its dashboard, but NOT its lens frontends, so a route
|
|
240
|
+
// like app.<key>.local pointed at a port nothing bound → 502). For each lens app: spawn its vite (+ browser
|
|
241
|
+
// capture) in the registered workspace's OWN root + event bus, on a COLLISION-SAFE port (offset off the
|
|
242
|
+
// daemon's own + other workspaces', since vite's --strictPort exits on a clash), and update the route
|
|
243
|
+
// TARGET to that port so the merged table (rebuilt right after this in the control handler) upstreams
|
|
244
|
+
// correctly. Unique labels (`<ws>:<proj>:vite`) keep auto-heal + the reaper from touching the daemon's own
|
|
245
|
+
// identically-named vites. Returns a reaper the control endpoint calls on unregister.
|
|
246
|
+
const hostWorkspaceApps = (fragment) => {
|
|
247
|
+
// Group a project's (possibly several host) routes so one spawned vite serves them all.
|
|
248
|
+
const byProject = new Map();
|
|
249
|
+
for (const r of fragment.routes) {
|
|
250
|
+
if (!r.lens)
|
|
251
|
+
continue;
|
|
252
|
+
const list = byProject.get(r.lens.project) ?? [];
|
|
253
|
+
list.push(r);
|
|
254
|
+
byProject.set(r.lens.project, list);
|
|
255
|
+
}
|
|
256
|
+
if (byProject.size === 0)
|
|
257
|
+
return () => { };
|
|
258
|
+
const eventFile = path.join(fragment.root, '.lensmcp', 'events.jsonl'); // the registered workspace's OWN bus
|
|
259
|
+
const labels = [];
|
|
260
|
+
const ports = [];
|
|
261
|
+
for (const [project, routes] of byProject) {
|
|
262
|
+
const lens0 = routes[0].lens;
|
|
263
|
+
if (lens0.started)
|
|
264
|
+
continue;
|
|
265
|
+
// Collision-safe port: keep the declared port unless already claimed, else step up.
|
|
266
|
+
let port = lens0.port;
|
|
267
|
+
while (usedAppPorts.has(port))
|
|
268
|
+
port += 1;
|
|
269
|
+
usedAppPorts.add(port);
|
|
270
|
+
ports.push(port);
|
|
271
|
+
// Point every route of this project at the actual bound port (all share the one vite).
|
|
272
|
+
for (const r of routes) {
|
|
273
|
+
r.lens.started = true;
|
|
274
|
+
r.lens.port = port;
|
|
275
|
+
r.target = `http://localhost:${port}`;
|
|
276
|
+
}
|
|
277
|
+
const labelPrefix = `${fragment.wsKey}:${project}:`;
|
|
278
|
+
const viteLabel = `${labelPrefix}vite`;
|
|
279
|
+
const captureLabel = `${labelPrefix}browser-capture`;
|
|
280
|
+
labels.push(viteLabel, captureLabel);
|
|
281
|
+
removedLabels.delete(viteLabel); // a fresh registration after a prior unregister → allow auto-heal again
|
|
282
|
+
removedLabels.delete(captureLabel);
|
|
283
|
+
// A cwd-injecting spawn view: the registered workspace's children run in ITS root (not the daemon's).
|
|
284
|
+
const spawnInRoot = (label, bin, args, env, optional, respawn) => spawnManagedChild(label, bin, args, env, optional, respawn, fragment.root);
|
|
285
|
+
console.log(`[gateway] hosting lens app '${project}' of '${fragment.wsKey}' → vite :${port}`);
|
|
286
|
+
try {
|
|
287
|
+
(0, lens_frontend_1.spawnLensFrontend)({
|
|
288
|
+
projectRoot: lens0.projectRoot,
|
|
289
|
+
workspaceRoot: fragment.root,
|
|
290
|
+
project,
|
|
291
|
+
eventFile,
|
|
292
|
+
port,
|
|
293
|
+
chrome: true,
|
|
294
|
+
headless: true,
|
|
295
|
+
viteEnv: { LENSMCP_PROJECT: project },
|
|
296
|
+
labelPrefix,
|
|
297
|
+
log: (l) => console.log(l),
|
|
298
|
+
}, spawnInRoot);
|
|
299
|
+
emit('info', `lens app started: ${fragment.wsKey}/${project} (vite :${port})`, `cluster-lens-app:${fragment.wsKey}:${project}`, { kind: 'lens-app-up', project, port });
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
console.error(`[gateway] lens app ${fragment.wsKey}/${project}: ${e.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return () => {
|
|
306
|
+
for (const label of labels) {
|
|
307
|
+
removedLabels.add(label); // stop auto-heal resurrecting it
|
|
308
|
+
try {
|
|
309
|
+
labeledChildren.get(label)?.kill('SIGTERM');
|
|
310
|
+
}
|
|
311
|
+
catch { /* already gone */ }
|
|
312
|
+
labeledChildren.delete(label);
|
|
313
|
+
}
|
|
314
|
+
for (const p of ports)
|
|
315
|
+
usedAppPorts.delete(p);
|
|
316
|
+
};
|
|
317
|
+
};
|
|
230
318
|
const reapAll = () => {
|
|
231
319
|
if (lensSweeper) {
|
|
232
320
|
clearInterval(lensSweeper);
|
|
@@ -239,5 +327,5 @@ function createLensChildren(rt, obs, options) {
|
|
|
239
327
|
catch { /* already gone */ }
|
|
240
328
|
}
|
|
241
329
|
};
|
|
242
|
-
return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, reapAll };
|
|
330
|
+
return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, hostWorkspaceApps, reapAll };
|
|
243
331
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAsB,MAAM,SAAS,CAAC;AAkDxH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAsB,MAAM,SAAS,CAAC;AAkDxH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkPxB"}
|
|
@@ -150,23 +150,24 @@ async function startGateway(options, context) {
|
|
|
150
150
|
// The daemon CONTROL PLANE (loopback ~/.lensmcp/control.sock): a second workspace's `gateway start`
|
|
151
151
|
// REGISTERS its fragment here instead of fighting for :443, and the daemon HOSTS its services (spawns
|
|
152
152
|
// them, model A) + its dashboard (planning/multi-workspace-gateway.md P3). Harmless for a lone gateway.
|
|
153
|
-
const
|
|
153
|
+
const workspaceReapers = new Map();
|
|
154
154
|
let controlServer;
|
|
155
155
|
try {
|
|
156
156
|
controlServer = (0, control_1.startControlServer)({
|
|
157
157
|
rt,
|
|
158
158
|
killService: (svc, reason) => svcLayer.killSpawned(svc, reason),
|
|
159
159
|
onRegister: (fragment) => {
|
|
160
|
-
// Host the registered workspace's dashboard
|
|
161
|
-
//
|
|
160
|
+
// Host the registered workspace's dashboard AND its lens FRONTEND apps (both spawned in ITS root/bus;
|
|
161
|
+
// the dashboard appends its lensmcp.local/<key> route + the apps rewrite their route targets to the
|
|
162
|
+
// actual bound port — all BEFORE the control handler's rebuild). The cert re-mint happens in
|
|
162
163
|
// rebuildRoutes (called by the control handler right after this), once the full table is live.
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
const reapDash = lens.hostWorkspaceDashboard(fragment);
|
|
165
|
+
const reapApps = lens.hostWorkspaceApps(fragment);
|
|
166
|
+
workspaceReapers.set(fragment.wsKey, () => { reapDash?.(); reapApps(); });
|
|
166
167
|
},
|
|
167
168
|
onUnregister: (fragment) => {
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
workspaceReapers.get(fragment.wsKey)?.();
|
|
170
|
+
workspaceReapers.delete(fragment.wsKey);
|
|
170
171
|
},
|
|
171
172
|
});
|
|
172
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.18",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/node-instrumentation": "1.16.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.16.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.18",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.18",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|