@buoy-gg/mcp 7.0.69 → 7.0.70
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-XFE5QEXH.mjs → chunk-5IXJF7C3.mjs} +1 -1
- package/dist/{chunk-QXS5X2WL.mjs → chunk-G6XAWQR4.mjs} +63 -63
- package/dist/{chunk-5ZQ6OP5I.mjs → chunk-IK7Z7CXI.mjs} +1 -1
- package/dist/{chunk-XUN4M5BJ.mjs → chunk-QZCF3GJR.mjs} +1 -1
- package/dist/cli.js +27 -27
- package/dist/cli.mjs +4 -4
- package/dist/{flowRun-HQR7LNLI.mjs → flowRun-IVEWWP72.mjs} +1 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{init-VSU462YF.mjs → init-6V5V3LC4.mjs} +1 -1
- package/dist/{main-6CDJH7KT.mjs → main-XEZDQNJS.mjs} +2 -2
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(e,n)=>function t(){return e&&(n=(0,e[__getOwnPropNames(e)[0]])(e=0)),n};var __commonJS=(e,n)=>function t(){return n||(0,e[__getOwnPropNames(e)[0]])((n={exports:{}}).exports,n),n.exports};var __export=(e,n)=>{for(var t in n)__defProp(e,t,{get:n[t],enumerable:true})};var __copyProps=(e,n,t,s)=>{if(n&&typeof n==="object"||typeof n==="function"){for(let i of __getOwnPropNames(n))if(!__hasOwnProp.call(e,i)&&i!==t)__defProp(e,i,{get:()=>n[i],enumerable:!(s=__getOwnPropDesc(n,i))||s.enumerable})}return e};var __toESM=(e,n,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(n||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));function stopAllHidStreamsNow(){for(const e of workers.values())e.child.kill("SIGKILL")}function runHidStream(e,n,t,s=""){const i=n;const r=(queues.get(i)??Promise.resolve()).catch(()=>void 0).then(()=>send(i,e,t,s));queues.set(i,r);void r.finally(()=>{if(queues.get(i)===r)queues.delete(i)}).catch(()=>void 0);return r}async function send(e,n,t,s){let i=workers.get(e);if(i&&i.bin!==n)i.stop(new Error("The touch helper build changed."));if(i?.stopped){await i.exited;i=void 0}if(!i){const o=(0,import_node_child_process.spawn)(n,["--stream"],{stdio:["pipe","pipe","pipe"]});let a;const l=new Promise(d=>{a=d});i={bin:n,child:o,buffer:"",stopped:false,exited:l,stop:()=>{}};if(workers.size===0)process.once("exit",stopAllHidStreamsNow);workers.set(e,i);const c=i;const u=d=>{if(c.stopped)return;c.stopped=true;if(c.idle)clearTimeout(c.idle);const h=c.pending;c.pending=void 0;if(h)clearTimeout(h.timer);o.ref();o.kill("SIGKILL");void l.then(()=>{if(workers.get(e)===c)workers.delete(e);if(workers.size===0)process.removeListener("exit",stopAllHidStreamsNow);h?.reject(d)})};c.stop=u;o.on("error",d=>{a();u(d)});o.on("exit",()=>{a();u(new Error("The touch helper closed. Try the next touch."))});o.stdin.on("error",d=>u(d));o.stdout.on("data",d=>{c.buffer+=d;let h;while((h=c.buffer.indexOf("\n"))>=0){const p=c.buffer.slice(0,h);c.buffer=c.buffer.slice(h+1);const m=c.pending;if(!m)continue;try{const y=JSON.parse(p);if(!y||typeof y.ok!=="boolean")throw new Error("Bad reply.");if(y.ok===true){clearTimeout(m.timer);c.pending=void 0;m.resolve()}else u(new Error(y.error??"The touch failed."))}catch{u(new Error("The touch helper sent a bad reply."))}}});o.stderr.resume();o.unref();for(const d of[o.stdin,o.stdout,o.stderr])d.unref?.()}const r=i;if(r.idle)clearTimeout(r.idle);return new Promise((o,a)=>{const l=setTimeout(()=>{r.stop(new Error("The touch helper timed out."))},45e3);r.pending={resolve:o,reject:a,timer:l};r.child.stdin.write(JSON.stringify({args:t,input:s})+"\n")}).finally(()=>{if(r.stopped)return;r.idle=setTimeout(()=>{r.child.stdin.end();r.stop(new Error("The touch helper went idle."))},1e4);r.idle.unref()})}var import_node_child_process,workers,queues;var init_hidStream=__esm({"src/capture/hidStream.ts"(){"use strict";import_node_child_process=require("child_process");workers=new Map;queues=new Map}});function setRecordingProbe(e){recordingActive=e}function hasSecrets(e){PLACEHOLDER.lastIndex=0;return PLACEHOLDER.test(e)}function fillSecrets(e,n=process.env){if(!hasSecrets(e))return e;if(recordingActive()){throw new Error("A screen recording is running, and a video can't hide a typed secret. Stop the recording first (screen_recording stop), then type it.")}const t=[];const s=[];const i=e.replace(PLACEHOLDER,(r,o)=>{const a=n[`BUOY_SECRET_${o}`]??n[o];if(a===void 0||a===""){t.push(o);return""}if(a.length<MIN_SECRET_LENGTH){s.push(o);return""}used.set(o,a);return a});if(t.length){throw new Error(`No value for ${t.map(r=>`{{secret:${r}}}`).join(", ")}. Set ${t.map(r=>`BUOY_SECRET_${r}`).join(", ")} in the environment the MCP server runs in (for example in your editor's MCP config "env").`)}if(s.length){throw new Error(`${s.map(r=>`{{secret:${r}}}`).join(", ")} is shorter than ${MIN_SECRET_LENGTH} characters, so Buoy can't hide it reliably in what it sends back. Use a longer value, or type it as plain text if it isn't sensitive.`)}return i}function knownSecrets(e=process.env){const n=new Map(used);for(const[t,s]of Object.entries(e))if(t.startsWith("BUOY_SECRET_")&&s)n.set(t.slice("BUOY_SECRET_".length),s);return n}function scrubSecrets(e){const n=new Map;for(const[i,r]of knownSecrets()){if(r.length<MIN_SECRET_LENGTH)continue;for(const o of[r,JSON.stringify(r).slice(1,-1),encodeURIComponent(r)])n.set(o,i);for(let o=r.length-1;o>=MIN_SECRET_LENGTH;o--){n.set(`${r.slice(0,o)}\u2026`,i);n.set(`${r.slice(0,o)}...`,i);if(o>=8)n.set(r.slice(0,o),i)}}if(n.size===0)return e;const t=[...n.keys()].sort((i,r)=>r.length-i.length);const s=new RegExp(t.map(escapeRe).join("|"),"g");return e.replace(s,(i,r,o)=>{if(/^\d+$/.test(i)){const a=o.slice(Math.max(0,r-8),r);const l=o.slice(r+i.length,r+i.length+8);if(/(\d|\d\.)$/.test(a)||/^(\d|\.\d|[eE][+-]?\d)/.test(l))return i;if(/[:[,]\s*$/.test(a)&&/^\s*([,\]}]|$)/.test(l)&&!insideJsonString(o,r))return`"{{secret:${n.get(i)}}}"`}return`{{secret:${n.get(i)}}}`})}function insideJsonString(e,n){let t=false;for(let s=0;s<n;s++){if(e[s]==="\\")s++;else if(e[s]==='"')t=!t}return t}function typedSecretValues(){return[...used.values()]}function secretValues(){return[...knownSecrets().values()].filter(e=>e.length>=MIN_SECRET_LENGTH)}var PLACEHOLDER,MIN_SECRET_LENGTH,used,recordingActive,escapeRe;var init_secrets=__esm({"src/tools/secrets.ts"(){"use strict";PLACEHOLDER=/\{\{\s*secret:([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;MIN_SECRET_LENGTH=6;used=new Map;recordingActive=()=>false;escapeRe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}});function q(e){return`'${e.replace(/'/g,`'\\''`)}'`}function assertPackage(e){if(!/^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$/.test(e))throw new Error(`"${e}" is not a valid Android package name.`);return e}async function adb(e,n,t=15e3){try{const{stdout:s}=await execFileAsync("adb",e?["-s",e,...n]:n,{timeout:t,maxBuffer:32*1024*1024});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(ADB_MISSING);const r=n[0]==="shell"?`shell ${(n[1]??"").split(" ")[0]}`:n.slice(0,2).join(" ");const o=n[0]==="shell"?i.stderr||(i.killed?"timed out":"no error text"):i.stderr||i.message||"";const a=scrubSecrets(o.replace(/'[^']*'/g,"'\u2026'")).trim().slice(0,300);throw new Error(`adb ${r} failed: ${a}`)}}function adbShell(e,n,t=15e3){return adb(e,["shell",n],t)}async function adbBytes(e,n,t=15e3){try{const{stdout:s}=await execFileAsync("adb",["-s",e,...n],{timeout:t,encoding:"buffer",maxBuffer:64*1024*1024});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(ADB_MISSING);throw new Error(`adb ${n.slice(0,2).join(" ")} failed: ${i.message.slice(0,300)}`)}}async function adbSerials(){return parseAdbDevices(await adb(null,["devices"]))}function parseAdbDevices(e){return e.split("\n").slice(1).map(n=>n.trim().split(/\s+/)).filter(n=>n[1]==="device").map(n=>n[0])}async function adbScreenSize(e){const n=parseWmSize(await adbShell(e,"wm size"));const t=parseRotation(await adbShell(e,"dumpsys input").catch(()=>""));return t%2===1?{width:n.height,height:n.width}:n}function parseWmSize(e){const n=/Override size:\s*(\d+)x(\d+)/.exec(e);const t=/Physical size:\s*(\d+)x(\d+)/.exec(e);const s=n??t;if(!s)throw new Error(`Could not read the Android screen size (adb said: ${e.trim().slice(0,120)}).`);return{width:Number(s[1]),height:Number(s[2])}}function parseRotation(e){const n=/SurfaceOrientation:\s*(\d)/.exec(e);return n?Number(n[1]):0}async function adbAppFrame(e,n){const t=await adbShell(e,"dumpsys window windows").catch(()=>"");return parseAppFrame(t,n)}function parseAppFrame(e,n){const t=e.split(/\n(?=\s*Window #\d+)/);for(const s of t){if(!s.includes(`${n}/`))continue;const i=/mFrame=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(s)??/\bframe=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(s);if(i){const[r,o,a,l]=i.slice(1).map(Number);if(a>r&&l>o)return{x:r,y:o,width:a-r,height:l-o}}}return null}async function adbForegroundPackage(e){return parseForegroundPackage(await adbShell(e,"dumpsys activity activities"))}function parseForegroundPackage(e){const n=/(?:topResumedActivity|mResumedActivity|ResumedActivity)[^\n]*?\s([a-zA-Z][\w.]*)\/[\w.$]+/.exec(e);return n?n[1]:null}async function adbUserPackages(e){const n=await adbShell(e,"pm list packages -3");return n.split("\n").map(t=>t.trim().replace(/^package:/,"")).filter(t=>/^[A-Za-z][\w.]+$/.test(t))}async function adbInstallId(e,n){assertPackage(n);const t=await adbShell(e,`run-as ${q(n)} cat files/react-buoy/@react_buoy_sync_device_id.json 2>/dev/null`,5e3).catch(()=>"");const s=t.trim().replace(/^"|"$/g,"");if(/^[\w-]{4,64}$/.test(s))return s;const i=await adbBytes(e,["exec-out",`run-as ${q(n)} sh -c 'cat app_webview/Default/"Local Storage"/leveldb/*.log app_webview/Default/"Local Storage"/leveldb/*.ldb 2>/dev/null'`],5e3).catch(()=>null);return i?webViewLevelDbInstallId(i):null}function webViewLevelDbInstallId(e){const n=e.toString("latin1");const t=/@react_buoy_sync_device_id([\s\S])\x01([\w-]{4,64})/g;let s=null;for(let i=t.exec(n);i;i=t.exec(n)){const r=i[1].charCodeAt(0)-1;if(r>=4&&r<=i[2].length)s=i[2].slice(0,r)}return s}function escapeAdbText(e){if(/[^\x20-\x7e]/.test(e))throw new Error("adb can only type plain ASCII text. Use tap_element's text for other characters.");return q(e.replace(/ /g,"%s"))}async function adbTap(e,n,t,s){const[i,r]=[Math.round(n),Math.round(t)];if(s)await adbShell(e,`input swipe ${i} ${r} ${i} ${r} ${Math.round(s)}`);else await adbShell(e,`input tap ${i} ${r}`)}async function adbSwipe(e,n,t,s){const i=[...n,...t].map(r=>Math.round(r)).join(" ");await adbShell(e,`input swipe ${i} ${Math.round(s)}`)}async function adbText(e,n){const t=n.replace(/ /g,"\0");for(let s=0;s<t.length;s+=40){const i=t.slice(s,s+40).replace(/\u0000/g," ");await adbShell(e,`input text ${escapeAdbText(i)}`)}}async function adbScreenshot(e){const n=await adbBytes(e,["exec-out","screencap","-p"]);if(n.length<8||n.readUInt32BE(0)!==2303741511)throw new Error("The Android screenshot was not a PNG.");return n}async function adbDemoMode(e,n){if(n){await adbShell(e,"settings put global sysui_demo_allowed 1");for(const t of["-e command enter","-e command clock -e hhmm 0941","-e command battery -e level 100 -e plugged false","-e command network -e wifi show -e level 4","-e command notifications -e visible false"]){await adbShell(e,`am broadcast -a com.android.systemui.demo ${t}`).catch(()=>void 0)}}else{await adbShell(e,"am broadcast -a com.android.systemui.demo -e command exit").catch(()=>void 0)}}async function adbUiNodes(e){const n=await adb(e,["exec-out","uiautomator","dump","/dev/tty"],2e4);return parseUiDump(n)}function parseUiDump(e){const n=[];const t=(s,i)=>{const r=new RegExp(`\\s${i}="([^"]*)"`).exec(s);return r?decodeXml(r[1]):""};for(const s of e.matchAll(/<node\b[^>]*>/g)){const i=s[0];const r=/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(t(i,"bounds"));if(!r)continue;const[o,a,l,c]=r.slice(1).map(Number);n.push({text:t(i,"text"),desc:t(i,"content-desc"),id:t(i,"resource-id"),cls:t(i,"class"),clickable:t(i,"clickable")==="true",bounds:{x:o,y:a,width:l-o,height:c-a}})}return n}function decodeXml(e){return e.replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(n,t)=>String.fromCharCode(Number(t))).replace(/&/g,"&")}function findUiNodes(e,n){const t=n.trim().toLowerCase();const s=l=>{let c=0;for(const u of[l.desc,l.text,l.id.split("/").pop()??""]){const d=u.trim().toLowerCase();if(!d)continue;if(d===t)c=Math.max(c,3);else if(d.startsWith(t))c=Math.max(c,2);else if(d.includes(t))c=Math.max(c,1)}return c};const i=e.filter(l=>l.bounds.width>0&&l.bounds.height>0&&s(l)>0);const r=Math.max(0,...i.map(s));const o=i.filter(l=>s(l)===r);let a=o.filter((l,c)=>o.findIndex(u=>JSON.stringify(u.bounds)===JSON.stringify(l.bounds))===c);if(a.length>1){const l=a.filter(c=>[c.desc,c.text,c.id.split("/").pop()??""].some(u=>u.trim()===n.trim()));if(l.length)a=l}if(a.length>1){const l=a.filter(c=>c.clickable||/EditText|Button/.test(c.cls));if(l.length)a=l}return a}var import_node_child_process2,import_node_util,execFileAsync,ADB_MISSING;var init_adb=__esm({"src/capture/adb.ts"(){"use strict";import_node_child_process2=require("child_process");import_node_util=require("util");init_secrets();execFileAsync=(0,import_node_util.promisify)(import_node_child_process2.execFile);ADB_MISSING="Android control needs adb on this computer (Android SDK platform-tools). Install it, or add it to PATH."}});var require_chunkstream=__commonJS({"../../node_modules/pngjs/lib/chunkstream.js"(e,n){"use strict";var t=require("util");var s=require("stream");var i=n.exports=function(){s.call(this);this._buffers=[];this._buffered=0;this._reads=[];this._paused=false;this._encoding="utf8";this.writable=true};t.inherits(i,s);i.prototype.read=function(r,o){this._reads.push({length:Math.abs(r),allowLess:r<0,func:o});process.nextTick(function(){this._process();if(this._paused&&this._reads&&this._reads.length>0){this._paused=false;this.emit("drain")}}.bind(this))};i.prototype.write=function(r,o){if(!this.writable){this.emit("error",new Error("Stream not writable"));return false}let a;if(Buffer.isBuffer(r)){a=r}else{a=Buffer.from(r,o||this._encoding)}this._buffers.push(a);this._buffered+=a.length;this._process();if(this._reads&&this._reads.length===0){this._paused=true}return this.writable&&!this._paused};i.prototype.end=function(r,o){if(r){this.write(r,o)}this.writable=false;if(!this._buffers){return}if(this._buffers.length===0){this._end()}else{this._buffers.push(null);this._process()}};i.prototype.destroySoon=i.prototype.end;i.prototype._end=function(){if(this._reads.length>0){this.emit("error",new Error("Unexpected end of input"))}this.destroy()};i.prototype.destroy=function(){if(!this._buffers){return}this.writable=false;this._reads=null;this._buffers=null;this.emit("close")};i.prototype._processReadAllowingLess=function(r){this._reads.shift();let o=this._buffers[0];if(o.length>r.length){this._buffered-=r.length;this._buffers[0]=o.slice(r.length);r.func.call(this,o.slice(0,r.length))}else{this._buffered-=o.length;this._buffers.shift();r.func.call(this,o)}};i.prototype._processRead=function(r){this._reads.shift();let o=0;let a=0;let l=Buffer.alloc(r.length);while(o<r.length){let c=this._buffers[a++];let u=Math.min(c.length,r.length-o);c.copy(l,o,0,u);o+=u;if(u!==c.length){this._buffers[--a]=c.slice(u)}}if(a>0){this._buffers.splice(0,a)}this._buffered-=r.length;r.func.call(this,l)};i.prototype._process=function(){try{while(this._buffered>0&&this._reads&&this._reads.length>0){let r=this._reads[0];if(r.allowLess){this._processReadAllowingLess(r)}else if(this._buffered>=r.length){this._processRead(r)}else{break}}if(this._buffers&&!this.writable){this._end()}}catch(r){this.emit("error",r)}}}});var require_interlace=__commonJS({"../../node_modules/pngjs/lib/interlace.js"(e){"use strict";var n=[{x:[0],y:[0]},{x:[4],y:[0]},{x:[0,4],y:[4]},{x:[2,6],y:[0,4]},{x:[0,2,4,6],y:[2,6]},{x:[1,3,5,7],y:[0,2,4,6]},{x:[0,1,2,3,4,5,6,7],y:[1,3,5,7]}];e.getImagePasses=function(t,s){let i=[];let r=t%8;let o=s%8;let a=(t-r)/8;let l=(s-o)/8;for(let c=0;c<n.length;c++){let u=n[c];let d=a*u.x.length;let h=l*u.y.length;for(let p=0;p<u.x.length;p++){if(u.x[p]<r){d++}else{break}}for(let p=0;p<u.y.length;p++){if(u.y[p]<o){h++}else{break}}if(d>0&&h>0){i.push({width:d,height:h,index:c})}}return i};e.getInterlaceIterator=function(t){return function(s,i,r){let o=s%n[r].x.length;let a=(s-o)/n[r].x.length*8+n[r].x[o];let l=i%n[r].y.length;let c=(i-l)/n[r].y.length*8+n[r].y[l];return a*4+c*t*4}}}});var require_paeth_predictor=__commonJS({"../../node_modules/pngjs/lib/paeth-predictor.js"(e,n){"use strict";n.exports=function t(s,i,r){let o=s+i-r;let a=Math.abs(o-s);let l=Math.abs(o-i);let c=Math.abs(o-r);if(a<=l&&a<=c){return s}if(l<=c){return i}return r}}});var require_filter_parse=__commonJS({"../../node_modules/pngjs/lib/filter-parse.js"(e,n){"use strict";var t=require_interlace();var s=require_paeth_predictor();function i(o,a,l){let c=o*a;if(l!==8){c=Math.ceil(c/(8/l))}return c}var r=n.exports=function(o,a){let l=o.width;let c=o.height;let u=o.interlace;let d=o.bpp;let h=o.depth;this.read=a.read;this.write=a.write;this.complete=a.complete;this._imageIndex=0;this._images=[];if(u){let p=t.getImagePasses(l,c);for(let m=0;m<p.length;m++){this._images.push({byteWidth:i(p[m].width,d,h),height:p[m].height,lineIndex:0})}}else{this._images.push({byteWidth:i(l,d,h),height:c,lineIndex:0})}if(h===8){this._xComparison=d}else if(h===16){this._xComparison=d*2}else{this._xComparison=1}};r.prototype.start=function(){this.read(this._images[this._imageIndex].byteWidth+1,this._reverseFilterLine.bind(this))};r.prototype._unFilterType1=function(o,a,l){let c=this._xComparison;let u=c-1;for(let d=0;d<l;d++){let h=o[1+d];let p=d>u?a[d-c]:0;a[d]=h+p}};r.prototype._unFilterType2=function(o,a,l){let c=this._lastLine;for(let u=0;u<l;u++){let d=o[1+u];let h=c?c[u]:0;a[u]=d+h}};r.prototype._unFilterType3=function(o,a,l){let c=this._xComparison;let u=c-1;let d=this._lastLine;for(let h=0;h<l;h++){let p=o[1+h];let m=d?d[h]:0;let y=h>u?a[h-c]:0;let g=Math.floor((y+m)/2);a[h]=p+g}};r.prototype._unFilterType4=function(o,a,l){let c=this._xComparison;let u=c-1;let d=this._lastLine;for(let h=0;h<l;h++){let p=o[1+h];let m=d?d[h]:0;let y=h>u?a[h-c]:0;let g=h>u&&d?d[h-c]:0;let f=s(y,m,g);a[h]=p+f}};r.prototype._reverseFilterLine=function(o){let a=o[0];let l;let c=this._images[this._imageIndex];let u=c.byteWidth;if(a===0){l=o.slice(1,u+1)}else{l=Buffer.alloc(u);switch(a){case 1:this._unFilterType1(o,l,u);break;case 2:this._unFilterType2(o,l,u);break;case 3:this._unFilterType3(o,l,u);break;case 4:this._unFilterType4(o,l,u);break;default:throw new Error("Unrecognised filter type - "+a)}}this.write(l);c.lineIndex++;if(c.lineIndex>=c.height){this._lastLine=null;this._imageIndex++;c=this._images[this._imageIndex]}else{this._lastLine=l}if(c){this.read(c.byteWidth+1,this._reverseFilterLine.bind(this))}else{this._lastLine=null;this.complete()}}}});var require_filter_parse_async=__commonJS({"../../node_modules/pngjs/lib/filter-parse-async.js"(e,n){"use strict";var t=require("util");var s=require_chunkstream();var i=require_filter_parse();var r=n.exports=function(o){s.call(this);let a=[];let l=this;this._filter=new i(o,{read:this.read.bind(this),write:function(c){a.push(c)},complete:function(){l.emit("complete",Buffer.concat(a))}});this._filter.start()};t.inherits(r,s)}});var require_constants=__commonJS({"../../node_modules/pngjs/lib/constants.js"(e,n){"use strict";n.exports={PNG_SIGNATURE:[137,80,78,71,13,10,26,10],TYPE_IHDR:1229472850,TYPE_IEND:1229278788,TYPE_IDAT:1229209940,TYPE_PLTE:1347179589,TYPE_tRNS:1951551059,TYPE_gAMA:1732332865,COLORTYPE_GRAYSCALE:0,COLORTYPE_PALETTE:1,COLORTYPE_COLOR:2,COLORTYPE_ALPHA:4,COLORTYPE_PALETTE_COLOR:3,COLORTYPE_COLOR_ALPHA:6,COLORTYPE_TO_BPP_MAP:{0:1,2:3,3:1,4:2,6:4},GAMMA_DIVISION:1e5}}});var require_crc=__commonJS({"../../node_modules/pngjs/lib/crc.js"(e,n){"use strict";var t=[];(function(){for(let i=0;i<256;i++){let r=i;for(let o=0;o<8;o++){if(r&1){r=3988292384^r>>>1}else{r=r>>>1}}t[i]=r}})();var s=n.exports=function(){this._crc=-1};s.prototype.write=function(i){for(let r=0;r<i.length;r++){this._crc=t[(this._crc^i[r])&255]^this._crc>>>8}return true};s.prototype.crc32=function(){return this._crc^-1};s.crc32=function(i){let r=-1;for(let o=0;o<i.length;o++){r=t[(r^i[o])&255]^r>>>8}return r^-1}}});var require_parser=__commonJS({"../../node_modules/pngjs/lib/parser.js"(e,n){"use strict";var t=require_constants();var s=require_crc();var i=n.exports=function(r,o){this._options=r;r.checkCRC=r.checkCRC!==false;this._hasIHDR=false;this._hasIEND=false;this._emittedHeadersFinished=false;this._palette=[];this._colorType=0;this._chunks={};this._chunks[t.TYPE_IHDR]=this._handleIHDR.bind(this);this._chunks[t.TYPE_IEND]=this._handleIEND.bind(this);this._chunks[t.TYPE_IDAT]=this._handleIDAT.bind(this);this._chunks[t.TYPE_PLTE]=this._handlePLTE.bind(this);this._chunks[t.TYPE_tRNS]=this._handleTRNS.bind(this);this._chunks[t.TYPE_gAMA]=this._handleGAMA.bind(this);this.read=o.read;this.error=o.error;this.metadata=o.metadata;this.gamma=o.gamma;this.transColor=o.transColor;this.palette=o.palette;this.parsed=o.parsed;this.inflateData=o.inflateData;this.finished=o.finished;this.simpleTransparency=o.simpleTransparency;this.headersFinished=o.headersFinished||function(){}};i.prototype.start=function(){this.read(t.PNG_SIGNATURE.length,this._parseSignature.bind(this))};i.prototype._parseSignature=function(r){let o=t.PNG_SIGNATURE;for(let a=0;a<o.length;a++){if(r[a]!==o[a]){this.error(new Error("Invalid file signature"));return}}this.read(8,this._parseChunkBegin.bind(this))};i.prototype._parseChunkBegin=function(r){let o=r.readUInt32BE(0);let a=r.readUInt32BE(4);let l="";for(let u=4;u<8;u++){l+=String.fromCharCode(r[u])}let c=Boolean(r[4]&32);if(!this._hasIHDR&&a!==t.TYPE_IHDR){this.error(new Error("Expected IHDR on beggining"));return}this._crc=new s;this._crc.write(Buffer.from(l));if(this._chunks[a]){return this._chunks[a](o)}if(!c){this.error(new Error("Unsupported critical chunk type "+l));return}this.read(o+4,this._skipChunk.bind(this))};i.prototype._skipChunk=function(){this.read(8,this._parseChunkBegin.bind(this))};i.prototype._handleChunkEnd=function(){this.read(4,this._parseChunkEnd.bind(this))};i.prototype._parseChunkEnd=function(r){let o=r.readInt32BE(0);let a=this._crc.crc32();if(this._options.checkCRC&&a!==o){this.error(new Error("Crc error - "+o+" - "+a));return}if(!this._hasIEND){this.read(8,this._parseChunkBegin.bind(this))}};i.prototype._handleIHDR=function(r){this.read(r,this._parseIHDR.bind(this))};i.prototype._parseIHDR=function(r){this._crc.write(r);let o=r.readUInt32BE(0);let a=r.readUInt32BE(4);let l=r[8];let c=r[9];let u=r[10];let d=r[11];let h=r[12];if(l!==8&&l!==4&&l!==2&&l!==1&&l!==16){this.error(new Error("Unsupported bit depth "+l));return}if(!(c in t.COLORTYPE_TO_BPP_MAP)){this.error(new Error("Unsupported color type"));return}if(u!==0){this.error(new Error("Unsupported compression method"));return}if(d!==0){this.error(new Error("Unsupported filter method"));return}if(h!==0&&h!==1){this.error(new Error("Unsupported interlace method"));return}this._colorType=c;let p=t.COLORTYPE_TO_BPP_MAP[this._colorType];this._hasIHDR=true;this.metadata({width:o,height:a,depth:l,interlace:Boolean(h),palette:Boolean(c&t.COLORTYPE_PALETTE),color:Boolean(c&t.COLORTYPE_COLOR),alpha:Boolean(c&t.COLORTYPE_ALPHA),bpp:p,colorType:c});this._handleChunkEnd()};i.prototype._handlePLTE=function(r){this.read(r,this._parsePLTE.bind(this))};i.prototype._parsePLTE=function(r){this._crc.write(r);let o=Math.floor(r.length/3);for(let a=0;a<o;a++){this._palette.push([r[a*3],r[a*3+1],r[a*3+2],255])}this.palette(this._palette);this._handleChunkEnd()};i.prototype._handleTRNS=function(r){this.simpleTransparency();this.read(r,this._parseTRNS.bind(this))};i.prototype._parseTRNS=function(r){this._crc.write(r);if(this._colorType===t.COLORTYPE_PALETTE_COLOR){if(this._palette.length===0){this.error(new Error("Transparency chunk must be after palette"));return}if(r.length>this._palette.length){this.error(new Error("More transparent colors than palette size"));return}for(let o=0;o<r.length;o++){this._palette[o][3]=r[o]}this.palette(this._palette)}if(this._colorType===t.COLORTYPE_GRAYSCALE){this.transColor([r.readUInt16BE(0)])}if(this._colorType===t.COLORTYPE_COLOR){this.transColor([r.readUInt16BE(0),r.readUInt16BE(2),r.readUInt16BE(4)])}this._handleChunkEnd()};i.prototype._handleGAMA=function(r){this.read(r,this._parseGAMA.bind(this))};i.prototype._parseGAMA=function(r){this._crc.write(r);this.gamma(r.readUInt32BE(0)/t.GAMMA_DIVISION);this._handleChunkEnd()};i.prototype._handleIDAT=function(r){if(!this._emittedHeadersFinished){this._emittedHeadersFinished=true;this.headersFinished()}this.read(-r,this._parseIDAT.bind(this,r))};i.prototype._parseIDAT=function(r,o){this._crc.write(o);if(this._colorType===t.COLORTYPE_PALETTE_COLOR&&this._palette.length===0){throw new Error("Expected palette not found")}this.inflateData(o);let a=r-o.length;if(a>0){this._handleIDAT(a)}else{this._handleChunkEnd()}};i.prototype._handleIEND=function(r){this.read(r,this._parseIEND.bind(this))};i.prototype._parseIEND=function(r){this._crc.write(r);this._hasIEND=true;this._handleChunkEnd();if(this.finished){this.finished()}}}});var require_bitmapper=__commonJS({"../../node_modules/pngjs/lib/bitmapper.js"(e){"use strict";var n=require_interlace();var t=[function(){},function(a,l,c,u){if(u===l.length){throw new Error("Ran out of data")}let d=l[u];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=255},function(a,l,c,u){if(u+1>=l.length){throw new Error("Ran out of data")}let d=l[u];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=l[u+1]},function(a,l,c,u){if(u+2>=l.length){throw new Error("Ran out of data")}a[c]=l[u];a[c+1]=l[u+1];a[c+2]=l[u+2];a[c+3]=255},function(a,l,c,u){if(u+3>=l.length){throw new Error("Ran out of data")}a[c]=l[u];a[c+1]=l[u+1];a[c+2]=l[u+2];a[c+3]=l[u+3]}];var s=[function(){},function(a,l,c,u){let d=l[0];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=u},function(a,l,c){let u=l[0];a[c]=u;a[c+1]=u;a[c+2]=u;a[c+3]=l[1]},function(a,l,c,u){a[c]=l[0];a[c+1]=l[1];a[c+2]=l[2];a[c+3]=u},function(a,l,c){a[c]=l[0];a[c+1]=l[1];a[c+2]=l[2];a[c+3]=l[3]}];function i(a,l){let c=[];let u=0;function d(){if(u===a.length){throw new Error("Ran out of data")}let h=a[u];u++;let p,m,y,g,f,w,v,b;switch(l){default:throw new Error("unrecognised depth");case 16:v=a[u];u++;c.push((h<<8)+v);break;case 4:v=h&15;b=h>>4;c.push(b,v);break;case 2:f=h&3;w=h>>2&3;v=h>>4&3;b=h>>6&3;c.push(b,v,w,f);break;case 1:p=h&1;m=h>>1&1;y=h>>2&1;g=h>>3&1;f=h>>4&1;w=h>>5&1;v=h>>6&1;b=h>>7&1;c.push(b,v,w,f,g,y,m,p);break}}return{get:function(h){while(c.length<h){d()}let p=c.slice(0,h);c=c.slice(h);return p},resetAfterLine:function(){c.length=0},end:function(){if(u!==a.length){throw new Error("extra data found")}}}}function r(a,l,c,u,d,h){let p=a.width;let m=a.height;let y=a.index;for(let g=0;g<m;g++){for(let f=0;f<p;f++){let w=c(f,g,y);t[u](l,d,w,h);h+=u}}return h}function o(a,l,c,u,d,h){let p=a.width;let m=a.height;let y=a.index;for(let g=0;g<m;g++){for(let f=0;f<p;f++){let w=d.get(u);let v=c(f,g,y);s[u](l,w,v,h)}d.resetAfterLine()}}e.dataToBitMap=function(a,l){let c=l.width;let u=l.height;let d=l.depth;let h=l.bpp;let p=l.interlace;let m;if(d!==8){m=i(a,d)}let y;if(d<=8){y=Buffer.alloc(c*u*4)}else{y=new Uint16Array(c*u*4)}let g=Math.pow(2,d)-1;let f=0;let w;let v;if(p){w=n.getImagePasses(c,u);v=n.getInterlaceIterator(c,u)}else{let b=0;v=function(){let k=b;b+=4;return k};w=[{width:c,height:u}]}for(let b=0;b<w.length;b++){if(d===8){f=r(w[b],y,v,h,a,f)}else{o(w[b],y,v,h,m,g)}}if(d===8){if(f!==a.length){throw new Error("extra data found")}}else{m.end()}return y}}});var require_format_normaliser=__commonJS({"../../node_modules/pngjs/lib/format-normaliser.js"(e,n){"use strict";function t(r,o,a,l,c){let u=0;for(let d=0;d<l;d++){for(let h=0;h<a;h++){let p=c[r[u]];if(!p){throw new Error("index "+r[u]+" not in palette")}for(let m=0;m<4;m++){o[u+m]=p[m]}u+=4}}}function s(r,o,a,l,c){let u=0;for(let d=0;d<l;d++){for(let h=0;h<a;h++){let p=false;if(c.length===1){if(c[0]===r[u]){p=true}}else if(c[0]===r[u]&&c[1]===r[u+1]&&c[2]===r[u+2]){p=true}if(p){for(let m=0;m<4;m++){o[u+m]=0}}u+=4}}}function i(r,o,a,l,c){let u=255;let d=Math.pow(2,c)-1;let h=0;for(let p=0;p<l;p++){for(let m=0;m<a;m++){for(let y=0;y<4;y++){o[h+y]=Math.floor(r[h+y]*u/d+.5)}h+=4}}}n.exports=function(r,o,a=false){let l=o.depth;let c=o.width;let u=o.height;let d=o.colorType;let h=o.transColor;let p=o.palette;let m=r;if(d===3){t(r,m,c,u,p)}else{if(h){s(r,m,c,u,h)}if(l!==8&&!a){if(l===16){m=Buffer.alloc(c*u*4)}i(r,m,c,u,l)}}return m}}});var require_parser_async=__commonJS({"../../node_modules/pngjs/lib/parser-async.js"(e,n){"use strict";var t=require("util");var s=require("zlib");var i=require_chunkstream();var r=require_filter_parse_async();var o=require_parser();var a=require_bitmapper();var l=require_format_normaliser();var c=n.exports=function(u){i.call(this);this._parser=new o(u,{read:this.read.bind(this),error:this._handleError.bind(this),metadata:this._handleMetaData.bind(this),gamma:this.emit.bind(this,"gamma"),palette:this._handlePalette.bind(this),transColor:this._handleTransColor.bind(this),finished:this._finished.bind(this),inflateData:this._inflateData.bind(this),simpleTransparency:this._simpleTransparency.bind(this),headersFinished:this._headersFinished.bind(this)});this._options=u;this.writable=true;this._parser.start()};t.inherits(c,i);c.prototype._handleError=function(u){this.emit("error",u);this.writable=false;this.destroy();if(this._inflate&&this._inflate.destroy){this._inflate.destroy()}if(this._filter){this._filter.destroy();this._filter.on("error",function(){})}this.errord=true};c.prototype._inflateData=function(u){if(!this._inflate){if(this._bitmapInfo.interlace){this._inflate=s.createInflate();this._inflate.on("error",this.emit.bind(this,"error"));this._filter.on("complete",this._complete.bind(this));this._inflate.pipe(this._filter)}else{let d=(this._bitmapInfo.width*this._bitmapInfo.bpp*this._bitmapInfo.depth+7>>3)+1;let h=d*this._bitmapInfo.height;let p=Math.max(h,s.Z_MIN_CHUNK);this._inflate=s.createInflate({chunkSize:p});let m=h;let y=this.emit.bind(this,"error");this._inflate.on("error",function(f){if(!m){return}y(f)});this._filter.on("complete",this._complete.bind(this));let g=this._filter.write.bind(this._filter);this._inflate.on("data",function(f){if(!m){return}if(f.length>m){f=f.slice(0,m)}m-=f.length;g(f)});this._inflate.on("end",this._filter.end.bind(this._filter))}}this._inflate.write(u)};c.prototype._handleMetaData=function(u){this._metaData=u;this._bitmapInfo=Object.create(u);this._filter=new r(this._bitmapInfo)};c.prototype._handleTransColor=function(u){this._bitmapInfo.transColor=u};c.prototype._handlePalette=function(u){this._bitmapInfo.palette=u};c.prototype._simpleTransparency=function(){this._metaData.alpha=true};c.prototype._headersFinished=function(){this.emit("metadata",this._metaData)};c.prototype._finished=function(){if(this.errord){return}if(!this._inflate){this.emit("error","No Inflate block")}else{this._inflate.end()}};c.prototype._complete=function(u){if(this.errord){return}let d;try{let h=a.dataToBitMap(u,this._bitmapInfo);d=l(h,this._bitmapInfo,this._options.skipRescale);h=null}catch(h){this._handleError(h);return}this.emit("parsed",d)}}});var require_bitpacker=__commonJS({"../../node_modules/pngjs/lib/bitpacker.js"(e,n){"use strict";var t=require_constants();n.exports=function(s,i,r,o){let a=[t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(o.colorType)!==-1;if(o.colorType===o.inputColorType){let f=(function(){let w=new ArrayBuffer(2);new DataView(w).setInt16(0,256,true);return new Int16Array(w)[0]!==256})();if(o.bitDepth===8||o.bitDepth===16&&f){return s}}let l=o.bitDepth!==16?s:new Uint16Array(s.buffer);let c=255;let u=t.COLORTYPE_TO_BPP_MAP[o.inputColorType];if(u===4&&!o.inputHasAlpha){u=3}let d=t.COLORTYPE_TO_BPP_MAP[o.colorType];if(o.bitDepth===16){c=65535;d*=2}let h=Buffer.alloc(i*r*d);let p=0;let m=0;let y=o.bgColor||{};if(y.red===void 0){y.red=c}if(y.green===void 0){y.green=c}if(y.blue===void 0){y.blue=c}function g(){let f;let w;let v;let b=c;switch(o.inputColorType){case t.COLORTYPE_COLOR_ALPHA:b=l[p+3];f=l[p];w=l[p+1];v=l[p+2];break;case t.COLORTYPE_COLOR:f=l[p];w=l[p+1];v=l[p+2];break;case t.COLORTYPE_ALPHA:b=l[p+1];f=l[p];w=f;v=f;break;case t.COLORTYPE_GRAYSCALE:f=l[p];w=f;v=f;break;default:throw new Error("input color type:"+o.inputColorType+" is not supported at present")}if(o.inputHasAlpha){if(!a){b/=c;f=Math.min(Math.max(Math.round((1-b)*y.red+b*f),0),c);w=Math.min(Math.max(Math.round((1-b)*y.green+b*w),0),c);v=Math.min(Math.max(Math.round((1-b)*y.blue+b*v),0),c)}}return{red:f,green:w,blue:v,alpha:b}}for(let f=0;f<r;f++){for(let w=0;w<i;w++){let v=g(l,p);switch(o.colorType){case t.COLORTYPE_COLOR_ALPHA:case t.COLORTYPE_COLOR:if(o.bitDepth===8){h[m]=v.red;h[m+1]=v.green;h[m+2]=v.blue;if(a){h[m+3]=v.alpha}}else{h.writeUInt16BE(v.red,m);h.writeUInt16BE(v.green,m+2);h.writeUInt16BE(v.blue,m+4);if(a){h.writeUInt16BE(v.alpha,m+6)}}break;case t.COLORTYPE_ALPHA:case t.COLORTYPE_GRAYSCALE:{let b=(v.red+v.green+v.blue)/3;if(o.bitDepth===8){h[m]=b;if(a){h[m+1]=v.alpha}}else{h.writeUInt16BE(b,m);if(a){h.writeUInt16BE(v.alpha,m+2)}}break}default:throw new Error("unrecognised color Type "+o.colorType)}p+=u;m+=d}}return h}}});var require_filter_pack=__commonJS({"../../node_modules/pngjs/lib/filter-pack.js"(e,n){"use strict";var t=require_paeth_predictor();function s(y,g,f,w,v){for(let b=0;b<f;b++){w[v+b]=y[g+b]}}function i(y,g,f){let w=0;let v=g+f;for(let b=g;b<v;b++){w+=Math.abs(y[b])}return w}function r(y,g,f,w,v,b){for(let k=0;k<f;k++){let $=k>=b?y[g+k-b]:0;let _=y[g+k]-$;w[v+k]=_}}function o(y,g,f,w){let v=0;for(let b=0;b<f;b++){let k=b>=w?y[g+b-w]:0;let $=y[g+b]-k;v+=Math.abs($)}return v}function a(y,g,f,w,v){for(let b=0;b<f;b++){let k=g>0?y[g+b-f]:0;let $=y[g+b]-k;w[v+b]=$}}function l(y,g,f){let w=0;let v=g+f;for(let b=g;b<v;b++){let k=g>0?y[b-f]:0;let $=y[b]-k;w+=Math.abs($)}return w}function c(y,g,f,w,v,b){for(let k=0;k<f;k++){let $=k>=b?y[g+k-b]:0;let _=g>0?y[g+k-f]:0;let S=y[g+k]-($+_>>1);w[v+k]=S}}function u(y,g,f,w){let v=0;for(let b=0;b<f;b++){let k=b>=w?y[g+b-w]:0;let $=g>0?y[g+b-f]:0;let _=y[g+b]-(k+$>>1);v+=Math.abs(_)}return v}function d(y,g,f,w,v,b){for(let k=0;k<f;k++){let $=k>=b?y[g+k-b]:0;let _=g>0?y[g+k-f]:0;let S=g>0&&k>=b?y[g+k-(f+b)]:0;let E=y[g+k]-t($,_,S);w[v+k]=E}}function h(y,g,f,w){let v=0;for(let b=0;b<f;b++){let k=b>=w?y[g+b-w]:0;let $=g>0?y[g+b-f]:0;let _=g>0&&b>=w?y[g+b-(f+w)]:0;let S=y[g+b]-t(k,$,_);v+=Math.abs(S)}return v}var p={0:s,1:r,2:a,3:c,4:d};var m={0:i,1:o,2:l,3:u,4:h};n.exports=function(y,g,f,w,v){let b;if(!("filterType"in w)||w.filterType===-1){b=[0,1,2,3,4]}else if(typeof w.filterType==="number"){b=[w.filterType]}else{throw new Error("unrecognised filter types")}if(w.bitDepth===16){v*=2}let k=g*v;let $=0;let _=0;let S=Buffer.alloc((k+1)*f);let E=b[0];for(let A=0;A<f;A++){if(b.length>1){let P=Infinity;for(let x=0;x<b.length;x++){let T=m[b[x]](y,_,k,v);if(T<P){E=b[x];P=T}}}S[$]=E;$++;p[E](y,_,k,S,$,v);$+=k;_+=k}return S}}});var require_packer=__commonJS({"../../node_modules/pngjs/lib/packer.js"(e,n){"use strict";var t=require_constants();var s=require_crc();var i=require_bitpacker();var r=require_filter_pack();var o=require("zlib");var a=n.exports=function(l){this._options=l;l.deflateChunkSize=l.deflateChunkSize||32*1024;l.deflateLevel=l.deflateLevel!=null?l.deflateLevel:9;l.deflateStrategy=l.deflateStrategy!=null?l.deflateStrategy:3;l.inputHasAlpha=l.inputHasAlpha!=null?l.inputHasAlpha:true;l.deflateFactory=l.deflateFactory||o.createDeflate;l.bitDepth=l.bitDepth||8;l.colorType=typeof l.colorType==="number"?l.colorType:t.COLORTYPE_COLOR_ALPHA;l.inputColorType=typeof l.inputColorType==="number"?l.inputColorType:t.COLORTYPE_COLOR_ALPHA;if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(l.colorType)===-1){throw new Error("option color type:"+l.colorType+" is not supported at present")}if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(l.inputColorType)===-1){throw new Error("option input color type:"+l.inputColorType+" is not supported at present")}if(l.bitDepth!==8&&l.bitDepth!==16){throw new Error("option bit depth:"+l.bitDepth+" is not supported at present")}};a.prototype.getDeflateOptions=function(){return{chunkSize:this._options.deflateChunkSize,level:this._options.deflateLevel,strategy:this._options.deflateStrategy}};a.prototype.createDeflate=function(){return this._options.deflateFactory(this.getDeflateOptions())};a.prototype.filterData=function(l,c,u){let d=i(l,c,u,this._options);let h=t.COLORTYPE_TO_BPP_MAP[this._options.colorType];let p=r(d,c,u,this._options,h);return p};a.prototype._packChunk=function(l,c){let u=c?c.length:0;let d=Buffer.alloc(u+12);d.writeUInt32BE(u,0);d.writeUInt32BE(l,4);if(c){c.copy(d,8)}d.writeInt32BE(s.crc32(d.slice(4,d.length-4)),d.length-4);return d};a.prototype.packGAMA=function(l){let c=Buffer.alloc(4);c.writeUInt32BE(Math.floor(l*t.GAMMA_DIVISION),0);return this._packChunk(t.TYPE_gAMA,c)};a.prototype.packIHDR=function(l,c){let u=Buffer.alloc(13);u.writeUInt32BE(l,0);u.writeUInt32BE(c,4);u[8]=this._options.bitDepth;u[9]=this._options.colorType;u[10]=0;u[11]=0;u[12]=0;return this._packChunk(t.TYPE_IHDR,u)};a.prototype.packIDAT=function(l){return this._packChunk(t.TYPE_IDAT,l)};a.prototype.packIEND=function(){return this._packChunk(t.TYPE_IEND,null)}}});var require_packer_async=__commonJS({"../../node_modules/pngjs/lib/packer-async.js"(e,n){"use strict";var t=require("util");var s=require("stream");var i=require_constants();var r=require_packer();var o=n.exports=function(a){s.call(this);let l=a||{};this._packer=new r(l);this._deflate=this._packer.createDeflate();this.readable=true};t.inherits(o,s);o.prototype.pack=function(a,l,c,u){this.emit("data",Buffer.from(i.PNG_SIGNATURE));this.emit("data",this._packer.packIHDR(l,c));if(u){this.emit("data",this._packer.packGAMA(u))}let d=this._packer.filterData(a,l,c);this._deflate.on("error",this.emit.bind(this,"error"));this._deflate.on("data",function(h){this.emit("data",this._packer.packIDAT(h))}.bind(this));this._deflate.on("end",function(){this.emit("data",this._packer.packIEND());this.emit("end")}.bind(this));this._deflate.end(d)}}});var require_sync_inflate=__commonJS({"../../node_modules/pngjs/lib/sync-inflate.js"(e,n){"use strict";var t=require("assert").ok;var s=require("zlib");var i=require("util");var r=require("buffer").kMaxLength;function o(d){if(!(this instanceof o)){return new o(d)}if(d&&d.chunkSize<s.Z_MIN_CHUNK){d.chunkSize=s.Z_MIN_CHUNK}s.Inflate.call(this,d);this._offset=this._offset===void 0?this._outOffset:this._offset;this._buffer=this._buffer||this._outBuffer;if(d&&d.maxLength!=null){this._maxLength=d.maxLength}}function a(d){return new o(d)}function l(d,h){if(h){process.nextTick(h)}if(!d._handle){return}d._handle.close();d._handle=null}o.prototype._processChunk=function(d,h,p){if(typeof p==="function"){return s.Inflate._processChunk.call(this,d,h,p)}let m=this;let y=d&&d.length;let g=this._chunkSize-this._offset;let f=this._maxLength;let w=0;let v=[];let b=0;let k;this.on("error",function(E){k=E});function $(E,A){if(m._hadError){return}let P=g-A;t(P>=0,"have should not go down");if(P>0){let x=m._buffer.slice(m._offset,m._offset+P);m._offset+=P;if(x.length>f){x=x.slice(0,f)}v.push(x);b+=x.length;f-=x.length;if(f===0){return false}}if(A===0||m._offset>=m._chunkSize){g=m._chunkSize;m._offset=0;m._buffer=Buffer.allocUnsafe(m._chunkSize)}if(A===0){w+=y-E;y=E;return true}return false}t(this._handle,"zlib binding closed");let _;do{_=this._handle.writeSync(h,d,w,y,this._buffer,this._offset,g);_=_||this._writeState}while(!this._hadError&&$(_[0],_[1]));if(this._hadError){throw k}if(b>=r){l(this);throw new RangeError("Cannot create final Buffer. It would be larger than 0x"+r.toString(16)+" bytes")}let S=Buffer.concat(v,b);l(this);return S};i.inherits(o,s.Inflate);function c(d,h){if(typeof h==="string"){h=Buffer.from(h)}if(!(h instanceof Buffer)){throw new TypeError("Not a string or buffer")}let p=d._finishFlushFlag;if(p==null){p=s.Z_FINISH}return d._processChunk(h,p)}function u(d,h){return c(new o(h),d)}n.exports=e=u;e.Inflate=o;e.createInflate=a;e.inflateSync=u}});var require_sync_reader=__commonJS({"../../node_modules/pngjs/lib/sync-reader.js"(e,n){"use strict";var t=n.exports=function(s){this._buffer=s;this._reads=[]};t.prototype.read=function(s,i){this._reads.push({length:Math.abs(s),allowLess:s<0,func:i})};t.prototype.process=function(){while(this._reads.length>0&&this._buffer.length){let s=this._reads[0];if(this._buffer.length&&(this._buffer.length>=s.length||s.allowLess)){this._reads.shift();let i=this._buffer;this._buffer=i.slice(s.length);s.func.call(this,i.slice(0,s.length))}else{break}}if(this._reads.length>0){throw new Error("There are some read requests waitng on finished stream")}if(this._buffer.length>0){throw new Error("unrecognised content at end of stream")}}}});var require_filter_parse_sync=__commonJS({"../../node_modules/pngjs/lib/filter-parse-sync.js"(e){"use strict";var n=require_sync_reader();var t=require_filter_parse();e.process=function(s,i){let r=[];let o=new n(s);let a=new t(i,{read:o.read.bind(o),write:function(l){r.push(l)},complete:function(){}});a.start();o.process();return Buffer.concat(r)}}});var require_parser_sync=__commonJS({"../../node_modules/pngjs/lib/parser-sync.js"(e,n){"use strict";var t=true;var s=require("zlib");var i=require_sync_inflate();if(!s.deflateSync){t=false}var r=require_sync_reader();var o=require_filter_parse_sync();var a=require_parser();var l=require_bitmapper();var c=require_format_normaliser();n.exports=function(u,d){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let h;function p(I){h=I}let m;function y(I){m=I}function g(I){m.transColor=I}function f(I){m.palette=I}function w(){m.alpha=true}let v;function b(I){v=I}let k=[];function $(I){k.push(I)}let _=new r(u);let S=new a(d,{read:_.read.bind(_),error:p,metadata:y,gamma:b,palette:f,transColor:g,inflateData:$,simpleTransparency:w});S.start();_.process();if(h){throw h}let E=Buffer.concat(k);k.length=0;let A;if(m.interlace){A=s.inflateSync(E)}else{let I=(m.width*m.bpp*m.depth+7>>3)+1;let O=I*m.height;A=i(E,{chunkSize:O,maxLength:O})}E=null;if(!A||!A.length){throw new Error("bad png - invalid inflate data response")}let P=o.process(A,m);E=null;let x=l.dataToBitMap(P,m);P=null;let T=c(x,m,d.skipRescale);m.data=T;m.gamma=v||0;return m}}});var require_packer_sync=__commonJS({"../../node_modules/pngjs/lib/packer-sync.js"(e,n){"use strict";var t=true;var s=require("zlib");if(!s.deflateSync){t=false}var i=require_constants();var r=require_packer();n.exports=function(o,a){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let l=a||{};let c=new r(l);let u=[];u.push(Buffer.from(i.PNG_SIGNATURE));u.push(c.packIHDR(o.width,o.height));if(o.gamma){u.push(c.packGAMA(o.gamma))}let d=c.filterData(o.data,o.width,o.height);let h=s.deflateSync(d,c.getDeflateOptions());d=null;if(!h||!h.length){throw new Error("bad png - invalid compressed data response")}u.push(c.packIDAT(h));u.push(c.packIEND());return Buffer.concat(u)}}});var require_png_sync=__commonJS({"../../node_modules/pngjs/lib/png-sync.js"(e){"use strict";var n=require_parser_sync();var t=require_packer_sync();e.read=function(s,i){return n(s,i||{})};e.write=function(s,i){return t(s,i)}}});var require_png=__commonJS({"../../node_modules/pngjs/lib/png.js"(e){"use strict";var n=require("util");var t=require("stream");var s=require_parser_async();var i=require_packer_async();var r=require_png_sync();var o=e.PNG=function(a){t.call(this);a=a||{};this.width=a.width|0;this.height=a.height|0;this.data=this.width>0&&this.height>0?Buffer.alloc(4*this.width*this.height):null;if(a.fill&&this.data){this.data.fill(0)}this.gamma=0;this.readable=this.writable=true;this._parser=new s(a);this._parser.on("error",this.emit.bind(this,"error"));this._parser.on("close",this._handleClose.bind(this));this._parser.on("metadata",this._metadata.bind(this));this._parser.on("gamma",this._gamma.bind(this));this._parser.on("parsed",function(l){this.data=l;this.emit("parsed",l)}.bind(this));this._packer=new i(a);this._packer.on("data",this.emit.bind(this,"data"));this._packer.on("end",this.emit.bind(this,"end"));this._parser.on("close",this._handleClose.bind(this));this._packer.on("error",this.emit.bind(this,"error"))};n.inherits(o,t);o.sync=r;o.prototype.pack=function(){if(!this.data||!this.data.length){this.emit("error","No data provided");return this}process.nextTick(function(){this._packer.pack(this.data,this.width,this.height,this.gamma)}.bind(this));return this};o.prototype.parse=function(a,l){if(l){let c,u;c=function(d){this.removeListener("error",u);this.data=d;l(null,this)}.bind(this);u=function(d){this.removeListener("parsed",c);l(d,null)}.bind(this);this.once("parsed",c);this.once("error",u)}this.end(a);return this};o.prototype.write=function(a){this._parser.write(a);return true};o.prototype.end=function(a){this._parser.end(a)};o.prototype._metadata=function(a){this.width=a.width;this.height=a.height;this.emit("metadata",a)};o.prototype._gamma=function(a){this.gamma=a};o.prototype._handleClose=function(){if(!this._parser.writable&&!this._packer.readable){this.emit("close")}};o.bitblt=function(a,l,c,u,d,h,p,m){c|=0;u|=0;d|=0;h|=0;p|=0;m|=0;if(c>a.width||u>a.height||c+d>a.width||u+h>a.height){throw new Error("bitblt reading outside image")}if(p>l.width||m>l.height||p+d>l.width||m+h>l.height){throw new Error("bitblt writing outside image")}for(let y=0;y<h;y++){a.data.copy(l.data,(m+y)*l.width+p<<2,(u+y)*a.width+c<<2,(u+y)*a.width+c+d<<2)}};o.prototype.bitblt=function(a,l,c,u,d,h,p){o.bitblt(this,a,l,c,u,d,h,p);return this};o.adjustGamma=function(a){if(a.gamma){for(let l=0;l<a.height;l++){for(let c=0;c<a.width;c++){let u=a.width*l+c<<2;for(let d=0;d<3;d++){let h=a.data[u+d]/255;h=Math.pow(h,1/2.2/a.gamma);a.data[u+d]=Math.round(h*255)}}}a.gamma=0}};o.prototype.adjustGamma=function(){o.adjustGamma(this)}}});async function devicectl(e,n=3e4){const t=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-devicectl-${(0,import_node_crypto.randomUUID)()}.json`);try{await execFileAsync2("xcrun",["devicectl",...e,"--quiet","--json-output",t],{timeout:n,maxBuffer:16*1024*1024});return JSON.parse(await import_node_fs.promises.readFile(t,"utf8"))}catch(s){const i=s;let r=(i.stderr||i.stdout||i.message||"").trim();try{const o=JSON.parse(await import_node_fs.promises.readFile(t,"utf8"));r=o.error?.userInfo?.NSLocalizedDescription?.string??r}catch{}if(/locked/i.test(r))throw new Error("The iPhone is locked. Unlock it and try again.");throw new Error(`devicectl ${e.slice(0,2).join(" ")} failed: ${r.split("\n").slice(-3).join(" ").slice(0,300)}`)}finally{await import_node_fs.promises.unlink(t).catch(()=>void 0)}}async function physicalPhones(){if(process.platform!=="darwin")return[];return parsePhones(await devicectl(["list","devices"],15e3).catch(()=>null))}function parsePhones(e){const n=e;return(n?.result?.devices??[]).filter(t=>t.hardwareProperties?.reality==="physical"&&t.hardwareProperties?.platform==="iOS"&&t.connectionProperties?.pairingState==="paired"&&t.connectionProperties?.tunnelState!=="unavailable").map(t=>({udid:t.hardwareProperties.udid,name:t.deviceProperties?.name??"iPhone"})).filter(t=>!!t.udid)}async function developerApps(e){const n=await devicectl(["device","info","apps","--device",e]);return(n.result?.apps??[]).filter(t=>t.bundleIdentifier&&!t.bundleIdentifier.startsWith("com.apple.")&&t.builtByDeveloper!==false).map(t=>({bundleId:t.bundleIdentifier,name:t.name??t.bundleIdentifier,path:t.url?.replace(/^file:\/\//,"")}))}async function readAppFile(e,n,t){const s=await devicectl(["device","info","files","--device",e,"--domain-type","appDataContainer","--domain-identifier",n],2e4);const i=new Set((s.result?.files??[]).map(o=>o.relativePath??o.name??""));if(!i.has(t))return null;const r=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-devfile-${(0,import_node_crypto.randomUUID)()}`);try{await devicectl(["device","copy","from","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",r],2e4);return await import_node_fs.promises.readFile(r)}finally{await import_node_fs.promises.rm(r,{recursive:true,force:true}).catch(()=>void 0)}}async function phoneInstallId(e,n){const t=await readAppFile(e,n,"Documents/react-buoy/@react_buoy_sync_device_id.json").catch(()=>null);const s=t?.toString("utf8").trim().replace(/^"|"$/g,"")??"";return/^[\w-]{4,64}$/.test(s)?s:null}async function phoneScreenshot(e){const n=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-phone-shot-${(0,import_node_crypto.randomUUID)()}.png`);try{await devicectl(["device","capture","screenshot","--device",e,"--destination",n],3e4);return await import_node_fs.promises.readFile(n)}finally{await import_node_fs.promises.unlink(n).catch(()=>void 0)}}function isAllBlack(e){try{const n=import_pngjs.PNG.sync.read(e);const t=Math.max(1,Math.floor(n.width*n.height/4e3));for(let s=0;s<n.width*n.height;s+=t){const i=s*4;if(n.data[i]>12||n.data[i+1]>12||n.data[i+2]>12)return false}return true}catch{return false}}async function phoneLaunch(e,n,t={}){await devicectl(["device","process","launch","--device",e,...t.terminateExisting?["--terminate-existing"]:[],...t.url?["--payload-url",t.url]:[],n])}async function phoneOpenUrl(e,n){await devicectl(["device","process","openURL","--device",e,n])}async function phoneQuit(e,n){const t=await developerApps(e);const s=t.find(o=>o.bundleId===n)?.path;const i=await devicectl(["device","info","processes","--device",e]);const r=(i.result?.runningProcesses??[]).find(o=>s&&decodeURIComponent(o.executable??"").replace(/^file:\/\//,"").startsWith(s.replace(/\/$/,"")));if(!r?.processIdentifier)return false;await devicectl(["device","process","terminate","--device",e,"--pid",String(r.processIdentifier)]);return true}async function phoneUninstall(e,n){await devicectl(["device","uninstall","app","--device",e,n],12e4)}async function phoneInstall(e,n){await devicectl(["device","install","app","--device",e,n],3e5)}async function writeAppFile(e,n,t,s){await devicectl(["device","copy","to","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",s],3e4)}function phoneRecordArgs(e,n,t){return["devicectl","device","capture","screen-record","--device",e,"--destination",n,"--duration",String(t)]}async function coreDeviceId(e){const n=coreIds.get(e);if(n)return n;const t=await devicectl(["list","devices"]);const s=t.result?.devices?.find(i=>i.hardwareProperties?.udid===e)?.identifier;if(!s)throw new Error("CoreDevice doesn't list this iPhone right now. Check that it is connected and unlocked.");coreIds.set(e,s);return s}var import_node_child_process3,import_node_util2,import_node_fs,import_node_os,import_node_path,import_node_crypto,import_pngjs,execFileAsync2,BLACK_PHONE_NOTE,coreIds;var init_iosDevice=__esm({"src/capture/iosDevice.ts"(){"use strict";import_node_child_process3=require("child_process");import_node_util2=require("util");import_node_fs=require("fs");import_node_os=__toESM(require("os"));import_node_path=__toESM(require("path"));import_node_crypto=require("crypto");import_pngjs=__toESM(require_png());execFileAsync2=(0,import_node_util2.promisify)(import_node_child_process3.execFile);BLACK_PHONE_NOTE="The picture is all black. If the app isn't really showing black, the iPhone's own screen is off: it is asleep, or the Mac shows it in iPhone Mirroring (the phone stays locked then). Quit iPhone Mirroring, unlock the phone and keep its screen on, then try again.";coreIds=new Map}});async function startRecording(e,n){await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(n),{recursive:true});const t=Date.now();let s;let i;if(e.platform==="ios"){s=(0,import_node_child_process4.spawn)("xcrun",["simctl","io",e.udid,"recordVideo","--codec=h264","--force",n],{stdio:["ignore","ignore","pipe"]})}else if(e.platform==="ios-device"){s=(0,import_node_child_process4.spawn)("xcrun",phoneRecordArgs(e.udid,n,MAX_RECORDING_MS/1e3),{stdio:["ignore","ignore","pipe"]})}else{i=`/sdcard/buoy-recording-${t}-${Math.random().toString(36).slice(2,8)}.mp4`;s=(0,import_node_child_process4.spawn)("adb",["-s",e.serial,"shell",`echo $$; exec screenrecord --time-limit ${MAX_RECORDING_MS/1e3} ${q(i)}`],{stdio:["ignore","pipe","pipe"]})}const r={host:e,file:n,startedAt:t,proc:s,remote:i};live.add(r);let o="";let a=t;let l;s.stderr?.on("data",d=>{o+=String(d);if(/Recording started/i.test(String(d)))a=Date.now()});s.stdout?.on("data",d=>{const h=/^(\d+)/m.exec(String(d));if(h&&l===void 0)l=Number(h[1])});const c=await new Promise(d=>{const h=setTimeout(()=>d(null),e.platform==="ios"?3e3:e.platform==="ios-device"?4e3:1200);const p=setInterval(()=>{if(e.platform==="ios"&&a!==t||e.platform==="android"&&l!==void 0){clearTimeout(h);clearInterval(p);d(null)}},50);s.once("exit",m=>{clearTimeout(h);clearInterval(p);d(m??-1)});s.once("error",()=>{clearTimeout(h);clearInterval(p);d(-1)})});live.delete(r);if(c!==null){if(/not supported by this device/i.test(o)){throw new Error("This iPhone can't record its screen through Xcode (devicectl says the device doesn't support it). Record on a simulator, or use the iPhone's own Screen Recording in Control Center.")}throw new Error(`The screen recorder stopped right away: ${o.trim().slice(0,300)||`exit ${c}`}`)}if(e.platform==="ios"&&s.pid){const d=s.pid;const h=(0,import_node_child_process4.spawn)("sh",["-c",`sleep ${MAX_RECORDING_MS/1e3+15}; ps -p ${d} -o command= | grep -q 'simctl io .* recordVideo' && kill -INT ${d}`],{detached:true,stdio:"ignore"});h.on("error",()=>void 0);h.unref();s.once("exit",()=>{try{if(h.pid)process.kill(-h.pid,"SIGTERM")}catch{h.kill("SIGTERM")}})}const u={host:e,file:n,startedAt:a,proc:s,remote:i,remotePid:l};live.add(u);return u}async function stopRecording(e){if(e.stopTimer)clearTimeout(e.stopTimer);const n=Date.now();const t=new Promise(r=>{if(e.proc.exitCode!==null||e.proc.signalCode!==null)return r();e.proc.once("exit",()=>r())});if(e.host.platform==="ios"||e.host.platform==="ios-device"){e.proc.kill("SIGINT")}else if(e.remotePid){await adbShell(e.host.serial,`kill -INT ${e.remotePid}`).catch(()=>void 0)}await Promise.race([t,new Promise(r=>setTimeout(r,e.host.platform==="ios-device"?6e4:1e4))]);if(e.proc.exitCode===null&&e.proc.signalCode===null)e.proc.kill("SIGKILL");live.delete(e);if(e.host.platform==="android"&&e.remote){await new Promise(r=>setTimeout(r,800));await adb(e.host.serial,["pull",e.remote,e.file],6e4);await adbShell(e.host.serial,`rm -f ${q(e.remote)}`).catch(()=>void 0)}const s=await import_node_fs2.promises.stat(e.file).catch(()=>null);if(!s||s.size<1e3)throw new Error("The recording file is missing or empty. Was the screen showing anything?");const i=n-e.startedAt;if(e.host.platform==="ios"||e.host.platform==="ios-device")await padToWallTime(e.file,i).catch(()=>void 0);return i}async function padToWallTime(e,n){const t=(await execFileAsync3("ffprobe",["-v","error","-show_entries","format=duration","-of","csv=p=0",e],{timeout:2e4})).stdout;const s=Number(t.trim());const i=n/1e3-.3-s;if(!Number.isFinite(s)||i<.25)return;const r=e.replace(/\.mp4$/,".pad.mp4");const o=a=>execFileAsync3("ffmpeg",["-y","-v","error","-i",e,"-vf",`tpad=stop_mode=clone:stop_duration=${i.toFixed(2)},fps=30`,"-an",...a,"-pix_fmt","yuv420p","-movflags","+faststart",r],{timeout:24e4,maxBuffer:4*1024*1024});try{await o(["-c:v","h264_videotoolbox","-b:v","6M"])}catch{await o(["-c:v","libx264","-preset","veryfast","-crf","22"])}await import_node_fs2.promises.rename(r,e)}function stopAllRecordingsNow(){for(const e of live){try{if(e.host.platform!=="android")e.proc.kill("SIGINT");else if(e.remotePid)(0,import_node_child_process4.spawn)("adb",["-s",e.host.serial,"shell",`kill -INT ${e.remotePid}`],{stdio:"ignore",detached:true}).unref()}catch{}}live.clear()}function markPoint(e,n,t,s){const i=n.width!==n.height&&t.width!==t.height&&n.width>n.height!==t.width>t.height;if(!i)return e;const r=s==="landscapeRight";const o=n.width>n.height?!r:r;return o?[1-e[1],e[0]]:[e[1],1-e[0]]}function pinchPath(e){if(e.kind!=="pinch"||e.from.length!==2||e.to?.length!==2)return null;const{width:n,height:t}=e.screen;const s=h=>[h[0]*n,h[1]*t];const[i,r,o,a]=[s(e.from[0]),s(e.from[1]),s(e.to[0]),s(e.to[1])];const l=[(i[0]+r[0])/2,(i[1]+r[1])/2];const c=[(o[0]+a[0])/2,(o[1]+a[1])/2];const u=Math.atan2(i[1]-l[1],i[0]-l[0]);let d=Math.atan2(o[1]-c[1],o[0]-c[0])-u;if(d>Math.PI)d-=2*Math.PI;if(d<-Math.PI)d+=2*Math.PI;return{c0:l,c1:c,r0:Math.hypot(i[0]-l[0],i[1]-l[1]),r1:Math.hypot(o[0]-c[0],o[1]-c[1]),a0:u,turn:d}}function touchDotNote(e){const n=Math.max(0,e.reduce((t,s)=>t+s.from.length,0)-MAX_BURN_DOTS);return n?`${n} touch dots were left out of the MP4. The viewer keeps all dots.`:null}async function burnTouchMarks(e,n,t){let s;try{s=(await execFileAsync3("ffprobe",["-v","error","-select_streams","v:0","-show_entries","stream=width,height","-of","csv=p=0:s=x",e],{timeout:2e4})).stdout}catch(y){if(y.code==="ENOENT")return null;throw y}const[i,r]=s.trim().split("x").map(Number);if(!i||!r)throw new Error("could not read the video's size");const o={width:i,height:r};const a=Math.max(8,Math.round(Math.min(i,r)*.045));const l=a*2;const c=Math.max(2,Math.round(a*.18));const u=y=>y.toFixed(3);const d=[];for(const y of n){const g=y.t/1e3;const f=Math.max(y.ms,1)/1e3;const w=`clip((t-${u(g)})/${u(f)},0,1)`;const v=`enable='between(t,${u(g)},${u(g+f+.35)})':eval=frame:shortest=1`;const b=pinchPath(y);if(b){const{width:k,height:$}=y.screen;const _=markPoint([0,0],y.screen,o,y.orientation);const S=markPoint([1,0],y.screen,o,y.orientation);const E=markPoint([0,1],y.screen,o,y.orientation);[1,-1].forEach(A=>{const P=`(${u(b.r0)}+(${u(b.r1-b.r0)})*${w})`;const x=`(${u(b.a0)}+(${u(b.turn)})*${w})`;const T=`clip((${u(b.c0[0])}+(${u(b.c1[0]-b.c0[0])})*${w}+${A}*${P}*cos(${x}))/${u(k)},0,1)`;const I=`clip((${u(b.c0[1])}+(${u(b.c1[1]-b.c0[1])})*${w}+${A}*${P}*sin(${x}))/${u($)},0,1)`;const[O,N]=[0,1].map(F=>`(${_[F]}+(${S[F]-_[F]})*${T}+(${E[F]-_[F]})*${I})`);d.push(`overlay=x='${O}*${i}-${a}':y='${N}*${r}-${a}':${v}`)});continue}y.from.forEach((k,$)=>{const[_,S]=markPoint(k,y.screen,o,y.orientation);const[E,A]=markPoint(y.to?.[$]??k,y.screen,o,y.orientation);d.push(`overlay=x='${u(_*i-a)}+(${u((E-_)*i)})*${w}':y='${u(S*r-a)}+(${u((A-S)*r)})*${w}':${v}`)})}const h=d.slice(0,MAX_BURN_DOTS);const p=["[0:v]fps=30[base]",`[1:v]split=${h.length}${h.map((y,g)=>`[c${g}]`).join("")}`,...h.map((y,g)=>`${g===0?"[base]":`[v${g-1}]`}[c${g}]${y}[v${g}]`)].join(";");const m=`color=c=white@0:s=${l}x${l},format=rgba,geq=r='255':g='255':b='255':a='if(lte(hypot(X-${a},Y-${a}),${a}),if(gte(hypot(X-${a},Y-${a}),${a-c}),235,105),0)'`;await execFileAsync3("ffmpeg",["-y","-v","error","-i",e,"-f","lavfi","-i",m,"-filter_complex",p,"-map",`[v${h.length-1}]`,"-an","-c:v","libx264","-preset","veryfast","-crf","22","-pix_fmt","yuv420p","-movflags","+faststart",t],{timeout:24e4,maxBuffer:4*1024*1024});return t}function redact(e){return e.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,"$1 [redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,"[redacted jwt]").replace(/((?:api[_-]?key|token|secret|password|passwd|authorization|cookie|session)["']?\s*[:=]\s*["']?)([^\s"'&,;}]{4,})/gi,"$1[redacted]")}function viewerHtml(e){const n=(e.marks??[]).map(s=>({...s,arc:pinchPath(s)??void 0}));const t=JSON.stringify({...e,marks:n}).replace(/</g,"\\u003c");return`<!doctype html>
|
|
2
|
+
"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(e,n)=>function t(){return e&&(n=(0,e[__getOwnPropNames(e)[0]])(e=0)),n};var __commonJS=(e,n)=>function t(){return n||(0,e[__getOwnPropNames(e)[0]])((n={exports:{}}).exports,n),n.exports};var __export=(e,n)=>{for(var t in n)__defProp(e,t,{get:n[t],enumerable:true})};var __copyProps=(e,n,t,s)=>{if(n&&typeof n==="object"||typeof n==="function"){for(let i of __getOwnPropNames(n))if(!__hasOwnProp.call(e,i)&&i!==t)__defProp(e,i,{get:()=>n[i],enumerable:!(s=__getOwnPropDesc(n,i))||s.enumerable})}return e};var __toESM=(e,n,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(n||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));function stopAllHidStreamsNow(){for(const e of workers.values())e.child.kill("SIGKILL")}function runHidStream(e,n,t,s=""){const i=n;const r=(queues.get(i)??Promise.resolve()).catch(()=>void 0).then(()=>send(i,e,t,s));queues.set(i,r);void r.finally(()=>{if(queues.get(i)===r)queues.delete(i)}).catch(()=>void 0);return r}async function send(e,n,t,s){let i=workers.get(e);if(i&&i.bin!==n)i.stop(new Error("The touch helper build changed."));if(i?.stopped){await i.exited;i=void 0}if(!i){const o=(0,import_node_child_process.spawn)(n,["--stream"],{stdio:["pipe","pipe","pipe"]});let a;const l=new Promise(d=>{a=d});i={bin:n,child:o,buffer:"",stopped:false,exited:l,stop:()=>{}};if(workers.size===0)process.once("exit",stopAllHidStreamsNow);workers.set(e,i);const c=i;const u=d=>{if(c.stopped)return;c.stopped=true;if(c.idle)clearTimeout(c.idle);const h=c.pending;c.pending=void 0;if(h)clearTimeout(h.timer);o.ref();o.kill("SIGKILL");void l.then(()=>{if(workers.get(e)===c)workers.delete(e);if(workers.size===0)process.removeListener("exit",stopAllHidStreamsNow);h?.reject(d)})};c.stop=u;o.on("error",d=>{a();u(d)});o.on("exit",()=>{a();u(new Error("The touch helper closed. Try the next touch."))});o.stdin.on("error",d=>u(d));o.stdout.on("data",d=>{c.buffer+=d;let h;while((h=c.buffer.indexOf("\n"))>=0){const p=c.buffer.slice(0,h);c.buffer=c.buffer.slice(h+1);const m=c.pending;if(!m)continue;try{const y=JSON.parse(p);if(!y||typeof y.ok!=="boolean")throw new Error("Bad reply.");if(y.ok===true){clearTimeout(m.timer);c.pending=void 0;m.resolve()}else u(new Error(y.error??"The touch failed."))}catch{u(new Error("The touch helper sent a bad reply."))}}});o.stderr.resume();o.unref();for(const d of[o.stdin,o.stdout,o.stderr])d.unref?.()}const r=i;if(r.idle)clearTimeout(r.idle);return new Promise((o,a)=>{const l=setTimeout(()=>{r.stop(new Error("The touch helper timed out."))},45e3);r.pending={resolve:o,reject:a,timer:l};r.child.stdin.write(JSON.stringify({args:t,input:s})+"\n")}).finally(()=>{if(r.stopped)return;r.idle=setTimeout(()=>{r.child.stdin.end();r.stop(new Error("The touch helper went idle."))},1e4);r.idle.unref()})}var import_node_child_process,workers,queues;var init_hidStream=__esm({"src/capture/hidStream.ts"(){"use strict";import_node_child_process=require("child_process");workers=new Map;queues=new Map}});function setRecordingProbe(e){recordingActive=e}function hasSecrets(e){PLACEHOLDER.lastIndex=0;return PLACEHOLDER.test(e)}function fillSecrets(e,n=process.env){if(!hasSecrets(e))return e;if(recordingActive()){throw new Error("A screen recording is running, and a video can't hide a typed secret. Stop the recording first (screen_recording stop), then type it.")}const t=[];const s=[];const i=e.replace(PLACEHOLDER,(r,o)=>{const a=n[`BUOY_SECRET_${o}`]??n[o];if(a===void 0||a===""){t.push(o);return""}if(a.length<MIN_SECRET_LENGTH){s.push(o);return""}used.set(o,a);return a});if(t.length){throw new Error(`No value for ${t.map(r=>`{{secret:${r}}}`).join(", ")}. Set ${t.map(r=>`BUOY_SECRET_${r}`).join(", ")} in the environment the MCP server runs in (for example in your editor's MCP config "env").`)}if(s.length){throw new Error(`${s.map(r=>`{{secret:${r}}}`).join(", ")} is shorter than ${MIN_SECRET_LENGTH} characters, so Buoy can't hide it reliably in what it sends back. Use a longer value, or type it as plain text if it isn't sensitive.`)}return i}function knownSecrets(e=process.env){const n=new Map(used);for(const[t,s]of Object.entries(e))if(t.startsWith("BUOY_SECRET_")&&s)n.set(t.slice("BUOY_SECRET_".length),s);return n}function scrubSecrets(e){const n=new Map;for(const[i,r]of knownSecrets()){if(r.length<MIN_SECRET_LENGTH)continue;for(const o of[r,JSON.stringify(r).slice(1,-1),encodeURIComponent(r)])n.set(o,i);for(let o=r.length-1;o>=MIN_SECRET_LENGTH;o--){n.set(`${r.slice(0,o)}\u2026`,i);n.set(`${r.slice(0,o)}...`,i);if(o>=8)n.set(r.slice(0,o),i)}}if(n.size===0)return e;const t=[...n.keys()].sort((i,r)=>r.length-i.length);const s=new RegExp(t.map(escapeRe).join("|"),"g");return e.replace(s,(i,r,o)=>{if(/^\d+$/.test(i)){const a=o.slice(Math.max(0,r-8),r);const l=o.slice(r+i.length,r+i.length+8);if(/(\d|\d\.)$/.test(a)||/^(\d|\.\d|[eE][+-]?\d)/.test(l))return i;if(/[:[,]\s*$/.test(a)&&/^\s*([,\]}]|$)/.test(l)&&!insideJsonString(o,r))return`"{{secret:${n.get(i)}}}"`}return`{{secret:${n.get(i)}}}`})}function insideJsonString(e,n){let t=false;for(let s=0;s<n;s++){if(e[s]==="\\")s++;else if(e[s]==='"')t=!t}return t}function typedSecretValues(){return[...used.values()]}function secretValues(){return[...knownSecrets().values()].filter(e=>e.length>=MIN_SECRET_LENGTH)}var PLACEHOLDER,MIN_SECRET_LENGTH,used,recordingActive,escapeRe;var init_secrets=__esm({"src/tools/secrets.ts"(){"use strict";PLACEHOLDER=/\{\{\s*secret:([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;MIN_SECRET_LENGTH=6;used=new Map;recordingActive=()=>false;escapeRe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}});function q(e){return`'${e.replace(/'/g,`'\\''`)}'`}function assertPackage(e){if(!/^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$/.test(e))throw new Error(`"${e}" is not a valid Android package name.`);return e}async function adb(e,n,t=15e3){try{const{stdout:s}=await execFileAsync("adb",e?["-s",e,...n]:n,{timeout:t,maxBuffer:32*1024*1024});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(ADB_MISSING);const r=n[0]==="shell"?`shell ${(n[1]??"").split(" ")[0]}`:n.slice(0,2).join(" ");const o=n[0]==="shell"?i.stderr||(i.killed?"timed out":"no error text"):i.stderr||i.message||"";const a=scrubSecrets(o.replace(/'[^']*'/g,"'\u2026'")).trim().slice(0,300);throw new Error(`adb ${r} failed: ${a}`)}}function adbShell(e,n,t=15e3){return adb(e,["shell",n],t)}async function adbBytes(e,n,t=15e3){try{const{stdout:s}=await execFileAsync("adb",["-s",e,...n],{timeout:t,encoding:"buffer",maxBuffer:64*1024*1024});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(ADB_MISSING);throw new Error(`adb ${n.slice(0,2).join(" ")} failed: ${i.message.slice(0,300)}`)}}async function adbSerials(){return parseAdbDevices(await adb(null,["devices"]))}function parseAdbDevices(e){return e.split("\n").slice(1).map(n=>n.trim().split(/\s+/)).filter(n=>n[1]==="device").map(n=>n[0])}async function adbScreenSize(e){const n=parseWmSize(await adbShell(e,"wm size"));const t=parseRotation(await adbShell(e,"dumpsys input").catch(()=>""));return t%2===1?{width:n.height,height:n.width}:n}function parseWmSize(e){const n=/Override size:\s*(\d+)x(\d+)/.exec(e);const t=/Physical size:\s*(\d+)x(\d+)/.exec(e);const s=n??t;if(!s)throw new Error(`Could not read the Android screen size (adb said: ${e.trim().slice(0,120)}).`);return{width:Number(s[1]),height:Number(s[2])}}function parseRotation(e){const n=/SurfaceOrientation:\s*(\d)/.exec(e);return n?Number(n[1]):0}async function adbAppFrame(e,n){const t=await adbShell(e,"dumpsys window windows").catch(()=>"");return parseAppFrame(t,n)}function parseAppFrame(e,n){const t=e.split(/\n(?=\s*Window #\d+)/);for(const s of t){if(!s.includes(`${n}/`))continue;const i=/mFrame=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(s)??/\bframe=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(s);if(i){const[r,o,a,l]=i.slice(1).map(Number);if(a>r&&l>o)return{x:r,y:o,width:a-r,height:l-o}}}return null}async function adbForegroundPackage(e){return parseForegroundPackage(await adbShell(e,"dumpsys activity activities"))}function parseForegroundPackage(e){const n=/(?:topResumedActivity|mResumedActivity|ResumedActivity)[^\n]*?\s([a-zA-Z][\w.]*)\/[\w.$]+/.exec(e);return n?n[1]:null}async function adbUserPackages(e){const n=await adbShell(e,"pm list packages -3");return n.split("\n").map(t=>t.trim().replace(/^package:/,"")).filter(t=>/^[A-Za-z][\w.]+$/.test(t))}async function adbInstallId(e,n){assertPackage(n);const t=await adbShell(e,`run-as ${q(n)} cat files/react-buoy/@react_buoy_sync_device_id.json 2>/dev/null`,5e3).catch(()=>"");const s=t.trim().replace(/^"|"$/g,"");if(/^[\w-]{4,64}$/.test(s))return s;const i=await adbBytes(e,["exec-out",`run-as ${q(n)} sh -c 'cat app_webview/Default/"Local Storage"/leveldb/*.log app_webview/Default/"Local Storage"/leveldb/*.ldb 2>/dev/null'`],5e3).catch(()=>null);return i?webViewLevelDbInstallId(i):null}function webViewLevelDbInstallId(e){const n=e.toString("latin1");const t=/@react_buoy_sync_device_id([\s\S])\x01([\w-]{4,64})/g;let s=null;for(let i=t.exec(n);i;i=t.exec(n)){const r=i[1].charCodeAt(0)-1;if(r>=4&&r<=i[2].length)s=i[2].slice(0,r)}return s}function escapeAdbText(e){if(/[^\x20-\x7e]/.test(e))throw new Error("adb can only type plain ASCII text. Use tap_element's text for other characters.");return q(e.replace(/ /g,"%s"))}async function adbTap(e,n,t,s){const[i,r]=[Math.round(n),Math.round(t)];if(s)await adbShell(e,`input swipe ${i} ${r} ${i} ${r} ${Math.round(s)}`);else await adbShell(e,`input tap ${i} ${r}`)}async function adbSwipe(e,n,t,s){const i=[...n,...t].map(r=>Math.round(r)).join(" ");await adbShell(e,`input swipe ${i} ${Math.round(s)}`)}async function adbText(e,n){const t=n.replace(/ /g,"\0");for(let s=0;s<t.length;s+=40){const i=t.slice(s,s+40).replace(/\u0000/g," ");await adbShell(e,`input text ${escapeAdbText(i)}`)}}async function adbScreenshot(e){const n=await adbBytes(e,["exec-out","screencap","-p"]);if(n.length<8||n.readUInt32BE(0)!==2303741511)throw new Error("The Android screenshot was not a PNG.");return n}async function adbDemoMode(e,n){if(n){await adbShell(e,"settings put global sysui_demo_allowed 1");for(const t of["-e command enter","-e command clock -e hhmm 0941","-e command battery -e level 100 -e plugged false","-e command network -e wifi show -e level 4","-e command notifications -e visible false"]){await adbShell(e,`am broadcast -a com.android.systemui.demo ${t}`).catch(()=>void 0)}}else{await adbShell(e,"am broadcast -a com.android.systemui.demo -e command exit").catch(()=>void 0)}}async function adbUiNodes(e){const n=await adb(e,["exec-out","uiautomator","dump","/dev/tty"],2e4);return parseUiDump(n)}function parseUiDump(e){const n=[];const t=(s,i)=>{const r=new RegExp(`\\s${i}="([^"]*)"`).exec(s);return r?decodeXml(r[1]):""};for(const s of e.matchAll(/<node\b[^>]*>/g)){const i=s[0];const r=/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(t(i,"bounds"));if(!r)continue;const[o,a,l,c]=r.slice(1).map(Number);n.push({text:t(i,"text"),desc:t(i,"content-desc"),id:t(i,"resource-id"),cls:t(i,"class"),clickable:t(i,"clickable")==="true",bounds:{x:o,y:a,width:l-o,height:c-a}})}return n}function decodeXml(e){return e.replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(n,t)=>String.fromCharCode(Number(t))).replace(/&/g,"&")}function findUiNodes(e,n){const t=n.trim().toLowerCase();const s=l=>{let c=0;for(const u of[l.desc,l.text,l.id.split("/").pop()??""]){const d=u.trim().toLowerCase();if(!d)continue;if(d===t)c=Math.max(c,3);else if(d.startsWith(t))c=Math.max(c,2);else if(d.includes(t))c=Math.max(c,1)}return c};const i=e.filter(l=>l.bounds.width>0&&l.bounds.height>0&&s(l)>0);const r=Math.max(0,...i.map(s));const o=i.filter(l=>s(l)===r);let a=o.filter((l,c)=>o.findIndex(u=>JSON.stringify(u.bounds)===JSON.stringify(l.bounds))===c);if(a.length>1){const l=a.filter(c=>[c.desc,c.text,c.id.split("/").pop()??""].some(u=>u.trim()===n.trim()));if(l.length)a=l}if(a.length>1){const l=a.filter(c=>c.clickable||/EditText|Button/.test(c.cls));if(l.length)a=l}return a}var import_node_child_process2,import_node_util,execFileAsync,ADB_MISSING;var init_adb=__esm({"src/capture/adb.ts"(){"use strict";import_node_child_process2=require("child_process");import_node_util=require("util");init_secrets();execFileAsync=(0,import_node_util.promisify)(import_node_child_process2.execFile);ADB_MISSING="Android control needs adb on this computer (Android SDK platform-tools). Install it, or add it to PATH."}});var require_chunkstream=__commonJS({"../../node_modules/pngjs/lib/chunkstream.js"(e,n){"use strict";var t=require("util");var s=require("stream");var i=n.exports=function(){s.call(this);this._buffers=[];this._buffered=0;this._reads=[];this._paused=false;this._encoding="utf8";this.writable=true};t.inherits(i,s);i.prototype.read=function(r,o){this._reads.push({length:Math.abs(r),allowLess:r<0,func:o});process.nextTick(function(){this._process();if(this._paused&&this._reads&&this._reads.length>0){this._paused=false;this.emit("drain")}}.bind(this))};i.prototype.write=function(r,o){if(!this.writable){this.emit("error",new Error("Stream not writable"));return false}let a;if(Buffer.isBuffer(r)){a=r}else{a=Buffer.from(r,o||this._encoding)}this._buffers.push(a);this._buffered+=a.length;this._process();if(this._reads&&this._reads.length===0){this._paused=true}return this.writable&&!this._paused};i.prototype.end=function(r,o){if(r){this.write(r,o)}this.writable=false;if(!this._buffers){return}if(this._buffers.length===0){this._end()}else{this._buffers.push(null);this._process()}};i.prototype.destroySoon=i.prototype.end;i.prototype._end=function(){if(this._reads.length>0){this.emit("error",new Error("Unexpected end of input"))}this.destroy()};i.prototype.destroy=function(){if(!this._buffers){return}this.writable=false;this._reads=null;this._buffers=null;this.emit("close")};i.prototype._processReadAllowingLess=function(r){this._reads.shift();let o=this._buffers[0];if(o.length>r.length){this._buffered-=r.length;this._buffers[0]=o.slice(r.length);r.func.call(this,o.slice(0,r.length))}else{this._buffered-=o.length;this._buffers.shift();r.func.call(this,o)}};i.prototype._processRead=function(r){this._reads.shift();let o=0;let a=0;let l=Buffer.alloc(r.length);while(o<r.length){let c=this._buffers[a++];let u=Math.min(c.length,r.length-o);c.copy(l,o,0,u);o+=u;if(u!==c.length){this._buffers[--a]=c.slice(u)}}if(a>0){this._buffers.splice(0,a)}this._buffered-=r.length;r.func.call(this,l)};i.prototype._process=function(){try{while(this._buffered>0&&this._reads&&this._reads.length>0){let r=this._reads[0];if(r.allowLess){this._processReadAllowingLess(r)}else if(this._buffered>=r.length){this._processRead(r)}else{break}}if(this._buffers&&!this.writable){this._end()}}catch(r){this.emit("error",r)}}}});var require_interlace=__commonJS({"../../node_modules/pngjs/lib/interlace.js"(e){"use strict";var n=[{x:[0],y:[0]},{x:[4],y:[0]},{x:[0,4],y:[4]},{x:[2,6],y:[0,4]},{x:[0,2,4,6],y:[2,6]},{x:[1,3,5,7],y:[0,2,4,6]},{x:[0,1,2,3,4,5,6,7],y:[1,3,5,7]}];e.getImagePasses=function(t,s){let i=[];let r=t%8;let o=s%8;let a=(t-r)/8;let l=(s-o)/8;for(let c=0;c<n.length;c++){let u=n[c];let d=a*u.x.length;let h=l*u.y.length;for(let p=0;p<u.x.length;p++){if(u.x[p]<r){d++}else{break}}for(let p=0;p<u.y.length;p++){if(u.y[p]<o){h++}else{break}}if(d>0&&h>0){i.push({width:d,height:h,index:c})}}return i};e.getInterlaceIterator=function(t){return function(s,i,r){let o=s%n[r].x.length;let a=(s-o)/n[r].x.length*8+n[r].x[o];let l=i%n[r].y.length;let c=(i-l)/n[r].y.length*8+n[r].y[l];return a*4+c*t*4}}}});var require_paeth_predictor=__commonJS({"../../node_modules/pngjs/lib/paeth-predictor.js"(e,n){"use strict";n.exports=function t(s,i,r){let o=s+i-r;let a=Math.abs(o-s);let l=Math.abs(o-i);let c=Math.abs(o-r);if(a<=l&&a<=c){return s}if(l<=c){return i}return r}}});var require_filter_parse=__commonJS({"../../node_modules/pngjs/lib/filter-parse.js"(e,n){"use strict";var t=require_interlace();var s=require_paeth_predictor();function i(o,a,l){let c=o*a;if(l!==8){c=Math.ceil(c/(8/l))}return c}var r=n.exports=function(o,a){let l=o.width;let c=o.height;let u=o.interlace;let d=o.bpp;let h=o.depth;this.read=a.read;this.write=a.write;this.complete=a.complete;this._imageIndex=0;this._images=[];if(u){let p=t.getImagePasses(l,c);for(let m=0;m<p.length;m++){this._images.push({byteWidth:i(p[m].width,d,h),height:p[m].height,lineIndex:0})}}else{this._images.push({byteWidth:i(l,d,h),height:c,lineIndex:0})}if(h===8){this._xComparison=d}else if(h===16){this._xComparison=d*2}else{this._xComparison=1}};r.prototype.start=function(){this.read(this._images[this._imageIndex].byteWidth+1,this._reverseFilterLine.bind(this))};r.prototype._unFilterType1=function(o,a,l){let c=this._xComparison;let u=c-1;for(let d=0;d<l;d++){let h=o[1+d];let p=d>u?a[d-c]:0;a[d]=h+p}};r.prototype._unFilterType2=function(o,a,l){let c=this._lastLine;for(let u=0;u<l;u++){let d=o[1+u];let h=c?c[u]:0;a[u]=d+h}};r.prototype._unFilterType3=function(o,a,l){let c=this._xComparison;let u=c-1;let d=this._lastLine;for(let h=0;h<l;h++){let p=o[1+h];let m=d?d[h]:0;let y=h>u?a[h-c]:0;let g=Math.floor((y+m)/2);a[h]=p+g}};r.prototype._unFilterType4=function(o,a,l){let c=this._xComparison;let u=c-1;let d=this._lastLine;for(let h=0;h<l;h++){let p=o[1+h];let m=d?d[h]:0;let y=h>u?a[h-c]:0;let g=h>u&&d?d[h-c]:0;let f=s(y,m,g);a[h]=p+f}};r.prototype._reverseFilterLine=function(o){let a=o[0];let l;let c=this._images[this._imageIndex];let u=c.byteWidth;if(a===0){l=o.slice(1,u+1)}else{l=Buffer.alloc(u);switch(a){case 1:this._unFilterType1(o,l,u);break;case 2:this._unFilterType2(o,l,u);break;case 3:this._unFilterType3(o,l,u);break;case 4:this._unFilterType4(o,l,u);break;default:throw new Error("Unrecognised filter type - "+a)}}this.write(l);c.lineIndex++;if(c.lineIndex>=c.height){this._lastLine=null;this._imageIndex++;c=this._images[this._imageIndex]}else{this._lastLine=l}if(c){this.read(c.byteWidth+1,this._reverseFilterLine.bind(this))}else{this._lastLine=null;this.complete()}}}});var require_filter_parse_async=__commonJS({"../../node_modules/pngjs/lib/filter-parse-async.js"(e,n){"use strict";var t=require("util");var s=require_chunkstream();var i=require_filter_parse();var r=n.exports=function(o){s.call(this);let a=[];let l=this;this._filter=new i(o,{read:this.read.bind(this),write:function(c){a.push(c)},complete:function(){l.emit("complete",Buffer.concat(a))}});this._filter.start()};t.inherits(r,s)}});var require_constants=__commonJS({"../../node_modules/pngjs/lib/constants.js"(e,n){"use strict";n.exports={PNG_SIGNATURE:[137,80,78,71,13,10,26,10],TYPE_IHDR:1229472850,TYPE_IEND:1229278788,TYPE_IDAT:1229209940,TYPE_PLTE:1347179589,TYPE_tRNS:1951551059,TYPE_gAMA:1732332865,COLORTYPE_GRAYSCALE:0,COLORTYPE_PALETTE:1,COLORTYPE_COLOR:2,COLORTYPE_ALPHA:4,COLORTYPE_PALETTE_COLOR:3,COLORTYPE_COLOR_ALPHA:6,COLORTYPE_TO_BPP_MAP:{0:1,2:3,3:1,4:2,6:4},GAMMA_DIVISION:1e5}}});var require_crc=__commonJS({"../../node_modules/pngjs/lib/crc.js"(e,n){"use strict";var t=[];(function(){for(let i=0;i<256;i++){let r=i;for(let o=0;o<8;o++){if(r&1){r=3988292384^r>>>1}else{r=r>>>1}}t[i]=r}})();var s=n.exports=function(){this._crc=-1};s.prototype.write=function(i){for(let r=0;r<i.length;r++){this._crc=t[(this._crc^i[r])&255]^this._crc>>>8}return true};s.prototype.crc32=function(){return this._crc^-1};s.crc32=function(i){let r=-1;for(let o=0;o<i.length;o++){r=t[(r^i[o])&255]^r>>>8}return r^-1}}});var require_parser=__commonJS({"../../node_modules/pngjs/lib/parser.js"(e,n){"use strict";var t=require_constants();var s=require_crc();var i=n.exports=function(r,o){this._options=r;r.checkCRC=r.checkCRC!==false;this._hasIHDR=false;this._hasIEND=false;this._emittedHeadersFinished=false;this._palette=[];this._colorType=0;this._chunks={};this._chunks[t.TYPE_IHDR]=this._handleIHDR.bind(this);this._chunks[t.TYPE_IEND]=this._handleIEND.bind(this);this._chunks[t.TYPE_IDAT]=this._handleIDAT.bind(this);this._chunks[t.TYPE_PLTE]=this._handlePLTE.bind(this);this._chunks[t.TYPE_tRNS]=this._handleTRNS.bind(this);this._chunks[t.TYPE_gAMA]=this._handleGAMA.bind(this);this.read=o.read;this.error=o.error;this.metadata=o.metadata;this.gamma=o.gamma;this.transColor=o.transColor;this.palette=o.palette;this.parsed=o.parsed;this.inflateData=o.inflateData;this.finished=o.finished;this.simpleTransparency=o.simpleTransparency;this.headersFinished=o.headersFinished||function(){}};i.prototype.start=function(){this.read(t.PNG_SIGNATURE.length,this._parseSignature.bind(this))};i.prototype._parseSignature=function(r){let o=t.PNG_SIGNATURE;for(let a=0;a<o.length;a++){if(r[a]!==o[a]){this.error(new Error("Invalid file signature"));return}}this.read(8,this._parseChunkBegin.bind(this))};i.prototype._parseChunkBegin=function(r){let o=r.readUInt32BE(0);let a=r.readUInt32BE(4);let l="";for(let u=4;u<8;u++){l+=String.fromCharCode(r[u])}let c=Boolean(r[4]&32);if(!this._hasIHDR&&a!==t.TYPE_IHDR){this.error(new Error("Expected IHDR on beggining"));return}this._crc=new s;this._crc.write(Buffer.from(l));if(this._chunks[a]){return this._chunks[a](o)}if(!c){this.error(new Error("Unsupported critical chunk type "+l));return}this.read(o+4,this._skipChunk.bind(this))};i.prototype._skipChunk=function(){this.read(8,this._parseChunkBegin.bind(this))};i.prototype._handleChunkEnd=function(){this.read(4,this._parseChunkEnd.bind(this))};i.prototype._parseChunkEnd=function(r){let o=r.readInt32BE(0);let a=this._crc.crc32();if(this._options.checkCRC&&a!==o){this.error(new Error("Crc error - "+o+" - "+a));return}if(!this._hasIEND){this.read(8,this._parseChunkBegin.bind(this))}};i.prototype._handleIHDR=function(r){this.read(r,this._parseIHDR.bind(this))};i.prototype._parseIHDR=function(r){this._crc.write(r);let o=r.readUInt32BE(0);let a=r.readUInt32BE(4);let l=r[8];let c=r[9];let u=r[10];let d=r[11];let h=r[12];if(l!==8&&l!==4&&l!==2&&l!==1&&l!==16){this.error(new Error("Unsupported bit depth "+l));return}if(!(c in t.COLORTYPE_TO_BPP_MAP)){this.error(new Error("Unsupported color type"));return}if(u!==0){this.error(new Error("Unsupported compression method"));return}if(d!==0){this.error(new Error("Unsupported filter method"));return}if(h!==0&&h!==1){this.error(new Error("Unsupported interlace method"));return}this._colorType=c;let p=t.COLORTYPE_TO_BPP_MAP[this._colorType];this._hasIHDR=true;this.metadata({width:o,height:a,depth:l,interlace:Boolean(h),palette:Boolean(c&t.COLORTYPE_PALETTE),color:Boolean(c&t.COLORTYPE_COLOR),alpha:Boolean(c&t.COLORTYPE_ALPHA),bpp:p,colorType:c});this._handleChunkEnd()};i.prototype._handlePLTE=function(r){this.read(r,this._parsePLTE.bind(this))};i.prototype._parsePLTE=function(r){this._crc.write(r);let o=Math.floor(r.length/3);for(let a=0;a<o;a++){this._palette.push([r[a*3],r[a*3+1],r[a*3+2],255])}this.palette(this._palette);this._handleChunkEnd()};i.prototype._handleTRNS=function(r){this.simpleTransparency();this.read(r,this._parseTRNS.bind(this))};i.prototype._parseTRNS=function(r){this._crc.write(r);if(this._colorType===t.COLORTYPE_PALETTE_COLOR){if(this._palette.length===0){this.error(new Error("Transparency chunk must be after palette"));return}if(r.length>this._palette.length){this.error(new Error("More transparent colors than palette size"));return}for(let o=0;o<r.length;o++){this._palette[o][3]=r[o]}this.palette(this._palette)}if(this._colorType===t.COLORTYPE_GRAYSCALE){this.transColor([r.readUInt16BE(0)])}if(this._colorType===t.COLORTYPE_COLOR){this.transColor([r.readUInt16BE(0),r.readUInt16BE(2),r.readUInt16BE(4)])}this._handleChunkEnd()};i.prototype._handleGAMA=function(r){this.read(r,this._parseGAMA.bind(this))};i.prototype._parseGAMA=function(r){this._crc.write(r);this.gamma(r.readUInt32BE(0)/t.GAMMA_DIVISION);this._handleChunkEnd()};i.prototype._handleIDAT=function(r){if(!this._emittedHeadersFinished){this._emittedHeadersFinished=true;this.headersFinished()}this.read(-r,this._parseIDAT.bind(this,r))};i.prototype._parseIDAT=function(r,o){this._crc.write(o);if(this._colorType===t.COLORTYPE_PALETTE_COLOR&&this._palette.length===0){throw new Error("Expected palette not found")}this.inflateData(o);let a=r-o.length;if(a>0){this._handleIDAT(a)}else{this._handleChunkEnd()}};i.prototype._handleIEND=function(r){this.read(r,this._parseIEND.bind(this))};i.prototype._parseIEND=function(r){this._crc.write(r);this._hasIEND=true;this._handleChunkEnd();if(this.finished){this.finished()}}}});var require_bitmapper=__commonJS({"../../node_modules/pngjs/lib/bitmapper.js"(e){"use strict";var n=require_interlace();var t=[function(){},function(a,l,c,u){if(u===l.length){throw new Error("Ran out of data")}let d=l[u];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=255},function(a,l,c,u){if(u+1>=l.length){throw new Error("Ran out of data")}let d=l[u];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=l[u+1]},function(a,l,c,u){if(u+2>=l.length){throw new Error("Ran out of data")}a[c]=l[u];a[c+1]=l[u+1];a[c+2]=l[u+2];a[c+3]=255},function(a,l,c,u){if(u+3>=l.length){throw new Error("Ran out of data")}a[c]=l[u];a[c+1]=l[u+1];a[c+2]=l[u+2];a[c+3]=l[u+3]}];var s=[function(){},function(a,l,c,u){let d=l[0];a[c]=d;a[c+1]=d;a[c+2]=d;a[c+3]=u},function(a,l,c){let u=l[0];a[c]=u;a[c+1]=u;a[c+2]=u;a[c+3]=l[1]},function(a,l,c,u){a[c]=l[0];a[c+1]=l[1];a[c+2]=l[2];a[c+3]=u},function(a,l,c){a[c]=l[0];a[c+1]=l[1];a[c+2]=l[2];a[c+3]=l[3]}];function i(a,l){let c=[];let u=0;function d(){if(u===a.length){throw new Error("Ran out of data")}let h=a[u];u++;let p,m,y,g,f,w,v,b;switch(l){default:throw new Error("unrecognised depth");case 16:v=a[u];u++;c.push((h<<8)+v);break;case 4:v=h&15;b=h>>4;c.push(b,v);break;case 2:f=h&3;w=h>>2&3;v=h>>4&3;b=h>>6&3;c.push(b,v,w,f);break;case 1:p=h&1;m=h>>1&1;y=h>>2&1;g=h>>3&1;f=h>>4&1;w=h>>5&1;v=h>>6&1;b=h>>7&1;c.push(b,v,w,f,g,y,m,p);break}}return{get:function(h){while(c.length<h){d()}let p=c.slice(0,h);c=c.slice(h);return p},resetAfterLine:function(){c.length=0},end:function(){if(u!==a.length){throw new Error("extra data found")}}}}function r(a,l,c,u,d,h){let p=a.width;let m=a.height;let y=a.index;for(let g=0;g<m;g++){for(let f=0;f<p;f++){let w=c(f,g,y);t[u](l,d,w,h);h+=u}}return h}function o(a,l,c,u,d,h){let p=a.width;let m=a.height;let y=a.index;for(let g=0;g<m;g++){for(let f=0;f<p;f++){let w=d.get(u);let v=c(f,g,y);s[u](l,w,v,h)}d.resetAfterLine()}}e.dataToBitMap=function(a,l){let c=l.width;let u=l.height;let d=l.depth;let h=l.bpp;let p=l.interlace;let m;if(d!==8){m=i(a,d)}let y;if(d<=8){y=Buffer.alloc(c*u*4)}else{y=new Uint16Array(c*u*4)}let g=Math.pow(2,d)-1;let f=0;let w;let v;if(p){w=n.getImagePasses(c,u);v=n.getInterlaceIterator(c,u)}else{let b=0;v=function(){let _=b;b+=4;return _};w=[{width:c,height:u}]}for(let b=0;b<w.length;b++){if(d===8){f=r(w[b],y,v,h,a,f)}else{o(w[b],y,v,h,m,g)}}if(d===8){if(f!==a.length){throw new Error("extra data found")}}else{m.end()}return y}}});var require_format_normaliser=__commonJS({"../../node_modules/pngjs/lib/format-normaliser.js"(e,n){"use strict";function t(r,o,a,l,c){let u=0;for(let d=0;d<l;d++){for(let h=0;h<a;h++){let p=c[r[u]];if(!p){throw new Error("index "+r[u]+" not in palette")}for(let m=0;m<4;m++){o[u+m]=p[m]}u+=4}}}function s(r,o,a,l,c){let u=0;for(let d=0;d<l;d++){for(let h=0;h<a;h++){let p=false;if(c.length===1){if(c[0]===r[u]){p=true}}else if(c[0]===r[u]&&c[1]===r[u+1]&&c[2]===r[u+2]){p=true}if(p){for(let m=0;m<4;m++){o[u+m]=0}}u+=4}}}function i(r,o,a,l,c){let u=255;let d=Math.pow(2,c)-1;let h=0;for(let p=0;p<l;p++){for(let m=0;m<a;m++){for(let y=0;y<4;y++){o[h+y]=Math.floor(r[h+y]*u/d+.5)}h+=4}}}n.exports=function(r,o,a=false){let l=o.depth;let c=o.width;let u=o.height;let d=o.colorType;let h=o.transColor;let p=o.palette;let m=r;if(d===3){t(r,m,c,u,p)}else{if(h){s(r,m,c,u,h)}if(l!==8&&!a){if(l===16){m=Buffer.alloc(c*u*4)}i(r,m,c,u,l)}}return m}}});var require_parser_async=__commonJS({"../../node_modules/pngjs/lib/parser-async.js"(e,n){"use strict";var t=require("util");var s=require("zlib");var i=require_chunkstream();var r=require_filter_parse_async();var o=require_parser();var a=require_bitmapper();var l=require_format_normaliser();var c=n.exports=function(u){i.call(this);this._parser=new o(u,{read:this.read.bind(this),error:this._handleError.bind(this),metadata:this._handleMetaData.bind(this),gamma:this.emit.bind(this,"gamma"),palette:this._handlePalette.bind(this),transColor:this._handleTransColor.bind(this),finished:this._finished.bind(this),inflateData:this._inflateData.bind(this),simpleTransparency:this._simpleTransparency.bind(this),headersFinished:this._headersFinished.bind(this)});this._options=u;this.writable=true;this._parser.start()};t.inherits(c,i);c.prototype._handleError=function(u){this.emit("error",u);this.writable=false;this.destroy();if(this._inflate&&this._inflate.destroy){this._inflate.destroy()}if(this._filter){this._filter.destroy();this._filter.on("error",function(){})}this.errord=true};c.prototype._inflateData=function(u){if(!this._inflate){if(this._bitmapInfo.interlace){this._inflate=s.createInflate();this._inflate.on("error",this.emit.bind(this,"error"));this._filter.on("complete",this._complete.bind(this));this._inflate.pipe(this._filter)}else{let d=(this._bitmapInfo.width*this._bitmapInfo.bpp*this._bitmapInfo.depth+7>>3)+1;let h=d*this._bitmapInfo.height;let p=Math.max(h,s.Z_MIN_CHUNK);this._inflate=s.createInflate({chunkSize:p});let m=h;let y=this.emit.bind(this,"error");this._inflate.on("error",function(f){if(!m){return}y(f)});this._filter.on("complete",this._complete.bind(this));let g=this._filter.write.bind(this._filter);this._inflate.on("data",function(f){if(!m){return}if(f.length>m){f=f.slice(0,m)}m-=f.length;g(f)});this._inflate.on("end",this._filter.end.bind(this._filter))}}this._inflate.write(u)};c.prototype._handleMetaData=function(u){this._metaData=u;this._bitmapInfo=Object.create(u);this._filter=new r(this._bitmapInfo)};c.prototype._handleTransColor=function(u){this._bitmapInfo.transColor=u};c.prototype._handlePalette=function(u){this._bitmapInfo.palette=u};c.prototype._simpleTransparency=function(){this._metaData.alpha=true};c.prototype._headersFinished=function(){this.emit("metadata",this._metaData)};c.prototype._finished=function(){if(this.errord){return}if(!this._inflate){this.emit("error","No Inflate block")}else{this._inflate.end()}};c.prototype._complete=function(u){if(this.errord){return}let d;try{let h=a.dataToBitMap(u,this._bitmapInfo);d=l(h,this._bitmapInfo,this._options.skipRescale);h=null}catch(h){this._handleError(h);return}this.emit("parsed",d)}}});var require_bitpacker=__commonJS({"../../node_modules/pngjs/lib/bitpacker.js"(e,n){"use strict";var t=require_constants();n.exports=function(s,i,r,o){let a=[t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(o.colorType)!==-1;if(o.colorType===o.inputColorType){let f=(function(){let w=new ArrayBuffer(2);new DataView(w).setInt16(0,256,true);return new Int16Array(w)[0]!==256})();if(o.bitDepth===8||o.bitDepth===16&&f){return s}}let l=o.bitDepth!==16?s:new Uint16Array(s.buffer);let c=255;let u=t.COLORTYPE_TO_BPP_MAP[o.inputColorType];if(u===4&&!o.inputHasAlpha){u=3}let d=t.COLORTYPE_TO_BPP_MAP[o.colorType];if(o.bitDepth===16){c=65535;d*=2}let h=Buffer.alloc(i*r*d);let p=0;let m=0;let y=o.bgColor||{};if(y.red===void 0){y.red=c}if(y.green===void 0){y.green=c}if(y.blue===void 0){y.blue=c}function g(){let f;let w;let v;let b=c;switch(o.inputColorType){case t.COLORTYPE_COLOR_ALPHA:b=l[p+3];f=l[p];w=l[p+1];v=l[p+2];break;case t.COLORTYPE_COLOR:f=l[p];w=l[p+1];v=l[p+2];break;case t.COLORTYPE_ALPHA:b=l[p+1];f=l[p];w=f;v=f;break;case t.COLORTYPE_GRAYSCALE:f=l[p];w=f;v=f;break;default:throw new Error("input color type:"+o.inputColorType+" is not supported at present")}if(o.inputHasAlpha){if(!a){b/=c;f=Math.min(Math.max(Math.round((1-b)*y.red+b*f),0),c);w=Math.min(Math.max(Math.round((1-b)*y.green+b*w),0),c);v=Math.min(Math.max(Math.round((1-b)*y.blue+b*v),0),c)}}return{red:f,green:w,blue:v,alpha:b}}for(let f=0;f<r;f++){for(let w=0;w<i;w++){let v=g(l,p);switch(o.colorType){case t.COLORTYPE_COLOR_ALPHA:case t.COLORTYPE_COLOR:if(o.bitDepth===8){h[m]=v.red;h[m+1]=v.green;h[m+2]=v.blue;if(a){h[m+3]=v.alpha}}else{h.writeUInt16BE(v.red,m);h.writeUInt16BE(v.green,m+2);h.writeUInt16BE(v.blue,m+4);if(a){h.writeUInt16BE(v.alpha,m+6)}}break;case t.COLORTYPE_ALPHA:case t.COLORTYPE_GRAYSCALE:{let b=(v.red+v.green+v.blue)/3;if(o.bitDepth===8){h[m]=b;if(a){h[m+1]=v.alpha}}else{h.writeUInt16BE(b,m);if(a){h.writeUInt16BE(v.alpha,m+2)}}break}default:throw new Error("unrecognised color Type "+o.colorType)}p+=u;m+=d}}return h}}});var require_filter_pack=__commonJS({"../../node_modules/pngjs/lib/filter-pack.js"(e,n){"use strict";var t=require_paeth_predictor();function s(y,g,f,w,v){for(let b=0;b<f;b++){w[v+b]=y[g+b]}}function i(y,g,f){let w=0;let v=g+f;for(let b=g;b<v;b++){w+=Math.abs(y[b])}return w}function r(y,g,f,w,v,b){for(let _=0;_<f;_++){let $=_>=b?y[g+_-b]:0;let k=y[g+_]-$;w[v+_]=k}}function o(y,g,f,w){let v=0;for(let b=0;b<f;b++){let _=b>=w?y[g+b-w]:0;let $=y[g+b]-_;v+=Math.abs($)}return v}function a(y,g,f,w,v){for(let b=0;b<f;b++){let _=g>0?y[g+b-f]:0;let $=y[g+b]-_;w[v+b]=$}}function l(y,g,f){let w=0;let v=g+f;for(let b=g;b<v;b++){let _=g>0?y[b-f]:0;let $=y[b]-_;w+=Math.abs($)}return w}function c(y,g,f,w,v,b){for(let _=0;_<f;_++){let $=_>=b?y[g+_-b]:0;let k=g>0?y[g+_-f]:0;let S=y[g+_]-($+k>>1);w[v+_]=S}}function u(y,g,f,w){let v=0;for(let b=0;b<f;b++){let _=b>=w?y[g+b-w]:0;let $=g>0?y[g+b-f]:0;let k=y[g+b]-(_+$>>1);v+=Math.abs(k)}return v}function d(y,g,f,w,v,b){for(let _=0;_<f;_++){let $=_>=b?y[g+_-b]:0;let k=g>0?y[g+_-f]:0;let S=g>0&&_>=b?y[g+_-(f+b)]:0;let E=y[g+_]-t($,k,S);w[v+_]=E}}function h(y,g,f,w){let v=0;for(let b=0;b<f;b++){let _=b>=w?y[g+b-w]:0;let $=g>0?y[g+b-f]:0;let k=g>0&&b>=w?y[g+b-(f+w)]:0;let S=y[g+b]-t(_,$,k);v+=Math.abs(S)}return v}var p={0:s,1:r,2:a,3:c,4:d};var m={0:i,1:o,2:l,3:u,4:h};n.exports=function(y,g,f,w,v){let b;if(!("filterType"in w)||w.filterType===-1){b=[0,1,2,3,4]}else if(typeof w.filterType==="number"){b=[w.filterType]}else{throw new Error("unrecognised filter types")}if(w.bitDepth===16){v*=2}let _=g*v;let $=0;let k=0;let S=Buffer.alloc((_+1)*f);let E=b[0];for(let A=0;A<f;A++){if(b.length>1){let P=Infinity;for(let x=0;x<b.length;x++){let T=m[b[x]](y,k,_,v);if(T<P){E=b[x];P=T}}}S[$]=E;$++;p[E](y,k,_,S,$,v);$+=_;k+=_}return S}}});var require_packer=__commonJS({"../../node_modules/pngjs/lib/packer.js"(e,n){"use strict";var t=require_constants();var s=require_crc();var i=require_bitpacker();var r=require_filter_pack();var o=require("zlib");var a=n.exports=function(l){this._options=l;l.deflateChunkSize=l.deflateChunkSize||32*1024;l.deflateLevel=l.deflateLevel!=null?l.deflateLevel:9;l.deflateStrategy=l.deflateStrategy!=null?l.deflateStrategy:3;l.inputHasAlpha=l.inputHasAlpha!=null?l.inputHasAlpha:true;l.deflateFactory=l.deflateFactory||o.createDeflate;l.bitDepth=l.bitDepth||8;l.colorType=typeof l.colorType==="number"?l.colorType:t.COLORTYPE_COLOR_ALPHA;l.inputColorType=typeof l.inputColorType==="number"?l.inputColorType:t.COLORTYPE_COLOR_ALPHA;if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(l.colorType)===-1){throw new Error("option color type:"+l.colorType+" is not supported at present")}if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(l.inputColorType)===-1){throw new Error("option input color type:"+l.inputColorType+" is not supported at present")}if(l.bitDepth!==8&&l.bitDepth!==16){throw new Error("option bit depth:"+l.bitDepth+" is not supported at present")}};a.prototype.getDeflateOptions=function(){return{chunkSize:this._options.deflateChunkSize,level:this._options.deflateLevel,strategy:this._options.deflateStrategy}};a.prototype.createDeflate=function(){return this._options.deflateFactory(this.getDeflateOptions())};a.prototype.filterData=function(l,c,u){let d=i(l,c,u,this._options);let h=t.COLORTYPE_TO_BPP_MAP[this._options.colorType];let p=r(d,c,u,this._options,h);return p};a.prototype._packChunk=function(l,c){let u=c?c.length:0;let d=Buffer.alloc(u+12);d.writeUInt32BE(u,0);d.writeUInt32BE(l,4);if(c){c.copy(d,8)}d.writeInt32BE(s.crc32(d.slice(4,d.length-4)),d.length-4);return d};a.prototype.packGAMA=function(l){let c=Buffer.alloc(4);c.writeUInt32BE(Math.floor(l*t.GAMMA_DIVISION),0);return this._packChunk(t.TYPE_gAMA,c)};a.prototype.packIHDR=function(l,c){let u=Buffer.alloc(13);u.writeUInt32BE(l,0);u.writeUInt32BE(c,4);u[8]=this._options.bitDepth;u[9]=this._options.colorType;u[10]=0;u[11]=0;u[12]=0;return this._packChunk(t.TYPE_IHDR,u)};a.prototype.packIDAT=function(l){return this._packChunk(t.TYPE_IDAT,l)};a.prototype.packIEND=function(){return this._packChunk(t.TYPE_IEND,null)}}});var require_packer_async=__commonJS({"../../node_modules/pngjs/lib/packer-async.js"(e,n){"use strict";var t=require("util");var s=require("stream");var i=require_constants();var r=require_packer();var o=n.exports=function(a){s.call(this);let l=a||{};this._packer=new r(l);this._deflate=this._packer.createDeflate();this.readable=true};t.inherits(o,s);o.prototype.pack=function(a,l,c,u){this.emit("data",Buffer.from(i.PNG_SIGNATURE));this.emit("data",this._packer.packIHDR(l,c));if(u){this.emit("data",this._packer.packGAMA(u))}let d=this._packer.filterData(a,l,c);this._deflate.on("error",this.emit.bind(this,"error"));this._deflate.on("data",function(h){this.emit("data",this._packer.packIDAT(h))}.bind(this));this._deflate.on("end",function(){this.emit("data",this._packer.packIEND());this.emit("end")}.bind(this));this._deflate.end(d)}}});var require_sync_inflate=__commonJS({"../../node_modules/pngjs/lib/sync-inflate.js"(e,n){"use strict";var t=require("assert").ok;var s=require("zlib");var i=require("util");var r=require("buffer").kMaxLength;function o(d){if(!(this instanceof o)){return new o(d)}if(d&&d.chunkSize<s.Z_MIN_CHUNK){d.chunkSize=s.Z_MIN_CHUNK}s.Inflate.call(this,d);this._offset=this._offset===void 0?this._outOffset:this._offset;this._buffer=this._buffer||this._outBuffer;if(d&&d.maxLength!=null){this._maxLength=d.maxLength}}function a(d){return new o(d)}function l(d,h){if(h){process.nextTick(h)}if(!d._handle){return}d._handle.close();d._handle=null}o.prototype._processChunk=function(d,h,p){if(typeof p==="function"){return s.Inflate._processChunk.call(this,d,h,p)}let m=this;let y=d&&d.length;let g=this._chunkSize-this._offset;let f=this._maxLength;let w=0;let v=[];let b=0;let _;this.on("error",function(E){_=E});function $(E,A){if(m._hadError){return}let P=g-A;t(P>=0,"have should not go down");if(P>0){let x=m._buffer.slice(m._offset,m._offset+P);m._offset+=P;if(x.length>f){x=x.slice(0,f)}v.push(x);b+=x.length;f-=x.length;if(f===0){return false}}if(A===0||m._offset>=m._chunkSize){g=m._chunkSize;m._offset=0;m._buffer=Buffer.allocUnsafe(m._chunkSize)}if(A===0){w+=y-E;y=E;return true}return false}t(this._handle,"zlib binding closed");let k;do{k=this._handle.writeSync(h,d,w,y,this._buffer,this._offset,g);k=k||this._writeState}while(!this._hadError&&$(k[0],k[1]));if(this._hadError){throw _}if(b>=r){l(this);throw new RangeError("Cannot create final Buffer. It would be larger than 0x"+r.toString(16)+" bytes")}let S=Buffer.concat(v,b);l(this);return S};i.inherits(o,s.Inflate);function c(d,h){if(typeof h==="string"){h=Buffer.from(h)}if(!(h instanceof Buffer)){throw new TypeError("Not a string or buffer")}let p=d._finishFlushFlag;if(p==null){p=s.Z_FINISH}return d._processChunk(h,p)}function u(d,h){return c(new o(h),d)}n.exports=e=u;e.Inflate=o;e.createInflate=a;e.inflateSync=u}});var require_sync_reader=__commonJS({"../../node_modules/pngjs/lib/sync-reader.js"(e,n){"use strict";var t=n.exports=function(s){this._buffer=s;this._reads=[]};t.prototype.read=function(s,i){this._reads.push({length:Math.abs(s),allowLess:s<0,func:i})};t.prototype.process=function(){while(this._reads.length>0&&this._buffer.length){let s=this._reads[0];if(this._buffer.length&&(this._buffer.length>=s.length||s.allowLess)){this._reads.shift();let i=this._buffer;this._buffer=i.slice(s.length);s.func.call(this,i.slice(0,s.length))}else{break}}if(this._reads.length>0){throw new Error("There are some read requests waitng on finished stream")}if(this._buffer.length>0){throw new Error("unrecognised content at end of stream")}}}});var require_filter_parse_sync=__commonJS({"../../node_modules/pngjs/lib/filter-parse-sync.js"(e){"use strict";var n=require_sync_reader();var t=require_filter_parse();e.process=function(s,i){let r=[];let o=new n(s);let a=new t(i,{read:o.read.bind(o),write:function(l){r.push(l)},complete:function(){}});a.start();o.process();return Buffer.concat(r)}}});var require_parser_sync=__commonJS({"../../node_modules/pngjs/lib/parser-sync.js"(e,n){"use strict";var t=true;var s=require("zlib");var i=require_sync_inflate();if(!s.deflateSync){t=false}var r=require_sync_reader();var o=require_filter_parse_sync();var a=require_parser();var l=require_bitmapper();var c=require_format_normaliser();n.exports=function(u,d){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let h;function p(I){h=I}let m;function y(I){m=I}function g(I){m.transColor=I}function f(I){m.palette=I}function w(){m.alpha=true}let v;function b(I){v=I}let _=[];function $(I){_.push(I)}let k=new r(u);let S=new a(d,{read:k.read.bind(k),error:p,metadata:y,gamma:b,palette:f,transColor:g,inflateData:$,simpleTransparency:w});S.start();k.process();if(h){throw h}let E=Buffer.concat(_);_.length=0;let A;if(m.interlace){A=s.inflateSync(E)}else{let I=(m.width*m.bpp*m.depth+7>>3)+1;let O=I*m.height;A=i(E,{chunkSize:O,maxLength:O})}E=null;if(!A||!A.length){throw new Error("bad png - invalid inflate data response")}let P=o.process(A,m);E=null;let x=l.dataToBitMap(P,m);P=null;let T=c(x,m,d.skipRescale);m.data=T;m.gamma=v||0;return m}}});var require_packer_sync=__commonJS({"../../node_modules/pngjs/lib/packer-sync.js"(e,n){"use strict";var t=true;var s=require("zlib");if(!s.deflateSync){t=false}var i=require_constants();var r=require_packer();n.exports=function(o,a){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let l=a||{};let c=new r(l);let u=[];u.push(Buffer.from(i.PNG_SIGNATURE));u.push(c.packIHDR(o.width,o.height));if(o.gamma){u.push(c.packGAMA(o.gamma))}let d=c.filterData(o.data,o.width,o.height);let h=s.deflateSync(d,c.getDeflateOptions());d=null;if(!h||!h.length){throw new Error("bad png - invalid compressed data response")}u.push(c.packIDAT(h));u.push(c.packIEND());return Buffer.concat(u)}}});var require_png_sync=__commonJS({"../../node_modules/pngjs/lib/png-sync.js"(e){"use strict";var n=require_parser_sync();var t=require_packer_sync();e.read=function(s,i){return n(s,i||{})};e.write=function(s,i){return t(s,i)}}});var require_png=__commonJS({"../../node_modules/pngjs/lib/png.js"(e){"use strict";var n=require("util");var t=require("stream");var s=require_parser_async();var i=require_packer_async();var r=require_png_sync();var o=e.PNG=function(a){t.call(this);a=a||{};this.width=a.width|0;this.height=a.height|0;this.data=this.width>0&&this.height>0?Buffer.alloc(4*this.width*this.height):null;if(a.fill&&this.data){this.data.fill(0)}this.gamma=0;this.readable=this.writable=true;this._parser=new s(a);this._parser.on("error",this.emit.bind(this,"error"));this._parser.on("close",this._handleClose.bind(this));this._parser.on("metadata",this._metadata.bind(this));this._parser.on("gamma",this._gamma.bind(this));this._parser.on("parsed",function(l){this.data=l;this.emit("parsed",l)}.bind(this));this._packer=new i(a);this._packer.on("data",this.emit.bind(this,"data"));this._packer.on("end",this.emit.bind(this,"end"));this._parser.on("close",this._handleClose.bind(this));this._packer.on("error",this.emit.bind(this,"error"))};n.inherits(o,t);o.sync=r;o.prototype.pack=function(){if(!this.data||!this.data.length){this.emit("error","No data provided");return this}process.nextTick(function(){this._packer.pack(this.data,this.width,this.height,this.gamma)}.bind(this));return this};o.prototype.parse=function(a,l){if(l){let c,u;c=function(d){this.removeListener("error",u);this.data=d;l(null,this)}.bind(this);u=function(d){this.removeListener("parsed",c);l(d,null)}.bind(this);this.once("parsed",c);this.once("error",u)}this.end(a);return this};o.prototype.write=function(a){this._parser.write(a);return true};o.prototype.end=function(a){this._parser.end(a)};o.prototype._metadata=function(a){this.width=a.width;this.height=a.height;this.emit("metadata",a)};o.prototype._gamma=function(a){this.gamma=a};o.prototype._handleClose=function(){if(!this._parser.writable&&!this._packer.readable){this.emit("close")}};o.bitblt=function(a,l,c,u,d,h,p,m){c|=0;u|=0;d|=0;h|=0;p|=0;m|=0;if(c>a.width||u>a.height||c+d>a.width||u+h>a.height){throw new Error("bitblt reading outside image")}if(p>l.width||m>l.height||p+d>l.width||m+h>l.height){throw new Error("bitblt writing outside image")}for(let y=0;y<h;y++){a.data.copy(l.data,(m+y)*l.width+p<<2,(u+y)*a.width+c<<2,(u+y)*a.width+c+d<<2)}};o.prototype.bitblt=function(a,l,c,u,d,h,p){o.bitblt(this,a,l,c,u,d,h,p);return this};o.adjustGamma=function(a){if(a.gamma){for(let l=0;l<a.height;l++){for(let c=0;c<a.width;c++){let u=a.width*l+c<<2;for(let d=0;d<3;d++){let h=a.data[u+d]/255;h=Math.pow(h,1/2.2/a.gamma);a.data[u+d]=Math.round(h*255)}}}a.gamma=0}};o.prototype.adjustGamma=function(){o.adjustGamma(this)}}});async function devicectl(e,n=3e4){const t=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-devicectl-${(0,import_node_crypto.randomUUID)()}.json`);try{await execFileAsync2("xcrun",["devicectl",...e,"--quiet","--json-output",t],{timeout:n,maxBuffer:16*1024*1024});return JSON.parse(await import_node_fs.promises.readFile(t,"utf8"))}catch(s){const i=s;let r=(i.stderr||i.stdout||i.message||"").trim();try{const o=JSON.parse(await import_node_fs.promises.readFile(t,"utf8"));r=o.error?.userInfo?.NSLocalizedDescription?.string??r}catch{}if(/locked/i.test(r))throw new Error("The iPhone is locked. Unlock it and try again.");throw new Error(`devicectl ${e.slice(0,2).join(" ")} failed: ${r.split("\n").slice(-3).join(" ").slice(0,300)}`)}finally{await import_node_fs.promises.unlink(t).catch(()=>void 0)}}async function physicalPhones(){if(process.platform!=="darwin")return[];return parsePhones(await devicectl(["list","devices"],15e3).catch(()=>null))}function parsePhones(e){const n=e;return(n?.result?.devices??[]).filter(t=>t.hardwareProperties?.reality==="physical"&&t.hardwareProperties?.platform==="iOS"&&t.connectionProperties?.pairingState==="paired"&&t.connectionProperties?.tunnelState!=="unavailable").map(t=>({udid:t.hardwareProperties.udid,name:t.deviceProperties?.name??"iPhone"})).filter(t=>!!t.udid)}async function developerApps(e){const n=await devicectl(["device","info","apps","--device",e]);return(n.result?.apps??[]).filter(t=>t.bundleIdentifier&&!t.bundleIdentifier.startsWith("com.apple.")&&t.builtByDeveloper!==false).map(t=>({bundleId:t.bundleIdentifier,name:t.name??t.bundleIdentifier,path:t.url?.replace(/^file:\/\//,"")}))}async function readAppFile(e,n,t){const s=await devicectl(["device","info","files","--device",e,"--domain-type","appDataContainer","--domain-identifier",n],2e4);const i=new Set((s.result?.files??[]).map(o=>o.relativePath??o.name??""));if(!i.has(t))return null;const r=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-devfile-${(0,import_node_crypto.randomUUID)()}`);try{await devicectl(["device","copy","from","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",r],2e4);return await import_node_fs.promises.readFile(r)}finally{await import_node_fs.promises.rm(r,{recursive:true,force:true}).catch(()=>void 0)}}async function phoneInstallId(e,n){const t=await readAppFile(e,n,"Documents/react-buoy/@react_buoy_sync_device_id.json").catch(()=>null);const s=t?.toString("utf8").trim().replace(/^"|"$/g,"")??"";return/^[\w-]{4,64}$/.test(s)?s:null}async function phoneScreenshot(e){const n=import_node_path.default.join(import_node_os.default.tmpdir(),`buoy-phone-shot-${(0,import_node_crypto.randomUUID)()}.png`);try{await devicectl(["device","capture","screenshot","--device",e,"--destination",n],3e4);return await import_node_fs.promises.readFile(n)}finally{await import_node_fs.promises.unlink(n).catch(()=>void 0)}}function isAllBlack(e){try{const n=import_pngjs.PNG.sync.read(e);const t=Math.max(1,Math.floor(n.width*n.height/4e3));for(let s=0;s<n.width*n.height;s+=t){const i=s*4;if(n.data[i]>12||n.data[i+1]>12||n.data[i+2]>12)return false}return true}catch{return false}}async function phoneLaunch(e,n,t={}){await devicectl(["device","process","launch","--device",e,...t.terminateExisting?["--terminate-existing"]:[],...t.url?["--payload-url",t.url]:[],n])}async function phoneOpenUrl(e,n){await devicectl(["device","process","openURL","--device",e,n])}async function phoneQuit(e,n){const t=await developerApps(e);const s=t.find(o=>o.bundleId===n)?.path;const i=await devicectl(["device","info","processes","--device",e]);const r=(i.result?.runningProcesses??[]).find(o=>s&&decodeURIComponent(o.executable??"").replace(/^file:\/\//,"").startsWith(s.replace(/\/$/,"")));if(!r?.processIdentifier)return false;await devicectl(["device","process","terminate","--device",e,"--pid",String(r.processIdentifier)]);return true}async function phoneUninstall(e,n){await devicectl(["device","uninstall","app","--device",e,n],12e4)}async function phoneInstall(e,n){await devicectl(["device","install","app","--device",e,n],3e5)}async function writeAppFile(e,n,t,s){await devicectl(["device","copy","to","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",s],3e4)}function phoneRecordArgs(e,n,t){return["devicectl","device","capture","screen-record","--device",e,"--destination",n,"--duration",String(t)]}async function coreDeviceId(e){const n=coreIds.get(e);if(n)return n;const t=await devicectl(["list","devices"]);const s=t.result?.devices?.find(i=>i.hardwareProperties?.udid===e)?.identifier;if(!s)throw new Error("CoreDevice doesn't list this iPhone right now. Check that it is connected and unlocked.");coreIds.set(e,s);return s}var import_node_child_process3,import_node_util2,import_node_fs,import_node_os,import_node_path,import_node_crypto,import_pngjs,execFileAsync2,BLACK_PHONE_NOTE,coreIds;var init_iosDevice=__esm({"src/capture/iosDevice.ts"(){"use strict";import_node_child_process3=require("child_process");import_node_util2=require("util");import_node_fs=require("fs");import_node_os=__toESM(require("os"));import_node_path=__toESM(require("path"));import_node_crypto=require("crypto");import_pngjs=__toESM(require_png());execFileAsync2=(0,import_node_util2.promisify)(import_node_child_process3.execFile);BLACK_PHONE_NOTE="The picture is all black. If the app isn't really showing black, the iPhone's own screen is off: it is asleep, or the Mac shows it in iPhone Mirroring (the phone stays locked then). Quit iPhone Mirroring, unlock the phone and keep its screen on, then try again.";coreIds=new Map}});async function startRecording(e,n){await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(n),{recursive:true});const t=Date.now();let s;let i;if(e.platform==="ios"){s=(0,import_node_child_process4.spawn)("xcrun",["simctl","io",e.udid,"recordVideo","--codec=h264","--force",n],{stdio:["ignore","ignore","pipe"]})}else if(e.platform==="ios-device"){s=(0,import_node_child_process4.spawn)("xcrun",phoneRecordArgs(e.udid,n,MAX_RECORDING_MS/1e3),{stdio:["ignore","ignore","pipe"]})}else{i=`/sdcard/buoy-recording-${t}-${Math.random().toString(36).slice(2,8)}.mp4`;s=(0,import_node_child_process4.spawn)("adb",["-s",e.serial,"shell",`echo $$; exec screenrecord --time-limit ${MAX_RECORDING_MS/1e3} ${q(i)}`],{stdio:["ignore","pipe","pipe"]})}const r={host:e,file:n,startedAt:t,proc:s,remote:i};live.add(r);let o="";let a=t;let l;s.stderr?.on("data",d=>{o+=String(d);if(/Recording started/i.test(String(d)))a=Date.now()});s.stdout?.on("data",d=>{const h=/^(\d+)/m.exec(String(d));if(h&&l===void 0)l=Number(h[1])});const c=await new Promise(d=>{const h=setTimeout(()=>d(null),e.platform==="ios"?3e3:e.platform==="ios-device"?4e3:1200);const p=setInterval(()=>{if(e.platform==="ios"&&a!==t||e.platform==="android"&&l!==void 0){clearTimeout(h);clearInterval(p);d(null)}},50);s.once("exit",m=>{clearTimeout(h);clearInterval(p);d(m??-1)});s.once("error",()=>{clearTimeout(h);clearInterval(p);d(-1)})});live.delete(r);if(c!==null){if(/not supported by this device/i.test(o)){throw new Error("This iPhone can't record its screen through Xcode (devicectl says the device doesn't support it). Record on a simulator, or use the iPhone's own Screen Recording in Control Center.")}throw new Error(`The screen recorder stopped right away: ${o.trim().slice(0,300)||`exit ${c}`}`)}if(e.platform==="ios"&&s.pid){const d=s.pid;const h=(0,import_node_child_process4.spawn)("sh",["-c",`sleep ${MAX_RECORDING_MS/1e3+15}; ps -p ${d} -o command= | grep -q 'simctl io .* recordVideo' && kill -INT ${d}`],{detached:true,stdio:"ignore"});h.on("error",()=>void 0);h.unref();s.once("exit",()=>{try{if(h.pid)process.kill(-h.pid,"SIGTERM")}catch{h.kill("SIGTERM")}})}const u={host:e,file:n,startedAt:a,proc:s,remote:i,remotePid:l};live.add(u);return u}async function stopRecording(e){if(e.stopTimer)clearTimeout(e.stopTimer);const n=Date.now();const t=new Promise(r=>{if(e.proc.exitCode!==null||e.proc.signalCode!==null)return r();e.proc.once("exit",()=>r())});if(e.host.platform==="ios"||e.host.platform==="ios-device"){e.proc.kill("SIGINT")}else if(e.remotePid){await adbShell(e.host.serial,`kill -INT ${e.remotePid}`).catch(()=>void 0)}await Promise.race([t,new Promise(r=>setTimeout(r,e.host.platform==="ios-device"?6e4:1e4))]);if(e.proc.exitCode===null&&e.proc.signalCode===null)e.proc.kill("SIGKILL");live.delete(e);if(e.host.platform==="android"&&e.remote){await new Promise(r=>setTimeout(r,800));await adb(e.host.serial,["pull",e.remote,e.file],6e4);await adbShell(e.host.serial,`rm -f ${q(e.remote)}`).catch(()=>void 0)}const s=await import_node_fs2.promises.stat(e.file).catch(()=>null);if(!s||s.size<1e3)throw new Error("The recording file is missing or empty. Was the screen showing anything?");const i=n-e.startedAt;if(e.host.platform==="ios"||e.host.platform==="ios-device")await padToWallTime(e.file,i).catch(()=>void 0);return i}async function padToWallTime(e,n){const t=(await execFileAsync3("ffprobe",["-v","error","-show_entries","format=duration","-of","csv=p=0",e],{timeout:2e4})).stdout;const s=Number(t.trim());const i=n/1e3-.3-s;if(!Number.isFinite(s)||i<.25)return;const r=e.replace(/\.mp4$/,".pad.mp4");const o=a=>execFileAsync3("ffmpeg",["-y","-v","error","-i",e,"-vf",`tpad=stop_mode=clone:stop_duration=${i.toFixed(2)},fps=30`,"-an",...a,"-pix_fmt","yuv420p","-movflags","+faststart",r],{timeout:24e4,maxBuffer:4*1024*1024});try{await o(["-c:v","h264_videotoolbox","-b:v","6M"])}catch{await o(["-c:v","libx264","-preset","veryfast","-crf","22"])}await import_node_fs2.promises.rename(r,e)}function stopAllRecordingsNow(){for(const e of live){try{if(e.host.platform!=="android")e.proc.kill("SIGINT");else if(e.remotePid)(0,import_node_child_process4.spawn)("adb",["-s",e.host.serial,"shell",`kill -INT ${e.remotePid}`],{stdio:"ignore",detached:true}).unref()}catch{}}live.clear()}function markPoint(e,n,t,s){const i=n.width!==n.height&&t.width!==t.height&&n.width>n.height!==t.width>t.height;if(!i)return e;const r=s==="landscapeRight";const o=n.width>n.height?!r:r;return o?[1-e[1],e[0]]:[e[1],1-e[0]]}function pinchPath(e){if(e.kind!=="pinch"||e.from.length!==2||e.to?.length!==2)return null;const{width:n,height:t}=e.screen;const s=h=>[h[0]*n,h[1]*t];const[i,r,o,a]=[s(e.from[0]),s(e.from[1]),s(e.to[0]),s(e.to[1])];const l=[(i[0]+r[0])/2,(i[1]+r[1])/2];const c=[(o[0]+a[0])/2,(o[1]+a[1])/2];const u=Math.atan2(i[1]-l[1],i[0]-l[0]);let d=Math.atan2(o[1]-c[1],o[0]-c[0])-u;if(d>Math.PI)d-=2*Math.PI;if(d<-Math.PI)d+=2*Math.PI;return{c0:l,c1:c,r0:Math.hypot(i[0]-l[0],i[1]-l[1]),r1:Math.hypot(o[0]-c[0],o[1]-c[1]),a0:u,turn:d}}function touchDotNote(e){const n=Math.max(0,e.reduce((t,s)=>t+s.from.length,0)-MAX_BURN_DOTS);return n?`${n} touch dots were left out of the MP4. The viewer keeps all dots.`:null}async function burnTouchMarks(e,n,t){let s;try{s=(await execFileAsync3("ffprobe",["-v","error","-select_streams","v:0","-show_entries","stream=width,height","-of","csv=p=0:s=x",e],{timeout:2e4})).stdout}catch(y){if(y.code==="ENOENT")return null;throw y}const[i,r]=s.trim().split("x").map(Number);if(!i||!r)throw new Error("could not read the video's size");const o={width:i,height:r};const a=Math.max(8,Math.round(Math.min(i,r)*.045));const l=a*2;const c=Math.max(2,Math.round(a*.18));const u=y=>y.toFixed(3);const d=[];for(const y of n){const g=y.t/1e3;const f=Math.max(y.ms,1)/1e3;const w=`clip((t-${u(g)})/${u(f)},0,1)`;const v=`enable='between(t,${u(g)},${u(g+f+.35)})':eval=frame:shortest=1`;const b=pinchPath(y);if(b){const{width:_,height:$}=y.screen;const k=markPoint([0,0],y.screen,o,y.orientation);const S=markPoint([1,0],y.screen,o,y.orientation);const E=markPoint([0,1],y.screen,o,y.orientation);[1,-1].forEach(A=>{const P=`(${u(b.r0)}+(${u(b.r1-b.r0)})*${w})`;const x=`(${u(b.a0)}+(${u(b.turn)})*${w})`;const T=`clip((${u(b.c0[0])}+(${u(b.c1[0]-b.c0[0])})*${w}+${A}*${P}*cos(${x}))/${u(_)},0,1)`;const I=`clip((${u(b.c0[1])}+(${u(b.c1[1]-b.c0[1])})*${w}+${A}*${P}*sin(${x}))/${u($)},0,1)`;const[O,N]=[0,1].map(F=>`(${k[F]}+(${S[F]-k[F]})*${T}+(${E[F]-k[F]})*${I})`);d.push(`overlay=x='${O}*${i}-${a}':y='${N}*${r}-${a}':${v}`)});continue}y.from.forEach((_,$)=>{const[k,S]=markPoint(_,y.screen,o,y.orientation);const[E,A]=markPoint(y.to?.[$]??_,y.screen,o,y.orientation);d.push(`overlay=x='${u(k*i-a)}+(${u((E-k)*i)})*${w}':y='${u(S*r-a)}+(${u((A-S)*r)})*${w}':${v}`)})}const h=d.slice(0,MAX_BURN_DOTS);const p=["[0:v]fps=30[base]",`[1:v]split=${h.length}${h.map((y,g)=>`[c${g}]`).join("")}`,...h.map((y,g)=>`${g===0?"[base]":`[v${g-1}]`}[c${g}]${y}[v${g}]`)].join(";");const m=`color=c=white@0:s=${l}x${l},format=rgba,geq=r='255':g='255':b='255':a='if(lte(hypot(X-${a},Y-${a}),${a}),if(gte(hypot(X-${a},Y-${a}),${a-c}),235,105),0)'`;await execFileAsync3("ffmpeg",["-y","-v","error","-i",e,"-f","lavfi","-i",m,"-filter_complex",p,"-map",`[v${h.length-1}]`,"-an","-c:v","libx264","-preset","veryfast","-crf","22","-pix_fmt","yuv420p","-movflags","+faststart",t],{timeout:24e4,maxBuffer:4*1024*1024});return t}function redact(e){return e.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,"$1 [redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,"[redacted jwt]").replace(/((?:api[_-]?key|token|secret|password|passwd|authorization|cookie|session)["']?\s*[:=]\s*["']?)([^\s"'&,;}]{4,})/gi,"$1[redacted]")}function viewerHtml(e){const n=(e.marks??[]).map(s=>({...s,arc:pinchPath(s)??void 0}));const t=JSON.stringify({...e,marks:n}).replace(/</g,"\\u003c");return`<!doctype html>
|
|
3
3
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4
4
|
<title>${escapeHtml(e.title)}</title>
|
|
5
5
|
<style>
|
|
@@ -51,8 +51,8 @@ const c=(n)=>Math.min(1,Math.max(0,n));for(const s of [1,-1])dot(place([c((cx+s*
|
|
|
51
51
|
m.from.forEach((p,i)=>{const q=(m.to&&m.to[i])||p;const a=place(p,m),b=place(q,m);dot([a[0]+(b[0]-a[0])*k,a[1]+(b[1]-a[1])*k])})}}
|
|
52
52
|
(function tick(){marks();requestAnimationFrame(tick)})();
|
|
53
53
|
<\/script></body></html>
|
|
54
|
-
`}function escapeHtml(e){return e.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var import_node_child_process4,import_node_util3,import_node_fs2,import_node_path2,MAX_RECORDING_MS,live,MAX_BURN_DOTS,execFileAsync3;var init_recorder=__esm({"src/capture/recorder.ts"(){"use strict";import_node_child_process4=require("child_process");import_node_util3=require("util");import_node_fs2=require("fs");import_node_path2=__toESM(require("path"));init_adb();init_iosDevice();MAX_RECORDING_MS=3*6e4;live=new Set;MAX_BURN_DOTS=300;execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process4.execFile)}});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 s;try{s=(0,import_node_fs3.readFileSync)((0,import_node_path3.join)(e,t),"utf8")}catch{continue}const i=new Map;for(const r of s.split(/\r?\n/)){const o=r.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!o)continue;const a=o[2].trim();const l=a.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);i.set(o[1],l?l[2]:a.split("#")[0].trim())}for(const r of KEY_NAMES)if(i.has(r))return(0,import_headless.normalizeLicenseKey)(i.get(r))||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 l=Math.min(Date.now(),a.claims.iat*1e3);const c=a.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${a.claims.sid}`,valid:true,tier:c,isPro:c==="pro",expiresAt:new Date(a.claims.exp*1e3).toISOString(),cachedAt:l,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 l=Date.now();const c=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(c).toISOString(),cachedAt:l,lastValidatedAt:l});this.persistCache();this.retry=0;this.nextCheck=Math.max(l+6e4,c-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 l=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${a.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:l,lastValidatedAt:l});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 s=t.data?.relationships?.product?.data?.id;const i=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(s&&s!==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 r=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:r,isPro:r==="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 s=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(s??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:s}=await execFileAsync4("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const i=s.split("\n").map(a=>a.trim()).find(Boolean);if(!i)return null;const{stdout:r}=await execFileAsync4("lsof",["-a","-p",i,"-d","cwd","-Fn"],{timeout:3e3});const o=r.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(s=>s?.address?.toLowerCase()===t)}function pickByProject(e,n){const t=import_node_path4.default.resolve(n);const s=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(s.length===0)return null;const i=Math.min(...s.map(o=>o.score));const r=s.filter(o=>o.score===i);return r.length===1?r[0].deviceId:null}function closeness(e,n){if(e===n)return 0;const t=import_node_path4.default.relative(e,n);const s=import_node_path4.default.relative(n,e);const i=r=>r&&!r.startsWith("..")&&!import_node_path4.default.isAbsolute(r);if(i(t))return t.split(import_node_path4.default.sep).length;if(i(s))return s.split(import_node_path4.default.sep).length;return-1}var import_node_child_process5,import_node_util4,import_node_path4,import_node_os3,execFileAsync4;var init_projectRoot=__esm({"src/sync/projectRoot.ts"(){"use strict";import_node_child_process5=require("child_process");import_node_util4=require("util");import_node_path4=__toESM(require("path"));import_node_os3=__toESM(require("os"));execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process5.execFile)}});var policy_exports={};__export(policy_exports,{FREE_SUMMARY:()=>FREE_SUMMARY,accessFor:()=>accessFor,currentAccess:()=>currentAccess,freeToolNames:()=>freeToolNames,hasProAccess:()=>hasProAccess,isFreeAppCall:()=>isFreeAppCall,isFreeTool:()=>isFreeTool,isWeekendPass:()=>isWeekendPass,meetsAccess:()=>meetsAccess,outsideCalls:()=>outsideCalls,proRequiredMessage:()=>proRequiredMessage,runWithAccess:()=>runWithAccess,setClockForTests:()=>setClockForTests,weekendNow:()=>weekendNow});function isFreeAppCall(e,n){return Object.prototype.hasOwnProperty.call(FREE_APP_CALLS,e)&&FREE_APP_CALLS[e].includes(n)}function setClockForTests(e){clock=e??(()=>new Date)}function isWeekendPass(e=clock()){const n=e.getDay();return n===0||n===6}function accessFor(e,n){const t=isWeekendPass();if(!Object.prototype.hasOwnProperty.call(FREE_TOOLS,e))return{tool:e,level:"pro",weekend:t};const s=FREE_TOOLS[e];const i=s?s(n&&typeof n==="object"?n:{}):null;return i?{tool:e,level:"pro",proOption:i,weekend:t}:{tool:e,level:"free",weekend:t}}function weekendNow(){return scope.getStore()?.weekend??isWeekendPass()}function hasProAccess(e){if(!e)return false;if(e.isPro===true)return true;return e.tier==="free"&&weekendNow()}function isFreeTool(e){return Object.prototype.hasOwnProperty.call(FREE_TOOLS,e)}function freeToolNames(){return Object.keys(FREE_TOOLS).sort()}function runWithAccess(e,n){return scope.run(Object.freeze({...e}),n)}function outsideCalls(e){return scope.exit(e)}function currentAccess(){return scope.getStore()??{tool:"",level:"pro"}}function meetsAccess(e,n){if(!e)return false;if(hasProAccess(e))return true;return n==="free"&&e.tier==="free"}function proRequiredMessage(e,n){const t=e.proOption?`\`${e.tool}\` with \`${e.proOption}\` needs Buoy Pro.`:e.tool?`\`${e.tool}\` needs Buoy Pro.`:"This needs Buoy Pro.";return`${t}${n} ${FREE_SUMMARY} Tell the user Pro unlocks the rest (https://buoy.gg/pricing). To add a license, enter it in the app, or pass \`licenseKey\` to \`<FloatingDevTools />\` for an app with no on-device UI. No setting, argument or other tool turns Pro on, so don't try to work around it.`}var import_node_async_hooks,FREE_TOOLS,FREE_APP_CALLS,clock,scope,FREE_SUMMARY;var init_policy=__esm({"src/access/policy.ts"(){"use strict";import_node_async_hooks=require("async_hooks");FREE_TOOLS={list_devices:null,list_tool_actions:null,camera_diagnose:null,describe_screen:e=>e.includeBuoy?"includeBuoy":null,tap_element:e=>e.includeBuoy?"includeBuoy":null,native_tap:e=>e.allowBuoy?"allowBuoy":null,native_type:null,native_swipe:e=>e.allowBuoy?"allowBuoy":null,native_pinch:e=>e.allowBuoy?"allowBuoy":null,native_inspect:null,run_flow:null,screenshot_component:null,screen_recording:null,device_control:null,reload_app:null,navigate:null,get_routes:null,get_console:null,get_network_requests:e=>e.includeBodies?"includeBodies":null,get_triage:null,run_benchmark_batch:null,wait_for_automation:null,get_automation_status:null,get_batch_report:null,compare_reports:null,get_benchmark_settings:null,set_benchmark_settings:null,get_js_thread_top:null,run_web_benchmark:e=>e.traceDir?"traceDir":null,find_web_ceiling:null,screenshot_web_variants:null,start_optimize_session:null,sim_farm_status:null,sim_farm_setup:null,sim_farm_acquire:null,sim_farm_release:null,sim_farm_action:null,sim_farm_lock:null,sim_farm_report:null,sim_farm_extend:null,sim_farm_finish:null};FREE_APP_CALLS={"highlight-updates":["describeScreen","tapElement","hitTest","readMaps","setSilentTracking"],"route-events":["@snapshot","getCurrentRoute","navigate"],app:["@snapshot","ping","reloadApp"],console:["@snapshot"],network:["@snapshot"],storage:["async.getAllKeys","secure.keys"],"perf-monitor":["@snapshot","getAutomationConfig","setAutomationConfig","acknowledgeAutomation","setEnabled","refreshIndex","startAutomation","loadReport"],"js-top":["sample"]};clock=()=>new Date;scope=new import_node_async_hooks.AsyncLocalStorage;FREE_SUMMARY="Free covers reading the screen, taps, swipes and typing, run_flow, screenshots, screen recording, device_control, reload_app, navigate, get_routes, get_console, the request list and get_triage. It also covers Buoy Optimize and the Sim Farm: benchmark batches, reports, web benchmarks and JS thread totals, without the render names, script names and JS origins that say why. Every Saturday and Sunday a free account gets everything Pro does."}});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 s=import_node_http.default.get(e,i=>{i.resume();t((i.statusCode??0)>0)});s.setTimeout(n,()=>{s.destroy();t(false)});s.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_policy();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.transportLost=false;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}
|
|
55
|
-
`));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:s=>{if(s.code==="EADDRINUSE")t=true;this.log(`broker error: ${s.code??s.message}`)}});await waitForBroker(this.url).catch(s=>{if(!t)throw s});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(s){this.log(`could not write the broker token file: ${s.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 outsideCalls(()=>new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:a=>{const l=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));a(l?{dashboardToken:l}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let s=false;const i=()=>{if(s)return;s=true;this.log("connected to broker");e()};const r=setTimeout(()=>{if(s)return;s=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 l=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(l);if(l){clearTimeout(r);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(s)return;s=true;clearTimeout(r);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(l=>l.toolId==="console"))this.ensureWatching("console",a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let l=false;for(const u of a){if(u.deviceId&&(u.tier==="locked"||u.tier==="checking"))this.lockDevice(String(u.deviceId),u.tier==="checking");else if(u.deviceId&&(u.tier==="free"||u.tier==="pro")){l=this.lockedDevices.delete(String(u.deviceId))||l}}this.devices=a;const c=Date.now();for(const u of a)if(u.deviceId)this.lastSeen.set(u.deviceId,c);this.pruneStaleDevices();if(l)this.rearmWatches();else this.rearmReconnectedDevices(a)});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const l=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(l,a.payload);this.snapshotAt.set(l,Date.now());if(a.toolId==="console")this.noteCrashReport(a.persistentDeviceId,a.payload);const c=this.snapshotWaiters.get(l);if(c&&c.length){this.snapshotWaiters.set(l,[]);for(const u of c)u(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const l=this.pendingActions.get(a.requestId);if(!l)return;this.pendingActions.delete(a.requestId);clearTimeout(l.timer);l.resolve(a)})}))}isAdmitted(){return this.admitted}requireAdmission(){if(this.transportLost)throw new Error("The broker link was lost. Wait for it to reconnect.");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.transportLost=!e&&n;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(n?"The broker link was lost during the call. Its result is unknown.":"Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const s of t)s(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.devices.some(n=>n.deviceId===e&&n.tier==="checking"))throw new Error(`${NOT_READY_MARK} The new app link is still checking access.`);if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e,n=false){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 t=`${e}::`;for(const s of this.snapshots.keys())if(s.startsWith(t)){this.snapshots.delete(s);this.snapshotAt.delete(s)}for(const[s,i]of this.snapshotWaiters)if(s.startsWith(t)){this.snapshotWaiters.delete(s);for(const r of i)r(void 0)}for(const[s,i]of this.pendingActions)if(i.target===e||i.target===ALL_DEVICES){clearTimeout(i.timer);this.pendingActions.delete(s);i.reject(new Error(n?"The app link changed during the call. Its result is unknown.":"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 s=`${n}::`;for(const i of[...this.snapshots.keys()]){if(i.startsWith(s)){this.snapshots.delete(i);this.snapshotAt.delete(i)}}for(const i of[...this.watching.keys()]){if(i.startsWith(s))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==="checking"?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(s=>s.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}peekProjectDevice(){if(!this.admitted)return null;if(this.sessionDevice)return this.sessionDevice;const e=this.connectedDevices().filter(t=>!this.lockedDevices.has(t.deviceId)&&t.tools.length>0);const n=process.env.BUOY_PROJECT_ROOT||process.cwd();return pickByProject(e.map(t=>({deviceId:t.deviceId,root:this.devServerRoots.get(t.deviceId)})),n)??null}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(c,u)=>new Error(`${NOT_READY_MARK} "${c}" (${u}) 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 c=this.devices.find(u=>u.deviceId===this.sessionDevice);if(c&&c.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const s=t.filter(c=>!this.lockedDevices.has(c.deviceId));const i=s.filter(c=>c.tools.length>0);const r=process.env.BUOY_PROJECT_ROOT||process.cwd();const o=pickByProject(i.map(c=>({deviceId:c.deviceId,root:this.devServerRoots.get(c.deviceId)})),r);if(o)return this.sessionDevice=o;const a=pickByProject(this.devices.filter(c=>c.deviceId&&c.isConnected===false&&!this.lockedDevices.has(c.deviceId)).map(c=>({deviceId:c.deviceId,root:this.devServerRoots.get(c.deviceId)})),r);if(a)throw n(a,"this project's app");if(s.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(c=>c.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(s.length===1)return this.sessionDevice=s[0].deviceId;if(i.length===1)return this.sessionDevice=i[0].deviceId;if(i.length===0){const c=s.map(u=>u.deviceName).join(", ");throw new Error(`Connected devices (${c}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const l=i.map(c=>`${c.deviceName} (${c.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${l}`)}requireProDevice(e){this.requireAdmission();const n=currentAccess();if(e&&e!==ALL_DEVICES)this.sessionDevice=e;const t=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const s=o=>n.level==="pro"?proRequiredMessage(n,t(o)):`The Buoy MCP needs an app on a Buoy account.${t(o)} Run \`npx buoy login\` in the project (free), restart the dev server with its cache cleared, then run \`list_devices\` to check.`;if(e===ALL_DEVICES){const o=this.connectedDevices().filter(l=>l.isPro||l.tier==="free");const a=o.find(l=>!meetsAccess(l,n.level));if(o.length===0)throw new Error(s());if(a)throw new Error(s(a));return ALL_DEVICES}const i=this.resolveDevice(e);const r=this.connectedDevices().find(o=>o.deviceId===i);if(!meetsAccess(r,n.level)){const o=this.accessState(i,n.level);if(o==="offline")throw new Error(`${NOT_READY_MARK} "${i}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(o==="pending")throw new Error(`${NOT_READY_MARK} account access for "${i}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(s(r))}return i}checkAppCall(e,n,t){const s=currentAccess();if(!s.tool)return;const i=t===ALL_DEVICES?this.connectedDevices().find(r=>r.tier==="free"&&!hasProAccess(r)):this.listDevices().find(r=>r.deviceId===t&&!hasProAccess(r));if(!i)return;if(s.level==="free"&&isFreeAppCall(e,n))return;throw new Error(proRequiredMessage({...s,level:"pro"},` The app "${i.deviceName}" is on the ${i.tier} tier${s.level==="free"?`, and this part of \`${s.tool}\` reads its ${e} data, which is Pro`:""}.`))}refuseBuoyPanelTap(e,n,t,s){if(e!=="highlight-updates"||n!=="tapElement")return null;const i=currentAccess();if(!i.tool||i.level!=="free")return null;const r=t??{};const o=typeof r.nativeTag==="number"?r.nativeTag:null;const a=typeof r.testID==="string"&&r.testID?r.testID.toLowerCase():null;const l=o!=null||a!=null||!!r.includeBuoy;if(s===ALL_DEVICES){if(!l||!this.connectedDevices().some(d=>d.tier==="free"&&!hasProAccess(d)))return null;return Promise.reject(new Error(proRequiredMessage({...i,level:"pro"},' On a free app, tap one app at a time (not "All") when tapping by testID or nativeTag.')))}const c=this.listDevices().find(d=>d.deviceId===s);if(!c||hasProAccess(c)||!l)return null;const u=d=>new Error(proRequiredMessage({...i,level:"pro"},d));if(r.includeBuoy)return Promise.reject(u(" That reaches Buoy's own panel, not the app."));return(async()=>{let d;try{d=await this.callAction("highlight-updates","describeScreen",{includeBuoy:false,includeHidden:true},s,{timeoutMs:4e3,silent:true})}catch(p){throw new Error(`Could not check that target before tapping (${p instanceof Error?p.message:String(p)}). On a free app, tap by query, or retry once the screen settles.`)}const h=(d?.elements??[]).some(p=>o!=null?p.nativeTag===o:p.testID?.toLowerCase()===a);if(!h&&o==null&&typeof r.query==="string"){const{testID:p,...m}=r;return m}if(!h){throw new Error(`"${o??String(r.testID)}" is not on the app's screen right now. On a free app, a tap by testID or nativeTag must hit an element describe_screen lists, and Buoy's own panels are Pro. Swipe it into view first, or tap by query.`)}})()}fullResultsFor(e){if(e===ALL_DEVICES){const n=this.connectedDevices();return n.length>0&&n.every(t=>hasProAccess(t))}return hasProAccess(this.listDevices().find(n=>n.deviceId===e))}deviceWasAllowed(e){return meetsAccess(this.listDevices().find(n=>n.deviceId===e),currentAccess().level)}accessState(e,n=currentAccess().level){const t=this.devices.find(s=>s.deviceId===e);if(!t||t.isConnected===false)return"offline";if(hasProAccess(t)||n==="free"&&t.tier==="free")return"ready";return t.tier===void 0||t.tier==="checking"?"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 s=this.healthOf(e);const{timeoutMs:i,usedFullBudget:r}=actionTimeoutMs({consecutiveTimeouts:s.consecutiveTimeouts,lastFullBudgetAt:s.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(r)s.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 s=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`:""}).`,s?"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 r=this.lastFatalFor(e);if(r)i.push(`Last captured error: ${r}`);i.push(r?"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 s=Math.max(...t.map(o=>typeof o?.timestamp==="number"?o.timestamp:-Infinity));const i=this.crashSeen.get(e)??new Set;let r=false;for(const o of t){const a=o?.fatal||/^\[(RENDER ERROR|FATAL)/.test(o?.message??"");if(!a||typeof o.timestamp!=="number")continue;const l=`${o.id??""}|${o.timestamp}|${(o.message??"").slice(0,120)}`;if(i.has(l))continue;i.add(l);if(s-o.timestamp<3e4)r=true}if(i.size>200)for(const o of[...i].slice(0,i.size-200))i.delete(o);this.crashSeen.set(e,i);if(!r)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 s=t.length-1;s>=0;s--){const i=t[s];if(i?.fatal||i?.level==="error"){const r=(i.message??"").split("\n")[0];if(r)return r.length>300?`${r.slice(0,300)}\u2026`:r}}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 s=devServerRoot(n).then(i=>{if(i&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,i)});this.trackRootLookup(s)}}}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 s=`${n.id}|${n.connectionNonce??""}`;const i=this.deviceConnections.get(t);this.deviceConnections.set(t,s);if(i===void 0||i===s)continue;for(const{toolId:r,targetDeviceId:o}of this.watching.values()){if(o===t)this.sendWatch(r,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);this.checkAppCall(e,"@snapshot",n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const s=this.resolveDevice(n);this.checkAppCall(e,"@snapshot",s);const i=snapKey(s,e);this.ensureWatching(e,s);let r=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(s)){const c=this.healthOf(s);if(Date.now()-c.lastFullBudgetAt>RECOVERY_PROBE_MS){c.lastFullBudgetAt=Date.now()}else{r=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const o=this.waitForSnapshot(i,r);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:s});const a=await o;this.requireAdmission();this.requireDeviceAccount(s);if(a!==void 0){if(!t.silent)this.noteOk(s);return{deviceId:s,payload:a}}if(!t.silent)this.noteTimeout(s);const l=this.snapshots.get(i);if(l!==void 0){return{deviceId:s,payload:l,stale:this.staleNote(e,s)}}throw new Error(`Timed out waiting for "${e}" state from ${s} (is the tool active?). ${this.unresponsiveNote(s)}`)}lastSync(e,n){this.checkAppCall(n,"@snapshot",e);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 s=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${s}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const s=this.snapshotWaiters.get(e)??[];const i=setTimeout(()=>{const o=(this.snapshotWaiters.get(e)??[]).filter(a=>a!==r);this.snapshotWaiters.set(e,o);t(void 0)},n);const r=o=>{clearTimeout(i);t(o)};s.push(r);this.snapshotWaiters.set(e,s)})}async callAction(e,n,t,s,i={}){this.requireAdmission();const r=s===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(s);this.checkAppCall(e,n,r);const o=this.refuseBuoyPanelTap(e,n,t,r);if(o)t=await o??t;if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto3.randomUUID)();const l=i.timeoutMs??this.timeoutFor(r,e,n);const c=await new Promise((u,d)=>{const h=Date.now();const p=()=>{this.pendingActions.delete(a);if(r!==ALL_DEVICES&&!i.silent)this.noteTimeout(r);d(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-h}ms. `+(r===ALL_DEVICES?"No device responded.":this.unresponsiveNote(r))))};const m=setTimeout(p,l);this.pendingActions.set(a,{resolve:u,reject:d,timer:m,target:r,expire:p,deadline:h+l});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:r,action:n,params:t??{}})});if(r!==ALL_DEVICES&&!i.silent)this.noteOk(r);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.69"?"7.0.69":"0.0.0-dev";PACKAGE_NAME="@buoy-gg/mcp"}});function isNewer(e,n){const t=r=>r.split("-")[0].split(".").map(o=>Number(o)||0);const s=t(e);const i=t(n);for(let r=0;r<3;r++){const o=s[r]??0;const a=i[r]??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 s=await fetch(REGISTRY_URL,{signal:n.signal});if(!s.ok)return null;const i=await s.json();const r=typeof i.version==="string"?i.version:null;if(r){await import_node_fs4.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:r})).catch(()=>{})}return r}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,s)=>e[s]===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 s of await listDir(t)){const i=import_node_path6.default.join(t,s,"WebsiteData","Default");for(const r of await listDir(i)){for(const o of await listDir(import_node_path6.default.join(i,r))){const a=import_node_path6.default.join(i,r,o,"LocalStorage","localstorage.sqlite3");try{await import_node_fs5.promises.access(a);const{stdout:l}=await execFileAsync5("sqlite3",["-readonly",a,`SELECT hex(value) FROM ItemTable WHERE key='${WEBVIEW_ID_KEY}' LIMIT 1;`],{timeout:1500});const c=l.trim();if(c)n.push(Buffer.from(c,"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:s}=await execFileAsync5("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const i=JSON.parse(s).devices;n=Object.entries(i).filter(([r])=>/iOS/.test(r)).flatMap(([,r])=>r.filter(o=>o.state==="Booted"))}catch(s){return{ok:false,error:humanizeSimctlError(s)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const s of n){if(!s.dataPath)continue;const i=import_node_path6.default.join(s.dataPath,"Containers","Data","Application");let r=[];try{r=await import_node_fs5.promises.readdir(i)}catch{continue}for(const o of r){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:s.udid,how:`matched ${e} to "${s.name}"`}}catch{}}}}if(t&&e){const s=n.map(l=>l.udid).sort().join(",");const i=webViewLookups.get(e);const r=i?.booted===s&&(i.udid!==null||Date.now()-i.at<WEBVIEW_MISS_MS);const o=r&&i?.udid?n.find(l=>l.udid===i.udid):void 0;if(o)return{ok:true,udid:o.udid,how:`matched ${e} to "${o.name}" (WebView storage)`};const a=[];if(!r){const l=await Promise.all(n.map(c=>c.dataPath?webViewInstallIds(import_node_path6.default.join(c.dataPath,"Containers","Data","Application")):Promise.resolve([])));l.forEach((c,u)=>{if(c.includes(t))a.push(n[u])});webViewLookups.set(e,{booted:s,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(l=>l.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(s=>s.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 execFileAsync5("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const r=JSON.parse(i).devices;t=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return n}const s=new Map;for(const i of t){if(!i.dataPath)continue;const r=import_node_path6.default.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await import_node_fs5.promises.readdir(r)}catch{continue}for(const a of o){try{const l=(await import_node_fs5.promises.readFile(import_node_path6.default.join(r,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(l)s.set(l,{name:i.name,udid:i.udid})}catch{}}}for(const i of e){const r=s.get(i.split("-").pop()??"");if(r)n.set(i,r)}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 execFileAsync5("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 s=pngSize(t);if(!s)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:s.width,height:s.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function cropToBuffer(e,n,t){const s=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 r=clamp(Math.round(t.width),1,n.width-s);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 execFileAsync5("sips",["-c",String(o),String(r),"--cropOffset",String(i),String(s),e,"--out",a],{timeout:1e4});const l=await import_node_fs5.promises.readFile(a);return{ok:true,buffer:l,width:r,height:o}}catch(l){const c=l;return{ok:false,error:`Crop failed: ${c?.stderr||c?.message||l}`}}finally{await import_node_fs5.promises.unlink(a).catch(()=>{});await import_node_fs5.promises.unlink(e).catch(()=>{})}}var import_node_child_process6,import_node_util5,import_node_fs5,import_node_os5,import_node_path6,import_node_crypto4,execFileAsync5,clamp,WEBVIEW_ID_KEY,webViewLookups,WEBVIEW_MISS_MS;var init_simctl=__esm({"src/capture/simctl.ts"(){"use strict";import_node_child_process6=require("child_process");import_node_util5=require("util");import_node_fs5=require("fs");import_node_os5=__toESM(require("os"));import_node_path6=__toESM(require("path"));import_node_crypto4=require("crypto");execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process6.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}});var PROTOCOL_VERSION,STATE_VERSION,DEFAULT_SETTINGS,FarmError;var init_types=__esm({"../sim-farm/src/types.ts"(){"use strict";PROTOCOL_VERSION=1;STATE_VERSION=1;DEFAULT_SETTINGS={parkAfterMinutes:10,maxConcurrentBoots:3,reserveRamGB:24,reserveDiskGB:40,slimSimGB:2,stockSimGB:6,cloneDiskGB:2,freezePilot:false,runCapMinutes:60,keepRunsHours:24,fairMinFreeShare:.25,fairMaxSwingShare:.12,fairMaxThermalLevel:0,fairMaxSwapGrowthMB:256,phoneProfile:"iphone-se-2nd-gen",phoneOnLimit:"warn",phoneCapMB:0};FarmError=class extends Error{constructor(e,n,t,s=false){super(n);this.code=e;this.next=t;this.retryable=s;this.name="FarmError"}toJSON(){return{code:this.code,message:this.message,next:this.next,retryable:this.retryable}}}}});function slimLabels(){const e=new Set(ALWAYS_ENABLED);const n=new Set;for(const t of SLIM_CATEGORIES)for(const s of t.labels)if(!e.has(s))n.add(s);return[...n].sort()}function profileLabels(e){return e==="slim"?slimLabels():[]}function profileHash(e){return(0,import_node_crypto5.createHash)("sha256").update(e+"\n"+profileLabels(e).join("\n")).digest("hex").slice(0,16)}var import_node_crypto5,ALWAYS_ENABLED,SLIM_CATEGORIES;var init_profiles=__esm({"../sim-farm/src/profiles.ts"(){"use strict";import_node_crypto5=require("crypto");ALWAYS_ENABLED=["com.apple.sharingd"];SLIM_CATEGORIES=[{"id":"widgets","name":"Widgets & Wallpaper","downside":"Home and Lock Screen widgets, wallpaper posters, and Live Activities stop updating.","approxMemoryMB":675,"labels":["com.apple.PosterBoard","com.apple.chronod","com.apple.liveactivitiesd"]},{"id":"siri","name":"Siri & Intelligence","downside":"Siri, speech features, and Apple Intelligence services are unavailable.","approxMemoryMB":265,"labels":["com.apple.assistantd","com.apple.assistant_cdmd","com.apple.assistant_service","com.apple.siriactionsd","com.apple.siriinferenced","com.apple.siriknowledged","com.apple.sirittsd","com.apple.siri.context.service","com.apple.siri.acousticsignature","com.apple.corespeechd","com.apple.voiced","com.apple.voicebankingd","com.apple.speechmodeltrainingd","com.apple.intelligenceplatformd","com.apple.intelligencecontextd","com.apple.intelligenceflowd","com.apple.intelligencetasksd","com.apple.generativeexperiencesd","com.apple.knowledgeconstructiond","com.apple.naturallanguaged","com.apple.textunderstandingd","com.apple.modelcatalogd","com.apple.modelmanagerd","com.apple.mlhostd","com.apple.mlruntimed","com.apple.suggestd","com.apple.parsecd","com.apple.parsec-fbf","com.apple.proactiveeventtrackerd"]},{"id":"search","name":"Spotlight & Search","downside":"Spotlight and Settings search return no results.","approxMemoryMB":50,"labels":["com.apple.searchd","com.apple.searchtoold","com.apple.spotlightknowledged","com.apple.spotlightknowledged.updater","com.apple.corespotlightservice"]},{"id":"icloud","name":"iCloud & Apple Account","downside":"iCloud sync, Apple Account, Keychain, and backup workflows will not work.","approxMemoryMB":100,"labels":["com.apple.appleaccountd","com.apple.appleaccounttransparencyd","com.apple.appleidsetupd","com.apple.akd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.cloudd","com.apple.cloudphotod","com.apple.ckdiscretionaryd","com.apple.cloudsettingssyncagent","com.apple.bird","com.apple.syncdefaultsd","com.apple.cdpd","com.apple.sosd","com.apple.SecureBackupDaemon","com.apple.TrustedPeersHelper","com.apple.protectedcloudstorage.protectedcloudkeysyncing","com.apple.icloudmailagent","com.apple.icloudsubscriptionoptimizerd","com.apple.communicationtrustd"]},{"id":"store","name":"App Store, Push & Media","downside":"Remote push notifications and StoreKit or App Store testing will not work.","approxMemoryMB":80,"labels":["com.apple.appstored","com.apple.appstorecomponentsd","com.apple.apsd","com.apple.itunescloudd","com.apple.itunesstored","com.apple.storekitd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.passd","com.apple.financed","com.apple.videosubscriptionsd","com.apple.assetsubscriptiond","com.apple.musicd"]},{"id":"pim","name":"Mail, Calendar & Contacts","downside":"Contacts, Calendar, Reminders, and Mail-backed pickers or sync may fail.","approxMemoryMB":80,"labels":["com.apple.email.maild","com.apple.exchangesyncd","com.apple.dataaccess.dataaccessd","com.apple.calaccessd","com.apple.remindd","com.apple.contactsd","com.apple.contacts.postersyncd","com.apple.peopled"]},{"id":"web","name":"Safari Sync & Web Services","downside":"Universal links and Safari sync or background web services will not work.","approxMemoryMB":50,"labels":["com.apple.SafariBookmarksSyncAgent","com.apple.Safari.History","com.apple.Safari.passwordbreachd","com.apple.Safari.SafeBrowsing.Service","com.apple.safarifetcherd","com.apple.WebBookmarks.webbookmarksd","com.apple.webkit.adattributiond","com.apple.webkit.webpushd","com.apple.webprivacyd","com.apple.swcd"]},{"id":"family","name":"Family & Screen Time","downside":"Family Sharing, Screen Time, and usage tracking stop working.","approxMemoryMB":65,"labels":["com.apple.familycircled","com.apple.FamilyControlsAgent","com.apple.familynotification","com.apple.askpermissiond","com.apple.asktod","com.apple.ScreenTimeAgent","com.apple.ScreenTimeSettingsAgent","com.apple.UsageTrackingAgent"]},{"id":"health","name":"Health, Home & Fitness","downside":"HealthKit, HomeKit, and Fitness integrations will not work.","approxMemoryMB":135,"labels":["com.apple.healthd","com.apple.healthappd","com.apple.healthcontentd","com.apple.healtheventsd","com.apple.healthrecordsd","com.apple.finhealthd","com.apple.homed","com.apple.homeeventsd","com.apple.fitcore","com.apple.fitcore.session","com.apple.fitnesscoachingd","com.apple.fitnessintelligenced","com.apple.activityawardsd","com.apple.activitysharingd"]},{"id":"photos","name":"Photos & Media Analysis","downside":"Photo picker, Photos-library workflows, and media analysis may fail.","approxMemoryMB":60,"labels":["com.apple.photoanalysisd","com.apple.photosface","com.apple.mediaanalysisd","com.apple.mediaanalysisd.service","com.apple.mediastream.mstreamd","com.apple.medialibraryd","com.apple.assetsd","com.apple.assetsd.nebulad"]},{"id":"apps","name":"News, Weather, Maps & Games","downside":"News, Weather, Maps background data, and game-controller services are unavailable.","approxMemoryMB":90,"labels":["com.apple.newsd","com.apple.weatherd","com.apple.Maps.mapssyncd","com.apple.Maps.mapspushd","com.apple.Maps.geocorrectiond","com.apple.maps.destinationd","com.apple.MapKit.SnapshotService","com.apple.jetpackassetd","com.apple.tipsd","com.apple.gamed","com.apple.gamesaved","com.apple.GameController.gamecontrollerd"]},{"id":"messaging","name":"Messaging & FaceTime","downside":"iMessage, FaceTime, and related identity services will not work.","approxMemoryMB":60,"labels":["com.apple.identityservicesd","com.apple.ids_simd","com.apple.imautomatichistorydeletionagent","com.apple.imcore.imtransferagent","com.apple.imdpersistence.IMDPersistenceAgent","com.apple.facetimemessagestored","com.apple.telephonyutilities.callservicesd"]},{"id":"connectivity","name":"Sharing & Device Connectivity","downside":"AirDrop, Continuity, CarPlay, Watch, and Find My connectivity will not work.","approxMemoryMB":65,"labels":["com.apple.rapportd","com.apple.companiond","com.apple.carkitd","com.apple.wcd","com.apple.tvremoted","com.apple.avatarsd","com.apple.stickersd","com.apple.sociallayerd","com.apple.announced","com.apple.navd","com.apple.findmy.findmylocated"]},{"id":"telemetry","name":"Ads, Diagnostics & Telemetry","downside":"DeviceCheck plus analytics, diagnostics, and feedback services are unavailable.","approxMemoryMB":105,"labels":["com.apple.ap.adprivacyd","com.apple.ap.promotedcontentd","com.apple.diagnosticextensionsd","com.apple.feedbackd","com.apple.rtcreportingd","com.apple.securityuploadd","com.apple.geoanalyticsd","com.apple.triald","com.apple.followupd","com.apple.purplebuddy.budd","com.apple.devicecheckd"]},{"id":"other","name":"Other Background Services","downside":"Wallet, merchant, business, asset, and miscellaneous background services are unavailable.","approxMemoryMB":195,"labels":["com.apple.financed","com.apple.passd","com.apple.merchantd","com.apple.coreidvd","com.apple.businessservicesd","com.apple.deviceaccessd","com.apple.replicatord","com.apple.linkd","com.apple.ind","com.apple.storagedatad","com.apple.StatusKitAgent","com.apple.countryd","com.apple.mobileassetd","com.apple.managedconfiguration.passcodenagd"]}]}});function liveChildCount(){return liveChildren.size}function killAllChildren(){for(const e of liveChildren){try{e.kill("SIGKILL")}catch{}}}function run(e,n,t={}){const{timeoutMs:s=3e4,maxBytes:i=4*1024*1024}=t;return new Promise(r=>{const o=(0,import_node_child_process7.spawn)(e,n,{env:t.env??process.env,stdio:[t.input==null?"ignore":"pipe","pipe","pipe"]});liveChildren.add(o);let a="";let l="";let c=false;o.stdout.on("data",d=>{if(a.length<i)a+=d.toString("utf8")});o.stderr.on("data",d=>{if(l.length<i)l+=d.toString("utf8")});if(t.input!=null)o.stdin.end(t.input);const u=setTimeout(()=>{c=true;o.kill("SIGTERM");setTimeout(()=>o.kill("SIGKILL"),2e3).unref?.()},s);o.on("error",d=>{liveChildren.delete(o);clearTimeout(u);r({code:null,stdout:a,stderr:l+String(d),timedOut:c})});o.on("close",d=>{liveChildren.delete(o);clearTimeout(u);r({code:d,stdout:a,stderr:l,timedOut:c})})})}async function runOk(e,n,t={}){const s=await run(e,n,t);if(s.timedOut)throw new Error(`${e} ${n.slice(0,3).join(" ")} timed out after ${t.timeoutMs??3e4} ms`);if(s.code!==0)throw new Error(`${e} ${n.slice(0,3).join(" ")} failed (${s.code}): ${(s.stderr||s.stdout).trim().slice(0,500)}`);return s.stdout}async function listProcesses(){const e=await runOk("ps",["-axww","-o","pid=,ppid=,lstart=,stat=,comm="],{timeoutMs:15e3});const n=await runOk("ps",["-axww","-o","pid=,command="],{timeoutMs:15e3});const t=new Map;for(const i of n.split("\n")){const r=/^\s*(\d+)\s+(.*)$/.exec(i);if(r)t.set(Number(r[1]),r[2])}const s=[];for(const i of e.split("\n")){const r=/^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(\S+)\s+(.*)$/.exec(i);if(!r)continue;const o=Number(r[1]);s.push({pid:o,ppid:Number(r[2]),start:r[3].replace(/\s+/g," "),stat:r[4],comm:r[5].trim(),command:t.get(o)??r[5].trim()})}return s}function descendants(e,n){const t=new Map;const s=new Map;for(const l of e){s.set(l.pid,l);const c=t.get(l.ppid)??[];c.push(l);t.set(l.ppid,c)}const i=[];const r=s.get(n);if(!r)return i;const o=[r];const a=new Set;while(o.length){const l=o.pop();if(a.has(l.pid))continue;a.add(l.pid);i.push(l);for(const c of t.get(l.pid)??[])o.push(c)}return i}async function pidStart(e){const n=await run("ps",["-o","lstart=","-p",String(e)],{timeoutMs:5e3});const t=n.stdout.trim().replace(/\s+/g," ");return n.code===0&&t?t:null}async function pidAlive(e,n,t=false){if(!Number.isInteger(e)||e<=0)return false;try{process.kill(e,0)}catch(i){if(i.code!=="EPERM")return false}if(!n)return true;const s=await pidStart(e);if(s===null)return t;return s===n}var import_node_child_process7,liveChildren,sleep;var init_proc=__esm({"../sim-farm/src/proc.ts"(){"use strict";import_node_child_process7=require("child_process");liveChildren=new Set;sleep=e=>new Promise(n=>setTimeout(n,e))}});function storeDir(e,n=DISABLED_STORE_ROOT){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return import_node_path7.default.join(n,`com.apple.CoreSimulator.SimDevice.${e}`)}function storePath(e,n=DISABLED_STORE_ROOT){return import_node_path7.default.join(storeDir(e,n),"disabled.plist")}async function readStore(e,n=DISABLED_STORE_ROOT){const t=storePath(e,n);let s;try{s=await import_node_fs6.promises.lstat(t)}catch{return{}}if(!s.isFile())throw new Error(`refusing ${t}: not a regular file`);const i=await runOk("plutil",["-convert","json","-o","-",t],{timeoutMs:1e4});const r=JSON.parse(i);const o={};for(const[a,l]of Object.entries(r))if(typeof l==="boolean")o[a]=l;return o}function xmlEscape(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function encodeStore(e){const n=Object.keys(e).sort();let t='<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n';for(const s of n)t+=` <key>${xmlEscape(s)}</key>
|
|
54
|
+
`}function escapeHtml(e){return e.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var import_node_child_process4,import_node_util3,import_node_fs2,import_node_path2,MAX_RECORDING_MS,live,MAX_BURN_DOTS,execFileAsync3;var init_recorder=__esm({"src/capture/recorder.ts"(){"use strict";import_node_child_process4=require("child_process");import_node_util3=require("util");import_node_fs2=require("fs");import_node_path2=__toESM(require("path"));init_adb();init_iosDevice();MAX_RECORDING_MS=3*6e4;live=new Set;MAX_BURN_DOTS=300;execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process4.execFile)}});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 s;try{s=(0,import_node_fs3.readFileSync)((0,import_node_path3.join)(e,t),"utf8")}catch{continue}const i=new Map;for(const r of s.split(/\r?\n/)){const o=r.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!o)continue;const a=o[2].trim();const l=a.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);i.set(o[1],l?l[2]:a.split("#")[0].trim())}for(const r of KEY_NAMES)if(i.has(r))return(0,import_headless.normalizeLicenseKey)(i.get(r))||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 l=Math.min(Date.now(),a.claims.iat*1e3);const c=a.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${a.claims.sid}`,valid:true,tier:c,isPro:c==="pro",expiresAt:new Date(a.claims.exp*1e3).toISOString(),cachedAt:l,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 l=Date.now();const c=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(c).toISOString(),cachedAt:l,lastValidatedAt:l});this.persistCache();this.retry=0;this.nextCheck=Math.max(l+6e4,c-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 l=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${a.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:l,lastValidatedAt:l});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 s=t.data?.relationships?.product?.data?.id;const i=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(s&&s!==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 r=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:r,isPro:r==="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 s=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(s??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:s}=await execFileAsync4("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const i=s.split("\n").map(a=>a.trim()).find(Boolean);if(!i)return null;const{stdout:r}=await execFileAsync4("lsof",["-a","-p",i,"-d","cwd","-Fn"],{timeout:3e3});const o=r.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(s=>s?.address?.toLowerCase()===t)}function pickByProject(e,n){const t=import_node_path4.default.resolve(n);const s=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(s.length===0)return null;const i=Math.min(...s.map(o=>o.score));const r=s.filter(o=>o.score===i);return r.length===1?r[0].deviceId:null}function closeness(e,n){if(e===n)return 0;const t=import_node_path4.default.relative(e,n);const s=import_node_path4.default.relative(n,e);const i=r=>r&&!r.startsWith("..")&&!import_node_path4.default.isAbsolute(r);if(i(t))return t.split(import_node_path4.default.sep).length;if(i(s))return s.split(import_node_path4.default.sep).length;return-1}var import_node_child_process5,import_node_util4,import_node_path4,import_node_os3,execFileAsync4;var init_projectRoot=__esm({"src/sync/projectRoot.ts"(){"use strict";import_node_child_process5=require("child_process");import_node_util4=require("util");import_node_path4=__toESM(require("path"));import_node_os3=__toESM(require("os"));execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process5.execFile)}});var policy_exports={};__export(policy_exports,{FREE_SUMMARY:()=>FREE_SUMMARY,accessFor:()=>accessFor,currentAccess:()=>currentAccess,freeToolNames:()=>freeToolNames,hasProAccess:()=>hasProAccess,isFreeAppCall:()=>isFreeAppCall,isFreeTool:()=>isFreeTool,isWeekendPass:()=>isWeekendPass,meetsAccess:()=>meetsAccess,outsideCalls:()=>outsideCalls,proRequiredMessage:()=>proRequiredMessage,runWithAccess:()=>runWithAccess,setClockForTests:()=>setClockForTests,weekendNow:()=>weekendNow});function isFreeAppCall(e,n){return Object.prototype.hasOwnProperty.call(FREE_APP_CALLS,e)&&FREE_APP_CALLS[e].includes(n)}function setClockForTests(e){clock=e??(()=>new Date)}function isWeekendPass(e=clock()){const n=e.getDay();return n===0||n===6}function accessFor(e,n){const t=isWeekendPass();if(!Object.prototype.hasOwnProperty.call(FREE_TOOLS,e))return{tool:e,level:"pro",weekend:t};const s=FREE_TOOLS[e];const i=s?s(n&&typeof n==="object"?n:{}):null;return i?{tool:e,level:"pro",proOption:i,weekend:t}:{tool:e,level:"free",weekend:t}}function weekendNow(){return scope.getStore()?.weekend??isWeekendPass()}function hasProAccess(e){if(!e)return false;if(e.isPro===true)return true;return e.tier==="free"&&weekendNow()}function isFreeTool(e){return Object.prototype.hasOwnProperty.call(FREE_TOOLS,e)}function freeToolNames(){return Object.keys(FREE_TOOLS).sort()}function runWithAccess(e,n){return scope.run(Object.freeze({...e}),n)}function outsideCalls(e){return scope.exit(e)}function currentAccess(){return scope.getStore()??{tool:"",level:"pro"}}function meetsAccess(e,n){if(!e)return false;if(hasProAccess(e))return true;return n==="free"&&e.tier==="free"}function proRequiredMessage(e,n){const t=e.proOption?`\`${e.tool}\` with \`${e.proOption}\` needs Buoy Pro.`:e.tool?`\`${e.tool}\` needs Buoy Pro.`:"This needs Buoy Pro.";return`${t}${n} ${FREE_SUMMARY} Tell the user Pro unlocks the rest (https://buoy.gg/pricing). To add a license, enter it in the app, or pass \`licenseKey\` to \`<FloatingDevTools />\` for an app with no on-device UI. No setting, argument or other tool turns Pro on, so don't try to work around it.`}var import_node_async_hooks,FREE_TOOLS,FREE_APP_CALLS,clock,scope,FREE_SUMMARY;var init_policy=__esm({"src/access/policy.ts"(){"use strict";import_node_async_hooks=require("async_hooks");FREE_TOOLS={list_devices:null,list_tool_actions:null,camera_diagnose:null,describe_screen:e=>e.includeBuoy?"includeBuoy":null,tap_element:e=>e.includeBuoy?"includeBuoy":null,native_tap:e=>e.allowBuoy?"allowBuoy":null,native_type:null,native_swipe:e=>e.allowBuoy?"allowBuoy":null,native_pinch:e=>e.allowBuoy?"allowBuoy":null,native_inspect:null,run_flow:null,screenshot_component:null,screen_recording:null,device_control:null,reload_app:null,navigate:null,get_routes:null,get_console:null,get_network_requests:e=>e.includeBodies?"includeBodies":null,get_triage:null,run_benchmark_batch:null,wait_for_automation:null,get_automation_status:null,get_batch_report:null,compare_reports:null,get_benchmark_settings:null,set_benchmark_settings:null,get_js_thread_top:null,run_web_benchmark:e=>e.traceDir?"traceDir":null,find_web_ceiling:null,screenshot_web_variants:null,start_optimize_session:null,sim_farm_status:null,sim_farm_setup:null,sim_farm_acquire:null,sim_farm_release:null,sim_farm_action:null,sim_farm_lock:null,sim_farm_report:null,sim_farm_extend:null,sim_farm_finish:null};FREE_APP_CALLS={"highlight-updates":["describeScreen","tapElement","hitTest","readMaps","setSilentTracking"],"route-events":["@snapshot","getCurrentRoute","navigate"],app:["@snapshot","ping","reloadApp"],console:["@snapshot"],network:["@snapshot"],storage:["async.getAllKeys","secure.keys"],"perf-monitor":["@snapshot","getAutomationConfig","setAutomationConfig","acknowledgeAutomation","setEnabled","refreshIndex","startAutomation","loadReport"],"js-top":["sample"]};clock=()=>new Date;scope=new import_node_async_hooks.AsyncLocalStorage;FREE_SUMMARY="Free covers reading the screen, taps, swipes and typing, run_flow, screenshots, screen recording, device_control, reload_app, navigate, get_routes, get_console, the request list and get_triage. It also covers Buoy Optimize and the Sim Farm: benchmark batches, reports, web benchmarks and JS thread totals, without the render names, script names and JS origins that say why. Every Saturday and Sunday a free account gets everything Pro does."}});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}function selfTimedBudgetMs(e,n,t){if(e!=="highlight-updates"||n!=="waitFor")return 0;const s=Number(t?.timeoutMs);if(!Number.isFinite(s)||s<=0)return 0;return Math.min(s,WAIT_FOR_MAX_MS)+WAIT_FOR_MARGIN_MS}var WAIT_FOR_MAX_MS,WAIT_FOR_MARGIN_MS;var init_actionTimeout=__esm({"src/sync/actionTimeout.ts"(){"use strict";WAIT_FOR_MAX_MS=3e4;WAIT_FOR_MARGIN_MS=5e3}});function probeBroker(e,n=800){return new Promise(t=>{const s=import_node_http.default.get(e,i=>{i.resume();t((i.statusCode??0)>0)});s.setTimeout(n,()=>{s.destroy();t(false)});s.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_policy();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.transportLost=false;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}
|
|
55
|
+
`));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:s=>{if(s.code==="EADDRINUSE")t=true;this.log(`broker error: ${s.code??s.message}`)}});await waitForBroker(this.url).catch(s=>{if(!t)throw s});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(s){this.log(`could not write the broker token file: ${s.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 outsideCalls(()=>new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:a=>{const l=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));a(l?{dashboardToken:l}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let s=false;const i=()=>{if(s)return;s=true;this.log("connected to broker");e()};const r=setTimeout(()=>{if(s)return;s=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 l=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(l);if(l){clearTimeout(r);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(s)return;s=true;clearTimeout(r);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(l=>l.toolId==="console"))this.ensureWatching("console",a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let l=false;for(const u of a){if(u.deviceId&&(u.tier==="locked"||u.tier==="checking"))this.lockDevice(String(u.deviceId),u.tier==="checking");else if(u.deviceId&&(u.tier==="free"||u.tier==="pro")){l=this.lockedDevices.delete(String(u.deviceId))||l}}this.devices=a;const c=Date.now();for(const u of a)if(u.deviceId)this.lastSeen.set(u.deviceId,c);this.pruneStaleDevices();if(l)this.rearmWatches();else this.rearmReconnectedDevices(a)});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const l=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(l,a.payload);this.snapshotAt.set(l,Date.now());if(a.toolId==="console")this.noteCrashReport(a.persistentDeviceId,a.payload);const c=this.snapshotWaiters.get(l);if(c&&c.length){this.snapshotWaiters.set(l,[]);for(const u of c)u(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const l=this.pendingActions.get(a.requestId);if(!l)return;this.pendingActions.delete(a.requestId);clearTimeout(l.timer);l.resolve(a)})}))}isAdmitted(){return this.admitted}requireAdmission(){if(this.transportLost)throw new Error("The broker link was lost. Wait for it to reconnect.");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.transportLost=!e&&n;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(n?"The broker link was lost during the call. Its result is unknown.":"Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const s of t)s(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.devices.some(n=>n.deviceId===e&&n.tier==="checking"))throw new Error(`${NOT_READY_MARK} The new app link is still checking access.`);if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e,n=false){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 t=`${e}::`;for(const s of this.snapshots.keys())if(s.startsWith(t)){this.snapshots.delete(s);this.snapshotAt.delete(s)}for(const[s,i]of this.snapshotWaiters)if(s.startsWith(t)){this.snapshotWaiters.delete(s);for(const r of i)r(void 0)}for(const[s,i]of this.pendingActions)if(i.target===e||i.target===ALL_DEVICES){clearTimeout(i.timer);this.pendingActions.delete(s);i.reject(new Error(n?"The app link changed during the call. Its result is unknown.":"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 s=`${n}::`;for(const i of[...this.snapshots.keys()]){if(i.startsWith(s)){this.snapshots.delete(i);this.snapshotAt.delete(i)}}for(const i of[...this.watching.keys()]){if(i.startsWith(s))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==="checking"?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(s=>s.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}peekProjectDevice(){if(!this.admitted)return null;if(this.sessionDevice)return this.sessionDevice;const e=this.connectedDevices().filter(t=>!this.lockedDevices.has(t.deviceId)&&t.tools.length>0);const n=process.env.BUOY_PROJECT_ROOT||process.cwd();return pickByProject(e.map(t=>({deviceId:t.deviceId,root:this.devServerRoots.get(t.deviceId)})),n)??null}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(c,u)=>new Error(`${NOT_READY_MARK} "${c}" (${u}) 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 c=this.devices.find(u=>u.deviceId===this.sessionDevice);if(c&&c.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const s=t.filter(c=>!this.lockedDevices.has(c.deviceId));const i=s.filter(c=>c.tools.length>0);const r=process.env.BUOY_PROJECT_ROOT||process.cwd();const o=pickByProject(i.map(c=>({deviceId:c.deviceId,root:this.devServerRoots.get(c.deviceId)})),r);if(o)return this.sessionDevice=o;const a=pickByProject(this.devices.filter(c=>c.deviceId&&c.isConnected===false&&!this.lockedDevices.has(c.deviceId)).map(c=>({deviceId:c.deviceId,root:this.devServerRoots.get(c.deviceId)})),r);if(a)throw n(a,"this project's app");if(s.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(c=>c.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(s.length===1)return this.sessionDevice=s[0].deviceId;if(i.length===1)return this.sessionDevice=i[0].deviceId;if(i.length===0){const c=s.map(u=>u.deviceName).join(", ");throw new Error(`Connected devices (${c}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const l=i.map(c=>`${c.deviceName} (${c.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${l}`)}requireProDevice(e){this.requireAdmission();const n=currentAccess();if(e&&e!==ALL_DEVICES)this.sessionDevice=e;const t=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const s=o=>n.level==="pro"?proRequiredMessage(n,t(o)):`The Buoy MCP needs an app on a Buoy account.${t(o)} Run \`npx buoy login\` in the project (free), restart the dev server with its cache cleared, then run \`list_devices\` to check.`;if(e===ALL_DEVICES){const o=this.connectedDevices().filter(l=>l.isPro||l.tier==="free");const a=o.find(l=>!meetsAccess(l,n.level));if(o.length===0)throw new Error(s());if(a)throw new Error(s(a));return ALL_DEVICES}const i=this.resolveDevice(e);const r=this.connectedDevices().find(o=>o.deviceId===i);if(!meetsAccess(r,n.level)){const o=this.accessState(i,n.level);if(o==="offline")throw new Error(`${NOT_READY_MARK} "${i}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(o==="pending")throw new Error(`${NOT_READY_MARK} account access for "${i}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(s(r))}return i}checkAppCall(e,n,t){const s=currentAccess();if(!s.tool)return;const i=t===ALL_DEVICES?this.connectedDevices().find(r=>r.tier==="free"&&!hasProAccess(r)):this.listDevices().find(r=>r.deviceId===t&&!hasProAccess(r));if(!i)return;if(s.level==="free"&&isFreeAppCall(e,n))return;throw new Error(proRequiredMessage({...s,level:"pro"},` The app "${i.deviceName}" is on the ${i.tier} tier${s.level==="free"?`, and this part of \`${s.tool}\` reads its ${e} data, which is Pro`:""}.`))}refuseBuoyPanelTap(e,n,t,s){if(e!=="highlight-updates"||n!=="tapElement")return null;const i=currentAccess();if(!i.tool||i.level!=="free")return null;const r=t??{};const o=typeof r.nativeTag==="number"?r.nativeTag:null;const a=typeof r.testID==="string"&&r.testID?r.testID.toLowerCase():null;const l=o!=null||a!=null||!!r.includeBuoy;if(s===ALL_DEVICES){if(!l||!this.connectedDevices().some(d=>d.tier==="free"&&!hasProAccess(d)))return null;return Promise.reject(new Error(proRequiredMessage({...i,level:"pro"},' On a free app, tap one app at a time (not "All") when tapping by testID or nativeTag.')))}const c=this.listDevices().find(d=>d.deviceId===s);if(!c||hasProAccess(c)||!l)return null;const u=d=>new Error(proRequiredMessage({...i,level:"pro"},d));if(r.includeBuoy)return Promise.reject(u(" That reaches Buoy's own panel, not the app."));return(async()=>{let d;try{d=await this.callAction("highlight-updates","describeScreen",{includeBuoy:false,includeHidden:true},s,{timeoutMs:4e3,silent:true})}catch(p){throw new Error(`Could not check that target before tapping (${p instanceof Error?p.message:String(p)}). On a free app, tap by query, or retry once the screen settles.`)}const h=(d?.elements??[]).some(p=>o!=null?p.nativeTag===o:p.testID?.toLowerCase()===a);if(!h&&o==null&&typeof r.query==="string"){const{testID:p,...m}=r;return m}if(!h){throw new Error(`"${o??String(r.testID)}" is not on the app's screen right now. On a free app, a tap by testID or nativeTag must hit an element describe_screen lists, and Buoy's own panels are Pro. Swipe it into view first, or tap by query.`)}})()}fullResultsFor(e){if(e===ALL_DEVICES){const n=this.connectedDevices();return n.length>0&&n.every(t=>hasProAccess(t))}return hasProAccess(this.listDevices().find(n=>n.deviceId===e))}deviceWasAllowed(e){return meetsAccess(this.listDevices().find(n=>n.deviceId===e),currentAccess().level)}accessState(e,n=currentAccess().level){const t=this.devices.find(s=>s.deviceId===e);if(!t||t.isConnected===false)return"offline";if(hasProAccess(t)||n==="free"&&t.tier==="free")return"ready";return t.tier===void 0||t.tier==="checking"?"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 s=this.healthOf(e);const{timeoutMs:i,usedFullBudget:r}=actionTimeoutMs({consecutiveTimeouts:s.consecutiveTimeouts,lastFullBudgetAt:s.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(r)s.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 s=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`:""}).`,s?"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 r=this.lastFatalFor(e);if(r)i.push(`Last captured error: ${r}`);i.push(r?"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 s=Math.max(...t.map(o=>typeof o?.timestamp==="number"?o.timestamp:-Infinity));const i=this.crashSeen.get(e)??new Set;let r=false;for(const o of t){const a=o?.fatal||/^\[(RENDER ERROR|FATAL)/.test(o?.message??"");if(!a||typeof o.timestamp!=="number")continue;const l=`${o.id??""}|${o.timestamp}|${(o.message??"").slice(0,120)}`;if(i.has(l))continue;i.add(l);if(s-o.timestamp<3e4)r=true}if(i.size>200)for(const o of[...i].slice(0,i.size-200))i.delete(o);this.crashSeen.set(e,i);if(!r)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 s=t.length-1;s>=0;s--){const i=t[s];if(i?.fatal||i?.level==="error"){const r=(i.message??"").split("\n")[0];if(r)return r.length>300?`${r.slice(0,300)}\u2026`:r}}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 s=devServerRoot(n).then(i=>{if(i&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,i)});this.trackRootLookup(s)}}}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 s=`${n.id}|${n.connectionNonce??""}`;const i=this.deviceConnections.get(t);this.deviceConnections.set(t,s);if(i===void 0||i===s)continue;for(const{toolId:r,targetDeviceId:o}of this.watching.values()){if(o===t)this.sendWatch(r,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);this.checkAppCall(e,"@snapshot",n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const s=this.resolveDevice(n);this.checkAppCall(e,"@snapshot",s);const i=snapKey(s,e);this.ensureWatching(e,s);let r=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(s)){const c=this.healthOf(s);if(Date.now()-c.lastFullBudgetAt>RECOVERY_PROBE_MS){c.lastFullBudgetAt=Date.now()}else{r=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const o=this.waitForSnapshot(i,r);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:s});const a=await o;this.requireAdmission();this.requireDeviceAccount(s);if(a!==void 0){if(!t.silent)this.noteOk(s);return{deviceId:s,payload:a}}if(!t.silent)this.noteTimeout(s);const l=this.snapshots.get(i);if(l!==void 0){return{deviceId:s,payload:l,stale:this.staleNote(e,s)}}throw new Error(`Timed out waiting for "${e}" state from ${s} (is the tool active?). ${this.unresponsiveNote(s)}`)}lastSync(e,n){this.checkAppCall(n,"@snapshot",e);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 s=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${s}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const s=this.snapshotWaiters.get(e)??[];const i=setTimeout(()=>{const o=(this.snapshotWaiters.get(e)??[]).filter(a=>a!==r);this.snapshotWaiters.set(e,o);t(void 0)},n);const r=o=>{clearTimeout(i);t(o)};s.push(r);this.snapshotWaiters.set(e,s)})}async callAction(e,n,t,s,i={}){this.requireAdmission();const r=s===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(s);this.checkAppCall(e,n,r);const o=this.refuseBuoyPanelTap(e,n,t,r);if(o)t=await o??t;if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto3.randomUUID)();const l=i.timeoutMs??Math.max(this.timeoutFor(r,e,n),selfTimedBudgetMs(e,n,t));const c=await new Promise((u,d)=>{const h=Date.now();const p=()=>{this.pendingActions.delete(a);if(r!==ALL_DEVICES&&!i.silent)this.noteTimeout(r);d(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-h}ms. `+(r===ALL_DEVICES?"No device responded.":this.unresponsiveNote(r))))};const m=setTimeout(p,l);this.pendingActions.set(a,{resolve:u,reject:d,timer:m,target:r,expire:p,deadline:h+l});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:r,action:n,params:t??{}})});if(r!==ALL_DEVICES&&!i.silent)this.noteOk(r);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.70"?"7.0.70":"0.0.0-dev";PACKAGE_NAME="@buoy-gg/mcp"}});function isNewer(e,n){const t=r=>r.split("-")[0].split(".").map(o=>Number(o)||0);const s=t(e);const i=t(n);for(let r=0;r<3;r++){const o=s[r]??0;const a=i[r]??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 s=await fetch(REGISTRY_URL,{signal:n.signal});if(!s.ok)return null;const i=await s.json();const r=typeof i.version==="string"?i.version:null;if(r){await import_node_fs4.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:r})).catch(()=>{})}return r}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,s)=>e[s]===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 s of await listDir(t)){const i=import_node_path6.default.join(t,s,"WebsiteData","Default");for(const r of await listDir(i)){for(const o of await listDir(import_node_path6.default.join(i,r))){const a=import_node_path6.default.join(i,r,o,"LocalStorage","localstorage.sqlite3");try{await import_node_fs5.promises.access(a);const{stdout:l}=await execFileAsync5("sqlite3",["-readonly",a,`SELECT hex(value) FROM ItemTable WHERE key='${WEBVIEW_ID_KEY}' LIMIT 1;`],{timeout:1500});const c=l.trim();if(c)n.push(Buffer.from(c,"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:s}=await execFileAsync5("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const i=JSON.parse(s).devices;n=Object.entries(i).filter(([r])=>/iOS/.test(r)).flatMap(([,r])=>r.filter(o=>o.state==="Booted"))}catch(s){return{ok:false,error:humanizeSimctlError(s)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const s of n){if(!s.dataPath)continue;const i=import_node_path6.default.join(s.dataPath,"Containers","Data","Application");let r=[];try{r=await import_node_fs5.promises.readdir(i)}catch{continue}for(const o of r){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:s.udid,how:`matched ${e} to "${s.name}"`}}catch{}}}}if(t&&e){const s=n.map(l=>l.udid).sort().join(",");const i=webViewLookups.get(e);const r=i?.booted===s&&(i.udid!==null||Date.now()-i.at<WEBVIEW_MISS_MS);const o=r&&i?.udid?n.find(l=>l.udid===i.udid):void 0;if(o)return{ok:true,udid:o.udid,how:`matched ${e} to "${o.name}" (WebView storage)`};const a=[];if(!r){const l=await Promise.all(n.map(c=>c.dataPath?webViewInstallIds(import_node_path6.default.join(c.dataPath,"Containers","Data","Application")):Promise.resolve([])));l.forEach((c,u)=>{if(c.includes(t))a.push(n[u])});webViewLookups.set(e,{booted:s,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(l=>l.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(s=>s.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 execFileAsync5("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const r=JSON.parse(i).devices;t=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return n}const s=new Map;for(const i of t){if(!i.dataPath)continue;const r=import_node_path6.default.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await import_node_fs5.promises.readdir(r)}catch{continue}for(const a of o){try{const l=(await import_node_fs5.promises.readFile(import_node_path6.default.join(r,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(l)s.set(l,{name:i.name,udid:i.udid})}catch{}}}for(const i of e){const r=s.get(i.split("-").pop()??"");if(r)n.set(i,r)}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 execFileAsync5("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 s=pngSize(t);if(!s)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:s.width,height:s.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function cropToBuffer(e,n,t){const s=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 r=clamp(Math.round(t.width),1,n.width-s);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 execFileAsync5("sips",["-c",String(o),String(r),"--cropOffset",String(i),String(s),e,"--out",a],{timeout:1e4});const l=await import_node_fs5.promises.readFile(a);return{ok:true,buffer:l,width:r,height:o}}catch(l){const c=l;return{ok:false,error:`Crop failed: ${c?.stderr||c?.message||l}`}}finally{await import_node_fs5.promises.unlink(a).catch(()=>{});await import_node_fs5.promises.unlink(e).catch(()=>{})}}var import_node_child_process6,import_node_util5,import_node_fs5,import_node_os5,import_node_path6,import_node_crypto4,execFileAsync5,clamp,WEBVIEW_ID_KEY,webViewLookups,WEBVIEW_MISS_MS;var init_simctl=__esm({"src/capture/simctl.ts"(){"use strict";import_node_child_process6=require("child_process");import_node_util5=require("util");import_node_fs5=require("fs");import_node_os5=__toESM(require("os"));import_node_path6=__toESM(require("path"));import_node_crypto4=require("crypto");execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process6.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}});var PROTOCOL_VERSION,STATE_VERSION,DEFAULT_SETTINGS,FarmError;var init_types=__esm({"../sim-farm/src/types.ts"(){"use strict";PROTOCOL_VERSION=1;STATE_VERSION=1;DEFAULT_SETTINGS={parkAfterMinutes:10,maxConcurrentBoots:3,reserveRamGB:24,reserveDiskGB:40,slimSimGB:2,stockSimGB:6,cloneDiskGB:2,freezePilot:false,runCapMinutes:60,keepRunsHours:24,fairMinFreeShare:.25,fairMaxSwingShare:.12,fairMaxThermalLevel:0,fairMaxSwapGrowthMB:256,phoneProfile:"iphone-se-2nd-gen",phoneOnLimit:"warn",phoneCapMB:0};FarmError=class extends Error{constructor(e,n,t,s=false){super(n);this.code=e;this.next=t;this.retryable=s;this.name="FarmError"}toJSON(){return{code:this.code,message:this.message,next:this.next,retryable:this.retryable}}}}});function slimLabels(){const e=new Set(ALWAYS_ENABLED);const n=new Set;for(const t of SLIM_CATEGORIES)for(const s of t.labels)if(!e.has(s))n.add(s);return[...n].sort()}function profileLabels(e){return e==="slim"?slimLabels():[]}function profileHash(e){return(0,import_node_crypto5.createHash)("sha256").update(e+"\n"+profileLabels(e).join("\n")).digest("hex").slice(0,16)}var import_node_crypto5,ALWAYS_ENABLED,SLIM_CATEGORIES;var init_profiles=__esm({"../sim-farm/src/profiles.ts"(){"use strict";import_node_crypto5=require("crypto");ALWAYS_ENABLED=["com.apple.sharingd"];SLIM_CATEGORIES=[{"id":"widgets","name":"Widgets & Wallpaper","downside":"Home and Lock Screen widgets, wallpaper posters, and Live Activities stop updating.","approxMemoryMB":675,"labels":["com.apple.PosterBoard","com.apple.chronod","com.apple.liveactivitiesd"]},{"id":"siri","name":"Siri & Intelligence","downside":"Siri, speech features, and Apple Intelligence services are unavailable.","approxMemoryMB":265,"labels":["com.apple.assistantd","com.apple.assistant_cdmd","com.apple.assistant_service","com.apple.siriactionsd","com.apple.siriinferenced","com.apple.siriknowledged","com.apple.sirittsd","com.apple.siri.context.service","com.apple.siri.acousticsignature","com.apple.corespeechd","com.apple.voiced","com.apple.voicebankingd","com.apple.speechmodeltrainingd","com.apple.intelligenceplatformd","com.apple.intelligencecontextd","com.apple.intelligenceflowd","com.apple.intelligencetasksd","com.apple.generativeexperiencesd","com.apple.knowledgeconstructiond","com.apple.naturallanguaged","com.apple.textunderstandingd","com.apple.modelcatalogd","com.apple.modelmanagerd","com.apple.mlhostd","com.apple.mlruntimed","com.apple.suggestd","com.apple.parsecd","com.apple.parsec-fbf","com.apple.proactiveeventtrackerd"]},{"id":"search","name":"Spotlight & Search","downside":"Spotlight and Settings search return no results.","approxMemoryMB":50,"labels":["com.apple.searchd","com.apple.searchtoold","com.apple.spotlightknowledged","com.apple.spotlightknowledged.updater","com.apple.corespotlightservice"]},{"id":"icloud","name":"iCloud & Apple Account","downside":"iCloud sync, Apple Account, Keychain, and backup workflows will not work.","approxMemoryMB":100,"labels":["com.apple.appleaccountd","com.apple.appleaccounttransparencyd","com.apple.appleidsetupd","com.apple.akd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.cloudd","com.apple.cloudphotod","com.apple.ckdiscretionaryd","com.apple.cloudsettingssyncagent","com.apple.bird","com.apple.syncdefaultsd","com.apple.cdpd","com.apple.sosd","com.apple.SecureBackupDaemon","com.apple.TrustedPeersHelper","com.apple.protectedcloudstorage.protectedcloudkeysyncing","com.apple.icloudmailagent","com.apple.icloudsubscriptionoptimizerd","com.apple.communicationtrustd"]},{"id":"store","name":"App Store, Push & Media","downside":"Remote push notifications and StoreKit or App Store testing will not work.","approxMemoryMB":80,"labels":["com.apple.appstored","com.apple.appstorecomponentsd","com.apple.apsd","com.apple.itunescloudd","com.apple.itunesstored","com.apple.storekitd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.passd","com.apple.financed","com.apple.videosubscriptionsd","com.apple.assetsubscriptiond","com.apple.musicd"]},{"id":"pim","name":"Mail, Calendar & Contacts","downside":"Contacts, Calendar, Reminders, and Mail-backed pickers or sync may fail.","approxMemoryMB":80,"labels":["com.apple.email.maild","com.apple.exchangesyncd","com.apple.dataaccess.dataaccessd","com.apple.calaccessd","com.apple.remindd","com.apple.contactsd","com.apple.contacts.postersyncd","com.apple.peopled"]},{"id":"web","name":"Safari Sync & Web Services","downside":"Universal links and Safari sync or background web services will not work.","approxMemoryMB":50,"labels":["com.apple.SafariBookmarksSyncAgent","com.apple.Safari.History","com.apple.Safari.passwordbreachd","com.apple.Safari.SafeBrowsing.Service","com.apple.safarifetcherd","com.apple.WebBookmarks.webbookmarksd","com.apple.webkit.adattributiond","com.apple.webkit.webpushd","com.apple.webprivacyd","com.apple.swcd"]},{"id":"family","name":"Family & Screen Time","downside":"Family Sharing, Screen Time, and usage tracking stop working.","approxMemoryMB":65,"labels":["com.apple.familycircled","com.apple.FamilyControlsAgent","com.apple.familynotification","com.apple.askpermissiond","com.apple.asktod","com.apple.ScreenTimeAgent","com.apple.ScreenTimeSettingsAgent","com.apple.UsageTrackingAgent"]},{"id":"health","name":"Health, Home & Fitness","downside":"HealthKit, HomeKit, and Fitness integrations will not work.","approxMemoryMB":135,"labels":["com.apple.healthd","com.apple.healthappd","com.apple.healthcontentd","com.apple.healtheventsd","com.apple.healthrecordsd","com.apple.finhealthd","com.apple.homed","com.apple.homeeventsd","com.apple.fitcore","com.apple.fitcore.session","com.apple.fitnesscoachingd","com.apple.fitnessintelligenced","com.apple.activityawardsd","com.apple.activitysharingd"]},{"id":"photos","name":"Photos & Media Analysis","downside":"Photo picker, Photos-library workflows, and media analysis may fail.","approxMemoryMB":60,"labels":["com.apple.photoanalysisd","com.apple.photosface","com.apple.mediaanalysisd","com.apple.mediaanalysisd.service","com.apple.mediastream.mstreamd","com.apple.medialibraryd","com.apple.assetsd","com.apple.assetsd.nebulad"]},{"id":"apps","name":"News, Weather, Maps & Games","downside":"News, Weather, Maps background data, and game-controller services are unavailable.","approxMemoryMB":90,"labels":["com.apple.newsd","com.apple.weatherd","com.apple.Maps.mapssyncd","com.apple.Maps.mapspushd","com.apple.Maps.geocorrectiond","com.apple.maps.destinationd","com.apple.MapKit.SnapshotService","com.apple.jetpackassetd","com.apple.tipsd","com.apple.gamed","com.apple.gamesaved","com.apple.GameController.gamecontrollerd"]},{"id":"messaging","name":"Messaging & FaceTime","downside":"iMessage, FaceTime, and related identity services will not work.","approxMemoryMB":60,"labels":["com.apple.identityservicesd","com.apple.ids_simd","com.apple.imautomatichistorydeletionagent","com.apple.imcore.imtransferagent","com.apple.imdpersistence.IMDPersistenceAgent","com.apple.facetimemessagestored","com.apple.telephonyutilities.callservicesd"]},{"id":"connectivity","name":"Sharing & Device Connectivity","downside":"AirDrop, Continuity, CarPlay, Watch, and Find My connectivity will not work.","approxMemoryMB":65,"labels":["com.apple.rapportd","com.apple.companiond","com.apple.carkitd","com.apple.wcd","com.apple.tvremoted","com.apple.avatarsd","com.apple.stickersd","com.apple.sociallayerd","com.apple.announced","com.apple.navd","com.apple.findmy.findmylocated"]},{"id":"telemetry","name":"Ads, Diagnostics & Telemetry","downside":"DeviceCheck plus analytics, diagnostics, and feedback services are unavailable.","approxMemoryMB":105,"labels":["com.apple.ap.adprivacyd","com.apple.ap.promotedcontentd","com.apple.diagnosticextensionsd","com.apple.feedbackd","com.apple.rtcreportingd","com.apple.securityuploadd","com.apple.geoanalyticsd","com.apple.triald","com.apple.followupd","com.apple.purplebuddy.budd","com.apple.devicecheckd"]},{"id":"other","name":"Other Background Services","downside":"Wallet, merchant, business, asset, and miscellaneous background services are unavailable.","approxMemoryMB":195,"labels":["com.apple.financed","com.apple.passd","com.apple.merchantd","com.apple.coreidvd","com.apple.businessservicesd","com.apple.deviceaccessd","com.apple.replicatord","com.apple.linkd","com.apple.ind","com.apple.storagedatad","com.apple.StatusKitAgent","com.apple.countryd","com.apple.mobileassetd","com.apple.managedconfiguration.passcodenagd"]}]}});function liveChildCount(){return liveChildren.size}function killAllChildren(){for(const e of liveChildren){try{e.kill("SIGKILL")}catch{}}}function run(e,n,t={}){const{timeoutMs:s=3e4,maxBytes:i=4*1024*1024}=t;return new Promise(r=>{const o=(0,import_node_child_process7.spawn)(e,n,{env:t.env??process.env,stdio:[t.input==null?"ignore":"pipe","pipe","pipe"]});liveChildren.add(o);let a="";let l="";let c=false;o.stdout.on("data",d=>{if(a.length<i)a+=d.toString("utf8")});o.stderr.on("data",d=>{if(l.length<i)l+=d.toString("utf8")});if(t.input!=null)o.stdin.end(t.input);const u=setTimeout(()=>{c=true;o.kill("SIGTERM");setTimeout(()=>o.kill("SIGKILL"),2e3).unref?.()},s);o.on("error",d=>{liveChildren.delete(o);clearTimeout(u);r({code:null,stdout:a,stderr:l+String(d),timedOut:c})});o.on("close",d=>{liveChildren.delete(o);clearTimeout(u);r({code:d,stdout:a,stderr:l,timedOut:c})})})}async function runOk(e,n,t={}){const s=await run(e,n,t);if(s.timedOut)throw new Error(`${e} ${n.slice(0,3).join(" ")} timed out after ${t.timeoutMs??3e4} ms`);if(s.code!==0)throw new Error(`${e} ${n.slice(0,3).join(" ")} failed (${s.code}): ${(s.stderr||s.stdout).trim().slice(0,500)}`);return s.stdout}async function listProcesses(){const e=await runOk("ps",["-axww","-o","pid=,ppid=,lstart=,stat=,comm="],{timeoutMs:15e3});const n=await runOk("ps",["-axww","-o","pid=,command="],{timeoutMs:15e3});const t=new Map;for(const i of n.split("\n")){const r=/^\s*(\d+)\s+(.*)$/.exec(i);if(r)t.set(Number(r[1]),r[2])}const s=[];for(const i of e.split("\n")){const r=/^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(\S+)\s+(.*)$/.exec(i);if(!r)continue;const o=Number(r[1]);s.push({pid:o,ppid:Number(r[2]),start:r[3].replace(/\s+/g," "),stat:r[4],comm:r[5].trim(),command:t.get(o)??r[5].trim()})}return s}function descendants(e,n){const t=new Map;const s=new Map;for(const l of e){s.set(l.pid,l);const c=t.get(l.ppid)??[];c.push(l);t.set(l.ppid,c)}const i=[];const r=s.get(n);if(!r)return i;const o=[r];const a=new Set;while(o.length){const l=o.pop();if(a.has(l.pid))continue;a.add(l.pid);i.push(l);for(const c of t.get(l.pid)??[])o.push(c)}return i}async function pidStart(e){const n=await run("ps",["-o","lstart=","-p",String(e)],{timeoutMs:5e3});const t=n.stdout.trim().replace(/\s+/g," ");return n.code===0&&t?t:null}async function pidAlive(e,n,t=false){if(!Number.isInteger(e)||e<=0)return false;try{process.kill(e,0)}catch(i){if(i.code!=="EPERM")return false}if(!n)return true;const s=await pidStart(e);if(s===null)return t;return s===n}var import_node_child_process7,liveChildren,sleep;var init_proc=__esm({"../sim-farm/src/proc.ts"(){"use strict";import_node_child_process7=require("child_process");liveChildren=new Set;sleep=e=>new Promise(n=>setTimeout(n,e))}});function storeDir(e,n=DISABLED_STORE_ROOT){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return import_node_path7.default.join(n,`com.apple.CoreSimulator.SimDevice.${e}`)}function storePath(e,n=DISABLED_STORE_ROOT){return import_node_path7.default.join(storeDir(e,n),"disabled.plist")}async function readStore(e,n=DISABLED_STORE_ROOT){const t=storePath(e,n);let s;try{s=await import_node_fs6.promises.lstat(t)}catch{return{}}if(!s.isFile())throw new Error(`refusing ${t}: not a regular file`);const i=await runOk("plutil",["-convert","json","-o","-",t],{timeoutMs:1e4});const r=JSON.parse(i);const o={};for(const[a,l]of Object.entries(r))if(typeof l==="boolean")o[a]=l;return o}function xmlEscape(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function encodeStore(e){const n=Object.keys(e).sort();let t='<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n';for(const s of n)t+=` <key>${xmlEscape(s)}</key>
|
|
56
56
|
<${e[s]?"true":"false"}/>
|
|
57
57
|
`;return t+"</dict>\n</plist>\n"}async function applyDisabled(e,n,t=DISABLED_STORE_ROOT){const s=storeDir(e,t);await import_node_fs6.promises.mkdir(s,{recursive:true});const i=await import_node_fs6.promises.lstat(s);if(!i.isDirectory())throw new Error(`refusing ${s}: not a directory`);const r=await readStore(e,t);const o=import_node_path7.default.join(s,"buoy-farm-prior.json");let a={};try{a=JSON.parse(await import_node_fs6.promises.readFile(o,"utf8"))}catch{}const l={...r};for(const c of n){if(!(c in a))a[c]=c in r?r[c]:null;l[c]=true}await writeAtomic(o,JSON.stringify(a,null,2)+"\n");await writeAtomic(storePath(e,t),encodeStore(l))}async function storeHasAll(e,n,t=DISABLED_STORE_ROOT){const s=await readStore(e,t);return n.every(i=>s[i]===true)}async function writeAtomic(e,n){const t=`${e}.${(0,import_node_crypto6.randomBytes)(4).toString("hex")}.tmp`;const s=await import_node_fs6.promises.open(t,"w",420);try{await s.writeFile(n);await s.sync()}finally{await s.close()}await import_node_fs6.promises.rename(t,e)}var import_node_fs6,import_node_path7,import_node_crypto6,DISABLED_STORE_ROOT;var init_disabledStore=__esm({"../sim-farm/src/disabledStore.ts"(){"use strict";import_node_fs6=require("fs");import_node_path7=__toESM(require("path"));import_node_crypto6=require("crypto");init_proc();DISABLED_STORE_ROOT=process.env.BUOY_FARM_DISABLED_ROOT||"/private/var/tmp"}});function seedProblem(e){if(!(0,import_node_fs7.existsSync)(import_node_path8.default.join(e,"seed.json")))return`No seed at ${e}. Run: pnpm e2e:pool seed`;if((0,import_node_fs7.existsSync)(import_node_path8.default.join(e,"Documents/buoy-license"))){for(const n of["buoy-gg-license.json","buoy-gg-device-fingerprint.json"]){if(!(0,import_node_fs7.existsSync)(import_node_path8.default.join(e,"Documents/buoy-license",n)))return`The seed lacks ${n}.`}}return null}async function seedHash(e){const n=(0,import_node_crypto7.createHash)("sha256");async function t(s){let i;try{i=await import_node_fs7.promises.readdir(s,{withFileTypes:true})}catch{return}i.sort((r,o)=>r.name.localeCompare(o.name));for(const r of i){const o=import_node_path8.default.join(s,r.name);if(r.isDirectory())await t(o);else{const a=await import_node_fs7.promises.stat(o);n.update(`${import_node_path8.default.relative(e,o)}:${a.size}:${Math.floor(a.mtimeMs)}
|
|
58
58
|
`)}}}await t(e);return n.digest("hex").slice(0,16)}async function appHash(e){const n=(0,import_node_crypto7.createHash)("sha256");const t=import_node_path8.default.join(e,"Info.plist");n.update(await import_node_fs7.promises.readFile(t));const s=JSON.parse(await runOk("plutil",["-convert","json","-o","-",t],{timeoutMs:1e4}));const i=import_node_path8.default.join(e,String(s.CFBundleExecutable??""));const r=await import_node_fs7.promises.stat(i);n.update(`${r.size}:${Math.floor(r.mtimeMs)}`);return n.digest("hex").slice(0,16)}async function appBundleId(e){const n=JSON.parse(await runOk("plutil",["-convert","json","-o","-",import_node_path8.default.join(e,"Info.plist")],{timeoutMs:1e4}));if(!n.CFBundleIdentifier)throw new Error(`${e} has no CFBundleIdentifier`);return String(n.CFBundleIdentifier)}async function findDataContainer(e,n){const t=import_node_path8.default.join(e,"Containers/Data/Application");let s;try{s=await import_node_fs7.promises.readdir(t)}catch{return null}for(const i of s){const r=import_node_path8.default.join(t,i,".com.apple.mobile_container_manager.metadata.plist");if(!(0,import_node_fs7.existsSync)(r))continue;const o=await run("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",r],{timeoutMs:5e3});if(o.code===0&&o.stdout.trim()===n)return import_node_path8.default.join(t,i)}return null}function asyncStorageDir(e,n){return import_node_path8.default.join(e,"Library/Application Support",n,"RCTAsyncLocalStorage_V1")}async function rsync(e,n,t,s){s();await import_node_fs7.promises.mkdir(n,{recursive:true});const i=["-a","--delete"];for(const r of t)i.push("--exclude",r);i.push(e.replace(/\/?$/,"/"),n.replace(/\/?$/,"/"));s();await runOk("rsync",i,{timeoutMs:6e4})}async function applySeed(e,n,t,s=()=>{}){s();const i=seedProblem(t);if(i)throw new Error(i);const r=[...IDENTITY_FILES.map(l=>`react-buoy/${l}`),`react-buoy/${WINDOW_STATE_GLOB}`];if((0,import_node_fs7.existsSync)(import_node_path8.default.join(t,"Documents")))await rsync(import_node_path8.default.join(t,"Documents"),import_node_path8.default.join(e,"Documents"),r,s);if((0,import_node_fs7.existsSync)(import_node_path8.default.join(t,"AsyncStorage/manifest.json"))){await rsync(import_node_path8.default.join(t,"AsyncStorage"),asyncStorageDir(e,n),[],s)}const o=import_node_path8.default.join(t,"Preferences",`${n}.plist`);if((0,import_node_fs7.existsSync)(o)){s();await import_node_fs7.promises.mkdir(import_node_path8.default.join(e,"Library/Preferences"),{recursive:true});await import_node_fs7.promises.copyFile(o,import_node_path8.default.join(e,"Library/Preferences",`${n}.plist`))}const a=import_node_path8.default.join(e,"Documents/react-buoy");try{for(const l of await import_node_fs7.promises.readdir(a)){s();if(l.startsWith("@buoy~1floating-tool~1"))await import_node_fs7.promises.rm(import_node_path8.default.join(a,l),{force:true,recursive:true})}}catch{}}async function stripIdentity(e,n=()=>{}){n();const t=[];const s=import_node_path8.default.join(e,"Documents/react-buoy");let i=[];try{i=await import_node_fs7.promises.readdir(s)}catch{return t}for(const r of i){if(IDENTITY_FILES.includes(r)||r.startsWith("@buoy~1floating-tool~1")){n();await import_node_fs7.promises.rm(import_node_path8.default.join(s,r),{force:true,recursive:true});t.push(r)}}return t}async function readBuoyInstallId(e){let n;try{n=await import_node_fs7.promises.readFile(import_node_path8.default.join(e,"Documents/react-buoy/@react_buoy_sync_device_id.json"),"utf8")}catch{return null}try{const t=JSON.parse(n);if(typeof t==="string")return t||null;if(typeof t?.value==="string")return t.value||null}catch{}return n.trim()||null}var import_node_fs7,import_node_path8,import_node_crypto7,IDENTITY_FILES,WINDOW_STATE_GLOB;var init_seed=__esm({"../sim-farm/src/seed.ts"(){"use strict";import_node_fs7=require("fs");import_node_path8=__toESM(require("path"));import_node_crypto7=require("crypto");init_proc();IDENTITY_FILES=["@react_buoy_sync_device_id.json","@react_buoy_install.json"];WINDOW_STATE_GLOB="@buoy~1floating-tool~1*"}});function avg(e){return e.length?e.reduce((n,t)=>n+t,0)/e.length:0}function parseCpuTime(e){const n=e.trim().split(":").map(Number);if(!n.length||n.some(t=>!Number.isFinite(t)))return 0;return Math.round(n.reduce((t,s)=>t*60+s,0)*1e3)}var KEEP_MS,TREE_EVERY,PROCS_EVERY,COVERAGE,round,median,LoadMeter;var init_meter=__esm({"../sim-farm/src/meter.ts"(){"use strict";KEEP_MS=2*60*6e4;TREE_EVERY=10;PROCS_EVERY=5;COVERAGE=.8;round=e=>Math.round(e*10)/10;median=e=>{if(!e.length)return 0;const n=[...e].sort((s,i)=>s-i);const t=n.length>>1;return n.length%2?n[t]:(n[t-1]+n[t])/2};LoadMeter=class{constructor(e,n,t=1e3,s=Date.now){this.host=e;this.target=n;this.periodMs=t;this.now=s;this.samples=[];this.prev=null;this.pids=[];this.treeAge=0;this.treeKey="";this.sampling=false;this.ticks=0;this.lastProcs=null;this.rates={ours:0,graphics:0,gpuPct:null};this.cpuCount=0}async tick(){if(this.sampling)return;this.sampling=true;try{const e=this.target();if(!e){this.prev=null;this.treeKey="";return}const n=`${e.leaseId}:${e.udids.join(",")}`;if(n!==this.treeKey||++this.treeAge>=TREE_EVERY){const h=await Promise.all(e.udids.map(p=>this.host.simTree(p)));this.pids=h.flat().map(p=>p.pid);this.treeAge=0;if(n!==this.treeKey){this.prev=null;this.lastProcs=null;this.ticks=0;this.rates={ours:0,graphics:0,gpuPct:null}}this.treeKey=n}const t=this.ticks++;const s=await this.host.loadCounters(this.pids,t<2||t%PROCS_EVERY===0);this.cpuCount=s.cpuCount;const i=this.now();const r=this.prev;this.prev={...s,at:i,leaseId:e.leaseId};const o=h=>(s.hostTotalMs-h)/Math.max(1,s.cpuCount);if(s.pidsCpuMs!==null&&s.graphicsCpuMs!==null){const h=this.lastProcs;const p=h?o(h.hostTotalMs):0;if(h&&p>0){this.rates.ours=Math.max(0,(s.pidsCpuMs-h.pidsCpuMs)/p);this.rates.graphics=Math.max(0,(s.graphicsCpuMs-h.graphicsCpuMs)/p)}this.rates.gpuPct=s.gpuPct;this.lastProcs={hostTotalMs:s.hostTotalMs,pidsCpuMs:s.pidsCpuMs,graphicsCpuMs:s.graphicsCpuMs}}if(!r||r.leaseId!==e.leaseId)return;const a=o(r.hostTotalMs);if(a<=0)return;const l=Math.max(0,(s.hostBusyMs-r.hostBusyMs)/a);const c=this.rates.ours;const u=this.rates.graphics;this.samples.push({at:Math.round((i+r.at)/2),leaseId:e.leaseId,freeCores:round(Math.max(0,s.cpuCount-l)),ourCores:round(c),graphicsCores:round(u),outsideCores:round(Math.max(0,l-c-u)),gpuPct:this.rates.gpuPct,memLevel:s.memPressureLevel,swapMB:s.swapUsedMB,thermal:s.thermalLevel});const d=i-KEEP_MS;while(this.samples.length&&this.samples[0].at<d)this.samples.shift()}finally{this.sampling=false}}before(e,n){for(let t=this.samples.length-1;t>=0;t--){const s=this.samples[t];if(s.at<n)return s.leaseId===e?s:void 0}return void 0}latest(){return this.samples[this.samples.length-1]}verdicts(e,n,t){const s=this.periodMs/2;const i=n.map(o=>{const a=this.samples.filter(c=>c.at>=o.from-s&&c.at<=o.to+s);const l=a.filter(c=>c.leaseId===e);return{w:o,all:a,mine:l}});const r=median(i.filter(o=>o.mine.length).map(o=>avg(o.mine.map(a=>a.outsideCores))));return i.map(({w:o,all:a,mine:l})=>{const c=Math.max(1,Math.round((o.to-o.from)/this.periodMs));const u={id:o.id,fair:false,seconds:l.length};if(!a.length&&(o.from>this.now()+6e4||o.to<this.now()-KEEP_MS)){return{...u,reason:"This run's time doesn't match the Mac's clock. Was the app's clock moved? Put it back and run again."}}if(!l.length||l.length<a.length||l.length<Math.floor(c*COVERAGE)){return{...u,reason:"No load record for all of this run. Hold perf-slot for the whole test."}}const d=l.map(g=>g.outsideCores);const h=l.map(g=>g.gpuPct).filter(g=>g!==null);const p=l.map(g=>g.thermal).filter(g=>g!==null);const m={...u,freeCoresMin:Math.min(...l.map(g=>g.freeCores)),outsideCoresAvg:round(avg(d)),outsideCoresMax:Math.max(...d),ourCoresAvg:round(avg(l.map(g=>g.ourCores))),graphicsCoresMax:Math.max(...l.map(g=>g.graphicsCores)),gpuMaxPct:h.length?Math.max(...h):null,swapGrowthMB:Math.round(Math.max(0,l[l.length-1].swapMB-(this.before(e,o.from)?.swapMB??l[0].swapMB))),thermalMax:p.length?Math.max(...p):null};if(m.thermalMax!=null&&m.thermalMax>t.maxThermalLevel){return{...m,reason:"The Mac got hot and slowed itself down. Let it cool, then run again."}}if(l.some(g=>g.memLevel>=4)){return{...m,reason:"The Mac ran low on memory. Close apps or stop sims you don't need."}}if(m.swapGrowthMB>t.maxSwapGrowthMB){return{...m,reason:`The Mac swapped ${m.swapGrowthMB} MB of memory to disk.`}}if(m.freeCoresMin<t.minFreeCores){return{...m,reason:`The Mac ran out of spare CPU (${m.freeCoresMin} cores free, needs ${t.minFreeCores}).`}}const y=round(m.outsideCoresMax-r);if(y>t.maxSwingCores){return{...m,reason:`Other work on the Mac jumped by ${y} cores during this run.`}}return{...m,fair:true}})}}}});var PHONE_PROFILE_SOURCE,PHONE_PROFILE_HASH,PHONE_PROBE_SOURCE;var init_phoneProfileSource_generated=__esm({"../sim-farm/src/phoneProfileSource.generated.ts"(){"use strict";PHONE_PROFILE_SOURCE=`// BuoyPhoneProfile: makes an app on the iOS Simulator act like a real iPhone.
|
|
@@ -417,7 +417,7 @@ static void buoyPhoneProfileInit(void) {
|
|
|
417
417
|
`;PHONE_PROFILE_HASH="499c4e3de0f8";PHONE_PROBE_SOURCE='// PhoneProbe: a tiny app `farm phone measure` puts on a real iPhone for a few\n// seconds. It prints what the phone reports, then quits:\n// physicalMemory the RAM iOS reports\n// cpus the CPU count\n// footprint this app\'s own footprint right now\n// available os_proc_available_memory(): how much more this app may use\n// footprint + available is the per-app limit (jetsam) on this phone.\n\n#import <UIKit/UIKit.h>\n#import <mach/mach.h>\n#import <os/proc.h>\n\nstatic uint64_t footprintBytes(void) {\n task_vm_info_data_t info;\n mach_msg_type_number_t count = TASK_VM_INFO_COUNT;\n if (task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&info, &count) != KERN_SUCCESS) return 0;\n return info.phys_footprint;\n}\n\n// iOS 27 ends apps that don\'t use scenes, so the window comes from a scene delegate.\n@interface ProbeSceneDelegate : UIResponder <UIWindowSceneDelegate>\n@property (nonatomic, strong) UIWindow *window;\n@end\n\n@implementation ProbeSceneDelegate\n- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options {\n self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];\n self.window.rootViewController = [UIViewController new];\n self.window.rootViewController.view.backgroundColor = UIColor.systemTealColor;\n [self.window makeKeyAndVisible];\n}\n@end\n\n@interface ProbeDelegate : UIResponder <UIApplicationDelegate>\n@end\n\n@implementation ProbeDelegate\n- (UISceneConfiguration *)application:(UIApplication *)app configurationForConnectingSceneSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options {\n UISceneConfiguration *c = [[UISceneConfiguration alloc] initWithName:@"Default" sessionRole:session.role];\n c.delegateClass = ProbeSceneDelegate.class;\n return c;\n}\n- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts {\n NSProcessInfo *pi = NSProcessInfo.processInfo;\n fprintf(stdout, "BUOY_PROBE physicalMemory=%llu cpus=%lu footprint=%llu available=%zu\\n",\n pi.physicalMemory, (unsigned long)pi.processorCount, footprintBytes(), os_proc_available_memory());\n fflush(stdout);\n dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ exit(0); });\n return YES;\n}\n@end\n\nint main(int argc, char *argv[]) {\n @autoreleasepool {\n return UIApplicationMain(argc, argv, nil, NSStringFromClass(ProbeDelegate.class));\n }\n}\n'}});function farmHome(){return process.env.BUOY_FARM_HOME||import_node_path9.default.join(import_node_os6.default.homedir(),".buoy","farm")}function phoneDir(e=farmHome()){return import_node_path9.default.join(e,"phone")}function userPhonesFile(e=farmHome()){return import_node_path9.default.join(phoneDir(e),"phones.json")}function isValidPhone(e){const n=e;const t=(s,i,r)=>typeof s==="number"&&Number.isInteger(s)&&s>=i&&s<=r;return!!n&&typeof n==="object"&&typeof n.name==="string"&&n.name.length>0&&n.name.length<=100&&typeof n.productType==="string"&&t(n.ramMB,256,1<<20)&&t(n.cpus,1,256)&&t(n.limitMB,64,n.ramMB)&&t(n.warnMB,32,n.limitMB-1)}function loadPhones(e=farmHome()){const n={...BUILT_IN_PHONES};try{const t=JSON.parse((0,import_node_fs8.readFileSync)(userPhonesFile(e),"utf8"));for(const[s,i]of Object.entries(t?.phones??{})){if(/^[a-z0-9][a-z0-9-]{0,63}$/.test(s)&&isValidPhone(i))n[s]=i}}catch{}return n}async function savePhone(e,n,t=farmHome()){if(!/^[a-z0-9][a-z0-9-]{0,63}$/.test(e))throw new Error(`Bad phone id ${e}.`);if(!isValidPhone(n))throw new Error(`Bad phone numbers for ${e}.`);const s=userPhonesFile(t);let i={phones:{}};try{i=JSON.parse(await import_node_fs8.promises.readFile(s,"utf8"));if(!i||typeof i.phones!=="object")i={phones:{}}}catch{}i.phones[e]=n;await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(s),{recursive:true});const r=`${s}.${process.pid}.tmp`;await import_node_fs8.promises.writeFile(r,JSON.stringify(i,null,2)+"\n");await import_node_fs8.promises.rename(r,s)}function defaultCapMB(e){return Math.max(3072,e.limitMB+1024)}function profileVars(e,n,t){const s=n.capMB>e.limitMB?n.capMB:defaultCapMB(e);return{BUOY_PROFILE_NAME:e.name,BUOY_PROFILE_RAM_MB:String(e.ramMB),BUOY_PROFILE_CPUS:String(e.cpus),BUOY_PROFILE_LIMIT_MB:String(e.limitMB),BUOY_PROFILE_WARN_MB:String(e.warnMB),BUOY_PROFILE_ON_LIMIT:n.onLimit,BUOY_PROFILE_CAP_MB:String(s),BUOY_PROFILE_EVENTS:t}}function joinInsertList(...e){const n=new Set;const t=[];for(const s of e){for(const i of(s??"").split(":")){const r=i.trim();if(r&&!n.has(r)){n.add(r);t.push(r)}}}return t.join(":")}function launchFilePath(e,n=farmHome()){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return import_node_path9.default.join(phoneDir(n),`${e}.launch.json`)}function eventsFilePath(e,n=farmHome()){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return import_node_path9.default.join(phoneDir(n),`${e}.events.jsonl`)}async function writeLaunchFile(e,n=farmHome()){const t=launchFilePath(e.udid,n);await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(t),{recursive:true});const s=`${t}.${process.pid}.tmp`;await import_node_fs8.promises.writeFile(s,JSON.stringify(e,null,2)+"\n");await import_node_fs8.promises.rename(s,t)}async function removeLaunchFile(e,n=farmHome()){await import_node_fs8.promises.rm(launchFilePath(e,n),{force:true})}async function readLaunchFile(e,n=farmHome()){try{const t=JSON.parse(await import_node_fs8.promises.readFile(launchFilePath(e,n),"utf8"));if(t?.udid!==e||typeof t.dylib!=="string"||!t.vars||typeof t.vars!=="object")return null;await import_node_fs8.promises.access(t.dylib);return t}catch{return null}}async function simWideInsertList(e){try{const{stdout:n}=await execFileAsync6("xcrun",["simctl","spawn",e,"launchctl","getenv","DYLD_INSERT_LIBRARIES"],{timeout:1e4});return n.trim()}catch{return""}}async function simLaunchEnv(e,n,t,s=farmHome(),i=simWideInsertList){const r=await readLaunchFile(e,s);if(!r||r.bundleId!==n){return t?{SIMCTL_CHILD_DYLD_INSERT_LIBRARIES:joinInsertList(process.env.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES,await i(e),t)}:{}}const o={};for(const[a,l]of Object.entries(r.vars))o[`SIMCTL_CHILD_${a}`]=l;o.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES=joinInsertList(process.env.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES,await i(e),t,r.dylib);return o}function parseEvents(e){const n=[];for(const t of e.split("\n")){if(!t.trim())continue;try{const s=JSON.parse(t);if(s&&typeof s.event==="string"&&typeof s.pid==="number")n.push(s)}catch{}}return n}function summarize(e){let n=-1;for(let r=e.length-1;r>=0;r--)if(e[r].event==="profile_loaded"){n=r;break}if(n<0)return{loaded:false,ignored:[],memoryWarnings:0,overLimit:false,peakMB:0};const t=e[n];const s=e.slice(n).filter(r=>r.pid===t.pid);const i={pid:t.pid,loaded:true,ignored:Array.isArray(t.ignored)?t.ignored:[],memoryWarnings:0,overLimit:false,peakMB:0};for(const r of s){i.peakMB=Math.max(i.peakMB,r.footprintMB??0,typeof r.peakMB==="number"?r.peakMB:0);i.lastFootprintMB=r.footprintMB;if(r.event==="memory_warning")i.memoryWarnings++;if(r.event==="over_limit")i.overLimit=true;if(r.event==="jetsam_kill"){i.overLimit=true;i.ended="limit"}if(r.event==="safety_kill"){i.overLimit=true;i.ended="safety"}}return i}async function readSummary(e,n=farmHome()){const t=eventsFilePath(e,n);try{const s=await import_node_fs8.promises.open(t,"r");try{const{size:i}=await s.stat();const r=Math.min(i,256*1024);const o=Buffer.alloc(r);await s.read(o,0,r,i-r);let a=o.toString("utf8");if(r<i)a=a.slice(a.indexOf("\n")+1);return summarize(parseEvents(a))}finally{await s.close()}}catch{return null}}async function xcodeBuildId(){try{const{stdout:e}=await execFileAsync6("xcodebuild",["-version"],{timeout:3e4});return e.trim().split("\n").pop().replace(/^Build version\s*/,"").replace(/[^\w.-]/g,"")||"unknown"}catch{return"unknown"}}async function buildDylib(e){const n=import_node_path9.default.join(e,"BuoyPhoneProfile.dylib");try{await import_node_fs8.promises.access(n);return n}catch{}const t=await import_node_fs8.promises.mkdtemp(import_node_path9.default.join(import_node_os6.default.tmpdir(),"buoy-phone-profile-"));try{const s=import_node_path9.default.join(t,"BuoyPhoneProfile.m");await import_node_fs8.promises.writeFile(s,PHONE_PROFILE_SOURCE);const i=import_node_path9.default.join(t,"BuoyPhoneProfile.dylib");await execFileAsync6("xcrun",["--sdk","iphonesimulator","clang","-arch","arm64","-arch","x86_64","-mios-simulator-version-min=13.0","-fobjc-arc","-O2","-Wall","-Werror","-dynamiclib","-framework","Foundation","-install_name","@rpath/BuoyPhoneProfile.dylib",s,"-o",i],{timeout:18e4,maxBuffer:8*1024*1024,env:{...process.env,TMPDIR:t}});await execFileAsync6("codesign",["-f","-s","-",i],{timeout:6e4});await import_node_fs8.promises.mkdir(e,{recursive:true});const r=`${n}.${process.pid}`;await import_node_fs8.promises.copyFile(i,r);await import_node_fs8.promises.rename(r,n);return n}finally{await import_node_fs8.promises.rm(t,{recursive:true,force:true}).catch(()=>void 0)}}var import_node_child_process8,import_node_fs8,import_node_os6,import_node_path9,import_node_util6,execFileAsync6,BUILT_IN_PHONES,DEFAULT_PHONE,PHONE_OFF,LocalDylibBuilder;var init_phoneProfile=__esm({"../sim-farm/src/phoneProfile.ts"(){"use strict";import_node_child_process8=require("child_process");import_node_fs8=require("fs");import_node_os6=__toESM(require("os"));import_node_path9=__toESM(require("path"));import_node_util6=require("util");init_phoneProfileSource_generated();execFileAsync6=(0,import_node_util6.promisify)(import_node_child_process8.execFile);BUILT_IN_PHONES={"iphone-se-2nd-gen":{name:"iPhone SE (2nd generation)",productType:"iPhone12,8",ramMB:2957,cpus:6,limitMB:2098,warnMB:1707,warnSource:"measured (plain memory test); the phone warned at 1,134 MB in a photos test",ios:"26.5",measuredAt:"2026-10-14"}};DEFAULT_PHONE="iphone-se-2nd-gen";PHONE_OFF="off";LocalDylibBuilder=class{constructor(e=import_node_path9.default.join(import_node_os6.default.homedir(),".buoy","phone-profile")){this.root=e;this.built=null;this.key="";this.error="";this.failedAt=0}dylib(){if(process.platform!=="darwin"){this.error="Phone profiles need a Mac.";return Promise.resolve(null)}const e=process.env.DEVELOPER_DIR??"";if(this.built&&this.key===e)return this.built;if(this.key===e&&this.failedAt&&Date.now()-this.failedAt<5*6e4)return Promise.resolve(null);if(this.key!==e)this.failedAt=0;this.key=e;this.error="";this.built=xcodeBuildId().then(n=>buildDylib(import_node_path9.default.join(this.root,`${n}-${PHONE_PROFILE_HASH}`))).catch(n=>{const t=n;this.error=(t.stderr||t.message||"build failed").trim().split("\n").slice(-3).join(" ").slice(0,300);this.built=null;this.failedAt=Date.now();return null});return this.built}lastError(){return this.error}}}});function ramReserveGB(e,n){return Math.min(n,Math.max(4,Math.round(e*.375)))}function memHeadroomGB(e){return Math.min(4,Math.max(1.5,e*.1))}function fairRules(e,n){const t=s=>Math.round(s*10)/10;return{minFreeCores:t(Math.max(2,e.fairMinFreeShare*n)),maxSwingCores:t(Math.max(1,e.fairMaxSwingShare*n)),maxThermalLevel:e.fairMaxThermalLevel,maxSwapGrowthMB:e.fairMaxSwapGrowthMB}}function crashLoop(e,n=Date.now()-3*6e4){const t=new Map;for(const s of e.split("\n")){const i=/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ \((?:user|system)\/\d*\/?([\w.-]+) \[\d+\]\) <\w+>: exited due to (SIG\w+)/.exec(s);if(!i||i[3]==="SIGKILL"||i[3]==="SIGTERM")continue;const r=Date.parse(i[1].replace(" ","T"));if(!(r>=n))continue;const o=t.get(i[2])??[];o.push(r);t.set(i[2],o)}for(const[s,i]of t){i.sort((r,o)=>r-o);for(let r=2;r<i.length;r++)if(i[r]-i[r-2]<=6e4)return s.replace(/^com\.apple\./,"")}return null}var import_node_fs9,import_node_path10,import_node_async_hooks2,import_node_crypto8,GB,NAME_PREFIX,MARKER,SLIM_MAX_PROCS,STOCK_MIN_PROCS,LEASE_GRACE_MS,REQUEST_MEMORY,RUN_NOTES,MAX_SHOTS,ENDED,LANE_STATES,monotonic,PHONE_RETRY_MS,KeyedMutex,BusySims,Semaphore,Engine;var init_engine=__esm({"../sim-farm/src/engine.ts"(){"use strict";import_node_fs9=require("fs");import_node_path10=__toESM(require("path"));import_node_async_hooks2=require("async_hooks");import_node_crypto8=require("crypto");init_disabledStore();init_profiles();init_proc();init_seed();init_meter();init_phoneProfile();init_types();GB=1024**3;NAME_PREFIX="Buoy Farm ";MARKER=".buoy-farm.json";SLIM_MAX_PROCS=120;STOCK_MIN_PROCS=180;LEASE_GRACE_MS=9e4;REQUEST_MEMORY=500;RUN_NOTES=200;MAX_SHOTS=3;ENDED=["done","failed","stopped","interrupted"];LANE_STATES=["setting-up","working","waiting","passed","failed","needs-help"];monotonic={now:()=>performance.now()};PHONE_RETRY_MS=10*6e4;KeyedMutex=class{constructor(){this.tails=new Map}run(e,n){const t=this.tails.get(e)??Promise.resolve();const s=t.catch(()=>{}).then(n);this.tails.set(e,s);s.finally(()=>{if(this.tails.get(e)===s)this.tails.delete(e)}).catch(()=>{});return s}};BusySims=class{constructor(){this.counts=new Map}get size(){return this.counts.size}has(e){return this.counts.has(e)}add(e){this.counts.set(e,(this.counts.get(e)??0)+1)}delete(e){const n=(this.counts.get(e)??0)-1;if(n>0)this.counts.set(e,n);else this.counts.delete(e)}};Semaphore=class{constructor(e){this.free=e;this.waiting=[]}async use(e){if(this.free>0)this.free--;else await new Promise(n=>this.waiting.push(n));try{return await e()}finally{const n=this.waiting.shift();if(n)n();else this.free++}}};Engine=class{constructor(e){this.phoneFailed=new Map;this.simLock=new KeyedMutex;this.deadlines=new Map;this.reservations=new Map;this.busy=new BusySims;this.ticking=false;this.hardDeadlines=new Map;this.acquiring=new Set;this.budgetLock=new KeyedMutex;this.labels=new Map;this.inflight=new Map;this.workContext=new import_node_async_hooks2.AsyncLocalStorage;this.activeWork=0;this.lockQueues=new Map;this.fileTasks=new Set;this.checkActive=()=>this.store.assertActive();this.shotsInFlight=0;this.pendingBg=0;this.host=new Proxy(e.host,{get:(n,t)=>{const s=Reflect.get(n,t);if(typeof s!=="function")return s;return async(...i)=>{this.store.assertActive();const r=await s.apply(n,i);this.store.assertActive();return r}}});this.store=e.store;this.clock=e.clock??monotonic;this.log=e.log??(()=>{});this.pollMs=e.pollMs??1e3;this.readyTimeoutMs=e.readyTimeoutMs??12e4;this.phoneBuilder=e.phoneBuilder??new LocalDylibBuilder;this.phoneLoadTimeoutMs=e.phoneLoadTimeoutMs??1e4;this.bootGate=new Semaphore(this.state.settings.maxConcurrentBoots);this.meter=new LoadMeter(this.host,()=>{const n=this.state.locks["perf-slot"];const t=n&&this.state.leases[n.leaseId];return t?{leaseId:t.id,udids:t.sims}:null})}async createTemplate(e){return this.work(void 0,()=>this.createTemplateWork(e))}async cloneSim(e,n){return this.work(void 0,()=>this.cloneSimWork(e,n))}async bootSim(e,n){return this.work(n,()=>this.bootSimWork(e,n))}async parkSim(e,n="idle"){return this.work(void 0,()=>this.parkSimWork(e,n))}async deleteSim(e,n){return this.work(n,()=>this.deleteSimWork(e,n))}async rebuildSim(e,n){return this.work(void 0,()=>this.simLock.run(`rebuild:${e}`,()=>this.rebuildSimWork(e,n)))}async fastReset(e,n){return this.work(void 0,()=>this.fastResetWork(e,n))}async claimOne(e,n){return this.work(e.id,()=>this.claimOneWork(e,n))}async action(e,n,t,s,i,r){this.touch(this.liveLease(e,n,r));const o=this.state.locks["perf-slot"];if(s==="screenshot"&&o&&o.leaseId!==e)throw new FarmError("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);return this.work(s==="reset"||s==="rebuild"?void 0:e,()=>this.actionWork(e,n,t,s,i,r))}async work(e,n){this.store.assertActive();if(this.workContext.getStore()?.active)return n();while(this.state.locks["perf-slot"]&&this.state.locks["perf-slot"].leaseId!==e){await sleep(this.pollMs);this.store.assertActive()}const t={active:true,leaseId:e};this.activeWork++;try{return await this.workContext.run(t,n)}finally{t.active=false;this.activeWork--}}async files(e){this.checkActive();const n=e();this.fileTasks.add(n);try{const t=await n;this.checkActive();return t}finally{this.fileTasks.delete(n)}}fence(){this.store.fence()}async settleFiles(){await Promise.allSettled([...this.fileTasks])}get state(){return this.store.state}async recover(){for(const[s,i]of Object.entries(this.state.locks)){if(!this.state.leases[i.leaseId])delete this.state.locks[s]}const e=await this.host.listDevices();const n=new Map(e.map(s=>[s.udid,s]));for(const s of Object.values(this.state.sims)){const i=n.get(s.udid);if(!i){this.store.event({kind:"lost",message:`${s.name} was deleted outside the farm.`,udid:s.udid});if(s.leaseId)this.dropFromLease(s.leaseId,s.udid);await this.files(()=>this.forgetPhone(s.udid)).catch(()=>void 0);delete this.state.sims[s.udid];continue}if(i.state==="Booted"){s.power=s.power==="frozen"?"frozen":"awake";if(s.power!=="frozen"){const r=this.state.templates[s.templateId];const o=await this.health(s.udid,r?profileLabels(r.profile):[]);s.profile=o.profile;s.lastCheckAt=new Date().toISOString();if(!o.ok||r?.profile==="slim"&&o.profile==="unknown"){s.health="suspect";s.lastError=o.faults.join("; ")||"Slim state is not known."}}}else{s.power=s.power==="parked"?"parked":"cold"}if(["creating","booting","thawing","deleting"].includes(s.power))s.power=i.state==="Booted"?"awake":"cold"}for(const s of Object.values(this.state.templates)){const i=n.get(s.udid);if(!i){s.usable=false;s.note="Its sim was deleted outside the farm."}else if(!(0,import_node_fs9.existsSync)(s.appPath)){s.note="Its app build is gone. Clones still work; rebuild the template for a new build."}}for(const s of Object.keys(this.state.orphans)){if(!n.has(s))delete this.state.orphans[s]}for(const s of this.store.pending){if(s.op==="clone"){const i=s.data?.name;const r=e.find(o=>o.name===i&&!this.state.sims[o.udid]);if(r){this.state.orphans[r.udid]={udid:r.udid,name:r.name,cloneIntentName:i};this.store.event({kind:"orphan",message:`Found ${r.name} with no record after a crash. Held for review. Use farm delete to remove it.`,udid:r.udid})}}if(s.op!=="rebuild")this.store.result(s.seq,s.op,{recovered:true})}const t=new Set;for(const s of[...this.store.pending]){if(s.op!=="rebuild")continue;const{udid:i,name:r,templateId:o}=s.data;if(Object.values(this.state.orphans).some(a=>a.name===r))continue;t.add(i);this.busy.add(i);this.background(`resume rebuild ${r}`,()=>this.work(void 0,async()=>{try{const a=Object.values(this.state.sims).find(l=>l.name===r&&l.udid!==i);if(!a){if(this.state.sims[i]){const l=this.state.sims[i];if(l.leaseId)this.dropFromLease(l.leaseId,i);l.leaseId=void 0;await this.deleteSim(i)}await this.cloneSim(o,r)}this.store.result(s.seq,"rebuild",{recovered:true})}finally{this.busy.delete(i)}}))}for(const s of Object.values(this.state.leases)){this.deadlines.set(s.id,this.clock.now()+LEASE_GRACE_MS);const i=Date.parse(s.createdAt)+s.ttlMs-Date.now();this.hardDeadlines.set(s.id,this.clock.now()+Math.max(i,0));if(i<=0)this.background(`expire ${s.id}`,()=>this.endLease(s.id,"ran out of time while the farm was down","full","deadline"))}for(const s of Object.values(this.state.sims)){if(s.health==="dirty"&&!s.leaseId&&!t.has(s.udid)){this.busy.add(s.udid);this.background(`resume reset ${s.name}`,async()=>{try{await this.rebuildSim(s.udid)}finally{this.busy.delete(s.udid)}})}}this.store.save()}async dataPath(e){const n=(await this.host.listDevices()).find(t=>t.udid===e);if(!n)throw new FarmError("NOT_FOUND",`No sim ${e} on this Mac.`);return n.dataPath}async isOwned(e){const n=this.state.sims[e]??Object.values(this.state.templates).find(t=>t.udid===e);if(!n)return false;try{const t=JSON.parse(await import_node_fs9.promises.readFile(import_node_path10.default.join(await this.dataPath(e),MARKER),"utf8"));return t.farmId===this.state.farmId}catch{return false}}async assertOwned(e){if(!await this.isOwned(e)){throw new FarmError("NOT_OWNED",`Sim ${e} is not a farm sim. The farm never changes sims it did not make.`)}}async writeMarker(e,n){const t=await this.dataPath(e);await this.files(async()=>{await import_node_fs9.promises.mkdir(t,{recursive:true});this.checkActive();await import_node_fs9.promises.writeFile(import_node_path10.default.join(t,MARKER),JSON.stringify({farmId:this.state.farmId,...n})+"\n")})}farmAwakeGB(){const e=this.state.settings;let n=0;for(const t of Object.values(this.state.sims)){if(["awake","booting","frozen","thawing"].includes(t.power))n+=t.profile==="slim"?e.slimSimGB:e.stockSimGB}return n}async budget(e={ramGB:0,diskGB:0}){const n=this.state.settings;const t=await this.host.hostStats();let s=0;let i=0;for(const p of this.reservations.values()){s+=p.ramGB;i+=p.diskGB}const r=t.totalMemBytes/GB;const o=memHeadroomGB(r)*(t.memPressure==="warn"?2:1);const a=t.availableMemBytes/GB-s;const l=r-ramReserveGB(r,n.reserveRamGB)-this.farmAwakeGB()-s;const c=t.freeDiskBytes/GB-i-n.reserveDiskGB;const u=Math.max(0,Math.floor(Math.min(a-o,l,c/n.cloneDiskGB*n.slimSimGB)/n.slimSimGB));const d=(p,m)=>({ok:false,code:p,reason:m,stats:t,roomFor:0});if(t.memPressure==="critical")return d("RAM_LOW","This Mac is low on memory. New sims will wait.");if(e.diskGB>0&&c-e.diskGB<0){return d("DISK_LOW",`Need ${n.reserveDiskGB} GB free after this. This Mac has ${Math.floor(t.freeDiskBytes/GB)} GB.`)}if(e.ramGB>0){if(a-e.ramGB<o||l-e.ramGB<0){return d("RAM_LOW",`Not enough free memory for another sim (${Math.max(0,Math.floor(a))} GB free).`)}}const h=this.state.locks["perf-slot"];if(h&&(!this.workContext.getStore()?.active||this.workContext.getStore()?.leaseId!==h.leaseId))return d("HOST_BUSY","A speed test is running. New sims will wait.");return{ok:true,stats:t,roomFor:u}}async withReservation(e,n){const t=(0,import_node_crypto8.randomBytes)(4).toString("hex");await this.budgetLock.run("budget",async()=>{const s=await this.budget(e);if(!s.ok)throw new FarmError(s.code,s.reason,void 0,true);this.reservations.set(t,e)});try{return await n()}finally{this.reservations.delete(t)}}async health(e,n=[]){const t=[];let s;try{s=await this.host.simTree(e)}catch(a){this.checkActive();return{ok:false,profile:"unknown",faults:[`Could not read sim processes: ${a.message}`],procs:0}}if(s.length===0)return{ok:false,profile:"unknown",faults:["launchd_sim is not running"],procs:0};const i=new Set(s.map(a=>a.comm.split("/").pop()));for(const a of["SpringBoard","backboardd"])if(!i.has(a))t.push(`${a} is not running`);const r=crashLoop(await this.host.launchdLogTail(e));if(r)t.push(`${r} keeps crashing`);if(t.length===0){const a=import_node_path10.default.join(this.store.shotsPath,`${e}.png`);await this.files(()=>import_node_fs9.promises.mkdir(import_node_path10.default.dirname(a),{recursive:true}));if(!await this.host.screenshot(e,a,1e4))t.push("screenshot did not come back")}let o=s.length<SLIM_MAX_PROCS?"slim":s.length>STOCK_MIN_PROCS?"stock":"unknown";if(t.length===0&&n.length){const a=await this.host.spawn(e,["launchctl","print-disabled","system"],2e4);if(a.code===0&&!a.timedOut){const l=new Set([...a.stdout.matchAll(/"([^"]+)" => (?:disabled|true)/g)].map(u=>u[1]));const c=n.filter(u=>!l.has(u)).length;o=c===0?"slim":c===n.length?"stock":"unknown"}else{o="unknown"}}return{ok:t.length===0,profile:o,faults:t,procs:s.length}}async waitHealthy(e,n=[]){const t=performance.now()+this.readyTimeoutMs;let s=await this.health(e,n);while(!s.ok&&performance.now()<t){await sleep(this.pollMs);s=await this.health(e,n)}return s}async createTemplateWork(e){if(!["slim","stock"].includes(e.profile)||!Number.isInteger(e.metroPort)||e.metroPort<1||e.metroPort>65535){throw new FarmError("BAD_REQUEST","Use slim or stock and a port from 1 to 65535.")}const n=e.seedDir==null?null:seedProblem(e.seedDir);if(n)throw new FarmError("NO_SEED",n,"Run: pnpm e2e:pool seed");if(!(0,import_node_fs9.existsSync)(import_node_path10.default.join(e.appPath,"Info.plist"))){throw new FarmError("BAD_REQUEST",`No app build at ${e.appPath}.`,"Build the app first.")}const t=await appBundleId(e.appPath);const s=`t-${(0,import_node_crypto8.randomBytes)(3).toString("hex")}`;const i=e.name??`${NAME_PREFIX}Template ${s.slice(2)}`;return this.withReservation({ramGB:this.state.settings.stockSimGB,diskGB:4},async()=>{const r=this.store.intent("create-template",{id:s,name:i});const o=await this.host.create(i,e.deviceType,e.runtime);const a={id:s,udid:o,name:i,bundleId:t,appPath:e.appPath,appHash:await appHash(e.appPath),seedDir:e.seedDir??null,seedHash:e.seedDir?await seedHash(e.seedDir):null,launchUrl:e.launchUrl,deviceType:e.deviceType,runtime:e.runtime,xcodeBuild:await this.host.xcodeBuild(),profile:e.profile,profileHash:profileHash(e.profile),metroPort:e.metroPort,createdAt:new Date().toISOString(),usable:false,note:"Being built."};this.state.templates[s]=a;await this.writeMarker(o,{template:s});this.store.result(r,"create-template",{udid:o});try{await this.buildTemplate(a,e.warmLaunch!==false);a.usable=true;a.note=void 0;this.store.event({kind:"template",message:`Template ${i} is ready.`,udid:o})}catch(l){a.note=`Build failed: ${l.message}`;this.store.event({kind:"template-failed",message:a.note,udid:o});await this.host.shutdown(o).catch(()=>{});throw l}finally{this.store.save()}return a})}async buildTemplate(e,n){const t=profileLabels(e.profile);if(t.length)await this.files(()=>applyDisabled(e.udid,t,this.host.disabledRoot));await this.host.boot(e.udid);const s=await this.waitHealthy(e.udid,t);if(!s.ok)throw new Error(`template did not come up: ${s.faults.join("; ")}`);if(e.profile==="slim"&&s.profile!=="slim")throw new Error(`slim did not apply (${s.procs} processes)`);await this.host.install(e.udid,e.appPath);const i=await this.host.appContainer(e.udid,e.bundleId);if(!i)throw new Error("app installed but has no data container");if(e.seedDir)await this.files(()=>applySeed(i,e.bundleId,e.seedDir,this.checkActive));else await this.files(()=>stripIdentity(i,this.checkActive));await this.setPrefs(e.udid,e);if(n){await this.launchApp(e.udid,e,{phone:false});await sleep(Math.min(15e3,this.pollMs*15));await this.host.terminate(e.udid,e.bundleId)}await this.host.shutdown(e.udid);await this.files(()=>stripIdentity(i,this.checkActive))}async launchApp(e,n,t={}){if(t.phone===false){await this.host.launch(e,n.bundleId)}else{await this.launchAsPhone(e,n.bundleId)}if(n.launchUrl)await this.host.openUrl(e,n.launchUrl)}async launchAsPhone(e,n){const t=this.store.homeDir;const s=this.state.settings;const i=f=>{const w=this.state.sims[e];if(w)w.phone=f};const r=async f=>{const w=await this.host.spawn(f,["launchctl","getenv","DYLD_INSERT_LIBRARIES"],1e4).catch(()=>null);return w&&w.code===0?w.stdout.trim():""};const o=async f=>{await this.files(()=>removeLaunchFile(e,t));const w=await simLaunchEnv(e,n,void 0,t,r);await this.host.launch(e,n,w);i(f)};if(s.phoneProfile===PHONE_OFF)return o({status:"off"});const a=loadPhones(t);const l=a[s.phoneProfile]?s.phoneProfile:DEFAULT_PHONE;const c=a[l];const u=l!==s.phoneProfile?`No phone "${s.phoneProfile}" on this Mac, so it uses ${c.name}. `:"";const d=this.phoneFailed.get(n);if(d!==void 0&&performance.now()-d<PHONE_RETRY_MS){return o({status:"fallback",phoneId:l,phoneName:c.name,note:`${u}This app did not start with the phone profile a moment ago, so it runs as a plain simulator app. The farm tries again in a few minutes.`})}const h=await this.phoneBuilder.dylib();if(!h){const f=this.phoneBuilder.lastError()||"unknown error";this.log(`phone profile: could not build the library: ${f}`);return o({status:"fallback",phoneId:l,phoneName:c.name,note:`${u}The phone profile could not be built (${f}), so the app runs as a plain simulator app.`})}const p=eventsFilePath(e,t);await this.files(async()=>{await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(p),{recursive:true});await import_node_fs9.promises.writeFile(p,"");await writeLaunchFile({udid:e,bundleId:n,dylib:h,phoneId:l,vars:profileVars(c,{phone:l,onLimit:s.phoneOnLimit,capMB:s.phoneCapMB},p),writtenAt:new Date().toISOString()},t)});const m=await simLaunchEnv(e,n,void 0,t,r);await this.host.launch(e,n,m);const y=performance.now()+this.phoneLoadTimeoutMs;let g=null;for(;;){g=await readSummary(e,t);if(g?.loaded||performance.now()>=y)break;await sleep(Math.min(250,Math.max(10,this.pollMs/4)))}if(g?.loaded){this.phoneFailed.delete(n);const f=g.ignored.length?` Ignored bad settings: ${g.ignored.join(", ")}.`:"";i({status:"on",phoneId:l,phoneName:c.name,pid:g.pid,note:(u+f).trim()||void 0});return}this.log(`phone profile: ${n} on ${e} did not report it; launching without it`);this.phoneFailed.set(n,performance.now());await this.host.terminate(e,n);await o({status:"fallback",phoneId:l,phoneName:c.name,note:`${u}The app did not start with the phone profile, so it runs as a plain simulator app.`})}async setPrefs(e,n){const t=["defaults","write","com.apple.keyboard.preferences"];await this.host.spawn(e,[...t,"AutomaticMinimizationEnabled","-bool","true"]);await this.host.spawn(e,[...t,"DidShowContinuousPathIntroduction","-bool","true"]);await this.host.spawn(e,["defaults","write",n.bundleId,"RCT_jsLocation",`localhost:${n.metroPort}`])}async nextName(){const e=await this.host.listDevices();const n=new Set([...e.map(s=>s.name),...Object.values(this.state.sims).map(s=>s.name)]);let t=this.state.nextIndex;while(n.has(`${NAME_PREFIX}${String(t).padStart(2,"0")}`))t++;this.state.nextIndex=t+1;return`${NAME_PREFIX}${String(t).padStart(2,"0")}`}template(e){const n=this.state.templates[e];if(!n)throw new FarmError("NO_TEMPLATE",`No template ${e}.`,"Run sim_farm_setup to make one.");if(!n.usable)throw new FarmError("NO_TEMPLATE",`Template ${n.name} can't be used: ${n.note??"not ready"}.`);return n}async cloneSimWork(e,n,t){n??=await this.nextName();const s=n;const i=this.template(e);return this.withReservation({ramGB:0,diskGB:this.state.settings.cloneDiskGB},()=>this.simLock.run(`template:${i.id}`,async()=>{await this.assertOwned(i.udid);const r=this.store.intent("clone",{templateId:e,name:s});const o=await this.host.clone(i.udid,s);const a={udid:o,name:s,templateId:e,power:"creating",profile:"unknown",health:"ok",bootEpoch:0,failures:0,createdAt:new Date().toISOString()};this.state.sims[o]=a;this.store.result(r,"clone",{udid:o});await this.writeMarker(o,{template:i.id});const l=await findDataContainer(await this.dataPath(o),i.bundleId);if(l)await this.files(()=>stripIdentity(l,this.checkActive));const c=t?this.state.leases[t.leaseId]:void 0;if(c&&!this.leaseOverdue(c)){a.leaseId=c.id;c.sims=c.sims.map(u=>u===t.udid?o:u)}a.power="cold";this.store.event({kind:"clone",message:`Made ${n}.`,udid:o});this.store.save();return a}))}async bootSimWork(e,n){await this.assertOwned(e);const t=this.sim(e);if(t.power==="frozen"||t.power==="thawing"){throw new FarmError("BAD_REQUEST",`${t.name} is frozen. Thaw it first.`)}const s=this.state.templates[t.templateId];const i=this.state.settings;const r={ramGB:s?.profile==="slim"?i.slimSimGB:i.stockSimGB,diskGB:0};return this.withReservation(r,()=>this.bootGate.use(()=>this.simLock.run(e,async()=>{this.assertLeaseState(t,n);if(t.power==="awake")return t;const o=s?profileLabels(s.profile):[];if(o.length&&!await storeHasAll(e,o,this.host.disabledRoot)){await this.files(()=>applyDisabled(e,o,this.host.disabledRoot))}t.power="booting";t.bootEpoch++;this.store.save();await this.host.boot(e);const a=await this.waitHealthy(e,s?.profile==="slim"?o:[]);t.lastCheckAt=new Date().toISOString();t.profile=a.profile;t.power="awake";if(!a.ok){t.failures++;t.health=t.failures>=2?"broken":"suspect";t.lastError=a.faults.join("; ");this.store.event({kind:"unhealthy",message:`${t.name}: ${t.lastError}`,udid:e});this.store.save();throw new FarmError("HOST_ERROR",`${t.name} did not come up: ${t.lastError}`,"The farm will rebuild it.",true)}t.failures=0;if(t.health==="suspect")t.health="ok";t.idleSince=new Date().toISOString();if(s?.profile==="slim"&&a.profile==="unknown"){t.health="suspect";t.lastError="can't tell if slim applied";this.store.event({kind:"unknown-profile",message:`${t.name}: can't tell if slim worked. Held for now.`,udid:e});this.store.save();throw new FarmError("HOST_ERROR",`${t.name}: can't tell if slim applied.`,"The farm will rebuild it.",true)}if(s?.profile==="slim"&&a.profile==="stock"){this.store.event({kind:"stock",message:`${t.name}: slim didn't work here (${a.profile}). It uses full RAM.`,udid:e});const l=await this.budget({ramGB:i.stockSimGB-i.slimSimGB,diskGB:0});if(!l.ok){t.health="suspect";t.lastError="slim did not apply and there is no RAM for a full sim";await this.host.shutdown(e).catch(()=>{});t.power="parked";this.store.save();throw new FarmError("RAM_LOW",`${t.name}: slim didn't apply and there's no room for a full sim.`,void 0,true)}}this.store.save();return t})))}assertLeaseState(e,n){if(!this.state.sims[e.udid])throw new FarmError("NOT_FOUND",`${e.name} is gone.`);if(n){const t=this.state.leases[n];if(e.leaseId!==n||!t||this.leaseOverdue(t)){throw new FarmError("LEASE_EXPIRED",`${e.name} is no longer in lease ${n}.`)}}else if(e.leaseId){throw new FarmError("BAD_REQUEST",`${e.name} is in use. End its lease first.`)}}async parkSimWork(e,n="idle"){await this.assertOwned(e);const t=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(t);if(this.busy.has(e))throw new FarmError("BAD_REQUEST",`${t.name} is being reset.`);if(t.power==="frozen"||t.power==="thawing")throw new FarmError("BAD_REQUEST",`${t.name} is frozen. Wake it before parking.`);if(t.power!=="awake")return;this.busy.add(e);try{await this.host.shutdown(e);t.power="parked"}finally{this.busy.delete(e)}this.store.event({kind:"park",message:`Parked ${t.name} (${n}).`,udid:e});this.store.save()})}sim(e){const n=this.state.sims[e];if(!n)throw new FarmError("NOT_OWNED",`Sim ${e} is not a farm sim.`);return n}async deleteSimWork(e,n){const t=this.state.orphans[e];if(t){await this.simLock.run(e,async()=>{const i=(await this.host.listDevices()).find(a=>a.udid===e);if(!i)throw new FarmError("NOT_FOUND","This sim is gone.");let r=false;try{const a=JSON.parse(await import_node_fs9.promises.readFile(import_node_path10.default.join(i.dataPath,MARKER),"utf8"));r=a.farmId===this.state.farmId}catch(a){r=a.code==="ENOENT"&&i.name===t.cloneIntentName}if(!r)throw new FarmError("NOT_OWNED","This sim has no valid farm mark.");const o=this.store.intent("delete",{udid:e});await this.host.shutdown(e);await this.host.delete(e);await this.files(()=>import_node_fs9.promises.rm(import_node_path10.default.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));await this.files(()=>this.forgetPhone(e));delete this.state.orphans[e];this.store.result(o,"delete",{})});return}await this.assertOwned(e);const s=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(s,n);if(s.power==="frozen"||s.power==="thawing")throw new FarmError("BAD_REQUEST",`${s.name} is frozen. Wake it first.`);s.power="deleting";const i=this.store.intent("delete",{udid:e});await this.host.shutdown(e).catch(()=>{});await this.host.delete(e);await this.files(()=>import_node_fs9.promises.rm(import_node_path10.default.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));await this.files(()=>this.forgetPhone(e));delete this.state.sims[e];this.store.result(i,"delete",{});this.store.event({kind:"delete",message:`Deleted ${s.name}.`,udid:e})})}async rebuildSimWork(e,n){const t=this.sim(e);const{name:s,templateId:i}=t;await this.assertOwned(e);await this.simLock.run(e,async()=>{this.assertLeaseState(t,n);this.busy.add(e)});const r=this.store.intent("rebuild",{udid:e,name:s,templateId:i});try{await this.deleteSim(e,n);const o=await this.cloneSimWork(i,s,n?{udid:e,leaseId:n}:void 0);this.store.result(r,"rebuild",{udid:o.udid});return o}finally{this.busy.delete(e)}}async fastResetWork(e,n){await this.assertOwned(e);const t=this.sim(e);const s=this.template(t.templateId);await this.simLock.run(e,async()=>{this.assertLeaseState(t,n);if(t.power==="frozen"||t.power==="thawing")throw new FarmError("BAD_REQUEST",`${t.name} is frozen. Wake it first.`);this.busy.add(e);try{if(t.power==="awake")await this.host.terminate(e,s.bundleId);const i=await findDataContainer(await this.dataPath(e),s.bundleId);if(!i)throw new Error("app container not found");if(s.seedDir)await this.files(()=>applySeed(i,s.bundleId,s.seedDir,this.checkActive));else await this.files(()=>stripIdentity(i,this.checkActive));t.health="ok";this.store.save()}finally{this.busy.delete(e)}})}async grow(e,n){if(!Number.isInteger(n)||n<1||n>64)throw new FarmError("BAD_REQUEST","count must be 1 to 64.");const t=[];for(let s=0;s<n;s++)t.push(await this.cloneSim(e));return t}async shrink(e,n){if(!Number.isInteger(n)||n<1||n>64)throw new FarmError("BAD_REQUEST","count must be 1 to 64.");const t=["cold","parked","awake"];const s=Object.values(this.state.sims).filter(r=>r.templateId===e&&!r.leaseId&&t.includes(r.power)&&!this.busy.has(r.udid)).sort((r,o)=>t.indexOf(r.power)-t.indexOf(o.power));const i=[];for(const r of s.slice(0,n)){try{await this.deleteSim(r.udid);i.push(r.name)}catch(o){if(!(o instanceof FarmError))throw o}}return i}async removeTemplate(e){return this.work(void 0,async()=>{const n=this.state.templates[e];if(!n)throw new FarmError("NO_TEMPLATE",`No template ${e}.`);const t=Object.values(this.state.sims).filter(o=>o.templateId===e);const s=t.filter(o=>o.leaseId||this.busy.has(o.udid));if(s.length){throw new FarmError("HOST_BUSY",`${s.map(o=>o.name).join(", ")} ${s.length===1?"is":"are"} in use.`,"End those uses first.",true)}const i=[];for(const o of t){await this.deleteSim(o.udid);i.push(o.name)}const r=(await this.host.listDevices()).some(o=>o.udid===n.udid);if(r){await this.assertOwned(n.udid);await this.simLock.run(n.udid,async()=>{const o=this.store.intent("delete",{udid:n.udid});await this.host.shutdown(n.udid).catch(()=>{});await this.host.delete(n.udid);await this.files(()=>import_node_fs9.promises.rm(import_node_path10.default.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${n.udid}`),{recursive:true,force:true}));this.store.result(o,"delete",{})})}delete this.state.templates[e];i.push(n.name);this.store.event({kind:"delete",message:`Removed template ${n.name} and its ${t.length} sim(s).`});return{removed:i}})}leaseOverdue(e){const n=this.clock.now();if(n>(this.hardDeadlines.get(e.id)??Infinity))return true;return!this.acquiring.has(e.id)&&n>(this.deadlines.get(e.id)??0)}leaseDeadline(e){return Math.min(this.deadlines.get(e.id)??0,this.hardDeadlines.get(e.id)??Infinity)}async acquire(e){if(e.ttlMs!==void 0&&(typeof e.ttlMs!=="number"||!Number.isFinite(e.ttlMs)||e.ttlMs<=0)){throw new FarmError("BAD_REQUEST","ttlMs must be a finite number above zero.")}const n=this.template(e.templateId);if(!Number.isInteger(e.count)||e.count<1||e.count>64)throw new FarmError("BAD_REQUEST","count must be 1 to 64.");const t=Math.min(Math.max(e.ttlMs??15*6e4,3e4),24*36e5);const s=e.runId?this.ownRun(e.runId,e.owner.clientId):this.newRun(e,n.id,t);const i=Date.parse(s.deadlineAt)-Date.now();if(i<3e4)throw new FarmError("RUN_ENDED","This run is almost out of time.","Ask for more with sim_farm_extend, or finish it and start a new run.");const r={id:`l-${(0,import_node_crypto8.randomBytes)(4).toString("hex")}`,epoch:1,owner:e.owner,purpose:e.purpose,runId:s.id,templateId:n.id,sims:[],createdAt:new Date().toISOString(),ttlMs:Math.min(t,i)};s.leases.push(r.id);s.lastActivityAt=r.createdAt;if(e.labels?.length)this.labels.set(r.id,e.labels);this.state.leases[r.id]=r;this.hardDeadlines.set(r.id,this.clock.now()+r.ttlMs);this.acquiring.add(r.id);const o=()=>this.deadlines.set(r.id,this.clock.now()+Math.min(r.ttlMs,LEASE_GRACE_MS));o();const a=performance.now()+(e.waitMs??0);const l=()=>{if(!this.state.leases[r.id])throw new FarmError("LEASE_EXPIRED","The lease ended while sims were being prepared.");if(this.leaseOverdue(r))throw new FarmError("LEASE_EXPIRED","The lease ran out of time while sims were being prepared.")};try{while(r.sims.length<e.count){l();o();const c=await this.claimOne(r,n);l();if(c)continue;if(performance.now()>=a)break;if(!r.sims.length&&s.state==="active"&&!Object.keys(s.lanes).length)s.state="waiting";await sleep(Math.max(this.pollMs,2e3))}if(r.sims.length<e.count&&!e.allowPartial){const c=await this.budget({ramGB:this.state.settings.slimSimGB,diskGB:0});throw new FarmError(c.code??"HOST_BUSY",`Got ${r.sims.length} of ${e.count} sims. ${c.reason??"Others are in use."}`,"Try again later or ask for fewer.",true)}if(r.sims.length===0)throw new FarmError("HOST_BUSY","No sim is free right now.","Try again later.",true);if(e.launch)await this.work(r.id,async()=>{l();await Promise.all(r.sims.map(c=>this.launchApp(c,n)));o();await this.checkIdentities(r,6e4);l()});o();this.store.event({kind:"lease",message:`${e.owner.label} took ${r.sims.length} sim(s) for ${s.goal}.`,leaseId:r.id,runId:s.id});this.note(s,"farm",`Got ${r.sims.length} sim${r.sims.length===1?"":"s"}.`);this.store.save();return{lease:r,run:s,sims:r.sims.map(c=>this.state.sims[c])}}catch(c){if(this.state.leases[r.id])this.note(s,"farm",`Could not get sims: ${c.message}`);await this.endLease(r.id,"acquire failed","fast","acquire-failed").catch(()=>{});throw c}finally{this.acquiring.delete(r.id);this.labels.delete(r.id)}}async claimOneWork(e,n){const t=Object.values(this.state.sims).filter(o=>o.templateId===n.id&&!o.leaseId&&o.health==="ok"&&!this.busy.has(o.udid));const s=t.find(o=>o.power==="awake")??t.find(o=>o.power==="parked"||o.power==="cold");const i=async o=>{const a=await this.isOwned(o.udid);return this.simLock.run(o.udid,async()=>{if(o.leaseId||this.busy.has(o.udid)||o.health!=="ok"||!this.state.sims[o.udid])return null;if(!a){o.health="broken";o.lastError="farm marker missing";this.store.save();return null}if(!this.state.leases[e.id]||this.leaseOverdue(e))return null;o.leaseId=e.id;e.sims.push(o.udid);o.idleSince=void 0;this.addLane(e,o);this.store.save();return o})};if(s?.power==="awake")return i(s);let r=null;try{if(s){r=await i(s)}else{const o=await this.budget({ramGB:n.profile==="slim"?this.state.settings.slimSimGB:this.state.settings.stockSimGB,diskGB:this.state.settings.cloneDiskGB});if(!o.ok)return null;r=await i(await this.cloneSim(n.id))}if(!r)return null;await this.bootSim(r.udid,e.id);return r}catch(o){if(r)this.unclaim(e,r.udid);if(o instanceof FarmError&&(o.code==="RAM_LOW"||o.code==="DISK_LOW"||o.code==="HOST_BUSY"||o.code==="LEASE_EXPIRED")){return null}if(r&&this.state.sims[r.udid]?.health!=="ok"){const a=r.udid;this.background(`rebuild ${a}`,()=>this.rebuildSim(a))}throw o}}unclaim(e,n){e.sims=e.sims.filter(i=>i!==n);const t=e.runId?this.state.runs[e.runId]:void 0;if(t?.lanes[n])delete t.lanes[n];const s=this.state.sims[n];if(s&&s.leaseId===e.id)s.leaseId=void 0;this.store.save()}async checkIdentities(e,n=6e4){const t=this.template(e.templateId);const s=performance.now()+n;const i=new Map;for(;;){for(const o of e.sims){if(i.has(o))continue;const a=await findDataContainer(await this.dataPath(o),t.bundleId);const l=a?await readBuoyInstallId(a):null;if(!l)continue;const c=[...i.entries()].find(([,d])=>d===l);if(c)throw new FarmError("ID_CLASH",`${this.state.sims[o].name} and ${this.state.sims[c[0]].name} share one Buoy ID.`);const u=Object.values(this.state.sims).find(d=>d.udid!==o&&!e.sims.includes(d.udid)&&d.buoyDeviceId===l);if(u)throw new FarmError("ID_CLASH",`${this.state.sims[o].name} and ${u.name} share one Buoy ID.`);i.set(o,l);this.state.sims[o].buoyDeviceId=l}if(i.size===e.sims.length||performance.now()>=s)break;await sleep(Math.max(this.pollMs,1e3))}const r=e.sims.filter(o=>!i.has(o));if(r.length){this.store.event({kind:"no-id",message:`No Buoy ID yet on ${r.map(o=>this.state.sims[o].name).join(", ")}. Is the app loading? Check Metro.`,leaseId:e.id})}}liveLease(e,n,t){const s=this.state.leases[e];if(!s){const i=this.runOfLease(e);if(i&&(i.endedBy==="user"||i.endedBy==="deadline"))this.assertRunOpen(i);throw new FarmError("LEASE_EXPIRED",`Lease ${e} has ended.`,"Acquire new sims.")}if(s.epoch!==n)throw new FarmError("STALE_EPOCH",`Lease ${e} is at epoch ${s.epoch}, not ${n}.`);if(t&&s.owner.clientId!==t)throw new FarmError("NOT_OWNED",`Lease ${e} belongs to ${s.owner.label}.`);if(this.leaseOverdue(s)){const i=this.overdueEnd(s);this.background(`expire ${s.id}`,()=>this.endLease(s.id,"expired","full",i));if(i==="deadline")throw new FarmError("RUN_ENDED","This run's time expired.","Ask for more time with sim_farm_extend before the end next time. Start a new run to go on.");throw new FarmError("LEASE_EXPIRED",`Lease ${e} has expired.`,"Acquire new sims.")}return s}renew(e,n,t){const s=this.liveLease(e,n,t);const i=Math.min(this.clock.now()+Math.min(s.ttlMs,LEASE_GRACE_MS),this.hardDeadlines.get(s.id)??Infinity);this.deadlines.set(s.id,i);return{expiresInMs:Math.round(i-this.clock.now())}}async release(e,n,t="fast",s,i=false){if(t!=="fast"&&t!=="full")throw new FarmError("BAD_REQUEST","Use fast or full reset.");this.liveLease(e,n,s);return this.endLease(e,"released",t,i?null:"released")}async endLease(e,n,t,s="released"){const i=this.state.leases[e];if(!i)return{};i.epoch++;delete this.state.leases[e];this.deadlines.delete(e);this.hardDeadlines.delete(e);for(const[a,l]of Object.entries(this.state.locks))if(l.leaseId===e)delete this.state.locks[a];const r=new Set(i.sims.filter(a=>{const l=this.state.sims[a];return l?.bootEpoch===0&&l.power==="cold"&&l.health==="ok"}));const o={};for(const a of i.sims){const l=this.state.sims[a];if(!l||l.leaseId!==e)continue;l.leaseId=void 0;l.health="dirty";l.idleSince=new Date().toISOString();this.busy.add(a);o[l.name]="resetting";void this.files(()=>removeLaunchFile(a,this.store.homeDir)).catch(()=>void 0)}this.store.event({kind:"lease-end",message:`${i.owner.label}'s ${i.purpose} ended (${n}).`,leaseId:e,runId:i.runId});this.settleRun(i,s);this.store.save();this.background(`reset after ${e}`,()=>this.work(void 0,async()=>{for(const a of i.sims){if(!this.state.sims[a])continue;try{const l=Date.parse(i.createdAt)-2e3;for(const c of await this.host.clientsOf(a)){if(Date.parse(c.start)>=l)await this.host.kill(c.pid,c.start)}if(t==="full"&&!r.has(a)||this.state.sims[a].failures>0)await this.rebuildSim(a);else await this.fastReset(a)}catch(l){this.store.event({kind:"reset-failed",message:`${this.state.sims[a]?.name??a}: ${l.message}. Rebuilding.`,udid:a});await this.rebuildSim(a).catch(()=>{})}finally{this.busy.delete(a)}}}));return o}dropFromLease(e,n){const t=this.state.leases[e];if(t)t.sims=t.sims.filter(s=>s!==n)}async actionWork(e,n,t,s,i,r){const o=this.liveLease(e,n,r);const a={};for(const l of t){if(!o.sims.includes(l))throw new FarmError("NOT_OWNED",`${l} is not in lease ${e}.`);await this.assertOwned(l);this.liveLease(e,n,r);const c=this.template(o.templateId);switch(s){case"screenshot":{const u=this.state.locks["perf-slot"];if(u&&u.leaseId!==o.id)throw new FarmError("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);const d=import_node_path10.default.join(this.store.shotsPath,`${l}.png`);a[l]=await this.host.screenshot(l,d)?d:null;break}case"open-url":if(!i)throw new FarmError("BAD_REQUEST","open-url needs a url.");await this.host.openUrl(l,i);a[l]="opened";break;case"launch":await this.launchApp(l,c);a[l]="launched";break;case"reset":await this.fastReset(l,o.id);a[l]="reset";break;case"rebuild":{const u=await this.rebuildSim(l,o.id);if(!this.state.leases[o.id])throw new FarmError("LEASE_EXPIRED",`Lease ${e} ended during the rebuild.`);u.leaseId=o.id;o.sims=o.sims.map(p=>p===l?u.udid:p);const d=o.runId?this.state.runs[o.runId]:void 0;const h=d?.lanes[l];if(d&&h){delete d.lanes[l];d.lanes[u.udid]={...h,udid:u.udid,name:u.name,state:"setting-up",note:"Rebuilt"}}await this.bootSim(u.udid,o.id);a[l]=u.udid;break}default:throw new FarmError("BAD_REQUEST",`Unknown action ${s}.`)}}this.store.save();return a}async lock(e,n,t,s=0,i){if(typeof e!=="string"||!/^[a-z0-9-]{1,40}$/.test(e))throw new FarmError("BAD_REQUEST","Lock names are short lowercase words.");this.touch(this.liveLease(n,t,i));if(this.state.locks[e]?.leaseId===n)return{held:true};const r=this.lockQueues.get(e)??[];this.lockQueues.set(e,r);const o={};r.push(o);const a=performance.now()+s;try{for(;;){this.store.assertActive();this.liveLease(n,t,i);const l=this.state.locks[e];const c=e==="perf-slot"&&(this.activeWork>0||this.busy.size>0||this.pendingBg>0);if(r[0]===o&&!l&&!c){this.state.locks[e]={name:e,leaseId:n,since:new Date().toISOString()};this.store.save();return{held:true}}if(performance.now()>=a){return{held:false,holder:l?this.state.leases[l.leaseId]?.owner.label??"someone":"the farm (work is still running)",since:l?.since}}await sleep(Math.max(1,Math.min(this.pollMs,a-performance.now())))}}finally{r.splice(r.indexOf(o),1);if(!r.length)this.lockQueues.delete(e)}}fairness(e,n,t,s){this.liveLease(e,n,s);const i=this.state.settings;const r=fairRules(i,this.meter.cpuCount);const o=this.meter.verdicts(e,t,r);return{rules:r,fair:o.filter(a=>a.fair).length,unfair:o.filter(a=>!a.fair).length,runs:o}}unlock(e,n,t,s){this.liveLease(n,t,s);if(this.state.locks[e]?.leaseId===n)delete this.state.locks[e];this.store.save();return{held:false}}async shot(e){if(this.state.locks["perf-slot"])throw new FarmError("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(this.shotsInFlight>=MAX_SHOTS)throw new FarmError("HOST_BUSY","Taking other pictures. Try again in a moment.",void 0,true);this.shotsInFlight++;try{return await this.work(void 0,()=>this.shotWork(e))}finally{this.shotsInFlight--}}async shotWork(e){await this.assertOwned(e);const n=this.sim(e);if(this.state.locks["perf-slot"])throw new FarmError("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(n.power!=="awake")throw new FarmError("BAD_REQUEST",`${n.name} is ${n.power}.`);const t=import_node_path10.default.join(this.store.shotsPath,`${e}.png`);await this.files(()=>import_node_fs9.promises.mkdir(import_node_path10.default.dirname(t),{recursive:true}));if(!await this.host.screenshot(e,t,8e3))throw new FarmError("HOST_ERROR",`No still from ${n.name} in time.`,void 0,true);return{file:t,at:new Date().toISOString()}}async show(e){return this.work(this.state.sims[e]?.leaseId,()=>this.showWork(e))}async showWork(e){await this.assertOwned(e);const n=this.sim(e);if(n.power==="frozen"||n.power==="thawing")throw new FarmError("BAD_REQUEST",`${n.name} is paused. Wake it first.`);if(n.power!=="awake")await this.bootSim(e,n.leaseId);await this.host.showWindow(e);return"shown"}async tick(){if(this.ticking)return;this.ticking=true;try{for(const t of Object.values(this.state.leases)){const s=!await pidAlive(t.owner.pid,t.owner.pidStart,true);if(s)await this.endLease(t.id,"its owner exited","full","owner-exited");else if(this.leaseOverdue(t))await this.endLease(t.id,"no renewal","full",this.overdueEnd(t))}await this.tickRuns();await this.tickPhones().catch(t=>this.log(`phones: ${t.message}`));const e=this.state.settings.parkAfterMinutes;if(e>0){for(const t of Object.values(this.state.sims)){if(t.power!=="awake"||t.leaseId||this.busy.has(t.udid)||!t.idleSince)continue;if(Date.now()-Date.parse(t.idleSince)>e*6e4){await this.parkSim(t.udid).catch(s=>this.log(`park ${t.name}: ${s.message}`))}}}const n=Object.keys(this.state.requests);if(n.length>REQUEST_MEMORY)for(const t of n.slice(0,n.length-REQUEST_MEMORY))delete this.state.requests[t]}finally{this.ticking=false}}async drain(e=10*6e4){const n=performance.now()+e;while((this.busy.size>0||this.pendingBg>0||this.activeWork>0)&&performance.now()<n)await sleep(200);return this.busy.size===0&&this.pendingBg===0&&this.activeWork===0}idle(){return Object.keys(this.state.leases).length===0&&this.busy.size===0&&this.activeWork===0&&this.pendingBg===0&&this.acquiring.size===0}async parkAllIdle(){for(const e of Object.values(this.state.sims)){if(e.power==="awake"&&!e.leaseId&&!this.busy.has(e.udid))await this.parkSim(e.udid,"farm stopping").catch(()=>{})}}background(e,n){this.pendingBg++;this.workContext.exit(n).catch(t=>{this.log(`${e}: ${t.message}`);try{this.store.event({kind:"error",message:`${e}: ${t.message}`})}catch{}}).finally(()=>this.pendingBg--)}async once(e,n,t,s){if(!n)return s();const i=`${e}:${n}`;const r=(0,import_node_crypto8.createHash)("sha256").update(JSON.stringify(t)).digest("hex").slice(0,16);const o=this.state.requests[i];if(o){if(o.hash!==r)throw new FarmError("REQUEST_REUSED",`Request ${n} was already used for a different call.`);return o.result}const a=this.inflight.get(i);if(a){if(a.hash!==r)throw new FarmError("REQUEST_REUSED",`Request ${n} was already used for a different call.`);return a.promise}const l=s();this.inflight.set(i,{hash:r,promise:l});try{const c=await l;this.state.requests[i]={hash:r,result:c,at:new Date().toISOString()};this.store.save();return c}finally{this.inflight.delete(i)}}newRun(e,n,t){const s=Date.now();const i=e.owner.agent?this.state.settings.runCapMinutes*6e4:24*36e5;const r=(e.goal??"").trim()||e.purpose;const o={id:`r-${(0,import_node_crypto8.randomBytes)(4).toString("hex")}`,goal:r.slice(0,200),kind:e.kind?.slice(0,40),owner:e.owner,templateId:n,state:"active",createdAt:new Date(s).toISOString(),deadlineAt:new Date(s+Math.min(t,i)).toISOString(),capAt:new Date(s+i).toISOString(),leases:[],lanes:{},notes:[],lastActivityAt:new Date(s).toISOString(),auto:!e.goal};this.state.runs[o.id]=o;return o}touch(e){const n=e.runId?this.state.runs[e.runId]:void 0;if(n)n.lastActivityAt=new Date().toISOString()}runOfLease(e){return Object.values(this.state.runs).find(n=>n.leases.includes(e))}assertRunOpen(e){if(e.state==="stopping"||e.state==="stopped"){if(e.endedBy==="user")throw new FarmError("RUN_STOPPED","The person stopped this run in Buoy Desktop (reason=user).","Tell them: You stopped this run in Sims. Start a new run only if they ask.");if(e.endedBy==="deadline")throw new FarmError("RUN_ENDED","This run ran out of time.","Start a new run to go on. Ask for more time with sim_farm_extend before the end next time.")}if(ENDED.includes(e.state)||e.state==="stopping")throw new FarmError("RUN_ENDED",`This run has ended (${e.state}).`,"Start a new run.")}ownRun(e,n){const t=this.state.runs[e];if(!t)throw new FarmError("NOT_FOUND",`No run ${e}.`,"Use the runId that sim_farm_acquire returned.");if(n&&t.owner.clientId!==n)throw new FarmError("NOT_OWNED",`Run ${e} belongs to ${t.owner.label}.`);this.assertRunOpen(t);return t}async forgetPhone(e){await import_node_fs9.promises.rm(launchFilePath(e,this.store.homeDir),{force:true});await import_node_fs9.promises.rm(eventsFilePath(e,this.store.homeDir),{force:true})}async tickPhones(){for(const e of Object.values(this.state.sims)){if(e.phone?.status!=="on"||e.power!=="awake")continue;const n=e.leaseId?this.state.leases[e.leaseId]:void 0;const t=n?.runId?this.state.runs[n.runId]:void 0;if(!t||ENDED.includes(t.state))continue;const s=await readSummary(e.udid,this.store.homeDir);if(!s?.overLimit||!s.pid||e.phone.notedPid===s.pid)continue;e.phone.notedPid=s.pid;const i=t.lanes[e.udid]?.label??e.name;const r=e.phone.phoneName??"the phone";this.note(t,"farm",s.ended==="safety"?`${i}: the app used ${s.peakMB} MB and hit the farm's safety cap, so it was closed. ${r} would have closed it sooner.`:s.ended==="limit"?`${i}: the app went past ${r}'s memory limit (${s.peakMB} MB) and was closed, like on a real phone.`:`${i}: the app went past ${r}'s memory limit (${s.peakMB} MB). A real phone would close it.`,e.udid);this.store.save()}}note(e,n,t,s){e.notes.push({at:new Date().toISOString(),by:n,text:t.slice(0,500),udid:s});if(e.notes.length>RUN_NOTES)e.notes.splice(0,e.notes.length-RUN_NOTES)}addLane(e,n){const t=e.runId?this.state.runs[e.runId]:void 0;if(!t)return;const s=this.labels.get(e.id)?.[e.sims.length-1];t.lanes[n.udid]={udid:n.udid,name:n.name,label:s?.slice(0,80),state:"setting-up",at:new Date().toISOString()};if(t.state==="waiting")t.state="active"}overdueEnd(e){return this.clock.now()>=(this.hardDeadlines.get(e.id)??Infinity)?"deadline":"lost"}settleRun(e,n){const t=e.runId?this.state.runs[e.runId]:void 0;if(!t)return;for(const i of e.sims)if(t.lanes[i])t.lanes[i].gone=true;if(ENDED.includes(t.state)||t.state==="stopping"||n===null)return;const s=t.leases.some(i=>this.state.leases[i]);switch(n){case"released":if(!s)this.endRun(t,this.outcomeOf(t)==="failed"?"failed":"done","released");return;case"acquire-failed":if(!s&&!Object.keys(t.lanes).length)this.endRun(t,"failed","acquire-failed");return;case"user":return this.endRun(t,"stopped","user","You stopped this run.");case"deadline":return this.endRun(t,"stopped","deadline","Ran out of time. The sims were taken back.");case"owner-exited":return this.endRun(t,"interrupted","owner-exited","The chat closed. Cleaning up.");case"lost":return this.endRun(t,"interrupted","lost","Lost touch with the chat. Cleaning up.");case"finished":return this.endRun(t,"done","finished")}}endRun(e,n,t,s,i="full"){if(ENDED.includes(e.state))return;e.state="stopping";e.endedBy=t;if(s)this.note(e,t==="user"?"person":"farm",s);for(const r of e.leases){if(this.state.leases[r])void this.endLease(r,s??t,t==="finished"||t==="released"?i:"full",null)}for(const r of Object.values(e.lanes))r.gone=true;e.state=n;e.endedAt=new Date().toISOString();e.outcome=e.outcome??this.outcomeOf(e);this.store.event({kind:"run-end",message:`${e.owner.label}: "${e.goal}" ${n==="done"?"finished":n}.`,runId:e.id})}outcomeOf(e){const n=Object.values(e.lanes);const t=n.filter(i=>i.state==="passed").length;const s=n.filter(i=>i.state==="failed").length;if(!t&&!s)return"unknown";if(s&&t)return"mixed";return s?"failed":"passed"}report(e,n){const t=this.ownRun(e.runId,n);const s=new Date().toISOString();if(e.sequence!==void 0){if(!Number.isInteger(e.sequence)||e.sequence<0)throw new FarmError("BAD_REQUEST","sequence must be a whole number.");if(t.lastSeq!==void 0&&e.sequence<=t.lastSeq)return{applied:false,reason:"already have this report",...this.timeLeft(t)};t.lastSeq=e.sequence}if(e.state!==void 0&&!LANE_STATES.includes(e.state))throw new FarmError("BAD_REQUEST",`state must be one of ${LANE_STATES.join(", ")}.`);if(e.udid!==void 0){const i=t.lanes[e.udid];if(!i)throw new FarmError("NOT_FOUND",`That sim is not in run ${t.id}.`,"Use a UDID from this run's sims.");if(e.state)i.state=e.state;if(e.state==="passed"||e.state==="failed")i.resultSource=e.resultSource==="test"?"test":"agent";if(e.note!==void 0)i.note=String(e.note).slice(0,200);i.at=s}if(e.counts!==void 0){const i=e.counts;const r=o=>o===void 0||Number.isInteger(o)&&o>=0;if(!i||typeof i!=="object"||!r(i.done)||!r(i.total)||!r(i.passed)||!r(i.failed)||i.done>i.total){throw new FarmError("BAD_REQUEST","counts needs whole numbers, with done no more than total.")}t.counts={done:i.done,total:i.total,passed:i.passed,failed:i.failed}}if(e.note)this.note(t,"agent",String(e.note),e.udid);t.lastReportAt=t.lastActivityAt=s;this.store.save();return{applied:true,...this.timeLeft(t)}}extend(e,n,t){const s=this.ownRun(e,t);if(!Number.isFinite(n)||n<=0)throw new FarmError("BAD_REQUEST","minutes must be above zero.");const i=Date.parse(s.deadlineAt);const r=Date.now()+n*6e4;const o=Math.min(r,Date.parse(s.capAt));if(o<=i){return{granted:false,partly:false,reason:"This run is at its time limit.",...this.timeLeft(s)}}s.deadlineAt=new Date(o).toISOString();s.lastActivityAt=new Date().toISOString();for(const a of s.leases){const l=this.state.leases[a];if(!l)continue;l.ttlMs=o-Date.parse(l.createdAt);this.hardDeadlines.set(a,this.clock.now()+(o-Date.now()))}this.note(s,"farm",`More time: until ${new Date(o).toLocaleTimeString()}.`);this.store.save();return{granted:true,partly:o<r,...this.timeLeft(s)}}finish(e,n,t,s){const i=this.state.runs[e];if(i?.endedBy==="finished"&&i.owner.clientId===s)return{state:i.state,outcome:i.outcome};const r=this.ownRun(e,s);if(n&&!["passed","failed","mixed","unknown"].includes(n))throw new FarmError("BAD_REQUEST","outcome must be passed, failed, mixed or unknown.");r.outcome=n??this.outcomeOf(r);if(t)r.summary=t.slice(0,600);this.endRun(r,r.outcome==="failed"?"failed":"done","finished",void 0,"fast");this.store.save();return{state:r.state,outcome:r.outcome}}stopRun(e,n="you"){const t=this.state.runs[e];if(!t)throw new FarmError("NOT_FOUND",`No run ${e}.`);if(ENDED.includes(t.state))return{state:t.state};this.endRun(t,"stopped","user",n==="you"?"You stopped this run.":`Stopped by ${n}.`);this.store.save();return{state:t.state}}stopAll(e="you"){const n=Object.values(this.state.runs).filter(t=>!ENDED.includes(t.state));for(const t of n)this.stopRun(t.id,e);return{stopped:n.length}}timeLeft(e){return{deadlineAt:e.deadlineAt,capAt:e.capAt,secondsLeft:Math.max(0,Math.round((Date.parse(e.deadlineAt)-Date.now())/1e3))}}async tickRuns(){const e=Date.now();for(const i of Object.values(this.state.runs)){if(ENDED.includes(i.state))continue;if(e>=Date.parse(i.deadlineAt)){this.endRun(i,"stopped","deadline","Ran out of time. The sims were taken back.");this.store.save()}else if(!i.leases.some(r=>this.state.leases[r])&&!await pidAlive(i.owner.pid,i.owner.pidStart,true)){this.endRun(i,"interrupted","owner-exited","The chat closed.");this.store.save()}}const n=this.state.settings.keepRunsHours*36e5;const t=Object.values(this.state.runs).filter(i=>ENDED.includes(i.state)).sort((i,r)=>Date.parse(r.endedAt??r.createdAt)-Date.parse(i.endedAt??i.createdAt));let s=false;t.forEach((i,r)=>{if(r>=200||e-Date.parse(i.endedAt??i.createdAt)>n){delete this.state.runs[i.id];s=true}});if(s)this.store.save()}async status(e={}){const n=await this.budget();const t=await Promise.all(Object.values(this.state.sims).map(async i=>{const r=i.leaseId?this.state.leases[i.leaseId]:void 0;const o=r?.runId?this.state.runs[r.runId]:void 0;const a=i.phone?.status==="on"?await readSummary(i.udid,this.store.homeDir):null;const l=i.phone&&{...i.phone,notedPid:void 0,...a?.loaded?{pid:a.pid,peakMB:a.peakMB,memoryWarnings:a.memoryWarnings,overLimit:a.overLimit,ended:a.ended}:{}};return{...i,phone:l,owner:r?.owner.label,purpose:o?.goal??r?.purpose,runId:o?.id,lane:o?.lanes[i.udid]?.label,busy:this.busy.has(i.udid)}}));const s={farmId:this.state.farmId,counts:{pool:t.length,awake:t.filter(i=>i.power==="awake").length,leased:t.filter(i=>i.leaseId).length},budget:{roomForMoreSims:n.roomFor,freeDiskGB:Math.floor(n.stats.freeDiskBytes/GB),availableMemGB:Math.floor(n.stats.availableMemBytes/GB),memPressure:n.stats.memPressure,loadAvg1:Number(n.stats.loadAvg1.toFixed(1)),cpuCount:n.stats.cpuCount},templates:Object.values(this.state.templates),sims:t,orphans:Object.values(this.state.orphans),leases:Object.values(this.state.leases).map(i=>({...i,expiresInMs:Math.max(0,Math.round(this.leaseDeadline(i)-this.clock.now()))})),runs:Object.values(this.state.runs).sort((i,r)=>Date.parse(r.createdAt)-Date.parse(i.createdAt)).map(i=>({...i,notes:i.notes.slice(-50),secondsLeft:ENDED.includes(i.state)?0:Math.max(0,Math.round((Date.parse(i.deadlineAt)-Date.now())/1e3))})),locks:Object.values(this.state.locks),recentEvents:this.state.events.slice(-25),settings:this.state.settings};if(e.outside){const i=new Set([...Object.keys(this.state.sims),...Object.keys(this.state.orphans),...Object.values(this.state.templates).map(r=>r.udid)]);s.outsideSims=(await this.host.listDevices()).filter(r=>!i.has(r.udid)&&r.available).map(r=>({udid:r.udid,name:r.name,state:r.state,runtime:r.runtime}))}return s}setSettings(e){const n=this.state.settings;const t={parkAfterMinutes:[0,24*60],maxConcurrentBoots:[1,8],reserveRamGB:[4,512],reserveDiskGB:[5,2048],slimSimGB:[.5,16],stockSimGB:[1,32],cloneDiskGB:[.1,32],runCapMinutes:[5,24*60],keepRunsHours:[1,24*14],fairMinFreeShare:[0,1],fairMaxSwingShare:[.02,1],fairMaxThermalLevel:[0,4],fairMaxSwapGrowthMB:[0,65536],phoneCapMB:[0,1048576]};if(!e||typeof e!=="object")throw new FarmError("BAD_REQUEST","settings must be an object.");if("phoneProfile"in e){const i=loadPhones(this.store.homeDir);if(e.phoneProfile!==PHONE_OFF&&!(typeof e.phoneProfile==="string"&&i[e.phoneProfile])){throw new FarmError("BAD_REQUEST",`phoneProfile must be "off" or one of: ${Object.keys(i).join(", ")}.`)}}if("phoneOnLimit"in e&&e.phoneOnLimit!=="warn"&&e.phoneOnLimit!=="kill"){throw new FarmError("BAD_REQUEST",`phoneOnLimit must be "warn" or "kill".`)}if("phoneCapMB"in e&&(!Number.isInteger(e.phoneCapMB)||e.phoneCapMB!==0&&e.phoneCapMB<3072)){throw new FarmError("BAD_REQUEST","phoneCapMB must be 0 (automatic) or a whole number of MB, at least 3072.")}for(const[i,r]of Object.entries(e)){if(!(i in n)||typeof r!==typeof n[i])throw new FarmError("BAD_REQUEST",`Bad setting ${i}.`);const o=t[i];if(o&&(typeof r!=="number"||!Number.isFinite(r)||r<o[0]||r>o[1])){throw new FarmError("BAD_REQUEST",`${i} must be ${o[0]} to ${o[1]}.`)}}const s=["phoneProfile","phoneOnLimit","phoneCapMB"].some(i=>i in e&&e[i]!==n[i]);for(const[i,r]of Object.entries(e))n[i]=r;this.store.save();if(s){for(const i of Object.keys(this.state.sims))void removeLaunchFile(i,this.store.homeDir).catch(()=>void 0)}return n}}}});var import_node_fs10,import_node_os7,import_node_path11,SIMCTL_TIMEOUT,SimctlHost;var init_host=__esm({"../sim-farm/src/host.ts"(){"use strict";import_node_fs10=require("fs");import_node_os7=__toESM(require("os"));import_node_path11=__toESM(require("path"));init_disabledStore();init_meter();init_proc();SIMCTL_TIMEOUT=6e4;SimctlHost=class{constructor(){this.disabledRoot=DISABLED_STORE_ROOT;this.devicesDir=import_node_path11.default.join(import_node_os7.default.homedir(),"Library/Developer/CoreSimulator/Devices");this.graphicsPids=[];this.loadCalls=0}simctl(e,n=SIMCTL_TIMEOUT){return runOk("xcrun",["simctl",...e],{timeoutMs:n})}async listDevices(){const e=JSON.parse(await this.simctl(["list","devices","--json"],3e4));const n=[];for(const[t,s]of Object.entries(e.devices??{})){for(const i of s){n.push({udid:i.udid,name:i.name,state:i.state,runtime:t,deviceType:i.deviceTypeIdentifier,dataPath:i.dataPath??import_node_path11.default.join(this.devicesDir,i.udid,"data"),available:i.isAvailable!==false})}}return n}async create(e,n,t){return(await this.simctl(["create",e,n,t])).trim()}async clone(e,n){return(await this.simctl(["clone",e,n])).trim()}async boot(e){const n=await run("xcrun",["simctl","boot",e],{timeoutMs:SIMCTL_TIMEOUT});if(n.code!==0&&!/current state: Booted/i.test(n.stderr)){throw new Error(`simctl boot ${e} failed: ${n.stderr.trim().slice(0,300)}`)}}async shutdown(e){const n=await run("xcrun",["simctl","shutdown",e],{timeoutMs:9e4});if(n.code!==0&&!/current state: Shutdown/i.test(n.stderr)){throw new Error(`simctl shutdown ${e} failed: ${n.stderr.trim().slice(0,300)}`)}}async delete(e){await this.simctl(["delete",e],9e4)}async install(e,n){await this.simctl(["install",e,n],18e4)}async launch(e,n,t){await runOk("xcrun",["simctl","launch","--terminate-running-process",e,n],{timeoutMs:6e4,env:t&&Object.keys(t).length?{...process.env,...t}:void 0})}async terminate(e,n){await run("xcrun",["simctl","terminate",e,n],{timeoutMs:2e4})}async openUrl(e,n){await this.simctl(["openurl",e,n],2e4)}async screenshot(e,n,t=1e4){const s=await run("xcrun",["simctl","io",e,"screenshot",n],{timeoutMs:t});return s.code===0&&!s.timedOut}spawn(e,n,t=2e4){return run("xcrun",["simctl","spawn",e,...n],{timeoutMs:t})}async appContainer(e,n){const t=await run("xcrun",["simctl","get_app_container",e,n,"data"],{timeoutMs:2e4});return t.code===0?t.stdout.trim()||null:null}async simTree(e){const n=await listProcesses();const t=n.find(s=>/launchd_sim/.test(s.command)&&s.command.includes(e));return t?descendants(n,t.pid):[]}async signal(e,n){for(const t of e){try{process.kill(t,n)}catch{}}}async launchdLogTail(e,n=256*1024){const t=import_node_path11.default.join(this.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`,"launchd.log");try{const s=await import_node_fs10.promises.open(t,"r");try{const{size:i}=await s.stat();const r=Math.max(0,i-n);const o=Buffer.alloc(i-r);await s.read(o,0,o.length,r);return o.toString("utf8")}finally{await s.close()}}catch{return""}}async hostStats(){const[e,n,t,s]=await Promise.all([runOk("vm_stat",[],{timeoutMs:1e4}),run("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{timeoutMs:5e3}),run("sysctl",["-n","vm.swapusage"],{timeoutMs:5e3}),runOk("df",["-k",import_node_os7.default.homedir()],{timeoutMs:1e4})]);const i=Number(/page size of (\d+)/.exec(e)?.[1]??16384);const r=d=>Number(new RegExp(`${d}:\\s+(\\d+)`).exec(e)?.[1]??0);const o=r("Pages free")+r("Pages inactive")+r("Pages speculative")+r("Pages purgeable");const a=Number(n.stdout.trim());const l=/used = ([\d.]+)M/.exec(t.stdout)?.[1];const c=s.trim().split("\n").pop()??"";const u=Number(c.split(/\s+/)[3]??0);return{totalMemBytes:import_node_os7.default.totalmem(),availableMemBytes:o*i,memPressure:a>=4?"critical":a>=2?"warn":"normal",swapUsedBytes:l?Number(l)*1024*1024:0,freeDiskBytes:u*1024,cpuCount:import_node_os7.default.cpus().length,loadAvg1:import_node_os7.default.loadavg()[0]}}async showWindow(e){const n=await run("open",["-b","com.apple.iphonesimulator","--args","-CurrentDeviceUDID",e],{timeoutMs:2e4});if(n.code===0)return;await runOk("open",["-b","com.apple.dt.Devices"],{timeoutMs:2e4})}async loadCounters(e,n=true){if(n&&this.loadCalls++%10===0){const y=await run("pgrep",["-x","SimMetalHost"],{timeoutMs:5e3});this.graphicsPids=y.stdout.split("\n").map(Number).filter(g=>g>0)}const t=import_node_os7.default.cpus();let s=0;let i=0;for(const y of t){const g=y.times;const f=g.user+g.nice+g.sys+g.idle+g.irq;i+=f;s+=f-g.idle}const r=[...e,...this.graphicsPids];const[o,a,l,c]=await Promise.all([n&&r.length?run("ps",["-o","pid=,time=","-p",r.join(",")],{timeoutMs:5e3}):null,n?run("ioreg",["-r","-c","IOAccelerator","-d","1"],{timeoutMs:5e3}):null,run("sysctl",["-n","kern.memorystatus_vm_pressure_level","vm.swapusage"],{timeoutMs:5e3}),run("notifyutil",["-g","com.apple.system.thermalpressurelevel"],{timeoutMs:5e3})]);const u=new Set(this.graphicsPids);let d=0;let h=0;for(const y of o?.stdout.split("\n")??[]){const g=/^\s*(\d+)\s+(\S+)/.exec(y);if(!g)continue;const f=parseCpuTime(g[2]);if(u.has(Number(g[1])))h+=f;else d+=f}const p=a&&/"Device Utilization %"=(\d+)/.exec(a.stdout);const m=/thermalpressurelevel\s+(\d+)/.exec(c.stdout);return{hostBusyMs:s,hostTotalMs:i,cpuCount:t.length,pidsCpuMs:n?d:null,graphicsCpuMs:n?h:null,gpuPct:p?Number(p[1]):null,memPressureLevel:Number(l.stdout.trim().split("\n")[0])||1,swapUsedMB:Number(/used = ([\d.]+)M/.exec(l.stdout)?.[1]??0),thermalLevel:m?Number(m[1]):null}}async xcodeBuild(){const e=await run("xcodebuild",["-version"],{timeoutMs:2e4});return e.stdout.trim().replace(/\s+/g," ")||"unknown"}async clientsOf(e){const n=await listProcesses();const t=new Set((await this.simTree(e)).map(i=>i.pid));const s=new Set(descendants(n,process.pid).map(i=>i.pid));return n.filter(i=>!s.has(i.pid)&&!t.has(i.pid)&&i.command.includes(e)&&!/launchd_sim|CoreSimulator/.test(i.command))}async kill(e,n){if(!await pidAlive(e,n))return;try{process.kill(e,"SIGTERM")}catch{return}for(let t=0;t<20;t++){await new Promise(s=>setTimeout(s,100));if(!await pidAlive(e,n))return}if(await pidAlive(e,n)){try{process.kill(e,"SIGKILL")}catch{}}}}}});function farmHome2(){return process.env.BUOY_FARM_HOME||import_node_path12.default.join(import_node_os8.default.homedir(),".buoy","farm")}function ensureHome(e=farmHome2()){(0,import_node_fs11.mkdirSync)(e,{recursive:true,mode:448});try{(0,import_node_fs11.chmodSync)(e,448)}catch{}}function readOrCreateToken(e=farmHome2()){const n=paths(e).token;try{const t=(0,import_node_fs11.readFileSync)(n,"utf8").trim();if(t)return t}catch{}ensureHome(e);try{(0,import_node_fs11.writeFileSync)(n,(0,import_node_crypto9.randomBytes)(24).toString("hex")+"\n",{mode:384,flag:"wx"})}catch(t){if(t.code!=="EEXIST")throw t}for(let t=0;t<20;t++){const s=(0,import_node_fs11.readFileSync)(n,"utf8").trim();if(s)return s;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,25)}throw new Error(`farm token at ${n} is empty`)}async function readOwner(e=farmHome2()){try{return JSON.parse(await import_node_fs11.promises.readFile(paths(e).lockOwner,"utf8"))}catch{return null}}async function ownerAlive(e){return!!e&&await pidAlive(e.pid,e.start,true)}async function withClaim(e,n){const t=import_node_path12.default.join(e,"owner.claim");for(let s=0;s<20;s++){try{(0,import_node_fs11.mkdirSync)(t,{mode:448});(0,import_node_fs11.writeFileSync)(import_node_path12.default.join(t,"pid"),`${process.pid}
|
|
418
418
|
${await pidStart(process.pid)??""}`);try{return await n()}finally{(0,import_node_fs11.rmSync)(t,{recursive:true,force:true})}}catch(i){if(i.code!=="EEXIST")throw i;const r=(0,import_node_fs11.statSync)(t,{throwIfNoEntry:false});if(r&&Date.now()-r.mtimeMs>3e4){let o=true;try{const[a,l]=(0,import_node_fs11.readFileSync)(import_node_path12.default.join(t,"pid"),"utf8").split("\n");o=!await pidAlive(Number(a),l||void 0)}catch{}if(o){try{(0,import_node_fs11.renameSync)(t,import_node_path12.default.join(e,`owner.claim-dead-${(0,import_node_crypto9.randomBytes)(4).toString("hex")}`))}catch{}}}await new Promise(o=>setTimeout(o,100))}}return null}async function claimOwner(e,n,t=farmHome2()){ensureHome(t);const s=paths(t);const i=await pidStart(process.pid)??"";for(let r=0;r<3;r++){try{(0,import_node_fs11.mkdirSync)(s.lock,{mode:448})}catch(a){if(a.code!=="EEXIST")throw a;const l=await readOwner(t);if(l&&await ownerAlive(l))return null;if(!l){await new Promise(d=>setTimeout(d,300));const u=await readOwner(t);if(u&&await ownerAlive(u))return null}const c=await withClaim(t,async()=>{const u=await readOwner(t);if(u&&await ownerAlive(u))return false;if(!(0,import_node_fs11.existsSync)(s.lock))return true;(0,import_node_fs11.renameSync)(s.lock,import_node_path12.default.join(t,`owner.stale-${(0,import_node_crypto9.randomBytes)(4).toString("hex")}`));return true});if(c===false)return null;continue}const o={pid:process.pid,start:i,version:e,host:n,since:new Date().toISOString()};(0,import_node_fs11.writeFileSync)(s.lockOwner,JSON.stringify(o),{mode:384});cleanupStale(t);return{info:o,release(){try{const a=JSON.parse((0,import_node_fs11.readFileSync)(s.lockOwner,"utf8"));if(a.pid!==process.pid)return}catch{return}try{(0,import_node_fs11.rmSync)(s.socket,{force:true})}catch{}(0,import_node_fs11.rmSync)(s.lock,{recursive:true,force:true})}}}return null}function cleanupStale(e){try{for(const n of(0,import_node_fs11.readdirSync)(e)){if(n.startsWith("owner.stale-")||n.startsWith("owner.claim-dead-"))(0,import_node_fs11.rmSync)(import_node_path12.default.join(e,n),{recursive:true,force:true})}}catch{}}function emptyState(){return{version:STATE_VERSION,seq:0,farmId:(0,import_node_crypto9.randomBytes)(6).toString("hex"),nextIndex:1,templates:{},sims:{},orphans:{},leases:{},runs:{},locks:{},requests:{},events:[],settings:{...DEFAULT_SETTINGS}}}function fsyncWrite(e,n,t){const s=(0,import_node_fs11.openSync)(e,t,384);try{(0,import_node_fs11.writeFileSync)(s,n);(0,import_node_fs11.fsyncSync)(s)}finally{(0,import_node_fs11.closeSync)(s)}}function farmOwnedUdids(e=farmHome2()){try{const n=JSON.parse((0,import_node_fs11.readFileSync)(paths(e).state,"utf8"));return new Set([...Object.keys(n.sims??{}),...Object.keys(n.orphans??{}),...Object.values(n.templates??{}).map(t=>t.udid)])}catch{return new Set}}var import_node_fs11,import_node_os8,import_node_path12,import_node_crypto9,paths,checksum,Store;var init_store=__esm({"../sim-farm/src/store.ts"(){"use strict";import_node_fs11=require("fs");import_node_os8=__toESM(require("os"));import_node_path12=__toESM(require("path"));import_node_crypto9=require("crypto");init_proc();init_types();paths=(e=farmHome2())=>({home:e,lock:import_node_path12.default.join(e,"owner.lock"),lockOwner:import_node_path12.default.join(e,"owner.lock","owner.json"),socket:import_node_path12.default.join(e,"farmd.sock"),token:import_node_path12.default.join(e,"token"),state:import_node_path12.default.join(e,"state.json"),journal:import_node_path12.default.join(e,"journal.ndjson"),log:import_node_path12.default.join(e,"farmd.log"),shots:import_node_path12.default.join(e,"shots")});checksum=e=>(0,import_node_crypto9.createHash)("sha256").update(JSON.stringify(e)).digest("hex").slice(0,12);Store=class{constructor(e=farmHome2()){this.fenced=false;this.pending=[];this.open=new Map;this.p=paths(e);ensureHome(e);this.state=this.load()}get shotsPath(){return this.p.shots}get homeDir(){return this.p.home}fence(){this.fenced=true}assertActive(){if(this.fenced)throw new Error("This farm has stopped.")}load(){let e=emptyState();if((0,import_node_fs11.existsSync)(this.p.state)){const i=JSON.parse((0,import_node_fs11.readFileSync)(this.p.state,"utf8"));if(i.version>STATE_VERSION){throw new Error(`farm state is version ${i.version}; this farmd understands ${STATE_VERSION}. Update Buoy.`)}e={...emptyState(),...i,settings:{...DEFAULT_SETTINGS,...i.settings}}}delete e.settings.activeSlots;const n=new Map;const t=new Set;let s=e.seq;if((0,import_node_fs11.existsSync)(this.p.journal)){for(const i of(0,import_node_fs11.readFileSync)(this.p.journal,"utf8").split("\n")){if(!i.trim())continue;let r;try{r=JSON.parse(i)}catch{continue}const{sum:o,...a}=r;if(checksum(a)!==o||r.seq<=e.seq)continue;s=Math.max(s,r.seq);if(r.type==="intent")n.set(r.seq,r);else{n.delete(r.seq);t.add(r.seq)}}}for(const i of e.openIntents??[]){if(!t.has(i.seq)&&!n.has(i.seq))n.set(i.seq,{...i,type:"intent",sum:""})}this.pending=[...n.values()].sort((i,r)=>i.seq-r.seq);for(const i of this.pending)this.open.set(i.seq,i);e.seq=s;e.openIntents=[];return e}intent(e,n){this.assertActive();const t=++this.state.seq;const s={seq:t,at:new Date().toISOString(),type:"intent",op:e,data:n};this.append(s);this.open.set(t,{...s,sum:""});return t}result(e,n,t){this.assertActive();this.open.delete(e);this.pending=this.pending.filter(s=>s.seq!==e);this.save();this.append({seq:e,at:new Date().toISOString(),type:"result",op:n,data:t})}append(e){this.assertActive();fsyncWrite(this.p.journal,JSON.stringify({...e,sum:checksum(e)})+"\n","a")}save(){this.assertActive();const e=`${this.p.state}.${(0,import_node_crypto9.randomBytes)(4).toString("hex")}.tmp`;this.state.openIntents=[...this.open.values()].map(({seq:n,at:t,op:s,data:i})=>({seq:n,at:t,op:s,data:i}));fsyncWrite(e,JSON.stringify(this.state,null,1),"w");(0,import_node_fs11.renameSync)(e,this.p.state);try{if((0,import_node_fs11.statSync)(this.p.journal).size>2*1024*1024)(0,import_node_fs11.rmSync)(this.p.journal,{force:true})}catch{}}event(e){this.assertActive();this.state.events.push({at:new Date().toISOString(),...e});if(this.state.events.length>300)this.state.events.splice(0,this.state.events.length-300)}}}});async function startFarmd(e={}){const n=e.home??farmHome2();const t=paths(n);if(Buffer.byteLength(t.socket)>103){const r=new FarmError("BAD_REQUEST",`The farm folder's path is too long for macOS (${Buffer.byteLength(t.socket)} of 103 bytes).`,"Set BUOY_FARM_HOME to a shorter folder.");try{(0,import_node_fs12.mkdirSync)(n,{recursive:true});(0,import_node_fs12.appendFileSync)(t.log,`${new Date().toISOString()} ${r.message} ${r.next}
|
|
419
419
|
`)}catch{}throw r}const s=await claimOwner(PROTOCOL_VERSION,e.hostLabel??"node",n);if(!s)return null;const i=r=>{try{(0,import_node_fs12.appendFileSync)(t.log,`${new Date().toISOString()} ${r}
|
|
420
|
-
`)}catch{}e.log?.(r)};try{return await serve(e,n,s,i)}catch(r){s.release();throw r}}async function serve(e,n,t,s){const i=paths(n);const r=readOrCreateToken(n);const o=new Store(n);const a=new Engine({host:e.host??new SimctlHost,store:o,log:s,pollMs:e.pollMs,readyTimeoutMs:e.readyTimeoutMs,phoneBuilder:e.phoneBuilder});await a.recover();(0,import_node_fs12.rmSync)(i.socket,{force:true});const l=new Set;let c=Date.now();let u=false;let d=0;const h=import_node_net.default.createServer(k=>{l.add(k);k.on("close",()=>l.delete(k));k.on("error",()=>{});let $="";k.on("data",_=>{$+=_.toString("utf8");if($.length>1024*1024)return k.destroy();let S;while((S=$.indexOf("\n"))>=0){const E=$.slice(0,S);$=$.slice(S+1);if(E.trim())void m(k,E)}})});const p=(k,$,_)=>{if(!k.destroyed)k.write(JSON.stringify({id:$,..._})+"\n")};async function m(k,$){c=Date.now();let _;try{_=JSON.parse($)}catch{return p(k,-1,{ok:false,error:{code:"BAD_REQUEST",message:"Not JSON."}})}d++;try{if(!_||typeof _!=="object")throw new FarmError("BAD_REQUEST","Use a request object.");if(!tokenOk(_.token,r))throw new FarmError("UNAUTHORIZED","Bad farm token.");if(!_.client||typeof _.client.clientId!=="string"||!_.client.clientId.trim())throw new FarmError("BAD_REQUEST","Each call needs a clientId.");if(u&&!["hello","status"].includes(_.method)){throw new FarmError("HOST_BUSY","The farm is stopping.","Try again in a few seconds.",true)}if(_.v>PROTOCOL_VERSION&&WRITE_METHODS.has(_.method)){throw new FarmError("FARMD_VERSION",`This farm runs protocol ${PROTOCOL_VERSION}; the client speaks ${_.v}.`,"Ask it to stop when idle (method shutdown), then start a newer farm.")}if(_.v<PROTOCOL_VERSION&&WRITE_METHODS.has(_.method)){throw new FarmError("READ_ONLY",`This client speaks farm protocol ${_.v}; the farm runs ${PROTOCOL_VERSION}.`,"Update Buoy to change the farm.")}validateRequest(_);const S=await y(_);p(k,_.id,{ok:true,result:S})}catch(S){const E=S instanceof FarmError?S.toJSON():{code:"HOST_ERROR",message:S.message,retryable:false};p(k,_?.id??-1,{ok:false,error:E})}finally{d--}}async function y(k){const $=k.params??{};const _=k.client;const S=E=>a.once(_.clientId,k.requestId,{m:k.method,q:$},E);switch(k.method){case"hello":return{protocol:PROTOCOL_VERSION,pid:process.pid,host:t.info.host,farmId:a.state.farmId};case"status":return a.status({outside:$.outside});case"budget":return a.budget({ramGB:$.ramGB??0,diskGB:$.diskGB??0});case"setup":return S(()=>a.createTemplate($));case"grow":return S(async()=>(await a.grow($.templateId,$.count)).map(E=>E.name));case"shrink":return S(()=>a.shrink($.templateId,$.count));case"removeTemplate":return S(()=>a.removeTemplate($.templateId));case"acquire":return S(async()=>{const{lease:E,run:A,sims:P}=await a.acquire({...$,owner:_});return{runId:A.id,leaseId:E.id,epoch:E.epoch,deadlineAt:A.deadlineAt,capAt:A.capAt,sims:P.map(x=>({udid:x.udid,name:x.name,buoyDeviceId:x.buoyDeviceId,profile:x.profile}))}});case"renew":return a.renew($.leaseId,$.epoch,_.clientId);case"release":return S(()=>a.release($.leaseId,$.epoch,$.reset,_.clientId,$.keepRun===true));case"action":return S(()=>a.action($.leaseId,$.epoch,$.udids,$.action,$.arg,_.clientId));case"lock":return a.lock($.name,$.leaseId,$.epoch,$.waitMs??0,_.clientId);case"unlock":return a.unlock($.name,$.leaseId,$.epoch,_.clientId);case"fairness":return a.fairness($.leaseId,$.epoch,$.windows,_.clientId);case"park":return S(async()=>(await a.parkSim($.udid,"asked"),"parked"));case"wake":return S(async()=>(await a.bootSim($.udid)).power);case"reset":return S(async()=>(await a.fastReset($.udid),"reset"));case"rebuild":return S(async()=>(await a.rebuildSim($.udid)).udid);case"delete":return S(async()=>(await a.deleteSim($.udid),"deleted"));case"endLease":{const E=a.state.leases[$.leaseId];if(!E)return{};if(E.owner.clientId!==_.clientId&&$.force!==true){throw new FarmError("NOT_OWNED",`Lease ${$.leaseId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return S(()=>a.endLease($.leaseId,`ended by ${_.label}`,"full",E.owner.clientId===_.clientId?"released":"user"))}case"report":return S(async()=>a.report($,_.clientId));case"extend":return S(async()=>a.extend($.runId,$.minutes,_.clientId));case"finish":return S(async()=>a.finish($.runId,$.outcome,$.summary,_.clientId));case"stopRun":{const E=a.state.runs[$.runId];if(E&&E.owner.clientId!==_.clientId&&$.force!==true){throw new FarmError("NOT_OWNED",`Run ${$.runId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return S(async()=>a.stopRun($.runId))}case"stopAll":if($.force!==true)throw new FarmError("BAD_REQUEST","stopAll needs force: true, after a person confirms.");return S(async()=>a.stopAll());case"settings":return a.setSettings($);case"shot":return a.shot($.udid);case"show":return a.show($.udid);case"shutdown":if($.ifIdle!==false&&(!a.idle()||d>1))return{stopping:false,reason:"busy"};void v("asked to stop");return{stopping:true};default:throw new FarmError("BAD_REQUEST",`Unknown method ${k.method}.`)}}await new Promise((k,$)=>{h.once("error",$);h.listen(i.socket,()=>k())});(0,import_node_fs12.chmodSync)(i.socket,384);s(`farmd ${PROTOCOL_VERSION} up, pid ${process.pid}, host ${e.hostLabel??"node"}`);const g=setInterval(()=>{void a.tick().catch($=>s(`tick: ${$.message}`));const k=Date.now()-c;if(e.idleExitMs&&l.size===0&&k>e.idleExitMs){void(async()=>{await a.parkAllIdle();if(a.idle()&&l.size===0&&d===0&&Date.now()-c>e.idleExitMs)await v("idle")})()}},e.tickMs??5e3);g.unref?.();const f=setInterval(()=>void a.meter.tick().catch(k=>s(`meter: ${k.message}`)),1e3);f.unref?.();let w=null;function v(k,$=10*6e4){if(!w)w=b(k,$);return w}async function b(k,$){if(u)return;u=true;clearInterval(g);clearInterval(f);s(`farmd stopping: ${k}`);const _=Date.now()+$;while(d>0&&Date.now()<_)await new Promise(A=>setTimeout(A,200));const S=await a.drain(Math.max(0,_-Date.now()));let E=false;if(d>0||!S||liveChildCount()>0){o.save();a.fence();E=true;s(`farmd stopping with ${d} call(s) and ${liveChildCount()} child process(es) still running; killing them`);killAllChildren();const A=Date.now()+5e3;while(liveChildCount()>0&&Date.now()<A)await new Promise(P=>setTimeout(P,100))}await a.settleFiles();if(!E&&d===0){try{await a.parkAllIdle()}catch{}}for(const A of l)A.destroy();await new Promise(A=>h.close(()=>A()));if(!E){o.save();a.fence()}t.release();e.onExit?.(k)}return{engine:a,owner:t,stop:v}}function tokenOk(e,n){if(typeof e!=="string"||e.length!==n.length)return false;return(0,import_node_crypto10.timingSafeEqual)(Buffer.from(e),Buffer.from(n))}function validateRequest(e){const n=o=>{throw new FarmError("BAD_REQUEST",o)};const t=e.params??{};if(typeof t!=="object"||Array.isArray(t))n("Params must be an object.");const s=(o,a=false)=>{if(a&&t[o]===void 0)return;if(typeof t[o]!=="string"||!t[o].trim())n(`${o} must be text.`)};const i=o=>{if(t[o]!==void 0&&typeof t[o]!=="boolean")n(`${o} must be true or false.`)};const r=(o,a,l,c=false,u=false)=>{const d=t[o];if(u&&d===void 0)return;if(typeof d!=="number"||!Number.isFinite(d)||d<a||d>l||c&&!Number.isInteger(d))n(`${o} must be a number from ${a} to ${l}.`)};switch(e.method){case"status":i("outside");break;case"budget":r("ramGB",0,Number.MAX_VALUE,false,true);r("diskGB",0,Number.MAX_VALUE,false,true);break;case"setup":for(const o of["appPath","deviceType","runtime"])s(o);if(t.seedDir!=null)s("seedDir");s("name",true);s("launchUrl",true);i("warmLaunch");if(!["slim","stock"].includes(t.profile))n("Use slim or stock.");r("metroPort",1,65535,true);break;case"removeTemplate":s("templateId");break;case"grow":case"shrink":case"acquire":s("templateId");r("count",1,64,true);if(e.method==="acquire"){s("purpose");r("ttlMs",1,Number.MAX_VALUE,false,true);r("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);i("launch");i("allowPartial");s("goal",true);s("runId",true);s("kind",true);if(t.labels!==void 0&&(!Array.isArray(t.labels)||t.labels.some(o=>typeof o!=="string")))n("labels must be a list of text.");if(!Number.isInteger(e.client.pid)||e.client.pid<=0)n("The client needs a valid pid.")}break;case"release":if(t.reset!==void 0&&!["fast","full"].includes(t.reset))n("Use fast or full reset.");break;case"action":if(!Array.isArray(t.udids)||t.udids.some(o=>typeof o!=="string"||!o))n("udids must be a list of sim ids.");if(!["reset","rebuild","open-url","screenshot","launch"].includes(t.action))n("Unknown sim action.");s("arg",t.action!=="open-url");break;case"lock":case"unlock":s("name");if(!/^[a-z0-9-]{1,40}$/.test(t.name))n("Lock names are short lowercase words.");r("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);break;case"fairness":if(!Array.isArray(t.windows)||t.windows.length>500||t.windows.some(o=>!o||typeof o.id!=="string"||!Number.isFinite(o.from)||!Number.isFinite(o.to)||o.to<o.from)){n("windows must be a list of { id, from, to } times in ms.")}break;case"endLease":s("leaseId");break;case"report":s("runId");s("udid",true);s("note",true);r("sequence",0,Number.MAX_SAFE_INTEGER,true,true);if(t.state!==void 0&&typeof t.state!=="string")n("state must be text.");break;case"extend":s("runId");r("minutes",1,24*60);break;case"finish":s("runId");s("summary",true);s("outcome",true);break;case"stopRun":s("runId");i("force");break;case"stopAll":i("force");break;case"shutdown":i("ifIdle");break}if(["renew","release","action","lock","unlock","fairness"].includes(e.method))s("leaseId")}var import_node_net,import_node_fs12,import_node_crypto10,WRITE_METHODS;var init_server=__esm({"../sim-farm/src/server.ts"(){"use strict";import_node_net=__toESM(require("net"));import_node_fs12=require("fs");import_node_crypto10=require("crypto");init_engine();init_host();init_store();init_types();init_proc();WRITE_METHODS=new Set(["setup","grow","shrink","acquire","release","action","lock","unlock","park","wake","reset","rebuild","delete","endLease","settings","deleteFarm","show","removeTemplate","report","extend","finish","stopRun","stopAll"])}});function nodeLauncher(e,n=["farmd"]){return()=>{const t=(0,import_node_child_process9.spawn)(process.execPath,[...process.execArgv,e,...n],{detached:true,stdio:"ignore",env:{...process.env}});t.unref()}}async function connectFarm(e){return new FarmClient(e).connect()}var import_node_net2,import_node_fs13,import_node_child_process9,import_node_crypto11,FarmClient;var init_client=__esm({"../sim-farm/src/client.ts"(){"use strict";import_node_net2=__toESM(require("net"));import_node_fs13=require("fs");import_node_child_process9=require("child_process");import_node_crypto11=require("crypto");init_store();init_proc();init_types();FarmClient=class{constructor(e){this.opts=e;this.sock=null;this.nextId=1;this.pending=new Map;this.buf="";this.renewals=new Map;this.reconnecting=null;this.closed=false;this.home=e.home??farmHome2();this.clientIdValue=`c-${(0,import_node_crypto11.randomBytes)(5).toString("hex")}`}get clientId(){return this.owner?.clientId}async connect(){this.token=readOrCreateToken(this.home);this.owner={clientId:this.clientIdValue,pid:process.pid,pidStart:await pidStart(process.pid)??void 0,label:this.opts.label,...this.opts.agent?{agent:this.opts.agent}:{}};if(await this.tryOpen())return this.checkVersion();const e=await readOwner(this.home);if(!await ownerAlive(e)){if(!this.opts.launcher)throw new FarmError("HOST_ERROR","The farm is not running.","Start it from Buoy Desktop or `pnpm farm status`.");this.opts.launcher()}const n=Date.now()+(this.opts.startTimeoutMs??2e4);while(Date.now()<n){await sleep(250);if(await this.tryOpen())return this.checkVersion()}throw new FarmError("HOST_ERROR","The farm did not start in time.",`Check ${paths(this.home).log}.`)}async checkVersion(){const e=await this.send("hello",void 0,{});if(e.protocol<PROTOCOL_VERSION&&this.opts.launcher){const n=await this.send("shutdown",{ifIdle:true},{});if(n.stopping){this.close();await sleep(500);return this.connect()}}return this}tryOpen(){const e=paths(this.home).socket;if(!(0,import_node_fs13.existsSync)(e))return Promise.resolve(false);return new Promise(n=>{const t=import_node_net2.default.createConnection(e);t.once("connect",()=>{this.sock=t;t.on("data",s=>this.onData(s));t.on("close",()=>this.onClose());t.on("error",()=>{});n(true)});t.once("error",()=>n(false))})}onData(e){this.buf+=e.toString("utf8");let n;while((n=this.buf.indexOf("\n"))>=0){const t=this.buf.slice(0,n);this.buf=this.buf.slice(n+1);let s;try{s=JSON.parse(t)}catch{continue}const i=this.pending.get(s.id);if(!i)continue;this.pending.delete(s.id);clearTimeout(i.timer);if(s.ok)i.resolve(s.result);else{const r=s.error??{};i.reject(new FarmError(r.code??"HOST_ERROR",r.message??"farm error",r.next,r.retryable))}}}onClose(){this.sock=null;for(const[,e]of this.pending){clearTimeout(e.timer);e.reject(new FarmError("HOST_ERROR","Lost the connection to the farm.",void 0,true))}this.pending.clear()}reconnect(){if(!this.reconnecting){this.reconnecting=this.connect().then(()=>void 0).finally(()=>{this.reconnecting=null})}return this.reconnecting}async call(e,n,t={}){if(!this.sock&&!this.closed)await this.reconnect();return this.send(e,n,t)}send(e,n,t){if(!this.sock)return Promise.reject(new FarmError("HOST_ERROR","Not connected to the farm.",void 0,true));const s=this.nextId++;const i={id:s,v:PROTOCOL_VERSION,token:this.token,client:this.owner,method:e,params:n,requestId:t.requestId};return new Promise((r,o)=>{const a=setTimeout(()=>{this.pending.delete(s);o(new FarmError("HOST_ERROR",`Farm call ${e} timed out.`,void 0,true))},t.timeoutMs??3e4);this.pending.set(s,{resolve:r,reject:o,timer:a});this.sock.write(JSON.stringify(i)+"\n")})}async acquire(e){const{requestId:n,...t}=e;const s=n??`acq-${(0,import_node_crypto11.randomBytes)(4).toString("hex")}`;const i=await this.call("acquire",t,{timeoutMs:15*6e4+(e.waitMs??0),requestId:s});if(this.renewals.has(i.leaseId))return i;const r=setInterval(()=>{this.call("renew",{leaseId:i.leaseId,epoch:i.epoch}).catch(o=>{if(["LEASE_EXPIRED","STALE_EPOCH","RUN_STOPPED","RUN_ENDED"].includes(o.code))this.stopRenew(i.leaseId)})},15e3);r.unref?.();this.renewals.set(i.leaseId,r);return i}stopRenew(e){const n=this.renewals.get(e);if(n)clearInterval(n);this.renewals.delete(e)}async release(e,n,t="fast",s=false){this.stopRenew(e);return this.call("release",{leaseId:e,epoch:n,reset:t,keepRun:s},{timeoutMs:6e4,requestId:`rel-${e}-${n}`})}close(){this.closed=true;for(const e of[...this.renewals.keys()])this.stopRenew(e);this.sock?.end();this.sock=null}}}});function findRepoRoot(e=process.cwd()){let n=e;for(;;){if((0,import_node_fs14.existsSync)(import_node_path13.default.join(n,"buoy.tools.json")))return n;const t=import_node_path13.default.dirname(n);if(t===n)return null;n=t}}function findExampleBuild(){const e=import_node_path13.default.join(import_node_os9.default.homedir(),"Library/Developer/Xcode/DerivedData");let n=null;try{for(const t of(0,import_node_fs14.readdirSync)(e)){if(!t.startsWith("exampledevbuild-"))continue;const s=import_node_path13.default.join(e,t,"Build/Products/Debug-iphonesimulator/exampledevbuild.app");if(!(0,import_node_fs14.existsSync)(s))continue;const i=(0,import_node_fs14.statSync)(s).mtimeMs;if(!n||i>n.t)n={p:s,t:i}}}catch{}return n?.p??null}function defaultSeedDir(e=findRepoRoot()){return e?import_node_path13.default.join(e,"example-dev-build/artifacts/pool-seed"):null}async function farmDefaults(e=findRepoRoot()){const n=defaultSeedDir(e);let t=[];let s=[];try{const i=JSON.parse(await runOk("xcrun",["simctl","list","runtimes","--json"],{timeoutMs:3e4}));t=(i.runtimes??[]).filter(o=>o.isAvailable!==false&&/^iOS/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}));const r=JSON.parse(await runOk("xcrun",["simctl","list","devicetypes","--json"],{timeoutMs:3e4}));s=(r.devicetypes??[]).filter(o=>/iPhone|iPad/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}))}catch{}return{appPath:findExampleBuild(),seedDir:n&&(0,import_node_fs14.existsSync)(n)?n:null,seedProblem:n?seedProblem(n):"Could not find the repo's seed folder.",runtimes:t,deviceTypes:s,slimTurnsOff:SLIM_CATEGORIES.map(i=>({name:i.name,downside:i.downside}))}}var import_node_path13,import_node_os9,import_node_fs14;var init_discover=__esm({"../sim-farm/src/discover.ts"(){"use strict";import_node_path13=__toESM(require("path"));import_node_os9=__toESM(require("os"));import_node_fs14=require("fs");init_proc();init_seed();init_profiles()}});async function parentOf(e){const n=await run("ps",["-o","ppid=,comm=","-p",String(e)],{timeoutMs:3e3});const t=/^\s*(\d+)\s+(.+?)\s*$/.exec(n.stdout);return n.code===0&&t?{ppid:Number(t[1]),comm:t[2]}:null}async function cwdOf(e){const n=await run("lsof",["-a","-p",String(e),"-d","cwd","-Fn"],{timeoutMs:3e3});const t=n.stdout.split("\n").find(s=>s.startsWith("n"));return t?t.slice(1):null}function fromClientName(e){if(!e)return void 0;return APPS.find(n=>e.toLowerCase().includes(n.app))}async function detectAgent(e={}){const n=e.env??process.env;let t=e.pid??process.pid;let s=null;for(let u=0;u<5&&t>1;u++){const d=await parentOf(t).catch(()=>null);if(!d)break;const h=APPS.find(p=>p.match.test(d.comm));if(u>0&&h){s={app:h,pid:t};break}t=d.ppid}const i=s?.app??fromClientName(e.clientName);const r=(s?await cwdOf(s.pid).catch(()=>null):null)??process.cwd();const o=(await run("git",["-C",r,"rev-parse","--abbrev-ref","HEAD"],{timeoutMs:3e3}).catch(()=>null))?.stdout.trim();const a=n.CLAUDE_CODE_SESSION_ID?.trim();let l;let c="none";if(a&&(i?.app??"claude")==="claude"){l=`claude:${a}`;c="claude-session"}else if(s){l=`${s.app.app}:${s.pid}:${await pidStart(s.pid)??""}`;c="parent-process"}return{app:i?.app??"other",appName:i?.name??(e.clientName||"Agent"),chatKey:l,chatKeySource:c,project:import_node_path14.default.basename(r)||void 0,projectPath:r,branch:o&&o!=="HEAD"?o:void 0,clientName:e.clientName}}function agentLabel(e){return e.project?`${e.appName} \xB7 ${e.project}`:e.appName}var import_node_path14,APPS;var init_agent=__esm({"../sim-farm/src/agent.ts"(){"use strict";import_node_path14=__toESM(require("path"));init_proc();APPS=[{app:"claude",name:"Claude",match:/(^|\/)claude$/i},{app:"codex",name:"Codex",match:/(^|\/)codex$/i},{app:"cursor",name:"Cursor",match:/cursor/i}]}});function parseProbeLine(e){const n=e.match(/BUOY_PROBE physicalMemory=(\d+) cpus=(\d+) footprint=(\d+) available=(\d+)/);if(!n)return null;const[t,s,i,r]=n.slice(1).map(Number);const o=i+r;if(!t||!s||r<=0||o>t)return null;return{ramMB:Math.floor(t/MB),cpus:s,limitMB:Math.floor(o/MB)}}function plistString(e,n){return e.match(new RegExp(`<key>${n}</key>\\s*<string>([^<]*)</string>`))?.[1]}function plistArray(e,n,t){const s=e.match(new RegExp(`<key>${n}</key>\\s*<array>([\\s\\S]*?)</array>`))?.[1]??"";return[...s.matchAll(new RegExp(`<${t}>([\\s\\S]*?)</${t}>`,"g"))].map(i=>i[1].replace(/\s+/g,""))}function parseProvisioningProfile(e,n){const t=n.match(/<key>Entitlements<\/key>\s*<dict>([\s\S]*?)<\/dict>/)?.[1]??"";const s=plistString(t,"application-identifier");const i=plistArray(n,"TeamIdentifier","string")[0];const r=n.match(/<key>ExpirationDate<\/key>\s*<date>([^<]+)<\/date>/)?.[1];if(!s||!i||!r)return null;return{file:e,name:plistString(n,"Name")??import_node_path15.default.basename(e),team:i,appId:s,expires:new Date(r),devices:plistArray(n,"ProvisionedDevices","string"),allDevices:/<key>ProvisionsAllDevices<\/key>\s*<true\/>/.test(n),getTaskAllow:/<key>get-task-allow<\/key>\s*<true\/>/.test(t),certSha1s:plistArray(n,"DeveloperCertificates","data").map(o=>(0,import_node_crypto12.createHash)("sha1").update(Buffer.from(o,"base64")).digest("hex").toUpperCase())}}function pickProfile(e,n,t,s=new Date){const i=e.filter(o=>o.getTaskAllow&&o.expires>s&&(o.allDevices||o.devices.includes(n))&&(o.appId===`${o.team}.*`||o.appId===`${o.team}.${PROBE_ID_SUFFIX}`)&&o.certSha1s.some(a=>t.has(a)));i.sort((o,a)=>Number(a.appId.endsWith(".*"))-Number(o.appId.endsWith(".*"))||a.expires.getTime()-o.expires.getTime());const r=i[0];return r?{profile:r,identity:r.certSha1s.find(o=>t.has(o))}:null}async function localProfiles(){const e=[import_node_path15.default.join(import_node_os10.default.homedir(),"Library/Developer/Xcode/UserData/Provisioning Profiles"),import_node_path15.default.join(import_node_os10.default.homedir(),"Library/MobileDevice/Provisioning Profiles")];const n=[];for(const t of e){let s=[];try{s=(await import_node_fs15.promises.readdir(t)).filter(i=>i.endsWith(".mobileprovision"))}catch{continue}for(const i of s){const r=import_node_path15.default.join(t,i);try{const{stdout:o}=await execFileAsync7("security",["cms","-D","-i",r],{timeout:15e3,maxBuffer:16*MB});const a=parseProvisioningProfile(r,o);if(a)n.push(a)}catch{}}}return n}async function codesignIdentities(){const{stdout:e}=await execFileAsync7("security",["find-identity","-v","-p","codesigning"],{timeout:15e3});return new Set([...e.matchAll(/^\s*\d+\)\s+([0-9A-F]{40})\s/gm)].map(n=>n[1]))}async function physicalPhones2(){const e=await import_node_fs15.promises.mkdtemp(import_node_path15.default.join(import_node_os10.default.tmpdir(),"buoy-phones-"));try{const n=import_node_path15.default.join(e,"devices.json");await execFileAsync7("xcrun",["devicectl","list","devices","--json-output",n],{timeout:3e4});const t=JSON.parse(await import_node_fs15.promises.readFile(n,"utf8"))?.result?.devices??[];return t.filter(s=>s.hardwareProperties?.reality==="physical"&&s.hardwareProperties?.deviceType==="iPhone"&&s.connectionProperties?.pairingState==="paired").map(s=>({udid:s.hardwareProperties.udid,name:s.hardwareProperties.marketingName??"iPhone",productType:s.hardwareProperties.productType,ios:s.deviceProperties?.osVersionNumber??""}))}finally{await import_node_fs15.promises.rm(e,{recursive:true,force:true}).catch(()=>void 0)}}function phoneKey(e,n,t){const s=Object.keys(e).find(r=>e[r].productType===n);if(s)return s;const i=r=>r.toLowerCase().replace(/[()]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");return`${i(t)}-${i(n)}`.slice(0,64).replace(/-$/,"")}async function buildProbe(e,n,t,s){const i=import_node_path15.default.join(e,"PhoneProbe.app");await import_node_fs15.promises.mkdir(i,{recursive:true});const r=import_node_path15.default.join(e,"PhoneProbe.m");await import_node_fs15.promises.writeFile(r,PHONE_PROBE_SOURCE);await execFileAsync7("xcrun",["--sdk","iphoneos","clang","-arch","arm64","-miphoneos-version-min=13.0","-fobjc-arc","-O2","-framework","Foundation","-framework","UIKit",r,"-o",import_node_path15.default.join(i,"PhoneProbe")],{timeout:18e4,maxBuffer:8*MB,env:{...process.env,TMPDIR:e}});const o=PROBE_ID_SUFFIX;await import_node_fs15.promises.writeFile(import_node_path15.default.join(i,"Info.plist"),`<?xml version="1.0" encoding="UTF-8"?>
|
|
420
|
+
`)}catch{}e.log?.(r)};try{return await serve(e,n,s,i)}catch(r){s.release();throw r}}async function serve(e,n,t,s){const i=paths(n);const r=readOrCreateToken(n);const o=new Store(n);const a=new Engine({host:e.host??new SimctlHost,store:o,log:s,pollMs:e.pollMs,readyTimeoutMs:e.readyTimeoutMs,phoneBuilder:e.phoneBuilder});await a.recover();(0,import_node_fs12.rmSync)(i.socket,{force:true});const l=new Set;let c=Date.now();let u=false;let d=0;const h=import_node_net.default.createServer(_=>{l.add(_);_.on("close",()=>l.delete(_));_.on("error",()=>{});let $="";_.on("data",k=>{$+=k.toString("utf8");if($.length>1024*1024)return _.destroy();let S;while((S=$.indexOf("\n"))>=0){const E=$.slice(0,S);$=$.slice(S+1);if(E.trim())void m(_,E)}})});const p=(_,$,k)=>{if(!_.destroyed)_.write(JSON.stringify({id:$,...k})+"\n")};async function m(_,$){c=Date.now();let k;try{k=JSON.parse($)}catch{return p(_,-1,{ok:false,error:{code:"BAD_REQUEST",message:"Not JSON."}})}d++;try{if(!k||typeof k!=="object")throw new FarmError("BAD_REQUEST","Use a request object.");if(!tokenOk(k.token,r))throw new FarmError("UNAUTHORIZED","Bad farm token.");if(!k.client||typeof k.client.clientId!=="string"||!k.client.clientId.trim())throw new FarmError("BAD_REQUEST","Each call needs a clientId.");if(u&&!["hello","status"].includes(k.method)){throw new FarmError("HOST_BUSY","The farm is stopping.","Try again in a few seconds.",true)}if(k.v>PROTOCOL_VERSION&&WRITE_METHODS.has(k.method)){throw new FarmError("FARMD_VERSION",`This farm runs protocol ${PROTOCOL_VERSION}; the client speaks ${k.v}.`,"Ask it to stop when idle (method shutdown), then start a newer farm.")}if(k.v<PROTOCOL_VERSION&&WRITE_METHODS.has(k.method)){throw new FarmError("READ_ONLY",`This client speaks farm protocol ${k.v}; the farm runs ${PROTOCOL_VERSION}.`,"Update Buoy to change the farm.")}validateRequest(k);const S=await y(k);p(_,k.id,{ok:true,result:S})}catch(S){const E=S instanceof FarmError?S.toJSON():{code:"HOST_ERROR",message:S.message,retryable:false};p(_,k?.id??-1,{ok:false,error:E})}finally{d--}}async function y(_){const $=_.params??{};const k=_.client;const S=E=>a.once(k.clientId,_.requestId,{m:_.method,q:$},E);switch(_.method){case"hello":return{protocol:PROTOCOL_VERSION,pid:process.pid,host:t.info.host,farmId:a.state.farmId};case"status":return a.status({outside:$.outside});case"budget":return a.budget({ramGB:$.ramGB??0,diskGB:$.diskGB??0});case"setup":return S(()=>a.createTemplate($));case"grow":return S(async()=>(await a.grow($.templateId,$.count)).map(E=>E.name));case"shrink":return S(()=>a.shrink($.templateId,$.count));case"removeTemplate":return S(()=>a.removeTemplate($.templateId));case"acquire":return S(async()=>{const{lease:E,run:A,sims:P}=await a.acquire({...$,owner:k});return{runId:A.id,leaseId:E.id,epoch:E.epoch,deadlineAt:A.deadlineAt,capAt:A.capAt,sims:P.map(x=>({udid:x.udid,name:x.name,buoyDeviceId:x.buoyDeviceId,profile:x.profile}))}});case"renew":return a.renew($.leaseId,$.epoch,k.clientId);case"release":return S(()=>a.release($.leaseId,$.epoch,$.reset,k.clientId,$.keepRun===true));case"action":return S(()=>a.action($.leaseId,$.epoch,$.udids,$.action,$.arg,k.clientId));case"lock":return a.lock($.name,$.leaseId,$.epoch,$.waitMs??0,k.clientId);case"unlock":return a.unlock($.name,$.leaseId,$.epoch,k.clientId);case"fairness":return a.fairness($.leaseId,$.epoch,$.windows,k.clientId);case"park":return S(async()=>(await a.parkSim($.udid,"asked"),"parked"));case"wake":return S(async()=>(await a.bootSim($.udid)).power);case"reset":return S(async()=>(await a.fastReset($.udid),"reset"));case"rebuild":return S(async()=>(await a.rebuildSim($.udid)).udid);case"delete":return S(async()=>(await a.deleteSim($.udid),"deleted"));case"endLease":{const E=a.state.leases[$.leaseId];if(!E)return{};if(E.owner.clientId!==k.clientId&&$.force!==true){throw new FarmError("NOT_OWNED",`Lease ${$.leaseId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return S(()=>a.endLease($.leaseId,`ended by ${k.label}`,"full",E.owner.clientId===k.clientId?"released":"user"))}case"report":return S(async()=>a.report($,k.clientId));case"extend":return S(async()=>a.extend($.runId,$.minutes,k.clientId));case"finish":return S(async()=>a.finish($.runId,$.outcome,$.summary,k.clientId));case"stopRun":{const E=a.state.runs[$.runId];if(E&&E.owner.clientId!==k.clientId&&$.force!==true){throw new FarmError("NOT_OWNED",`Run ${$.runId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return S(async()=>a.stopRun($.runId))}case"stopAll":if($.force!==true)throw new FarmError("BAD_REQUEST","stopAll needs force: true, after a person confirms.");return S(async()=>a.stopAll());case"settings":return a.setSettings($);case"shot":return a.shot($.udid);case"show":return a.show($.udid);case"shutdown":if($.ifIdle!==false&&(!a.idle()||d>1))return{stopping:false,reason:"busy"};void v("asked to stop");return{stopping:true};default:throw new FarmError("BAD_REQUEST",`Unknown method ${_.method}.`)}}await new Promise((_,$)=>{h.once("error",$);h.listen(i.socket,()=>_())});(0,import_node_fs12.chmodSync)(i.socket,384);s(`farmd ${PROTOCOL_VERSION} up, pid ${process.pid}, host ${e.hostLabel??"node"}`);const g=setInterval(()=>{void a.tick().catch($=>s(`tick: ${$.message}`));const _=Date.now()-c;if(e.idleExitMs&&l.size===0&&_>e.idleExitMs){void(async()=>{await a.parkAllIdle();if(a.idle()&&l.size===0&&d===0&&Date.now()-c>e.idleExitMs)await v("idle")})()}},e.tickMs??5e3);g.unref?.();const f=setInterval(()=>void a.meter.tick().catch(_=>s(`meter: ${_.message}`)),1e3);f.unref?.();let w=null;function v(_,$=10*6e4){if(!w)w=b(_,$);return w}async function b(_,$){if(u)return;u=true;clearInterval(g);clearInterval(f);s(`farmd stopping: ${_}`);const k=Date.now()+$;while(d>0&&Date.now()<k)await new Promise(A=>setTimeout(A,200));const S=await a.drain(Math.max(0,k-Date.now()));let E=false;if(d>0||!S||liveChildCount()>0){o.save();a.fence();E=true;s(`farmd stopping with ${d} call(s) and ${liveChildCount()} child process(es) still running; killing them`);killAllChildren();const A=Date.now()+5e3;while(liveChildCount()>0&&Date.now()<A)await new Promise(P=>setTimeout(P,100))}await a.settleFiles();if(!E&&d===0){try{await a.parkAllIdle()}catch{}}for(const A of l)A.destroy();await new Promise(A=>h.close(()=>A()));if(!E){o.save();a.fence()}t.release();e.onExit?.(_)}return{engine:a,owner:t,stop:v}}function tokenOk(e,n){if(typeof e!=="string"||e.length!==n.length)return false;return(0,import_node_crypto10.timingSafeEqual)(Buffer.from(e),Buffer.from(n))}function validateRequest(e){const n=o=>{throw new FarmError("BAD_REQUEST",o)};const t=e.params??{};if(typeof t!=="object"||Array.isArray(t))n("Params must be an object.");const s=(o,a=false)=>{if(a&&t[o]===void 0)return;if(typeof t[o]!=="string"||!t[o].trim())n(`${o} must be text.`)};const i=o=>{if(t[o]!==void 0&&typeof t[o]!=="boolean")n(`${o} must be true or false.`)};const r=(o,a,l,c=false,u=false)=>{const d=t[o];if(u&&d===void 0)return;if(typeof d!=="number"||!Number.isFinite(d)||d<a||d>l||c&&!Number.isInteger(d))n(`${o} must be a number from ${a} to ${l}.`)};switch(e.method){case"status":i("outside");break;case"budget":r("ramGB",0,Number.MAX_VALUE,false,true);r("diskGB",0,Number.MAX_VALUE,false,true);break;case"setup":for(const o of["appPath","deviceType","runtime"])s(o);if(t.seedDir!=null)s("seedDir");s("name",true);s("launchUrl",true);i("warmLaunch");if(!["slim","stock"].includes(t.profile))n("Use slim or stock.");r("metroPort",1,65535,true);break;case"removeTemplate":s("templateId");break;case"grow":case"shrink":case"acquire":s("templateId");r("count",1,64,true);if(e.method==="acquire"){s("purpose");r("ttlMs",1,Number.MAX_VALUE,false,true);r("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);i("launch");i("allowPartial");s("goal",true);s("runId",true);s("kind",true);if(t.labels!==void 0&&(!Array.isArray(t.labels)||t.labels.some(o=>typeof o!=="string")))n("labels must be a list of text.");if(!Number.isInteger(e.client.pid)||e.client.pid<=0)n("The client needs a valid pid.")}break;case"release":if(t.reset!==void 0&&!["fast","full"].includes(t.reset))n("Use fast or full reset.");break;case"action":if(!Array.isArray(t.udids)||t.udids.some(o=>typeof o!=="string"||!o))n("udids must be a list of sim ids.");if(!["reset","rebuild","open-url","screenshot","launch"].includes(t.action))n("Unknown sim action.");s("arg",t.action!=="open-url");break;case"lock":case"unlock":s("name");if(!/^[a-z0-9-]{1,40}$/.test(t.name))n("Lock names are short lowercase words.");r("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);break;case"fairness":if(!Array.isArray(t.windows)||t.windows.length>500||t.windows.some(o=>!o||typeof o.id!=="string"||!Number.isFinite(o.from)||!Number.isFinite(o.to)||o.to<o.from)){n("windows must be a list of { id, from, to } times in ms.")}break;case"endLease":s("leaseId");break;case"report":s("runId");s("udid",true);s("note",true);r("sequence",0,Number.MAX_SAFE_INTEGER,true,true);if(t.state!==void 0&&typeof t.state!=="string")n("state must be text.");break;case"extend":s("runId");r("minutes",1,24*60);break;case"finish":s("runId");s("summary",true);s("outcome",true);break;case"stopRun":s("runId");i("force");break;case"stopAll":i("force");break;case"shutdown":i("ifIdle");break}if(["renew","release","action","lock","unlock","fairness"].includes(e.method))s("leaseId")}var import_node_net,import_node_fs12,import_node_crypto10,WRITE_METHODS;var init_server=__esm({"../sim-farm/src/server.ts"(){"use strict";import_node_net=__toESM(require("net"));import_node_fs12=require("fs");import_node_crypto10=require("crypto");init_engine();init_host();init_store();init_types();init_proc();WRITE_METHODS=new Set(["setup","grow","shrink","acquire","release","action","lock","unlock","park","wake","reset","rebuild","delete","endLease","settings","deleteFarm","show","removeTemplate","report","extend","finish","stopRun","stopAll"])}});function nodeLauncher(e,n=["farmd"]){return()=>{const t=(0,import_node_child_process9.spawn)(process.execPath,[...process.execArgv,e,...n],{detached:true,stdio:"ignore",env:{...process.env}});t.unref()}}async function connectFarm(e){return new FarmClient(e).connect()}var import_node_net2,import_node_fs13,import_node_child_process9,import_node_crypto11,FarmClient;var init_client=__esm({"../sim-farm/src/client.ts"(){"use strict";import_node_net2=__toESM(require("net"));import_node_fs13=require("fs");import_node_child_process9=require("child_process");import_node_crypto11=require("crypto");init_store();init_proc();init_types();FarmClient=class{constructor(e){this.opts=e;this.sock=null;this.nextId=1;this.pending=new Map;this.buf="";this.renewals=new Map;this.reconnecting=null;this.closed=false;this.home=e.home??farmHome2();this.clientIdValue=`c-${(0,import_node_crypto11.randomBytes)(5).toString("hex")}`}get clientId(){return this.owner?.clientId}async connect(){this.token=readOrCreateToken(this.home);this.owner={clientId:this.clientIdValue,pid:process.pid,pidStart:await pidStart(process.pid)??void 0,label:this.opts.label,...this.opts.agent?{agent:this.opts.agent}:{}};if(await this.tryOpen())return this.checkVersion();const e=await readOwner(this.home);if(!await ownerAlive(e)){if(!this.opts.launcher)throw new FarmError("HOST_ERROR","The farm is not running.","Start it from Buoy Desktop or `pnpm farm status`.");this.opts.launcher()}const n=Date.now()+(this.opts.startTimeoutMs??2e4);while(Date.now()<n){await sleep(250);if(await this.tryOpen())return this.checkVersion()}throw new FarmError("HOST_ERROR","The farm did not start in time.",`Check ${paths(this.home).log}.`)}async checkVersion(){const e=await this.send("hello",void 0,{});if(e.protocol<PROTOCOL_VERSION&&this.opts.launcher){const n=await this.send("shutdown",{ifIdle:true},{});if(n.stopping){this.close();await sleep(500);return this.connect()}}return this}tryOpen(){const e=paths(this.home).socket;if(!(0,import_node_fs13.existsSync)(e))return Promise.resolve(false);return new Promise(n=>{const t=import_node_net2.default.createConnection(e);t.once("connect",()=>{this.sock=t;t.on("data",s=>this.onData(s));t.on("close",()=>this.onClose());t.on("error",()=>{});n(true)});t.once("error",()=>n(false))})}onData(e){this.buf+=e.toString("utf8");let n;while((n=this.buf.indexOf("\n"))>=0){const t=this.buf.slice(0,n);this.buf=this.buf.slice(n+1);let s;try{s=JSON.parse(t)}catch{continue}const i=this.pending.get(s.id);if(!i)continue;this.pending.delete(s.id);clearTimeout(i.timer);if(s.ok)i.resolve(s.result);else{const r=s.error??{};i.reject(new FarmError(r.code??"HOST_ERROR",r.message??"farm error",r.next,r.retryable))}}}onClose(){this.sock=null;for(const[,e]of this.pending){clearTimeout(e.timer);e.reject(new FarmError("HOST_ERROR","Lost the connection to the farm.",void 0,true))}this.pending.clear()}reconnect(){if(!this.reconnecting){this.reconnecting=this.connect().then(()=>void 0).finally(()=>{this.reconnecting=null})}return this.reconnecting}async call(e,n,t={}){if(!this.sock&&!this.closed)await this.reconnect();return this.send(e,n,t)}send(e,n,t){if(!this.sock)return Promise.reject(new FarmError("HOST_ERROR","Not connected to the farm.",void 0,true));const s=this.nextId++;const i={id:s,v:PROTOCOL_VERSION,token:this.token,client:this.owner,method:e,params:n,requestId:t.requestId};return new Promise((r,o)=>{const a=setTimeout(()=>{this.pending.delete(s);o(new FarmError("HOST_ERROR",`Farm call ${e} timed out.`,void 0,true))},t.timeoutMs??3e4);this.pending.set(s,{resolve:r,reject:o,timer:a});this.sock.write(JSON.stringify(i)+"\n")})}async acquire(e){const{requestId:n,...t}=e;const s=n??`acq-${(0,import_node_crypto11.randomBytes)(4).toString("hex")}`;const i=await this.call("acquire",t,{timeoutMs:15*6e4+(e.waitMs??0),requestId:s});if(this.renewals.has(i.leaseId))return i;const r=setInterval(()=>{this.call("renew",{leaseId:i.leaseId,epoch:i.epoch}).catch(o=>{if(["LEASE_EXPIRED","STALE_EPOCH","RUN_STOPPED","RUN_ENDED"].includes(o.code))this.stopRenew(i.leaseId)})},15e3);r.unref?.();this.renewals.set(i.leaseId,r);return i}stopRenew(e){const n=this.renewals.get(e);if(n)clearInterval(n);this.renewals.delete(e)}async release(e,n,t="fast",s=false){this.stopRenew(e);return this.call("release",{leaseId:e,epoch:n,reset:t,keepRun:s},{timeoutMs:6e4,requestId:`rel-${e}-${n}`})}close(){this.closed=true;for(const e of[...this.renewals.keys()])this.stopRenew(e);this.sock?.end();this.sock=null}}}});function findRepoRoot(e=process.cwd()){let n=e;for(;;){if((0,import_node_fs14.existsSync)(import_node_path13.default.join(n,"buoy.tools.json")))return n;const t=import_node_path13.default.dirname(n);if(t===n)return null;n=t}}function findExampleBuild(){const e=import_node_path13.default.join(import_node_os9.default.homedir(),"Library/Developer/Xcode/DerivedData");let n=null;try{for(const t of(0,import_node_fs14.readdirSync)(e)){if(!t.startsWith("exampledevbuild-"))continue;const s=import_node_path13.default.join(e,t,"Build/Products/Debug-iphonesimulator/exampledevbuild.app");if(!(0,import_node_fs14.existsSync)(s))continue;const i=(0,import_node_fs14.statSync)(s).mtimeMs;if(!n||i>n.t)n={p:s,t:i}}}catch{}return n?.p??null}function defaultSeedDir(e=findRepoRoot()){return e?import_node_path13.default.join(e,"example-dev-build/artifacts/pool-seed"):null}async function farmDefaults(e=findRepoRoot()){const n=defaultSeedDir(e);let t=[];let s=[];try{const i=JSON.parse(await runOk("xcrun",["simctl","list","runtimes","--json"],{timeoutMs:3e4}));t=(i.runtimes??[]).filter(o=>o.isAvailable!==false&&/^iOS/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}));const r=JSON.parse(await runOk("xcrun",["simctl","list","devicetypes","--json"],{timeoutMs:3e4}));s=(r.devicetypes??[]).filter(o=>/iPhone|iPad/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}))}catch{}return{appPath:findExampleBuild(),seedDir:n&&(0,import_node_fs14.existsSync)(n)?n:null,seedProblem:n?seedProblem(n):"Could not find the repo's seed folder.",runtimes:t,deviceTypes:s,slimTurnsOff:SLIM_CATEGORIES.map(i=>({name:i.name,downside:i.downside}))}}var import_node_path13,import_node_os9,import_node_fs14;var init_discover=__esm({"../sim-farm/src/discover.ts"(){"use strict";import_node_path13=__toESM(require("path"));import_node_os9=__toESM(require("os"));import_node_fs14=require("fs");init_proc();init_seed();init_profiles()}});async function parentOf(e){const n=await run("ps",["-o","ppid=,comm=","-p",String(e)],{timeoutMs:3e3});const t=/^\s*(\d+)\s+(.+?)\s*$/.exec(n.stdout);return n.code===0&&t?{ppid:Number(t[1]),comm:t[2]}:null}async function cwdOf(e){const n=await run("lsof",["-a","-p",String(e),"-d","cwd","-Fn"],{timeoutMs:3e3});const t=n.stdout.split("\n").find(s=>s.startsWith("n"));return t?t.slice(1):null}function fromClientName(e){if(!e)return void 0;return APPS.find(n=>e.toLowerCase().includes(n.app))}async function detectAgent(e={}){const n=e.env??process.env;let t=e.pid??process.pid;let s=null;for(let u=0;u<5&&t>1;u++){const d=await parentOf(t).catch(()=>null);if(!d)break;const h=APPS.find(p=>p.match.test(d.comm));if(u>0&&h){s={app:h,pid:t};break}t=d.ppid}const i=s?.app??fromClientName(e.clientName);const r=(s?await cwdOf(s.pid).catch(()=>null):null)??process.cwd();const o=(await run("git",["-C",r,"rev-parse","--abbrev-ref","HEAD"],{timeoutMs:3e3}).catch(()=>null))?.stdout.trim();const a=n.CLAUDE_CODE_SESSION_ID?.trim();let l;let c="none";if(a&&(i?.app??"claude")==="claude"){l=`claude:${a}`;c="claude-session"}else if(s){l=`${s.app.app}:${s.pid}:${await pidStart(s.pid)??""}`;c="parent-process"}return{app:i?.app??"other",appName:i?.name??(e.clientName||"Agent"),chatKey:l,chatKeySource:c,project:import_node_path14.default.basename(r)||void 0,projectPath:r,branch:o&&o!=="HEAD"?o:void 0,clientName:e.clientName}}function agentLabel(e){return e.project?`${e.appName} \xB7 ${e.project}`:e.appName}var import_node_path14,APPS;var init_agent=__esm({"../sim-farm/src/agent.ts"(){"use strict";import_node_path14=__toESM(require("path"));init_proc();APPS=[{app:"claude",name:"Claude",match:/(^|\/)claude$/i},{app:"codex",name:"Codex",match:/(^|\/)codex$/i},{app:"cursor",name:"Cursor",match:/cursor/i}]}});function parseProbeLine(e){const n=e.match(/BUOY_PROBE physicalMemory=(\d+) cpus=(\d+) footprint=(\d+) available=(\d+)/);if(!n)return null;const[t,s,i,r]=n.slice(1).map(Number);const o=i+r;if(!t||!s||r<=0||o>t)return null;return{ramMB:Math.floor(t/MB),cpus:s,limitMB:Math.floor(o/MB)}}function plistString(e,n){return e.match(new RegExp(`<key>${n}</key>\\s*<string>([^<]*)</string>`))?.[1]}function plistArray(e,n,t){const s=e.match(new RegExp(`<key>${n}</key>\\s*<array>([\\s\\S]*?)</array>`))?.[1]??"";return[...s.matchAll(new RegExp(`<${t}>([\\s\\S]*?)</${t}>`,"g"))].map(i=>i[1].replace(/\s+/g,""))}function parseProvisioningProfile(e,n){const t=n.match(/<key>Entitlements<\/key>\s*<dict>([\s\S]*?)<\/dict>/)?.[1]??"";const s=plistString(t,"application-identifier");const i=plistArray(n,"TeamIdentifier","string")[0];const r=n.match(/<key>ExpirationDate<\/key>\s*<date>([^<]+)<\/date>/)?.[1];if(!s||!i||!r)return null;return{file:e,name:plistString(n,"Name")??import_node_path15.default.basename(e),team:i,appId:s,expires:new Date(r),devices:plistArray(n,"ProvisionedDevices","string"),allDevices:/<key>ProvisionsAllDevices<\/key>\s*<true\/>/.test(n),getTaskAllow:/<key>get-task-allow<\/key>\s*<true\/>/.test(t),certSha1s:plistArray(n,"DeveloperCertificates","data").map(o=>(0,import_node_crypto12.createHash)("sha1").update(Buffer.from(o,"base64")).digest("hex").toUpperCase())}}function pickProfile(e,n,t,s=new Date){const i=e.filter(o=>o.getTaskAllow&&o.expires>s&&(o.allDevices||o.devices.includes(n))&&(o.appId===`${o.team}.*`||o.appId===`${o.team}.${PROBE_ID_SUFFIX}`)&&o.certSha1s.some(a=>t.has(a)));i.sort((o,a)=>Number(a.appId.endsWith(".*"))-Number(o.appId.endsWith(".*"))||a.expires.getTime()-o.expires.getTime());const r=i[0];return r?{profile:r,identity:r.certSha1s.find(o=>t.has(o))}:null}async function localProfiles(){const e=[import_node_path15.default.join(import_node_os10.default.homedir(),"Library/Developer/Xcode/UserData/Provisioning Profiles"),import_node_path15.default.join(import_node_os10.default.homedir(),"Library/MobileDevice/Provisioning Profiles")];const n=[];for(const t of e){let s=[];try{s=(await import_node_fs15.promises.readdir(t)).filter(i=>i.endsWith(".mobileprovision"))}catch{continue}for(const i of s){const r=import_node_path15.default.join(t,i);try{const{stdout:o}=await execFileAsync7("security",["cms","-D","-i",r],{timeout:15e3,maxBuffer:16*MB});const a=parseProvisioningProfile(r,o);if(a)n.push(a)}catch{}}}return n}async function codesignIdentities(){const{stdout:e}=await execFileAsync7("security",["find-identity","-v","-p","codesigning"],{timeout:15e3});return new Set([...e.matchAll(/^\s*\d+\)\s+([0-9A-F]{40})\s/gm)].map(n=>n[1]))}async function physicalPhones2(){const e=await import_node_fs15.promises.mkdtemp(import_node_path15.default.join(import_node_os10.default.tmpdir(),"buoy-phones-"));try{const n=import_node_path15.default.join(e,"devices.json");await execFileAsync7("xcrun",["devicectl","list","devices","--json-output",n],{timeout:3e4});const t=JSON.parse(await import_node_fs15.promises.readFile(n,"utf8"))?.result?.devices??[];return t.filter(s=>s.hardwareProperties?.reality==="physical"&&s.hardwareProperties?.deviceType==="iPhone"&&s.connectionProperties?.pairingState==="paired").map(s=>({udid:s.hardwareProperties.udid,name:s.hardwareProperties.marketingName??"iPhone",productType:s.hardwareProperties.productType,ios:s.deviceProperties?.osVersionNumber??""}))}finally{await import_node_fs15.promises.rm(e,{recursive:true,force:true}).catch(()=>void 0)}}function phoneKey(e,n,t){const s=Object.keys(e).find(r=>e[r].productType===n);if(s)return s;const i=r=>r.toLowerCase().replace(/[()]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");return`${i(t)}-${i(n)}`.slice(0,64).replace(/-$/,"")}async function buildProbe(e,n,t,s){const i=import_node_path15.default.join(e,"PhoneProbe.app");await import_node_fs15.promises.mkdir(i,{recursive:true});const r=import_node_path15.default.join(e,"PhoneProbe.m");await import_node_fs15.promises.writeFile(r,PHONE_PROBE_SOURCE);await execFileAsync7("xcrun",["--sdk","iphoneos","clang","-arch","arm64","-miphoneos-version-min=13.0","-fobjc-arc","-O2","-framework","Foundation","-framework","UIKit",r,"-o",import_node_path15.default.join(i,"PhoneProbe")],{timeout:18e4,maxBuffer:8*MB,env:{...process.env,TMPDIR:e}});const o=PROBE_ID_SUFFIX;await import_node_fs15.promises.writeFile(import_node_path15.default.join(i,"Info.plist"),`<?xml version="1.0" encoding="UTF-8"?>
|
|
421
421
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
422
422
|
<plist version="1.0"><dict>
|
|
423
423
|
<key>CFBundleIdentifier</key><string>${o}</string>
|
|
@@ -486,8 +486,8 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
|
|
|
486
486
|
Tell the user to sign in to Buoy Desktop, or run \`npx buoy login\` in the project. Then run this again.`}var init_account=__esm({"src/access/account.ts"(){"use strict";init_camera()}});var simFarm_exports={};__export(simFarm_exports,{farmFairness:()=>farmFairness,registerSimFarmTools:()=>registerSimFarmTools,runFarmd:()=>runFarmd});function farm(){if(!client2){client2=detectAgent({clientName:clientName()}).catch(()=>void 0).then(e=>connectFarm({label:process.env.BUOY_AGENT_LABEL||(e?agentLabel(e):"Buoy MCP"),agent:e,launcher:nodeLauncher(process.argv[1],["farmd"])})).catch(e=>{client2=null;throw e})}return client2}async function call(e,n,t,s){return(await farm()).call(e,n,{timeoutMs:t,requestId:s})}function forget(e){for(let n=held.length-1;n>=0;n--)if(e(held[n]))held.splice(n,1)}async function farmFairness(e,n){const t=()=>held.find(i=>i.buoyDeviceId&&e.endsWith(i.buoyDeviceId));let s=t();if(!s&&held.length){await learnIds().catch(()=>void 0);s=t()}if(!s)return null;try{return{checked:true,...await call("fairness",{leaseId:s.leaseId,epoch:s.epoch,windows:n},3e4)}}catch(i){if(i instanceof FarmError&&i.code==="BAD_REQUEST"&&/Unknown method/.test(i.message)){return{checked:false,why:"This farm is too old to check fairness. Restart Buoy Desktop when the farm is idle."}}return{checked:false,why:i instanceof FarmError?`${i.code}: ${i.message}`:String(i?.message??i)}}}async function learnIds(){const e=await call("status");const n=new Set(held.map(t=>t.leaseId));for(const t of e.sims){if(!t.leaseId||!n.has(t.leaseId))continue;let s=held.find(l=>l.udid===t.udid);if(!s){const l=held.find(c=>c.leaseId===t.leaseId);s={...l,udid:t.udid,buoyDeviceId:void 0};held.push(s)}if(t.buoyDeviceId){s.buoyDeviceId=t.buoyDeviceId;continue}const i=e.templates.find(l=>l.id===t.templateId)?.bundleId;const r=import_node_path20.default.join(import_node_os14.default.homedir(),"Library/Developer/CoreSimulator/Devices",t.udid,"data");const o=i?await findDataContainer(r,i):null;const a=o?await readBuoyInstallId(o):null;if(a)s.buoyDeviceId=a}}function explain(e){if(e instanceof FarmError&&e.code==="RUN_STOPPED"){return fail3('RUN_STOPPED: The person stopped this run in Buoy Desktop (reason=user). Stop work on it. Tell them: "You stopped this run in Sims." Start a new run only if they ask.')}if(e instanceof FarmError)return fail3(`${e.code}: ${e.message}${e.next?`
|
|
487
487
|
Next: ${e.next}`:""}`);return fail3(`Sim Farm error: ${e?.message??e}`)}async function waitForApps(e,n,t){const s=new Map;const i=Date.now()+t;for(;;){for(const r of n){if(s.has(r))continue;const o=e.connectedDevices().find(a=>a.deviceId.endsWith(r)&&a.tier!=="checking");if(o)s.set(r,o.deviceId)}if(s.size===n.length||Date.now()>=i)return s;await new Promise(r=>setTimeout(r,500))}}function registerSimFarmTools(e,n){clientName=()=>e.server.getClientVersion()?.name;farmApps=n;e.registerTool("sim_farm_status",{description:"Show the Sim Farm: how many farm simulators exist, which are awake or in use and by whom, free memory and disk, how many more sims fit, templates, leases, and recent events. Farm sims are slim iOS simulators (about 1.5 GB each instead of 5) that the farm makes from a template. Apps launch as a real phone (an iPhone SE, 2nd gen, by default): they see its RAM and cores and get its memory warnings. Each sim's `phone` shows the app's peak memory, and `overLimit: true` means a real phone would have closed the app. A safety cap closes an app that keeps growing. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:import_zod3.z.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:t})=>{try{return json2(await call("status",{outside:!!t}))}catch(s){return explain(s)}});e.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and, optionally, a seed folder (signed-in app data); clones are made from it in about 4 s each. Dev-client apps (Expo) need `launchUrl` so they open on Metro instead of the launcher. `grow` / `shrink` change how many sims exist. Use `dryRun` first: it returns the plan, what slim mode turns off (push, StoreKit, app links, widgets, Spotlight\u2026) and whether the Mac has room. You own the pool: Buoy Desktop has no size control. sim_farm_acquire makes sims as needed; `shrink` the ones you no longer need so they stop using disk space.",inputSchema:{mode:import_zod3.z.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size."),templateId:import_zod3.z.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:import_zod3.z.number().int().min(1).max(64).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:import_zod3.z.string().optional().describe("For create: path to a simulator .app build."),seedDir:import_zod3.z.string().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:import_zod3.z.string().optional().describe("For create: URL opened after each launch, e.g. exp+my-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081."),deviceType:import_zod3.z.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:import_zod3.z.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:import_zod3.z.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:import_zod3.z.number().int().min(1).max(65535).optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:import_zod3.z.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId}},async t=>{const s=gate("sim_farm_setup");if(s)return s;try{if(t.mode==="create"){const o={appPath:t.appPath,seedDir:t.seedDir,deviceType:t.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:t.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:t.profile??"slim",metroPort:t.metroPort??8802,...t.launchUrl?{launchUrl:t.launchUrl}:{}};if(!o.appPath)return fail3("create needs appPath.");if(o.seedDir===void 0)return fail3("create needs seedDir, or seedDir: null for an app with no seed.");if(t.dryRun)return json2({wouldCreateTemplate:o,budget:await call("budget",{ramGB:6,diskGB:4})});return json2(await call("setup",o,15*6e4,t.requestId))}let i=t.templateId;if(!i){const o=await call("status");i=o.templates.filter(a=>a.usable).pop()?.id;if(!i)return fail3("No usable template yet. Run sim_farm_setup with mode create first.")}const r=t.count??1;if(t.dryRun){const o=await call("budget",{ramGB:0,diskGB:t.mode==="grow"?2*r:0});return json2({mode:t.mode,templateId:i,count:r,roomForMoreSims:o.roomFor,ok:o.ok,reason:o.reason})}return json2(await call(t.mode,{templateId:i,count:r},30*6e4,t.requestId))}catch(i){return explain(i)}});e.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. The person watches your work live in Buoy Desktop, so give a clear `goal` and a `labels` entry per sim. Returns a run (runId, deadlineAt), a lease (leaseId + epoch) and exact sims (UDID, name, and buoyDeviceId once the app ran: the 8-character tail that its list_devices id ends with; with launch, also deviceId, the full id to pass to other tools). Parked sims are booted and new ones cloned as needed, within the Mac's memory and disk limits. This MCP keeps the lease alive while it runs; release it with sim_farm_finish when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run. While working, send sim_farm_report when a sim starts, passes or fails. The run stops at deadlineAt unless you ask for more time with sim_farm_extend. If any call returns RUN_STOPPED, the person stopped you: stop and tell them.",inputSchema:{count:import_zod3.z.number().int().min(1).max(64).describe("How many sims. There is no fixed cap: the farm gives as many as this Mac's memory and disk allow. Use allowPartial to take what fits."),goal:import_zod3.z.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'. With runId, the run's goal is used if you leave this out."),labels:import_zod3.z.array(import_zod3.z.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:import_zod3.z.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:import_zod3.z.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:import_zod3.z.string().optional().describe("Old name for goal. Use goal."),templateId:import_zod3.z.string().optional().describe("Defaults to the newest usable template."),launch:import_zod3.z.boolean().optional().describe("Launch the app on each sim and wait (up to about 30 s) until it joins Buoy. Each sim then has deviceId for other tools."),allowPartial:import_zod3.z.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:import_zod3.z.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:import_zod3.z.number().int().min(1).max(1440).optional().describe("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId}},async t=>{const s=gate("sim_farm_acquire");if(s)return s;try{let i=t.templateId;if(!i){const u=await call("status");i=u.templates.filter(d=>d.usable).pop()?.id;if(!i)return fail3("No usable template yet. Run sim_farm_setup with mode create first.")}let r=(t.goal??t.purpose??"").trim();if(!r&&t.runId)r=((await call("status")).runs??[]).find(u=>u.id===t.runId)?.goal??"";if(!r)return fail3("Give a goal: one short sentence about what you are doing.");const o=await farm();const a=await o.acquire({templateId:i,count:t.count,purpose:r,goal:r,labels:t.labels,kind:t.kind,runId:t.runId,launch:t.launch,allowPartial:t.allowPartial,waitMs:(t.waitSeconds??0)*1e3,ttlMs:(t.ttlMinutes??15)*6e4,requestId:t.requestId});for(const u of a.sims){const d=held.findIndex(h=>h.udid===u.udid);if(d>=0)held.splice(d,1);held.push({leaseId:a.leaseId,epoch:a.epoch,runId:a.runId,udid:u.udid,buoyDeviceId:u.buoyDeviceId??void 0})}let l=a;if(t.launch&&n){const u=a.sims.map(p=>p.buoyDeviceId).filter(p=>!!p);const d=u.length?await waitForApps(n,u,3e4):new Map;l={...a,sims:a.sims.map(p=>({...p,deviceId:p.buoyDeviceId&&d.get(p.buoyDeviceId)||null}))};const h=a.sims.filter(p=>p.buoyDeviceId&&!d.has(p.buoyDeviceId));if(h.length){l.warning=`The app started on ${h.map(p=>p.name).join(", ")} but has not joined Buoy after 30 s. Run list_devices again in a moment, or take a screenshot with sim_farm_action to see the screen.`}}const c=t.launch?a.sims.filter(u=>!u.buoyDeviceId):[];if(c.length){const u=(await call("status").catch(()=>null))?.templates?.find(h=>h.id===i);const d=u?.metroPort?` on port ${u.metroPort}`:"";return json2({...l,warning:`The app did not report a Buoy id on ${c.map(h=>h.name).join(", ")}. It may still be loading, or Metro${d} may be down or broken. Check Metro${d}, then take a screenshot with sim_farm_action to see the screen.`})}return json2(l)}catch(i){return explain(i)}});e.registerTool("sim_farm_release",{description:"Give one lease's sims back. `fast` (default) stops the app and restores its seeded data in about 2 s; `full` rebuilds each sim from the template. Use `full` after anything changed OS settings (permissions, keychain, location) or after a crash. Releasing your last lease ends the run, unless `keepRun` is true. To end a run with a result, use sim_farm_finish instead.",inputSchema:{...leaseFields,reset:import_zod3.z.enum(["fast","full"]).optional(),keepRun:import_zod3.z.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async t=>{try{const s=await farm();const i=await s.release(t.leaseId,t.epoch,t.reset??"fast",t.keepRun===true);forget(r=>r.leaseId===t.leaseId);return json2(i)}catch(s){return explain(s)}});e.registerTool("sim_farm_action",{description:"Act on sims in your lease: `screenshot` (returns file paths), `open-url` (deep link, pass `url`), `launch` the app, `reset` (restore seeded app data), or `rebuild` (fresh clone; the UDID changes, the result maps old to new).",inputSchema:{...leaseFields,udids:import_zod3.z.array(import_zod3.z.string()).min(1).describe("UDIDs from your lease."),action:import_zod3.z.enum(["screenshot","open-url","launch","reset","rebuild"]),url:import_zod3.z.string().optional().describe("For open-url."),requestId}},async t=>{const s=gate("sim_farm_action");if(s)return s;try{return json2(await call("action",{leaseId:t.leaseId,epoch:t.epoch,udids:t.udids,action:t.action,arg:t.url},10*6e4,t.requestId))}catch(i){return explain(i)}});e.registerTool("sim_farm_lock",{description:"Hold or free a Mac-wide lock tied to your lease. `perf-slot` is for fair speed tests: while you hold it the farm boots, clones and screenshots nothing else, and it records how busy the Mac is every second. It waits only for farm work in flight, not for other runs to end, so other sims and other apps may still use the Mac. run_benchmark_batch on your farm sim then marks each run fair or unfair from that record; run the unfair cases again. Measure only while you hold it. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...leaseFields,name:import_zod3.z.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:import_zod3.z.boolean().describe("true = take it, false = free it."),waitSeconds:import_zod3.z.number().int().min(0).max(1800).optional()}},async t=>{const s=gate("sim_farm_lock");if(s)return s;try{const i={leaseId:t.leaseId,epoch:t.epoch,name:t.name,waitMs:(t.waitSeconds??0)*1e3};return json2(await call(t.hold?"lock":"unlock",i,(t.waitSeconds??0)*1e3+3e4))}catch(i){return explain(i)}});e.registerTool("sim_farm_report",{description:"Tell the person how your run is going. They watch it live in Buoy Desktop. Send one when a sim starts work, passes, fails or needs help, with a short note. `counts` only when you know the full task list. Reports never add time. Pass an increasing `sequence` so a retry is never counted twice.",inputSchema:{runId,udid:import_zod3.z.string().optional().describe("The sim this is about. Leave out for the whole run."),state:import_zod3.z.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:import_zod3.z.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:import_zod3.z.object({done:import_zod3.z.number().int().min(0),total:import_zod3.z.number().int().min(0),passed:import_zod3.z.number().int().min(0).optional(),failed:import_zod3.z.number().int().min(0).optional()}).optional(),resultSource:import_zod3.z.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:import_zod3.z.number().int().min(0).optional()}},async t=>{try{return json2(await call("report",t,3e4))}catch(s){return explain(s)}});e.registerTool("sim_farm_extend",{description:"Ask for more time on your run before deadlineAt. The farm never goes past the run's cap (capAt). It says how much you got.",inputSchema:{runId,minutes:import_zod3.z.number().int().min(1).max(1440).describe("Minutes from now.")}},async t=>{try{return json2(await call("extend",t))}catch(s){return explain(s)}});e.registerTool("sim_farm_finish",{description:"End your run with a result. Gives back every sim it still holds. The person sees the summary in Buoy Desktop, so make it one or two plain sentences, e.g. 'Idea 3 won: 52 \u2192 58 fps.'",inputSchema:{runId,outcome:import_zod3.z.enum(["passed","failed","mixed","unknown"]).optional(),summary:import_zod3.z.string().max(600).optional()}},async t=>{try{const s=await call("finish",t,6e4,`fin-${t.runId}`);forget(i=>i.runId===t.runId);return json2(s)}catch(s){return explain(s)}})}async function runFarmd(){const{startFarmd:e}=await Promise.resolve().then(()=>(init_src(),src_exports));const n=await e({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!n)process.exit(0);for(const t of["SIGTERM","SIGINT"])process.on(t,()=>void n.stop(t))}var import_node_os14,import_node_path20,import_zod3,text2,fail3,json2,farmApps,gate,client2,clientName,held,leaseFields,requestId,runId;var init_simFarm=__esm({"src/tools/packs/simFarm.ts"(){"use strict";import_node_os14=__toESM(require("os"));import_node_path20=__toESM(require("path"));import_zod3=require("zod");init_account();init_src();text2=e=>({content:[{type:"text",text:e}]});fail3=e=>({isError:true,content:[{type:"text",text:e}]});json2=e=>text2(JSON.stringify(e,null,2));gate=e=>hasBuoyAccount(farmApps)?null:fail3(accountRequired(e));client2=null;clientName=()=>void 0;held=[];leaseFields={leaseId:import_zod3.z.string().describe("The lease id from sim_farm_acquire."),epoch:import_zod3.z.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};requestId=import_zod3.z.string().optional().describe("Any unique string for this call. Repeating the same call with the same requestId returns the first result instead of acting twice.");runId=import_zod3.z.string().describe("The runId from sim_farm_acquire.")}});var freeView_exports={};__export(freeView_exports,{freeComparison:()=>freeComparison,freeJsTop:()=>freeJsTop,freeRanking:()=>freeRanking,freeWebBench:()=>freeWebBench,lockedLines:()=>lockedLines,nothingLocked:()=>nothingLocked,resetLockedPitchForTests:()=>resetLockedPitchForTests,withLockedLines:()=>withLockedLines});function addNames(e,n){for(const t of n??[])e.add(t.name)}function freeRanking(e){const n=new Set;const t=new Set;const s=e.rows.map(i=>{addNames(n,i.topRenderers);addNames(t,i.topScripts);return{...i,topRenderers:[],...i.topScripts?{topScripts:[]}:{}}});return{ranking:{...e,rows:s},locked:{...nothingLocked(),renderComponents:n.size,scripts:t.size}}}function freeComparison(e){const n=nothingLocked();const t={...e};const s=t.topScripts;if(s){const r=new Set;addNames(r,s.a);addNames(r,s.b);n.scripts=r.size;delete t.topScripts}const i=t?.renders;if(i&&i.causes){const{causes:r,...o}=i;n.renderCauses=Object.keys(r.a??{}).length+Object.keys(r.b??{}).length>0;t.renders=o}return{comparison:t,locked:n}}function freeWebBench(e){const n=new Set;const t=i=>{addNames(n,i.topScripts);return{...i,topScripts:[]}};const s=e.results.map(i=>({...i,runs:i.runs.map(t),median:i.median?t(i.median):null}));return{result:{...e,results:s},locked:{...nothingLocked(),scripts:n.size}}}function freeJsTop(e){return{snap:{...e,rows:[]},locked:{...nothingLocked(),jsOrigins:e.rows.length}}}function resetLockedPitchForTests(){pitched=false}function lockedLines(e){const n=[];if(e.renderComponents>0){n.push(`Render causes for ${e.renderComponents} component${e.renderComponents===1?"":"s"} (which parts re-render, and why)`)}else if(e.renderCauses){n.push("Why the components rendered (render causes)")}if(e.scripts>0)n.push(`${e.scripts} script${e.scripts===1?"":"s"} named in slow frames`);if(e.jsOrigins>0)n.push(`Which code used the JS thread (${e.jsOrigins} task origin${e.jsOrigins===1?"":"s"})`);if(!n.length)return null;if(pitched)return`\u{1F512} Pro would also show: ${n.join("; ")}.`;pitched=true;return["\u{1F512} More from this run needs Buoy Pro:",...n.map(t=>` \u2022 ${t}`),` Pro shows them. Free 14-day trial: ${TRIAL_URL}`," Tell the user this once, then keep working with the free numbers above. Don't stop or ask again."].join("\n")}function withLockedLines(e,n){const t=lockedLines(n);return t?`${e}
|
|
488
488
|
|
|
489
|
-
${t}`:e}var nothingLocked,TRIAL_URL,pitched;var init_freeView=__esm({"src/access/freeView.ts"(){"use strict";nothingLocked=()=>({renderComponents:0,renderCauses:false,scripts:0,jsOrigins:0});TRIAL_URL="https://buoy.gg/trial";pitched=false}});function pick(e,n,t){return e??n??t}function buildAutomationConfig(e,n={}){const t=[];const s=(e.targetRoute||"").trim();if(!s)throw new Error("targetRoute is required (the screen to benchmark).");let i=(e.bounceRoute||"").trim();if(!i)i=s==="/"?"/_buoy_bounce":"/";if(i===s){throw new Error(`bounceRoute ("${i}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const r=e.cases.map((d,h)=>{const p=(d.name||"").trim()||`case ${h+1}`;return{id:`case-${h}`,name:p,params:d.params??{},route:d.route}});const o=clamp2(Math.round(pick(e.runsPerCase,n.runsPerCase,FALLBACK.runsPerCase)),1,10);const a=clamp2(Math.round(pick(e.coolDownMs,n.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const l=clamp2(Math.round(pick(e.discardWarmupRuns,n.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,o-1);const c=e.shuffleCases??n.shuffleCases??r.length>3;if(r.length>3&&c===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(o===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:s,bounceRoute:i,perCaseDurationMs:clamp2(Math.round(pick(e.perCaseDurationMs,n.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp2(Math.round(pick(e.settleMs,n.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp2(Math.round(pick(e.navTimeoutMs,n.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:c,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:u,warnings:t}}function describeBenchmarkSettings(e){const n=l=>l>=1e3?`${(l/1e3).toFixed(l%1e3?1:0)}s`:`${l}ms`;const t=e.runsPerCase??1;const s=estimateBatchMs(e);const i=s/6e4;const r=i>=1?`~${i.toFixed(1)} min`:`~${Math.round(s/1e3)}s`;const o=[`**${n(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const a=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${o.join(", ")}. That's ~${n(a)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${r}** total.`}function estimateBatchMs(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let s=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)s+=e.cases.length*3e3;return s}function computeFlags(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){n.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){n.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)n.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)n.push("JS thread stalls");return n}function computeWebFlags(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.jsFps!=null&&t&&e.jsFps<t*.95){n.push("Low main-thread frame rate")}else if(e.longFrames!=null&&e.frames>0&&e.longFrames/e.frames>=.02){n.push("Frequent long frames")}const s=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(s>=100)n.push(`Freeze of ${Math.round(s)} ms`);if(e.longTaskPct!=null&&e.longTaskPct>=20)n.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50){n.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`)}if(e.styleLayoutMs!=null&&e.styleLayoutMs>=100){n.push(`Style and layout in slow frames (${Math.round(e.styleLayoutMs)} ms)`)}if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){n.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}if(e.layoutShift!=null&&e.layoutShift>=.1)n.push(`Layout shifts (${e.layoutShift})`);if(e.domGrowth!=null&&e.domGrowth>0){n.push(`DOM grew every run (+${e.domGrowth} nodes): check for a leak`)}return n}function domGrowthAcrossRuns(e){const n=e.filter(s=>!s.batchFailureReason&&!s.discardedWarmup).sort((s,i)=>(s.runIndex??0)-(i.runIndex??0)).map(s=>s.webRun?.domNodesStart).filter(s=>typeof s==="number");if(n.length<3)return null;for(let s=1;s<n.length;s++)if(n[s]<=n[s-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function pickBaseline(e,n){const t=e.find(i=>i.isBaseline);if(t)return t;const s=n?.[0]?.name;if(s){const i=e.filter(r=>r.caseName===s);if(i.length===1)return i[0]}return e[0]}function rankBatchFromIndex(e,n,t,s=[]){const i=e.filter(f=>f.batchId===n);const r=new Map(s.map(f=>[f.index,f.reason]));const o=Array.from(new Set(i.map(f=>f.batchIndex??0))).sort((f,w)=>f-w);const a=f=>{const w=i.filter(k=>(k.batchIndex??0)===f);if(w.length===0)return void 0;const v=w.filter(k=>!k.batchFailureReason&&!k.discardedWarmup);const b=v.filter(k=>k.webRun?.fps!=null);if(b.length>0&&b.length===v.length){const k=[...b].sort(($,_)=>$.webRun.fps-_.webRun.fps);return k[Math.floor((k.length-1)/2)]}if(v.length>0){return v.find(k=>k.isMedianRun===true)??v.find(k=>k.runIndex===0||k.runIndex===void 0)??v[0]}return w.find(k=>k.batchFailureReason)??w[0]};const l=f=>typeof f==="number"?f:null;const c=o.map(f=>{const w=a(f);const v=i.filter(F=>(F.batchIndex??0)===f);const b=v.length;const k=v.filter(F=>!F.batchFailureReason&&!F.discardedWarmup);const $=F=>F.webRun?F.webRun.fps??F.jsFpsAvg:F.uiFpsAvg;const _=k.map($).filter(F=>typeof F==="number");const S=w?.name??t?.[f]?.name??`case ${f+1}`;const E=i.some(F=>(F.batchIndex??0)===f&&F.isBaseline===true);const A=r.get(f)??w?.batchFailureReason;const P=v.every(F=>F.batchFailureReason||F.discardedWarmup);const x=Boolean(A)||!w||P;const T={batchIndex:f,caseName:S,params:w?.params??{},runs:b,failed:x,...w?.route?{route:w.route}:{},...E?{isBaseline:true}:{},failureReason:x?A??(P&&w?"only warmup runs were saved":"no run recorded"):void 0,uiFps:l(w?.uiFpsAvg),jsFps:l(w?.jsFpsAvg),cpu:l(w?.cpuAvg),memMb:l(w?.memMaxMb),jsJank:l(w?.jsJankFrames),uiJank:l(w?.uiJankFrames),deviceMaxRefreshRate:l(w?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:k.length,fpsMin:_.length?round2(Math.min(..._),1):null,fpsMax:_.length?round2(Math.max(..._),1):null,...w?.thermalState?{thermalState:w.thermalState}:{},renderCommits:l(w?.renderCommits),renderWasted:w?.renderWasted,topRenderers:w?.topRenderers??[]};const I=w?.webRun;if(!I)return{...T,flags:x?[]:computeFlags(T)};const O=[...I.longTasksSupported?[]:["long tasks"],...I.heapSupported?[]:["JS heap"],...I.loaf===null?["long animation frames"]:[]];const N={...T,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:I.heapSupported?T.memMb:null,deviceMaxRefreshRate:Math.max(T.deviceMaxRefreshRate??0,I.refreshHz??0)||null,longTaskPct:I.longTaskPct,frameGapP95Ms:I.frameGapP95Ms,frameGapMaxMs:I.frameGapMaxMs,edgeSilenceMs:I.edgeSilenceMs??null,jsFps:I.fps??T.jsFps,longFrames:I.longFrames,...O.length?{missingMetrics:O}:{},loafBlockingMs:I.loaf?.blockingMs??null,styleLayoutMs:I.loaf?.styleLayoutMs??null,forcedLayoutMs:I.loaf?.forcedLayoutMs??null,topScripts:I.loaf?.topScripts??[],worstInteractionMs:I.interactions?.worstMs??null,layoutShift:I.layoutShift??null,domNodes:I.domNodesEnd??null,domGrowth:domGrowthAcrossRuns(v),pageLoad:I.pageLoad??null,refreshCalibrated:I.refreshCalibrated??null};return{...N,flags:x?[]:computeWebFlags({...N,frames:I.frames})}}).filter(f=>f.caseName!==WARMUP_CASE_NAME).map((f,w)=>({...f,batchIndex:w}));const u=pickBaseline(c,t);if(u){u.isBaseline=true;for(const f of c){if(f===u)continue;if(f.uiFps!=null&&u.uiFps!=null)f.deltaUiFps=round2(f.uiFps-u.uiFps,1);if(f.jsFps!=null&&u.jsFps!=null)f.deltaJsFps=round2(f.jsFps-u.jsFps,1)}}const d=c.filter(f=>!f.failed);const h=(f,w)=>{let v;let b=NaN;for(const k of d){const $=f(k);if($==null)continue;if(v===void 0||w($,b)){v=k;b=$}}return v?.caseName};const p=f=>f.platform==="web"?f.longFrames??null:f.jsJank==null&&f.uiJank==null?null:(f.jsJank??0)+(f.uiJank??0);const m={uiFps:h(f=>f.uiFps,(f,w)=>f>w),jsFps:h(f=>f.jsFps,(f,w)=>f>w),lowestCpu:h(f=>f.cpu,(f,w)=>f<w),lowestMem:h(f=>f.memMb,(f,w)=>f<w),fewestJank:h(p,(f,w)=>f<w),fewestLongTasks:h(f=>f.longTaskPct??null,(f,w)=>f<w)};const y=d.filter(f=>f.flags.length>0).map(f=>({caseName:f.caseName,flags:f.flags}));const g=d.filter(f=>f.flags.length===0).sort((f,w)=>(w.uiFps??w.jsFps??0)-(f.uiFps??f.jsFps??0)||(f.cpu??f.longTaskPct??Infinity)-(w.cpu??w.longTaskPct??Infinity)||(f.memMb??Infinity)-(w.memMb??Infinity)).map(f=>f.caseName);return{batchId:n,baselineName:u?.caseName??null,deviceMaxRefreshRate:d.find(f=>f.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:c,leaders:m,flagged:y,balanced:g}}function pageLoadLines(e){const n=e.filter(o=>!o.failed);if(n.length===0)return[];const t=n.map(o=>o.pageLoad?.navigationId);if(t.some(o=>o==null)||new Set(t).size!==t.length)return[];if(n.some(o=>(o.pageLoad?.pageAgeMs??Infinity)>3e4))return[];const s=o=>(o??"").replace(/\/+$/,"")||"/";const i=o=>{const a=o.pageLoad;if(!a?.loadPath||s(a.loadPath)!==s(o.route))return false;const l=new URLSearchParams(a.loadSearch??"");const c=[...l.keys()];if(new Set(c).size!==c.length)return false;const u=Object.entries(o.params);return c.length===u.length&&u.every(([d,h])=>l.get(d)===h)};if(n.some(o=>!i(o)))return[];const r=o=>o==null?"\u2014":String(Math.round(o));return[`**Page load** (each case started on a fresh page load):`,"",`| Case | TTFB ms | FCP ms | LCP ms | DOM ready ms | Script KB | Total KB |`,`|------|--------:|-------:|-------:|-------------:|----------:|---------:|`,...n.map(o=>{const a=o.pageLoad;return`| ${o.caseName} | ${r(a.ttfbMs)} | ${r(a.fcpMs)} | ${r(a.lcpMs)} | ${r(a.domContentLoadedMs)} | ${r(a.scriptKb)} | ${r(a.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function closeCallNote(e){const n=r=>r.platform==="web"?r.jsFps:r.uiFps;const t=e.filter(r=>!r.failed&&n(r)!=null&&r.fpsMin!=null&&r.fpsMax!=null).sort((r,o)=>(n(o)??0)-(n(r)??0));if(t.length<2)return null;const[s,i]=t;if((s.validRuns??0)<2||(i.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${s.caseName} and ${i.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((i.fpsMax??0)>=(s.fpsMin??0)||(n(s)??0)-(n(i)??0)<=2){return`**Too close to call:** ${s.caseName} (${s.fpsMin}\u2013${s.fpsMax} FPS) and ${i.caseName} (${i.fpsMin}\u2013${i.fpsMax} FPS) overlap or sit within 2 FPS. No clear winner between them; more runs may separate them.`}return null}function formatRankingMarkdown(e){const n=(h,p=1)=>h==null?"\u2014":h.toFixed(p);const t=h=>h==null?"":h>0?` (+${h.toFixed(1)})`:` (${h.toFixed(1)})`;const s=[];s.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(h=>h.platform==="web")){formatWebTable(e,s);return s.join("\n")}s.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);s.push("");const i=e.rows.some(h=>h.thermalState);const r=i?" Thermal |":"";const o=i?"-------|":"";s.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${r} Flags |`);s.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${o}-------|`);for(const h of e.rows){const p=i?` ${THERMAL_MARK[h.thermalState??"unknown"]??"\u2014"} |`:"";if(h.failed){s.push(`| ${h.batchIndex} | ${h.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${p} \u274C FAILED (${h.failureReason}) |`);continue}const m=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";s.push(`| ${h.batchIndex} | ${h.caseName} | ${n(h.uiFps)}${t(h.deltaUiFps)} | ${n(h.jsFps)}${t(h.deltaJsFps)} | ${n(h.cpu)} | ${n(h.memMb)} | ${n(h.jsJank,0)} | ${n(h.uiJank,0)} |${p} ${m} |`)}const a=closeCallNote(e.rows);if(a){s.push("");s.push(a)}const l=e.rows.filter(h=>h.thermalState==="serious"||h.thermalState==="critical");if(l.length){s.push("");s.push(`**\u{1F525} Thermally throttled: ${l.map(h=>h.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}s.push("");const c=e.leaders;const u=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(u)s.push(`**Per-metric best:** ${u}`);s.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){s.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(h=>`${h.caseName} (${h.flags.join("; ")})`).join(" \xB7 ")}`)}const d=e.rows.filter(h=>!h.failed&&(h.renderCommits!=null||h.topRenderers.length>0)).map(h=>{const p=h.topRenderers.map(g=>`${g.name} \xD7${g.renders} (${g.totalMs}ms${g.cause?`, ${g.cause}`:""})`).join(", ");const m=h.renderCommits!=null?`${h.renderCommits} commits`:"";const y=h.renderWasted!=null?`${h.renderWasted} wasted`:"";return`- **${h.caseName}** \u2014 ${[m,y,p].filter(Boolean).join(": ")}`});if(d.length){s.push("");s.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");s.push(...d)}s.push("");s.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return s.join("\n")}function compareReportStats(e,n){const t=(s,i)=>({a:{avg:round2(s.avg,1),p95:round2(s.p95,1)},b:{avg:round2(i.avg,1),p95:round2(i.p95,1)},deltaAvg:round2(i.avg-s.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,n.renders)}}function compareWebReports(e,n){const t=(o,a,l)=>({a:o??null,b:a??null,delta:o!=null&&a!=null?round2(a-o,1):null,better:l});const s=o=>o.webRun.heapSupported?round2(o.stats.memoryUsage.max,1):null;const i=e.webRun;const r=n.webRun;return{a:e.name,b:n.name,platform:"web",note:"Browser runs: FPS is the main thread's whole-run requestAnimationFrame rate; long tasks % is not CPU. null = this browser couldn't measure it.",fps:t(i.fps??e.stats.jsFps.avg,r.fps??n.stats.jsFps.avg,"higher"),frameGapP95Ms:t(i.frameGapP95Ms,r.frameGapP95Ms,"lower"),frameGapMaxMs:t(i.frameGapMaxMs,r.frameGapMaxMs,"lower"),longFrames:t(i.longFrames,r.longFrames,"lower"),longTaskPct:t(i.longTaskPct,r.longTaskPct,"lower"),slowFrameBlockingMs:t(i.loaf?.blockingMs,r.loaf?.blockingMs,"lower"),styleLayoutPaintMs:t(i.loaf?.styleLayoutMs,r.loaf?.styleLayoutMs,"lower"),forcedLayoutMs:t(i.loaf?.forcedLayoutMs,r.loaf?.forcedLayoutMs,"lower"),worstInteractionMs:t(i.interactions?.worstMs,r.interactions?.worstMs,"lower"),layoutShift:t(i.layoutShift,r.layoutShift,"lower"),domNodes:t(i.domNodesEnd,r.domNodesEnd,"lower"),jsHeapMb:t(s(e),s(n),"lower"),topScripts:{a:i.loaf?.topScripts??[],b:r.loaf?.topScripts??[]},...compareRenders(e.renders,n.renders)}}function compareRenders(e,n){if(!e&&!n)return null;const t=s=>s??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function round2(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function formatWebTable(e,n){const t=(u,d=1)=>u==null?"\u2014":u.toFixed(d);const s=u=>u==null?"":u>0?` (+${u.toFixed(1)})`:` (${u.toFixed(1)})`;n.push(`**How to read this.** This is a browser run. FPS is the main thread's frame rate (requestAnimationFrame); it can't see frames dropped later by the GPU or compositor. Long tasks % is time the main thread was blocked by tasks over 50 ms, not CPU use. For Chrome's own dropped-frame count, use run_web_benchmark.`);n.push("");n.push(`| # | Case | FPS | p95 frame gap ms | Worst gap ms | Long frames | Long tasks % | JS heap MB | Flags |`);n.push(`|---|------|----:|-----------------:|-------------:|------------:|-------------:|--------:|-------|`);for(const u of e.rows){const d=u.isBaseline?`${u.caseName} (baseline)`:u.caseName;if(u.failed){n.push(`| ${u.batchIndex} | ${d} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${u.failureReason}) |`);continue}const h=u.flags.length?`\u26A0\uFE0F ${u.flags.join("; ")}`:"no flags";n.push(`| ${u.batchIndex} | ${d} | ${t(u.jsFps)}${s(u.deltaJsFps)} | ${t(u.frameGapP95Ms)} | ${t(u.frameGapMaxMs)} | ${t(u.longFrames,0)} | ${t(u.longTaskPct)} | ${t(u.memMb)} | ${h} |`)}const i=e.rows.filter(u=>!u.failed).map(u=>{const d=[u.loafBlockingMs!=null&&u.loafBlockingMs>0&&`slow frames blocked input ${u.loafBlockingMs} ms`,u.styleLayoutMs!=null&&u.styleLayoutMs>0&&`style + layout ${u.styleLayoutMs} ms`,u.forcedLayoutMs!=null&&u.forcedLayoutMs>0&&`forced layout ${u.forcedLayoutMs} ms`,u.worstInteractionMs!=null&&`slowest input ${u.worstInteractionMs} ms`,u.layoutShift!=null&&u.layoutShift>0&&`layout shift ${u.layoutShift}`,u.domNodes!=null&&`${u.domNodes} DOM nodes${u.domGrowth?` (+${u.domGrowth} over the runs)`:""}`,u.topScripts&&u.topScripts.length>0&&`top scripts: ${u.topScripts.slice(0,3).map(h=>`${h.name} ${h.ms} ms`).join(", ")}`].filter(Boolean);return d.length?`- **${u.caseName}**: ${d.join(" \xB7 ")}`:null}).filter(Boolean);if(i.length){n.push("");n.push(`**Why frames were slow** (Long Animation Frames, Event Timing, DOM size; slow frames that ran past a run's end are left out):`);n.push(...i)}const r=pageLoadLines(e.rows);if(r.length){n.push("");n.push(...r)}const o=closeCallNote(e.rows);if(o){n.push("");n.push(o)}const a=Array.from(new Set(e.rows.flatMap(u=>u.missingMetrics??[])));if(a.length){n.push("");n.push(`**Not measured in this browser:** ${a.join(", ")}. These show as \u2014 and can't win or lose.`)}if(e.rows.some(u=>u.refreshCalibrated===false)){n.push("");n.push(`**Refresh rate not checked on an idle screen:** the batch couldn't reach its bounce screen first, so the screen rate (and frame budgets) may read low.`)}if(e.deviceMaxRefreshRate){n.push("");n.push(`Screen rate: ${e.deviceMaxRefreshRate} Hz. A frame gap over ${(1500/e.deviceMaxRefreshRate).toFixed(1)} ms missed at least one frame.`)}n.push("");const l=e.leaders;const c=[l.jsFps&&`FPS \u2192 **${l.jsFps}**`,l.fewestJank&&`fewest long frames \u2192 **${l.fewestJank}**`,l.fewestLongTasks&&`fewest long tasks \u2192 **${l.fewestLongTasks}**`,l.lowestMem&&`lowest heap \u2192 **${l.lowestMem}**`].filter(Boolean).join(" \xB7 ");if(c)n.push(`**Per-metric leaders:** ${c}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(u=>`${u.caseName} (${u.flags.join("; ")})`).join(" \xB7 ")}`)}n.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);n.push("");n.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Confirm finalists on a real phone browser: desktop numbers don't predict mobile GPU, memory or heat._`)}var clamp2,WARMUP_CASE_NAME,THERMAL_MARK,FALLBACK;var init_perfRanking=__esm({"src/tools/packs/perfRanking.ts"(){"use strict";clamp2=(e,n,t)=>Math.max(n,Math.min(t,e));WARMUP_CASE_NAME="__batch_warmup__";THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0}}});async function readDeviceSettings(e,n){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:s,settleMs:i,navTimeoutMs:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,shuffleCases:c,reloadBetweenCases:u,captureRenders:d}=t;return{perCaseDurationMs:s,settleMs:i,navTimeoutMs:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,shuffleCases:c,reloadBetweenCases:u,captureRenders:d}}catch{return{}}}async function readPerfSnapshot(e,n){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,n,{timeoutMs:4e3});return t}function batchIdsIn(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function runCountFor(e,n){return(e??[]).filter(t=>t.batchId===n).length}function newestBatchId(e){let n=null;for(const t of e??[]){if(t.batchId&&(!n||t.createdAt>n.at))n={id:t.batchId,at:t.createdAt}}return n?.id??null}function failuresFromIndex(e,n){const t=new Map;for(const s of e??[]){if(s.batchId===n&&s.batchFailureReason&&!t.has(s.batchIndex??0)){t.set(s.batchIndex??0,s.batchFailureReason)}}return[...t.entries()].map(([s,i])=>({index:s,reason:i}))}function expectedRunsFor(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,n,t,s=SETTLE_WAIT_MS){const i=Date.now();while(Date.now()-i<s){const r=await readPerfSnapshot(e,n).catch(()=>null);if(r&&!isRunnerBusy(r.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(s/1e3)}s`);return false}async function waitForBatchByIndex(e,n,t){const{preBatchIds:s,expectedRuns:i,maxWaitMs:r,log:o}=t;const a=t.reloadsBetweenCases?10:3;const l=Date.now();let c="";let u=false;let d=0;while(Date.now()-l<r){let m;try{m=await readPerfSnapshot(e,n)}catch{await delay2(POLL_INTERVAL_MS);continue}const y=m.index??[];const g=m.live?.automation?.phase??"idle";const f=m.live?.automationCompleted;if(f?.phase==="cancelled"&&!s.has(f.batchId)){return{batchId:f.batchId,failures:[],cancelled:true}}if(f?.phase==="done"&&!s.has(f.batchId)){return{batchId:f.batchId,failures:f.failures??failuresFromIndex(y,f.batchId)}}if(g!=="idle")u=true;if(g!==c){c=g;o(`automation phase: ${g}`)}const w=[...batchIdsIn(y)].find(v=>!s.has(v));if(w){const v=runCountFor(y,w);if(i>0&&v>=i){await waitForRunnerSettled(e,n,o);o(`batch ${w} complete (${v}/${i} runs saved)`);return{batchId:w,failures:failuresFromIndex(y,w)}}const b=(y??[]).filter(S=>S.batchId===w&&S.name!==WARMUP_CASE_NAME).length;const k=m.live?.pendingBatchId===w;const $=Boolean(m.live&&"pendingBatchId"in m.live);const _=!t.reloadsBetweenCases||$;if(g==="idle"&&u&&b>0&&!k&&_){if(++d>=a){o(`batch ${w} settled (${v} runs, automation idle)`);return{batchId:w,failures:failuresFromIndex(y,w)}}}else{d=0}}await delay2(POLL_INTERVAL_MS)}const h=await readPerfSnapshot(e,n).catch(()=>null);const p=h?[...batchIdsIn(h.index)].find(m=>!s.has(m)):void 0;if(p&&h)return{batchId:p,failures:failuresFromIndex(h.index,p),timedOut:true};throw new Error(`Batch did not start within ${Math.round(r/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}async function checkFairness(e,n,t,s){const i=n.filter(y=>y.batchId===t&&!y.discardedWarmup&&y.name!==WARMUP_CASE_NAME&&y.durationMs);if(!i.length)return null;const r=i.map(y=>({id:y.id,from:y.createdAt-y.durationMs,to:y.createdAt}));const o=await farmFairness(e,r);if(!o)return null;if(!o.checked)return{line:`**Fair check:** not done. ${o.why}`,detail:o};const a=new Map(i.map(y=>[y.id,y]));const l=y=>s.find(g=>g.id===y?.caseId)?.name??y?.name??"?";const c=o.runs.filter(y=>!y.fair).map(y=>{const g=a.get(y.id);return{case:l(g),run:(g?.runIndex??0)+1,median:g?.isMedianRun===true,reason:y.reason}});const u=[...new Set(c.map(y=>y.case))];const d=new Map;for(const y of c){const g=y.reason??"No reason given.";d.set(g,[...d.get(g)??[],`${y.case} run ${y.run}`])}const h=[...d].map(([y,g])=>`${y} (${g.join(", ")})`).join(" ");const p=c.length>0&&c.every(y=>/perf-slot/.test(y.reason??""));const m=!c.length?`**Fair check:** all ${o.runs.length} runs were fair. The Mac stayed calm.`:`**Fair check:** ${c.length} of ${o.runs.length} runs were not fair. ${h} Don't rank on these. `+(p?`Take perf-slot with sim_farm_lock, hold it for the whole batch, then run the batch again.`:`Run these cases again with the baseline once the Mac is calm: ${u.join(", ")}.`);return{line:m,detail:{fair:o.fair,unfair:o.unfair,rules:o.rules,unfairRuns:c,redoCases:u,runs:o.runs}}}function registerPerfMonitorTools(e,n){e.registerTool("run_benchmark_batch",{description:"Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",inputSchema:{targetRoute:import_zod4.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod4.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod4.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod4.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod4.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod4.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod4.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod4.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod4.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod4.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod4.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod4.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod4.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."),maxWaitMs:import_zod4.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const s=i=>process.stderr.write(`[buoy-mcp] ${i}
|
|
490
|
-
`);try{const i=n.requireProDevice(t.deviceId);const r=await readDeviceSettings(n,i);const{config:o,warnings:a}=buildAutomationConfig(t,r);const l=estimateBatchMs(o);const c=t.maxWaitMs??l*3+6e4;const u=expectedRunsFor(o);if(!await waitForRunnerSettled(n,i,s)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"refreshIndex",{},i).catch(()=>{});const d=await readPerfSnapshot(n,i);const h=batchIdsIn(d.index);s(`starting batch: ${o.cases.length} cases \xD7 ${o.runsPerCase} runs (~${Math.round(l/1e3)}s estimated) on ${i}`);await n.callAction(PERF_TOOL_ID,"startAutomation",{config:o},i);const p=await waitForBatchByIndex(n,i,{preBatchIds:h,expectedRuns:u,maxWaitMs:c,log:s,reloadsBetweenCases:o.reloadBetweenCases===true});if(p.cancelled){await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});return fail2(`Batch ${p.batchId} was cancelled before completing.`)}await n.callAction(PERF_TOOL_ID,"refreshIndex",{},i).catch(()=>{});const m=await readPerfSnapshot(n,i);const y=rankBatchFromIndex(m.index??[],p.batchId,o.cases,p.failures);const{ranking:g,locked:f}=n.fullResultsFor(i)?{ranking:y,locked:nothingLocked()}:freeRanking(y);await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},i).catch(()=>{});if(g.rows.length===0){const
|
|
489
|
+
${t}`:e}var nothingLocked,TRIAL_URL,pitched;var init_freeView=__esm({"src/access/freeView.ts"(){"use strict";nothingLocked=()=>({renderComponents:0,renderCauses:false,scripts:0,jsOrigins:0});TRIAL_URL="https://buoy.gg/trial";pitched=false}});function pick(e,n,t){return e??n??t}function buildAutomationConfig(e,n={}){const t=[];const s=(e.targetRoute||"").trim();if(!s)throw new Error("targetRoute is required (the screen to benchmark).");let i=(e.bounceRoute||"").trim();if(!i)i=s==="/"?"/_buoy_bounce":"/";if(i===s){throw new Error(`bounceRoute ("${i}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const r=e.cases.map((d,h)=>{const p=(d.name||"").trim()||`case ${h+1}`;return{id:`case-${h}`,name:p,params:d.params??{},route:d.route}});const o=clamp2(Math.round(pick(e.runsPerCase,n.runsPerCase,FALLBACK.runsPerCase)),1,10);const a=clamp2(Math.round(pick(e.coolDownMs,n.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const l=clamp2(Math.round(pick(e.discardWarmupRuns,n.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,o-1);const c=e.shuffleCases??n.shuffleCases??r.length>3;if(r.length>3&&c===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(o===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:s,bounceRoute:i,perCaseDurationMs:clamp2(Math.round(pick(e.perCaseDurationMs,n.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp2(Math.round(pick(e.settleMs,n.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp2(Math.round(pick(e.navTimeoutMs,n.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:c,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:u,warnings:t}}function describeBenchmarkSettings(e){const n=l=>l>=1e3?`${(l/1e3).toFixed(l%1e3?1:0)}s`:`${l}ms`;const t=e.runsPerCase??1;const s=estimateBatchMs(e);const i=s/6e4;const r=i>=1?`~${i.toFixed(1)} min`:`~${Math.round(s/1e3)}s`;const o=[`**${n(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const a=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${o.join(", ")}. That's ~${n(a)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${r}** total.`}function estimateBatchMs(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let s=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)s+=e.cases.length*3e3;return s}function computeFlags(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){n.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){n.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)n.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)n.push("JS thread stalls");return n}function computeWebFlags(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.jsFps!=null&&t&&e.jsFps<t*.95){n.push("Low main-thread frame rate")}else if(e.longFrames!=null&&e.frames>0&&e.longFrames/e.frames>=.02){n.push("Frequent long frames")}const s=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(s>=100)n.push(`Freeze of ${Math.round(s)} ms`);if(e.longTaskPct!=null&&e.longTaskPct>=20)n.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50){n.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`)}if(e.styleLayoutMs!=null&&e.styleLayoutMs>=100){n.push(`Style and layout in slow frames (${Math.round(e.styleLayoutMs)} ms)`)}if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){n.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}if(e.layoutShift!=null&&e.layoutShift>=.1)n.push(`Layout shifts (${e.layoutShift})`);if(e.domGrowth!=null&&e.domGrowth>0){n.push(`DOM grew every run (+${e.domGrowth} nodes): check for a leak`)}return n}function domGrowthAcrossRuns(e){const n=e.filter(s=>!s.batchFailureReason&&!s.discardedWarmup).sort((s,i)=>(s.runIndex??0)-(i.runIndex??0)).map(s=>s.webRun?.domNodesStart).filter(s=>typeof s==="number");if(n.length<3)return null;for(let s=1;s<n.length;s++)if(n[s]<=n[s-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function pickBaseline(e,n){const t=e.find(i=>i.isBaseline);if(t)return t;const s=n?.[0]?.name;if(s){const i=e.filter(r=>r.caseName===s);if(i.length===1)return i[0]}return e[0]}function rankBatchFromIndex(e,n,t,s=[]){const i=e.filter(f=>f.batchId===n);const r=new Map(s.map(f=>[f.index,f.reason]));const o=Array.from(new Set(i.map(f=>f.batchIndex??0))).sort((f,w)=>f-w);const a=f=>{const w=i.filter(_=>(_.batchIndex??0)===f);if(w.length===0)return void 0;const v=w.filter(_=>!_.batchFailureReason&&!_.discardedWarmup);const b=v.filter(_=>_.webRun?.fps!=null);if(b.length>0&&b.length===v.length){const _=[...b].sort(($,k)=>$.webRun.fps-k.webRun.fps);return _[Math.floor((_.length-1)/2)]}if(v.length>0){return v.find(_=>_.isMedianRun===true)??v.find(_=>_.runIndex===0||_.runIndex===void 0)??v[0]}return w.find(_=>_.batchFailureReason)??w[0]};const l=f=>typeof f==="number"?f:null;const c=o.map(f=>{const w=a(f);const v=i.filter(F=>(F.batchIndex??0)===f);const b=v.length;const _=v.filter(F=>!F.batchFailureReason&&!F.discardedWarmup);const $=F=>F.webRun?F.webRun.fps??F.jsFpsAvg:F.uiFpsAvg;const k=_.map($).filter(F=>typeof F==="number");const S=w?.name??t?.[f]?.name??`case ${f+1}`;const E=i.some(F=>(F.batchIndex??0)===f&&F.isBaseline===true);const A=r.get(f)??w?.batchFailureReason;const P=v.every(F=>F.batchFailureReason||F.discardedWarmup);const x=Boolean(A)||!w||P;const T={batchIndex:f,caseName:S,params:w?.params??{},runs:b,failed:x,...w?.route?{route:w.route}:{},...E?{isBaseline:true}:{},failureReason:x?A??(P&&w?"only warmup runs were saved":"no run recorded"):void 0,uiFps:l(w?.uiFpsAvg),jsFps:l(w?.jsFpsAvg),cpu:l(w?.cpuAvg),memMb:l(w?.memMaxMb),jsJank:l(w?.jsJankFrames),uiJank:l(w?.uiJankFrames),deviceMaxRefreshRate:l(w?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:_.length,fpsMin:k.length?round2(Math.min(...k),1):null,fpsMax:k.length?round2(Math.max(...k),1):null,...w?.thermalState?{thermalState:w.thermalState}:{},renderCommits:l(w?.renderCommits),renderWasted:w?.renderWasted,topRenderers:w?.topRenderers??[]};const I=w?.webRun;if(!I)return{...T,flags:x?[]:computeFlags(T)};const O=[...I.longTasksSupported?[]:["long tasks"],...I.heapSupported?[]:["JS heap"],...I.loaf===null?["long animation frames"]:[]];const N={...T,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:I.heapSupported?T.memMb:null,deviceMaxRefreshRate:Math.max(T.deviceMaxRefreshRate??0,I.refreshHz??0)||null,longTaskPct:I.longTaskPct,frameGapP95Ms:I.frameGapP95Ms,frameGapMaxMs:I.frameGapMaxMs,edgeSilenceMs:I.edgeSilenceMs??null,jsFps:I.fps??T.jsFps,longFrames:I.longFrames,...O.length?{missingMetrics:O}:{},loafBlockingMs:I.loaf?.blockingMs??null,styleLayoutMs:I.loaf?.styleLayoutMs??null,forcedLayoutMs:I.loaf?.forcedLayoutMs??null,topScripts:I.loaf?.topScripts??[],worstInteractionMs:I.interactions?.worstMs??null,layoutShift:I.layoutShift??null,domNodes:I.domNodesEnd??null,domGrowth:domGrowthAcrossRuns(v),pageLoad:I.pageLoad??null,refreshCalibrated:I.refreshCalibrated??null};return{...N,flags:x?[]:computeWebFlags({...N,frames:I.frames})}}).filter(f=>f.caseName!==WARMUP_CASE_NAME).map((f,w)=>({...f,batchIndex:w}));const u=pickBaseline(c,t);if(u){u.isBaseline=true;for(const f of c){if(f===u)continue;if(f.uiFps!=null&&u.uiFps!=null)f.deltaUiFps=round2(f.uiFps-u.uiFps,1);if(f.jsFps!=null&&u.jsFps!=null)f.deltaJsFps=round2(f.jsFps-u.jsFps,1)}}const d=c.filter(f=>!f.failed);const h=(f,w)=>{let v;let b=NaN;for(const _ of d){const $=f(_);if($==null)continue;if(v===void 0||w($,b)){v=_;b=$}}return v?.caseName};const p=f=>f.platform==="web"?f.longFrames??null:f.jsJank==null&&f.uiJank==null?null:(f.jsJank??0)+(f.uiJank??0);const m={uiFps:h(f=>f.uiFps,(f,w)=>f>w),jsFps:h(f=>f.jsFps,(f,w)=>f>w),lowestCpu:h(f=>f.cpu,(f,w)=>f<w),lowestMem:h(f=>f.memMb,(f,w)=>f<w),fewestJank:h(p,(f,w)=>f<w),fewestLongTasks:h(f=>f.longTaskPct??null,(f,w)=>f<w)};const y=d.filter(f=>f.flags.length>0).map(f=>({caseName:f.caseName,flags:f.flags}));const g=d.filter(f=>f.flags.length===0).sort((f,w)=>(w.uiFps??w.jsFps??0)-(f.uiFps??f.jsFps??0)||(f.cpu??f.longTaskPct??Infinity)-(w.cpu??w.longTaskPct??Infinity)||(f.memMb??Infinity)-(w.memMb??Infinity)).map(f=>f.caseName);return{batchId:n,baselineName:u?.caseName??null,deviceMaxRefreshRate:d.find(f=>f.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:c,leaders:m,flagged:y,balanced:g}}function pageLoadLines(e){const n=e.filter(o=>!o.failed);if(n.length===0)return[];const t=n.map(o=>o.pageLoad?.navigationId);if(t.some(o=>o==null)||new Set(t).size!==t.length)return[];if(n.some(o=>(o.pageLoad?.pageAgeMs??Infinity)>3e4))return[];const s=o=>(o??"").replace(/\/+$/,"")||"/";const i=o=>{const a=o.pageLoad;if(!a?.loadPath||s(a.loadPath)!==s(o.route))return false;const l=new URLSearchParams(a.loadSearch??"");const c=[...l.keys()];if(new Set(c).size!==c.length)return false;const u=Object.entries(o.params);return c.length===u.length&&u.every(([d,h])=>l.get(d)===h)};if(n.some(o=>!i(o)))return[];const r=o=>o==null?"\u2014":String(Math.round(o));return[`**Page load** (each case started on a fresh page load):`,"",`| Case | TTFB ms | FCP ms | LCP ms | DOM ready ms | Script KB | Total KB |`,`|------|--------:|-------:|-------:|-------------:|----------:|---------:|`,...n.map(o=>{const a=o.pageLoad;return`| ${o.caseName} | ${r(a.ttfbMs)} | ${r(a.fcpMs)} | ${r(a.lcpMs)} | ${r(a.domContentLoadedMs)} | ${r(a.scriptKb)} | ${r(a.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function closeCallNote(e){const n=r=>r.platform==="web"?r.jsFps:r.uiFps;const t=e.filter(r=>!r.failed&&n(r)!=null&&r.fpsMin!=null&&r.fpsMax!=null).sort((r,o)=>(n(o)??0)-(n(r)??0));if(t.length<2)return null;const[s,i]=t;if((s.validRuns??0)<2||(i.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${s.caseName} and ${i.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((i.fpsMax??0)>=(s.fpsMin??0)||(n(s)??0)-(n(i)??0)<=2){return`**Too close to call:** ${s.caseName} (${s.fpsMin}\u2013${s.fpsMax} FPS) and ${i.caseName} (${i.fpsMin}\u2013${i.fpsMax} FPS) overlap or sit within 2 FPS. No clear winner between them; more runs may separate them.`}return null}function formatRankingMarkdown(e){const n=(h,p=1)=>h==null?"\u2014":h.toFixed(p);const t=h=>h==null?"":h>0?` (+${h.toFixed(1)})`:` (${h.toFixed(1)})`;const s=[];s.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(h=>h.platform==="web")){formatWebTable(e,s);return s.join("\n")}s.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);s.push("");const i=e.rows.some(h=>h.thermalState);const r=i?" Thermal |":"";const o=i?"-------|":"";s.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${r} Flags |`);s.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${o}-------|`);for(const h of e.rows){const p=i?` ${THERMAL_MARK[h.thermalState??"unknown"]??"\u2014"} |`:"";if(h.failed){s.push(`| ${h.batchIndex} | ${h.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${p} \u274C FAILED (${h.failureReason}) |`);continue}const m=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";s.push(`| ${h.batchIndex} | ${h.caseName} | ${n(h.uiFps)}${t(h.deltaUiFps)} | ${n(h.jsFps)}${t(h.deltaJsFps)} | ${n(h.cpu)} | ${n(h.memMb)} | ${n(h.jsJank,0)} | ${n(h.uiJank,0)} |${p} ${m} |`)}const a=closeCallNote(e.rows);if(a){s.push("");s.push(a)}const l=e.rows.filter(h=>h.thermalState==="serious"||h.thermalState==="critical");if(l.length){s.push("");s.push(`**\u{1F525} Thermally throttled: ${l.map(h=>h.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}s.push("");const c=e.leaders;const u=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(u)s.push(`**Per-metric best:** ${u}`);s.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){s.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(h=>`${h.caseName} (${h.flags.join("; ")})`).join(" \xB7 ")}`)}const d=e.rows.filter(h=>!h.failed&&(h.renderCommits!=null||h.topRenderers.length>0)).map(h=>{const p=h.topRenderers.map(g=>`${g.name} \xD7${g.renders} (${g.totalMs}ms${g.cause?`, ${g.cause}`:""})`).join(", ");const m=h.renderCommits!=null?`${h.renderCommits} commits`:"";const y=h.renderWasted!=null?`${h.renderWasted} wasted`:"";return`- **${h.caseName}** \u2014 ${[m,y,p].filter(Boolean).join(": ")}`});if(d.length){s.push("");s.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");s.push(...d)}s.push("");s.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return s.join("\n")}function compareReportStats(e,n){const t=(s,i)=>({a:{avg:round2(s.avg,1),p95:round2(s.p95,1)},b:{avg:round2(i.avg,1),p95:round2(i.p95,1)},deltaAvg:round2(i.avg-s.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,n.renders)}}function compareWebReports(e,n){const t=(o,a,l)=>({a:o??null,b:a??null,delta:o!=null&&a!=null?round2(a-o,1):null,better:l});const s=o=>o.webRun.heapSupported?round2(o.stats.memoryUsage.max,1):null;const i=e.webRun;const r=n.webRun;return{a:e.name,b:n.name,platform:"web",note:"Browser runs: FPS is the main thread's whole-run requestAnimationFrame rate; long tasks % is not CPU. null = this browser couldn't measure it.",fps:t(i.fps??e.stats.jsFps.avg,r.fps??n.stats.jsFps.avg,"higher"),frameGapP95Ms:t(i.frameGapP95Ms,r.frameGapP95Ms,"lower"),frameGapMaxMs:t(i.frameGapMaxMs,r.frameGapMaxMs,"lower"),longFrames:t(i.longFrames,r.longFrames,"lower"),longTaskPct:t(i.longTaskPct,r.longTaskPct,"lower"),slowFrameBlockingMs:t(i.loaf?.blockingMs,r.loaf?.blockingMs,"lower"),styleLayoutPaintMs:t(i.loaf?.styleLayoutMs,r.loaf?.styleLayoutMs,"lower"),forcedLayoutMs:t(i.loaf?.forcedLayoutMs,r.loaf?.forcedLayoutMs,"lower"),worstInteractionMs:t(i.interactions?.worstMs,r.interactions?.worstMs,"lower"),layoutShift:t(i.layoutShift,r.layoutShift,"lower"),domNodes:t(i.domNodesEnd,r.domNodesEnd,"lower"),jsHeapMb:t(s(e),s(n),"lower"),topScripts:{a:i.loaf?.topScripts??[],b:r.loaf?.topScripts??[]},...compareRenders(e.renders,n.renders)}}function compareRenders(e,n){if(!e&&!n)return null;const t=s=>s??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function round2(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function formatWebTable(e,n){const t=(u,d=1)=>u==null?"\u2014":u.toFixed(d);const s=u=>u==null?"":u>0?` (+${u.toFixed(1)})`:` (${u.toFixed(1)})`;n.push(`**How to read this.** This is a browser run. FPS is the main thread's frame rate (requestAnimationFrame); it can't see frames dropped later by the GPU or compositor. Long tasks % is time the main thread was blocked by tasks over 50 ms, not CPU use. For Chrome's own dropped-frame count, use run_web_benchmark.`);n.push("");n.push(`| # | Case | FPS | p95 frame gap ms | Worst gap ms | Long frames | Long tasks % | JS heap MB | Flags |`);n.push(`|---|------|----:|-----------------:|-------------:|------------:|-------------:|--------:|-------|`);for(const u of e.rows){const d=u.isBaseline?`${u.caseName} (baseline)`:u.caseName;if(u.failed){n.push(`| ${u.batchIndex} | ${d} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${u.failureReason}) |`);continue}const h=u.flags.length?`\u26A0\uFE0F ${u.flags.join("; ")}`:"no flags";n.push(`| ${u.batchIndex} | ${d} | ${t(u.jsFps)}${s(u.deltaJsFps)} | ${t(u.frameGapP95Ms)} | ${t(u.frameGapMaxMs)} | ${t(u.longFrames,0)} | ${t(u.longTaskPct)} | ${t(u.memMb)} | ${h} |`)}const i=e.rows.filter(u=>!u.failed).map(u=>{const d=[u.loafBlockingMs!=null&&u.loafBlockingMs>0&&`slow frames blocked input ${u.loafBlockingMs} ms`,u.styleLayoutMs!=null&&u.styleLayoutMs>0&&`style + layout ${u.styleLayoutMs} ms`,u.forcedLayoutMs!=null&&u.forcedLayoutMs>0&&`forced layout ${u.forcedLayoutMs} ms`,u.worstInteractionMs!=null&&`slowest input ${u.worstInteractionMs} ms`,u.layoutShift!=null&&u.layoutShift>0&&`layout shift ${u.layoutShift}`,u.domNodes!=null&&`${u.domNodes} DOM nodes${u.domGrowth?` (+${u.domGrowth} over the runs)`:""}`,u.topScripts&&u.topScripts.length>0&&`top scripts: ${u.topScripts.slice(0,3).map(h=>`${h.name} ${h.ms} ms`).join(", ")}`].filter(Boolean);return d.length?`- **${u.caseName}**: ${d.join(" \xB7 ")}`:null}).filter(Boolean);if(i.length){n.push("");n.push(`**Why frames were slow** (Long Animation Frames, Event Timing, DOM size; slow frames that ran past a run's end are left out):`);n.push(...i)}const r=pageLoadLines(e.rows);if(r.length){n.push("");n.push(...r)}const o=closeCallNote(e.rows);if(o){n.push("");n.push(o)}const a=Array.from(new Set(e.rows.flatMap(u=>u.missingMetrics??[])));if(a.length){n.push("");n.push(`**Not measured in this browser:** ${a.join(", ")}. These show as \u2014 and can't win or lose.`)}if(e.rows.some(u=>u.refreshCalibrated===false)){n.push("");n.push(`**Refresh rate not checked on an idle screen:** the batch couldn't reach its bounce screen first, so the screen rate (and frame budgets) may read low.`)}if(e.deviceMaxRefreshRate){n.push("");n.push(`Screen rate: ${e.deviceMaxRefreshRate} Hz. A frame gap over ${(1500/e.deviceMaxRefreshRate).toFixed(1)} ms missed at least one frame.`)}n.push("");const l=e.leaders;const c=[l.jsFps&&`FPS \u2192 **${l.jsFps}**`,l.fewestJank&&`fewest long frames \u2192 **${l.fewestJank}**`,l.fewestLongTasks&&`fewest long tasks \u2192 **${l.fewestLongTasks}**`,l.lowestMem&&`lowest heap \u2192 **${l.lowestMem}**`].filter(Boolean).join(" \xB7 ");if(c)n.push(`**Per-metric leaders:** ${c}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(u=>`${u.caseName} (${u.flags.join("; ")})`).join(" \xB7 ")}`)}n.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);n.push("");n.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Confirm finalists on a real phone browser: desktop numbers don't predict mobile GPU, memory or heat._`)}var clamp2,WARMUP_CASE_NAME,THERMAL_MARK,FALLBACK;var init_perfRanking=__esm({"src/tools/packs/perfRanking.ts"(){"use strict";clamp2=(e,n,t)=>Math.max(n,Math.min(t,e));WARMUP_CASE_NAME="__batch_warmup__";THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0}}});async function readDeviceSettings(e,n){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:s,settleMs:i,navTimeoutMs:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,shuffleCases:c,reloadBetweenCases:u,captureRenders:d}=t;return{perCaseDurationMs:s,settleMs:i,navTimeoutMs:r,runsPerCase:o,coolDownMs:a,discardWarmupRuns:l,shuffleCases:c,reloadBetweenCases:u,captureRenders:d}}catch{return{}}}async function readPerfSnapshot(e,n){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,n,{timeoutMs:4e3});return t}function batchIdsIn(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function runCountFor(e,n){return(e??[]).filter(t=>t.batchId===n).length}function newestBatchId(e){let n=null;for(const t of e??[]){if(t.batchId&&(!n||t.createdAt>n.at))n={id:t.batchId,at:t.createdAt}}return n?.id??null}function failuresFromIndex(e,n){const t=new Map;for(const s of e??[]){if(s.batchId===n&&s.batchFailureReason&&!t.has(s.batchIndex??0)){t.set(s.batchIndex??0,s.batchFailureReason)}}return[...t.entries()].map(([s,i])=>({index:s,reason:i}))}function expectedRunsFor(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,n,t,s=SETTLE_WAIT_MS){const i=Date.now();while(Date.now()-i<s){const r=await readPerfSnapshot(e,n).catch(()=>null);if(r&&!isRunnerBusy(r.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(s/1e3)}s`);return false}async function waitForBatchByIndex(e,n,t){const{preBatchIds:s,expectedRuns:i,maxWaitMs:r,log:o}=t;const a=t.reloadsBetweenCases?10:3;const l=Date.now();let c="";let u=false;let d=0;while(Date.now()-l<r){let m;try{m=await readPerfSnapshot(e,n)}catch{await delay2(POLL_INTERVAL_MS);continue}const y=m.index??[];const g=m.live?.automation?.phase??"idle";const f=m.live?.automationCompleted;if(f?.phase==="cancelled"&&!s.has(f.batchId)){return{batchId:f.batchId,failures:[],cancelled:true}}if(f?.phase==="done"&&!s.has(f.batchId)){return{batchId:f.batchId,failures:f.failures??failuresFromIndex(y,f.batchId)}}if(g!=="idle")u=true;if(g!==c){c=g;o(`automation phase: ${g}`)}const w=[...batchIdsIn(y)].find(v=>!s.has(v));if(w){const v=runCountFor(y,w);if(i>0&&v>=i){await waitForRunnerSettled(e,n,o);o(`batch ${w} complete (${v}/${i} runs saved)`);return{batchId:w,failures:failuresFromIndex(y,w)}}const b=(y??[]).filter(S=>S.batchId===w&&S.name!==WARMUP_CASE_NAME).length;const _=m.live?.pendingBatchId===w;const $=Boolean(m.live&&"pendingBatchId"in m.live);const k=!t.reloadsBetweenCases||$;if(g==="idle"&&u&&b>0&&!_&&k){if(++d>=a){o(`batch ${w} settled (${v} runs, automation idle)`);return{batchId:w,failures:failuresFromIndex(y,w)}}}else{d=0}}await delay2(POLL_INTERVAL_MS)}const h=await readPerfSnapshot(e,n).catch(()=>null);const p=h?[...batchIdsIn(h.index)].find(m=>!s.has(m)):void 0;if(p&&h)return{batchId:p,failures:failuresFromIndex(h.index,p),timedOut:true};throw new Error(`Batch did not start within ${Math.round(r/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}async function checkFairness(e,n,t,s){const i=n.filter(y=>y.batchId===t&&!y.discardedWarmup&&y.name!==WARMUP_CASE_NAME&&y.durationMs);if(!i.length)return null;const r=i.map(y=>({id:y.id,from:y.createdAt-y.durationMs,to:y.createdAt}));const o=await farmFairness(e,r);if(!o)return null;if(!o.checked)return{line:`**Fair check:** not done. ${o.why}`,detail:o};const a=new Map(i.map(y=>[y.id,y]));const l=y=>s.find(g=>g.id===y?.caseId)?.name??y?.name??"?";const c=o.runs.filter(y=>!y.fair).map(y=>{const g=a.get(y.id);return{case:l(g),run:(g?.runIndex??0)+1,median:g?.isMedianRun===true,reason:y.reason}});const u=[...new Set(c.map(y=>y.case))];const d=new Map;for(const y of c){const g=y.reason??"No reason given.";d.set(g,[...d.get(g)??[],`${y.case} run ${y.run}`])}const h=[...d].map(([y,g])=>`${y} (${g.join(", ")})`).join(" ");const p=c.length>0&&c.every(y=>/perf-slot/.test(y.reason??""));const m=!c.length?`**Fair check:** all ${o.runs.length} runs were fair. The Mac stayed calm.`:`**Fair check:** ${c.length} of ${o.runs.length} runs were not fair. ${h} Don't rank on these. `+(p?`Take perf-slot with sim_farm_lock, hold it for the whole batch, then run the batch again.`:`Run these cases again with the baseline once the Mac is calm: ${u.join(", ")}.`);return{line:m,detail:{fair:o.fair,unfair:o.unfair,rules:o.rules,unfairRuns:c,redoCases:u,runs:o.runs}}}function registerPerfMonitorTools(e,n){e.registerTool("run_benchmark_batch",{description:"Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",inputSchema:{targetRoute:import_zod4.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod4.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod4.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod4.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod4.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod4.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod4.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod4.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod4.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod4.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod4.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod4.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod4.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."),maxWaitMs:import_zod4.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const s=i=>process.stderr.write(`[buoy-mcp] ${i}
|
|
490
|
+
`);try{const i=n.requireProDevice(t.deviceId);const r=await readDeviceSettings(n,i);const{config:o,warnings:a}=buildAutomationConfig(t,r);const l=estimateBatchMs(o);const c=t.maxWaitMs??l*3+6e4;const u=expectedRunsFor(o);if(!await waitForRunnerSettled(n,i,s)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"refreshIndex",{},i).catch(()=>{});const d=await readPerfSnapshot(n,i);const h=batchIdsIn(d.index);s(`starting batch: ${o.cases.length} cases \xD7 ${o.runsPerCase} runs (~${Math.round(l/1e3)}s estimated) on ${i}`);await n.callAction(PERF_TOOL_ID,"startAutomation",{config:o},i);const p=await waitForBatchByIndex(n,i,{preBatchIds:h,expectedRuns:u,maxWaitMs:c,log:s,reloadsBetweenCases:o.reloadBetweenCases===true});if(p.cancelled){await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});return fail2(`Batch ${p.batchId} was cancelled before completing.`)}await n.callAction(PERF_TOOL_ID,"refreshIndex",{},i).catch(()=>{});const m=await readPerfSnapshot(n,i);const y=rankBatchFromIndex(m.index??[],p.batchId,o.cases,p.failures);const{ranking:g,locked:f}=n.fullResultsFor(i)?{ranking:y,locked:nothingLocked()}:freeRanking(y);await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},i).catch(()=>{});if(g.rows.length===0){const _=n.connectedDevices().some(S=>S.deviceId===i);const $=m.live?.pendingBatchId===p.batchId;const k=!_?`The device disconnected during the batch (a browser tab that closes, reloads or sleeps drops off). Reconnect it and run again.`:$?`The batch reloaded the app between cases and never resumed. Check that <AutomationResumer/> is mounted (the web host mounts it), or pass reloadBetweenCases: false.`:p.timedOut?`No runs were saved before the wait cap (${Math.round(c/1e3)}s). On web, a hidden or background tab pauses the run: keep the tab in front. Check get_automation_status for the device's phase.`:`The batch finished without saving any runs. Check get_automation_status and the device console.`;return fail2(`Benchmark batch ${p.batchId||"(no id)"} saved no runs. ${k}`)}const w=await checkFairness(i,m.index??[],p.batchId,o.cases);const v=w?.detail&&"unfairRuns"in w.detail&&w.detail.unfairRuns.length?`${w.line}
|
|
491
491
|
|
|
492
492
|
${formatRankingMarkdown(g)}`:formatRankingMarkdown(g)+(w?`
|
|
493
493
|
|
|
@@ -522,8 +522,8 @@ _${e} more withheld ${e===1?"body was":"bodies were"} not fetched (cap ${MAX_BOD
|
|
|
522
522
|
\u2514\u2500 response: ${s(e.responseData,t?.responseData,e.responseBodyOmitted)}`}function formatRow(e,n,t){const s=n.path||n.url;const i=[`${n.duration!==void 0?`${Math.round(n.duration)}ms`:"\u2014"}`,formatBytes(n.responseSize),n.host??"",n.requestClient??""].filter(Boolean);const r=n.operationName?` (${n.operationName})`:"";return`${e}. \`${n.id}\` ${statusIcon(n)} ${statusLabel(n)} ${n.method} ${s}${r}${t?` ${t}`:""}
|
|
523
523
|
\u2514\u2500 ${i.join(" \xB7 ")}`}function registerNetworkTools(e,n){e.registerTool("network_conditions",{description:"Read or set the device's network conditions. Offline blocks new intercepted HTTP requests; slow adds 500 ms and verySlow adds 2000 ms before dispatch. Normal clears conditions. State resets on JS reload. Check app and server observations to verify the effect.",inputSchema:{action:import_zod6.z.enum(["get","set"]).describe("Read the effective profile or select one."),profile:import_zod6.z.enum(["normal","offline","slow","verySlow"]).optional().describe("Required for set."),deviceId:deviceIdInput}},async t=>{try{if(t.action==="set"&&!t.profile)return fail2("Set requires a profile.");const s=n.resolveDevice(t.deviceId);const i=await n.callAction(NETWORK_TOOL_ID,t.action==="get"?"getNetworkConditions":"setNetworkConditions",t.action==="set"?{profile:t.profile}:void 0,s);if(!i||typeof i!=="object"||!("profile"in i))return fail2("The device did not report its effective network conditions.");return text(JSON.stringify(i))}catch(s){return fail2(errMsg2(s))}});e.registerTool("get_network_requests",{description:"List captured HTTP requests with IDs, available status/timing/size fields and pinned or saved markers. Capture depends on supported transports, installed hooks and recording timing; requests made before capture starts may be absent. Use IDs for network actions and flagged to inspect retained records. A flag does not establish who created it or the cause of a problem. Filter by status or URL pattern. includeBodies:true requests available truncated bodies, which can be missing or withheld. Needs the network integration. The list works on the free tier; includeBodies needs Buoy Pro.",inputSchema:{limit:import_zod6.z.number().optional().describe("Most-recent N requests. Default 25."),pattern:import_zod6.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),status:import_zod6.z.enum(["all","errors","success","pending"]).optional().describe("Filter by outcome. Default all."),flagged:import_zod6.z.enum(["pinned","saved","any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These are separate from the live list; restart retention depends on successful persistence."),includeBodies:import_zod6.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),deviceId:deviceIdInput}},async t=>{try{const s=n.requireProDevice(t.deviceId);const{payload:i,stale:r}=await n.getSnapshot(NETWORK_TOOL_ID,s);const{events:o,saved:a}=parseSnapshot(i);const l=typeof t.limit==="number"&&t.limit>0?t.limit:25;const c=t.pattern?.toLowerCase();const u=new Set;const d=new Set;for(const g of a){if(g.pinned)u.add(g.event?.id);if(g.saved)d.add(g.event?.id);if(g.pinned)u.add(g.liveId);if(g.saved)d.add(g.liveId)}if(t.flagged){const g=a.filter(b=>t.flagged==="pinned"?b.pinned:t.flagged==="saved"?b.saved:b.pinned||b.saved);const f=g.filter(b=>b.event&&matchesStatus(b.event,t.status)).filter(b=>!c||(b.event.url??"").toLowerCase().includes(c)).slice(0,l);if(f.length===0){return text(staleHeader(r)+`No ${t.flagged==="any"?"pinned or saved":t.flagged} requests on ${s}.
|
|
524
524
|
|
|
525
|
-
The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`)}const w=t.includeBodies?await resolveBodies(n,f.map(b=>b.event),s):{bodies:new Map,skipped:0};const v=f.map((b,
|
|
526
|
-
\u2514\u2500 key \`${b.key}\`${
|
|
525
|
+
The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`)}const w=t.includeBodies?await resolveBodies(n,f.map(b=>b.event),s):{bodies:new Map,skipped:0};const v=f.map((b,_)=>{const $=[b.pinned?"\u{1F4CC} pinned":"",b.saved?"\u{1F516} saved":""].filter(Boolean).join(" ");const k=b.event.id?.startsWith("saved:")?" \xB7 from an earlier app run":"";const S=b.bodyTruncated?" \xB7 body truncated":"";let E=formatRow(_+1,b.event,$);E+=`
|
|
526
|
+
\u2514\u2500 key \`${b.key}\`${k}${S}`;if(t.includeBodies)E+=bodyLines(b.event,w.bodies);return E}).join("\n");return text(staleHeader(r)+`**Kept requests on ${s}** \u2014 ${f.length} of ${g.length}
|
|
527
527
|
|
|
528
528
|
${v}
|
|
529
529
|
|
|
@@ -580,7 +580,7 @@ ${JSON.stringify(i.state,null,2)}
|
|
|
580
580
|
_Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${o}`)}catch(s){return fail2(errMsg2(s))}});e.registerTool("redux_dispatch",{description:"Dispatch a Redux action to the app's store (e.g. to trigger a state change while debugging). Provide the action type and optional payload \u2014 they're sent as { type, payload }.",inputSchema:{type:import_zod10.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),payload:import_zod10.z.unknown().optional().describe("Optional action payload (any JSON)."),deviceId:deviceIdInput}},async t=>{try{const s=n.requireProDevice(t.deviceId);const i={type:t.type};if(t.payload!==void 0)i.payload=t.payload;const r=await n.callAction(REDUX_TOOL_ID,"dispatch",{action:i},s);if(!r||!r.dispatched)return fail2(noStoreMessage(r?.reason));return text(`Dispatched \`${t.type}\`${t.payload!==void 0?` with payload ${JSON.stringify(t.payload)}`:""}.`)}catch(s){return fail2(errMsg2(s))}})}var import_zod10,REDUX_TOOL_ID,TOP_LEVEL_ONLY_NOTE;var init_redux=__esm({"src/tools/packs/redux.ts"(){"use strict";import_zod10=require("zod");init_packKit();REDUX_TOOL_ID="redux";TOP_LEVEL_ONLY_NOTE="\n\n_Note: capture is top-level-only on this device (thunk-internal / RTK Query actions aren't recorded). Put `import '@buoy-gg/redux';` first in the app entry, or add `buoyReduxMiddleware`, for full capture._"}});function truncate2(e,n=200){if(e===null||e===void 0)return"(null)";const t=typeof e==="string"?e:JSON.stringify(e);return t.length>n?`${t.slice(0,n)}\u2026(+${t.length-n} chars)`:t}function registerStorageTools(e,n){e.registerTool("get_storage",{description:"Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Data wrong or missing after an app restart \u2192 read the persisted values here FIRST \u2014 the bug is usually a stale, missing, or malformed stored value, and reading it beats hunting for it in code. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out.",inputSchema:{pattern:import_zod11.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),includeValues:import_zod11.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),limit:import_zod11.z.number().optional().describe("Max keys per backend section. Default 100."),deviceId:deviceIdInput}},async t=>{try{const s=n.requireProDevice(t.deviceId);const i=t.pattern?.toLowerCase();const r=typeof t.limit==="number"&&t.limit>0?t.limit:100;const o=m=>!i||m.toLowerCase().includes(i);const a=[];const l=await n.callAction(STORAGE_TOOL_ID,"async.getAllKeys",{},s);if(!Array.isArray(l)){return fail2("No storage result from the device. The storage tool may be inactive.")}const c=l.filter(o).slice(0,r);if(c.length>0){let m;if(t.includeValues){const y=await n.callAction(STORAGE_TOOL_ID,"async.multiGet",{keys:c},s);const g=new Map((y??[]).map(([f,w])=>[f,w]));m=c.map(f=>`- \`${f}\` = ${truncate2(g.get(f)??null)}`)}else{m=c.map(y=>`- \`${y}\``)}a.push(`**AsyncStorage (${c.length}/${l.length} keys):**
|
|
581
581
|
${m.join("\n")}`)}const u=await n.callAction(STORAGE_TOOL_ID,"mmkv.snapshot",{},s).catch(()=>null);for(const m of u??[]){const y=m.entries.filter(w=>o(w.key)).slice(0,r);if(y.length===0)continue;const g=y.map(w=>t.includeValues?`- \`${w.key}\` (${w.valueType}) = ${truncate2(w.value)}`:`- \`${w.key}\``);const f=[m.encrypted&&"encrypted",m.readOnly&&"read-only"].filter(Boolean).join(", ");a.push(`**MMKV \`${m.id}\`${f?` (${f})`:""} (${y.length}/${m.entries.length} keys):**
|
|
582
582
|
${g.join("\n")}`)}const d=await n.callAction(STORAGE_TOOL_ID,"secure.keys",{},s).catch(()=>null);const h=(d??[]).filter(m=>o(m.key)).slice(0,r);if(h.length>0){const m=await Promise.all(h.map(async y=>{if(y.requireAuthentication)return`- \`${y.key}\` \u{1F512} (biometric-protected \u2014 not read)`;if(!t.includeValues)return`- \`${y.key}\``;const g=await n.callAction(STORAGE_TOOL_ID,"secure.get",{key:y.key},s).catch(()=>null);return`- \`${y.key}\` = ${truncate2(g)}`}));a.push(`**SecureStore (${h.length} registered keys):**
|
|
583
|
-
${m.join("\n")}`)}if(a.length===0){return text(`No storage keys${i?` matching "${t.pattern}"`:""} in any backend.`)}const p=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return text(`${a.join("\n\n")}${p}`)}catch(s){return fail2(errMsg2(s))}});e.registerTool("storage_action",{description:"Modify the app's AsyncStorage: set a key, remove a key, or clear supported app AsyncStorage keys (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",inputSchema:{action:import_zod11.z.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:import_zod11.z.string().optional().describe("The key (required for set and remove)."),value:import_zod11.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:deviceIdInput}},async t=>{try{const s=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(STORAGE_TOOL_ID,"clearAppStorage",{},s);return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return fail2(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(STORAGE_TOOL_ID,"async.removeItem",{key:t.key},s);return text(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return fail2("'set' needs a value (a string).")}await n.callAction(STORAGE_TOOL_ID,"async.setItem",{key:t.key,value:t.value},s);return text(`Requested update of storage key "${t.key}".`)}catch(s){return fail2(errMsg2(s))}})}var import_zod11,STORAGE_TOOL_ID;var init_storage=__esm({"src/tools/packs/storage.ts"(){"use strict";import_zod11=require("zod");init_packKit();STORAGE_TOOL_ID="storage"}});function valueKey(e){if(typeof e==="number"){if(!Number.isFinite(e)||Number.isInteger(e)&&Math.abs(e)<10)return null;return`n:${e}`}if(typeof e!=="string")return null;const n=e.trim();if(n.length<3||n.length>80)return null;const t=/^[$€£]?-?[\d,]+(\.\d+)?$/.test(n)?Number(n.replace(/[$€£,]/g,"")):NaN;if(Number.isFinite(t)&&/[\d]/.test(n))return valueKey(t);return`s:${n}`}function walk(e,n,t,s){if(s.length>=MAX_LEAVES||t>MAX_DEPTH||e==null)return;if(typeof e==="object"){const r=Array.isArray(e)?e.slice(0,50).map((o,a)=>[String(a),o]):Object.entries(e);for(const[o,a]of r)walk(a,n?`${n}.${o}`:o,t+1,s);return}const i=valueKey(e);if(i)s.push([i,n])}function indexState(e,n,t){const s=[];walk(t,"",0,s);for(const[i,r]of s){const o=r?`${n}, ${r}`:n;const a=e.get(i)??[];if(a.length<3)a.push(o);e.set(i,a)}}async function liveStateIndex(e,n){const t=new Set(e.connectedDevices().find(o=>o.deviceId===n)?.tools.map(o=>o.toolId)??[]);const s={timeoutMs:1500,silent:true};const i=[];const r=new Map;if(t.has("zustand")){i.push(e.callAction("zustand","listStores",{includeValues:true},n,s).then(o=>{for(const a of o?.stores??[])indexState(r,`${a.name} store`,a.currentState)}))}if(t.has("redux")){i.push(e.callAction("redux","getState",{includeValues:true},n,s).then(o=>{const a=o;if(a?.available)indexState(r,"redux state",a.state)}))}if(t.has("jotai")){i.push(e.callAction("jotai","listAtoms",{includeValues:true},n,s).then(o=>{for(const a of o?.atoms??[])indexState(r,`atom ${a.label}`,a.currentValue)}))}if(!i.length)return null;await Promise.all(i.map(o=>o.catch(()=>void 0)));return r.size?r:null}function stateNote(e,n){if(!e||!n)return"";const t=valueKey(n);const s=t?e.get(t):void 0;return s?.length?` = live state: ${s.join("; ")}`:""}var MAX_LEAVES,MAX_DEPTH;var init_liveValues=__esm({"src/tools/packs/liveValues.ts"(){"use strict";MAX_LEAVES=4e3;MAX_DEPTH=6}});function pixelmatch(e,n,t,s,i,r={}){const{threshold:o=.1,alpha:a=.1,aaColor:l=[255,255,0],diffColor:c=[255,0,0],checkerboard:u=true,includeAA:d,diffColorAlt:h,diffMask:p}=r;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!==s*i*4)throw new Error(`Image data size does not match width/height. Expecting ${s*i*4}. Got ${e.length}`);const m=s*i;const y=new Uint32Array(e.buffer,e.byteOffset,m);const g=new Uint32Array(n.buffer,n.byteOffset,m);let f=true;for(let T=0;T<m;T++){if(y[T]!==g[T]){f=false;break}}if(f){if(t&&!p){for(let T=0,I=0;T<m;T++,I+=4)drawGrayPixel(e,I,a,t)}return 0}const w=35215*o*o;const[v,b,k]=l;const[$,_,S]=c;const[E,A,P]=h||c;let x=0;for(let T=0,I=0;T<m;T++,I+=4){const O=y[T]===g[T]?0:colorDelta(e,n,I,I,u);if(Math.abs(O)>w){const N=T%s;const F=T/s|0;const X=!d&&(antialiased(e,N,F,s,i,y,g,u)||antialiased(n,N,F,s,i,g,y,u));if(X){if(t&&!p)drawPixel(t,I,v,b,k)}else{if(t){if(O<0){drawPixel(t,I,E,A,P)}else{drawPixel(t,I,$,_,S)}}x++}}else if(t&&!p){drawGrayPixel(e,I,a,t)}}return x}function isPixelData(e){return ArrayBuffer.isView(e)&&e.BYTES_PER_ELEMENT===1}function antialiased(e,n,t,s,i,r,o,a){const l=Math.max(n-1,0);const c=Math.max(t-1,0);const u=Math.min(n+1,s-1);const d=Math.min(t+1,i-1);const h=(t*s+n)*4;const p=e[h];const m=e[h+1];const y=e[h+2];const g=e[h+3];let f=n===l||n===u||t===c||t===d?1:0;let w=0;let v=0;let b=0;let k=0;let $=0;let _=0;for(let S=l;S<=u;S++){for(let E=c;E<=d;E++){if(S===n&&E===t)continue;const A=brightnessDelta(e,h,(E*s+S)*4,p,m,y,g,a);if(A===0){f++;if(f>2)return false}else if(A<w){w=A;b=S;k=E}else if(A>v){v=A;$=S;_=E}}}if(w===0||v===0)return false;return hasManySiblings(r,b,k,s,i)&&hasManySiblings(o,b,k,s,i)||hasManySiblings(r,$,_,s,i)&&hasManySiblings(o,$,_,s,i)}function hasManySiblings(e,n,t,s,i){const r=Math.max(n-1,0);const o=Math.max(t-1,0);const a=Math.min(n+1,s-1);const l=Math.min(t+1,i-1);const c=e[t*s+n];let u=n===r||n===a||t===o||t===l?1:0;for(let d=r;d<=a;d++){for(let h=o;h<=l;h++){if(d===n&&h===t)continue;u+=+(c===e[h*s+d]);if(u>2)return true}}return false}function colorDelta(e,n,t,s,i){const r=e[t];const o=e[t+1];const a=e[t+2];const l=e[t+3];const c=n[s];const u=n[s+1];const d=n[s+2];const h=n[s+3];let p=r-c;let m=o-u;let y=a-d;const g=l-h;if(l<255||h<255){let k=255,$=255,_=255;if(i){k=48+159*(t%2);$=48+159*((t/1.618033988749895|0)%2);_=48+159*((t/2.618033988749895|0)%2)}p=(r*l-c*h-k*g)/255;m=(o*l-u*h-$*g)/255;y=(a*l-d*h-_*g)/255}const f=p*.29889531+m*.58662247+y*.11448223;const w=p*.59597799-m*.2741761-y*.32180189;const v=p*.21147017-m*.52261711+y*.31114694;const b=.5053*f*f+.299*w*w+.1957*v*v;return f>0?-b:b}function brightnessDelta(e,n,t,s,i,r,o,a){const l=e[t];const c=e[t+1];const u=e[t+2];const d=e[t+3];let h=s-l;let p=i-c;let m=r-u;const y=o-d;if(!h&&!p&&!m&&!y)return 0;if(o<255||d<255){let g=255,f=255,w=255;if(a){g=48+159*(n%2);f=48+159*((n/1.618033988749895|0)%2);w=48+159*((n/2.618033988749895|0)%2)}h=(s*o-l*d-g*y)/255;p=(i*o-c*d-f*y)/255;m=(r*o-u*d-w*y)/255}return h*.29889531+p*.58662247+m*.11448223}function drawPixel(e,n,t,s,i){e[n]=t;e[n+1]=s;e[n+2]=i;e[n+3]=255}function drawGrayPixel(e,n,t,s){const i=255+(e[n]*.29889531+e[n+1]*.58662247+e[n+2]*.11448223-255)*t*e[n+3]/255;drawPixel(s,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 s=decodePng(e);const i=decodePng(n);if(s.width!==i.width||s.height!==i.height){throw new Error(`The screenshots are different sizes (${s.width}\xD7${s.height} vs ${i.width}\xD7${i.height}). Compare on the same device and orientation, or save a new baseline.`)}const{width:r,height:o}=s;const a=Math.round(o*(t.ignoreTop??0));for(const p of[s,i])p.data.fill(0,0,a*r*4);for(const p of t.masks??[]){const m=Math.max(0,Math.floor(p.x*r));const y=Math.min(r,Math.ceil((p.x+p.width)*r));const g=Math.max(0,Math.floor(p.y*o));const f=Math.min(o,Math.ceil((p.y+p.height)*o));for(const w of[s,i])for(let v=g;v<f;v++)w.data.fill(0,(v*r+m)*4,(v*r+y)*4)}const l=t.threshold??.1;const c=new import_pngjs3.PNG({width:r,height:o});const u=pixelmatch(s.data,i.data,c.data,r,o,{threshold:l,includeAA:false,diffMask:true});const d=new import_pngjs3.PNG({width:r,height:o});pixelmatch(s.data,i.data,d.data,r,o,{threshold:l,includeAA:false,alpha:.25,diffColor:[230,30,40]});const h=r*(o-a);return{width:r,height:o,changedPixels:u,percent:h?u/h*100:0,boxes:changedBoxes(c.data,r,o),diffPng:import_pngjs3.PNG.sync.write(d)}}function changedBoxes(e,n,t){const s=Math.max(8,Math.round(n/48));const i=Math.ceil(n/s);const r=Math.ceil(t/s);const o=new Uint32Array(i*r);for(let u=0;u<t;u++){for(let d=0;d<n;d++){if(e[(u*n+d)*4+3]>0)o[Math.floor(u/s)*i+Math.floor(d/s)]++}}const a=1;const l=new Uint8Array(i*r);const c=[];for(let u=0;u<o.length;u++){if(l[u]||o[u]<a)continue;const d={x0:Infinity,y0:Infinity,x1:-1,y1:-1,pixels:0};const h=[u];l[u]=1;while(h.length){const p=h.pop();const m=p%i;const y=Math.floor(p/i);d.x0=Math.min(d.x0,m);d.y0=Math.min(d.y0,y);d.x1=Math.max(d.x1,m);d.y1=Math.max(d.y1,y);d.pixels+=o[p];for(let g=-2;g<=2;g++){for(let f=-2;f<=2;f++){const w=m+f;const v=y+g;if(w<0||v<0||w>=i||v>=r)continue;const b=v*i+w;if(!l[b]&&o[b]>=a){l[b]=1;h.push(b)}}}}c.push(d)}return c.sort((u,d)=>d.pixels-u.pixels).slice(0,8).map(u=>({x:u.x0*s/n,y:u.y0*s/t,width:Math.min(n,(u.x1+1)*s-u.x0*s)/n,height:Math.min(t,(u.y1+1)*s-u.y0*s)/t,pixels:u.pixels}))}function boxColors(e,n,t,s=0){const i=decodePng(e);const r=decodePng(n);if(i.width!==r.width||i.height!==r.height)return null;const{width:o,height:a}=i;const l=Math.max(0,Math.floor(t.x*o));const c=Math.min(o,Math.ceil((t.x+t.width)*o));const u=Math.max(Math.round(a*s),Math.floor(t.y*a));const d=Math.min(a,Math.ceil((t.y+t.height)*a));const h=new Map;const p=new Map;for(let w=u;w<d;w++){for(let v=l;v<c;v++){const b=(w*o+v)*4;const k=i.data[b]<<16|i.data[b+1]<<8|i.data[b+2];const $=r.data[b]<<16|r.data[b+1]<<8|r.data[b+2];if(k===$)continue;h.set(k,(h.get(k)??0)+1);p.set($,(p.get($)??0)+1)}}const m=w=>[...w].sort((v,b)=>b[1]-v[1])[0]?.[0];const y=m(h);const g=m(p);if(y===void 0||g===void 0)return null;const f=w=>`#${w.toString(16).padStart(6,"0").toUpperCase()}`;return{before:f(y),after:f(g)}}function describeSpot(e){const n=e.x+e.width/2;const t=e.y+e.height/2;const s=t<.33?"top":t<.66?"middle":"bottom";const i=n<.33?" left":n<.66?"":" right";return`${s}${i}`}function cropPng(e,n){const t=decodePng(e);const s=Math.max(0,Math.floor(n.x*t.width));const i=Math.max(0,Math.floor(n.y*t.height));const r=Math.max(1,Math.min(t.width-s,Math.round(n.width*t.width)));const o=Math.max(1,Math.min(t.height-i,Math.round(n.height*t.height)));const a=new import_pngjs3.PNG({width:r,height:o});import_pngjs3.PNG.bitblt(t,a,s,i,r,o,0,0);return import_pngjs3.PNG.sync.write(a)}var import_pngjs3;var init_imageDiff=__esm({"src/capture/imageDiff.ts"(){"use strict";import_pngjs3=__toESM(require_png());init_pixelmatch()}});function touchOwnerFromLog(e){let n=null;let t;const s=[];for(const i of e.split("\n")){const r=/digitizer attached \(sender ([0-9A-F]+)\)/i.exec(i);if(r){if(!s.includes(r[1]))s.push(r[1]);t=n;n=r[1];continue}if(/already have a main display digitizer/i.test(i)){if(t!==void 0)n=t??OTHER_UNSEEN;t=void 0;continue}t=void 0;const o=/digitizer detached \(sender ([0-9A-F]+)\)/i.exec(i);if(o){const a=s.indexOf(o[1]);if(a>=0)s.splice(a,1);if(n===o[1]||n===OTHER_UNSEEN&&a<0)n=s.find(l=>IDB_SENDER.test(l))??s[s.length-1]??null}}if(!n)return"unknown";return IDB_SENDER.test(n)?"idb":"other"}async function simTouchOwner(e){const n=cache.get(e);if(n&&Date.now()-n.at<6e4)return n.owner;let t="unknown";try{const{stdout:s}=await execFileAsync12("xcrun",["simctl","spawn",e,"log","show","--last","24h","--style","compact","--predicate",'process == "backboardd" AND category == "TouchEvents"'],{timeout:2e4,maxBuffer:16*1024*1024});t=touchOwnerFromLog(s.split("\n").filter(i=>/digitizer|already have a main/i.test(i)).join("\n"))}catch{t="unknown"}cache.set(e,{owner:t,at:Date.now()});return t}async function assertSimTouchable(e){if(await simTouchOwner(e)==="other")throw new Error(TOUCH_TAKEN)}var import_node_child_process15,import_node_util12,execFileAsync12,IDB_SENDER,OTHER_UNSEEN,TOUCH_TAKEN,cache;var init_simTouch=__esm({"src/capture/simTouch.ts"(){"use strict";import_node_child_process15=require("child_process");import_node_util12=require("util");execFileAsync12=(0,import_node_util12.promisify)(import_node_child_process15.execFile);IDB_SENDER=/^ACEFADE/i;OTHER_UNSEEN="unseen";TOUCH_TAKEN="Real touches can't reach this simulator right now. On Xcode 27, DeviceHub (the app that shows simulators) takes over each booted simulator's touch screen, so a touch sent from here is dropped. Use tap_element for React elements (it presses them inside the app), or run_flow. Don't quit DeviceHub to fix this: quitting it shuts down every simulator.";cache=new Map}});var HID_SWIFT,HID_INTERFACE,HID_HASH;var init_hidSource_generated=__esm({"src/capture/hidSource.generated.ts"(){"use strict";HID_SWIFT=`import Foundation
|
|
583
|
+
${m.join("\n")}`)}if(a.length===0){return text(`No storage keys${i?` matching "${t.pattern}"`:""} in any backend.`)}const p=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return text(`${a.join("\n\n")}${p}`)}catch(s){return fail2(errMsg2(s))}});e.registerTool("storage_action",{description:"Modify the app's AsyncStorage: set a key, remove a key, or clear supported app AsyncStorage keys (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",inputSchema:{action:import_zod11.z.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:import_zod11.z.string().optional().describe("The key (required for set and remove)."),value:import_zod11.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:deviceIdInput}},async t=>{try{const s=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(STORAGE_TOOL_ID,"clearAppStorage",{},s);return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return fail2(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(STORAGE_TOOL_ID,"async.removeItem",{key:t.key},s);return text(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return fail2("'set' needs a value (a string).")}await n.callAction(STORAGE_TOOL_ID,"async.setItem",{key:t.key,value:t.value},s);return text(`Requested update of storage key "${t.key}".`)}catch(s){return fail2(errMsg2(s))}})}var import_zod11,STORAGE_TOOL_ID;var init_storage=__esm({"src/tools/packs/storage.ts"(){"use strict";import_zod11=require("zod");init_packKit();STORAGE_TOOL_ID="storage"}});function valueKey(e){if(typeof e==="number"){if(!Number.isFinite(e)||Number.isInteger(e)&&Math.abs(e)<10)return null;return`n:${e}`}if(typeof e!=="string")return null;const n=e.trim();if(n.length<3||n.length>80)return null;const t=/^[$€£]?-?[\d,]+(\.\d+)?$/.test(n)?Number(n.replace(/[$€£,]/g,"")):NaN;if(Number.isFinite(t)&&/[\d]/.test(n))return valueKey(t);return`s:${n}`}function walk(e,n,t,s){if(s.length>=MAX_LEAVES||t>MAX_DEPTH||e==null)return;if(typeof e==="object"){const r=Array.isArray(e)?e.slice(0,50).map((o,a)=>[String(a),o]):Object.entries(e);for(const[o,a]of r)walk(a,n?`${n}.${o}`:o,t+1,s);return}const i=valueKey(e);if(i)s.push([i,n])}function indexState(e,n,t){const s=[];walk(t,"",0,s);for(const[i,r]of s){const o=r?`${n}, ${r}`:n;const a=e.get(i)??[];if(a.length<3)a.push(o);e.set(i,a)}}async function liveStateIndex(e,n){const t=new Set(e.connectedDevices().find(o=>o.deviceId===n)?.tools.map(o=>o.toolId)??[]);const s={timeoutMs:1500,silent:true};const i=[];const r=new Map;if(t.has("zustand")){i.push(e.callAction("zustand","listStores",{includeValues:true},n,s).then(o=>{for(const a of o?.stores??[])indexState(r,`${a.name} store`,a.currentState)}))}if(t.has("redux")){i.push(e.callAction("redux","getState",{includeValues:true},n,s).then(o=>{const a=o;if(a?.available)indexState(r,"redux state",a.state)}))}if(t.has("jotai")){i.push(e.callAction("jotai","listAtoms",{includeValues:true},n,s).then(o=>{for(const a of o?.atoms??[])indexState(r,`atom ${a.label}`,a.currentValue)}))}if(!i.length)return null;await Promise.all(i.map(o=>o.catch(()=>void 0)));return r.size?r:null}function stateNote(e,n){if(!e||!n)return"";const t=valueKey(n);const s=t?e.get(t):void 0;return s?.length?` = live state: ${s.join("; ")}`:""}var MAX_LEAVES,MAX_DEPTH;var init_liveValues=__esm({"src/tools/packs/liveValues.ts"(){"use strict";MAX_LEAVES=4e3;MAX_DEPTH=6}});function pixelmatch(e,n,t,s,i,r={}){const{threshold:o=.1,alpha:a=.1,aaColor:l=[255,255,0],diffColor:c=[255,0,0],checkerboard:u=true,includeAA:d,diffColorAlt:h,diffMask:p}=r;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!==s*i*4)throw new Error(`Image data size does not match width/height. Expecting ${s*i*4}. Got ${e.length}`);const m=s*i;const y=new Uint32Array(e.buffer,e.byteOffset,m);const g=new Uint32Array(n.buffer,n.byteOffset,m);let f=true;for(let T=0;T<m;T++){if(y[T]!==g[T]){f=false;break}}if(f){if(t&&!p){for(let T=0,I=0;T<m;T++,I+=4)drawGrayPixel(e,I,a,t)}return 0}const w=35215*o*o;const[v,b,_]=l;const[$,k,S]=c;const[E,A,P]=h||c;let x=0;for(let T=0,I=0;T<m;T++,I+=4){const O=y[T]===g[T]?0:colorDelta(e,n,I,I,u);if(Math.abs(O)>w){const N=T%s;const F=T/s|0;const X=!d&&(antialiased(e,N,F,s,i,y,g,u)||antialiased(n,N,F,s,i,g,y,u));if(X){if(t&&!p)drawPixel(t,I,v,b,_)}else{if(t){if(O<0){drawPixel(t,I,E,A,P)}else{drawPixel(t,I,$,k,S)}}x++}}else if(t&&!p){drawGrayPixel(e,I,a,t)}}return x}function isPixelData(e){return ArrayBuffer.isView(e)&&e.BYTES_PER_ELEMENT===1}function antialiased(e,n,t,s,i,r,o,a){const l=Math.max(n-1,0);const c=Math.max(t-1,0);const u=Math.min(n+1,s-1);const d=Math.min(t+1,i-1);const h=(t*s+n)*4;const p=e[h];const m=e[h+1];const y=e[h+2];const g=e[h+3];let f=n===l||n===u||t===c||t===d?1:0;let w=0;let v=0;let b=0;let _=0;let $=0;let k=0;for(let S=l;S<=u;S++){for(let E=c;E<=d;E++){if(S===n&&E===t)continue;const A=brightnessDelta(e,h,(E*s+S)*4,p,m,y,g,a);if(A===0){f++;if(f>2)return false}else if(A<w){w=A;b=S;_=E}else if(A>v){v=A;$=S;k=E}}}if(w===0||v===0)return false;return hasManySiblings(r,b,_,s,i)&&hasManySiblings(o,b,_,s,i)||hasManySiblings(r,$,k,s,i)&&hasManySiblings(o,$,k,s,i)}function hasManySiblings(e,n,t,s,i){const r=Math.max(n-1,0);const o=Math.max(t-1,0);const a=Math.min(n+1,s-1);const l=Math.min(t+1,i-1);const c=e[t*s+n];let u=n===r||n===a||t===o||t===l?1:0;for(let d=r;d<=a;d++){for(let h=o;h<=l;h++){if(d===n&&h===t)continue;u+=+(c===e[h*s+d]);if(u>2)return true}}return false}function colorDelta(e,n,t,s,i){const r=e[t];const o=e[t+1];const a=e[t+2];const l=e[t+3];const c=n[s];const u=n[s+1];const d=n[s+2];const h=n[s+3];let p=r-c;let m=o-u;let y=a-d;const g=l-h;if(l<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=(r*l-c*h-_*g)/255;m=(o*l-u*h-$*g)/255;y=(a*l-d*h-k*g)/255}const f=p*.29889531+m*.58662247+y*.11448223;const w=p*.59597799-m*.2741761-y*.32180189;const v=p*.21147017-m*.52261711+y*.31114694;const b=.5053*f*f+.299*w*w+.1957*v*v;return f>0?-b:b}function brightnessDelta(e,n,t,s,i,r,o,a){const l=e[t];const c=e[t+1];const u=e[t+2];const d=e[t+3];let h=s-l;let p=i-c;let m=r-u;const y=o-d;if(!h&&!p&&!m&&!y)return 0;if(o<255||d<255){let g=255,f=255,w=255;if(a){g=48+159*(n%2);f=48+159*((n/1.618033988749895|0)%2);w=48+159*((n/2.618033988749895|0)%2)}h=(s*o-l*d-g*y)/255;p=(i*o-c*d-f*y)/255;m=(r*o-u*d-w*y)/255}return h*.29889531+p*.58662247+m*.11448223}function drawPixel(e,n,t,s,i){e[n]=t;e[n+1]=s;e[n+2]=i;e[n+3]=255}function drawGrayPixel(e,n,t,s){const i=255+(e[n]*.29889531+e[n+1]*.58662247+e[n+2]*.11448223-255)*t*e[n+3]/255;drawPixel(s,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 s=decodePng(e);const i=decodePng(n);if(s.width!==i.width||s.height!==i.height){throw new Error(`The screenshots are different sizes (${s.width}\xD7${s.height} vs ${i.width}\xD7${i.height}). Compare on the same device and orientation, or save a new baseline.`)}const{width:r,height:o}=s;const a=Math.round(o*(t.ignoreTop??0));for(const p of[s,i])p.data.fill(0,0,a*r*4);for(const p of t.masks??[]){const m=Math.max(0,Math.floor(p.x*r));const y=Math.min(r,Math.ceil((p.x+p.width)*r));const g=Math.max(0,Math.floor(p.y*o));const f=Math.min(o,Math.ceil((p.y+p.height)*o));for(const w of[s,i])for(let v=g;v<f;v++)w.data.fill(0,(v*r+m)*4,(v*r+y)*4)}const l=t.threshold??.1;const c=new import_pngjs3.PNG({width:r,height:o});const u=pixelmatch(s.data,i.data,c.data,r,o,{threshold:l,includeAA:false,diffMask:true});const d=new import_pngjs3.PNG({width:r,height:o});pixelmatch(s.data,i.data,d.data,r,o,{threshold:l,includeAA:false,alpha:.25,diffColor:[230,30,40]});const h=r*(o-a);return{width:r,height:o,changedPixels:u,percent:h?u/h*100:0,boxes:changedBoxes(c.data,r,o),diffPng:import_pngjs3.PNG.sync.write(d)}}function changedBoxes(e,n,t){const s=Math.max(8,Math.round(n/48));const i=Math.ceil(n/s);const r=Math.ceil(t/s);const o=new Uint32Array(i*r);for(let u=0;u<t;u++){for(let d=0;d<n;d++){if(e[(u*n+d)*4+3]>0)o[Math.floor(u/s)*i+Math.floor(d/s)]++}}const a=1;const l=new Uint8Array(i*r);const c=[];for(let u=0;u<o.length;u++){if(l[u]||o[u]<a)continue;const d={x0:Infinity,y0:Infinity,x1:-1,y1:-1,pixels:0};const h=[u];l[u]=1;while(h.length){const p=h.pop();const m=p%i;const y=Math.floor(p/i);d.x0=Math.min(d.x0,m);d.y0=Math.min(d.y0,y);d.x1=Math.max(d.x1,m);d.y1=Math.max(d.y1,y);d.pixels+=o[p];for(let g=-2;g<=2;g++){for(let f=-2;f<=2;f++){const w=m+f;const v=y+g;if(w<0||v<0||w>=i||v>=r)continue;const b=v*i+w;if(!l[b]&&o[b]>=a){l[b]=1;h.push(b)}}}}c.push(d)}return c.sort((u,d)=>d.pixels-u.pixels).slice(0,8).map(u=>({x:u.x0*s/n,y:u.y0*s/t,width:Math.min(n,(u.x1+1)*s-u.x0*s)/n,height:Math.min(t,(u.y1+1)*s-u.y0*s)/t,pixels:u.pixels}))}function boxColors(e,n,t,s=0){const i=decodePng(e);const r=decodePng(n);if(i.width!==r.width||i.height!==r.height)return null;const{width:o,height:a}=i;const l=Math.max(0,Math.floor(t.x*o));const c=Math.min(o,Math.ceil((t.x+t.width)*o));const u=Math.max(Math.round(a*s),Math.floor(t.y*a));const d=Math.min(a,Math.ceil((t.y+t.height)*a));const h=new Map;const p=new Map;for(let w=u;w<d;w++){for(let v=l;v<c;v++){const b=(w*o+v)*4;const _=i.data[b]<<16|i.data[b+1]<<8|i.data[b+2];const $=r.data[b]<<16|r.data[b+1]<<8|r.data[b+2];if(_===$)continue;h.set(_,(h.get(_)??0)+1);p.set($,(p.get($)??0)+1)}}const m=w=>[...w].sort((v,b)=>b[1]-v[1])[0]?.[0];const y=m(h);const g=m(p);if(y===void 0||g===void 0)return null;const f=w=>`#${w.toString(16).padStart(6,"0").toUpperCase()}`;return{before:f(y),after:f(g)}}function describeSpot(e){const n=e.x+e.width/2;const t=e.y+e.height/2;const s=t<.33?"top":t<.66?"middle":"bottom";const i=n<.33?" left":n<.66?"":" right";return`${s}${i}`}function cropPng(e,n){const t=decodePng(e);const s=Math.max(0,Math.floor(n.x*t.width));const i=Math.max(0,Math.floor(n.y*t.height));const r=Math.max(1,Math.min(t.width-s,Math.round(n.width*t.width)));const o=Math.max(1,Math.min(t.height-i,Math.round(n.height*t.height)));const a=new import_pngjs3.PNG({width:r,height:o});import_pngjs3.PNG.bitblt(t,a,s,i,r,o,0,0);return import_pngjs3.PNG.sync.write(a)}var import_pngjs3;var init_imageDiff=__esm({"src/capture/imageDiff.ts"(){"use strict";import_pngjs3=__toESM(require_png());init_pixelmatch()}});function touchOwnerFromLog(e){let n=null;let t;const s=[];for(const i of e.split("\n")){const r=/digitizer attached \(sender ([0-9A-F]+)\)/i.exec(i);if(r){if(!s.includes(r[1]))s.push(r[1]);t=n;n=r[1];continue}if(/already have a main display digitizer/i.test(i)){if(t!==void 0)n=t??OTHER_UNSEEN;t=void 0;continue}t=void 0;const o=/digitizer detached \(sender ([0-9A-F]+)\)/i.exec(i);if(o){const a=s.indexOf(o[1]);if(a>=0)s.splice(a,1);if(n===o[1]||n===OTHER_UNSEEN&&a<0)n=s.find(l=>IDB_SENDER.test(l))??s[s.length-1]??null}}if(!n)return"unknown";return IDB_SENDER.test(n)?"idb":"other"}async function simTouchOwner(e){const n=cache.get(e);if(n&&Date.now()-n.at<6e4)return n.owner;let t="unknown";try{const{stdout:s}=await execFileAsync12("xcrun",["simctl","spawn",e,"log","show","--last","24h","--style","compact","--predicate",'process == "backboardd" AND category == "TouchEvents"'],{timeout:2e4,maxBuffer:16*1024*1024});t=touchOwnerFromLog(s.split("\n").filter(i=>/digitizer|already have a main/i.test(i)).join("\n"))}catch{t="unknown"}cache.set(e,{owner:t,at:Date.now()});return t}async function assertSimTouchable(e){if(await simTouchOwner(e)==="other")throw new Error(TOUCH_TAKEN)}var import_node_child_process15,import_node_util12,execFileAsync12,IDB_SENDER,OTHER_UNSEEN,TOUCH_TAKEN,cache;var init_simTouch=__esm({"src/capture/simTouch.ts"(){"use strict";import_node_child_process15=require("child_process");import_node_util12=require("util");execFileAsync12=(0,import_node_util12.promisify)(import_node_child_process15.execFile);IDB_SENDER=/^ACEFADE/i;OTHER_UNSEEN="unseen";TOUCH_TAKEN="Real touches can't reach this simulator right now. On Xcode 27, DeviceHub (the app that shows simulators) takes over each booted simulator's touch screen, so a touch sent from here is dropped. Use tap_element for React elements (it presses them inside the app), or run_flow. Don't quit DeviceHub to fix this: quitting it shuts down every simulator.";cache=new Map}});var HID_SWIFT,HID_INTERFACE,HID_HASH;var init_hidSource_generated=__esm({"src/capture/hidSource.generated.ts"(){"use strict";HID_SWIFT=`import Foundation
|
|
584
584
|
import Darwin
|
|
585
585
|
import CryptoKit
|
|
586
586
|
|
|
@@ -1056,7 +1056,7 @@ struct Command {
|
|
|
1056
1056
|
`;HID_INTERFACE="// swift-interface-format-version: 1.0\n// swift-compiler-version: Apple Swift version 6.4\n// swift-module-flags: -target arm64-apple-macosx15.0 -enable-library-evolution -module-name CoreDevice\n// Reconstructed API subset. No replacement framework code is linked.\n// Non-frozen types and protocol/class calls resolve through Apple metadata and thunks.\nimport Foundation\nimport CoreGraphics\npublic struct Capability {}\npublic protocol CoreDeviceServiceConnection {}\npublic struct CoreDeviceService {\n public static var sharedConnection: any CoreDeviceServiceConnection { get }\n}\npublic enum DeviceVisibilityClass: Swift.Hashable {\n case `default`, wirelessPairableDevices, devicesBeingProvisioned, simulators\n public static func == (lhs: DeviceVisibilityClass, rhs: DeviceVisibilityClass) -> Swift.Bool\n public func hash(into hasher: inout Swift.Hasher)\n public var hashValue: Swift.Int { get }\n}\npublic class DeviceManager {\n public static var shared: DeviceManager { get }\n public init(serviceConnection: any CoreDeviceServiceConnection, allowedDeviceVisibilityClasses: Swift.Set<DeviceVisibilityClass>)\n public func awaitFullInitialization() async\n public func waitForPostPluginLoadCheckIn() async\n public var devices: [Foundation.UUID: RemoteDevice] { get }\n public func deviceNamed(_ name: Swift.String) throws -> RemoteDevice\n}\npublic class RemoteDevice {\n public var deviceIdentifier: Foundation.UUID { get }\n public func getImplementation<C: DeviceCapability>(for capability: CapabilityStaticMember<C>) async throws -> C.ProtocolType\n}\npublic protocol DeviceCapability {\n associatedtype ProtocolType\n static var capability: Capability { get }\n}\npublic struct CapabilityStaticMember<C> {\n public init()\n}\npublic struct DigitizerHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDDigitizer\n public static var capability: Capability { get }\n}\npublic protocol DeviceCapabilityProtocol {\n static var capability: Capability { get }\n static var identifier: Swift.String { get }\n static var identifierSuffix: Swift.String { get }\n static var priority: Swift.Int { get }\n}\npublic protocol HIDDigitizer: DeviceCapabilityProtocol {\n func send(pointOne: DigitizerPoint, pointTwo: DigitizerPoint?, eventType: DigitizerEventType, edge: DigitizerEdge, target: DigitizerTarget) throws\n func sendBarrier()\n}\npublic struct DigitizerPoint {\n public init(x: Swift.Double, y: Swift.Double)\n}\npublic enum DigitizerEventType { case start, position, end }\npublic enum DigitizerEdge { case none, top, left, bottom, right }\npublic enum DigitizerTarget { case mainScreen, display1, display2, display3, display4, display5, display6, display7, display8, display9, display10 }\npublic struct KeyboardHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDKeyboard\n public static var capability: Capability { get }\n}\npublic protocol HIDKeyboard: DeviceCapabilityProtocol {\n func send(key: HIDKeyboardUsageCode, state: HIDButtonState) throws\n func sendBarrier()\n}\npublic struct HIDKeyboardUsageCode {\n public init(rawValue: Swift.UInt16)\n}\npublic enum HIDButtonState { case down, up }\npublic struct ButtonHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDButton\n public static var capability: Capability { get }\n}\npublic protocol HIDButton: DeviceCapabilityProtocol {\n func sendCustomButton(usagePage: Swift.Int, usageCode: Swift.Int, state: HIDButtonState) throws\n func sendBarrier()\n}\n";HID_HASH="718c261deb06"}});async function xcodeBuild(){try{const{stdout:e}=await execFileAsync13("xcode-select",["-p"],{timeout:1e4});const n=import_node_path21.default.join(process.env.DEVELOPER_DIR||e.trim(),"..","Info.plist");const{stdout:t}=await execFileAsync13("plutil",["-extract","DTXcodeBuild","raw",n],{timeout:1e4});return t.trim().replace(/[^\w.-]/g,"")||"unknown"}catch{return"unknown"}}async function build(e){if(process.platform!=="darwin")return null;const n=import_node_path21.default.join(import_node_os15.default.homedir(),".buoy","hidtouch",`${e}-${HID_HASH}`);const t=import_node_path21.default.join(n,"hidtouch");try{await import_node_fs20.promises.access(t);return t}catch{}try{await import_node_fs20.promises.access(import_node_path21.default.join(PRIVATE_FRAMEWORKS,"CoreDevice.framework"))}catch{buildError="CoreDevice is not installed (it ships with Xcode 15 and later).";return null}const s=await import_node_fs20.promises.mkdtemp(import_node_path21.default.join(import_node_os15.default.tmpdir(),"buoy-hidtouch-"));try{await import_node_fs20.promises.mkdir(import_node_path21.default.join(s,"modules"));await import_node_fs20.promises.writeFile(import_node_path21.default.join(s,"hidtouch.swift"),HID_SWIFT);await import_node_fs20.promises.writeFile(import_node_path21.default.join(s,"modules","CoreDevice.swiftinterface"),HID_INTERFACE);const i=import_node_path21.default.join(s,"hidtouch");await execFileAsync13("xcrun",["swiftc","-O","-module-cache-path",import_node_path21.default.join(s,"cache"),"-I",import_node_path21.default.join(s,"modules"),"-F",PRIVATE_FRAMEWORKS,"-framework","CoreDevice","-parse-as-library",import_node_path21.default.join(s,"hidtouch.swift"),"-o",i],{timeout:18e4,maxBuffer:8*1024*1024,env:{...process.env,TMPDIR:s}});await import_node_fs20.promises.mkdir(n,{recursive:true});const r=`${t}.${process.pid}`;await import_node_fs20.promises.copyFile(i,r);await import_node_fs20.promises.chmod(r,493);await import_node_fs20.promises.rename(r,t);return t}catch(i){const r=i;buildError=(r.stderr||r.message||"").trim().split("\n").slice(-3).join(" ").slice(0,300);return null}finally{await import_node_fs20.promises.rm(s,{recursive:true,force:true}).catch(()=>void 0)}}function simHidTool(){const e=Date.now();const n=!built||e-lastUsed>=1e4||developerDir!==process.env.DEVELOPER_DIR;lastUsed=e;if(refreshing)return refreshing;if(!n)return built;developerDir=process.env.DEVELOPER_DIR;const t=developerDir;refreshing=xcodeBuild().then(s=>{const i=JSON.stringify([t,s]);if(!built||buildKey!==i){buildKey=i;buildError="";built=build(s)}return built}).finally(()=>{refreshing=null});return refreshing}async function deviceOrientation(e){try{const{stdout:n}=await execFileAsync13("xcrun",["devicectl","device","orientation","get","--device",e],{timeout:15e3});const t=/Orientation:\s*(\w+)/.exec(n)?.[1];return t==="portrait"||t==="portraitUpsideDown"||t==="landscapeLeft"||t==="landscapeRight"?t:null}catch{return null}}async function appUpsideDown(e,n){if(!n)return null;try{const{stdout:t}=await execFileAsync13("xcrun",["simctl","get_app_container",e,n,"app"],{timeout:1e4});const{stdout:s}=await execFileAsync13("plutil",["-convert","json","-o","-",import_node_path21.default.join(t.trim(),"Info.plist")],{timeout:1e4});const i=JSON.parse(s);const{stdout:r}=await execFileAsync13("xcrun",["simctl","getenv",e,"SIMULATOR_MODEL_IDENTIFIER"],{timeout:1e4});const o=r.trim().startsWith("iPad")?"ipad":"iphone";const a=i[`UISupportedInterfaceOrientations~${o}`]??i.UISupportedInterfaceOrientations;if(!Array.isArray(a)||!a.every(l=>typeof l==="string"))return null;return a.includes("UIInterfaceOrientationPortraitUpsideDown")}catch{return null}}function toDigitizer(e,n,t,s,i){if(s==="portraitUpsideDown"){if(i==null)throw new Error("Upside-down touch needs the app turn list. Turn the device upright and try again.");return i?[1-e,1-n]:[e,n]}if(!t)return[e,n];if(s==="landscapeRight")return[n,1-e];return[1-n,e]}async function run3(e,n,t,s=false){try{const i=n[n.indexOf("--device")+1];await runHidStream(e,i,n,t)}catch(i){const r=i;const o=r.stderr||r.message||"";if(/HID Digitizer/i.test(o)&&/not supported/i.test(o)){console.error("[buoy:hid]",o);throw new Error(s?"Your iPhone isn't connected. Unlock it and plug it in, or put it on the same Wi-Fi. Then try again.":"Touch is not supported on this iOS Simulator.")}throw new Error(`The touch failed: ${o.replace(/^hidtouch:\s*/,"").split("\n")[0].slice(0,300)}`)}}async function simHidTap(e,n,t,s,i,r,o){const a=t.width>t.height;const l=o!==void 0?o:await deviceOrientation(n);const c=l==="portraitUpsideDown"?await appUpsideDown(n,t.bundleId):null;const[u,d]=toDigitizer(s/t.width,i/t.height,a,l,c);await run3(e,r?["--device",n,"long-press",frac(u),frac(d),String(Math.round(r))]:["--device",n,"tap",frac(u),frac(d)],void 0,t.isPhone);return l}async function simHidSwipe(e,n,t,s,i,r,o=0,a=0,l){const c=t.width>t.height;const u=l!==void 0?l:await deviceOrientation(n);const d=u==="portraitUpsideDown"?await appUpsideDown(n,t.bundleId):null;const h=toDigitizer(s[0]/t.width,s[1]/t.height,c,u,d);const p=toDigitizer(i[0]/t.width,i[1]/t.height,c,u,d);const m=String(Math.round(Math.min(1e4,Math.max(1,r))));const y=[frac(h[0]),frac(h[1]),frac(p[0]),frac(p[1])];const g=o>0?["--device",n,"drag",...y,String(Math.round(Math.min(1e4,o))),m]:a>0?["--device",n,"scroll",...y,m,String(Math.round(Math.min(1e4,a)))]:["--device",n,"swipe",...y,m];await run3(e,g,void 0,t.isPhone);return u}async function simHidTwo(e,n,t,s,i,r,o,a){const l=t.width>t.height;const c=await deviceOrientation(n);const u=c==="portraitUpsideDown"?await appUpsideDown(n,t.bundleId):null;const d=l?{w:t.height,h:t.width}:{w:t.width,h:t.height};const h=[s,i,r,o].flatMap(p=>{const[m,y]=toDigitizer(p[0]/t.width,p[1]/t.height,l,c,u);return[(clamp3(m)*d.w).toFixed(2),(clamp3(y)*d.h).toFixed(2)]});await run3(e,["--device",n,"--points",String(d.w),String(d.h),"two",...h,String(Math.round(Math.min(1e4,Math.max(1,a))))],void 0,t.isPhone);return c}async function simHidType(e,n,t){if(!SIM_TYPEABLE.test(t))throw new Error("The simulator keyboard types plain ASCII letters, digits, punctuation and newlines only. For other text use tap_element with text.");if(!t)return;await run3(e,["--device",n,"type","-"],t)}var import_node_child_process16,import_node_util13,import_node_fs20,import_node_os15,import_node_path21,execFileAsync13,PRIVATE_FRAMEWORKS,built,buildError,lastUsed,developerDir,buildKey,refreshing,clamp3,frac,SIM_TYPEABLE;var init_simHid=__esm({"src/capture/simHid.ts"(){"use strict";import_node_child_process16=require("child_process");import_node_util13=require("util");import_node_fs20=require("fs");import_node_os15=__toESM(require("os"));import_node_path21=__toESM(require("path"));init_hidSource_generated();init_hidStream();execFileAsync13=(0,import_node_util13.promisify)(import_node_child_process16.execFile);PRIVATE_FRAMEWORKS="/Library/Developer/PrivateFrameworks";built=null;buildError="";lastUsed=0;refreshing=null;clamp3=e=>Math.min(1,Math.max(0,e));frac=e=>clamp3(e).toFixed(5);SIM_TYPEABLE=/^[\x20-\x7e\n]*$/}});function pickDescribeScreenMatch(e,n,t){if(t!=null){const r=e.find(o=>o.nativeTag===t);if(r)return r}const s=(n??"").trim().toLowerCase();if(!s)return void 0;let i;for(const r of e){const o=[r.testID,r.label,r.text,r.name].filter(Boolean).map(c=>c.toLowerCase());let a=0;for(const c of o){if(c===s)a=Math.max(a,100);else if(c.startsWith(s))a=Math.max(a,50);else if(c.includes(s))a=Math.max(a,25)}const l=r.tap;if(l&&l.x>=0&&l.x<=1&&l.y>=0&&l.y<=1)a+=20;else if(l)a-=40;if(a<=0)continue;if(!i||a>i.score)i={el:r,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 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 s=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${s?` (${s})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
|
|
1057
1057
|
|
|
1058
1058
|
Closest matches (retry with a more precise query, or pass nativeTag directly):
|
|
1059
|
-
${n.join("\n")}`}function registerScreenshotTools(e,n){e.registerTool("screenshot_component",{_meta:coreTool(),description:"Take a screenshot of the app. Pass compareTo with a saved PNG path to view both screens. It first says what changed, measured: where, which element, and the exact colors before and now. Then the saved file, then the live screen. Leave query and nativeTag out for the whole screen. To crop a React Native view on an iOS Sim, pass query or nativeTag. The tool may scroll that view into sight. Use describe_screen to read text or find controls. Pictures use more tokens. Run the MCP on the device host.",inputSchema:{compareTo:import_zod12.z.string().min(1).optional().describe("A saved PNG file to view next to this screen. Use a full path or a path from the project root. The file stays as it is."),query:import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:import_zod12.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),serial:import_zod12.z.string().optional().describe("Android: the adb serial, when several devices are connected."),baseline:import_zod12.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_zod12.z.boolean().optional().describe("With baseline: save the current screen as the new baseline instead of comparing."),steady:import_zod12.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_zod12.z.number().min(0).max(1).optional().describe("With baseline: per-pixel color tolerance, 0 to 1. Default 0.1."),ignore:import_zod12.z.array(import_zod12.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_zod12.z.array(import_zod12.z.object({x:import_zod12.z.number(),y:import_zod12.z.number(),width:import_zod12.z.number(),height:import_zod12.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.compareTo){if(t.query||t.nativeTag!=null)return fail4("compareTo uses the whole screen. Leave query and nativeTag out.");if(t.baseline||t.updateBaseline!=null||t.steady!=null||t.threshold!=null||t.ignore!=null||t.ignoreAreas!=null)return fail4("compareTo shows both pictures. Use baseline for diff options.");return await compareFileShot(n,t.compareTo,t.deviceId,t.serial)}if(t.baseline){if(t.query||t.nativeTag!=null)return fail4("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:b,host:k,online:$}=await hostTarget(n,t.deviceId,{serial:t.serial},{allowOffline:true});let _=Buffer.alloc(0);const S=await maskSecrets(n,b,$,async()=>_=await hostPng(k));const E={length:S.covered};const A=await toSmallImage(S.png);const P=k.platform==="ios-device"&&isAllBlack(_)?` ${BLACK_PHONE_NOTE}`:"";return{content:[{type:"image",data:A.data,mimeType:A.mimeType},{type:"text",text:`Whole screen of ${hostLabel(k)}, ${A.width}\xD7${A.height}px.${E.length?` ${E.length} element(s) showing a secret were blacked out.`:""}${P}`}]}}const s=n.requireProDevice(t.deviceId);await n.callAction(TOOL_ID,"setSilentTracking",{enabled:true},s).catch(()=>{});let i=t.nativeTag;const r=i==null&&t.query?await exactTag(n,s,t.query):null;if(r)i=r.tag;const o=await n.callAction(TOOL_ID,"locateComponent",{query:i==null?t.query:void 0,nativeTag:i,scrollIntoView:t.scrollIntoView??true,margin:MARGIN_PT},s);let a=o;if(!a.matched&&(a.reason==="no-renders"||a.reason==="no-match")){const b=await n.callAction(TOOL_ID,"describeScreen",{},s).catch(()=>null);const k=b?.elements?pickDescribeScreenMatch(b.elements,t.query,i):void 0;if(k?.frame&&b?.screen){a={matched:true,live:true,rect:frameToRect(k.frame,b.screen),screen:b.screen,nativeTag:k.nativeTag??void 0,componentName:k.name,testID:k.testID}}else if(a.reason==="no-renders"){a={matched:false,reason:"no-match",candidates:a.candidates}}}if(!a.matched||!a.rect||!a.screen){return fail4(reasonMessage(a,t.query)+candidatesText(a.candidates))}if(i==null&&t.query){const b=t.query.toLowerCase();const k=a.candidates?.find(A=>A.nativeTag===a.nativeTag);const $=await n.callAction(TOOL_ID,"describeScreen",{},s,{timeoutMs:4e3}).catch(()=>null);const _=$?.elements?.find(A=>A.nativeTag===a.nativeTag);const S=[a.testID,a.nativeID,a.componentName,k?.testID,k?.nativeID,k?.componentName,_?.testID,_?.nativeID,_?.text,_?.label].filter(Boolean).map(A=>String(A).toLowerCase());const E=S.some(A=>A.includes(b)||A.length>3&&b.includes(A));if(!E){return fail4(`"${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:l}=await hostTarget(n,s);if(l.platform!=="ios")return fail4("Cropping one component works on the iOS Simulator. Leave query out for a whole-screen picture.");let c={ok:false};const u=await maskSecrets(n,s,true,async()=>{c=await captureIosFramebuffer(l.udid);return c.ok&&c.srcPath?import_node_fs21.promises.readFile(c.srcPath):Buffer.alloc(0)});if(u.covered&&c.ok&&c.srcPath)await import_node_fs21.promises.writeFile(c.srcPath,u.png);if(!c.ok||!c.srcPath||!c.width||!c.height){return fail4(c.error??"Screenshot capture failed.")}const d=c.width/a.screen.width;const h=a.rect;const p=CAPTURE_PADDING_PT;const m=await cropToBuffer(c.srcPath,{width:c.width,height:c.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 fail4(m.error??"Crop failed.")}const y=a.componentName||r?.name||"component";const g=a.testID??r?.testID;const f=g?` (testID: "${g}")`:"";const w=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 "${y}"${f} \u2014 ${m.width}\xD7${m.height}px.${w}`;return{content:[{type:"image",data:m.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:v}]}}catch(s){return fail4(errMsg3(s))}})}async function maskSecrets(e,n,t,s){const i=typeof s==="function"?s:async()=>s;const r=secretValues();if(!r.length)return{png:await i(),covered:0};const o=t?await secretBoxes(e,n,r):null;const a=await i();const l=t?await secretBoxes(e,n,r):null;if(o===null||l===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 c=[...o,...l];if(!a.length)return{png:a,covered:0};return{png:c.length?blackOut(a,c):a,covered:Math.max(o.length,l.length)}}async function secretBoxes(e,n,t){const s=await e.callAction(TOOL_ID,"describeScreen",{includeHidden:false,includeBuoy:true,fullText:true},n,{timeoutMs:4e3}).catch(()=>null);if(!s?.screen)return null;const i=typedSecretValues().length>0;const r=[];for(const o of s.elements??[]){const a=o.value;const l=[o.text,o.label,a].filter(h=>h!=null&&h!=="").map(String);const c=l.some(h=>t.some(p=>showsSecret(h,p)));const u=i&&o.control==="text"&&a===void 0;if(!c&&!u)continue;const d=o.frame?.width&&o.frame.height?fractionFrame(o,s.screen):void 0;if(!d)return null;r.push({...d,pixels:0})}return r}function showsSecret(e,n){const t=e.replace(/\s+/g," ");const s=n.replace(/\s+/g," ").trim();if(t.includes(s))return true;const i=t.replace(/(…|\.\.\.)$/,"");for(let r=s.length-1;r>=MIN_SECRET_LENGTH;r--)if(i.endsWith(s.slice(0,r)))return true;return false}function blackOut(e,n){const t=import_pngjs4.PNG.sync.read(e);for(const s of n){const i=Math.max(0,Math.floor((s.x-.01)*t.width));const r=Math.min(t.width,Math.ceil((s.x+s.width+.01)*t.width));const o=Math.max(0,Math.floor((s.y-.005)*t.height));const a=Math.min(t.height,Math.ceil((s.y+s.height+.005)*t.height));for(let l=o;l<a;l++){for(let c=i;c<r;c++){const u=(l*t.width+c)*4;t.data[u]=t.data[u+1]=t.data[u+2]=0;t.data[u+3]=255}}}return import_pngjs4.PNG.sync.write(t)}async function exactTag(e,n,t){const s=await e.callAction(TOOL_ID,"describeScreen",{},n,{timeoutMs:4e3}).catch(()=>null);const i=s?.elements??[];const r=c=>({tag:c.nativeTag,name:c.name,testID:c.testID});const o=i.filter(c=>c.testID===t&&c.nativeTag!=null);if(o.length===1)return r(o[0]);const a=t.trim().toLowerCase();const l=i.filter(c=>(c.text??c.label??"").trim().toLowerCase()===a&&c.nativeTag!=null);return l.length===1?r(l[0]):null}async function readComparisonPng(e){const n=32*1024*1024;const t=i=>{if(!i.isFile()||i.size>n)throw new Error("Use a PNG file under 32 MB.")};t(await import_node_fs21.promises.stat(e));const s=await import_node_fs21.promises.open(e,import_node_fs21.constants.O_RDONLY|import_node_fs21.constants.O_NONBLOCK);try{t(await s.stat());const i=[];let r=0;while(r<=n){const c=Buffer.alloc(Math.min(64*1024,n+1-r));const{bytesRead:u}=await s.read(c,0,c.length,null);if(!u)break;r+=u;if(r>n)throw new Error("Use a PNG file under 32 MB.");i.push(c.subarray(0,u))}const o=Buffer.concat(i,r);if(o.length<24||o.subarray(0,8).toString("hex")!=="89504e470d0a1a0a")throw new Error("The saved file must be a PNG.");const a=o.readUInt32BE(16),l=o.readUInt32BE(20);if(!a||!l||a*l>32e6)throw new Error("Use a PNG with at most 32 million pixels.");return o}finally{await s.close()}}async function compareFileShot(e,n,t,s){const i=n==="~"?import_node_os16.default.homedir():n.startsWith("~/")?import_node_path22.default.join(import_node_os16.default.homedir(),n.slice(2)):n;const r=await readComparisonPng(import_node_path22.default.resolve(process.env.BUOY_PROJECT_ROOT||process.cwd(),i));const{id:o,host:a,online:l}=await hostTarget(e,t,{serial:s},{allowOffline:true});const c=await maskSecrets(e,o,l,()=>hostPng(a));const u=await toSmallImage(r);const d=await toSmallImage(c.png);const h=await measureChange(e,o,l,r,c.png);return{content:[...h?[{type:"text",text:h}]:[],{type:"text",text:`Saved file: ${n} (${u.width}x${u.height}).`},{type:"image",data:u.data,mimeType:u.mimeType},{type:"text",text:`Current screen (${d.width}x${d.height}). Compare it with the saved file above.${c.covered?" Fields with secrets are hidden in this screen.":""}`},{type:"image",data:d.data,mimeType:d.mimeType}]}}async function measureChange(e,n,t,s,i){const r=.06;let o;try{o=diffScreens(s,i,{ignoreTop:r})}catch{return null}if(!o.boxes.length)return"Measured: no change outside the status bar.";let a=[];if(t){try{const c=await e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:3e3,silent:true});const u=c?.screen??{width:1,height:1};a=(c?.elements??[]).map(d=>({name:d.name,words:d.text??d.label,testID:d.testID,frame:fractionFrame(d,u)}))}catch{}}const l=o.boxes.slice(0,3).map(c=>{const u=elementsIn(c,a)[0];const d=boxColors(s,i,c,r);return`- ${describeSpot(c)}${u?`, ${u}`:""}${d?`: main color ${d.before} before, ${d.after} now`:""}.`});return[`Measured: ${o.percent.toFixed(1)}% of the screen changed, in ${o.boxes.length} area${o.boxes.length===1?"":"s"}.`,...l].join("\n")}function baselinePath(e){const n=e.trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"")||"screen";return import_node_path22.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 s of n){const i=s.frame;if(!i)continue;const r=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||r*o/a<.5)continue;t.push({label:labelOf(s),area:a,words:!!s.words})}return[...new Set(t.sort((s,i)=>Number(i.words)-Number(s.words)||s.area-i.area).map(s=>s.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 s=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 r=new Set([...s.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=s.get(d);if(p){if(p.words!==h.words)a.push(`changed: ${labelOf({...h,words:void 0})} "${p.words}" \u2192 "${h.words}"`)}else if(!r.has(h.words))a.push(`appeared: ${labelOf(h)}`)}for(const[d,h]of s)if(!i.has(d)&&!o.has(h.words))a.push(`gone: ${labelOf(h)}`);const l=[...s.entries()].filter(([d,h])=>!i.has(d)&&!o.has(h.words)).map(([,d])=>d);const c=[...i.entries()].filter(([d,h])=>!s.has(d)&&!r.has(h.words)).map(([,d])=>d);const u=(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 c){const h=l.find(m=>m.name===d.name&&u(m.frame,d.frame));if(!h)continue;l.splice(l.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,s,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 r=s?.screen??t?.screen;if(!r)return null;const o=new Set((s?.elements??[]).map(u=>u.nativeTag));const a=[];for(const u of t?.elements??[]){if(o.has(u.nativeTag))continue;const d=fractionFrame(u,r);if(d)a.push({x:d.x-.01,y:d.y-.01,width:d.width+.02,height:d.height+.02,pixels:0})}const l=(s?.elements??[]).map(u=>{const d=scrubSecrets((u.text??u.label??"").replace(/[\s\u200b\u00a0]+/g," ").trim());return{name:u.name,words:d||void 0,testID:u.testID?scrubSecrets(u.testID):void 0,frame:fractionFrame(u,r)}});const c=i?.path??i?.pathname;return{elements:l,masks:a,route:c?scrubSecrets(c):void 0}}catch{return null}}async function withFrozenClock(e,n,t,s){let i;let r;let o;try{await e.callAction("clock","freeze",{time:t},n);await new Promise(l=>setTimeout(l,1300));i=await s()}catch(l){r=l}finally{try{await e.callAction("clock","reset",{},n)}catch(l){o=l}}const a=o?` WARNING: putting the app's clock back failed (${errMsg3(o)}); run clock_action reset.`:"";if(r)throw new Error(`${errMsg3(r)}${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:s,host:i}=await hostTarget(e,t.deviceId,{serial:t.serial});const r=baselinePath(n);const o=r.replace(/\.png$/,".json");const a=import_node_path22.default.relative(process.env.BUOY_PROJECT_ROOT||process.cwd(),r);let l=null;try{l=await import_node_fs21.promises.readFile(r)}catch{}const c=!l||!!t.update;const u=c?{}:JSON.parse(await import_node_fs21.promises.readFile(o,"utf8").catch(()=>"{}"));const d=hostLabel(i);const h=e.connectedDevices().find(L=>L.deviceId===s);const p=h?.tools.some(L=>L.toolId==="clock")??false;const m=p&&hasProAccess(h);const y=t.steady??m;let g="";const f=async()=>{const{result:L,steadied:W}=await withSteadyStatusBar(i,async()=>({png:(await maskSecrets(e,s,true,()=>hostPng(i))).png,screen:await readScreen(e,s)}));return{...L,steadied:W}};let w;if(y&&m){const L=await e.callAction("clock","getState",{},s).catch(()=>null);if(L?.mode==="real"){const W=c||!u.steadyTime?Math.floor(Date.now()/6e4)*6e4:u.steadyTime;u.steadyTime=W;const Q=await withFrozenClock(e,s,W,f);w=Q.result;g+=Q.note}else{w=await f();g+=" The app's clock was already overridden, so it was left as it was."}}else{w=await f();if(y&&!m)g+=p?" (steady freezes the app clock, which needs Buoy Pro; times on screen may differ.)":" (steady needs the Clock tool in the app; times on screen may differ.)"}const v={width:w.png.readUInt32BE(16),height:w.png.readUInt32BE(20)};if(c){await import_node_fs21.promises.mkdir(import_node_path22.default.dirname(r),{recursive:true});await import_node_fs21.promises.writeFile(r,w.png);const L={...u,device:d,size:v,route:w.screen?.route,elements:w.screen?.elements};await import_node_fs21.promises.writeFile(o,JSON.stringify(L,null,1));const W=await toSmallImage(w.png);const Q=`${l?"Replaced":"Saved"} baseline "${n}"`;return{outcome:{saved:true,changed:false,percent:0,texts:0,summary:`${Q} (first run)`},result:{content:[{type:"image",data:W.data,mimeType:W.mimeType},{type:"text",text:`${l?"Replaced":"Saved"} baseline "${n}" (${a}) from ${d}${w.screen?.route?` on ${w.screen.route}`:""}. Call again with the same baseline to compare.${g}`}]}}}const b=[];if(u.device&&u.device!==d)b.push(`The baseline was saved on ${u.device}; this is ${d}.`);if(u.route&&w.screen?.route&&u.route!==w.screen.route)b.push(`The baseline was on ${u.route}; the app is now on ${w.screen.route}.`);const k=[...t.ignoreAreas??[]];const $=new Set(t.ignore??[]);const _=new Set($);for(const L of[...u.elements??[],...w.screen?.elements??[]]){if(!L.testID||!$.has(L.testID)||!L.frame)continue;_.delete(L.testID);k.push({x:L.frame.x-.005,y:L.frame.y-.005,width:L.frame.width+.01,height:L.frame.height+.01})}if(_.size)b.push(`Not on screen, so not left out: ${[..._].join(", ")}.`);const S=i.platform==="android"?[]:[{x:.28,y:0,width:.44,height:.055}];const E=diffScreens(l,w.png,{threshold:t.threshold,masks:[...w.screen?.masks??[],...k,...S],ignoreTop:w.steadied?0:.065});const A=L=>!!L&&k.some(W=>L.x+L.width/2>=W.x&&L.x+L.width/2<=W.x+W.width&&L.y+L.height/2>=W.y&&L.y+L.height/2<=W.y+W.height);const P=L=>(!L.testID||!$.has(L.testID))&&!A(L.frame);const x=u.elements&&w.screen?textChanges(u.elements.filter(P),w.screen.elements.filter(P)):[];const T=[w.steadied?"":"status bar",w.screen?.masks.length?"Buoy's overlay":"",k.length?"the parts you asked to ignore":""].filter(Boolean);const I=T.join(" and ");const O=I?`; ${I} left out`:"";const N=b.length?`
|
|
1059
|
+
${n.join("\n")}`}function registerScreenshotTools(e,n){e.registerTool("screenshot_component",{_meta:coreTool(),description:"Take a screenshot of the app. Pass compareTo with a saved PNG path to view both screens. It first says what changed, measured: where, which element, and the exact colors before and now. Then the saved file, then the live screen. Leave query and nativeTag out for the whole screen. To crop a React Native view on an iOS Sim, pass query or nativeTag. The tool may scroll that view into sight. Use describe_screen to read text or find controls. Pictures use more tokens. Run the MCP on the device host.",inputSchema:{compareTo:import_zod12.z.string().min(1).optional().describe("A saved PNG file to view next to this screen. Use a full path or a path from the project root. The file stays as it is."),query:import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:import_zod12.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),serial:import_zod12.z.string().optional().describe("Android: the adb serial, when several devices are connected."),baseline:import_zod12.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_zod12.z.boolean().optional().describe("With baseline: save the current screen as the new baseline instead of comparing."),steady:import_zod12.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_zod12.z.number().min(0).max(1).optional().describe("With baseline: per-pixel color tolerance, 0 to 1. Default 0.1."),ignore:import_zod12.z.array(import_zod12.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_zod12.z.array(import_zod12.z.object({x:import_zod12.z.number(),y:import_zod12.z.number(),width:import_zod12.z.number(),height:import_zod12.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.compareTo){if(t.query||t.nativeTag!=null)return fail4("compareTo uses the whole screen. Leave query and nativeTag out.");if(t.baseline||t.updateBaseline!=null||t.steady!=null||t.threshold!=null||t.ignore!=null||t.ignoreAreas!=null)return fail4("compareTo shows both pictures. Use baseline for diff options.");return await compareFileShot(n,t.compareTo,t.deviceId,t.serial)}if(t.baseline){if(t.query||t.nativeTag!=null)return fail4("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:b,host:_,online:$}=await hostTarget(n,t.deviceId,{serial:t.serial},{allowOffline:true});let k=Buffer.alloc(0);const S=await maskSecrets(n,b,$,async()=>k=await hostPng(_));const E={length:S.covered};const A=await toSmallImage(S.png);const P=_.platform==="ios-device"&&isAllBlack(k)?` ${BLACK_PHONE_NOTE}`:"";return{content:[{type:"image",data:A.data,mimeType:A.mimeType},{type:"text",text:`Whole screen of ${hostLabel(_)}, ${A.width}\xD7${A.height}px.${E.length?` ${E.length} element(s) showing a secret were blacked out.`:""}${P}`}]}}const s=n.requireProDevice(t.deviceId);await n.callAction(TOOL_ID,"setSilentTracking",{enabled:true},s).catch(()=>{});let i=t.nativeTag;const r=i==null&&t.query?await exactTag(n,s,t.query):null;if(r)i=r.tag;const o=await n.callAction(TOOL_ID,"locateComponent",{query:i==null?t.query:void 0,nativeTag:i,scrollIntoView:t.scrollIntoView??true,margin:MARGIN_PT},s);let a=o;if(!a.matched&&(a.reason==="no-renders"||a.reason==="no-match")){const b=await n.callAction(TOOL_ID,"describeScreen",{},s).catch(()=>null);const _=b?.elements?pickDescribeScreenMatch(b.elements,t.query,i):void 0;if(_?.frame&&b?.screen){a={matched:true,live:true,rect:frameToRect(_.frame,b.screen),screen:b.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 fail4(reasonMessage(a,t.query)+candidatesText(a.candidates))}if(i==null&&t.query){const b=t.query.toLowerCase();const _=a.candidates?.find(A=>A.nativeTag===a.nativeTag);const $=await n.callAction(TOOL_ID,"describeScreen",{},s,{timeoutMs:4e3}).catch(()=>null);const k=$?.elements?.find(A=>A.nativeTag===a.nativeTag);const S=[a.testID,a.nativeID,a.componentName,_?.testID,_?.nativeID,_?.componentName,k?.testID,k?.nativeID,k?.text,k?.label].filter(Boolean).map(A=>String(A).toLowerCase());const E=S.some(A=>A.includes(b)||A.length>3&&b.includes(A));if(!E){return fail4(`"${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:l}=await hostTarget(n,s);if(l.platform!=="ios")return fail4("Cropping one component works on the iOS Simulator. Leave query out for a whole-screen picture.");let c={ok:false};const u=await maskSecrets(n,s,true,async()=>{c=await captureIosFramebuffer(l.udid);return c.ok&&c.srcPath?import_node_fs21.promises.readFile(c.srcPath):Buffer.alloc(0)});if(u.covered&&c.ok&&c.srcPath)await import_node_fs21.promises.writeFile(c.srcPath,u.png);if(!c.ok||!c.srcPath||!c.width||!c.height){return fail4(c.error??"Screenshot capture failed.")}const d=c.width/a.screen.width;const h=a.rect;const p=CAPTURE_PADDING_PT;const m=await cropToBuffer(c.srcPath,{width:c.width,height:c.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 fail4(m.error??"Crop failed.")}const y=a.componentName||r?.name||"component";const g=a.testID??r?.testID;const f=g?` (testID: "${g}")`:"";const w=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 "${y}"${f} \u2014 ${m.width}\xD7${m.height}px.${w}`;return{content:[{type:"image",data:m.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:v}]}}catch(s){return fail4(errMsg3(s))}})}async function maskSecrets(e,n,t,s){const i=typeof s==="function"?s:async()=>s;const r=secretValues();if(!r.length)return{png:await i(),covered:0};const o=t?await secretBoxes(e,n,r):null;const a=await i();const l=t?await secretBoxes(e,n,r):null;if(o===null||l===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 c=[...o,...l];if(!a.length)return{png:a,covered:0};return{png:c.length?blackOut(a,c):a,covered:Math.max(o.length,l.length)}}async function secretBoxes(e,n,t){const s=await e.callAction(TOOL_ID,"describeScreen",{includeHidden:false,includeBuoy:true,fullText:true},n,{timeoutMs:4e3}).catch(()=>null);if(!s?.screen)return null;const i=typedSecretValues().length>0;const r=[];for(const o of s.elements??[]){const a=o.value;const l=[o.text,o.label,a].filter(h=>h!=null&&h!=="").map(String);const c=l.some(h=>t.some(p=>showsSecret(h,p)));const u=i&&o.control==="text"&&a===void 0;if(!c&&!u)continue;const d=o.frame?.width&&o.frame.height?fractionFrame(o,s.screen):void 0;if(!d)return null;r.push({...d,pixels:0})}return r}function showsSecret(e,n){const t=e.replace(/\s+/g," ");const s=n.replace(/\s+/g," ").trim();if(t.includes(s))return true;const i=t.replace(/(…|\.\.\.)$/,"");for(let r=s.length-1;r>=MIN_SECRET_LENGTH;r--)if(i.endsWith(s.slice(0,r)))return true;return false}function blackOut(e,n){const t=import_pngjs4.PNG.sync.read(e);for(const s of n){const i=Math.max(0,Math.floor((s.x-.01)*t.width));const r=Math.min(t.width,Math.ceil((s.x+s.width+.01)*t.width));const o=Math.max(0,Math.floor((s.y-.005)*t.height));const a=Math.min(t.height,Math.ceil((s.y+s.height+.005)*t.height));for(let l=o;l<a;l++){for(let c=i;c<r;c++){const u=(l*t.width+c)*4;t.data[u]=t.data[u+1]=t.data[u+2]=0;t.data[u+3]=255}}}return import_pngjs4.PNG.sync.write(t)}async function exactTag(e,n,t){const s=await e.callAction(TOOL_ID,"describeScreen",{},n,{timeoutMs:4e3}).catch(()=>null);const i=s?.elements??[];const r=c=>({tag:c.nativeTag,name:c.name,testID:c.testID});const o=i.filter(c=>c.testID===t&&c.nativeTag!=null);if(o.length===1)return r(o[0]);const a=t.trim().toLowerCase();const l=i.filter(c=>(c.text??c.label??"").trim().toLowerCase()===a&&c.nativeTag!=null);return l.length===1?r(l[0]):null}async function readComparisonPng(e){const n=32*1024*1024;const t=i=>{if(!i.isFile()||i.size>n)throw new Error("Use a PNG file under 32 MB.")};t(await import_node_fs21.promises.stat(e));const s=await import_node_fs21.promises.open(e,import_node_fs21.constants.O_RDONLY|import_node_fs21.constants.O_NONBLOCK);try{t(await s.stat());const i=[];let r=0;while(r<=n){const c=Buffer.alloc(Math.min(64*1024,n+1-r));const{bytesRead:u}=await s.read(c,0,c.length,null);if(!u)break;r+=u;if(r>n)throw new Error("Use a PNG file under 32 MB.");i.push(c.subarray(0,u))}const o=Buffer.concat(i,r);if(o.length<24||o.subarray(0,8).toString("hex")!=="89504e470d0a1a0a")throw new Error("The saved file must be a PNG.");const a=o.readUInt32BE(16),l=o.readUInt32BE(20);if(!a||!l||a*l>32e6)throw new Error("Use a PNG with at most 32 million pixels.");return o}finally{await s.close()}}async function compareFileShot(e,n,t,s){const i=n==="~"?import_node_os16.default.homedir():n.startsWith("~/")?import_node_path22.default.join(import_node_os16.default.homedir(),n.slice(2)):n;const r=await readComparisonPng(import_node_path22.default.resolve(process.env.BUOY_PROJECT_ROOT||process.cwd(),i));const{id:o,host:a,online:l}=await hostTarget(e,t,{serial:s},{allowOffline:true});const c=await maskSecrets(e,o,l,()=>hostPng(a));const u=await toSmallImage(r);const d=await toSmallImage(c.png);const h=await measureChange(e,o,l,r,c.png);return{content:[...h?[{type:"text",text:h}]:[],{type:"text",text:`Saved file: ${n} (${u.width}x${u.height}).`},{type:"image",data:u.data,mimeType:u.mimeType},{type:"text",text:`Current screen (${d.width}x${d.height}). Compare it with the saved file above.${c.covered?" Fields with secrets are hidden in this screen.":""}`},{type:"image",data:d.data,mimeType:d.mimeType}]}}async function measureChange(e,n,t,s,i){const r=.06;let o;try{o=diffScreens(s,i,{ignoreTop:r})}catch{return null}if(!o.boxes.length)return"Measured: no change outside the status bar.";let a=[];if(t){try{const c=await e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:3e3,silent:true});const u=c?.screen??{width:1,height:1};a=(c?.elements??[]).map(d=>({name:d.name,words:d.text??d.label,testID:d.testID,frame:fractionFrame(d,u)}))}catch{}}const l=o.boxes.slice(0,3).map(c=>{const u=elementsIn(c,a)[0];const d=boxColors(s,i,c,r);return`- ${describeSpot(c)}${u?`, ${u}`:""}${d?`: main color ${d.before} before, ${d.after} now`:""}.`});return[`Measured: ${o.percent.toFixed(1)}% of the screen changed, in ${o.boxes.length} area${o.boxes.length===1?"":"s"}.`,...l].join("\n")}function baselinePath(e){const n=e.trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"")||"screen";return import_node_path22.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 s of n){const i=s.frame;if(!i)continue;const r=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||r*o/a<.5)continue;t.push({label:labelOf(s),area:a,words:!!s.words})}return[...new Set(t.sort((s,i)=>Number(i.words)-Number(s.words)||s.area-i.area).map(s=>s.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 s=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 r=new Set([...s.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=s.get(d);if(p){if(p.words!==h.words)a.push(`changed: ${labelOf({...h,words:void 0})} "${p.words}" \u2192 "${h.words}"`)}else if(!r.has(h.words))a.push(`appeared: ${labelOf(h)}`)}for(const[d,h]of s)if(!i.has(d)&&!o.has(h.words))a.push(`gone: ${labelOf(h)}`);const l=[...s.entries()].filter(([d,h])=>!i.has(d)&&!o.has(h.words)).map(([,d])=>d);const c=[...i.entries()].filter(([d,h])=>!s.has(d)&&!r.has(h.words)).map(([,d])=>d);const u=(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 c){const h=l.find(m=>m.name===d.name&&u(m.frame,d.frame));if(!h)continue;l.splice(l.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,s,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 r=s?.screen??t?.screen;if(!r)return null;const o=new Set((s?.elements??[]).map(u=>u.nativeTag));const a=[];for(const u of t?.elements??[]){if(o.has(u.nativeTag))continue;const d=fractionFrame(u,r);if(d)a.push({x:d.x-.01,y:d.y-.01,width:d.width+.02,height:d.height+.02,pixels:0})}const l=(s?.elements??[]).map(u=>{const d=scrubSecrets((u.text??u.label??"").replace(/[\s\u200b\u00a0]+/g," ").trim());return{name:u.name,words:d||void 0,testID:u.testID?scrubSecrets(u.testID):void 0,frame:fractionFrame(u,r)}});const c=i?.path??i?.pathname;return{elements:l,masks:a,route:c?scrubSecrets(c):void 0}}catch{return null}}async function withFrozenClock(e,n,t,s){let i;let r;let o;try{await e.callAction("clock","freeze",{time:t},n);await new Promise(l=>setTimeout(l,1300));i=await s()}catch(l){r=l}finally{try{await e.callAction("clock","reset",{},n)}catch(l){o=l}}const a=o?` WARNING: putting the app's clock back failed (${errMsg3(o)}); run clock_action reset.`:"";if(r)throw new Error(`${errMsg3(r)}${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:s,host:i}=await hostTarget(e,t.deviceId,{serial:t.serial});const r=baselinePath(n);const o=r.replace(/\.png$/,".json");const a=import_node_path22.default.relative(process.env.BUOY_PROJECT_ROOT||process.cwd(),r);let l=null;try{l=await import_node_fs21.promises.readFile(r)}catch{}const c=!l||!!t.update;const u=c?{}:JSON.parse(await import_node_fs21.promises.readFile(o,"utf8").catch(()=>"{}"));const d=hostLabel(i);const h=e.connectedDevices().find(L=>L.deviceId===s);const p=h?.tools.some(L=>L.toolId==="clock")??false;const m=p&&hasProAccess(h);const y=t.steady??m;let g="";const f=async()=>{const{result:L,steadied:W}=await withSteadyStatusBar(i,async()=>({png:(await maskSecrets(e,s,true,()=>hostPng(i))).png,screen:await readScreen(e,s)}));return{...L,steadied:W}};let w;if(y&&m){const L=await e.callAction("clock","getState",{},s).catch(()=>null);if(L?.mode==="real"){const W=c||!u.steadyTime?Math.floor(Date.now()/6e4)*6e4:u.steadyTime;u.steadyTime=W;const Q=await withFrozenClock(e,s,W,f);w=Q.result;g+=Q.note}else{w=await f();g+=" The app's clock was already overridden, so it was left as it was."}}else{w=await f();if(y&&!m)g+=p?" (steady freezes the app clock, which needs Buoy Pro; times on screen may differ.)":" (steady needs the Clock tool in the app; times on screen may differ.)"}const v={width:w.png.readUInt32BE(16),height:w.png.readUInt32BE(20)};if(c){await import_node_fs21.promises.mkdir(import_node_path22.default.dirname(r),{recursive:true});await import_node_fs21.promises.writeFile(r,w.png);const L={...u,device:d,size:v,route:w.screen?.route,elements:w.screen?.elements};await import_node_fs21.promises.writeFile(o,JSON.stringify(L,null,1));const W=await toSmallImage(w.png);const Q=`${l?"Replaced":"Saved"} baseline "${n}"`;return{outcome:{saved:true,changed:false,percent:0,texts:0,summary:`${Q} (first run)`},result:{content:[{type:"image",data:W.data,mimeType:W.mimeType},{type:"text",text:`${l?"Replaced":"Saved"} baseline "${n}" (${a}) from ${d}${w.screen?.route?` on ${w.screen.route}`:""}. Call again with the same baseline to compare.${g}`}]}}}const b=[];if(u.device&&u.device!==d)b.push(`The baseline was saved on ${u.device}; this is ${d}.`);if(u.route&&w.screen?.route&&u.route!==w.screen.route)b.push(`The baseline was on ${u.route}; the app is now on ${w.screen.route}.`);const _=[...t.ignoreAreas??[]];const $=new Set(t.ignore??[]);const k=new Set($);for(const L of[...u.elements??[],...w.screen?.elements??[]]){if(!L.testID||!$.has(L.testID)||!L.frame)continue;k.delete(L.testID);_.push({x:L.frame.x-.005,y:L.frame.y-.005,width:L.frame.width+.01,height:L.frame.height+.01})}if(k.size)b.push(`Not on screen, so not left out: ${[...k].join(", ")}.`);const S=i.platform==="android"?[]:[{x:.28,y:0,width:.44,height:.055}];const E=diffScreens(l,w.png,{threshold:t.threshold,masks:[...w.screen?.masks??[],..._,...S],ignoreTop:w.steadied?0:.065});const A=L=>!!L&&_.some(W=>L.x+L.width/2>=W.x&&L.x+L.width/2<=W.x+W.width&&L.y+L.height/2>=W.y&&L.y+L.height/2<=W.y+W.height);const P=L=>(!L.testID||!$.has(L.testID))&&!A(L.frame);const x=u.elements&&w.screen?textChanges(u.elements.filter(P),w.screen.elements.filter(P)):[];const T=[w.steadied?"":"status bar",w.screen?.masks.length?"Buoy's overlay":"",_.length?"the parts you asked to ignore":""].filter(Boolean);const I=T.join(" and ");const O=I?`; ${I} left out`:"";const N=b.length?`
|
|
1060
1060
|
|
|
1061
1061
|
\u26A0\uFE0F ${b.join(" ")}`:"";if(E.changedPixels===0&&x.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}${O}).${g}${N}`)}}const F=E.boxes.map((L,W)=>{const Q=w.screen?elementsIn(L,w.screen.elements):[];const J=`${describeSpot(L)} (x ${L.x.toFixed(2)}, y ${L.y.toFixed(2)}, ${L.width.toFixed(2)}\xD7${L.height.toFixed(2)})`;return`${W+1}. ${J}${Q.length?`: ${Q.join(", ")}`:""}`});const X=await toSmallImage(E.diffPng);const C=E.percent<.01&&E.changedPixels>0?"<0.01":E.percent.toFixed(2);const U=F[0]?`; ${F[0].replace(/^1\. /,"")}`:"";const j=x[0]?`; ${x[0]}`:"";const H=E.changedPixels===0?`no pixel change from baseline "${n}", but text changed`:`${C}% changed from baseline "${n}"`;const V={saved:false,changed:true,percent:E.percent,texts:x.length,summary:`${H}${U}${j}`};return{outcome:V,result:{content:[{type:"image",data:X.data,mimeType:X.mimeType},{type:"text",text:`**Changed from baseline "${n}": ${E.percent<.01&&E.changedPixels>0?"<0.01":E.percent.toFixed(2)}% of the screen** (${E.changedPixels} px${O}). Changes are red in the picture.${g}`+(F.length?`
|
|
1062
1062
|
|
|
@@ -1066,14 +1066,14 @@ ${F.join("\n")}`:"")+(x.length?`
|
|
|
1066
1066
|
Text:
|
|
1067
1067
|
${x.map(L=>`- ${L}`).join("\n")}`:"")+`${N}
|
|
1068
1068
|
|
|
1069
|
-
If this change is expected, save it with updateBaseline: true.`}]}}}var import_zod12,import_node_os16,import_node_fs21,import_node_path22,import_pngjs4,TOOL_ID,MARGIN_PT,CAPTURE_PADDING_PT,fail4,errMsg3,labelOf;var init_screenshot=__esm({"src/tools/packs/screenshot.ts"(){"use strict";import_zod12=require("zod");init_policy();init_packKit();init_simctl();init_screenshotLocate();init_hostDevice();init_imageDiff();import_node_os16=__toESM(require("os"));import_node_fs21=require("fs");import_node_path22=__toESM(require("path"));init_hostKit();init_secrets();init_iosDevice();import_pngjs4=__toESM(require_png());TOOL_ID="highlight-updates";MARGIN_PT=12;CAPTURE_PADDING_PT=6;fail4=e=>({isError:true,content:[{type:"text",text:e}]});errMsg3=e=>e instanceof Error?e.message:String(e);labelOf=e=>`${e.name??"View"}${e.words?` "${e.words.length>40?`${e.words.slice(0,40)}\u2026`:e.words}"`:""}${e.testID?` [${e.testID}]`:""}`}});async function listSavedFlows(){try{return(await import_node_fs22.promises.readdir(FLOW_DIR())).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5))}catch{return[]}}function matches(e,n){if(e.testID&&e.testID===n)return true;const t=n.toLowerCase();return[e.testID,e.text,e.label,e.name].filter(Boolean).some(s=>String(s).toLowerCase().includes(t))}function normalizeStep(e){if(!e||typeof e!=="object"||Array.isArray(e))return e;const n={...e};const t=o=>{if(!o||typeof o!=="object"||Array.isArray(o))return o;const a=o;return[a.testID,a.text,a.query,a.label,a.name].find(l=>typeof l==="string")??o};for(const o of["tap","into","waitFor","waitGone","expect","expectGone"])n[o]=t(n[o]);for(const o of Object.keys(n))if(n[o]===void 0)delete n[o];if(typeof n.into==="string")return n;const s=typeof n.type==="string"&&STEP_KINDS.includes(n.type)?"type":typeof n.action==="string"&&STEP_KINDS.includes(n.action)?"action":null;if(!s)return n;const i=n[s];delete n[s];const r=[n.query,n.text,n.target,n.selector,n.element,n.testID].map(t).find(o=>typeof o==="string");for(const o of["query","text","target","selector","element","testID"])delete n[o];if(i==="reload")n.reload=true;else if(i==="swipe"){n.swipe=n.direction??r??"up";delete n.direction}else if(i==="wait")n.waitMs=n.waitMs??n.ms??n.durationMs??n.timeoutMs;else if(r!==void 0&&n[i]===void 0)n[i]=r;delete n.ms;delete n.durationMs;return n}function registerFlowTools(e,n){e.registerTool("run_flow",{_meta:coreTool(),description:"Run supported UI actions and element checks in sequence, with per-step results. Stops on the first failed step unless continueOnFailure is true. Depending on include, attempts to return the final element tree and newly observed captured network/console entries. Background activity can appear in that output; it does not establish causation. Taps use exposed app handlers by default; a tap with no handler match (a native tab bar) falls back to a real touch on that native label. Pass real: true to make every tap a real touch. A tap waits up to 2 s for its target to show, so steps rarely need waitMs between them. A swipe step is a real swipe; add untilEnd: true to scroll to the end of a list. Review failures and check the expected behavior. To wait for something slow, use waitFor or waitGone with a timeoutMs, not a long waitMs. Needs the app interaction integration. Works on the free tier.",inputSchema:flowInput},async t=>runFlow(t));runFlow=async t=>{const s=Date.now();try{let i=t.steps;if(t.flow){if(!FLOW_NAME.test(t.flow))return fail5(`Flow names use letters, numbers, - and _ (got "${t.flow}").`);try{i=JSON.parse(await import_node_fs22.promises.readFile(import_node_path23.default.join(FLOW_DIR(),`${t.flow}.json`),"utf8")).steps}catch{const x=await listSavedFlows();return fail5(`No saved flow "${t.flow}". ${x.length?`Saved flows: ${x.join(", ")}.`:'No flows are saved yet; run one with save:"name" first.'}`)}}if(!i||i.length===0)return fail5('Pass steps, or flow:"name" to replay a saved flow.');if(t.save&&!FLOW_NAME.test(t.save))return fail5(`Flow names use letters, numbers, - and _ (got "${t.save}").`);const r=n.requireProDevice(t.deviceId);const o=t.include??"all";const a=nativeKit(n);let l=null;const c=()=>l??=hostTarget(n,r).then(async x=>{await a.primeScreen(r,x.host,x.online);return x});let u;const d=async(x,T,I,O)=>{const N=T?await a.buoyCover(r,x,I):null;if(N)throw new Error(N);const F=O?600:void 0;const X=await a.touch(x,I,F,u);if(X!==void 0)u=X;await a.mark(r,x,F?"hold":"tap",[[I.x,I.y]],void 0,F??150,X);await delay3(120);return`${O?"long-pressed":"tapped"} ${I.what} (real touch)`};const h=async(x,T,I)=>{const{host:O,online:N}=await c();const F=Date.now();let X=false;for(;;){let C=await a.findSpot(r,O,x,N,"app");if("miss"in C&&!C.tie&&(X||!N||Date.now()-F>=600)){if(!X){X=true;C=await a.findSpot(r,O,x,N,"native")}if("miss"in C&&!C.tie)C=await a.findSpot(r,O,x,N,"loose")}if(!("miss"in C))return d(O,N,C,T);if(C.tie||Date.now()-F>=I)throw new Error(C.miss);await delay3(POLL_MS)}};const p=async(x,T,I)=>{let O;try{O=await c()}catch{return null}if(O.host.platform==="ios-device")return null;const N=await a.findSpot(r,O.host,x,O.online,I?"nativeExact":"native").catch(()=>null);if(!N||"miss"in N)return null;return d(O.host,O.online,N,T)};const m=async()=>{const x=await n.callAction(UI_TOOL_ID,"describeScreen",{},r);return x?.elements??[]};const y=async()=>{try{const x=await n.callAction(APP_TOOL_ID,"ping",{},r,{timeoutMs:REALM_PROBE_MS,silent:true});return typeof x?.startedAt==="number"?x.startedAt:null}catch{return null}};const g=async(x,T)=>{const I=Date.now();while(Date.now()-I<T){const O=await y();if(O!==null&&O!==x)return Date.now()-I;await delay3(POLL_MS)}return null};const f=new Set;const w=new Set;const v=new Set((n.connectedDevices().find(x=>x.deviceId===r)?.tools??[]).map(x=>x.toolId));if(o==="all"){if(v.has("console")){const{payload:x}=await n.getSnapshot("console",r).catch(()=>({payload:null}));const T=x?.entries??[];for(const I of T)f.add(I.id)}if(v.has("network")){const{payload:x}=await n.getSnapshot("network",r).catch(()=>({payload:null}));const T=Array.isArray(x)?x:x?.events??[];for(const I of T)w.add(I.id)}}const b=async(x,T)=>{const I=Date.now();let O=false;for(;;){if(Date.now()-I>=T||Date.now()-s>MAX_FLOW_MS){return{ok:false,ms:Date.now()-I,unread:!O}}let N;try{N=await m();O=true}catch{await delay3(POLL_MS);continue}if(x(N))return{ok:true,ms:Date.now()-I,unread:false};await delay3(POLL_MS)}};const k=x=>`could not read the screen within ${x}ms, so this step is unknown`;const $=[];let _=null;let S=0;for(let x=0;x<i.length;x++){const T=i[x];const I=`${x+1}`;const O=T.timeoutMs??DEFAULT_WAIT_MS;if(Date.now()-s>MAX_FLOW_MS){$.push(`${I} \u2717 flow exceeded ${MAX_FLOW_MS/1e3}s \u2014 stopped here`);_=x;break}const N=[];const F=[];const X=T.reload||T.tap||T.swipe||T.type!==void 0||T.waitMs||T.waitFor||T.waitGone||T.expect||T.expectGone||T.snapshot;if(!X){$.push(`${x+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${T.set!==void 0?" (`set` needs `tap` naming the control)":""}`);_=_??x;if(!t.continueOnFailure)break;continue}try{if(T.reload){const C=await y();await n.callAction(APP_TOOL_ID,"reloadApp",{strategy:"auto"},r);await delay3(TEARDOWN_GRACE_MS);const U=await g(C,Math.max(O,2e4));if(U==null){N.push("app did not come back after reload")}else{F.push(`reloaded (${U}ms)`);await b(j=>j.length>0,1e4)}}if(T.type!==void 0){const C=T.into??T.tap;if(!C){N.push("`type` needs `into` (which input to type into)")}else{const U=await n.callAction(UI_TOOL_ID,"tapElement",{...selector(C),text:fillSecrets(T.type)},r);if(!U?.tapped){N.push(U?.reason??`could not type into "${C}"`)}else if(U.matched?.via&&U.matched.via!=="onChangeText"){N.push(`"${C}" is not a text input \u2014 it ran ${U.matched.via} instead, and the text was not entered`)}else{F.push(`typed ${JSON.stringify(T.type)} into "${C}"`)}}}else if(T.tap&&(T.real??t.real)&&T.set===void 0){F.push(await h(T.tap,T.longPress,T.timeoutMs??3e3))}else if(T.tap){const C={...selector(T.tap)};if(T.longPress)C.longPress=true;if(T.set!==void 0)C.value=T.set;let U=await n.callAction(UI_TOOL_ID,"tapElement",C,r);for(let L=0;!U?.tapped&&/No element matched/.test(U?.reason??"")&&L<TAP_SETTLE_MS;L+=POLL_MS){await delay3(POLL_MS);U=await n.callAction(UI_TOOL_ID,"tapElement",C,r)}const j=/isn't showing|No element matched|no JS press handler|can't be driven in JS/;const H=/equally well/.test(U?.reason??"");const V=!U?.tapped&&T.set===void 0&&(H||j.test(U?.reason??""))?await p(T.tap,T.longPress,H):null;if(V){F.push(V)}else if(!U?.tapped){N.push(U?.reason??`no element matched "${T.tap}"`)}else{const L=U.matched;const W=L?.text??L?.label??L?.name??T.tap;if(T.set!==void 0&&L?.via&&L.via!=="onValueChange"){N.push(`"${W}" is not a toggle/slider \u2014 it ran ${L.via} instead, so it was not set to ${JSON.stringify(T.set)}`)}else{F.push(T.set!==void 0?`set "${W}" \u2192 ${JSON.stringify(L?.value??T.set)}`:`${T.longPress?"long-pressed":"tapped"} "${W}"`)}}}if(!N.length&&T.swipe){const{host:C,online:U}=await c();const j=await a.screenOf(C);const H=T.swipe;const{from:V,to:L}=swipePoints(H,!!T.untilEnd);const W=[V[0]*j.width,V[1]*j.height];const Q=[L[0]*j.width,L[1]*j.height];const J=U?await a.buoyCover(r,C,{x:W[0],y:W[1]}):null;if(J)throw new Error(J);if(T.untilEnd){const{swipes:te,atEnd:ae,lastRow:ne}=await a.scrollToEnd(r,C,W,Q,250);if(ae)F.push(`swiped ${H} to the end (${te} swipes)${ne?`; the last row is "${ne}"`:""}`);else N.push(`swiped ${H} ${te} times and the screen was still moving`)}else{const te=await a.swipe(C,W,Q,300);await a.mark(r,C,"swipe",[W],[Q],300,te);await delay3(400);F.push(`swiped ${H}`)}}if(!N.length&&T.waitMs){await delay3(T.waitMs);F.push(`waited ${T.waitMs}ms`)}if(!N.length&&T.waitFor){const C=await b(U=>U.some(j=>matches(j,T.waitFor)),O);if(C.ok)F.push(`"${T.waitFor}" appeared (${C.ms}ms)`);else N.push(C.unread?k(O):`"${T.waitFor}" never appeared within ${O}ms`)}if(!N.length&&T.waitGone){const C=await b(U=>!U.some(j=>matches(j,T.waitGone)),O);if(C.ok)F.push(`"${T.waitGone}" gone (${C.ms}ms)`);else N.push(C.unread?k(O):`"${T.waitGone}" was still there after ${O}ms`)}if(!N.length&&T.expect){const C=await b(U=>U.some(j=>matches(j,T.expect)),T.timeoutMs??ASSERT_SETTLE_MS);if(C.ok)F.push(`"${T.expect}" is on screen`);else N.push(C.unread?k(T.timeoutMs??ASSERT_SETTLE_MS):`expected "${T.expect}" on screen \u2014 not found`)}if(!N.length&&T.expectGone){const C=await b(U=>!U.some(j=>matches(j,T.expectGone)),T.timeoutMs??ASSERT_SETTLE_MS);if(C.ok)F.push(`"${T.expectGone}" is gone`);else N.push(C.unread?k(T.timeoutMs??ASSERT_SETTLE_MS):`expected "${T.expectGone}" to be gone \u2014 still on screen`)}if(!N.length&&T.snapshot){const{outcome:C}=await runBaseline(n,T.snapshot,{deviceId:r,update:t.updateSnapshots});const U=T.maxChange??.05;if(C.changed&&(C.texts>0||C.percent>U)){N.push(`${C.summary}${C.texts>0?"":` (more than ${U}%)`}. Run screenshot_component with baseline "${T.snapshot}" to see the diff, or updateSnapshots: true if it is expected.`)}else F.push(C.summary)}}catch(C){N.push(errMsg4(C))}if(N.length){$.push(`${I} \u2717 ${describeStep(T)} \u2014 ${N.join("; ")}`);_=_??x;if(!t.continueOnFailure)break}else{S++;$.push(`${I} \u2713 ${F.join(", ")||describeStep(T)}`)}}const E=((Date.now()-s)/1e3).toFixed(1);const A=_===null?`**Flow: ${S}/${i.length} steps OK (${E}s)**`:`**Flow FAILED at step ${_+1} \u2014 ${S}/${i.length} steps OK (${E}s)**`;const P=[A,$.join("\n")];if(o!=="none"){P.push(`**Screen now:**${await observeAfterAction(n,r,void 0,0)}`)}if(o==="all"){if(v.has("network")){const{payload:x}=await n.getSnapshot("network",r).catch(()=>({payload:null}));const T=Array.isArray(x)?x:x?.events??[];const I=T.filter(O=>!w.has(O.id)).slice(-15);if(I.length){P.push(`**Network during this flow (${I.length}):**
|
|
1069
|
+
If this change is expected, save it with updateBaseline: true.`}]}}}var import_zod12,import_node_os16,import_node_fs21,import_node_path22,import_pngjs4,TOOL_ID,MARGIN_PT,CAPTURE_PADDING_PT,fail4,errMsg3,labelOf;var init_screenshot=__esm({"src/tools/packs/screenshot.ts"(){"use strict";import_zod12=require("zod");init_policy();init_packKit();init_simctl();init_screenshotLocate();init_hostDevice();init_imageDiff();import_node_os16=__toESM(require("os"));import_node_fs21=require("fs");import_node_path22=__toESM(require("path"));init_hostKit();init_secrets();init_iosDevice();import_pngjs4=__toESM(require_png());TOOL_ID="highlight-updates";MARGIN_PT=12;CAPTURE_PADDING_PT=6;fail4=e=>({isError:true,content:[{type:"text",text:e}]});errMsg3=e=>e instanceof Error?e.message:String(e);labelOf=e=>`${e.name??"View"}${e.words?` "${e.words.length>40?`${e.words.slice(0,40)}\u2026`:e.words}"`:""}${e.testID?` [${e.testID}]`:""}`}});async function listSavedFlows(){try{return(await import_node_fs22.promises.readdir(FLOW_DIR())).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5))}catch{return[]}}function matches(e,n){if(e.testID&&e.testID===n)return true;const t=n.toLowerCase();return[e.testID,e.text,e.label,e.name].filter(Boolean).some(s=>String(s).toLowerCase().includes(t))}function normalizeStep(e){if(!e||typeof e!=="object"||Array.isArray(e))return e;const n={...e};const t=o=>{if(!o||typeof o!=="object"||Array.isArray(o))return o;const a=o;return[a.testID,a.text,a.query,a.label,a.name].find(l=>typeof l==="string")??o};for(const o of["tap","into","waitFor","waitGone","expect","expectGone"])n[o]=t(n[o]);for(const o of Object.keys(n))if(n[o]===void 0)delete n[o];if(typeof n.into==="string")return n;const s=typeof n.type==="string"&&STEP_KINDS.includes(n.type)?"type":typeof n.action==="string"&&STEP_KINDS.includes(n.action)?"action":null;if(!s)return n;const i=n[s];delete n[s];const r=[n.query,n.text,n.target,n.selector,n.element,n.testID].map(t).find(o=>typeof o==="string");for(const o of["query","text","target","selector","element","testID"])delete n[o];if(i==="reload")n.reload=true;else if(i==="swipe"){n.swipe=n.direction??r??"up";delete n.direction}else if(i==="wait")n.waitMs=n.waitMs??n.ms??n.durationMs??n.timeoutMs;else if(r!==void 0&&n[i]===void 0)n[i]=r;delete n.ms;delete n.durationMs;return n}function registerFlowTools(e,n){e.registerTool("run_flow",{_meta:coreTool(),description:"Run supported UI actions and element checks in sequence, with per-step results. Stops on the first failed step unless continueOnFailure is true. Depending on include, attempts to return the final element tree and newly observed captured network/console entries. Background activity can appear in that output; it does not establish causation. Taps use exposed app handlers by default; a tap with no handler match (a native tab bar) falls back to a real touch on that native label. Pass real: true to make every tap a real touch. A tap waits up to 2 s for its target to show, so steps rarely need waitMs between them. A swipe step is a real swipe; add untilEnd: true to scroll to the end of a list. Review failures and check the expected behavior. To wait for something slow, use waitFor or waitGone with a timeoutMs, not a long waitMs. Needs the app interaction integration. Works on the free tier.",inputSchema:flowInput},async t=>runFlow(t));runFlow=async t=>{const s=Date.now();try{let i=t.steps;if(t.flow){if(!FLOW_NAME.test(t.flow))return fail5(`Flow names use letters, numbers, - and _ (got "${t.flow}").`);try{i=JSON.parse(await import_node_fs22.promises.readFile(import_node_path23.default.join(FLOW_DIR(),`${t.flow}.json`),"utf8")).steps}catch{const x=await listSavedFlows();return fail5(`No saved flow "${t.flow}". ${x.length?`Saved flows: ${x.join(", ")}.`:'No flows are saved yet; run one with save:"name" first.'}`)}}if(!i||i.length===0)return fail5('Pass steps, or flow:"name" to replay a saved flow.');if(t.save&&!FLOW_NAME.test(t.save))return fail5(`Flow names use letters, numbers, - and _ (got "${t.save}").`);const r=n.requireProDevice(t.deviceId);const o=t.include??"all";const a=nativeKit(n);let l=null;const c=()=>l??=hostTarget(n,r).then(async x=>{await a.primeScreen(r,x.host,x.online);return x});let u;const d=async(x,T,I,O)=>{const N=T?await a.buoyCover(r,x,I):null;if(N)throw new Error(N);const F=O?600:void 0;const X=await a.touch(x,I,F,u);if(X!==void 0)u=X;await a.mark(r,x,F?"hold":"tap",[[I.x,I.y]],void 0,F??150,X);await delay3(120);return`${O?"long-pressed":"tapped"} ${I.what} (real touch)`};const h=async(x,T,I)=>{const{host:O,online:N}=await c();const F=Date.now();let X=false;for(;;){let C=await a.findSpot(r,O,x,N,"app");if("miss"in C&&!C.tie&&(X||!N||Date.now()-F>=600)){if(!X){X=true;C=await a.findSpot(r,O,x,N,"native")}if("miss"in C&&!C.tie)C=await a.findSpot(r,O,x,N,"loose")}if(!("miss"in C))return d(O,N,C,T);if(C.tie||Date.now()-F>=I)throw new Error(C.miss);await delay3(POLL_MS)}};const p=async(x,T,I)=>{let O;try{O=await c()}catch{return null}if(O.host.platform==="ios-device")return null;const N=await a.findSpot(r,O.host,x,O.online,I?"nativeExact":"native").catch(()=>null);if(!N||"miss"in N)return null;return d(O.host,O.online,N,T)};const m=async()=>{const x=await n.callAction(UI_TOOL_ID,"describeScreen",{},r);return x?.elements??[]};const y=async()=>{try{const x=await n.callAction(APP_TOOL_ID,"ping",{},r,{timeoutMs:REALM_PROBE_MS,silent:true});return typeof x?.startedAt==="number"?x.startedAt:null}catch{return null}};const g=async(x,T)=>{const I=Date.now();while(Date.now()-I<T){const O=await y();if(O!==null&&O!==x)return Date.now()-I;await delay3(POLL_MS)}return null};const f=new Set;const w=new Set;const v=new Set((n.connectedDevices().find(x=>x.deviceId===r)?.tools??[]).map(x=>x.toolId));if(o==="all"){if(v.has("console")){const{payload:x}=await n.getSnapshot("console",r).catch(()=>({payload:null}));const T=x?.entries??[];for(const I of T)f.add(I.id)}if(v.has("network")){const{payload:x}=await n.getSnapshot("network",r).catch(()=>({payload:null}));const T=Array.isArray(x)?x:x?.events??[];for(const I of T)w.add(I.id)}}const b=async(x,T)=>{const I=Date.now();let O=false;for(;;){if(Date.now()-I>=T||Date.now()-s>MAX_FLOW_MS){return{ok:false,ms:Date.now()-I,unread:!O}}let N;try{N=await m();O=true}catch{await delay3(POLL_MS);continue}if(x(N))return{ok:true,ms:Date.now()-I,unread:false};await delay3(POLL_MS)}};const _=x=>`could not read the screen within ${x}ms, so this step is unknown`;const $=[];let k=null;let S=0;for(let x=0;x<i.length;x++){const T=i[x];const I=`${x+1}`;const O=T.timeoutMs??DEFAULT_WAIT_MS;if(Date.now()-s>MAX_FLOW_MS){$.push(`${I} \u2717 flow exceeded ${MAX_FLOW_MS/1e3}s \u2014 stopped here`);k=x;break}const N=[];const F=[];const X=T.reload||T.tap||T.swipe||T.type!==void 0||T.waitMs||T.waitFor||T.waitGone||T.expect||T.expectGone||T.snapshot;if(!X){$.push(`${x+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${T.set!==void 0?" (`set` needs `tap` naming the control)":""}`);k=k??x;if(!t.continueOnFailure)break;continue}try{if(T.reload){const C=await y();await n.callAction(APP_TOOL_ID,"reloadApp",{strategy:"auto"},r);await delay3(TEARDOWN_GRACE_MS);const U=await g(C,Math.max(O,2e4));if(U==null){N.push("app did not come back after reload")}else{F.push(`reloaded (${U}ms)`);await b(j=>j.length>0,1e4)}}if(T.type!==void 0){const C=T.into??T.tap;if(!C){N.push("`type` needs `into` (which input to type into)")}else{const U=await n.callAction(UI_TOOL_ID,"tapElement",{...selector(C),text:fillSecrets(T.type)},r);if(!U?.tapped){N.push(U?.reason??`could not type into "${C}"`)}else if(U.matched?.via&&U.matched.via!=="onChangeText"){N.push(`"${C}" is not a text input \u2014 it ran ${U.matched.via} instead, and the text was not entered`)}else{F.push(`typed ${JSON.stringify(T.type)} into "${C}"`)}}}else if(T.tap&&(T.real??t.real)&&T.set===void 0){F.push(await h(T.tap,T.longPress,T.timeoutMs??3e3))}else if(T.tap){const C={...selector(T.tap)};if(T.longPress)C.longPress=true;if(T.set!==void 0)C.value=T.set;let U=await n.callAction(UI_TOOL_ID,"tapElement",C,r);for(let L=0;!U?.tapped&&/No element matched/.test(U?.reason??"")&&L<TAP_SETTLE_MS;L+=POLL_MS){await delay3(POLL_MS);U=await n.callAction(UI_TOOL_ID,"tapElement",C,r)}const j=/isn't showing|No element matched|no JS press handler|can't be driven in JS/;const H=/equally well/.test(U?.reason??"");const V=!U?.tapped&&T.set===void 0&&(H||j.test(U?.reason??""))?await p(T.tap,T.longPress,H):null;if(V){F.push(V)}else if(!U?.tapped){N.push(U?.reason??`no element matched "${T.tap}"`)}else{const L=U.matched;const W=L?.text??L?.label??L?.name??T.tap;if(T.set!==void 0&&L?.via&&L.via!=="onValueChange"){N.push(`"${W}" is not a toggle/slider \u2014 it ran ${L.via} instead, so it was not set to ${JSON.stringify(T.set)}`)}else{F.push(T.set!==void 0?`set "${W}" \u2192 ${JSON.stringify(L?.value??T.set)}`:`${T.longPress?"long-pressed":"tapped"} "${W}"`)}}}if(!N.length&&T.swipe){const{host:C,online:U}=await c();const j=await a.screenOf(C);const H=T.swipe;const{from:V,to:L}=swipePoints(H,!!T.untilEnd);const W=[V[0]*j.width,V[1]*j.height];const Q=[L[0]*j.width,L[1]*j.height];const J=U?await a.buoyCover(r,C,{x:W[0],y:W[1]}):null;if(J)throw new Error(J);if(T.untilEnd){const{swipes:te,atEnd:ae,lastRow:ne}=await a.scrollToEnd(r,C,W,Q,250);if(ae)F.push(`swiped ${H} to the end (${te} swipes)${ne?`; the last row is "${ne}"`:""}`);else N.push(`swiped ${H} ${te} times and the screen was still moving`)}else{const te=await a.swipe(C,W,Q,300);await a.mark(r,C,"swipe",[W],[Q],300,te);await delay3(400);F.push(`swiped ${H}`)}}if(!N.length&&T.waitMs){await delay3(T.waitMs);F.push(`waited ${T.waitMs}ms`)}if(!N.length&&T.waitFor){const C=await b(U=>U.some(j=>matches(j,T.waitFor)),O);if(C.ok)F.push(`"${T.waitFor}" appeared (${C.ms}ms)`);else N.push(C.unread?_(O):`"${T.waitFor}" never appeared within ${O}ms`)}if(!N.length&&T.waitGone){const C=await b(U=>!U.some(j=>matches(j,T.waitGone)),O);if(C.ok)F.push(`"${T.waitGone}" gone (${C.ms}ms)`);else N.push(C.unread?_(O):`"${T.waitGone}" was still there after ${O}ms`)}if(!N.length&&T.expect){const C=await b(U=>U.some(j=>matches(j,T.expect)),T.timeoutMs??ASSERT_SETTLE_MS);if(C.ok)F.push(`"${T.expect}" is on screen`);else N.push(C.unread?_(T.timeoutMs??ASSERT_SETTLE_MS):`expected "${T.expect}" on screen \u2014 not found`)}if(!N.length&&T.expectGone){const C=await b(U=>!U.some(j=>matches(j,T.expectGone)),T.timeoutMs??ASSERT_SETTLE_MS);if(C.ok)F.push(`"${T.expectGone}" is gone`);else N.push(C.unread?_(T.timeoutMs??ASSERT_SETTLE_MS):`expected "${T.expectGone}" to be gone \u2014 still on screen`)}if(!N.length&&T.snapshot){const{outcome:C}=await runBaseline(n,T.snapshot,{deviceId:r,update:t.updateSnapshots});const U=T.maxChange??.05;if(C.changed&&(C.texts>0||C.percent>U)){N.push(`${C.summary}${C.texts>0?"":` (more than ${U}%)`}. Run screenshot_component with baseline "${T.snapshot}" to see the diff, or updateSnapshots: true if it is expected.`)}else F.push(C.summary)}}catch(C){N.push(errMsg4(C))}if(N.length){$.push(`${I} \u2717 ${describeStep(T)} \u2014 ${N.join("; ")}`);k=k??x;if(!t.continueOnFailure)break}else{S++;$.push(`${I} \u2713 ${F.join(", ")||describeStep(T)}`)}}const E=((Date.now()-s)/1e3).toFixed(1);const A=k===null?`**Flow: ${S}/${i.length} steps OK (${E}s)**`:`**Flow FAILED at step ${k+1} \u2014 ${S}/${i.length} steps OK (${E}s)**`;const P=[A,$.join("\n")];if(o!=="none"){P.push(`**Screen now:**${await observeAfterAction(n,r,void 0,0)}`)}if(o==="all"){if(v.has("network")){const{payload:x}=await n.getSnapshot("network",r).catch(()=>({payload:null}));const T=Array.isArray(x)?x:x?.events??[];const I=T.filter(O=>!w.has(O.id)).slice(-15);if(I.length){P.push(`**Network during this flow (${I.length}):**
|
|
1070
1070
|
`+I.map(O=>`${O.error||(O.status??0)>=400?"\u274C":"\u2705"} ${O.method??"GET"} ${O.path??O.url??"?"} \u2192 ${O.error??O.status??"pending"}`).join("\n"))}}if(v.has("console")){const{payload:x}=await n.getSnapshot("console",r).catch(()=>({payload:null}));const T=x?.entries??[];const I=T.filter(O=>!f.has(O.id)&&(O.level==="error"||O.level==="warning")).slice(-10);if(I.length){P.push(`**Console during this flow (${I.length}):**
|
|
1071
|
-
`+I.map(O=>`[${O.level==="error"?"ERR":"warn"}] ${cap(firstLines(scrubSecrets(O.message??"")),220)}`).join("\n"))}}}if(t.save&&_===null){await import_node_fs22.promises.mkdir(FLOW_DIR(),{recursive:true});const x=import_node_path23.default.join(FLOW_DIR(),`${t.save}.json`);await import_node_fs22.promises.writeFile(x,JSON.stringify({steps:i},null,2)+"\n");P.push(`Saved as flow "${t.save}" (${import_node_path23.default.relative(process.cwd(),x)}). Replay with run_flow({flow:"${t.save}"}).`)}else if(t.save){P.push(`Not saved: the flow failed. Fix it, then run it again with save:"${t.save}".`)}return _===null?text3(P.join("\n\n")):{...text3(P.join("\n\n")),isError:true}}catch(i){return fail5(errMsg4(i))}}}function selector(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function describeStep(e){if(e.tap)return`tap "${e.tap}"`;if(e.type!==void 0)return`type into "${e.into??"?"}"`;if(e.reload)return"reload";if(e.swipe)return`swipe ${e.swipe}${e.untilEnd?" to the end":""}`;if(e.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;if(e.expectGone)return`expectGone "${e.expectGone}"`;if(e.snapshot)return`snapshot "${e.snapshot}"`;return"step"}var import_zod13,import_node_fs22,import_node_path23,FLOW_DIR,FLOW_NAME,UI_TOOL_ID,APP_TOOL_ID,text3,fail5,errMsg4,delay3,POLL_MS,TAP_SETTLE_MS,DEFAULT_WAIT_MS,ASSERT_SETTLE_MS,TEARDOWN_GRACE_MS,REALM_PROBE_MS,MAX_FLOW_MS,STEP_KINDS,stepSchema,flowInput,runFlow,cap,firstLines;var init_flow=__esm({"src/tools/packs/flow.ts"(){"use strict";init_screenshot();init_secrets();import_zod13=require("zod");init_packKit();import_node_fs22=require("fs");import_node_path23=__toESM(require("path"));init_native();init_interact();init_hostKit();FLOW_DIR=()=>import_node_path23.default.join(process.cwd(),".buoy","flows");FLOW_NAME=/^[A-Za-z0-9_-]{1,64}$/;UI_TOOL_ID="highlight-updates";APP_TOOL_ID="app";text3=e=>({content:[{type:"text",text:e}]});fail5=e=>({isError:true,content:[{type:"text",text:e}]});errMsg4=e=>e instanceof Error?e.message:String(e);delay3=e=>new Promise(n=>setTimeout(n,e));POLL_MS=250;TAP_SETTLE_MS=2e3;DEFAULT_WAIT_MS=5e3;ASSERT_SETTLE_MS=2e3;TEARDOWN_GRACE_MS=1200;REALM_PROBE_MS=1500;MAX_FLOW_MS=12e4;STEP_KINDS=["tap","waitFor","waitGone","expect","expectGone","reload","wait","swipe"];stepSchema=import_zod13.z.preprocess(normalizeStep,import_zod13.z.object({tap:import_zod13.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:import_zod13.z.boolean().optional().describe("Make `tap` a long-press instead."),type:import_zod13.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod13.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:import_zod13.z.union([import_zod13.z.number(),import_zod13.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),swipe:import_zod13.z.enum(["up","down","left","right"]).optional().describe('A real swipe, the way the finger moves: "up" scrolls down the list.'),untilEnd:import_zod13.z.boolean().optional().describe("With swipe: keep swiping until the screen stops moving (the end of the list)."),real:import_zod13.z.boolean().optional().describe("Make this step's tap a real touch. Default: the flow's `real`."),reload:import_zod13.z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:import_zod13.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000). To wait for a result (a slow request, a new screen), use waitFor with timeoutMs instead: it stops as soon as the element shows."),waitFor:import_zod13.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:import_zod13.z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:import_zod13.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:import_zod13.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),snapshot:import_zod13.z.string().optional().describe("Compare the whole screen to the baseline with this name (saved on the first run, like screenshot_component baseline). Fails the step on a text change or a change bigger than maxChange."),maxChange:import_zod13.z.number().min(0).max(100).optional().describe("snapshot: allowed changed share of the screen, in percent. Default 0.05."),timeoutMs:import_zod13.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")}).describe("One flow step. Order within a step: reload \u2192 tap/type/set/swipe \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect."));flowInput={steps:import_zod13.z.array(stepSchema).min(1).optional().describe("Steps to run, in order. Leave out when replaying a saved flow."),flow:import_zod13.z.string().optional().describe("Replay a saved flow by name (from .buoy/flows in the project) instead of passing steps."),save:import_zod13.z.string().optional().describe('Save these steps under this name when the flow passes, so it can be replayed with flow:"name".'),include:import_zod13.z.enum(["all","screen","none"]).optional().describe("What to report after the flow: 'all' (default) = available final element tree + newly observed captured network/console entries; 'screen' = available final element tree only; 'none' = step results only."),updateSnapshots:import_zod13.z.boolean().optional().describe("Save every snapshot step's screen as its new baseline instead of comparing."),continueOnFailure:import_zod13.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),real:import_zod13.z.boolean().optional().describe("Make every tap a real OS touch (iOS Simulator, Android), found by testID, text or native label. Default false."),deviceId:deviceIdInput};runFlow=async()=>fail5("run_flow is not ready.");cap=(e,n=160)=>{const t=scrubSecrets(e);return t.length>n?`${t.slice(0,n)}\u2026`:t};firstLines=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ")}});function projectDir(){return process.env.BUOY_PROJECT_ROOT||process.cwd()}function stamp(e=new Date){const n=t=>String(t).padStart(2,"0");return`${e.getFullYear()}${n(e.getMonth()+1)}${n(e.getDate())}-${n(e.getHours())}${n(e.getMinutes())}${n(e.getSeconds())}`}function noteAgentCall(e,n,t){if(READ_ONLY.test(n)||sessions.size===0)return;let s=typeof t?.deviceId==="string"?t.deviceId:void 0;if(!s){try{s=e.resolveDevice()}catch{return}}const i=s?sessions.get(s):void 0;if(!i?.rec)return;const r=Object.entries(t??{}).filter(([o,a])=>o!=="deviceId"&&a!==void 0&&typeof a!=="object").map(([o,a])=>`${o}: ${String(a).slice(0,60)}`).join(", ");i.agent.push({at:Date.now(),t:0,source:"agent",status:"neutral",title:n,detail:r?redact(r):void 0});clearTimeout(i.afterAction);i.afterAction=setTimeout(()=>void gather(e,i),700);i.afterAction.unref?.()}function isRecording(e){return!!sessions.get(e)?.rec}function noteTouch(e,n){const t=sessions.get(e);if(t?.rec&&t.marks.length<500)t.marks.push(n)}function gather(e,n){return runWithAccess({tool:"screen_recording",level:"free"},()=>gatherNow(e,n)).catch(()=>{n.failedPolls++})}async function gatherNow(e,n){const t=e.connectedDevices().some(i=>i.deviceId===n.id);if(!t){n.failedPolls++;addConsole(n,(e.lastSync(n.id,"console")?.payload??{}).entries??[]);return}if(!e.connectedDevices().some(i=>i.deviceId===n.id&&hasProAccess(i))){n.notes.add("The events timeline needs Buoy Pro; this video shows console lines and agent steps only.")}else try{const i=await e.callAction("events","exportEvents",{settings:{format:"json",includeEventData:false},limit:500},n.id,{timeoutMs:4e3,silent:true});const r=i?.output?JSON.parse(i.output):null;const o=Array.isArray(r)?r:r?.events??[];for(const a of o){const l=a.timestamp?Date.parse(a.timestamp):NaN;if(!Number.isFinite(l))continue;const c=a.status==="error"||a.status==="success"||a.status==="pending"?a.status:"neutral";const u={at:l,t:0,source:a.source??"event",status:c,title:scrubSecrets(redact(a.title??"")),detail:a.subtitle?scrubSecrets(redact(a.subtitle)):void 0};n.events.set(`${l}|${u.source}|${u.title}|${u.detail??""}|${c}`,u)}if((i?.returned??0)>=500)n.notes.add("Busy app: some events may be missing from busy stretches.")}catch{n.failedPolls++}let s=[];try{const{payload:i}=await e.getSnapshot("console",n.id,{timeoutMs:2500,silent:true});s=i?.entries??[]}catch{s=(e.lastSync(n.id,"console")?.payload??{}).entries??[]}addConsole(n,s)}function addConsole(e,n){for(const t of n){if(typeof t.timestamp!=="number")continue;const[s,...i]=scrubSecrets(redact(t.message??"")).split("\n");const r=t.level==="error"?"error":t.level==="warning"?"warning":"neutral";e.events.set(`console|${t.timestamp}|${t.id??s}`,{at:t.timestamp,t:0,source:"console",status:r,title:s.slice(0,200),detail:i.join("\n").trim().slice(0,600)||void 0})}}async function finish(e,n){const t=n.rec;if(n.poll)clearInterval(n.poll);clearTimeout(n.afterAction);n.stopped??={durationMs:await stopRecording(t)};const{durationMs:s}=n.stopped;await gather(e,n);if(n.failedPolls)n.notes.add(`The app's events could not be read ${n.failedPolls} time(s) during the recording (the app may have been reloading or stuck), so parts of the timeline may be missing.`);const i=t.startedAt;const r=i+s;const o=[...n.events.values(),...n.agent].filter(g=>g.at>=i-500&&g.at<=r+500).map(({at:g,...f})=>({...f,title:scrubSecrets(f.title),detail:f.detail?scrubSecrets(f.detail):void 0,t:Math.max(0,g-i)})).sort((g,f)=>g.t-f.t);if(!o.some(g=>g.source!=="agent"))n.notes.add("No app events were captured. Is the Events tool installed in the app?");const a=t.file.replace(/\.mp4$/,"");const l=hostLabel(t.host);const c=n.marks.filter(g=>g.at>=i-500&&g.at<=r).map(({at:g,...f})=>({...f,t:Math.max(0,g-i)})).sort((g,f)=>g.t-f.t);let u=null;if(c.length){try{u=await burnTouchMarks(t.file,c,`${a}-taps.mp4`);if(u){const g=touchDotNote(c);if(g)n.notes.add(g)}if(!u)n.notes.add("Install ffmpeg to also get a copy of the video with the taps drawn in. The viewer shows them either way.")}catch(g){n.notes.add(`Could not draw the taps into a copy of the video: ${errMsg2(g).slice(0,160)}. The viewer shows them either way.`)}}const d=[...n.notes];await import_node_fs23.promises.writeFile(`${a}.timeline.json`,JSON.stringify({device:l,startedAt:new Date(i).toISOString(),durationMs:s,notes:d,items:o,touches:c},null,1));await import_node_fs23.promises.writeFile(`${a}.html`,viewerHtml({title:n.name,video:import_node_path24.default.basename(t.file),durationMs:s,items:o,device:l,notes:d,marks:c}));sessions.delete(n.id);const h=g=>import_node_path24.default.relative(projectDir(),g);const p=new Map;for(const g of o)p.set(g.source,(p.get(g.source)??0)+1);const m=g=>`${(g/1e3).toFixed(1)}s`;const y=o.filter(g=>g.status==="error"||g.source==="route"||g.source==="agent").slice(0,14);return[`Saved a ${m(s)} recording of ${l}.`,`- Video: ${h(t.file)}`,`- Viewer (video + timeline, click an event to jump there): ${h(`${a}.html`)}`,`- Timeline data: ${h(`${a}.timeline.json`)}`,...u?[`- Video with touch dots: ${h(u)}`]:c.length?[`- The viewer draws the ${c.length} real touch(es) on the video.`]:[],"",o.length?`Events: ${[...p].map(([g,f])=>`${g} ${f}`).join(", ")}.`:"No events in this window.",...y.length?["What happened:",...y.map(g=>`- ${m(g.t)} [${g.source}]${g.status==="error"?" ERROR":""} ${g.title}${g.detail&&g.source!=="route"?` (${g.detail.split("\n")[0].slice(0,80)})`:""}`)]:[],...d.map(g=>`Note: ${g}`),"Recordings are kept out of git (.buoy/recordings/.gitignore). Secrets in logs are hidden, but the video shows the screen as it was: check it before sharing."].join("\n")}function registerRecordingTools(e,n){setRecordingProbe(()=>sessions.size>0);e.registerTool("screen_recording",{_meta:coreTool(),description:"Record the screen of the iOS Simulator or Android device (start, do the steps, stop). On stop, saves an MP4 plus a viewer page that plays it beside a timeline of what the app did (web calls, query fetches, route changes, store writes, console errors) and what you did (your taps and other actions), and returns a short written summary. Real touches (native_tap, native_swipe, native_pinch) show as marks on the video, and with ffmpeg installed a copy of the MP4 has them drawn in. In .buoy/recordings. Use it to show a bug or a fix, e.g. for a pull request. Max 3 minutes. To record a set of taps or swipes, pass them as steps with start: Buoy records, runs them with real touches, stops and saves, all in one call.",inputSchema:{action:import_zod14.z.enum(["start","stop"]),name:import_zod14.z.string().optional().describe('start: a short name for the files, e.g. "checkout-timeout".'),steps:import_zod14.z.array(stepSchema).min(1).optional().describe('start: run_flow steps to record, e.g. [{tap:"One"},{tap:"Two"}]. Taps and swipes are real touches, so they show on the video. Each tap waits for its target, so no waitMs is needed between taps. The video stops and saves when they finish; no stop call needed.'),serial:import_zod14.z.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:deviceIdInput}},async t=>{try{if(t.action==="start"){const{id:r,host:o}=await hostTarget(n,t.deviceId,{serial:t.serial});if(sessions.has(r))return fail2("A recording is already running on this device. Stop it first.");const a=(t.name??"recording").trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"recording";const l={id:r,name:a,events:new Map,agent:[],notes:new Set,failedPolls:0,marks:[]};sessions.set(r,l);let c;l.starting=new Promise(d=>c=d);try{const d=import_node_path24.default.join(projectDir(),".buoy","recordings");await import_node_fs23.promises.mkdir(d,{recursive:true});await import_node_fs23.promises.writeFile(import_node_path24.default.join(d,".gitignore"),"*\n",{flag:"wx"}).catch(()=>void 0);if(n.connectedDevices().some(p=>p.deviceId===r&&hasProAccess(p)))await n.getSnapshot("events",r,{timeoutMs:2e3,silent:true}).catch(()=>void 0);const h=import_node_path24.default.join(d,`${a}-${stamp()}-${(0,import_node_crypto14.randomUUID)().slice(0,6)}.mp4`);l.rec=await startRecording(o,h)}catch(d){sessions.delete(r);throw d}finally{c()}l.poll=setInterval(()=>void gather(n,l),2e3);l.poll.unref?.();l.rec.stopTimer=setTimeout(()=>{l.finishing??=finish(n,l);l.finishing.then(d=>finished.set(r,`The recording hit the ${MAX_RECORDING_MS/6e4}-minute limit and was saved.
|
|
1071
|
+
`+I.map(O=>`[${O.level==="error"?"ERR":"warn"}] ${cap(firstLines(scrubSecrets(O.message??"")),220)}`).join("\n"))}}}if(t.save&&k===null){await import_node_fs22.promises.mkdir(FLOW_DIR(),{recursive:true});const x=import_node_path23.default.join(FLOW_DIR(),`${t.save}.json`);await import_node_fs22.promises.writeFile(x,JSON.stringify({steps:i},null,2)+"\n");P.push(`Saved as flow "${t.save}" (${import_node_path23.default.relative(process.cwd(),x)}). Replay with run_flow({flow:"${t.save}"}).`)}else if(t.save){P.push(`Not saved: the flow failed. Fix it, then run it again with save:"${t.save}".`)}return k===null?text3(P.join("\n\n")):{...text3(P.join("\n\n")),isError:true}}catch(i){return fail5(errMsg4(i))}}}function selector(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function describeStep(e){if(e.tap)return`tap "${e.tap}"`;if(e.type!==void 0)return`type into "${e.into??"?"}"`;if(e.reload)return"reload";if(e.swipe)return`swipe ${e.swipe}${e.untilEnd?" to the end":""}`;if(e.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;if(e.expectGone)return`expectGone "${e.expectGone}"`;if(e.snapshot)return`snapshot "${e.snapshot}"`;return"step"}var import_zod13,import_node_fs22,import_node_path23,FLOW_DIR,FLOW_NAME,UI_TOOL_ID,APP_TOOL_ID,text3,fail5,errMsg4,delay3,POLL_MS,TAP_SETTLE_MS,DEFAULT_WAIT_MS,ASSERT_SETTLE_MS,TEARDOWN_GRACE_MS,REALM_PROBE_MS,MAX_FLOW_MS,STEP_KINDS,stepSchema,flowInput,runFlow,cap,firstLines;var init_flow=__esm({"src/tools/packs/flow.ts"(){"use strict";init_screenshot();init_secrets();import_zod13=require("zod");init_packKit();import_node_fs22=require("fs");import_node_path23=__toESM(require("path"));init_native();init_interact();init_hostKit();FLOW_DIR=()=>import_node_path23.default.join(process.cwd(),".buoy","flows");FLOW_NAME=/^[A-Za-z0-9_-]{1,64}$/;UI_TOOL_ID="highlight-updates";APP_TOOL_ID="app";text3=e=>({content:[{type:"text",text:e}]});fail5=e=>({isError:true,content:[{type:"text",text:e}]});errMsg4=e=>e instanceof Error?e.message:String(e);delay3=e=>new Promise(n=>setTimeout(n,e));POLL_MS=250;TAP_SETTLE_MS=2e3;DEFAULT_WAIT_MS=5e3;ASSERT_SETTLE_MS=2e3;TEARDOWN_GRACE_MS=1200;REALM_PROBE_MS=1500;MAX_FLOW_MS=12e4;STEP_KINDS=["tap","waitFor","waitGone","expect","expectGone","reload","wait","swipe"];stepSchema=import_zod13.z.preprocess(normalizeStep,import_zod13.z.object({tap:import_zod13.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:import_zod13.z.boolean().optional().describe("Make `tap` a long-press instead."),type:import_zod13.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod13.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:import_zod13.z.union([import_zod13.z.number(),import_zod13.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),swipe:import_zod13.z.enum(["up","down","left","right"]).optional().describe('A real swipe, the way the finger moves: "up" scrolls down the list.'),untilEnd:import_zod13.z.boolean().optional().describe("With swipe: keep swiping until the screen stops moving (the end of the list)."),real:import_zod13.z.boolean().optional().describe("Make this step's tap a real touch. Default: the flow's `real`."),reload:import_zod13.z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:import_zod13.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000). To wait for a result (a slow request, a new screen), use waitFor with timeoutMs instead: it stops as soon as the element shows."),waitFor:import_zod13.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:import_zod13.z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:import_zod13.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:import_zod13.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),snapshot:import_zod13.z.string().optional().describe("Compare the whole screen to the baseline with this name (saved on the first run, like screenshot_component baseline). Fails the step on a text change or a change bigger than maxChange."),maxChange:import_zod13.z.number().min(0).max(100).optional().describe("snapshot: allowed changed share of the screen, in percent. Default 0.05."),timeoutMs:import_zod13.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")}).describe("One flow step. Order within a step: reload \u2192 tap/type/set/swipe \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect."));flowInput={steps:import_zod13.z.array(stepSchema).min(1).optional().describe("Steps to run, in order. Leave out when replaying a saved flow."),flow:import_zod13.z.string().optional().describe("Replay a saved flow by name (from .buoy/flows in the project) instead of passing steps."),save:import_zod13.z.string().optional().describe('Save these steps under this name when the flow passes, so it can be replayed with flow:"name".'),include:import_zod13.z.enum(["all","screen","none"]).optional().describe("What to report after the flow: 'all' (default) = available final element tree + newly observed captured network/console entries; 'screen' = available final element tree only; 'none' = step results only."),updateSnapshots:import_zod13.z.boolean().optional().describe("Save every snapshot step's screen as its new baseline instead of comparing."),continueOnFailure:import_zod13.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),real:import_zod13.z.boolean().optional().describe("Make every tap a real OS touch (iOS Simulator, Android), found by testID, text or native label. Default false."),deviceId:deviceIdInput};runFlow=async()=>fail5("run_flow is not ready.");cap=(e,n=160)=>{const t=scrubSecrets(e);return t.length>n?`${t.slice(0,n)}\u2026`:t};firstLines=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ")}});function projectDir(){return process.env.BUOY_PROJECT_ROOT||process.cwd()}function stamp(e=new Date){const n=t=>String(t).padStart(2,"0");return`${e.getFullYear()}${n(e.getMonth()+1)}${n(e.getDate())}-${n(e.getHours())}${n(e.getMinutes())}${n(e.getSeconds())}`}function noteAgentCall(e,n,t){if(READ_ONLY.test(n)||sessions.size===0)return;let s=typeof t?.deviceId==="string"?t.deviceId:void 0;if(!s){try{s=e.resolveDevice()}catch{return}}const i=s?sessions.get(s):void 0;if(!i?.rec)return;const r=Object.entries(t??{}).filter(([o,a])=>o!=="deviceId"&&a!==void 0&&typeof a!=="object").map(([o,a])=>`${o}: ${String(a).slice(0,60)}`).join(", ");i.agent.push({at:Date.now(),t:0,source:"agent",status:"neutral",title:n,detail:r?redact(r):void 0});clearTimeout(i.afterAction);i.afterAction=setTimeout(()=>void gather(e,i),700);i.afterAction.unref?.()}function isRecording(e){return!!sessions.get(e)?.rec}function noteTouch(e,n){const t=sessions.get(e);if(t?.rec&&t.marks.length<500)t.marks.push(n)}function gather(e,n){return runWithAccess({tool:"screen_recording",level:"free"},()=>gatherNow(e,n)).catch(()=>{n.failedPolls++})}async function gatherNow(e,n){const t=e.connectedDevices().some(i=>i.deviceId===n.id);if(!t){n.failedPolls++;addConsole(n,(e.lastSync(n.id,"console")?.payload??{}).entries??[]);return}if(!e.connectedDevices().some(i=>i.deviceId===n.id&&hasProAccess(i))){n.notes.add("The events timeline needs Buoy Pro; this video shows console lines and agent steps only.")}else try{const i=await e.callAction("events","exportEvents",{settings:{format:"json",includeEventData:false},limit:500},n.id,{timeoutMs:4e3,silent:true});const r=i?.output?JSON.parse(i.output):null;const o=Array.isArray(r)?r:r?.events??[];for(const a of o){const l=a.timestamp?Date.parse(a.timestamp):NaN;if(!Number.isFinite(l))continue;const c=a.status==="error"||a.status==="success"||a.status==="pending"?a.status:"neutral";const u={at:l,t:0,source:a.source??"event",status:c,title:scrubSecrets(redact(a.title??"")),detail:a.subtitle?scrubSecrets(redact(a.subtitle)):void 0};n.events.set(`${l}|${u.source}|${u.title}|${u.detail??""}|${c}`,u)}if((i?.returned??0)>=500)n.notes.add("Busy app: some events may be missing from busy stretches.")}catch{n.failedPolls++}let s=[];try{const{payload:i}=await e.getSnapshot("console",n.id,{timeoutMs:2500,silent:true});s=i?.entries??[]}catch{s=(e.lastSync(n.id,"console")?.payload??{}).entries??[]}addConsole(n,s)}function addConsole(e,n){for(const t of n){if(typeof t.timestamp!=="number")continue;const[s,...i]=scrubSecrets(redact(t.message??"")).split("\n");const r=t.level==="error"?"error":t.level==="warning"?"warning":"neutral";e.events.set(`console|${t.timestamp}|${t.id??s}`,{at:t.timestamp,t:0,source:"console",status:r,title:s.slice(0,200),detail:i.join("\n").trim().slice(0,600)||void 0})}}async function finish(e,n){const t=n.rec;if(n.poll)clearInterval(n.poll);clearTimeout(n.afterAction);n.stopped??={durationMs:await stopRecording(t)};const{durationMs:s}=n.stopped;await gather(e,n);if(n.failedPolls)n.notes.add(`The app's events could not be read ${n.failedPolls} time(s) during the recording (the app may have been reloading or stuck), so parts of the timeline may be missing.`);const i=t.startedAt;const r=i+s;const o=[...n.events.values(),...n.agent].filter(g=>g.at>=i-500&&g.at<=r+500).map(({at:g,...f})=>({...f,title:scrubSecrets(f.title),detail:f.detail?scrubSecrets(f.detail):void 0,t:Math.max(0,g-i)})).sort((g,f)=>g.t-f.t);if(!o.some(g=>g.source!=="agent"))n.notes.add("No app events were captured. Is the Events tool installed in the app?");const a=t.file.replace(/\.mp4$/,"");const l=hostLabel(t.host);const c=n.marks.filter(g=>g.at>=i-500&&g.at<=r).map(({at:g,...f})=>({...f,t:Math.max(0,g-i)})).sort((g,f)=>g.t-f.t);let u=null;if(c.length){try{u=await burnTouchMarks(t.file,c,`${a}-taps.mp4`);if(u){const g=touchDotNote(c);if(g)n.notes.add(g)}if(!u)n.notes.add("Install ffmpeg to also get a copy of the video with the taps drawn in. The viewer shows them either way.")}catch(g){n.notes.add(`Could not draw the taps into a copy of the video: ${errMsg2(g).slice(0,160)}. The viewer shows them either way.`)}}const d=[...n.notes];await import_node_fs23.promises.writeFile(`${a}.timeline.json`,JSON.stringify({device:l,startedAt:new Date(i).toISOString(),durationMs:s,notes:d,items:o,touches:c},null,1));await import_node_fs23.promises.writeFile(`${a}.html`,viewerHtml({title:n.name,video:import_node_path24.default.basename(t.file),durationMs:s,items:o,device:l,notes:d,marks:c}));sessions.delete(n.id);const h=g=>import_node_path24.default.relative(projectDir(),g);const p=new Map;for(const g of o)p.set(g.source,(p.get(g.source)??0)+1);const m=g=>`${(g/1e3).toFixed(1)}s`;const y=o.filter(g=>g.status==="error"||g.source==="route"||g.source==="agent").slice(0,14);return[`Saved a ${m(s)} recording of ${l}.`,`- Video: ${h(t.file)}`,`- Viewer (video + timeline, click an event to jump there): ${h(`${a}.html`)}`,`- Timeline data: ${h(`${a}.timeline.json`)}`,...u?[`- Video with touch dots: ${h(u)}`]:c.length?[`- The viewer draws the ${c.length} real touch(es) on the video.`]:[],"",o.length?`Events: ${[...p].map(([g,f])=>`${g} ${f}`).join(", ")}.`:"No events in this window.",...y.length?["What happened:",...y.map(g=>`- ${m(g.t)} [${g.source}]${g.status==="error"?" ERROR":""} ${g.title}${g.detail&&g.source!=="route"?` (${g.detail.split("\n")[0].slice(0,80)})`:""}`)]:[],...d.map(g=>`Note: ${g}`),"Recordings are kept out of git (.buoy/recordings/.gitignore). Secrets in logs are hidden, but the video shows the screen as it was: check it before sharing."].join("\n")}function registerRecordingTools(e,n){setRecordingProbe(()=>sessions.size>0);e.registerTool("screen_recording",{_meta:coreTool(),description:"Record the screen of the iOS Simulator or Android device (start, do the steps, stop). On stop, saves an MP4 plus a viewer page that plays it beside a timeline of what the app did (web calls, query fetches, route changes, store writes, console errors) and what you did (your taps and other actions), and returns a short written summary. Real touches (native_tap, native_swipe, native_pinch) show as marks on the video, and with ffmpeg installed a copy of the MP4 has them drawn in. In .buoy/recordings. Use it to show a bug or a fix, e.g. for a pull request. Max 3 minutes. To record a set of taps or swipes, pass them as steps with start: Buoy records, runs them with real touches, stops and saves, all in one call.",inputSchema:{action:import_zod14.z.enum(["start","stop"]),name:import_zod14.z.string().optional().describe('start: a short name for the files, e.g. "checkout-timeout".'),steps:import_zod14.z.array(stepSchema).min(1).optional().describe('start: run_flow steps to record, e.g. [{tap:"One"},{tap:"Two"}]. Taps and swipes are real touches, so they show on the video. Each tap waits for its target, so no waitMs is needed between taps. The video stops and saves when they finish; no stop call needed.'),serial:import_zod14.z.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:deviceIdInput}},async t=>{try{if(t.action==="start"){const{id:r,host:o}=await hostTarget(n,t.deviceId,{serial:t.serial});if(sessions.has(r))return fail2("A recording is already running on this device. Stop it first.");const a=(t.name??"recording").trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"recording";const l={id:r,name:a,events:new Map,agent:[],notes:new Set,failedPolls:0,marks:[]};sessions.set(r,l);let c;l.starting=new Promise(d=>c=d);try{const d=import_node_path24.default.join(projectDir(),".buoy","recordings");await import_node_fs23.promises.mkdir(d,{recursive:true});await import_node_fs23.promises.writeFile(import_node_path24.default.join(d,".gitignore"),"*\n",{flag:"wx"}).catch(()=>void 0);if(n.connectedDevices().some(p=>p.deviceId===r&&hasProAccess(p)))await n.getSnapshot("events",r,{timeoutMs:2e3,silent:true}).catch(()=>void 0);const h=import_node_path24.default.join(d,`${a}-${stamp()}-${(0,import_node_crypto14.randomUUID)().slice(0,6)}.mp4`);l.rec=await startRecording(o,h)}catch(d){sessions.delete(r);throw d}finally{c()}l.poll=setInterval(()=>void gather(n,l),2e3);l.poll.unref?.();l.rec.stopTimer=setTimeout(()=>{l.finishing??=finish(n,l);l.finishing.then(d=>finished.set(r,`The recording hit the ${MAX_RECORDING_MS/6e4}-minute limit and was saved.
|
|
1072
1072
|
${d}`),d=>finished.set(r,`The recording hit the time limit and could not be saved: ${errMsg2(d)}`))},MAX_RECORDING_MS);l.rec.stopTimer.unref?.();if(t.steps){const d=Date.now();const h=await runFlow({steps:t.steps,real:true,include:"none",deviceId:r});const p=Math.max(STEPS_HOLD_MS,STEPS_MIN_MS-(Date.now()-d));const[m]=await Promise.all([observeAfterAction(n,r,void 0,0).catch(()=>""),delay4(p)]);l.finishing??=finish(n,l);let y;try{y=await l.finishing}catch(f){l.finishing=void 0;return fail2(`${h.content[0]?.text??""}
|
|
1073
1073
|
|
|
1074
1074
|
Could not save the recording: ${errMsg2(f)}. Call screen_recording with action "stop" to retry.`)}const g=text(`${h.content.map(f=>f.text).join("\n")}
|
|
1075
1075
|
|
|
1076
|
-
${y}${m}`);return h.isError?{...g,isError:true}:g}const u=typedSecretValues().length?" Note: a secret was typed earlier this session; if it is still on screen, the video will show it. Typing secrets is blocked until the recording stops.":" Typing secrets ({{secret:NAME}}) is blocked until the recording stops, because a video can't hide them.";return text(`Recording ${hostLabel(o)}. Do the steps, then call screen_recording with action "stop". It stops by itself after ${MAX_RECORDING_MS/6e4} minutes.${u}`)}let s=t.deviceId;if(!s){try{s=n.requireProDevice()}catch{s=void 0}}if(!s||!sessions.has(s)){if(s&&finished.has(s)){const r=finished.get(s);finished.delete(s);return text(r)}if(sessions.size===1)s=[...sessions.keys()][0];else return fail2(sessions.size?"Several recordings are running. Pass deviceId.":'No recording is running. Start one with action "start".')}const i=sessions.get(s);await i.starting;if(!i.rec)return fail2("The recording did not start.");i.finishing??=finish(n,i);try{return text(await i.finishing)}catch(r){i.finishing=void 0;return fail2(`Could not save the recording: ${errMsg2(r)}. Call stop again to retry.`)}}catch(s){return fail2(errMsg2(s))}})}var import_zod14,import_node_fs23,import_node_path24,import_node_crypto14,sessions,finished,delay4,READ_ONLY,STEPS_HOLD_MS,STEPS_MIN_MS;var init_recording=__esm({"src/tools/packs/recording.ts"(){"use strict";init_policy();import_zod14=require("zod");import_node_fs23=require("fs");import_node_path24=__toESM(require("path"));import_node_crypto14=require("crypto");init_packKit();init_hostKit();init_secrets();init_flow();init_interact();init_recorder();sessions=new Map;finished=new Map;delay4=e=>new Promise(n=>setTimeout(n,e));READ_ONLY=/^(get_|list_|describe_screen$|screenshot_component$|screen_recording$|wait_for_automation$|compare_reports$)/;STEPS_HOLD_MS=1200;STEPS_MIN_MS=3e3}});async function phoneScreenSize(e){const n=phoneScreens.get(e);if(n&&Date.now()-n.at<SCREEN_TTL_MS)return n.size;const t=await phoneScreenshot(e);const s={width:t.readUInt32BE(16),height:t.readUInt32BE(20)};phoneScreens.set(e,{size:s,at:Date.now()});return s}function swipePoints(e,n){if(n){if(e==="up")return{from:[.5,.88],to:[.5,.06]};if(e==="down")return{from:[.5,.3],to:[.5,.94]};if(e==="left")return{from:[.9,.5],to:[.1,.5]};return{from:[.1,.5],to:[.9,.5]}}const t=e==="left"?-.35:e==="right"?.35:0;const s=e==="up"?-.3:e==="down"?.3:0;return{from:[.5-t/2,.5-s/2],to:[.5+t/2,.5+s/2]}}function forgetScreenSize(e){barCache.delete(e);simScreens.delete(e);phoneScreens.delete(e)}function freshScreen(e){if(e.platform==="android")return;simScreens.delete(e.udid);phoneScreens.delete(e.udid)}async function xcodeShimDir(){const e=(await execFileAsync14("xcode-select",["-p"],{timeout:5e3}).catch(()=>({stdout:""}))).stdout.trim();if(!e.endsWith("/Contents/Developer"))return null;const n=import_node_path25.default.dirname(e);const t=import_node_path25.default.join(e,"Library","PrivateFrameworks","SimulatorKit.framework");const s=import_node_path25.default.join(n,"SharedFrameworks","SimulatorKit.framework");const i=c=>import_node_fs24.promises.access(c).then(()=>true,()=>false);if(await i(t)||!await i(s))return null;const r=(await execFileAsync14("plutil",["-extract","DTXcodeBuild","raw","-o","-",import_node_path25.default.join(n,"Info.plist")]).catch(()=>({stdout:"x"}))).stdout.trim();const o=import_node_path25.default.join(import_node_os17.default.homedir(),".buoy","xcode-shim",r.replace(/[^\w.-]/g,"_"),"Contents");const a=import_node_path25.default.join(o,"Developer");if(await i(import_node_path25.default.join(a,"Library","PrivateFrameworks","SimulatorKit.framework")))return a;await import_node_fs24.promises.mkdir(import_node_path25.default.join(a,"Library","PrivateFrameworks"),{recursive:true});const l=async(c,u,d)=>{for(const h of await import_node_fs24.promises.readdir(c)){if(h===d)continue;await import_node_fs24.promises.symlink(import_node_path25.default.join(c,h),import_node_path25.default.join(u,h)).catch(()=>void 0)}};await l(n,o,"Developer");await l(e,a,"Library");await l(import_node_path25.default.join(e,"Library"),import_node_path25.default.join(a,"Library"),"PrivateFrameworks");await l(import_node_path25.default.join(e,"Library","PrivateFrameworks"),import_node_path25.default.join(a,"Library","PrivateFrameworks"),"").catch(()=>void 0);await import_node_fs24.promises.symlink(s,import_node_path25.default.join(a,"Library","PrivateFrameworks","SimulatorKit.framework")).catch(()=>void 0);return a}async function restartCompanion(e){const n=`/tmp/idb/${e}.buoy-restart`;await import_node_fs24.promises.mkdir("/tmp/idb",{recursive:true}).catch(()=>void 0);const t=async()=>{try{await(await import_node_fs24.promises.open(n,"wx")).close();return true}catch{const s=await import_node_fs24.promises.stat(n).then(i=>Date.now()-i.mtimeMs,()=>0);if(s>15e3){const i=`${n}.stale-${process.pid}-${Date.now()}`;await import_node_fs24.promises.rename(n,i).then(()=>import_node_fs24.promises.unlink(i).catch(()=>void 0),()=>void 0);return t()}return false}};if(!await t()){for(let s=0;s<60;s++){await delay5(250);if(!await import_node_fs24.promises.stat(n).then(()=>true,()=>false))return}return}try{await execFileAsync14("pkill",["-f",`idb_companion --udid ${e}`],{timeout:5e3}).catch(()=>void 0);await delay5(300);const s=await xcodeShimDir().catch(()=>null);const i=(0,import_node_child_process17.spawn)("idb_companion",["--udid",e,"--grpc-domain-sock",`/tmp/idb/${e}_companion.sock`,"--only","simulator"],{detached:true,stdio:"ignore",env:s?{...process.env,DEVELOPER_DIR:s}:process.env});i.on("error",()=>void 0);i.unref();await delay5(1800)}finally{await import_node_fs24.promises.unlink(n).catch(()=>void 0)}}async function idb(e,n=15e3,t=false){try{const{stdout:s}=await execFileAsync14("idb",e,{timeout:n});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(IDB_MISSING);const r=(i.stderr||i.message||"").slice(0,300);const o=e.includes("--udid")?e[e.indexOf("--udid")+1]:void 0;if(!t&&o&&/not booted|connection refused|Failed to connect to companion/i.test(r)){await restartCompanion(o);return idb(e,n,true)}if(/SimulatorKit is required/i.test(r)){if(!t&&o&&await xcodeShimDir().catch(()=>null)){await restartCompanion(o);return idb(e,n,true)}throw new Error("idb can't send touches with this Xcode: it can't load SimulatorKit, and Buoy could not work around it. Use tap_element (it calls the element's handler) for now. Reading the screen (describe_screen, screenshot_component) still works.")}throw new Error(`idb ${e[0]} ${e[1]??""} failed: ${r}`)}}async function iosTree(e){const n=JSON.parse(await idb(["ui","describe-all","--udid",e,"--json","--nested"]));const t=[];const s=r=>{t.push(r);for(const o of r.children??[])s(o)};n.forEach(s);const i=t.find(r=>r.type==="Application"&&r.frame);if(!t.some(r=>(r.frame?.width??0)>0))throw new AxShellOnlyError(e);return{all:t,screen:i?.frame?{width:i.frame.width,height:i.frame.height}:{width:0,height:0}}}async function loadBars(){if(barFileRead||process.env.VITEST)return;barFileRead=true;try{const e=JSON.parse(await import_node_fs24.promises.readFile(BAR_FILE,"utf8"));for(const[n,t]of Object.entries(e))if(!barCache.has(n)&&t?.screen&&Array.isArray(t.els))barCache.set(n,t)}catch{}}function saveBars(){if(process.env.VITEST)return;const e=Object.fromEntries([...barCache].map(([t,s])=>[t,{screen:s.screen,els:s.els.map(i=>({AXLabel:i.AXLabel,type:i.type,frame:i.frame}))}]));const n=`${BAR_FILE}.${process.pid}`;void import_node_fs24.promises.mkdir(import_node_path25.default.dirname(BAR_FILE),{recursive:true}).then(()=>import_node_fs24.promises.writeFile(n,JSON.stringify(e))).then(()=>import_node_fs24.promises.rename(n,BAR_FILE)).catch(()=>void 0)}async function cachedBarControl(e,n){await loadBars();const t=barCache.get(e);const s=n.trim().toLowerCase();const i=t?.els.find(r=>(r.AXLabel??"").trim().toLowerCase()===s&&r.frame);if(!t||!i?.frame)return null;try{const r=Math.round(i.frame.x+i.frame.width/2);const o=Math.round(i.frame.y+i.frame.height/2);const a=JSON.parse(await idb(["ui","describe-point","--udid",e,String(r),String(o),"--json"],3e3));if((a.AXLabel??"").trim().toLowerCase()===s&&a.frame)return{el:a,screen:t.screen}}catch{}barCache.delete(e);saveBars();return null}async function findByLabel(e,n,t,s=false,i=[.95,.97,.09,.06]){const r=n.trim().toLowerCase();const o=Date.now()+8e3;const a=m=>{const y=(m.AXLabel??"").trim().toLowerCase();if(!y)return 0;if(y===r)return 3;if(y.startsWith(r))return 2;if(y.includes(r))return 1;return 0};const{width:l,height:c}=t.screen;const u=m=>{if(!l||!m.frame)return true;const y=m.frame.x+m.frame.width/2;const g=m.frame.y+m.frame.height/2;return y>=0&&y<=l&&g>=0&&g<=c};let d=t.all.filter(m=>a(m)>0&&m.frame&&m.type!=="Application"&&u(m));if(!d.some(m=>a(m)===3)&&t.screen.width){const m=i.map(y=>{const g=[];for(let f=20;f<t.screen.width;f+=40)g.push([f,Math.round(t.screen.height*y)]);return g});for(const y of m){if(Date.now()>=o)break;const g=await Promise.allSettled(y.map(async([w,v])=>JSON.parse(await idb(["ui","describe-point","--udid",e,String(w),String(v),"--json"],3e3))));const f=[];for(const w of g){if(w.status==="rejected"){if(IDB_SETUP.test(String(w.reason)))throw w.reason;continue}const v=w.value;if(v?.frame&&v.AXLabel&&v.type&&v.type!=="Application")f.push(v);if(a(v)>0&&v.frame)d.push(v)}if(f.length){const w=barCache.get(e);const v=w&&w.screen.width===t.screen.width&&w.screen.height===t.screen.height;const b=[...v?w.els:[],...f].filter((k,$,_)=>_.findIndex(S=>S.AXLabel===k.AXLabel&&JSON.stringify(S.frame)===JSON.stringify(k.frame))===$);barCache.set(e,{screen:t.screen,els:b});saveBars()}if(d.some(w=>a(w)===3))break}}const h=Math.max(0,...d.map(a));if(s&&h<3)return[];d=d.filter(m=>a(m)===h);let p=d.filter((m,y)=>d.findIndex(g=>JSON.stringify(g.frame)===JSON.stringify(m.frame))===y);if(p.length>1){const m=p.filter(y=>(y.AXLabel??"").trim()===n.trim());if(m.length)p=m}if(p.length>1){const m=p.filter(y=>y.type&&!/StaticText|Text$|Other|Image/.test(y.type));if(m.length)p=m}return p}async function iosNativeElements(e){const n=await iosTree(e);const t=n.all.filter(a=>a.frame&&a.frame.width>0&&a.type!=="Application");const s=a=>`${a.AXLabel??""}|${JSON.stringify(a.frame)}`;const i=new Set(t.map(s));const{width:r,height:o}=n.screen;if(r){const a=t.filter(d=>!!d.type&&!/StaticText|Text$|Other|Image|Group|Heading/.test(d.type));const l=(d,h)=>a.some(p=>d>=p.frame.x&&d<=p.frame.x+p.frame.width&&h>=p.frame.y&&h<=p.frame.y+p.frame.height);const c=[];for(const d of[.95,.97,.09,.06])for(let h=20;h<r;h+=40){const p=Math.round(o*d);if(!l(h,p))c.push([h,p])}const u=Date.now()+6e3;for(let d=0;d<c.length&&Date.now()<u;d+=8){const h=await Promise.all(c.slice(d,d+8).map(([p,m])=>idb(["ui","describe-point","--udid",e,String(p),String(m),"--json"],3e3).then(y=>JSON.parse(y),y=>{if(IDB_SETUP.test(String(y)))throw y;return null})));for(const p of h){if(!p?.type||!p.frame?.width||p.type==="Application"||i.has(s(p)))continue;i.add(s(p));t.push(p)}}}t.sort((a,l)=>a.frame.y-l.frame.y||a.frame.x-l.frame.x);return{elements:t,screen:n.screen}}function axLine(e){const n=e.frame;const t=[e.type??"Element",e.AXLabel?JSON.stringify(e.AXLabel.slice(0,80)):"",e.AXUniqueId?`id=${e.AXUniqueId}`:"",e.AXValue?`value=${JSON.stringify(String(e.AXValue).slice(0,40))}`:"",e.enabled===false?"(disabled)":""].filter(Boolean);return`${t.join(" ")} at (${Math.round(n.x)}, ${Math.round(n.y)}) ${Math.round(n.width)}x${Math.round(n.height)}`}function nativeKit(e){async function n(w,v,b){if(v.platform!=="ios"||!b)return;try{const k=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:2e3,silent:true});const $=k?.screen?.width??0;const _=k?.screen?.height??0;const S=Math.max($,_)/Math.max(1,Math.min($,_));if($>0&&_>0&&S>1.7)simScreens.set(v.udid,{size:{width:$,height:_},at:Date.now()})}catch{}}async function t(w,v){if(w.platform==="android")return adbScreenSize(w.serial);if(w.platform==="ios-device")return{...await phoneScreenSize(w.udid),isPhone:true};const b=simScreens.get(w.udid);if(!v&&b&&Date.now()-b.at<SCREEN_TTL_MS)return b.size;try{const k=(v??await iosTree(w.udid)).screen;if(!k.width)throw new Error("Could not read the simulator's screen size from idb.");simScreens.set(w.udid,{size:k,at:Date.now()});return k}catch(k){if(!v&&b&&k instanceof AxShellOnlyError)return b.size;if(v||!await simHidTool())throw k;const $=import_node_path25.default.join(import_node_os17.default.tmpdir(),`buoy-size-${process.pid}-${Date.now()}.png`);try{await execFileAsync14("xcrun",["simctl","io",w.udid,"screenshot","--type=png",$],{timeout:2e4});const _=await import_node_fs24.promises.readFile($);return{width:_.readUInt32BE(16),height:_.readUInt32BE(20)}}finally{await import_node_fs24.promises.rm($,{force:true}).catch(()=>void 0)}}}async function s(w,v,b,k){if(w.platform==="android")await adbTap(w.serial,v.x,v.y,b);else if(w.platform==="ios-device"){const $=await simHidTool();if(!$)throw new Error(PHONE_TOUCH);return simHidTap($,await coreDeviceId(w.udid),await t(w),v.x,v.y,b,k)}else if(await simHidTool()){return simHidTap(await simHidTool(),w.udid,{...await t(w),bundleId:w.bundleId},v.x,v.y,b,k)}else{await assertSimTouchable(w.udid);await idb(["ui","tap","--udid",w.udid,...b?["--duration",(b/1e3).toFixed(2)]:[],String(Math.round(v.x)),String(Math.round(v.y))])}}async function i(w,v){if(w.platform==="android"){const k=await adbAppFrame(w.serial,w.packageName);const $=k??{x:0,y:0,...await adbScreenSize(w.serial)};return[(v.x-$.x)/$.width,(v.y-$.y)/$.height]}const b=await t(w);return[v.x/b.width,v.y/b.height]}async function r(w,v,b){try{const[k,$]=await i(v,b);if(!(k>=0&&k<=1&&$>=0&&$<=1))return null;return await e.callAction(UI_TOOL_ID2,"hitTest",{x:k,y:$},w,{timeoutMs:2500,silent:true})}catch{return null}}async function o(w,v,b){const k=await r(w,v,b);if(!k?.found||!k.buoy)return null;const $=k.path?.length?` (${k.path.slice(0,3).join(" in ")})`:"";return`Buoy's own overlay${$} is on top at that spot, so a real touch would land on Buoy, not the app. Nothing was touched. Close or minimize the Buoy panel first, or pass allowBuoy: true to touch Buoy on purpose.`}async function a(w,v,b,k,$,_,S){if(!isRecording(w))return;try{const E=await t(v);const A=T=>[T[0]/E.width,T[1]/E.height];const P=Date.now()-_-(v.platform==="android"?50:80);const x=S!==void 0?S:v.platform==="android"?null:await deviceOrientation(v.udid);noteTouch(w,{at:P,kind:b,ms:_,from:k.map(A),to:$?.map(A),screen:E,orientation:x})}catch{}}async function l(w,v,b){const k=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w);const $=(b.query??"").toLowerCase();const _=(k.elements??[]).filter(x=>b.nativeTag!=null?x.nativeTag===b.nativeTag:b.testID?x.testID===b.testID:(x.text??x.label??"").toLowerCase()===$);const S=_.filter(x=>x.tap);if(S.length===0)return"No element on screen matches that. Call describe_screen, or use label for native controls.";if(S.length>1&&b.nativeTag==null)return`${S.length} elements match equally well, so nothing was tapped. Use nativeTag: ${S.map(x=>`#${x.nativeTag}`).join(", ")}.`;const E=S[0].tap;const A=`"${S[0].text??S[0].label??S[0].name}"`;if(v.platform==="android"){const x=await adbAppFrame(v.serial,v.packageName);const T=x??{x:0,y:0,...await adbScreenSize(v.serial)};return{x:T.x+E.x*T.width,y:T.y+E.y*T.height,what:A}}const P=await t(v);return{x:E.x*P.width,y:E.y*P.height,what:A}}async function c(w,v,b=false,k){if(w.platform==="android"){const A=findUiNodes(await adbUiNodes(w.serial),v).filter(x=>!b||[x.desc,x.text].some(T=>(T??"").trim().toLowerCase()===v.trim().toLowerCase()));if(A.length===0)return`No native element labeled "${v}" was found on screen.`;if(A.length>1)return`${A.length} native elements match "${v}" equally well, so nothing was tapped: ${A.map(x=>`"${x.desc||x.text||x.id}" at (${x.bounds.x}, ${x.bounds.y})`).join(", ")}. Tap one by x/y.`;const P=A[0].bounds;return{x:P.x+P.width/2,y:P.y+P.height/2,what:`"${A[0].desc||A[0].text||A[0].id}" (${A[0].cls.split(".").pop()})`}}const $=await cachedBarControl(w.udid,v);if($){simScreens.set(w.udid,{size:$.screen,at:Date.now()});const A=$.el.frame;return{x:A.x+A.width/2,y:A.y+A.height/2,what:`"${$.el.AXLabel}" (${$.el.type})`}}let _;try{_=await iosTree(w.udid)}catch(A){if(!(A instanceof AxShellOnlyError))throw A;if(k?.online){const P=await f(k.id,w,v,true,"app");if(!("miss"in P))return{...P,what:`${P.what} (found in the app's own screen, because idb can only see an empty shell on this simulator)`};if(P.tie)return P.miss}return A.message}await t(w,_);const S=await findByLabel(w.udid,v,_,b);if(S.length===0)return`No native element labeled "${v}" was found on screen.`;if(S.length>1)return`${S.length} native elements match "${v}" equally well, so nothing was tapped: ${S.map(A=>`"${A.AXLabel}" at (${Math.round(A.frame.x)}, ${Math.round(A.frame.y)})`).join(", ")}. Tap one by x/y.`;const E=S[0].frame;return{x:E.x+E.width/2,y:E.y+E.height/2,what:`"${S[0].AXLabel}" (${S[0].type})`}}async function u(w,v,b,k,$=false,_=0,S){const E=[...v,...b].map(P=>String(Math.round(P)));const A=w.platform==="android"?null:await simHidTool();if(w.platform!=="android"&&!A)await assertSimTouchable(w.udid);if(w.platform==="android"){if($)await adbShell(w.serial,`input draganddrop ${E.join(" ")} ${Math.round(k)}`);else await adbSwipe(w.serial,v,b,k);return void 0}if(A){const P=w.platform==="ios-device"?await coreDeviceId(w.udid):w.udid;const x=S?.screen??await t(w);return simHidSwipe(A,P,{...x,bundleId:w.platform==="ios"?w.bundleId:null},v,b,k,$?700:0,$?0:_,S?.orientation)}if($)await idb(["ui","swipe","--udid",w.udid,...E,"--duration",(k/1e3).toFixed(2),"--delta","4"]);else await idb(["ui","swipe","--udid",w.udid,...E,"--duration",(k/1e3).toFixed(2)]);return void 0}async function d(w){try{const v=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:3e3,silent:true});const b=new Map;for(const k of v?.elements??[]){if(!k.tap)continue;const $=`${k.name??""}|${k.text??k.label??""}|${k.nativeTag??""}`;if(b.has($))b.set($,{x:NaN,y:NaN});else b.set($,k.tap)}return b}catch{return null}}function h(w,v,b,k,$=true){let _=0;let S=0;for(const[E,A]of v){const P=w.get(E);if(!P)S++;else if(Number.isFinite(P[b])&&Number.isFinite(A[b])&&Math.abs(A[b]-P[b])>k)_++}return _+($&&S>=2?S:0)}function p(w,v,b){const k=[];for(const[$,_]of v){const S=w.get($);if(S&&Number.isFinite(S[b])&&Number.isFinite(_[b])&&Math.abs(_[b]-S[b])>.002)k.push(_[b]-S[b])}if(k.length<2)return 0;k.sort(($,_)=>$-_);return k[Math.floor(k.length/2)]}async function m(w,v){await delay5(120);let b=await d(w);const k=Date.now()+2500;while(b&&Date.now()<k){await delay5(100);const $=await d(w);if(!$)break;const _=h(b,$,v,.002,false)===0;b=$;if(_)break}return b}async function y(w,v,b,k,$,_=15){const S=Math.abs(k[1]-b[1])>=Math.abs(k[0]-b[0])?"y":"x";let E=await d(w);if(E===null)throw new Error("Scrolling to the end needs the app connected, to see when the list stops moving.");let A=0;let P=false;const x={screen:await t(v)};const T=S==="y"?1:0;const I=S==="y"?x.screen.height:x.screen.width;const O=Math.sign(k[T]-b[T]);const N=Math.abs(k[T]-b[T])/I;const F=new Set;const X=(V,L)=>{for(const[W,Q]of L){const J=V.get(W);if(J&&Number.isFinite(J[S])&&Number.isFinite(Q[S])&&Math.abs(Q[S]-J[S])>.01)F.add(W)}};let C=E;while(A<_){const V=await u(v,b,k,$,false,SCROLL_REST_MS,x);if(V!==void 0)x.orientation=V;A++;await a(w,v,"swipe",[b],[k],$,V);await delay5(90);const L=await d(w);await delay5(70);const W=await d(w);if(!L||!W)throw new Error("The app stopped answering while scrolling.");const Q=p(L,W,S)*O;X(E,W);C=W;if(Q>.004||Q>=-.004&&h(L,W,S,.002,false)===0&&h(E,W,S,.01)>0){E=W;continue}const J=await m(w,S);if(!J)throw new Error("The app stopped answering while scrolling.");X(E,J);C=J;if(h(E,J,S,.01)===0){P=true;break}const te=p(E,J,S)*O;if(te>.01&&te<N*.85){P=true;break}E=J}const U=[...C].filter(([V,L])=>F.has(V)&&L[S]>=0&&L[S]<=1&&V.split("|")[1]);const j=V=>V.sort((L,W)=>(W[1][S]-L[1][S])*-O)[0];const H=j(U.filter(([V])=>!V.startsWith("Text|")))??j(U);return{swipes:A,atEnd:P,lastRow:H?H[0].split("|")[1]:void 0}}async function g(w,v){if(w.platform==="android"){const k=await adbAppFrame(w.serial,w.packageName);const $=k??{x:0,y:0,...await adbScreenSize(w.serial)};return{x:$.x+v.x*$.width,y:$.y+v.y*$.height}}const b=await t(w);return{x:v.x*b.width,y:v.y*b.height}}async function f(w,v,b,k,$){if(k&&($==="app"||$==="loose")){const _=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:4e3,silent:true}).catch(()=>null);const S=(_?.elements??[]).filter(x=>x.tap&&x.tap.x>=0&&x.tap.x<=1&&x.tap.y>=0&&x.tap.y<=1);const E=b.trim().toLowerCase();const A=x=>(x.text??x.label??"").trim().toLowerCase();const P=$==="app"?[S.filter(x=>x.testID===b),S.filter(x=>A(x)===E)]:[S.filter(x=>!!E&&A(x).includes(E))];for(const x of P){if(!x.length)continue;const T=x.filter(F=>F.interactive);const I=T.length?T:x;const O=I.filter((F,X)=>I.findIndex(C=>Math.abs(C.tap.x-F.tap.x)<.02&&Math.abs(C.tap.y-F.tap.y)<.02)===X);if(O.length>1){return{tie:true,miss:`${O.length} elements match "${b}", so nothing was touched: ${O.slice(0,5).map(F=>F.testID?`testID ${F.testID}`:JSON.stringify(F.text??F.label??F.name)).join(", ")}. Use an exact testID.`}}const N=O[0];return{...await g(v,N.tap),what:`"${N.text??N.label??N.name}"${N.testID?` [testID=${N.testID}]`:""}`}}}if(($==="native"||$==="nativeExact")&&v.platform!=="ios-device"){const _=await c(v,b,$==="nativeExact",{id:w,online:k});if(typeof _!=="string")return _;if(/equally well/.test(_))return{miss:_,tie:true};if(/empty app shell/.test(_))return{miss:_}}return{miss:`Nothing on screen matches "${b}".`}}return{primeScreen:n,screenOf:t,touch:s,hitAt:r,buoyCover:o,mark:a,reactSpot:l,labelSpot:c,swipe:u,scrollToEnd:y,findSpot:f}}async function nativeLabelTap(e,n,t,s,i=false){const r=nativeKit(e);const{id:o,host:a,online:l}=await hostTarget(e,n);if(a.platform==="ios-device")return null;freshScreen(a);await r.primeScreen(o,a,l);const c=await r.findSpot(o,a,t,l,i?"nativeExact":"native");if("miss"in c)return null;if(l&&await r.buoyCover(o,a,c))return null;const u=s?600:void 0;const d=await r.touch(a,c,u);await r.mark(o,a,u?"hold":"tap",[[c.x,c.y]],void 0,u??150,d);return`${u?"Long-pressed":"Tapped"} ${c.what} with a real touch at (${Math.round(c.x)}, ${Math.round(c.y)}).`}function warmNativeTouch(e){if(process.platform!=="darwin"||process.env.BUOY_WARM_TOUCH==="0")return;const n=Date.now();const t=async()=>{try{const s=e.peekProjectDevice();const i=s?e.listDevices().find(r=>r.deviceId===s)?.platform:void 0;if(s&&i==="ios"){const{host:r}=await hostTarget(e,s);if(r.platform!=="ios")return;const o=await simHidTool();if(o)await runHidStream(o,r.udid,["--device",r.udid,"ping"]);await deviceOrientation(r.udid);await loadBars();if(!barCache.has(r.udid)){const a=await iosTree(r.udid).catch(()=>null);if(a?.screen.width)await findByLabel(r.udid,"\0no such label",a,false,[.95]).catch(()=>void 0)}return}if(s&&i)return}catch{}if(Date.now()-n<3e4)setTimeout(()=>void t(),500).unref()};setTimeout(()=>void t(),300).unref()}function registerNativeTools(e,n){warmNativeTouch(n);const{primeScreen:t,screenOf:s,touch:i,hitAt:r,buoyCover:o,mark:a,reactSpot:l,labelSpot:c,swipe:u,scrollToEnd:d}=nativeKit(n);const h=import_zod15.z.string().optional().describe("Android: the adb serial, when several devices are connected.");const p=m=>m.platform==="ios"?"pt":"px";e.registerTool("native_tap",{_meta:coreTool(),description:"Tap the screen for real (an OS touch: CoreDevice on the iOS Simulator, the way Xcode sends it, or idb on older Xcode; adb on Android), for native-only controls like a native tab bar or a system dialog, or when tap_element finds no handler. Pass holdMs for a long press. label and x/y work even when the app's JS is stuck.",inputSchema:{label:import_zod15.z.string().optional().describe('Native accessibility label, e.g. "Rewards" for a native tab.'),query:import_zod15.z.string().optional().describe("Text of an element from describe_screen."),testID:import_zod15.z.string().optional(),nativeTag:import_zod15.z.number().optional(),x:import_zod15.z.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:import_zod15.z.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),holdMs:import_zod15.z.number().min(100).max(1e4).optional().describe("Hold the touch this long: a long press (context menus, Paste, reorder handles). 600 is a typical long press."),allowBuoy:import_zod15.z.boolean().optional().describe("Touch even if Buoy's own panel covers the spot. By default the tap stops there, because it would hit Buoy instead of the app."),serial:h,deviceId:deviceIdInput}},async m=>{try{const y=m.nativeTag!=null||!!m.testID||!!m.query;const{id:g,host:f,online:w}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:!y});freshScreen(f);await t(g,f,w);if(f.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);let v;if(m.x!=null&&m.y!=null){const $=await s(f);v={x:m.x*$.width,y:m.y*$.height,what:`(${m.x.toFixed(2)}, ${m.y.toFixed(2)})`}}else if(y){v=await l(g,f,m)}else if(m.label){if(f.platform==="ios-device")return fail2("label works on simulators and Android. On a physical iPhone, tap by query, testID or nativeTag (from describe_screen), or by x and y.");v=await c(f,m.label,false,{id:g,online:w})}else{return fail2("Give label, query, testID, nativeTag, or x and y.")}if(typeof v==="string")return fail2(v);if(w&&!m.allowBuoy){const $=await o(g,f,v);if($)return fail2($)}const b=await i(f,v,m.holdMs);await a(g,f,m.holdMs?"hold":"tap",[[v.x,v.y]],void 0,m.holdMs??150,b);const k=w?await observeAfterAction(n,g):" (The app is not connected, so its screen was not read back.)";return text(`${m.holdMs?`Long-pressed (${m.holdMs}ms)`:"Tapped"} ${v.what} with a real touch at (${Math.round(v.x)}, ${Math.round(v.y)}) ${p(f)}.${k}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_type",{description:"Type text on the real keyboard into the focused field (CoreDevice or idb on the iOS Simulator, adb on Android). Tap the field first (tap_element or native_tap). Unlike tap_element's text, this goes through the keyboard like a person typing. It types plain ASCII only (letters, digits, punctuation, newlines).",inputSchema:{text:import_zod15.z.string().describe("The text to type. Write {{secret:NAME}} for a password: Buoy types BUOY_SECRET_NAME from its environment and hides it in what it returns. Prefer real password fields (shown as dots)."),serial:h,deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});if(g.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);const w=fillSecrets(m.text);let v=false;if(g.platform==="android")await adbText(g.serial,w);else if(g.platform==="ios-device")await simHidType(await simHidTool(),await coreDeviceId(g.udid),w);else{const k=await simHidTool();if(k){await simHidType(k,g.udid,w);v=true}else{await assertSimTouchable(g.udid);await idb(["ui","text","--udid",g.udid,w])}}if(!v)await delay5(300);const b=f?await observeAfterAction(n,y):"";return text(`Typed ${JSON.stringify(m.text)} on the keyboard.${b}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_swipe",{_meta:coreTool(),description:"Swipe the screen for real (CoreDevice or idb on the iOS Simulator, adb on Android): scroll a list, dismiss a sheet, change a page. Give a direction (the way your finger moves) or from/to points as fractions of the screen. To reach the bottom of a list, pass untilEnd: true. It swipes again until the screen stops moving, in one call.",inputSchema:{direction:import_zod15.z.enum(["up","down","left","right"]).optional().describe('"up" scrolls content down the list (finger moves up).'),fromX:import_zod15.z.number().min(0).max(1).optional(),fromY:import_zod15.z.number().min(0).max(1).optional(),toX:import_zod15.z.number().min(0).max(1).optional(),toY:import_zod15.z.number().min(0).max(1).optional(),durationMs:import_zod15.z.number().min(50).max(5e3).optional().describe("Default 300 (1200 with drag)."),drag:import_zod15.z.boolean().optional().describe("Drag instead of flick: press, hold, then move (reorder handles, drag and drop, sliders). On the iOS Simulator with older Xcode (idb) it is a slow swipe only."),untilEnd:import_zod15.z.boolean().optional().describe("Keep swiping until the screen stops moving (the end of the list), up to 15 swipes. Returns the last screen."),allowBuoy:import_zod15.z.boolean().optional().describe("Swipe even if Buoy's own panel covers the starting point."),serial:h,deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});freshScreen(g);await t(y,g,f);if(g.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);const w=await s(g);let v;let b;if(m.fromX!=null&&m.fromY!=null&&m.toX!=null&&m.toY!=null){v=[m.fromX,m.fromY];b=[m.toX,m.toY]}else{({from:v,to:b}=swipePoints(m.direction??"up",!!m.untilEnd))}const k=[v[0]*w.width,v[1]*w.height];const $=[b[0]*w.width,b[1]*w.height];let _=0;if(m.drag&&g.platform==="android"){const x=Number((await adbShell(g.serial,"settings get secure long_press_timeout").catch(()=>"")).trim())||500;_=x*1.5+600}const S=Math.max(m.durationMs??(m.drag?1200:300),_);if(f&&!m.allowBuoy){const x=await o(y,g,{x:k[0],y:k[1]});if(x)return fail2(x)}if(m.untilEnd){if(m.drag)return fail2("untilEnd scrolls with flicks. Leave out drag.");if(!f)return fail2("untilEnd needs the app connected, to see when the list stops moving.");const x=await d(y,g,k,$,m.durationMs??250);const{swipes:T,atEnd:I}=x;const O=await observeAfterAction(n,y,void 0,0);const{lastRow:N}=x;return text(`${I?`Reached the end of the list after ${T} swipe(s)${N?`. The last row is "${N}"`:""}`:`Swiped ${T} times and the screen was still moving`}.${O}`)}const E=g.platform==="android"?null:await simHidTool();const A=await u(g,k,$,S,m.drag);await a(y,g,m.drag?"drag":"swipe",[k],[$],S+(m.drag&&E?700:0),A);await delay5(400);const P=f?await observeAfterAction(n,y):"";return text(`${m.drag?"Dragged":"Swiped"} from (${k.map(Math.round).join(", ")}) to (${$.map(Math.round).join(", ")}) ${p(g)}.${P}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_inspect",{description:"Read native controls on the screen. Pass pointX and pointY for points on iOS Sims, pixels elsewhere. Use x and y for screen fractions (0 to 1). Leave all points out to list controls, with types, labels, IDs, and frames. This includes tabs, alerts, and sheets. A point read also checks the app view and Buoy's cover. iOS Sims use idb; Android uses uiautomator. Real iPhones show just the app view.",inputSchema:{pointX:import_zod15.z.number().finite().min(0).optional().describe("From the left. iOS Sims use points. Other hosts use pixels."),pointY:import_zod15.z.number().finite().min(0).optional().describe("From the top. iOS Sims use points. Other hosts use pixels."),x:import_zod15.z.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:import_zod15.z.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),serial:h,deviceId:deviceIdInput}},async m=>{try{const y=m.pointX!=null||m.pointY!=null;if(y&&(m.pointX==null||m.pointY==null))return fail2("Use pointX and pointY together.");if(y&&(m.x!=null||m.y!=null))return fail2("Use pointX/pointY or x/y, not both.");if(!y&&m.x==null!==(m.y==null))return fail2("Use x and y together.");const{id:g,host:f,online:w}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});freshScreen(f);await t(g,f,w);const v=p(f);if(!y&&(m.x==null||m.y==null)){if(f.platform==="ios-device")return fail2("A physical iPhone doesn't share its native element tree with this Mac. Use describe_screen for the app's elements, or native_inspect with x and y to see what is at a point.");let _;let S=0;if(f.platform==="android"){const A=await adbUiNodes(f.serial);_=A.filter(P=>P.text||P.desc||P.id||P.clickable).map(P=>{const x=P.bounds;const T=[P.cls.split(".").pop(),P.text?JSON.stringify(P.text.slice(0,80)):"",P.desc?`desc=${JSON.stringify(P.desc.slice(0,60))}`:"",P.id?`id=${P.id.split("/").pop()}`:"",P.clickable?"(clickable)":""];return`${T.filter(Boolean).join(" ")} at (${x.x}, ${x.y}) ${x.width}x${x.height}`})}else{const{elements:A,screen:P}=await iosNativeElements(f.udid);const x=A.filter(T=>!P.width||T.frame.y<P.height&&T.frame.y+T.frame.height>0&&T.frame.x<P.width&&T.frame.x+T.frame.width>0);S=A.length-x.length;_=x.map(axLine)}const E=_.slice(0,150);return text([`${_.length} native element(s) on screen, top to bottom (${v}; frames are x, y, width x height)${S?`. ${S} more are scrolled out of view`:""}:`,...E.map(A=>`- ${A}`),..._.length>E.length?[`(${_.length-E.length} more not shown.)`]:[],"Tap one with native_tap label or x/y. For the app's React elements (testID, handlers), use describe_screen."].join("\n"))}let b;if(y){const _=await s(f);b={x:m.pointX,y:m.pointY};if(b.x>=_.width||b.y>=_.height){return fail2(`That point is outside the screen (${_.width} x ${_.height} ${v}).`)}}else{const _=await s(f);b={x:m.x*_.width,y:m.y*_.height}}const k=[y?`At (${Math.round(b.x)}, ${Math.round(b.y)}) ${v}:`:`At (${m.x.toFixed(2)}, ${m.y.toFixed(2)}) = (${Math.round(b.x)}, ${Math.round(b.y)}) ${v}:`];const $=w?r(g,f,b):Promise.resolve(null);if(f.platform==="android"){const _=(await adbUiNodes(f.serial)).filter(E=>{const A=E.bounds;return b.x>=A.x&&b.x<=A.x+A.width&&b.y>=A.y&&b.y<=A.y+A.height});const S=_.sort((E,A)=>E.bounds.width*E.bounds.height-A.bounds.width*A.bounds.height)[0];k.push(S?`- Native: ${S.cls.split(".").pop()}${S.text?` ${JSON.stringify(S.text.slice(0,80))}`:""}${S.desc?` desc=${JSON.stringify(S.desc.slice(0,60))}`:""}${S.id?` id=${S.id.split("/").pop()}`:""} at (${S.bounds.x}, ${S.bounds.y}) ${S.bounds.width}x${S.bounds.height}`:"- Native: nothing with a label or id here.")}else if(f.platform==="ios"){const _=JSON.parse(await idb(["ui","describe-point","--udid",f.udid,String(Math.round(b.x)),String(Math.round(b.y)),"--json"],5e3));k.push(_?.type&&_.frame?.width?`- Native: ${axLine(_)}`:"- Native: no accessible element here (plain views and canvases often have none).")}else{k.push("- Native: not available on a physical iPhone.")}if(!w)k.push("- App: not connected, so its views can't be read.");else{const _=await $;if(!_)k.push("- App: no answer (an older Buoy without hitTest, or the JS thread is busy).");else if(!_.found)k.push(`- App: ${_.reason??"no React view here (native UI, or outside the app's views)."}`);else{const S=_.view;const E=[S.name,S.testID?`testID=${S.testID}`:"",S.label?`label=${JSON.stringify(S.label)}`:"",S.text?JSON.stringify(S.text.slice(0,60)):""].filter(Boolean).join(" ");k.push(`- App: ${E}${_.path?.length?` in ${_.path.join(" < ")}`:""}.`);if(_.buoy)k.push("- This spot is Buoy's own overlay. A real touch here hits Buoy, not the app; close or minimize the panel first.");else if(_.target){const A=_.target;k.push(`- A tap here presses ${A.testID?`testID ${A.testID}`:JSON.stringify(A.text??A.label??A.name)} (${A.name}${A.nativeTag!=null?`, nativeTag ${A.nativeTag}`:""}).`)}else k.push("- Nothing tappable in the app here.")}}return text(k.join("\n"))}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_pinch",{description:"Pinch, spread or turn with two real fingers (CoreDevice on the iOS Simulator and a physical iPhone): zoom a map or photo, or rotate, in one call. scale above 1 zooms in (fingers spread), below 1 zooms out. The reply says how far a map turned and zoomed, read from the map itself. For other views it says how much of the area changed and shows it. No screenshot is needed. Not on Android or older Xcode, which can't send two fingers.",_meta:coreTool(),inputSchema:{x:import_zod15.z.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),y:import_zod15.z.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),scale:import_zod15.z.number().min(.1).max(10).optional().describe("How far the fingers spread: 2 doubles the gap (zoom in), 0.5 halves it (zoom out). Default 2."),rotate:import_zod15.z.number().min(-180).max(180).optional().describe("Turn the fingers this many degrees while pinching, clockwise. Default 0."),durationMs:import_zod15.z.number().min(100).max(5e3).optional().describe("Default 500."),allowBuoy:import_zod15.z.boolean().optional().describe("Pinch even if a finger starts on Buoy."),deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{},{allowOffline:true});freshScreen(g);await t(y,g,f);if(g.platform==="android")return fail2("Android's input command sends one finger only, so Buoy can't pinch there yet. Use the app's zoom buttons, or native_swipe for one-finger moves.");const w=await simHidTool();if(!w)return fail2(g.platform==="ios-device"?PHONE_TOUCH:"Pinching needs Xcode's CoreDevice touch support (Xcode 26 or later); idb sends one finger only.");const v=await s(g);const b=[(m.x??.5)*v.width,(m.y??.5)*v.height];const k=m.scale??2;const $=Math.min(v.width,v.height);const _=$*.4;const S=k>=1?Math.max($*.04,_/k):_;const E=k>=1?_:Math.max($*.04,_*k);const A=(m.rotate??0)*Math.PI/180;const P=ie=>[Math.min(v.width-1,Math.max(1,ie[0])),Math.min(v.height-1,Math.max(1,ie[1]))];const x=(ie,se)=>[P([b[0]-ie*Math.cos(se),b[1]-ie*Math.sin(se)]),P([b[0]+ie*Math.cos(se),b[1]+ie*Math.sin(se)])];const[T,I]=x(S,0);const[O,N]=x(E,A);if(f&&!m.allowBuoy){for(const[ie,se]of[T,I]){const re=await o(y,g,{x:ie,y:se});if(re)return fail2(re)}}const F=m.durationMs??500;const X={x:Math.max(0,(b[0]-_)/v.width),y:Math.max(0,(b[1]-_)/v.height),width:Math.min(1,2*_/v.width),height:Math.min(1,2*_/v.height)};const C=f?await readMaps(n,y):[];const U=ie=>ie.find(se=>!se.frame||(m.x??.5)>=se.frame.x&&(m.x??.5)<=se.frame.x+se.frame.width&&(m.y??.5)>=se.frame.y&&(m.y??.5)<=se.frame.y+se.frame.height);const j=U(C);const H=j?null:await hostPng(g).catch(()=>null);const V=g.platform==="ios-device"?await coreDeviceId(g.udid):g.udid;const L=await simHidTwo(w,V,{...v,bundleId:g.platform==="ios"?g.bundleId:null},T,I,O,N,F);await a(y,g,"pinch",[T,I],[O,N],F,L);await delay5(400);const[W,Q,J]=await Promise.all([f?observeAfterAction(n,y,void 0,0):Promise.resolve(""),H?hostPng(g).catch(()=>null):Promise.resolve(null),j?readMaps(n,y):Promise.resolve([])]);const te=E/S;const ae=te>1.01?`Spread (zoom in, x${te.toFixed(2)})`:te<.99?`Pinched (zoom out, x${te.toFixed(2)})`:"Turned";const ne=`${ae}${m.rotate?`, turning ${m.rotate} degrees,`:""} around (${Math.round(b[0])}, ${Math.round(b[1])}) ${p(g)} with two real fingers.`;const fe=j?J.find(ie=>ie.name===j.name&&ie.testID===j.testID):void 0;const ge=j&&fe?mapChange(j,fe):null;if(ge)return text(`${ne}
|
|
1076
|
+
${y}${m}`);return h.isError?{...g,isError:true}:g}const u=typedSecretValues().length?" Note: a secret was typed earlier this session; if it is still on screen, the video will show it. Typing secrets is blocked until the recording stops.":" Typing secrets ({{secret:NAME}}) is blocked until the recording stops, because a video can't hide them.";return text(`Recording ${hostLabel(o)}. Do the steps, then call screen_recording with action "stop". It stops by itself after ${MAX_RECORDING_MS/6e4} minutes.${u}`)}let s=t.deviceId;if(!s){try{s=n.requireProDevice()}catch{s=void 0}}if(!s||!sessions.has(s)){if(s&&finished.has(s)){const r=finished.get(s);finished.delete(s);return text(r)}if(sessions.size===1)s=[...sessions.keys()][0];else return fail2(sessions.size?"Several recordings are running. Pass deviceId.":'No recording is running. Start one with action "start".')}const i=sessions.get(s);await i.starting;if(!i.rec)return fail2("The recording did not start.");i.finishing??=finish(n,i);try{return text(await i.finishing)}catch(r){i.finishing=void 0;return fail2(`Could not save the recording: ${errMsg2(r)}. Call stop again to retry.`)}}catch(s){return fail2(errMsg2(s))}})}var import_zod14,import_node_fs23,import_node_path24,import_node_crypto14,sessions,finished,delay4,READ_ONLY,STEPS_HOLD_MS,STEPS_MIN_MS;var init_recording=__esm({"src/tools/packs/recording.ts"(){"use strict";init_policy();import_zod14=require("zod");import_node_fs23=require("fs");import_node_path24=__toESM(require("path"));import_node_crypto14=require("crypto");init_packKit();init_hostKit();init_secrets();init_flow();init_interact();init_recorder();sessions=new Map;finished=new Map;delay4=e=>new Promise(n=>setTimeout(n,e));READ_ONLY=/^(get_|list_|describe_screen$|screenshot_component$|screen_recording$|wait_for_automation$|compare_reports$)/;STEPS_HOLD_MS=1200;STEPS_MIN_MS=3e3}});async function phoneScreenSize(e){const n=phoneScreens.get(e);if(n&&Date.now()-n.at<SCREEN_TTL_MS)return n.size;const t=await phoneScreenshot(e);const s={width:t.readUInt32BE(16),height:t.readUInt32BE(20)};phoneScreens.set(e,{size:s,at:Date.now()});return s}function swipePoints(e,n){if(n){if(e==="up")return{from:[.5,.88],to:[.5,.06]};if(e==="down")return{from:[.5,.3],to:[.5,.94]};if(e==="left")return{from:[.9,.5],to:[.1,.5]};return{from:[.1,.5],to:[.9,.5]}}const t=e==="left"?-.35:e==="right"?.35:0;const s=e==="up"?-.3:e==="down"?.3:0;return{from:[.5-t/2,.5-s/2],to:[.5+t/2,.5+s/2]}}function forgetScreenSize(e){barCache.delete(e);simScreens.delete(e);phoneScreens.delete(e)}function freshScreen(e){if(e.platform==="android")return;simScreens.delete(e.udid);phoneScreens.delete(e.udid)}async function xcodeShimDir(){const e=(await execFileAsync14("xcode-select",["-p"],{timeout:5e3}).catch(()=>({stdout:""}))).stdout.trim();if(!e.endsWith("/Contents/Developer"))return null;const n=import_node_path25.default.dirname(e);const t=import_node_path25.default.join(e,"Library","PrivateFrameworks","SimulatorKit.framework");const s=import_node_path25.default.join(n,"SharedFrameworks","SimulatorKit.framework");const i=c=>import_node_fs24.promises.access(c).then(()=>true,()=>false);if(await i(t)||!await i(s))return null;const r=(await execFileAsync14("plutil",["-extract","DTXcodeBuild","raw","-o","-",import_node_path25.default.join(n,"Info.plist")]).catch(()=>({stdout:"x"}))).stdout.trim();const o=import_node_path25.default.join(import_node_os17.default.homedir(),".buoy","xcode-shim",r.replace(/[^\w.-]/g,"_"),"Contents");const a=import_node_path25.default.join(o,"Developer");if(await i(import_node_path25.default.join(a,"Library","PrivateFrameworks","SimulatorKit.framework")))return a;await import_node_fs24.promises.mkdir(import_node_path25.default.join(a,"Library","PrivateFrameworks"),{recursive:true});const l=async(c,u,d)=>{for(const h of await import_node_fs24.promises.readdir(c)){if(h===d)continue;await import_node_fs24.promises.symlink(import_node_path25.default.join(c,h),import_node_path25.default.join(u,h)).catch(()=>void 0)}};await l(n,o,"Developer");await l(e,a,"Library");await l(import_node_path25.default.join(e,"Library"),import_node_path25.default.join(a,"Library"),"PrivateFrameworks");await l(import_node_path25.default.join(e,"Library","PrivateFrameworks"),import_node_path25.default.join(a,"Library","PrivateFrameworks"),"").catch(()=>void 0);await import_node_fs24.promises.symlink(s,import_node_path25.default.join(a,"Library","PrivateFrameworks","SimulatorKit.framework")).catch(()=>void 0);return a}async function restartCompanion(e){const n=`/tmp/idb/${e}.buoy-restart`;await import_node_fs24.promises.mkdir("/tmp/idb",{recursive:true}).catch(()=>void 0);const t=async()=>{try{await(await import_node_fs24.promises.open(n,"wx")).close();return true}catch{const s=await import_node_fs24.promises.stat(n).then(i=>Date.now()-i.mtimeMs,()=>0);if(s>15e3){const i=`${n}.stale-${process.pid}-${Date.now()}`;await import_node_fs24.promises.rename(n,i).then(()=>import_node_fs24.promises.unlink(i).catch(()=>void 0),()=>void 0);return t()}return false}};if(!await t()){for(let s=0;s<60;s++){await delay5(250);if(!await import_node_fs24.promises.stat(n).then(()=>true,()=>false))return}return}try{await execFileAsync14("pkill",["-f",`idb_companion --udid ${e}`],{timeout:5e3}).catch(()=>void 0);await delay5(300);const s=await xcodeShimDir().catch(()=>null);const i=(0,import_node_child_process17.spawn)("idb_companion",["--udid",e,"--grpc-domain-sock",`/tmp/idb/${e}_companion.sock`,"--only","simulator"],{detached:true,stdio:"ignore",env:s?{...process.env,DEVELOPER_DIR:s}:process.env});i.on("error",()=>void 0);i.unref();await delay5(1800)}finally{await import_node_fs24.promises.unlink(n).catch(()=>void 0)}}async function idb(e,n=15e3,t=false){try{const{stdout:s}=await execFileAsync14("idb",e,{timeout:n});return s}catch(s){const i=s;if(i.code==="ENOENT")throw new Error(IDB_MISSING);const r=(i.stderr||i.message||"").slice(0,300);const o=e.includes("--udid")?e[e.indexOf("--udid")+1]:void 0;if(!t&&o&&/not booted|connection refused|Failed to connect to companion/i.test(r)){await restartCompanion(o);return idb(e,n,true)}if(/SimulatorKit is required/i.test(r)){if(!t&&o&&await xcodeShimDir().catch(()=>null)){await restartCompanion(o);return idb(e,n,true)}throw new Error("idb can't send touches with this Xcode: it can't load SimulatorKit, and Buoy could not work around it. Use tap_element (it calls the element's handler) for now. Reading the screen (describe_screen, screenshot_component) still works.")}throw new Error(`idb ${e[0]} ${e[1]??""} failed: ${r}`)}}async function iosTree(e){const n=JSON.parse(await idb(["ui","describe-all","--udid",e,"--json","--nested"]));const t=[];const s=r=>{t.push(r);for(const o of r.children??[])s(o)};n.forEach(s);const i=t.find(r=>r.type==="Application"&&r.frame);if(!t.some(r=>(r.frame?.width??0)>0))throw new AxShellOnlyError(e);return{all:t,screen:i?.frame?{width:i.frame.width,height:i.frame.height}:{width:0,height:0}}}async function loadBars(){if(barFileRead||process.env.VITEST)return;barFileRead=true;try{const e=JSON.parse(await import_node_fs24.promises.readFile(BAR_FILE,"utf8"));for(const[n,t]of Object.entries(e))if(!barCache.has(n)&&t?.screen&&Array.isArray(t.els))barCache.set(n,t)}catch{}}function saveBars(){if(process.env.VITEST)return;const e=Object.fromEntries([...barCache].map(([t,s])=>[t,{screen:s.screen,els:s.els.map(i=>({AXLabel:i.AXLabel,type:i.type,frame:i.frame}))}]));const n=`${BAR_FILE}.${process.pid}`;void import_node_fs24.promises.mkdir(import_node_path25.default.dirname(BAR_FILE),{recursive:true}).then(()=>import_node_fs24.promises.writeFile(n,JSON.stringify(e))).then(()=>import_node_fs24.promises.rename(n,BAR_FILE)).catch(()=>void 0)}async function cachedBarControl(e,n){await loadBars();const t=barCache.get(e);const s=n.trim().toLowerCase();const i=t?.els.find(r=>(r.AXLabel??"").trim().toLowerCase()===s&&r.frame);if(!t||!i?.frame)return null;try{const r=Math.round(i.frame.x+i.frame.width/2);const o=Math.round(i.frame.y+i.frame.height/2);const a=JSON.parse(await idb(["ui","describe-point","--udid",e,String(r),String(o),"--json"],3e3));if((a.AXLabel??"").trim().toLowerCase()===s&&a.frame)return{el:a,screen:t.screen}}catch{}barCache.delete(e);saveBars();return null}async function findByLabel(e,n,t,s=false,i=[.95,.97,.09,.06]){const r=n.trim().toLowerCase();const o=Date.now()+8e3;const a=m=>{const y=(m.AXLabel??"").trim().toLowerCase();if(!y)return 0;if(y===r)return 3;if(y.startsWith(r))return 2;if(y.includes(r))return 1;return 0};const{width:l,height:c}=t.screen;const u=m=>{if(!l||!m.frame)return true;const y=m.frame.x+m.frame.width/2;const g=m.frame.y+m.frame.height/2;return y>=0&&y<=l&&g>=0&&g<=c};let d=t.all.filter(m=>a(m)>0&&m.frame&&m.type!=="Application"&&u(m));if(!d.some(m=>a(m)===3)&&t.screen.width){const m=i.map(y=>{const g=[];for(let f=20;f<t.screen.width;f+=40)g.push([f,Math.round(t.screen.height*y)]);return g});for(const y of m){if(Date.now()>=o)break;const g=await Promise.allSettled(y.map(async([w,v])=>JSON.parse(await idb(["ui","describe-point","--udid",e,String(w),String(v),"--json"],3e3))));const f=[];for(const w of g){if(w.status==="rejected"){if(IDB_SETUP.test(String(w.reason)))throw w.reason;continue}const v=w.value;if(v?.frame&&v.AXLabel&&v.type&&v.type!=="Application")f.push(v);if(a(v)>0&&v.frame)d.push(v)}if(f.length){const w=barCache.get(e);const v=w&&w.screen.width===t.screen.width&&w.screen.height===t.screen.height;const b=[...v?w.els:[],...f].filter((_,$,k)=>k.findIndex(S=>S.AXLabel===_.AXLabel&&JSON.stringify(S.frame)===JSON.stringify(_.frame))===$);barCache.set(e,{screen:t.screen,els:b});saveBars()}if(d.some(w=>a(w)===3))break}}const h=Math.max(0,...d.map(a));if(s&&h<3)return[];d=d.filter(m=>a(m)===h);let p=d.filter((m,y)=>d.findIndex(g=>JSON.stringify(g.frame)===JSON.stringify(m.frame))===y);if(p.length>1){const m=p.filter(y=>(y.AXLabel??"").trim()===n.trim());if(m.length)p=m}if(p.length>1){const m=p.filter(y=>y.type&&!/StaticText|Text$|Other|Image/.test(y.type));if(m.length)p=m}return p}async function iosNativeElements(e){const n=await iosTree(e);const t=n.all.filter(a=>a.frame&&a.frame.width>0&&a.type!=="Application");const s=a=>`${a.AXLabel??""}|${JSON.stringify(a.frame)}`;const i=new Set(t.map(s));const{width:r,height:o}=n.screen;if(r){const a=t.filter(d=>!!d.type&&!/StaticText|Text$|Other|Image|Group|Heading/.test(d.type));const l=(d,h)=>a.some(p=>d>=p.frame.x&&d<=p.frame.x+p.frame.width&&h>=p.frame.y&&h<=p.frame.y+p.frame.height);const c=[];for(const d of[.95,.97,.09,.06])for(let h=20;h<r;h+=40){const p=Math.round(o*d);if(!l(h,p))c.push([h,p])}const u=Date.now()+6e3;for(let d=0;d<c.length&&Date.now()<u;d+=8){const h=await Promise.all(c.slice(d,d+8).map(([p,m])=>idb(["ui","describe-point","--udid",e,String(p),String(m),"--json"],3e3).then(y=>JSON.parse(y),y=>{if(IDB_SETUP.test(String(y)))throw y;return null})));for(const p of h){if(!p?.type||!p.frame?.width||p.type==="Application"||i.has(s(p)))continue;i.add(s(p));t.push(p)}}}t.sort((a,l)=>a.frame.y-l.frame.y||a.frame.x-l.frame.x);return{elements:t,screen:n.screen}}function axLine(e){const n=e.frame;const t=[e.type??"Element",e.AXLabel?JSON.stringify(e.AXLabel.slice(0,80)):"",e.AXUniqueId?`id=${e.AXUniqueId}`:"",e.AXValue?`value=${JSON.stringify(String(e.AXValue).slice(0,40))}`:"",e.enabled===false?"(disabled)":""].filter(Boolean);return`${t.join(" ")} at (${Math.round(n.x)}, ${Math.round(n.y)}) ${Math.round(n.width)}x${Math.round(n.height)}`}function nativeKit(e){async function n(w,v,b){if(v.platform!=="ios"||!b)return;try{const _=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:2e3,silent:true});const $=_?.screen?.width??0;const k=_?.screen?.height??0;const S=Math.max($,k)/Math.max(1,Math.min($,k));if($>0&&k>0&&S>1.7)simScreens.set(v.udid,{size:{width:$,height:k},at:Date.now()})}catch{}}async function t(w,v){if(w.platform==="android")return adbScreenSize(w.serial);if(w.platform==="ios-device")return{...await phoneScreenSize(w.udid),isPhone:true};const b=simScreens.get(w.udid);if(!v&&b&&Date.now()-b.at<SCREEN_TTL_MS)return b.size;try{const _=(v??await iosTree(w.udid)).screen;if(!_.width)throw new Error("Could not read the simulator's screen size from idb.");simScreens.set(w.udid,{size:_,at:Date.now()});return _}catch(_){if(!v&&b&&_ instanceof AxShellOnlyError)return b.size;if(v||!await simHidTool())throw _;const $=import_node_path25.default.join(import_node_os17.default.tmpdir(),`buoy-size-${process.pid}-${Date.now()}.png`);try{await execFileAsync14("xcrun",["simctl","io",w.udid,"screenshot","--type=png",$],{timeout:2e4});const k=await import_node_fs24.promises.readFile($);return{width:k.readUInt32BE(16),height:k.readUInt32BE(20)}}finally{await import_node_fs24.promises.rm($,{force:true}).catch(()=>void 0)}}}async function s(w,v,b,_){if(w.platform==="android")await adbTap(w.serial,v.x,v.y,b);else if(w.platform==="ios-device"){const $=await simHidTool();if(!$)throw new Error(PHONE_TOUCH);return simHidTap($,await coreDeviceId(w.udid),await t(w),v.x,v.y,b,_)}else if(await simHidTool()){return simHidTap(await simHidTool(),w.udid,{...await t(w),bundleId:w.bundleId},v.x,v.y,b,_)}else{await assertSimTouchable(w.udid);await idb(["ui","tap","--udid",w.udid,...b?["--duration",(b/1e3).toFixed(2)]:[],String(Math.round(v.x)),String(Math.round(v.y))])}}async function i(w,v){if(w.platform==="android"){const _=await adbAppFrame(w.serial,w.packageName);const $=_??{x:0,y:0,...await adbScreenSize(w.serial)};return[(v.x-$.x)/$.width,(v.y-$.y)/$.height]}const b=await t(w);return[v.x/b.width,v.y/b.height]}async function r(w,v,b){try{const[_,$]=await i(v,b);if(!(_>=0&&_<=1&&$>=0&&$<=1))return null;return await e.callAction(UI_TOOL_ID2,"hitTest",{x:_,y:$},w,{timeoutMs:2500,silent:true})}catch{return null}}async function o(w,v,b){const _=await r(w,v,b);if(!_?.found||!_.buoy)return null;const $=_.path?.length?` (${_.path.slice(0,3).join(" in ")})`:"";return`Buoy's own overlay${$} is on top at that spot, so a real touch would land on Buoy, not the app. Nothing was touched. Close or minimize the Buoy panel first, or pass allowBuoy: true to touch Buoy on purpose.`}async function a(w,v,b,_,$,k,S){if(!isRecording(w))return;try{const E=await t(v);const A=T=>[T[0]/E.width,T[1]/E.height];const P=Date.now()-k-(v.platform==="android"?50:80);const x=S!==void 0?S:v.platform==="android"?null:await deviceOrientation(v.udid);noteTouch(w,{at:P,kind:b,ms:k,from:_.map(A),to:$?.map(A),screen:E,orientation:x})}catch{}}async function l(w,v,b){const _=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w);const $=(b.query??"").toLowerCase();const k=(_.elements??[]).filter(x=>b.nativeTag!=null?x.nativeTag===b.nativeTag:b.testID?x.testID===b.testID:(x.text??x.label??"").toLowerCase()===$);const S=k.filter(x=>x.tap);if(S.length===0)return"No element on screen matches that. Call describe_screen, or use label for native controls.";if(S.length>1&&b.nativeTag==null)return`${S.length} elements match equally well, so nothing was tapped. Use nativeTag: ${S.map(x=>`#${x.nativeTag}`).join(", ")}.`;const E=S[0].tap;const A=`"${S[0].text??S[0].label??S[0].name}"`;if(v.platform==="android"){const x=await adbAppFrame(v.serial,v.packageName);const T=x??{x:0,y:0,...await adbScreenSize(v.serial)};return{x:T.x+E.x*T.width,y:T.y+E.y*T.height,what:A}}const P=await t(v);return{x:E.x*P.width,y:E.y*P.height,what:A}}async function c(w,v,b=false,_){if(w.platform==="android"){const A=findUiNodes(await adbUiNodes(w.serial),v).filter(x=>!b||[x.desc,x.text].some(T=>(T??"").trim().toLowerCase()===v.trim().toLowerCase()));if(A.length===0)return`No native element labeled "${v}" was found on screen.`;if(A.length>1)return`${A.length} native elements match "${v}" equally well, so nothing was tapped: ${A.map(x=>`"${x.desc||x.text||x.id}" at (${x.bounds.x}, ${x.bounds.y})`).join(", ")}. Tap one by x/y.`;const P=A[0].bounds;return{x:P.x+P.width/2,y:P.y+P.height/2,what:`"${A[0].desc||A[0].text||A[0].id}" (${A[0].cls.split(".").pop()})`}}const $=await cachedBarControl(w.udid,v);if($){simScreens.set(w.udid,{size:$.screen,at:Date.now()});const A=$.el.frame;return{x:A.x+A.width/2,y:A.y+A.height/2,what:`"${$.el.AXLabel}" (${$.el.type})`}}let k;try{k=await iosTree(w.udid)}catch(A){if(!(A instanceof AxShellOnlyError))throw A;if(_?.online){const P=await f(_.id,w,v,true,"app");if(!("miss"in P))return{...P,what:`${P.what} (found in the app's own screen, because idb can only see an empty shell on this simulator)`};if(P.tie)return P.miss}return A.message}await t(w,k);const S=await findByLabel(w.udid,v,k,b);if(S.length===0)return`No native element labeled "${v}" was found on screen.`;if(S.length>1)return`${S.length} native elements match "${v}" equally well, so nothing was tapped: ${S.map(A=>`"${A.AXLabel}" at (${Math.round(A.frame.x)}, ${Math.round(A.frame.y)})`).join(", ")}. Tap one by x/y.`;const E=S[0].frame;return{x:E.x+E.width/2,y:E.y+E.height/2,what:`"${S[0].AXLabel}" (${S[0].type})`}}async function u(w,v,b,_,$=false,k=0,S){const E=[...v,...b].map(P=>String(Math.round(P)));const A=w.platform==="android"?null:await simHidTool();if(w.platform!=="android"&&!A)await assertSimTouchable(w.udid);if(w.platform==="android"){if($)await adbShell(w.serial,`input draganddrop ${E.join(" ")} ${Math.round(_)}`);else await adbSwipe(w.serial,v,b,_);return void 0}if(A){const P=w.platform==="ios-device"?await coreDeviceId(w.udid):w.udid;const x=S?.screen??await t(w);return simHidSwipe(A,P,{...x,bundleId:w.platform==="ios"?w.bundleId:null},v,b,_,$?700:0,$?0:k,S?.orientation)}if($)await idb(["ui","swipe","--udid",w.udid,...E,"--duration",(_/1e3).toFixed(2),"--delta","4"]);else await idb(["ui","swipe","--udid",w.udid,...E,"--duration",(_/1e3).toFixed(2)]);return void 0}async function d(w){try{const v=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:3e3,silent:true});const b=new Map;for(const _ of v?.elements??[]){if(!_.tap)continue;const $=`${_.name??""}|${_.text??_.label??""}|${_.nativeTag??""}`;if(b.has($))b.set($,{x:NaN,y:NaN});else b.set($,_.tap)}return b}catch{return null}}function h(w,v,b,_,$=true){let k=0;let S=0;for(const[E,A]of v){const P=w.get(E);if(!P)S++;else if(Number.isFinite(P[b])&&Number.isFinite(A[b])&&Math.abs(A[b]-P[b])>_)k++}return k+($&&S>=2?S:0)}function p(w,v,b){const _=[];for(const[$,k]of v){const S=w.get($);if(S&&Number.isFinite(S[b])&&Number.isFinite(k[b])&&Math.abs(k[b]-S[b])>.002)_.push(k[b]-S[b])}if(_.length<2)return 0;_.sort(($,k)=>$-k);return _[Math.floor(_.length/2)]}async function m(w,v){await delay5(120);let b=await d(w);const _=Date.now()+2500;while(b&&Date.now()<_){await delay5(100);const $=await d(w);if(!$)break;const k=h(b,$,v,.002,false)===0;b=$;if(k)break}return b}async function y(w,v,b,_,$,k=15){const S=Math.abs(_[1]-b[1])>=Math.abs(_[0]-b[0])?"y":"x";let E=await d(w);if(E===null)throw new Error("Scrolling to the end needs the app connected, to see when the list stops moving.");let A=0;let P=false;const x={screen:await t(v)};const T=S==="y"?1:0;const I=S==="y"?x.screen.height:x.screen.width;const O=Math.sign(_[T]-b[T]);const N=Math.abs(_[T]-b[T])/I;const F=new Set;const X=(V,L)=>{for(const[W,Q]of L){const J=V.get(W);if(J&&Number.isFinite(J[S])&&Number.isFinite(Q[S])&&Math.abs(Q[S]-J[S])>.01)F.add(W)}};let C=E;while(A<k){const V=await u(v,b,_,$,false,SCROLL_REST_MS,x);if(V!==void 0)x.orientation=V;A++;await a(w,v,"swipe",[b],[_],$,V);await delay5(90);const L=await d(w);await delay5(70);const W=await d(w);if(!L||!W)throw new Error("The app stopped answering while scrolling.");const Q=p(L,W,S)*O;X(E,W);C=W;if(Q>.004||Q>=-.004&&h(L,W,S,.002,false)===0&&h(E,W,S,.01)>0){E=W;continue}const J=await m(w,S);if(!J)throw new Error("The app stopped answering while scrolling.");X(E,J);C=J;if(h(E,J,S,.01)===0){P=true;break}const te=p(E,J,S)*O;if(te>.01&&te<N*.85){P=true;break}E=J}const U=[...C].filter(([V,L])=>F.has(V)&&L[S]>=0&&L[S]<=1&&V.split("|")[1]);const j=V=>V.sort((L,W)=>(W[1][S]-L[1][S])*-O)[0];const H=j(U.filter(([V])=>!V.startsWith("Text|")))??j(U);return{swipes:A,atEnd:P,lastRow:H?H[0].split("|")[1]:void 0}}async function g(w,v){if(w.platform==="android"){const _=await adbAppFrame(w.serial,w.packageName);const $=_??{x:0,y:0,...await adbScreenSize(w.serial)};return{x:$.x+v.x*$.width,y:$.y+v.y*$.height}}const b=await t(w);return{x:v.x*b.width,y:v.y*b.height}}async function f(w,v,b,_,$){if(_&&($==="app"||$==="loose")){const k=await e.callAction(UI_TOOL_ID2,"describeScreen",{},w,{timeoutMs:4e3,silent:true}).catch(()=>null);const S=(k?.elements??[]).filter(x=>x.tap&&x.tap.x>=0&&x.tap.x<=1&&x.tap.y>=0&&x.tap.y<=1);const E=b.trim().toLowerCase();const A=x=>(x.text??x.label??"").trim().toLowerCase();const P=$==="app"?[S.filter(x=>x.testID===b),S.filter(x=>A(x)===E)]:[S.filter(x=>!!E&&A(x).includes(E))];for(const x of P){if(!x.length)continue;const T=x.filter(F=>F.interactive);const I=T.length?T:x;const O=I.filter((F,X)=>I.findIndex(C=>Math.abs(C.tap.x-F.tap.x)<.02&&Math.abs(C.tap.y-F.tap.y)<.02)===X);if(O.length>1){return{tie:true,miss:`${O.length} elements match "${b}", so nothing was touched: ${O.slice(0,5).map(F=>F.testID?`testID ${F.testID}`:JSON.stringify(F.text??F.label??F.name)).join(", ")}. Use an exact testID.`}}const N=O[0];return{...await g(v,N.tap),what:`"${N.text??N.label??N.name}"${N.testID?` [testID=${N.testID}]`:""}`}}}if(($==="native"||$==="nativeExact")&&v.platform!=="ios-device"){const k=await c(v,b,$==="nativeExact",{id:w,online:_});if(typeof k!=="string")return k;if(/equally well/.test(k))return{miss:k,tie:true};if(/empty app shell/.test(k))return{miss:k}}return{miss:`Nothing on screen matches "${b}".`}}return{primeScreen:n,screenOf:t,touch:s,hitAt:r,buoyCover:o,mark:a,reactSpot:l,labelSpot:c,swipe:u,scrollToEnd:y,findSpot:f}}async function nativeLabelTap(e,n,t,s,i=false){const r=nativeKit(e);const{id:o,host:a,online:l}=await hostTarget(e,n);if(a.platform==="ios-device")return null;freshScreen(a);await r.primeScreen(o,a,l);const c=await r.findSpot(o,a,t,l,i?"nativeExact":"native");if("miss"in c)return null;if(l&&await r.buoyCover(o,a,c))return null;const u=s?600:void 0;const d=await r.touch(a,c,u);await r.mark(o,a,u?"hold":"tap",[[c.x,c.y]],void 0,u??150,d);return`${u?"Long-pressed":"Tapped"} ${c.what} with a real touch at (${Math.round(c.x)}, ${Math.round(c.y)}).`}function warmNativeTouch(e){if(process.platform!=="darwin"||process.env.BUOY_WARM_TOUCH==="0")return;const n=Date.now();const t=async()=>{try{const s=e.peekProjectDevice();const i=s?e.listDevices().find(r=>r.deviceId===s)?.platform:void 0;if(s&&i==="ios"){const{host:r}=await hostTarget(e,s);if(r.platform!=="ios")return;const o=await simHidTool();if(o)await runHidStream(o,r.udid,["--device",r.udid,"ping"]);await deviceOrientation(r.udid);await loadBars();if(!barCache.has(r.udid)){const a=await iosTree(r.udid).catch(()=>null);if(a?.screen.width)await findByLabel(r.udid,"\0no such label",a,false,[.95]).catch(()=>void 0)}return}if(s&&i)return}catch{}if(Date.now()-n<3e4)setTimeout(()=>void t(),500).unref()};setTimeout(()=>void t(),300).unref()}function registerNativeTools(e,n){warmNativeTouch(n);const{primeScreen:t,screenOf:s,touch:i,hitAt:r,buoyCover:o,mark:a,reactSpot:l,labelSpot:c,swipe:u,scrollToEnd:d}=nativeKit(n);const h=import_zod15.z.string().optional().describe("Android: the adb serial, when several devices are connected.");const p=m=>m.platform==="ios"?"pt":"px";e.registerTool("native_tap",{_meta:coreTool(),description:"Tap the screen for real (an OS touch: CoreDevice on the iOS Simulator, the way Xcode sends it, or idb on older Xcode; adb on Android), for native-only controls like a native tab bar or a system dialog, or when tap_element finds no handler. Pass holdMs for a long press. label and x/y work even when the app's JS is stuck.",inputSchema:{label:import_zod15.z.string().optional().describe('Native accessibility label, e.g. "Rewards" for a native tab.'),query:import_zod15.z.string().optional().describe("Text of an element from describe_screen."),testID:import_zod15.z.string().optional(),nativeTag:import_zod15.z.number().optional(),x:import_zod15.z.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:import_zod15.z.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),holdMs:import_zod15.z.number().min(100).max(1e4).optional().describe("Hold the touch this long: a long press (context menus, Paste, reorder handles). 600 is a typical long press."),allowBuoy:import_zod15.z.boolean().optional().describe("Touch even if Buoy's own panel covers the spot. By default the tap stops there, because it would hit Buoy instead of the app."),serial:h,deviceId:deviceIdInput}},async m=>{try{const y=m.nativeTag!=null||!!m.testID||!!m.query;const{id:g,host:f,online:w}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:!y});freshScreen(f);await t(g,f,w);if(f.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);let v;if(m.x!=null&&m.y!=null){const $=await s(f);v={x:m.x*$.width,y:m.y*$.height,what:`(${m.x.toFixed(2)}, ${m.y.toFixed(2)})`}}else if(y){v=await l(g,f,m)}else if(m.label){if(f.platform==="ios-device")return fail2("label works on simulators and Android. On a physical iPhone, tap by query, testID or nativeTag (from describe_screen), or by x and y.");v=await c(f,m.label,false,{id:g,online:w})}else{return fail2("Give label, query, testID, nativeTag, or x and y.")}if(typeof v==="string")return fail2(v);if(w&&!m.allowBuoy){const $=await o(g,f,v);if($)return fail2($)}const b=await i(f,v,m.holdMs);await a(g,f,m.holdMs?"hold":"tap",[[v.x,v.y]],void 0,m.holdMs??150,b);const _=w?await observeAfterAction(n,g):" (The app is not connected, so its screen was not read back.)";return text(`${m.holdMs?`Long-pressed (${m.holdMs}ms)`:"Tapped"} ${v.what} with a real touch at (${Math.round(v.x)}, ${Math.round(v.y)}) ${p(f)}.${_}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_type",{description:"Type text on the real keyboard into the focused field (CoreDevice or idb on the iOS Simulator, adb on Android). Tap the field first (tap_element or native_tap). Unlike tap_element's text, this goes through the keyboard like a person typing. It types plain ASCII only (letters, digits, punctuation, newlines).",inputSchema:{text:import_zod15.z.string().describe("The text to type. Write {{secret:NAME}} for a password: Buoy types BUOY_SECRET_NAME from its environment and hides it in what it returns. Prefer real password fields (shown as dots)."),serial:h,deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});if(g.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);const w=fillSecrets(m.text);let v=false;if(g.platform==="android")await adbText(g.serial,w);else if(g.platform==="ios-device")await simHidType(await simHidTool(),await coreDeviceId(g.udid),w);else{const _=await simHidTool();if(_){await simHidType(_,g.udid,w);v=true}else{await assertSimTouchable(g.udid);await idb(["ui","text","--udid",g.udid,w])}}if(!v)await delay5(300);const b=f?await observeAfterAction(n,y):"";return text(`Typed ${JSON.stringify(m.text)} on the keyboard.${b}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_swipe",{_meta:coreTool(),description:"Swipe the screen for real (CoreDevice or idb on the iOS Simulator, adb on Android): scroll a list, dismiss a sheet, change a page. Give a direction (the way your finger moves) or from/to points as fractions of the screen. To reach the bottom of a list, pass untilEnd: true. It swipes again until the screen stops moving, in one call.",inputSchema:{direction:import_zod15.z.enum(["up","down","left","right"]).optional().describe('"up" scrolls content down the list (finger moves up).'),fromX:import_zod15.z.number().min(0).max(1).optional(),fromY:import_zod15.z.number().min(0).max(1).optional(),toX:import_zod15.z.number().min(0).max(1).optional(),toY:import_zod15.z.number().min(0).max(1).optional(),durationMs:import_zod15.z.number().min(50).max(5e3).optional().describe("Default 300 (1200 with drag)."),drag:import_zod15.z.boolean().optional().describe("Drag instead of flick: press, hold, then move (reorder handles, drag and drop, sliders). On the iOS Simulator with older Xcode (idb) it is a slow swipe only."),untilEnd:import_zod15.z.boolean().optional().describe("Keep swiping until the screen stops moving (the end of the list), up to 15 swipes. Returns the last screen."),allowBuoy:import_zod15.z.boolean().optional().describe("Swipe even if Buoy's own panel covers the starting point."),serial:h,deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});freshScreen(g);await t(y,g,f);if(g.platform==="ios-device"&&!await simHidTool())return fail2(PHONE_TOUCH);const w=await s(g);let v;let b;if(m.fromX!=null&&m.fromY!=null&&m.toX!=null&&m.toY!=null){v=[m.fromX,m.fromY];b=[m.toX,m.toY]}else{({from:v,to:b}=swipePoints(m.direction??"up",!!m.untilEnd))}const _=[v[0]*w.width,v[1]*w.height];const $=[b[0]*w.width,b[1]*w.height];let k=0;if(m.drag&&g.platform==="android"){const x=Number((await adbShell(g.serial,"settings get secure long_press_timeout").catch(()=>"")).trim())||500;k=x*1.5+600}const S=Math.max(m.durationMs??(m.drag?1200:300),k);if(f&&!m.allowBuoy){const x=await o(y,g,{x:_[0],y:_[1]});if(x)return fail2(x)}if(m.untilEnd){if(m.drag)return fail2("untilEnd scrolls with flicks. Leave out drag.");if(!f)return fail2("untilEnd needs the app connected, to see when the list stops moving.");const x=await d(y,g,_,$,m.durationMs??250);const{swipes:T,atEnd:I}=x;const O=await observeAfterAction(n,y,void 0,0);const{lastRow:N}=x;return text(`${I?`Reached the end of the list after ${T} swipe(s)${N?`. The last row is "${N}"`:""}`:`Swiped ${T} times and the screen was still moving`}.${O}`)}const E=g.platform==="android"?null:await simHidTool();const A=await u(g,_,$,S,m.drag);await a(y,g,m.drag?"drag":"swipe",[_],[$],S+(m.drag&&E?700:0),A);await delay5(400);const P=f?await observeAfterAction(n,y):"";return text(`${m.drag?"Dragged":"Swiped"} from (${_.map(Math.round).join(", ")}) to (${$.map(Math.round).join(", ")}) ${p(g)}.${P}`)}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_inspect",{description:"Read native controls on the screen. Pass pointX and pointY for points on iOS Sims, pixels elsewhere. Use x and y for screen fractions (0 to 1). Leave all points out to list controls, with types, labels, IDs, and frames. This includes tabs, alerts, and sheets. A point read also checks the app view and Buoy's cover. iOS Sims use idb; Android uses uiautomator. Real iPhones show just the app view.",inputSchema:{pointX:import_zod15.z.number().finite().min(0).optional().describe("From the left. iOS Sims use points. Other hosts use pixels."),pointY:import_zod15.z.number().finite().min(0).optional().describe("From the top. iOS Sims use points. Other hosts use pixels."),x:import_zod15.z.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:import_zod15.z.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),serial:h,deviceId:deviceIdInput}},async m=>{try{const y=m.pointX!=null||m.pointY!=null;if(y&&(m.pointX==null||m.pointY==null))return fail2("Use pointX and pointY together.");if(y&&(m.x!=null||m.y!=null))return fail2("Use pointX/pointY or x/y, not both.");if(!y&&m.x==null!==(m.y==null))return fail2("Use x and y together.");const{id:g,host:f,online:w}=await hostTarget(n,m.deviceId,{serial:m.serial},{allowOffline:true});freshScreen(f);await t(g,f,w);const v=p(f);if(!y&&(m.x==null||m.y==null)){if(f.platform==="ios-device")return fail2("A physical iPhone doesn't share its native element tree with this Mac. Use describe_screen for the app's elements, or native_inspect with x and y to see what is at a point.");let k;let S=0;if(f.platform==="android"){const A=await adbUiNodes(f.serial);k=A.filter(P=>P.text||P.desc||P.id||P.clickable).map(P=>{const x=P.bounds;const T=[P.cls.split(".").pop(),P.text?JSON.stringify(P.text.slice(0,80)):"",P.desc?`desc=${JSON.stringify(P.desc.slice(0,60))}`:"",P.id?`id=${P.id.split("/").pop()}`:"",P.clickable?"(clickable)":""];return`${T.filter(Boolean).join(" ")} at (${x.x}, ${x.y}) ${x.width}x${x.height}`})}else{const{elements:A,screen:P}=await iosNativeElements(f.udid);const x=A.filter(T=>!P.width||T.frame.y<P.height&&T.frame.y+T.frame.height>0&&T.frame.x<P.width&&T.frame.x+T.frame.width>0);S=A.length-x.length;k=x.map(axLine)}const E=k.slice(0,150);return text([`${k.length} native element(s) on screen, top to bottom (${v}; frames are x, y, width x height)${S?`. ${S} more are scrolled out of view`:""}:`,...E.map(A=>`- ${A}`),...k.length>E.length?[`(${k.length-E.length} more not shown.)`]:[],"Tap one with native_tap label or x/y. For the app's React elements (testID, handlers), use describe_screen."].join("\n"))}let b;if(y){const k=await s(f);b={x:m.pointX,y:m.pointY};if(b.x>=k.width||b.y>=k.height){return fail2(`That point is outside the screen (${k.width} x ${k.height} ${v}).`)}}else{const k=await s(f);b={x:m.x*k.width,y:m.y*k.height}}const _=[y?`At (${Math.round(b.x)}, ${Math.round(b.y)}) ${v}:`:`At (${m.x.toFixed(2)}, ${m.y.toFixed(2)}) = (${Math.round(b.x)}, ${Math.round(b.y)}) ${v}:`];const $=w?r(g,f,b):Promise.resolve(null);if(f.platform==="android"){const k=(await adbUiNodes(f.serial)).filter(E=>{const A=E.bounds;return b.x>=A.x&&b.x<=A.x+A.width&&b.y>=A.y&&b.y<=A.y+A.height});const S=k.sort((E,A)=>E.bounds.width*E.bounds.height-A.bounds.width*A.bounds.height)[0];_.push(S?`- Native: ${S.cls.split(".").pop()}${S.text?` ${JSON.stringify(S.text.slice(0,80))}`:""}${S.desc?` desc=${JSON.stringify(S.desc.slice(0,60))}`:""}${S.id?` id=${S.id.split("/").pop()}`:""} at (${S.bounds.x}, ${S.bounds.y}) ${S.bounds.width}x${S.bounds.height}`:"- Native: nothing with a label or id here.")}else if(f.platform==="ios"){const k=JSON.parse(await idb(["ui","describe-point","--udid",f.udid,String(Math.round(b.x)),String(Math.round(b.y)),"--json"],5e3));_.push(k?.type&&k.frame?.width?`- Native: ${axLine(k)}`:"- Native: no accessible element here (plain views and canvases often have none).")}else{_.push("- Native: not available on a physical iPhone.")}if(!w)_.push("- App: not connected, so its views can't be read.");else{const k=await $;if(!k)_.push("- App: no answer (an older Buoy without hitTest, or the JS thread is busy).");else if(!k.found)_.push(`- App: ${k.reason??"no React view here (native UI, or outside the app's views)."}`);else{const S=k.view;const E=[S.name,S.testID?`testID=${S.testID}`:"",S.label?`label=${JSON.stringify(S.label)}`:"",S.text?JSON.stringify(S.text.slice(0,60)):""].filter(Boolean).join(" ");_.push(`- App: ${E}${k.path?.length?` in ${k.path.join(" < ")}`:""}.`);if(k.buoy)_.push("- This spot is Buoy's own overlay. A real touch here hits Buoy, not the app; close or minimize the panel first.");else if(k.target){const A=k.target;_.push(`- A tap here presses ${A.testID?`testID ${A.testID}`:JSON.stringify(A.text??A.label??A.name)} (${A.name}${A.nativeTag!=null?`, nativeTag ${A.nativeTag}`:""}).`)}else _.push("- Nothing tappable in the app here.")}}return text(_.join("\n"))}catch(y){return fail2(errMsg2(y))}});e.registerTool("native_pinch",{description:"Pinch, spread or turn with two real fingers (CoreDevice on the iOS Simulator and a physical iPhone): zoom a map or photo, or rotate, in one call. scale above 1 zooms in (fingers spread), below 1 zooms out. The reply says how far a map turned and zoomed, read from the map itself. For other views it says how much of the area changed and shows it. No screenshot is needed. Not on Android or older Xcode, which can't send two fingers.",_meta:coreTool(),inputSchema:{x:import_zod15.z.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),y:import_zod15.z.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),scale:import_zod15.z.number().min(.1).max(10).optional().describe("How far the fingers spread: 2 doubles the gap (zoom in), 0.5 halves it (zoom out). Default 2."),rotate:import_zod15.z.number().min(-180).max(180).optional().describe("Turn the fingers this many degrees while pinching, clockwise. Default 0."),durationMs:import_zod15.z.number().min(100).max(5e3).optional().describe("Default 500."),allowBuoy:import_zod15.z.boolean().optional().describe("Pinch even if a finger starts on Buoy."),deviceId:deviceIdInput}},async m=>{try{const{id:y,host:g,online:f}=await hostTarget(n,m.deviceId,{},{allowOffline:true});freshScreen(g);await t(y,g,f);if(g.platform==="android")return fail2("Android's input command sends one finger only, so Buoy can't pinch there yet. Use the app's zoom buttons, or native_swipe for one-finger moves.");const w=await simHidTool();if(!w)return fail2(g.platform==="ios-device"?PHONE_TOUCH:"Pinching needs Xcode's CoreDevice touch support (Xcode 26 or later); idb sends one finger only.");const v=await s(g);const b=[(m.x??.5)*v.width,(m.y??.5)*v.height];const _=m.scale??2;const $=Math.min(v.width,v.height);const k=$*.4;const S=_>=1?Math.max($*.04,k/_):k;const E=_>=1?k:Math.max($*.04,k*_);const A=(m.rotate??0)*Math.PI/180;const P=ie=>[Math.min(v.width-1,Math.max(1,ie[0])),Math.min(v.height-1,Math.max(1,ie[1]))];const x=(ie,se)=>[P([b[0]-ie*Math.cos(se),b[1]-ie*Math.sin(se)]),P([b[0]+ie*Math.cos(se),b[1]+ie*Math.sin(se)])];const[T,I]=x(S,0);const[O,N]=x(E,A);if(f&&!m.allowBuoy){for(const[ie,se]of[T,I]){const re=await o(y,g,{x:ie,y:se});if(re)return fail2(re)}}const F=m.durationMs??500;const X={x:Math.max(0,(b[0]-k)/v.width),y:Math.max(0,(b[1]-k)/v.height),width:Math.min(1,2*k/v.width),height:Math.min(1,2*k/v.height)};const C=f?await readMaps(n,y):[];const U=ie=>ie.find(se=>!se.frame||(m.x??.5)>=se.frame.x&&(m.x??.5)<=se.frame.x+se.frame.width&&(m.y??.5)>=se.frame.y&&(m.y??.5)<=se.frame.y+se.frame.height);const j=U(C);const H=j?null:await hostPng(g).catch(()=>null);const V=g.platform==="ios-device"?await coreDeviceId(g.udid):g.udid;const L=await simHidTwo(w,V,{...v,bundleId:g.platform==="ios"?g.bundleId:null},T,I,O,N,F);await a(y,g,"pinch",[T,I],[O,N],F,L);await delay5(400);const[W,Q,J]=await Promise.all([f?observeAfterAction(n,y,void 0,0):Promise.resolve(""),H?hostPng(g).catch(()=>null):Promise.resolve(null),j?readMaps(n,y):Promise.resolve([])]);const te=E/S;const ae=te>1.01?`Spread (zoom in, x${te.toFixed(2)})`:te<.99?`Pinched (zoom out, x${te.toFixed(2)})`:"Turned";const ne=`${ae}${m.rotate?`, turning ${m.rotate} degrees,`:""} around (${Math.round(b[0])}, ${Math.round(b[1])}) ${p(g)} with two real fingers.`;const fe=j?J.find(ie=>ie.name===j.name&&ie.testID===j.testID):void 0;const ge=j&&fe?mapChange(j,fe):null;if(ge)return text(`${ne}
|
|
1077
1077
|
${ge}${W}`);const de=H&&Q?await pinchProof(H,Q,X):null;if(!de)return text(`${ne}${W}`);return{content:[{type:"text",text:`${ne}
|
|
1078
1078
|
${de.line}${W}`},{type:"image",data:de.image.data,mimeType:de.image.mimeType}]}}catch(y){return fail2(errMsg2(y))}})}async function pinchProof(e,n,t){try{const s=cropPng(e,t);const i=cropPng(n,t);const{percent:r}=diffScreens(s,i);const o=r<2?`Checked: the area under the fingers looks the same as before (${r.toFixed(1)}% changed). The pinch did not move anything there.`:`Checked: ${r.toFixed(0)}% of the area under the fingers changed, so it moved. It looks like this now:`;return{line:o,image:await toSmallImage(i,480)}}catch{return null}}async function readMaps(e,n){try{const t=await e.callAction(UI_TOOL_ID2,"readMaps",{},n,{timeoutMs:2e3,silent:true});return t?.maps??[]}catch{return[]}}function mapChange(e,n){const t=`The map${e.testID?` [${e.testID}]`:""}`;const s=[];const i=e.camera.heading,r=n.camera.heading;if(typeof i==="number"&&typeof r==="number"){const d=m=>(m%360+360)%360;const h=m=>Math.round(Math.min(d(m),360-d(m)));const p=Math.round(Math.abs((d(r)-d(i)+540)%360-180));s.push(`heading ${Math.round(d(i))}\xB0 before, ${Math.round(d(r))}\xB0 now (turned ${p}\xB0, now ${h(r)}\xB0 from north)`)}const o=e.camera.altitude,a=n.camera.altitude;const l=e.camera.zoom,c=n.camera.zoom;const u=typeof o==="number"&&typeof a==="number"&&o>0&&a>0?o/a:typeof l==="number"&&typeof c==="number"?Math.pow(2,c-l):null;if(u!=null&&Number.isFinite(u)){const d=u*u;const h=u>1.01?`zoomed in x${u.toFixed(2)}, so it shows about 1/${d.toFixed(1)} of the area it did`:u<.99?`zoomed out x${(1/u).toFixed(2)}, so it shows about ${d>0?(1/d).toFixed(1):"?"} times the area`:"same zoom";const p=typeof o==="number"&&typeof a==="number"?` (altitude ${Math.round(o).toLocaleString("en-US")} m to ${Math.round(a).toLocaleString("en-US")} m)`:typeof l==="number"&&typeof c==="number"?` (zoom ${l.toFixed(1)} to ${c.toFixed(1)})`:"";s.push(`${h}${p}`)}return s.length?`Checked in the app: ${t} ${s.join("; ")}.`:null}var import_zod15,import_node_child_process17,import_node_util14,import_node_fs24,import_node_os17,import_node_path25,execFileAsync14,UI_TOOL_ID2,delay5,IDB_MISSING,PHONE_TOUCH,phoneScreens,simScreens,SCROLL_REST_MS,SCREEN_TTL_MS,IDB_SETUP,AxShellOnlyError,barCache,BAR_FILE,barFileRead;var init_native=__esm({"src/tools/packs/native.ts"(){"use strict";import_zod15=require("zod");import_node_child_process17=require("child_process");import_node_util14=require("util");import_node_fs24=require("fs");import_node_os17=__toESM(require("os"));import_node_path25=__toESM(require("path"));init_packKit();init_interact();init_adb();init_hostDevice();init_imageDiff();init_hostKit();init_secrets();init_simTouch();init_simHid();init_recording();init_iosDevice();init_hidStream();execFileAsync14=(0,import_node_util14.promisify)(import_node_child_process17.execFile);UI_TOOL_ID2="highlight-updates";delay5=e=>new Promise(n=>setTimeout(n,e));IDB_MISSING="Native touch on the iOS Simulator needs idb on this Mac (brew install facebook/fb/idb-companion && pipx install fb-idb).";PHONE_TOUCH="Real touches on a physical iPhone need Xcode's CoreDevice touch support, and Buoy couldn't set it up on this Mac. Use tap_element (it calls the element's handler) or run_flow; screenshots, recording and app control work on the iPhone.";phoneScreens=new Map;simScreens=new Map;SCROLL_REST_MS=60;SCREEN_TTL_MS=3e3;IDB_SETUP=/idb on this Mac|SimulatorKit|not booted|connection refused|Failed to connect to companion/i;AxShellOnlyError=class extends Error{constructor(e){super(`idb can only see an empty app shell on this simulator, so native labels and screen size can't be read. This happens after Maestro or another XCTest tool ran on it, and lasts until the simulator reboots. To fix it, reboot just this simulator: xcrun simctl shutdown ${e} && xcrun simctl boot ${e}. Until then, tap_element and taps by testID or query still work.`);this.name="AxShellOnlyError"}};barCache=new Map;BAR_FILE=import_node_path25.default.join(import_node_os17.default.homedir(),".buoy","cache","native-bars.json");barFileRead=false}});async function observeAfterAction(e,n,t,s=300){let i="";for(let o=0;o<(t?6:1);o++){await delay6(o===0?s:250);try{const a=await e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:3e3,silent:true});i=String(a?.pathname??a?.path??"")}catch{break}if(!t||i.replace(/\(.*?\)\//g,"").replace(/\/$/,"")===t.replace(/\/$/,"")||i.endsWith(t))break}let r="";try{const o=await e.callAction(TOOL_ID2,"describeScreen",{},n,{timeoutMs:5e3,silent:true});const a=o?.elements??[];const l=new Set(a.filter(y=>y.name!=="Text").map(y=>y.text??y.label).filter(Boolean));const c=a.filter(y=>y.name!=="Text"&&y.tap&&(y.text??y.label));const u=(y,g)=>!!y.tap&&c.some(f=>Math.abs(f.tap.y-y.tap.y)<.06&&(f.text??f.label).includes(g));const d=a.filter(y=>{const g=y.text??y.label;if(y.name==="Text"&&(!g||/^[\s\uE000-\uF8FF]*$/.test(g)))return false;if(y.name==="Text"&&l.has(g))return false;if(y.name==="Text"&&g&&u(y,g))return false;return true});const h=y=>!y.tap||y.tap.x>=0&&y.tap.x<=1&&y.tap.y>=0&&y.tap.y<=1;const p=[...d.filter(h),...d.filter(y=>!h(y))];const m=p.slice(0,OBSERVE_MAX);r=m.map(formatElement).join("\n");if(p.length>m.length)r+=`
|
|
1079
1079
|
\u2026 ${p.length-m.length} more (describe_screen lists everything)`}catch{r="(could not read the screen)"}return`
|
|
@@ -1105,7 +1105,7 @@ ${h.join("\n")}
|
|
|
1105
1105
|
|
|
1106
1106
|
_Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`)}catch(s){return fail7(errMsg6(s))}});e.registerTool("get_sentry_cost",{description:"Inspect captured session usage and the tool's configured monthly extrapolation and quota assumptions. These estimates do not establish Sentry ingestion, current plan limits or an actual bill. Check the account's current terms and usage before making cost decisions.",inputSchema:{deviceId:import_zod19.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.")}},async t=>{try{const s=n.requireProDevice(t.deviceId);const{payload:i,stale:r}=await n.getSnapshot(SENTRY_TOOL_ID,s);const o=r?`${r}
|
|
1107
1107
|
|
|
1108
|
-
`:"";const a=i??{};const l=a.usage;if(!l?.session){return text5(`${o}No usage is available in this capture.${statusNote(a.status)}`)}const c=l.session;const u=l.projectedMonthly;const d=Object.keys(c);const h=d.map(g=>{const f=CATEGORY_FORMAT[g];const w=units(c[g],f);if(!u)return`- ${g}: ${w} this session`;const v=u[g];const b=FREE_QUOTA[g];const
|
|
1108
|
+
`:"";const a=i??{};const l=a.usage;if(!l?.session){return text5(`${o}No usage is available in this capture.${statusNote(a.status)}`)}const c=l.session;const u=l.projectedMonthly;const d=Object.keys(c);const h=d.map(g=>{const f=CATEGORY_FORMAT[g];const w=units(c[g],f);if(!u)return`- ${g}: ${w} this session`;const v=u[g];const b=FREE_QUOTA[g];const _=b>0?Math.round(v/b*100):null;const $=_===null?" (no configured reference quota)":` \u2014 ${_}% of the configured reference quota of ${units(b,f)}`;return`- ${g}: ${w} this session \u2192 ~${units(v,f)}/mo${$}`});const p=l.windowMs?` over ${Math.round(l.windowMs/1e3)}s`:"";const m=l.topContributors?.length?`
|
|
1109
1109
|
|
|
1110
1110
|
**What produced it:**
|
|
1111
1111
|
${l.topContributors.map(g=>`- ${g.label} \u2014 ${g.units} ${g.category}`).join("\n")}`:"";const y=u?"\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._":"\n\n_Session too short to project a month from._";return text5(`${o}**Sentry usage estimate \u2014 ${a.envelopes?.length??0} envelopes${p}:**
|
|
@@ -1307,15 +1307,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
1307
1307
|
else requestAnimationFrame(tick);
|
|
1308
1308
|
};
|
|
1309
1309
|
requestAnimationFrame(tick);
|
|
1310
|
-
})`}});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 s=e.median;const i=[];const r=[];const o=(d,h,p)=>{if(h==null)return;if(p==null)r.push(d);else if(p>h)i.push(`${d} ${p} > ${h}`)};o("droppedPct",n.droppedPct,e.droppedMin!=null?s?.droppedPct:null);const a=e.runs.filter(d=>!d.failed&&d.fps!=null);const l=t.some(d=>"click"in d||"press"in d||"type"in d);const c=a.map(d=>d.interactions==null||!d.inputCaptureComplete?null:d.worstInteractionMs??16);o("inputMs",n.inputMs,l&&c.length&&c.every(d=>d!=null)?Math.max(...c):null);const u=a.map(d=>Math.max(d.frameGapMaxMs??0,d.edgeSilenceMs??0));o("freezeMs",n.freezeMs,u.length?Math.max(...u):null);o("mainThreadPct",n.mainThreadPct,s?.mainThreadPct);o("jsHeapMb",n.jsHeapMb,s?.heapMb);return{meets:Boolean(s)&&i.length===0&&r.length===0,over:i,unknown:r}}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,r)=>i-r);const s=Math.ceil(n/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,s))]}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 s=()=>{t=t+1831565813>>>0;let r=t;r=Math.imul(r^r>>>15,r|1);r^=r+Math.imul(r^r>>>7,r|61);return((r^r>>>14)>>>0)/4294967296};const i=[...e];for(let r=i.length-1;r>0;r--){const o=Math.floor(s()*(r+1));[i[r],i[o]]=[i[o],i[r]]}return i}function caseUrl(e,n,t){const s=new URL(e);const i=n.path??t;if(i)s.pathname=i.startsWith("/")?i:`${s.pathname.replace(/\/$/,"")}/${i}`;for(const[r,o]of Object.entries(n.params??{}))s.searchParams.set(r,o);return s.toString()}function summarizeFrameTrace(e){const n=p=>e.find(m=>m.name===p&&m.ts!=null);const t=n("__buoy_bench_start");const s=n("__buoy_bench_end");if(!t||!s||s.ts<=t.ts)return null;if(t.pid!==s.pid)return null;const i=t.pid;const r={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>s.ts)continue;const m=p.args?.frame_reporter;if(!m||m.frame_sequence==null)continue;const y=m.state;if(!FRAME_STATES.includes(y))continue;const g=`${m.layer_tree_host_id??""}:${m.frame_source??""}:${m.frame_sequence}`;const f=Boolean(m.affects_smoothness)&&y!=="STATE_PRESENTED_ALL";const w=o.get(g);if(!w)o.set(g,{state:y,fault:f});else{if(r[y]>r[w.state])w.state=y;w.fault=w.fault||f}}let a=0;let l=0;let c=0;let u=0;for(const p of o.values()){if(p.state==="STATE_PRESENTED_ALL")a++;else if(p.state==="STATE_PRESENTED_PARTIAL")l++;else c++;if(p.fault)u++}const d=a+l+c;if(d===0)return null;const h=(s.ts-t.ts)/1e6;return{full:a,partial:l,dropped:c,faults:u,shownFps:r1((a+l)/h),droppedPct:r1(u/d*100)}}function keyMatches(e,n){const t=e.split("+");const s=t[t.length-1];const i=new Set(t.slice(0,-1).map(a=>a.toLowerCase()));if(n.key!==s)return false;const r=s.toLowerCase();const o={ctrl:i.has("control")||r==="control",alt:i.has("alt")||r==="alt",meta:i.has("meta")||i.has("command")||r==="meta",shift:i.has("shift")||r==="shift"};if(Boolean(n.ctrl)!==o.ctrl||Boolean(n.alt)!==o.alt||Boolean(n.meta)!==o.meta)return false;if(!o.shift&&n.shift&&s.length!==1)return false;if(o.shift&&!n.shift)return false;return true}function mergeInteractions(e){const n=[];for(const t of e){const s=t.startTime+t.duration;const i=n.find(o=>o.interactionId===t.interactionId&&Math.abs(o.startTime+o.duration-s)<=8);if(!i){n.push({...t});continue}const r=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(r,s)-i.startTime;if(t.processingEnd-t.processingStart>0)i.name=t.name}return n}function summarizeProbe(e,n){const{start:t,end:s}=e;const i=Math.max(1,s-t);const r=[];for(let
|
|
1311
|
-
(Searched from ${e}. Pass projectRoot to look elsewhere.)`)}async function withDeadline(e,n,t){let s;const i=new Promise(r=>{s=setTimeout(()=>r("timeout"),n)});try{const r=await Promise.race([e,i]);if(r==="timeout"){e.catch(()=>{});return await t()}return r}finally{clearTimeout(s)}}function gitCommit(e){try{return(0,import_node_child_process21.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_process21.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,l,c)=>{if(o==null)return;if(!Number.isInteger(o)||o<l||o>c)throw new Error(`${a} must be a whole number from ${l} to ${c}.`)};const s=(o,a,l)=>{if(o==null)return;if(!Number.isFinite(o)||o<l)throw new Error(`${a} must be a number of at least ${l}.`)};const i=(o,a,l,c)=>{if(o==null)return;if(!Number.isFinite(o)||o<l||o>c)throw new Error(`${a} must be from ${l} to ${c}.`)};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 r=e.cpuSlowdown??[];for(const o of r)i(o,"each cpuSlowdown rate",1,20);if(new Set(r).size!==r.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 s of n){if("expect"in s)continue;const i="click"in s?s.click:"type"in s?s.type.selector:null;if(!i||t.has(i))continue;const r=await e.$(i);const o=r?await r.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 s of n){if("click"in s){const i=t.get(s.click);await e.mouse.click(i.x,i.y)}else if("press"in s)await e.keyboard.press(s.press);else if("type"in s){const i=t.get(s.type.selector);await e.mouse.click(i.x,i.y);await e.keyboard.type(s.type.text)}else if("scroll"in s)await e.mouse.wheel(0,s.scroll);else if("wait"in s)await sleep3(s.wait)}}function missedInput(e,n,t=[]){const s=[...n].sort((u,d)=>u.t-d.t);const i=[...t].sort((u,d)=>u.t-d.t);let r=0;let o=0;let a=-Infinity;const l=u=>{while(r<s.length){const d=s[r++];if(d.t>=a&&d.hit.includes(u)){a=d.t;return true}}return false};const c=u=>{while(o<i.length){const d=i[o++];if(d.t>=a&&keyMatches(u,d)){a=d.t;return true}}return false};for(const[u,d]of e.entries()){if("click"in d&&!l(d.click)){return`step ${u+1} (click ${d.click}) has no real click on its target, in order (moved, covered, or ignored)`}if("type"in d){if(!l(d.type.selector))return`step ${u+1} (type into ${d.type.selector}) has no real click on its target`;for(const h of d.type.text){if(!c(h))return`step ${u+1} (type "${d.type.text}") didn't deliver the key "${h}"`}}if("press"in d&&!c(d.press))return`step ${u+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:s,text:i}=t.expect;const r=await e.evaluate(`(() => { const n = document.querySelector(${JSON.stringify(s)}); if (!n) return false; ${i==null?"return true;":`return (n.textContent || "").includes(${JSON.stringify(i)});`} })()`);if(r)return i==null?s:`${s} contains "${i}"`}return null}async function checkTargetsStill(e,n){for(const[t,s]of n){const i=await e.$(t);const r=i?await i.boundingBox():null;if(!r)return`step target ${t} was gone after the run`;const o=r.x+r.width/2;const a=r.y+r.height/2;if(Math.abs(o-s.x)>4||Math.abs(a-s.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 s={browserLeftRunning:false};try{return await runWebBenchLocked(e,n,s)}finally{t(s.browserLeftRunning)}}async function waitForHostLock(e,n=()=>{},t=2e3){const s=Date.now()+Math.max(0,e);let i=false;for(;;){try{return takeHostLock()}catch(r){const o=r?.message??String(r);const a=/Another web benchmark|already running/.test(o);if(!a||Date.now()+t>s)throw r;if(!i){n("another web benchmark is running; waiting for it to finish");i=true}await new Promise(l=>setTimeout(l,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 r=`${LOCK_FILE}.${e.replace(/[^\w]/g,"")}.tmp`;try{(0,import_node_fs25.writeFileSync)(r,n);(0,import_node_fs25.linkSync)(r,LOCK_FILE);return true}catch{return false}finally{try{(0,import_node_fs25.unlinkSync)(r)}catch{}}};const s=()=>{try{return JSON.parse((0,import_node_fs25.readFileSync)(LOCK_FILE,"utf8"))}catch{return null}};const i=r=>{if(!r)return false;try{process.kill(r,0);return true}catch{return false}};if(!t()){const r=s();if(!r)throw new Error("Another web benchmark is starting. Wait for it to finish.");if(i(r.pid)){throw new Error(`Another web benchmark is running (pid ${r.pid}, since ${r.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_fs25.statSync)(o).mtimeMs>3e4){const l=`${o}.${e.replace(/[^\w]/g,"")}.old`;(0,import_node_fs25.renameSync)(o,l);if(Date.now()-(0,import_node_fs25.statSync)(l).mtimeMs>3e4)(0,import_node_fs25.unlinkSync)(l);else{try{(0,import_node_fs25.linkSync)(l,o)}catch{}(0,import_node_fs25.unlinkSync)(l)}}}catch{}try{a=(0,import_node_fs25.openSync)(o,"wx")}catch{throw new Error("Another web benchmark is clearing an old lock. Try again in a moment.")}try{const l=s();if(l?.token===r.token)(0,import_node_fs25.unlinkSync)(LOCK_FILE);if(!t())throw new Error("Another web benchmark just started. Wait for it to finish.")}finally{(0,import_node_fs25.closeSync)(a);try{(0,import_node_fs25.unlinkSync)(o)}catch{}}}lockedHere=true;return(r=false)=>{if(r)return;lockedHere=false;if(s()?.token===e){try{(0,import_node_fs25.unlinkSync)(LOCK_FILE)}catch{}}}}async function runWebBenchLocked(e,n,t){const s=e.runsPerCase??3;const i=Math.max(500,e.durationMs??5e3);const r=Math.max(0,e.settleMs??1e3);const o=e.frameTrace!==false;const a=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(I=>Math.max(1,I));let l=e.refreshHz??60;const c={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const u=[];const d=[];let h=null;if(s<2)u.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 y=[];const g=[];const f=Date.now();const w=new Date(f).toISOString();const v=e.maxBatchMs??30*6e4;let b=false;let k=false;const $={commit:gitCommit(e.projectRoot??process.cwd()),dirty:gitDirty(e.projectRoot??process.cwd())};try{const I={viewport:{width:c.width,height:c.height},deviceScaleFactor:c.deviceScaleFactor};const O=await withDeadline(m.newContext(I),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 N=3e4+15e3+r+i+6e4;const F=async(j,H,V,L)=>{const W={};return withDeadline(C(W,j,H,V,L),N,async()=>{W.cancelled=true;if(!W.ctx)b=true;else if(!await closeWithin(W.ctx,1e4))b=true;return{...X(j.name,H,V),failed:`run took over ${Math.round(N/1e3)} s (the page may be stuck in a loop)`}})};const X=(j,H,V)=>({caseName:j,cpu:H,round:V,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 C=async(j,H,V,L,W)=>{const Q=await m.newContext(I);j.ctx=Q;if(j.cancelled){await closeWithin(Q,1e4);throw new Error("run was cancelled during setup")}const J=await Q.newPage();const te=[];J.on("pageerror",ne=>te.push(String(ne?.message??ne)));const ae={...X(H.name,V,L),pageErrors:te};try{const ne=await Q.newCDPSession(J);await ne.send("Performance.enable",{timeDomain:"timeTicks"});if(V>1)await ne.send("Emulation.setCPUThrottlingRate",{rate:V});const fe=await J.goto(caseUrl(e.url,H,e.path),{waitUntil:"load",timeout:3e4});if(fe&&fe.status()>=400)return{...ae,failed:`HTTP ${fe.status()}`};if(e.readySelector)await J.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep3(r);const ge=W||o;if(ge){await ne.send("Tracing.start",{transferMode:"ReturnAsStream",categories:(W?FULL_TRACE_CATEGORIES:FRAME_TRACE_CATEGORIES).join(",")})}const de=e.steps?.length?await locateStepTargets(J,e.steps):new Map;if(e.steps?.length){const R=await expectsAlreadyTrue(J,e.steps);if(R)return{...ae,failed:`expect ${R} already held before the steps, so it can't prove they worked`};const B=[...de.keys()];const z=e.steps.flatMap(G=>"expect"in G?[G.expect]:[]);await J.evaluate(`window.__buoyBenchClickTargets = ${JSON.stringify(B)}; window.__buoyBenchExpects = ${JSON.stringify(z)}; true`)}const ie=await readMetrics(ne);await J.evaluate(START_PROBE_JS);const se=Date.now();if(e.steps?.length)await runSteps(J,e.steps,de);const re=i-(Date.now()-se);if(re>0)await sleep3(re);await J.evaluate(END_PROBE_JS);const ce=await readMetrics(ne);const ye=Date.now()-se;const he=await J.evaluate(COLLECT_PROBE_JS);let M=null;if(ge){const R=new Promise(ee=>ne.on("Tracing.tracingComplete",K=>ee(K.stream)));await ne.send("Tracing.end");const B=await R;const z=W?(0,import_node_fs25.openSync)(W,"w"):null;const G=[];let Y=0;try{for(;;){const ee=await ne.send("IO.read",{handle:B});const K=ee.base64Encoded?Buffer.from(ee.data,"base64").toString("utf8"):ee.data;Y+=K.length;if(z!=null)(0,import_node_fs25.writeSync)(z,K);if(Y<=MAX_TRACE_CHARS)G.push(K);else G.length=0;if(ee.eof)break}}finally{await ne.send("IO.close",{handle:B}).catch(()=>{});if(z!=null)(0,import_node_fs25.closeSync)(z)}if(W)d.push(W);if(Y>MAX_TRACE_CHARS){u.push(`Trace for ${H.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 ee=JSON.parse(G.join(""));M=summarizeFrameTrace(Array.isArray(ee)?ee:ee.traceEvents??[])}catch{M=null}}}if(!he)return{...ae,failed:"probe returned nothing (page navigated away?)"};if(he.hidden)return{...ae,failed:"page was hidden during the run"};if(he.resized)return{...ae,failed:"the window size or zoom changed during the run"};if(te.length&&!e.allowPageErrors){return{...ae,failed:`page error: ${oneLine(te[0])}`}}if(e.steps?.length){const R=missedInput(e.steps,he.clicks??[],he.keys??[]);if(R)return{...ae,failed:R};const B=expectFailureFrom(he.expectResults??[]);if(B)return{...ae,failed:B};const z=await checkTargetsStill(J,de);if(z)return{...ae,failed:z}}const D="Timestamp"in ie&&"Timestamp"in ce?r1((ce.Timestamp-ie.Timestamp)*1e3):null;return{...ae,...summarizeProbe(he,l),...cdpDeltas(ie,ce,ye),probeSpanMs:r1(he.end-he.start),cdpSpanMs:D,shownFps:M?.shownFps??null,droppedFrames:M?.faults??null,frameCounts:M?{full:M.full,partial:M.partial,dropped:M.dropped,faults:M.faults}:null,droppedPct:M?.droppedPct??null}}catch(ne){return{...ae,failed:oneLine(ne?.message??String(ne))}}finally{await closeWithin(J,5e3);if(!await closeWithin(Q,1e4))b=true}};{const j=await withDeadline(O.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the GPU check page in 15 s.")});try{h=await withDeadline(j.evaluate(GPU_RENDERER_JS),5e3,async()=>null)??null}catch{h=null}finally{await closeWithin(j,5e3)}if(h)n(`GPU ${h}`);if(h&&isSoftwareGpu(h)){u.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 j=await withDeadline(O.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the calibration page in 15 s.")});try{await withDeadline(j.goto("about:blank"),15e3,async()=>null);const H=await withDeadline(j.evaluate(MEASURE_REFRESH_JS),5e3,async()=>{throw new Error("idle rate check timed out")});l=snapRefreshRate(H);n(`idle frame rate ${H.toFixed(1)} \u2192 ${l} Hz`)}catch{u.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await closeWithin(j,5e3)}}n("warmup load");await F(e.cases[0],1,-1);const U=async(j,H,V,L)=>{if(b){y.push({...X(j.name,H,V),failed:"an earlier run left the browser stuck; batch stopped"});return}if(Date.now()-f>v){y.push({...X(j.name,H,V),failed:"batch time limit reached; run skipped"});return}n(L);g.push({cpu:H,round:V,caseName:j.name});y.push(await F(j,H,V))};for(const j of a){for(let H=0;H<s;H++){const V=shuffled(e.cases,2654435769^H*7919+j);for(const L of V)await U(L,j,H,`cpu ${j}x \xB7 round ${H+1}/${s} \xB7 ${L.name}`)}for(let H=0;H<(e.extraRoundsWhenClose??0);H++){const V=e.cases.map(J=>aggregateCase(y.filter(te=>te.cpu===j&&te.caseName===J.name),l));for(const J of V)J.control=e.cases.find(te=>te.name===J.caseName)?.control===true;const L=new Set([...tiedPairs(V).flat(),...V.filter(J=>!J.control&&J.validRuns<2&&J.plannedRuns>0).map(J=>J.caseName)]);if(L.size===0)break;const W=s+H;const Q=shuffled(e.cases.filter(J=>L.has(J.name)),20973^W*7919+j);for(const J of Q)await U(J,j,W,`cpu ${j}x \xB7 extra round ${H+1} (close call) \xB7 ${J.name}`)}}if(e.traceDir){(0,import_node_fs25.mkdirSync)(e.traceDir,{recursive:true});for(const[j,H]of e.cases.entries()){const V=(0,import_node_path26.join)(e.traceDir,`${String(j+1).padStart(2,"0")}-${H.name.replace(/[^\w.-]+/g,"_")}.json`);if(b||Date.now()-f>v){u.push(`Trace run for ${H.name} skipped (batch time limit or a stuck browser).`);continue}n(`trace \xB7 ${H.name}`);const L=await F(H,a[0],-1,V);if(L.failed)u.push(`Trace run for ${H.name} failed: ${L.failed}`)}}await closeWithin(O,1e4)}finally{const I=await closeWithin(m,15e3);t.browserLeftRunning=!I;if(!I){k=true;u.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 _=[];for(const I of a){for(const O of e.cases){_.push(aggregateCase(y.filter(N=>N.cpu===I&&N.caseName===O.name),l))}}for(const I of _)I.control=e.cases.find(O=>O.name===I.caseName)?.control===true;for(const I of a){const O=_.filter(C=>C.cpu===I);const N=O.find(C=>!C.control);if(!N)continue;N.isBaseline=true;const F=C=>C.droppedMin!=null?C.median?.droppedPct??null:null;const X=F(N);for(const C of O.filter(U=>U!==N&&!U.control)){const U=F(C);C.deltaDroppedPct=X!=null&&U!=null?r1(U-X):null}}if(e.allowPageErrors&&y.some(I=>I.pageErrors.length)){u.push("Some runs threw page errors (kept because allowPageErrors is on).")}if(b)u.push("A browser context wouldn't close, so the batch stopped early. Restart and run again.");if(e.budgets)for(const I of _)I.budget=checkBudgets(I,e.budgets,e.steps??[]);const S=_.filter(I=>I.median).length;const E=_.every(I=>I.validRuns===I.plannedRuns);const A=S===0?"failed":S<_.length||!E?"partial":"ok";const P=_.filter(I=>I.median&&I.validRuns>0&&I.validRuns<I.plannedRuns);for(const I of P){const O=Array.from(new Set(I.runs.filter(N=>N.failed).map(N=>N.failed)));u.push(`${I.caseName} (CPU ${I.cpu}x): ${I.plannedRuns-I.validRuns} of ${I.plannedRuns} runs failed: ${O.join("; ")}`)}for(const I of _){const O=I.median;if(O?.probeSpanMs&&O.cdpSpanMs&&Math.abs(O.cdpSpanMs-O.probeSpanMs)/O.probeSpanMs>.15){u.push(`${I.caseName} (CPU ${I.cpu}x): Chrome's counters cover ${Math.round(O.cdpSpanMs)} ms but frames cover ${Math.round(O.probeSpanMs)} ms, so its cost columns include extra time.`)}if(O?.probeSpanMs!=null&&O.probeSpanMs<i*.8){u.push(`${I.caseName} (CPU ${I.cpu}x) recorded ${Math.round(O.probeSpanMs)} ms, not the ${i} ms asked for.`)}}const x={status:A,hasSteps:Boolean(e.steps?.length),browserLeftRunning:k,replay:{startedAt:w,...$,runOrder:g},url:e.url,browser:`Chromium ${m.version()}${e.headed?"":" (headless)"}`,gpu:h,viewport:c,refreshHz:l,durationMs:i,runsPerCase:s,results:_,traces:d,warnings:u};const T=e.redact?e.redact(x):x;if(e.outFile){try{const{redact:I,...O}=e;(0,import_node_fs25.writeFileSync)(e.outFile,JSON.stringify({options:O,result:T},null,2))}catch(I){T.warnings.push(`Couldn't write outFile ${e.outFile}: ${oneLine(I?.message??String(I))}`)}}return T}function tiedPairs(e){const n=e.filter(r=>!r.control&&r.median&&r.validRuns>1);const t=n.filter(r=>r.droppedMin!=null);const s=[];if(t.length>=2&&t.length===n.length){const r=[...t].sort((o,a)=>o.median.droppedPct-a.median.droppedPct);for(let o=1;o<r.length;o++){const[a,l]=[r[o-1],r[o]];const c=l.median.droppedPct-a.median.droppedPct<=MIN_DROPPED_GAP_PP;if(l.droppedMin<=a.droppedMax||c)s.push([a.caseName,l.caseName])}return s}if(t.length>0)return s;const i=n.filter(r=>r.median.fps!=null).sort((r,o)=>o.median.fps-r.median.fps);for(let r=1;r<i.length;r++){const[o,a]=[i[r-1],i[r]];const l=o.median.fps-a.median.fps<=MIN_FPS_GAP;if((a.fpsMax??0)>=(o.fpsMin??0)||l)s.push([o.caseName,a.caseName])}return s}function closeCall(e){const n=e.filter(a=>!a.control&&a.median);const t=[];const s=n.filter(a=>a.validRuns<2);if(s.length)t.push(`**Too few runs to rank:** ${s.map(a=>a.caseName).join(", ")} (need 2 or more valid runs).`);const i=n.filter(a=>a.validRuns>1);const r=i.filter(a=>a.droppedMin!=null);if(r.length>0&&r.length<i.length){t.push(`**Not enough traced runs to compare smoothness:** ${i.filter(a=>!r.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,l]of tiedPairs(e)){const c=o.get(a);const u=o.get(l);t.push(r.length?`**Too close to call:** ${a} (${c.droppedMin}\u2013${c.droppedMax}% dropped) and ${l} (${u.droppedMin}\u2013${u.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 ${l} 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,l=1)=>a==null?"\u2014":a.toFixed(l);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 s=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 r=a=>`${(Math.min(...a)/1e3).toFixed(2)}\u2013${(Math.max(...a)/1e3).toFixed(2)} s`;if(s.length){t.push(`Recorded: frames over ${r(s)}${i.length?`; cost columns (Chrome's counters) over ${r(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 g=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 (${g}) |`);continue}const m=h.fpsMin!=null&&h.fpsMax!=null&&h.validRuns>1?` (${n(h.fpsMin)}\u2013${n(h.fpsMax)})`:"";const y=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)} | ${y} |`)}const l=closeCall(e.results.filter(h=>h.cpu===a));if(l){t.push("");t.push(l)}const c=e.results.filter(h=>h.cpu===a&&h.budget);if(c.length){t.push("");t.push(`Budget:`);for(const h of c){const p=h.budget;const m=[...p.over,...p.unknown.map(g=>`${g} not measured`)].join("; ");const y=h.control?`${h.caseName} (control)`:h.caseName;t.push(`- **${y}**: ${p.meets?"meets every limit":`misses (${m||"no result"})`}`)}}const u=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 y=h.runs.filter(f=>!f.failed&&f.fps!=null&&f.inputCaptureComplete===false).length;const g=y?` \xB7 \u26A0\uFE0F timing incomplete in ${y} 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}${g}`;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}${g}`});if(u.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(...u)}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_process21,import_node_path26,import_node_fs25,import_node_os18,import_node_url,r1,GPU_RENDERER_JS,COMMON_RATES,FRAME_STATES,PLAYWRIGHT_MISSING,FRAME_TRACE_CATEGORIES,FULL_TRACE_CATEGORIES,MAX_TRACE_CHARS,sleep3,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_process21=require("child_process");import_node_path26=require("path");import_node_fs25=require("fs");import_node_os18=require("os");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;GPU_RENDERER_JS=`(() => {
|
|
1310
|
+
})`}});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 s=e.median;const i=[];const r=[];const o=(d,h,p)=>{if(h==null)return;if(p==null)r.push(d);else if(p>h)i.push(`${d} ${p} > ${h}`)};o("droppedPct",n.droppedPct,e.droppedMin!=null?s?.droppedPct:null);const a=e.runs.filter(d=>!d.failed&&d.fps!=null);const l=t.some(d=>"click"in d||"press"in d||"type"in d);const c=a.map(d=>d.interactions==null||!d.inputCaptureComplete?null:d.worstInteractionMs??16);o("inputMs",n.inputMs,l&&c.length&&c.every(d=>d!=null)?Math.max(...c):null);const u=a.map(d=>Math.max(d.frameGapMaxMs??0,d.edgeSilenceMs??0));o("freezeMs",n.freezeMs,u.length?Math.max(...u):null);o("mainThreadPct",n.mainThreadPct,s?.mainThreadPct);o("jsHeapMb",n.jsHeapMb,s?.heapMb);return{meets:Boolean(s)&&i.length===0&&r.length===0,over:i,unknown:r}}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,r)=>i-r);const s=Math.ceil(n/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,s))]}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 s=()=>{t=t+1831565813>>>0;let r=t;r=Math.imul(r^r>>>15,r|1);r^=r+Math.imul(r^r>>>7,r|61);return((r^r>>>14)>>>0)/4294967296};const i=[...e];for(let r=i.length-1;r>0;r--){const o=Math.floor(s()*(r+1));[i[r],i[o]]=[i[o],i[r]]}return i}function caseUrl(e,n,t){const s=new URL(e);const i=n.path??t;if(i)s.pathname=i.startsWith("/")?i:`${s.pathname.replace(/\/$/,"")}/${i}`;for(const[r,o]of Object.entries(n.params??{}))s.searchParams.set(r,o);return s.toString()}function summarizeFrameTrace(e){const n=p=>e.find(m=>m.name===p&&m.ts!=null);const t=n("__buoy_bench_start");const s=n("__buoy_bench_end");if(!t||!s||s.ts<=t.ts)return null;if(t.pid!==s.pid)return null;const i=t.pid;const r={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>s.ts)continue;const m=p.args?.frame_reporter;if(!m||m.frame_sequence==null)continue;const y=m.state;if(!FRAME_STATES.includes(y))continue;const g=`${m.layer_tree_host_id??""}:${m.frame_source??""}:${m.frame_sequence}`;const f=Boolean(m.affects_smoothness)&&y!=="STATE_PRESENTED_ALL";const w=o.get(g);if(!w)o.set(g,{state:y,fault:f});else{if(r[y]>r[w.state])w.state=y;w.fault=w.fault||f}}let a=0;let l=0;let c=0;let u=0;for(const p of o.values()){if(p.state==="STATE_PRESENTED_ALL")a++;else if(p.state==="STATE_PRESENTED_PARTIAL")l++;else c++;if(p.fault)u++}const d=a+l+c;if(d===0)return null;const h=(s.ts-t.ts)/1e6;return{full:a,partial:l,dropped:c,faults:u,shownFps:r1((a+l)/h),droppedPct:r1(u/d*100)}}function keyMatches(e,n){const t=e.split("+");const s=t[t.length-1];const i=new Set(t.slice(0,-1).map(a=>a.toLowerCase()));if(n.key!==s)return false;const r=s.toLowerCase();const o={ctrl:i.has("control")||r==="control",alt:i.has("alt")||r==="alt",meta:i.has("meta")||i.has("command")||r==="meta",shift:i.has("shift")||r==="shift"};if(Boolean(n.ctrl)!==o.ctrl||Boolean(n.alt)!==o.alt||Boolean(n.meta)!==o.meta)return false;if(!o.shift&&n.shift&&s.length!==1)return false;if(o.shift&&!n.shift)return false;return true}function mergeInteractions(e){const n=[];for(const t of e){const s=t.startTime+t.duration;const i=n.find(o=>o.interactionId===t.interactionId&&Math.abs(o.startTime+o.duration-s)<=8);if(!i){n.push({...t});continue}const r=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(r,s)-i.startTime;if(t.processingEnd-t.processingStart>0)i.name=t.name}return n}function summarizeProbe(e,n){const{start:t,end:s}=e;const i=Math.max(1,s-t);const r=[];for(let k=1;k<e.frames.length;k++)r.push(e.frames[k]-e.frames[k-1]);const o=1e3/n;const a=percentile(r,95);let l=null;if(e.longTasks){let k=0;for(const S of e.longTasks){const E=Math.min(S.end,s)-Math.max(S.start,t);if(E>0)k+=E}l=r1(Math.min(100,k/i*100))}let c=null;let u=null;let d=null;let h=[];let p=0;if(e.loaf){let k=0;let S=0;let E=0;const A=new Map;for(const P of e.loaf){if(P.startTime>=t&&P.startTime<=s&&P.startTime+P.duration>s)p++;if(P.startTime<t||P.startTime+P.duration>s)continue;k+=P.blockingDuration;if(P.styleAndLayoutStart>P.startTime)S+=Math.max(0,P.startTime+P.duration-P.styleAndLayoutStart);for(const x of P.scripts){E+=x.forced;const T=A.get(x.name)??{ms:0,count:0};T.ms+=x.duration;T.count++;A.set(x.name,T)}}c=r1(k);u=r1(S);d=r1(E);h=[...A.entries()].map(([P,x])=>({name:P,ms:r1(x.ms),count:x.count})).sort((P,x)=>x.ms-P.ms).slice(0,5)}let m=null;if(e.shifts){let k=0;for(const S of e.shifts)if(!S.hadRecentInput&&S.startTime>=t&&S.startTime<=s)k+=S.value;m=Math.round(k*1e3)/1e3}let y=null;let g=null;if(e.events){const k=mergeInteractions(e.events.filter(S=>S.startTime>=t&&S.startTime<=s));y=new Set(k.map(S=>S.interactionId)).size;for(const S of k)if(!g||S.duration>g.duration)g=S}const f=g;const w=e.frames;const v=[...(e.clicks??[]).map(k=>k.t),...(e.keys??[]).map(k=>k.t)];const b=v.length?Math.max(...v):null;const _=b==null||w.filter(k=>k>b).length>=2;const $=w.length===0?i:Math.max(0,w[0]-t,s-w[w.length-1]);return{edgeSilenceMs:r1($),frameGapCount:r.length,inputCaptureComplete:_,resized:Boolean(e.resized),interactions:y,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:r.length?r1(Math.max(...r)):null,longFrames:r.filter(k=>k>o*1.5).length,longTaskPct:l,loafBlockingMs:c,styleLayoutMs:u,forcedLayoutMs:d,topScripts:h,loafCutOff:p,layoutShift:m,domNodesStart:e.domStart,domNodesEnd:e.domEnd}}function cdpDeltas(e,n,t){const s=a=>a in e&&a in n?n[a]-e[a]:null;const i=a=>{const l=s(a);return l==null?null:r1(l*1e3)};const r=i("TaskDuration");const o=i("Timestamp")??t;return{mainThreadPct:r==null?null:r1(r/Math.max(1,o)*100),scriptMs:i("ScriptDuration"),styleMs:i("RecalcStyleDuration"),layoutMs:i("LayoutDuration"),layoutCount:s("LayoutCount"),styleCount:s("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 s=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(s>=100)t.push(`Freeze of ${Math.round(s)} 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(l=>!l.failed&&l.fps!=null);const s=[...t].sort((l,c)=>(l.fps??0)-(c.fps??0));const i=t.filter(l=>l.droppedPct!=null).length;const r=t.length>0&&i===t.length;const o=r?[...t].sort((l,c)=>l.droppedPct-c.droppedPct):s;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 l=r?t.map(u=>u.droppedPct):[];const c=t.map(u=>u.worstInteractionMs).filter(u=>u!=null);return{droppedMin:l.length?Math.min(...l):null,droppedMax:l.length?Math.max(...l):null,inputMin:c.length?Math.min(...c):null,inputMax:c.length?Math.max(...c):null}})(),runs:e,median:a,fpsMin:s.length?s[0].fps:null,fpsMax:s.length?s[s.length-1].fps:null,flags:a?webBenchFlags(r?a:{...a,droppedPct:null},n):[]}}async function loadChromium(e){const n=(0,import_node_module.createRequire)((0,import_node_path26.join)(e,"noop.js"));for(const t of["playwright","playwright-core"]){let s;try{s=n.resolve(t)}catch{continue}const i=await import((0,import_node_url.pathToFileURL)(s).href);const r=i.chromium??i.default?.chromium;if(r)return r}throw new Error(`${PLAYWRIGHT_MISSING}
|
|
1311
|
+
(Searched from ${e}. Pass projectRoot to look elsewhere.)`)}async function withDeadline(e,n,t){let s;const i=new Promise(r=>{s=setTimeout(()=>r("timeout"),n)});try{const r=await Promise.race([e,i]);if(r==="timeout"){e.catch(()=>{});return await t()}return r}finally{clearTimeout(s)}}function gitCommit(e){try{return(0,import_node_child_process21.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_process21.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,l,c)=>{if(o==null)return;if(!Number.isInteger(o)||o<l||o>c)throw new Error(`${a} must be a whole number from ${l} to ${c}.`)};const s=(o,a,l)=>{if(o==null)return;if(!Number.isFinite(o)||o<l)throw new Error(`${a} must be a number of at least ${l}.`)};const i=(o,a,l,c)=>{if(o==null)return;if(!Number.isFinite(o)||o<l||o>c)throw new Error(`${a} must be from ${l} to ${c}.`)};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 r=e.cpuSlowdown??[];for(const o of r)i(o,"each cpuSlowdown rate",1,20);if(new Set(r).size!==r.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 s of n){if("expect"in s)continue;const i="click"in s?s.click:"type"in s?s.type.selector:null;if(!i||t.has(i))continue;const r=await e.$(i);const o=r?await r.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 s of n){if("click"in s){const i=t.get(s.click);await e.mouse.click(i.x,i.y)}else if("press"in s)await e.keyboard.press(s.press);else if("type"in s){const i=t.get(s.type.selector);await e.mouse.click(i.x,i.y);await e.keyboard.type(s.type.text)}else if("scroll"in s)await e.mouse.wheel(0,s.scroll);else if("wait"in s)await sleep3(s.wait)}}function missedInput(e,n,t=[]){const s=[...n].sort((u,d)=>u.t-d.t);const i=[...t].sort((u,d)=>u.t-d.t);let r=0;let o=0;let a=-Infinity;const l=u=>{while(r<s.length){const d=s[r++];if(d.t>=a&&d.hit.includes(u)){a=d.t;return true}}return false};const c=u=>{while(o<i.length){const d=i[o++];if(d.t>=a&&keyMatches(u,d)){a=d.t;return true}}return false};for(const[u,d]of e.entries()){if("click"in d&&!l(d.click)){return`step ${u+1} (click ${d.click}) has no real click on its target, in order (moved, covered, or ignored)`}if("type"in d){if(!l(d.type.selector))return`step ${u+1} (type into ${d.type.selector}) has no real click on its target`;for(const h of d.type.text){if(!c(h))return`step ${u+1} (type "${d.type.text}") didn't deliver the key "${h}"`}}if("press"in d&&!c(d.press))return`step ${u+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:s,text:i}=t.expect;const r=await e.evaluate(`(() => { const n = document.querySelector(${JSON.stringify(s)}); if (!n) return false; ${i==null?"return true;":`return (n.textContent || "").includes(${JSON.stringify(i)});`} })()`);if(r)return i==null?s:`${s} contains "${i}"`}return null}async function checkTargetsStill(e,n){for(const[t,s]of n){const i=await e.$(t);const r=i?await i.boundingBox():null;if(!r)return`step target ${t} was gone after the run`;const o=r.x+r.width/2;const a=r.y+r.height/2;if(Math.abs(o-s.x)>4||Math.abs(a-s.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 s={browserLeftRunning:false};try{return await runWebBenchLocked(e,n,s)}finally{t(s.browserLeftRunning)}}async function waitForHostLock(e,n=()=>{},t=2e3){const s=Date.now()+Math.max(0,e);let i=false;for(;;){try{return takeHostLock()}catch(r){const o=r?.message??String(r);const a=/Another web benchmark|already running/.test(o);if(!a||Date.now()+t>s)throw r;if(!i){n("another web benchmark is running; waiting for it to finish");i=true}await new Promise(l=>setTimeout(l,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 r=`${LOCK_FILE}.${e.replace(/[^\w]/g,"")}.tmp`;try{(0,import_node_fs25.writeFileSync)(r,n);(0,import_node_fs25.linkSync)(r,LOCK_FILE);return true}catch{return false}finally{try{(0,import_node_fs25.unlinkSync)(r)}catch{}}};const s=()=>{try{return JSON.parse((0,import_node_fs25.readFileSync)(LOCK_FILE,"utf8"))}catch{return null}};const i=r=>{if(!r)return false;try{process.kill(r,0);return true}catch{return false}};if(!t()){const r=s();if(!r)throw new Error("Another web benchmark is starting. Wait for it to finish.");if(i(r.pid)){throw new Error(`Another web benchmark is running (pid ${r.pid}, since ${r.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_fs25.statSync)(o).mtimeMs>3e4){const l=`${o}.${e.replace(/[^\w]/g,"")}.old`;(0,import_node_fs25.renameSync)(o,l);if(Date.now()-(0,import_node_fs25.statSync)(l).mtimeMs>3e4)(0,import_node_fs25.unlinkSync)(l);else{try{(0,import_node_fs25.linkSync)(l,o)}catch{}(0,import_node_fs25.unlinkSync)(l)}}}catch{}try{a=(0,import_node_fs25.openSync)(o,"wx")}catch{throw new Error("Another web benchmark is clearing an old lock. Try again in a moment.")}try{const l=s();if(l?.token===r.token)(0,import_node_fs25.unlinkSync)(LOCK_FILE);if(!t())throw new Error("Another web benchmark just started. Wait for it to finish.")}finally{(0,import_node_fs25.closeSync)(a);try{(0,import_node_fs25.unlinkSync)(o)}catch{}}}lockedHere=true;return(r=false)=>{if(r)return;lockedHere=false;if(s()?.token===e){try{(0,import_node_fs25.unlinkSync)(LOCK_FILE)}catch{}}}}async function runWebBenchLocked(e,n,t){const s=e.runsPerCase??3;const i=Math.max(500,e.durationMs??5e3);const r=Math.max(0,e.settleMs??1e3);const o=e.frameTrace!==false;const a=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(I=>Math.max(1,I));let l=e.refreshHz??60;const c={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const u=[];const d=[];let h=null;if(s<2)u.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 y=[];const g=[];const f=Date.now();const w=new Date(f).toISOString();const v=e.maxBatchMs??30*6e4;let b=false;let _=false;const $={commit:gitCommit(e.projectRoot??process.cwd()),dirty:gitDirty(e.projectRoot??process.cwd())};try{const I={viewport:{width:c.width,height:c.height},deviceScaleFactor:c.deviceScaleFactor};const O=await withDeadline(m.newContext(I),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 N=3e4+15e3+r+i+6e4;const F=async(j,H,V,L)=>{const W={};return withDeadline(C(W,j,H,V,L),N,async()=>{W.cancelled=true;if(!W.ctx)b=true;else if(!await closeWithin(W.ctx,1e4))b=true;return{...X(j.name,H,V),failed:`run took over ${Math.round(N/1e3)} s (the page may be stuck in a loop)`}})};const X=(j,H,V)=>({caseName:j,cpu:H,round:V,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 C=async(j,H,V,L,W)=>{const Q=await m.newContext(I);j.ctx=Q;if(j.cancelled){await closeWithin(Q,1e4);throw new Error("run was cancelled during setup")}const J=await Q.newPage();const te=[];J.on("pageerror",ne=>te.push(String(ne?.message??ne)));const ae={...X(H.name,V,L),pageErrors:te};try{const ne=await Q.newCDPSession(J);await ne.send("Performance.enable",{timeDomain:"timeTicks"});if(V>1)await ne.send("Emulation.setCPUThrottlingRate",{rate:V});const fe=await J.goto(caseUrl(e.url,H,e.path),{waitUntil:"load",timeout:3e4});if(fe&&fe.status()>=400)return{...ae,failed:`HTTP ${fe.status()}`};if(e.readySelector)await J.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep3(r);const ge=W||o;if(ge){await ne.send("Tracing.start",{transferMode:"ReturnAsStream",categories:(W?FULL_TRACE_CATEGORIES:FRAME_TRACE_CATEGORIES).join(",")})}const de=e.steps?.length?await locateStepTargets(J,e.steps):new Map;if(e.steps?.length){const R=await expectsAlreadyTrue(J,e.steps);if(R)return{...ae,failed:`expect ${R} already held before the steps, so it can't prove they worked`};const B=[...de.keys()];const z=e.steps.flatMap(G=>"expect"in G?[G.expect]:[]);await J.evaluate(`window.__buoyBenchClickTargets = ${JSON.stringify(B)}; window.__buoyBenchExpects = ${JSON.stringify(z)}; true`)}const ie=await readMetrics(ne);await J.evaluate(START_PROBE_JS);const se=Date.now();if(e.steps?.length)await runSteps(J,e.steps,de);const re=i-(Date.now()-se);if(re>0)await sleep3(re);await J.evaluate(END_PROBE_JS);const ce=await readMetrics(ne);const ye=Date.now()-se;const he=await J.evaluate(COLLECT_PROBE_JS);let M=null;if(ge){const R=new Promise(ee=>ne.on("Tracing.tracingComplete",K=>ee(K.stream)));await ne.send("Tracing.end");const B=await R;const z=W?(0,import_node_fs25.openSync)(W,"w"):null;const G=[];let Y=0;try{for(;;){const ee=await ne.send("IO.read",{handle:B});const K=ee.base64Encoded?Buffer.from(ee.data,"base64").toString("utf8"):ee.data;Y+=K.length;if(z!=null)(0,import_node_fs25.writeSync)(z,K);if(Y<=MAX_TRACE_CHARS)G.push(K);else G.length=0;if(ee.eof)break}}finally{await ne.send("IO.close",{handle:B}).catch(()=>{});if(z!=null)(0,import_node_fs25.closeSync)(z)}if(W)d.push(W);if(Y>MAX_TRACE_CHARS){u.push(`Trace for ${H.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 ee=JSON.parse(G.join(""));M=summarizeFrameTrace(Array.isArray(ee)?ee:ee.traceEvents??[])}catch{M=null}}}if(!he)return{...ae,failed:"probe returned nothing (page navigated away?)"};if(he.hidden)return{...ae,failed:"page was hidden during the run"};if(he.resized)return{...ae,failed:"the window size or zoom changed during the run"};if(te.length&&!e.allowPageErrors){return{...ae,failed:`page error: ${oneLine(te[0])}`}}if(e.steps?.length){const R=missedInput(e.steps,he.clicks??[],he.keys??[]);if(R)return{...ae,failed:R};const B=expectFailureFrom(he.expectResults??[]);if(B)return{...ae,failed:B};const z=await checkTargetsStill(J,de);if(z)return{...ae,failed:z}}const D="Timestamp"in ie&&"Timestamp"in ce?r1((ce.Timestamp-ie.Timestamp)*1e3):null;return{...ae,...summarizeProbe(he,l),...cdpDeltas(ie,ce,ye),probeSpanMs:r1(he.end-he.start),cdpSpanMs:D,shownFps:M?.shownFps??null,droppedFrames:M?.faults??null,frameCounts:M?{full:M.full,partial:M.partial,dropped:M.dropped,faults:M.faults}:null,droppedPct:M?.droppedPct??null}}catch(ne){return{...ae,failed:oneLine(ne?.message??String(ne))}}finally{await closeWithin(J,5e3);if(!await closeWithin(Q,1e4))b=true}};{const j=await withDeadline(O.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the GPU check page in 15 s.")});try{h=await withDeadline(j.evaluate(GPU_RENDERER_JS),5e3,async()=>null)??null}catch{h=null}finally{await closeWithin(j,5e3)}if(h)n(`GPU ${h}`);if(h&&isSoftwareGpu(h)){u.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 j=await withDeadline(O.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the calibration page in 15 s.")});try{await withDeadline(j.goto("about:blank"),15e3,async()=>null);const H=await withDeadline(j.evaluate(MEASURE_REFRESH_JS),5e3,async()=>{throw new Error("idle rate check timed out")});l=snapRefreshRate(H);n(`idle frame rate ${H.toFixed(1)} \u2192 ${l} Hz`)}catch{u.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await closeWithin(j,5e3)}}n("warmup load");await F(e.cases[0],1,-1);const U=async(j,H,V,L)=>{if(b){y.push({...X(j.name,H,V),failed:"an earlier run left the browser stuck; batch stopped"});return}if(Date.now()-f>v){y.push({...X(j.name,H,V),failed:"batch time limit reached; run skipped"});return}n(L);g.push({cpu:H,round:V,caseName:j.name});y.push(await F(j,H,V))};for(const j of a){for(let H=0;H<s;H++){const V=shuffled(e.cases,2654435769^H*7919+j);for(const L of V)await U(L,j,H,`cpu ${j}x \xB7 round ${H+1}/${s} \xB7 ${L.name}`)}for(let H=0;H<(e.extraRoundsWhenClose??0);H++){const V=e.cases.map(J=>aggregateCase(y.filter(te=>te.cpu===j&&te.caseName===J.name),l));for(const J of V)J.control=e.cases.find(te=>te.name===J.caseName)?.control===true;const L=new Set([...tiedPairs(V).flat(),...V.filter(J=>!J.control&&J.validRuns<2&&J.plannedRuns>0).map(J=>J.caseName)]);if(L.size===0)break;const W=s+H;const Q=shuffled(e.cases.filter(J=>L.has(J.name)),20973^W*7919+j);for(const J of Q)await U(J,j,W,`cpu ${j}x \xB7 extra round ${H+1} (close call) \xB7 ${J.name}`)}}if(e.traceDir){(0,import_node_fs25.mkdirSync)(e.traceDir,{recursive:true});for(const[j,H]of e.cases.entries()){const V=(0,import_node_path26.join)(e.traceDir,`${String(j+1).padStart(2,"0")}-${H.name.replace(/[^\w.-]+/g,"_")}.json`);if(b||Date.now()-f>v){u.push(`Trace run for ${H.name} skipped (batch time limit or a stuck browser).`);continue}n(`trace \xB7 ${H.name}`);const L=await F(H,a[0],-1,V);if(L.failed)u.push(`Trace run for ${H.name} failed: ${L.failed}`)}}await closeWithin(O,1e4)}finally{const I=await closeWithin(m,15e3);t.browserLeftRunning=!I;if(!I){_=true;u.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 I of a){for(const O of e.cases){k.push(aggregateCase(y.filter(N=>N.cpu===I&&N.caseName===O.name),l))}}for(const I of k)I.control=e.cases.find(O=>O.name===I.caseName)?.control===true;for(const I of a){const O=k.filter(C=>C.cpu===I);const N=O.find(C=>!C.control);if(!N)continue;N.isBaseline=true;const F=C=>C.droppedMin!=null?C.median?.droppedPct??null:null;const X=F(N);for(const C of O.filter(U=>U!==N&&!U.control)){const U=F(C);C.deltaDroppedPct=X!=null&&U!=null?r1(U-X):null}}if(e.allowPageErrors&&y.some(I=>I.pageErrors.length)){u.push("Some runs threw page errors (kept because allowPageErrors is on).")}if(b)u.push("A browser context wouldn't close, so the batch stopped early. Restart and run again.");if(e.budgets)for(const I of k)I.budget=checkBudgets(I,e.budgets,e.steps??[]);const S=k.filter(I=>I.median).length;const E=k.every(I=>I.validRuns===I.plannedRuns);const A=S===0?"failed":S<k.length||!E?"partial":"ok";const P=k.filter(I=>I.median&&I.validRuns>0&&I.validRuns<I.plannedRuns);for(const I of P){const O=Array.from(new Set(I.runs.filter(N=>N.failed).map(N=>N.failed)));u.push(`${I.caseName} (CPU ${I.cpu}x): ${I.plannedRuns-I.validRuns} of ${I.plannedRuns} runs failed: ${O.join("; ")}`)}for(const I of k){const O=I.median;if(O?.probeSpanMs&&O.cdpSpanMs&&Math.abs(O.cdpSpanMs-O.probeSpanMs)/O.probeSpanMs>.15){u.push(`${I.caseName} (CPU ${I.cpu}x): Chrome's counters cover ${Math.round(O.cdpSpanMs)} ms but frames cover ${Math.round(O.probeSpanMs)} ms, so its cost columns include extra time.`)}if(O?.probeSpanMs!=null&&O.probeSpanMs<i*.8){u.push(`${I.caseName} (CPU ${I.cpu}x) recorded ${Math.round(O.probeSpanMs)} ms, not the ${i} ms asked for.`)}}const x={status:A,hasSteps:Boolean(e.steps?.length),browserLeftRunning:_,replay:{startedAt:w,...$,runOrder:g},url:e.url,browser:`Chromium ${m.version()}${e.headed?"":" (headless)"}`,gpu:h,viewport:c,refreshHz:l,durationMs:i,runsPerCase:s,results:k,traces:d,warnings:u};const T=e.redact?e.redact(x):x;if(e.outFile){try{const{redact:I,...O}=e;(0,import_node_fs25.writeFileSync)(e.outFile,JSON.stringify({options:O,result:T},null,2))}catch(I){T.warnings.push(`Couldn't write outFile ${e.outFile}: ${oneLine(I?.message??String(I))}`)}}return T}function tiedPairs(e){const n=e.filter(r=>!r.control&&r.median&&r.validRuns>1);const t=n.filter(r=>r.droppedMin!=null);const s=[];if(t.length>=2&&t.length===n.length){const r=[...t].sort((o,a)=>o.median.droppedPct-a.median.droppedPct);for(let o=1;o<r.length;o++){const[a,l]=[r[o-1],r[o]];const c=l.median.droppedPct-a.median.droppedPct<=MIN_DROPPED_GAP_PP;if(l.droppedMin<=a.droppedMax||c)s.push([a.caseName,l.caseName])}return s}if(t.length>0)return s;const i=n.filter(r=>r.median.fps!=null).sort((r,o)=>o.median.fps-r.median.fps);for(let r=1;r<i.length;r++){const[o,a]=[i[r-1],i[r]];const l=o.median.fps-a.median.fps<=MIN_FPS_GAP;if((a.fpsMax??0)>=(o.fpsMin??0)||l)s.push([o.caseName,a.caseName])}return s}function closeCall(e){const n=e.filter(a=>!a.control&&a.median);const t=[];const s=n.filter(a=>a.validRuns<2);if(s.length)t.push(`**Too few runs to rank:** ${s.map(a=>a.caseName).join(", ")} (need 2 or more valid runs).`);const i=n.filter(a=>a.validRuns>1);const r=i.filter(a=>a.droppedMin!=null);if(r.length>0&&r.length<i.length){t.push(`**Not enough traced runs to compare smoothness:** ${i.filter(a=>!r.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,l]of tiedPairs(e)){const c=o.get(a);const u=o.get(l);t.push(r.length?`**Too close to call:** ${a} (${c.droppedMin}\u2013${c.droppedMax}% dropped) and ${l} (${u.droppedMin}\u2013${u.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 ${l} 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,l=1)=>a==null?"\u2014":a.toFixed(l);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 s=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 r=a=>`${(Math.min(...a)/1e3).toFixed(2)}\u2013${(Math.max(...a)/1e3).toFixed(2)} s`;if(s.length){t.push(`Recorded: frames over ${r(s)}${i.length?`; cost columns (Chrome's counters) over ${r(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 g=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 (${g}) |`);continue}const m=h.fpsMin!=null&&h.fpsMax!=null&&h.validRuns>1?` (${n(h.fpsMin)}\u2013${n(h.fpsMax)})`:"";const y=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)} | ${y} |`)}const l=closeCall(e.results.filter(h=>h.cpu===a));if(l){t.push("");t.push(l)}const c=e.results.filter(h=>h.cpu===a&&h.budget);if(c.length){t.push("");t.push(`Budget:`);for(const h of c){const p=h.budget;const m=[...p.over,...p.unknown.map(g=>`${g} not measured`)].join("; ");const y=h.control?`${h.caseName} (control)`:h.caseName;t.push(`- **${y}**: ${p.meets?"meets every limit":`misses (${m||"no result"})`}`)}}const u=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 y=h.runs.filter(f=>!f.failed&&f.fps!=null&&f.inputCaptureComplete===false).length;const g=y?` \xB7 \u26A0\uFE0F timing incomplete in ${y} 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}${g}`;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}${g}`});if(u.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(...u)}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_process21,import_node_path26,import_node_fs25,import_node_os18,import_node_url,r1,GPU_RENDERER_JS,COMMON_RATES,FRAME_STATES,PLAYWRIGHT_MISSING,FRAME_TRACE_CATEGORIES,FULL_TRACE_CATEGORIES,MAX_TRACE_CHARS,sleep3,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_process21=require("child_process");import_node_path26=require("path");import_node_fs25=require("fs");import_node_os18=require("os");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;GPU_RENDERER_JS=`(() => {
|
|
1312
1312
|
try {
|
|
1313
1313
|
const gl = document.createElement("canvas").getContext("webgl2") || document.createElement("canvas").getContext("webgl");
|
|
1314
1314
|
if (!gl) return null;
|
|
1315
1315
|
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1316
1316
|
return String(gl.getParameter(ext ? ext.UNMASKED_RENDERER_WEBGL : gl.RENDERER));
|
|
1317
1317
|
} catch { return null; }
|
|
1318
|
-
})()`;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;sleep3=e=>new Promise(n=>setTimeout(n,e));LOCK_FILE=(0,import_node_path26.join)((0,import_node_os18.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 s=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/s)*s}function median2(e){if(!e.length)return null;const n=[...e].sort((s,i)=>s-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 s=(c,u,d,h)=>({status:c,stopReason:u,highestPass:d?{load:d.load,fps:d.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:c==="below_one"||c==="bracketed"&&!!d&&!!h&&bracketResolved(d.load,h.load,n.precisionPct),steps:t});const i=(c,u,d)=>s("invalid",c.fps==null?"measurement_failed":"page_error",u,d);const r=async(c,u=false)=>{const d=await e(c);if(u)d.confirm=true;t.push(d);return d};let o=null;let a=null;let l=await r(n.start);if(!l.valid)return i(l,null,null);if(l.pass)o=l;else{a=l;let c=n.start;while(!o){if(c<=1)return s("below_one","performance",null,a);if(!n.timeLeft())return s("lower_bound","deadline",null,a);c=Math.max(1,Math.floor(c/2));l=await r(c);if(!l.valid)return i(l,null,a);if(l.pass)o=l;else a=l}}while(!a||a.load<=o.load){if(o.load>=n.max)return s("lower_bound","max",o,null);if(!n.timeLeft())return s("lower_bound","deadline",o,null);l=await r(Math.min(n.max,nextLoad(o.load,n.growth)));if(!l.valid)return i(l,o,null);if(l.pass){o=l;continue}if(!n.timeLeft())return s("bracketed","performance",o,l);const c=await r(l.load,true);if(!c.valid)return i(c,o,null);if(c.pass)o=c;else a=c}while(!bracketResolved(o.load,a.load,n.precisionPct)&&n.timeLeft()){const c=Math.round((o.load+a.load)/2);if(c<=o.load||c>=a.load)break;l=await r(c);if(!l.valid)break;if(l.pass)o=l;else a=l}return s("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=(b,
|
|
1318
|
+
})()`;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;sleep3=e=>new Promise(n=>setTimeout(n,e));LOCK_FILE=(0,import_node_path26.join)((0,import_node_os18.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 s=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/s)*s}function median2(e){if(!e.length)return null;const n=[...e].sort((s,i)=>s-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 s=(c,u,d,h)=>({status:c,stopReason:u,highestPass:d?{load:d.load,fps:d.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:c==="below_one"||c==="bracketed"&&!!d&&!!h&&bracketResolved(d.load,h.load,n.precisionPct),steps:t});const i=(c,u,d)=>s("invalid",c.fps==null?"measurement_failed":"page_error",u,d);const r=async(c,u=false)=>{const d=await e(c);if(u)d.confirm=true;t.push(d);return d};let o=null;let a=null;let l=await r(n.start);if(!l.valid)return i(l,null,null);if(l.pass)o=l;else{a=l;let c=n.start;while(!o){if(c<=1)return s("below_one","performance",null,a);if(!n.timeLeft())return s("lower_bound","deadline",null,a);c=Math.max(1,Math.floor(c/2));l=await r(c);if(!l.valid)return i(l,null,a);if(l.pass)o=l;else a=l}}while(!a||a.load<=o.load){if(o.load>=n.max)return s("lower_bound","max",o,null);if(!n.timeLeft())return s("lower_bound","deadline",o,null);l=await r(Math.min(n.max,nextLoad(o.load,n.growth)));if(!l.valid)return i(l,o,null);if(l.pass){o=l;continue}if(!n.timeLeft())return s("bracketed","performance",o,l);const c=await r(l.load,true);if(!c.valid)return i(c,o,null);if(c.pass)o=c;else a=c}while(!bracketResolved(o.load,a.load,n.precisionPct)&&n.timeLeft()){const c=Math.round((o.load+a.load)/2);if(c<=o.load||c>=a.load)break;l=await r(c);if(!l.valid)break;if(l.pass)o=l;else a=l}return s("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=(b,_)=>{if(b!=null&&!Number.isFinite(b))throw new Error(`${_} must be a finite number.`)};for(const[b,_]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(_,b);const s=Math.max(1,Math.floor(e.start??100));const i=Math.min(4,Math.max(1.05,e.growth??1.3));const r=Math.max(s,Math.floor(e.max??1e9));const o=e.targetFps??55;const a=Math.min(50,Math.max(.5,e.precisionPct??4));const l=Math.max(1,Math.min(5,Math.floor(e.rounds??1)));const c=Math.max(0,e.settleMs??1e3);const u=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 y=m+(e.maxBatchMs??40*6e4);const g=()=>y-Date.now();const f=m>>>0;const w=await waitForHostLock(Math.min(e.lockWaitMs??20*6e4,Math.max(0,g())),n);let v=false;try{const b=await loadChromium(e.projectRoot??process.cwd());const _=await b.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 S=60;{const x=await _.newContext($);try{const T=await x.newPage();await T.goto("about:blank");k=await T.evaluate(GPU_RENDERER_JS)??null;const I=await withDeadline(T.evaluate(measureFramesJs(200,1e3)),1e4,async()=>({fps:60}));S=snapRefreshRate(I.fps)}finally{await closeWithin(x,5e3)}}n(`GPU ${k??"unknown"} \xB7 idle ${S} 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>S*.95){p.push(`The target (${o} fps) is at or above this browser's idle frame rate (${S} 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 E=e.cases.map(x=>({caseName:x.name,ceiling:null,atLeast:null,rounds:[],errors:[]}));const A=async(x,T,I,O)=>{const N=O.length;const F=caseUrl(e.url,{...T,params:{...T.params??{},[e.param]:String(I)}},e.path);const X=C=>Math.max(1e3,Math.min(C,g()+15e3));try{await x.goto(F,{waitUntil:"load",timeout:X(6e4)});if(e.readySelector)await x.waitForSelector(e.readySelector,{state:"attached",timeout:X(6e4)});const C=await withDeadline(x.evaluate(measureFramesJs(c,u)),c+u+6e4,async()=>{throw new Error("the page stopped drawing (no frames for 60 s)")});const U=O.length>N?O[O.length-1]:void 0;if(U)return{load:I,fps:C.fps,p95GapMs:C.p95,valid:false,pass:false,error:U};return{load:I,fps:C.fps,p95GapMs:C.p95,valid:true,pass:C.fps>=o}}catch(C){return{load:I,fps:null,p95GapMs:null,valid:false,pass:false,error:(C?.message??String(C)).split("\n")[0].slice(0,200)}}};let P=false;for(let x=0;x<l&&!P;x++){const T=shuffled(e.cases.map((I,O)=>({c:I,i:O})),f+x*7919>>>0);for(const{c:I,i:O}of T){const N=E[O];if(P||g()<=0){N.rounds.push({round:x+1,status:"skipped",stopReason:"not_run",highestPass:null,lowestMiss:null,resolved:false,steps:[]});continue}n(`round ${x+1}/${l} \xB7 ${I.name}`);let F;try{F=await _.newContext($);const X=await F.newPage();X.on("pageerror",U=>N.errors.push(String(U?.message??U).slice(0,200)));if(d>1){const U=await F.newCDPSession(X);await U.send("Emulation.setCPUThrottlingRate",{rate:d})}const C=await rampOnce(U=>A(X,I,U,N.errors),{start:s,growth:i,max:r,precisionPct:a,timeLeft:()=>g()>0});N.rounds.push({round:x+1,...C});n(` ${I.name}: ${C.status} ${C.highestPass?.load??"-"}`)}catch(X){N.errors.push((X?.message??String(X)).split("\n")[0].slice(0,200));N.rounds.push({round:x+1,status:"invalid",stopReason:"measurement_failed",highestPass:null,lowestMiss:null,resolved:false,steps:[]})}finally{if(F&&!await closeWithin(F,1e4)){v=true;P=true;p.push(`${I.name}'s browser context didn't close, so the batch stopped to keep later runs clean.`)}}}}for(const x of E){const T=x.rounds.filter(O=>O.status==="bracketed"||O.status==="below_one");x.ceiling=median2(T.map(O=>O.highestPass?.load??0));const I=x.rounds.filter(O=>O.status==="lower_bound"&&O.highestPass).map(O=>O.highestPass.load);x.atLeast=I.length?Math.max(...I):null}if(g()<=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:S,targetFps:o,cpuSlowdown:d,viewport:h,seed:f,results:E,warnings:p}}finally{if(!await closeWithin(_,1e4))v=true}}finally{w(v)}}function formatWebRampMarkdown(e){const n=r=>r==null?"\u2014":r.toLocaleString("en-US");const t=r=>r?`${n(r.load)} @ ${r.fps.toFixed(1)}`:"\u2014";const s=[];s.push(`### Web ceiling \xB7 ${e.url}`);s.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`:""}`);s.push("");s.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.");s.push("");const i=Math.max(0,...e.results.map(r=>r.ceiling??0));s.push("| Case | Ceiling | Each round: highest pass @ fps \u2192 lowest miss @ fps | Notes |");s.push("|------|--------:|------------------------------------------------------|-------|");for(const r of e.results){const o=r.rounds.map(l=>{if(l.status==="skipped")return`r${l.round}: not run`;if(l.status==="invalid")return`r${l.round}: invalid (${l.stopReason}) after ${t(l.highestPass)}`;if(l.status==="lower_bound")return`r${l.round}: at least ${t(l.highestPass)} (${l.stopReason})`;if(l.status==="below_one")return`r${l.round}: even 1 missed (${t(l.lowestMiss)})`;return`r${l.round}: ${t(l.highestPass)} \u2192 ${t(l.lowestMiss)}${l.resolved?"":" (wide)"}`}).join("<br>");const a=[r.ceiling===i&&i>0?"best":"",r.atLeast!=null?`at least ${n(r.atLeast)}`:"",r.rounds.some(l=>l.status==="invalid")?"a round broke: check the page before trusting it":"",r.errors.length?`${r.errors.length} page error(s): ${r.errors[0]}`:""].filter(Boolean);s.push(`| ${r.caseName} | ${n(r.ceiling)} | ${o} | ${a.join("; ")} |`)}if(e.warnings.length){s.push("");for(const r of e.warnings)s.push(`- \u26A0\uFE0F ${r}`)}return s.join("\n")}var measureFramesJs;var init_webRamp=__esm({"src/webBench/webRamp.ts"(){"use strict";init_webBench();measureFramesJs=(e,n)=>`new Promise((resolve) => {
|
|
1319
1319
|
setTimeout(() => {
|
|
1320
1320
|
const deltas = [];
|
|
1321
1321
|
let start;
|
|
@@ -1337,7 +1337,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
1337
1337
|
};
|
|
1338
1338
|
requestAnimationFrame(loop);
|
|
1339
1339
|
}, ${e});
|
|
1340
|
-
})`}});async function runWebShots(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.outDir)throw new Error("Pass outDir, the folder to save screenshots in.");const t=[...new Set((e.atMs?.length?e.atMs:[500,2e3]).map(l=>Math.max(0,Math.round(l))))].sort((l,c)=>l-c);const s=(0,import_path.join)(e.outDir,new Date().toISOString().replace(/[:.]/g,"-"));(0,import_fs.mkdirSync)(s,{recursive:true});const i={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const r={dir:"",files:[],pageErrors:[],diffs:[],outliers:[]};r.dir=s;const o=await waitForHostLock(e.lockWaitMs??20*6e4,n);let a=false;try{const l=await loadChromium(e.projectRoot??process.cwd());const c=await l.launch({headless:true,args:["--enable-gpu"]});try{for(const[u,d]of e.cases.entries()){let h;try{h=await c.newContext({viewport:{width:i.width,height:i.height},deviceScaleFactor:i.deviceScaleFactor});const p=await h.newPage();p.on("pageerror",y=>r.pageErrors.push({caseName:d.name,error:String(y?.message??y).slice(0,200)}));await p.goto(caseUrl(e.url,d,e.path),{waitUntil:"load",timeout:6e4});if(e.readySelector)await p.waitForSelector(e.readySelector,{state:"attached",timeout:6e4});const m=Date.now();for(const y of t){const g=m+y-Date.now();if(g>0)await p.waitForTimeout(g);const f=Date.now()-m;const w=(0,import_path.join)(s,`${String(u+1).padStart(2,"0")}-${safe(d.name)}-${y}ms.png`);try{if(e.clipSelector){const v=await p.$(e.clipSelector);if(!v)throw new Error(`clipSelector ${e.clipSelector} not found`);await v.screenshot({path:w})}else await p.screenshot({path:w});r.files.push({caseName:d.name,atMs:y,actualMs:f,file:w})}catch(v){r.files.push({caseName:d.name,atMs:y,actualMs:f,file:null,error:(v?.message??String(v)).split("\n")[0]})}}n(`${d.name}: ${t.length} shot(s)`)}catch(p){for(const m of t)r.files.push({caseName:d.name,atMs:m,file:null,error:(p?.message??String(p)).split("\n")[0]})}finally{if(h&&!await closeWithin(h,1e4))a=true}}if(e.cases.length>1){let u;try{u=await c.newContext({viewport:{width:400,height:300}});const d=await u.newPage();const h=e.cases[0].name;for(const f of t){const w=r.files.find(b=>b.caseName===h&&b.atMs===f&&b.file)?.file;if(!w)continue;const v=(0,import_fs.readFileSync)(w).toString("base64");for(const b of r.files.filter(
|
|
1340
|
+
})`}});async function runWebShots(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.outDir)throw new Error("Pass outDir, the folder to save screenshots in.");const t=[...new Set((e.atMs?.length?e.atMs:[500,2e3]).map(l=>Math.max(0,Math.round(l))))].sort((l,c)=>l-c);const s=(0,import_path.join)(e.outDir,new Date().toISOString().replace(/[:.]/g,"-"));(0,import_fs.mkdirSync)(s,{recursive:true});const i={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const r={dir:"",files:[],pageErrors:[],diffs:[],outliers:[]};r.dir=s;const o=await waitForHostLock(e.lockWaitMs??20*6e4,n);let a=false;try{const l=await loadChromium(e.projectRoot??process.cwd());const c=await l.launch({headless:true,args:["--enable-gpu"]});try{for(const[u,d]of e.cases.entries()){let h;try{h=await c.newContext({viewport:{width:i.width,height:i.height},deviceScaleFactor:i.deviceScaleFactor});const p=await h.newPage();p.on("pageerror",y=>r.pageErrors.push({caseName:d.name,error:String(y?.message??y).slice(0,200)}));await p.goto(caseUrl(e.url,d,e.path),{waitUntil:"load",timeout:6e4});if(e.readySelector)await p.waitForSelector(e.readySelector,{state:"attached",timeout:6e4});const m=Date.now();for(const y of t){const g=m+y-Date.now();if(g>0)await p.waitForTimeout(g);const f=Date.now()-m;const w=(0,import_path.join)(s,`${String(u+1).padStart(2,"0")}-${safe(d.name)}-${y}ms.png`);try{if(e.clipSelector){const v=await p.$(e.clipSelector);if(!v)throw new Error(`clipSelector ${e.clipSelector} not found`);await v.screenshot({path:w})}else await p.screenshot({path:w});r.files.push({caseName:d.name,atMs:y,actualMs:f,file:w})}catch(v){r.files.push({caseName:d.name,atMs:y,actualMs:f,file:null,error:(v?.message??String(v)).split("\n")[0]})}}n(`${d.name}: ${t.length} shot(s)`)}catch(p){for(const m of t)r.files.push({caseName:d.name,atMs:m,file:null,error:(p?.message??String(p)).split("\n")[0]})}finally{if(h&&!await closeWithin(h,1e4))a=true}}if(e.cases.length>1){let u;try{u=await c.newContext({viewport:{width:400,height:300}});const d=await u.newPage();const h=e.cases[0].name;for(const f of t){const w=r.files.find(b=>b.caseName===h&&b.atMs===f&&b.file)?.file;if(!w)continue;const v=(0,import_fs.readFileSync)(w).toString("base64");for(const b of r.files.filter(_=>_.caseName!==h&&_.atMs===f&&_.file)){const _=await d.evaluate(diffJs(v,(0,import_fs.readFileSync)(b.file).toString("base64")));if(_)r.diffs.push({caseName:b.caseName,atMs:f,..._})}}const p=new Map;for(const f of r.diffs)p.set(f.caseName,[...p.get(f.caseName)??[],f.changedPct]);const m=[...p].map(([f,w])=>[f,w.reduce((v,b)=>v+b,0)/w.length]);const y=m.map(([,f])=>f).sort((f,w)=>f-w);const g=y.length?y[Math.floor(y.length/2)]:0;r.outliers=m.filter(([,f])=>f>Math.max(1,g*2)).map(([f])=>f)}catch{}finally{if(u)await closeWithin(u,1e4)}}}finally{if(!await closeWithin(c,1e4))a=true}}finally{o(a)}return r}var import_fs,import_path,safe,diffJs;var init_webShots=__esm({"src/webBench/webShots.ts"(){"use strict";import_fs=require("fs");import_path=require("path");init_webBench();safe=e=>e.replace(/[^\w.-]+/g,"_").slice(0,80);diffJs=(e,n)=>`(async () => {
|
|
1341
1341
|
const load = (s) => new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = "data:image/png;base64," + s; });
|
|
1342
1342
|
const [ia, ib] = await Promise.all([load(${JSON.stringify(e)}), load(${JSON.stringify(n)})]);
|
|
1343
1343
|
const w = ia.naturalWidth, h = ia.naturalHeight;
|
|
@@ -1407,10 +1407,10 @@ ${JSON.stringify(u)}
|
|
|
1407
1407
|
${s.join(" \xB7 ")} \u2014 read with get_storage`:""}async function section(e,n){try{const t=await n();return t?`${e}
|
|
1408
1408
|
${t}`:null}catch(t){const s=errMsg8(t);if(/not responding|timed out|got no answer/i.test(s)){return`${e}
|
|
1409
1409
|
(no answer \u2014 see the note above)`}return`${e}
|
|
1410
|
-
(unavailable: ${cap2(s,120)})`}}function registerTriageTools(e,n){e.registerTool("get_triage",{description:"Summarize available runtime observations for a connected app: captured console errors, recent network requests, configured storage and screen information. Use the returned sections and errors to choose follow-up tools. Missing data or an unresponsive app does not prove a crash; crash output may be unavailable. Free apps get storage key names only; values need Buoy Pro.",inputSchema:{deviceId:import_zod35.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const s=n.requireProDevice(t.deviceId);const i=n.connectedDevices().find(y=>y.deviceId===s);const r=new Set((i?.tools??[]).map(y=>y.toolId));const o=`**${i?.deviceName??s}** (${i?.platform??"?"}, id \`${s}\`) \u2014 tools: ${[...r].join(", ")||"none"}`;const a={timeoutMs:PROBE_TIMEOUT_MS2};const[l,c,u,d]=await Promise.all([section("**Console \u2014 recent errors/warnings:**",async()=>{if(!r.has("console"))return null;const{payload:y}=await n.getSnapshot("console",s,a);const g=y;const f=Array.isArray(g)?g:g?.entries??[];const w=f.filter(b=>b.level==="error"||b.level==="warning");const v=w.slice(-8);if(v.length===0){return`(none \u2014 ${f.length} entries captured)`}return v.map(b=>`${hhmmss3(b.timestamp)} [${b.level==="error"?"ERR":"warn"}] ${cap2((b.message??"").split("\n").map(
|
|
1411
|
-
_Values need Buoy Pro (get_storage includeValues:true)._`}let f=[];try{f=await n.callAction("storage","async.multiGet",{keys:g},s,a)??[]}catch{return`${g.map(
|
|
1412
|
-
_Values: get_storage includeValues:true_`}const w=new Map(f);let v=STORAGE_VALUE_BUDGET;const b=g.map(
|
|
1413
|
-
\u2026+${
|
|
1410
|
+
(unavailable: ${cap2(s,120)})`}}function registerTriageTools(e,n){e.registerTool("get_triage",{description:"Summarize available runtime observations for a connected app: captured console errors, recent network requests, configured storage and screen information. Use the returned sections and errors to choose follow-up tools. Missing data or an unresponsive app does not prove a crash; crash output may be unavailable. Free apps get storage key names only; values need Buoy Pro.",inputSchema:{deviceId:import_zod35.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const s=n.requireProDevice(t.deviceId);const i=n.connectedDevices().find(y=>y.deviceId===s);const r=new Set((i?.tools??[]).map(y=>y.toolId));const o=`**${i?.deviceName??s}** (${i?.platform??"?"}, id \`${s}\`) \u2014 tools: ${[...r].join(", ")||"none"}`;const a={timeoutMs:PROBE_TIMEOUT_MS2};const[l,c,u,d]=await Promise.all([section("**Console \u2014 recent errors/warnings:**",async()=>{if(!r.has("console"))return null;const{payload:y}=await n.getSnapshot("console",s,a);const g=y;const f=Array.isArray(g)?g:g?.entries??[];const w=f.filter(b=>b.level==="error"||b.level==="warning");const v=w.slice(-8);if(v.length===0){return`(none \u2014 ${f.length} entries captured)`}return v.map(b=>`${hhmmss3(b.timestamp)} [${b.level==="error"?"ERR":"warn"}] ${cap2((b.message??"").split("\n").map(_=>_.trim()).filter(Boolean).slice(0,3).join(" \u23CE "),220)}`).join("\n")}),section("**Network \u2014 recent requests:**",async()=>{if(!r.has("network"))return null;const{payload:y}=await n.getSnapshot("network",s,a);const g=Array.isArray(y)?y:y?.events??[];if(g.length===0)return"(no requests captured yet)";const f=g.slice(-8);return f.map(w=>{const v=w.error||(w.status??0)>=400?"\u274C":"\u2705";const b=w.path??w.url??"?";const _=w.error??w.status??"pending";return`${v} ${w.method??"GET"} ${cap2(String(b),80)} \u2192 ${_}${w.duration?` (${Math.round(w.duration)}ms)`:""}`}).join("\n")}),section("**Storage \u2014 persisted values:**",async()=>{if(!r.has("storage"))return null;const y=await n.callAction("storage","async.getAllKeys",{},s,a);if(!Array.isArray(y)||y.length===0){return"(no app keys)"}const g=y.slice(0,MAX_STORAGE_KEYS);if(!hasProAccess(i)){const k=y.length-g.length;return`${g.map(S=>`\`${S}\``).join(", ")}${k>0?` (+${k} more)`:""}
|
|
1411
|
+
_Values need Buoy Pro (get_storage includeValues:true)._`}let f=[];try{f=await n.callAction("storage","async.multiGet",{keys:g},s,a)??[]}catch{return`${g.map(k=>`\`${k}\``).join(", ")}
|
|
1412
|
+
_Values: get_storage includeValues:true_`}const w=new Map(f);let v=STORAGE_VALUE_BUDGET;const b=g.map(k=>{const S=w.get(k);if(S==null)return`- \`${k}\` = (null)`;if(S.length>MAX_STORAGE_VALUE){return`- \`${k}\` = (${(S.length/1024).toFixed(1)} KB \u2014 read with get_storage)`}if(v<=0)return`- \`${k}\` = (not shown \u2014 triage budget spent; read with get_storage)`;v-=S.length;return`- \`${k}\` = ${S}`});const _=y.length-g.length;const $=await otherBackends(n,s,a);return`${b.join("\n")}${_>0?`
|
|
1413
|
+
\u2026+${_} more keys`:""}${$}`}),section("**Screen \u2014 what's visible now:**",async()=>{if(!r.has("highlight-updates"))return null;const y=await n.callAction("highlight-updates","describeScreen",{},s,a);const g=(y?.elements??[]).filter(f=>f.interactive);if(g.length===0)return"(nothing interactive on screen)";return g.slice(0,20).map(f=>`\u2022 ${f.name}${f.text||f.label?` "${cap2(f.text??f.label??"",60)}"`:""}${f.testID?` [testID=${f.testID}]`:""}`).join("\n")})]);const h=n.isUnresponsive(s);const p=h?`
|
|
1414
1414
|
|
|
1415
1415
|
\u{1F6D1} ${n.unresponsiveNote(s)}`:"";const m=[l,c,u,d].filter(Boolean).join("\n\n");return text9(`${o}${p}
|
|
1416
1416
|
|
|
@@ -1420,7 +1420,7 @@ _Drill in with get_console, get_network_requests (includeBodies:true for respons
|
|
|
1420
1420
|
|
|
1421
1421
|
---
|
|
1422
1422
|
\u26A0\uFE0F **${e.length} source file${e.length===1?"":"s"} changed since anything was last checked on the device** (${n}${t}).
|
|
1423
|
-
For a behavior change, repeat the affected interaction and check the expected result. Use \`measure_renders\` with the same steps and \`compareToPrevious: true\` for render comparisons, or \`run_flow\` for supported behavior checks. Report what was checked and any remaining limits. This reminder tracks edits and tool calls; clearing it does not prove the change is correct.`}};editWatcher=new EditWatcher}});function hasProProof(e){return e.listDevices().some(n=>hasProAccess(n))||entitlementHasPro()}function createMcpServer(e){const n=new import_mcp.McpServer({name:"buoy",version:VERSION},{capabilities:{tools:{}},instructions:SERVER_INSTRUCTIONS});editWatcher.start(process.cwd(),resolveMode(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((s,i,r)=>t(s,i,(async(...o)=>runWithAccess(accessFor(s,i?.inputSchema?o[0]:{}),async()=>{const a=o[0];if(s==="list_devices"){}else if(a?.deviceId)await e.awaitBroker();else await e.settleDeviceChoice();const l=currentAccess();if(l.level==="pro"&&!hasProProof(e)){const f=Date.now()+NOT_READY_WAIT_MS;while(!hasProProof(e)&&Date.now()<f&&e.connectedDevices().some(w=>e.accessState(w.deviceId,"pro")==="pending")){await new Promise(w=>setTimeout(w,250))}if(!hasProProof(e)){const w=e.connectedDevices().find(
|
|
1423
|
+
For a behavior change, repeat the affected interaction and check the expected result. Use \`measure_renders\` with the same steps and \`compareToPrevious: true\` for render comparisons, or \`run_flow\` for supported behavior checks. Report what was checked and any remaining limits. This reminder tracks edits and tool calls; clearing it does not prove the change is correct.`}};editWatcher=new EditWatcher}});function hasProProof(e){return e.listDevices().some(n=>hasProAccess(n))||entitlementHasPro()}function createMcpServer(e){const n=new import_mcp.McpServer({name:"buoy",version:VERSION},{capabilities:{tools:{}},instructions:SERVER_INSTRUCTIONS});editWatcher.start(process.cwd(),resolveMode(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((s,i,r)=>t(s,i,(async(...o)=>runWithAccess(accessFor(s,i?.inputSchema?o[0]:{}),async()=>{const a=o[0];if(s==="list_devices"){}else if(a?.deviceId)await e.awaitBroker();else await e.settleDeviceChoice();const l=currentAccess();if(l.level==="pro"&&!hasProProof(e)){const f=Date.now()+NOT_READY_WAIT_MS;while(!hasProProof(e)&&Date.now()<f&&e.connectedDevices().some(w=>e.accessState(w.deviceId,"pro")==="pending")){await new Promise(w=>setTimeout(w,250))}if(!hasProProof(e)){const w=e.connectedDevices().find(_=>e.accessState(_.deviceId,"pro")==="pending");if(w){return{isError:true,content:[{type:"text",text:`${NOT_READY_MARK} account access for "${w.deviceId}" is still being confirmed (the app just connected). Try again in a moment.`}]}}if(!e.isAdmitted()){return{isError:true,content:[{type:"text",text:"A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP."}]}}const v=e.connectedDevices();const b=v.length?` The connected app${v.length>1?"s are":" is"} on ${[...new Set(v.map(_=>_.tier))].join(" / ")}.`:" No app on Buoy Pro has connected in this session.";return{isError:true,content:[{type:"text",text:proRequiredMessage(l,b)}]}}}noteAgentCall(e,s,a);const c=async()=>{const f=await r(...o);for(const w of f?.content??[]){if(w.type==="text"&&typeof w.text==="string")w.text=scrubSecrets(w.text)}return f};let u=await c();const d=f=>{const w=f;return w?.isError===true&&(w.content?.[0]?.text??"").startsWith(NOT_READY_MARK)};if(d(u)){const f=u.content[0].text;const w=/"([^"]+)"/.exec(f)?.[1];const v=Date.now()+NOT_READY_WAIT_MS;while(Date.now()<v){await new Promise(b=>setTimeout(b,250));if(w?e.accessState(w)==="ready":e.connectedDevices().some(b=>meetsAccess(b,currentAccess().level)))break}u=await c()}const h=u?.isError===true;let p=null;if(VERIFYING_TOOLS.has(s)&&!h)editWatcher.markVerified();else p=editWatcher.nudge();const m=optimizeNote();const y=[p,m].filter(f=>!!f);if(!y.length)return u;const g=u;if(Array.isArray(g?.content)){return{...g,content:[...g.content,...y.map(f=>({type:"text",text:f}))]}}return u}))));registerTriageTools(n,e);registerFlowTools(n,e);registerRenderTools(n,e);registerGenericTools(n,e);registerPerfMonitorTools(n,e);registerEventsTools(n,e);registerNetworkTools(n,e);registerReactQueryTools(n,e);registerZustandTools(n,e);registerJotaiTools(n,e);registerReduxTools(n,e);registerStorageTools(n,e);registerRoutesTools(n,e);registerConsoleTools(n,e);registerSentryTools(n,e);registerTimeMachineTools(n,e);registerClockTools(n,e);registerPermissionsTools(n,e);registerLifecycleTools(n,e);registerLocationTools(n,e);registerScenariosTools(n,e);registerScreenshotTools(n,e);registerCameraTools(n);registerSimFarmTools(n,e);registerWebBenchTools(n,e);registerInteractTools(n,e);registerNativeTools(n,e);registerRecordingTools(n,e);registerDeviceTools(n,e);registerSourceTools(n,e);registerJsThreadTools(n,e);registerImagesTools(n,e);registerAssetsTools(n,e);registerThreeTools(n,e);registerReloadTools(n,e);return n}var import_mcp,import_zod37,NOT_READY_WAIT_MS,SERVER_INSTRUCTIONS,VERIFYING_TOOLS,deviceIdField;var init_server2=__esm({"src/server.ts"(){"use strict";import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");import_zod37=require("zod");init_SyncClient();init_generic();init_perfMonitor();init_events();init_network();init_reactQuery();init_zustand();init_jotai();init_redux();init_storage();init_routes();init_console();init_sentry();init_timeMachine();init_clock();init_permissions();init_lifecycle();init_location();init_scenarios();init_screenshot();init_camera2();init_simFarm();init_webBench2();init_interact();init_native();init_recording();init_device();init_source();init_secrets();init_jsThread();init_images();init_three();init_assets();init_reload();init_triage();init_flow();init_renders();init_session();init_editWatcher();init_version();init_policy();init_camera();NOT_READY_WAIT_MS=6e3;SERVER_INSTRUCTIONS=`Buoy MCP connects your editor to supported Buoy tools through the configured broker.
|
|
1424
1424
|
|
|
1425
1425
|
Start with account and connection setup. Use \`list_devices\` to select an app and inspect
|
|
1426
1426
|
its available tools. An app on the free tier can use the screen, touch, flow, screenshot,
|
|
@@ -1575,10 +1575,10 @@ only source review or a build was performed.
|
|
|
1575
1575
|
reminders, \`always\` repeats until checked, and \`never\` disables them.
|
|
1576
1576
|
A reminder is not a test result. Choose checks appropriate to the change;
|
|
1577
1577
|
prose-only edits do not require running device actions.
|
|
1578
|
-
`}});var HELPER_JS;var init_helperScript_generated=__esm({"src/web/helperScript.generated.ts"(){"use strict";HELPER_JS='// Buoy Web helper. Chrome starts it for the Buoy Web extension (native messaging) and\n// ends it when Chrome closes. It passes messages between the extension and Buoy MCP\n// sessions, which connect over a Unix socket only this user can open.\n//\n// extension <-- stdin/stdout (native messaging) --> helper <-- ~/.buoy/web/chrome-<pid>.sock --> MCP sessions\nimport fs from "node:fs";\nimport net from "node:net";\nimport os from "node:os";\nimport path from "node:path";\n\nconst dir = process.env.BUOY_WEB_DIR ?? path.join(os.homedir(), ".buoy", "web");\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nfs.chmodSync(dir, 0o700);\nconst sock = path.join(dir, `chrome-${process.pid}.sock`);\nconst info = path.join(dir, `chrome-${process.pid}.json`);\n\n// ---- native messaging: 4-byte little-endian length, then JSON -----------------\nlet inbuf = Buffer.alloc(0);\nprocess.stdin.on("data", (chunk) => {\n inbuf = Buffer.concat([inbuf, chunk]);\n while (inbuf.length >= 4) {\n const n = inbuf.readUInt32LE(0);\n if (inbuf.length < 4 + n) break;\n const msg = JSON.parse(inbuf.subarray(4, 4 + n).toString("utf8"));\n inbuf = inbuf.subarray(4 + n);\n fromExtension(msg);\n }\n});\nprocess.stdin.on("end", quit); // Chrome closed or the extension let go\nprocess.stdout.on("error", quit);\nfunction toExtension(msg) {\n const body = Buffer.from(JSON.stringify(msg), "utf8");\n if (body.length > 1024 * 1024) throw new Error("Message too big for Chrome (over 1 MB).");\n const head = Buffer.alloc(4);\n head.writeUInt32LE(body.length, 0);\n process.stdout.write(Buffer.concat([head, body]));\n}\n\n// ---- MCP sessions: one JSON message per line ------------------------------------\nconst clients = new Map(); // id -> socket\nlet nextClient = 1;\nfunction fromExtension(msg) {\n const s = clients.get(msg.c);\n if (!s) return;\n const { c, ...rest } = msg;\n s.write(JSON.stringify(rest) + "\\n");\n}\n\nconst server = net.createServer((s) => {\n const c = nextClient++;\n clients.set(c, s);\n let buf = "";\n s.setEncoding("utf8");\n s.on("data", (d) => {\n buf += d;\n let i;\n while ((i = buf.indexOf("\\n")) >= 0) {\n const line = buf.slice(0, i);\n buf = buf.slice(i + 1);\n if (!line) continue;\n let msg;\n try {\n msg = JSON.parse(line);\n toExtension({ ...msg, c });\n } catch (e) {\n if (msg?.id != null) s.write(JSON.stringify({ id: msg.id, error: String(e.message ?? e) }) + "\\n");\n }\n }\n });\n const gone = () => {\n if (!clients.delete(c)) return;\n try {\n toExtension({ c, method: "client.closed" });\n } catch {}\n };\n s.on("close", gone);\n s.on("error", gone);\n});\n\n// Clear out sockets left by helpers that died without cleaning up.\nfor (const f of fs.readdirSync(dir)) {\n const m = /^chrome-(\\d+)\\.(sock|json)$/.exec(f);\n if (!m || Number(m[1]) === process.pid) continue;\n try {\n process.kill(Number(m[1]), 0);\n } catch {\n fs.rmSync(path.join(dir, f), { force: true });\n }\n}\n\nconst old = process.umask(0o177); // the socket file is created 0600\nserver.listen(sock, () => {\n process.umask(old);\n fs.writeFileSync(info, JSON.stringify({ pid: process.pid, sock, started: Date.now() }), { mode: 0o600 });\n // Tells Buoy the extension is set up, so a closed Chrome isn\'t mistaken for "no extension".\n fs.writeFileSync(path.join(dir, "seen"), new Date().toISOString(), { mode: 0o600 });\n toExtension({ ready: true }); // the extension shows "connected" only after this\n});\n\nfunction quit() {\n for (const f of [sock, info]) fs.rmSync(f, { force: true });\n process.exit(0);\n}\nprocess.on("SIGTERM", quit);\nprocess.on("SIGINT", quit);\n'}});function findHelpers(e=webDir()){let n=[];try{n=import_node_fs29.default.readdirSync(e)}catch{return[]}const t=[];for(const s of n){const i=/^chrome-(\d+)\.json$/.exec(s);if(!i)continue;try{process.kill(Number(i[1]),0);const r=JSON.parse(import_node_fs29.default.readFileSync(import_node_path31.default.join(e,s),"utf8"));if(import_node_fs29.default.existsSync(r.sock))t.push(r)}catch{}}return t.sort((s,i)=>i.started-s.started)}function connectHelper(e){return new Promise((n,t)=>{const s=import_node_net3.default.createConnection(e);const i=new Map;let r=1;let o="";let a=false;let l=false;const c={onEvent:()=>{},onClose:()=>{},call(u,d){if(l)return Promise.reject(new Error("Chrome's Buoy helper closed."));const h=r++;s.write(JSON.stringify({id:h,method:u,params:d})+"\n");return new Promise((p,m)=>i.set(h,{res:p,rej:m}))},close:()=>s.end()};s.setEncoding("utf8");s.on("connect",()=>{a=true;n(c)});s.on("error",u=>{if(!a)t(u)});s.on("data",u=>{o+=u;let d;while((d=o.indexOf("\n"))>=0){const h=JSON.parse(o.slice(0,d));o=o.slice(d+1);if(h.id!=null&&i.has(h.id)){const{res:p,rej:m}=i.get(h.id);i.delete(h.id);if(h.error)m(new Error(h.error));else p(h.result)}else if(h.event)c.onEvent(h.event,h.params)}});s.on("close",()=>{l=true;for(const{rej:u}of i.values())u(new Error("Chrome's Buoy helper closed."));i.clear();c.onClose()})})}async function startRelay({sock:e}){const n=await connectHelper(e);const t=await n.call("hello");const s=new Map;let i=1;let r=false;let o=null;const a=new Set;const l=f=>{if(o?.readyState===1)o.send(JSON.stringify(f))};const c=f=>[...s.values()].find(f);const u={url:"",hello:t,onDetach:()=>{},onClose:()=>{},idle:()=>Promise.all([...a]),close:()=>{}};async function d(f,{settle:w=false}={}){const v=s.get(f);if(v)return v;await n.call("debugger.attach",{tabId:f});if(w){const E=Date.now();while(Date.now()-E<5e3){const A=await n.call("debugger.send",{tabId:f,method:"Runtime.evaluate",params:{expression:"[location.href, document.readyState]",returnByValue:true}}).catch(()=>null);const[P,x]=A?.result?.value??[];if(P&&P!=="about:blank"&&x!=="loading")break;await new Promise(T=>setTimeout(T,50))}}const{targetInfo:b}=await n.call("debugger.send",{tabId:f,method:"Target.getTargetInfo"});const{openerId:
|
|
1578
|
+
`}});var HELPER_JS;var init_helperScript_generated=__esm({"src/web/helperScript.generated.ts"(){"use strict";HELPER_JS='// Buoy Web helper. Chrome starts it for the Buoy Web extension (native messaging) and\n// ends it when Chrome closes. It passes messages between the extension and Buoy MCP\n// sessions, which connect over a Unix socket only this user can open.\n//\n// extension <-- stdin/stdout (native messaging) --> helper <-- ~/.buoy/web/chrome-<pid>.sock --> MCP sessions\nimport fs from "node:fs";\nimport net from "node:net";\nimport os from "node:os";\nimport path from "node:path";\n\nconst dir = process.env.BUOY_WEB_DIR ?? path.join(os.homedir(), ".buoy", "web");\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nfs.chmodSync(dir, 0o700);\nconst sock = path.join(dir, `chrome-${process.pid}.sock`);\nconst info = path.join(dir, `chrome-${process.pid}.json`);\n\n// ---- native messaging: 4-byte little-endian length, then JSON -----------------\nlet inbuf = Buffer.alloc(0);\nprocess.stdin.on("data", (chunk) => {\n inbuf = Buffer.concat([inbuf, chunk]);\n while (inbuf.length >= 4) {\n const n = inbuf.readUInt32LE(0);\n if (inbuf.length < 4 + n) break;\n const msg = JSON.parse(inbuf.subarray(4, 4 + n).toString("utf8"));\n inbuf = inbuf.subarray(4 + n);\n fromExtension(msg);\n }\n});\nprocess.stdin.on("end", quit); // Chrome closed or the extension let go\nprocess.stdout.on("error", quit);\nfunction toExtension(msg) {\n const body = Buffer.from(JSON.stringify(msg), "utf8");\n if (body.length > 1024 * 1024) throw new Error("Message too big for Chrome (over 1 MB).");\n const head = Buffer.alloc(4);\n head.writeUInt32LE(body.length, 0);\n process.stdout.write(Buffer.concat([head, body]));\n}\n\n// ---- MCP sessions: one JSON message per line ------------------------------------\nconst clients = new Map(); // id -> socket\nlet nextClient = 1;\nfunction fromExtension(msg) {\n const s = clients.get(msg.c);\n if (!s) return;\n const { c, ...rest } = msg;\n s.write(JSON.stringify(rest) + "\\n");\n}\n\nconst server = net.createServer((s) => {\n const c = nextClient++;\n clients.set(c, s);\n let buf = "";\n s.setEncoding("utf8");\n s.on("data", (d) => {\n buf += d;\n let i;\n while ((i = buf.indexOf("\\n")) >= 0) {\n const line = buf.slice(0, i);\n buf = buf.slice(i + 1);\n if (!line) continue;\n let msg;\n try {\n msg = JSON.parse(line);\n toExtension({ ...msg, c });\n } catch (e) {\n if (msg?.id != null) s.write(JSON.stringify({ id: msg.id, error: String(e.message ?? e) }) + "\\n");\n }\n }\n });\n const gone = () => {\n if (!clients.delete(c)) return;\n try {\n toExtension({ c, method: "client.closed" });\n } catch {}\n };\n s.on("close", gone);\n s.on("error", gone);\n});\n\n// Clear out sockets left by helpers that died without cleaning up.\nfor (const f of fs.readdirSync(dir)) {\n const m = /^chrome-(\\d+)\\.(sock|json)$/.exec(f);\n if (!m || Number(m[1]) === process.pid) continue;\n try {\n process.kill(Number(m[1]), 0);\n } catch {\n fs.rmSync(path.join(dir, f), { force: true });\n }\n}\n\nconst old = process.umask(0o177); // the socket file is created 0600\nserver.listen(sock, () => {\n process.umask(old);\n fs.writeFileSync(info, JSON.stringify({ pid: process.pid, sock, started: Date.now() }), { mode: 0o600 });\n // Tells Buoy the extension is set up, so a closed Chrome isn\'t mistaken for "no extension".\n fs.writeFileSync(path.join(dir, "seen"), new Date().toISOString(), { mode: 0o600 });\n toExtension({ ready: true }); // the extension shows "connected" only after this\n});\n\nfunction quit() {\n for (const f of [sock, info]) fs.rmSync(f, { force: true });\n process.exit(0);\n}\nprocess.on("SIGTERM", quit);\nprocess.on("SIGINT", quit);\n'}});function findHelpers(e=webDir()){let n=[];try{n=import_node_fs29.default.readdirSync(e)}catch{return[]}const t=[];for(const s of n){const i=/^chrome-(\d+)\.json$/.exec(s);if(!i)continue;try{process.kill(Number(i[1]),0);const r=JSON.parse(import_node_fs29.default.readFileSync(import_node_path31.default.join(e,s),"utf8"));if(import_node_fs29.default.existsSync(r.sock))t.push(r)}catch{}}return t.sort((s,i)=>i.started-s.started)}function connectHelper(e){return new Promise((n,t)=>{const s=import_node_net3.default.createConnection(e);const i=new Map;let r=1;let o="";let a=false;let l=false;const c={onEvent:()=>{},onClose:()=>{},call(u,d){if(l)return Promise.reject(new Error("Chrome's Buoy helper closed."));const h=r++;s.write(JSON.stringify({id:h,method:u,params:d})+"\n");return new Promise((p,m)=>i.set(h,{res:p,rej:m}))},close:()=>s.end()};s.setEncoding("utf8");s.on("connect",()=>{a=true;n(c)});s.on("error",u=>{if(!a)t(u)});s.on("data",u=>{o+=u;let d;while((d=o.indexOf("\n"))>=0){const h=JSON.parse(o.slice(0,d));o=o.slice(d+1);if(h.id!=null&&i.has(h.id)){const{res:p,rej:m}=i.get(h.id);i.delete(h.id);if(h.error)m(new Error(h.error));else p(h.result)}else if(h.event)c.onEvent(h.event,h.params)}});s.on("close",()=>{l=true;for(const{rej:u}of i.values())u(new Error("Chrome's Buoy helper closed."));i.clear();c.onClose()})})}async function startRelay({sock:e}){const n=await connectHelper(e);const t=await n.call("hello");const s=new Map;let i=1;let r=false;let o=null;const a=new Set;const l=f=>{if(o?.readyState===1)o.send(JSON.stringify(f))};const c=f=>[...s.values()].find(f);const u={url:"",hello:t,onDetach:()=>{},onClose:()=>{},idle:()=>Promise.all([...a]),close:()=>{}};async function d(f,{settle:w=false}={}){const v=s.get(f);if(v)return v;await n.call("debugger.attach",{tabId:f});if(w){const E=Date.now();while(Date.now()-E<5e3){const A=await n.call("debugger.send",{tabId:f,method:"Runtime.evaluate",params:{expression:"[location.href, document.readyState]",returnByValue:true}}).catch(()=>null);const[P,x]=A?.result?.value??[];if(P&&P!=="about:blank"&&x!=="loading")break;await new Promise(T=>setTimeout(T,50))}}const{targetInfo:b}=await n.call("debugger.send",{tabId:f,method:"Target.getTargetInfo"});const{openerId:_,openerFrameId:$,...k}=b;k.canAccessOpener=false;const S={tabId:f,sessionId:`buoy-tab-${i++}`,targetInfo:k,children:new Set,framesSent:false,held:[]};s.set(f,S);l({method:"Target.attachedToTarget",params:{sessionId:S.sessionId,targetInfo:{...k,attached:true},waitingForDebugger:false}});return S}function h(f){const w=s.get(f);if(!w)return;s.delete(f);l({method:"Target.detachedFromTarget",params:{sessionId:w.sessionId,targetId:w.targetInfo?.targetId}})}n.onEvent=(f,w)=>{if(f==="debugger.event"){const v=s.get(w.tabId);if(!v)return;const b=w.params?.sessionId;if(w.method==="Target.attachedToTarget"&&b)v.children.add(b);if(w.method==="Target.detachedFromTarget"&&b)v.children.delete(b);const _={sessionId:w.sessionId||v.sessionId,method:w.method,params:w.params};if(!v.framesSent)v.held.push(_);else l(_)}else if(f==="debugger.detached"){h(w.tabId);u.onDetach(w)}else if(f==="tabs.created"){if(r){const v=d(w.id,{settle:true}).catch(()=>{});a.add(v);void v.finally(()=>a.delete(v))}}else if(f==="tabs.removed")h(w.tabId)};n.onClose=()=>{for(const f of[...s.keys()])h(f);o?.close();u.onClose()};async function p(f,w,v){if(!v){switch(f){case"Browser.getVersion":return{protocolVersion:"1.3",product:"Chrome/BuoyWeb",userAgent:t.userAgent,jsVersion:""};case"Browser.setDownloadBehavior":case"Target.setDiscoverTargets":case"Browser.close":return{};case"Target.setAutoAttach":r=true;return{};case"Target.getTargetInfo":return{targetInfo:{targetId:"buoy-browser",type:"browser",title:"",url:"",attached:true,canAccessOpener:false}};case"Target.getBrowserContexts":return{browserContextIds:[]};case"Target.createTarget":{const k=await n.call("tabs.create",{url:w?.url});const S=await d(k.id);return{targetId:S.targetInfo.targetId}}case"Target.closeTarget":{const k=c(S=>S.targetInfo?.targetId===w?.targetId);if(!k)return{success:false};await n.call("tabs.remove",{tabId:k.tabId});return{success:true}}}const $=s.values().next().value;if(!$)throw new Error(`No Buoy tab to run ${f} on.`);return n.call("debugger.send",{tabId:$.tabId,method:f,params:w})}if(f==="Target.getTargetInfo"){const $=c(k=>k.sessionId===v);if($)return{targetInfo:$.targetInfo}}let b=c($=>$.sessionId===v);let _;if(!b){b=c($=>$.children.has(v));_=v}if(!b)throw new Error(`No Buoy tab for session ${v}.`);return n.call("debugger.send",{tabId:b.tabId,sessionId:_,method:f,params:w})}const m=import_node_crypto16.default.randomUUID();const y=import_node_http2.default.createServer((f,w)=>w.writeHead(404).end());const g=new import_ws3.WebSocketServer({server:y});g.on("connection",(f,w)=>{if(w.url!==`/${m}`||o)return f.close(4004,"No.");o=f;f.on("message",async v=>{const{id:b,sessionId:_,method:$,params:k}=JSON.parse(v.toString());try{const S=await p($,k,_);log("ok ",_??"-",$);f.send(JSON.stringify({id:b,sessionId:_,result:S??{}}));if($==="Page.getFrameTree"){const E=c(A=>A.sessionId===_);if(E&&!E.framesSent){E.framesSent=true;for(const A of E.held.splice(0))l(A)}}}catch(S){log("ERR",_??"-",$,S.message);f.send(JSON.stringify({id:b,sessionId:_,error:{message:String(S.message??S)}}))}});f.on("close",()=>{o=null;n.close()})});await new Promise(f=>y.listen(0,"127.0.0.1",f));u.url=`ws://127.0.0.1:${y.address().port}/${m}`;u.close=()=>{n.close();g.close();y.close()};return u}var import_node_crypto16,import_node_fs29,import_node_http2,import_node_net3,import_node_os21,import_node_path31,import_ws3,LOG,log,webDir;var init_relay=__esm({"src/web/relay.ts"(){"use strict";import_node_crypto16=__toESM(require("crypto"));import_node_fs29=__toESM(require("fs"));import_node_http2=__toESM(require("http"));import_node_net3=__toESM(require("net"));import_node_os21=__toESM(require("os"));import_node_path31=__toESM(require("path"));import_ws3=require("ws");LOG=process.env.BUOY_WEB_RELAYLOG==="1";log=(...e)=>LOG&&process.stderr.write(`[buoy-web relay] ${e.join(" ")}
|
|
1579
1579
|
`);webDir=()=>process.env.BUOY_WEB_DIR??import_node_path31.default.join(import_node_os21.default.homedir(),".buoy","web")}});function helperManifestDirs(){if(process.env.BUOY_WEB_NMH_DIRS)return process.env.BUOY_WEB_NMH_DIRS.split(import_node_path32.default.delimiter).filter(Boolean);if(process.platform!=="darwin")return[];const e=import_node_path32.default.join(import_node_os22.default.homedir(),"Library","Application Support");return MAC_BROWSERS.map(n=>import_node_path32.default.join(e,n)).filter(n=>import_node_fs30.default.existsSync(n)).map(n=>import_node_path32.default.join(n,"NativeMessagingHosts"))}function ensureHelper(){try{const e=import_node_path32.default.join(webDir(),"helper");import_node_fs30.default.mkdirSync(e,{recursive:true,mode:448});const n=import_node_path32.default.join(e,"helper.mjs");const t=import_node_path32.default.join(e,"helper.sh");writeIfChanged(n,HELPER_JS,384);const s=a=>`'${a.replace(/'/g,`'\\''`)}'`;const i=process.env.BUOY_WEB_DIR?`BUOY_WEB_DIR=${s(process.env.BUOY_WEB_DIR)} `:"";writeIfChanged(t,`#!/bin/sh
|
|
1580
1580
|
${i}exec ${s(process.execPath)} ${s(n)} "$@"
|
|
1581
|
-
`,448);const r=JSON.stringify({name:HOST_NAME,description:"Buoy Web helper: lets the Buoy MCP use Buoy's own tabs through the Buoy Web extension.",path:t,type:"stdio",allowed_origins:EXTENSION_IDS.map(a=>`chrome-extension://${a}/`)},null,2);const o=[];for(const a of helperManifestDirs()){try{import_node_fs30.default.mkdirSync(a,{recursive:true});writeIfChanged(import_node_path32.default.join(a,`${HOST_NAME}.json`),r+"\n",420);o.push(a)}catch{}}return o}catch{return[]}}function extensionInstalled(){if(process.env.BUOY_WEB_NMH_DIRS)return false;if(process.platform!=="darwin")return false;const e=import_node_path32.default.join(import_node_os22.default.homedir(),"Library","Application Support");for(const n of MAC_BROWSERS){let t=[];try{t=import_node_fs30.default.readdirSync(import_node_path32.default.join(e,n))}catch{continue}for(const s of t){if(s!=="Default"&&!/^Profile \d+$/.test(s))continue;if(EXTENSION_IDS.some(i=>import_node_fs30.default.existsSync(import_node_path32.default.join(e,n,s,"Extensions",i))))return true}}return false}var import_node_fs30,import_node_os22,import_node_path32,HOST_NAME,EXTENSION_IDS,MAC_BROWSERS,writeIfChanged;var init_install=__esm({"src/web/install.ts"(){"use strict";import_node_fs30=__toESM(require("fs"));import_node_os22=__toESM(require("os"));import_node_path32=__toESM(require("path"));init_helperScript_generated();init_relay();HOST_NAME="gg.buoy.web";EXTENSION_IDS=["cfolfiajhlenfifknbomdogooagnlamf","kjbgbjimhjiocnfifnemmenkondjeifp",...(process.env.BUOY_WEB_EXTENSION_IDS??"").split(",").filter(Boolean)];MAC_BROWSERS=["Google/Chrome","Google/Chrome Beta","Google/Chrome Dev","Google/Chrome Canary","Chromium","BraveSoftware/Brave-Browser","Microsoft Edge","Vivaldi","Arc/User Data"];writeIfChanged=(e,n,t)=>{try{if(import_node_fs30.default.readFileSync(e,"utf8")===n)return}catch{}import_node_fs30.default.writeFileSync(e,n,{mode:t});import_node_fs30.default.chmodSync(e,t)}}});function createWebCore(e){const n=e.pageScript;const t=(...M)=>e.trace?.(M.join(" "));let s=null;let i=null;let r=null;let o=null;let a=null;let l=false;let c=null;let u=new WeakSet;let d=[];let h=[];let p=null;let m=[];let y=[];const g=()=>i?.pages().filter(M=>u.has(M)&&!M.isClosed())??[];const f=()=>!r||r.url()==="about:blank";function w(M){s=i=r=null;o?.close?.();o=null;a=null;u=new WeakSet;y=[];p=null;d=[];x=new WeakMap;c??=M??null}async function v(){o=await e.connect(B=>{c=B});s=o.browser;i=o.context;a=o.mode;if(o.ownsAll)for(const B of i.pages())u.add(B);const M=s;const D=i;const R=()=>{if(i===D)w("Chrome closed or restarted, so Buoy reconnected in a new tab. Open the link again.")};M?.on("disconnected",R);D.on("close",R);$(D)}async function b(){if(r&&!r.isClosed())return r;if(c){const M=c;c=null;if(/Cancel/.test(M)){w();c=null;throw new Error(M)}}if(!i)await v();l=true;try{r=g()[0]??await i.newPage()}finally{l=false}u.add(r);return r}const
|
|
1581
|
+
`,448);const r=JSON.stringify({name:HOST_NAME,description:"Buoy Web helper: lets the Buoy MCP use Buoy's own tabs through the Buoy Web extension.",path:t,type:"stdio",allowed_origins:EXTENSION_IDS.map(a=>`chrome-extension://${a}/`)},null,2);const o=[];for(const a of helperManifestDirs()){try{import_node_fs30.default.mkdirSync(a,{recursive:true});writeIfChanged(import_node_path32.default.join(a,`${HOST_NAME}.json`),r+"\n",420);o.push(a)}catch{}}return o}catch{return[]}}function extensionInstalled(){if(process.env.BUOY_WEB_NMH_DIRS)return false;if(process.platform!=="darwin")return false;const e=import_node_path32.default.join(import_node_os22.default.homedir(),"Library","Application Support");for(const n of MAC_BROWSERS){let t=[];try{t=import_node_fs30.default.readdirSync(import_node_path32.default.join(e,n))}catch{continue}for(const s of t){if(s!=="Default"&&!/^Profile \d+$/.test(s))continue;if(EXTENSION_IDS.some(i=>import_node_fs30.default.existsSync(import_node_path32.default.join(e,n,s,"Extensions",i))))return true}}return false}var import_node_fs30,import_node_os22,import_node_path32,HOST_NAME,EXTENSION_IDS,MAC_BROWSERS,writeIfChanged;var init_install=__esm({"src/web/install.ts"(){"use strict";import_node_fs30=__toESM(require("fs"));import_node_os22=__toESM(require("os"));import_node_path32=__toESM(require("path"));init_helperScript_generated();init_relay();HOST_NAME="gg.buoy.web";EXTENSION_IDS=["cfolfiajhlenfifknbomdogooagnlamf","kjbgbjimhjiocnfifnemmenkondjeifp",...(process.env.BUOY_WEB_EXTENSION_IDS??"").split(",").filter(Boolean)];MAC_BROWSERS=["Google/Chrome","Google/Chrome Beta","Google/Chrome Dev","Google/Chrome Canary","Chromium","BraveSoftware/Brave-Browser","Microsoft Edge","Vivaldi","Arc/User Data"];writeIfChanged=(e,n,t)=>{try{if(import_node_fs30.default.readFileSync(e,"utf8")===n)return}catch{}import_node_fs30.default.writeFileSync(e,n,{mode:t});import_node_fs30.default.chmodSync(e,t)}}});function createWebCore(e){const n=e.pageScript;const t=(...M)=>e.trace?.(M.join(" "));let s=null;let i=null;let r=null;let o=null;let a=null;let l=false;let c=null;let u=new WeakSet;let d=[];let h=[];let p=null;let m=[];let y=[];const g=()=>i?.pages().filter(M=>u.has(M)&&!M.isClosed())??[];const f=()=>!r||r.url()==="about:blank";function w(M){s=i=r=null;o?.close?.();o=null;a=null;u=new WeakSet;y=[];p=null;d=[];x=new WeakMap;c??=M??null}async function v(){o=await e.connect(B=>{c=B});s=o.browser;i=o.context;a=o.mode;if(o.ownsAll)for(const B of i.pages())u.add(B);const M=s;const D=i;const R=()=>{if(i===D)w("Chrome closed or restarted, so Buoy reconnected in a new tab. Open the link again.")};M?.on("disconnected",R);D.on("close",R);$(D)}async function b(){if(r&&!r.isClosed())return r;if(c){const M=c;c=null;if(/Cancel/.test(M)){w();c=null;throw new Error(M)}}if(!i)await v();l=true;try{r=g()[0]??await i.newPage()}finally{l=false}u.add(r);return r}const _=new WeakSet;function $(M){if(_.has(M))return;_.add(M);for(const D of M.pages())D.on("dialog",R=>u.has(D)&&k(R));M.on("page",D=>{D.on("dialog",R=>u.has(D)&&k(R));if(l)return;if(a==="extension"){u.add(D);d=[...d,D];return}h.push(D.opener().catch(()=>null).then(R=>{if(!R||!u.has(R))return;u.add(D);d=[...d,D]}))})}const k=M=>{p=M;m.splice(0).forEach(D=>D())};async function S(M){let D;const R=new Promise(z=>m.push(D=z));const B=M();try{await Promise.race([B,R])}finally{m=m.filter(z=>z!==D);B.catch(()=>{})}}async function E(M=r){t("helper: check");const D=await M.evaluate(()=>!!globalThis.__buoyWeb).catch(()=>false);if(!D){await M.evaluate(n);await M.evaluate(R=>Object.assign(globalThis.__buoyWeb.cfg,R),{dedup:true,jump:true})}}async function A(M,...D){const R=await b();if(p)throw new Error(`A browser dialog is open: "${p.message()}". Answer it with dialog first.`);await E(R);return R.evaluate(({fn:B,args:z})=>globalThis.__buoyWeb[B](...z),{fn:M,args:D})}const P=M=>{try{return new URL(M).origin}catch{return""}};let x=new WeakMap;let T=1;const I=M=>{let D=x.get(M);if(!D)x.set(M,D=T++);return D};function O(){if(!r||typeof r.mainFrame!=="function")return[];const M=r.mainFrame();return r.frames().filter(D=>{if(D===M||!/^https?:/.test(D.url()))return false;const R=D.parentFrame?.();return!!R&&P(D.url())!==P(R.url())})}async function N(M,D,...R){const B=await M.evaluate(()=>!!globalThis.__buoyWeb).catch(()=>false);if(!B){await M.evaluate(n);await M.evaluate(z=>Object.assign(globalThis.__buoyWeb.cfg,z),{dedup:true,jump:true})}return M.evaluate(({fn:z,args:G})=>globalThis.__buoyWeb[z](...G),{fn:D,args:R})}async function F(M){if(M==null)return{f:null,t:M};const D=String(M).trim();const R=/^f(\d+)(e\d+)$/.exec(D);const B=O();if(R){const z=B.find(G=>I(G)===Number(R[1]));if(!z)throw new Error(`${M} is no longer on the page (its frame closed or changed). Read again.`);return{f:z,t:R[2]}}if(/^e\d+$/.test(D))return{f:null,t:M};if(!B.length||await A("find",M))return{f:null,t:M};for(const z of B)if(await N(z,"find",M).catch(()=>false))return{f:z,t:M};return{f:null,t:M}}async function X(M){try{const G=await M.frameElement();await G.scrollIntoViewIfNeeded().catch(()=>{});const Y=await G.boundingBox();const ee=await G.evaluate(K=>{const Z=K.ownerDocument.defaultView.getComputedStyle(K);return[K.clientLeft+(parseFloat(Z.paddingLeft)||0),K.clientTop+(parseFloat(Z.paddingTop)||0)]});if(Y)return{x:Y.x+ee[0],y:Y.y+ee[1]}}catch{}const D=M.parentFrame?.();const R=D&&D!==r.mainFrame()?O().find(G=>G===D)??null:null;const B=R?await N(R,"frameRect",M.url()):await A("frameRect",M.url());if(!B)throw new Error(`Couldn't find where the frame from ${P(M.url())} sits on the page.`);const z=R?await X(R):{x:0,y:0};return{x:B.x+z.x,y:B.y+z.y}}async function C(M,D,...R){const{f:B,t:z}=await F(D);if(!B)return A(M,z,...R);const G=await N(B,M,z,...R);if(G&&typeof G==="object"&&typeof G.x==="number"){const Y=await X(B);G.x+=Y.x;G.y+=Y.y}return G}async function U(){if(await A("submitBlocked",null))return true;for(const M of O())if(await N(M,"submitBlocked",null).catch(()=>false))return true;return false}const j=()=>p?Promise.resolve():Promise.race([r.evaluate(([M,D])=>globalThis.__buoyWeb?.settle(M,D),SETTLE),sleep4(SETTLE[1]+200)]).catch(()=>{});async function H(){const M=g().length;return`Tab: ${await r.title().catch(()=>"")} (${r.url()})${M>1?`, ${M} tabs open`:""}
|
|
1582
1582
|
`}const V=M=>M.map(D=>D.replace(/^(f\d+)?e\d+ /,""));async function L(){const M=(await A("outline",{})).split("\n");for(const D of O()){const R=String(await N(D,"outline",{}).catch(()=>"")).split("\n").filter(Boolean);if(R.length)M.push(`In a frame from ${P(D.url())}:`,...R.map(B=>B.replace(/^e(\d+) /,`f${I(D)}e$1 `)))}return{full:M,plain:V(M)}}const W=M=>Promise.race([M.waitForLoadState("domcontentloaded"),M.evaluate(()=>{const D=globalThis;return new Promise(R=>D.document.readyState!=="loading"?R():D.addEventListener("DOMContentLoaded",()=>R(),{once:true}))}),sleep4(5e3)]).catch(()=>{});async function Q(){if(p)return`A browser dialog is open: "${p.message()}". Use dialog to answer it.`;const{full:M,plain:D}=await L();const R=K=>K.reduce((Z,oe)=>Z.set(oe,(Z.get(oe)??0)+1),new Map);const B=K=>Z=>(K.get(Z)??0)>0?(K.set(Z,(K.get(Z)??0)-1),true):false;const z=B(R(y));const G=B(R(D));const Y=M.filter((K,Z)=>!z(D[Z]));const ee=[];if(Y.length>12){ee.push(`The page changed a lot. It now shows:
|
|
1583
1583
|
${M.join("\n")}`)}else{const K=y.filter(Z=>!G(Z)).slice(0,6);if(Y.length)ee.push("New on page:",...Y.map(Z=>`+ ${Z}`));if(K.length)ee.push("Gone:",...K.map(Z=>`- ${Z}`))}y=D;await Promise.all(h.splice(0));await o?.idle?.();if(d.length){const K=g();for(const Z of d){await W(Z);let oe="";try{await E(Z);oe=(await Z.evaluate(()=>globalThis.__buoyWeb.outline({textOnly:true}))).split("\n").slice(0,15).join("\n")}catch{}ee.push(`A new tab opened (tab ${K.indexOf(Z)}, ${Z.url()}). You are still on tab ${K.indexOf(r)}. It shows:
|
|
1584
1584
|
${oe}
|
|
@@ -1597,8 +1597,8 @@ ${R.join("\n")}`)}return text11(D.map((R,B)=>`${B}${R===r?" (current)":""} ${R.u
|
|
|
1597
1597
|
${BLANK}`:""))},"read");re("select","Choose an option in any dropdown: a native select, or a custom list or menu (it opens the list, finds the option by name, scrolling if needed, and clicks it). One call.",{target:ce,option:import_zod38.z.string()},async({target:M,option:D})=>{const R=await C("tagOf",M);if(R!=="SELECT"){const z=await te(M);await S(()=>r.mouse.click(z.x,z.y));const G=await r.evaluate(({o:Y})=>globalThis.__buoyWeb.waitFor(Y,"appears",3e3,true),{o:D});if(!G?.ok)throw new Error(`Opened ${z.label}, but no option "${D}" showed.`);await S(()=>r.mouse.click(G.x,G.y));await j();return text11(`Chose "${D}" in ${z.label}.
|
|
1598
1598
|
${await Q()}`)}const B=await te(M);await r.mouse.click(B.x,B.y);await r.keyboard.press("Escape");await ne();await r.keyboard.type(D,{delay:10});await j();return text11(`Chose "${D}" in ${B.label}.
|
|
1599
1599
|
${await Q()}`)});re("dialog","Answer an open browser dialog (alert, confirm or prompt).",{accept:import_zod38.z.boolean(),text:import_zod38.z.string().optional()},async({accept:M,text:D})=>{if(!p)return text11("No dialog is open.");const R=p;p=null;await(M?R.accept(D):R.dismiss());await j();return text11(`Dialog ${M?"accepted":"dismissed"}.
|
|
1600
|
-
${await Q()}`)});re("screenshot",'A picture of the visible page (only when you need to see pixels). Its pixels match the page, so click, hover or drag can aim at a spot with target "x,y".',{},async()=>{await b();const M=await r.screenshot({type:"jpeg",quality:60,scale:"css"});return{content:[{type:"image",data:M.toString("base64"),mimeType:"image/jpeg"}]}},"read");for(const M of e.extraTools?.({page:()=>r,pointAt:te,frame:j,changes:Q,target:ce})??[])se.push(M);return{tools:se,labelOf:async M=>{try{const D=await C("locate",M);return D?.ok?D.label:null}catch{return null}},currentUrl:()=>r&&!r.isClosed()?r.url():null,async close(){const M=o;o?.close?.();if(M?.mode==="window")await i?.close().catch(()=>{})}}}var import_zod38,sleep4,text11,fail13,BLANK,NOSECRET,EMPTY_SECRET,SETTLE;var init_core=__esm({"src/web/core.ts"(){"use strict";import_zod38=require("zod");sleep4=e=>new Promise(n=>setTimeout(n,e));text11=e=>({content:[{type:"text",text:e}]});fail13=e=>({content:[{type:"text",text:`Error: ${e?.message??e}`}],isError:true});BLANK="This is Buoy's own new tab, and it's blank. Buoy works only in its own tabs and can't see the person's other tabs. Use open with the link.";NOSECRET="That's a password, code or card field. Buoy never types those. Stop here and don't submit the form: ask the person to type it into Buoy's tab themselves, and carry on only after they say it's done.";EMPTY_SECRET="That would send the form with its password, code or card field still empty. Stop here: ask the person to type it into Buoy's tab, then try again after they say it's done.";SETTLE=[100,500]}});var PAGE_JS;var init_pageScript_generated=__esm({"src/web/pageScript.generated.ts"(){"use strict";PAGE_JS='// Buoy Web page helper (installed into the page by the MCP server).\n// outline(): visible text and controls, each control with a short ref.\n// locate(target): a control\'s on-screen center by ref or visible name.\n(() => {\n if (globalThis.__buoyWeb) return;\n const cfg = {}; // switches from the server (Buoy Optimize)\n // A new id every time the helper is installed (a new page), so old refs are never reused.\n const pageId = Math.random().toString(36).slice(2, 10);\n const refs = new Map(); // ref -> element\n const refNames = new Map(); // ref -> the name it had when the agent saw it\n const ids = new WeakMap(); // element -> ref\n // Refs start at a new range on each page load in a tab (e1\u2026, then e2000\u2026, e3000\u2026), so a ref\n // read before a reload never matches something on the new page.\n let next = 1;\n try {\n const n = (Number(sessionStorage.getItem("__buoyRefBase")) || 0) + 1;\n sessionStorage.setItem("__buoyRefBase", String(n));\n if (n > 1) next = n * 1000;\n } catch {\n next = (1 + Math.floor(Math.random() * 900)) * 1000;\n }\n const refOf = (el) => {\n let r = ids.get(el);\n if (!r) {\n r = `e${next++}`;\n ids.set(el, r);\n }\n refs.set(r, el);\n return r;\n };\n const CONTROL = "a[href],button,input,select,textarea,summary,[role=button],[role=link],[role=tab],[role=menuitem],[role=option],[role=checkbox],[role=radio],[role=switch],[role=slider],[role=listitem][tabindex],[role=textbox],[contenteditable=\'\'],[contenteditable=true],[tabindex]:not([tabindex=\'-1\'])";\n // Elements a page made clickable or hoverable in script (el.onclick = ..., onmouseenter\n // on a chart bar), with no control role: calendar cells, chart bars, map pins.\n const ACTIVE = ["onclick", "onmousedown", "onpointerdown", "onmouseenter", "onmouseover", "onpointerenter", "ondblclick"];\n const active = (el) => el !== document.body && el !== document.documentElement && ACTIVE.some((k) => typeof el[k] === "function");\n let rankCache = new WeakMap(); // parent -> where each child sits among same-tag siblings\n const isCtl = (el) => el.matches(CONTROL) || (el.tagName === "LI" && !!el.closest("[role=listbox]")) || active(el);\n const hidden = (el) => {\n for (let n = el; n && n.nodeType === 1; n = n.parentElement || n.getRootNode()?.host) {\n if (n.hidden || n.getAttribute?.("aria-hidden") === "true") return true;\n const s = (n.ownerDocument?.defaultView ?? window).getComputedStyle(n);\n if (s.display === "none" || s.visibility === "hidden") return true;\n }\n return false;\n };\n // Match names loosely: case, spaces and number commas don\'t matter ("Row 9,040" = "row 9040").\n const norm = (x) => String(x).toLowerCase().replace(/(\\d),(?=\\d{3}\\b)/g, "$1").replace(/\\s+/g, " ").trim();\n const nameOf = (el) => {\n const a = el.getAttribute("aria-label");\n if (a) return a.trim();\n if (el.labels?.length)\n return [...el.labels].map((l) => {\n // A label that wraps its field would also pull in the field\'s own text (a select\'s options).\n const c = l.cloneNode(true);\n c.querySelectorAll("select,option,datalist,textarea,input").forEach((n) => n.remove());\n return c.textContent.replace(/\\s+/g, " ").trim();\n }).join(" ");\n if (el.tagName === "INPUT" && el.type === "button") return el.value;\n const t = (el.innerText ?? el.textContent ?? "").replace(/\\s+/g, " ").trim();\n const n = t || el.getAttribute("title") || el.getAttribute("placeholder") || el.getAttribute("name") || "";\n if (n || !active(el)) return n;\n // A shape with no name (a chart bar): "rect 3 of 7", by its place among its siblings.\n // Ranks are worked out once per parent, so a chart with thousands of shapes stays fast.\n const par = el.parentNode;\n let ranks = rankCache.get(par);\n if (!ranks) {\n ranks = new Map();\n const count = {};\n for (const x of par?.children ?? []) ranks.set(x, (count[x.tagName] = (count[x.tagName] ?? 0) + 1));\n ranks.total = count;\n rankCache.set(par, ranks);\n }\n return `${el.tagName.toLowerCase()} ${ranks.get(el)} of ${ranks.total[el.tagName]}`;\n };\n const roleOf = (el) => {\n const r = el.getAttribute("role");\n if (r) return r;\n const t = el.tagName.toLowerCase();\n if (t === "a") return "link";\n if (t === "input") return { checkbox: "checkbox", radio: "radio", range: "slider", button: "button", submit: "button", file: "file", date: "date", time: "time", "datetime-local": "date-time", month: "month", number: "number" }[el.type] ?? "textbox";\n if (t === "select") return "select";\n if (t === "textarea") return "textbox";\n if (t === "button") return "button";\n if (active(el) && !el.matches(CONTROL)) return ACTIVE.slice(0, 3).some((k) => typeof el[k] === "function") || typeof el.ondblclick === "function" ? "clickable" : "hover-area";\n return "control";\n };\n // A rich text box\'s text as plain lines (" / " between them), plus its formatting listed\n // apart, so typed asterisks never look like bold.\n function richText(root) {\n const lines = [];\n let line = "";\n const flush = () => { if (line.trim()) lines.push(line.trim()); line = ""; };\n const go = (n) => {\n if (n.nodeType === 3) { line += n.textContent.replace(/\\s+/g, " "); return; }\n if (n.nodeType !== 1) return;\n if (n.tagName === "BR") return flush();\n const block = /^(DIV|P|LI|UL|OL|H\\d|BLOCKQUOTE)$/.test(n.tagName);\n if (block) flush();\n for (const c of n.childNodes) go(c);\n if (block) flush();\n };\n for (const c of root.childNodes) go(c);\n flush();\n const q = (els) => [...new Set([...els].map((e) => e.innerText.replace(/\\s+/g, " ").trim()).filter(Boolean))].map((t) => `"${t.slice(0, 40).replace(/"/g, "\'")}"`).join(", ");\n const parts = [`text="${lines.join(" / ").replace(/"/g, "\'").slice(0, 300)}"`];\n const bold = q(root.querySelectorAll("b,strong")), it = q(root.querySelectorAll("i,em"));\n if (bold) parts.push(`bold: ${bold}`);\n if (it) parts.push(`italic: ${it}`);\n const lis = root.querySelectorAll("li").length;\n if (lis) parts.push(`list items: ${lis}`);\n return parts.join(" ");\n }\n // Walk the page in order, into open shadow roots and same-origin iframes.\n function walk(root, offset, out) {\n const nodes = root.querySelectorAll ? root.querySelectorAll("*") : [];\n for (const el of nodes) {\n if (el.shadowRoot) walk(el.shadowRoot, offset, out);\n if (el.tagName === "IFRAME") {\n try {\n const d = el.contentDocument;\n const r = el.getBoundingClientRect();\n if (d) walk(d, { x: offset.x + r.x, y: offset.y + r.y }, out);\n } catch { /* best effort: nothing to do */ }\n }\n out.push({ el, offset });\n }\n return out;\n }\n // Tags that sit inside a sentence. Their text is read as part of it.\n const INLINE = new Set(["B", "STRONG", "I", "EM", "SPAN", "CODE", "SMALL", "SUP", "SUB", "MARK", "ABBR", "CITE", "Q", "U", "S", "TIME", "KBD", "VAR", "DFN", "BDI", "FONT", "BR", "WBR"]);\n // A sentence with links in it, links kept in place: "[The Demi-Virgin](e23) is a play by [Avery Hopwood](e24)".\n // Without this the sentence reads "is a play by ." and the links come after it, out of order.\n // Returns null (read it the old way) if the element holds anything but text, inline tags and links.\n function sentence(el, textOnly) {\n let out = "";\n const parts = [];\n const visit = (node) => {\n for (const n of node.childNodes) {\n if (n.nodeType === 3) { out += n.textContent; continue; }\n if (n.nodeType !== 1 || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(n.tagName)) continue;\n if (n.tagName === "A" && isCtl(n) && !n.querySelector(CONTROL)) {\n if (hidden(n)) { parts.push(n); continue; }\n const text = (n.textContent || "").replace(/\\s+/g, " ").trim();\n const name = (text || nameOf(n)).slice(0, 80);\n out += textOnly ? name : `[${name}](${refOf(n)})`;\n parts.push(n, { link: n, name });\n continue;\n }\n if (n.tagName === "BR") { out += " "; continue; }\n if (!INLINE.has(n.tagName) || isCtl(n) || n.shadowRoot) return false;\n parts.push(n);\n if (visit(n) === false) return false;\n }\n };\n if (visit(el) === false || !parts.some((x) => x.link)) return null;\n for (const x of parts) {\n if (!x.link) { used.add(x); continue; }\n if (!seen.has(x.link)) seen.set(x.link, sig(x.link));\n if (!textOnly) refNames.set(refOf(x.link), x.name);\n }\n return out.replace(/\\s+/g, " ").trim().slice(0, 500);\n }\n let used = new WeakSet(); // inline tags and links already read as part of a sentence\n function outline({ textOnly = false } = {}) {\n rankCache = new WeakMap(); // the page may have changed since the last read\n used = new WeakSet();\n const lines = [];\n let lastText = "";\n for (const { el } of walk(document, { x: 0, y: 0 }, [])) {\n if (el?.nodeType !== 1 || used.has(el) || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "OPTION"].includes(el.tagName)) continue;\n const isControl = isCtl(el);\n if (isControl) {\n if (hidden(el)) continue;\n const name = nameOf(el).slice(0, 80);\n const role = roleOf(el);\n const extra = [];\n if (el.disabled) extra.push("disabled");\n if (["date", "time", "date-time", "month", "number"].includes(role) && el.value) extra.push(`value="${el.value}"`);\n if (role === "textbox" && el.value) extra.push(el.type === "password" ? "filled" : `value="${String(el.value).slice(0, 40)}"`);\n // A rich text box has no value: show its text, with bold, italic and list items marked.\n if (el.isContentEditable && !el.parentElement?.isContentEditable) extra.push(richText(el));\n if (role === "select") {\n // Every option up to 40, so a long list (time zones, countries) never hides the one asked for.\n const opts = [...el.options].map((o) => o.text.trim());\n extra.push(`value="${el.options[el.selectedIndex]?.text ?? ""}"`, `options: ${opts.slice(0, 40).join(" | ")}${opts.length > 40 ? ` | \u2026and ${opts.length - 40} more (select picks any by name)` : ""}`);\n }\n if (role === "checkbox" || role === "radio") extra.push(el.checked ? "checked" : "unchecked");\n if (role === "slider") extra.push(`value=${el.value}`);\n if (el.getAttribute("aria-selected") === "true") extra.push("selected");\n if (!seen.has(el)) seen.set(el, sig(el)); // the first look: "changes" means changes from how it first looked\n if (!textOnly) refNames.set(refOf(el), name);\n lastText = "";\n lines.push(textOnly ? `${role} "${name}"${extra.length ? " " + extra.join(" ") : ""}` : `${refOf(el)} ${role} "${name}"${extra.length ? " " + extra.join(" ") : ""}`);\n continue;\n }\n // Plain text: only an element\'s own direct text. A repeat is dropped only right after itself,\n // since lists repeat lines that matter ("Not available for these dates" under each cabin).\n let own = "";\n for (const n of el.childNodes) if (n.nodeType === 3) own += n.textContent;\n own = own.replace(/\\s+/g, " ").trim();\n if (!own || el.closest?.(CONTROL) || (el.parentElement && active(el.parentElement)) || own === lastText || hidden(el)) continue;\n if (cfg.dedup && el.tagName === "LABEL" && el.control) { lastText = own; continue; } // the field line carries this name\n const line = sentence(el, textOnly) ?? own.slice(0, 200);\n lastText = own;\n lines.push(line);\n }\n return lines.join("\\n");\n }\n // A watched element: click it the moment its look or name changes.\n let watched = null;\n let watchedAt = null;\n // If the page draws the watched element again, follow whatever now sits in its spot.\n const rewatch = () => {\n if (watched?.isConnected || !watchedAt) return;\n let n = document.elementFromPoint(...watchedAt);\n while (n && n.nodeType === 1 && !isCtl(n)) n = n.parentElement;\n watched = n && n.nodeType === 1 ? n : null;\n };\n const seen = new WeakMap(); // element -> its look when the agent last read the page\n const sig = (el) => [nameOf(el), el.getAttribute("class"), el.getAttribute("aria-pressed"), el.getAttribute("aria-checked"), el.disabled].join("|");\n function watch(target) {\n // "Changed" means changed from what the agent last saw, so a change that\n // already happened while it was thinking counts at once.\n watched = find(target);\n if (watched) { const r = watched.getBoundingClientRect(); watchedAt = [r.x + r.width / 2, r.y + r.height / 2]; }\n return watched ? (seen.get(watched) ?? sig(watched)) : null;\n }\n function waitSig(base, changed, ms, point) {\n return until(() => {\n rewatch();\n if (!watched?.isConnected) return false;\n if ((sig(watched) !== base) !== changed) return false;\n if (!point) return true;\n const r = locate("@watched");\n return { ...r, seenAt: Math.round(performance.now()), now: sig(watched), base };\n }, ms);\n }\n function find(target) {\n const t = String(target).trim();\n if (t === "@watched") { rewatch(); return watched?.isConnected ? watched : null; }\n if (/^e\\d+$/.test(t) && refs.has(t)) {\n const el = refs.get(t);\n if (el.isConnected && !hidden(el)) return el;\n refs.delete(t);\n // The page drew it again (a calendar, a list that re-renders): find it by the name it had,\n // but only when exactly one visible control has that name, so a ref never jumps to a twin.\n const old = refNames.get(t);\n if (old) {\n const same = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((e) => e?.nodeType === 1 && isCtl(e) && !hidden(e) && norm(nameOf(e)) === norm(old));\n if (same.length === 1) {\n refs.set(t, same[0]); // later lookups in the same action find it too\n return same[0];\n }\n }\n return null;\n }\n const all = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el?.nodeType === 1 && isCtl(el) && !hidden(el));\n const want = norm(t);\n let ctl;\n if (cfg.rank) {\n // RANK: exact, then starts with, then contains; on screen before off screen.\n const onScreen = (el) => { const r = el.getBoundingClientRect(); return r.bottom > 0 && r.top < innerHeight && r.right > 0 && r.left < innerWidth; };\n const tests = [(n) => n === want, (n) => n.startsWith(want), (n) => n.includes(want)];\n for (const test of tests) {\n const hits = all.filter((el) => test(norm(nameOf(el))));\n if (hits.length) { ctl = hits.find(onScreen) ?? hits[0]; break; }\n }\n } else ctl = all.find((el) => norm(nameOf(el)) === want) ?? all.find((el) => norm(nameOf(el)).includes(want));\n if (ctl) return ctl;\n // Not a control: any visible element whose text matches (bins, cards,\n // menu rows drawn as plain boxes). The smallest match is the most exact.\n const texts = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el?.nodeType === 1 && !["SCRIPT", "STYLE", "HTML", "BODY", "HEAD"].includes(el.tagName));\n const txt = (el) => norm(el.innerText ?? "");\n const area = (el) => { const r = el.getBoundingClientRect(); return r.width * r.height; };\n for (const match of [(el) => txt(el) === want, (el) => txt(el).includes(want) && txt(el).length <= want.length + 16]) {\n const hits = texts.filter((el) => match(el) && area(el) > 0 && !hidden(el));\n if (hits.length) return hits.sort((a, b) => area(a) - area(b))[0];\n }\n return null;\n }\n // From a frame\'s coordinates up to this helper\'s own window. A frame on another site\n // has no frameElement here; Buoy adds that frame\'s offset itself.\n function toTop(w, x, y) {\n for (; w && w !== window; w = w.parent) {\n let fe = null;\n try { fe = w.frameElement; } catch { /* best effort: nothing to do */ }\n if (!fe) break;\n const fr = fe.getBoundingClientRect();\n x += fr.x;\n y += fr.y;\n }\n return { x, y };\n }\n // Where a frame from another site sits on this page (its content box), found by its\n // address, after scrolling it into view. Buoy adds this to spots found inside it.\n function frameRect(url) {\n const same = (a, b) => { try { return new URL(a, location.href).href === new URL(b).href; } catch { return false; } };\n const origin = (a) => { try { return new URL(a, location.href).origin; } catch { return ""; } };\n const frames = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el.tagName === "IFRAME");\n const el = frames.find((f) => same(f.src, url)) ?? frames.find((f) => origin(f.src) === origin(url));\n if (!el) return null;\n el.scrollIntoView({ block: "nearest", inline: "nearest" });\n const r = el.getBoundingClientRect();\n const st = getComputedStyle(el);\n const pad = (k) => parseFloat(st[k]) || 0;\n return toTop(el.ownerDocument.defaultView, r.x + el.clientLeft + pad("paddingLeft"), r.y + el.clientTop + pad("paddingTop"));\n }\n // The element\'s center in the top page\'s viewport, after scrolling it into view.\n function locate(target) {\n const el = find(target);\n if (!el) return { ok: false, error: /^e\\d+$/.test(String(target).trim()) ? `${target} is no longer on the page (it was removed or drawn again). Use its name, or read again.` : `Nothing visible matches "${target}". Read the page to see what is there.` };\n el.scrollIntoView({ block: "center", inline: "center" });\n let r = el.getBoundingClientRect();\n let x = r.x + r.width / 2, y = r.y + r.height / 2;\n ({ x, y } = toTop(el.ownerDocument.defaultView, x, y));\n return { ok: true, x, y, label: isCtl(el) ? `${roleOf(el)} "${nameOf(el).slice(0, 40)}"` : `"${(el.innerText ?? "").replace(/\\s+/g, " ").trim().slice(0, 40)}"` };\n }\n // Wait until check() returns something truthy. Page-change events fire at\n // once even when the window is behind others (paint callbacks get slowed\n // down there), with a short timer as a backstop.\n function until(check, ms) {\n return new Promise((res) => {\n const t0 = performance.now();\n let done = false, timer = 0;\n const mo = new MutationObserver(() => run());\n const finish = (v) => { done = true; mo.disconnect(); clearTimeout(timer); res(v); };\n const run = () => {\n if (done) return;\n const v = check();\n if (v) return finish(v);\n if (performance.now() - t0 > ms) return finish(false);\n };\n const poll = () => { run(); if (!done) timer = setTimeout(poll, 16); };\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n poll();\n });\n }\n const ready = (el, when) => !!el && (when !== "enabled" || (!el.disabled && el.getAttribute("aria-disabled") !== "true"));\n // Resolve the frame a control with this name shows (or turns on).\n function waitFor(target, when, ms, point) {\n return until(() => ready(find(target), when) && (point ? locate(target) : true), ms);\n }\n function waitGone(target, ms) {\n return until(() => !find(target), ms).then((v) => !!v);\n }\n // Scroll the page and every scroll box until the text is drawn and visible.\n async function scrollTo(text, ms) {\n const t0 = performance.now();\n const has = () => find(text); // the same matching as click: controls by name, then short text\n const boxes = () => [document.scrollingElement, ...[...document.querySelectorAll("*")].filter((e) => e.scrollHeight > e.clientHeight + 4 && /(auto|scroll)/.test(getComputedStyle(e).overflowY))];\n // JUMP: in a numbered list ("Row 7,342"), read the numbers on screen and\n // jump near the target, then fine-tune, instead of a screen at a time.\n const num = (x) => Number(String(x).replace(/,/g, ""));\n const want = String(text).match(/^(.*?)([\\d,]+)\\s*$/);\n if (cfg.jump && want && !has()) {\n const prefix = want[1].trim().toLowerCase(), N = num(want[2]);\n for (let k = 0; k < 10 && !has(); k++) {\n let moved = false;\n for (const b of boxes()) {\n const br = b === document.scrollingElement ? { top: 0, bottom: innerHeight } : b.getBoundingClientRect();\n const seen = [];\n for (const el of b.querySelectorAll("*")) {\n if (el.children.length) continue;\n const m = (el.textContent || "").trim().match(/^(.*?)([\\d,]+)\\s*$/);\n if (!m || m[1].trim().toLowerCase() !== prefix) continue;\n const r = el.getBoundingClientRect();\n if (r.bottom < br.top || r.top > br.bottom || !r.height) continue;\n seen.push([num(m[2]), r.top]);\n }\n if (seen.length < 2) continue;\n seen.sort((a, c) => a[0] - c[0]);\n const [n0, y0] = seen[0], [n1, y1] = seen[seen.length - 1];\n if (n1 === n0) continue;\n const pxPer = (y1 - y0) / (n1 - n0);\n const delta = (N - (n0 + n1) / 2) * pxPer;\n if (Math.abs(delta) < 2) continue;\n b.scrollTop += delta;\n moved = true;\n }\n if (!moved) break;\n await new Promise((r) => setTimeout(r, 40));\n }\n }\n while (performance.now() - t0 < ms) {\n const el = has();\n if (el) {\n el.scrollIntoView({ block: "center" });\n return { ok: true };\n }\n let moved = false;\n for (const b of boxes()) {\n if (b.scrollTop + b.clientHeight < b.scrollHeight - 2) {\n b.scrollTop += Math.max(200, b.clientHeight * 0.9);\n moved = true;\n }\n }\n await new Promise((r) => setTimeout(r, moved ? 30 : 250));\n if (!moved && !has()) {\n // Nothing left to scroll here; a feed may still be loading more.\n if (performance.now() - t0 > ms / 2) break;\n }\n }\n return { ok: !!has(), error: `"${text}" did not show after scrolling.` };\n }\n // Resolve once the page has gone quiet (no DOM change for `quiet` ms), or at `max`.\n function settle(quiet, max) {\n return new Promise((res) => {\n const t0 = performance.now();\n let last = t0, timer = 0;\n const mo = new MutationObserver(() => (last = performance.now()));\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n const tick = () => {\n const now = performance.now();\n if (now - last >= quiet || now - t0 >= max) { mo.disconnect(); return res(Math.round(now - t0)); }\n timer = setTimeout(tick, 10);\n };\n tick();\n });\n }\n // For a native date field: parse the text and give back the digits in the\n // order this browser\'s field wants (e.g. MM DD YYYY), plus the field\'s left edge.\n function dateKeys(target, text) {\n const el = find(target);\n if (!el || el.tagName !== "INPUT" || el.type !== "date") return null;\n const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];\n let y, m, d, t = String(text).trim();\n let k;\n if ((k = t.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/))) [, y, m, d] = k;\n else if ((k = t.match(/^(\\d{1,2})[/.-](\\d{1,2})[/.-](\\d{4})$/))) [, m, d, y] = k;\n else if ((k = t.match(/^([A-Za-z]+)\\.? (\\d{1,2}),? (\\d{4})$/))) { m = months.indexOf(k[1].slice(0, 3).toLowerCase()) + 1; d = k[2]; y = k[3]; }\n else if ((k = t.match(/^(\\d{1,2}) ([A-Za-z]+)\\.? (\\d{4})$/))) { d = k[1]; m = months.indexOf(k[2].slice(0, 3).toLowerCase()) + 1; y = k[3]; }\n if (!y || !m || !d || m < 1) return null;\n const parts = new Intl.DateTimeFormat(navigator.language, { year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(2001, 1, 3)).map((p) => p.type).filter((x) => ["year", "month", "day"].includes(x));\n const val = { year: String(y).padStart(4, "0"), month: String(m).padStart(2, "0"), day: String(d).padStart(2, "0") };\n el.scrollIntoView({ block: "center" });\n const r = el.getBoundingClientRect();\n const { x: px, y: py } = toTop(el.ownerDocument.defaultView, r.x + 10, r.y + r.height / 2);\n return { keys: parts.map((p) => val[p]).join(""), x: px, y: py, want: `${val.year}-${val.month}-${val.day}` };\n }\n // A slider\'s value (native range or ARIA slider), or null if the target isn\'t a slider.\n function sliderOf(target) {\n const el = find(target);\n if (!el) return null;\n if (el.tagName === "INPUT" && el.type === "range") return { v: Number(el.value), min: Number(el.min || 0), max: Number(el.max || 100) };\n if (el.getAttribute("role") === "slider") return { v: Number(el.getAttribute("aria-valuenow")), min: Number(el.getAttribute("aria-valuemin") ?? 0), max: Number(el.getAttribute("aria-valuemax") ?? 100) };\n return null;\n }\n // INPUTSCROLL: plan one scroll step toward the text; the server does it with the real wheel.\n function scrollPlan(text) {\n const el = find(text);\n if (el) {\n const r = el.getBoundingClientRect();\n const inView = r.top >= 0 && r.bottom <= innerHeight && r.left >= 0 && r.right <= innerWidth;\n return { found: true, inView };\n }\n // Inner scroll boxes first (deepest last in document order), the page itself last.\n const inner = [...document.querySelectorAll("*")].filter((e) => e.scrollHeight > e.clientHeight + 4 && /(auto|scroll)/.test(getComputedStyle(e).overflowY)).reverse();\n const boxes = [...inner, document.scrollingElement];\n const num = (x) => Number(String(x).replace(/,/g, ""));\n const want = String(text).match(/^(.*?)([\\d,]+)\\s*$/);\n for (const b of boxes) {\n const top = b === document.scrollingElement;\n const br = top ? { top: 0, bottom: innerHeight, left: 0, right: innerWidth } : b.getBoundingClientRect();\n const cx = Math.max(0, Math.min(innerWidth - 1, (br.left + br.right) / 2)), cy = Math.max(0, Math.min(innerHeight - 1, (br.top + br.bottom) / 2));\n if (want && cfg.jump) {\n const prefix = want[1].trim().toLowerCase(), N = num(want[2]);\n const seen = [];\n for (const e of b.querySelectorAll("*")) {\n if (e.children.length) continue;\n const m = (e.textContent || "").trim().match(/^(.*?)([\\d,]+)\\s*$/);\n if (!m || m[1].trim().toLowerCase() !== prefix) continue;\n const r = e.getBoundingClientRect();\n if (r.bottom < br.top || r.top > br.bottom || !r.height) continue;\n seen.push([num(m[2]), r.top]);\n }\n if (seen.length >= 2) {\n seen.sort((a, c) => a[0] - c[0]);\n const [n0, y0] = seen[0], [n1, y1] = seen[seen.length - 1];\n if (n1 !== n0) {\n const dy = (N - (n0 + n1) / 2) * ((y1 - y0) / (n1 - n0));\n if (Math.abs(dy) >= 2) return { x: cx, y: cy, dy: Math.round(dy) };\n }\n }\n }\n if (b.scrollTop + b.clientHeight < b.scrollHeight - 2) return { x: cx, y: cy, dy: Math.round(Math.max(200, b.clientHeight * 0.9)) };\n }\n return { wait: true }; // nothing left to scroll; a feed may still load more\n }\n // Fields we never type into: passwords, one-time codes and card numbers. The person does those.\n // autocomplete can hold several words ("billing cc-number", "section-login current-password").\n const SECRET = /^(current-password|new-password|one-time-code|cc-number|cc-csc|cc-exp|cc-exp-month|cc-exp-year)$/;\n const isSecret = (el) => !!el && el.tagName === "INPUT" && (el.type === "password" || String(el.autocomplete || "").toLowerCase().split(/\\s+/).some((t) => SECRET.test(t)));\n function focused() {\n let a = document.activeElement;\n for (;;) {\n if (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;\n else if (a?.tagName === "IFRAME") {\n try { a = a.contentDocument?.activeElement ?? null; } catch { return a; }\n if (!a) return null;\n } else return a;\n }\n }\n const secret = (t) => (t == null ? isSecret(focused()) : isSecret(find(t)));\n // Would this click (or Enter in the focused field) send a form whose password, code or\n // card field is still empty? Buoy refuses: the person types those first.\n const emptySecretIn = (form) => !!form && [...form.querySelectorAll("input")].some((i) => isSecret(i) && !i.value && !hidden(i));\n const isSubmit = (el) => !!el && ((el.tagName === "BUTTON" && (!el.type || el.type === "submit")) || (el.tagName === "INPUT" && (el.type === "submit" || el.type === "image")));\n // The same check for a click at a spot: what\'s there, and is it a send button for such a form?\n function submitBlockedAt(x, y) {\n let el = document.elementFromPoint(x, y);\n while (el?.tagName === "IFRAME") {\n try {\n const r = el.getBoundingClientRect();\n x -= r.x; y -= r.y;\n el = el.contentDocument.elementFromPoint(x, y);\n } catch { break; }\n }\n while (el?.shadowRoot) { const inner = el.shadowRoot.elementFromPoint(x, y); if (!inner || inner === el) break; el = inner; }\n const btn = el?.closest?.("button, input") ?? el;\n return isSubmit(btn) && emptySecretIn(btn.form ?? btn.closest("form"));\n }\n function submitBlocked(t) {\n if (t == null) {\n const f = focused();\n return !!f && (f.tagName === "INPUT" || isSubmit(f)) && emptySecretIn(f.form ?? f.closest?.("form"));\n }\n const el = find(t);\n const btn = el?.closest?.("button, input") ?? el;\n return isSubmit(btn) && emptySecretIn(btn.form ?? btn.closest("form"));\n }\n // Is the target a rich text box (contenteditable), and is the cursor already in it?\n function editInfo(t) {\n const el = find(t);\n if (!el) return null;\n const box = el.isContentEditable ? el : null;\n if (!box) return { rich: false };\n const f = focused();\n return { rich: true, focused: !!f && (f === box || box.contains(f)) };\n }\n globalThis.__buoyWeb = { pageId, submitBlockedAt, editInfo, frameRect, scrollPlan, secret, submitBlocked, sliderOf, dateKeys, valueOf: (t) => find(t)?.value ?? null, tagOf: (t) => find(t)?.tagName ?? null, cfg, settle, outline, locate, find: (t) => !!find(t), waitFor, waitGone, scrollTo, watch, waitSig };\n})();\n'}});function telemetryOff(){const e=(process.env.BUOY_TELEMETRY??"").toLowerCase();return e==="0"||e==="false"||e==="off"||process.env.DO_NOT_TRACK==="1"}function noteUse(){if(sentThisRun||telemetryOff())return;sentThisRun=true;try{const e=import_node_path33.default.join(webDir(),"install.json");let n={};try{n=JSON.parse(import_node_fs31.default.readFileSync(e,"utf8"))}catch{}if(typeof n.installId!=="string")n={installId:import_node_crypto17.default.randomUUID(),lastPingAt:0};if(Date.now()-(n.lastPingAt??0)<DAY_MS)return;n.lastPingAt=Date.now();import_node_fs31.default.mkdirSync(import_node_path33.default.dirname(e),{recursive:true,mode:448});import_node_fs31.default.writeFileSync(e,JSON.stringify(n),{mode:384});const t=JSON.stringify({installId:n.installId,version:"7.0.69",platform:process.platform==="darwin"?"macos":process.platform==="win32"?"windows":"linux",tier:"locked",surface:"web-mcp"});void fetch(ENDPOINT,{method:"POST",headers:{"content-type":"application/json"},body:t,signal:AbortSignal.timeout(5e3)}).catch(()=>{})}catch{}}var import_node_crypto17,import_node_fs31,import_node_path33,ENDPOINT,DAY_MS,sentThisRun;var init_usage=__esm({"src/web/usage.ts"(){"use strict";import_node_crypto17=__toESM(require("crypto"));import_node_fs31=__toESM(require("fs"));import_node_path33=__toESM(require("path"));init_relay();ENDPOINT=process.env.BUOY_TELEMETRY_URL??"https://buoy.gg/api/t";DAY_MS=24*60*60*1e3;sentThisRun=false}});function cdpEndpoint(){const e=process.env.BUOY_WEB_CDP;if(e!=="auto")return e;const n=import_node_path34.default.join(import_node_os23.default.homedir(),"Library/Application Support/Google/Chrome/DevToolsActivePort");if(!import_node_fs32.default.existsSync(n))throw new Error("Chrome isn't open for tools yet. In Chrome, open chrome://inspect/#remote-debugging and turn it on.");const[t,s]=import_node_fs32.default.readFileSync(n,"utf8").trim().split("\n");return`ws://127.0.0.1:${t}${s}`}async function connectNode(e,n){const{chromium:t}=await e();let s=!process.env.BUOY_WEB_PROFILE?findHelpers()[0]?.sock:void 0;if(!s&&!process.env.BUOY_WEB_PROFILE&&!process.env.BUOY_WEB_CDP&&extensionInstalled()){for(let o=0;o<40&&!s;o++){await sleep5(200);s=findHelpers()[0]?.sock}}if(process.env.BUOY_WEB_CDP){const o=await t.connectOverCDP(cdpEndpoint());return{browser:o,context:o.contexts()[0],mode:"cdp"}}if(s){const o=await startRelay({sock:s});o.onDetach=({reason:l})=>{if(l==="canceled_by_user")n("The person clicked Cancel on Chrome's debugging bar, so Buoy stopped. Ask them before you try again.")};const a=await t.connectOverCDP(o.url);return{browser:a,context:a.contexts()[0],mode:"extension",idle:o.idle,close:o.close}}if(!process.env.BUOY_WEB_PROFILE&&seenExtension()){throw new Error("Chrome is closed, or the Buoy Web extension is turned off. Ask the person to open Chrome, then try again.")}const i=process.env.BUOY_WEB_PROFILE??process.env.BUOY_WEB_WINDOW_DIR??import_node_path34.default.join(import_node_os23.default.homedir(),".buoy","web-profile");const r=await t.launchPersistentContext(i,{headless:false,channel:"chrome",viewport:{width:1280,height:900},args:["--no-first-run","--no-default-browser-check"]});return{browser:r.browser(),context:r,mode:"window",ownsAll:true}}function checkUpload(e){const n=[process.cwd(),...(process.env.BUOY_WEB_UPLOAD_DIRS??"").split(import_node_path34.default.delimiter).filter(Boolean)].map(s=>import_node_fs32.default.realpathSync(s));let t;try{t=import_node_fs32.default.realpathSync(import_node_path34.default.resolve(e))}catch{throw new Error(`No file at ${e}.`)}if(!n.some(s=>t===s||t.startsWith(s+import_node_path34.default.sep)))throw new Error(`Buoy only uploads files inside the project folder (${n[0]}). ${e} is outside it.`);if(SECRET_FILE.test(t))throw new Error(`${import_node_path34.default.basename(t)} looks like a secret (keys, passwords or tokens). Buoy doesn't upload those.`);return t}function createWebServer(e={}){const n=e.loadPlaywright??(()=>import("playwright-core"));const t=Date.now();const s=createWebCore({connect:l=>connectNode(n,l),pageScript:PAGE_JS,isMac:process.platform==="darwin",trace:process.env.BUOY_WEB_TRACE==="1"?l=>process.stderr.write(`[buoy-web ${Date.now()-t}ms] ${l}
|
|
1601
|
-
`):void 0,extraTools:l=>[uploadTool(l)]});const i=new import_mcp2.McpServer({name:"buoy-web",version:"7.0.
|
|
1600
|
+
${await Q()}`)});re("screenshot",'A picture of the visible page (only when you need to see pixels). Its pixels match the page, so click, hover or drag can aim at a spot with target "x,y".',{},async()=>{await b();const M=await r.screenshot({type:"jpeg",quality:60,scale:"css"});return{content:[{type:"image",data:M.toString("base64"),mimeType:"image/jpeg"}]}},"read");for(const M of e.extraTools?.({page:()=>r,pointAt:te,frame:j,changes:Q,target:ce})??[])se.push(M);return{tools:se,labelOf:async M=>{try{const D=await C("locate",M);return D?.ok?D.label:null}catch{return null}},currentUrl:()=>r&&!r.isClosed()?r.url():null,async close(){const M=o;o?.close?.();if(M?.mode==="window")await i?.close().catch(()=>{})}}}var import_zod38,sleep4,text11,fail13,BLANK,NOSECRET,EMPTY_SECRET,SETTLE;var init_core=__esm({"src/web/core.ts"(){"use strict";import_zod38=require("zod");sleep4=e=>new Promise(n=>setTimeout(n,e));text11=e=>({content:[{type:"text",text:e}]});fail13=e=>({content:[{type:"text",text:`Error: ${e?.message??e}`}],isError:true});BLANK="This is Buoy's own new tab, and it's blank. Buoy works only in its own tabs and can't see the person's other tabs. Use open with the link.";NOSECRET="That's a password, code or card field. Buoy never types those. Stop here and don't submit the form: ask the person to type it into Buoy's tab themselves, and carry on only after they say it's done.";EMPTY_SECRET="That would send the form with its password, code or card field still empty. Stop here: ask the person to type it into Buoy's tab, then try again after they say it's done.";SETTLE=[100,500]}});var PAGE_JS;var init_pageScript_generated=__esm({"src/web/pageScript.generated.ts"(){"use strict";PAGE_JS='// Buoy Web page helper (installed into the page by the MCP server).\n// outline(): visible text and controls, each control with a short ref.\n// locate(target): a control\'s on-screen center by ref or visible name.\n(() => {\n if (globalThis.__buoyWeb) return;\n const cfg = {}; // switches from the server (Buoy Optimize)\n // A new id every time the helper is installed (a new page), so old refs are never reused.\n const pageId = Math.random().toString(36).slice(2, 10);\n const refs = new Map(); // ref -> element\n const refNames = new Map(); // ref -> the name it had when the agent saw it\n const ids = new WeakMap(); // element -> ref\n // Refs start at a new range on each page load in a tab (e1\u2026, then e2000\u2026, e3000\u2026), so a ref\n // read before a reload never matches something on the new page.\n let next = 1;\n try {\n const n = (Number(sessionStorage.getItem("__buoyRefBase")) || 0) + 1;\n sessionStorage.setItem("__buoyRefBase", String(n));\n if (n > 1) next = n * 1000;\n } catch {\n next = (1 + Math.floor(Math.random() * 900)) * 1000;\n }\n const refOf = (el) => {\n let r = ids.get(el);\n if (!r) {\n r = `e${next++}`;\n ids.set(el, r);\n }\n refs.set(r, el);\n return r;\n };\n const CONTROL = "a[href],button,input,select,textarea,summary,[role=button],[role=link],[role=tab],[role=menuitem],[role=option],[role=checkbox],[role=radio],[role=switch],[role=slider],[role=listitem][tabindex],[role=textbox],[contenteditable=\'\'],[contenteditable=true],[tabindex]:not([tabindex=\'-1\'])";\n // Elements a page made clickable or hoverable in script (el.onclick = ..., onmouseenter\n // on a chart bar), with no control role: calendar cells, chart bars, map pins.\n const ACTIVE = ["onclick", "onmousedown", "onpointerdown", "onmouseenter", "onmouseover", "onpointerenter", "ondblclick"];\n const active = (el) => el !== document.body && el !== document.documentElement && ACTIVE.some((k) => typeof el[k] === "function");\n let rankCache = new WeakMap(); // parent -> where each child sits among same-tag siblings\n const isCtl = (el) => el.matches(CONTROL) || (el.tagName === "LI" && !!el.closest("[role=listbox]")) || active(el);\n const hidden = (el) => {\n for (let n = el; n && n.nodeType === 1; n = n.parentElement || n.getRootNode()?.host) {\n if (n.hidden || n.getAttribute?.("aria-hidden") === "true") return true;\n const s = (n.ownerDocument?.defaultView ?? window).getComputedStyle(n);\n if (s.display === "none" || s.visibility === "hidden") return true;\n }\n return false;\n };\n // Match names loosely: case, spaces and number commas don\'t matter ("Row 9,040" = "row 9040").\n const norm = (x) => String(x).toLowerCase().replace(/(\\d),(?=\\d{3}\\b)/g, "$1").replace(/\\s+/g, " ").trim();\n const nameOf = (el) => {\n const a = el.getAttribute("aria-label");\n if (a) return a.trim();\n if (el.labels?.length)\n return [...el.labels].map((l) => {\n // A label that wraps its field would also pull in the field\'s own text (a select\'s options).\n const c = l.cloneNode(true);\n c.querySelectorAll("select,option,datalist,textarea,input").forEach((n) => n.remove());\n return c.textContent.replace(/\\s+/g, " ").trim();\n }).join(" ");\n if (el.tagName === "INPUT" && el.type === "button") return el.value;\n const t = (el.innerText ?? el.textContent ?? "").replace(/\\s+/g, " ").trim();\n const n = t || el.getAttribute("title") || el.getAttribute("placeholder") || el.getAttribute("name") || "";\n if (n || !active(el)) return n;\n // A shape with no name (a chart bar): "rect 3 of 7", by its place among its siblings.\n // Ranks are worked out once per parent, so a chart with thousands of shapes stays fast.\n const par = el.parentNode;\n let ranks = rankCache.get(par);\n if (!ranks) {\n ranks = new Map();\n const count = {};\n for (const x of par?.children ?? []) ranks.set(x, (count[x.tagName] = (count[x.tagName] ?? 0) + 1));\n ranks.total = count;\n rankCache.set(par, ranks);\n }\n return `${el.tagName.toLowerCase()} ${ranks.get(el)} of ${ranks.total[el.tagName]}`;\n };\n const roleOf = (el) => {\n const r = el.getAttribute("role");\n if (r) return r;\n const t = el.tagName.toLowerCase();\n if (t === "a") return "link";\n if (t === "input") return { checkbox: "checkbox", radio: "radio", range: "slider", button: "button", submit: "button", file: "file", date: "date", time: "time", "datetime-local": "date-time", month: "month", number: "number" }[el.type] ?? "textbox";\n if (t === "select") return "select";\n if (t === "textarea") return "textbox";\n if (t === "button") return "button";\n if (active(el) && !el.matches(CONTROL)) return ACTIVE.slice(0, 3).some((k) => typeof el[k] === "function") || typeof el.ondblclick === "function" ? "clickable" : "hover-area";\n return "control";\n };\n // A rich text box\'s text as plain lines (" / " between them), plus its formatting listed\n // apart, so typed asterisks never look like bold.\n function richText(root) {\n const lines = [];\n let line = "";\n const flush = () => { if (line.trim()) lines.push(line.trim()); line = ""; };\n const go = (n) => {\n if (n.nodeType === 3) { line += n.textContent.replace(/\\s+/g, " "); return; }\n if (n.nodeType !== 1) return;\n if (n.tagName === "BR") return flush();\n const block = /^(DIV|P|LI|UL|OL|H\\d|BLOCKQUOTE)$/.test(n.tagName);\n if (block) flush();\n for (const c of n.childNodes) go(c);\n if (block) flush();\n };\n for (const c of root.childNodes) go(c);\n flush();\n const q = (els) => [...new Set([...els].map((e) => e.innerText.replace(/\\s+/g, " ").trim()).filter(Boolean))].map((t) => `"${t.slice(0, 40).replace(/"/g, "\'")}"`).join(", ");\n const parts = [`text="${lines.join(" / ").replace(/"/g, "\'").slice(0, 300)}"`];\n const bold = q(root.querySelectorAll("b,strong")), it = q(root.querySelectorAll("i,em"));\n if (bold) parts.push(`bold: ${bold}`);\n if (it) parts.push(`italic: ${it}`);\n const lis = root.querySelectorAll("li").length;\n if (lis) parts.push(`list items: ${lis}`);\n return parts.join(" ");\n }\n // Walk the page in order, into open shadow roots and same-origin iframes.\n function walk(root, offset, out) {\n const nodes = root.querySelectorAll ? root.querySelectorAll("*") : [];\n for (const el of nodes) {\n if (el.shadowRoot) walk(el.shadowRoot, offset, out);\n if (el.tagName === "IFRAME") {\n try {\n const d = el.contentDocument;\n const r = el.getBoundingClientRect();\n if (d) walk(d, { x: offset.x + r.x, y: offset.y + r.y }, out);\n } catch { /* best effort: nothing to do */ }\n }\n out.push({ el, offset });\n }\n return out;\n }\n // Tags that sit inside a sentence. Their text is read as part of it.\n const INLINE = new Set(["B", "STRONG", "I", "EM", "SPAN", "CODE", "SMALL", "SUP", "SUB", "MARK", "ABBR", "CITE", "Q", "U", "S", "TIME", "KBD", "VAR", "DFN", "BDI", "FONT", "BR", "WBR"]);\n // A sentence with links in it, links kept in place: "[The Demi-Virgin](e23) is a play by [Avery Hopwood](e24)".\n // Without this the sentence reads "is a play by ." and the links come after it, out of order.\n // Returns null (read it the old way) if the element holds anything but text, inline tags and links.\n function sentence(el, textOnly) {\n let out = "";\n const parts = [];\n const visit = (node) => {\n for (const n of node.childNodes) {\n if (n.nodeType === 3) { out += n.textContent; continue; }\n if (n.nodeType !== 1 || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(n.tagName)) continue;\n if (n.tagName === "A" && isCtl(n) && !n.querySelector(CONTROL)) {\n if (hidden(n)) { parts.push(n); continue; }\n const text = (n.textContent || "").replace(/\\s+/g, " ").trim();\n const name = (text || nameOf(n)).slice(0, 80);\n out += textOnly ? name : `[${name}](${refOf(n)})`;\n parts.push(n, { link: n, name });\n continue;\n }\n if (n.tagName === "BR") { out += " "; continue; }\n if (!INLINE.has(n.tagName) || isCtl(n) || n.shadowRoot) return false;\n parts.push(n);\n if (visit(n) === false) return false;\n }\n };\n if (visit(el) === false || !parts.some((x) => x.link)) return null;\n for (const x of parts) {\n if (!x.link) { used.add(x); continue; }\n if (!seen.has(x.link)) seen.set(x.link, sig(x.link));\n if (!textOnly) refNames.set(refOf(x.link), x.name);\n }\n return out.replace(/\\s+/g, " ").trim().slice(0, 500);\n }\n let used = new WeakSet(); // inline tags and links already read as part of a sentence\n function outline({ textOnly = false } = {}) {\n rankCache = new WeakMap(); // the page may have changed since the last read\n used = new WeakSet();\n const lines = [];\n let lastText = "";\n for (const { el } of walk(document, { x: 0, y: 0 }, [])) {\n if (el?.nodeType !== 1 || used.has(el) || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "OPTION"].includes(el.tagName)) continue;\n const isControl = isCtl(el);\n if (isControl) {\n if (hidden(el)) continue;\n const name = nameOf(el).slice(0, 80);\n const role = roleOf(el);\n const extra = [];\n if (el.disabled) extra.push("disabled");\n if (["date", "time", "date-time", "month", "number"].includes(role) && el.value) extra.push(`value="${el.value}"`);\n if (role === "textbox" && el.value) extra.push(el.type === "password" ? "filled" : `value="${String(el.value).slice(0, 40)}"`);\n // A rich text box has no value: show its text, with bold, italic and list items marked.\n if (el.isContentEditable && !el.parentElement?.isContentEditable) extra.push(richText(el));\n if (role === "select") {\n // Every option up to 40, so a long list (time zones, countries) never hides the one asked for.\n const opts = [...el.options].map((o) => o.text.trim());\n extra.push(`value="${el.options[el.selectedIndex]?.text ?? ""}"`, `options: ${opts.slice(0, 40).join(" | ")}${opts.length > 40 ? ` | \u2026and ${opts.length - 40} more (select picks any by name)` : ""}`);\n }\n if (role === "checkbox" || role === "radio") extra.push(el.checked ? "checked" : "unchecked");\n if (role === "slider") extra.push(`value=${el.value}`);\n if (el.getAttribute("aria-selected") === "true") extra.push("selected");\n if (!seen.has(el)) seen.set(el, sig(el)); // the first look: "changes" means changes from how it first looked\n if (!textOnly) refNames.set(refOf(el), name);\n lastText = "";\n lines.push(textOnly ? `${role} "${name}"${extra.length ? " " + extra.join(" ") : ""}` : `${refOf(el)} ${role} "${name}"${extra.length ? " " + extra.join(" ") : ""}`);\n continue;\n }\n // Plain text: only an element\'s own direct text. A repeat is dropped only right after itself,\n // since lists repeat lines that matter ("Not available for these dates" under each cabin).\n let own = "";\n for (const n of el.childNodes) if (n.nodeType === 3) own += n.textContent;\n own = own.replace(/\\s+/g, " ").trim();\n if (!own || el.closest?.(CONTROL) || (el.parentElement && active(el.parentElement)) || own === lastText || hidden(el)) continue;\n if (cfg.dedup && el.tagName === "LABEL" && el.control) { lastText = own; continue; } // the field line carries this name\n const line = sentence(el, textOnly) ?? own.slice(0, 200);\n lastText = own;\n lines.push(line);\n }\n return lines.join("\\n");\n }\n // A watched element: click it the moment its look or name changes.\n let watched = null;\n let watchedAt = null;\n // If the page draws the watched element again, follow whatever now sits in its spot.\n const rewatch = () => {\n if (watched?.isConnected || !watchedAt) return;\n let n = document.elementFromPoint(...watchedAt);\n while (n && n.nodeType === 1 && !isCtl(n)) n = n.parentElement;\n watched = n && n.nodeType === 1 ? n : null;\n };\n const seen = new WeakMap(); // element -> its look when the agent last read the page\n const sig = (el) => [nameOf(el), el.getAttribute("class"), el.getAttribute("aria-pressed"), el.getAttribute("aria-checked"), el.disabled].join("|");\n function watch(target) {\n // "Changed" means changed from what the agent last saw, so a change that\n // already happened while it was thinking counts at once.\n watched = find(target);\n if (watched) { const r = watched.getBoundingClientRect(); watchedAt = [r.x + r.width / 2, r.y + r.height / 2]; }\n return watched ? (seen.get(watched) ?? sig(watched)) : null;\n }\n function waitSig(base, changed, ms, point) {\n return until(() => {\n rewatch();\n if (!watched?.isConnected) return false;\n if ((sig(watched) !== base) !== changed) return false;\n if (!point) return true;\n const r = locate("@watched");\n return { ...r, seenAt: Math.round(performance.now()), now: sig(watched), base };\n }, ms);\n }\n function find(target) {\n const t = String(target).trim();\n if (t === "@watched") { rewatch(); return watched?.isConnected ? watched : null; }\n if (/^e\\d+$/.test(t) && refs.has(t)) {\n const el = refs.get(t);\n if (el.isConnected && !hidden(el)) return el;\n refs.delete(t);\n // The page drew it again (a calendar, a list that re-renders): find it by the name it had,\n // but only when exactly one visible control has that name, so a ref never jumps to a twin.\n const old = refNames.get(t);\n if (old) {\n const same = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((e) => e?.nodeType === 1 && isCtl(e) && !hidden(e) && norm(nameOf(e)) === norm(old));\n if (same.length === 1) {\n refs.set(t, same[0]); // later lookups in the same action find it too\n return same[0];\n }\n }\n return null;\n }\n const all = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el?.nodeType === 1 && isCtl(el) && !hidden(el));\n const want = norm(t);\n let ctl;\n if (cfg.rank) {\n // RANK: exact, then starts with, then contains; on screen before off screen.\n const onScreen = (el) => { const r = el.getBoundingClientRect(); return r.bottom > 0 && r.top < innerHeight && r.right > 0 && r.left < innerWidth; };\n const tests = [(n) => n === want, (n) => n.startsWith(want), (n) => n.includes(want)];\n for (const test of tests) {\n const hits = all.filter((el) => test(norm(nameOf(el))));\n if (hits.length) { ctl = hits.find(onScreen) ?? hits[0]; break; }\n }\n } else ctl = all.find((el) => norm(nameOf(el)) === want) ?? all.find((el) => norm(nameOf(el)).includes(want));\n if (ctl) return ctl;\n // Not a control: any visible element whose text matches (bins, cards,\n // menu rows drawn as plain boxes). The smallest match is the most exact.\n const texts = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el?.nodeType === 1 && !["SCRIPT", "STYLE", "HTML", "BODY", "HEAD"].includes(el.tagName));\n const txt = (el) => norm(el.innerText ?? "");\n const area = (el) => { const r = el.getBoundingClientRect(); return r.width * r.height; };\n for (const match of [(el) => txt(el) === want, (el) => txt(el).includes(want) && txt(el).length <= want.length + 16]) {\n const hits = texts.filter((el) => match(el) && area(el) > 0 && !hidden(el));\n if (hits.length) return hits.sort((a, b) => area(a) - area(b))[0];\n }\n return null;\n }\n // From a frame\'s coordinates up to this helper\'s own window. A frame on another site\n // has no frameElement here; Buoy adds that frame\'s offset itself.\n function toTop(w, x, y) {\n for (; w && w !== window; w = w.parent) {\n let fe = null;\n try { fe = w.frameElement; } catch { /* best effort: nothing to do */ }\n if (!fe) break;\n const fr = fe.getBoundingClientRect();\n x += fr.x;\n y += fr.y;\n }\n return { x, y };\n }\n // Where a frame from another site sits on this page (its content box), found by its\n // address, after scrolling it into view. Buoy adds this to spots found inside it.\n function frameRect(url) {\n const same = (a, b) => { try { return new URL(a, location.href).href === new URL(b).href; } catch { return false; } };\n const origin = (a) => { try { return new URL(a, location.href).origin; } catch { return ""; } };\n const frames = walk(document, { x: 0, y: 0 }, []).map((x) => x.el).filter((el) => el.tagName === "IFRAME");\n const el = frames.find((f) => same(f.src, url)) ?? frames.find((f) => origin(f.src) === origin(url));\n if (!el) return null;\n el.scrollIntoView({ block: "nearest", inline: "nearest" });\n const r = el.getBoundingClientRect();\n const st = getComputedStyle(el);\n const pad = (k) => parseFloat(st[k]) || 0;\n return toTop(el.ownerDocument.defaultView, r.x + el.clientLeft + pad("paddingLeft"), r.y + el.clientTop + pad("paddingTop"));\n }\n // The element\'s center in the top page\'s viewport, after scrolling it into view.\n function locate(target) {\n const el = find(target);\n if (!el) return { ok: false, error: /^e\\d+$/.test(String(target).trim()) ? `${target} is no longer on the page (it was removed or drawn again). Use its name, or read again.` : `Nothing visible matches "${target}". Read the page to see what is there.` };\n el.scrollIntoView({ block: "center", inline: "center" });\n let r = el.getBoundingClientRect();\n let x = r.x + r.width / 2, y = r.y + r.height / 2;\n ({ x, y } = toTop(el.ownerDocument.defaultView, x, y));\n return { ok: true, x, y, label: isCtl(el) ? `${roleOf(el)} "${nameOf(el).slice(0, 40)}"` : `"${(el.innerText ?? "").replace(/\\s+/g, " ").trim().slice(0, 40)}"` };\n }\n // Wait until check() returns something truthy. Page-change events fire at\n // once even when the window is behind others (paint callbacks get slowed\n // down there), with a short timer as a backstop.\n function until(check, ms) {\n return new Promise((res) => {\n const t0 = performance.now();\n let done = false, timer = 0;\n const mo = new MutationObserver(() => run());\n const finish = (v) => { done = true; mo.disconnect(); clearTimeout(timer); res(v); };\n const run = () => {\n if (done) return;\n const v = check();\n if (v) return finish(v);\n if (performance.now() - t0 > ms) return finish(false);\n };\n const poll = () => { run(); if (!done) timer = setTimeout(poll, 16); };\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n poll();\n });\n }\n const ready = (el, when) => !!el && (when !== "enabled" || (!el.disabled && el.getAttribute("aria-disabled") !== "true"));\n // Resolve the frame a control with this name shows (or turns on).\n function waitFor(target, when, ms, point) {\n return until(() => ready(find(target), when) && (point ? locate(target) : true), ms);\n }\n function waitGone(target, ms) {\n return until(() => !find(target), ms).then((v) => !!v);\n }\n // Scroll the page and every scroll box until the text is drawn and visible.\n async function scrollTo(text, ms) {\n const t0 = performance.now();\n const has = () => find(text); // the same matching as click: controls by name, then short text\n const boxes = () => [document.scrollingElement, ...[...document.querySelectorAll("*")].filter((e) => e.scrollHeight > e.clientHeight + 4 && /(auto|scroll)/.test(getComputedStyle(e).overflowY))];\n // JUMP: in a numbered list ("Row 7,342"), read the numbers on screen and\n // jump near the target, then fine-tune, instead of a screen at a time.\n const num = (x) => Number(String(x).replace(/,/g, ""));\n const want = String(text).match(/^(.*?)([\\d,]+)\\s*$/);\n if (cfg.jump && want && !has()) {\n const prefix = want[1].trim().toLowerCase(), N = num(want[2]);\n for (let k = 0; k < 10 && !has(); k++) {\n let moved = false;\n for (const b of boxes()) {\n const br = b === document.scrollingElement ? { top: 0, bottom: innerHeight } : b.getBoundingClientRect();\n const seen = [];\n for (const el of b.querySelectorAll("*")) {\n if (el.children.length) continue;\n const m = (el.textContent || "").trim().match(/^(.*?)([\\d,]+)\\s*$/);\n if (!m || m[1].trim().toLowerCase() !== prefix) continue;\n const r = el.getBoundingClientRect();\n if (r.bottom < br.top || r.top > br.bottom || !r.height) continue;\n seen.push([num(m[2]), r.top]);\n }\n if (seen.length < 2) continue;\n seen.sort((a, c) => a[0] - c[0]);\n const [n0, y0] = seen[0], [n1, y1] = seen[seen.length - 1];\n if (n1 === n0) continue;\n const pxPer = (y1 - y0) / (n1 - n0);\n const delta = (N - (n0 + n1) / 2) * pxPer;\n if (Math.abs(delta) < 2) continue;\n b.scrollTop += delta;\n moved = true;\n }\n if (!moved) break;\n await new Promise((r) => setTimeout(r, 40));\n }\n }\n while (performance.now() - t0 < ms) {\n const el = has();\n if (el) {\n el.scrollIntoView({ block: "center" });\n return { ok: true };\n }\n let moved = false;\n for (const b of boxes()) {\n if (b.scrollTop + b.clientHeight < b.scrollHeight - 2) {\n b.scrollTop += Math.max(200, b.clientHeight * 0.9);\n moved = true;\n }\n }\n await new Promise((r) => setTimeout(r, moved ? 30 : 250));\n if (!moved && !has()) {\n // Nothing left to scroll here; a feed may still be loading more.\n if (performance.now() - t0 > ms / 2) break;\n }\n }\n return { ok: !!has(), error: `"${text}" did not show after scrolling.` };\n }\n // Resolve once the page has gone quiet (no DOM change for `quiet` ms), or at `max`.\n function settle(quiet, max) {\n return new Promise((res) => {\n const t0 = performance.now();\n let last = t0, timer = 0;\n const mo = new MutationObserver(() => (last = performance.now()));\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n const tick = () => {\n const now = performance.now();\n if (now - last >= quiet || now - t0 >= max) { mo.disconnect(); return res(Math.round(now - t0)); }\n timer = setTimeout(tick, 10);\n };\n tick();\n });\n }\n // For a native date field: parse the text and give back the digits in the\n // order this browser\'s field wants (e.g. MM DD YYYY), plus the field\'s left edge.\n function dateKeys(target, text) {\n const el = find(target);\n if (!el || el.tagName !== "INPUT" || el.type !== "date") return null;\n const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];\n let y, m, d, t = String(text).trim();\n let k;\n if ((k = t.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/))) [, y, m, d] = k;\n else if ((k = t.match(/^(\\d{1,2})[/.-](\\d{1,2})[/.-](\\d{4})$/))) [, m, d, y] = k;\n else if ((k = t.match(/^([A-Za-z]+)\\.? (\\d{1,2}),? (\\d{4})$/))) { m = months.indexOf(k[1].slice(0, 3).toLowerCase()) + 1; d = k[2]; y = k[3]; }\n else if ((k = t.match(/^(\\d{1,2}) ([A-Za-z]+)\\.? (\\d{4})$/))) { d = k[1]; m = months.indexOf(k[2].slice(0, 3).toLowerCase()) + 1; y = k[3]; }\n if (!y || !m || !d || m < 1) return null;\n const parts = new Intl.DateTimeFormat(navigator.language, { year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(2001, 1, 3)).map((p) => p.type).filter((x) => ["year", "month", "day"].includes(x));\n const val = { year: String(y).padStart(4, "0"), month: String(m).padStart(2, "0"), day: String(d).padStart(2, "0") };\n el.scrollIntoView({ block: "center" });\n const r = el.getBoundingClientRect();\n const { x: px, y: py } = toTop(el.ownerDocument.defaultView, r.x + 10, r.y + r.height / 2);\n return { keys: parts.map((p) => val[p]).join(""), x: px, y: py, want: `${val.year}-${val.month}-${val.day}` };\n }\n // A slider\'s value (native range or ARIA slider), or null if the target isn\'t a slider.\n function sliderOf(target) {\n const el = find(target);\n if (!el) return null;\n if (el.tagName === "INPUT" && el.type === "range") return { v: Number(el.value), min: Number(el.min || 0), max: Number(el.max || 100) };\n if (el.getAttribute("role") === "slider") return { v: Number(el.getAttribute("aria-valuenow")), min: Number(el.getAttribute("aria-valuemin") ?? 0), max: Number(el.getAttribute("aria-valuemax") ?? 100) };\n return null;\n }\n // INPUTSCROLL: plan one scroll step toward the text; the server does it with the real wheel.\n function scrollPlan(text) {\n const el = find(text);\n if (el) {\n const r = el.getBoundingClientRect();\n const inView = r.top >= 0 && r.bottom <= innerHeight && r.left >= 0 && r.right <= innerWidth;\n return { found: true, inView };\n }\n // Inner scroll boxes first (deepest last in document order), the page itself last.\n const inner = [...document.querySelectorAll("*")].filter((e) => e.scrollHeight > e.clientHeight + 4 && /(auto|scroll)/.test(getComputedStyle(e).overflowY)).reverse();\n const boxes = [...inner, document.scrollingElement];\n const num = (x) => Number(String(x).replace(/,/g, ""));\n const want = String(text).match(/^(.*?)([\\d,]+)\\s*$/);\n for (const b of boxes) {\n const top = b === document.scrollingElement;\n const br = top ? { top: 0, bottom: innerHeight, left: 0, right: innerWidth } : b.getBoundingClientRect();\n const cx = Math.max(0, Math.min(innerWidth - 1, (br.left + br.right) / 2)), cy = Math.max(0, Math.min(innerHeight - 1, (br.top + br.bottom) / 2));\n if (want && cfg.jump) {\n const prefix = want[1].trim().toLowerCase(), N = num(want[2]);\n const seen = [];\n for (const e of b.querySelectorAll("*")) {\n if (e.children.length) continue;\n const m = (e.textContent || "").trim().match(/^(.*?)([\\d,]+)\\s*$/);\n if (!m || m[1].trim().toLowerCase() !== prefix) continue;\n const r = e.getBoundingClientRect();\n if (r.bottom < br.top || r.top > br.bottom || !r.height) continue;\n seen.push([num(m[2]), r.top]);\n }\n if (seen.length >= 2) {\n seen.sort((a, c) => a[0] - c[0]);\n const [n0, y0] = seen[0], [n1, y1] = seen[seen.length - 1];\n if (n1 !== n0) {\n const dy = (N - (n0 + n1) / 2) * ((y1 - y0) / (n1 - n0));\n if (Math.abs(dy) >= 2) return { x: cx, y: cy, dy: Math.round(dy) };\n }\n }\n }\n if (b.scrollTop + b.clientHeight < b.scrollHeight - 2) return { x: cx, y: cy, dy: Math.round(Math.max(200, b.clientHeight * 0.9)) };\n }\n return { wait: true }; // nothing left to scroll; a feed may still load more\n }\n // Fields we never type into: passwords, one-time codes and card numbers. The person does those.\n // autocomplete can hold several words ("billing cc-number", "section-login current-password").\n const SECRET = /^(current-password|new-password|one-time-code|cc-number|cc-csc|cc-exp|cc-exp-month|cc-exp-year)$/;\n const isSecret = (el) => !!el && el.tagName === "INPUT" && (el.type === "password" || String(el.autocomplete || "").toLowerCase().split(/\\s+/).some((t) => SECRET.test(t)));\n function focused() {\n let a = document.activeElement;\n for (;;) {\n if (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;\n else if (a?.tagName === "IFRAME") {\n try { a = a.contentDocument?.activeElement ?? null; } catch { return a; }\n if (!a) return null;\n } else return a;\n }\n }\n const secret = (t) => (t == null ? isSecret(focused()) : isSecret(find(t)));\n // Would this click (or Enter in the focused field) send a form whose password, code or\n // card field is still empty? Buoy refuses: the person types those first.\n const emptySecretIn = (form) => !!form && [...form.querySelectorAll("input")].some((i) => isSecret(i) && !i.value && !hidden(i));\n const isSubmit = (el) => !!el && ((el.tagName === "BUTTON" && (!el.type || el.type === "submit")) || (el.tagName === "INPUT" && (el.type === "submit" || el.type === "image")));\n // The same check for a click at a spot: what\'s there, and is it a send button for such a form?\n function submitBlockedAt(x, y) {\n let el = document.elementFromPoint(x, y);\n while (el?.tagName === "IFRAME") {\n try {\n const r = el.getBoundingClientRect();\n x -= r.x; y -= r.y;\n el = el.contentDocument.elementFromPoint(x, y);\n } catch { break; }\n }\n while (el?.shadowRoot) { const inner = el.shadowRoot.elementFromPoint(x, y); if (!inner || inner === el) break; el = inner; }\n const btn = el?.closest?.("button, input") ?? el;\n return isSubmit(btn) && emptySecretIn(btn.form ?? btn.closest("form"));\n }\n function submitBlocked(t) {\n if (t == null) {\n const f = focused();\n return !!f && (f.tagName === "INPUT" || isSubmit(f)) && emptySecretIn(f.form ?? f.closest?.("form"));\n }\n const el = find(t);\n const btn = el?.closest?.("button, input") ?? el;\n return isSubmit(btn) && emptySecretIn(btn.form ?? btn.closest("form"));\n }\n // Is the target a rich text box (contenteditable), and is the cursor already in it?\n function editInfo(t) {\n const el = find(t);\n if (!el) return null;\n const box = el.isContentEditable ? el : null;\n if (!box) return { rich: false };\n const f = focused();\n return { rich: true, focused: !!f && (f === box || box.contains(f)) };\n }\n globalThis.__buoyWeb = { pageId, submitBlockedAt, editInfo, frameRect, scrollPlan, secret, submitBlocked, sliderOf, dateKeys, valueOf: (t) => find(t)?.value ?? null, tagOf: (t) => find(t)?.tagName ?? null, cfg, settle, outline, locate, find: (t) => !!find(t), waitFor, waitGone, scrollTo, watch, waitSig };\n})();\n'}});function telemetryOff(){const e=(process.env.BUOY_TELEMETRY??"").toLowerCase();return e==="0"||e==="false"||e==="off"||process.env.DO_NOT_TRACK==="1"}function noteUse(){if(sentThisRun||telemetryOff())return;sentThisRun=true;try{const e=import_node_path33.default.join(webDir(),"install.json");let n={};try{n=JSON.parse(import_node_fs31.default.readFileSync(e,"utf8"))}catch{}if(typeof n.installId!=="string")n={installId:import_node_crypto17.default.randomUUID(),lastPingAt:0};if(Date.now()-(n.lastPingAt??0)<DAY_MS)return;n.lastPingAt=Date.now();import_node_fs31.default.mkdirSync(import_node_path33.default.dirname(e),{recursive:true,mode:448});import_node_fs31.default.writeFileSync(e,JSON.stringify(n),{mode:384});const t=JSON.stringify({installId:n.installId,version:"7.0.70",platform:process.platform==="darwin"?"macos":process.platform==="win32"?"windows":"linux",tier:"locked",surface:"web-mcp"});void fetch(ENDPOINT,{method:"POST",headers:{"content-type":"application/json"},body:t,signal:AbortSignal.timeout(5e3)}).catch(()=>{})}catch{}}var import_node_crypto17,import_node_fs31,import_node_path33,ENDPOINT,DAY_MS,sentThisRun;var init_usage=__esm({"src/web/usage.ts"(){"use strict";import_node_crypto17=__toESM(require("crypto"));import_node_fs31=__toESM(require("fs"));import_node_path33=__toESM(require("path"));init_relay();ENDPOINT=process.env.BUOY_TELEMETRY_URL??"https://buoy.gg/api/t";DAY_MS=24*60*60*1e3;sentThisRun=false}});function cdpEndpoint(){const e=process.env.BUOY_WEB_CDP;if(e!=="auto")return e;const n=import_node_path34.default.join(import_node_os23.default.homedir(),"Library/Application Support/Google/Chrome/DevToolsActivePort");if(!import_node_fs32.default.existsSync(n))throw new Error("Chrome isn't open for tools yet. In Chrome, open chrome://inspect/#remote-debugging and turn it on.");const[t,s]=import_node_fs32.default.readFileSync(n,"utf8").trim().split("\n");return`ws://127.0.0.1:${t}${s}`}async function connectNode(e,n){const{chromium:t}=await e();let s=!process.env.BUOY_WEB_PROFILE?findHelpers()[0]?.sock:void 0;if(!s&&!process.env.BUOY_WEB_PROFILE&&!process.env.BUOY_WEB_CDP&&extensionInstalled()){for(let o=0;o<40&&!s;o++){await sleep5(200);s=findHelpers()[0]?.sock}}if(process.env.BUOY_WEB_CDP){const o=await t.connectOverCDP(cdpEndpoint());return{browser:o,context:o.contexts()[0],mode:"cdp"}}if(s){const o=await startRelay({sock:s});o.onDetach=({reason:l})=>{if(l==="canceled_by_user")n("The person clicked Cancel on Chrome's debugging bar, so Buoy stopped. Ask them before you try again.")};const a=await t.connectOverCDP(o.url);return{browser:a,context:a.contexts()[0],mode:"extension",idle:o.idle,close:o.close}}if(!process.env.BUOY_WEB_PROFILE&&seenExtension()){throw new Error("Chrome is closed, or the Buoy Web extension is turned off. Ask the person to open Chrome, then try again.")}const i=process.env.BUOY_WEB_PROFILE??process.env.BUOY_WEB_WINDOW_DIR??import_node_path34.default.join(import_node_os23.default.homedir(),".buoy","web-profile");const r=await t.launchPersistentContext(i,{headless:false,channel:"chrome",viewport:{width:1280,height:900},args:["--no-first-run","--no-default-browser-check"]});return{browser:r.browser(),context:r,mode:"window",ownsAll:true}}function checkUpload(e){const n=[process.cwd(),...(process.env.BUOY_WEB_UPLOAD_DIRS??"").split(import_node_path34.default.delimiter).filter(Boolean)].map(s=>import_node_fs32.default.realpathSync(s));let t;try{t=import_node_fs32.default.realpathSync(import_node_path34.default.resolve(e))}catch{throw new Error(`No file at ${e}.`)}if(!n.some(s=>t===s||t.startsWith(s+import_node_path34.default.sep)))throw new Error(`Buoy only uploads files inside the project folder (${n[0]}). ${e} is outside it.`);if(SECRET_FILE.test(t))throw new Error(`${import_node_path34.default.basename(t)} looks like a secret (keys, passwords or tokens). Buoy doesn't upload those.`);return t}function createWebServer(e={}){const n=e.loadPlaywright??(()=>import("playwright-core"));const t=Date.now();const s=createWebCore({connect:l=>connectNode(n,l),pageScript:PAGE_JS,isMac:process.platform==="darwin",trace:process.env.BUOY_WEB_TRACE==="1"?l=>process.stderr.write(`[buoy-web ${Date.now()-t}ms] ${l}
|
|
1601
|
+
`):void 0,extraTools:l=>[uploadTool(l)]});const i=new import_mcp2.McpServer({name:"buoy-web",version:"7.0.70"});const r=Number(process.env.BUOY_WEB_CALL_LIMIT_MS??6e4);const o=["open","read","click","type","key","wait","scroll","drag","hover","upload","tabs","select","dialog","screenshot"];const a=[...s.tools].sort((l,c)=>o.indexOf(l.name)-o.indexOf(c.name));for(const l of a){i.tool(l.name,l.description,l.shape,async c=>{let u;const d=new Promise(p=>{u=setTimeout(()=>p(fail13(new Error(`This call took over ${r/1e3} s, so Buoy stopped waiting. The page may be stuck or still loading. Try read, or open the link again.`))),r)});const h=await Promise.race([l.run(c).catch(fail13),d]).finally(()=>clearTimeout(u));noteUse();return h})}return{server:i,close:s.close}}var import_node_fs32,import_node_os23,import_node_path34,import_mcp2,import_zod39,sleep5,seenExtension,SECRET_FILE,uploadTool;var init_server3=__esm({"src/web/server.ts"(){"use strict";import_node_fs32=__toESM(require("fs"));import_node_os23=__toESM(require("os"));import_node_path34=__toESM(require("path"));import_mcp2=require("@modelcontextprotocol/sdk/server/mcp.js");import_zod39=require("zod");init_core();init_install();init_pageScript_generated();init_relay();init_usage();sleep5=e=>new Promise(n=>setTimeout(n,e));seenExtension=()=>import_node_fs32.default.existsSync(import_node_path34.default.join(webDir(),"seen"));SECRET_FILE=/(^|[/\\])(\.env(\..*)?|\.npmrc|\.netrc|\.pgpass|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?|.*\.(pem|key|p12|pfx|keychain|kdbx))$|[/\\]\.(ssh|aws|gnupg|kube|docker)[/\\]/i;uploadTool=e=>({name:"upload",description:"Pick files in a file field (the real file picker, filled for you). Paths must be inside the project folder; leave empty to send a small test file.",shape:{target:e.target,paths:import_zod39.z.array(import_zod39.z.string()).optional()},effect:"write",async run({target:n,paths:t}){const s=t?.length?t.map(checkUpload):null;const i=await e.pointAt(n);const r=e.page();const[o]=await Promise.all([r.waitForEvent("filechooser",{timeout:5e3}),r.mouse.click(i.x,i.y)]);await o.setFiles(s??[{name:"test.txt",mimeType:"text/plain",buffer:Buffer.from("hello from Buoy\n")}]);await e.frame();return text11(`Picked ${s?.length??1} file(s) in ${i.label}.
|
|
1602
1602
|
${await e.changes()}`)}})}});var main_exports={};__export(main_exports,{runWebServer:()=>runWebServer});async function runWebServer(){ensureHelper();const{server:e,close:n}=createWebServer();const t=new import_stdio.StdioServerTransport;const s=t;const i=s.send.bind(s);s.send=(d,...h)=>{if(d?.result?.tools)for(const p of d.result.tools)p._meta={...p._meta??{},"anthropic/alwaysLoad":true};return i(d,...h)};let r=false;const o=()=>{if(r)return;r=true;setTimeout(()=>process.exit(0),1500).unref();void n().finally(()=>process.exit(0))};process.stdin.once("end",o);process.stdin.once("close",o);process.on("SIGINT",o);process.on("SIGTERM",o);process.on("SIGHUP",o);await e.connect(t);const a=s.onmessage;const l=new Map;let c=Promise.resolve();s.onmessage=(d,h)=>{if(d?.method==="notifications/cancelled")l.get(d.params?.requestId)?.();if(d?.method!=="tools/call")return a(d,h);c=c.then(()=>new Promise(p=>{l.set(d.id,()=>{l.delete(d.id);p()});a(d,h)}))};const u=s.send.bind(s);s.send=(d,...h)=>{const p=u(d,...h);if(d?.id!=null&&("result"in d||"error"in d))l.get(d.id)?.();return p}}var import_stdio;var init_main=__esm({"src/web/main.ts"(){"use strict";import_stdio=require("@modelcontextprotocol/sdk/server/stdio.js");init_install();init_server3()}});var flowRun_exports={};__export(flowRun_exports,{formatFlowResults:()=>formatFlowResults,parseFlowRunArgs:()=>parseFlowRunArgs,runFlows:()=>runFlows});function parseFlowRunArgs(e){const n={names:[]};for(let t=0;t<e.length;t++){const s=e[t];if(s==="--device")n.deviceId=e[++t];else if(s==="--update-snapshots")n.updateSnapshots=true;else if(s==="--json")n.json=true;else if(s==="--wait")n.waitMs=Number(e[++t])*1e3;else if(s==="--all")continue;else if(!s.startsWith("--"))n.names.push(s);else throw new Error(`Unknown option ${s}.`)}return n}async function savedFlowNames(){const e=import_node_path35.default.join(process.cwd(),".buoy","flows");return(await import_node_fs33.promises.readdir(e).catch(()=>[])).filter(n=>n.endsWith(".json")).map(n=>n.replace(/\.json$/,"")).sort()}async function runFlows(e,n=t=>process.stderr.write(`${t}
|
|
1603
1603
|
`)){const t=e.names.length?e.names:await savedFlowNames();if(t.length===0)throw new Error('No saved flows. Save one with run_flow and save:"name", or pass a flow name.');const s=process.env.BUOY_BROKER_PORT;const i=new SyncClient({url:process.env.BUOY_BROKER_URL,port:s?Number(s):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const r=createMcpServer(i);const[o,a]=import_inMemory.InMemoryTransport.createLinkedPair();const l=new import_client3.Client({name:"buoy-flow-run",version:"1"});try{await r.connect(a);await l.connect(o);await i.start();const c=Date.now()+(e.waitMs??6e4);while(Date.now()<c&&!i.connectedDevices().some(d=>(d.isPro||d.tier==="free")&&(!e.deviceId||d.deviceId===e.deviceId))){await new Promise(d=>setTimeout(d,500))}if(!i.connectedDevices().some(d=>d.isPro||d.tier==="free")){throw new Error("No app on a Buoy account is connected. Start the app (with Buoy) on a simulator or device, then run again.")}const u=[];for(const d of t){const h=Date.now();n(`\u25B6 ${d}`);const p=await l.callTool({name:"run_flow",arguments:{flow:d,include:"none",...e.deviceId?{deviceId:e.deviceId}:{},...e.updateSnapshots?{updateSnapshots:true}:{}}},void 0,{timeout:10*6e4});const m=(p.content??[]).filter(g=>g.type==="text").map(g=>g.text).join("\n");const y=p.isError!==true;u.push({name:d,passed:y,ms:Date.now()-h,report:m});n(`${y?"\u2713":"\u2717"} ${d} (${((Date.now()-h)/1e3).toFixed(1)}s)`)}return u}finally{await l.close().catch(()=>void 0);await Promise.race([i.stop().catch(()=>void 0),new Promise(c=>setTimeout(c,2e3))])}}function formatFlowResults(e){const n=e.filter(s=>!s.passed);const t=[`${e.length-n.length} of ${e.length} flows passed.`];for(const s of n){t.push("",`\u2717 ${s.name}`,s.report.trim(),` Rerun: npx @buoy-gg/mcp flow run ${s.name}`)}return t.join("\n")}var import_node_fs33,import_node_path35,import_client3,import_inMemory;var init_flowRun=__esm({"src/flowRun.ts"(){"use strict";import_node_fs33=require("fs");import_node_path35=__toESM(require("path"));import_client3=require("@modelcontextprotocol/sdk/client/index.js");import_inMemory=require("@modelcontextprotocol/sdk/inMemory.js");init_SyncClient();init_server2()}});var init_exports={};__export(init_exports,{WEB_DOCS_URL:()=>WEB_DOCS_URL,runWebInit:()=>runWebInit});function runWebInit(e={}){const n=e.log??(c=>process.stdout.write(c+"\n"));const t=resolveEntry2({mode:e.mode,registry:e.registry},n);const s={type:"stdio",command:t.command,args:[...t.args,"web"]};const i=JSON.stringify(s);const r=[];const o=[];if(has2("claude")){run6("claude",["mcp","remove",NAME,"-s","user"]);const c=run6("claude",["mcp","add-json",NAME,i,"-s","user"]);if(c.status===0)r.push("Claude Code: added `buoy-web` for all your projects");else o.push(`Claude Code: couldn't add it (${(c.stderr||c.stdout||"").trim().split("\n")[0]}). Run: claude mcp add-json ${NAME} '${i}' -s user`)}else o.push(`Claude Code not found. If you use it, run: claude mcp add-json ${NAME} '${i}' -s user`);const a=import_node_path36.default.join(import_node_os24.default.homedir(),".cursor");if(import_node_fs34.default.existsSync(a)){const c=[];upsertMcpConfig(import_node_path36.default.join(a,"mcp.json"),"mcpServers",s,import_node_os24.default.homedir(),c,NAME);r.push(...c.map(u=>/backed up/.test(u)?`Cursor: ${u}`:"Cursor: added `buoy-web` for all your projects"))}if(has2("codex")){run6("codex",["mcp","remove",NAME]);const c=run6("codex",["mcp","add",NAME,"--",s.command,...s.args]);if(c.status===0)r.push("Codex: added `buoy-web`");else o.push(`Codex: couldn't add it. Run: codex mcp add ${NAME} -- ${s.command} ${s.args.join(" ")}`)}if(has2("code")){const c=run6("code",["--add-mcp",JSON.stringify({name:NAME,...s})]);if(c.status===0)r.push("VS Code: added `buoy-web`")}const l=ensureHelper();if(l.length)r.push(`Chrome helper set up for ${l.length===1?"1 browser":`${l.length} browsers`}`);else if(!helperManifestDirs().length)o.push("No Chrome-family browser found. Buoy Web will open its own Chrome window instead.");n("Buoy Web is set up:");for(const c of r)n(` \u2022 ${c}`);for(const c of o)n(` \u2022 ${c}`);n("");n("Next steps:");n(` 1. Add the Buoy Web extension to Chrome: ${WEB_DOCS_URL}`);n(" Without it, Buoy Web opens its own Chrome window instead of tabs in yours.");n(" 2. Restart your editor, then ask your AI to do something on the web.");n("");n("Buoy Web only works in its own tabs. It never types passwords or card numbers; you do.");if(!telemetryOff()){n("It sends a small usage count once a day: a random id and its version, no web addresses or page text.");n("Turn that off with BUOY_TELEMETRY=0.")}if(s.command==="npx")n(`It runs with \`npx -y ${PACKAGE_NAME}@latest web\`, so it stays up to date.`)}var import_node_child_process24,import_node_fs34,import_node_os24,import_node_path36,NAME,WEB_DOCS_URL,has2,run6;var init_init2=__esm({"src/web/init.ts"(){"use strict";import_node_child_process24=require("child_process");import_node_fs34=__toESM(require("fs"));import_node_os24=__toESM(require("os"));import_node_path36=__toESM(require("path"));init_init();init_version();init_install();init_usage();NAME="buoy-web";WEB_DOCS_URL="https://buoy.gg/buoy/latest/docs/buoy-web";has2=e=>(0,import_node_child_process24.spawnSync)("/bin/sh",["-c",`command -v ${e}`],{encoding:"utf8"}).status===0;run6=(e,n)=>(0,import_node_child_process24.spawnSync)(e,n,{encoding:"utf8",timeout:3e4})}});var import_stdio2=require("@modelcontextprotocol/sdk/server/stdio.js");init_hidStream();init_recorder();init_SyncClient();init_server2();init_init();var stdoutLog=console.log;console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function main(){const e=process.env.BUOY_BROKER_PORT;const n=new SyncClient({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const t=createMcpServer(n);const s=new import_stdio2.StdioServerTransport;let i=false;const r=o=>{if(i)return;i=true;process.stderr.write(`[buoy-mcp] shutting down (${o})
|
|
1604
1604
|
`);stopAllRecordingsNow();stopAllHidStreamsNow();setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await n.stop().catch(()=>{});await t.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>r("stdin closed"));process.stdin.once("close",()=>r("stdin closed"));t.server.onclose=()=>r("transport closed");process.on("SIGINT",()=>r("SIGINT"));process.on("SIGTERM",()=>r("SIGTERM"));process.on("SIGHUP",()=>r("SIGHUP"));await t.connect(s);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
|