lensmcp 1.16.26 → 1.16.28

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.
@@ -1295,9 +1295,9 @@ function makeUnixTmpDir() {
1295
1295
  function makeWin32TmpDir() {
1296
1296
  const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
1297
1297
  const randomNumber = Math.floor(Math.random() * 9e7 + 1e7);
1298
- const tmpdir = join(winTmpPath, "lighthouse." + randomNumber);
1299
- mkdirSync(tmpdir, { recursive: true });
1300
- return tmpdir;
1298
+ const tmpdir2 = join(winTmpPath, "lighthouse." + randomNumber);
1299
+ mkdirSync(tmpdir2, { recursive: true });
1300
+ return tmpdir2;
1301
1301
  }
1302
1302
  var import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
1303
1303
  var init_utils = __esm({
@@ -1958,8 +1958,8 @@ var init_chrome_launcher = __esm({
1958
1958
  this.fs.closeSync(this.errFile);
1959
1959
  delete this.errFile;
1960
1960
  }
1961
- const rmSync = this.fs.rmSync || this.fs.rmdirSync;
1962
- rmSync(this.userDataDir, { recursive: true, force: true, maxRetries: 10 });
1961
+ const rmSync2 = this.fs.rmSync || this.fs.rmdirSync;
1962
+ rmSync2(this.userDataDir, { recursive: true, force: true, maxRetries: 10 });
1963
1963
  }
1964
1964
  };
1965
1965
  }
@@ -33206,15 +33206,15 @@ var require_api = __commonJS({
33206
33206
  }
33207
33207
  });
33208
33208
  }
