@lensmcp/cluster 1.16.12 → 1.16.13

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/basic-ssl.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CA_CERT_FILE = "davnx-local-ca.crt";
1
+ export declare const CA_CERT_FILE = "lensmcp-local-ca.crt";
2
2
  export declare function getCertificateSync(cacheDir: string, name?: string, domains?: string[]): string;
3
3
  /** Absolute path of the trust-once CA certificate inside `cacheDir`. */
4
4
  export declare function caCertPath(cacheDir: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"basic-ssl.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/basic-ssl.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,YAAY,uBAAuB,CAAC;AAGjD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAc,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM,CAoCvG;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrD"}
1
+ {"version":3,"file":"basic-ssl.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/basic-ssl.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,YAAY,yBAAyB,CAAC;AAKnD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAgB,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM,CAoCzG;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrD"}
package/basic-ssl.js CHANGED
@@ -20,7 +20,7 @@ const tslib_1 = require("tslib");
20
20
  *
21
21
  * Trust once (macOS):
22
22
  * sudo security add-trusted-cert -d -r trustRoot \
23
- * -k /Library/Keychains/System.keychain <cacheDir>/davnx-local-ca.crt
23
+ * -k /Library/Keychains/System.keychain <cacheDir>/lensmcp-local-ca.crt
24
24
  */
25
25
  const crypto = tslib_1.__importStar(require("node:crypto"));
26
26
  const fs = tslib_1.__importStar(require("node:fs"));
@@ -30,9 +30,11 @@ const LEAF_MAX_AGE_MS = 25 * 24 * 60 * 60 * 1000;
30
30
  /** CA lifetime (~10 years) and the remaining-validity floor for reuse. */
31
31
  const CA_TTL_DAYS = 3650;
32
32
  const CA_MIN_REMAINING_MS = 30 * 24 * 60 * 60 * 1000;
33
- exports.CA_CERT_FILE = 'davnx-local-ca.crt';
33
+ exports.CA_CERT_FILE = 'lensmcp-local-ca.crt';
34
+ /** The legacy (pre-rebrand) CA filename — swept on mint so a stale `davnx-local-ca.crt` never lingers. */
35
+ const LEGACY_CA_CERT_FILE = 'davnx-local-ca.crt';
34
36
  const CA_KEY_FILE = '_ca-key.pem';
