@lensmcp/cluster 1.18.8 → 1.18.11

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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tell a lens app's OPEN BROWSER TABS to reload when the gateway recycles its vite child.
3
+ *
4
+ * WHY THIS EXISTS. A `stale-source-set` recycle is usually CORRECT — a file was added in a bucket the app
5
+ * can import from (`sourceScopeDirs` deliberately includes every unmanaged bucket, e.g. `shared/`), so the
6
+ * running vite may not resolve it and a fresh one must re-scan. But the recycle silently ORPHANS every open
7
+ * tab: the replacement vite re-runs dependency optimization and mints a new `browserHash`, so every
8
+ * `/deps/*.js?v=<old>` baked into an already-loaded page answers `504 Outdated Optimize Dep` forever. The
9
+ * page looks fine until it lazily imports a route, which then fails with `Failed to fetch dynamically
10
+ * imported module` — and RETRYING NEVER HELPS, because only a reload rebuilds the module graph. Worked
11
+ * failure (foodguard, 2026-08-10): one tab failed on the same stale `?t=` twice, 21 minutes apart, across
12
+ * two different routes, while the dashboard vite was recycled 131 times.
13
+ *
14
+ * Vite normally handles exactly this by pushing a `full-reload` over the HMR socket when it re-optimizes.
15
+ * It cannot here: the process that owned those sockets is the one being killed, and its replacement has no
16
+ * idea who was connected. THE GATEWAY IS THE ONLY COMPONENT THAT KNOWS BOTH — it decided to recycle, and it
17
+ * proxied every HMR socket. So it sends the reload itself, immediately before the SIGTERM.
18
+ *
19
+ * The sockets are raw duplexes (node-http-proxy pipes bytes; it never parses WebSocket frames), so we
20
+ * hand-encode one server→client text frame carrying vite's own `{"type":"full-reload"}` payload. Server
21
+ * frames are UNMASKED per RFC 6455 §5.1, which keeps the encoder small.
22
+ */
23
+ import type { GatewayRuntime, HmrClientSocket } from './types';
24
+ /** Vite's own HMR payload for "throw the page away and start over" (`HMRPayload` → `FullReloadPayload`). */
25
+ export declare const VITE_FULL_RELOAD_PAYLOAD = "{\"type\":\"full-reload\"}";
26
+ /**
27
+ * Encode ONE unmasked server→client WebSocket TEXT frame (RFC 6455 §5.2). Server frames must not be masked,
28
+ * so there is no masking key and the payload is written verbatim. Pure + exported for the spec: the length
29
+ * encoding has three branches and only the first is exercised by the reload payload, so the other two are
30
+ * unit-tested rather than trusted.
31
+ */
32
+ export declare function encodeWsTextFrame(payload: string): Buffer;
33
+ /**
34
+ * Track a lens app's HMR socket so a later recycle can reach it. Self-cleaning: the socket removes itself on
35
+ * `close`/`error`, so a long-lived gateway does not accumulate dead handles (a tab is opened and closed far
36
+ * more often than a pod is recycled). No-op for a project with no lens frontend.
37
+ */
38
+ export declare function registerHmrClient(rt: GatewayRuntime, project: string, socket: HmrClientSocket): void;
39
+ /**
40
+ * Push `full-reload` to every live tab of `project`. Returns how many sockets were written, so the caller can
41
+ * log it (and a test can assert it without a browser).
42
+ *
43
+ * Called immediately BEFORE the SIGTERM, while the child is still quiet — the recycle sweeper only fires on a
44
+ * settled source-set change with no in-flight work, so we are not interleaving this frame with a half-written
45
+ * proxied one. A write failure is swallowed per-socket: a tab that vanished mid-broadcast must never take
46
+ * down the recycle (or the daemon).
47
+ */
48
+ export declare function broadcastFullReload(rt: GatewayRuntime, project: string): number;
@@ -0,0 +1 @@
1
+ "use strict";var a=Object.defineProperty;var l=(n,e)=>a(n,"name",{value:e,configurable:!0});var c=Object.defineProperty,s=l((n,e)=>c(n,"name",{value:e,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.VITE_FULL_RELOAD_PAYLOAD=void 0,exports.encodeWsTextFrame=encodeWsTextFrame,exports.registerHmrClient=registerHmrClient,exports.broadcastFullReload=broadcastFullReload,exports.VITE_FULL_RELOAD_PAYLOAD='{"type":"full-reload"}';function encodeWsTextFrame(n){const e=Buffer.from(n,"utf8"),t=e.length;if(t<126)return Buffer.concat([Buffer.from([129,t]),e]);if(t<=65535){const r=Buffer.alloc(4);return r[0]=129,r[1]=126,r.writeUInt16BE(t,2),Buffer.concat([r,e])}const o=Buffer.alloc(10);return o[0]=129,o[1]=127,o.writeBigUInt64BE(BigInt(t),2),Buffer.concat([o,e])}l(encodeWsTextFrame,"encodeWsTextFrame"),s(encodeWsTextFrame,"encodeWsTextFrame");function registerHmrClient(n,e,t){const o=n.hmrClients??=new Map;let r=o.get(e);r||o.set(e,r=new Set),r.add(t);const f=s(()=>{r.delete(t),r.size===0&&o.delete(e)},"drop");t.once("close",f),t.once("error",f)}l(registerHmrClient,"registerHmrClient"),s(registerHmrClient,"registerHmrClient");function broadcastFullReload(n,e){const t=n.hmrClients?.get(e);if(!t||t.size===0)return 0;const o=encodeWsTextFrame(exports.VITE_FULL_RELOAD_PAYLOAD);let r=0;for(const f of t)if(!f.destroyed)try{f.write(o),r+=1}catch{}return r}l(broadcastFullReload,"broadcastFullReload"),s(broadcastFullReload,"broadcastFullReload");
@@ -1 +1 @@
1
- "use strict";var J=Object.defineProperty;var L=(r,p)=>J(r,"name",{value:p,configurable:!0});var z=Object.defineProperty,i=L((r,p)=>z(r,"name",{value:p,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(r,p){return new Promise(l=>{const E=net.createServer();E.unref(),E.once("error",d=>{const h=d.code??"";l(h==="EADDRINUSE"||h==="EACCES"?"in-use":"unavailable")}),E.listen(p===void 0?{port:r}:{port:r,host:p},()=>E.close(()=>l("free")))})}L(tryBind,"tryBind"),i(tryBind,"tryBind");async function probePortFree(r){for(const p of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(r,p)==="in-use")return!1;return!0}L(probePortFree,"probePortFree"),i(probePortFree,"probePortFree");function probeConnect(r,p,l){return new Promise(E=>{let d=!1;const h=net.createConnection({host:r,port:p});h.unref();const S=setTimeout(()=>M(!1),l);S.unref?.();function M(C){d||(d=!0,clearTimeout(S),h.destroy(),E(C))}L(M,"M"),i(M,"finish"),h.once("connect",()=>M(!0)),h.once("error",()=>M(!1))})}L(probeConnect,"probeConnect"),i(probeConnect,"probeConnect");async function probePortListening(r,p=1e3){return await probeConnect("127.0.0.1",r,p)?!0:probeConnect("::1",r,p)}L(probePortListening,"probePortListening"),i(probePortListening,"probePortListening");const sleepUnref=i(r=>new Promise(p=>{setTimeout(p,r).unref?.()}),"sleepUnref");async function waitForPortFree(r,p,l={}){const E=l.isPortFree??probePortFree,d=Math.max(1,l.intervalMs??types_1.PORT_FREE_POLL_MS),h=Date.now()+Math.max(0,p);for(;;){if(await E(r))return!0;const S=h-Date.now();if(S<=0)return!1;await sleepUnref(Math.min(d,S))}}L(waitForPortFree,"waitForPortFree"),i(waitForPortFree,"waitForPortFree");function viteArgPort(r){const p=i(l=>Number.isInteger(l)&&l>0&&l<65536?l:void 0,"valid");for(let l=0;l<r.length;l+=1){const E=r[l];if(E==="--port")return p(Number(r[l+1]));if(E.startsWith("--port="))return p(Number(E.slice(7)))}}L(viteArgPort,"viteArgPort"),i(viteArgPort,"viteArgPort");function createLensChildren(r,p,l,E){const{emit:d}=p,h=E?.isPortFree??probePortFree,S=[],M=new Map,C=new Map,m=new Map;let $;const v=new Map,y=new Set,j=new Set,F=new Set,O=new Set,B=i((t,a,s)=>{if(O.has(t))return;O.add(t);const e=setInterval(()=>{(async()=>{if(r.stopped()||y.has(t)){clearInterval(e),O.delete(t);return}await h(a)&&(clearInterval(e),O.delete(t),console.log(`[gateway] lens child ${t}: :${a} freed \u2014 respawning.`),s())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);e.unref?.()},"parkUntilPortFree"),N=i(async(t,a)=>{let s=t;for(;a.has(s)||!await h(s);)s+=1;return a.add(s),s!==t&&console.warn(`[gateway] port ${t} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${s} instead.`),s},"claimFreePort"),b=i((t,a,s,e,n=!1,o,c,u)=>{const P=Date.now(),g=(0,node_child_process_1.spawn)(a,s,{cwd:c??r.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,r.root,c??r.root),...e??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});S.push(g),v.set(t,g);const A=t==="vite"||t.endsWith(":vite"),_=A?viteArgPort(s):u;if(A){const R=typeof e?.LENSMCP_EVENT_FILE=="string"?e.LENSMCP_EVENT_FILE:void 0;C.set(g,{project:e?.LENSMCP_PROJECT??"app",spawnedAt:P,port:_,probeFails:0,listening:!1,...R&&R!==r.eventFile?{bus:R}:{}})}return g.on("exit",(R,w)=>{const T=S.indexOf(g);if(T>=0&&S.splice(T,1),C.delete(g),r.stopped()||y.has(t))return;const I=Date.now()-P,f=M.get(t)??{attempts:0,portConflicts:0};(async()=>{const x=_!==void 0&&!await h(_),D=(0,lifecycle_1.isPortConflictExit)({code:R,signal:w,ranMs:I,portHeld:x},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(D&&f.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)f.portConflicts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} lost the :${_} bind race (EADDRINUSE, code=${R}); waiting for the port, then retrying (port-retry ${f.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(D){f.portConflicts=0,M.set(t,f),console.warn(`[gateway] lens child ${t}: :${_} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),d("warning",`lens child parked: ${t} (:${_} held externally)`,`cluster-lens-child:${t}`,{kind:"lens-child-parked",label:t,port:_}),B(t,_,()=>b(t,a,s,e,n,o,c,u));return}else{f.portConflicts=0,I>=HEAL_HEALTHY_MS&&(f.attempts=0);const G=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**f.attempts);f.attempts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} exited (code=${R} sig=${w}); auto-healing in ${Math.round(G/1e3)}s (attempt ${f.attempts}).`),await sleepUnref(G)}r.stopped()||y.has(t)||(_!==void 0&&!await waitForPortFree(_,types_1.PORT_FREE_WAIT_MS,{isPortFree:h,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${t}: :${_} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(r.stopped()||y.has(t))&&b(t,a,s,e,n,o,c,u))})().catch(()=>{})}),g},"spawnManagedChild");let k=!1;const H=i(async(t,a)=>{if(!k){k=!0;try{for(const[s,e]of[...C]){if(r.stopped())return;if(e.port===void 0||s.exitCode!==null||s.signalCode!==null)continue;if(await probePortListening(e.port,types_1.LENS_PROBE_TIMEOUT_MS)){e.probeFails=0,e.listening||(e.listening=!0,d("info",`lens app up: ${e.project} (vite :${e.port} listening)`,`cluster-lens-app:${e.project}`,{kind:"service-up",project:e.project,port:e.port},void 0,e.bus));continue}const n=e.listening?t.graceMs:Math.max(t.graceMs,t.neverListenedGraceMs);if(Date.now()-e.spawnedAt<n){e.probeFails=0;continue}if(!a)continue;e.probeFails+=1;const o=(0,lifecycle_1.lensChildWedged)({spawnedAt:e.spawnedAt,probeFails:e.probeFails,hasListened:e.listening},m.get(e.project)??0,Date.now(),t);if(!o)continue;const c=e.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(n/1e3)}s boot allowance`;m.set(e.project,Date.now()),e.probeFails=0,e.listening=!1,console.error(`[gateway] wedge recycle: lens app ${e.project} is ALIVE but NOT LISTENING on :${e.port} after ${Math.max(2,t.failureThreshold)} probes \u2014 ${c} \u2014 SIGKILL; a fresh vite is coming.`),d("warning",`wedge recycle: ${e.project} (${o})`,`cluster-lens-app:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o,port:e.port},void 0,e.bus);try{s.kill("SIGKILL")}catch{}}}finally{k=!1}}},"probeLensChildren"),W=i(()=>{if($||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const t={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};$=setInterval(()=>{if(r.stopped()||C.size===0||(K(),!types_1.POD_RECYCLE_ENABLED))return;const a=r.sourceSetChangedAt??0;if(a<=0)return;const s=Date.now();for(const[e,n]of C){const o=r.sourceSetChangedFor?.get(n.project)??a,c=m.get(n.project)??0;if((0,lifecycle_1.lensFrontendStale)(n.spawnedAt,c,o,s,t)){m.set(n.project,s),n.listening=!1,console.log(`[gateway] zombie recycle: lens app ${n.project} (stale-source-set) \u2014 vite missed a file add; restarting it`),d("warning",`zombie recycle: ${n.project} (stale-source-set)`,`cluster-lens-app:${n.project}`,{kind:"pod-recycle",project:n.project,reason:"stale-source-set"},void 0,n.bus);try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${n.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{e.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),$.unref?.()},"startLensSweeper"),q=i(async()=>{const t=[...new Map(r.routes.filter(e=>e.lens).map(e=>[e.project,e.lens])).values()],a=l.dashboard!==!1,s=l.mcp!==!1&&t.length>0;if(a){const e=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!e)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const n=(0,scope_1.readLensScope)(r.root),o=await N(l.dashboardPort??n.dashboardPort,j),c=l.lensHost??"lensmcp.local",u=n.basePath;r.registry.appendRoutes(r.wsKey,[{host:c,prefix:u,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),r.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:n.key,root:r.root,base:u,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${o} (routed at https://${c}${u}/)`),d("info",`lens dashboard up: ${c}${u} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:c,port:o,base:u}),b("lens-dashboard",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:u},!1,void 0,void 0,o)}}if(s){const e=(0,lens_frontend_1.findMcpBundle)(r.root);if(!e)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const n=(0,scope_1.readLensScope)(r.root),o=Number(process.env.LENSMCP_PORT??n.mcpHttpPort),c=i(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${o} (${e})`),b("lens-mcp",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(o),LENSMCP_WORKSPACE_KEY:n.key,LENSMCP_WORKSPACE_ROOT:r.root},!1,void 0,void 0,o)},"spawnMcp");await h(o)?c():(console.warn(`[gateway] lens MCP port :${o} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),d("info",`lens MCP served externally on :${o} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-mcp-external",port:o}),B("lens-mcp",o,c))}}for(const e of t){if(e.started)continue;e.started=!0;const n=await N(e.port,F);if(n!==e.port){for(const o of r.routes)o.lens?.project===e.project&&(o.lens.port=n,o.target=`http://localhost:${n}`);e.port=n}try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:r.root,project:e.project,eventFile:r.eventFile,port:e.port,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project},log:i(o=>console.log(o),"log")},b),d("info",`lens app started: ${e.project} (vite :${e.port})`,`cluster-lens-app:${e.project}`,{kind:"lens-app-up",project:e.project,port:e.port})}catch(o){console.error(`[gateway] lens app ${e.project}: ${o.message}`)}}W()},"bootSingletonsAndApps"),Y=i(async t=>{const a=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!a)return;const s=(0,scope_1.readLensScope)(t.root),e=l.lensHost??"lensmcp.local",n=s.basePath,o=await N(s.dashboardPort,j),c=`lens-dashboard:${t.wsKey}`,u=path.join(t.root,".lensmcp","events.jsonl");return y.delete(c),r.registry.appendRoutes(t.wsKey,[{host:e,prefix:n,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),(0,workspace_registry_1.registerWorkspace)({key:t.wsKey,root:t.root,base:n,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${t.wsKey}' \u2192 http://localhost:${o} (routed at https://${e}${n}/)`),d("info",`lens dashboard up: ${e}${n} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:e,port:o,base:n}),b(c,process.execPath,[a],{LENSMCP_EVENT_FILE:u,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:n,LENSMCP_WS_KEY:t.wsKey},!1,void 0,t.root,o),()=>{y.add(c),j.delete(o),(0,workspace_registry_1.unregisterWorkspace)(t.wsKey);try{v.get(c)?.kill("SIGTERM")}catch{}v.delete(c)}},"hostWorkspaceDashboard"),V=i(async t=>{const a=new Map;for(const o of t.routes){if(!o.lens)continue;const c=a.get(o.lens.project)??[];c.push(o),a.set(o.lens.project,c)}if(a.size===0)return()=>{};const s=path.join(t.root,".lensmcp","events.jsonl"),e=[],n=[];for(const[o,c]of a){const u=c[0].lens;if(u.started)continue;const P=await N(u.port,F);n.push(P);for(const w of c)w.lens.started=!0,w.lens.port=P,w.target=`http://localhost:${P}`;const g=`${t.wsKey}:${o}:`,A=`${g}vite`,_=`${g}browser-capture`;e.push(A,_),y.delete(A),y.delete(_);const R=i((w,T,I,f,x,D)=>b(w,T,I,f,x,D,t.root),"spawnInRoot");console.log(`[gateway] hosting lens app '${o}' of '${t.wsKey}' \u2192 vite :${P}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:u.projectRoot,workspaceRoot:t.root,project:o,eventFile:s,port:P,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:o},labelPrefix:g,log:i(w=>console.log(w),"log")},R),d("info",`lens app started: ${t.wsKey}/${o} (vite :${P})`,`cluster-lens-app:${t.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:P},void 0,s)}catch(w){console.error(`[gateway] lens app ${t.wsKey}/${o}: ${w.message}`)}}return()=>{for(const o of e){y.add(o);try{v.get(o)?.kill("SIGTERM")}catch{}v.delete(o)}for(const o of n)F.delete(o)}},"hostWorkspaceApps"),U=i(async(t=2500)=>{$&&(clearInterval($),$=void 0);const a=[...S];for(const n of a)try{n.kill("SIGTERM")}catch{}const s=i(n=>n.exitCode===null&&n.signalCode===null,"alive"),e=Date.now()+t;for(;Date.now()<e&&a.some(s);)await new Promise(n=>setTimeout(n,100));for(const n of a)if(s(n))try{n.kill("SIGKILL")}catch{}},"reapAll"),K=i(()=>H({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:b,bootSingletonsAndApps:q,hostWorkspaceDashboard:Y,hostWorkspaceApps:V,probeLensFrontends:K,reapAll:U}}L(createLensChildren,"createLensChildren"),i(createLensChildren,"createLensChildren");
1
+ "use strict";var J=Object.defineProperty;var L=(r,p)=>J(r,"name",{value:p,configurable:!0});var z=Object.defineProperty,i=L((r,p)=>z(r,"name",{value:p,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),hmr_reload_1=require("./hmr-reload"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(r,p){return new Promise(c=>{const E=net.createServer();E.unref(),E.once("error",u=>{const h=u.code??"";c(h==="EADDRINUSE"||h==="EACCES"?"in-use":"unavailable")}),E.listen(p===void 0?{port:r}:{port:r,host:p},()=>E.close(()=>c("free")))})}L(tryBind,"tryBind"),i(tryBind,"tryBind");async function probePortFree(r){for(const p of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(r,p)==="in-use")return!1;return!0}L(probePortFree,"probePortFree"),i(probePortFree,"probePortFree");function probeConnect(r,p,c){return new Promise(E=>{let u=!1;const h=net.createConnection({host:r,port:p});h.unref();const S=setTimeout(()=>M(!1),c);S.unref?.();function M(C){u||(u=!0,clearTimeout(S),h.destroy(),E(C))}L(M,"M"),i(M,"finish"),h.once("connect",()=>M(!0)),h.once("error",()=>M(!1))})}L(probeConnect,"probeConnect"),i(probeConnect,"probeConnect");async function probePortListening(r,p=1e3){return await probeConnect("127.0.0.1",r,p)?!0:probeConnect("::1",r,p)}L(probePortListening,"probePortListening"),i(probePortListening,"probePortListening");const sleepUnref=i(r=>new Promise(p=>{setTimeout(p,r).unref?.()}),"sleepUnref");async function waitForPortFree(r,p,c={}){const E=c.isPortFree??probePortFree,u=Math.max(1,c.intervalMs??types_1.PORT_FREE_POLL_MS),h=Date.now()+Math.max(0,p);for(;;){if(await E(r))return!0;const S=h-Date.now();if(S<=0)return!1;await sleepUnref(Math.min(u,S))}}L(waitForPortFree,"waitForPortFree"),i(waitForPortFree,"waitForPortFree");function viteArgPort(r){const p=i(c=>Number.isInteger(c)&&c>0&&c<65536?c:void 0,"valid");for(let c=0;c<r.length;c+=1){const E=r[c];if(E==="--port")return p(Number(r[c+1]));if(E.startsWith("--port="))return p(Number(E.slice(7)))}}L(viteArgPort,"viteArgPort"),i(viteArgPort,"viteArgPort");function createLensChildren(r,p,c,E){const{emit:u}=p,h=E?.isPortFree??probePortFree,S=[],M=new Map,C=new Map,A=new Map;let $;const v=new Map,y=new Set,j=new Set,F=new Set,O=new Set,B=i((t,a,s)=>{if(O.has(t))return;O.add(t);const e=setInterval(()=>{(async()=>{if(r.stopped()||y.has(t)){clearInterval(e),O.delete(t);return}await h(a)&&(clearInterval(e),O.delete(t),console.log(`[gateway] lens child ${t}: :${a} freed \u2014 respawning.`),s())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);e.unref?.()},"parkUntilPortFree"),N=i(async(t,a)=>{let s=t;for(;a.has(s)||!await h(s);)s+=1;return a.add(s),s!==t&&console.warn(`[gateway] port ${t} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${s} instead.`),s},"claimFreePort"),b=i((t,a,s,e,n=!1,o,l,d)=>{const P=Date.now(),g=(0,node_child_process_1.spawn)(a,s,{cwd:l??r.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,r.root,l??r.root),...e??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});S.push(g),v.set(t,g);const m=t==="vite"||t.endsWith(":vite"),_=m?viteArgPort(s):d;if(m){const R=typeof e?.LENSMCP_EVENT_FILE=="string"?e.LENSMCP_EVENT_FILE:void 0;C.set(g,{project:e?.LENSMCP_PROJECT??"app",spawnedAt:P,port:_,probeFails:0,listening:!1,...R&&R!==r.eventFile?{bus:R}:{}})}return g.on("exit",(R,w)=>{const T=S.indexOf(g);if(T>=0&&S.splice(T,1),C.delete(g),r.stopped()||y.has(t))return;const I=Date.now()-P,f=M.get(t)??{attempts:0,portConflicts:0};(async()=>{const x=_!==void 0&&!await h(_),D=(0,lifecycle_1.isPortConflictExit)({code:R,signal:w,ranMs:I,portHeld:x},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(D&&f.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)f.portConflicts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} lost the :${_} bind race (EADDRINUSE, code=${R}); waiting for the port, then retrying (port-retry ${f.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(D){f.portConflicts=0,M.set(t,f),console.warn(`[gateway] lens child ${t}: :${_} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),u("warning",`lens child parked: ${t} (:${_} held externally)`,`cluster-lens-child:${t}`,{kind:"lens-child-parked",label:t,port:_}),B(t,_,()=>b(t,a,s,e,n,o,l,d));return}else{f.portConflicts=0,I>=HEAL_HEALTHY_MS&&(f.attempts=0);const G=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**f.attempts);f.attempts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} exited (code=${R} sig=${w}); auto-healing in ${Math.round(G/1e3)}s (attempt ${f.attempts}).`),await sleepUnref(G)}r.stopped()||y.has(t)||(_!==void 0&&!await waitForPortFree(_,types_1.PORT_FREE_WAIT_MS,{isPortFree:h,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${t}: :${_} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(r.stopped()||y.has(t))&&b(t,a,s,e,n,o,l,d))})().catch(()=>{})}),g},"spawnManagedChild");let k=!1;const H=i(async(t,a)=>{if(!k){k=!0;try{for(const[s,e]of[...C]){if(r.stopped())return;if(e.port===void 0||s.exitCode!==null||s.signalCode!==null)continue;if(await probePortListening(e.port,types_1.LENS_PROBE_TIMEOUT_MS)){e.probeFails=0,e.listening||(e.listening=!0,u("info",`lens app up: ${e.project} (vite :${e.port} listening)`,`cluster-lens-app:${e.project}`,{kind:"service-up",project:e.project,port:e.port},void 0,e.bus));continue}const n=e.listening?t.graceMs:Math.max(t.graceMs,t.neverListenedGraceMs);if(Date.now()-e.spawnedAt<n){e.probeFails=0;continue}if(!a)continue;e.probeFails+=1;const o=(0,lifecycle_1.lensChildWedged)({spawnedAt:e.spawnedAt,probeFails:e.probeFails,hasListened:e.listening},A.get(e.project)??0,Date.now(),t);if(!o)continue;const l=e.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(n/1e3)}s boot allowance`;A.set(e.project,Date.now()),e.probeFails=0,e.listening=!1,console.error(`[gateway] wedge recycle: lens app ${e.project} is ALIVE but NOT LISTENING on :${e.port} after ${Math.max(2,t.failureThreshold)} probes \u2014 ${l} \u2014 SIGKILL; a fresh vite is coming.`),u("warning",`wedge recycle: ${e.project} (${o})`,`cluster-lens-app:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o,port:e.port},void 0,e.bus);try{s.kill("SIGKILL")}catch{}}}finally{k=!1}}},"probeLensChildren"),W=i(()=>{if($||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const t={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};$=setInterval(()=>{if(r.stopped()||C.size===0||(K(),!types_1.POD_RECYCLE_ENABLED))return;const a=r.sourceSetChangedAt??0;if(a<=0)return;const s=Date.now();for(const[e,n]of C){const o=r.sourceSetChangedFor?.get(n.project)??a,l=A.get(n.project)??0;if(!(0,lifecycle_1.lensFrontendStale)(n.spawnedAt,l,o,s,t))continue;A.set(n.project,s),n.listening=!1,console.log(`[gateway] zombie recycle: lens app ${n.project} (stale-source-set) \u2014 vite missed a file add; restarting it`),u("warning",`zombie recycle: ${n.project} (stale-source-set)`,`cluster-lens-app:${n.project}`,{kind:"pod-recycle",project:n.project,reason:"stale-source-set"},void 0,n.bus);const d=(0,hmr_reload_1.broadcastFullReload)(r,n.project);d>0&&console.log(`[gateway] \u2192 told ${d} open tab(s) to reload (stale optimize-dep generation)`);try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${n.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{e.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),$.unref?.()},"startLensSweeper"),q=i(async()=>{const t=[...new Map(r.routes.filter(e=>e.lens).map(e=>[e.project,e.lens])).values()],a=c.dashboard!==!1,s=c.mcp!==!1&&t.length>0;if(a){const e=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!e)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const n=(0,scope_1.readLensScope)(r.root),o=await N(c.dashboardPort??n.dashboardPort,j),l=c.lensHost??"lensmcp.local",d=n.basePath;r.registry.appendRoutes(r.wsKey,[{host:l,prefix:d,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),r.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:n.key,root:r.root,base:d,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${o} (routed at https://${l}${d}/)`),u("info",`lens dashboard up: ${l}${d} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:l,port:o,base:d}),b("lens-dashboard",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:d},!1,void 0,void 0,o)}}if(s){const e=(0,lens_frontend_1.findMcpBundle)(r.root);if(!e)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const n=(0,scope_1.readLensScope)(r.root),o=Number(process.env.LENSMCP_PORT??n.mcpHttpPort),l=i(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${o} (${e})`),b("lens-mcp",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(o),LENSMCP_WORKSPACE_KEY:n.key,LENSMCP_WORKSPACE_ROOT:r.root},!1,void 0,void 0,o)},"spawnMcp");await h(o)?l():(console.warn(`[gateway] lens MCP port :${o} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),u("info",`lens MCP served externally on :${o} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-mcp-external",port:o}),B("lens-mcp",o,l))}}for(const e of t){if(e.started)continue;e.started=!0;const n=await N(e.port,F);if(n!==e.port){for(const o of r.routes)o.lens?.project===e.project&&(o.lens.port=n,o.target=`http://localhost:${n}`);e.port=n}try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:r.root,project:e.project,eventFile:r.eventFile,port:e.port,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project},log:i(o=>console.log(o),"log")},b),u("info",`lens app started: ${e.project} (vite :${e.port})`,`cluster-lens-app:${e.project}`,{kind:"lens-app-up",project:e.project,port:e.port})}catch(o){console.error(`[gateway] lens app ${e.project}: ${o.message}`)}}W()},"bootSingletonsAndApps"),Y=i(async t=>{const a=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!a)return;const s=(0,scope_1.readLensScope)(t.root),e=c.lensHost??"lensmcp.local",n=s.basePath,o=await N(s.dashboardPort,j),l=`lens-dashboard:${t.wsKey}`,d=path.join(t.root,".lensmcp","events.jsonl");return y.delete(l),r.registry.appendRoutes(t.wsKey,[{host:e,prefix:n,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),(0,workspace_registry_1.registerWorkspace)({key:t.wsKey,root:t.root,base:n,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${t.wsKey}' \u2192 http://localhost:${o} (routed at https://${e}${n}/)`),u("info",`lens dashboard up: ${e}${n} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:e,port:o,base:n}),b(l,process.execPath,[a],{LENSMCP_EVENT_FILE:d,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:n,LENSMCP_WS_KEY:t.wsKey},!1,void 0,t.root,o),()=>{y.add(l),j.delete(o),(0,workspace_registry_1.unregisterWorkspace)(t.wsKey);try{v.get(l)?.kill("SIGTERM")}catch{}v.delete(l)}},"hostWorkspaceDashboard"),V=i(async t=>{const a=new Map;for(const o of t.routes){if(!o.lens)continue;const l=a.get(o.lens.project)??[];l.push(o),a.set(o.lens.project,l)}if(a.size===0)return()=>{};const s=path.join(t.root,".lensmcp","events.jsonl"),e=[],n=[];for(const[o,l]of a){const d=l[0].lens;if(d.started)continue;const P=await N(d.port,F);n.push(P);for(const w of l)w.lens.started=!0,w.lens.port=P,w.target=`http://localhost:${P}`;const g=`${t.wsKey}:${o}:`,m=`${g}vite`,_=`${g}browser-capture`;e.push(m,_),y.delete(m),y.delete(_);const R=i((w,T,I,f,x,D)=>b(w,T,I,f,x,D,t.root),"spawnInRoot");console.log(`[gateway] hosting lens app '${o}' of '${t.wsKey}' \u2192 vite :${P}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:d.projectRoot,workspaceRoot:t.root,project:o,eventFile:s,port:P,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:o},labelPrefix:g,log:i(w=>console.log(w),"log")},R),u("info",`lens app started: ${t.wsKey}/${o} (vite :${P})`,`cluster-lens-app:${t.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:P},void 0,s)}catch(w){console.error(`[gateway] lens app ${t.wsKey}/${o}: ${w.message}`)}}return()=>{for(const o of e){y.add(o);try{v.get(o)?.kill("SIGTERM")}catch{}v.delete(o)}for(const o of n)F.delete(o)}},"hostWorkspaceApps"),U=i(async(t=2500)=>{$&&(clearInterval($),$=void 0);const a=[...S];for(const n of a)try{n.kill("SIGTERM")}catch{}const s=i(n=>n.exitCode===null&&n.signalCode===null,"alive"),e=Date.now()+t;for(;Date.now()<e&&a.some(s);)await new Promise(n=>setTimeout(n,100));for(const n of a)if(s(n))try{n.kill("SIGKILL")}catch{}},"reapAll"),K=i(()=>H({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:b,bootSingletonsAndApps:q,hostWorkspaceDashboard:Y,hostWorkspaceApps:V,probeLensFrontends:K,reapAll:U}}L(createLensChildren,"createLensChildren"),i(createLensChildren,"createLensChildren");
@@ -352,6 +352,13 @@ export interface GwTrace {
352
352
  * REFERENCE and mutated in place (e.g. the dashboard route is pushed onto
353
353
  * `routes`, dead pods are filtered out of a pool).
354
354
  */
355
+ /** The half of a proxied HMR client socket the reload broadcast needs. Structural (not `stream.Duplex`) so
356
+ * the recycle path can be unit-tested with a plain fake — the real value IS the upgrade's duplex. */
357
+ export interface HmrClientSocket {
358
+ write(chunk: Buffer): boolean;
359
+ once(event: 'close' | 'error', listener: () => void): unknown;
360
+ readonly destroyed: boolean;
361
+ }
355
362
  export interface GatewayRuntime {
356
363
  readonly root: string;
357
364
  /** The per-workspace key (from `readLensScope`) — namespaces this workspace's pod sock dirs so a shared
@@ -390,6 +397,13 @@ export interface GatewayRuntime {
390
397
  * child's watch scope, and tells it which top-level buckets are OWNED by a managed project. Absent ⇒
391
398
  * every consumer falls back to the workspace-wide clock (previous behavior). */
392
399
  readonly projectRoots?: Readonly<Record<string, string>>;
400
+ /** project → the live HMR sockets of its open browser tabs, registered by the WS upgrade pipeline. A
401
+ * recycle pushes `full-reload` to these before killing the child ({@link ./hmr-reload}): the dying vite
402
+ * cannot announce its own death and its replacement never knew these tabs, so the gateway — which owns
403
+ * the decision AND proxied the sockets — is the only place the handoff can happen. Without it every open
404
+ * tab is left pinned to a dead optimize-dep generation that no retry can repair. Mutable; self-cleaning
405
+ * (each socket drops itself on close/error). */
406
+ hmrClients?: Map<string, Set<HmrClientSocket>>;
393
407
  /** Teardown flag — a GETTER, never a captured boolean, so every layer reads
394
408
  * the LIVE value (an in-flight cold start / auto-heal must not act after stop). */
395
409
  stopped(): boolean;
@@ -1 +1 @@
1
- "use strict";var v=Object.defineProperty;var p=(i,n)=>v(i,"name",{value:n,configurable:!0});var y=Object.defineProperty,a=p((i,n)=>y(i,"name",{value:n,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge");function createUpgrade(i){const{rt:n,auth:l,proxy:f,hooks:c}=i,h=a(e=>(0,manifest_1.matchRoute)(n.routes,(e.headers.host||"").split(":")[0],e.url??"/"),"match"),g=a(async(e,t,d)=>{for(const o of edge_1.GATEWAY_OWNED_HEADERS)delete e.headers[o];const r=h(e);if(!r){t.destroy();return}const s=c.upgradeContext(e,t,void 0);if(s.route=r,r.internal){const o=l.internal(e,r,void 0);if(!o.ok){t.destroy();return}s.caller=o.caller}else{const o=l.public(e,r,void 0);if(!o.ok){t.destroy();return}if(s.claims=o.claims,c.has("afterAuth")&&(await c.run("afterAuth",s),s.responded()))return}if(r.pool&&n.serviceKeys[r.project]&&(e.headers["x-internal-token"]=n.serviceKeys[r.project]),r.prependPrefix&&!e.url?.startsWith(r.prependPrefix)&&(e.url=r.prependPrefix+(e.url??"/")),c.has("onUpgrade")&&(await c.run("onUpgrade",s),s.responded()))return;const u=r.pool?(()=>{const o=(0,discovery_1.pickSock)(r.pool,n.scanTtlMs);return o?{socketPath:o}:void 0})():r.target;if(!u){t.destroy();return}f.proxy.ws(e,t,d,{target:u,changeOrigin:!r.pool,agent:f.agentForTarget(u)},()=>t.destroy())},"run");return(e,t,d)=>{t.on("error",()=>t.destroy()),g(e,t,d).catch(()=>t.destroy())}}p(createUpgrade,"createUpgrade"),a(createUpgrade,"createUpgrade");
1
+ "use strict";var m=Object.defineProperty;var u=(n,s)=>m(n,"name",{value:s,configurable:!0});var y=Object.defineProperty,a=u((n,s)=>y(n,"name",{value:s,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge"),hmr_reload_1=require("./hmr-reload");function createUpgrade(n){const{rt:s,auth:f,proxy:h,hooks:c}=n,g=a(r=>(0,manifest_1.matchRoute)(s.routes,(r.headers.host||"").split(":")[0],r.url??"/"),"match"),v=a(async(r,o,d)=>{for(const t of edge_1.GATEWAY_OWNED_HEADERS)delete r.headers[t];const e=g(r);if(!e){o.destroy();return}const i=c.upgradeContext(r,o,void 0);if(i.route=e,e.internal){const t=f.internal(r,e,void 0);if(!t.ok){o.destroy();return}i.caller=t.caller}else{const t=f.public(r,e,void 0);if(!t.ok){o.destroy();return}if(i.claims=t.claims,c.has("afterAuth")&&(await c.run("afterAuth",i),i.responded()))return}if(e.pool&&s.serviceKeys[e.project]&&(r.headers["x-internal-token"]=s.serviceKeys[e.project]),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),c.has("onUpgrade")&&(await c.run("onUpgrade",i),i.responded()))return;const l=e.pool?(()=>{const t=(0,discovery_1.pickSock)(e.pool,s.scanTtlMs);return t?{socketPath:t}:void 0})():e.target;if(!l){o.destroy();return}if(e.lens){const t=String(r.headers["sec-websocket-protocol"]??"").split(",").map(p=>p.trim());(t.length===0||t.includes("vite-hmr")||t.every(p=>p===""))&&(0,hmr_reload_1.registerHmrClient)(s,e.lens.project,o)}h.proxy.ws(r,o,d,{target:l,changeOrigin:!e.pool,agent:h.agentForTarget(l)},()=>o.destroy())},"run");return(r,o,d)=>{o.on("error",()=>o.destroy()),v(r,o,d).catch(()=>o.destroy())}}u(createUpgrade,"createUpgrade"),a(createUpgrade,"createUpgrade");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.18.8",
3
+ "version": "1.18.11",
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,9 +65,9 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/core": "1.18.8",
69
- "@lensmcp/node-instrumentation": "1.18.8",
70
- "@lensmcp/nx-plugin": "1.18.8",
68
+ "@lensmcp/core": "1.18.11",
69
+ "@lensmcp/node-instrumentation": "1.18.11",
70
+ "@lensmcp/nx-plugin": "1.18.11",
71
71
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
72
72
  "fork-ts-checker-webpack-plugin": "^9.0.0",
73
73
  "glob": "^11.0.0",