@buoy-gg/mcp 7.0.58 → 7.0.59
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/dist/{chunk-XUPR22SJ.mjs → chunk-LRFKXGF6.mjs} +2 -2
- package/dist/{chunk-XDBVI5HB.mjs → chunk-ORAXGCZM.mjs} +1 -1
- package/dist/cli.js +6 -6
- package/dist/cli.mjs +2 -2
- package/dist/{flowRun-5HGAFDNI.mjs → flowRun-WDIVQCCL.mjs} +1 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -37,8 +37,8 @@ for(const s of sources){const b=document.createElement('button');b.className='ch
|
|
|
37
37
|
v.ontimeupdate=()=>{const ms=v.currentTime*1000;let cur=null;for(const li of list.children){li.classList.remove('now');if(+li.dataset.t<=ms)cur=li}if(cur){cur.classList.add('now');if(!v.paused)cur.scrollIntoView({block:'nearest'})}};
|
|
38
38
|
draw();
|
|
39
39
|
<\/script></body></html>
|
|
40
|
-
`}function escapeHtml(e){return e.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var import_node_child_process3,import_node_fs2,import_node_path2,MAX_RECORDING_MS,live;var init_recorder=__esm({"src/capture/recorder.ts"(){"use strict";import_node_child_process3=require("child_process");import_node_fs2=require("fs");import_node_path2=__toESM(require("path"));init_adb();init_iosDevice();MAX_RECORDING_MS=3*6e4;live=new Set}});function readProjectKey(e,n=process.env){for(const t of KEY_NAMES)if(n[t]!==void 0)return(0,import_headless.normalizeLicenseKey)(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let r;try{r=(0,import_node_fs3.readFileSync)((0,import_node_path3.join)(e,t),"utf8")}catch{continue}const i=new Map;for(const s of r.split(/\r?\n/)){const o=s.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!o)continue;const a=o[2].trim();const c=a.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);i.set(o[1],c?c[2]:a.split("#")[0].trim())}for(const s of KEY_NAMES)if(i.has(s))return(0,import_headless.normalizeLicenseKey)(i.get(s))||null}return null}var import_node_fs3,import_node_path3,import_node_os2,import_node_crypto2,import_headless,KEY_NAMES,NodeAccount;var init_NodeAccount=__esm({"src/sync/NodeAccount.ts"(){"use strict";import_node_fs3=require("fs");import_node_path3=require("path");import_node_os2=require("os");import_node_crypto2=require("crypto");import_headless=require("@buoy-gg/license/headless");KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];NodeAccount=class{constructor(e,n={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.unsubscribeSession=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>this.hasKeyAccess()||this.session?.getStatus().state==="active";this.lastSessionSync=0;this.projectDir=(0,import_node_path3.resolve)(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto2.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??(0,import_node_path3.join)((0,import_node_os2.homedir)(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:(0,import_headless.createNodeSession)({audience:import_headless.CLI_AUDIENCE,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return(0,import_headless.resolveAccountAccess)({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted"}start(){if(this.timer)return;if(this.session){this.unsubscribeSession=this.session.subscribe(()=>this.notify());void this.session.load().then(()=>this.notify(),()=>{})}try{const e=JSON.parse((0,import_node_fs3.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;this.unsubscribeSession?.();this.unsubscribeSession=null;if(this.timer)clearInterval(this.timer);this.timer=null;this.key=null;this.cache=null;this.notify()}notify(){const e=this.hasAccess();if(e===this.lastAdmission)return;this.lastAdmission=e;this.changed()}async tick(){if(this.session&&Date.now()-this.lastSessionSync>15e3){this.lastSessionSync=Date.now();void this.session.sync().catch(()=>{})}const e=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const n=this.generation;try{if((0,import_headless.isDevToken)(e)){const a=(0,import_headless.checkDevToken)(e);if(!a.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),a.claims.iat*1e3);const u=a.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${a.claims.sid}`,valid:true,tier:u,isPro:u==="pro",expiresAt:new Date(a.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}if((0,import_headless.isServiceKey)(e)){const a=await(0,import_headless.exchangeServiceKey)(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;if(!a.ok){if(a.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL}else{this.nextCheck=Date.now()+(import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]??import_headless.LICENSE_REVALIDATION_INTERVAL)}return}const c=Date.now();const u=a.claims.exp*1e3;this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`service:${a.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(u).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,u-60*60*1e3);return}if((0,import_headless.isBuoyToken)(e)){const a=(0,import_headless.verifyFreeToken)(e);if(!a){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${a.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;const r=t.data?.relationships?.product?.data?.id;const i=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(r&&r!==import_headless.KEYGEN_CONFIG.productId||!i&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const s=i?"free":(0,import_headless.resolveTier)(t.data);const o=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:s,isPro:s==="pro",expiresAt:i?null:t.data.attributes.expiry,cachedAt:o,lastValidatedAt:o});this.persistCache();this.retry=0;this.nextCheck=o+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(n!==this.generation)return;const r=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(r??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs3.mkdirSync)((0,import_node_path3.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs3.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs3.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs3.unlinkSync)(this.cacheFile)}catch{}}}}});var import_sync_broker,BROKER_PORT,BROKER_URL,DEFAULT_ACTION_TIMEOUT_MS,UNRESPONSIVE_ACTION_TIMEOUT_MS,UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS,RECOVERY_PROBE_MS,UNRESPONSIVE_AFTER_MS,DEFAULT_SNAPSHOT_TIMEOUT_MS,DEVICE_RETENTION_MS,DASHBOARD_NAME,ALL_DEVICES;var init_config=__esm({"src/config.ts"(){"use strict";import_sync_broker=require("@buoy-gg/sync-broker");BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;BROKER_URL=`http://localhost:${BROKER_PORT}`;DEFAULT_ACTION_TIMEOUT_MS=15e3;UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;RECOVERY_PROBE_MS=2e4;UNRESPONSIVE_AFTER_MS=3e4;DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;DEVICE_RETENTION_MS=15*6e4;DASHBOARD_NAME="Dashboard";ALL_DEVICES="All"}});async function devServerRoot(e){let n;try{n=new URL(e)}catch{return null}if(!isLocalHost(n.hostname))return null;const t=n.port||(n.protocol==="https:"?"443":"80");try{const{stdout:r}=await execFileAsync3("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const i=r.split("\n").map(a=>a.trim()).find(Boolean);if(!i)return null;const{stdout:s}=await execFileAsync3("lsof",["-a","-p",i,"-d","cwd","-Fn"],{timeout:3e3});const o=s.split("\n").find(a=>a.startsWith("n"));return o?import_node_path4.default.resolve(o.slice(1)):null}catch{return null}}function isLocalHost(e,n=import_node_os3.default.networkInterfaces()){const t=e.replace(/^\[|\]$/g,"").toLowerCase();if(t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="10.0.2.2"||t==="10.0.3.2")return true;return Object.values(n).flat().some(r=>r?.address?.toLowerCase()===t)}function pickByProject(e,n){const t=import_node_path4.default.resolve(n);const r=e.map(o=>({deviceId:o.deviceId,score:o.root?closeness(import_node_path4.default.resolve(o.root),t):-1})).filter(o=>o.score>=0);if(r.length===0)return null;const i=Math.min(...r.map(o=>o.score));const s=r.filter(o=>o.score===i);return s.length===1?s[0].deviceId:null}function closeness(e,n){if(e===n)return 0;const t=import_node_path4.default.relative(e,n);const r=import_node_path4.default.relative(n,e);const i=s=>s&&!s.startsWith("..")&&!import_node_path4.default.isAbsolute(s);if(i(t))return t.split(import_node_path4.default.sep).length;if(i(r))return r.split(import_node_path4.default.sep).length;return-1}var import_node_child_process4,import_node_util3,import_node_path4,import_node_os3,execFileAsync3;var init_projectRoot=__esm({"src/sync/projectRoot.ts"(){"use strict";import_node_child_process4=require("child_process");import_node_util3=require("util");import_node_path4=__toESM(require("path"));import_node_os3=__toESM(require("os"));execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process4.execFile)}});function isRecoveryAction(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var init_actionTimeout=__esm({"src/sync/actionTimeout.ts"(){"use strict"}});function probeBroker(e,n=800){return new Promise(t=>{const r=import_node_http.default.get(e,i=>{i.resume();t((i.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function waitForBroker(e,n=20){for(let t=0;t<n;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var import_node_http,import_node_crypto3,import_socket,import_sync_broker2,NOT_READY_MARK,SyncClient,snapKey,delay;var init_SyncClient=__esm({"src/sync/SyncClient.ts"(){"use strict";init_NodeAccount();import_node_http=__toESM(require("http"));import_node_crypto3=require("crypto");import_socket=require("socket.io-client");import_sync_broker2=require("@buoy-gg/sync-broker");init_config();init_projectRoot();init_actionTimeout();NOT_READY_MARK="Not ready:";SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.createdAt=Date.now();this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.sessionDevice=null;this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.crashSeen=new Map;this.rootLookups=new Set;this.devServerRoots=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.deviceConnections=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
|
|
41
|
-
`));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url)||await probeBroker(this.url,8e3);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();let t=false;this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:r=>{if(r.code==="EADDRINUSE")t=true;this.log(`broker error: ${r.code??r.message}`)}});await waitForBroker(this.url).catch(r=>{if(!t)throw r});if(!this.broker.httpServer.listening)t=true;if(t){this.log(`another broker holds :${this.port} \u2014 connecting to it as a dashboard`);await this.broker.close().catch(()=>void 0);this.broker=null;this.hostAccount.stop();this.hostAccount=null;await this.connectSocket();return"connected"}try{(0,import_sync_broker2.writeBrokerToken)(this.broker.dashboardToken,(0,import_sync_broker2.brokerTokenPath)(this.port))}catch(r){this.log(`could not write the broker token file: ${r.message}`)}n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:a=>{const c=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));a(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const i=()=>{if(r)return;r=true;this.log("connected to broker");e()};const s=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const c=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(c);if(c){clearTimeout(s);this.rearmWatches();i()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const o=a=>a.message===import_sync_broker2.DASHBOARD_TOKEN_REQUIRED;t.once("connect_error",a=>{if(r)return;r=true;clearTimeout(s);n(new Error(o(a)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${(0,import_sync_broker2.brokerTokenPath)(this.port)} is missing or stale. Update Buoy Desktop, or restart it so it writes the file.`:`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("connect_error",a=>{if(!o(a)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);this.trackRootLookup(this.learnDevServerOrigin(a.persistentDeviceId));if(a.tools?.some(c=>c.toolId==="console"))this.ensureWatching("console",a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let c=false;for(const l of a){if(l.deviceId&&l.tier==="locked")this.lockDevice(String(l.deviceId));else if(l.deviceId&&(l.tier==="free"||l.tier==="pro")){c=this.lockedDevices.delete(String(l.deviceId))||c}}this.devices=a;const u=Date.now();for(const l of a)if(l.deviceId)this.lastSeen.set(l.deviceId,u);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(a)});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const c=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(c,a.payload);this.snapshotAt.set(c,Date.now());if(a.toolId==="console")this.noteCrashReport(a.persistentDeviceId,a.payload);const u=this.snapshotWaiters.get(c);if(u&&u.length){this.snapshotWaiters.set(c,[]);for(const l of u)l(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const c=this.pendingActions.get(a.requestId);if(!c)return;this.pendingActions.delete(a.requestId);clearTimeout(c.timer);c.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.devServerRoots.clear();this.crashSeen.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.devServerRoots.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const i of r)i(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===ALL_DEVICES){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(i=>i.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.devServerRoots.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const i of[...this.snapshots.keys()]){if(i.startsWith(r)){this.snapshots.delete(i);this.snapshotAt.delete(i)}}for(const i of[...this.watching.keys()]){if(i.startsWith(r))this.watching.delete(i)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(u,l)=>new Error(`${NOT_READY_MARK} "${u}" (${l}) is not connected right now. If it is restarting, it usually takes a few seconds; to use another device, pass deviceId (run list_devices to see them).`);if(this.sessionDevice){const u=this.devices.find(l=>l.deviceId===this.sessionDevice);if(u&&u.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const r=t.filter(u=>!this.lockedDevices.has(u.deviceId));const i=r.filter(u=>u.tools.length>0);const s=process.env.BUOY_PROJECT_ROOT||process.cwd();const o=pickByProject(i.map(u=>({deviceId:u.deviceId,root:this.devServerRoots.get(u.deviceId)})),s);if(o)return this.sessionDevice=o;const a=pickByProject(this.devices.filter(u=>u.deviceId&&u.isConnected===false&&!this.lockedDevices.has(u.deviceId)).map(u=>({deviceId:u.deviceId,root:this.devServerRoots.get(u.deviceId)})),s);if(a)throw n(a,"this project's app");if(r.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(u=>u.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(r.length===1)return this.sessionDevice=r[0].deviceId;if(i.length===1)return this.sessionDevice=i[0].deviceId;if(i.length===0){const u=r.map(l=>l.deviceName).join(", ");throw new Error(`Connected devices (${u}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const c=i.map(u=>`${u.deviceName} (${u.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${c}`)}requireProDevice(e){this.requireAdmission();if(e&&e!==ALL_DEVICES)this.sessionDevice=e;const n=s=>{if(!s)return"";return s.tier==="locked"?` The broker has not confirmed account access for "${s.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${s.deviceName}" is on the ${s.tier} tier.`};const t=s=>`Buoy Pro is required to use the MCP.${n(s)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(s=>s.isPro))throw new Error(t());return ALL_DEVICES}const r=this.resolveDevice(e);const i=this.connectedDevices().find(s=>s.deviceId===r);if(!i?.isPro){const s=this.accessState(r);if(s==="offline")throw new Error(`${NOT_READY_MARK} "${r}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(s==="pending")throw new Error(`${NOT_READY_MARK} account access for "${r}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(t(i))}return r}accessState(e){const n=this.devices.find(t=>t.deviceId===e);if(!n||n.isConnected===false)return"offline";if(n.isPro===true)return"ready";return n.tier===void 0?"pending":"denied"}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:i,usedFullBudget:s}=actionTimeoutMs({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(s)r.lastFullBudgetAt=Date.now();return i}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=diagnoseCrashed({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const i=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const s=this.lastFatalFor(e);if(s)i.push(`Last captured error: ${s}`);i.push(s?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return i.join(" ")}noteCrashReport(e,n){const t=n?.entries;if(!Array.isArray(t)||t.length===0)return;const r=Math.max(...t.map(o=>typeof o?.timestamp==="number"?o.timestamp:-Infinity));const i=this.crashSeen.get(e)??new Set;let s=false;for(const o of t){const a=o?.fatal||/^\[(RENDER ERROR|FATAL)/.test(o?.message??"");if(!a||typeof o.timestamp!=="number")continue;const c=`${o.id??""}|${o.timestamp}|${(o.message??"").slice(0,120)}`;if(i.has(c))continue;i.add(c);if(r-o.timestamp<3e4)s=true}if(i.size>200)for(const o of[...i].slice(0,i.size-200))i.delete(o);this.crashSeen.set(e,i);if(!s)return;if(![...this.pendingActions.values()].some(o=>o.target===e&&o.expire&&!o.cut))return;void this.callAction("app","ping",{},e,{timeoutMs:1500,silent:true}).then(()=>void 0,()=>{const o=Date.now();for(const a of this.pendingActions.values()){if(a.target!==e||!a.expire||a.cut)continue;a.cut=true;if(a.deadline!==void 0&&a.deadline-o<=UNRESPONSIVE_ACTION_TIMEOUT_MS)continue;clearTimeout(a.timer);a.timer=setTimeout(a.expire,UNRESPONSIVE_ACTION_TIMEOUT_MS)}})}lastFatalFor(e){const n=this.snapshots.get(snapKey(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const i=t[r];if(i?.fatal||i?.level==="error"){const s=(i.message??"").split("\n")[0];if(s)return s.length>300?`${s.slice(0,300)}\u2026`:s}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(snapKey(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){const t=this.devServerOrigins.get(e)!==n;this.devServerOrigins.set(e,n);if(t)this.devServerRoots.delete(e);if(t||!this.devServerRoots.has(e)){const r=devServerRoot(n).then(i=>{if(i&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,i)});this.trackRootLookup(r)}}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}trackRootLookup(e){const n=e.catch(()=>void 0).finally(()=>this.rootLookups.delete(n));this.rootLookups.add(n)}async awaitBroker(){if(Date.now()-this.createdAt>15e3)return;for(let e=0;!this.admitted&&e<4e3;e+=100)await new Promise(n=>setTimeout(n,100));for(let e=0;this.admitted&&this.devices.length===0&&e<1500;e+=100)await new Promise(n=>setTimeout(n,100))}async settleDeviceChoice(e=1500){await this.awaitBroker();if(this.sessionDevice||this.rootLookups.size===0||this.connectedDevices().length<2)return;await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,e))]);if(this.rootLookups.size)await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,Math.min(800,e)))])}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=snapKey(n,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:n});this.sendWatch(e,n)}sendWatch(e,n){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(n))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:n}of this.watching.values()){this.sendWatch(e,n)}}rearmReconnectedDevices(e){for(const n of e){if(!n.deviceId||n.isConnected===false)continue;const t=String(n.deviceId);const r=`${n.id}|${n.connectionNonce??""}`;const i=this.deviceConnections.get(t);this.deviceConnections.set(t,r);if(i===void 0||i===r)continue;for(const{toolId:s,targetDeviceId:o}of this.watching.values()){if(o===t)this.sendWatch(s,o)}}}unwatch(e,n){const t=snapKey(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const i=snapKey(r,e);this.ensureWatching(e,r);let s=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const u=this.healthOf(r);if(Date.now()-u.lastFullBudgetAt>RECOVERY_PROBE_MS){u.lastFullBudgetAt=Date.now()}else{s=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const o=this.waitForSnapshot(i,s);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const a=await o;this.requireAdmission();this.requireDeviceAccount(r);if(a!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:a}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(i);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}lastSync(e,n){const t=this.snapshots.get(snapKey(e,n));return t===void 0?void 0:{payload:t,ageMs:this.snapshotAgeMs(n,e)}}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const i=setTimeout(()=>{const o=(this.snapshotWaiters.get(e)??[]).filter(a=>a!==s);this.snapshotWaiters.set(e,o);t(void 0)},n);const s=o=>{clearTimeout(i);t(o)};r.push(s);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,i={}){this.requireAdmission();const s=r===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const o=(0,import_node_crypto3.randomUUID)();const a=i.timeoutMs??this.timeoutFor(s,e,n);const c=await new Promise((u,l)=>{const d=Date.now();const h=()=>{this.pendingActions.delete(o);if(s!==ALL_DEVICES&&!i.silent)this.noteTimeout(s);l(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-d}ms. `+(s===ALL_DEVICES?"No device responded.":this.unresponsiveNote(s))))};const p=setTimeout(h,a);this.pendingActions.set(o,{resolve:u,reject:l,timer:p,target:s,expire:h,deadline:d+a});this.socket.emit("devtool-action",{requestId:o,toolId:e,targetDeviceId:s,action:n,params:t??{}})});if(s!==ALL_DEVICES&&!i.silent)this.noteOk(s);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};snapKey=(e,n)=>`${e}::${n}`;delay=e=>new Promise(n=>setTimeout(n,e))}});var VERSION,PACKAGE_NAME;var init_version=__esm({"src/version.ts"(){"use strict";VERSION="7.0.58"?"7.0.58":"0.0.0-dev";PACKAGE_NAME="@buoy-gg/mcp"}});function isNewer(e,n){const t=s=>s.split("-")[0].split(".").map(o=>Number(o)||0);const r=t(e);const i=t(n);for(let s=0;s<3;s++){const o=r[s]??0;const a=i[s]??0;if(o>a)return true;if(o<a)return false}return false}async function readCache(){try{const e=await import_node_fs4.promises.readFile(CACHE_FILE,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function fetchLatest(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),FETCH_TIMEOUT_MS);try{const r=await fetch(REGISTRY_URL,{signal:n.signal});if(!r.ok)return null;const i=await r.json();const s=typeof i.version==="string"?i.version:null;if(s){await import_node_fs4.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:s})).catch(()=>{})}return s}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const n=await readCache();if(n&&e-n.checkedAt<CHECK_TTL_MS)return n.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const n=await resolveLatest(e);if(n&&isNewer(n,VERSION)){return`A newer buoy-mcp is available (${n}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}var import_node_fs4,import_node_os4,import_node_path5,CHECK_TTL_MS,FETCH_TIMEOUT_MS,REGISTRY_URL,CACHE_FILE;var init_checkForUpdate=__esm({"src/update/checkForUpdate.ts"(){"use strict";import_node_fs4=require("fs");import_node_os4=__toESM(require("os"));import_node_path5=__toESM(require("path"));init_version();CHECK_TTL_MS=24*60*60*1e3;FETCH_TIMEOUT_MS=1500;REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;CACHE_FILE=import_node_path5.default.join(import_node_os4.default.tmpdir(),"buoy-mcp-update-check.json")}});function humanizeSimctlError(e){const n=e;if(n?.code==="ENOENT"){return"Xcode command-line tools (xcrun) not found. Install Xcode to capture the iOS Simulator."}const t=String(n?.stderr||n?.message||e);if(/No devices are booted|Unable to find device|current device/i.test(t)){return"No booted iOS Simulator found. Boot the Simulator with your app running, then try again."}return t.trim()||"Failed to capture the iOS Simulator."}function pngSize(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,r)=>e[r]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}async function listDir(e){try{return await import_node_fs5.promises.readdir(e)}catch{return[]}}async function webViewInstallIds(e){const n=[];for(const t of await listDir(e))n.push(...await containerWebViewInstallIds(import_node_path6.default.join(e,t)));return n}async function containerWebViewInstallIds(e){const n=[];const t=import_node_path6.default.join(e,"Library","WebKit");for(const r of await listDir(t)){const i=import_node_path6.default.join(t,r,"WebsiteData","Default");for(const s of await listDir(i)){for(const o of await listDir(import_node_path6.default.join(i,s))){const a=import_node_path6.default.join(i,s,o,"LocalStorage","localstorage.sqlite3");try{await import_node_fs5.promises.access(a);const{stdout:c}=await execFileAsync4("sqlite3",["-readonly",a,`SELECT hex(value) FROM ItemTable WHERE key='${WEBVIEW_ID_KEY}' LIMIT 1;`],{timeout:1500});const u=c.trim();if(u)n.push(Buffer.from(u,"hex").toString("utf16le").trim())}catch{}}}}return n}async function resolveSimUdid(e){if(process.platform!=="darwin")return{ok:false,error:"Simulator lookup needs macOS."};let n=[];try{const{stdout:r}=await execFileAsync4("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const i=JSON.parse(r).devices;n=Object.entries(i).filter(([s])=>/iOS/.test(s)).flatMap(([,s])=>s.filter(o=>o.state==="Booted"))}catch(r){return{ok:false,error:humanizeSimctlError(r)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const r of n){if(!r.dataPath)continue;const i=import_node_path6.default.join(r.dataPath,"Containers","Data","Application");let s=[];try{s=await import_node_fs5.promises.readdir(i)}catch{continue}for(const o of s){try{const a=(await import_node_fs5.promises.readFile(import_node_path6.default.join(i,o,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(a===t)return{ok:true,udid:r.udid,how:`matched ${e} to "${r.name}"`}}catch{}}}}if(t&&e){const r=n.map(c=>c.udid).sort().join(",");const i=webViewLookups.get(e);const s=i?.booted===r&&(i.udid!==null||Date.now()-i.at<WEBVIEW_MISS_MS);const o=s&&i?.udid?n.find(c=>c.udid===i.udid):void 0;if(o)return{ok:true,udid:o.udid,how:`matched ${e} to "${o.name}" (WebView storage)`};const a=[];if(!s){const c=await Promise.all(n.map(u=>u.dataPath?webViewInstallIds(import_node_path6.default.join(u.dataPath,"Containers","Data","Application")):Promise.resolve([])));c.forEach((u,l)=>{if(u.includes(t))a.push(n[l])});webViewLookups.set(e,{booted:r,udid:a.length===1?a[0].udid:null,at:Date.now()})}if(a.length===1){return{ok:true,udid:a[0].udid,how:`matched ${e} to "${a[0].name}" (WebView storage)`}}if(a.length>1){return{ok:false,error:`${a.length} booted simulators (${a.map(c=>c.name).join(", ")}) hold ${e}. Pass the udid, or shut one down.`}}}if(n.length===1)return{ok:true,udid:n[0].udid,how:`only booted simulator "${n[0].name}"`};return{ok:false,error:`${n.length} iOS Simulators are booted (${n.map(r=>r.name).join(", ")}) and none could be matched to ${e??"this device"}. Shut down the others, or capture from a single booted simulator.`}}async function resolveSims(e){const n=new Map;if(process.platform!=="darwin"||e.length===0)return n;let t=[];try{const{stdout:i}=await execFileAsync4("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const s=JSON.parse(i).devices;t=Object.entries(s).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return n}const r=new Map;for(const i of t){if(!i.dataPath)continue;const s=import_node_path6.default.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await import_node_fs5.promises.readdir(s)}catch{continue}for(const a of o){try{const c=(await import_node_fs5.promises.readFile(import_node_path6.default.join(s,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(c)r.set(c,{name:i.name,udid:i.udid})}catch{}}}for(const i of e){const s=r.get(i.split("-").pop()??"");if(s)n.set(i,s)}return n}async function captureIosFramebuffer(e="booted"){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const n=import_node_path6.default.join(import_node_os5.default.tmpdir(),`buoy-mcp-sim-${(0,import_node_crypto4.randomUUID)()}.png`);try{await execFileAsync4("xcrun",["simctl","io",e,"screenshot","--type=png",n],{timeout:15e3})}catch(t){return{ok:false,error:humanizeSimctlError(t)}}try{const t=await import_node_fs5.promises.readFile(n);const r=pngSize(t);if(!r)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:r.width,height:r.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function cropToBuffer(e,n,t){const r=clamp(Math.round(t.x),0,Math.max(0,n.width-1));const i=clamp(Math.round(t.y),0,Math.max(0,n.height-1));const s=clamp(Math.round(t.width),1,n.width-r);const o=clamp(Math.round(t.height),1,n.height-i);const a=import_node_path6.default.join(import_node_os5.default.tmpdir(),`buoy-mcp-crop-${(0,import_node_crypto4.randomUUID)()}.png`);try{await execFileAsync4("sips",["-c",String(o),String(s),"--cropOffset",String(i),String(r),e,"--out",a],{timeout:1e4});const c=await import_node_fs5.promises.readFile(a);return{ok:true,buffer:c,width:s,height:o}}catch(c){const u=c;return{ok:false,error:`Crop failed: ${u?.stderr||u?.message||c}`}}finally{await import_node_fs5.promises.unlink(a).catch(()=>{});await import_node_fs5.promises.unlink(e).catch(()=>{})}}var import_node_child_process5,import_node_util4,import_node_fs5,import_node_os5,import_node_path6,import_node_crypto4,execFileAsync4,clamp,WEBVIEW_ID_KEY,webViewLookups,WEBVIEW_MISS_MS;var init_simctl=__esm({"src/capture/simctl.ts"(){"use strict";import_node_child_process5=require("child_process");import_node_util4=require("util");import_node_fs5=require("fs");import_node_os5=__toESM(require("os"));import_node_path6=__toESM(require("path"));import_node_crypto4=require("crypto");execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process5.execFile);clamp=(e,n,t)=>Math.min(Math.max(e,n),t);WEBVIEW_ID_KEY="@react_buoy_sync_device_id";webViewLookups=new Map;WEBVIEW_MISS_MS=3e4}});async function listDir2(e){return import_node_fs6.promises.readdir(e).catch(()=>[])}async function containerLocalStorageFiles(e){const n=[];const t=import_node_path7.default.join(e,"Library","WebKit");for(const r of await listDir2(t)){const i=import_node_path7.default.join(t,r,"WebsiteData","Default");for(const s of await listDir2(i)){for(const o of await listDir2(import_node_path7.default.join(i,s))){const a=import_node_path7.default.join(i,s,o);const c=import_node_path7.default.join(a,"LocalStorage","localstorage.sqlite3");if(!await import_node_fs6.promises.access(c).then(()=>true,()=>false))continue;const u=await import_node_fs6.promises.readFile(import_node_path7.default.join(a,"origin")).then(l=>l.toString("hex"),()=>"");if(u)n.push({db:c,origin:u})}}}return n}async function iosReadWebKeep(e){const n=[];for(const{db:t,origin:r}of await containerLocalStorageFiles(e)){const i=await sqlite(t,"SELECT key, hex(value) FROM ItemTable;");for(const s of i.split("\n")){const o=s.lastIndexOf("|");if(o<0)continue;const a=s.slice(0,o);if(WEB_KEEP_KEY.test(a))n.push({key:a,value:s.slice(o+1),origin:r})}}return n}async function iosWriteWebKeep(e,n){if(!n.length)return false;const t=await containerLocalStorageFiles(e);const r=new Set(n.map(i=>i.origin??""));for(const i of r){const s=i?t.find(a=>a.origin===i):void 0;if(!s)return false;const o=n.filter(a=>(a.origin??"")===i&&/^[0-9A-F]*$/i.test(a.value)).map(a=>`('${a.key.replace(/'/g,"''")}', X'${a.value}')`).join(",");if(o)await sqlite(s.db,`INSERT OR REPLACE INTO ItemTable (key, value) VALUES ${o};`,false)}return true}async function iosWaitForLocalStorage(e,n=15e3){const t=Date.now()+n;while(Date.now()<t){if((await containerLocalStorageFiles(e)).length)return true;await new Promise(r=>setTimeout(r,500))}return false}async function evaluateInWebView(e,n,t){const r=(await adbShell(e,`pidof ${n}`).catch(()=>"")).trim().split(/\s+/)[0];if(!r)throw new Error("the app is not running");const i=(await adb(e,["forward","tcp:0",`localabstract:webview_devtools_remote_${r}`])).trim();try{const s=await(await fetch(`http://127.0.0.1:${i}/json`)).json();const o=s.find(a=>a.type==="page"&&a.webSocketDebuggerUrl);if(!o?.webSocketDebuggerUrl)throw new Error("no debuggable WebView page (needs a debug build)");return await new Promise((a,c)=>{const u=new import_ws.WebSocket(o.webSocketDebuggerUrl);const l=setTimeout(()=>{u.close();c(new Error("the WebView did not answer"))},5e3);u.on("open",()=>u.send(JSON.stringify({id:1,method:"Runtime.evaluate",params:{expression:t,returnByValue:true}})));u.on("message",d=>{const h=JSON.parse(String(d));if(h.id!==1)return;clearTimeout(l);u.close();if(h.result?.exceptionDetails)c(new Error("the WebView script failed"));else a(h.result?.result?.value)});u.on("error",d=>{clearTimeout(l);c(d)})})}finally{await adb(e,["forward","--remove",`tcp:${i}`]).catch(()=>void 0)}}async function evaluateWhenReady(e,n,t,r=15e3){const i=Date.now()+r;let s;while(Date.now()<i){try{return await evaluateInWebView(e,n,t)}catch(o){s=o;await new Promise(a=>setTimeout(a,700))}}throw s instanceof Error?s:new Error("the WebView did not open")}async function androidHasWebViewStorage(e,n){const t=await adbShell(e,`run-as ${n} sh -c 'ls app_webview/Default/"Local Storage"/leveldb 2>/dev/null'`).catch(()=>"");return/CURRENT|\.log/.test(t)}async function androidReadWebKeep(e,n){if(!(await adbShell(e,`pidof ${n}`).catch(()=>"")).trim()){await adbShell(e,`monkey -p ${n} -c android.intent.category.LAUNCHER 1`)}const t=await evaluateWhenReady(e,n,`(() => { const re = ${WEB_KEEP_KEY}; const out = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && re.test(k)) out.push({ key: k, value: localStorage.getItem(k) }); } return out; })()`);return Array.isArray(t)?t.filter(r=>typeof r.key==="string"&&typeof r.value==="string"):[]}async function androidWriteWebKeep(e,n,t){await evaluateWhenReady(e,n,`(() => { for (const r of ${JSON.stringify(t)}) localStorage.setItem(r.key, r.value); setTimeout(() => location.reload(), 50); return true; })()`)}var import_node_child_process6,import_node_util5,import_node_fs6,import_node_path7,import_ws,execFileAsync5,WEB_KEEP_KEY,sqlite;var init_webViewKeep=__esm({"src/capture/webViewKeep.ts"(){"use strict";import_node_child_process6=require("child_process");import_node_util5=require("util");import_node_fs6=require("fs");import_node_path7=__toESM(require("path"));import_ws=require("ws");init_adb();execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process6.execFile);WEB_KEEP_KEY=/^(@buoy\/session:|buoy-gg-license|buoy-gg-device-fingerprint$|@react_buoy_sync_device_id$|@react_buoy_install)/;sqlite=(e,n,t=true)=>execFileAsync5("sqlite3",[...t?["-readonly"]:[],e,n],{timeout:5e3}).then(r=>r.stdout)}});function rememberedHost(e){return hosts.get(e)}async function sh(e,n,t=2e4){try{const{stdout:r}=await execFileAsync6(e,n,{timeout:t,maxBuffer:16*1024*1024});return r}catch(r){const i=r;if(i.code==="ENOENT")throw new Error(e==="xcrun"?"This needs macOS with Xcode (xcrun simctl).":`${e} is not installed.`);throw new Error(`${e} ${n.slice(0,2).join(" ")} failed: ${(i.stderr||i.message||"").trim().slice(0,300)}`)}}async function simsHolding(e){let n=[];try{const r=JSON.parse(await simctl(["list","devices","booted","-j"])).devices;n=Object.entries(r).filter(([i])=>/iOS/.test(i)).flatMap(([,i])=>i.filter(s=>s.state==="Booted"))}catch{return[]}const t=[];for(const r of n)if(await findIosContainer(e,r.udid))t.push(r);return t}function cloneError(e,n){return new Error(`${n.length} booted simulators hold "${e}" (one is probably a clone): ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}. Pass udid to choose, or shut the extra one down.`)}async function findIosContainer(e,n){const t=tailOf(e);if(!t)return null;let r;try{const o=JSON.parse(await simctl(["list","devices","-j"])).devices;r=Object.values(o).flat().find(a=>a.udid===n)?.dataPath}catch{return null}if(!r)return null;const i=import_node_path8.default.join(r,"Containers","Data","Application");let s=[];try{s=await import_node_fs7.promises.readdir(i)}catch{return null}for(const o of s){const a=import_node_path8.default.join(i,o);if(!await iosContainerHolds(a,t))continue;let c=null;try{c=(await sh("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",import_node_path8.default.join(a,".com.apple.mobile_container_manager.metadata.plist")])).trim()||null}catch{}return{container:a,bundleId:c}}return null}async function iosContainerHolds(e,n){try{if((await import_node_fs7.promises.readFile(import_node_path8.default.join(e,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim()===n)return true}catch{}return(await containerWebViewInstallIds(e).catch(()=>[])).includes(n)}async function findAndroid(e,n){const t=tailOf(e);const r=n.serial?[n.serial]:await adbSerials();if(r.length===0)throw new Error("No Android device or emulator is connected to adb.");for(const i of r){const s=await adbForegroundPackage(i).catch(()=>null);const o=await adbUserPackages(i).catch(()=>[]);const a=[n.bundleId,s,...o].filter((c,u,l)=>!!c&&l.indexOf(c)===u).slice(0,80);for(const c of a){if(await adbInstallId(i,c).catch(()=>null)===t)return{platform:"android",serial:i,packageName:c}}}return null}async function findPhone(e){const n=tailOf(e);for(const t of await physicalPhones()){for(const r of await developerApps(t.udid).catch(()=>[])){if(await phoneInstallId(t.udid,r.bundleId)===n)return{platform:"ios-device",udid:t.udid,name:t.name,bundleId:r.bundleId}}}return null}async function findHost(e,n,t={}){if(e==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');const r=hosts.get(e);if(r&&!t.udid&&!t.serial&&await stillHolds(e,r)){if(r.platform==="ios"&&!r.picked){const c=await simsHolding(e);if(c.length>1)throw cloneError(e,c)}return r}if(r)hosts.delete(e);if(n==="android"){const c=await findAndroid(e,t);if(!c){throw new Error(`Could not prove which Android device and app run "${e}". Buoy checks the app's install id with run-as, which needs a debug build of the app on a device adb can reach${t.serial?` (looked on ${t.serial})`:""}.`)}hosts.set(e,c);return c}if(n&&n!=="ios")throw new Error(`Device control works on iOS Simulators and Android. This app runs on "${n}".`);if(process.platform!=="darwin")throw new Error("iOS control needs macOS with Xcode.");let i=t.udid;let s=i??"";if(!i){const c=await simsHolding(e);if(c.length>1)throw cloneError(e,c);const u=c.length===1?{ok:true,udid:c[0].udid,how:`matched ${e} to "${c[0].name}"`}:await resolveSimUdid(e);if(!u.ok||!/^matched /.test(u.how)){const l=await findPhone(e).catch(()=>null);if(l){hosts.set(e,l);return l}throw new Error(`Could not find which simulator or iPhone runs "${e}": no booted simulator holds its install id, and no connected iPhone has it (an iPhone must be unlocked, paired, and run a development build).`)}i=u.udid;s=/"([^"]+)"/.exec(u.how)?.[1]??i}const o=await findIosContainer(e,i);if(!o&&t.udid)throw new Error(`Simulator ${t.udid} does not hold "${e}".`);const a={platform:"ios",udid:i,name:s,bundleId:t.bundleId??o?.bundleId??null,container:o?.container??null,picked:!!t.udid};hosts.set(e,a);return a}async function verifyHost(e,n){if(n.platform==="ios"&&!n.picked){const t=await simsHolding(e);if(t.length>1)throw cloneError(e,t)}if(await stillHolds(e,n))return n;hosts.delete(e);return findHost(e,n.platform==="android"?"android":"ios",n.platform==="ios"&&n.picked?{udid:n.udid}:{})}async function stillHolds(e,n){const t=tailOf(e);if(n.platform==="android")return await adbInstallId(n.serial,n.packageName).catch(()=>null)===t;if(n.platform==="ios-device")return await phoneInstallId(n.udid,n.bundleId).catch(()=>null)===t;if(n.container)return iosContainerHolds(n.container,t);const r=await resolveSimUdid(e);return r.ok&&r.udid===n.udid&&/^matched /.test(r.how)}function needApp(e){if(e.platform==="android")return assertPackage(e.packageName);if(e.platform==="ios-device")return e.bundleId;if(!e.bundleId)throw new Error("Could not tell which app this is on the simulator (its container has no bundle metadata).");return e.bundleId}async function hostPng(e){if(e.platform==="android")return adbScreenshot(e.serial);if(e.platform==="ios-device")return phoneScreenshot(e.udid);const n=await captureIosFramebuffer(e.udid);if(!n.ok||!n.srcPath)throw new Error(n.error??"Screenshot failed.");try{return await import_node_fs7.promises.readFile(n.srcPath)}finally{await import_node_fs7.promises.unlink(n.srcPath).catch(()=>{})}}async function toSmallImage(e,n=1280){if(process.platform==="darwin"){const i=import_node_path8.default.join(import_node_os6.default.tmpdir(),`buoy-mcp-img-${(0,import_node_crypto5.randomUUID)()}.png`);const s=i.replace(/\.png$/,".jpg");await import_node_fs7.promises.writeFile(i,e);try{await sh("sips",["-Z",String(n),"-s","format","jpeg","-s","formatOptions","70",i,"--out",s],15e3);const o=await sh("sips",["-g","pixelWidth","-g","pixelHeight",s]);return{data:(await import_node_fs7.promises.readFile(s)).toString("base64"),mimeType:"image/jpeg",width:Number(/pixelWidth:\s*(\d+)/.exec(o)?.[1]??0),height:Number(/pixelHeight:\s*(\d+)/.exec(o)?.[1]??0)}}finally{await import_node_fs7.promises.unlink(i).catch(()=>{});await import_node_fs7.promises.unlink(s).catch(()=>{})}}const t=downscalePng(import_pngjs2.PNG.sync.read(e),Math.min(n,1024));const r=import_pngjs2.PNG.sync.write(t,{colorType:2,deflateLevel:9});return{data:r.toString("base64"),mimeType:"image/png",width:t.width,height:t.height}}function downscalePng(e,n){const t=Math.min(1,n/Math.max(e.width,e.height));if(t===1)return e;const r=Math.max(1,Math.round(e.width*t));const i=Math.max(1,Math.round(e.height*t));const s=new import_pngjs2.PNG({width:r,height:i});const o=e.width/r;const a=e.height/i;for(let c=0;c<i;c++){const u=Math.floor(c*a);const l=Math.max(u+1,Math.floor((c+1)*a));for(let d=0;d<r;d++){const h=Math.floor(d*o);const p=Math.max(h+1,Math.floor((d+1)*o));const m=[0,0,0,0];let g=0;for(let f=u;f<l;f++){for(let b=h;b<p;b++){const v=(f*e.width+b)*4;for(let y=0;y<4;y++)m[y]+=e.data[v+y];g++}}const w=(c*r+d)*4;for(let f=0;f<4;f++)s.data[w+f]=Math.round(m[f]/g)}}return s}async function withSteadyStatusBar(e,n){let t=false;try{if(e.platform==="ios"){const r=await simctl(["status_bar",e.udid,"list"]).catch(()=>"");const i=r.split(/^=+$/m).slice(1).join("").trim();if(!i){await simctl(["status_bar",e.udid,"override","--time","9:41","--batteryState","charged","--batteryLevel","100","--wifiBars","3","--cellularBars","4"]);t=true}}else if(e.platform==="android"){const r=(await adbShell(e.serial,"settings get global sysui_demo_allowed").catch(()=>"")).trim();if(r!=="1"&&!steadying.has(e.serial)){steadying.add(e.serial);await adbDemoMode(e.serial,true);t=true}}if(t)await new Promise(r=>setTimeout(r,400))}catch{t=false}try{return{result:await n(),steadied:t}}finally{if(t){if(e.platform==="ios")await simctl(["status_bar",e.udid,"clear"]).catch(()=>void 0);else if(e.platform==="android"){await adbDemoMode(e.serial,false);await adbShell(e.serial,"settings put global sysui_demo_allowed 0").catch(()=>void 0);steadying.delete(e.serial)}}}}async function hostQuit(e){const n=needApp(e);if(e.platform==="ios-device"){const t=await phoneQuit(e.udid,n);return t?`Quit ${n}.`:`${n} was not running.`}if(e.platform==="ios"){await simctl(["terminate",e.udid,n]).catch(t=>{if(!/found nothing to terminate|not running/i.test(String(t)))throw t})}else{await adbShell(e.serial,`am force-stop ${q(n)}`)}return`Quit ${n}.`}async function hostLaunch(e){const n=needApp(e);if(e.platform==="ios")await simctl(["launch",e.udid,n]);else if(e.platform==="ios-device")await phoneLaunch(e.udid,n);else await adbShell(e.serial,`monkey -p ${q(n)} -c android.intent.category.LAUNCHER 1`);return`Launched ${n}.`}async function hostBackground(e){if(e.platform==="ios-device"){await phoneLaunch(e.udid,"com.apple.Preferences");return"Sent to the background for real (Settings opened on top)."}if(e.platform==="ios"){await simctl(["launch",e.udid,"com.apple.Preferences"]);return"Sent to the background for real (Settings opened on top)."}await adbShell(e.serial,"input keyevent 3");return"Sent to the background for real (pressed Home)."}async function hostOpenUrl(e,n){if(!/^[a-zA-Z][\w+.-]*:/.test(n))throw new Error(`"${n}" is not a link with a scheme (like https://\u2026 or myapp://\u2026).`);if(e.platform==="ios-device"){await phoneOpenUrl(e.udid,n);return"Opened through the OS on the iPhone."}if(e.platform==="ios"){await simctl(["openurl",e.udid,n]);return'Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.'}await adbShell(e.serial,`am start -a android.intent.action.VIEW -d ${q(n)}`);return"Opened through the OS (an Android VIEW intent)."}async function appBundleInfo(e){const n=import_node_path8.default.join(e,"Info.plist");const t=o=>sh("plutil",["-extract",o,"raw","-o","-",n]).then(a=>a.trim()).catch(()=>"");const r=await t("CFBundleIdentifier");if(!r)throw new Error(`${e} has no Info.plist with a bundle id. Pass a built .app folder.`);const i=(await sh("plutil",["-extract","CFBundleSupportedPlatforms","json","-o","-",n]).catch(()=>"[]")).toLowerCase();const s=(await t("DTPlatformName")).toLowerCase();return{bundleId:r,simulator:i.includes("iphonesimulator")||s==="iphonesimulator"}}async function apkPackage(e){const n=[process.env.ANDROID_HOME,process.env.ANDROID_SDK_ROOT,import_node_path8.default.join(import_node_os6.default.homedir(),"Library","Android","sdk"),import_node_path8.default.join(import_node_os6.default.homedir(),"Android","Sdk")].filter(t=>!!t);for(const t of n){const r=import_node_path8.default.join(t,"build-tools");const i=(await import_node_fs7.promises.readdir(r).catch(()=>[])).sort().reverse();for(const s of i){for(const o of["aapt2","aapt"]){const a=import_node_path8.default.join(r,s,o);try{await import_node_fs7.promises.access(a)}catch{continue}const c=await sh(a,o==="aapt2"?["dump","packagename",e]:["dump","badging",e]).catch(()=>"");const u=o==="aapt2"?c.trim().split("\n")[0]:/package: name='([^']+)'/.exec(c)?.[1]??"";if(u)return u}}}throw new Error("Could not read the APK's package name (needs aapt2 from the Android SDK build-tools). Nothing was changed.")}async function hostReinstall(e,n,t=true){const r=needApp(e);const i=import_node_path8.default.resolve(n);try{await import_node_fs7.promises.access(i)}catch{throw new Error(`No build at ${i}. Pass appPath: the built .app folder (iOS Simulator) or .apk file (Android). Nothing was changed.`)}if(e.platform==="android")return reinstallAndroid(e,r,i,t);if(e.platform==="ios-device")return reinstallPhone(e,r,i,t);if(!/\.app\/?$/i.test(i))throw new Error("On the iOS Simulator, appPath must be a built .app folder. Nothing was changed.");const s=await appBundleInfo(i);if(s.bundleId!==r)throw new Error(`${import_node_path8.default.basename(i)} is ${s.bundleId}, but this app is ${r}. Nothing was changed.`);if(!s.simulator)throw new Error(`${import_node_path8.default.basename(i)} is a device build, not a simulator build. Nothing was changed.`);const o=t?await import_node_fs7.promises.mkdtemp(import_node_path8.default.join(import_node_os6.default.tmpdir(),"buoy-mcp-keep-")):null;try{let a=false;let c=[];if(o){await import_node_fs7.promises.chmod(o,448);const l=(await simctl(["get_app_container",e.udid,r,"data"]).catch(()=>"")).trim();if(l)a=await copyBuoyFiles(import_node_path8.default.join(l,"Documents"),o);if(l&&!a){try{c=await iosReadWebKeep(l)}catch(d){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView storage (${d.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}}await simctl(["terminate",e.udid,r]).catch(()=>void 0);await simctl(["uninstall",e.udid,r]);try{await simctl(["install",e.udid,i],18e4)}catch(l){throw new Error(`The app was removed, but installing ${import_node_path8.default.basename(i)} failed: ${l.message}. Install it again by hand.`)}let u="";if(o&&a){try{const l=(await simctl(["get_app_container",e.udid,r,"data"])).trim();await copyBuoyFiles(o,import_node_path8.default.join(l,"Documents"));e.container=l;u=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(l){u=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(c.length){try{const l=(await simctl(["get_app_container",e.udid,r,"data"])).trim();await simctl(["launch",e.udid,r]);const d=await iosWaitForLocalStorage(l);await simctl(["terminate",e.udid,r]).catch(()=>void 0);if(!d||!await iosWriteWebKeep(l,c))throw new Error("the app made no WebView storage");e.container=l;u=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(l){u=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(t){u=" The old install had no Buoy sign-in to keep."}await simctl(["launch",e.udid,r]);return`Reinstalled ${r} from ${i} and launched it. The app's own data is gone, like a new user.${u}`}finally{if(o)await import_node_fs7.promises.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function copyBuoyFiles(e,n){let t=false;for(const r of BUOY_KEEP){const i=import_node_path8.default.join(e,r);try{await import_node_fs7.promises.access(i)}catch{continue}const s=import_node_path8.default.join(n,r);await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(s),{recursive:true});await import_node_fs7.promises.cp(i,s,{recursive:true,force:true});t=true}return t}async function reinstallAndroid(e,n,t,r){if(!/\.apk$/i.test(t))throw new Error("On Android, appPath must be an .apk file. Nothing was changed.");const i=await apkPackage(t);if(i!==n)throw new Error(`${import_node_path8.default.basename(t)} is ${i}, but this app is ${n}. Nothing was changed.`);const s=[];if(r){const c=await adbShell(e.serial,`run-as ${q(n)} find files/buoy-license files/react-buoy -maxdepth 1 -type f 2>/dev/null`).catch(()=>"");const u=c.split("\n").map(l=>l.trim()).filter(l=>l.startsWith("files/buoy-license/")||/react-buoy\/@react_buoy_(sync_device_id|install)\.json$/.test(l));for(const l of u){const d=await adbShell(e.serial,`run-as ${q(n)} base64 ${q(l)}`).catch(()=>"");if(d.trim())s.push({rel:l,data:d.replace(/\s+/g,"")})}}let o=[];if(r&&!s.length&&await androidHasWebViewStorage(e.serial,n)){try{o=await androidReadWebKeep(e.serial,n)}catch(c){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView (${c.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}await adb(e.serial,["install","-r","-d",t],18e4);await adbShell(e.serial,`pm clear ${q(n)}`);let a="";if(o.length){await hostLaunch(e);try{await androidWriteWebKeep(e.serial,n,o);a=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){a=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${a}`}if(s.length){try{for(const c of s){const u=import_node_path8.default.posix.dirname(c.rel);await adbShell(e.serial,`echo ${q(c.data)} | run-as ${q(n)} sh -c ${q(`mkdir -p ${q(u)} && base64 -d > ${q(c.rel)}`)}`)}a=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){a=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}}else if(r){a=" No Buoy sign-in could be read to keep (needs a debug build): sign in to Buoy again in the app."}await hostLaunch(e);return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${a}`}async function reinstallPhone(e,n,t,r){if(!/\.app\/?$/i.test(t))throw new Error("On an iPhone, appPath must be a built .app folder (a device build). Nothing was changed.");const i=await appBundleInfo(t);if(i.bundleId!==n)throw new Error(`${import_node_path8.default.basename(t)} is ${i.bundleId}, but this app is ${n}. Nothing was changed.`);if(i.simulator)throw new Error(`${import_node_path8.default.basename(t)} is a simulator build, not a device build. Nothing was changed.`);const s=["Documents/buoy-license/buoy-gg-license.json","Documents/react-buoy/@react_buoy_sync_device_id.json","Documents/react-buoy/@react_buoy_install.json"];const o=r?await import_node_fs7.promises.mkdtemp(import_node_path8.default.join(import_node_os6.default.tmpdir(),"buoy-mcp-keep-")):null;try{const a=[];if(o){await import_node_fs7.promises.chmod(o,448);for(const u of s){let l;try{l=await readAppFile(e.udid,n,u)}catch(h){throw new Error(`Could not back up Buoy's ${import_node_path8.default.basename(u)} from the iPhone (${h.message}). Nothing was changed.`)}if(!l)continue;const d=import_node_path8.default.join(o,u.replace(/\//g,"__"));await import_node_fs7.promises.writeFile(d,l,{mode:384});a.push({rel:u,local:d})}}await phoneQuit(e.udid,n).catch(()=>false);await phoneUninstall(e.udid,n);try{await phoneInstall(e.udid,t)}catch(u){throw new Error(`The app was removed, but installing ${import_node_path8.default.basename(t)} failed: ${u.message}. Install it again from Xcode.`)}let c="";if(a.length){try{for(const d of a)await writeAppFile(e.udid,n,d.local,d.rel);const u=a.some(d=>d.rel.includes("buoy-license"));const l=a.some(d=>d.rel.includes("sync_device_id"));c=u?` Kept Buoy's sign-in${l?" and device id, so the same deviceId still works":""}.`:` Kept the device id; there was no Buoy sign-in to keep, so sign in to Buoy in the app.`}catch(u){c=` Buoy's sign-in could not be restored (${u.message}): sign in to Buoy again in the app.`}}else if(r)c=" The old install had no Buoy sign-in to keep.";await phoneLaunch(e.udid,n);return`Reinstalled ${n} on the iPhone and launched it. The app's own data is gone, like a new user.${c}`}finally{if(o)await import_node_fs7.promises.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function bootSimulator(e){const n=JSON.parse(await simctl(["list","devices","available","-j"])).devices;const t=Object.entries(n).filter(([a])=>/iOS/.test(a)).flatMap(([,a])=>a);const r=e.trim().toLowerCase();const i=t.filter(a=>a.udid.toLowerCase()===r||a.name.toLowerCase()===r);const s=i.length?i:t.filter(a=>a.name.toLowerCase().includes(r));if(s.length===0)throw new Error(`No iOS Simulator matches "${e}". Run \`xcrun simctl list devices available\` to see them.`);if(s.length>1)throw new Error(`${s.length} simulators match "${e}": ${s.map(a=>`${a.name} (${a.udid})`).join(", ")}. Pass the udid.`);const o=s[0];if(o.state!=="Booted")await simctl(["boot",o.udid],12e4);await sh("open",["-a","Simulator","--args","-CurrentDeviceUDID",o.udid]).catch(()=>void 0);return o.state==="Booted"?`${o.name} (${o.udid}) was already booted.`:`Booted ${o.name} (${o.udid}).`}var import_node_child_process7,import_node_util6,import_node_fs7,import_node_os6,import_node_path8,import_node_crypto5,import_pngjs2,execFileAsync6,hosts,simctl,tailOf,steadying,BUOY_KEEP;var init_hostDevice=__esm({"src/capture/hostDevice.ts"(){"use strict";import_node_child_process7=require("child_process");import_node_util6=require("util");import_node_fs7=require("fs");import_node_os6=__toESM(require("os"));import_node_path8=__toESM(require("path"));import_node_crypto5=require("crypto");import_pngjs2=__toESM(require_png());init_simctl();init_iosDevice();init_webViewKeep();init_adb();execFileAsync6=(0,import_node_util6.promisify)(import_node_child_process7.execFile);hosts=new Map;simctl=(e,n)=>sh("xcrun",["simctl",...e],n);tailOf=e=>e.split("-").pop()??"";steadying=new Set;BUOY_KEEP=["buoy-license","react-buoy/@react_buoy_sync_device_id.json","react-buoy/@react_buoy_install.json"]}});async function hostTarget(e,n,t={},r={}){if(n==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');let i;let s=true;try{i=e.requireProDevice(n)}catch(c){const u=c instanceof Error?c.message:String(c);const l=u.startsWith(NOT_READY_MARK)&&/is not connected right now/.test(u)?/"([^"]+)"/.exec(u)?.[1]:void 0;const d=l?e.listDevices().some(h=>h.deviceId===l&&h.isPro):false;if(!r.allowOffline||!l||!d)throw c;i=l;s=false}const o=e.listDevices().find(c=>c.deviceId===i)?.platform;let a=await findHost(i,o,t);if(r.changesApp&&rememberedHost(i)===a)a=await verifyHost(i,a);return{id:i,host:a,online:s}}function hostLabel(e){if(e.platform==="ios")return`simulator "${e.name}"`;if(e.platform==="ios-device")return`iPhone "${e.name}"`;return`Android device ${e.serial}`}function has(e){if(!onPath.has(e))onPath.set(e,execFileAsync7("which",[e],{timeout:3e3}).then(()=>true,()=>false));return onPath.get(e)}async function hostSummary(e,n){let t=n;if(e.platform==="android"){const i=rememberedHost(e.deviceId);const s=i??(e.isConnected?await Promise.race([findHost(e.deviceId,"android").catch(()=>void 0),new Promise(a=>setTimeout(()=>a(void 0),3e3))]):void 0);const o=await has("adb");return{kind:"android",...s&&s.platform==="android"?{id:s.serial,name:s.packageName}:{},can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall","real lifecycle events","native_tap/type/swipe"],...!o?{missing:["adb (Android SDK platform-tools)"]}:!s?{missing:["proof of which phone runs it (needs a debug build reachable by adb)"]}:{}}}if(e.platform!=="ios")return{kind:e.platform??"unknown",can:[],missing:["host control works on iOS and Android apps"]};let r=rememberedHost(e.deviceId);if(!r&&!t&&e.isConnected){r=await Promise.race([findHost(e.deviceId,"ios").catch(()=>void 0),new Promise(i=>setTimeout(()=>i(void 0),4e3))])}if(!t&&r?.platform==="ios")t={name:r.name,udid:r.udid};if(r?.platform==="ios-device"){return{kind:"physical iPhone",id:r.udid,name:r.name,can:["screenshot","screen_recording (if this iPhone supports it)","baseline diffs","quit/launch/reinstall","open_url","native_tap/type/swipe (by query, testID, nativeTag or x/y; not label)"]}}if(t){const i=await has("idb");return{kind:"ios simulator",id:t.udid,name:t.name,can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall/boot","real lifecycle events",...i?["native_tap/type/swipe"]:[]],...i?{}:{missing:["idb for real touches (brew install facebook/fb/idb-companion && pipx install fb-idb)"]}}}return void 0}var import_node_child_process8,import_node_util7,execFileAsync7,onPath;var init_hostKit=__esm({"src/tools/hostKit.ts"(){"use strict";init_SyncClient();import_node_child_process8=require("child_process");import_node_util7=require("util");init_hostDevice();execFileAsync7=(0,import_node_util7.promisify)(import_node_child_process8.execFile);onPath=new Map}});function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,n){e.registerTool("list_devices",{description:"List the apps connected to Buoy: id, name, platform, whether it is connected and has MCP access, the simulator or phone it runs on with the host controls that work there (screenshot, recording, app control, real touches) and anything missing, and the Buoy tools it exposes. Start here. Pass details:true for every tool's action names.",inputSchema:{details:import_zod.z.boolean().optional().describe("Also list every action of every tool. Default false (tool names only); list_tool_actions gives one tool's actions."),includeDisconnected:import_zod.z.boolean().optional().describe("Also list apps that were seen before but are not connected now. Default false.")}},async(t={})=>{const r=await getUpdateNotice();let i=n.listDevices();for(let p=0;i.length===0&&p<10;p++){await new Promise(m=>setTimeout(m,200));i=n.listDevices()}if(i.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...r?{updateAvailable:r}:{}})}const s=t.includeDisconnected?0:i.filter(p=>!p.isConnected).length;if(!t.includeDisconnected)i=i.filter(p=>p.isConnected);if(i.length===0){return json({devices:[],hint:`No app is connected right now${s?` (${s} seen before; pass includeDisconnected:true to list them)`:""}. Open the app with Buoy devtools running, then retry.`,...r?{updateAvailable:r}:{}})}const o=await resolveSims(i.filter(p=>p.platform==="ios").map(p=>p.deviceId)).catch(()=>new Map);const a=i.filter(p=>p.isConnected);const c=a.some(p=>p.isPro);const u=a.some(p=>p.tier!=="locked");const l=a.filter(p=>p.collision);const d=await Promise.all(i.map(p=>p.isConnected?hostSummary(p,o.get(p.deviceId)).catch(()=>void 0):void 0));const h=i.map((p,m)=>{const g=o.get(p.deviceId);const w=d[m];return{deviceId:p.deviceId,deviceName:p.deviceName,platform:p.platform,isConnected:p.isConnected,isPro:p.isPro,tier:p.tier,access:n.accessState(p.deviceId),...g?{simulator:g}:{},...w?{host:w}:{},...p.collision?{collision:true}:{},tools:t.details?p.tools:p.tools.map(f=>f.toolId)}});return json({devices:h,...t.details?{}:{note:`Tool names only. Pass details:true, or list_tool_actions({toolId}), for action names.${s?` ${s} apps seen before but not connected now are left out (includeDisconnected:true lists them).`:""}`},...l.length>0?{warning:`${l.map(p=>`"${p.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...c?{}:{hint:u?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...r?{updateAvailable:r}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a tool exposes, and which connected devices offer it. Leave toolId out to list every tool's actions. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().optional().describe('The tool id, e.g. "perf-monitor", "network", "react-query". Leave out for every tool.'),tool:import_zod.z.string().optional().describe("Same as toolId."),deviceId:import_zod.z.string().optional().describe("Only list what this device offers.")}},async({toolId:t,tool:r,deviceId:i})=>{const s=t??r;if(!s){const c=n.connectedDevices().filter(l=>!i||l.deviceId===i);if(c.length===0)return fail(i?`No connected device "${i}". Call list_devices.`:"No devices are connected. Call list_devices.");const u=c.map(l=>[`**${l.deviceName}** (${l.deviceId})`,...l.tools.map(d=>`- ${d.toolId}: ${d.actions.join(", ")}`)].join("\n"));return{content:[{type:"text",text:`${u.join("\n\n")}
|
|
40
|
+
`}function escapeHtml(e){return e.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var import_node_child_process3,import_node_fs2,import_node_path2,MAX_RECORDING_MS,live;var init_recorder=__esm({"src/capture/recorder.ts"(){"use strict";import_node_child_process3=require("child_process");import_node_fs2=require("fs");import_node_path2=__toESM(require("path"));init_adb();init_iosDevice();MAX_RECORDING_MS=3*6e4;live=new Set}});function readProjectKey(e,n=process.env){for(const t of KEY_NAMES)if(n[t]!==void 0)return(0,import_headless.normalizeLicenseKey)(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let r;try{r=(0,import_node_fs3.readFileSync)((0,import_node_path3.join)(e,t),"utf8")}catch{continue}const i=new Map;for(const s of r.split(/\r?\n/)){const o=s.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!o)continue;const a=o[2].trim();const c=a.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);i.set(o[1],c?c[2]:a.split("#")[0].trim())}for(const s of KEY_NAMES)if(i.has(s))return(0,import_headless.normalizeLicenseKey)(i.get(s))||null}return null}var import_node_fs3,import_node_path3,import_node_os2,import_node_crypto2,import_headless,KEY_NAMES,NodeAccount;var init_NodeAccount=__esm({"src/sync/NodeAccount.ts"(){"use strict";import_node_fs3=require("fs");import_node_path3=require("path");import_node_os2=require("os");import_node_crypto2=require("crypto");import_headless=require("@buoy-gg/license/headless");KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","NEXT_PUBLIC_BUOY_KEY","REACT_APP_BUOY_KEY"];NodeAccount=class{constructor(e,n={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.unsubscribeSession=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>this.hasKeyAccess()||this.session?.getStatus().state==="active";this.lastSessionSync=0;this.projectDir=(0,import_node_path3.resolve)(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto2.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??(0,import_node_path3.join)((0,import_node_os2.homedir)(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:(0,import_headless.createNodeSession)({audience:import_headless.CLI_AUDIENCE,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return(0,import_headless.resolveAccountAccess)({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted"}start(){if(this.timer)return;if(this.session){this.unsubscribeSession=this.session.subscribe(()=>this.notify());void this.session.load().then(()=>this.notify(),()=>{})}try{const e=JSON.parse((0,import_node_fs3.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;this.unsubscribeSession?.();this.unsubscribeSession=null;if(this.timer)clearInterval(this.timer);this.timer=null;this.key=null;this.cache=null;this.notify()}notify(){const e=this.hasAccess();if(e===this.lastAdmission)return;this.lastAdmission=e;this.changed()}async tick(){if(this.session&&Date.now()-this.lastSessionSync>15e3){this.lastSessionSync=Date.now();void this.session.sync().catch(()=>{})}const e=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const n=this.generation;try{if((0,import_headless.isDevToken)(e)){const a=(0,import_headless.checkDevToken)(e);if(!a.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),a.claims.iat*1e3);const u=a.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${a.claims.sid}`,valid:true,tier:u,isPro:u==="pro",expiresAt:new Date(a.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}if((0,import_headless.isServiceKey)(e)){const a=await(0,import_headless.exchangeServiceKey)(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;if(!a.ok){if(a.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL}else{this.nextCheck=Date.now()+(import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]??import_headless.LICENSE_REVALIDATION_INTERVAL)}return}const c=Date.now();const u=a.claims.exp*1e3;this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`service:${a.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(u).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,u-60*60*1e3);return}if((0,import_headless.isBuoyToken)(e)){const a=(0,import_headless.verifyFreeToken)(e);if(!a){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${a.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;const r=t.data?.relationships?.product?.data?.id;const i=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(r&&r!==import_headless.KEYGEN_CONFIG.productId||!i&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const s=i?"free":(0,import_headless.resolveTier)(t.data);const o=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:s,isPro:s==="pro",expiresAt:i?null:t.data.attributes.expiry,cachedAt:o,lastValidatedAt:o});this.persistCache();this.retry=0;this.nextCheck=o+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(n!==this.generation)return;const r=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(r??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs3.mkdirSync)((0,import_node_path3.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs3.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs3.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs3.unlinkSync)(this.cacheFile)}catch{}}}}});var import_sync_broker,BROKER_PORT,BROKER_URL,DEFAULT_ACTION_TIMEOUT_MS,UNRESPONSIVE_ACTION_TIMEOUT_MS,UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS,RECOVERY_PROBE_MS,UNRESPONSIVE_AFTER_MS,DEFAULT_SNAPSHOT_TIMEOUT_MS,DEVICE_RETENTION_MS,DASHBOARD_NAME,ALL_DEVICES;var init_config=__esm({"src/config.ts"(){"use strict";import_sync_broker=require("@buoy-gg/sync-broker");BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;BROKER_URL=`http://localhost:${BROKER_PORT}`;DEFAULT_ACTION_TIMEOUT_MS=15e3;UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;RECOVERY_PROBE_MS=2e4;UNRESPONSIVE_AFTER_MS=3e4;DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;DEVICE_RETENTION_MS=15*6e4;DASHBOARD_NAME="Dashboard";ALL_DEVICES="All"}});async function devServerRoot(e){let n;try{n=new URL(e)}catch{return null}if(!isLocalHost(n.hostname))return null;const t=n.port||(n.protocol==="https:"?"443":"80");try{const{stdout:r}=await execFileAsync3("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const i=r.split("\n").map(a=>a.trim()).find(Boolean);if(!i)return null;const{stdout:s}=await execFileAsync3("lsof",["-a","-p",i,"-d","cwd","-Fn"],{timeout:3e3});const o=s.split("\n").find(a=>a.startsWith("n"));return o?import_node_path4.default.resolve(o.slice(1)):null}catch{return null}}function isLocalHost(e,n=import_node_os3.default.networkInterfaces()){const t=e.replace(/^\[|\]$/g,"").toLowerCase();if(t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="10.0.2.2"||t==="10.0.3.2")return true;return Object.values(n).flat().some(r=>r?.address?.toLowerCase()===t)}function pickByProject(e,n){const t=import_node_path4.default.resolve(n);const r=e.map(o=>({deviceId:o.deviceId,score:o.root?closeness(import_node_path4.default.resolve(o.root),t):-1})).filter(o=>o.score>=0);if(r.length===0)return null;const i=Math.min(...r.map(o=>o.score));const s=r.filter(o=>o.score===i);return s.length===1?s[0].deviceId:null}function closeness(e,n){if(e===n)return 0;const t=import_node_path4.default.relative(e,n);const r=import_node_path4.default.relative(n,e);const i=s=>s&&!s.startsWith("..")&&!import_node_path4.default.isAbsolute(s);if(i(t))return t.split(import_node_path4.default.sep).length;if(i(r))return r.split(import_node_path4.default.sep).length;return-1}var import_node_child_process4,import_node_util3,import_node_path4,import_node_os3,execFileAsync3;var init_projectRoot=__esm({"src/sync/projectRoot.ts"(){"use strict";import_node_child_process4=require("child_process");import_node_util3=require("util");import_node_path4=__toESM(require("path"));import_node_os3=__toESM(require("os"));execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process4.execFile)}});function isRecoveryAction(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var init_actionTimeout=__esm({"src/sync/actionTimeout.ts"(){"use strict"}});function probeBroker(e,n=800){return new Promise(t=>{const r=import_node_http.default.get(e,i=>{i.resume();t((i.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function waitForBroker(e,n=20){for(let t=0;t<n;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var import_node_http,import_node_crypto3,import_socket,import_sync_broker2,NOT_READY_MARK,SyncClient,snapKey,delay;var init_SyncClient=__esm({"src/sync/SyncClient.ts"(){"use strict";init_NodeAccount();import_node_http=__toESM(require("http"));import_node_crypto3=require("crypto");import_socket=require("socket.io-client");import_sync_broker2=require("@buoy-gg/sync-broker");init_config();init_projectRoot();init_actionTimeout();NOT_READY_MARK="Not ready:";SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.createdAt=Date.now();this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.sessionDevice=null;this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.crashSeen=new Map;this.rootLookups=new Set;this.devServerRoots=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.deviceConnections=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
|
|
41
|
+
`));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url)||await probeBroker(this.url,8e3);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();let t=false;this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:r=>{if(r.code==="EADDRINUSE")t=true;this.log(`broker error: ${r.code??r.message}`)}});await waitForBroker(this.url).catch(r=>{if(!t)throw r});if(!this.broker.httpServer.listening)t=true;if(t){this.log(`another broker holds :${this.port} \u2014 connecting to it as a dashboard`);await this.broker.close().catch(()=>void 0);this.broker=null;this.hostAccount.stop();this.hostAccount=null;await this.connectSocket();return"connected"}try{(0,import_sync_broker2.writeBrokerToken)(this.broker.dashboardToken,(0,import_sync_broker2.brokerTokenPath)(this.port))}catch(r){this.log(`could not write the broker token file: ${r.message}`)}n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:a=>{const c=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));a(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const i=()=>{if(r)return;r=true;this.log("connected to broker");e()};const s=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const c=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(c);if(c){clearTimeout(s);this.rearmWatches();i()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const o=a=>a.message===import_sync_broker2.DASHBOARD_TOKEN_REQUIRED;t.once("connect_error",a=>{if(r)return;r=true;clearTimeout(s);n(new Error(o(a)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${(0,import_sync_broker2.brokerTokenPath)(this.port)} is missing or stale. Update Buoy Desktop, or restart it so it writes the file.`:`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("connect_error",a=>{if(!o(a)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);this.trackRootLookup(this.learnDevServerOrigin(a.persistentDeviceId));if(a.tools?.some(c=>c.toolId==="console"))this.ensureWatching("console",a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let c=false;for(const l of a){if(l.deviceId&&l.tier==="locked")this.lockDevice(String(l.deviceId));else if(l.deviceId&&(l.tier==="free"||l.tier==="pro")){c=this.lockedDevices.delete(String(l.deviceId))||c}}this.devices=a;const u=Date.now();for(const l of a)if(l.deviceId)this.lastSeen.set(l.deviceId,u);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(a)});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const c=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(c,a.payload);this.snapshotAt.set(c,Date.now());if(a.toolId==="console")this.noteCrashReport(a.persistentDeviceId,a.payload);const u=this.snapshotWaiters.get(c);if(u&&u.length){this.snapshotWaiters.set(c,[]);for(const l of u)l(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const c=this.pendingActions.get(a.requestId);if(!c)return;this.pendingActions.delete(a.requestId);clearTimeout(c.timer);c.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.devServerRoots.clear();this.crashSeen.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.devServerRoots.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const i of r)i(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===ALL_DEVICES){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(i=>i.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.devServerRoots.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const i of[...this.snapshots.keys()]){if(i.startsWith(r)){this.snapshots.delete(i);this.snapshotAt.delete(i)}}for(const i of[...this.watching.keys()]){if(i.startsWith(r))this.watching.delete(i)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(u,l)=>new Error(`${NOT_READY_MARK} "${u}" (${l}) is not connected right now. If it is restarting, it usually takes a few seconds; to use another device, pass deviceId (run list_devices to see them).`);if(this.sessionDevice){const u=this.devices.find(l=>l.deviceId===this.sessionDevice);if(u&&u.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const r=t.filter(u=>!this.lockedDevices.has(u.deviceId));const i=r.filter(u=>u.tools.length>0);const s=process.env.BUOY_PROJECT_ROOT||process.cwd();const o=pickByProject(i.map(u=>({deviceId:u.deviceId,root:this.devServerRoots.get(u.deviceId)})),s);if(o)return this.sessionDevice=o;const a=pickByProject(this.devices.filter(u=>u.deviceId&&u.isConnected===false&&!this.lockedDevices.has(u.deviceId)).map(u=>({deviceId:u.deviceId,root:this.devServerRoots.get(u.deviceId)})),s);if(a)throw n(a,"this project's app");if(r.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(u=>u.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(r.length===1)return this.sessionDevice=r[0].deviceId;if(i.length===1)return this.sessionDevice=i[0].deviceId;if(i.length===0){const u=r.map(l=>l.deviceName).join(", ");throw new Error(`Connected devices (${u}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const c=i.map(u=>`${u.deviceName} (${u.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${c}`)}requireProDevice(e){this.requireAdmission();if(e&&e!==ALL_DEVICES)this.sessionDevice=e;const n=s=>{if(!s)return"";return s.tier==="locked"?` The broker has not confirmed account access for "${s.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${s.deviceName}" is on the ${s.tier} tier.`};const t=s=>`Buoy Pro is required to use the MCP.${n(s)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(s=>s.isPro))throw new Error(t());return ALL_DEVICES}const r=this.resolveDevice(e);const i=this.connectedDevices().find(s=>s.deviceId===r);if(!i?.isPro){const s=this.accessState(r);if(s==="offline")throw new Error(`${NOT_READY_MARK} "${r}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(s==="pending")throw new Error(`${NOT_READY_MARK} account access for "${r}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(t(i))}return r}accessState(e){const n=this.devices.find(t=>t.deviceId===e);if(!n||n.isConnected===false)return"offline";if(n.isPro===true)return"ready";return n.tier===void 0?"pending":"denied"}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:i,usedFullBudget:s}=actionTimeoutMs({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(s)r.lastFullBudgetAt=Date.now();return i}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=diagnoseCrashed({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const i=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const s=this.lastFatalFor(e);if(s)i.push(`Last captured error: ${s}`);i.push(s?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return i.join(" ")}noteCrashReport(e,n){const t=n?.entries;if(!Array.isArray(t)||t.length===0)return;const r=Math.max(...t.map(o=>typeof o?.timestamp==="number"?o.timestamp:-Infinity));const i=this.crashSeen.get(e)??new Set;let s=false;for(const o of t){const a=o?.fatal||/^\[(RENDER ERROR|FATAL)/.test(o?.message??"");if(!a||typeof o.timestamp!=="number")continue;const c=`${o.id??""}|${o.timestamp}|${(o.message??"").slice(0,120)}`;if(i.has(c))continue;i.add(c);if(r-o.timestamp<3e4)s=true}if(i.size>200)for(const o of[...i].slice(0,i.size-200))i.delete(o);this.crashSeen.set(e,i);if(!s)return;if(![...this.pendingActions.values()].some(o=>o.target===e&&o.expire&&!o.cut))return;void this.callAction("app","ping",{},e,{timeoutMs:1500,silent:true}).then(()=>void 0,()=>{const o=Date.now();for(const a of this.pendingActions.values()){if(a.target!==e||!a.expire||a.cut)continue;a.cut=true;if(a.deadline!==void 0&&a.deadline-o<=UNRESPONSIVE_ACTION_TIMEOUT_MS)continue;clearTimeout(a.timer);a.timer=setTimeout(a.expire,UNRESPONSIVE_ACTION_TIMEOUT_MS)}})}lastFatalFor(e){const n=this.snapshots.get(snapKey(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const i=t[r];if(i?.fatal||i?.level==="error"){const s=(i.message??"").split("\n")[0];if(s)return s.length>300?`${s.slice(0,300)}\u2026`:s}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(snapKey(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){const t=this.devServerOrigins.get(e)!==n;this.devServerOrigins.set(e,n);if(t)this.devServerRoots.delete(e);if(t||!this.devServerRoots.has(e)){const r=devServerRoot(n).then(i=>{if(i&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,i)});this.trackRootLookup(r)}}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}trackRootLookup(e){const n=e.catch(()=>void 0).finally(()=>this.rootLookups.delete(n));this.rootLookups.add(n)}async awaitBroker(){if(Date.now()-this.createdAt>15e3)return;for(let e=0;!this.admitted&&e<4e3;e+=100)await new Promise(n=>setTimeout(n,100));for(let e=0;this.admitted&&this.devices.length===0&&e<1500;e+=100)await new Promise(n=>setTimeout(n,100))}async settleDeviceChoice(e=1500){await this.awaitBroker();if(this.sessionDevice||this.rootLookups.size===0||this.connectedDevices().length<2)return;await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,e))]);if(this.rootLookups.size)await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,Math.min(800,e)))])}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=snapKey(n,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:n});this.sendWatch(e,n)}sendWatch(e,n){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(n))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:n}of this.watching.values()){this.sendWatch(e,n)}}rearmReconnectedDevices(e){for(const n of e){if(!n.deviceId||n.isConnected===false)continue;const t=String(n.deviceId);const r=`${n.id}|${n.connectionNonce??""}`;const i=this.deviceConnections.get(t);this.deviceConnections.set(t,r);if(i===void 0||i===r)continue;for(const{toolId:s,targetDeviceId:o}of this.watching.values()){if(o===t)this.sendWatch(s,o)}}}unwatch(e,n){const t=snapKey(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const i=snapKey(r,e);this.ensureWatching(e,r);let s=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const u=this.healthOf(r);if(Date.now()-u.lastFullBudgetAt>RECOVERY_PROBE_MS){u.lastFullBudgetAt=Date.now()}else{s=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const o=this.waitForSnapshot(i,s);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const a=await o;this.requireAdmission();this.requireDeviceAccount(r);if(a!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:a}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(i);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}lastSync(e,n){const t=this.snapshots.get(snapKey(e,n));return t===void 0?void 0:{payload:t,ageMs:this.snapshotAgeMs(n,e)}}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const i=setTimeout(()=>{const o=(this.snapshotWaiters.get(e)??[]).filter(a=>a!==s);this.snapshotWaiters.set(e,o);t(void 0)},n);const s=o=>{clearTimeout(i);t(o)};r.push(s);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,i={}){this.requireAdmission();const s=r===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const o=(0,import_node_crypto3.randomUUID)();const a=i.timeoutMs??this.timeoutFor(s,e,n);const c=await new Promise((u,l)=>{const d=Date.now();const h=()=>{this.pendingActions.delete(o);if(s!==ALL_DEVICES&&!i.silent)this.noteTimeout(s);l(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-d}ms. `+(s===ALL_DEVICES?"No device responded.":this.unresponsiveNote(s))))};const p=setTimeout(h,a);this.pendingActions.set(o,{resolve:u,reject:l,timer:p,target:s,expire:h,deadline:d+a});this.socket.emit("devtool-action",{requestId:o,toolId:e,targetDeviceId:s,action:n,params:t??{}})});if(s!==ALL_DEVICES&&!i.silent)this.noteOk(s);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};snapKey=(e,n)=>`${e}::${n}`;delay=e=>new Promise(n=>setTimeout(n,e))}});var VERSION,PACKAGE_NAME;var init_version=__esm({"src/version.ts"(){"use strict";VERSION="7.0.59"?"7.0.59":"0.0.0-dev";PACKAGE_NAME="@buoy-gg/mcp"}});function isNewer(e,n){const t=s=>s.split("-")[0].split(".").map(o=>Number(o)||0);const r=t(e);const i=t(n);for(let s=0;s<3;s++){const o=r[s]??0;const a=i[s]??0;if(o>a)return true;if(o<a)return false}return false}async function readCache(){try{const e=await import_node_fs4.promises.readFile(CACHE_FILE,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function fetchLatest(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),FETCH_TIMEOUT_MS);try{const r=await fetch(REGISTRY_URL,{signal:n.signal});if(!r.ok)return null;const i=await r.json();const s=typeof i.version==="string"?i.version:null;if(s){await import_node_fs4.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:s})).catch(()=>{})}return s}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const n=await readCache();if(n&&e-n.checkedAt<CHECK_TTL_MS)return n.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const n=await resolveLatest(e);if(n&&isNewer(n,VERSION)){return`A newer buoy-mcp is available (${n}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}var import_node_fs4,import_node_os4,import_node_path5,CHECK_TTL_MS,FETCH_TIMEOUT_MS,REGISTRY_URL,CACHE_FILE;var init_checkForUpdate=__esm({"src/update/checkForUpdate.ts"(){"use strict";import_node_fs4=require("fs");import_node_os4=__toESM(require("os"));import_node_path5=__toESM(require("path"));init_version();CHECK_TTL_MS=24*60*60*1e3;FETCH_TIMEOUT_MS=1500;REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;CACHE_FILE=import_node_path5.default.join(import_node_os4.default.tmpdir(),"buoy-mcp-update-check.json")}});function humanizeSimctlError(e){const n=e;if(n?.code==="ENOENT"){return"Xcode command-line tools (xcrun) not found. Install Xcode to capture the iOS Simulator."}const t=String(n?.stderr||n?.message||e);if(/No devices are booted|Unable to find device|current device/i.test(t)){return"No booted iOS Simulator found. Boot the Simulator with your app running, then try again."}return t.trim()||"Failed to capture the iOS Simulator."}function pngSize(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,r)=>e[r]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}async function listDir(e){try{return await import_node_fs5.promises.readdir(e)}catch{return[]}}async function webViewInstallIds(e){const n=[];for(const t of await listDir(e))n.push(...await containerWebViewInstallIds(import_node_path6.default.join(e,t)));return n}async function containerWebViewInstallIds(e){const n=[];const t=import_node_path6.default.join(e,"Library","WebKit");for(const r of await listDir(t)){const i=import_node_path6.default.join(t,r,"WebsiteData","Default");for(const s of await listDir(i)){for(const o of await listDir(import_node_path6.default.join(i,s))){const a=import_node_path6.default.join(i,s,o,"LocalStorage","localstorage.sqlite3");try{await import_node_fs5.promises.access(a);const{stdout:c}=await execFileAsync4("sqlite3",["-readonly",a,`SELECT hex(value) FROM ItemTable WHERE key='${WEBVIEW_ID_KEY}' LIMIT 1;`],{timeout:1500});const u=c.trim();if(u)n.push(Buffer.from(u,"hex").toString("utf16le").trim())}catch{}}}}return n}async function resolveSimUdid(e){if(process.platform!=="darwin")return{ok:false,error:"Simulator lookup needs macOS."};let n=[];try{const{stdout:r}=await execFileAsync4("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const i=JSON.parse(r).devices;n=Object.entries(i).filter(([s])=>/iOS/.test(s)).flatMap(([,s])=>s.filter(o=>o.state==="Booted"))}catch(r){return{ok:false,error:humanizeSimctlError(r)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const r of n){if(!r.dataPath)continue;const i=import_node_path6.default.join(r.dataPath,"Containers","Data","Application");let s=[];try{s=await import_node_fs5.promises.readdir(i)}catch{continue}for(const o of s){try{const a=(await import_node_fs5.promises.readFile(import_node_path6.default.join(i,o,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(a===t)return{ok:true,udid:r.udid,how:`matched ${e} to "${r.name}"`}}catch{}}}}if(t&&e){const r=n.map(c=>c.udid).sort().join(",");const i=webViewLookups.get(e);const s=i?.booted===r&&(i.udid!==null||Date.now()-i.at<WEBVIEW_MISS_MS);const o=s&&i?.udid?n.find(c=>c.udid===i.udid):void 0;if(o)return{ok:true,udid:o.udid,how:`matched ${e} to "${o.name}" (WebView storage)`};const a=[];if(!s){const c=await Promise.all(n.map(u=>u.dataPath?webViewInstallIds(import_node_path6.default.join(u.dataPath,"Containers","Data","Application")):Promise.resolve([])));c.forEach((u,l)=>{if(u.includes(t))a.push(n[l])});webViewLookups.set(e,{booted:r,udid:a.length===1?a[0].udid:null,at:Date.now()})}if(a.length===1){return{ok:true,udid:a[0].udid,how:`matched ${e} to "${a[0].name}" (WebView storage)`}}if(a.length>1){return{ok:false,error:`${a.length} booted simulators (${a.map(c=>c.name).join(", ")}) hold ${e}. Pass the udid, or shut one down.`}}}if(n.length===1)return{ok:true,udid:n[0].udid,how:`only booted simulator "${n[0].name}"`};return{ok:false,error:`${n.length} iOS Simulators are booted (${n.map(r=>r.name).join(", ")}) and none could be matched to ${e??"this device"}. Shut down the others, or capture from a single booted simulator.`}}async function resolveSims(e){const n=new Map;if(process.platform!=="darwin"||e.length===0)return n;let t=[];try{const{stdout:i}=await execFileAsync4("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const s=JSON.parse(i).devices;t=Object.entries(s).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return n}const r=new Map;for(const i of t){if(!i.dataPath)continue;const s=import_node_path6.default.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await import_node_fs5.promises.readdir(s)}catch{continue}for(const a of o){try{const c=(await import_node_fs5.promises.readFile(import_node_path6.default.join(s,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(c)r.set(c,{name:i.name,udid:i.udid})}catch{}}}for(const i of e){const s=r.get(i.split("-").pop()??"");if(s)n.set(i,s)}return n}async function captureIosFramebuffer(e="booted"){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const n=import_node_path6.default.join(import_node_os5.default.tmpdir(),`buoy-mcp-sim-${(0,import_node_crypto4.randomUUID)()}.png`);try{await execFileAsync4("xcrun",["simctl","io",e,"screenshot","--type=png",n],{timeout:15e3})}catch(t){return{ok:false,error:humanizeSimctlError(t)}}try{const t=await import_node_fs5.promises.readFile(n);const r=pngSize(t);if(!r)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:r.width,height:r.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function cropToBuffer(e,n,t){const r=clamp(Math.round(t.x),0,Math.max(0,n.width-1));const i=clamp(Math.round(t.y),0,Math.max(0,n.height-1));const s=clamp(Math.round(t.width),1,n.width-r);const o=clamp(Math.round(t.height),1,n.height-i);const a=import_node_path6.default.join(import_node_os5.default.tmpdir(),`buoy-mcp-crop-${(0,import_node_crypto4.randomUUID)()}.png`);try{await execFileAsync4("sips",["-c",String(o),String(s),"--cropOffset",String(i),String(r),e,"--out",a],{timeout:1e4});const c=await import_node_fs5.promises.readFile(a);return{ok:true,buffer:c,width:s,height:o}}catch(c){const u=c;return{ok:false,error:`Crop failed: ${u?.stderr||u?.message||c}`}}finally{await import_node_fs5.promises.unlink(a).catch(()=>{});await import_node_fs5.promises.unlink(e).catch(()=>{})}}var import_node_child_process5,import_node_util4,import_node_fs5,import_node_os5,import_node_path6,import_node_crypto4,execFileAsync4,clamp,WEBVIEW_ID_KEY,webViewLookups,WEBVIEW_MISS_MS;var init_simctl=__esm({"src/capture/simctl.ts"(){"use strict";import_node_child_process5=require("child_process");import_node_util4=require("util");import_node_fs5=require("fs");import_node_os5=__toESM(require("os"));import_node_path6=__toESM(require("path"));import_node_crypto4=require("crypto");execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process5.execFile);clamp=(e,n,t)=>Math.min(Math.max(e,n),t);WEBVIEW_ID_KEY="@react_buoy_sync_device_id";webViewLookups=new Map;WEBVIEW_MISS_MS=3e4}});async function listDir2(e){return import_node_fs6.promises.readdir(e).catch(()=>[])}async function containerLocalStorageFiles(e){const n=[];const t=import_node_path7.default.join(e,"Library","WebKit");for(const r of await listDir2(t)){const i=import_node_path7.default.join(t,r,"WebsiteData","Default");for(const s of await listDir2(i)){for(const o of await listDir2(import_node_path7.default.join(i,s))){const a=import_node_path7.default.join(i,s,o);const c=import_node_path7.default.join(a,"LocalStorage","localstorage.sqlite3");if(!await import_node_fs6.promises.access(c).then(()=>true,()=>false))continue;const u=await import_node_fs6.promises.readFile(import_node_path7.default.join(a,"origin")).then(l=>l.toString("hex"),()=>"");if(u)n.push({db:c,origin:u})}}}return n}async function iosReadWebKeep(e){const n=[];for(const{db:t,origin:r}of await containerLocalStorageFiles(e)){const i=await sqlite(t,"SELECT key, hex(value) FROM ItemTable;");for(const s of i.split("\n")){const o=s.lastIndexOf("|");if(o<0)continue;const a=s.slice(0,o);if(WEB_KEEP_KEY.test(a))n.push({key:a,value:s.slice(o+1),origin:r})}}return n}async function iosWriteWebKeep(e,n){if(!n.length)return false;const t=await containerLocalStorageFiles(e);const r=new Set(n.map(i=>i.origin??""));for(const i of r){const s=i?t.find(a=>a.origin===i):void 0;if(!s)return false;const o=n.filter(a=>(a.origin??"")===i&&/^[0-9A-F]*$/i.test(a.value)).map(a=>`('${a.key.replace(/'/g,"''")}', X'${a.value}')`).join(",");if(o)await sqlite(s.db,`INSERT OR REPLACE INTO ItemTable (key, value) VALUES ${o};`,false)}return true}async function iosWaitForLocalStorage(e,n=15e3){const t=Date.now()+n;while(Date.now()<t){if((await containerLocalStorageFiles(e)).length)return true;await new Promise(r=>setTimeout(r,500))}return false}async function evaluateInWebView(e,n,t){const r=(await adbShell(e,`pidof ${n}`).catch(()=>"")).trim().split(/\s+/)[0];if(!r)throw new Error("the app is not running");const i=(await adb(e,["forward","tcp:0",`localabstract:webview_devtools_remote_${r}`])).trim();try{const s=await(await fetch(`http://127.0.0.1:${i}/json`)).json();const o=s.find(a=>a.type==="page"&&a.webSocketDebuggerUrl);if(!o?.webSocketDebuggerUrl)throw new Error("no debuggable WebView page (needs a debug build)");return await new Promise((a,c)=>{const u=new import_ws.WebSocket(o.webSocketDebuggerUrl);const l=setTimeout(()=>{u.close();c(new Error("the WebView did not answer"))},5e3);u.on("open",()=>u.send(JSON.stringify({id:1,method:"Runtime.evaluate",params:{expression:t,returnByValue:true}})));u.on("message",d=>{const h=JSON.parse(String(d));if(h.id!==1)return;clearTimeout(l);u.close();if(h.result?.exceptionDetails)c(new Error("the WebView script failed"));else a(h.result?.result?.value)});u.on("error",d=>{clearTimeout(l);c(d)})})}finally{await adb(e,["forward","--remove",`tcp:${i}`]).catch(()=>void 0)}}async function evaluateWhenReady(e,n,t,r=15e3){const i=Date.now()+r;let s;while(Date.now()<i){try{return await evaluateInWebView(e,n,t)}catch(o){s=o;await new Promise(a=>setTimeout(a,700))}}throw s instanceof Error?s:new Error("the WebView did not open")}async function androidHasWebViewStorage(e,n){const t=await adbShell(e,`run-as ${n} sh -c 'ls app_webview/Default/"Local Storage"/leveldb 2>/dev/null'`).catch(()=>"");return/CURRENT|\.log/.test(t)}async function androidReadWebKeep(e,n){if(!(await adbShell(e,`pidof ${n}`).catch(()=>"")).trim()){await adbShell(e,`monkey -p ${n} -c android.intent.category.LAUNCHER 1`)}const t=await evaluateWhenReady(e,n,`(() => { const re = ${WEB_KEEP_KEY}; const out = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && re.test(k)) out.push({ key: k, value: localStorage.getItem(k) }); } return out; })()`);return Array.isArray(t)?t.filter(r=>typeof r.key==="string"&&typeof r.value==="string"):[]}async function androidWriteWebKeep(e,n,t){await evaluateWhenReady(e,n,`(() => { for (const r of ${JSON.stringify(t)}) localStorage.setItem(r.key, r.value); setTimeout(() => location.reload(), 50); return true; })()`)}var import_node_child_process6,import_node_util5,import_node_fs6,import_node_path7,import_ws,execFileAsync5,WEB_KEEP_KEY,sqlite;var init_webViewKeep=__esm({"src/capture/webViewKeep.ts"(){"use strict";import_node_child_process6=require("child_process");import_node_util5=require("util");import_node_fs6=require("fs");import_node_path7=__toESM(require("path"));import_ws=require("ws");init_adb();execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process6.execFile);WEB_KEEP_KEY=/^(@buoy\/session:|buoy-gg-license|buoy-gg-device-fingerprint$|@react_buoy_sync_device_id$|@react_buoy_install)/;sqlite=(e,n,t=true)=>execFileAsync5("sqlite3",[...t?["-readonly"]:[],e,n],{timeout:5e3}).then(r=>r.stdout)}});function rememberedHost(e){return hosts.get(e)}async function sh(e,n,t=2e4){try{const{stdout:r}=await execFileAsync6(e,n,{timeout:t,maxBuffer:16*1024*1024});return r}catch(r){const i=r;if(i.code==="ENOENT")throw new Error(e==="xcrun"?"This needs macOS with Xcode (xcrun simctl).":`${e} is not installed.`);throw new Error(`${e} ${n.slice(0,2).join(" ")} failed: ${(i.stderr||i.message||"").trim().slice(0,300)}`)}}async function simsHolding(e){let n=[];try{const r=JSON.parse(await simctl(["list","devices","booted","-j"])).devices;n=Object.entries(r).filter(([i])=>/iOS/.test(i)).flatMap(([,i])=>i.filter(s=>s.state==="Booted"))}catch{return[]}const t=[];for(const r of n)if(await findIosContainer(e,r.udid))t.push(r);return t}function cloneError(e,n){return new Error(`${n.length} booted simulators hold "${e}" (one is probably a clone): ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}. Pass udid to choose, or shut the extra one down.`)}async function findIosContainer(e,n){const t=tailOf(e);if(!t)return null;let r;try{const o=JSON.parse(await simctl(["list","devices","-j"])).devices;r=Object.values(o).flat().find(a=>a.udid===n)?.dataPath}catch{return null}if(!r)return null;const i=import_node_path8.default.join(r,"Containers","Data","Application");let s=[];try{s=await import_node_fs7.promises.readdir(i)}catch{return null}for(const o of s){const a=import_node_path8.default.join(i,o);if(!await iosContainerHolds(a,t))continue;let c=null;try{c=(await sh("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",import_node_path8.default.join(a,".com.apple.mobile_container_manager.metadata.plist")])).trim()||null}catch{}return{container:a,bundleId:c}}return null}async function iosContainerHolds(e,n){try{if((await import_node_fs7.promises.readFile(import_node_path8.default.join(e,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim()===n)return true}catch{}return(await containerWebViewInstallIds(e).catch(()=>[])).includes(n)}async function findAndroid(e,n){const t=tailOf(e);const r=n.serial?[n.serial]:await adbSerials();if(r.length===0)throw new Error("No Android device or emulator is connected to adb.");for(const i of r){const s=await adbForegroundPackage(i).catch(()=>null);const o=await adbUserPackages(i).catch(()=>[]);const a=[n.bundleId,s,...o].filter((c,u,l)=>!!c&&l.indexOf(c)===u).slice(0,80);for(const c of a){if(await adbInstallId(i,c).catch(()=>null)===t)return{platform:"android",serial:i,packageName:c}}}return null}async function findPhone(e){const n=tailOf(e);for(const t of await physicalPhones()){for(const r of await developerApps(t.udid).catch(()=>[])){if(await phoneInstallId(t.udid,r.bundleId)===n)return{platform:"ios-device",udid:t.udid,name:t.name,bundleId:r.bundleId}}}return null}async function findHost(e,n,t={}){if(e==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');const r=hosts.get(e);if(r&&!t.udid&&!t.serial&&await stillHolds(e,r)){if(r.platform==="ios"&&!r.picked){const c=await simsHolding(e);if(c.length>1)throw cloneError(e,c)}return r}if(r)hosts.delete(e);if(n==="android"){const c=await findAndroid(e,t);if(!c){throw new Error(`Could not prove which Android device and app run "${e}". Buoy checks the app's install id with run-as, which needs a debug build of the app on a device adb can reach${t.serial?` (looked on ${t.serial})`:""}.`)}hosts.set(e,c);return c}if(n&&n!=="ios")throw new Error(`Device control works on iOS Simulators and Android. This app runs on "${n}".`);if(process.platform!=="darwin")throw new Error("iOS control needs macOS with Xcode.");let i=t.udid;let s=i??"";if(!i){const c=await simsHolding(e);if(c.length>1)throw cloneError(e,c);const u=c.length===1?{ok:true,udid:c[0].udid,how:`matched ${e} to "${c[0].name}"`}:await resolveSimUdid(e);if(!u.ok||!/^matched /.test(u.how)){const l=await findPhone(e).catch(()=>null);if(l){hosts.set(e,l);return l}throw new Error(`Could not find which simulator or iPhone runs "${e}": no booted simulator holds its install id, and no connected iPhone has it (an iPhone must be unlocked, paired, and run a development build).`)}i=u.udid;s=/"([^"]+)"/.exec(u.how)?.[1]??i}const o=await findIosContainer(e,i);if(!o&&t.udid)throw new Error(`Simulator ${t.udid} does not hold "${e}".`);const a={platform:"ios",udid:i,name:s,bundleId:t.bundleId??o?.bundleId??null,container:o?.container??null,picked:!!t.udid};hosts.set(e,a);return a}async function verifyHost(e,n){if(n.platform==="ios"&&!n.picked){const t=await simsHolding(e);if(t.length>1)throw cloneError(e,t)}if(await stillHolds(e,n))return n;hosts.delete(e);return findHost(e,n.platform==="android"?"android":"ios",n.platform==="ios"&&n.picked?{udid:n.udid}:{})}async function stillHolds(e,n){const t=tailOf(e);if(n.platform==="android")return await adbInstallId(n.serial,n.packageName).catch(()=>null)===t;if(n.platform==="ios-device")return await phoneInstallId(n.udid,n.bundleId).catch(()=>null)===t;if(n.container)return iosContainerHolds(n.container,t);const r=await resolveSimUdid(e);return r.ok&&r.udid===n.udid&&/^matched /.test(r.how)}function needApp(e){if(e.platform==="android")return assertPackage(e.packageName);if(e.platform==="ios-device")return e.bundleId;if(!e.bundleId)throw new Error("Could not tell which app this is on the simulator (its container has no bundle metadata).");return e.bundleId}async function hostPng(e){if(e.platform==="android")return adbScreenshot(e.serial);if(e.platform==="ios-device")return phoneScreenshot(e.udid);const n=await captureIosFramebuffer(e.udid);if(!n.ok||!n.srcPath)throw new Error(n.error??"Screenshot failed.");try{return await import_node_fs7.promises.readFile(n.srcPath)}finally{await import_node_fs7.promises.unlink(n.srcPath).catch(()=>{})}}async function toSmallImage(e,n=1280){if(process.platform==="darwin"){const i=import_node_path8.default.join(import_node_os6.default.tmpdir(),`buoy-mcp-img-${(0,import_node_crypto5.randomUUID)()}.png`);const s=i.replace(/\.png$/,".jpg");await import_node_fs7.promises.writeFile(i,e);try{await sh("sips",["-Z",String(n),"-s","format","jpeg","-s","formatOptions","70",i,"--out",s],15e3);const o=await sh("sips",["-g","pixelWidth","-g","pixelHeight",s]);return{data:(await import_node_fs7.promises.readFile(s)).toString("base64"),mimeType:"image/jpeg",width:Number(/pixelWidth:\s*(\d+)/.exec(o)?.[1]??0),height:Number(/pixelHeight:\s*(\d+)/.exec(o)?.[1]??0)}}finally{await import_node_fs7.promises.unlink(i).catch(()=>{});await import_node_fs7.promises.unlink(s).catch(()=>{})}}const t=downscalePng(import_pngjs2.PNG.sync.read(e),Math.min(n,1024));const r=import_pngjs2.PNG.sync.write(t,{colorType:2,deflateLevel:9});return{data:r.toString("base64"),mimeType:"image/png",width:t.width,height:t.height}}function downscalePng(e,n){const t=Math.min(1,n/Math.max(e.width,e.height));if(t===1)return e;const r=Math.max(1,Math.round(e.width*t));const i=Math.max(1,Math.round(e.height*t));const s=new import_pngjs2.PNG({width:r,height:i});const o=e.width/r;const a=e.height/i;for(let c=0;c<i;c++){const u=Math.floor(c*a);const l=Math.max(u+1,Math.floor((c+1)*a));for(let d=0;d<r;d++){const h=Math.floor(d*o);const p=Math.max(h+1,Math.floor((d+1)*o));const m=[0,0,0,0];let g=0;for(let f=u;f<l;f++){for(let b=h;b<p;b++){const v=(f*e.width+b)*4;for(let y=0;y<4;y++)m[y]+=e.data[v+y];g++}}const w=(c*r+d)*4;for(let f=0;f<4;f++)s.data[w+f]=Math.round(m[f]/g)}}return s}async function withSteadyStatusBar(e,n){let t=false;try{if(e.platform==="ios"){const r=await simctl(["status_bar",e.udid,"list"]).catch(()=>"");const i=r.split(/^=+$/m).slice(1).join("").trim();if(!i){await simctl(["status_bar",e.udid,"override","--time","9:41","--batteryState","charged","--batteryLevel","100","--wifiBars","3","--cellularBars","4"]);t=true}}else if(e.platform==="android"){const r=(await adbShell(e.serial,"settings get global sysui_demo_allowed").catch(()=>"")).trim();if(r!=="1"&&!steadying.has(e.serial)){steadying.add(e.serial);await adbDemoMode(e.serial,true);t=true}}if(t)await new Promise(r=>setTimeout(r,400))}catch{t=false}try{return{result:await n(),steadied:t}}finally{if(t){if(e.platform==="ios")await simctl(["status_bar",e.udid,"clear"]).catch(()=>void 0);else if(e.platform==="android"){await adbDemoMode(e.serial,false);await adbShell(e.serial,"settings put global sysui_demo_allowed 0").catch(()=>void 0);steadying.delete(e.serial)}}}}async function hostQuit(e){const n=needApp(e);if(e.platform==="ios-device"){const t=await phoneQuit(e.udid,n);return t?`Quit ${n}.`:`${n} was not running.`}if(e.platform==="ios"){await simctl(["terminate",e.udid,n]).catch(t=>{if(!/found nothing to terminate|not running/i.test(String(t)))throw t})}else{await adbShell(e.serial,`am force-stop ${q(n)}`)}return`Quit ${n}.`}async function hostLaunch(e){const n=needApp(e);if(e.platform==="ios")await simctl(["launch",e.udid,n]);else if(e.platform==="ios-device")await phoneLaunch(e.udid,n);else await adbShell(e.serial,`monkey -p ${q(n)} -c android.intent.category.LAUNCHER 1`);return`Launched ${n}.`}async function hostBackground(e){if(e.platform==="ios-device"){await phoneLaunch(e.udid,"com.apple.Preferences");return"Sent to the background for real (Settings opened on top)."}if(e.platform==="ios"){await simctl(["launch",e.udid,"com.apple.Preferences"]);return"Sent to the background for real (Settings opened on top)."}await adbShell(e.serial,"input keyevent 3");return"Sent to the background for real (pressed Home)."}async function hostOpenUrl(e,n){if(!/^[a-zA-Z][\w+.-]*:/.test(n))throw new Error(`"${n}" is not a link with a scheme (like https://\u2026 or myapp://\u2026).`);if(e.platform==="ios-device"){await phoneOpenUrl(e.udid,n);return"Opened through the OS on the iPhone."}if(e.platform==="ios"){await simctl(["openurl",e.udid,n]);return'Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.'}await adbShell(e.serial,`am start -a android.intent.action.VIEW -d ${q(n)}`);return"Opened through the OS (an Android VIEW intent)."}async function appBundleInfo(e){const n=import_node_path8.default.join(e,"Info.plist");const t=o=>sh("plutil",["-extract",o,"raw","-o","-",n]).then(a=>a.trim()).catch(()=>"");const r=await t("CFBundleIdentifier");if(!r)throw new Error(`${e} has no Info.plist with a bundle id. Pass a built .app folder.`);const i=(await sh("plutil",["-extract","CFBundleSupportedPlatforms","json","-o","-",n]).catch(()=>"[]")).toLowerCase();const s=(await t("DTPlatformName")).toLowerCase();return{bundleId:r,simulator:i.includes("iphonesimulator")||s==="iphonesimulator"}}async function apkPackage(e){const n=[process.env.ANDROID_HOME,process.env.ANDROID_SDK_ROOT,import_node_path8.default.join(import_node_os6.default.homedir(),"Library","Android","sdk"),import_node_path8.default.join(import_node_os6.default.homedir(),"Android","Sdk")].filter(t=>!!t);for(const t of n){const r=import_node_path8.default.join(t,"build-tools");const i=(await import_node_fs7.promises.readdir(r).catch(()=>[])).sort().reverse();for(const s of i){for(const o of["aapt2","aapt"]){const a=import_node_path8.default.join(r,s,o);try{await import_node_fs7.promises.access(a)}catch{continue}const c=await sh(a,o==="aapt2"?["dump","packagename",e]:["dump","badging",e]).catch(()=>"");const u=o==="aapt2"?c.trim().split("\n")[0]:/package: name='([^']+)'/.exec(c)?.[1]??"";if(u)return u}}}throw new Error("Could not read the APK's package name (needs aapt2 from the Android SDK build-tools). Nothing was changed.")}async function hostReinstall(e,n,t=true){const r=needApp(e);const i=import_node_path8.default.resolve(n);try{await import_node_fs7.promises.access(i)}catch{throw new Error(`No build at ${i}. Pass appPath: the built .app folder (iOS Simulator) or .apk file (Android). Nothing was changed.`)}if(e.platform==="android")return reinstallAndroid(e,r,i,t);if(e.platform==="ios-device")return reinstallPhone(e,r,i,t);if(!/\.app\/?$/i.test(i))throw new Error("On the iOS Simulator, appPath must be a built .app folder. Nothing was changed.");const s=await appBundleInfo(i);if(s.bundleId!==r)throw new Error(`${import_node_path8.default.basename(i)} is ${s.bundleId}, but this app is ${r}. Nothing was changed.`);if(!s.simulator)throw new Error(`${import_node_path8.default.basename(i)} is a device build, not a simulator build. Nothing was changed.`);const o=t?await import_node_fs7.promises.mkdtemp(import_node_path8.default.join(import_node_os6.default.tmpdir(),"buoy-mcp-keep-")):null;try{let a=false;let c=[];if(o){await import_node_fs7.promises.chmod(o,448);const l=(await simctl(["get_app_container",e.udid,r,"data"]).catch(()=>"")).trim();if(l)a=await copyBuoyFiles(import_node_path8.default.join(l,"Documents"),o);if(l&&!a){try{c=await iosReadWebKeep(l)}catch(d){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView storage (${d.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}}await simctl(["terminate",e.udid,r]).catch(()=>void 0);await simctl(["uninstall",e.udid,r]);try{await simctl(["install",e.udid,i],18e4)}catch(l){throw new Error(`The app was removed, but installing ${import_node_path8.default.basename(i)} failed: ${l.message}. Install it again by hand.`)}let u="";if(o&&a){try{const l=(await simctl(["get_app_container",e.udid,r,"data"])).trim();await copyBuoyFiles(o,import_node_path8.default.join(l,"Documents"));e.container=l;u=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(l){u=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(c.length){try{const l=(await simctl(["get_app_container",e.udid,r,"data"])).trim();await simctl(["launch",e.udid,r]);const d=await iosWaitForLocalStorage(l);await simctl(["terminate",e.udid,r]).catch(()=>void 0);if(!d||!await iosWriteWebKeep(l,c))throw new Error("the app made no WebView storage");e.container=l;u=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(l){u=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(t){u=" The old install had no Buoy sign-in to keep."}await simctl(["launch",e.udid,r]);return`Reinstalled ${r} from ${i} and launched it. The app's own data is gone, like a new user.${u}`}finally{if(o)await import_node_fs7.promises.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function copyBuoyFiles(e,n){let t=false;for(const r of BUOY_KEEP){const i=import_node_path8.default.join(e,r);try{await import_node_fs7.promises.access(i)}catch{continue}const s=import_node_path8.default.join(n,r);await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(s),{recursive:true});await import_node_fs7.promises.cp(i,s,{recursive:true,force:true});t=true}return t}async function reinstallAndroid(e,n,t,r){if(!/\.apk$/i.test(t))throw new Error("On Android, appPath must be an .apk file. Nothing was changed.");const i=await apkPackage(t);if(i!==n)throw new Error(`${import_node_path8.default.basename(t)} is ${i}, but this app is ${n}. Nothing was changed.`);const s=[];if(r){const c=await adbShell(e.serial,`run-as ${q(n)} find files/buoy-license files/react-buoy -maxdepth 1 -type f 2>/dev/null`).catch(()=>"");const u=c.split("\n").map(l=>l.trim()).filter(l=>l.startsWith("files/buoy-license/")||/react-buoy\/@react_buoy_(sync_device_id|install)\.json$/.test(l));for(const l of u){const d=await adbShell(e.serial,`run-as ${q(n)} base64 ${q(l)}`).catch(()=>"");if(d.trim())s.push({rel:l,data:d.replace(/\s+/g,"")})}}let o=[];if(r&&!s.length&&await androidHasWebViewStorage(e.serial,n)){try{o=await androidReadWebKeep(e.serial,n)}catch(c){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView (${c.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}await adb(e.serial,["install","-r","-d",t],18e4);await adbShell(e.serial,`pm clear ${q(n)}`);let a="";if(o.length){await hostLaunch(e);try{await androidWriteWebKeep(e.serial,n,o);a=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){a=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${a}`}if(s.length){try{for(const c of s){const u=import_node_path8.default.posix.dirname(c.rel);await adbShell(e.serial,`echo ${q(c.data)} | run-as ${q(n)} sh -c ${q(`mkdir -p ${q(u)} && base64 -d > ${q(c.rel)}`)}`)}a=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){a=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}}else if(r){a=" No Buoy sign-in could be read to keep (needs a debug build): sign in to Buoy again in the app."}await hostLaunch(e);return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${a}`}async function reinstallPhone(e,n,t,r){if(!/\.app\/?$/i.test(t))throw new Error("On an iPhone, appPath must be a built .app folder (a device build). Nothing was changed.");const i=await appBundleInfo(t);if(i.bundleId!==n)throw new Error(`${import_node_path8.default.basename(t)} is ${i.bundleId}, but this app is ${n}. Nothing was changed.`);if(i.simulator)throw new Error(`${import_node_path8.default.basename(t)} is a simulator build, not a device build. Nothing was changed.`);const s=["Documents/buoy-license/buoy-gg-license.json","Documents/react-buoy/@react_buoy_sync_device_id.json","Documents/react-buoy/@react_buoy_install.json"];const o=r?await import_node_fs7.promises.mkdtemp(import_node_path8.default.join(import_node_os6.default.tmpdir(),"buoy-mcp-keep-")):null;try{const a=[];if(o){await import_node_fs7.promises.chmod(o,448);for(const u of s){let l;try{l=await readAppFile(e.udid,n,u)}catch(h){throw new Error(`Could not back up Buoy's ${import_node_path8.default.basename(u)} from the iPhone (${h.message}). Nothing was changed.`)}if(!l)continue;const d=import_node_path8.default.join(o,u.replace(/\//g,"__"));await import_node_fs7.promises.writeFile(d,l,{mode:384});a.push({rel:u,local:d})}}await phoneQuit(e.udid,n).catch(()=>false);await phoneUninstall(e.udid,n);try{await phoneInstall(e.udid,t)}catch(u){throw new Error(`The app was removed, but installing ${import_node_path8.default.basename(t)} failed: ${u.message}. Install it again from Xcode.`)}let c="";if(a.length){try{for(const d of a)await writeAppFile(e.udid,n,d.local,d.rel);const u=a.some(d=>d.rel.includes("buoy-license"));const l=a.some(d=>d.rel.includes("sync_device_id"));c=u?` Kept Buoy's sign-in${l?" and device id, so the same deviceId still works":""}.`:` Kept the device id; there was no Buoy sign-in to keep, so sign in to Buoy in the app.`}catch(u){c=` Buoy's sign-in could not be restored (${u.message}): sign in to Buoy again in the app.`}}else if(r)c=" The old install had no Buoy sign-in to keep.";await phoneLaunch(e.udid,n);return`Reinstalled ${n} on the iPhone and launched it. The app's own data is gone, like a new user.${c}`}finally{if(o)await import_node_fs7.promises.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function bootSimulator(e){const n=JSON.parse(await simctl(["list","devices","available","-j"])).devices;const t=Object.entries(n).filter(([a])=>/iOS/.test(a)).flatMap(([,a])=>a);const r=e.trim().toLowerCase();const i=t.filter(a=>a.udid.toLowerCase()===r||a.name.toLowerCase()===r);const s=i.length?i:t.filter(a=>a.name.toLowerCase().includes(r));if(s.length===0)throw new Error(`No iOS Simulator matches "${e}". Run \`xcrun simctl list devices available\` to see them.`);if(s.length>1)throw new Error(`${s.length} simulators match "${e}": ${s.map(a=>`${a.name} (${a.udid})`).join(", ")}. Pass the udid.`);const o=s[0];if(o.state!=="Booted")await simctl(["boot",o.udid],12e4);await sh("open",["-a","Simulator","--args","-CurrentDeviceUDID",o.udid]).catch(()=>void 0);return o.state==="Booted"?`${o.name} (${o.udid}) was already booted.`:`Booted ${o.name} (${o.udid}).`}var import_node_child_process7,import_node_util6,import_node_fs7,import_node_os6,import_node_path8,import_node_crypto5,import_pngjs2,execFileAsync6,hosts,simctl,tailOf,steadying,BUOY_KEEP;var init_hostDevice=__esm({"src/capture/hostDevice.ts"(){"use strict";import_node_child_process7=require("child_process");import_node_util6=require("util");import_node_fs7=require("fs");import_node_os6=__toESM(require("os"));import_node_path8=__toESM(require("path"));import_node_crypto5=require("crypto");import_pngjs2=__toESM(require_png());init_simctl();init_iosDevice();init_webViewKeep();init_adb();execFileAsync6=(0,import_node_util6.promisify)(import_node_child_process7.execFile);hosts=new Map;simctl=(e,n)=>sh("xcrun",["simctl",...e],n);tailOf=e=>e.split("-").pop()??"";steadying=new Set;BUOY_KEEP=["buoy-license","react-buoy/@react_buoy_sync_device_id.json","react-buoy/@react_buoy_install.json"]}});async function hostTarget(e,n,t={},r={}){if(n==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');let i;let s=true;try{i=e.requireProDevice(n)}catch(c){const u=c instanceof Error?c.message:String(c);const l=u.startsWith(NOT_READY_MARK)&&/is not connected right now/.test(u)?/"([^"]+)"/.exec(u)?.[1]:void 0;const d=l?e.listDevices().some(h=>h.deviceId===l&&h.isPro):false;if(!r.allowOffline||!l||!d)throw c;i=l;s=false}const o=e.listDevices().find(c=>c.deviceId===i)?.platform;let a=await findHost(i,o,t);if(r.changesApp&&rememberedHost(i)===a)a=await verifyHost(i,a);return{id:i,host:a,online:s}}function hostLabel(e){if(e.platform==="ios")return`simulator "${e.name}"`;if(e.platform==="ios-device")return`iPhone "${e.name}"`;return`Android device ${e.serial}`}function has(e){if(!onPath.has(e))onPath.set(e,execFileAsync7("which",[e],{timeout:3e3}).then(()=>true,()=>false));return onPath.get(e)}async function hostSummary(e,n){let t=n;if(e.platform==="android"){const i=rememberedHost(e.deviceId);const s=i??(e.isConnected?await Promise.race([findHost(e.deviceId,"android").catch(()=>void 0),new Promise(a=>setTimeout(()=>a(void 0),3e3))]):void 0);const o=await has("adb");return{kind:"android",...s&&s.platform==="android"?{id:s.serial,name:s.packageName}:{},can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall","real lifecycle events","native_tap/type/swipe"],...!o?{missing:["adb (Android SDK platform-tools)"]}:!s?{missing:["proof of which phone runs it (needs a debug build reachable by adb)"]}:{}}}if(e.platform!=="ios")return{kind:e.platform??"unknown",can:[],missing:["host control works on iOS and Android apps"]};let r=rememberedHost(e.deviceId);if(!r&&!t&&e.isConnected){r=await Promise.race([findHost(e.deviceId,"ios").catch(()=>void 0),new Promise(i=>setTimeout(()=>i(void 0),4e3))])}if(!t&&r?.platform==="ios")t={name:r.name,udid:r.udid};if(r?.platform==="ios-device"){return{kind:"physical iPhone",id:r.udid,name:r.name,can:["screenshot","screen_recording (if this iPhone supports it)","baseline diffs","quit/launch/reinstall","open_url","native_tap/type/swipe (by query, testID, nativeTag or x/y; not label)"]}}if(t){const i=await has("idb");return{kind:"ios simulator",id:t.udid,name:t.name,can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall/boot","real lifecycle events",...i?["native_tap/type/swipe"]:[]],...i?{}:{missing:["idb for real touches (brew install facebook/fb/idb-companion && pipx install fb-idb)"]}}}return void 0}var import_node_child_process8,import_node_util7,execFileAsync7,onPath;var init_hostKit=__esm({"src/tools/hostKit.ts"(){"use strict";init_SyncClient();import_node_child_process8=require("child_process");import_node_util7=require("util");init_hostDevice();execFileAsync7=(0,import_node_util7.promisify)(import_node_child_process8.execFile);onPath=new Map}});function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,n){e.registerTool("list_devices",{description:"List the apps connected to Buoy: id, name, platform, whether it is connected and has MCP access, the simulator or phone it runs on with the host controls that work there (screenshot, recording, app control, real touches) and anything missing, and the Buoy tools it exposes. Start here. Pass details:true for every tool's action names.",inputSchema:{details:import_zod.z.boolean().optional().describe("Also list every action of every tool. Default false (tool names only); list_tool_actions gives one tool's actions."),includeDisconnected:import_zod.z.boolean().optional().describe("Also list apps that were seen before but are not connected now. Default false.")}},async(t={})=>{const r=await getUpdateNotice();let i=n.listDevices();for(let p=0;i.length===0&&p<10;p++){await new Promise(m=>setTimeout(m,200));i=n.listDevices()}if(i.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...r?{updateAvailable:r}:{}})}const s=t.includeDisconnected?0:i.filter(p=>!p.isConnected).length;if(!t.includeDisconnected)i=i.filter(p=>p.isConnected);if(i.length===0){return json({devices:[],hint:`No app is connected right now${s?` (${s} seen before; pass includeDisconnected:true to list them)`:""}. Open the app with Buoy devtools running, then retry.`,...r?{updateAvailable:r}:{}})}const o=await resolveSims(i.filter(p=>p.platform==="ios").map(p=>p.deviceId)).catch(()=>new Map);const a=i.filter(p=>p.isConnected);const c=a.some(p=>p.isPro);const u=a.some(p=>p.tier!=="locked");const l=a.filter(p=>p.collision);const d=await Promise.all(i.map(p=>p.isConnected?hostSummary(p,o.get(p.deviceId)).catch(()=>void 0):void 0));const h=i.map((p,m)=>{const g=o.get(p.deviceId);const w=d[m];return{deviceId:p.deviceId,deviceName:p.deviceName,platform:p.platform,isConnected:p.isConnected,isPro:p.isPro,tier:p.tier,access:n.accessState(p.deviceId),...g?{simulator:g}:{},...w?{host:w}:{},...p.collision?{collision:true}:{},tools:t.details?p.tools:p.tools.map(f=>f.toolId)}});return json({devices:h,...t.details?{}:{note:`Tool names only. Pass details:true, or list_tool_actions({toolId}), for action names.${s?` ${s} apps seen before but not connected now are left out (includeDisconnected:true lists them).`:""}`},...l.length>0?{warning:`${l.map(p=>`"${p.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...c?{}:{hint:u?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...r?{updateAvailable:r}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a tool exposes, and which connected devices offer it. Leave toolId out to list every tool's actions. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().optional().describe('The tool id, e.g. "perf-monitor", "network", "react-query". Leave out for every tool.'),tool:import_zod.z.string().optional().describe("Same as toolId."),deviceId:import_zod.z.string().optional().describe("Only list what this device offers.")}},async({toolId:t,tool:r,deviceId:i})=>{const s=t??r;if(!s){const c=n.connectedDevices().filter(l=>!i||l.deviceId===i);if(c.length===0)return fail(i?`No connected device "${i}". Call list_devices.`:"No devices are connected. Call list_devices.");const u=c.map(l=>[`**${l.deviceName}** (${l.deviceId})`,...l.tools.map(d=>`- ${d.toolId}: ${d.actions.join(", ")}`)].join("\n"));return{content:[{type:"text",text:`${u.join("\n\n")}
|
|
42
42
|
|
|
43
43
|
Call one with call_action({toolId, action, params}).`}]}}const o=new Set(n.connectedDevices().map(c=>c.deviceId));const a=n.toolActions(s).filter(c=>o.has(c.deviceId)&&(!i||c.deviceId===i));if(a.length===0){const c=[...new Set(n.connectedDevices().flatMap(u=>u.tools.map(l=>l.toolId)))].sort();return fail(`No connected device exposes tool "${s}". Tool ids: ${c.join(", ")||"(none connected)"}. MCP tool names like "${s}" are not tool ids.`)}return json({toolId:s,offerings:a})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().optional().describe("The tool id to read state from."),tool:import_zod.z.string().optional().describe("Same as toolId."),deviceId:import_zod.z.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,tool:r,deviceId:i,timeoutMs:s})=>{try{const o=t??r;if(!o)return fail("get_snapshot needs toolId (see list_tool_actions).");const a=n.requireProDevice(i);const{payload:c,stale:u}=await n.getSnapshot(o,a,{timeoutMs:s});return json({toolId:o,deviceId:a,...u?{stale:u}:{},snapshot:c})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().optional().describe('The tool id, e.g. "perf-monitor".'),tool:import_zod.z.string().optional().describe("Same as toolId."),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.union([import_zod.z.record(import_zod.z.unknown()),import_zod.z.string()]).optional().describe("JSON params object for the action (shape depends on the action)."),args:import_zod.z.union([import_zod.z.record(import_zod.z.unknown()),import_zod.z.string()]).optional().describe("Same as params."),deviceId:import_zod.z.string().optional().describe('The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out. Or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,tool:r,action:i,params:s,args:o,deviceId:a,timeoutMs:c})=>{try{const u=t??r;if(!u)return fail("call_action needs toolId (see list_tool_actions).");const l=actionParams(s??o);const d=n.requireProDevice(a);const h=await n.callAction(u,i,l,d,{timeoutMs:c});return json({toolId:u,action:i,ok:true,result:h??null})}catch(u){return fail(errMsg(u))}})}function actionParams(e){if(e===void 0||typeof e==="object")return e;const n=e.trim();if(!n)return void 0;let t;try{t=JSON.parse(n)}catch{throw new Error('params is a string but not JSON. Pass an object, e.g. {"user": {...}}.')}if(!t||typeof t!=="object"||Array.isArray(t))throw new Error("params must be a JSON object.");return t}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod;var init_generic=__esm({"src/tools/generic.ts"(){"use strict";import_zod=require("zod");init_checkForUpdate();init_simctl();init_hostKit()}});function clampLimit(e,n){return typeof e==="number"&&e>0?e:n}function staleHeader(e){return e?`${e}
|
|
44
44
|
|
|
@@ -223,12 +223,12 @@ Errors: ${o.errors.join("; ")}`:"";return text4(`**Deactivated.**
|
|
|
223
223
|
${a.map(d=>` \xB7 ${d}`).join("\n")}${u}${l}`)}case"save":{if(!t.scenario)return fail5("save requires `scenario`.");const s=await i("save",{scenario:t.scenario});if(!s.ok){return fail5(`Save rejected:
|
|
224
224
|
${(s.errors??[s.error??"unknown error"]).map(o=>` \xB7 ${o}`).join("\n")}`)}return text4(`Saved \`${s.id}\` **as a draft**.
|
|
225
225
|
|
|
226
|
-
It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`)}case"setFolder":{if(!t.id)return fail5("setFolder requires `id` (from get_scenarios).");const s=await i("setFolder",{id:t.id,folder:t.folder});return text4(s.folder?`Filed \`${s.id}\` under **${s.folder}**.`:`Removed \`${s.id}\` from its folder \u2014 it now shows under Ungrouped.`)}case"delete":{if(!t.id)return fail5("delete requires `id`.");await i("delete",{id:t.id});return text4(`Deleted \`${t.id}\` from the device library.`)}}}catch(r){return fail5(errMsg5(r))}})}var import_zod20,SCENARIOS_TOOL_ID,text4,fail5,errMsg5;var init_scenarios=__esm({"src/tools/packs/scenarios.ts"(){"use strict";import_zod20=require("zod");init_packKit();SCENARIOS_TOOL_ID="scenarios";text4=e=>({content:[{type:"text",text:e}]});fail5=e=>({isError:true,content:[{type:"text",text:e}]});errMsg5=e=>e instanceof Error?e.message:String(e)}});function pickDescribeScreenMatch(e,n,t){if(t!=null){const s=e.find(o=>o.nativeTag===t);if(s)return s}const r=(n??"").trim().toLowerCase();if(!r)return void 0;let i;for(const s of e){const o=[s.testID,s.label,s.text,s.name].filter(Boolean).map(u=>u.toLowerCase());let a=0;for(const u of o){if(u===r)a=Math.max(a,100);else if(u.startsWith(r))a=Math.max(a,50);else if(u.includes(r))a=Math.max(a,25)}const c=s.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)a+=20;else if(c)a-=40;if(a<=0)continue;if(!i||a>i.score)i={el:s,score:a}}return i?.el}function frameToRect(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var init_screenshotLocate=__esm({"src/tools/packs/screenshotLocate.ts"(){"use strict"}});function pixelmatch(e,n,t,r,i,s={}){const{threshold:o=.1,alpha:a=.1,aaColor:c=[255,255,0],diffColor:u=[255,0,0],checkerboard:l=true,includeAA:d,diffColorAlt:h,diffMask:p}=s;if(!isPixelData(e)||!isPixelData(n)||t&&!isPixelData(t))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(e.length!==n.length||t&&t.length!==e.length)throw new Error(`Image sizes do not match. Image 1 size: ${e.length}, image 2 size: ${n.length}`);if(e.length!==r*i*4)throw new Error(`Image data size does not match width/height. Expecting ${r*i*4}. Got ${e.length}`);const m=r*i;const g=new Uint32Array(e.buffer,e.byteOffset,m);const w=new Uint32Array(n.buffer,n.byteOffset,m);let f=true;for(let S=0;S<m;S++){if(g[S]!==w[S]){f=false;break}}if(f){if(t&&!p){for(let S=0,x=0;S<m;S++,x+=4)drawGrayPixel(e,x,a,t)}return 0}const b=35215*o*o;const[v,y,_]=c;const[$,k,T]=u;const[A,I,R]=h||u;let M=0;for(let S=0,x=0;S<m;S++,x+=4){const E=g[S]===w[S]?0:colorDelta(e,n,x,x,l);if(Math.abs(E)>b){const j=S%r;const P=S/r|0;const
|
|
226
|
+
It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`)}case"setFolder":{if(!t.id)return fail5("setFolder requires `id` (from get_scenarios).");const s=await i("setFolder",{id:t.id,folder:t.folder});return text4(s.folder?`Filed \`${s.id}\` under **${s.folder}**.`:`Removed \`${s.id}\` from its folder \u2014 it now shows under Ungrouped.`)}case"delete":{if(!t.id)return fail5("delete requires `id`.");await i("delete",{id:t.id});return text4(`Deleted \`${t.id}\` from the device library.`)}}}catch(r){return fail5(errMsg5(r))}})}var import_zod20,SCENARIOS_TOOL_ID,text4,fail5,errMsg5;var init_scenarios=__esm({"src/tools/packs/scenarios.ts"(){"use strict";import_zod20=require("zod");init_packKit();SCENARIOS_TOOL_ID="scenarios";text4=e=>({content:[{type:"text",text:e}]});fail5=e=>({isError:true,content:[{type:"text",text:e}]});errMsg5=e=>e instanceof Error?e.message:String(e)}});function pickDescribeScreenMatch(e,n,t){if(t!=null){const s=e.find(o=>o.nativeTag===t);if(s)return s}const r=(n??"").trim().toLowerCase();if(!r)return void 0;let i;for(const s of e){const o=[s.testID,s.label,s.text,s.name].filter(Boolean).map(u=>u.toLowerCase());let a=0;for(const u of o){if(u===r)a=Math.max(a,100);else if(u.startsWith(r))a=Math.max(a,50);else if(u.includes(r))a=Math.max(a,25)}const c=s.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)a+=20;else if(c)a-=40;if(a<=0)continue;if(!i||a>i.score)i={el:s,score:a}}return i?.el}function frameToRect(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var init_screenshotLocate=__esm({"src/tools/packs/screenshotLocate.ts"(){"use strict"}});function pixelmatch(e,n,t,r,i,s={}){const{threshold:o=.1,alpha:a=.1,aaColor:c=[255,255,0],diffColor:u=[255,0,0],checkerboard:l=true,includeAA:d,diffColorAlt:h,diffMask:p}=s;if(!isPixelData(e)||!isPixelData(n)||t&&!isPixelData(t))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(e.length!==n.length||t&&t.length!==e.length)throw new Error(`Image sizes do not match. Image 1 size: ${e.length}, image 2 size: ${n.length}`);if(e.length!==r*i*4)throw new Error(`Image data size does not match width/height. Expecting ${r*i*4}. Got ${e.length}`);const m=r*i;const g=new Uint32Array(e.buffer,e.byteOffset,m);const w=new Uint32Array(n.buffer,n.byteOffset,m);let f=true;for(let S=0;S<m;S++){if(g[S]!==w[S]){f=false;break}}if(f){if(t&&!p){for(let S=0,x=0;S<m;S++,x+=4)drawGrayPixel(e,x,a,t)}return 0}const b=35215*o*o;const[v,y,_]=c;const[$,k,T]=u;const[A,I,R]=h||u;let M=0;for(let S=0,x=0;S<m;S++,x+=4){const E=g[S]===w[S]?0:colorDelta(e,n,x,x,l);if(Math.abs(E)>b){const j=S%r;const P=S/r|0;const B=!d&&(antialiased(e,j,P,r,i,g,w,l)||antialiased(n,j,P,r,i,w,g,l));if(B){if(t&&!p)drawPixel(t,x,v,y,_)}else{if(t){if(E<0){drawPixel(t,x,A,I,R)}else{drawPixel(t,x,$,k,T)}}M++}}else if(t&&!p){drawGrayPixel(e,x,a,t)}}return M}function isPixelData(e){return ArrayBuffer.isView(e)&&e.BYTES_PER_ELEMENT===1}function antialiased(e,n,t,r,i,s,o,a){const c=Math.max(n-1,0);const u=Math.max(t-1,0);const l=Math.min(n+1,r-1);const d=Math.min(t+1,i-1);const h=(t*r+n)*4;const p=e[h];const m=e[h+1];const g=e[h+2];const w=e[h+3];let f=n===c||n===l||t===u||t===d?1:0;let b=0;let v=0;let y=0;let _=0;let $=0;let k=0;for(let T=c;T<=l;T++){for(let A=u;A<=d;A++){if(T===n&&A===t)continue;const I=brightnessDelta(e,h,(A*r+T)*4,p,m,g,w,a);if(I===0){f++;if(f>2)return false}else if(I<b){b=I;y=T;_=A}else if(I>v){v=I;$=T;k=A}}}if(b===0||v===0)return false;return hasManySiblings(s,y,_,r,i)&&hasManySiblings(o,y,_,r,i)||hasManySiblings(s,$,k,r,i)&&hasManySiblings(o,$,k,r,i)}function hasManySiblings(e,n,t,r,i){const s=Math.max(n-1,0);const o=Math.max(t-1,0);const a=Math.min(n+1,r-1);const c=Math.min(t+1,i-1);const u=e[t*r+n];let l=n===s||n===a||t===o||t===c?1:0;for(let d=s;d<=a;d++){for(let h=o;h<=c;h++){if(d===n&&h===t)continue;l+=+(u===e[h*r+d]);if(l>2)return true}}return false}function colorDelta(e,n,t,r,i){const s=e[t];const o=e[t+1];const a=e[t+2];const c=e[t+3];const u=n[r];const l=n[r+1];const d=n[r+2];const h=n[r+3];let p=s-u;let m=o-l;let g=a-d;const w=c-h;if(c<255||h<255){let _=255,$=255,k=255;if(i){_=48+159*(t%2);$=48+159*((t/1.618033988749895|0)%2);k=48+159*((t/2.618033988749895|0)%2)}p=(s*c-u*h-_*w)/255;m=(o*c-l*h-$*w)/255;g=(a*c-d*h-k*w)/255}const f=p*.29889531+m*.58662247+g*.11448223;const b=p*.59597799-m*.2741761-g*.32180189;const v=p*.21147017-m*.52261711+g*.31114694;const y=.5053*f*f+.299*b*b+.1957*v*v;return f>0?-y:y}function brightnessDelta(e,n,t,r,i,s,o,a){const c=e[t];const u=e[t+1];const l=e[t+2];const d=e[t+3];let h=r-c;let p=i-u;let m=s-l;const g=o-d;if(!h&&!p&&!m&&!g)return 0;if(o<255||d<255){let w=255,f=255,b=255;if(a){w=48+159*(n%2);f=48+159*((n/1.618033988749895|0)%2);b=48+159*((n/2.618033988749895|0)%2)}h=(r*o-c*d-w*g)/255;p=(i*o-u*d-f*g)/255;m=(s*o-l*d-b*g)/255}return h*.29889531+p*.58662247+m*.11448223}function drawPixel(e,n,t,r,i){e[n]=t;e[n+1]=r;e[n+2]=i;e[n+3]=255}function drawGrayPixel(e,n,t,r){const i=255+(e[n]*.29889531+e[n+1]*.58662247+e[n+2]*.11448223-255)*t*e[n+3]/255;drawPixel(r,n,i,i,i)}var init_pixelmatch=__esm({"../../node_modules/pixelmatch/index.js"(){"use strict"}});function decodePng(e){return import_pngjs3.PNG.sync.read(e)}function diffScreens(e,n,t={}){const r=decodePng(e);const i=decodePng(n);if(r.width!==i.width||r.height!==i.height){throw new Error(`The screenshots are different sizes (${r.width}\xD7${r.height} vs ${i.width}\xD7${i.height}). Compare on the same device and orientation, or save a new baseline.`)}const{width:s,height:o}=r;const a=Math.round(o*(t.ignoreTop??0));for(const p of[r,i])p.data.fill(0,0,a*s*4);for(const p of t.masks??[]){const m=Math.max(0,Math.floor(p.x*s));const g=Math.min(s,Math.ceil((p.x+p.width)*s));const w=Math.max(0,Math.floor(p.y*o));const f=Math.min(o,Math.ceil((p.y+p.height)*o));for(const b of[r,i])for(let v=w;v<f;v++)b.data.fill(0,(v*s+m)*4,(v*s+g)*4)}const c=t.threshold??.1;const u=new import_pngjs3.PNG({width:s,height:o});const l=pixelmatch(r.data,i.data,u.data,s,o,{threshold:c,includeAA:false,diffMask:true});const d=new import_pngjs3.PNG({width:s,height:o});pixelmatch(r.data,i.data,d.data,s,o,{threshold:c,includeAA:false,alpha:.25,diffColor:[230,30,40]});const h=s*(o-a);return{width:s,height:o,changedPixels:l,percent:h?l/h*100:0,boxes:changedBoxes(u.data,s,o),diffPng:import_pngjs3.PNG.sync.write(d)}}function changedBoxes(e,n,t){const r=Math.max(8,Math.round(n/48));const i=Math.ceil(n/r);const s=Math.ceil(t/r);const o=new Uint32Array(i*s);for(let l=0;l<t;l++){for(let d=0;d<n;d++){if(e[(l*n+d)*4+3]>0)o[Math.floor(l/r)*i+Math.floor(d/r)]++}}const a=1;const c=new Uint8Array(i*s);const u=[];for(let l=0;l<o.length;l++){if(c[l]||o[l]<a)continue;const d={x0:Infinity,y0:Infinity,x1:-1,y1:-1,pixels:0};const h=[l];c[l]=1;while(h.length){const p=h.pop();const m=p%i;const g=Math.floor(p/i);d.x0=Math.min(d.x0,m);d.y0=Math.min(d.y0,g);d.x1=Math.max(d.x1,m);d.y1=Math.max(d.y1,g);d.pixels+=o[p];for(let w=-2;w<=2;w++){for(let f=-2;f<=2;f++){const b=m+f;const v=g+w;if(b<0||v<0||b>=i||v>=s)continue;const y=v*i+b;if(!c[y]&&o[y]>=a){c[y]=1;h.push(y)}}}}u.push(d)}return u.sort((l,d)=>d.pixels-l.pixels).slice(0,8).map(l=>({x:l.x0*r/n,y:l.y0*r/t,width:Math.min(n,(l.x1+1)*r-l.x0*r)/n,height:Math.min(t,(l.y1+1)*r-l.y0*r)/t,pixels:l.pixels}))}function describeSpot(e){const n=e.x+e.width/2;const t=e.y+e.height/2;const r=t<.33?"top":t<.66?"middle":"bottom";const i=n<.33?" left":n<.66?"":" right";return`${r}${i}`}var import_pngjs3;var init_imageDiff=__esm({"src/capture/imageDiff.ts"(){"use strict";import_pngjs3=__toESM(require_png());init_pixelmatch()}});function reasonMessage(e,n){switch(e.reason){case"no-renders":return"No components matched via highlight tracking or the live fiber walk. Pass a testID / name from describe_screen, then retry.";case"no-match":return n?`Nothing matched "${n}".`:"No query or nativeTag given \u2014 pass one, or a testID/name to search for.";case"no-measurement":return"Found the component but couldn't measure it \u2014 it may have just unmounted. Re-navigate to it and retry.";default:return e.reason?`Locate failed: ${e.reason}`:"Couldn't locate that component."}}function candidatesText(e){if(!e||e.length===0)return"";const n=e.map(t=>{const r=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${r?` (${r})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
|
|
227
227
|
|
|
228
228
|
Closest matches (retry with a more precise query, or pass nativeTag directly):
|
|
229
229
|
${n.join("\n")}`}function registerScreenshotTools(e,n){e.registerTool("screenshot_component",{description:"Screenshot the app. Leave query and nativeTag out for the whole screen (iOS Simulator or Android, shrunk to keep it cheap). Give a query or nativeTag to crop one React Native component (iOS Simulator only); it can scroll the component into view. Prefer describe_screen for reading text and finding controls: a picture costs far more tokens. Needs the MCP server on the same computer as the simulator or adb device.",inputSchema:{query:import_zod21.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:import_zod21.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:import_zod21.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),serial:import_zod21.z.string().optional().describe("Android: the adb serial, when several devices are connected."),baseline:import_zod21.z.string().optional().describe('Whole screen only: a name like "home". The first call saves the screen as that baseline (in .buoy/baselines); later calls compare against it and report what changed, with a diff picture.'),updateBaseline:import_zod21.z.boolean().optional().describe("With baseline: save the current screen as the new baseline instead of comparing."),steady:import_zod21.z.boolean().optional().describe("With baseline: freeze the app's clock at the baseline's moment while capturing, so countdowns and times match (needs @buoy-gg/clock). Default true when the app has the clock tool."),threshold:import_zod21.z.number().min(0).max(1).optional().describe("With baseline: per-pixel color tolerance, 0 to 1. Default 0.1."),ignore:import_zod21.z.array(import_zod21.z.string()).optional().describe('With baseline: leave these out of the comparison, by testID (e.g. ["live-map", "ad-banner"]), for parts that change on their own.'),ignoreAreas:import_zod21.z.array(import_zod21.z.object({x:import_zod21.z.number(),y:import_zod21.z.number(),width:import_zod21.z.number(),height:import_zod21.z.number()})).optional().describe("With baseline: areas to leave out, as fractions of the screen (0 to 1)."),deviceId:deviceIdInput}},async t=>{try{if(t.baseline){if(t.query||t.nativeTag!=null)return fail6("baseline works on the whole screen: leave query and nativeTag out.");return await baselineShot(n,t.baseline,{deviceId:t.deviceId,serial:t.serial,update:t.updateBaseline,threshold:t.threshold,steady:t.steady,ignore:t.ignore,ignoreAreas:t.ignoreAreas})}if(!t.query&&t.nativeTag==null){const{id:y,host:_,online:$}=await hostTarget(n,t.deviceId,{serial:t.serial},{allowOffline:true});let k=Buffer.alloc(0);const T=await maskSecrets(n,y,$,async()=>k=await hostPng(_));const A={length:T.covered};const I=await toSmallImage(T.png);const R=_.platform==="ios-device"&&isAllBlack(k)?` ${BLACK_PHONE_NOTE}`:"";return{content:[{type:"image",data:I.data,mimeType:I.mimeType},{type:"text",text:`Whole screen of ${hostLabel(_)}, ${I.width}\xD7${I.height}px.${A.length?` ${A.length} element(s) showing a secret were blacked out.`:""}${R}`}]}}const r=n.requireProDevice(t.deviceId);await n.callAction(TOOL_ID2,"setSilentTracking",{enabled:true},r).catch(()=>{});let i=t.nativeTag;const s=i==null&&t.query?await exactTag(n,r,t.query):null;if(s)i=s.tag;const o=await n.callAction(TOOL_ID2,"locateComponent",{query:i==null?t.query:void 0,nativeTag:i,scrollIntoView:t.scrollIntoView??true,margin:MARGIN_PT},r);let a=o;if(!a.matched&&(a.reason==="no-renders"||a.reason==="no-match")){const y=await n.callAction(TOOL_ID2,"describeScreen",{},r).catch(()=>null);const _=y?.elements?pickDescribeScreenMatch(y.elements,t.query,i):void 0;if(_?.frame&&y?.screen){a={matched:true,live:true,rect:frameToRect(_.frame,y.screen),screen:y.screen,nativeTag:_.nativeTag??void 0,componentName:_.name,testID:_.testID}}else if(a.reason==="no-renders"){a={matched:false,reason:"no-match",candidates:a.candidates}}}if(!a.matched||!a.rect||!a.screen){return fail6(reasonMessage(a,t.query)+candidatesText(a.candidates))}if(i==null&&t.query){const y=t.query.toLowerCase();const _=a.candidates?.find(I=>I.nativeTag===a.nativeTag);const $=await n.callAction(TOOL_ID2,"describeScreen",{},r,{timeoutMs:4e3}).catch(()=>null);const k=$?.elements?.find(I=>I.nativeTag===a.nativeTag);const T=[a.testID,a.nativeID,a.componentName,_?.testID,_?.nativeID,_?.componentName,k?.testID,k?.nativeID,k?.text,k?.label].filter(Boolean).map(I=>String(I).toLowerCase());const A=T.some(I=>I.includes(y)||I.length>3&&y.includes(I));if(!A){return fail6(`"${t.query}" is not on the current screen (the closest component was ${a.componentName??"unknown"}${a.testID?` [${a.testID}]`:""}). Navigate to it first, or pass a testID from describe_screen.`+candidatesText(a.candidates))}}const{host:c}=await hostTarget(n,r);if(c.platform!=="ios")return fail6("Cropping one component works on the iOS Simulator. Leave query out for a whole-screen picture.");let u={ok:false};const l=await maskSecrets(n,r,true,async()=>{u=await captureIosFramebuffer(c.udid);return u.ok&&u.srcPath?import_node_fs8.promises.readFile(u.srcPath):Buffer.alloc(0)});if(l.covered&&u.ok&&u.srcPath)await import_node_fs8.promises.writeFile(u.srcPath,l.png);if(!u.ok||!u.srcPath||!u.width||!u.height){return fail6(u.error??"Screenshot capture failed.")}const d=u.width/a.screen.width;const h=a.rect;const p=CAPTURE_PADDING_PT;const m=await cropToBuffer(u.srcPath,{width:u.width,height:u.height},{x:(h.x-p)*d,y:(h.y-p)*d,width:(h.width+p*2)*d,height:(h.height+p*2)*d});if(!m.ok||!m.buffer){return fail6(m.error??"Crop failed.")}const g=a.componentName||s?.name||"component";const w=a.testID??s?.testID;const f=w?` (testID: "${w}")`:"";const b=a.inView===false?"\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge.":"";const v=`Captured "${g}"${f} \u2014 ${m.width}\xD7${m.height}px.${b}`;return{content:[{type:"image",data:m.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:v}]}}catch(r){return fail6(errMsg6(r))}})}async function maskSecrets(e,n,t,r){const i=typeof r==="function"?r:async()=>r;const s=secretValues();if(!s.length)return{png:await i(),covered:0};const o=t?await secretBoxes(e,n,s):null;const a=await i();const c=t?await secretBoxes(e,n,s):null;if(o===null||c===null){if(t||typedSecretValues().length){throw new Error("Buoy can't read the app's screen to hide secrets right now, so no picture was taken. Try again in a moment.")}return{png:a,covered:0}}const u=[...o,...c];if(!a.length)return{png:a,covered:0};return{png:u.length?blackOut(a,u):a,covered:Math.max(o.length,c.length)}}async function secretBoxes(e,n,t){const r=await e.callAction(TOOL_ID2,"describeScreen",{includeHidden:false,includeBuoy:true,fullText:true},n,{timeoutMs:4e3}).catch(()=>null);if(!r?.screen)return null;const i=typedSecretValues().length>0;const s=[];for(const o of r.elements??[]){const a=o.value;const c=[o.text,o.label,a].filter(h=>h!=null&&h!=="").map(String);const u=c.some(h=>t.some(p=>showsSecret(h,p)));const l=i&&o.control==="text"&&a===void 0;if(!u&&!l)continue;const d=o.frame?.width&&o.frame.height?fractionFrame(o,r.screen):void 0;if(!d)return null;s.push({...d,pixels:0})}return s}function showsSecret(e,n){const t=e.replace(/\s+/g," ");const r=n.replace(/\s+/g," ").trim();if(t.includes(r))return true;const i=t.replace(/(…|\.\.\.)$/,"");for(let s=r.length-1;s>=MIN_SECRET_LENGTH;s--)if(i.endsWith(r.slice(0,s)))return true;return false}function blackOut(e,n){const t=import_pngjs4.PNG.sync.read(e);for(const r of n){const i=Math.max(0,Math.floor((r.x-.01)*t.width));const s=Math.min(t.width,Math.ceil((r.x+r.width+.01)*t.width));const o=Math.max(0,Math.floor((r.y-.005)*t.height));const a=Math.min(t.height,Math.ceil((r.y+r.height+.005)*t.height));for(let c=o;c<a;c++){for(let u=i;u<s;u++){const l=(c*t.width+u)*4;t.data[l]=t.data[l+1]=t.data[l+2]=0;t.data[l+3]=255}}}return import_pngjs4.PNG.sync.write(t)}async function exactTag(e,n,t){const r=await e.callAction(TOOL_ID2,"describeScreen",{},n,{timeoutMs:4e3}).catch(()=>null);const i=r?.elements??[];const s=u=>({tag:u.nativeTag,name:u.name,testID:u.testID});const o=i.filter(u=>u.testID===t&&u.nativeTag!=null);if(o.length===1)return s(o[0]);const a=t.trim().toLowerCase();const c=i.filter(u=>(u.text??u.label??"").trim().toLowerCase()===a&&u.nativeTag!=null);return c.length===1?s(c[0]):null}function baselinePath(e){const n=e.trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"")||"screen";return import_node_path9.default.join(process.env.BUOY_PROJECT_ROOT||process.cwd(),".buoy","baselines",`${n}.png`)}function fractionFrame(e,n){if(e.frame&&e.frame.width&&e.frame.height){const t=e.frame.x+e.frame.width>1.5||e.frame.y+e.frame.height>1.5;return t?{x:e.frame.x/n.width,y:e.frame.y/n.height,width:e.frame.width/n.width,height:e.frame.height/n.height}:{...e.frame}}if(e.tap)return{x:e.tap.x-.01,y:e.tap.y-.01,width:.02,height:.02};return void 0}function elementsIn(e,n){const t=[];for(const r of n){const i=r.frame;if(!i)continue;const s=Math.max(0,Math.min(e.x+e.width,i.x+i.width)-Math.max(e.x,i.x));const o=Math.max(0,Math.min(e.y+e.height,i.y+i.height)-Math.max(e.y,i.y));const a=i.width*i.height;if(!a||s*o/a<.5)continue;t.push({label:labelOf(r),area:a,words:!!r.words})}return[...new Set(t.sort((r,i)=>Number(i.words)-Number(r.words)||r.area-i.area).map(r=>r.label))].slice(0,3)}function textChanges(e,n){const t=d=>d.testID?`id:${d.testID}`:`${d.name??""}@${d.frame?`${Math.round(d.frame.x*40)},${Math.round(d.frame.y*40)}`:"?"}`;const r=new Map(e.filter(d=>d.words).map(d=>[t(d),d]));const i=new Map(n.filter(d=>d.words).map(d=>[t(d),d]));const s=new Set([...r.values()].map(d=>d.words));const o=new Set([...i.values()].map(d=>d.words));const a=[];for(const[d,h]of i){const p=r.get(d);if(p){if(p.words!==h.words)a.push(`changed: ${labelOf({...h,words:void 0})} "${p.words}" \u2192 "${h.words}"`)}else if(!s.has(h.words))a.push(`appeared: ${labelOf(h)}`)}for(const[d,h]of r)if(!i.has(d)&&!o.has(h.words))a.push(`gone: ${labelOf(h)}`);const c=[...r.entries()].filter(([d,h])=>!i.has(d)&&!o.has(h.words)).map(([,d])=>d);const u=[...i.entries()].filter(([d,h])=>!r.has(d)&&!s.has(h.words)).map(([,d])=>d);const l=(d,h)=>!!d&&!!h&&d.x<h.x+h.width&&h.x<d.x+d.width&&d.y<h.y+h.height&&h.y<d.y+d.height;for(const d of u){const h=c.find(m=>m.name===d.name&&l(m.frame,d.frame));if(!h)continue;c.splice(c.indexOf(h),1);const p=new Set([`appeared: ${labelOf(d)}`,`gone: ${labelOf(h)}`]);for(let m=a.length-1;m>=0;m--)if(p.has(a[m]))a.splice(m,1);a.push(`changed: ${labelOf({...d,words:void 0})} "${h.words}" \u2192 "${d.words}"`)}return a.slice(0,12)}async function readScreen(e,n){try{const[t,r,i]=await Promise.all([e.callAction("highlight-updates","describeScreen",{includeBuoy:true},n,{timeoutMs:4e3}),e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:4e3}),e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:2e3}).catch(()=>null)]);const s=r?.screen??t?.screen;if(!s)return null;const o=new Set((r?.elements??[]).map(l=>l.nativeTag));const a=[];for(const l of t?.elements??[]){if(o.has(l.nativeTag))continue;const d=fractionFrame(l,s);if(d)a.push({x:d.x-.01,y:d.y-.01,width:d.width+.02,height:d.height+.02,pixels:0})}const c=(r?.elements??[]).map(l=>{const d=scrubSecrets((l.text??l.label??"").replace(/[\s\u200b\u00a0]+/g," ").trim());return{name:l.name,words:d||void 0,testID:l.testID?scrubSecrets(l.testID):void 0,frame:fractionFrame(l,s)}});const u=i?.path??i?.pathname;return{elements:c,masks:a,route:u?scrubSecrets(u):void 0}}catch{return null}}async function withFrozenClock(e,n,t,r){let i;let s;let o;try{await e.callAction("clock","freeze",{time:t},n);await new Promise(c=>setTimeout(c,1300));i=await r()}catch(c){s=c}finally{try{await e.callAction("clock","reset",{},n)}catch(c){o=c}}const a=o?` WARNING: putting the app's clock back failed (${errMsg6(o)}); run clock_action reset.`:"";if(s)throw new Error(`${errMsg6(s)}${a}`);return{result:i,note:` The app's clock was frozen at ${new Date(t).toISOString()} for the capture.${a}`}}async function baselineShot(...e){return(await runBaseline(...e)).result}async function runBaseline(e,n,t){const{id:r,host:i}=await hostTarget(e,t.deviceId,{serial:t.serial});const s=baselinePath(n);const o=s.replace(/\.png$/,".json");const a=import_node_path9.default.relative(process.env.BUOY_PROJECT_ROOT||process.cwd(),s);let c=null;try{c=await import_node_fs8.promises.readFile(s)}catch{}const u=!c||!!t.update;const l=u?{}:JSON.parse(await import_node_fs8.promises.readFile(o,"utf8").catch(()=>"{}"));const d=hostLabel(i);const h=e.connectedDevices().find(D=>D.deviceId===r)?.tools.some(D=>D.toolId==="clock")??false;const p=t.steady??h;let m="";const g=async()=>{const{result:D,steadied:z}=await withSteadyStatusBar(i,async()=>({png:(await maskSecrets(e,r,true,()=>hostPng(i))).png,screen:await readScreen(e,r)}));return{...D,steadied:z}};let w;if(p&&h){const D=await e.callAction("clock","getState",{},r).catch(()=>null);if(D?.mode==="real"){const z=u||!l.steadyTime?Math.floor(Date.now()/6e4)*6e4:l.steadyTime;l.steadyTime=z;const L=await withFrozenClock(e,r,z,g);w=L.result;m+=L.note}else{w=await g();m+=" The app's clock was already overridden, so it was left as it was."}}else{w=await g();if(p&&!h)m+=" (steady needs the Clock tool in the app; times on screen may differ.)"}const f={width:w.png.readUInt32BE(16),height:w.png.readUInt32BE(20)};if(u){await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(s),{recursive:true});await import_node_fs8.promises.writeFile(s,w.png);const D={...l,device:d,size:f,route:w.screen?.route,elements:w.screen?.elements};await import_node_fs8.promises.writeFile(o,JSON.stringify(D,null,1));const z=await toSmallImage(w.png);const L=`${c?"Replaced":"Saved"} baseline "${n}"`;return{outcome:{saved:true,changed:false,percent:0,texts:0,summary:`${L} (first run)`},result:{content:[{type:"image",data:z.data,mimeType:z.mimeType},{type:"text",text:`${c?"Replaced":"Saved"} baseline "${n}" (${a}) from ${d}${w.screen?.route?` on ${w.screen.route}`:""}. Call again with the same baseline to compare.${m}`}]}}}const b=[];if(l.device&&l.device!==d)b.push(`The baseline was saved on ${l.device}; this is ${d}.`);if(l.route&&w.screen?.route&&l.route!==w.screen.route)b.push(`The baseline was on ${l.route}; the app is now on ${w.screen.route}.`);const v=[...t.ignoreAreas??[]];const y=new Set(t.ignore??[]);const _=new Set(y);for(const D of[...l.elements??[],...w.screen?.elements??[]]){if(!D.testID||!y.has(D.testID)||!D.frame)continue;_.delete(D.testID);v.push({x:D.frame.x-.005,y:D.frame.y-.005,width:D.frame.width+.01,height:D.frame.height+.01})}if(_.size)b.push(`Not on screen, so not left out: ${[..._].join(", ")}.`);const $=i.platform==="android"?[]:[{x:.28,y:0,width:.44,height:.055}];const k=diffScreens(c,w.png,{threshold:t.threshold,masks:[...w.screen?.masks??[],...v,...$],ignoreTop:w.steadied?0:.065});const T=D=>!!D&&v.some(z=>D.x+D.width/2>=z.x&&D.x+D.width/2<=z.x+z.width&&D.y+D.height/2>=z.y&&D.y+D.height/2<=z.y+z.height);const A=D=>(!D.testID||!y.has(D.testID))&&!T(D.frame);const I=l.elements&&w.screen?textChanges(l.elements.filter(A),w.screen.elements.filter(A)):[];const R=[w.steadied?"":"status bar",w.screen?.masks.length?"Buoy's overlay":"",v.length?"the parts you asked to ignore":""].filter(Boolean);const M=R.join(" and ");const S=M?`; ${M} left out`:"";const x=b.length?`
|
|
230
230
|
|
|
231
|
-
\u26A0\uFE0F ${b.join(" ")}`:"";if(k.changedPixels===0&&I.length===0){return{outcome:{saved:false,changed:false,percent:0,texts:0,summary:`no change from baseline "${n}"`},result:text(`No change from baseline "${n}" (${a}${S}).${m}${x}`)}}const E=k.boxes.map((D,z)=>{const L=w.screen?elementsIn(D,w.screen.elements):[];const J=`${describeSpot(D)} (x ${D.x.toFixed(2)}, y ${D.y.toFixed(2)}, ${D.width.toFixed(2)}\xD7${D.height.toFixed(2)})`;return`${z+1}. ${J}${L.length?`: ${L.join(", ")}`:""}`});const j=await toSmallImage(k.diffPng);const P=k.percent<.01&&k.changedPixels>0?"<0.01":k.percent.toFixed(2);const
|
|
231
|
+
\u26A0\uFE0F ${b.join(" ")}`:"";if(k.changedPixels===0&&I.length===0){return{outcome:{saved:false,changed:false,percent:0,texts:0,summary:`no change from baseline "${n}"`},result:text(`No change from baseline "${n}" (${a}${S}).${m}${x}`)}}const E=k.boxes.map((D,z)=>{const L=w.screen?elementsIn(D,w.screen.elements):[];const J=`${describeSpot(D)} (x ${D.x.toFixed(2)}, y ${D.y.toFixed(2)}, ${D.width.toFixed(2)}\xD7${D.height.toFixed(2)})`;return`${z+1}. ${J}${L.length?`: ${L.join(", ")}`:""}`});const j=await toSmallImage(k.diffPng);const P=k.percent<.01&&k.changedPixels>0?"<0.01":k.percent.toFixed(2);const B=E[0]?`; ${E[0].replace(/^1\. /,"")}`:"";const F=I[0]?`; ${I[0]}`:"";const C=k.changedPixels===0?`no pixel change from baseline "${n}", but text changed`:`${P}% changed from baseline "${n}"`;const O={saved:false,changed:true,percent:k.percent,texts:I.length,summary:`${C}${B}${F}`};return{outcome:O,result:{content:[{type:"image",data:j.data,mimeType:j.mimeType},{type:"text",text:`**Changed from baseline "${n}": ${k.percent<.01&&k.changedPixels>0?"<0.01":k.percent.toFixed(2)}% of the screen** (${k.changedPixels} px${S}). Changes are red in the picture.${m}`+(E.length?`
|
|
232
232
|
|
|
233
233
|
Where:
|
|
234
234
|
${E.join("\n")}`:"")+(I.length?`
|
|
@@ -380,14 +380,14 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
380
380
|
};
|
|
381
381
|
requestAnimationFrame(tick);
|
|
382
382
|
})`}});var webBench_exports={};__export(webBench_exports,{GPU_RENDERER_JS:()=>GPU_RENDERER_JS,PLAYWRIGHT_MISSING:()=>PLAYWRIGHT_MISSING,aggregateCase:()=>aggregateCase,caseUrl:()=>caseUrl,cdpDeltas:()=>cdpDeltas,checkBudgets:()=>checkBudgets,closeCall:()=>closeCall,closeWithin:()=>closeWithin,expectFailureFrom:()=>expectFailureFrom,formatWebBenchMarkdown:()=>formatWebBenchMarkdown,isSoftwareGpu:()=>isSoftwareGpu,keyMatches:()=>keyMatches,loadChromium:()=>loadChromium,locateStepTargets:()=>locateStepTargets,mergeInteractions:()=>mergeInteractions,missedInput:()=>missedInput,oneLine:()=>oneLine,percentile:()=>percentile,runWebBench:()=>runWebBench,shuffled:()=>shuffled,snapRefreshRate:()=>snapRefreshRate,summarizeFrameTrace:()=>summarizeFrameTrace,summarizeProbe:()=>summarizeProbe,takeHostLock:()=>takeHostLock,tiedPairs:()=>tiedPairs,validateWebBenchOptions:()=>validateWebBenchOptions,waitForHostLock:()=>waitForHostLock,webBenchFlags:()=>webBenchFlags,withDeadline:()=>withDeadline});function checkBudgets(e,n,t=[]){const r=e.median;const i=[];const s=[];const o=(d,h,p)=>{if(h==null)return;if(p==null)s.push(d);else if(p>h)i.push(`${d} ${p} > ${h}`)};o("droppedPct",n.droppedPct,e.droppedMin!=null?r?.droppedPct:null);const a=e.runs.filter(d=>!d.failed&&d.fps!=null);const c=t.some(d=>"click"in d||"press"in d||"type"in d);const u=a.map(d=>d.interactions==null||!d.inputCaptureComplete?null:d.worstInteractionMs??16);o("inputMs",n.inputMs,c&&u.length&&u.every(d=>d!=null)?Math.max(...u):null);const l=a.map(d=>Math.max(d.frameGapMaxMs??0,d.edgeSilenceMs??0));o("freezeMs",n.freezeMs,l.length?Math.max(...l):null);o("mainThreadPct",n.mainThreadPct,r?.mainThreadPct);o("jsHeapMb",n.jsHeapMb,r?.heapMb);return{meets:Boolean(r)&&i.length===0&&s.length===0,over:i,unknown:s}}function isSoftwareGpu(e){return/swiftshader|llvmpipe|softpipe|software|microsoft basic render/i.test(e)}function percentile(e,n){if(e.length===0)return null;const t=[...e].sort((i,s)=>i-s);const r=Math.ceil(n/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,r))]}function snapRefreshRate(e){for(const n of COMMON_RATES)if(Math.abs(e-n)/n<=.08)return n;return Math.max(1,Math.round(e))}function shuffled(e,n){let t=n>>>0;const r=()=>{t=t+1831565813>>>0;let s=t;s=Math.imul(s^s>>>15,s|1);s^=s+Math.imul(s^s>>>7,s|61);return((s^s>>>14)>>>0)/4294967296};const i=[...e];for(let s=i.length-1;s>0;s--){const o=Math.floor(r()*(s+1));[i[s],i[o]]=[i[o],i[s]]}return i}function caseUrl(e,n,t){const r=new URL(e);const i=n.path??t;if(i)r.pathname=i.startsWith("/")?i:`${r.pathname.replace(/\/$/,"")}/${i}`;for(const[s,o]of Object.entries(n.params??{}))r.searchParams.set(s,o);return r.toString()}function summarizeFrameTrace(e){const n=p=>e.find(m=>m.name===p&&m.ts!=null);const t=n("__buoy_bench_start");const r=n("__buoy_bench_end");if(!t||!r||r.ts<=t.ts)return null;if(t.pid!==r.pid)return null;const i=t.pid;const s={STATE_PRESENTED_ALL:0,STATE_PRESENTED_PARTIAL:1,STATE_DROPPED:2};const o=new Map;for(const p of e){if(p.name!=="PipelineReporter"||p.ph!=="b")continue;if(i!=null&&p.pid!==i)continue;if(p.ts==null||p.ts<t.ts||p.ts>r.ts)continue;const m=p.args?.frame_reporter;if(!m||m.frame_sequence==null)continue;const g=m.state;if(!FRAME_STATES.includes(g))continue;const w=`${m.layer_tree_host_id??""}:${m.frame_source??""}:${m.frame_sequence}`;const f=Boolean(m.affects_smoothness)&&g!=="STATE_PRESENTED_ALL";const b=o.get(w);if(!b)o.set(w,{state:g,fault:f});else{if(s[g]>s[b.state])b.state=g;b.fault=b.fault||f}}let a=0;let c=0;let u=0;let l=0;for(const p of o.values()){if(p.state==="STATE_PRESENTED_ALL")a++;else if(p.state==="STATE_PRESENTED_PARTIAL")c++;else u++;if(p.fault)l++}const d=a+c+u;if(d===0)return null;const h=(r.ts-t.ts)/1e6;return{full:a,partial:c,dropped:u,faults:l,shownFps:r1((a+c)/h),droppedPct:r1(l/d*100)}}function keyMatches(e,n){const t=e.split("+");const r=t[t.length-1];const i=new Set(t.slice(0,-1).map(a=>a.toLowerCase()));if(n.key!==r)return false;const s=r.toLowerCase();const o={ctrl:i.has("control")||s==="control",alt:i.has("alt")||s==="alt",meta:i.has("meta")||i.has("command")||s==="meta",shift:i.has("shift")||s==="shift"};if(Boolean(n.ctrl)!==o.ctrl||Boolean(n.alt)!==o.alt||Boolean(n.meta)!==o.meta)return false;if(!o.shift&&n.shift&&r.length!==1)return false;if(o.shift&&!n.shift)return false;return true}function mergeInteractions(e){const n=[];for(const t of e){const r=t.startTime+t.duration;const i=n.find(o=>o.interactionId===t.interactionId&&Math.abs(o.startTime+o.duration-r)<=8);if(!i){n.push({...t});continue}const s=i.startTime+i.duration;i.startTime=Math.min(i.startTime,t.startTime);i.processingStart=Math.min(i.processingStart,t.processingStart);i.processingEnd=Math.max(i.processingEnd,t.processingEnd);i.duration=Math.max(s,r)-i.startTime;if(t.processingEnd-t.processingStart>0)i.name=t.name}return n}function summarizeProbe(e,n){const{start:t,end:r}=e;const i=Math.max(1,r-t);const s=[];for(let k=1;k<e.frames.length;k++)s.push(e.frames[k]-e.frames[k-1]);const o=1e3/n;const a=percentile(s,95);let c=null;if(e.longTasks){let k=0;for(const T of e.longTasks){const A=Math.min(T.end,r)-Math.max(T.start,t);if(A>0)k+=A}c=r1(Math.min(100,k/i*100))}let u=null;let l=null;let d=null;let h=[];let p=0;if(e.loaf){let k=0;let T=0;let A=0;const I=new Map;for(const R of e.loaf){if(R.startTime>=t&&R.startTime<=r&&R.startTime+R.duration>r)p++;if(R.startTime<t||R.startTime+R.duration>r)continue;k+=R.blockingDuration;if(R.styleAndLayoutStart>R.startTime)T+=Math.max(0,R.startTime+R.duration-R.styleAndLayoutStart);for(const M of R.scripts){A+=M.forced;const S=I.get(M.name)??{ms:0,count:0};S.ms+=M.duration;S.count++;I.set(M.name,S)}}u=r1(k);l=r1(T);d=r1(A);h=[...I.entries()].map(([R,M])=>({name:R,ms:r1(M.ms),count:M.count})).sort((R,M)=>M.ms-R.ms).slice(0,5)}let m=null;if(e.shifts){let k=0;for(const T of e.shifts)if(!T.hadRecentInput&&T.startTime>=t&&T.startTime<=r)k+=T.value;m=Math.round(k*1e3)/1e3}let g=null;let w=null;if(e.events){const k=mergeInteractions(e.events.filter(T=>T.startTime>=t&&T.startTime<=r));g=new Set(k.map(T=>T.interactionId)).size;for(const T of k)if(!w||T.duration>w.duration)w=T}const f=w;const b=e.frames;const v=[...(e.clicks??[]).map(k=>k.t),...(e.keys??[]).map(k=>k.t)];const y=v.length?Math.max(...v):null;const _=y==null||b.filter(k=>k>y).length>=2;const $=b.length===0?i:Math.max(0,b[0]-t,r-b[b.length-1]);return{edgeSilenceMs:r1($),frameGapCount:s.length,inputCaptureComplete:_,resized:Boolean(e.resized),interactions:g,worstInteractionMs:f?r1(f.duration):null,worstInputDelayMs:f?r1(Math.max(0,f.processingStart-f.startTime)):null,worstProcessingMs:f?r1(Math.max(0,f.processingEnd-f.processingStart)):null,worstPresentationMs:f?r1(Math.max(0,f.startTime+f.duration-f.processingEnd)):null,fps:r1(e.frames.length/i*1e3),frameGapP95Ms:a==null?null:r1(a),frameGapMaxMs:s.length?r1(Math.max(...s)):null,longFrames:s.filter(k=>k>o*1.5).length,longTaskPct:c,loafBlockingMs:u,styleLayoutMs:l,forcedLayoutMs:d,topScripts:h,loafCutOff:p,layoutShift:m,domNodesStart:e.domStart,domNodesEnd:e.domEnd}}function cdpDeltas(e,n,t){const r=a=>a in e&&a in n?n[a]-e[a]:null;const i=a=>{const c=r(a);return c==null?null:r1(c*1e3)};const s=i("TaskDuration");const o=i("Timestamp")??t;return{mainThreadPct:s==null?null:r1(s/Math.max(1,o)*100),scriptMs:i("ScriptDuration"),styleMs:i("RecalcStyleDuration"),layoutMs:i("LayoutDuration"),layoutCount:r("LayoutCount"),styleCount:r("RecalcStyleCount"),heapMb:"JSHeapUsedSize"in n?r1(n.JSHeapUsedSize/(1024*1024)):null}}function webBenchFlags(e,n){const t=[];if(e.droppedPct!=null&&e.droppedPct>=5)t.push(`${e.droppedPct}% of frames dropped or partial`);if(e.fps!=null&&e.fps<n*.95)t.push("Low main-thread frame rate");if(e.frameGapCount&&e.longFrames/e.frameGapCount>=.02)t.push("Frequent long frames");const r=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(r>=100)t.push(`Freeze of ${Math.round(r)} ms`);if(e.mainThreadPct!=null&&e.mainThreadPct>=80)t.push("Main thread near full");if(e.longTaskPct!=null&&e.longTaskPct>=20)t.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50)t.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`);if(e.layoutShift!=null&&e.layoutShift>=.1)t.push(`Layout shifts (${e.layoutShift})`);if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){t.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}return t}function aggregateCase(e,n){const t=e.filter(c=>!c.failed&&c.fps!=null);const r=[...t].sort((c,u)=>(c.fps??0)-(u.fps??0));const i=t.filter(c=>c.droppedPct!=null).length;const s=t.length>0&&i===t.length;const o=s?[...t].sort((c,u)=>c.droppedPct-u.droppedPct):r;const a=o.length?o[Math.floor((o.length-1)/2)]:null;return{caseName:e[0]?.caseName??"",cpu:e[0]?.cpu??1,validRuns:t.length,plannedRuns:e.length,isBaseline:false,deltaDroppedPct:null,tracedRuns:i,...(()=>{const c=s?t.map(l=>l.droppedPct):[];const u=t.map(l=>l.worstInteractionMs).filter(l=>l!=null);return{droppedMin:c.length?Math.min(...c):null,droppedMax:c.length?Math.max(...c):null,inputMin:u.length?Math.min(...u):null,inputMax:u.length?Math.max(...u):null}})(),runs:e,median:a,fpsMin:r.length?r[0].fps:null,fpsMax:r.length?r[r.length-1].fps:null,flags:a?webBenchFlags(s?a:{...a,droppedPct:null},n):[]}}async function loadChromium(e){const n=(0,import_node_module.createRequire)((0,import_node_path11.join)(e,"noop.js"));for(const t of["playwright","playwright-core"]){let r;try{r=n.resolve(t)}catch{continue}const i=await import((0,import_node_url.pathToFileURL)(r).href);const s=i.chromium??i.default?.chromium;if(s)return s}throw new Error(`${PLAYWRIGHT_MISSING}
|
|
383
|
-
(Searched from ${e}. Pass projectRoot to look elsewhere.)`)}async function withDeadline(e,n,t){let r;const i=new Promise(s=>{r=setTimeout(()=>s("timeout"),n)});try{const s=await Promise.race([e,i]);if(s==="timeout"){e.catch(()=>{});return await t()}return s}finally{clearTimeout(r)}}function gitCommit(e){try{return(0,import_node_child_process13.execFileSync)("git",["rev-parse","HEAD"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:3e3}).toString().trim()}catch{return null}}function gitDirty(e){try{const n=(0,import_node_child_process13.execFileSync)("git",["status","--porcelain"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:5e3});return n.toString().trim().length>0}catch{return null}}async function closeWithin(e,n){if(!e)return true;return withDeadline(e.close().then(()=>true,()=>false),n,async()=>false)}function validateWebBenchOptions(e){if(!e.cases?.length)throw new Error("Give at least one case.");const n=new Set;for(const o of e.cases){if(!o.name?.trim())throw new Error("Every case needs a name.");if(n.has(o.name))throw new Error(`Two cases are named "${o.name}". Give each case its own name.`);n.add(o.name)}const t=(o,a,c,u)=>{if(o==null)return;if(!Number.isInteger(o)||o<c||o>u)throw new Error(`${a} must be a whole number from ${c} to ${u}.`)};const r=(o,a,c)=>{if(o==null)return;if(!Number.isFinite(o)||o<c)throw new Error(`${a} must be a number of at least ${c}.`)};const i=(o,a,c,u)=>{if(o==null)return;if(!Number.isFinite(o)||o<c||o>u)throw new Error(`${a} must be from ${c} to ${u}.`)};t(e.runsPerCase,"runsPerCase",1,10);i(e.durationMs,"durationMs",500,12e4);i(e.settleMs,"settleMs",0,6e4);i(e.refreshHz,"refreshHz",1,500);const s=e.cpuSlowdown??[];for(const o of s)i(o,"each cpuSlowdown rate",1,20);if(new Set(s).size!==s.length)throw new Error("cpuSlowdown has the same rate twice.");if(e.viewport){t(e.viewport.width,"viewport.width",200,8e3);t(e.viewport.height,"viewport.height",200,8e3);i(e.viewport.deviceScaleFactor,"viewport.deviceScaleFactor",.5,4)}t(e.extraRoundsWhenClose,"extraRoundsWhenClose",0,10);i(e.maxBatchMs,"maxBatchMs",1e4,6*60*6e4);for(const[o,a]of Object.entries(e.budgets??{}))i(a,`budgets.${o}`,0,1e9);for(const o of e.steps??[]){if("wait"in o)i(o.wait,"a wait step",0,6e4);if("scroll"in o)i(o.scroll,"a scroll step",-1e5,1e5)}new URL(e.url)}function oneLine(e){return e.replace(/\u001b\[[0-9;]*m/g,"").split("\n")[0].trim().slice(0,160)}async function locateStepTargets(e,n){const t=new Map;for(const r of n){if("expect"in r)continue;const i="click"in r?r.click:"type"in r?r.type.selector:null;if(!i||t.has(i))continue;const s=await e.$(i);const o=s?await s.boundingBox():null;if(!o)throw new Error(`step target ${i} isn't on the page (or isn't visible)`);t.set(i,{x:o.x+o.width/2,y:o.y+o.height/2})}return t}async function runSteps(e,n,t){for(const r of n){if("click"in r){const i=t.get(r.click);await e.mouse.click(i.x,i.y)}else if("press"in r)await e.keyboard.press(r.press);else if("type"in r){const i=t.get(r.type.selector);await e.mouse.click(i.x,i.y);await e.keyboard.type(r.type.text)}else if("scroll"in r)await e.mouse.wheel(0,r.scroll);else if("wait"in r)await sleep2(r.wait)}}function missedInput(e,n,t=[]){const r=[...n].sort((l,d)=>l.t-d.t);const i=[...t].sort((l,d)=>l.t-d.t);let s=0;let o=0;let a=-Infinity;const c=l=>{while(s<r.length){const d=r[s++];if(d.t>=a&&d.hit.includes(l)){a=d.t;return true}}return false};const u=l=>{while(o<i.length){const d=i[o++];if(d.t>=a&&keyMatches(l,d)){a=d.t;return true}}return false};for(const[l,d]of e.entries()){if("click"in d&&!c(d.click)){return`step ${l+1} (click ${d.click}) has no real click on its target, in order (moved, covered, or ignored)`}if("type"in d){if(!c(d.type.selector))return`step ${l+1} (type into ${d.type.selector}) has no real click on its target`;for(const h of d.type.text){if(!u(h))return`step ${l+1} (type "${d.type.text}") didn't deliver the key "${h}"`}}if("press"in d&&!u(d.press))return`step ${l+1} (press ${d.press}) didn't deliver that key`}return null}async function expectsAlreadyTrue(e,n){for(const t of n){if(!("expect"in t))continue;const{selector:r,text:i}=t.expect;const s=await e.evaluate(`(() => { const n = document.querySelector(${JSON.stringify(r)}); if (!n) return false; ${i==null?"return true;":`return (n.textContent || "").includes(${JSON.stringify(i)});`} })()`);if(s)return i==null?r:`${r} contains "${i}"`}return null}async function checkTargetsStill(e,n){for(const[t,r]of n){const i=await e.$(t);const s=i?await i.boundingBox():null;if(!s)return`step target ${t} was gone after the run`;const o=s.x+s.width/2;const a=s.y+s.height/2;if(Math.abs(o-r.x)>4||Math.abs(a-r.y)>4){return`step target ${t} moved during the run, so a click may have missed it`}}return null}function expectFailureFrom(e){for(const n of e){if(!n.found)return`expected ${n.selector} at the end of the run, but it isn't there`;const t=n.matched??(n.text==null||(n.content??"").includes(n.text));if(!t){return`expected ${n.selector} to contain "${n.text}" at the end of the run, got "${(n.content??"").slice(0,60)}"`}}return null}async function readMetrics(e){const{metrics:n}=await e.send("Performance.getMetrics");return Object.fromEntries(n.map(t=>[t.name,t.value]))}async function runWebBench(e,n=()=>{}){validateWebBenchOptions(e);const t=await waitForHostLock(e.lockWaitMs??20*6e4,n);const r={browserLeftRunning:false};try{return await runWebBenchLocked(e,n,r)}finally{t(r.browserLeftRunning)}}async function waitForHostLock(e,n=()=>{},t=2e3){const r=Date.now()+Math.max(0,e);let i=false;for(;;){try{return takeHostLock()}catch(s){const o=s?.message??String(s);const a=/Another web benchmark|already running/.test(o);if(!a||Date.now()+t>r)throw s;if(!i){n("another web benchmark is running; waiting for it to finish");i=true}await new Promise(c=>setTimeout(c,t))}}}function takeHostLock(){if(lockedHere){throw new Error("A web benchmark is already running in this process (or left a stuck browser). Wait, or restart the MCP server.")}const e=`${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;const n=JSON.stringify({pid:process.pid,token:e,startedAt:new Date().toISOString()});const t=()=>{const s=`${LOCK_FILE}.${e.replace(/[^\w]/g,"")}.tmp`;try{(0,import_node_fs10.writeFileSync)(s,n);(0,import_node_fs10.linkSync)(s,LOCK_FILE);return true}catch{return false}finally{try{(0,import_node_fs10.unlinkSync)(s)}catch{}}};const r=()=>{try{return JSON.parse((0,import_node_fs10.readFileSync)(LOCK_FILE,"utf8"))}catch{return null}};const i=s=>{if(!s)return false;try{process.kill(s,0);return true}catch{return false}};if(!t()){const s=r();if(!s)throw new Error("Another web benchmark is starting. Wait for it to finish.");if(i(s.pid)){throw new Error(`Another web benchmark is running (pid ${s.pid}, since ${s.startedAt}). Two at once would skew both; wait for it to finish.`)}const o=`${LOCK_FILE}.cleanup`;let a=null;try{if(Date.now()-(0,import_node_fs10.statSync)(o).mtimeMs>3e4){const c=`${o}.${e.replace(/[^\w]/g,"")}.old`;(0,import_node_fs10.renameSync)(o,c);if(Date.now()-(0,import_node_fs10.statSync)(c).mtimeMs>3e4)(0,import_node_fs10.unlinkSync)(c);else{try{(0,import_node_fs10.linkSync)(c,o)}catch{}(0,import_node_fs10.unlinkSync)(c)}}}catch{}try{a=(0,import_node_fs10.openSync)(o,"wx")}catch{throw new Error("Another web benchmark is clearing an old lock. Try again in a moment.")}try{const c=r();if(c?.token===s.token)(0,import_node_fs10.unlinkSync)(LOCK_FILE);if(!t())throw new Error("Another web benchmark just started. Wait for it to finish.")}finally{(0,import_node_fs10.closeSync)(a);try{(0,import_node_fs10.unlinkSync)(o)}catch{}}}lockedHere=true;return(s=false)=>{if(s)return;lockedHere=false;if(r()?.token===e){try{(0,import_node_fs10.unlinkSync)(LOCK_FILE)}catch{}}}}async function runWebBenchLocked(e,n,t){const r=e.runsPerCase??3;const i=Math.max(500,e.durationMs??5e3);const s=Math.max(0,e.settleMs??1e3);const o=e.frameTrace!==false;const a=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(S=>Math.max(1,S));let c=e.refreshHz??60;const u={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const l=[];const d=[];let h=null;if(r<2)l.push("runsPerCase=1 gives no spread; use 3 or more before choosing.");const p=await loadChromium(e.projectRoot??process.cwd());const m=await p.launch({headless:!e.headed,args:["--enable-gpu"]});const g=[];const w=[];const f=Date.now();const b=new Date(f).toISOString();const v=e.maxBatchMs??30*6e4;let y=false;let _=false;const $={commit:gitCommit(e.projectRoot??process.cwd()),dirty:gitDirty(e.projectRoot??process.cwd())};try{const S={viewport:{width:u.width,height:u.height},deviceScaleFactor:u.deviceScaleFactor};const x=await withDeadline(m.newContext(S),3e4,async()=>{t.browserLeftRunning=true;throw new Error("Chromium didn't open a page in 30 s. Restart the MCP server and try again.")});const E=3e4+15e3+s+i+6e4;const j=async(C,O,D,z)=>{const L={};return withDeadline(F(L,C,O,D,z),E,async()=>{L.cancelled=true;if(!L.ctx)y=true;else if(!await closeWithin(L.ctx,1e4))y=true;return{...P(C.name,O,D),failed:`run took over ${Math.round(E/1e3)} s (the page may be stuck in a loop)`}})};const P=(C,O,D)=>({caseName:C,cpu:O,round:D,fps:null,frameGapP95Ms:null,frameGapMaxMs:null,longFrames:0,longTaskPct:null,loafBlockingMs:null,styleLayoutMs:null,forcedLayoutMs:null,topScripts:[],layoutShift:null,domNodesStart:null,domNodesEnd:null,mainThreadPct:null,scriptMs:null,styleMs:null,layoutMs:null,layoutCount:null,styleCount:null,heapMb:null,pageErrors:[]});const F=async(C,O,D,z,L)=>{const J=await m.newContext(S);C.ctx=J;if(C.cancelled){await closeWithin(J,1e4);throw new Error("run was cancelled during setup")}const N=await J.newPage();const K=[];N.on("pageerror",U=>K.push(String(U?.message??U)));const W={...P(O.name,D,z),pageErrors:K};try{const U=await J.newCDPSession(N);await U.send("Performance.enable",{timeDomain:"timeTicks"});if(D>1)await U.send("Emulation.setCPUThrottlingRate",{rate:D});const te=await N.goto(caseUrl(e.url,O,e.path),{waitUntil:"load",timeout:3e4});if(te&&te.status()>=400)return{...W,failed:`HTTP ${te.status()}`};if(e.readySelector)await N.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep2(s);const oe=L||o;if(oe){await U.send("Tracing.start",{transferMode:"ReturnAsStream",categories:(L?FULL_TRACE_CATEGORIES:FRAME_TRACE_CATEGORIES).join(",")})}const ne=e.steps?.length?await locateStepTargets(N,e.steps):new Map;if(e.steps?.length){const X=await expectsAlreadyTrue(N,e.steps);if(X)return{...W,failed:`expect ${X} already held before the steps, so it can't prove they worked`};const Q=[...ne.keys()];const Y=e.steps.flatMap(Z=>"expect"in Z?[Z.expect]:[]);await N.evaluate(`window.__buoyBenchClickTargets = ${JSON.stringify(Q)}; window.__buoyBenchExpects = ${JSON.stringify(Y)}; true`)}const re=await readMetrics(U);await N.evaluate(START_PROBE_JS);const ae=Date.now();if(e.steps?.length)await runSteps(N,e.steps,ne);const ce=i-(Date.now()-ae);if(ce>0)await sleep2(ce);await N.evaluate(END_PROBE_JS);const ie=await readMetrics(U);const le=Date.now()-ae;const V=await N.evaluate(COLLECT_PROBE_JS);let H=null;if(oe){const X=new Promise(G=>U.on("Tracing.tracingComplete",ee=>G(ee.stream)));await U.send("Tracing.end");const Q=await X;const Y=L?(0,import_node_fs10.openSync)(L,"w"):null;const Z=[];let se=0;try{for(;;){const G=await U.send("IO.read",{handle:Q});const ee=G.base64Encoded?Buffer.from(G.data,"base64").toString("utf8"):G.data;se+=ee.length;if(Y!=null)(0,import_node_fs10.writeSync)(Y,ee);if(se<=MAX_TRACE_CHARS)Z.push(ee);else Z.length=0;if(G.eof)break}}finally{await U.send("IO.close",{handle:Q}).catch(()=>{});if(Y!=null)(0,import_node_fs10.closeSync)(Y)}if(L)d.push(L);if(se>MAX_TRACE_CHARS){l.push(`Trace for ${O.name} was over ${Math.round(MAX_TRACE_CHARS/1e6)} MB, so its frames weren't counted. A page that busy shows its problem in the table already.`)}else{try{const G=JSON.parse(Z.join(""));H=summarizeFrameTrace(Array.isArray(G)?G:G.traceEvents??[])}catch{H=null}}}if(!V)return{...W,failed:"probe returned nothing (page navigated away?)"};if(V.hidden)return{...W,failed:"page was hidden during the run"};if(V.resized)return{...W,failed:"the window size or zoom changed during the run"};if(K.length&&!e.allowPageErrors){return{...W,failed:`page error: ${oneLine(K[0])}`}}if(e.steps?.length){const X=missedInput(e.steps,V.clicks??[],V.keys??[]);if(X)return{...W,failed:X};const Q=expectFailureFrom(V.expectResults??[]);if(Q)return{...W,failed:Q};const Y=await checkTargetsStill(N,ne);if(Y)return{...W,failed:Y}}const ue="Timestamp"in re&&"Timestamp"in ie?r1((ie.Timestamp-re.Timestamp)*1e3):null;return{...W,...summarizeProbe(V,c),...cdpDeltas(re,ie,le),probeSpanMs:r1(V.end-V.start),cdpSpanMs:ue,shownFps:H?.shownFps??null,droppedFrames:H?.faults??null,frameCounts:H?{full:H.full,partial:H.partial,dropped:H.dropped,faults:H.faults}:null,droppedPct:H?.droppedPct??null}}catch(U){return{...W,failed:oneLine(U?.message??String(U))}}finally{await closeWithin(N,5e3);if(!await closeWithin(J,1e4))y=true}};{const C=await withDeadline(x.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the GPU check page in 15 s.")});try{h=await withDeadline(C.evaluate(GPU_RENDERER_JS),5e3,async()=>null)??null}catch{h=null}finally{await closeWithin(C,5e3)}if(h)n(`GPU ${h}`);if(h&&isSoftwareGpu(h)){l.push(`Chrome is drawing with a software GPU (${h}). Canvas, WebGL and Skia pages run several times slower than on a real screen, so their numbers mean little. Try headed: true, or a machine with a GPU.`)}}if(e.refreshHz==null){const C=await withDeadline(x.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the calibration page in 15 s.")});try{await withDeadline(C.goto("about:blank"),15e3,async()=>null);const O=await withDeadline(C.evaluate(MEASURE_REFRESH_JS),5e3,async()=>{throw new Error("idle rate check timed out")});c=snapRefreshRate(O);n(`idle frame rate ${O.toFixed(1)} \u2192 ${c} Hz`)}catch{l.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await closeWithin(C,5e3)}}n("warmup load");await j(e.cases[0],1,-1);const B=async(C,O,D,z)=>{if(y){g.push({...P(C.name,O,D),failed:"an earlier run left the browser stuck; batch stopped"});return}if(Date.now()-f>v){g.push({...P(C.name,O,D),failed:"batch time limit reached; run skipped"});return}n(z);w.push({cpu:O,round:D,caseName:C.name});g.push(await j(C,O,D))};for(const C of a){for(let O=0;O<r;O++){const D=shuffled(e.cases,2654435769^O*7919+C);for(const z of D)await B(z,C,O,`cpu ${C}x \xB7 round ${O+1}/${r} \xB7 ${z.name}`)}for(let O=0;O<(e.extraRoundsWhenClose??0);O++){const D=e.cases.map(N=>aggregateCase(g.filter(K=>K.cpu===C&&K.caseName===N.name),c));for(const N of D)N.control=e.cases.find(K=>K.name===N.caseName)?.control===true;const z=new Set([...tiedPairs(D).flat(),...D.filter(N=>!N.control&&N.validRuns<2&&N.plannedRuns>0).map(N=>N.caseName)]);if(z.size===0)break;const L=r+O;const J=shuffled(e.cases.filter(N=>z.has(N.name)),20973^L*7919+C);for(const N of J)await B(N,C,L,`cpu ${C}x \xB7 extra round ${O+1} (close call) \xB7 ${N.name}`)}}if(e.traceDir){(0,import_node_fs10.mkdirSync)(e.traceDir,{recursive:true});for(const[C,O]of e.cases.entries()){const D=(0,import_node_path11.join)(e.traceDir,`${String(C+1).padStart(2,"0")}-${O.name.replace(/[^\w.-]+/g,"_")}.json`);if(y||Date.now()-f>v){l.push(`Trace run for ${O.name} skipped (batch time limit or a stuck browser).`);continue}n(`trace \xB7 ${O.name}`);const z=await j(O,a[0],-1,D);if(z.failed)l.push(`Trace run for ${O.name} failed: ${z.failed}`)}}await closeWithin(x,1e4)}finally{const S=await closeWithin(m,15e3);t.browserLeftRunning=!S;if(!S){_=true;l.push("Chromium didn't close in 15 s and may still be running. The benchmark lock stays held; restart the MCP server before the next batch.")}}const k=[];for(const S of a){for(const x of e.cases){k.push(aggregateCase(g.filter(E=>E.cpu===S&&E.caseName===x.name),c))}}for(const S of k)S.control=e.cases.find(x=>x.name===S.caseName)?.control===true;for(const S of a){const x=k.filter(F=>F.cpu===S);const E=x.find(F=>!F.control);if(!E)continue;E.isBaseline=true;const j=F=>F.droppedMin!=null?F.median?.droppedPct??null:null;const P=j(E);for(const F of x.filter(B=>B!==E&&!B.control)){const B=j(F);F.deltaDroppedPct=P!=null&&B!=null?r1(B-P):null}}if(e.allowPageErrors&&g.some(S=>S.pageErrors.length)){l.push("Some runs threw page errors (kept because allowPageErrors is on).")}if(y)l.push("A browser context wouldn't close, so the batch stopped early. Restart and run again.");if(e.budgets)for(const S of k)S.budget=checkBudgets(S,e.budgets,e.steps??[]);const T=k.filter(S=>S.median).length;const A=k.every(S=>S.validRuns===S.plannedRuns);const I=T===0?"failed":T<k.length||!A?"partial":"ok";const R=k.filter(S=>S.median&&S.validRuns>0&&S.validRuns<S.plannedRuns);for(const S of R){const x=Array.from(new Set(S.runs.filter(E=>E.failed).map(E=>E.failed)));l.push(`${S.caseName} (CPU ${S.cpu}x): ${S.plannedRuns-S.validRuns} of ${S.plannedRuns} runs failed: ${x.join("; ")}`)}for(const S of k){const x=S.median;if(x?.probeSpanMs&&x.cdpSpanMs&&Math.abs(x.cdpSpanMs-x.probeSpanMs)/x.probeSpanMs>.15){l.push(`${S.caseName} (CPU ${S.cpu}x): Chrome's counters cover ${Math.round(x.cdpSpanMs)} ms but frames cover ${Math.round(x.probeSpanMs)} ms, so its cost columns include extra time.`)}if(x?.probeSpanMs!=null&&x.probeSpanMs<i*.8){l.push(`${S.caseName} (CPU ${S.cpu}x) recorded ${Math.round(x.probeSpanMs)} ms, not the ${i} ms asked for.`)}}const M={status:I,hasSteps:Boolean(e.steps?.length),browserLeftRunning:_,replay:{startedAt:b,...$,runOrder:w},url:e.url,browser:`Chromium ${m.version()}${e.headed?"":" (headless)"}`,gpu:h,viewport:u,refreshHz:c,durationMs:i,runsPerCase:r,results:k,traces:d,warnings:l};if(e.outFile){try{(0,import_node_fs10.writeFileSync)(e.outFile,JSON.stringify({options:e,result:M},null,2))}catch(S){M.warnings.push(`Couldn't write outFile ${e.outFile}: ${oneLine(S?.message??String(S))}`)}}return M}function tiedPairs(e){const n=e.filter(s=>!s.control&&s.median&&s.validRuns>1);const t=n.filter(s=>s.droppedMin!=null);const r=[];if(t.length>=2&&t.length===n.length){const s=[...t].sort((o,a)=>o.median.droppedPct-a.median.droppedPct);for(let o=1;o<s.length;o++){const[a,c]=[s[o-1],s[o]];const u=c.median.droppedPct-a.median.droppedPct<=MIN_DROPPED_GAP_PP;if(c.droppedMin<=a.droppedMax||u)r.push([a.caseName,c.caseName])}return r}if(t.length>0)return r;const i=n.filter(s=>s.median.fps!=null).sort((s,o)=>o.median.fps-s.median.fps);for(let s=1;s<i.length;s++){const[o,a]=[i[s-1],i[s]];const c=o.median.fps-a.median.fps<=MIN_FPS_GAP;if((a.fpsMax??0)>=(o.fpsMin??0)||c)r.push([o.caseName,a.caseName])}return r}function closeCall(e){const n=e.filter(a=>!a.control&&a.median);const t=[];const r=n.filter(a=>a.validRuns<2);if(r.length)t.push(`**Too few runs to rank:** ${r.map(a=>a.caseName).join(", ")} (need 2 or more valid runs).`);const i=n.filter(a=>a.validRuns>1);const s=i.filter(a=>a.droppedMin!=null);if(s.length>0&&s.length<i.length){t.push(`**Not enough traced runs to compare smoothness:** ${i.filter(a=>!s.includes(a)).map(a=>`${a.caseName} (${a.tracedRuns}/${a.validRuns} traced)`).join(", ")}.`);return t.join("\n")}const o=new Map(i.map(a=>[a.caseName,a]));for(const[a,c]of tiedPairs(e)){const u=o.get(a);const l=o.get(c);t.push(s.length?`**Too close to call:** ${a} (${u.droppedMin}\u2013${u.droppedMax}% dropped) and ${c} (${l.droppedMin}\u2013${l.droppedMax}%) overlap or sit within ${MIN_DROPPED_GAP_PP} pp. No clear winner between them; more runs may separate them.`:`**Too close to call:** ${a} and ${c} overlap or sit within ${MIN_FPS_GAP} FPS. No clear winner between them; more runs may separate them.`)}return t.length?t.join("\n"):null}function formatWebBenchMarkdown(e){const n=(a,c=1)=>a==null?"\u2014":a.toFixed(c);const t=[];t.push(`### Web benchmark \xB7 ${e.url}`);t.push(`${e.browser}${e.gpu?` \xB7 GPU: ${e.gpu}${isSoftwareGpu(e.gpu)?" \u26A0\uFE0F software":""}`:""} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 ${e.runsPerCase} runs${e.results.some(a=>a.plannedRuns>e.runsPerCase)?" (more for close calls)":""} \xD7 ${e.durationMs/1e3}s per case, fresh browser context each run, case order shuffled each round.`);const r=e.results.map(a=>a.median?.probeSpanMs).filter(a=>a!=null);const i=e.results.map(a=>a.median?.cdpSpanMs).filter(a=>a!=null);const s=a=>`${(Math.min(...a)/1e3).toFixed(2)}\u2013${(Math.max(...a)/1e3).toFixed(2)} s`;if(r.length){t.push(`Recorded: frames over ${s(r)}${i.length?`; cost columns (Chrome's counters) over ${s(i)}`:""}, not the requested time.`)}if(e.replay?.dirty)t.push(`Note: the project had uncommitted changes, so commit ${e.replay.commit?.slice(0,10)??"?"} alone doesn't describe the code that ran.`);t.push("");t.push(`**How to read this.** Dropped % is the smoothness number: frames Chrome dropped or only partly produced that it says hurt smoothness, as a share of the frames it wanted, from its own verdicts in a trace. Rank on it first, and only across variants doing the same work. Shown FPS counts frames Chrome produced (headless Chrome displays nothing), so an animation that changes less often needs fewer; don't compare it across variants that update at different rates. Main-thread FPS and frame gaps come from requestAnimationFrame. Main thread % is Chrome's task time for the run. Desktop numbers, even with CPU slowdown, don't stand in for a real phone's GPU, memory or heat.`);const o=Array.from(new Set(e.results.map(a=>a.cpu)));for(const a of o){t.push("");t.push(a===1?`#### No CPU slowdown`:`#### CPU slowed ${a}x`);t.push("");t.push(`| Case | Runs | Shown FPS | Dropped % | Main-thread FPS (range) | p95 gap ms | Worst gap ms | Main thread % | Script ms | Style ms | Layout ms | Layouts | DOM nodes | JS heap MB | Flags |`);t.push(`|------|-----:|----------:|----------:|------------------------:|-----------:|-------------:|--------------:|----------:|---------:|----------:|--------:|----------:|--------:|-------|`);for(const h of e.results.filter(p=>p.cpu===a)){const p=h.median;if(!p){const w=h.runs.find(f=>f.failed)?.failed??"no valid runs";t.push(`| ${h.caseName} | 0/${h.plannedRuns} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${w}) |`);continue}const m=h.fpsMin!=null&&h.fpsMax!=null&&h.validRuns>1?` (${n(h.fpsMin)}\u2013${n(h.fpsMax)})`:"";const g=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";t.push(`| ${h.isBaseline?`${h.caseName} (baseline)`:h.control?`${h.caseName} (control)`:h.caseName} | ${h.validRuns}/${h.plannedRuns} | ${n(p.shownFps)} | ${h.droppedMin==null&&h.tracedRuns>0?`\u2014 (${h.tracedRuns}/${h.validRuns} traced)`:n(h.droppedMin==null?null:p.droppedPct)}${h.droppedMin!=null&&h.validRuns>1?` [${h.droppedMin}\u2013${h.droppedMax}]`:""}${h.deltaDroppedPct!=null?` (${h.deltaDroppedPct>0?"+":""}${h.deltaDroppedPct} pp)`:""} | ${n(p.fps)}${m} | ${n(p.frameGapP95Ms)} | ${n(p.frameGapMaxMs)} | ${n(p.mainThreadPct)} | ${n(p.scriptMs,0)} | ${n(p.styleMs,0)} | ${n(p.layoutMs,0)} | ${n(p.layoutCount,0)} | ${n(p.domNodesEnd,0)} | ${n(p.heapMb)} | ${g} |`)}const c=closeCall(e.results.filter(h=>h.cpu===a));if(c){t.push("");t.push(c)}const u=e.results.filter(h=>h.cpu===a&&h.budget);if(u.length){t.push("");t.push(`Budget:`);for(const h of u){const p=h.budget;const m=[...p.over,...p.unknown.map(w=>`${w} not measured`)].join("; ");const g=h.control?`${h.caseName} (control)`:h.caseName;t.push(`- **${g}**: ${p.meets?"meets every limit":`misses (${m||"no result"})`}`)}}const l=e.results.filter(h=>h.cpu===a&&h.median?.interactions!=null&&(e.hasSteps||h.median.interactions>0)).map(h=>{const p=h.median;const m=h.inputMin!=null&&h.inputMax!=null?` \xB7 slowest across all runs ${h.inputMax} ms (range ${h.inputMin}\u2013${h.inputMax})`:"";const g=h.runs.filter(f=>!f.failed&&f.fps!=null&&f.inputCaptureComplete===false).length;const w=g?` \xB7 \u26A0\uFE0F timing incomplete in ${g} run(s): the last input's paint came after the run ended, so its time is unknown (leave more time after the last step)`:"";if(!p.interactions)return`- **${h.caseName}**: middle run had no input entry (inputs under 16 ms aren't reported)${m}${w}`;return`- **${h.caseName}**: middle run: ${p.interactions} reported (16 ms or more), slowest ${p.worstInteractionMs} ms (waiting ${p.worstInputDelayMs} \xB7 handlers ${p.worstProcessingMs} \xB7 to paint ${p.worstPresentationMs})${m}${w}`});if(l.length){t.push("");t.push(`Input response (scripted steps, Event Timing). Inputs under 16 ms aren't reported. Under 200 ms is good.`);t.push(...l)}const d=e.results.filter(h=>h.cpu===a&&h.median&&h.median.topScripts.length).map(h=>`- **${h.caseName}**: ${h.median.topScripts.slice(0,3).map(p=>`${p.name} ${p.ms} ms`).join(", ")}`);if(d.length){t.push("");t.push(`Scripts in slow frames (Long Animation Frames; frames that ran past the end of a run are left out):`);t.push(...d)}}if(e.traces.length){t.push("");t.push(`Chrome traces (open in DevTools \u2192 Performance \u2192 Load profile): ${e.traces.join(", ")}`)}if(e.warnings.length){t.push("");t.push(...e.warnings.map(a=>`\u26A0\uFE0F ${a}`))}return t.join("\n")}var import_node_module,import_node_child_process13,import_node_path11,import_node_fs10,import_node_os8,import_node_url,r1,GPU_RENDERER_JS,COMMON_RATES,FRAME_STATES,PLAYWRIGHT_MISSING,FRAME_TRACE_CATEGORIES,FULL_TRACE_CATEGORIES,MAX_TRACE_CHARS,sleep2,LOCK_FILE,lockedHere,MIN_DROPPED_GAP_PP,MIN_FPS_GAP;var init_webBench=__esm({"src/webBench/webBench.ts"(){"use strict";import_node_module=require("module");import_node_child_process13=require("child_process");import_node_path11=require("path");import_node_fs10=require("fs");import_node_os8=require("os");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;GPU_RENDERER_JS=`(() => {
|
|
383
|
+
(Searched from ${e}. Pass projectRoot to look elsewhere.)`)}async function withDeadline(e,n,t){let r;const i=new Promise(s=>{r=setTimeout(()=>s("timeout"),n)});try{const s=await Promise.race([e,i]);if(s==="timeout"){e.catch(()=>{});return await t()}return s}finally{clearTimeout(r)}}function gitCommit(e){try{return(0,import_node_child_process13.execFileSync)("git",["rev-parse","HEAD"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:3e3}).toString().trim()}catch{return null}}function gitDirty(e){try{const n=(0,import_node_child_process13.execFileSync)("git",["status","--porcelain"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:5e3});return n.toString().trim().length>0}catch{return null}}async function closeWithin(e,n){if(!e)return true;return withDeadline(e.close().then(()=>true,()=>false),n,async()=>false)}function validateWebBenchOptions(e){if(!e.cases?.length)throw new Error("Give at least one case.");const n=new Set;for(const o of e.cases){if(!o.name?.trim())throw new Error("Every case needs a name.");if(n.has(o.name))throw new Error(`Two cases are named "${o.name}". Give each case its own name.`);n.add(o.name)}const t=(o,a,c,u)=>{if(o==null)return;if(!Number.isInteger(o)||o<c||o>u)throw new Error(`${a} must be a whole number from ${c} to ${u}.`)};const r=(o,a,c)=>{if(o==null)return;if(!Number.isFinite(o)||o<c)throw new Error(`${a} must be a number of at least ${c}.`)};const i=(o,a,c,u)=>{if(o==null)return;if(!Number.isFinite(o)||o<c||o>u)throw new Error(`${a} must be from ${c} to ${u}.`)};t(e.runsPerCase,"runsPerCase",1,10);i(e.durationMs,"durationMs",500,12e4);i(e.settleMs,"settleMs",0,6e4);i(e.refreshHz,"refreshHz",1,500);const s=e.cpuSlowdown??[];for(const o of s)i(o,"each cpuSlowdown rate",1,20);if(new Set(s).size!==s.length)throw new Error("cpuSlowdown has the same rate twice.");if(e.viewport){t(e.viewport.width,"viewport.width",200,8e3);t(e.viewport.height,"viewport.height",200,8e3);i(e.viewport.deviceScaleFactor,"viewport.deviceScaleFactor",.5,4)}t(e.extraRoundsWhenClose,"extraRoundsWhenClose",0,10);i(e.maxBatchMs,"maxBatchMs",1e4,6*60*6e4);for(const[o,a]of Object.entries(e.budgets??{}))i(a,`budgets.${o}`,0,1e9);for(const o of e.steps??[]){if("wait"in o)i(o.wait,"a wait step",0,6e4);if("scroll"in o)i(o.scroll,"a scroll step",-1e5,1e5)}new URL(e.url)}function oneLine(e){return e.replace(/\u001b\[[0-9;]*m/g,"").split("\n")[0].trim().slice(0,160)}async function locateStepTargets(e,n){const t=new Map;for(const r of n){if("expect"in r)continue;const i="click"in r?r.click:"type"in r?r.type.selector:null;if(!i||t.has(i))continue;const s=await e.$(i);const o=s?await s.boundingBox():null;if(!o)throw new Error(`step target ${i} isn't on the page (or isn't visible)`);t.set(i,{x:o.x+o.width/2,y:o.y+o.height/2})}return t}async function runSteps(e,n,t){for(const r of n){if("click"in r){const i=t.get(r.click);await e.mouse.click(i.x,i.y)}else if("press"in r)await e.keyboard.press(r.press);else if("type"in r){const i=t.get(r.type.selector);await e.mouse.click(i.x,i.y);await e.keyboard.type(r.type.text)}else if("scroll"in r)await e.mouse.wheel(0,r.scroll);else if("wait"in r)await sleep2(r.wait)}}function missedInput(e,n,t=[]){const r=[...n].sort((l,d)=>l.t-d.t);const i=[...t].sort((l,d)=>l.t-d.t);let s=0;let o=0;let a=-Infinity;const c=l=>{while(s<r.length){const d=r[s++];if(d.t>=a&&d.hit.includes(l)){a=d.t;return true}}return false};const u=l=>{while(o<i.length){const d=i[o++];if(d.t>=a&&keyMatches(l,d)){a=d.t;return true}}return false};for(const[l,d]of e.entries()){if("click"in d&&!c(d.click)){return`step ${l+1} (click ${d.click}) has no real click on its target, in order (moved, covered, or ignored)`}if("type"in d){if(!c(d.type.selector))return`step ${l+1} (type into ${d.type.selector}) has no real click on its target`;for(const h of d.type.text){if(!u(h))return`step ${l+1} (type "${d.type.text}") didn't deliver the key "${h}"`}}if("press"in d&&!u(d.press))return`step ${l+1} (press ${d.press}) didn't deliver that key`}return null}async function expectsAlreadyTrue(e,n){for(const t of n){if(!("expect"in t))continue;const{selector:r,text:i}=t.expect;const s=await e.evaluate(`(() => { const n = document.querySelector(${JSON.stringify(r)}); if (!n) return false; ${i==null?"return true;":`return (n.textContent || "").includes(${JSON.stringify(i)});`} })()`);if(s)return i==null?r:`${r} contains "${i}"`}return null}async function checkTargetsStill(e,n){for(const[t,r]of n){const i=await e.$(t);const s=i?await i.boundingBox():null;if(!s)return`step target ${t} was gone after the run`;const o=s.x+s.width/2;const a=s.y+s.height/2;if(Math.abs(o-r.x)>4||Math.abs(a-r.y)>4){return`step target ${t} moved during the run, so a click may have missed it`}}return null}function expectFailureFrom(e){for(const n of e){if(!n.found)return`expected ${n.selector} at the end of the run, but it isn't there`;const t=n.matched??(n.text==null||(n.content??"").includes(n.text));if(!t){return`expected ${n.selector} to contain "${n.text}" at the end of the run, got "${(n.content??"").slice(0,60)}"`}}return null}async function readMetrics(e){const{metrics:n}=await e.send("Performance.getMetrics");return Object.fromEntries(n.map(t=>[t.name,t.value]))}async function runWebBench(e,n=()=>{}){validateWebBenchOptions(e);const t=await waitForHostLock(e.lockWaitMs??20*6e4,n);const r={browserLeftRunning:false};try{return await runWebBenchLocked(e,n,r)}finally{t(r.browserLeftRunning)}}async function waitForHostLock(e,n=()=>{},t=2e3){const r=Date.now()+Math.max(0,e);let i=false;for(;;){try{return takeHostLock()}catch(s){const o=s?.message??String(s);const a=/Another web benchmark|already running/.test(o);if(!a||Date.now()+t>r)throw s;if(!i){n("another web benchmark is running; waiting for it to finish");i=true}await new Promise(c=>setTimeout(c,t))}}}function takeHostLock(){if(lockedHere){throw new Error("A web benchmark is already running in this process (or left a stuck browser). Wait, or restart the MCP server.")}const e=`${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;const n=JSON.stringify({pid:process.pid,token:e,startedAt:new Date().toISOString()});const t=()=>{const s=`${LOCK_FILE}.${e.replace(/[^\w]/g,"")}.tmp`;try{(0,import_node_fs10.writeFileSync)(s,n);(0,import_node_fs10.linkSync)(s,LOCK_FILE);return true}catch{return false}finally{try{(0,import_node_fs10.unlinkSync)(s)}catch{}}};const r=()=>{try{return JSON.parse((0,import_node_fs10.readFileSync)(LOCK_FILE,"utf8"))}catch{return null}};const i=s=>{if(!s)return false;try{process.kill(s,0);return true}catch{return false}};if(!t()){const s=r();if(!s)throw new Error("Another web benchmark is starting. Wait for it to finish.");if(i(s.pid)){throw new Error(`Another web benchmark is running (pid ${s.pid}, since ${s.startedAt}). Two at once would skew both; wait for it to finish.`)}const o=`${LOCK_FILE}.cleanup`;let a=null;try{if(Date.now()-(0,import_node_fs10.statSync)(o).mtimeMs>3e4){const c=`${o}.${e.replace(/[^\w]/g,"")}.old`;(0,import_node_fs10.renameSync)(o,c);if(Date.now()-(0,import_node_fs10.statSync)(c).mtimeMs>3e4)(0,import_node_fs10.unlinkSync)(c);else{try{(0,import_node_fs10.linkSync)(c,o)}catch{}(0,import_node_fs10.unlinkSync)(c)}}}catch{}try{a=(0,import_node_fs10.openSync)(o,"wx")}catch{throw new Error("Another web benchmark is clearing an old lock. Try again in a moment.")}try{const c=r();if(c?.token===s.token)(0,import_node_fs10.unlinkSync)(LOCK_FILE);if(!t())throw new Error("Another web benchmark just started. Wait for it to finish.")}finally{(0,import_node_fs10.closeSync)(a);try{(0,import_node_fs10.unlinkSync)(o)}catch{}}}lockedHere=true;return(s=false)=>{if(s)return;lockedHere=false;if(r()?.token===e){try{(0,import_node_fs10.unlinkSync)(LOCK_FILE)}catch{}}}}async function runWebBenchLocked(e,n,t){const r=e.runsPerCase??3;const i=Math.max(500,e.durationMs??5e3);const s=Math.max(0,e.settleMs??1e3);const o=e.frameTrace!==false;const a=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(S=>Math.max(1,S));let c=e.refreshHz??60;const u={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const l=[];const d=[];let h=null;if(r<2)l.push("runsPerCase=1 gives no spread; use 3 or more before choosing.");const p=await loadChromium(e.projectRoot??process.cwd());const m=await p.launch({headless:!e.headed,args:["--enable-gpu"]});const g=[];const w=[];const f=Date.now();const b=new Date(f).toISOString();const v=e.maxBatchMs??30*6e4;let y=false;let _=false;const $={commit:gitCommit(e.projectRoot??process.cwd()),dirty:gitDirty(e.projectRoot??process.cwd())};try{const S={viewport:{width:u.width,height:u.height},deviceScaleFactor:u.deviceScaleFactor};const x=await withDeadline(m.newContext(S),3e4,async()=>{t.browserLeftRunning=true;throw new Error("Chromium didn't open a page in 30 s. Restart the MCP server and try again.")});const E=3e4+15e3+s+i+6e4;const j=async(C,O,D,z)=>{const L={};return withDeadline(B(L,C,O,D,z),E,async()=>{L.cancelled=true;if(!L.ctx)y=true;else if(!await closeWithin(L.ctx,1e4))y=true;return{...P(C.name,O,D),failed:`run took over ${Math.round(E/1e3)} s (the page may be stuck in a loop)`}})};const P=(C,O,D)=>({caseName:C,cpu:O,round:D,fps:null,frameGapP95Ms:null,frameGapMaxMs:null,longFrames:0,longTaskPct:null,loafBlockingMs:null,styleLayoutMs:null,forcedLayoutMs:null,topScripts:[],layoutShift:null,domNodesStart:null,domNodesEnd:null,mainThreadPct:null,scriptMs:null,styleMs:null,layoutMs:null,layoutCount:null,styleCount:null,heapMb:null,pageErrors:[]});const B=async(C,O,D,z,L)=>{const J=await m.newContext(S);C.ctx=J;if(C.cancelled){await closeWithin(J,1e4);throw new Error("run was cancelled during setup")}const N=await J.newPage();const K=[];N.on("pageerror",U=>K.push(String(U?.message??U)));const W={...P(O.name,D,z),pageErrors:K};try{const U=await J.newCDPSession(N);await U.send("Performance.enable",{timeDomain:"timeTicks"});if(D>1)await U.send("Emulation.setCPUThrottlingRate",{rate:D});const te=await N.goto(caseUrl(e.url,O,e.path),{waitUntil:"load",timeout:3e4});if(te&&te.status()>=400)return{...W,failed:`HTTP ${te.status()}`};if(e.readySelector)await N.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep2(s);const oe=L||o;if(oe){await U.send("Tracing.start",{transferMode:"ReturnAsStream",categories:(L?FULL_TRACE_CATEGORIES:FRAME_TRACE_CATEGORIES).join(",")})}const ne=e.steps?.length?await locateStepTargets(N,e.steps):new Map;if(e.steps?.length){const X=await expectsAlreadyTrue(N,e.steps);if(X)return{...W,failed:`expect ${X} already held before the steps, so it can't prove they worked`};const Q=[...ne.keys()];const Y=e.steps.flatMap(Z=>"expect"in Z?[Z.expect]:[]);await N.evaluate(`window.__buoyBenchClickTargets = ${JSON.stringify(Q)}; window.__buoyBenchExpects = ${JSON.stringify(Y)}; true`)}const re=await readMetrics(U);await N.evaluate(START_PROBE_JS);const ae=Date.now();if(e.steps?.length)await runSteps(N,e.steps,ne);const ce=i-(Date.now()-ae);if(ce>0)await sleep2(ce);await N.evaluate(END_PROBE_JS);const ie=await readMetrics(U);const le=Date.now()-ae;const V=await N.evaluate(COLLECT_PROBE_JS);let H=null;if(oe){const X=new Promise(G=>U.on("Tracing.tracingComplete",ee=>G(ee.stream)));await U.send("Tracing.end");const Q=await X;const Y=L?(0,import_node_fs10.openSync)(L,"w"):null;const Z=[];let se=0;try{for(;;){const G=await U.send("IO.read",{handle:Q});const ee=G.base64Encoded?Buffer.from(G.data,"base64").toString("utf8"):G.data;se+=ee.length;if(Y!=null)(0,import_node_fs10.writeSync)(Y,ee);if(se<=MAX_TRACE_CHARS)Z.push(ee);else Z.length=0;if(G.eof)break}}finally{await U.send("IO.close",{handle:Q}).catch(()=>{});if(Y!=null)(0,import_node_fs10.closeSync)(Y)}if(L)d.push(L);if(se>MAX_TRACE_CHARS){l.push(`Trace for ${O.name} was over ${Math.round(MAX_TRACE_CHARS/1e6)} MB, so its frames weren't counted. A page that busy shows its problem in the table already.`)}else{try{const G=JSON.parse(Z.join(""));H=summarizeFrameTrace(Array.isArray(G)?G:G.traceEvents??[])}catch{H=null}}}if(!V)return{...W,failed:"probe returned nothing (page navigated away?)"};if(V.hidden)return{...W,failed:"page was hidden during the run"};if(V.resized)return{...W,failed:"the window size or zoom changed during the run"};if(K.length&&!e.allowPageErrors){return{...W,failed:`page error: ${oneLine(K[0])}`}}if(e.steps?.length){const X=missedInput(e.steps,V.clicks??[],V.keys??[]);if(X)return{...W,failed:X};const Q=expectFailureFrom(V.expectResults??[]);if(Q)return{...W,failed:Q};const Y=await checkTargetsStill(N,ne);if(Y)return{...W,failed:Y}}const ue="Timestamp"in re&&"Timestamp"in ie?r1((ie.Timestamp-re.Timestamp)*1e3):null;return{...W,...summarizeProbe(V,c),...cdpDeltas(re,ie,le),probeSpanMs:r1(V.end-V.start),cdpSpanMs:ue,shownFps:H?.shownFps??null,droppedFrames:H?.faults??null,frameCounts:H?{full:H.full,partial:H.partial,dropped:H.dropped,faults:H.faults}:null,droppedPct:H?.droppedPct??null}}catch(U){return{...W,failed:oneLine(U?.message??String(U))}}finally{await closeWithin(N,5e3);if(!await closeWithin(J,1e4))y=true}};{const C=await withDeadline(x.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the GPU check page in 15 s.")});try{h=await withDeadline(C.evaluate(GPU_RENDERER_JS),5e3,async()=>null)??null}catch{h=null}finally{await closeWithin(C,5e3)}if(h)n(`GPU ${h}`);if(h&&isSoftwareGpu(h)){l.push(`Chrome is drawing with a software GPU (${h}). Canvas, WebGL and Skia pages run several times slower than on a real screen, so their numbers mean little. Try headed: true, or a machine with a GPU.`)}}if(e.refreshHz==null){const C=await withDeadline(x.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the calibration page in 15 s.")});try{await withDeadline(C.goto("about:blank"),15e3,async()=>null);const O=await withDeadline(C.evaluate(MEASURE_REFRESH_JS),5e3,async()=>{throw new Error("idle rate check timed out")});c=snapRefreshRate(O);n(`idle frame rate ${O.toFixed(1)} \u2192 ${c} Hz`)}catch{l.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await closeWithin(C,5e3)}}n("warmup load");await j(e.cases[0],1,-1);const F=async(C,O,D,z)=>{if(y){g.push({...P(C.name,O,D),failed:"an earlier run left the browser stuck; batch stopped"});return}if(Date.now()-f>v){g.push({...P(C.name,O,D),failed:"batch time limit reached; run skipped"});return}n(z);w.push({cpu:O,round:D,caseName:C.name});g.push(await j(C,O,D))};for(const C of a){for(let O=0;O<r;O++){const D=shuffled(e.cases,2654435769^O*7919+C);for(const z of D)await F(z,C,O,`cpu ${C}x \xB7 round ${O+1}/${r} \xB7 ${z.name}`)}for(let O=0;O<(e.extraRoundsWhenClose??0);O++){const D=e.cases.map(N=>aggregateCase(g.filter(K=>K.cpu===C&&K.caseName===N.name),c));for(const N of D)N.control=e.cases.find(K=>K.name===N.caseName)?.control===true;const z=new Set([...tiedPairs(D).flat(),...D.filter(N=>!N.control&&N.validRuns<2&&N.plannedRuns>0).map(N=>N.caseName)]);if(z.size===0)break;const L=r+O;const J=shuffled(e.cases.filter(N=>z.has(N.name)),20973^L*7919+C);for(const N of J)await F(N,C,L,`cpu ${C}x \xB7 extra round ${O+1} (close call) \xB7 ${N.name}`)}}if(e.traceDir){(0,import_node_fs10.mkdirSync)(e.traceDir,{recursive:true});for(const[C,O]of e.cases.entries()){const D=(0,import_node_path11.join)(e.traceDir,`${String(C+1).padStart(2,"0")}-${O.name.replace(/[^\w.-]+/g,"_")}.json`);if(y||Date.now()-f>v){l.push(`Trace run for ${O.name} skipped (batch time limit or a stuck browser).`);continue}n(`trace \xB7 ${O.name}`);const z=await j(O,a[0],-1,D);if(z.failed)l.push(`Trace run for ${O.name} failed: ${z.failed}`)}}await closeWithin(x,1e4)}finally{const S=await closeWithin(m,15e3);t.browserLeftRunning=!S;if(!S){_=true;l.push("Chromium didn't close in 15 s and may still be running. The benchmark lock stays held; restart the MCP server before the next batch.")}}const k=[];for(const S of a){for(const x of e.cases){k.push(aggregateCase(g.filter(E=>E.cpu===S&&E.caseName===x.name),c))}}for(const S of k)S.control=e.cases.find(x=>x.name===S.caseName)?.control===true;for(const S of a){const x=k.filter(B=>B.cpu===S);const E=x.find(B=>!B.control);if(!E)continue;E.isBaseline=true;const j=B=>B.droppedMin!=null?B.median?.droppedPct??null:null;const P=j(E);for(const B of x.filter(F=>F!==E&&!F.control)){const F=j(B);B.deltaDroppedPct=P!=null&&F!=null?r1(F-P):null}}if(e.allowPageErrors&&g.some(S=>S.pageErrors.length)){l.push("Some runs threw page errors (kept because allowPageErrors is on).")}if(y)l.push("A browser context wouldn't close, so the batch stopped early. Restart and run again.");if(e.budgets)for(const S of k)S.budget=checkBudgets(S,e.budgets,e.steps??[]);const T=k.filter(S=>S.median).length;const A=k.every(S=>S.validRuns===S.plannedRuns);const I=T===0?"failed":T<k.length||!A?"partial":"ok";const R=k.filter(S=>S.median&&S.validRuns>0&&S.validRuns<S.plannedRuns);for(const S of R){const x=Array.from(new Set(S.runs.filter(E=>E.failed).map(E=>E.failed)));l.push(`${S.caseName} (CPU ${S.cpu}x): ${S.plannedRuns-S.validRuns} of ${S.plannedRuns} runs failed: ${x.join("; ")}`)}for(const S of k){const x=S.median;if(x?.probeSpanMs&&x.cdpSpanMs&&Math.abs(x.cdpSpanMs-x.probeSpanMs)/x.probeSpanMs>.15){l.push(`${S.caseName} (CPU ${S.cpu}x): Chrome's counters cover ${Math.round(x.cdpSpanMs)} ms but frames cover ${Math.round(x.probeSpanMs)} ms, so its cost columns include extra time.`)}if(x?.probeSpanMs!=null&&x.probeSpanMs<i*.8){l.push(`${S.caseName} (CPU ${S.cpu}x) recorded ${Math.round(x.probeSpanMs)} ms, not the ${i} ms asked for.`)}}const M={status:I,hasSteps:Boolean(e.steps?.length),browserLeftRunning:_,replay:{startedAt:b,...$,runOrder:w},url:e.url,browser:`Chromium ${m.version()}${e.headed?"":" (headless)"}`,gpu:h,viewport:u,refreshHz:c,durationMs:i,runsPerCase:r,results:k,traces:d,warnings:l};if(e.outFile){try{(0,import_node_fs10.writeFileSync)(e.outFile,JSON.stringify({options:e,result:M},null,2))}catch(S){M.warnings.push(`Couldn't write outFile ${e.outFile}: ${oneLine(S?.message??String(S))}`)}}return M}function tiedPairs(e){const n=e.filter(s=>!s.control&&s.median&&s.validRuns>1);const t=n.filter(s=>s.droppedMin!=null);const r=[];if(t.length>=2&&t.length===n.length){const s=[...t].sort((o,a)=>o.median.droppedPct-a.median.droppedPct);for(let o=1;o<s.length;o++){const[a,c]=[s[o-1],s[o]];const u=c.median.droppedPct-a.median.droppedPct<=MIN_DROPPED_GAP_PP;if(c.droppedMin<=a.droppedMax||u)r.push([a.caseName,c.caseName])}return r}if(t.length>0)return r;const i=n.filter(s=>s.median.fps!=null).sort((s,o)=>o.median.fps-s.median.fps);for(let s=1;s<i.length;s++){const[o,a]=[i[s-1],i[s]];const c=o.median.fps-a.median.fps<=MIN_FPS_GAP;if((a.fpsMax??0)>=(o.fpsMin??0)||c)r.push([o.caseName,a.caseName])}return r}function closeCall(e){const n=e.filter(a=>!a.control&&a.median);const t=[];const r=n.filter(a=>a.validRuns<2);if(r.length)t.push(`**Too few runs to rank:** ${r.map(a=>a.caseName).join(", ")} (need 2 or more valid runs).`);const i=n.filter(a=>a.validRuns>1);const s=i.filter(a=>a.droppedMin!=null);if(s.length>0&&s.length<i.length){t.push(`**Not enough traced runs to compare smoothness:** ${i.filter(a=>!s.includes(a)).map(a=>`${a.caseName} (${a.tracedRuns}/${a.validRuns} traced)`).join(", ")}.`);return t.join("\n")}const o=new Map(i.map(a=>[a.caseName,a]));for(const[a,c]of tiedPairs(e)){const u=o.get(a);const l=o.get(c);t.push(s.length?`**Too close to call:** ${a} (${u.droppedMin}\u2013${u.droppedMax}% dropped) and ${c} (${l.droppedMin}\u2013${l.droppedMax}%) overlap or sit within ${MIN_DROPPED_GAP_PP} pp. No clear winner between them; more runs may separate them.`:`**Too close to call:** ${a} and ${c} overlap or sit within ${MIN_FPS_GAP} FPS. No clear winner between them; more runs may separate them.`)}return t.length?t.join("\n"):null}function formatWebBenchMarkdown(e){const n=(a,c=1)=>a==null?"\u2014":a.toFixed(c);const t=[];t.push(`### Web benchmark \xB7 ${e.url}`);t.push(`${e.browser}${e.gpu?` \xB7 GPU: ${e.gpu}${isSoftwareGpu(e.gpu)?" \u26A0\uFE0F software":""}`:""} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 ${e.runsPerCase} runs${e.results.some(a=>a.plannedRuns>e.runsPerCase)?" (more for close calls)":""} \xD7 ${e.durationMs/1e3}s per case, fresh browser context each run, case order shuffled each round.`);const r=e.results.map(a=>a.median?.probeSpanMs).filter(a=>a!=null);const i=e.results.map(a=>a.median?.cdpSpanMs).filter(a=>a!=null);const s=a=>`${(Math.min(...a)/1e3).toFixed(2)}\u2013${(Math.max(...a)/1e3).toFixed(2)} s`;if(r.length){t.push(`Recorded: frames over ${s(r)}${i.length?`; cost columns (Chrome's counters) over ${s(i)}`:""}, not the requested time.`)}if(e.replay?.dirty)t.push(`Note: the project had uncommitted changes, so commit ${e.replay.commit?.slice(0,10)??"?"} alone doesn't describe the code that ran.`);t.push("");t.push(`**How to read this.** Dropped % is the smoothness number: frames Chrome dropped or only partly produced that it says hurt smoothness, as a share of the frames it wanted, from its own verdicts in a trace. Rank on it first, and only across variants doing the same work. Shown FPS counts frames Chrome produced (headless Chrome displays nothing), so an animation that changes less often needs fewer; don't compare it across variants that update at different rates. Main-thread FPS and frame gaps come from requestAnimationFrame. Main thread % is Chrome's task time for the run. Desktop numbers, even with CPU slowdown, don't stand in for a real phone's GPU, memory or heat.`);const o=Array.from(new Set(e.results.map(a=>a.cpu)));for(const a of o){t.push("");t.push(a===1?`#### No CPU slowdown`:`#### CPU slowed ${a}x`);t.push("");t.push(`| Case | Runs | Shown FPS | Dropped % | Main-thread FPS (range) | p95 gap ms | Worst gap ms | Main thread % | Script ms | Style ms | Layout ms | Layouts | DOM nodes | JS heap MB | Flags |`);t.push(`|------|-----:|----------:|----------:|------------------------:|-----------:|-------------:|--------------:|----------:|---------:|----------:|--------:|----------:|--------:|-------|`);for(const h of e.results.filter(p=>p.cpu===a)){const p=h.median;if(!p){const w=h.runs.find(f=>f.failed)?.failed??"no valid runs";t.push(`| ${h.caseName} | 0/${h.plannedRuns} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${w}) |`);continue}const m=h.fpsMin!=null&&h.fpsMax!=null&&h.validRuns>1?` (${n(h.fpsMin)}\u2013${n(h.fpsMax)})`:"";const g=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";t.push(`| ${h.isBaseline?`${h.caseName} (baseline)`:h.control?`${h.caseName} (control)`:h.caseName} | ${h.validRuns}/${h.plannedRuns} | ${n(p.shownFps)} | ${h.droppedMin==null&&h.tracedRuns>0?`\u2014 (${h.tracedRuns}/${h.validRuns} traced)`:n(h.droppedMin==null?null:p.droppedPct)}${h.droppedMin!=null&&h.validRuns>1?` [${h.droppedMin}\u2013${h.droppedMax}]`:""}${h.deltaDroppedPct!=null?` (${h.deltaDroppedPct>0?"+":""}${h.deltaDroppedPct} pp)`:""} | ${n(p.fps)}${m} | ${n(p.frameGapP95Ms)} | ${n(p.frameGapMaxMs)} | ${n(p.mainThreadPct)} | ${n(p.scriptMs,0)} | ${n(p.styleMs,0)} | ${n(p.layoutMs,0)} | ${n(p.layoutCount,0)} | ${n(p.domNodesEnd,0)} | ${n(p.heapMb)} | ${g} |`)}const c=closeCall(e.results.filter(h=>h.cpu===a));if(c){t.push("");t.push(c)}const u=e.results.filter(h=>h.cpu===a&&h.budget);if(u.length){t.push("");t.push(`Budget:`);for(const h of u){const p=h.budget;const m=[...p.over,...p.unknown.map(w=>`${w} not measured`)].join("; ");const g=h.control?`${h.caseName} (control)`:h.caseName;t.push(`- **${g}**: ${p.meets?"meets every limit":`misses (${m||"no result"})`}`)}}const l=e.results.filter(h=>h.cpu===a&&h.median?.interactions!=null&&(e.hasSteps||h.median.interactions>0)).map(h=>{const p=h.median;const m=h.inputMin!=null&&h.inputMax!=null?` \xB7 slowest across all runs ${h.inputMax} ms (range ${h.inputMin}\u2013${h.inputMax})`:"";const g=h.runs.filter(f=>!f.failed&&f.fps!=null&&f.inputCaptureComplete===false).length;const w=g?` \xB7 \u26A0\uFE0F timing incomplete in ${g} run(s): the last input's paint came after the run ended, so its time is unknown (leave more time after the last step)`:"";if(!p.interactions)return`- **${h.caseName}**: middle run had no input entry (inputs under 16 ms aren't reported)${m}${w}`;return`- **${h.caseName}**: middle run: ${p.interactions} reported (16 ms or more), slowest ${p.worstInteractionMs} ms (waiting ${p.worstInputDelayMs} \xB7 handlers ${p.worstProcessingMs} \xB7 to paint ${p.worstPresentationMs})${m}${w}`});if(l.length){t.push("");t.push(`Input response (scripted steps, Event Timing). Inputs under 16 ms aren't reported. Under 200 ms is good.`);t.push(...l)}const d=e.results.filter(h=>h.cpu===a&&h.median&&h.median.topScripts.length).map(h=>`- **${h.caseName}**: ${h.median.topScripts.slice(0,3).map(p=>`${p.name} ${p.ms} ms`).join(", ")}`);if(d.length){t.push("");t.push(`Scripts in slow frames (Long Animation Frames; frames that ran past the end of a run are left out):`);t.push(...d)}}if(e.traces.length){t.push("");t.push(`Chrome traces (open in DevTools \u2192 Performance \u2192 Load profile): ${e.traces.join(", ")}`)}if(e.warnings.length){t.push("");t.push(...e.warnings.map(a=>`\u26A0\uFE0F ${a}`))}return t.join("\n")}var import_node_module,import_node_child_process13,import_node_path11,import_node_fs10,import_node_os8,import_node_url,r1,GPU_RENDERER_JS,COMMON_RATES,FRAME_STATES,PLAYWRIGHT_MISSING,FRAME_TRACE_CATEGORIES,FULL_TRACE_CATEGORIES,MAX_TRACE_CHARS,sleep2,LOCK_FILE,lockedHere,MIN_DROPPED_GAP_PP,MIN_FPS_GAP;var init_webBench=__esm({"src/webBench/webBench.ts"(){"use strict";import_node_module=require("module");import_node_child_process13=require("child_process");import_node_path11=require("path");import_node_fs10=require("fs");import_node_os8=require("os");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;GPU_RENDERER_JS=`(() => {
|
|
384
384
|
try {
|
|
385
385
|
const gl = document.createElement("canvas").getContext("webgl2") || document.createElement("canvas").getContext("webgl");
|
|
386
386
|
if (!gl) return null;
|
|
387
387
|
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
388
388
|
return String(gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : gl.RENDERER));
|
|
389
389
|
} catch { return null; }
|
|
390
|
-
})()`;COMMON_RATES=[30,60,75,90,120,144,165,240];FRAME_STATES=["STATE_PRESENTED_ALL","STATE_PRESENTED_PARTIAL","STATE_DROPPED"];PLAYWRIGHT_MISSING="Playwright isn't installed in this project. Install it, then run again:\n npm i -D playwright && npx playwright install chromium";FRAME_TRACE_CATEGORIES=["disabled-by-default-devtools.timeline.frame","blink.user_timing"];FULL_TRACE_CATEGORIES=["devtools.timeline","disabled-by-default-devtools.timeline","disabled-by-default-devtools.timeline.frame","blink.user_timing","loading","latencyInfo","v8.execute"];MAX_TRACE_CHARS=15e7;sleep2=e=>new Promise(n=>setTimeout(n,e));LOCK_FILE=(0,import_node_path11.join)((0,import_node_os8.tmpdir)(),"buoy-web-bench.lock");lockedHere=false;MIN_DROPPED_GAP_PP=1.5;MIN_FPS_GAP=2}});function nextLoad(e,n){const t=Math.max(e+1,e*n);const r=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/r)*r}function median(e){if(!e.length)return null;const n=[...e].sort((r,i)=>r-i);const t=Math.floor(n.length/2);return n.length%2?n[t]:Math.round((n[t-1]+n[t])/2)}function bracketResolved(e,n,t){if(n-e<=1)return true;if(e<=0)return false;return(n-e)/e<=t/100}async function rampOnce(e,n){const t=[];const r=(u,l,d,h)=>({status:u,stopReason:l,highestPass:d?{load:d.load,fps:d.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:u==="below_one"||u==="bracketed"&&!!d&&!!h&&bracketResolved(d.load,h.load,n.precisionPct),steps:t});const i=(u,l,d)=>r("invalid",u.fps==null?"measurement_failed":"page_error",l,d);const s=async(u,l=false)=>{const d=await e(u);if(l)d.confirm=true;t.push(d);return d};let o=null;let a=null;let c=await s(n.start);if(!c.valid)return i(c,null,null);if(c.pass)o=c;else{a=c;let u=n.start;while(!o){if(u<=1)return r("below_one","performance",null,a);if(!n.timeLeft())return r("lower_bound","deadline",null,a);u=Math.max(1,Math.floor(u/2));c=await s(u);if(!c.valid)return i(c,null,a);if(c.pass)o=c;else a=c}}while(!a||a.load<=o.load){if(o.load>=n.max)return r("lower_bound","max",o,null);if(!n.timeLeft())return r("lower_bound","deadline",o,null);c=await s(Math.min(n.max,nextLoad(o.load,n.growth)));if(!c.valid)return i(c,o,null);if(c.pass){o=c;continue}if(!n.timeLeft())return r("bracketed","performance",o,c);const u=await s(c.load,true);if(!u.valid)return i(u,o,null);if(u.pass)o=u;else a=u}while(!bracketResolved(o.load,a.load,n.precisionPct)&&n.timeLeft()){const u=Math.round((o.load+a.load)/2);if(u<=o.load||u>=a.load)break;c=await s(u);if(!c.valid)break;if(c.pass)o=c;else a=c}return r("bracketed","performance",o,a)}async function runWebRamp(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.param)throw new Error('Pass `param`, the query param that sets the load (e.g. "lights").');const t=(y,_)=>{if(y!=null&&!Number.isFinite(y))throw new Error(`${_} must be a finite number.`)};for(const[y,_]of Object.entries({start:e.start,growth:e.growth,max:e.max,targetFps:e.targetFps,precisionPct:e.precisionPct,rounds:e.rounds,settleMs:e.settleMs,measureMs:e.measureMs,cpuSlowdown:e.cpuSlowdown}))t(_,y);const r=Math.max(1,Math.floor(e.start??100));const i=Math.min(4,Math.max(1.05,e.growth??1.3));const s=Math.max(r,Math.floor(e.max??1e9));const o=e.targetFps??55;const a=Math.min(50,Math.max(.5,e.precisionPct??4));const c=Math.max(1,Math.min(5,Math.floor(e.rounds??1)));const u=Math.max(0,e.settleMs??1e3);const l=Math.max(500,e.measureMs??2e3);const d=Math.max(1,e.cpuSlowdown??1);const h={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const p=[];const m=Date.now();const g=m+(e.maxBatchMs??40*6e4);const w=()=>g-Date.now();const f=m>>>0;const b=await waitForHostLock(Math.min(e.lockWaitMs??20*6e4,Math.max(0,w())),n);let v=false;try{const y=await loadChromium(e.projectRoot??process.cwd());const _=await y.launch({headless:!e.headed,args:["--enable-gpu","--disable-backgrounding-occluded-windows","--disable-renderer-backgrounding","--disable-background-timer-throttling"]});try{const $={viewport:{width:h.width,height:h.height},deviceScaleFactor:h.deviceScaleFactor};let k=null;let T=60;{const M=await _.newContext($);try{const S=await M.newPage();await S.goto("about:blank");k=await S.evaluate(GPU_RENDERER_JS)??null;const x=await withDeadline(S.evaluate(measureFramesJs(200,1e3)),1e4,async()=>({fps:60}));T=snapRefreshRate(x.fps)}finally{await closeWithin(M,5e3)}}n(`GPU ${k??"unknown"} \xB7 idle ${T} Hz`);if(k&&isSoftwareGpu(k))p.push(`Chrome is drawing with a software GPU (${k}), so ceilings are far lower than on a real screen.`);if(o>T*.95){p.push(`The target (${o} fps) is at or above this browser's idle frame rate (${T} Hz), so even an empty page can miss it. Use headed: true on a faster screen, or measure with a target your contract allows.`)}const A=e.cases.map(M=>({caseName:M.name,ceiling:null,atLeast:null,rounds:[],errors:[]}));const I=async(M,S,x,E)=>{const j=E.length;const P=caseUrl(e.url,{...S,params:{...S.params??{},[e.param]:String(x)}},e.path);const F
|
|
390
|
+
})()`;COMMON_RATES=[30,60,75,90,120,144,165,240];FRAME_STATES=["STATE_PRESENTED_ALL","STATE_PRESENTED_PARTIAL","STATE_DROPPED"];PLAYWRIGHT_MISSING="Playwright isn't installed in this project. Install it, then run again:\n npm i -D playwright && npx playwright install chromium";FRAME_TRACE_CATEGORIES=["disabled-by-default-devtools.timeline.frame","blink.user_timing"];FULL_TRACE_CATEGORIES=["devtools.timeline","disabled-by-default-devtools.timeline","disabled-by-default-devtools.timeline.frame","blink.user_timing","loading","latencyInfo","v8.execute"];MAX_TRACE_CHARS=15e7;sleep2=e=>new Promise(n=>setTimeout(n,e));LOCK_FILE=(0,import_node_path11.join)((0,import_node_os8.tmpdir)(),"buoy-web-bench.lock");lockedHere=false;MIN_DROPPED_GAP_PP=1.5;MIN_FPS_GAP=2}});function nextLoad(e,n){const t=Math.max(e+1,e*n);const r=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/r)*r}function median(e){if(!e.length)return null;const n=[...e].sort((r,i)=>r-i);const t=Math.floor(n.length/2);return n.length%2?n[t]:Math.round((n[t-1]+n[t])/2)}function bracketResolved(e,n,t){if(n-e<=1)return true;if(e<=0)return false;return(n-e)/e<=t/100}async function rampOnce(e,n){const t=[];const r=(u,l,d,h)=>({status:u,stopReason:l,highestPass:d?{load:d.load,fps:d.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:u==="below_one"||u==="bracketed"&&!!d&&!!h&&bracketResolved(d.load,h.load,n.precisionPct),steps:t});const i=(u,l,d)=>r("invalid",u.fps==null?"measurement_failed":"page_error",l,d);const s=async(u,l=false)=>{const d=await e(u);if(l)d.confirm=true;t.push(d);return d};let o=null;let a=null;let c=await s(n.start);if(!c.valid)return i(c,null,null);if(c.pass)o=c;else{a=c;let u=n.start;while(!o){if(u<=1)return r("below_one","performance",null,a);if(!n.timeLeft())return r("lower_bound","deadline",null,a);u=Math.max(1,Math.floor(u/2));c=await s(u);if(!c.valid)return i(c,null,a);if(c.pass)o=c;else a=c}}while(!a||a.load<=o.load){if(o.load>=n.max)return r("lower_bound","max",o,null);if(!n.timeLeft())return r("lower_bound","deadline",o,null);c=await s(Math.min(n.max,nextLoad(o.load,n.growth)));if(!c.valid)return i(c,o,null);if(c.pass){o=c;continue}if(!n.timeLeft())return r("bracketed","performance",o,c);const u=await s(c.load,true);if(!u.valid)return i(u,o,null);if(u.pass)o=u;else a=u}while(!bracketResolved(o.load,a.load,n.precisionPct)&&n.timeLeft()){const u=Math.round((o.load+a.load)/2);if(u<=o.load||u>=a.load)break;c=await s(u);if(!c.valid)break;if(c.pass)o=c;else a=c}return r("bracketed","performance",o,a)}async function runWebRamp(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.param)throw new Error('Pass `param`, the query param that sets the load (e.g. "lights").');const t=(y,_)=>{if(y!=null&&!Number.isFinite(y))throw new Error(`${_} must be a finite number.`)};for(const[y,_]of Object.entries({start:e.start,growth:e.growth,max:e.max,targetFps:e.targetFps,precisionPct:e.precisionPct,rounds:e.rounds,settleMs:e.settleMs,measureMs:e.measureMs,cpuSlowdown:e.cpuSlowdown}))t(_,y);const r=Math.max(1,Math.floor(e.start??100));const i=Math.min(4,Math.max(1.05,e.growth??1.3));const s=Math.max(r,Math.floor(e.max??1e9));const o=e.targetFps??55;const a=Math.min(50,Math.max(.5,e.precisionPct??4));const c=Math.max(1,Math.min(5,Math.floor(e.rounds??1)));const u=Math.max(0,e.settleMs??1e3);const l=Math.max(500,e.measureMs??2e3);const d=Math.max(1,e.cpuSlowdown??1);const h={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const p=[];const m=Date.now();const g=m+(e.maxBatchMs??40*6e4);const w=()=>g-Date.now();const f=m>>>0;const b=await waitForHostLock(Math.min(e.lockWaitMs??20*6e4,Math.max(0,w())),n);let v=false;try{const y=await loadChromium(e.projectRoot??process.cwd());const _=await y.launch({headless:!e.headed,args:["--enable-gpu","--disable-backgrounding-occluded-windows","--disable-renderer-backgrounding","--disable-background-timer-throttling"]});try{const $={viewport:{width:h.width,height:h.height},deviceScaleFactor:h.deviceScaleFactor};let k=null;let T=60;{const M=await _.newContext($);try{const S=await M.newPage();await S.goto("about:blank");k=await S.evaluate(GPU_RENDERER_JS)??null;const x=await withDeadline(S.evaluate(measureFramesJs(200,1e3)),1e4,async()=>({fps:60}));T=snapRefreshRate(x.fps)}finally{await closeWithin(M,5e3)}}n(`GPU ${k??"unknown"} \xB7 idle ${T} Hz`);if(k&&isSoftwareGpu(k))p.push(`Chrome is drawing with a software GPU (${k}), so ceilings are far lower than on a real screen.`);if(o>T*.95){p.push(`The target (${o} fps) is at or above this browser's idle frame rate (${T} Hz), so even an empty page can miss it. Use headed: true on a faster screen, or measure with a target your contract allows.`)}const A=e.cases.map(M=>({caseName:M.name,ceiling:null,atLeast:null,rounds:[],errors:[]}));const I=async(M,S,x,E)=>{const j=E.length;const P=caseUrl(e.url,{...S,params:{...S.params??{},[e.param]:String(x)}},e.path);const B=F=>Math.max(1e3,Math.min(F,w()+15e3));try{await M.goto(P,{waitUntil:"load",timeout:B(6e4)});if(e.readySelector)await M.waitForSelector(e.readySelector,{state:"attached",timeout:B(6e4)});const F=await withDeadline(M.evaluate(measureFramesJs(u,l)),u+l+6e4,async()=>{throw new Error("the page stopped drawing (no frames for 60 s)")});const C=E.length>j?E[E.length-1]:void 0;if(C)return{load:x,fps:F.fps,p95GapMs:F.p95,valid:false,pass:false,error:C};return{load:x,fps:F.fps,p95GapMs:F.p95,valid:true,pass:F.fps>=o}}catch(F){return{load:x,fps:null,p95GapMs:null,valid:false,pass:false,error:(F?.message??String(F)).split("\n")[0].slice(0,200)}}};let R=false;for(let M=0;M<c&&!R;M++){const S=shuffled(e.cases.map((x,E)=>({c:x,i:E})),f+M*7919>>>0);for(const{c:x,i:E}of S){const j=A[E];if(R||w()<=0){j.rounds.push({round:M+1,status:"skipped",stopReason:"not_run",highestPass:null,lowestMiss:null,resolved:false,steps:[]});continue}n(`round ${M+1}/${c} \xB7 ${x.name}`);let P;try{P=await _.newContext($);const B=await P.newPage();B.on("pageerror",C=>j.errors.push(String(C?.message??C).slice(0,200)));if(d>1){const C=await P.newCDPSession(B);await C.send("Emulation.setCPUThrottlingRate",{rate:d})}const F=await rampOnce(C=>I(B,x,C,j.errors),{start:r,growth:i,max:s,precisionPct:a,timeLeft:()=>w()>0});j.rounds.push({round:M+1,...F});n(` ${x.name}: ${F.status} ${F.highestPass?.load??"-"}`)}catch(B){j.errors.push((B?.message??String(B)).split("\n")[0].slice(0,200));j.rounds.push({round:M+1,status:"invalid",stopReason:"measurement_failed",highestPass:null,lowestMiss:null,resolved:false,steps:[]})}finally{if(P&&!await closeWithin(P,1e4)){v=true;R=true;p.push(`${x.name}'s browser context didn't close, so the batch stopped to keep later runs clean.`)}}}}for(const M of A){const S=M.rounds.filter(E=>E.status==="bracketed"||E.status==="below_one");M.ceiling=median(S.map(E=>E.highestPass?.load??0));const x=M.rounds.filter(E=>E.status==="lower_bound"&&E.highestPass).map(E=>E.highestPass.load);M.atLeast=x.length?Math.max(...x):null}if(w()<=0)p.push("The batch hit its time limit. Rounds marked skipped or lower_bound didn't finish.");return{url:e.url,browser:`Chromium ${_.version()}${e.headed?"":" (headless)"}`,gpu:k,refreshHz:T,targetFps:o,cpuSlowdown:d,viewport:h,seed:f,results:A,warnings:p}}finally{if(!await closeWithin(_,1e4))v=true}}finally{b(v)}}function formatWebRampMarkdown(e){const n=s=>s==null?"\u2014":s.toLocaleString("en-US");const t=s=>s?`${n(s.load)} @ ${s.fps.toFixed(1)}`:"\u2014";const r=[];r.push(`### Web ceiling \xB7 ${e.url}`);r.push(`${e.browser}${e.gpu?` \xB7 GPU: ${e.gpu}${isSoftwareGpu(e.gpu)?" \u26A0\uFE0F software":""}`:""} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 idle ${e.refreshHz} Hz \xB7 target ${e.targetFps} fps${e.cpuSlowdown>1?` \xB7 CPU slowed ${e.cpuSlowdown}x`:""}`);r.push("");r.push("**Ceiling** is the most load that held the target, timed from main-thread `requestAnimationFrame` callbacks (a stand-in for drawing speed; it can't see a canvas that stopped updating, so check the looks too). Each round ramps ~30% a step, confirms the first miss, then bisects. With several rounds the ceiling is their median. Ceilings within about 10% are a tie until more rounds agree.");r.push("");const i=Math.max(0,...e.results.map(s=>s.ceiling??0));r.push("| Case | Ceiling | Each round: highest pass @ fps \u2192 lowest miss @ fps | Notes |");r.push("|------|--------:|------------------------------------------------------|-------|");for(const s of e.results){const o=s.rounds.map(c=>{if(c.status==="skipped")return`r${c.round}: not run`;if(c.status==="invalid")return`r${c.round}: invalid (${c.stopReason}) after ${t(c.highestPass)}`;if(c.status==="lower_bound")return`r${c.round}: at least ${t(c.highestPass)} (${c.stopReason})`;if(c.status==="below_one")return`r${c.round}: even 1 missed (${t(c.lowestMiss)})`;return`r${c.round}: ${t(c.highestPass)} \u2192 ${t(c.lowestMiss)}${c.resolved?"":" (wide)"}`}).join("<br>");const a=[s.ceiling===i&&i>0?"best":"",s.atLeast!=null?`at least ${n(s.atLeast)}`:"",s.rounds.some(c=>c.status==="invalid")?"a round broke: check the page before trusting it":"",s.errors.length?`${s.errors.length} page error(s): ${s.errors[0]}`:""].filter(Boolean);r.push(`| ${s.caseName} | ${n(s.ceiling)} | ${o} | ${a.join("; ")} |`)}if(e.warnings.length){r.push("");for(const s of e.warnings)r.push(`- \u26A0\uFE0F ${s}`)}return r.join("\n")}var measureFramesJs;var init_webRamp=__esm({"src/webBench/webRamp.ts"(){"use strict";init_webBench();measureFramesJs=(e,n)=>`new Promise((resolve) => {
|
|
391
391
|
setTimeout(() => {
|
|
392
392
|
const deltas = [];
|
|
393
393
|
let start;
|