33209
- function addCommand(chrome, domainName, command) {
33209
+ function addCommand(chrome2, domainName, command) {
33210
33210
  const commandName = `${domainName}.${command.name}`;
33211
33211
  const handler = (params, sessionId, callback) => {
33212
- return chrome.send(commandName, params, sessionId, callback);
33212
+ return chrome2.send(commandName, params, sessionId, callback);
33213
33213
  };
33214
33214
  decorate(handler, "command", command);
33215
- chrome[commandName] = chrome[domainName][command.name] = handler;
33215
+ chrome2[commandName] = chrome2[domainName][command.name] = handler;
33216
33216
  }
33217
- function addEvent(chrome, domainName, event) {
33217
+ function addEvent(chrome2, domainName, event) {
33218
33218
  const eventName = `${domainName}.${event.name}`;
33219
33219
  const handler = (sessionId, handler2) => {
33220
33220
  if (typeof sessionId === "function") {
@@ -33223,22 +33223,22 @@ var require_api = __commonJS({
33223
33223
  }
33224
33224
  const rawEventName = sessionId ? `${eventName}.${sessionId}` : eventName;
33225
33225
  if (typeof handler2 === "function") {
33226
- chrome.on(rawEventName, handler2);
33227
- return () => chrome.removeListener(rawEventName, handler2);
33226
+ chrome2.on(rawEventName, handler2);
33227
+ return () => chrome2.removeListener(rawEventName, handler2);
33228
33228
  } else {
33229
33229
  return new Promise((fulfill, reject) => {
33230
- chrome.once(rawEventName, fulfill);
33230
+ chrome2.once(rawEventName, fulfill);
33231
33231
  });
33232
33232
  }
33233
33233
  };
33234
33234
  decorate(handler, "event", event);
33235
- chrome[eventName] = chrome[domainName][event.name] = handler;
33235
+ chrome2[eventName] = chrome2[domainName][event.name] = handler;
33236
33236
  }
33237
- function addType(chrome, domainName, type) {
33237
+ function addType(chrome2, domainName, type) {
33238
33238
  const typeName = `${domainName}.${type.id}`;
33239
33239
  const help = {};
33240
33240
  decorate(help, "type", type);
33241
- chrome[typeName] = chrome[domainName][type.id] = help;
33241
+ chrome2[typeName] = chrome2[domainName][type.id] = help;
33242
33242
  }
33243
33243
  function prepare(object, protocol) {
33244
33244
  object.protocol = protocol;
@@ -33560,6 +33560,490 @@ var require_chrome_remote_interface = __commonJS({
33560
33560
  // libs/browser-capture/src/capture-runner.ts
33561
33561
  import { appendFileSync, readFileSync, renameSync, statSync } from "node:fs";
33562
33562
 
33563
+ // libs/core/dist/lib/ulid.js
33564
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
33565
+ var RANDOM_LEN = 16;
33566
+ function encodeTime(now, len) {
33567
+ let out = "";
33568
+ let n = now;
33569
+ for (let i = len - 1; i >= 0; i--) {
33570
+ const mod = n % 32;
33571
+ out = ALPHABET[mod] + out;
33572
+ n = (n - mod) / 32;
33573
+ }
33574
+ return out;
33575
+ }
33576
+ function encodeRandom(len) {
33577
+ const bytes = new Uint8Array(len);
33578
+ globalThis.crypto.getRandomValues(bytes);
33579
+ let out = "";
33580
+ for (let i = 0; i < len; i++) {
33581
+ out += ALPHABET[(bytes[i] ?? 0) & 31];
33582
+ }
33583
+ return out;
33584
+ }
33585
+ function ulid(now = Date.now()) {
33586
+ return encodeTime(now, 10) + encodeRandom(RANDOM_LEN);
33587
+ }
33588
+
33589
+ // libs/core/dist/lib/fingerprint.js
33590
+ var FNV_OFFSET_BASIS = 2166136261;
33591
+ var FNV_PRIME = 16777619;
33592
+ function fnv1a32(input) {
33593
+ let h = FNV_OFFSET_BASIS;
33594
+ for (let i = 0; i < input.length; i++) {
33595
+ h ^= input.charCodeAt(i);
33596
+ h = Math.imul(h, FNV_PRIME);
33597
+ }
33598
+ return (h >>> 0).toString(16).padStart(8, "0");
33599
+ }
33600
+ function fingerprint(input) {
33601
+ const parts = [input.kind, input.identity, input.file ?? "", input.detail ?? ""];
33602
+ return `${input.kind}:${fnv1a32(parts.join("|"))}`;
33603
+ }
33604
+
33605
+ // libs/browser-sidecar/dist/lib/page-controller.js
33606
+ var PageController = class {
33607
+ constructor(args) {
33608
+ this.url = "";
33609
+ this.pending = /* @__PURE__ */ new Map();
33610
+ this.pageId = args.pageId;
33611
+ this.cdp = args.cdp;
33612
+ this.emit = args.emit;
33613
+ this.sessionId = args.sessionId;
33614
+ }
33615
+ async attach() {
33616
+ await Promise.all([
33617
+ this.cdp.Runtime.enable(),
33618
+ this.cdp.Network.enable(),
33619
+ this.cdp.Page.enable()
33620
+ ]);
33621
+ this.cdp.Runtime.consoleAPICalled((params) => {
33622
+ const text = params.args.map((a) => a.value !== void 0 ? String(a.value) : a.description ?? "?").join(" ");
33623
+ const lvl = params.type === "error" || params.type === "warning" ? params.type : "log";
33624
+ const severity = lvl === "error" ? "error" : lvl === "warning" ? "warning" : "info";
33625
+ this.emit({
33626
+ id: ulid(),
33627
+ sessionId: this.sessionId,
33628
+ timestamp: Math.round(params.timestamp),
33629
+ source: "chrome",
33630
+ category: "runtime",
33631
+ severity,
33632
+ context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
33633
+ fingerprint: fingerprint({
33634
+ kind: "runtime",
33635
+ identity: `console:${lvl}:${text.slice(0, 80)}`
33636
+ }),
33637
+ title: `console.${lvl}`,
33638
+ message: text,
33639
+ raw: params
33640
+ });
33641
+ });
33642
+ this.cdp.Runtime.exceptionThrown((params) => {
33643
+ const det = params.exceptionDetails;
33644
+ const desc = det.exception?.description ?? det.text;
33645
+ this.emit({
33646
+ id: ulid(),
33647
+ sessionId: this.sessionId,
33648
+ timestamp: Math.round(params.timestamp),
33649
+ source: "chrome",
33650
+ category: "runtime",
33651
+ severity: "error",
33652
+ context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
33653
+ fingerprint: fingerprint({
33654
+ kind: "runtime",
33655
+ identity: `exception:${normaliseTopFrame(desc)}`,
33656
+ file: det.url
33657
+ }),
33658
+ title: det.text,
33659
+ message: desc,
33660
+ location: det.url ? { file: det.url, line: det.lineNumber, column: det.columnNumber } : void 0,
33661
+ raw: params
33662
+ });
33663
+ });
33664
+ this.cdp.Network.requestWillBeSent((p) => {
33665
+ this.pending.set(p.requestId, {
33666
+ url: p.request.url,
33667
+ method: p.request.method,
33668
+ startMs: Math.round(p.timestamp * 1e3)
33669
+ });
33670
+ });
33671
+ this.cdp.Network.responseReceived((p) => {
33672
+ const pend = this.pending.get(p.requestId);
33673
+ const durationMs = pend ? Math.round(p.timestamp * 1e3) - pend.startMs : void 0;
33674
+ const severity = p.response.status >= 400 ? "error" : "info";
33675
+ this.emit({
33676
+ id: ulid(),
33677
+ sessionId: this.sessionId,
33678
+ timestamp: Math.round(p.timestamp * 1e3),
33679
+ source: "chrome",
33680
+ category: "network",
33681
+ severity,
33682
+ context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
33683
+ fingerprint: fingerprint({
33684
+ kind: "network",
33685
+ identity: `${pend?.method ?? "?"}:${p.response.url}:${p.response.status}`
33686
+ }),
33687
+ title: `${pend?.method ?? "?"} ${p.response.url} \u2192 ${p.response.status}`,
33688
+ message: durationMs !== void 0 ? `${durationMs}ms` : void 0,
33689
+ relatedUrls: [p.response.url],
33690
+ raw: p
33691
+ });
33692
+ });
33693
+ this.cdp.Network.loadingFailed((p) => {
33694
+ const pend = this.pending.get(p.requestId);
33695
+ this.pending.delete(p.requestId);
33696
+ this.emit({
33697
+ id: ulid(),
33698
+ sessionId: this.sessionId,
33699
+ timestamp: Math.round(p.timestamp * 1e3),
33700
+ source: "chrome",
33701
+ category: "network",
33702
+ severity: "error",
33703
+ context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
33704
+ fingerprint: fingerprint({
33705
+ kind: "network",
33706
+ identity: `failed:${pend?.method ?? "?"}:${pend?.url ?? p.requestId}`
33707
+ }),
33708
+ title: `Request failed: ${pend?.url ?? p.requestId}`,
33709
+ message: p.errorText,
33710
+ relatedUrls: pend ? [pend.url] : void 0,
33711
+ raw: p
33712
+ });
33713
+ });
33714
+ this.cdp.Page.frameNavigated((p) => {
33715
+ if (!p.frame.name) {
33716
+ this.url = p.frame.url;
33717
+ }
33718
+ });
33719
+ }
33720
+ async dispose() {
33721
+ if (this.cdp.close) {
33722
+ try {
33723
+ await this.cdp.close();
33724
+ } catch {
33725
+ }
33726
+ }
33727
+ }
33728
+ };
33729
+ function normaliseTopFrame(desc) {
33730
+ if (!desc)
33731
+ return "unknown";
33732
+ const m = desc.match(/at\s+([\w$.<>]+)/);
33733
+ return m?.[1] ?? "unknown";
33734
+ }
33735
+
33736
+ // libs/browser-sidecar/dist/lib/chrome-launch.js
33737
+ import { execFileSync as execFileSync2 } from "node:child_process";
33738
+ import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
33739
+ import { homedir as homedir2, tmpdir } from "node:os";
33740
+ import { join as join2 } from "node:path";
33741
+ var CAPTURE_DIR_MARKER = "lensmcp-capture-";
33742
+ function createManagedUserDataDir() {
33743
+ return mkdtempSync(join2(tmpdir(), `${CAPTURE_DIR_MARKER}${process.pid}-`));
33744
+ }
33745
+ function removeUserDataDir(dir) {
33746
+ try {
33747
+ rmSync(dir, { recursive: true, force: true });
33748
+ } catch {
33749
+ }
33750
+ }
33751
+ async function resolveChromePath2(log) {
33752
+ const override = process.env["LENSMCP_CHROME_PATH"] ?? process.env["CHROME_PATH"];
33753
+ if (override) {
33754
+ if (existsSync(override))
33755
+ return override;
33756
+ log?.(`[browser-capture] LENSMCP_CHROME_PATH/CHROME_PATH points at ${override}, which does not exist \u2014 auto-detecting instead`);
33757
+ }
33758
+ const isolated = chromeForTestingFromPuppeteerCache() ?? firstExisting(NON_COLLIDING_INSTALLS);
33759
+ if (isolated)
33760
+ return isolated;
33761
+ try {
33762
+ const { Launcher: Launcher2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
33763
+ const system = Launcher2.getFirstInstallation?.();
33764
+ if (system) {
33765
+ if (process.platform === "darwin") {
33766
+ log?.("[browser-capture] capturing with the system Chrome \u2014 while capture runs, opening Chrome from the Dock may misbehave. Install an isolated binary (`npx @puppeteer/browsers install chrome@stable`) or set LENSMCP_CHROME_PATH to avoid this.");
33767
+ }
33768
+ return system;
33769
+ }
33770
+ } catch {
33771
+ }
33772
+ return void 0;
33773
+ }
33774
+ var NON_COLLIDING_INSTALLS = process.platform === "darwin" ? [
33775
+ "/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
33776
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
33777
+ ] : [];
33778
+ function chromeForTestingFromPuppeteerCache() {
33779
+ const cacheRoot = process.env["PUPPETEER_CACHE_DIR"] ?? join2(homedir2(), ".cache", "puppeteer");
33780
+ const chromeRoot = join2(cacheRoot, "chrome");
33781
+ let versions;
33782
+ try {
33783
+ versions = readdirSync(chromeRoot);
33784
+ } catch {
33785
+ return void 0;
33786
+ }
33787
+ versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
33788
+ const layouts = [
33789
+ ["chrome-mac-arm64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"],
33790
+ ["chrome-mac-x64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"],
33791
+ ["chrome-linux64", "chrome"],
33792
+ ["chrome-win64", "chrome.exe"]
33793
+ ];
33794
+ for (const version of versions) {
33795
+ for (const parts of layouts) {
33796
+ const candidate = join2(chromeRoot, version, ...parts);
33797
+ if (existsSync(candidate))
33798
+ return candidate;
33799
+ }
33800
+ }
33801
+ return void 0;
33802
+ }
33803
+ function firstExisting(candidates) {
33804
+ return candidates.find((c) => existsSync(c));
33805
+ }
33806
+ function planOrphanReap(psLines, isOwnerAlive, selfPid) {
33807
+ const dirRe = new RegExp(`--user-data-dir=(\\S*${CAPTURE_DIR_MARKER}(\\d+)-\\S*)`);
33808
+ const orphans = [];
33809
+ for (const line of psLines) {
33810
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
33811
+ if (!m)
33812
+ continue;
33813
+ const cmd = m[2];
33814
+ if (cmd.includes("--type="))
33815
+ continue;
33816
+ const dir = dirRe.exec(cmd);
33817
+ if (!dir)
33818
+ continue;
33819
+ const ownerPid = Number(dir[2]);
33820
+ if (ownerPid === selfPid || isOwnerAlive(ownerPid))
33821
+ continue;
33822
+ orphans.push({ pid: Number(m[1]), ownerPid, userDataDir: dir[1] });
33823
+ }
33824
+ return orphans;
33825
+ }
33826
+ function reapOrphanCaptureChrome(log) {
33827
+ if (process.platform === "win32")
33828
+ return 0;
33829
+ let out;
33830
+ try {
33831
+ out = execFileSync2("ps", ["ax", "-ww", "-o", "pid=,command="], {
33832
+ encoding: "utf8",
33833
+ timeout: 5e3,
33834
+ maxBuffer: 64 * 1024 * 1024
33835
+ });
33836
+ } catch {
33837
+ return 0;
33838
+ }
33839
+ let reaped = 0;
33840
+ for (const orphan of planOrphanReap(out.split("\n"), isAlive, process.pid)) {
33841
+ try {
33842
+ process.kill(orphan.pid, "SIGKILL");
33843
+ reaped += 1;
33844
+ log?.(`[browser-capture] reaped orphaned capture Chrome pid=${orphan.pid} (owner ${orphan.ownerPid} is gone)`);
33845
+ } catch {
33846
+ }
33847
+ removeUserDataDir(orphan.userDataDir);
33848
+ }
33849
+ return reaped;
33850
+ }
33851
+ function isAlive(pid) {
33852
+ try {
33853
+ process.kill(pid, 0);
33854
+ return true;
33855
+ } catch (err) {
33856
+ return err.code === "EPERM";
33857
+ }
33858
+ }
33859
+
33860
+ // libs/browser-sidecar/dist/lib/dom-snapshot.js
33861
+ var COMPUTED_STYLE_WHITELIST = [
33862
+ // box model
33863
+ "display",
33864
+ "position",
33865
+ "box-sizing",
33866
+ "margin-top",
33867
+ "margin-right",
33868
+ "margin-bottom",
33869
+ "margin-left",
33870
+ "padding-top",
33871
+ "padding-right",
33872
+ "padding-bottom",
33873
+ "padding-left",
33874
+ // typography
33875
+ "font-family",
33876
+ "font-size",
33877
+ "font-weight",
33878
+ "line-height",
33879
+ "letter-spacing",
33880
+ "text-align",
33881
+ // color + bg
33882
+ "color",
33883
+ "background-color",
33884
+ "opacity",
33885
+ "visibility",
33886
+ // borders
33887
+ "border-top-width",
33888
+ "border-right-width",
33889
+ "border-bottom-width",
33890
+ "border-left-width",
33891
+ "border-radius",
33892
+ // layout
33893
+ "flex-direction",
33894
+ "justify-content",
33895
+ "align-items",
33896
+ "gap",
33897
+ "row-gap",
33898
+ "column-gap",
33899
+ "grid-template-columns",
33900
+ "grid-template-rows",
33901
+ "overflow",
33902
+ "overflow-x",
33903
+ "overflow-y",
33904
+ // stacking
33905
+ "z-index",
33906
+ // motion
33907
+ "transform",
33908
+ "transition",
33909
+ "animation"
33910
+ ];
33911
+ async function captureDomSnapshot(cdp, opts = {}) {
33912
+ const computedStyles = (opts.computedStyles ?? COMPUTED_STYLE_WHITELIST).slice();
33913
+ const raw = await cdp.DOMSnapshot.captureSnapshot({
33914
+ computedStyles,
33915
+ includePaintOrder: opts.includePaintOrder ?? true,
33916
+ includeDOMRects: true,
33917
+ includeBlendedBackgroundColors: opts.includeBlendedBackgroundColors ?? false,
33918
+ includeTextColorOpacities: opts.includeTextColorOpacities ?? false
33919
+ });
33920
+ const ax = opts.mergeAccessibility && cdp.Accessibility ? await cdp.Accessibility.getFullAXTree().catch(() => ({ nodes: [] })) : { nodes: [] };
33921
+ const viewport = await readViewport(cdp);
33922
+ const snapshotId = opts.snapshotId ?? `dom-${Date.now()}`;
33923
+ return normalise(raw, ax, computedStyles, viewport, snapshotId);
33924
+ }
33925
+ async function readViewport(cdp) {
33926
+ const lm = cdp.Page?.getLayoutMetrics;
33927
+ if (!lm)
33928
+ return { x: 0, y: 0, width: 0, height: 0 };
33929
+ try {
33930
+ const r = await lm();
33931
+ const v = r.cssVisualViewport ?? r.layoutViewport;
33932
+ if (!v)
33933
+ return { x: 0, y: 0, width: 0, height: 0 };
33934
+ return { x: 0, y: 0, width: v.clientWidth, height: v.clientHeight };
33935
+ } catch {
33936
+ return { x: 0, y: 0, width: 0, height: 0 };
33937
+ }
33938
+ }
33939
+ function normalise(raw, ax, computedStyles, viewport, snapshotId) {
33940
+ const nodes = {};
33941
+ const nodeOrder = [];
33942
+ const document2 = raw.documents[0];
33943
+ if (!document2) {
33944
+ return {
33945
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
33946
+ viewport,
33947
+ snapshotId,
33948
+ nodes,
33949
+ nodeOrder
33950
+ };
33951
+ }
33952
+ const strings = raw.strings;
33953
+ const decode = (i) => i >= 0 ? strings[i] ?? "" : "";
33954
+ const nodeCount = document2.nodes.backendNodeId.length;
33955
+ const byIndex = new Array(nodeCount);
33956
+ for (let i = 0; i < nodeCount; i++) {
33957
+ const backendNodeId = document2.nodes.backendNodeId[i];
33958
+ if (backendNodeId === void 0)
33959
+ continue;
33960
+ const tag = decode(document2.nodes.nodeName[i] ?? -1).toLowerCase();
33961
+ const attributePairs = document2.nodes.attributes[i] ?? [];
33962
+ const attributes = {};
33963
+ for (let j = 0; j + 1 < attributePairs.length; j += 2) {
33964
+ const name = decode(attributePairs[j]);
33965
+ const value = decode(attributePairs[j + 1]);
33966
+ if (name)
33967
+ attributes[name] = value;
33968
+ }
33969
+ const parentIndex = document2.nodes.parentIndex?.[i] ?? -1;
33970
+ const parentBackend = parentIndex >= 0 ? document2.nodes.backendNodeId[parentIndex] : void 0;
33971
+ const text = decode(document2.nodes.nodeValue[i] ?? -1).trim() || void 0;
33972
+ byIndex[i] = {
33973
+ backendNodeId,
33974
+ parentBackendNodeId: parentBackend,
33975
+ tag,
33976
+ attributes,
33977
+ text,
33978
+ rect: { x: 0, y: 0, width: 0, height: 0 },
33979
+ computedStyle: {},
33980
+ agentComponent: attributes["data-agent-component"]
33981
+ };
33982
+ }
33983
+ const layout = document2.layout;
33984
+ for (let li = 0; li < layout.nodeIndex.length; li++) {
33985
+ const idx = layout.nodeIndex[li];
33986
+ if (idx === void 0)
33987
+ continue;
33988
+ const node = byIndex[idx];
33989
+ if (!node)
33990
+ continue;
33991
+ const bounds = layout.bounds[li];
33992
+ if (bounds && bounds.length === 4) {
33993
+ const [x, y, w, h] = bounds;
33994
+ node.rect = {
33995
+ x: x ?? 0,
33996
+ y: y ?? 0,
33997
+ width: Math.max(0, w ?? 0),
33998
+ height: Math.max(0, h ?? 0)
33999
+ };
34000
+ }
34001
+ const styleRow = layout.styles[li];
34002
+ if (styleRow) {
34003
+ for (let s = 0; s < styleRow.length && s < computedStyles.length; s++) {
34004
+ const styleName = computedStyles[s];
34005
+ if (!styleName)
34006
+ continue;
34007
+ const styleValue = decode(styleRow[s]);
34008
+ if (styleValue)
34009
+ node.computedStyle[styleName] = styleValue;
34010
+ }
34011
+ }
34012
+ if (layout.paintOrders) {
34013
+ const po = layout.paintOrders[li];
34014
+ if (po !== void 0)
34015
+ node.paintOrder = po;
34016
+ }
34017
+ }
34018
+ const axByBackend = /* @__PURE__ */ new Map();
34019
+ for (const axNode of ax.nodes) {
34020
+ if (axNode.backendDOMNodeId !== void 0) {
34021
+ axByBackend.set(axNode.backendDOMNodeId, axNode);
34022
+ }
34023
+ }
34024
+ for (let i = 0; i < byIndex.length; i++) {
34025
+ const node = byIndex[i];
34026
+ if (!node)
34027
+ continue;
34028
+ const axNode = axByBackend.get(node.backendNodeId);
34029
+ if (axNode) {
34030
+ if (axNode.role?.value)
34031
+ node.role = axNode.role.value;
34032
+ if (axNode.name?.value)
34033
+ node.ariaName = axNode.name.value;
34034
+ }
34035
+ nodes[node.backendNodeId] = node;
34036
+ nodeOrder.push(node.backendNodeId);
34037
+ }
34038
+ return {
34039
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
34040
+ viewport,
34041
+ snapshotId,
34042
+ nodes,
34043
+ nodeOrder
34044
+ };
34045
+ }
34046
+
33563
34047
  // libs/visual-engine/dist/lib/diff.js
33564
34048
  var LAYOUT_TOLERANCE_PX = 0.5;
33565
34049
  function diffSnapshots(prev, curr, options) {
@@ -34186,372 +34670,12 @@ function resolveTargets(snapshot, rule) {
34186
34670
  return [];
34187
34671
  }
34188
34672
 
34189
- // libs/core/dist/lib/ulid.js
34190
- var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
34191
- var RANDOM_LEN = 16;
34192
- function encodeTime(now, len) {
34193
- let out = "";
34194
- let n = now;
34195
- for (let i = len - 1; i >= 0; i--) {
34196
- const mod = n % 32;
34197
- out = ALPHABET[mod] + out;
34198
- n = (n - mod) / 32;
34199
- }
34200
- return out;
34201
- }
34202
- function encodeRandom(len) {
34203
- const bytes = new Uint8Array(len);
34204
- globalThis.crypto.getRandomValues(bytes);
34205
- let out = "";
34206
- for (let i = 0; i < len; i++) {
34207
- out += ALPHABET[(bytes[i] ?? 0) & 31];
34208
- }
34209
- return out;
34210
- }
34211
- function ulid(now = Date.now()) {
34212
- return encodeTime(now, 10) + encodeRandom(RANDOM_LEN);
34213
- }
34214
-
34215
- // libs/core/dist/lib/fingerprint.js
34216
- var FNV_OFFSET_BASIS = 2166136261;
34217
- var FNV_PRIME = 16777619;
34218
- function fnv1a32(input) {
34219
- let h = FNV_OFFSET_BASIS;
34220
- for (let i = 0; i < input.length; i++) {
34221
- h ^= input.charCodeAt(i);
34222
- h = Math.imul(h, FNV_PRIME);
34223
- }
34224
- return (h >>> 0).toString(16).padStart(8, "0");
34225
- }
34226
- function fingerprint(input) {
34227
- const parts = [input.kind, input.identity, input.file ?? "", input.detail ?? ""];
34228
- return `${input.kind}:${fnv1a32(parts.join("|"))}`;
34229
- }
34230
-
34231
- // libs/browser-sidecar/dist/lib/page-controller.js
34232
- var PageController = class {
34233
- constructor(args) {
34234
- this.url = "";
34235
- this.pending = /* @__PURE__ */ new Map();
34236
- this.pageId = args.pageId;
34237
- this.cdp = args.cdp;
34238
- this.emit = args.emit;
34239
- this.sessionId = args.sessionId;
34240
- }
34241
- async attach() {
34242
- await Promise.all([
34243
- this.cdp.Runtime.enable(),
34244
- this.cdp.Network.enable(),
34245
- this.cdp.Page.enable()
34246
- ]);
34247
- this.cdp.Runtime.consoleAPICalled((params) => {
34248
- const text = params.args.map((a) => a.value !== void 0 ? String(a.value) : a.description ?? "?").join(" ");
34249
- const lvl = params.type === "error" || params.type === "warning" ? params.type : "log";
34250
- const severity = lvl === "error" ? "error" : lvl === "warning" ? "warning" : "info";
34251
- this.emit({
34252
- id: ulid(),
34253
- sessionId: this.sessionId,
34254
- timestamp: Math.round(params.timestamp),
34255
- source: "chrome",
34256
- category: "runtime",
34257
- severity,
34258
- context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
34259
- fingerprint: fingerprint({
34260
- kind: "runtime",
34261
- identity: `console:${lvl}:${text.slice(0, 80)}`
34262
- }),
34263
- title: `console.${lvl}`,
34264
- message: text,
34265
- raw: params
34266
- });
34267
- });
34268
- this.cdp.Runtime.exceptionThrown((params) => {
34269
- const det = params.exceptionDetails;
34270
- const desc = det.exception?.description ?? det.text;
34271
- this.emit({
34272
- id: ulid(),
34273
- sessionId: this.sessionId,
34274
- timestamp: Math.round(params.timestamp),
34275
- source: "chrome",
34276
- category: "runtime",
34277
- severity: "error",
34278
- context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
34279
- fingerprint: fingerprint({
34280
- kind: "runtime",
34281
- identity: `exception:${normaliseTopFrame(desc)}`,
34282
- file: det.url
34283
- }),
34284
- title: det.text,
34285
- message: desc,
34286
- location: det.url ? { file: det.url, line: det.lineNumber, column: det.columnNumber } : void 0,
34287
- raw: params
34288
- });
34289
- });
34290
- this.cdp.Network.requestWillBeSent((p) => {
34291
- this.pending.set(p.requestId, {
34292
- url: p.request.url,
34293
- method: p.request.method,
34294
- startMs: Math.round(p.timestamp * 1e3)
34295
- });
34296
- });
34297
- this.cdp.Network.responseReceived((p) => {
34298
- const pend = this.pending.get(p.requestId);
34299
- const durationMs = pend ? Math.round(p.timestamp * 1e3) - pend.startMs : void 0;
34300
- const severity = p.response.status >= 400 ? "error" : "info";
34301
- this.emit({
34302
- id: ulid(),
34303
- sessionId: this.sessionId,
34304
- timestamp: Math.round(p.timestamp * 1e3),
34305
- source: "chrome",
34306
- category: "network",
34307
- severity,
34308
- context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
34309
- fingerprint: fingerprint({
34310
- kind: "network",
34311
- identity: `${pend?.method ?? "?"}:${p.response.url}:${p.response.status}`
34312
- }),
34313
- title: `${pend?.method ?? "?"} ${p.response.url} \u2192 ${p.response.status}`,
34314
- message: durationMs !== void 0 ? `${durationMs}ms` : void 0,
34315
- relatedUrls: [p.response.url],
34316
- raw: p
34317
- });
34318
- });
34319
- this.cdp.Network.loadingFailed((p) => {
34320
- const pend = this.pending.get(p.requestId);
34321
- this.pending.delete(p.requestId);
34322
- this.emit({
34323
- id: ulid(),
34324
- sessionId: this.sessionId,
34325
- timestamp: Math.round(p.timestamp * 1e3),
34326
- source: "chrome",
34327
- category: "network",
34328
- severity: "error",
34329
- context: { sessionId: this.sessionId, tabId: this.pageId, url: this.url },
34330
- fingerprint: fingerprint({
34331
- kind: "network",
34332
- identity: `failed:${pend?.method ?? "?"}:${pend?.url ?? p.requestId}`
34333
- }),
34334
- title: `Request failed: ${pend?.url ?? p.requestId}`,
34335
- message: p.errorText,
34336
- relatedUrls: pend ? [pend.url] : void 0,
34337
- raw: p
34338
- });
34339
- });
34340
- this.cdp.Page.frameNavigated((p) => {
34341
- if (!p.frame.name) {
34342
- this.url = p.frame.url;
34343
- }
34344
- });
34345
- }
34346
- async dispose() {
34347
- if (this.cdp.close) {
34348
- try {
34349
- await this.cdp.close();
34350
- } catch {
34351
- }
34352
- }
34353
- }
34354
- };
34355
- function normaliseTopFrame(desc) {
34356
- if (!desc)
34357
- return "unknown";
34358
- const m = desc.match(/at\s+([\w$.<>]+)/);
34359
- return m?.[1] ?? "unknown";
34360
- }
34361
-
34362
- // libs/browser-sidecar/dist/lib/dom-snapshot.js
34363
- var COMPUTED_STYLE_WHITELIST = [
34364
- // box model
34365
- "display",
34366
- "position",
34367
- "box-sizing",
34368
- "margin-top",
34369
- "margin-right",
34370
- "margin-bottom",
34371
- "margin-left",
34372
- "padding-top",
34373
- "padding-right",
34374
- "padding-bottom",
34375
- "padding-left",
34376
- // typography
34377
- "font-family",
34378
- "font-size",
34379
- "font-weight",
34380
- "line-height",
34381
- "letter-spacing",
34382
- "text-align",
34383
- // color + bg
34384
- "color",
34385
- "background-color",
34386
- "opacity",
34387
- "visibility",
34388
- // borders
34389
- "border-top-width",
34390
- "border-right-width",
34391
- "border-bottom-width",
34392
- "border-left-width",
34393
- "border-radius",
34394
- // layout
34395
- "flex-direction",
34396
- "justify-content",
34397
- "align-items",
34398
- "gap",
34399
- "row-gap",
34400
- "column-gap",
34401
- "grid-template-columns",
34402
- "grid-template-rows",
34403
- "overflow",
34404
- "overflow-x",
34405
- "overflow-y",
34406
- // stacking
34407
- "z-index",
34408
- // motion
34409
- "transform",
34410
- "transition",
34411
- "animation"
34412
- ];
34413
- async function captureDomSnapshot(cdp, opts = {}) {
34414
- const computedStyles = (opts.computedStyles ?? COMPUTED_STYLE_WHITELIST).slice();
34415
- const raw = await cdp.DOMSnapshot.captureSnapshot({
34416
- computedStyles,
34417
- includePaintOrder: opts.includePaintOrder ?? true,
34418
- includeDOMRects: true,
34419
- includeBlendedBackgroundColors: opts.includeBlendedBackgroundColors ?? false,
34420
- includeTextColorOpacities: opts.includeTextColorOpacities ?? false
34421
- });
34422
- const ax = opts.mergeAccessibility && cdp.Accessibility ? await cdp.Accessibility.getFullAXTree().catch(() => ({ nodes: [] })) : { nodes: [] };
34423
- const viewport = await readViewport(cdp);
34424
- const snapshotId = opts.snapshotId ?? `dom-${Date.now()}`;
34425
- return normalise(raw, ax, computedStyles, viewport, snapshotId);
34426
- }
34427
- async function readViewport(cdp) {
34428
- const lm = cdp.Page?.getLayoutMetrics;
34429
- if (!lm)
34430
- return { x: 0, y: 0, width: 0, height: 0 };
34431
- try {
34432
- const r = await lm();
34433
- const v = r.cssVisualViewport ?? r.layoutViewport;
34434
- if (!v)
34435
- return { x: 0, y: 0, width: 0, height: 0 };
34436
- return { x: 0, y: 0, width: v.clientWidth, height: v.clientHeight };
34437
- } catch {
34438
- return { x: 0, y: 0, width: 0, height: 0 };
34439
- }
34440
- }
34441
- function normalise(raw, ax, computedStyles, viewport, snapshotId) {
34442
- const nodes = {};
34443
- const nodeOrder = [];
34444
- const document2 = raw.documents[0];
34445
- if (!document2) {
34446
- return {
34447
- capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
34448
- viewport,
34449
- snapshotId,
34450
- nodes,
34451
- nodeOrder
34452
- };
34453
- }
34454
- const strings = raw.strings;
34455
- const decode = (i) => i >= 0 ? strings[i] ?? "" : "";
34456
- const nodeCount = document2.nodes.backendNodeId.length;
34457
- const byIndex = new Array(nodeCount);
34458
- for (let i = 0; i < nodeCount; i++) {
34459
- const backendNodeId = document2.nodes.backendNodeId[i];
34460
- if (backendNodeId === void 0)
34461
- continue;
34462
- const tag = decode(document2.nodes.nodeName[i] ?? -1).toLowerCase();
34463
- const attributePairs = document2.nodes.attributes[i] ?? [];
34464
- const attributes = {};
34465
- for (let j = 0; j + 1 < attributePairs.length; j += 2) {
34466
- const name = decode(attributePairs[j]);
34467
- const value = decode(attributePairs[j + 1]);
34468
- if (name)
34469
- attributes[name] = value;
34470
- }
34471
- const parentIndex = document2.nodes.parentIndex?.[i] ?? -1;
34472
- const parentBackend = parentIndex >= 0 ? document2.nodes.backendNodeId[parentIndex] : void 0;
34473
- const text = decode(document2.nodes.nodeValue[i] ?? -1).trim() || void 0;
34474
- byIndex[i] = {
34475
- backendNodeId,
34476
- parentBackendNodeId: parentBackend,
34477
- tag,
34478
- attributes,
34479
- text,
34480
- rect: { x: 0, y: 0, width: 0, height: 0 },
34481
- computedStyle: {},
34482
- agentComponent: attributes["data-agent-component"]
34483
- };
34484
- }
34485
- const layout = document2.layout;
34486
- for (let li = 0; li < layout.nodeIndex.length; li++) {
34487
- const idx = layout.nodeIndex[li];
34488
- if (idx === void 0)
34489
- continue;
34490
- const node = byIndex[idx];
34491
- if (!node)
34492
- continue;
34493
- const bounds = layout.bounds[li];
34494
- if (bounds && bounds.length === 4) {
34495
- const [x, y, w, h] = bounds;
34496
- node.rect = {
34497
- x: x ?? 0,
34498
- y: y ?? 0,
34499
- width: Math.max(0, w ?? 0),
34500
- height: Math.max(0, h ?? 0)
34501
- };
34502
- }
34503
- const styleRow = layout.styles[li];
34504
- if (styleRow) {
34505
- for (let s = 0; s < styleRow.length && s < computedStyles.length; s++) {
34506
- const styleName = computedStyles[s];
34507
- if (!styleName)
34508
- continue;
34509
- const styleValue = decode(styleRow[s]);
34510
- if (styleValue)
34511
- node.computedStyle[styleName] = styleValue;
34512
- }
34513
- }
34514
- if (layout.paintOrders) {
34515
- const po = layout.paintOrders[li];
34516
- if (po !== void 0)
34517
- node.paintOrder = po;
34518
- }
34519
- }
34520
- const axByBackend = /* @__PURE__ */ new Map();
34521
- for (const axNode of ax.nodes) {
34522
- if (axNode.backendDOMNodeId !== void 0) {
34523
- axByBackend.set(axNode.backendDOMNodeId, axNode);
34524
- }
34525
- }
34526
- for (let i = 0; i < byIndex.length; i++) {
34527
- const node = byIndex[i];
34528
- if (!node)
34529
- continue;
34530
- const axNode = axByBackend.get(node.backendNodeId);
34531
- if (axNode) {
34532
- if (axNode.role?.value)
34533
- node.role = axNode.role.value;
34534
- if (axNode.name?.value)
34535
- node.ariaName = axNode.name.value;
34536
- }
34537
- nodes[node.backendNodeId] = node;
34538
- nodeOrder.push(node.backendNodeId);
34539
- }
34540
- return {
34541
- capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
34542
- viewport,
34543
- snapshotId,
34544
- nodes,
34545
- nodeOrder
34546
- };
34547
- }
34548
-
34549
34673
  // libs/browser-capture/src/lib/capture.ts
34550
34674
  async function startBrowserCapture(opts) {
34551
34675
  const log = opts.log ?? (() => void 0);
34552
34676
  const sessionId = opts.sessionId ?? ulid();
34553
34677
  const chromeLauncher = await Promise.resolve().then(() => (init_dist(), dist_exports));
34554
- const chromePath = safe(() => chromeLauncher.Launcher.getFirstInstallation?.());
34678
+ const chromePath = await resolveChromePath2(log);
34555
34679
  if (!chromePath) {
34556
34680
  log("[browser-capture] no Chrome installed \u2014 skipping live browser capture");
34557
34681
  return void 0;
@@ -34561,8 +34685,11 @@ async function startBrowserCapture(opts) {
34561
34685
  log(`[browser-capture] dev URL ${opts.url} not reachable \u2014 skipping`);
34562
34686
  return void 0;
34563
34687
  }
34564
- const chrome = await chromeLauncher.launch({
34688
+ const userDataDir = createManagedUserDataDir();
34689
+ const chrome2 = await chromeLauncher.launch({
34565
34690
  startingUrl: "about:blank",
34691
+ chromePath,
34692
+ userDataDir,
34566
34693
  chromeFlags: [
34567
34694
  ...opts.headless === false ? [] : ["--headless=new"],
34568
34695
  "--disable-gpu",
@@ -34570,12 +34697,20 @@ async function startBrowserCapture(opts) {
34570
34697
  "--disable-dev-shm-usage"
34571
34698
  ]
34572
34699
  });
34700
+ const killSync = () => {
34701
+ try {
34702
+ process.kill(chrome2.pid, "SIGKILL");
34703
+ } catch {
34704
+ }
34705
+ removeUserDataDir(userDataDir);
34706
+ };
34707
+ opts.onChrome?.({ pid: chrome2.pid, killSync });
34573
34708
  const CDP = (await Promise.resolve().then(() => __toESM(require_chrome_remote_interface(), 1))).default;
34574
34709
  const attachClient = async (navigate) => {
34575
- const targets = await CDP.List({ port: chrome.port });
34710
+ const targets = await CDP.List({ port: chrome2.port });
34576
34711
  const pageTarget = targets.find((t) => t.type === "page");
34577
34712
  if (!pageTarget) throw new Error("no page target");
34578
- const client = await CDP({ port: chrome.port, target: pageTarget });
34713
+ const client = await CDP({ port: chrome2.port, target: pageTarget });
34579
34714
  const controller = new PageController({
34580
34715
  pageId: pageTarget.id,
34581
34716
  cdp: client,
@@ -34593,7 +34728,8 @@ async function startBrowserCapture(opts) {
34593
34728
  try {
34594
34729
  cdp = await attachClient(true);
34595
34730
  } catch {
34596
- await chrome.kill();
34731
+ await chrome2.kill();
34732
+ removeUserDataDir(userDataDir);
34597
34733
  log("[browser-capture] no page target after launch \u2014 skipping");
34598
34734
  return void 0;
34599
34735
  }
@@ -34660,6 +34796,7 @@ async function startBrowserCapture(opts) {
34660
34796
  timer.unref?.();
34661
34797
  return {
34662
34798
  frameCount: () => frames,
34799
+ killSync,
34663
34800
  async stop() {
34664
34801
  stopped = true;
34665
34802
  clearInterval(timer);
@@ -34668,9 +34805,10 @@ async function startBrowserCapture(opts) {
34668
34805
  } catch {
34669
34806
  }
34670
34807
  try {
34671
- await chrome.kill();
34808
+ await chrome2.kill();
34672
34809
  } catch {
34673
34810
  }
34811
+ removeUserDataDir(userDataDir);
34674
34812
  }
34675
34813
  };
34676
34814
  }
@@ -34700,15 +34838,49 @@ async function waitForUrl(url, timeoutSec) {
34700
34838
  }
34701
34839
  return false;
34702
34840
  }
34703
- function safe(fn) {
34704
- try {
34705
- return fn();
34706
- } catch {
34707
- return void 0;
34708
- }
34709
- }
34710
34841
 
34711
34842
  // libs/browser-capture/src/capture-runner.ts
34843
+ var chrome;
34844
+ var capture;
34845
+ var shuttingDown = false;
34846
+ function shutdown() {
34847
+ if (shuttingDown) return;
34848
+ shuttingDown = true;
34849
+ const finish = () => process.exit(0);
34850
+ if (!capture) {
34851
+ chrome?.killSync();
34852
+ finish();
34853
+ return;
34854
+ }
34855
+ setTimeout(() => {
34856
+ chrome?.killSync();
34857
+ finish();
34858
+ }, 5e3).unref();
34859
+ void capture.stop().then(finish, () => {
34860
+ chrome?.killSync();
34861
+ finish();
34862
+ });
34863
+ }
34864
+ process.on("SIGINT", shutdown);
34865
+ process.on("SIGTERM", shutdown);
34866
+ process.on("SIGHUP", shutdown);
34867
+ process.on("exit", () => chrome?.killSync());
34868
+ process.on("uncaughtException", (err) => {
34869
+ console.error(`[browser-capture] fatal: ${err?.stack ?? String(err)}`);
34870
+ shutdown();
34871
+ });
34872
+ process.on("unhandledRejection", (reason) => {
34873
+ console.error(`[browser-capture] fatal: ${String(reason)}`);
34874
+ shutdown();
34875
+ });
34876
+ setInterval(() => {
34877
+ if (process.ppid === 1) {
34878
+ console.error("[browser-capture] parent process gone \u2014 shutting down");
34879
+ shutdown();
34880
+ }
34881
+ }, 5e3).unref();
34882
+ reapOrphanCaptureChrome((m) => console.error(m));
34883
+ setInterval(() => reapOrphanCaptureChrome((m) => console.error(m)), 6e4).unref();
34712
34884
  var MAX_EVENT_BYTES = (() => {
34713
34885
  const n = Number(process.env["LENSMCP_EVENT_MAX_BYTES"]);
34714
34886
  return Number.isFinite(n) && n > 0 ? n : 256 * 1024 * 1024;
@@ -34747,21 +34919,20 @@ async function main() {
34747
34919
  process.env["LENSMCP_TOKENS_FILE"] ?? process.cwd()
34748
34920
  );
34749
34921
  const rules = loadRules(process.env["LENSMCP_RULES_FILE"]);
34750
- const capture = await startBrowserCapture({
34922
+ capture = await startBrowserCapture({
34751
34923
  url,
34752
34924
  sink,
34753
34925
  tokens,
34754
34926
  rules,
34755
34927
  sessionId: process.env["LENSMCP_SESSION_ID"],
34756
34928
  headless: process.env["LENSMCP_HEADLESS"] !== "false",
34929
+ onChrome: (c) => {
34930
+ chrome = c;
34931
+ if (shuttingDown) c.killSync();
34932
+ },
34757
34933
  log: (m) => console.error(m)
34758
34934
  });
34759
- if (!capture) return;
34760
- const shutdown = () => {
34761
- void capture.stop().finally(() => process.exit(0));
34762
- };
34763
- process.on("SIGINT", shutdown);
34764
- process.on("SIGTERM", shutdown);
34935
+ if (!capture) process.exit(0);
34765
34936
  }
34766
34937
  function loadRules(rulesFile) {
34767
34938
  if (!rulesFile) return [];
package/lib/cli.d.ts CHANGED
@@ -13,5 +13,21 @@ interface CliContext {
13
13
  err?: (line: string) => void;
14
14
  }
15
15
  export declare function runCli(ctx: CliContext): Promise<CliResult>;
16
+ export type AliveGatewayReality = 'serving' | 'booting' | 'zombie';
17
+ /** Classify what a TRACKED-ALIVE gateway pid really is. ALIVE ≠ SERVING: the tracked pid is the
18
+ * `nx run` WRAPPER, and the process that actually binds :443 is its run-executor child. When that
19
+ * child crashes while the wrapper wedges (a lost nx-daemon connection), the pid file points at a live
20
+ * process that serves NOTHING — the state that made `start` a no-op forever ("already running") while
21
+ * the workspace 404'd. So:
22
+ * - serving: this workspace's gateway (the wrapper or a descendant) holds :443 — genuinely up.
23
+ * - booting: :443 is unbound and the spawn is fresh — the listeners just haven't bound yet.
24
+ * - zombie: past the boot window with nothing of ours on :443, or the port is already held by
25
+ * someone else (a boot could never win it) — dead weight to reap. */
26
+ export declare function classifyAliveGateway(o: {
27
+ oursOn443: boolean;
28
+ port443Held: boolean;
29
+ pidFileAgeMs: number | undefined;
30
+ bootGraceMs?: number;
31
+ }): AliveGatewayReality;
16
32
  export {};
17
33
  //# sourceMappingURL=cli.d.ts.map
package/lib/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAqFD,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA4ChE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAqFD,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA4ChE;AA0lBD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEnE;;;;;;;;iFAQiF;AACjF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,mBAAmB,CAItB"}
package/lib/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { request as httpRequest } from 'node:http';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { basename, dirname, join, relative, resolve } from 'node:path';
@@ -267,15 +267,36 @@ function runGateway(ctx, args, out, err) {
267
267
  const registeredMarker = join(cwd, '.lensmcp', 'registered.json');
268
268
  const dashUrl = `https://lensmcp.local${cfg.dashboardBasePath}/`;
269
269
  const start = () => {
270
- // Reconcile with reality FIRST: an alive pid-file wins; else adopt an orphaned gateway on :443 (the
271
- // desync self-heal) — so a second `start` NEVER spawns a duplicate that EADDRINUSE-crashes + orphans.
270
+ // Reconcile with reality FIRST: adopt an orphaned gateway on :443 (the desync self-heal) — so a
271
+ // second `start` NEVER spawns a duplicate that EADDRINUSE-crashes + orphans.
272
272
  const { pid: running, healed } = reconcileGateway(pidFile, cwd);
273
273
  if (running !== undefined && isAlive(running)) {
274
- if (healed)
275
- out(`healed: adopted a running gateway (pid ${running}) the pid file had lost no duplicate started.`);
276
- out(`gateway already running (pid ${running}).`);
277
- out(` dashboard ${dashUrl}`);
278
- return { exitCode: 0 };
274
+ // Bare liveness is NOT enough — classify what the alive pid really is (see classifyAliveGateway:
275
+ // the `nx run` wrapper can outlive its crashed run-executor child, the actual :443 binder).
276
+ const reality = classifyAliveGateway({
277
+ oursOn443: gatewayOnPort443(cwd) !== undefined,
278
+ port443Held: pidsOnPort443().length > 0,
279
+ pidFileAgeMs: pidFileAgeMs(pidFile),
280
+ });
281
+ if (reality === 'serving') {
282
+ if (healed)
283
+ out(`healed: adopted a running gateway (pid ${running}) the pid file had lost — no duplicate started.`);
284
+ out(`gateway already running (pid ${running}).`);
285
+ out(` dashboard → ${dashUrl}`);
286
+ return { exitCode: 0 };
287
+ }
288
+ if (reality === 'booting') {
289
+ out(`gateway starting (pid ${running}) — :443 not bound yet.`);
290
+ out(` dashboard → ${dashUrl}`);
291
+ out(` logs → ${logFile}`);
292
+ return { exitCode: 0 };
293
+ }
294
+ // A zombie: reap the wrapper tree (it would hold the nx run-lock against a respawn), clear the
295
+ // stale nx daemon state, and fall through to the real paths (register into a live daemon / spawn).
296
+ killGatewayTree(running, 'SIGKILL');
297
+ rmFile(pidFile);
298
+ refreshNxGraph(cwd, ctx.env, () => { });
299
+ out(`reaped a dead gateway (pid ${running} was alive but nothing served :443 for this workspace).`);
279
300
  }
280
301
  // :443 is held by another process. If it's a lens DAEMON (its control socket answers), REGISTER this
281
302
  // workspace INTO it — the daemon then hosts this workspace's services + dashboard under the one :443
@@ -283,7 +304,20 @@ function runGateway(ctx, args, out, err) {
283
304
  // EADDRINUSE-crash + orphan).
284
305
  const foreign = pidsOnPort443()[0];
285
306
  if (foreign !== undefined) {
286
- if (daemonRequest('GET', '/list') !== null) {
307
+ const list = daemonRequest('GET', '/list');
308
+ if (list !== null) {
309
+ // Idempotent for a GUEST too: an already-registered workspace short-circuits — re-registering
310
+ // would make the daemon tear down + respawn its services and dashboard for nothing.
311
+ const registered = list.status === 200 && list.json && typeof list.json === 'object'
312
+ ? (list.json.workspaces ?? []).some((w) => w.key === cfg.key)
313
+ : false;
314
+ if (registered) {
315
+ mkdirSync(dirname(registeredMarker), { recursive: true });
316
+ writeFileSync(registeredMarker, JSON.stringify({ wsKey: cfg.key }));
317
+ out(`already registered into the shared gateway daemon (pid ${foreign}).`);
318
+ out(` dashboard → ${dashUrl}`);
319
+ return { exitCode: 0 };
320
+ }
287
321
  const res = daemonRequest('POST', '/register', { wsKey: cfg.key, root: cwd, projects: buildProjectsMap(cwd) });
288
322
  if (res && res.status === 200) {
289
323
  mkdirSync(dirname(registeredMarker), { recursive: true });
@@ -330,8 +364,13 @@ function runGateway(ctx, args, out, err) {
330
364
  };
331
365
  const stop = () => {
332
366
  // If THIS workspace registered into a shared daemon (P4), UNREGISTER from it instead of killing a
333
- // gateway it never spawned — the daemon reaps this workspace's services + dashboard.
334
- if (existsSync(registeredMarker)) {
367
+ // gateway it never spawned — the daemon reaps this workspace's services + dashboard. But OWNERSHIP
368
+ // BEATS THE MARKER: when :443 is held by THIS workspace's OWN gateway, the marker is stale litter
369
+ // (left over from a guest era before this workspace's daemon took over). Honoring it would
370
+ // self-unregister and LEAVE THE OWNED DAEMON RUNNING (observed live: tetros owned the :443 daemon,
371
+ // yet `stop` "unregistered 'tetros'" from itself and the daemon kept serving). Guest path only when
372
+ // we genuinely do NOT own the port; otherwise clear the lying marker and fall through to the kill.
373
+ if (existsSync(registeredMarker) && gatewayOnPort443(cwd) === undefined) {
335
374
  let wsKey = cfg.key;
336
375
  try {
337
376
  wsKey = JSON.parse(readFileSync(registeredMarker, 'utf8')).wsKey ?? cfg.key;
@@ -342,6 +381,7 @@ function runGateway(ctx, args, out, err) {
342
381
  out(res && res.status === 200 ? `unregistered '${wsKey}' from the shared gateway daemon.` : 'unregister sent (the daemon may already be gone).');
343
382
  return { exitCode: 0 };
344
383
  }
384
+ rmFile(registeredMarker); // a stale marker on the daemon OWNER — clear it before the kill path
345
385
  // Reality-based: kill the tracked pid OR an orphaned gateway the pid file lost track of (so `stop`
346
386
  // works even after the desync — the case where the old CLI reported "not running" but :443 was held).
347
387
  const { pid, healed } = reconcileGateway(pidFile, cwd);
@@ -387,7 +427,16 @@ function runGateway(ctx, args, out, err) {
387
427
  };
388
428
  const status = () => {
389
429
  const { pid, healed } = reconcileGateway(pidFile, cwd);
390
- const alive = pid !== undefined && isAlive(pid);
430
+ // ALIVE SERVING (see classifyAliveGateway) a zombie `nx run` wrapper whose executor child died
431
+ // must not report "running — DAEMON on :443" while the workspace 404s.
432
+ const reality = pid !== undefined && isAlive(pid)
433
+ ? classifyAliveGateway({
434
+ oursOn443: gatewayOnPort443(cwd) !== undefined,
435
+ port443Held: pidsOnPort443().length > 0,
436
+ pidFileAgeMs: pidFileAgeMs(pidFile),
437
+ })
438
+ : undefined;
439
+ const alive = reality === 'serving' || reality === 'booting';
391
440
  // Machine-readable status for the IDE plugin / scripts: the daemon's rich /status (identity + workspaces
392
441
  // + per-service live state) plus THIS workspace's role + the dashboard URL. Degrades to a minimal object
393
442
  // when no daemon is reachable, so a consumer always gets valid JSON.
@@ -423,7 +472,9 @@ function runGateway(ctx, args, out, err) {
423
472
  };
424
473
  if (alive) {
425
474
  // This workspace's own pid owns :443 → it IS the daemon (it may be hosting other workspaces too).
426
- out(`gateway: running (pid ${pid}) — DAEMON on :443${healed ? ' — recovered a stale pid file' : ''}`);
475
+ out(reality === 'booting'
476
+ ? `gateway: starting (pid ${pid}) — :443 not bound yet`
477
+ : `gateway: running (pid ${pid}) — DAEMON on :443${healed ? ' — recovered a stale pid file' : ''}`);
427
478
  out(` workspace → ${cfg.key}`);
428
479
  out(` dashboard → ${dashUrl}`);
429
480
  out(` chooser → https://lensmcp.local/`);
@@ -441,6 +492,14 @@ function runGateway(ctx, args, out, err) {
441
492
  printWorkspaces();
442
493
  return { exitCode: 0 };
443
494
  }
495
+ if (reality === 'zombie') {
496
+ out(`gateway: NOT serving — tracked pid ${pid} is alive but nothing holds :443 for this workspace`);
497
+ out(' (a crashed gateway under a wedged nx wrapper) — run `lensmcp gateway restart` to recover.');
498
+ out(` workspace → ${cfg.key}`);
499
+ out(` logs → ${logFile}`);
500
+ printWorkspaces();
501
+ return { exitCode: 1 };
502
+ }
444
503
  out('gateway: stopped');
445
504
  out(` workspace → ${cfg.key}`);
446
505
  out(` dashboard → ${dashUrl}`);
@@ -596,6 +655,33 @@ function gatewayOnPort443(cwd) {
596
655
  }
597
656
  return undefined;
598
657
  }
658
+ /** How long since `start` wrote the pid file (ms) — the spawn timestamp. undefined when unreadable. */
659
+ function pidFileAgeMs(pidFile) {
660
+ try {
661
+ return Date.now() - statSync(pidFile).mtimeMs;
662
+ }
663
+ catch {
664
+ return undefined;
665
+ }
666
+ }
667
+ /** A freshly-spawned gateway legitimately holds NO port yet — services + lens children boot before the
668
+ * listeners bind. Give a spawn this long before an alive-but-portless pid is judged a zombie. */
669
+ const BOOT_GRACE_MS = 180_000;
670
+ /** Classify what a TRACKED-ALIVE gateway pid really is. ALIVE ≠ SERVING: the tracked pid is the
671
+ * `nx run` WRAPPER, and the process that actually binds :443 is its run-executor child. When that
672
+ * child crashes while the wrapper wedges (a lost nx-daemon connection), the pid file points at a live
673
+ * process that serves NOTHING — the state that made `start` a no-op forever ("already running") while
674
+ * the workspace 404'd. So:
675
+ * - serving: this workspace's gateway (the wrapper or a descendant) holds :443 — genuinely up.
676
+ * - booting: :443 is unbound and the spawn is fresh — the listeners just haven't bound yet.
677
+ * - zombie: past the boot window with nothing of ours on :443, or the port is already held by
678
+ * someone else (a boot could never win it) — dead weight to reap. */
679
+ export function classifyAliveGateway(o) {
680
+ if (o.oursOn443)
681
+ return 'serving';
682
+ const fresh = o.pidFileAgeMs !== undefined && o.pidFileAgeMs < (o.bootGraceMs ?? BOOT_GRACE_MS);
683
+ return !o.port443Held && fresh ? 'booting' : 'zombie';
684
+ }
599
685
  /** Reconcile the tracked pid with reality: a LIVE pid-file wins; otherwise ADOPT an orphaned gateway
600
686
  * still holding :443 (rewriting the pid file) — the self-heal for the stale-pid desync. */
601
687
  function reconcileGateway(pidFile, cwd) {
@@ -673,9 +759,10 @@ function firstClusterHost(cwd) {
673
759
  return undefined;
674
760
  }
675
761
  function rmFile(path) {
762
+ // DELETE, not truncate: an emptied-but-existing registered.json still `existsSync`s, so status/stop
763
+ // would keep treating an UNREGISTERED workspace as a guest (the lying-marker bug).
676
764
  try {
677
- if (existsSync(path))
678
- writeFileSync(path, '');
765
+ rmSync(path, { force: true });
679
766
  }
680
767
  catch {
681
768
  /* ignore */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.16.26",
3
+ "version": "1.16.28",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -2,7 +2,7 @@
2
2
  "name": "lensmcp",
3
3
  "displayName": "LensMCP",
4
4
  "description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
5
- "version": "1.16.26",
5
+ "version": "1.16.28",
6
6
  "author": {
7
7
  "name": "David Antoon",
8
8
  "email": "davidmantoon@gmail.com"