@buoy-gg/mcp 7.0.65 → 7.0.67

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/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createRequire as e}from"module";const L=e(import.meta.url);import{runInit as r}from"./chunk-WVVRNHPL.mjs";import{ALL_DEVICES as o,BROKER_PORT as t,BROKER_URL as i,DASHBOARD_NAME as s,DEFAULT_ACTION_TIMEOUT_MS as a,DEFAULT_SNAPSHOT_TIMEOUT_MS as T,PACKAGE_NAME as m,SyncClient as n,VERSION as g,buildAutomationConfig as l,compareReportStats as R,createMcpServer as p,estimateBatchMs as E,formatRankingMarkdown as _,getUpdateNotice as c,rankBatchFromIndex as u,registerConsoleTools as A,registerEventsTools as S,registerGenericTools as M,registerJotaiTools as O,registerPerfMonitorTools as C,registerReactQueryTools as I,registerReduxTools as d,registerRoutesTools as f,registerScreenshotTools as N,registerStorageTools as U,registerZustandTools as B}from"./chunk-SY6CJRCL.mjs";import"./chunk-QW2BPKIP.mjs";import"./chunk-6EOB4FHJ.mjs";import"./chunk-SWS5NTYM.mjs";import"./chunk-OPHPYPJD.mjs";import"./chunk-MTTWR4T7.mjs";export{o as ALL_DEVICES,t as BROKER_PORT,i as BROKER_URL,s as DASHBOARD_NAME,a as DEFAULT_ACTION_TIMEOUT_MS,T as DEFAULT_SNAPSHOT_TIMEOUT_MS,m as PACKAGE_NAME,n as SyncClient,g as VERSION,l as buildAutomationConfig,R as compareReportStats,p as createMcpServer,E as estimateBatchMs,_ as formatRankingMarkdown,c as getUpdateNotice,u as rankBatchFromIndex,A as registerConsoleTools,S as registerEventsTools,M as registerGenericTools,O as registerJotaiTools,C as registerPerfMonitorTools,I as registerReactQueryTools,d as registerReduxTools,f as registerRoutesTools,N as registerScreenshotTools,U as registerStorageTools,B as registerZustandTools,r as runInit};
1
+ import{createRequire as e}from"module";const L=e(import.meta.url);import{runInit as r}from"./chunk-7XCEVM3W.mjs";import{ALL_DEVICES as o,BROKER_PORT as t,BROKER_URL as i,DASHBOARD_NAME as s,DEFAULT_ACTION_TIMEOUT_MS as a,DEFAULT_SNAPSHOT_TIMEOUT_MS as T,PACKAGE_NAME as m,SyncClient as n,VERSION as g,buildAutomationConfig as l,compareReportStats as R,createMcpServer as p,estimateBatchMs as E,formatRankingMarkdown as _,getUpdateNotice as c,rankBatchFromIndex as u,registerConsoleTools as A,registerEventsTools as S,registerGenericTools as M,registerJotaiTools as O,registerPerfMonitorTools as C,registerReactQueryTools as I,registerReduxTools as d,registerRoutesTools as f,registerScreenshotTools as N,registerStorageTools as U,registerZustandTools as B}from"./chunk-LDULNQCZ.mjs";import"./chunk-2MSKDRYW.mjs";import"./chunk-VKWX4XAY.mjs";import"./chunk-AMU2EOGE.mjs";import"./chunk-OPHPYPJD.mjs";import"./chunk-MTTWR4T7.mjs";export{o as ALL_DEVICES,t as BROKER_PORT,i as BROKER_URL,s as DASHBOARD_NAME,a as DEFAULT_ACTION_TIMEOUT_MS,T as DEFAULT_SNAPSHOT_TIMEOUT_MS,m as PACKAGE_NAME,n as SyncClient,g as VERSION,l as buildAutomationConfig,R as compareReportStats,p as createMcpServer,E as estimateBatchMs,_ as formatRankingMarkdown,c as getUpdateNotice,u as rankBatchFromIndex,A as registerConsoleTools,S as registerEventsTools,M as registerGenericTools,O as registerJotaiTools,C as registerPerfMonitorTools,I as registerReactQueryTools,d as registerReduxTools,f as registerRoutesTools,N as registerScreenshotTools,U as registerStorageTools,B as registerZustandTools,r as runInit};
@@ -0,0 +1 @@
1
+ import{createRequire as r}from"module";const t=r(import.meta.url);import{farmFairness as e,registerSimFarmTools as m,runFarmd as o}from"./chunk-2MSKDRYW.mjs";import"./chunk-VKWX4XAY.mjs";import"./chunk-AMU2EOGE.mjs";import"./chunk-MTTWR4T7.mjs";export{e as farmFairness,m as registerSimFarmTools,o as runFarmd};
@@ -0,0 +1 @@
1
+ import{createRequire as e}from"module";const B=e(import.meta.url);import{ALWAYS_ENABLED as r,DEFAULT_SETTINGS as a,Engine as t,FarmClient as o,FarmError as i,LoadMeter as m,PROTOCOL_VERSION as n,SLIM_CATEGORIES as d,STATE_VERSION as l,SimctlHost as s,Store as p,agentLabel as f,connectFarm as E,crashLoop as u,defaultSeedDir as L,detectAgent as S,farmDefaults as c,farmHome as R,farmOwnedUdids as T,findDataContainer as A,findExampleBuild as I,findRepoRoot as O,nodeLauncher as _,parseCpuTime as C,paths as D,profileHash as F,profileLabels as b,readBuoyInstallId as h,slimLabels as N,startFarmd as g}from"./chunk-AMU2EOGE.mjs";import"./chunk-MTTWR4T7.mjs";export{r as ALWAYS_ENABLED,a as DEFAULT_SETTINGS,t as Engine,o as FarmClient,i as FarmError,m as LoadMeter,n as PROTOCOL_VERSION,d as SLIM_CATEGORIES,l as STATE_VERSION,s as SimctlHost,p as Store,f as agentLabel,E as connectFarm,u as crashLoop,L as defaultSeedDir,S as detectAgent,c as farmDefaults,R as farmHome,T as farmOwnedUdids,A as findDataContainer,I as findExampleBuild,O as findRepoRoot,_ as nodeLauncher,C as parseCpuTime,D as paths,F as profileHash,b as profileLabels,h as readBuoyInstallId,N as slimLabels,g as startFarmd};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/mcp",
3
3
  "mcpName": "io.github.Buoy-gg/mcp",
4
- "version": "7.0.65",
4
+ "version": "7.0.67",
5
5
  "buoy": {
6
6
  "dashboardAuthVersion": 1
7
7
  },
@@ -35,8 +35,8 @@
35
35
  "socket.io-client": "^4.8.1",
36
36
  "ws": "^8.18.0",
37
37
  "zod": "^3.25.0",
38
- "@buoy-gg/sync-broker": "^7.0.65",
39
- "@buoy-gg/license": "^7.0.65"
38
+ "@buoy-gg/license": "^7.0.67",
39
+ "@buoy-gg/sync-broker": "^7.0.67"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.0.0",
@@ -47,7 +47,7 @@
47
47
  "rimraf": "^5.0.0",
48
48
  "tsup": "^8.5.1",
49
49
  "typescript": "~5.8.3",
50
- "@buoy-gg/sim-farm": "7.0.65"
50
+ "@buoy-gg/sim-farm": "7.0.67"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=18"
@@ -84,6 +84,21 @@ Open the dev server's network URL on a phone on the same Wi-Fi (for Vite, `vite
84
84
  - A remount doesn't reset module state or storage. If you use `reloadBetweenCases`, make sure `AutomationResumer` is mounted.
85
85
  - A simulator result holds for that simulator. Confirm on real hardware before claiming phone speed.
86
86
 
87
+ ## Many ideas at once: the Sim Farm (iOS sims)
88
+
89
+ Use the farm when the `sim_farm_*` tools exist and you have many ideas. Check `sim_farm_status` first. Its `roomFor` says how many more sims fit. A 16 GB Mac may fit only 1 to 3. Then pass `allowPartial: true`, and open the ideas one after another on the sims you got.
90
+
91
+ Sims share the Mac's CPU and graphics with everything else on it. So many sims at once are good for looks and crashes. They are bad for speed. One run has two steps.
92
+
93
+ 1. **Look.** Take one sim per idea with `sim_farm_acquire`. Pass `launch: true`, a `goal`, and one `labels` entry per idea. Use the idea IDs from your bank as labels. Each sim comes back with a `deviceId`. Pass it to the other Buoy tools. Open each idea with `sim_farm_action` `open-url`, using the test route's link and that idea's params. If iOS asks "Open in …?", tap Open with `native_tap`. Take stills with `screenshot`. Mark ideas that crash or look wrong `invalid`. Send `sim_farm_report` for each one. Don't rank speed in this step.
94
+ 2. **Speed.** Release your sims with `sim_farm_release` and `keepRun: true`. Take one sim into the same run with `sim_farm_acquire` and its `runId`. Hold `perf-slot` with `sim_farm_lock`. Another agent may hold it, so pass `waitSeconds`. First run the baseline 5 times, so you know how much the numbers move on their own. Then run `run_benchmark_batch` on that sim with `captureRenders: false`. Keep the baseline in every batch.
95
+
96
+ While you hold `perf-slot`, the farm records how busy the Mac is every second. `run_benchmark_batch` on your farm sim then says which runs were fair. A run is unfair when the Mac got hot, ran low on memory, swapped, ran low on spare CPU, or had other work jump during it. Small laptops get hot and swap more often. If many runs come back unfair, wait longer between runs with `coolDownMs`, and close apps you don't need. Never rank on an unfair run. Run those cases again with the baseline once the Mac is calm. If the farm can't check, say your numbers are unchecked.
97
+
98
+ Finish with `sim_farm_finish` and a one line summary. Ask for more time with `sim_farm_extend` before `deadlineAt`. If a call says `RUN_STOPPED`, the person stopped you. Stop and tell them.
99
+
100
+ Farm numbers come from a slim debug sim. Say so in your report, and confirm the finalists on a real device.
101
+
87
102
  ## Tool list
88
103
 
89
104
  | Tool | Use |
@@ -97,4 +112,8 @@ Open the dev server's network URL on a phone on the same Wi-Fi (for Vite, `vite
97
112
  | `run_benchmark_batch` / `get_automation_status` | Measure variants in the app with Bench. |
98
113
  | `get_batch_report` / `compare_reports` | Read or compare saved runs. |
