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.
- package/bundled/capture-runner.js +566 -395
- package/lib/cli.d.ts +16 -0
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +102 -15
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
|
@@ -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
|
|
1299
|
-
mkdirSync(
|
|
1300
|
-
return
|
|
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
|
|
1962
|
-
|
|
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(
|
|
33209
|
+
function addCommand(chrome2, domainName, command) {
|
|
33210
33210
|
const commandName = `${domainName}.${command.name}`;
|
|
33211
33211
|
const handler = (params, sessionId, callback) => {
|
|
33212
|
-
return
|
|
33212
|
+
return chrome2.send(commandName, params, sessionId, callback);
|
|
33213
33213
|
};
|
|
33214
33214
|
decorate(handler, "command", command);
|
|
33215
|
-
|
|
33215
|
+
chrome2[commandName] = chrome2[domainName][command.name] = handler;
|
|
33216
33216
|
}
|
|
33217
|
-
function addEvent(
|
|
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
|
-
|
|
33227
|
-
return () =>
|
|
33226
|
+
chrome2.on(rawEventName, handler2);
|
|
33227
|
+
return () => chrome2.removeListener(rawEventName, handler2);
|
|
33228
33228
|
} else {
|
|
33229
33229
|
return new Promise((fulfill, reject) => {
|
|
33230
|
-
|
|
33230
|
+
chrome2.once(rawEventName, fulfill);
|
|
33231
33231
|
});
|
|
33232
33232
|
}
|
|
33233
33233
|
};
|
|
33234
33234
|
decorate(handler, "event", event);
|
|
33235
|
-
|
|
33235
|
+
chrome2[eventName] = chrome2[domainName][event.name] = handler;
|
|
33236
33236
|
}
|
|
33237
|
-
function addType(
|
|
33237
|
+
function addType(chrome2, domainName, type) {
|
|
33238
33238
|
const typeName = `${domainName}.${type.id}`;
|
|
33239
33239
|
const help = {};
|
|
33240
33240
|
decorate(help, "type", type);
|
|
33241
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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:
|
|
271
|
-
//
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
678
|
-
writeFileSync(path, '');
|
|
765
|
+
rmSync(path, { force: true });
|
|
679
766
|
}
|
|
680
767
|
catch {
|
|
681
768
|
/* ignore */
|
package/package.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.16.28",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "David Antoon",
|
|
8
8
|
"email": "davidmantoon@gmail.com"
|