35
- function getCertificateSync(cacheDir, name = 'davnx.dev', domains = []) {
37
+ function getCertificateSync(cacheDir, name = 'lensmcp.dev', domains = []) {
36
38
  // The SAN set is part of the cache key: adding a gateway hostname must
37
39
  // mint a fresh leaf, not serve a cached one that lacks the new SAN.
38
40
  const sanKey = crypto
@@ -104,8 +106,8 @@ function loadOrCreateCa(cacheDir, forge) {
104
106
  cert.validity.notAfter = new Date();
105
107
  cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + CA_TTL_DAYS);
106
108
  const attrs = [
107
- { name: 'commonName', value: 'davnx local dev CA' },
108
- { name: 'organizationName', value: 'davnx' },
109
+ { name: 'commonName', value: 'lensmcp local dev CA' },
110
+ { name: 'organizationName', value: 'lensmcp' },
109
111
  { shortName: 'OU', value: 'dev' },
110
112
  ];
111
113
  cert.setSubject(attrs);
@@ -118,6 +120,17 @@ function loadOrCreateCa(cacheDir, forge) {
118
120
  cert.sign(keys.privateKey, forge.md.sha256.create());
119
121
  fs.writeFileSync(certPath, forge.pki.certificateToPem(cert));
120
122
  fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(keys.privateKey), { mode: 0o600 });
123
+ // A freshly-minted CA invalidates every cached LEAF (they were signed by the OLD CA) — sweep them so the
124
+ // next getCertificateSync re-mints against THIS CA (otherwise the SAN-keyed leaf cache serves a stale,
125
+ // old-CA-signed cert). Also drop the legacy `davnx-local-ca.crt` so the lensmcp rebrand leaves nothing behind.
126
+ for (const f of fs.readdirSync(cacheDir)) {
127
+ if (/^_leaf-.*\.pem$/.test(f) || f === LEGACY_CA_CERT_FILE) {
128
+ try {
129
+ fs.unlinkSync(path.join(cacheDir, f));
130
+ }
131
+ catch { /* best effort */ }
132
+ }
133
+ }
121
134
  return { cert, key: keys.privateKey };
122
135
  }
123
136
  function createLeafCertificate(forge, ca, name, domains, ttlDays = 30) {
@@ -130,7 +143,7 @@ function createLeafCertificate(forge, ca, name, domains, ttlDays = 30) {
130
143
  cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + ttlDays);
131
144
  cert.setSubject([
132
145
  { name: 'commonName', value: name },
133
- { name: 'organizationName', value: 'davnx' },
146
+ { name: 'organizationName', value: 'lensmcp' },
134
147
  { shortName: 'OU', value: 'dev' },
135
148
  ]);
136
149
  cert.setIssuer(ca.cert.subject.attributes);
@@ -0,0 +1,20 @@
1
+ import type { GatewayRuntime } from './types';
2
+ import type { WorkspaceFragment } from './route-registry';
3
+ /** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
4
+ export declare function controlSocketPath(): string;
5
+ export interface ControlServerDeps {
6
+ rt: GatewayRuntime;
7
+ /** Reap a fragment's spawned devservers when it unregisters (the svcLayer's `killSpawned`). */
8
+ killService: (svc: WorkspaceFragment['services'][number], reason: string) => void;
9
+ /** Hooks so the daemon can host/tear-down a workspace's dashboard around the route merge (P3d). */
10
+ onRegister?: (fragment: WorkspaceFragment) => void;
11
+ onUnregister?: (fragment: WorkspaceFragment) => void;
12
+ }
13
+ export interface ControlServer {
14
+ socketPath: string;
15
+ close: () => void;
16
+ }
17
+ /** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
18
+ * is needed — anything that can open it is already on this machine as this user. */
19
+ export declare function startControlServer(deps: ControlServerDeps): ControlServer;
20
+ //# sourceMappingURL=control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/control.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,+GAA+G;AAC/G,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,cAAc,CAAC;IACnB,+FAA+F;IAC/F,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClF,mGAAmG;IACnG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAsBD;qFACqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA2DzE"}
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.controlSocketPath = controlSocketPath;
4
+ exports.startControlServer = startControlServer;
5
+ const tslib_1 = require("tslib");
6
+ /**
7
+ * The daemon CONTROL PLANE (planning/multi-workspace-gateway.md P3, model A). A UNIX-socket HTTP listener
8
+ * at `~/.lensmcp/control.sock` — loopback-only by nature (a filesystem socket, no TCP), so a second
9
+ * workspace's `gateway start` (P4) REGISTERS its fragment into the running `:443` daemon instead of
10
+ * fighting for the port. The daemon reconstructs the fragment via `discoverRoutes` on the second
11
+ * workspace's root (so it owns the pool controllers + spawns those devservers itself — model A), merges
12
+ * the routes live, and reaps them on unregister.
13
+ *
14
+ * POST /register { wsKey, root, projects } → discover + register + rebuild
15
+ * POST /unregister { wsKey } → unregister + reap its services + rebuild
16
+ * GET /list → the registered workspaces
17
+ */
18
+ const fs = tslib_1.__importStar(require("node:fs"));
19
+ const http = tslib_1.__importStar(require("node:http"));
20
+ const os = tslib_1.__importStar(require("node:os"));
21
+ const path = tslib_1.__importStar(require("node:path"));
22
+ const discovery_1 = require("./discovery");
23
+ /** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
24
+ function controlSocketPath() {
25
+ const home = process.env['LENSMCP_HOME'] || os.homedir();
26
+ return path.join(home, '.lensmcp', 'control.sock');
27
+ }
28
+ function readJsonBody(req) {
29
+ return new Promise((resolve, reject) => {
30
+ let raw = '';
31
+ req.on('data', (c) => {
32
+ raw += c;
33
+ if (raw.length > 1_000_000)
34
+ reject(new Error('body too large')); // bound the loopback input
35
+ });
36
+ req.on('end', () => {
37
+ try {
38
+ resolve(raw ? JSON.parse(raw) : {});
39
+ }
40
+ catch (e) {
41
+ reject(e);
42
+ }
43
+ });
44
+ req.on('error', reject);
45
+ });
46
+ }
47
+ /** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
48
+ * is needed — anything that can open it is already on this machine as this user. */
49
+ function startControlServer(deps) {
50
+ const { rt } = deps;
51
+ const socketPath = controlSocketPath();
52
+ fs.mkdirSync(path.dirname(socketPath), { recursive: true });
53
+ try {
54
+ fs.unlinkSync(socketPath);
55
+ }
56
+ catch { /* no stale socket to clear */ }
57
+ const send = (res, code, body) => {
58
+ res.writeHead(code, { 'content-type': 'application/json' });
59
+ res.end(JSON.stringify(body));
60
+ };
61
+ const server = http.createServer((req, res) => {
62
+ void (async () => {
63
+ try {
64
+ const url = (req.url ?? '').split('?')[0];
65
+ if (req.method === 'GET' && url === '/list') {
66
+ return send(res, 200, {
67
+ workspaces: rt.registry.keys().map((key) => {
68
+ const f = rt.registry.get(key);
69
+ return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0 };
70
+ }),
71
+ });
72
+ }
73
+ if (req.method === 'POST' && url === '/register') {
74
+ const body = (await readJsonBody(req));
75
+ if (!body.wsKey || !body.root)
76
+ return send(res, 400, { error: 'wsKey and root are required' });
77
+ const { routes, services } = (0, discovery_1.discoverRoutes)(body.root, body.projects ?? {}, undefined, body.wsKey);
78
+ const fragment = { wsKey: body.wsKey, root: body.root, routes, services };
79
+ rt.registry.register(fragment);
80
+ deps.onRegister?.(fragment); // hosts the dashboard + APPENDS its route to the fragment BEFORE the rebuild
81
+ rt.rebuildRoutes();
82
+ return send(res, 200, { ok: true, wsKey: body.wsKey, routes: routes.length, services: services.length, workspaces: rt.registry.keys() });
83
+ }
84
+ if (req.method === 'POST' && url === '/unregister') {
85
+ const body = (await readJsonBody(req));
86
+ if (!body.wsKey)
87
+ return send(res, 400, { error: 'wsKey is required' });
88
+ const dropped = rt.registry.unregister(body.wsKey);
89
+ if (dropped) {
90
+ for (const svc of dropped.services)
91
+ deps.killService(svc, 'workspace unregistered');
92
+ deps.onUnregister?.(dropped);
93
+ }
94
+ rt.rebuildRoutes();
95
+ return send(res, 200, { ok: true, removed: !!dropped, workspaces: rt.registry.keys() });
96
+ }
97
+ send(res, 404, { error: 'not found' });
98
+ }
99
+ catch (e) {
100
+ send(res, 500, { error: e.message });
101
+ }
102
+ })();
103
+ });
104
+ server.listen(socketPath);
105
+ return {
106
+ socketPath,
107
+ close: () => {
108
+ server.close();
109
+ try {
110
+ fs.unlinkSync(socketPath);
111
+ }
112
+ catch { /* already gone */ }
113
+ },
114
+ };
115
+ }
@@ -75,7 +75,7 @@ wsKey = '') {
75
75
  if (decl.lens)
76
76
  console.warn(`[gateway] ${name}: cluster.lens is ignored for pod (service) decls — backends are instrumented by serve-hmr.`);
77
77
  pool = { dir: sockDirFor(decl.service), socks: [], idx: -1, scannedAt: 0 };
78
- svc = { project: name, decl, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
78
+ svc = { project: name, decl, root: rootDir, wsKey, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
79
79
  services.push(svc);
80
80
  }
81
81
  else if (decl.lens) {
@@ -1,4 +1,5 @@
1
1
  import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
2
+ import type { WorkspaceFragment } from './route-registry';
2
3
  import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
3
4
  import { type Observability } from './observability';
4
5
  export interface LensChildren {
@@ -6,6 +7,10 @@ export interface LensChildren {
6
7
  /** Boot the dashboard + MCP singletons and every `lens:true` app. Pushes the
7
8
  * dashboard route onto `rt.routes` — MUST run before TLS SAN derivation. */
8
9
  bootSingletonsAndApps(): void;
10
+ /** Host a REGISTERED workspace's dashboard (P3d): spawn its dashboard child on its OWN events file + a
11
+ * distinct port, append its `lensmcp.local/<key>` route to its fragment. Returns a reaper (kills the
12
+ * child WITHOUT auto-heal respawn) for the daemon to call on unregister — or undefined if no bundle. */
13
+ hostWorkspaceDashboard(fragment: WorkspaceFragment): (() => void) | undefined;
9
14
  /** Reap all managed children (called from `stop()`). */
10
15
  reapAll(): void;
11
16
  }
@@ -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;AAI1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAQnF,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,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CAoKvH"}
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,EAAgB,MAAM,SAAS,CAAC;AAQnF,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,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA8MvH"}
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createLensChildren = createLensChildren;
4
+ const tslib_1 = require("tslib");
4
5
  /**
5
6
  * Lens-mode managed children: the dashboard + MCP SINGLETONS (cluster-wide,
6
7
  * owned by the gateway), and each `lens:true` app's Vite + browser capture.
7
8
  * EVERY managed child AUTO-HEALS — restarted with exponential backoff when it
8
9
  * exits while the gateway is up — and a dead child NEVER kills the cluster.
9
10
  */
11
+ const path = tslib_1.__importStar(require("node:path"));
10
12
  const node_child_process_1 = require("node:child_process");
11
- const manifest_1 = require("../manifest");
12
13
  const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
13
14
  const scope_1 = require("./scope");
14
15
  const workspace_registry_1 = require("./workspace-registry");
@@ -31,6 +32,12 @@ function createLensChildren(rt, obs, options) {
31
32
  // Per-project last-recycle time — the cooldown survives the kill→auto-heal→respawn (a new child handle).
32
33
  const viteRecycleAt = new Map();
33
34
  let lensSweeper;
35
+ // Per-workspace dashboard hosting (P3d): the current child by label, labels we've intentionally removed
36
+ // (so auto-heal does NOT respawn them on the reap kill), and the dashboard ports already in use (so two
37
+ // workspaces' dashboards get distinct ports).
38
+ const labeledChildren = new Map();
39
+ const removedLabels = new Set();
40
+ const usedDashboardPorts = new Set();
34
41
  // A `SpawnChild` over the gateway's managed-children array — the SAME shape `agent-dev` passes to
35
42
  // `spawnLensFrontend`. EVERY gateway-managed child AUTO-HEALS: the dashboard + MCP singletons AND each
36
43
  // `lens:true` vite app are restarted when they exit while the gateway is up, with exponential backoff.
@@ -44,6 +51,7 @@ function createLensChildren(rt, obs, options) {
44
51
  const spawnedAt = Date.now();
45
52
  const child = (0, node_child_process_1.spawn)(bin, args, { cwd: rt.root, env: { ...process.env, ...(env ?? {}) }, stdio: 'inherit' });
46
53
  lensChildren.push(child);
54
+ labeledChildren.set(label, child); // the CURRENT handle for this label (a per-workspace dashboard reap targets it)
47
55
  // Track a `lens:true` app's vite dev server (label 'vite') for the zombie recycle. Its `LENSMCP_PROJECT`
48
56
  // (set by lens-children when it spawns the frontend) names the app; a fresh respawn re-adds via this path.
49
57
  if (label === 'vite')
@@ -53,8 +61,8 @@ function createLensChildren(rt, obs, options) {
53
61
  if (idx >= 0)
54
62
  lensChildren.splice(idx, 1); // drop the dead handle so stop() won't signal it
55
63
  viteChildren.delete(child); // the auto-heal respawn re-registers a fresh handle with a new spawnedAt
56
- if (rt.stopped())
57
- return; // the gateway is shutting down — do NOT resurrect
64
+ if (rt.stopped() || removedLabels.has(label))
65
+ return; // shutting down, or this workspace unregistered — do NOT resurrect
58
66
  const heal = lensHeal.get(label) ?? { attempts: 0 };
59
67
  if (Date.now() - spawnedAt >= HEAL_HEALTHY_MS)
60
68
  heal.attempts = 0; // it was healthy → fresh budget
@@ -116,12 +124,15 @@ function createLensChildren(rt, obs, options) {
116
124
  // gateway routes pass-through (matchRoute on the prefix; the proxy never strips).
117
125
  const scope = (0, scope_1.readLensScope)(rt.root);
118
126
  const dashboardPort = options.dashboardPort ?? scope.dashboardPort;
127
+ usedDashboardPorts.add(dashboardPort); // reserve the daemon's own dashboard port so a registered workspace's differs
119
128
  const lensHost = options.lensHost ?? 'lensmcp.local';
120
129
  const lensBase = scope.basePath; // e.g. /tetros
121
130
  // Pushed BEFORE the cert SANs are derived (routes.map(r=>r.host)), so lensmcp.local
122
131
  // auto-joins the TLS SANs. Run `gateway:trust` to add it to /etc/hosts.
123
- rt.routes.push({ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${dashboardPort}` });
124
- (0, manifest_1.sortBySpecificity)(rt.routes);
132
+ // Add this workspace's dashboard route to ITS fragment (not straight to rt.routes) so it survives a
133
+ // registry rebuild when another workspace registers/unregisters, then rebuild the live merged table.
134
+ rt.registry.appendRoutes(rt.wsKey, [{ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${dashboardPort}` }]);
135
+ rt.rebuildRoutes();
125
136
  // Publish this workspace to the GLOBAL registry so the root chooser at `${lensHost}/` lists it
126
137
  // (and marks it live while this gateway runs). Best-effort — a registry write never blocks serving.
127
138
  (0, workspace_registry_1.registerWorkspace)({ key: scope.key, root: rt.root, base: lensBase, pid: process.pid, updatedAt: new Date().toISOString() });
@@ -176,6 +187,42 @@ function createLensChildren(rt, obs, options) {
176
187
  }
177
188
  startLensSweeper(); // watch the just-spawned lens vites for a stale-source-set zombie
178
189
  };
190
+ // P3d — host a REGISTERED workspace's dashboard: its OWN events file, a distinct port, and a
191
+ // `lensmcp.local/<key>` route on its fragment. The dashboard BUNDLE is the daemon's (rt.root); only the
192
+ // events file + base + port are the registered workspace's. Returns a reaper the control endpoint calls
193
+ // on unregister (marks the label removed so auto-heal won't respawn it, then kills the child).
194
+ const hostWorkspaceDashboard = (fragment) => {
195
+ const bundle = (0, lens_frontend_1.findDashboardBundle)(rt.root);
196
+ if (!bundle)
197
+ return undefined;
198
+ const scope = (0, scope_1.readLensScope)(fragment.root);
199
+ const lensHost = options.lensHost ?? 'lensmcp.local';
200
+ const lensBase = scope.basePath; // /<registered key>
201
+ let port = scope.dashboardPort;
202
+ while (usedDashboardPorts.has(port))
203
+ port += 1; // distinct from every other workspace's dashboard
204
+ usedDashboardPorts.add(port);
205
+ const label = `lens-dashboard:${fragment.wsKey}`;
206
+ const eventFile = path.join(fragment.root, '.lensmcp', 'events.jsonl'); // the registered workspace's OWN bus
207
+ removedLabels.delete(label); // a fresh registration after a prior unregister → allow auto-heal again
208
+ rt.registry.appendRoutes(fragment.wsKey, [{ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${port}` }]);
209
+ console.log(`[gateway] hosting dashboard for workspace '${fragment.wsKey}' → http://localhost:${port} (routed at https://${lensHost}${lensBase}/)`);
210
+ emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${port}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port, base: lensBase });
211
+ spawnManagedChild(label, process.execPath, [bundle], {
212
+ LENSMCP_EVENT_FILE: eventFile,
213
+ LENSMCP_DASHBOARD_PORT: String(port),
214
+ LENSMCP_DASHBOARD_BASE: lensBase,
215
+ });
216
+ return () => {
217
+ removedLabels.add(label); // stop auto-heal from resurrecting it
218
+ usedDashboardPorts.delete(port);
219
+ try {
220
+ labeledChildren.get(label)?.kill('SIGTERM');
221
+ }
222
+ catch { /* already gone */ }
223
+ labeledChildren.delete(label);
224
+ };
225
+ };
179
226
  const reapAll = () => {
180
227
  if (lensSweeper) {
181
228
  clearInterval(lensSweeper);
@@ -188,5 +235,5 @@ function createLensChildren(rt, obs, options) {
188
235
  catch { /* already gone */ }
189
236
  }
190
237
  };
191
- return { spawnManagedChild, bootSingletonsAndApps, reapAll };
238
+ return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, reapAll };
192
239
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA4CrD;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CA6PvF"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA4CrD;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CA+PvF"}
@@ -153,7 +153,9 @@ function createServiceLayer(rt, obs) {
153
153
  // childCount goes as a CLI option, NOT env: nx executes tasks via its
154
154
  // daemon, whose environment is not the gateway's — env vars get lost.
155
155
  const proc = (0, node_child_process_1.spawn)('yarn', ['nx', 'run', target, `--childCount=${min}`], {
156
- cwd: rt.root,
156
+ // The service's OWN workspace root — `rt.root` for the daemon's own workspace, the registered
157
+ // workspace's root for a second one (so ONE daemon hosts several workspaces' devservers — model (A)).
158
+ cwd: svc.root,
157
159
  detached: true,
158
160
  // NX_DAEMON=false keeps the serve task IN this spawned process group.
159
161
  // With the daemon on, Nx hands the long-running task to the persistent
@@ -162,9 +164,9 @@ function createServiceLayer(rt, obs) {
162
164
  // "idle-kill leaves live pods" leak. (--childCount as a CLI arg already
163
165
  // avoids the env-loss the daemon was relied on for.) NX_DAEMON=false also
164
166
  // means env vars DO reach the pod, so the east-west key + CA propagate.
165
- // LENSMCP_WS_KEY namespaces this service's pod sock dir ($TMPDIR/<wsKey>/<service>-devserver) so a
166
- // shared multi-workspace gateway's same-named services don't collide (planning/multi-workspace-gateway.md).
167
- env: { ...process.env, NX_DAEMON: 'false', ...(rt.wsKey ? { LENSMCP_WS_KEY: rt.wsKey } : {}), ...eastWestEnv(rt, svc.project) },
167
+ // LENSMCP_WS_KEY (the SERVICE's owning workspace) namespaces its pod sock dir
168
+ // ($TMPDIR/<wsKey>/<service>-devserver) the namespaced pool the daemon scans for this fragment.
169
+ env: { ...process.env, NX_DAEMON: 'false', ...(svc.wsKey ? { LENSMCP_WS_KEY: svc.wsKey } : {}), ...eastWestEnv(rt, svc.project) },
168
170
  stdio: ['ignore', 'pipe', 'pipe'],
169
171
  });
170
172
  svc.lastErrors = [];
@@ -0,0 +1,38 @@
1
+ import type { Route, ServiceCtl } from './types';
2
+ export interface WorkspaceFragment {
3
+ /** The workspace key (`readLensScope(root).key`). */
4
+ wsKey: string;
5
+ /** Absolute workspace root — the fragment's provenance (for the chooser + reaping). */
6
+ root: string;
7
+ /** Every route this workspace contributes (host + path mounts, internal.*, its default catch-all, and —
8
+ * appended after discovery — its `lensmcp.local/<key>` dashboard route). */
9
+ routes: Route[];
10
+ /** The pod-service controllers this workspace owns (scale-from-zero lifecycle is per-service). */
11
+ services: ServiceCtl[];
12
+ }
13
+ /** A live map of `wsKey → fragment`; `merged()` is the flat table the pipeline matches against. */
14
+ export declare class RouteRegistry {
15
+ private readonly fragments;
16
+ /** Register (or REPLACE) a workspace's fragment. Idempotent by `wsKey`. */
17
+ register(fragment: WorkspaceFragment): void;
18
+ /** Drop a workspace's fragment; returns it (so the caller can reap its services/dashboard). */
19
+ unregister(wsKey: string): WorkspaceFragment | undefined;
20
+ has(wsKey: string): boolean;
21
+ get(wsKey: string): WorkspaceFragment | undefined;
22
+ keys(): string[];
23
+ size(): number;
24
+ /** APPEND routes to an already-registered workspace's fragment (e.g. its dashboard route, added after
25
+ * discovery). No-op if the workspace isn't registered. */
26
+ appendRoutes(wsKey: string, routes: Route[]): void;
27
+ /** Every registered workspace's pod-service controllers, flattened (the daemon's full lifecycle set). */
28
+ allServices(): ServiceCtl[];
29
+ /** The merged, specificity-sorted route table (host+path ▸ host ▸ per-apex catch-all). A NEW array each
30
+ * call — the caller rebuilds `rt.routes` in place from it so the pipeline (which reads `rt.routes` fresh
31
+ * per request) picks the change up atomically. */
32
+ merged(): Route[];
33
+ }
34
+ /** Rebuild `routes` IN PLACE from the registry (mutate the SAME array the request pipeline holds, so a
35
+ * live register/unregister is picked up with no restart and no torn read — JS is single-threaded, so the
36
+ * splice+push is atomic w.r.t. request handling). */
37
+ export declare function rebuildRoutesInPlace(routes: Route[], registry: RouteRegistry): void;
38
+ //# sourceMappingURL=route-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-registry.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/route-registry.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb;iFAC6E;IAC7E,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kGAAkG;IAClG,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,mGAAmG;AACnG,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAElE,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAI3C,+FAA+F;IAC/F,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAMxD,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI3B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIjD,IAAI,IAAI,MAAM,EAAE;IAIhB,IAAI,IAAI,MAAM;IAId;+DAC2D;IAC3D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI;IAKlD,yGAAyG;IACzG,WAAW,IAAI,UAAU,EAAE;IAI3B;;uDAEmD;IACnD,MAAM,IAAI,KAAK,EAAE;CAKlB;AAED;;sDAEsD;AACtD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAInF"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RouteRegistry = void 0;
4
+ exports.rebuildRoutesInPlace = rebuildRoutesInPlace;
5
+ /**
6
+ * The live, multi-workspace ROUTE-FRAGMENT registry — the core of the shared daemon
7
+ * (planning/multi-workspace-gateway.md P3). ONE `:443` gateway holds a fragment per registered workspace
8
+ * (its routes + its pod-service controllers) and merges them into the single flat, specificity-sorted route
9
+ * table the request pipeline reads. A second workspace's `gateway start` REGISTERS its fragment; a stop
10
+ * UNREGISTERS it — the daemon rebuilds live, never restarts.
11
+ *
12
+ * For a SINGLE workspace the merged table is byte-identical to `discoverRoutes` + the dashboard route, so
13
+ * this is a no-op refactor until a second workspace registers. The per-apex `default` (see `matchRoute`)
14
+ * is what keeps two workspaces' catch-alls from shadowing each other.
15
+ */
16
+ const manifest_1 = require("../manifest");
17
+ /** A live map of `wsKey → fragment`; `merged()` is the flat table the pipeline matches against. */
18
+ class RouteRegistry {
19
+ constructor() {
20
+ this.fragments = new Map();
21
+ }
22
+ /** Register (or REPLACE) a workspace's fragment. Idempotent by `wsKey`. */
23
+ register(fragment) {
24
+ this.fragments.set(fragment.wsKey, fragment);
25
+ }
26
+ /** Drop a workspace's fragment; returns it (so the caller can reap its services/dashboard). */
27
+ unregister(wsKey) {
28
+ const f = this.fragments.get(wsKey);
29
+ this.fragments.delete(wsKey);
30
+ return f;
31
+ }
32
+ has(wsKey) {
33
+ return this.fragments.has(wsKey);
34
+ }
35
+ get(wsKey) {
36
+ return this.fragments.get(wsKey);
37
+ }
38
+ keys() {
39
+ return [...this.fragments.keys()];
40
+ }
41
+ size() {
42
+ return this.fragments.size;
43
+ }
44
+ /** APPEND routes to an already-registered workspace's fragment (e.g. its dashboard route, added after
45
+ * discovery). No-op if the workspace isn't registered. */
46
+ appendRoutes(wsKey, routes) {
47
+ const f = this.fragments.get(wsKey);
48
+ if (f)
49
+ f.routes.push(...routes);
50
+ }
51
+ /** Every registered workspace's pod-service controllers, flattened (the daemon's full lifecycle set). */
52
+ allServices() {
53
+ return [...this.fragments.values()].flatMap((f) => f.services);
54
+ }
55
+ /** The merged, specificity-sorted route table (host+path ▸ host ▸ per-apex catch-all). A NEW array each
56
+ * call — the caller rebuilds `rt.routes` in place from it so the pipeline (which reads `rt.routes` fresh
57
+ * per request) picks the change up atomically. */
58
+ merged() {
59
+ const all = [...this.fragments.values()].flatMap((f) => f.routes);
60
+ (0, manifest_1.sortBySpecificity)(all);
61
+ return all;
62
+ }
63
+ }
64
+ exports.RouteRegistry = RouteRegistry;
65
+ /** Rebuild `routes` IN PLACE from the registry (mutate the SAME array the request pipeline holds, so a
66
+ * live register/unregister is picked up with no restart and no torn read — JS is single-threaded, so the
67
+ * splice+push is atomic w.r.t. request handling). */
68
+ function rebuildRoutesInPlace(routes, registry) {
69
+ const merged = registry.merged();
70
+ routes.length = 0;
71
+ routes.push(...merged);
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAyBA,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,CAiLxB"}
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,CAqNxB"}
@@ -16,6 +16,8 @@ const http = tslib_1.__importStar(require("node:http"));
16
16
  const path = tslib_1.__importStar(require("node:path"));
17
17
  const discovery_1 = require("./discovery");
18
18
  const scope_1 = require("./scope");
19
+ const route_registry_1 = require("./route-registry");
20
+ const control_1 = require("./control");
19
21
  const workspace_registry_1 = require("./workspace-registry");
20
22
  const service_keys_1 = require("./service-keys");
21
23
  const observability_1 = require("./observability");
@@ -97,10 +99,19 @@ async function startGateway(options, context) {
97
99
  // (planning/multi-workspace-gateway.md). It flows to each service's devserver via `LENSMCP_WS_KEY` (see
98
100
  // lifecycle.ts), and `discoverRoutes` derives the matching pool dir the gateway scans.
99
101
  const wsKey = (0, scope_1.readLensScope)(context.root).key;
100
- const { routes, services } = (0, discovery_1.discoverRoutes)(context.root, context.projectsConfigurations?.projects ?? {}, undefined, wsKey);
101
- if (routes.length === 0) {
102
+ const { routes: discovered, services } = (0, discovery_1.discoverRoutes)(context.root, context.projectsConfigurations?.projects ?? {}, undefined, wsKey);
103
+ if (discovered.length === 0) {
102
104
  throw new Error('[gateway] no project declares a `cluster` field — nothing to route.');
103
105
  }
106
+ // --- the live multi-workspace route-fragment registry -----------------------
107
+ // This workspace registers its OWN fragment; `routes` is the merged table rebuilt IN PLACE from the
108
+ // registry (byte-identical to `discovered` for one workspace). A second workspace registers its fragment
109
+ // via the control endpoint and the daemon rebuilds live (planning/multi-workspace-gateway.md P3).
110
+ const registry = new route_registry_1.RouteRegistry();
111
+ registry.register({ wsKey, root: context.root, routes: discovered, services });
112
+ const routes = []; // the LIVE table the request pipeline reads (a stable ref, mutated in place)
113
+ const rebuildRoutes = () => (0, route_registry_1.rebuildRoutesInPlace)(routes, registry);
114
+ rebuildRoutes();
104
115
  // --- per-service api keys (the gateway is the trust boundary) ---------------
105
116
  const { serviceKeys, keyToProject } = (0, service_keys_1.loadServiceKeys)(context.root, services);
106
117
  // --- the shared runtime (state + knobs; stopped() is a live getter) ---------
@@ -109,6 +120,8 @@ async function startGateway(options, context) {
109
120
  wsKey,
110
121
  eventFile,
111
122
  routes,
123
+ registry,
124
+ rebuildRoutes,
112
125
  services,
113
126
  serviceKeys,
114
127
  keyToProject,
@@ -123,6 +136,31 @@ async function startGateway(options, context) {
123
136
  const obs = (0, observability_1.createObservability)(eventFile, { trafficFlushMs });
124
137
  const svcLayer = (0, lifecycle_1.createServiceLayer)(rt, obs);
125
138
  const lens = (0, lens_children_1.createLensChildren)(rt, obs, options);
139
+ // The daemon CONTROL PLANE (loopback ~/.lensmcp/control.sock): a second workspace's `gateway start`
140
+ // REGISTERS its fragment here instead of fighting for :443, and the daemon HOSTS its services (spawns
141
+ // them, model A) + its dashboard (planning/multi-workspace-gateway.md P3). Harmless for a lone gateway.
142
+ const dashboardReapers = new Map();
143
+ let controlServer;
144
+ try {
145
+ controlServer = (0, control_1.startControlServer)({
146
+ rt,
147
+ killService: (svc, reason) => svcLayer.killSpawned(svc, reason),
148
+ onRegister: (fragment) => {
149
+ // Host the registered workspace's dashboard (appends its lensmcp.local/<key> route to the fragment
150
+ // BEFORE the control handler's rebuild) and remember its reaper.
151
+ const reap = lens.hostWorkspaceDashboard(fragment);
152
+ if (reap)
153
+ dashboardReapers.set(fragment.wsKey, reap);
154
+ },
155
+ onUnregister: (fragment) => {
156
+ dashboardReapers.get(fragment.wsKey)?.();
157
+ dashboardReapers.delete(fragment.wsKey);
158
+ },
159
+ });
160
+ }
161
+ catch (e) {
162
+ console.warn('[gateway] control plane failed to start (multi-workspace register disabled):', e.message);
163
+ }
126
164
  const proxyLayer = (0, proxy_1.createProxy)(rt, obs, svcLayer);
127
165
  // Edge JWT verification (mirrors the prod gateway): when a JWKS source is configured/derivable, the dev
128
166
  // edge verifies RS256 tokens by `kid` against the IdP's published keys and REJECTS anything it can't
@@ -235,6 +273,7 @@ async function startGateway(options, context) {
235
273
  clearInterval(sweeper);
236
274
  obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
237
275
  jwtVerifier?.stop(); // clear the JWKS periodic-refresh timer
276
+ controlServer?.close(); // tear down the control plane + its socket
238
277
  (0, workspace_registry_1.unregisterWorkspace)(wsKey); // drop this workspace from the global chooser registry (graceful stop)
239
278
  for (const svc of services)
240
279
  svcLayer.killSpawned(svc, 'gateway shutdown');
@@ -11,6 +11,7 @@ import type * as stream from 'node:stream';
11
11
  import type { GatewayExecutorSchema } from '../schema';
12
12
  import type { AuthPolicy, Attrs } from '../manifest';
13
13
  import type { GatewayErrorReason } from '../gateway-errors';
14
+ import type { RouteRegistry } from './route-registry';
14
15
  export type { Attrs };
15
16
  export interface GatewayContext {
16
17
  root: string;
@@ -111,6 +112,13 @@ export interface LensFrontend {
111
112
  export interface ServiceCtl {
112
113
  project: string;
113
114
  decl: ClusterProjectDecl;
115
+ /** The owning workspace's absolute root — the `cwd` for its `nx run <svc>:serve-hmr` spawn. For the
116
+ * daemon's OWN workspace this is `rt.root`; for a registered second workspace it's THAT workspace's root
117
+ * (so one daemon can host several workspaces' devservers — planning/multi-workspace-gateway.md P3, (A)). */
118
+ root: string;
119
+ /** The owning workspace's key — passed as `LENSMCP_WS_KEY` to the spawn so the devserver writes its pods
120
+ * to `$TMPDIR/<wsKey>/<svc>-devserver` (the namespaced pool the daemon scans for this fragment). */
121
+ wsKey: string;
114
122
  pool: SockPool;
115
123
  proc?: ChildProcess;
116
124
  state: 'down' | 'starting' | 'up';
@@ -245,7 +253,14 @@ export interface GatewayRuntime {
245
253
  * each service devserver as `LENSMCP_WS_KEY`. */
246
254
  readonly wsKey: string;
247
255
  readonly eventFile: string;
256
+ /** The LIVE merged route table (mutated in place from `registry` on every register/unregister, so the
257
+ * request pipeline — which reads `rt.routes` fresh per request — picks the change up with no restart). */
248
258
  readonly routes: Route[];
259
+ /** The multi-workspace route-fragment registry (planning/multi-workspace-gateway.md P3). For a single
260
+ * workspace it holds ONE fragment and `routes` is byte-identical to `discoverRoutes` + the dashboard. */
261
+ readonly registry: RouteRegistry;
262
+ /** Rebuild `routes` in place from `registry` — called after a fragment register/unregister/append. */
263
+ readonly rebuildRoutes: () => void;
249
264
  readonly services: ServiceCtl[];
250
265
  readonly serviceKeys: Record<string, string>;
251
266
  readonly keyToProject: ReadonlyMap<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
@@ -39,7 +39,7 @@ async function trustExecutor(options, context) {
39
39
  return { success: true };
40
40
  }
41
41
  // --- 1) CA into the System keychain (skip when already there) -----------
42
- const present = (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'davnx local dev CA', '/Library/Keychains/System.keychain'], {
42
+ const present = (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'lensmcp local dev CA', '/Library/Keychains/System.keychain'], {
43
43
  stdio: 'ignore',
44
44
  }).status === 0;
45
45
  if (present) {
package/main.devserver.js CHANGED
@@ -674,7 +674,7 @@ else {
674
674
  const basicSsl = require('./basic-ssl');
675
675
  const certDomains = GATEWAY_ROUTES.map((r) => r.host).filter((h) => !!h);
676
676
  const certCacheDir = path.join(process.cwd(), 'node_modules', '.cache', 'davnx-webpack');
677
- gatewayPem = basicSsl.getCertificateSync(certCacheDir, SERVICE_NAME || 'davnx.dev', certDomains);
677
+ gatewayPem = basicSsl.getCertificateSync(certCacheDir, SERVICE_NAME || 'lensmcp.dev', certDomains);
678
678
  gatewayCaPath = basicSsl.caCertPath(certCacheDir);
679
679
  // eslint-disable-next-line @typescript-eslint/no-require-imports
680
680
  const httpsMod = require('node:https');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.16.12",
3
+ "version": "1.16.13",
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.12",
69
- "@lensmcp/nx-plugin": "1.16.12",
68
+ "@lensmcp/node-instrumentation": "1.16.13",
69
+ "@lensmcp/nx-plugin": "1.16.13",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",