99
114
  | `measure_renders` | Count renders in one interaction. |
115
+ | `sim_farm_acquire` / `sim_farm_release` / `sim_farm_finish` | Take farm sims for a run, give them back and end the run. |
116
+ | `sim_farm_action` | Open a link, take a still or relaunch the app on your farm sims. |
117
+ | `sim_farm_lock` | Hold `perf-slot` for fair speed tests on one farm sim. |
118
+ | `sim_farm_report` | Tell the person how each idea is going. |
100
119
  | `navigate` | Open a route in the app. |
@@ -90,7 +90,7 @@ Then measure every valid variant the same way. Screen them at a few loads (one l
90
90
  The ceiling is the score.
91
91
 
92
92
  - On web, use `find_web_ceiling`. It raises the load about 30% a step until the frame rate misses the target, then narrows the gap to a few percent. It has no cap by default (up to 1,000,000,000).
93
- - On React Native, ramp the count with `run_benchmark_batch` cases.
93
+ - On React Native, ramp the count with `run_benchmark_batch` cases. With many ideas on iOS sims, use the Sim Farm (see MEASURING.md): many sims check looks, and one quiet sim measures speed.
94
94
  - Record the highest pass and the lowest miss. A single miss can be noise; run the boundary again before you call it.
95
95
  - If nothing missed, the score is "at least" that load. Raise the max and run again.
96
96
  - **Rank on the score.** Dropped frames, frame times, CPU, memory and profiles explain why a variant won. They only rank when the contract says so.
@@ -1 +0,0 @@
1
- import{createRequire as e}from"module";const c=e(import.meta.url);import{cliPath as r,describeDevices as i,describeDiagnosis as t,describeInputs as o,proRequired as s,readEntitlement as m,run as u}from"./chunk-6EOB4FHJ.mjs";import"./chunk-MTTWR4T7.mjs";export{r as cliPath,i as describeDevices,t as describeDiagnosis,o as describeInputs,s as proRequired,m as readEntitlement,u as run};
@@ -1,2 +0,0 @@
1
- import{createRequire as g}from"module";const q=g(import.meta.url);import{proRequired as y,readEntitlement as k}from"./chunk-6EOB4FHJ.mjs";import{FarmError as _,connectFarm as S,nodeLauncher as I}from"./chunk-SWS5NTYM.mjs";import{z as t}from"zod";var x=r=>({content:[{type:"text",text:r}]});var l=r=>({isError:true,content:[{type:"text",text:r}]});var o=r=>x(JSON.stringify(r,null,2));var u=r=>k().isPro?null:l(y(r));var d=null;function h(){if(!d){d=S({label:process.env.BUOY_AGENT_LABEL||"Buoy MCP",launcher:I(process.argv[1],["farmd"])}).catch(r=>{d=null;throw r})}return d}async function n(r,e,s,a){return(await h()).call(r,e,{timeoutMs:s,requestId:a})}function c(r){if(r instanceof _)return l(`${r.code}: ${r.message}${r.next?`
2
- Next: ${r.next}`:""}`);return l(`Sim Farm error: ${r?.message??r}`)}var f={leaseId:t.string().describe("The lease id from sim_farm_acquire."),epoch:t.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};var w=t.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.");function D(r){r.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. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:t.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:e})=>{try{return o(await n("status",{outside:!!e}))}catch(s){return c(s)}});r.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and a seed folder (signed-in app data); clones are made from it in about 4 s each. `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.",inputSchema:{mode:t.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size."),templateId:t.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:t.number().int().min(1).max(40).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:t.string().optional().describe("For create: path to a simulator .app build."),seedDir:t.string().optional().describe("For create: seed folder (Documents/, AsyncStorage/, seed.json)."),deviceType:t.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:t.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:t.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:t.number().int().optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:t.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId:w}},async e=>{const s=u("sim_farm_setup");if(s)return s;try{if(e.mode==="create"){const i={appPath:e.appPath,seedDir:e.seedDir,deviceType:e.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:e.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:e.profile??"slim",metroPort:e.metroPort??8802};if(!i.appPath||!i.seedDir)return l("create needs appPath and seedDir.");if(e.dryRun)return o({wouldCreateTemplate:i,budget:await n("budget",{ramGB:6,diskGB:4})});return o(await n("setup",i,15*6e4,e.requestId))}let a=e.templateId;if(!a){const i=await n("status");a=i.templates.filter(p=>p.usable).pop()?.id;if(!a)return l("No usable template yet. Run sim_farm_setup with mode create first.")}const m=e.count??1;if(e.dryRun){const i=await n("budget",{ramGB:0,diskGB:e.mode==="grow"?2*m:0});return o({mode:e.mode,templateId:a,count:m,roomForMoreSims:i.roomFor,ok:i.ok,reason:i.reason})}return o(await n(e.mode,{templateId:a,count:m},30*6e4,e.requestId))}catch(a){return c(a)}});r.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. Returns 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). 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_release when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run.",inputSchema:{count:t.number().int().min(1).max(40).describe("How many sims."),purpose:t.string().describe("Shown to the person in Buoy Desktop, e.g. 'optimize: list variants'."),templateId:t.string().optional().describe("Defaults to the newest usable template."),launch:t.boolean().optional().describe("Launch the app on each sim and wait for its Buoy id."),allowPartial:t.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:t.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:t.number().int().min(1).max(1440).optional().describe("Hard limit on the lease's life. Default 15."),requestId:w}},async e=>{const s=u("sim_farm_acquire");if(s)return s;try{let a=e.templateId;if(!a){const p=await n("status");a=p.templates.filter(b=>b.usable).pop()?.id;if(!a)return l("No usable template yet. Run sim_farm_setup with mode create first.")}const m=await h();const i=await m.acquire({templateId:a,count:e.count,purpose:e.purpose,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});return o(i)}catch(a){return c(a)}});r.registerTool("sim_farm_release",{description:"Give farm 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.",inputSchema:{...f,reset:t.enum(["fast","full"]).optional()}},async e=>{try{const s=await h();return o(await s.release(e.leaseId,e.epoch,e.reset??"fast"))}catch(s){return c(s)}});r.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:{...f,udids:t.array(t.string()).min(1).describe("UDIDs from your lease."),action:t.enum(["screenshot","open-url","launch","reset","rebuild"]),url:t.string().optional().describe("For open-url."),requestId:w}},async e=>{const s=u("sim_farm_action");if(s)return s;try{return o(await n("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.url},10*6e4,e.requestId))}catch(a){return c(a)}});r.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, so measure only then. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...f,name:t.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:t.boolean().describe("true = take it, false = free it."),waitSeconds:t.number().int().min(0).max(1800).optional()}},async e=>{const s=u("sim_farm_lock");if(s)return s;try{const a={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return o(await n(e.hold?"lock":"unlock",a,(e.waitSeconds??0)*1e3+3e4))}catch(a){return c(a)}})}async function F(){const{startFarmd:r}=await import("./src-XWK66ACL.mjs");const e=await r({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!e)process.exit(0);for(const s of["SIGTERM","SIGINT"])process.on(s,()=>void e.stop(s))}export{D as registerSimFarmTools,F as runFarmd};
@@ -1,6 +0,0 @@
1
- import{createRequire as Ve}from"module";const Qt=Ve(import.meta.url);var M=1;var U=1;var Y={parkAfterMinutes:10,maxConcurrentBoots:3,activeSlots:6,reserveRamGB:24,reserveDiskGB:40,slimSimGB:2,stockSimGB:6,cloneDiskGB:2,freezePilot:false};var l=class extends Error{constructor(e,s,t,i=false){super(s);this.code=e;this.next=t;this.retryable=i;this.name="FarmError"}toJSON(){return{code:this.code,message:this.message,next:this.next,retryable:this.retryable}}};import{createHash as Ze}from"crypto";var ue=["com.apple.sharingd"];var V=[{"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 fe(){const e=new Set(ue);const s=new Set;for(const t of V)for(const i of t.labels)if(!e.has(i))s.add(i);return[...s].sort()}function F(e){return e==="slim"?fe():[]}function we(e){return Ze("sha256").update(e+"\n"+F(e).join("\n")).digest("hex").slice(0,16)}import{promises as D,existsSync as ye}from"fs";import I from"path";import{createHash as et,randomBytes as Z}from"crypto";import{promises as T}from"fs";import ee from"path";import{randomBytes as tt}from"crypto";import{spawn as st}from"child_process";var j=new Set;function te(){return j.size}function it(){for(const e of j){try{e.kill("SIGKILL")}catch{}}}function v(e,s,t={}){const{timeoutMs:i=3e4,maxBytes:a=4*1024*1024}=t;return new Promise(o=>{const n=st(e,s,{env:t.env??process.env,stdio:[t.input==null?"ignore":"pipe","pipe","pipe"]});j.add(n);let r="";let c="";let p=false;n.stdout.on("data",h=>{if(r.length<a)r+=h.toString("utf8")});n.stderr.on("data",h=>{if(c.length<a)c+=h.toString("utf8")});if(t.input!=null)n.stdin.end(t.input);const m=setTimeout(()=>{p=true;n.kill("SIGTERM");setTimeout(()=>n.kill("SIGKILL"),2e3).unref?.()},i);n.on("error",h=>{j.delete(n);clearTimeout(m);o({code:null,stdout:r,stderr:c+String(h),timedOut:p})});n.on("close",h=>{j.delete(n);clearTimeout(m);o({code:h,stdout:r,stderr:c,timedOut:p})})})}async function b(e,s,t={}){const i=await v(e,s,t);if(i.timedOut)throw new Error(`${e} ${s.slice(0,3).join(" ")} timed out after ${t.timeoutMs??3e4} ms`);if(i.code!==0)throw new Error(`${e} ${s.slice(0,3).join(" ")} failed (${i.code}): ${(i.stderr||i.stdout).trim().slice(0,500)}`);return i.stdout}async function ge(){const e=await b("ps",["-axww","-o","pid=,ppid=,lstart=,stat=,comm="],{timeoutMs:15e3});const s=await b("ps",["-axww","-o","pid=,command="],{timeoutMs:15e3});const t=new Map;for(const a of s.split("\n")){const o=/^\s*(\d+)\s+(.*)$/.exec(a);if(o)t.set(Number(o[1]),o[2])}const i=[];for(const a of e.split("\n")){const o=/^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(\S+)\s+(.*)$/.exec(a);if(!o)continue;const n=Number(o[1]);i.push({pid:n,ppid:Number(o[2]),start:o[3].replace(/\s+/g," "),stat:o[4],comm:o[5].trim(),command:t.get(n)??o[5].trim()})}return i}function Se(e,s){const t=new Map;const i=new Map;for(const c of e){i.set(c.pid,c);const p=t.get(c.ppid)??[];p.push(c);t.set(c.ppid,p)}const a=[];const o=i.get(s);if(!o)return a;const n=[o];const r=new Set;while(n.length){const c=n.pop();if(r.has(c.pid))continue;r.add(c.pid);a.push(c);for(const p of t.get(c.pid)??[])n.push(p)}return a}async function H(e){const s=await v("ps",["-o","lstart=","-p",String(e)],{timeoutMs:5e3});const t=s.stdout.trim().replace(/\s+/g," ");return s.code===0&&t?t:null}async function x(e,s){if(!Number.isInteger(e)||e<=0)return false;try{process.kill(e,0)}catch(t){if(t.code!=="EPERM")return false}if(!s)return true;return await H(e)===s}var B=e=>new Promise(s=>setTimeout(s,e));var R=process.env.BUOY_FARM_DISABLED_ROOT||"/private/var/tmp";function ke(e,s=R){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return ee.join(s,`com.apple.CoreSimulator.SimDevice.${e}`)}function ve(e,s=R){return ee.join(ke(e,s),"disabled.plist")}async function be(e,s=R){const t=ve(e,s);let i;try{i=await T.lstat(t)}catch{return{}}if(!i.isFile())throw new Error(`refusing ${t}: not a regular file`);const a=await b("plutil",["-convert","json","-o","-",t],{timeoutMs:1e4});const o=JSON.parse(a);const n={};for(const[r,c]of Object.entries(o))if(typeof c==="boolean")n[r]=c;return n}function at(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function nt(e){const s=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 i of s)t+=` <key>${at(i)}</key>
2
- <${e[i]?"true":"false"}/>
3
- `;return t+"</dict>\n</plist>\n"}async function Oe(e,s,t=R){const i=ke(e,t);await T.mkdir(i,{recursive:true});const a=await T.lstat(i);if(!a.isDirectory())throw new Error(`refusing ${i}: not a directory`);const o=await be(e,t);const n=ee.join(i,"buoy-farm-prior.json");let r={};try{r=JSON.parse(await T.readFile(n,"utf8"))}catch{}const c={...o};for(const p of s){if(!(p in r))r[p]=p in o?o[p]:null;c[p]=true}await Me(n,JSON.stringify(r,null,2)+"\n");await Me(ve(e,t),nt(c))}async function ot(e,s,t=R){const i=await be(e,t);return s.every(a=>i[a]===true)}async function Me(e,s){const t=`${e}.${tt(4).toString("hex")}.tmp`;const i=await T.open(t,"w",420);try{await i.writeFile(s);await i.sync()}finally{await i.close()}await T.rename(t,e)}import{promises as O,existsSync as q,readFileSync as as}from"fs";import y from"path";import{createHash as Ee}from"crypto";var Ie=["@react_buoy_sync_device_id.json","@react_buoy_install.json"];var rt="@buoy~1floating-tool~1*";function se(e){if(!q(y.join(e,"seed.json")))return`No seed at ${e}. Run: pnpm e2e:pool seed`;if(!q(y.join(e,"Documents/buoy-license/buoy-gg-license.json"))){return`The seed at ${e} has no license cache. A sim would register a new Keygen machine.`}if(!q(y.join(e,"AsyncStorage/manifest.json")))return`The seed at ${e} has no AsyncStorage manifest.`;return null}async function ct(e){const s=Ee("sha256");async function t(i){let a;try{a=await O.readdir(i,{withFileTypes:true})}catch{return}a.sort((o,n)=>o.name.localeCompare(n.name));for(const o of a){const n=y.join(i,o.name);if(o.isDirectory())await t(n);else{const r=await O.stat(n);s.update(`${y.relative(e,n)}:${r.size}:${Math.floor(r.mtimeMs)}
4
- `)}}}await t(e);return s.digest("hex").slice(0,16)}async function lt(e){const s=Ee("sha256");const t=y.join(e,"Info.plist");s.update(await O.readFile(t));const i=JSON.parse(await b("plutil",["-convert","json","-o","-",t],{timeoutMs:1e4}));const a=y.join(e,String(i.CFBundleExecutable??""));const o=await O.stat(a);s.update(`${o.size}:${Math.floor(o.mtimeMs)}`);return s.digest("hex").slice(0,16)}async function dt(e){const s=JSON.parse(await b("plutil",["-convert","json","-o","-",y.join(e,"Info.plist")],{timeoutMs:1e4}));if(!s.CFBundleIdentifier)throw new Error(`${e} has no CFBundleIdentifier`);return String(s.CFBundleIdentifier)}async function ie(e,s){const t=y.join(e,"Containers/Data/Application");let i;try{i=await O.readdir(t)}catch{return null}for(const a of i){const o=y.join(t,a,".com.apple.mobile_container_manager.metadata.plist");if(!q(o))continue;const n=await v("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",o],{timeoutMs:5e3});if(n.code===0&&n.stdout.trim()===s)return y.join(t,a)}return null}function pt(e,s){return y.join(e,"Library/Application Support",s,"RCTAsyncLocalStorage_V1")}async function Be(e,s,t){await O.mkdir(s,{recursive:true});const i=["-a","--delete"];for(const a of t)i.push("--exclude",a);i.push(e.replace(/\/?$/,"/"),s.replace(/\/?$/,"/"));await b("rsync",i,{timeoutMs:6e4})}async function $e(e,s,t){const i=se(t);if(i)throw new Error(i);const a=[...Ie.map(n=>`react-buoy/${n}`),`react-buoy/${rt}`];await Be(y.join(t,"Documents"),y.join(e,"Documents"),a);await Be(y.join(t,"AsyncStorage"),pt(e,s),[]);const o=y.join(e,"Documents/react-buoy");try{for(const n of await O.readdir(o)){if(n.startsWith("@buoy~1floating-tool~1"))await O.rm(y.join(o,n),{force:true,recursive:true})}}catch{}}async function De(e){const s=[];const t=y.join(e,"Documents/react-buoy");let i=[];try{i=await O.readdir(t)}catch{return s}for(const a of i){if(Ie.includes(a)||a.startsWith("@buoy~1floating-tool~1")){await O.rm(y.join(t,a),{force:true,recursive:true});s.push(a)}}return s}async function ht(e){try{const s=await O.readFile(y.join(e,"Documents/react-buoy/@react_buoy_sync_device_id.json"),"utf8");const t=JSON.parse(s);return typeof t==="string"?t:typeof t?.value==="string"?t.value:s.trim()}catch{return null}}import{promises as mt,existsSync as ae,mkdirSync as ne,readFileSync as $,renameSync as oe,writeFileSync as W,rmSync as N,openSync as ut,fsyncSync as ft,closeSync as wt,chmodSync as yt,readdirSync as gt,statSync as Te}from"fs";import St from"os";import S from"path";import{createHash as kt,randomBytes as L}from"crypto";function E(){return process.env.BUOY_FARM_HOME||S.join(St.homedir(),".buoy","farm")}var k=(e=E())=>({home:e,lock:S.join(e,"owner.lock"),lockOwner:S.join(e,"owner.lock","owner.json"),socket:S.join(e,"farmd.sock"),token:S.join(e,"token"),state:S.join(e,"state.json"),journal:S.join(e,"journal.ndjson"),log:S.join(e,"farmd.log"),shots:S.join(e,"shots")});function re(e=E()){ne(e,{recursive:true,mode:448});try{yt(e,448)}catch{}}function xe(e=E()){const s=k(e).token;try{const t=$(s,"utf8").trim();if(t)return t}catch{}re(e);try{W(s,L(24).toString("hex")+"\n",{mode:384,flag:"wx"})}catch(t){if(t.code!=="EEXIST")throw t}for(let t=0;t<20;t++){const i=$(s,"utf8").trim();if(i)return i;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,25)}throw new Error(`farm token at ${s} is empty`)}async function z(e=E()){try{return JSON.parse(await mt.readFile(k(e).lockOwner,"utf8"))}catch{return null}}async function J(e){return!!e&&await x(e.pid,e.start)}async function vt(e,s){const t=S.join(e,"owner.claim");for(let i=0;i<20;i++){try{ne(t,{mode:448});W(S.join(t,"pid"),`${process.pid}
5
- ${await H(process.pid)??""}`);try{return await s()}finally{N(t,{recursive:true,force:true})}}catch(a){if(a.code!=="EEXIST")throw a;const o=Te(t,{throwIfNoEntry:false});if(o&&Date.now()-o.mtimeMs>3e4){let n=true;try{const[r,c]=$(S.join(t,"pid"),"utf8").split("\n");n=!await x(Number(r),c||void 0)}catch{}if(n){try{oe(t,S.join(e,`owner.claim-dead-${L(4).toString("hex")}`))}catch{}}}await new Promise(n=>setTimeout(n,100))}}return null}async function bt(e,s,t=E()){re(t);const i=k(t);const a=await H(process.pid)??"";for(let o=0;o<3;o++){try{ne(i.lock,{mode:448})}catch(r){if(r.code!=="EEXIST")throw r;const c=await z(t);if(c&&await J(c))return null;if(!c){await new Promise(h=>setTimeout(h,300));const m=await z(t);if(m&&await J(m))return null}const p=await vt(t,async()=>{const m=await z(t);if(m&&await J(m))return false;if(!ae(i.lock))return true;oe(i.lock,S.join(t,`owner.stale-${L(4).toString("hex")}`));return true});if(p===false)return null;continue}const n={pid:process.pid,start:a,version:e,host:s,since:new Date().toISOString()};W(i.lockOwner,JSON.stringify(n),{mode:384});Ot(t);return{info:n,release(){try{const r=JSON.parse($(i.lockOwner,"utf8"));if(r.pid!==process.pid)return}catch{return}try{N(i.socket,{force:true})}catch{}N(i.lock,{recursive:true,force:true})}}}return null}function Ot(e){try{for(const s of gt(e)){if(s.startsWith("owner.stale-")||s.startsWith("owner.claim-dead-"))N(S.join(e,s),{recursive:true,force:true})}}catch{}}function Re(){return{version:U,seq:0,farmId:L(6).toString("hex"),nextIndex:1,templates:{},sims:{},leases:{},locks:{},requests:{},events:[],settings:{...Y}}}var Ae=e=>kt("sha256").update(JSON.stringify(e)).digest("hex").slice(0,12);function _e(e,s,t){const i=ut(e,t,384);try{W(i,s);ft(i)}finally{wt(i)}}var je=class{constructor(e=E()){this.pending=[];this.open=new Map;this.p=k(e);re(e);this.state=this.load()}load(){let e=Re();if(ae(this.p.state)){const i=JSON.parse($(this.p.state,"utf8"));if(i.version>U){throw new Error(`farm state is version ${i.version}; this farmd understands ${U}. Update Buoy.`)}e={...Re(),...i,settings:{...Y,...i.settings}}}const s=new Map;const t=new Set;if(ae(this.p.journal)){for(const i of $(this.p.journal,"utf8").split("\n")){if(!i.trim())continue;let a;try{a=JSON.parse(i)}catch{continue}const{sum:o,...n}=a;if(Ae(n)!==o||a.seq<=e.seq)continue;if(a.type==="intent")s.set(a.seq,a);else{s.delete(a.seq);t.add(a.seq)}}}for(const i of e.openIntents??[]){if(!t.has(i.seq)&&!s.has(i.seq))s.set(i.seq,{...i,type:"intent",sum:""})}this.pending=[...s.values()].sort((i,a)=>i.seq-a.seq);for(const i of this.pending)this.open.set(i.seq,i);e.openIntents=[];return e}intent(e,s){const t=++this.state.seq;const i={seq:t,at:new Date().toISOString(),type:"intent",op:e,data:s};this.append(i);this.open.set(t,{...i,sum:""});return t}result(e,s,t){this.open.delete(e);this.pending=this.pending.filter(i=>i.seq!==e);this.save();this.append({seq:e,at:new Date().toISOString(),type:"result",op:s,data:t})}append(e){_e(this.p.journal,JSON.stringify({...e,sum:Ae(e)})+"\n","a")}save(){const e=`${this.p.state}.${L(4).toString("hex")}.tmp`;this.state.openIntents=[...this.open.values()].map(({seq:s,at:t,op:i,data:a})=>({seq:s,at:t,op:i,data:a}));_e(e,JSON.stringify(this.state,null,1),"w");oe(e,this.p.state);try{if(Te(this.p.journal).size>2*1024*1024)N(this.p.journal,{force:true})}catch{}}event(e){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)}};function Mt(e=E()){try{const s=JSON.parse($(k(e).state,"utf8"));return new Set([...Object.keys(s.sims??{}),...Object.values(s.templates??{}).map(t=>t.udid)])}catch{return new Set}}var A=1024**3;var ce="Buoy Farm ";var Ne=".buoy-farm.json";var Et=120;var It=180;var le=9e4;var Le=500;var Bt={now:()=>performance.now()};var Pe=class{constructor(){this.tails=new Map}run(e,s){const t=this.tails.get(e)??Promise.resolve();const i=t.catch(()=>{}).then(s);this.tails.set(e,i);i.finally(()=>{if(this.tails.get(e)===i)this.tails.delete(e)}).catch(()=>{});return i}};var $t=class{constructor(e){this.free=e;this.waiting=[]}async use(e){if(this.free>0)this.free--;else await new Promise(s=>this.waiting.push(s));try{return await e()}finally{const s=this.waiting.shift();if(s)s();else this.free++}}};var Ge=class{constructor(e){this.simLock=new Pe;this.deadlines=new Map;this.reservations=new Map;this.busy=new Set;this.ticking=false;this.hardDeadlines=new Map;this.acquiring=new Set;this.budgetLock=new Pe;this.inflight=new Map;this.pendingBg=0;this.host=e.host;this.store=e.store;this.clock=e.clock??Bt;this.log=e.log??(()=>{});this.pollMs=e.pollMs??1e3;this.readyTimeoutMs=e.readyTimeoutMs??12e4;this.bootGate=new $t(this.state.settings.maxConcurrentBoots)}get state(){return this.store.state}async recover(){const e=await this.host.listDevices();const s=new Map(e.map(t=>[t.udid,t]));for(const t of Object.values(this.state.sims)){const i=s.get(t.udid);if(!i){this.store.event({kind:"lost",message:`${t.name} was deleted outside the farm.`,udid:t.udid});if(t.leaseId)this.dropFromLease(t.leaseId,t.udid);delete this.state.sims[t.udid];continue}if(i.state==="Booted"){t.power=t.power==="frozen"?"frozen":"awake"}else{t.power=t.power==="parked"?"parked":"cold"}if(["creating","booting","thawing","deleting"].includes(t.power))t.power=i.state==="Booted"?"awake":"cold"}for(const t of Object.values(this.state.templates)){const i=s.get(t.udid);if(!i){t.usable=false;t.note="Its sim was deleted outside the farm."}else if(!ye(t.appPath)){t.note="Its app build is gone. Clones still work; rebuild the template for a new build."}}for(const t of this.store.pending){if(t.op==="clone"){const i=t.data?.name;const a=e.find(o=>o.name===i&&!this.state.sims[o.udid]);if(a){this.store.event({kind:"orphan",message:`Found ${a.name} with no record after a crash. Held for review; the farm will not use or delete it.`,udid:a.udid})}}this.store.result(t.seq,t.op,{recovered:true})}for(const t of Object.values(this.state.leases)){this.deadlines.set(t.id,this.clock.now()+le);const i=Date.parse(t.createdAt)+t.ttlMs-Date.now();this.hardDeadlines.set(t.id,this.clock.now()+Math.max(i,0));if(i<=0)this.background(`expire ${t.id}`,()=>this.endLease(t.id,"ran out of time while the farm was down","full"))}for(const t of Object.values(this.state.sims)){if(t.health==="dirty"&&!t.leaseId){this.busy.add(t.udid);this.background(`resume reset ${t.name}`,async()=>{try{await this.rebuildSim(t.udid)}finally{this.busy.delete(t.udid)}})}}this.store.save()}async dataPath(e){const s=(await this.host.listDevices()).find(t=>t.udid===e);if(!s)throw new l("NOT_FOUND",`No sim ${e} on this Mac.`);return s.dataPath}async isOwned(e){const s=this.state.sims[e]??Object.values(this.state.templates).find(t=>t.udid===e);if(!s)return false;try{const t=JSON.parse(await D.readFile(I.join(await this.dataPath(e),Ne),"utf8"));return t.farmId===this.state.farmId}catch{return false}}async assertOwned(e){if(!await this.isOwned(e)){throw new l("NOT_OWNED",`Sim ${e} is not a farm sim. The farm never changes sims it did not make.`)}}async writeMarker(e,s){const t=await this.dataPath(e);await D.mkdir(t,{recursive:true});await D.writeFile(I.join(t,Ne),JSON.stringify({farmId:this.state.farmId,...s})+"\n")}farmAwakeGB(){const e=this.state.settings;let s=0;for(const t of Object.values(this.state.sims)){if(["awake","booting","frozen","thawing"].includes(t.power))s+=t.profile==="slim"?e.slimSimGB:e.stockSimGB}return s}async budget(e={ramGB:0,diskGB:0}){const s=this.state.settings;const t=await this.host.hostStats();let i=0;let a=0;for(const h of this.reservations.values()){i+=h.ramGB;a+=h.diskGB}const o=t.availableMemBytes/A-i;const n=t.totalMemBytes/A-s.reserveRamGB-this.farmAwakeGB()-i;const r=t.freeDiskBytes/A-a-s.reserveDiskGB;const c=Math.max(0,Math.floor(Math.min(o-4,n,r/s.cloneDiskGB*s.slimSimGB)/s.slimSimGB));const p=(h,_)=>({ok:false,code:h,reason:_,stats:t,roomFor:c});if(e.diskGB>0&&r-e.diskGB<0){return p("DISK_LOW",`Need ${s.reserveDiskGB} GB free after this. This Mac has ${Math.floor(t.freeDiskBytes/A)} GB.`)}if(e.ramGB>0){if(t.memPressure!=="normal")return p("RAM_LOW","This Mac is low on memory. New sims will wait.");if(o-e.ramGB<4||n-e.ramGB<0){return p("RAM_LOW",`Not enough free memory for another sim (${Math.max(0,Math.floor(o))} GB free).`)}}const m=this.state.locks["perf-slot"];if(m&&(e.ramGB>0||e.diskGB>0))return p("HOST_BUSY","A speed test is running. New sims will wait.");return{ok:true,stats:t,roomFor:c}}async withReservation(e,s){const t=Z(4).toString("hex");await this.budgetLock.run("budget",async()=>{const i=await this.budget(e);if(!i.ok)throw new l(i.code,i.reason,void 0,true);this.reservations.set(t,e)});try{return await s()}finally{this.reservations.delete(t)}}async health(e,s=[]){const t=[];const i=await this.host.simTree(e);if(i.length===0)return{ok:false,profile:"unknown",faults:["launchd_sim is not running"],procs:0};const a=new Set(i.map(r=>r.comm.split("/").pop()));for(const r of["SpringBoard","backboardd"])if(!a.has(r))t.push(`${r} is not running`);const o=Ce(await this.host.launchdLogTail(e));if(o)t.push(`${o} keeps crashing`);if(t.length===0){const r=I.join(k().shots,`${e}.png`);await D.mkdir(I.dirname(r),{recursive:true});if(!await this.host.screenshot(e,r,1e4))t.push("screenshot did not come back")}let n=i.length<Et?"slim":i.length>It?"stock":"unknown";if(t.length===0&&s.length){const r=await this.host.spawn(e,["launchctl","print-disabled","system"],2e4);if(r.code===0&&!r.timedOut){const c=new Set([...r.stdout.matchAll(/"([^"]+)" => (?:disabled|true)/g)].map(m=>m[1]));const p=s.filter(m=>!c.has(m)).length;n=p===0?"slim":p===s.length?"stock":"unknown"}else{n="unknown"}}return{ok:t.length===0,profile:n,faults:t,procs:i.length}}async waitHealthy(e,s=[]){const t=performance.now()+this.readyTimeoutMs;let i=await this.health(e,s);while(!i.ok&&performance.now()<t){await B(this.pollMs);i=await this.health(e,s)}return i}async createTemplate(e){const s=se(e.seedDir);if(s)throw new l("NO_SEED",s,"Run: pnpm e2e:pool seed");if(!ye(I.join(e.appPath,"Info.plist"))){throw new l("BAD_REQUEST",`No app build at ${e.appPath}.`,"Build the app first.")}const t=await dt(e.appPath);const i=`t-${Z(3).toString("hex")}`;const a=e.name??`${ce}Template ${i.slice(2)}`;return this.withReservation({ramGB:this.state.settings.stockSimGB,diskGB:4},async()=>{const o=this.store.intent("create-template",{id:i,name:a});const n=await this.host.create(a,e.deviceType,e.runtime);const r={id:i,udid:n,name:a,bundleId:t,appPath:e.appPath,appHash:await lt(e.appPath),seedDir:e.seedDir,seedHash:await ct(e.seedDir),deviceType:e.deviceType,runtime:e.runtime,xcodeBuild:await this.host.xcodeBuild(),profile:e.profile,profileHash:we(e.profile),metroPort:e.metroPort,createdAt:new Date().toISOString(),usable:false,note:"Being built."};this.state.templates[i]=r;await this.writeMarker(n,{template:i});this.store.result(o,"create-template",{udid:n});try{await this.buildTemplate(r,e.warmLaunch!==false);r.usable=true;r.note=void 0;this.store.event({kind:"template",message:`Template ${a} is ready.`,udid:n})}catch(c){r.note=`Build failed: ${c.message}`;this.store.event({kind:"template-failed",message:r.note,udid:n});await this.host.shutdown(n).catch(()=>{});throw c}finally{this.store.save()}return r})}async buildTemplate(e,s){const t=F(e.profile);if(t.length)await Oe(e.udid,t,this.host.disabledRoot);await this.host.boot(e.udid);const i=await this.waitHealthy(e.udid,t);if(!i.ok)throw new Error(`template did not come up: ${i.faults.join("; ")}`);if(e.profile==="slim"&&i.profile!=="slim")throw new Error(`slim did not apply (${i.procs} processes)`);await this.host.install(e.udid,e.appPath);const a=await this.host.appContainer(e.udid,e.bundleId);if(!a)throw new Error("app installed but has no data container");await $e(a,e.bundleId,e.seedDir);await this.setPrefs(e.udid,e);if(s){await this.host.launch(e.udid,e.bundleId);await B(Math.min(15e3,this.pollMs*15));await this.host.terminate(e.udid,e.bundleId)}await this.host.shutdown(e.udid);await De(a)}async setPrefs(e,s){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",s.bundleId,"RCT_jsLocation",`localhost:${s.metroPort}`])}nextName(){const e=new Set(Object.values(this.state.sims).map(t=>t.name));let s=this.state.nextIndex;while(e.has(`${ce}${String(s).padStart(2,"0")}`))s++;this.state.nextIndex=s+1;return`${ce}${String(s).padStart(2,"0")}`}template(e){const s=this.state.templates[e];if(!s)throw new l("NO_TEMPLATE",`No template ${e}.`,"Run sim_farm_setup to make one.");if(!s.usable)throw new l("NO_TEMPLATE",`Template ${s.name} can't be used: ${s.note??"not ready"}.`);return s}async cloneSim(e,s=this.nextName()){const t=this.template(e);return this.withReservation({ramGB:0,diskGB:this.state.settings.cloneDiskGB},()=>this.simLock.run(`template:${t.id}`,async()=>{await this.assertOwned(t.udid);const i=this.store.intent("clone",{templateId:e,name:s});const a=await this.host.clone(t.udid,s);const o={udid:a,name:s,templateId:e,power:"creating",profile:"unknown",health:"ok",bootEpoch:0,failures:0,createdAt:new Date().toISOString()};this.state.sims[a]=o;this.store.result(i,"clone",{udid:a});await this.writeMarker(a,{template:t.id});const n=await ie(await this.dataPath(a),t.bundleId);if(n)await De(n);o.power="cold";this.store.event({kind:"clone",message:`Made ${s}.`,udid:a});this.store.save();return o}))}async bootSim(e,s){await this.assertOwned(e);const t=this.sim(e);if(t.power==="frozen"||t.power==="thawing"){throw new l("BAD_REQUEST",`${t.name} is frozen. Thaw it first.`)}const i=this.state.templates[t.templateId];const a=this.state.settings;const o={ramGB:i?.profile==="slim"?a.slimSimGB:a.stockSimGB,diskGB:0};return this.withReservation(o,()=>this.bootGate.use(()=>this.simLock.run(e,async()=>{this.assertLeaseState(t,s);if(t.power==="awake")return t;const n=i?F(i.profile):[];if(n.length&&!await ot(e,n,this.host.disabledRoot)){await Oe(e,n,this.host.disabledRoot)}t.power="booting";t.bootEpoch++;this.store.save();await this.host.boot(e);const r=await this.waitHealthy(e,i?.profile==="slim"?n:[]);t.lastCheckAt=new Date().toISOString();t.profile=r.profile;t.power="awake";if(!r.ok){t.failures++;t.health=t.failures>=2?"broken":"suspect";t.lastError=r.faults.join("; ");this.store.event({kind:"unhealthy",message:`${t.name}: ${t.lastError}`,udid:e});this.store.save();throw new l("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(i?.profile==="slim"&&r.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 l("HOST_ERROR",`${t.name}: can't tell if slim applied.`,"The farm will rebuild it.",true)}if(i?.profile==="slim"&&r.profile==="stock"){this.store.event({kind:"stock",message:`${t.name}: slim didn't work here (${r.profile}). It uses full RAM.`,udid:e});const c=await this.budget({ramGB:a.stockSimGB-a.slimSimGB,diskGB:0});if(!c.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 l("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,s){if(!this.state.sims[e.udid])throw new l("NOT_FOUND",`${e.name} is gone.`);if(s){const t=this.state.leases[s];if(e.leaseId!==s||!t||this.leaseOverdue(t)){throw new l("LEASE_EXPIRED",`${e.name} is no longer in lease ${s}.`)}}else if(e.leaseId){throw new l("BAD_REQUEST",`${e.name} is in use. End its lease first.`)}}async parkSim(e,s="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 l("BAD_REQUEST",`${t.name} is being reset.`);if(t.power==="frozen"||t.power==="thawing")throw new l("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} (${s}).`,udid:e});this.store.save()})}sim(e){const s=this.state.sims[e];if(!s)throw new l("NOT_OWNED",`Sim ${e} is not a farm sim.`);return s}async deleteSim(e,s){await this.assertOwned(e);const t=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(t,s);if(t.power==="frozen"||t.power==="thawing")throw new l("BAD_REQUEST",`${t.name} is frozen. Wake it first.`);t.power="deleting";const i=this.store.intent("delete",{udid:e});await this.host.shutdown(e).catch(()=>{});await this.host.delete(e);await D.rm(I.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true});delete this.state.sims[e];this.store.result(i,"delete",{});this.store.event({kind:"delete",message:`Deleted ${t.name}.`,udid:e})})}async rebuildSim(e,s){const t=this.sim(e);const{name:i,templateId:a}=t;await this.deleteSim(e,s);return this.cloneSim(a,i)}async fastReset(e,s){await this.assertOwned(e);const t=this.sim(e);const i=this.template(t.templateId);await this.simLock.run(e,async()=>{this.assertLeaseState(t,s);if(t.power==="frozen"||t.power==="thawing")throw new l("BAD_REQUEST",`${t.name} is frozen. Wake it first.`);if(t.power==="awake")await this.host.terminate(e,i.bundleId);const a=await ie(await this.dataPath(e),i.bundleId);if(!a)throw new Error("app container not found");await $e(a,i.bundleId,i.seedDir);t.health="ok";this.store.save()})}async grow(e,s){if(!Number.isInteger(s)||s<1||s>40)throw new l("BAD_REQUEST","count must be 1 to 40.");const t=[];for(let i=0;i<s;i++)t.push(await this.cloneSim(e));return t}async shrink(e,s){if(!Number.isInteger(s)||s<1||s>64)throw new l("BAD_REQUEST","count must be 1 to 64.");const t=["cold","parked","awake"];const i=Object.values(this.state.sims).filter(o=>o.templateId===e&&!o.leaseId&&t.includes(o.power)&&!this.busy.has(o.udid)).sort((o,n)=>t.indexOf(o.power)-t.indexOf(n.power));const a=[];for(const o of i.slice(0,s)){try{await this.deleteSim(o.udid);a.push(o.name)}catch(n){if(!(n instanceof l))throw n}}return a}leaseOverdue(e){const s=this.clock.now();if(s>(this.hardDeadlines.get(e.id)??Infinity))return true;return!this.acquiring.has(e.id)&&s>(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){const s=this.template(e.templateId);if(!Number.isInteger(e.count)||e.count<1||e.count>64)throw new l("BAD_REQUEST","count must be 1 to 64.");const t={id:`l-${Z(4).toString("hex")}`,epoch:1,owner:e.owner,purpose:e.purpose,templateId:s.id,sims:[],createdAt:new Date().toISOString(),ttlMs:Math.min(Math.max(e.ttlMs??15*6e4,3e4),24*36e5)};this.state.leases[t.id]=t;this.hardDeadlines.set(t.id,this.clock.now()+t.ttlMs);this.acquiring.add(t.id);const i=()=>this.deadlines.set(t.id,this.clock.now()+Math.min(t.ttlMs,le));i();const a=performance.now()+(e.waitMs??0);const o=()=>{if(!this.state.leases[t.id])throw new l("LEASE_EXPIRED","The lease ended while sims were being prepared.");if(this.leaseOverdue(t))throw new l("LEASE_EXPIRED","The lease ran out of time while sims were being prepared.")};try{while(t.sims.length<e.count){o();i();const n=await this.claimOne(t,s);o();if(n)continue;if(performance.now()>=a)break;await B(Math.max(this.pollMs,2e3))}if(t.sims.length<e.count&&!e.allowPartial){const n=await this.budget({ramGB:this.state.settings.slimSimGB,diskGB:0});throw new l(n.code??"HOST_BUSY",`Got ${t.sims.length} of ${e.count} sims. ${n.reason??"Others are in use."}`,"Try again later or ask for fewer.",true)}if(t.sims.length===0)throw new l("HOST_BUSY","No sim is free right now.","Try again later.",true);if(e.launch){await Promise.all(t.sims.map(n=>this.host.launch(n,s.bundleId)));i();await this.checkIdentities(t,6e4);o()}i();this.store.event({kind:"lease",message:`${e.owner.label} took ${t.sims.length} sim(s) for ${e.purpose}.`,leaseId:t.id});this.store.save();return{lease:t,sims:t.sims.map(n=>this.state.sims[n])}}catch(n){await this.endLease(t.id,"acquire failed","fast").catch(()=>{});throw n}finally{this.acquiring.delete(t.id)}}async claimOne(e,s){const t=Object.values(this.state.sims).filter(n=>n.templateId===s.id&&!n.leaseId&&n.health==="ok"&&!this.busy.has(n.udid));const i=t.find(n=>n.power==="awake")??t.find(n=>n.power==="parked"||n.power==="cold");const a=n=>{n.leaseId=e.id;e.sims.push(n.udid);n.idleSince=void 0;this.store.save();return n};if(i?.power==="awake"){if(!await this.isOwned(i.udid)){i.health="broken";i.lastError="farm marker missing";this.store.save();return null}return a(i)}let o=null;try{if(i){o=a(i)}else{const n=await this.budget({ramGB:s.profile==="slim"?this.state.settings.slimSimGB:this.state.settings.stockSimGB,diskGB:this.state.settings.cloneDiskGB});if(!n.ok)return null;o=a(await this.cloneSim(s.id))}await this.bootSim(o.udid,e.id);return o}catch(n){if(o)this.unclaim(e,o.udid);if(n instanceof l&&(n.code==="RAM_LOW"||n.code==="DISK_LOW"||n.code==="HOST_BUSY"||n.code==="LEASE_EXPIRED")){return null}if(o&&this.state.sims[o.udid]?.health!=="ok"){const r=o.udid;this.background(`rebuild ${r}`,()=>this.rebuildSim(r))}throw n}}unclaim(e,s){e.sims=e.sims.filter(i=>i!==s);const t=this.state.sims[s];if(t&&t.leaseId===e.id)t.leaseId=void 0;this.store.save()}async checkIdentities(e,s=6e4){const t=this.template(e.templateId);const i=performance.now()+s;const a=new Map;for(;;){for(const n of e.sims){if(a.has(n))continue;const r=await ie(await this.dataPath(n),t.bundleId);const c=r?await ht(r):null;if(!c)continue;const p=[...a.entries()].find(([,h])=>h===c);if(p)throw new l("ID_CLASH",`${this.state.sims[n].name} and ${this.state.sims[p[0]].name} share one Buoy ID.`);const m=Object.values(this.state.sims).find(h=>h.udid!==n&&!e.sims.includes(h.udid)&&h.buoyDeviceId===c);if(m)throw new l("ID_CLASH",`${this.state.sims[n].name} and ${m.name} share one Buoy ID.`);a.set(n,c);this.state.sims[n].buoyDeviceId=c}if(a.size===e.sims.length||performance.now()>=i)break;await B(Math.max(this.pollMs,1e3))}const o=e.sims.filter(n=>!a.has(n));if(o.length){this.store.event({kind:"no-id",message:`No Buoy ID yet on ${o.map(n=>this.state.sims[n].name).join(", ")}. Is the app loading? Check Metro.`,leaseId:e.id})}}liveLease(e,s,t){const i=this.state.leases[e];if(!i)throw new l("LEASE_EXPIRED",`Lease ${e} has ended.`,"Acquire new sims.");if(i.epoch!==s)throw new l("STALE_EPOCH",`Lease ${e} is at epoch ${i.epoch}, not ${s}.`);if(t&&i.owner.clientId!==t)throw new l("NOT_OWNED",`Lease ${e} belongs to ${i.owner.label}.`);if(this.leaseOverdue(i)){this.background(`expire ${i.id}`,()=>this.endLease(i.id,"expired","full"));throw new l("LEASE_EXPIRED",`Lease ${e} has expired.`,"Acquire new sims.")}return i}renew(e,s,t){const i=this.liveLease(e,s,t);const a=Math.min(this.clock.now()+Math.min(i.ttlMs,le),this.hardDeadlines.get(i.id)??Infinity);this.deadlines.set(i.id,a);return{expiresInMs:Math.round(a-this.clock.now())}}async release(e,s,t="fast",i){this.liveLease(e,s,i);return this.endLease(e,"released",t)}async endLease(e,s,t){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[o,n]of Object.entries(this.state.locks))if(n.leaseId===e)delete this.state.locks[o];const a={};for(const o of i.sims){const n=this.state.sims[o];if(!n||n.leaseId!==e)continue;n.leaseId=void 0;n.health="dirty";n.idleSince=new Date().toISOString();this.busy.add(o);a[n.name]="resetting"}this.store.event({kind:"lease-end",message:`${i.owner.label}'s ${i.purpose} ended (${s}).`,leaseId:e});this.store.save();this.background(`reset after ${e}`,async()=>{for(const o of i.sims){if(!this.state.sims[o])continue;try{const n=Date.parse(i.createdAt)-2e3;for(const r of await this.host.clientsOf(o)){if(Date.parse(r.start)>=n)await this.host.kill(r.pid,r.start)}if(t==="full"||this.state.sims[o].failures>0)await this.rebuildSim(o);else await this.fastReset(o)}catch(n){this.store.event({kind:"reset-failed",message:`${this.state.sims[o]?.name??o}: ${n.message}. Rebuilding.`,udid:o});await this.rebuildSim(o).catch(()=>{})}finally{this.busy.delete(o)}}});return a}dropFromLease(e,s){const t=this.state.leases[e];if(t)t.sims=t.sims.filter(i=>i!==s)}async action(e,s,t,i,a,o){const n=this.liveLease(e,s,o);const r={};for(const c of t){if(!n.sims.includes(c))throw new l("NOT_OWNED",`${c} is not in lease ${e}.`);await this.assertOwned(c);this.liveLease(e,s,o);const p=this.template(n.templateId);switch(i){case"screenshot":{const m=this.state.locks["perf-slot"];if(m&&m.leaseId!==n.id)throw new l("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);const h=I.join(k().shots,`${c}.png`);r[c]=await this.host.screenshot(c,h)?h:null;break}case"open-url":if(!a)throw new l("BAD_REQUEST","open-url needs a url.");await this.host.openUrl(c,a);r[c]="opened";break;case"launch":await this.host.launch(c,p.bundleId);r[c]="launched";break;case"reset":await this.fastReset(c,n.id);r[c]="reset";break;case"rebuild":{const m=await this.rebuildSim(c,n.id);if(!this.state.leases[n.id])throw new l("LEASE_EXPIRED",`Lease ${e} ended during the rebuild.`);m.leaseId=n.id;n.sims=n.sims.map(h=>h===c?m.udid:h);await this.bootSim(m.udid,n.id);r[c]=m.udid;break}default:throw new l("BAD_REQUEST",`Unknown action ${i}.`)}}this.store.save();return r}async lock(e,s,t,i=0,a){if(!/^[a-z0-9-]{1,40}$/.test(e))throw new l("BAD_REQUEST","Lock names are short lowercase words.");const o=performance.now()+i;for(;;){this.liveLease(s,t,a);const n=this.state.locks[e];const r=e==="perf-slot"&&(this.reservations.size>0||this.busy.size>0||this.pendingBg>0);if((!n||n.leaseId===s)&&!r){this.state.locks[e]={name:e,leaseId:s,since:new Date().toISOString()};this.store.save();return{held:true}}if(performance.now()>=o){if(!n||n.leaseId===s)return{held:false,holder:"the farm (sims are still starting or resetting)"};const c=this.state.leases[n.leaseId];return{held:false,holder:c?.owner.label??"someone",since:n.since}}await B(Math.max(this.pollMs,1e3))}}unlock(e,s,t,i){this.liveLease(s,t,i);if(this.state.locks[e]?.leaseId===s)delete this.state.locks[e];this.store.save();return{held:false}}async shot(e){await this.assertOwned(e);const s=this.sim(e);if(this.state.locks["perf-slot"])throw new l("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(s.power!=="awake")throw new l("BAD_REQUEST",`${s.name} is ${s.power}.`);const t=I.join(k().shots,`${e}.png`);await D.mkdir(I.dirname(t),{recursive:true});if(!await this.host.screenshot(e,t,8e3))throw new l("HOST_ERROR",`No still from ${s.name} in time.`,void 0,true);return{file:t,at:new Date().toISOString()}}async show(e){await this.assertOwned(e);const s=this.sim(e);if(s.power==="frozen"||s.power==="thawing")throw new l("BAD_REQUEST",`${s.name} is paused. Wake it first.`);if(s.power!=="awake")await this.bootSim(e,s.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 i=!await x(t.owner.pid,t.owner.pidStart);if(i)await this.endLease(t.id,"its owner exited","full");else if(this.leaseOverdue(t))await this.endLease(t.id,"no renewal","full")}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(i=>this.log(`park ${t.name}: ${i.message}`))}}}const s=Object.keys(this.state.requests);if(s.length>Le)for(const t of s.slice(0,s.length-Le))delete this.state.requests[t]}finally{this.ticking=false}}async drain(e=10*6e4){const s=performance.now()+e;while((this.busy.size>0||this.pendingBg>0)&&performance.now()<s)await B(200)}idle(){return Object.keys(this.state.leases).length===0&&this.busy.size===0&&!Object.values(this.state.sims).some(e=>["awake","booting","frozen","thawing"].includes(e.power))}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,s){this.pendingBg++;s().catch(t=>{this.log(`${e}: ${t.message}`);this.store.event({kind:"error",message:`${e}: ${t.message}`})}).finally(()=>this.pendingBg--)}async once(e,s,t,i){if(!s)return i();const a=`${e}:${s}`;const o=et("sha256").update(JSON.stringify(t)).digest("hex").slice(0,16);const n=this.state.requests[a];if(n){if(n.hash!==o)throw new l("REQUEST_REUSED",`Request ${s} was already used for a different call.`);return n.result}const r=this.inflight.get(a);if(r){if(r.hash!==o)throw new l("REQUEST_REUSED",`Request ${s} was already used for a different call.`);return r.promise}const c=i();this.inflight.set(a,{hash:o,promise:c});try{const p=await c;this.state.requests[a]={hash:o,result:p,at:new Date().toISOString()};this.store.save();return p}finally{this.inflight.delete(a)}}async status(e={}){const s=await this.budget();const t=Object.values(this.state.sims).map(a=>{const o=a.leaseId?this.state.leases[a.leaseId]:void 0;return{...a,owner:o?.owner.label,purpose:o?.purpose,busy:this.busy.has(a.udid)}});const i={farmId:this.state.farmId,counts:{pool:t.length,awake:t.filter(a=>a.power==="awake").length,leased:t.filter(a=>a.leaseId).length},budget:{roomForMoreSims:s.roomFor,freeDiskGB:Math.floor(s.stats.freeDiskBytes/A),availableMemGB:Math.floor(s.stats.availableMemBytes/A),memPressure:s.stats.memPressure,loadAvg1:Number(s.stats.loadAvg1.toFixed(1)),cpuCount:s.stats.cpuCount},templates:Object.values(this.state.templates),sims:t,leases:Object.values(this.state.leases).map(a=>({...a,expiresInMs:Math.max(0,Math.round(this.leaseDeadline(a)-this.clock.now()))})),locks:Object.values(this.state.locks),recentEvents:this.state.events.slice(-25),settings:this.state.settings};if(e.outside){const a=new Set([...Object.keys(this.state.sims),...Object.values(this.state.templates).map(o=>o.udid)]);i.outsideSims=(await this.host.listDevices()).filter(o=>!a.has(o.udid)&&o.available).map(o=>({udid:o.udid,name:o.name,state:o.state,runtime:o.runtime}))}return i}setSettings(e){const s=this.state.settings;const t={parkAfterMinutes:[0,24*60],maxConcurrentBoots:[1,8],activeSlots:[1,64],reserveRamGB:[4,512],reserveDiskGB:[5,2048],slimSimGB:[.5,16],stockSimGB:[1,32],cloneDiskGB:[.1,32]};if(!e||typeof e!=="object")throw new l("BAD_REQUEST","settings must be an object.");for(const[i,a]of Object.entries(e)){if(!(i in s)||typeof a!==typeof s[i])throw new l("BAD_REQUEST",`Bad setting ${i}.`);const o=t[i];if(o&&(typeof a!=="number"||!Number.isFinite(a)||a<o[0]||a>o[1])){throw new l("BAD_REQUEST",`${i} must be ${o[0]} to ${o[1]}.`)}}for(const[i,a]of Object.entries(e))s[i]=a;this.store.save();return s}};function Ce(e,s=Date.now()-3*6e4){const t=new Map;for(const i of e.split("\n")){const a=/^(\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(i);if(!a||a[3]==="SIGKILL"||a[3]==="SIGTERM")continue;const o=Date.parse(a[1].replace(" ","T"));if(!(o>=s))continue;const n=t.get(a[2])??[];n.push(o);t.set(a[2],n)}for(const[i,a]of t){a.sort((o,n)=>o-n);for(let o=2;o<a.length;o++)if(a[o]-a[o-2]<=6e4)return i.replace(/^com\.apple\./,"")}return null}import{promises as Dt}from"fs";import P from"os";import de from"path";var Ue=6e4;var Fe=class{constructor(){this.disabledRoot=R;this.devicesDir=de.join(P.homedir(),"Library/Developer/CoreSimulator/Devices")}simctl(e,s=Ue){return b("xcrun",["simctl",...e],{timeoutMs:s})}async listDevices(){const e=JSON.parse(await this.simctl(["list","devices","--json"],3e4));const s=[];for(const[t,i]of Object.entries(e.devices??{})){for(const a of i){s.push({udid:a.udid,name:a.name,state:a.state,runtime:t,deviceType:a.deviceTypeIdentifier,dataPath:a.dataPath??de.join(this.devicesDir,a.udid,"data"),available:a.isAvailable!==false})}}return s}async create(e,s,t){return(await this.simctl(["create",e,s,t])).trim()}async clone(e,s){return(await this.simctl(["clone",e,s])).trim()}async boot(e){const s=await v("xcrun",["simctl","boot",e],{timeoutMs:Ue});if(s.code!==0&&!/current state: Booted/i.test(s.stderr)){throw new Error(`simctl boot ${e} failed: ${s.stderr.trim().slice(0,300)}`)}}async shutdown(e){const s=await v("xcrun",["simctl","shutdown",e],{timeoutMs:9e4});if(s.code!==0&&!/current state: Shutdown/i.test(s.stderr)){throw new Error(`simctl shutdown ${e} failed: ${s.stderr.trim().slice(0,300)}`)}}async delete(e){await this.simctl(["delete",e],9e4)}async install(e,s){await this.simctl(["install",e,s],18e4)}async launch(e,s){await this.simctl(["launch","--terminate-running-process",e,s],6e4)}async terminate(e,s){await v("xcrun",["simctl","terminate",e,s],{timeoutMs:2e4})}async openUrl(e,s){await this.simctl(["openurl",e,s],2e4)}async screenshot(e,s,t=1e4){const i=await v("xcrun",["simctl","io",e,"screenshot",s],{timeoutMs:t});return i.code===0&&!i.timedOut}spawn(e,s,t=2e4){return v("xcrun",["simctl","spawn",e,...s],{timeoutMs:t})}async appContainer(e,s){const t=await v("xcrun",["simctl","get_app_container",e,s,"data"],{timeoutMs:2e4});return t.code===0?t.stdout.trim()||null:null}async simTree(e){const s=await ge();const t=s.find(i=>/launchd_sim/.test(i.command)&&i.command.includes(e));return t?Se(s,t.pid):[]}async signal(e,s){for(const t of e){try{process.kill(t,s)}catch{}}}async launchdLogTail(e,s=256*1024){const t=de.join(this.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`,"launchd.log");try{const i=await Dt.open(t,"r");try{const{size:a}=await i.stat();const o=Math.max(0,a-s);const n=Buffer.alloc(a-o);await i.read(n,0,n.length,o);return n.toString("utf8")}finally{await i.close()}}catch{return""}}async hostStats(){const[e,s,t,i]=await Promise.all([b("vm_stat",[],{timeoutMs:1e4}),v("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{timeoutMs:5e3}),v("sysctl",["-n","vm.swapusage"],{timeoutMs:5e3}),b("df",["-k",P.homedir()],{timeoutMs:1e4})]);const a=Number(/page size of (\d+)/.exec(e)?.[1]??16384);const o=h=>Number(new RegExp(`${h}:\\s+(\\d+)`).exec(e)?.[1]??0);const n=o("Pages free")+o("Pages inactive")+o("Pages speculative")+o("Pages purgeable");const r=Number(s.stdout.trim());const c=/used = ([\d.]+)M/.exec(t.stdout)?.[1];const p=i.trim().split("\n").pop()??"";const m=Number(p.split(/\s+/)[3]??0);return{totalMemBytes:P.totalmem(),availableMemBytes:n*a,memPressure:r>=4?"critical":r>=2?"warn":"normal",swapUsedBytes:c?Number(c)*1024*1024:0,freeDiskBytes:m*1024,cpuCount:P.cpus().length,loadAvg1:P.loadavg()[0]}}async showWindow(e){await b("open",["-a","Simulator","--args","-CurrentDeviceUDID",e],{timeoutMs:2e4})}async xcodeBuild(){const e=await v("xcodebuild",["-version"],{timeoutMs:2e4});return e.stdout.trim().replace(/\s+/g," ")||"unknown"}async clientsOf(e){const s=await ge();const t=new Set((await this.simTree(e)).map(a=>a.pid));const i=new Set(Se(s,process.pid).map(a=>a.pid));return s.filter(a=>!i.has(a.pid)&&!t.has(a.pid)&&a.command.includes(e)&&!/launchd_sim|CoreSimulator/.test(a.command))}async kill(e,s){if(!await x(e,s))return;try{process.kill(e,"SIGTERM")}catch{return}for(let t=0;t<20;t++){await new Promise(i=>setTimeout(i,100));if(!await x(e,s))return}if(await x(e,s)){try{process.kill(e,"SIGKILL")}catch{}}}};import Tt from"net";import{chmodSync as xt,rmSync as Rt,appendFileSync as At}from"fs";import{timingSafeEqual as _t}from"crypto";var He=new Set(["setup","grow","shrink","acquire","release","action","lock","unlock","park","wake","reset","rebuild","delete","endLease","settings","deleteFarm","show"]);async function jt(e={}){const s=e.home??E();const t=k(s);const i=await bt(M,e.hostLabel??"node",s);if(!i)return null;const a=o=>{try{At(t.log,`${new Date().toISOString()} ${o}
6
- `)}catch{}e.log?.(o)};try{return await Nt(e,s,i,a)}catch(o){i.release();throw o}}async function Nt(e,s,t,i){const a=k(s);const o=xe(s);const n=new je(s);const r=new Ge({host:e.host??new Fe,store:n,log:i,pollMs:e.pollMs,readyTimeoutMs:e.readyTimeoutMs});await r.recover();Rt(a.socket,{force:true});const c=new Set;let p=Date.now();let m=false;let h=0;const _=Tt.createServer(u=>{c.add(u);u.on("close",()=>c.delete(u));u.on("error",()=>{});let d="";u.on("data",w=>{d+=w.toString("utf8");if(d.length>1024*1024)return u.destroy();let f;while((f=d.indexOf("\n"))>=0){const g=d.slice(0,f);d=d.slice(f+1);if(g.trim())void Qe(u,g)}})});const Q=(u,d,w)=>{if(!u.destroyed)u.write(JSON.stringify({id:d,...w})+"\n")};async function Qe(u,d){p=Date.now();let w;try{w=JSON.parse(d)}catch{return Q(u,-1,{ok:false,error:{code:"BAD_REQUEST",message:"Not JSON."}})}h++;try{if(!Lt(w.token,o))throw new l("UNAUTHORIZED","Bad farm token.");if(m&&!["hello","status"].includes(w.method)){throw new l("HOST_BUSY","The farm is stopping.","Try again in a few seconds.",true)}if(w.v>M&&He.has(w.method)){throw new l("FARMD_VERSION",`This farm runs protocol ${M}; the client speaks ${w.v}.`,"Ask it to stop when idle (method shutdown), then start a newer farm.")}if(w.v<M&&He.has(w.method)){throw new l("READ_ONLY",`This client speaks farm protocol ${w.v}; the farm runs ${M}.`,"Update Buoy to change the farm.")}const f=await Ke(w);Q(u,w.id,{ok:true,result:f})}catch(f){const g=f instanceof l?f.toJSON():{code:"HOST_ERROR",message:f.message,retryable:false};Q(u,w.id,{ok:false,error:g})}finally{h--}}async function Ke(u){const d=u.params??{};const w=u.client;const f=g=>r.once(w.clientId,u.requestId,{m:u.method,q:d},g);switch(u.method){case"hello":return{protocol:M,pid:process.pid,host:t.info.host,farmId:r.state.farmId};case"status":return r.status({outside:!!d.outside});case"budget":return r.budget({ramGB:Number(d.ramGB??0),diskGB:Number(d.diskGB??0)});case"setup":return f(()=>r.createTemplate(d));case"grow":return f(async()=>(await r.grow(d.templateId,Number(d.count))).map(g=>g.name));case"shrink":return f(()=>r.shrink(d.templateId,Number(d.count)));case"acquire":return f(async()=>{const{lease:g,sims:Ye}=await r.acquire({...d,owner:w});return{leaseId:g.id,epoch:g.epoch,sims:Ye.map(C=>({udid:C.udid,name:C.name,buoyDeviceId:C.buoyDeviceId,profile:C.profile}))}});case"renew":return r.renew(d.leaseId,d.epoch,w.clientId);case"release":return f(()=>r.release(d.leaseId,d.epoch,d.reset,w.clientId));case"action":return f(()=>r.action(d.leaseId,d.epoch,d.udids,d.action,d.arg,w.clientId));case"lock":return r.lock(d.name,d.leaseId,d.epoch,Number(d.waitMs??0),w.clientId);case"unlock":return r.unlock(d.name,d.leaseId,d.epoch,w.clientId);case"park":return f(async()=>(await r.parkSim(d.udid,"asked"),"parked"));case"wake":return f(async()=>(await r.bootSim(d.udid)).power);case"reset":return f(async()=>(await r.fastReset(d.udid),"reset"));case"rebuild":return f(async()=>(await r.rebuildSim(d.udid)).udid);case"delete":return f(async()=>(await r.deleteSim(d.udid),"deleted"));case"endLease":{const g=r.state.leases[d.leaseId];if(!g)return{};if(g.owner.clientId!==w.clientId&&d.force!==true){throw new l("NOT_OWNED",`Lease ${d.leaseId} belongs to ${g.owner.label}.`,"Pass force: true only after a person confirms.")}return f(()=>r.endLease(d.leaseId,`ended by ${w.label}`,"full"))}case"settings":return r.setSettings(d);case"shot":return r.shot(d.udid);case"show":return r.show(d.udid);case"shutdown":if(d.ifIdle&&!r.idle())return{stopping:false,reason:"busy"};setTimeout(()=>void X("asked to stop"),50);return{stopping:true};default:throw new l("BAD_REQUEST",`Unknown method ${u.method}.`)}}await new Promise((u,d)=>{_.once("error",d);_.listen(a.socket,()=>u())});xt(a.socket,384);i(`farmd ${M} up, pid ${process.pid}, host ${e.hostLabel??"node"}`);const me=setInterval(()=>{void r.tick().catch(d=>i(`tick: ${d.message}`));const u=Date.now()-p;if(e.idleExitMs&&c.size===0&&u>e.idleExitMs){void(async()=>{await r.parkAllIdle();if(r.idle())await X("idle")})()}},e.tickMs??5e3);me.unref?.();let K=null;function X(u,d=10*6e4){if(!K)K=Xe(u,d);return K}async function Xe(u,d){if(m)return;m=true;clearInterval(me);i(`farmd stopping: ${u}`);const w=Date.now()+d;while(h>0&&Date.now()<w)await new Promise(f=>setTimeout(f,200));await r.drain(Math.max(0,w-Date.now()));if(h>0||te()>0){i(`farmd stopping with ${h} call(s) and ${te()} child process(es) still running; killing them`);it();const f=Date.now()+5e3;while(te()>0&&Date.now()<f)await new Promise(g=>setTimeout(g,100))}try{await r.parkAllIdle()}catch{}for(const f of c)f.destroy();await new Promise(f=>_.close(()=>f()));n.save();t.release();e.onExit?.(u)}return{engine:r,owner:t,stop:X}}function Lt(e,s){if(typeof e!=="string"||e.length!==s.length)return false;return _t(Buffer.from(e),Buffer.from(s))}import Pt from"net";import{existsSync as Gt}from"fs";import{spawn as Ct}from"child_process";import{randomBytes as qe}from"crypto";function Ut(e,s=["farmd"]){return()=>{const t=Ct(process.execPath,[...process.execArgv,e,...s],{detached:true,stdio:"ignore",env:{...process.env}});t.unref()}}var We=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??E();this.clientIdValue=`c-${qe(5).toString("hex")}`}get clientId(){return this.owner?.clientId}async connect(){this.token=xe(this.home);this.owner={clientId:this.clientIdValue,pid:process.pid,pidStart:await H(process.pid)??void 0,label:this.opts.label};if(await this.tryOpen())return this.checkVersion();const e=await z(this.home);if(!await J(e)){if(!this.opts.launcher)throw new l("HOST_ERROR","The farm is not running.","Start it from Buoy Desktop or `pnpm farm status`.");this.opts.launcher()}const s=Date.now()+(this.opts.startTimeoutMs??2e4);while(Date.now()<s){await B(250);if(await this.tryOpen())return this.checkVersion()}throw new l("HOST_ERROR","The farm did not start in time.",`Check ${k(this.home).log}.`)}async checkVersion(){const e=await this.send("hello",void 0,{});if(e.protocol<M&&this.opts.launcher){const s=await this.send("shutdown",{ifIdle:true},{});if(s.stopping){this.close();await B(500);return this.connect()}}return this}tryOpen(){const e=k(this.home).socket;if(!Gt(e))return Promise.resolve(false);return new Promise(s=>{const t=Pt.createConnection(e);t.once("connect",()=>{this.sock=t;t.on("data",i=>this.onData(i));t.on("close",()=>this.onClose());t.on("error",()=>{});s(true)});t.once("error",()=>s(false))})}onData(e){this.buf+=e.toString("utf8");let s;while((s=this.buf.indexOf("\n"))>=0){const t=this.buf.slice(0,s);this.buf=this.buf.slice(s+1);let i;try{i=JSON.parse(t)}catch{continue}const a=this.pending.get(i.id);if(!a)continue;this.pending.delete(i.id);clearTimeout(a.timer);if(i.ok)a.resolve(i.result);else{const o=i.error??{};a.reject(new l(o.code??"HOST_ERROR",o.message??"farm error",o.next,o.retryable))}}}onClose(){this.sock=null;for(const[,e]of this.pending){clearTimeout(e.timer);e.reject(new l("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,s,t={}){if(!this.sock&&!this.closed)await this.reconnect();return this.send(e,s,t)}send(e,s,t){if(!this.sock)return Promise.reject(new l("HOST_ERROR","Not connected to the farm.",void 0,true));const i=this.nextId++;const a={id:i,v:M,token:this.token,client:this.owner,method:e,params:s,requestId:t.requestId};return new Promise((o,n)=>{const r=setTimeout(()=>{this.pending.delete(i);n(new l("HOST_ERROR",`Farm call ${e} timed out.`,void 0,true))},t.timeoutMs??3e4);this.pending.set(i,{resolve:o,reject:n,timer:r});this.sock.write(JSON.stringify(a)+"\n")})}async acquire(e){const{requestId:s,...t}=e;const i=s??`acq-${qe(4).toString("hex")}`;const a=await this.call("acquire",t,{timeoutMs:15*6e4+(e.waitMs??0),requestId:i});if(this.renewals.has(a.leaseId))return a;const o=setInterval(()=>{this.call("renew",{leaseId:a.leaseId,epoch:a.epoch}).catch(n=>{if(n.code==="LEASE_EXPIRED"||n.code==="STALE_EPOCH")this.stopRenew(a.leaseId)})},15e3);o.unref?.();this.renewals.set(a.leaseId,o);return a}stopRenew(e){const s=this.renewals.get(e);if(s)clearInterval(s);this.renewals.delete(e)}async release(e,s,t="fast"){this.stopRenew(e);return this.call("release",{leaseId:e,epoch:s,reset:t},{timeoutMs:6e4,requestId:`rel-${e}-${s}`})}close(){this.closed=true;for(const e of[...this.renewals.keys()])this.stopRenew(e);this.sock?.end();this.sock=null}};async function Ft(e){return new We(e).connect()}import G from"path";import Ht from"os";import{existsSync as pe,readdirSync as qt,statSync as Wt}from"fs";function he(e=process.cwd()){let s=e;for(;;){if(pe(G.join(s,"buoy.tools.json")))return s;const t=G.dirname(s);if(t===s)return null;s=t}}function ze(){const e=G.join(Ht.homedir(),"Library/Developer/Xcode/DerivedData");let s=null;try{for(const t of qt(e)){if(!t.startsWith("exampledevbuild-"))continue;const i=G.join(e,t,"Build/Products/Debug-iphonesimulator/exampledevbuild.app");if(!pe(i))continue;const a=Wt(i).mtimeMs;if(!s||a>s.t)s={p:i,t:a}}}catch{}return s?.p??null}function Je(e=he()){return e?G.join(e,"example-dev-build/artifacts/pool-seed"):null}async function zt(e=he()){const s=Je(e);let t=[];let i=[];try{const a=JSON.parse(await b("xcrun",["simctl","list","runtimes","--json"],{timeoutMs:3e4}));t=(a.runtimes??[]).filter(n=>n.isAvailable!==false&&/^iOS/.test(n.name??"")).map(n=>({id:n.identifier,name:n.name}));const o=JSON.parse(await b("xcrun",["simctl","list","devicetypes","--json"],{timeoutMs:3e4}));i=(o.devicetypes??[]).filter(n=>/iPhone|iPad/.test(n.name??"")).map(n=>({id:n.identifier,name:n.name}))}catch{}return{appPath:ze(),seedDir:s&&pe(s)?s:null,seedProblem:s?se(s):"Could not find the repo's seed folder.",runtimes:t,deviceTypes:i,slimTurnsOff:V.map(a=>({name:a.name,downside:a.downside}))}}export{M as PROTOCOL_VERSION,U as STATE_VERSION,Y as DEFAULT_SETTINGS,l as FarmError,ue as ALWAYS_ENABLED,V as SLIM_CATEGORIES,fe as slimLabels,F as profileLabels,we as profileHash,E as farmHome,k as paths,je as Store,Mt as farmOwnedUdids,Ge as Engine,Ce as crashLoop,Fe as SimctlHost,jt as startFarmd,Ut as nodeLauncher,We as FarmClient,Ft as connectFarm,he as findRepoRoot,ze as findExampleBuild,Je as defaultSeedDir,zt as farmDefaults};