@buoy-gg/mcp 7.0.64 → 7.0.66

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.
@@ -0,0 +1,2 @@
1
+ import{createRequire as S}from"module";const U=S(import.meta.url);import{proRequired as I,readEntitlement as x}from"./chunk-VKWX4XAY.mjs";import{FarmError as b,agentLabel as T,connectFarm as v,detectAgent as D,nodeLauncher as P}from"./chunk-Y7SNXKA2.mjs";import{z as t}from"zod";var F=r=>({content:[{type:"text",text:r}]});var l=r=>({isError:true,content:[{type:"text",text:r}]});var i=r=>F(JSON.stringify(r,null,2));var c=r=>x().isPro?null:l(I(r));var d=null;var y=()=>void 0;function h(){if(!d){d=D({clientName:y()}).catch(()=>void 0).then(r=>v({label:process.env.BUOY_AGENT_LABEL||(r?T(r):"Buoy MCP"),agent:r,launcher:P(process.argv[1],["farmd"])})).catch(r=>{d=null;throw r})}return d}async function n(r,e,s,o){return(await h()).call(r,e,{timeoutMs:s,requestId:o})}function u(r){if(r instanceof b&&r.code==="RUN_STOPPED"){return l('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(r instanceof b)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.");var g=t.string().describe("The runId from sim_farm_acquire.");function R(r){y=()=>r.server.getClientVersion()?.name;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 i(await n("status",{outside:!!e}))}catch(s){return u(s)}});r.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: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(64).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().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:t.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: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().min(1).max(65535).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=c("sim_farm_setup");if(s)return s;try{if(e.mode==="create"){const a={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,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(!a.appPath)return l("create needs appPath.");if(a.seedDir===void 0)return l("create needs seedDir, or seedDir: null for an app with no seed.");if(e.dryRun)return i({wouldCreateTemplate:a,budget:await n("budget",{ramGB:6,diskGB:4})});return i(await n("setup",a,15*6e4,e.requestId))}let o=e.templateId;if(!o){const a=await n("status");o=a.templates.filter(p=>p.usable).pop()?.id;if(!o)return l("No usable template yet. Run sim_farm_setup with mode create first.")}const m=e.count??1;if(e.dryRun){const a=await n("budget",{ramGB:0,diskGB:e.mode==="grow"?2*m:0});return i({mode:e.mode,templateId:o,count:m,roomForMoreSims:a.roomFor,ok:a.ok,reason:a.reason})}return i(await n(e.mode,{templateId:o,count:m},30*6e4,e.requestId))}catch(o){return u(o)}});r.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). 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:t.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:t.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'."),labels:t.array(t.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:t.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:t.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:t.string().optional().describe("Old name for goal. Use goal."),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("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId:w}},async e=>{const s=c("sim_farm_acquire");if(s)return s;try{let o=e.templateId;if(!o){const k=await n("status");o=k.templates.filter(_=>_.usable).pop()?.id;if(!o)return l("No usable template yet. Run sim_farm_setup with mode create first.")}const m=(e.goal??e.purpose??"").trim();if(!m)return l("Give a goal: one short sentence about what you are doing.");const a=await h();const p=await a.acquire({templateId:o,count:e.count,purpose:m,goal:m,labels:e.labels,kind:e.kind,runId:e.runId,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});return i(p)}catch(o){return u(o)}});r.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:{...f,reset:t.enum(["fast","full"]).optional(),keepRun:t.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async e=>{try{const s=await h();return i(await s.release(e.leaseId,e.epoch,e.reset??"fast",e.keepRun===true))}catch(s){return u(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=c("sim_farm_action");if(s)return s;try{return i(await n("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.url},10*6e4,e.requestId))}catch(o){return u(o)}});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=c("sim_farm_lock");if(s)return s;try{const o={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return i(await n(e.hold?"lock":"unlock",o,(e.waitSeconds??0)*1e3+3e4))}catch(o){return u(o)}});r.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:g,udid:t.string().optional().describe("The sim this is about. Leave out for the whole run."),state:t.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:t.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:t.object({done:t.number().int().min(0),total:t.number().int().min(0),passed:t.number().int().min(0).optional(),failed:t.number().int().min(0).optional()}).optional(),resultSource:t.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:t.number().int().min(0).optional()}},async e=>{try{return i(await n("report",e,3e4))}catch(s){return u(s)}});r.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:g,minutes:t.number().int().min(1).max(1440).describe("Minutes from now.")}},async e=>{try{return i(await n("extend",e))}catch(s){return u(s)}});r.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:g,outcome:t.enum(["passed","failed","mixed","unknown"]).optional(),summary:t.string().max(600).optional()}},async e=>{try{return i(await n("finish",e,6e4,`fin-${e.runId}`))}catch(s){return u(s)}})}async function q(){const{startFarmd:r}=await import("./src-XCBUDSR7.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{R as registerSimFarmTools,q as runFarmd};
@@ -1,4 +1,4 @@
1
- import{createRequire as x}from"module";const B=x(import.meta.url);import{PACKAGE_NAME as l,VERSION as y}from"./chunk-EY5YNHSL.mjs";import{spawnSync as p}from"child_process";import i from"fs";import _ from"os";import a from"path";function h(){return{BUOY_VERIFY:"auto"}}var v="https://registry.npmjs.org";var m=3e3;function j(e={}){const t=e.cwd??process.cwd();const r=e.skillSrcDir??a.resolve(__dirname,"../skill");const n=e.log??(c=>process.stdout.write(c+"\n"));const o=E(e,n);const s=[];f(a.join(t,".mcp.json"),"mcpServers",o,t,s);i.mkdirSync(a.join(t,".cursor"),{recursive:true});f(a.join(t,".cursor","mcp.json"),"mcpServers",o,t,s);if(i.existsSync(a.join(t,".vscode"))){f(a.join(t,".vscode","mcp.json"),"servers",o,t,s)}F(r,t,s);k(a.join(t,"CLAUDE.md"),t,s);k(a.join(t,"AGENTS.md"),t,s);n("Buoy MCP configuration written:");for(const c of s)n(" \u2022 "+c);n("");n("Next steps:");n(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");n(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");n(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");if(o.command==="npx"){n("");n(`The editor launches the server with \`npx -y ${l}@latest\`, so it stays`);n(`Registry access may be needed. Re-run \`npx -y ${l}@latest init\` to refresh configuration.`)}else if(!e.cliPath){n("");n(`This uses a pinned local install (no network on startup). To update it later,`);n(`To update the local package, run: npx -y ${o.args[0]?.replace(/dist[/\\]cli\.js$/,"")??""}@latest init --local`)}}function E(e,t){if(e.cliPath){return{type:"stdio",command:"node",args:[e.cliPath],env:h()}}const r=e.mode??"auto";if(r==="npx"){return g()}if(r==="local"){return b(e,t)}const n=C();if(n.reachable){return g()}t(`\u26A0 npm registry ${n.registry?`(${n.registry}) `:""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`);return b(e,t)}function g(){return{type:"stdio",command:"npx",args:["-y",`${l}@latest`],env:h()}}function b(e,t){const r=e.installDir??process.env.BUOY_MCP_HOME??a.join(_.homedir(),".buoy-mcp");const n=e.registry??v;const o=A(r,n,t);if(o){return{type:"stdio",command:"node",args:[o],env:h()}}t("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");return g()}function A(e,t,r){const n=`${l}@${y==="0.0.0-dev"?"latest":y}`;i.mkdirSync(e,{recursive:true});const o=process.platform==="win32"?"npm.cmd":"npm";r(`Installing ${n} into ${e} from ${t} \u2026`);const s=p(o,["install",n,"--prefix",e,"--registry",t,`--@buoy-gg:registry=${t}`,"--no-audit","--no-fund","--loglevel=error"],{stdio:["ignore","ignore","inherit"],encoding:"utf8"});if(s.status!==0){r(`\u26A0 npm install exited with code ${s.status??"?"}${s.error?` (${s.error.message})`:""}.`);return null}const c=a.join(e,"node_modules",l,"dist","cli.js");if(!i.existsSync(c)){r(`\u26A0 install finished but ${c} is missing.`);return null}return c}function C(){const e=P();if(!e)return{reachable:false,registry:null};if(/^https?:\/\/registry\.npmjs\.org\/?$/i.test(e)){return{reachable:true,registry:e}}return{reachable:R(e),registry:e}}function P(){const e=w("@buoy-gg:registry");if(e)return e;const t=w("registry");return t??v}function w(e){const t=process.platform==="win32"?"npm.cmd":"npm";const r=p(t,["config","get",e],{encoding:"utf8",timeout:m});if(r.status!==0||typeof r.stdout!=="string")return null;const n=r.stdout.trim();if(!n||n==="undefined"||n==="null")return null;return n}function R(e){const t=p(process.execPath,["-e",I,e,String(m)],{encoding:"utf8",timeout:m+2e3});return t.status===0}var I=`
1
+ import{createRequire as x}from"module";const B=x(import.meta.url);import{PACKAGE_NAME as l,VERSION as y}from"./chunk-EPYLVJ7F.mjs";import{spawnSync as p}from"child_process";import i from"fs";import _ from"os";import a from"path";function h(){return{BUOY_VERIFY:"auto"}}var v="https://registry.npmjs.org";var m=3e3;function j(e={}){const t=e.cwd??process.cwd();const r=e.skillSrcDir??a.resolve(__dirname,"../skill");const n=e.log??(c=>process.stdout.write(c+"\n"));const o=A(e,n);const s=[];f(a.join(t,".mcp.json"),"mcpServers",o,t,s);i.mkdirSync(a.join(t,".cursor"),{recursive:true});f(a.join(t,".cursor","mcp.json"),"mcpServers",o,t,s);if(i.existsSync(a.join(t,".vscode"))){f(a.join(t,".vscode","mcp.json"),"servers",o,t,s)}F(r,t,s);k(a.join(t,"CLAUDE.md"),t,s);k(a.join(t,"AGENTS.md"),t,s);n("Buoy MCP configuration written:");for(const c of s)n(" \u2022 "+c);n("");n("Next steps:");n(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");n(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");n(" 3. Confirm the device and tools are available. Free accounts get the basic MCP tools; Pro adds the rest.");if(o.command==="npx"){n("");n(`The editor launches the server with \`npx -y ${l}@latest\`, so it stays`);n(`Registry access may be needed. Re-run \`npx -y ${l}@latest init\` to refresh configuration.`)}else if(!e.cliPath){n("");n(`This uses a pinned local install (no network on startup). To update it later,`);n(`To update the local package, run: npx -y ${o.args[0]?.replace(/dist[/\\]cli\.js$/,"")??""}@latest init --local`)}}function A(e,t){if(e.cliPath){return{type:"stdio",command:"node",args:[e.cliPath],env:h()}}const r=e.mode??"auto";if(r==="npx"){return g()}if(r==="local"){return b(e,t)}const n=C();if(n.reachable){return g()}t(`\u26A0 npm registry ${n.registry?`(${n.registry}) `:""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`);return b(e,t)}function g(){return{type:"stdio",command:"npx",args:["-y",`${l}@latest`],env:h()}}function b(e,t){const r=e.installDir??process.env.BUOY_MCP_HOME??a.join(_.homedir(),".buoy-mcp");const n=e.registry??v;const o=E(r,n,t);if(o){return{type:"stdio",command:"node",args:[o],env:h()}}t("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");return g()}function E(e,t,r){const n=`${l}@${y==="0.0.0-dev"?"latest":y}`;i.mkdirSync(e,{recursive:true});const o=process.platform==="win32"?"npm.cmd":"npm";r(`Installing ${n} into ${e} from ${t} \u2026`);const s=p(o,["install",n,"--prefix",e,"--registry",t,`--@buoy-gg:registry=${t}`,"--no-audit","--no-fund","--loglevel=error"],{stdio:["ignore","ignore","inherit"],encoding:"utf8"});if(s.status!==0){r(`\u26A0 npm install exited with code ${s.status??"?"}${s.error?` (${s.error.message})`:""}.`);return null}const c=a.join(e,"node_modules",l,"dist","cli.js");if(!i.existsSync(c)){r(`\u26A0 install finished but ${c} is missing.`);return null}return c}function C(){const e=R();if(!e)return{reachable:false,registry:null};if(/^https?:\/\/registry\.npmjs\.org\/?$/i.test(e)){return{reachable:true,registry:e}}return{reachable:I(e),registry:e}}function R(){const e=w("@buoy-gg:registry");if(e)return e;const t=w("registry");return t??v}function w(e){const t=process.platform==="win32"?"npm.cmd":"npm";const r=p(t,["config","get",e],{encoding:"utf8",timeout:m});if(r.status!==0||typeof r.stdout!=="string")return null;const n=r.stdout.trim();if(!n||n==="undefined"||n==="null")return null;return n}function I(e){const t=p(process.execPath,["-e",P,e,String(m)],{encoding:"utf8",timeout:m+2e3});return t.status===0}var P=`
2
2
  const url = process.argv[1];
3
3
  const ms = Number(process.argv[2]) || 3000;
4
4
  const c = new AbortController();
@@ -14,8 +14,9 @@ fetch(url, { method: 'HEAD', signal: c.signal })
14
14
  `;function f(e,t,r,n,o){let s={};if(i.existsSync(e)){try{s=JSON.parse(i.readFileSync(e,"utf8"))}catch{i.copyFileSync(e,e+".bak");o.push(`existing ${u(e,n)} was invalid JSON \u2014 backed up to ${u(e,n)}.bak`);s={}}}else{i.mkdirSync(a.dirname(e),{recursive:true})}const c=s[t]&&typeof s[t]==="object"?s[t]:{};const $="buoy"in c;c.buoy=r;s[t]=c;i.writeFileSync(e,JSON.stringify(s,null,2)+"\n");o.push(`${$?"updated":"added"} \`buoy\` in ${u(e,n)}`)}var d="# Debugging with Buoy";var T=`${d}
15
15
 
16
16
  Use Buoy to inspect a connected app when runtime evidence helps the task.
17
- Complete account, tool integration and connection setup first. MCP data and
18
- action access requires Pro. Follow the user's scope and repository instructions.
17
+ Complete account, tool integration and connection setup first. A free app gets
18
+ the basic tools (screen, taps, flows, screenshots, logs, request list); Pro adds
19
+ the rest. Follow the user's scope and repository instructions.
19
20
 
20
21
  - \`get_triage\`: summarize available device and tool observations, then choose
21
22
  a relevant tool. Missing data is not proof that no problem occurred.
@@ -1,5 +1,5 @@
1
- import{createRequire as p}from"module";const j=p(import.meta.url);import{execFile as m}from"child_process";import{promisify as d}from"util";import{existsSync as l,readFileSync as h}from"fs";import u from"os";import i from"path";var f=d(m);function y(){const n=process.env.BUOY_SIMCAM_DIR;const e=[...n?[i.join(n,"buoycam")]:[],"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",i.join(u.homedir(),"Applications/Buoy.app/Contents/Resources/simcam/buoycam")];let t=__dirname;for(let o=0;o<8;o++){e.push(i.join(t,"apps/desktop/resources/simcam/buoycam"));const a=i.dirname(t);if(a===t)break;t=a}return e}var s=null;function c(){if(s)return s;s=y().find(n=>l(n))??null;return s}async function g(n){const e=c();if(!e){return{ok:false,error:"The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."}}try{const{stdout:t}=await f(e,[...n,"--json"],{timeout:3e4});return{ok:true,data:JSON.parse(t)}}catch(t){const o=t.stdout??"";try{return{ok:false,data:JSON.parse(o)}}catch{return{ok:false,error:t.message||"the simulator camera CLI failed"}}}}function b(n){const e=n.filter(r=>r.level==="fail");const t=n.filter(r=>r.level==="warn");if(e.length===0&&t.length===0){return"The simulator camera is healthy \u2014 every check passed."}const o=[];for(const r of[...e,...t]){o.push(`- **${r.name}** (${r.level}): ${r.detail}`);if(r.fix)o.push(` ${r.fix}`)}const a=e.length>0?`${e.length} problem${e.length===1?"":"s"} found.`:`No hard failures, but ${t.length} thing${t.length===1?"":"s"} worth knowing.`;return[a,"",...o].join("\n")}function _(n,e){if(n.length===0){return"No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._"}const t=[n.length===1?"1 booted simulator:":`${n.length} booted simulators:`,...n.map(o=>`- **${o.name}** (${o.runtime}) \u2014 \`${o.udid}\``)];if(n.length>1&&e===null){t.push("","_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._");return t.join("\n")}if(e===null){t.push("","_Could not read the installed apps._");return t.join("\n")}if(e.length===0){t.push("","No apps installed on it.");return t.join("\n")}t.push("",`${e.length} installed app${e.length===1?"":"s"}:`,...e.map(o=>`- ${o.name} \u2014 \`${o.bundleId}\``),"","_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._");return t.join("\n")}function $(n,e){const t=[];t.push(n.length?'**Mac cameras** (for `kind: "camera"`):':"**Mac cameras**: none found.");if(n.length){t.push(...n.map(o=>`- ${o.name} \u2014 \`${o.uniqueId}\``))}else{t.push("_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._")}t.push("");t.push(e.length?'**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):':"**Capturable windows**: none found.");if(e.length){t.push(...e.map(o=>`- ${o.app}${o.title?` \u2014 ${o.title}`:""} \u2014 \`window:${o.id}\``))}else{t.push("_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._")}return t.join("\n")}function k(){const n=process.env.BUOY_ENTITLEMENT_FILE;if(n)return[n];return[i.join(u.homedir(),"Library/Application Support/Buoy/entitlement.json"),i.join(u.homedir(),"Library/Application Support/buoy-desktop/entitlement.json")]}function S(){for(const n of k()){try{if(!l(n))continue;const e=JSON.parse(h(n,"utf8"));return{tier:e.tier==="pro"||e.tier==="free"?e.tier:"locked",isPro:e.isPro===true&&e.hasDesktopAccess===true&&typeof e.validUntil==="number"&&Number.isFinite(e.validUntil)&&Date.now()<e.validUntil&&typeof e.proValidUntil==="number"&&Number.isFinite(e.proValidUntil)&&Date.now()<e.proValidUntil}}catch{}}return{tier:"locked",isPro:false}}function v(n){return`\`${n}\` is a Buoy Pro feature.
1
+ import{createRequire as p}from"module";const B=p(import.meta.url);import{execFile as d}from"child_process";import{promisify as h}from"util";import{existsSync as c,readFileSync as f}from"fs";import u from"os";import i from"path";var y=h(d);function g(){const n=process.env.BUOY_SIMCAM_DIR;const e=[...n?[i.join(n,"buoycam")]:[],"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",i.join(u.homedir(),"Applications/Buoy.app/Contents/Resources/simcam/buoycam")];let t=__dirname;for(let o=0;o<8;o++){e.push(i.join(t,"apps/desktop/resources/simcam/buoycam"));const a=i.dirname(t);if(a===t)break;t=a}return e}var s=null;function m(){if(s)return s;s=g().find(n=>c(n))??null;return s}async function b(n){const e=m();if(!e){return{ok:false,error:"The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."}}try{const{stdout:t}=await y(e,[...n,"--json"],{timeout:3e4});return{ok:true,data:JSON.parse(t)}}catch(t){const o=t.stdout??"";try{return{ok:false,data:JSON.parse(o)}}catch{return{ok:false,error:t.message||"the simulator camera CLI failed"}}}}function _(n){const e=n.filter(r=>r.level==="fail");const t=n.filter(r=>r.level==="warn");if(e.length===0&&t.length===0){return"The simulator camera is healthy \u2014 every check passed."}const o=[];for(const r of[...e,...t]){o.push(`- **${r.name}** (${r.level}): ${r.detail}`);if(r.fix)o.push(` ${r.fix}`)}const a=e.length>0?`${e.length} problem${e.length===1?"":"s"} found.`:`No hard failures, but ${t.length} thing${t.length===1?"":"s"} worth knowing.`;return[a,"",...o].join("\n")}function $(n,e){if(n.length===0){return"No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._"}const t=[n.length===1?"1 booted simulator:":`${n.length} booted simulators:`,...n.map(o=>`- **${o.name}** (${o.runtime}) \u2014 \`${o.udid}\``)];if(n.length>1&&e===null){t.push("","_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._");return t.join("\n")}if(e===null){t.push("","_Could not read the installed apps._");return t.join("\n")}if(e.length===0){t.push("","No apps installed on it.");return t.join("\n")}t.push("",`${e.length} installed app${e.length===1?"":"s"}:`,...e.map(o=>`- ${o.name} \u2014 \`${o.bundleId}\``),"","_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._");return t.join("\n")}function k(n,e){const t=[];t.push(n.length?'**Mac cameras** (for `kind: "camera"`):':"**Mac cameras**: none found.");if(n.length){t.push(...n.map(o=>`- ${o.name} \u2014 \`${o.uniqueId}\``))}else{t.push("_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._")}t.push("");t.push(e.length?'**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):':"**Capturable windows**: none found.");if(e.length){t.push(...e.map(o=>`- ${o.app}${o.title?` \u2014 ${o.title}`:""} \u2014 \`window:${o.id}\``))}else{t.push("_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._")}return t.join("\n")}var l=null;function S(n){l=n}function v(){if(l)return[l];return[i.join(u.homedir(),"Library/Application Support/Buoy/entitlement.json"),i.join(u.homedir(),"Library/Application Support/buoy-desktop/entitlement.json")]}function w(){for(const n of v()){try{if(!c(n))continue;const e=JSON.parse(f(n,"utf8"));return{tier:e.tier==="pro"||e.tier==="free"?e.tier:"locked",isPro:e.isPro===true&&e.hasDesktopAccess===true&&typeof e.validUntil==="number"&&Number.isFinite(e.validUntil)&&Date.now()<e.validUntil&&typeof e.proValidUntil==="number"&&Number.isFinite(e.proValidUntil)&&Date.now()<e.proValidUntil}}catch{}}return{tier:"locked",isPro:false}}function j(n){return`\`${n}\` is a Buoy Pro feature.
2
2
 
3
3
  The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel with a Free account. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
4
4
 
5
- _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`}export{c as cliPath,g as run,b as describeDiagnosis,_ as describeDevices,$ as describeInputs,S as readEntitlement,v as proRequired};
5
+ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`}export{m as cliPath,b as run,_ as describeDiagnosis,$ as describeDevices,k as describeInputs,S as setEntitlementFileForTests,w as readEntitlement,j as proRequired};
@@ -0,0 +1,6 @@
1
+ import{createRequire as it}from"module";const ls=it(import.meta.url);var D=1;var z=1;var se={parkAfterMinutes:10,maxConcurrentBoots:3,reserveRamGB:24,reserveDiskGB:40,slimSimGB:2,stockSimGB:6,cloneDiskGB:2,freezePilot:false,runCapMinutes:60,keepRunsHours:24};var l=class extends Error{constructor(e,t,s,i=false){super(t);this.code=e;this.next=s;this.retryable=i;this.name="FarmError"}toJSON(){return{code:this.code,message:this.message,next:this.next,retryable:this.retryable}}};import{createHash as at}from"crypto";var ke=["com.apple.sharingd"];var ie=[{"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 ve(){const e=new Set(ke);const t=new Set;for(const s of ie)for(const i of s.labels)if(!e.has(i))t.add(i);return[...t].sort()}function U(e){return e==="slim"?ve():[]}function Se(e){return at("sha256").update(e+"\n"+U(e).join("\n")).digest("hex").slice(0,16)}import{promises as T,existsSync as be}from"fs";import b from"path";import{AsyncLocalStorage as nt}from"async_hooks";import{createHash as ot,randomBytes as J}from"crypto";import{promises as j}from"fs";import ae from"path";import{randomBytes as rt}from"crypto";import{spawn as ct}from"child_process";var C=new Set;function ne(){return C.size}function lt(){for(const e of C){try{e.kill("SIGKILL")}catch{}}}function k(e,t,s={}){const{timeoutMs:i=3e4,maxBytes:a=4*1024*1024}=s;return new Promise(n=>{const o=ct(e,t,{env:s.env??process.env,stdio:[s.input==null?"ignore":"pipe","pipe","pipe"]});C.add(o);let r="";let c="";let h=false;o.stdout.on("data",u=>{if(r.length<a)r+=u.toString("utf8")});o.stderr.on("data",u=>{if(c.length<a)c+=u.toString("utf8")});if(s.input!=null)o.stdin.end(s.input);const p=setTimeout(()=>{h=true;o.kill("SIGTERM");setTimeout(()=>o.kill("SIGKILL"),2e3).unref?.()},i);o.on("error",u=>{C.delete(o);clearTimeout(p);n({code:null,stdout:r,stderr:c+String(u),timedOut:h})});o.on("close",u=>{C.delete(o);clearTimeout(p);n({code:u,stdout:r,stderr:c,timedOut:h})})})}async function O(e,t,s={}){const i=await k(e,t,s);if(i.timedOut)throw new Error(`${e} ${t.slice(0,3).join(" ")} timed out after ${s.timeoutMs??3e4} ms`);if(i.code!==0)throw new Error(`${e} ${t.slice(0,3).join(" ")} failed (${i.code}): ${(i.stderr||i.stdout).trim().slice(0,500)}`);return i.stdout}async function Oe(){const e=await O("ps",["-axww","-o","pid=,ppid=,lstart=,stat=,comm="],{timeoutMs:15e3});const t=await O("ps",["-axww","-o","pid=,command="],{timeoutMs:15e3});const s=new Map;for(const a of t.split("\n")){const n=/^\s*(\d+)\s+(.*)$/.exec(a);if(n)s.set(Number(n[1]),n[2])}const i=[];for(const a of e.split("\n")){const n=/^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(\S+)\s+(.*)$/.exec(a);if(!n)continue;const o=Number(n[1]);i.push({pid:o,ppid:Number(n[2]),start:n[3].replace(/\s+/g," "),stat:n[4],comm:n[5].trim(),command:s.get(o)??n[5].trim()})}return i}function Ee(e,t){const s=new Map;const i=new Map;for(const c of e){i.set(c.pid,c);const h=s.get(c.ppid)??[];h.push(c);s.set(c.ppid,h)}const a=[];const n=i.get(t);if(!n)return a;const o=[n];const r=new Set;while(o.length){const c=o.pop();if(r.has(c.pid))continue;r.add(c.pid);a.push(c);for(const h of s.get(c.pid)??[])o.push(h)}return a}async function G(e){const t=await k("ps",["-o","lstart=","-p",String(e)],{timeoutMs:5e3});const s=t.stdout.trim().replace(/\s+/g," ");return t.code===0&&s?s:null}async function _(e,t){if(!Number.isInteger(e)||e<=0)return false;try{process.kill(e,0)}catch(s){if(s.code!=="EPERM")return false}if(!t)return true;return await G(e)===t}var $=e=>new Promise(t=>setTimeout(t,e));var L=process.env.BUOY_FARM_DISABLED_ROOT||"/private/var/tmp";function Ie(e,t=L){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return ae.join(t,`com.apple.CoreSimulator.SimDevice.${e}`)}function De(e,t=L){return ae.join(Ie(e,t),"disabled.plist")}async function Ae(e,t=L){const s=De(e,t);let i;try{i=await j.lstat(s)}catch{return{}}if(!i.isFile())throw new Error(`refusing ${s}: not a regular file`);const a=await O("plutil",["-convert","json","-o","-",s],{timeoutMs:1e4});const n=JSON.parse(a);const o={};for(const[r,c]of Object.entries(n))if(typeof c==="boolean")o[r]=c;return o}function dt(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function ht(e){const t=Object.keys(e).sort();let s='<?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 t)s+=` <key>${dt(i)}</key>
2
+ <${e[i]?"true":"false"}/>
3
+ `;return s+"</dict>\n</plist>\n"}async function Me(e,t,s=L){const i=Ie(e,s);await j.mkdir(i,{recursive:true});const a=await j.lstat(i);if(!a.isDirectory())throw new Error(`refusing ${i}: not a directory`);const n=await Ae(e,s);const o=ae.join(i,"buoy-farm-prior.json");let r={};try{r=JSON.parse(await j.readFile(o,"utf8"))}catch{}const c={...n};for(const h of t){if(!(h in r))r[h]=h in n?n[h]:null;c[h]=true}await Te(o,JSON.stringify(r,null,2)+"\n");await Te(De(e,s),ht(c))}async function ut(e,t,s=L){const i=await Ae(e,s);return t.every(a=>i[a]===true)}async function Te(e,t){const s=`${e}.${rt(4).toString("hex")}.tmp`;const i=await j.open(s,"w",420);try{await i.writeFile(t);await i.sync()}finally{await i.close()}await j.rename(s,e)}import{promises as S,existsSync as x,readFileSync as vs}from"fs";import g from"path";import{createHash as $e}from"crypto";var Re=["@react_buoy_sync_device_id.json","@react_buoy_install.json"];var pt="@buoy~1floating-tool~1*";function oe(e){if(!x(g.join(e,"seed.json")))return`No seed at ${e}. Run: pnpm e2e:pool seed`;if(x(g.join(e,"Documents/buoy-license"))){for(const t of["buoy-gg-license.json","buoy-gg-device-fingerprint.json"]){if(!x(g.join(e,"Documents/buoy-license",t)))return`The seed lacks ${t}.`}}return null}async function mt(e){const t=$e("sha256");async function s(i){let a;try{a=await S.readdir(i,{withFileTypes:true})}catch{return}a.sort((n,o)=>n.name.localeCompare(o.name));for(const n of a){const o=g.join(i,n.name);if(n.isDirectory())await s(o);else{const r=await S.stat(o);t.update(`${g.relative(e,o)}:${r.size}:${Math.floor(r.mtimeMs)}
4
+ `)}}}await s(e);return t.digest("hex").slice(0,16)}async function ft(e){const t=$e("sha256");const s=g.join(e,"Info.plist");t.update(await S.readFile(s));const i=JSON.parse(await O("plutil",["-convert","json","-o","-",s],{timeoutMs:1e4}));const a=g.join(e,String(i.CFBundleExecutable??""));const n=await S.stat(a);t.update(`${n.size}:${Math.floor(n.mtimeMs)}`);return t.digest("hex").slice(0,16)}async function wt(e){const t=JSON.parse(await O("plutil",["-convert","json","-o","-",g.join(e,"Info.plist")],{timeoutMs:1e4}));if(!t.CFBundleIdentifier)throw new Error(`${e} has no CFBundleIdentifier`);return String(t.CFBundleIdentifier)}async function re(e,t){const s=g.join(e,"Containers/Data/Application");let i;try{i=await S.readdir(s)}catch{return null}for(const a of i){const n=g.join(s,a,".com.apple.mobile_container_manager.metadata.plist");if(!x(n))continue;const o=await k("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",n],{timeoutMs:5e3});if(o.code===0&&o.stdout.trim()===t)return g.join(s,a)}return null}function gt(e,t){return g.join(e,"Library/Application Support",t,"RCTAsyncLocalStorage_V1")}async function Be(e,t,s,i){i();await S.mkdir(t,{recursive:true});const a=["-a","--delete"];for(const n of s)a.push("--exclude",n);a.push(e.replace(/\/?$/,"/"),t.replace(/\/?$/,"/"));i();await O("rsync",a,{timeoutMs:6e4})}async function _e(e,t,s,i=()=>{}){i();const a=oe(s);if(a)throw new Error(a);const n=[...Re.map(c=>`react-buoy/${c}`),`react-buoy/${pt}`];if(x(g.join(s,"Documents")))await Be(g.join(s,"Documents"),g.join(e,"Documents"),n,i);if(x(g.join(s,"AsyncStorage/manifest.json"))){await Be(g.join(s,"AsyncStorage"),gt(e,t),[],i)}const o=g.join(s,"Preferences",`${t}.plist`);if(x(o)){i();await S.mkdir(g.join(e,"Library/Preferences"),{recursive:true});await S.copyFile(o,g.join(e,"Library/Preferences",`${t}.plist`))}const r=g.join(e,"Documents/react-buoy");try{for(const c of await S.readdir(r)){i();if(c.startsWith("@buoy~1floating-tool~1"))await S.rm(g.join(r,c),{force:true,recursive:true})}}catch{}}async function X(e,t=()=>{}){t();const s=[];const i=g.join(e,"Documents/react-buoy");let a=[];try{a=await S.readdir(i)}catch{return s}for(const n of a){if(Re.includes(n)||n.startsWith("@buoy~1floating-tool~1")){t();await S.rm(g.join(i,n),{force:true,recursive:true});s.push(n)}}return s}async function yt(e){let t;try{t=await S.readFile(g.join(e,"Documents/react-buoy/@react_buoy_sync_device_id.json"),"utf8")}catch{return null}try{const s=JSON.parse(t);if(typeof s==="string")return s||null;if(typeof s?.value==="string")return s.value||null}catch{}return t.trim()||null}var P=1024**3;var ce="Buoy Farm ";var le=".buoy-farm.json";var kt=120;var vt=180;var de=9e4;var xe=500;var Ne=200;var St=3;var B=["done","failed","stopped","interrupted"];var je=["setting-up","working","waiting","passed","failed","needs-help"];var bt={now:()=>performance.now()};var Le=class{constructor(){this.tails=new Map}run(e,t){const s=this.tails.get(e)??Promise.resolve();const i=s.catch(()=>{}).then(t);this.tails.set(e,i);i.finally(()=>{if(this.tails.get(e)===i)this.tails.delete(e)}).catch(()=>{});return i}};var Ot=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 t=(this.counts.get(e)??0)-1;if(t>0)this.counts.set(e,t);else this.counts.delete(e)}};var Et=class{constructor(e){this.free=e;this.waiting=[]}async use(e){if(this.free>0)this.free--;else await new Promise(t=>this.waiting.push(t));try{return await e()}finally{const t=this.waiting.shift();if(t)t();else this.free++}}};var Pe=class{constructor(e){this.simLock=new Le;this.deadlines=new Map;this.reservations=new Map;this.busy=new Ot;this.ticking=false;this.hardDeadlines=new Map;this.acquiring=new Set;this.budgetLock=new Le;this.labels=new Map;this.inflight=new Map;this.workContext=new nt;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:(t,s)=>{const i=Reflect.get(t,s);if(typeof i!=="function")return i;return async(...a)=>{this.store.assertActive();const n=await i.apply(t,a);this.store.assertActive();return n}}});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 Et(this.state.settings.maxConcurrentBoots)}async createTemplate(e){return this.work(void 0,()=>this.createTemplateWork(e))}async cloneSim(e,t){return this.work(void 0,()=>this.cloneSimWork(e,t))}async bootSim(e,t){return this.work(t,()=>this.bootSimWork(e,t))}async parkSim(e,t="idle"){return this.work(void 0,()=>this.parkSimWork(e,t))}async deleteSim(e,t){return this.work(t,()=>this.deleteSimWork(e,t))}async rebuildSim(e,t){return this.work(void 0,()=>this.simLock.run(`rebuild:${e}`,()=>this.rebuildSimWork(e,t)))}async fastReset(e,t){return this.work(void 0,()=>this.fastResetWork(e,t))}async claimOne(e,t){return this.work(e.id,()=>this.claimOneWork(e,t))}async action(e,t,s,i,a,n){this.touch(this.liveLease(e,t,n));const o=this.state.locks["perf-slot"];if(i==="screenshot"&&o&&o.leaseId!==e)throw new l("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);return this.work(i==="reset"||i==="rebuild"?void 0:e,()=>this.actionWork(e,t,s,i,a,n))}async work(e,t){this.store.assertActive();if(this.workContext.getStore()?.active)return t();while(this.state.locks["perf-slot"]&&this.state.locks["perf-slot"].leaseId!==e){await $(this.pollMs);this.store.assertActive()}const s={active:true,leaseId:e};this.activeWork++;try{return await this.workContext.run(s,t)}finally{s.active=false;this.activeWork--}}async files(e){this.checkActive();const t=e();this.fileTasks.add(t);try{const s=await t;this.checkActive();return s}finally{this.fileTasks.delete(t)}}fence(){this.store.fence()}async settleFiles(){await Promise.allSettled([...this.fileTasks])}get state(){return this.store.state}async recover(){for(const[i,a]of Object.entries(this.state.locks)){if(!this.state.leases[a.leaseId])delete this.state.locks[i]}const e=await this.host.listDevices();const t=new Map(e.map(i=>[i.udid,i]));for(const i of Object.values(this.state.sims)){const a=t.get(i.udid);if(!a){this.store.event({kind:"lost",message:`${i.name} was deleted outside the farm.`,udid:i.udid});if(i.leaseId)this.dropFromLease(i.leaseId,i.udid);delete this.state.sims[i.udid];continue}if(a.state==="Booted"){i.power=i.power==="frozen"?"frozen":"awake";if(i.power!=="frozen"){const n=this.state.templates[i.templateId];const o=await this.health(i.udid,n?U(n.profile):[]);i.profile=o.profile;i.lastCheckAt=new Date().toISOString();if(!o.ok||n?.profile==="slim"&&o.profile==="unknown"){i.health="suspect";i.lastError=o.faults.join("; ")||"Slim state is not known."}}}else{i.power=i.power==="parked"?"parked":"cold"}if(["creating","booting","thawing","deleting"].includes(i.power))i.power=a.state==="Booted"?"awake":"cold"}for(const i of Object.values(this.state.templates)){const a=t.get(i.udid);if(!a){i.usable=false;i.note="Its sim was deleted outside the farm."}else if(!be(i.appPath)){i.note="Its app build is gone. Clones still work; rebuild the template for a new build."}}for(const i of Object.keys(this.state.orphans)){if(!t.has(i))delete this.state.orphans[i]}for(const i of this.store.pending){if(i.op==="clone"){const a=i.data?.name;const n=e.find(o=>o.name===a&&!this.state.sims[o.udid]);if(n){this.state.orphans[n.udid]={udid:n.udid,name:n.name,cloneIntentName:a};this.store.event({kind:"orphan",message:`Found ${n.name} with no record after a crash. Held for review. Use farm delete to remove it.`,udid:n.udid})}}if(i.op!=="rebuild")this.store.result(i.seq,i.op,{recovered:true})}const s=new Set;for(const i of[...this.store.pending]){if(i.op!=="rebuild")continue;const{udid:a,name:n,templateId:o}=i.data;if(Object.values(this.state.orphans).some(r=>r.name===n))continue;s.add(a);this.busy.add(a);this.background(`resume rebuild ${n}`,()=>this.work(void 0,async()=>{try{const r=Object.values(this.state.sims).find(c=>c.name===n&&c.udid!==a);if(!r){if(this.state.sims[a]){const c=this.state.sims[a];if(c.leaseId)this.dropFromLease(c.leaseId,a);c.leaseId=void 0;await this.deleteSim(a)}await this.cloneSim(o,n)}this.store.result(i.seq,"rebuild",{recovered:true})}finally{this.busy.delete(a)}}))}for(const i of Object.values(this.state.leases)){this.deadlines.set(i.id,this.clock.now()+de);const a=Date.parse(i.createdAt)+i.ttlMs-Date.now();this.hardDeadlines.set(i.id,this.clock.now()+Math.max(a,0));if(a<=0)this.background(`expire ${i.id}`,()=>this.endLease(i.id,"ran out of time while the farm was down","full","deadline"))}for(const i of Object.values(this.state.sims)){if(i.health==="dirty"&&!i.leaseId&&!s.has(i.udid)){this.busy.add(i.udid);this.background(`resume reset ${i.name}`,async()=>{try{await this.rebuildSim(i.udid)}finally{this.busy.delete(i.udid)}})}}this.store.save()}async dataPath(e){const t=(await this.host.listDevices()).find(s=>s.udid===e);if(!t)throw new l("NOT_FOUND",`No sim ${e} on this Mac.`);return t.dataPath}async isOwned(e){const t=this.state.sims[e]??Object.values(this.state.templates).find(s=>s.udid===e);if(!t)return false;try{const s=JSON.parse(await T.readFile(b.join(await this.dataPath(e),le),"utf8"));return s.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,t){const s=await this.dataPath(e);await this.files(async()=>{await T.mkdir(s,{recursive:true});this.checkActive();await T.writeFile(b.join(s,le),JSON.stringify({farmId:this.state.farmId,...t})+"\n")})}farmAwakeGB(){const e=this.state.settings;let t=0;for(const s of Object.values(this.state.sims)){if(["awake","booting","frozen","thawing"].includes(s.power))t+=s.profile==="slim"?e.slimSimGB:e.stockSimGB}return t}async budget(e={ramGB:0,diskGB:0}){const t=this.state.settings;const s=await this.host.hostStats();let i=0;let a=0;for(const u of this.reservations.values()){i+=u.ramGB;a+=u.diskGB}const n=s.availableMemBytes/P-i;const o=s.totalMemBytes/P-t.reserveRamGB-this.farmAwakeGB()-i;const r=s.freeDiskBytes/P-a-t.reserveDiskGB;const c=Math.max(0,Math.floor(Math.min(n-4,o,r/t.cloneDiskGB*t.slimSimGB)/t.slimSimGB));const h=(u,E)=>({ok:false,code:u,reason:E,stats:s,roomFor:0});if(s.memPressure!=="normal")return h("RAM_LOW","This Mac is low on memory. New sims will wait.");if(e.diskGB>0&&r-e.diskGB<0){return h("DISK_LOW",`Need ${t.reserveDiskGB} GB free after this. This Mac has ${Math.floor(s.freeDiskBytes/P)} GB.`)}if(e.ramGB>0){if(n-e.ramGB<4||o-e.ramGB<0){return h("RAM_LOW",`Not enough free memory for another sim (${Math.max(0,Math.floor(n))} GB free).`)}}const p=this.state.locks["perf-slot"];if(p&&(!this.workContext.getStore()?.active||this.workContext.getStore()?.leaseId!==p.leaseId))return h("HOST_BUSY","A speed test is running. New sims will wait.");return{ok:true,stats:s,roomFor:c}}async withReservation(e,t){const s=J(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(s,e)});try{return await t()}finally{this.reservations.delete(s)}}async health(e,t=[]){const s=[];let i;try{i=await this.host.simTree(e)}catch(r){this.checkActive();return{ok:false,profile:"unknown",faults:[`Could not read sim processes: ${r.message}`],procs:0}}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))s.push(`${r} is not running`);const n=Ue(await this.host.launchdLogTail(e));if(n)s.push(`${n} keeps crashing`);if(s.length===0){const r=b.join(this.store.shotsPath,`${e}.png`);await this.files(()=>T.mkdir(b.dirname(r),{recursive:true}));if(!await this.host.screenshot(e,r,1e4))s.push("screenshot did not come back")}let o=i.length<kt?"slim":i.length>vt?"stock":"unknown";if(s.length===0&&t.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(p=>p[1]));const h=t.filter(p=>!c.has(p)).length;o=h===0?"slim":h===t.length?"stock":"unknown"}else{o="unknown"}}return{ok:s.length===0,profile:o,faults:s,procs:i.length}}async waitHealthy(e,t=[]){const s=performance.now()+this.readyTimeoutMs;let i=await this.health(e,t);while(!i.ok&&performance.now()<s){await $(this.pollMs);i=await this.health(e,t)}return i}async createTemplateWork(e){if(!["slim","stock"].includes(e.profile)||!Number.isInteger(e.metroPort)||e.metroPort<1||e.metroPort>65535){throw new l("BAD_REQUEST","Use slim or stock and a port from 1 to 65535.")}const t=e.seedDir==null?null:oe(e.seedDir);if(t)throw new l("NO_SEED",t,"Run: pnpm e2e:pool seed");if(!be(b.join(e.appPath,"Info.plist"))){throw new l("BAD_REQUEST",`No app build at ${e.appPath}.`,"Build the app first.")}const s=await wt(e.appPath);const i=`t-${J(3).toString("hex")}`;const a=e.name??`${ce}Template ${i.slice(2)}`;return this.withReservation({ramGB:this.state.settings.stockSimGB,diskGB:4},async()=>{const n=this.store.intent("create-template",{id:i,name:a});const o=await this.host.create(a,e.deviceType,e.runtime);const r={id:i,udid:o,name:a,bundleId:s,appPath:e.appPath,appHash:await ft(e.appPath),seedDir:e.seedDir??null,seedHash:e.seedDir?await mt(e.seedDir):null,launchUrl:e.launchUrl,deviceType:e.deviceType,runtime:e.runtime,xcodeBuild:await this.host.xcodeBuild(),profile:e.profile,profileHash:Se(e.profile),metroPort:e.metroPort,createdAt:new Date().toISOString(),usable:false,note:"Being built."};this.state.templates[i]=r;await this.writeMarker(o,{template:i});this.store.result(n,"create-template",{udid:o});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:o})}catch(c){r.note=`Build failed: ${c.message}`;this.store.event({kind:"template-failed",message:r.note,udid:o});await this.host.shutdown(o).catch(()=>{});throw c}finally{this.store.save()}return r})}async buildTemplate(e,t){const s=U(e.profile);if(s.length)await this.files(()=>Me(e.udid,s,this.host.disabledRoot));await this.host.boot(e.udid);const i=await this.waitHealthy(e.udid,s);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");if(e.seedDir)await this.files(()=>_e(a,e.bundleId,e.seedDir,this.checkActive));else await this.files(()=>X(a,this.checkActive));await this.setPrefs(e.udid,e);if(t){await this.launchApp(e.udid,e);await $(Math.min(15e3,this.pollMs*15));await this.host.terminate(e.udid,e.bundleId)}await this.host.shutdown(e.udid);await this.files(()=>X(a,this.checkActive))}async launchApp(e,t){await this.host.launch(e,t.bundleId);if(t.launchUrl)await this.host.openUrl(e,t.launchUrl)}async setPrefs(e,t){const s=["defaults","write","com.apple.keyboard.preferences"];await this.host.spawn(e,[...s,"AutomaticMinimizationEnabled","-bool","true"]);await this.host.spawn(e,[...s,"DidShowContinuousPathIntroduction","-bool","true"]);await this.host.spawn(e,["defaults","write",t.bundleId,"RCT_jsLocation",`localhost:${t.metroPort}`])}async nextName(){const e=await this.host.listDevices();const t=new Set([...e.map(i=>i.name),...Object.values(this.state.sims).map(i=>i.name)]);let s=this.state.nextIndex;while(t.has(`${ce}${String(s).padStart(2,"0")}`))s++;this.state.nextIndex=s+1;return`${ce}${String(s).padStart(2,"0")}`}template(e){const t=this.state.templates[e];if(!t)throw new l("NO_TEMPLATE",`No template ${e}.`,"Run sim_farm_setup to make one.");if(!t.usable)throw new l("NO_TEMPLATE",`Template ${t.name} can't be used: ${t.note??"not ready"}.`);return t}async cloneSimWork(e,t,s){t??=await this.nextName();const i=t;const a=this.template(e);return this.withReservation({ramGB:0,diskGB:this.state.settings.cloneDiskGB},()=>this.simLock.run(`template:${a.id}`,async()=>{await this.assertOwned(a.udid);const n=this.store.intent("clone",{templateId:e,name:i});const o=await this.host.clone(a.udid,i);const r={udid:o,name:i,templateId:e,power:"creating",profile:"unknown",health:"ok",bootEpoch:0,failures:0,createdAt:new Date().toISOString()};this.state.sims[o]=r;this.store.result(n,"clone",{udid:o});await this.writeMarker(o,{template:a.id});const c=await re(await this.dataPath(o),a.bundleId);if(c)await this.files(()=>X(c,this.checkActive));const h=s?this.state.leases[s.leaseId]:void 0;if(h&&!this.leaseOverdue(h)){r.leaseId=h.id;h.sims=h.sims.map(p=>p===s.udid?o:p)}r.power="cold";this.store.event({kind:"clone",message:`Made ${t}.`,udid:o});this.store.save();return r}))}async bootSimWork(e,t){await this.assertOwned(e);const s=this.sim(e);if(s.power==="frozen"||s.power==="thawing"){throw new l("BAD_REQUEST",`${s.name} is frozen. Thaw it first.`)}const i=this.state.templates[s.templateId];const a=this.state.settings;const n={ramGB:i?.profile==="slim"?a.slimSimGB:a.stockSimGB,diskGB:0};return this.withReservation(n,()=>this.bootGate.use(()=>this.simLock.run(e,async()=>{this.assertLeaseState(s,t);if(s.power==="awake")return s;const o=i?U(i.profile):[];if(o.length&&!await ut(e,o,this.host.disabledRoot)){await this.files(()=>Me(e,o,this.host.disabledRoot))}s.power="booting";s.bootEpoch++;this.store.save();await this.host.boot(e);const r=await this.waitHealthy(e,i?.profile==="slim"?o:[]);s.lastCheckAt=new Date().toISOString();s.profile=r.profile;s.power="awake";if(!r.ok){s.failures++;s.health=s.failures>=2?"broken":"suspect";s.lastError=r.faults.join("; ");this.store.event({kind:"unhealthy",message:`${s.name}: ${s.lastError}`,udid:e});this.store.save();throw new l("HOST_ERROR",`${s.name} did not come up: ${s.lastError}`,"The farm will rebuild it.",true)}s.failures=0;if(s.health==="suspect")s.health="ok";s.idleSince=new Date().toISOString();if(i?.profile==="slim"&&r.profile==="unknown"){s.health="suspect";s.lastError="can't tell if slim applied";this.store.event({kind:"unknown-profile",message:`${s.name}: can't tell if slim worked. Held for now.`,udid:e});this.store.save();throw new l("HOST_ERROR",`${s.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:`${s.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){s.health="suspect";s.lastError="slim did not apply and there is no RAM for a full sim";await this.host.shutdown(e).catch(()=>{});s.power="parked";this.store.save();throw new l("RAM_LOW",`${s.name}: slim didn't apply and there's no room for a full sim.`,void 0,true)}}this.store.save();return s})))}assertLeaseState(e,t){if(!this.state.sims[e.udid])throw new l("NOT_FOUND",`${e.name} is gone.`);if(t){const s=this.state.leases[t];if(e.leaseId!==t||!s||this.leaseOverdue(s)){throw new l("LEASE_EXPIRED",`${e.name} is no longer in lease ${t}.`)}}else if(e.leaseId){throw new l("BAD_REQUEST",`${e.name} is in use. End its lease first.`)}}async parkSimWork(e,t="idle"){await this.assertOwned(e);const s=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(s);if(this.busy.has(e))throw new l("BAD_REQUEST",`${s.name} is being reset.`);if(s.power==="frozen"||s.power==="thawing")throw new l("BAD_REQUEST",`${s.name} is frozen. Wake it before parking.`);if(s.power!=="awake")return;this.busy.add(e);try{await this.host.shutdown(e);s.power="parked"}finally{this.busy.delete(e)}this.store.event({kind:"park",message:`Parked ${s.name} (${t}).`,udid:e});this.store.save()})}sim(e){const t=this.state.sims[e];if(!t)throw new l("NOT_OWNED",`Sim ${e} is not a farm sim.`);return t}async deleteSimWork(e,t){const s=this.state.orphans[e];if(s){await this.simLock.run(e,async()=>{const a=(await this.host.listDevices()).find(r=>r.udid===e);if(!a)throw new l("NOT_FOUND","This sim is gone.");let n=false;try{const r=JSON.parse(await T.readFile(b.join(a.dataPath,le),"utf8"));n=r.farmId===this.state.farmId}catch(r){n=r.code==="ENOENT"&&a.name===s.cloneIntentName}if(!n)throw new l("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(()=>T.rm(b.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));delete this.state.orphans[e];this.store.result(o,"delete",{})});return}await this.assertOwned(e);const i=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(i,t);if(i.power==="frozen"||i.power==="thawing")throw new l("BAD_REQUEST",`${i.name} is frozen. Wake it first.`);i.power="deleting";const a=this.store.intent("delete",{udid:e});await this.host.shutdown(e).catch(()=>{});await this.host.delete(e);await this.files(()=>T.rm(b.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));delete this.state.sims[e];this.store.result(a,"delete",{});this.store.event({kind:"delete",message:`Deleted ${i.name}.`,udid:e})})}async rebuildSimWork(e,t){const s=this.sim(e);const{name:i,templateId:a}=s;await this.assertOwned(e);await this.simLock.run(e,async()=>{this.assertLeaseState(s,t);this.busy.add(e)});const n=this.store.intent("rebuild",{udid:e,name:i,templateId:a});try{await this.deleteSim(e,t);const o=await this.cloneSimWork(a,i,t?{udid:e,leaseId:t}:void 0);this.store.result(n,"rebuild",{udid:o.udid});return o}finally{this.busy.delete(e)}}async fastResetWork(e,t){await this.assertOwned(e);const s=this.sim(e);const i=this.template(s.templateId);await this.simLock.run(e,async()=>{this.assertLeaseState(s,t);if(s.power==="frozen"||s.power==="thawing")throw new l("BAD_REQUEST",`${s.name} is frozen. Wake it first.`);this.busy.add(e);try{if(s.power==="awake")await this.host.terminate(e,i.bundleId);const a=await re(await this.dataPath(e),i.bundleId);if(!a)throw new Error("app container not found");if(i.seedDir)await this.files(()=>_e(a,i.bundleId,i.seedDir,this.checkActive));else await this.files(()=>X(a,this.checkActive));s.health="ok";this.store.save()}finally{this.busy.delete(e)}})}async grow(e,t){if(!Number.isInteger(t)||t<1||t>64)throw new l("BAD_REQUEST","count must be 1 to 64.");const s=[];for(let i=0;i<t;i++)s.push(await this.cloneSim(e));return s}async shrink(e,t){if(!Number.isInteger(t)||t<1||t>64)throw new l("BAD_REQUEST","count must be 1 to 64.");const s=["cold","parked","awake"];const i=Object.values(this.state.sims).filter(n=>n.templateId===e&&!n.leaseId&&s.includes(n.power)&&!this.busy.has(n.udid)).sort((n,o)=>s.indexOf(n.power)-s.indexOf(o.power));const a=[];for(const n of i.slice(0,t)){try{await this.deleteSim(n.udid);a.push(n.name)}catch(o){if(!(o instanceof l))throw o}}return a}async removeTemplate(e){return this.work(void 0,async()=>{const t=this.state.templates[e];if(!t)throw new l("NO_TEMPLATE",`No template ${e}.`);const s=Object.values(this.state.sims).filter(o=>o.templateId===e);const i=s.filter(o=>o.leaseId||this.busy.has(o.udid));if(i.length){throw new l("HOST_BUSY",`${i.map(o=>o.name).join(", ")} ${i.length===1?"is":"are"} in use.`,"End those uses first.",true)}const a=[];for(const o of s){await this.deleteSim(o.udid);a.push(o.name)}const n=(await this.host.listDevices()).some(o=>o.udid===t.udid);if(n){await this.assertOwned(t.udid);await this.simLock.run(t.udid,async()=>{const o=this.store.intent("delete",{udid:t.udid});await this.host.shutdown(t.udid).catch(()=>{});await this.host.delete(t.udid);await this.files(()=>T.rm(b.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${t.udid}`),{recursive:true,force:true}));this.store.result(o,"delete",{})})}delete this.state.templates[e];a.push(t.name);this.store.event({kind:"delete",message:`Removed template ${t.name} and its ${s.length} sim(s).`});return{removed:a}})}leaseOverdue(e){const t=this.clock.now();if(t>(this.hardDeadlines.get(e.id)??Infinity))return true;return!this.acquiring.has(e.id)&&t>(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 l("BAD_REQUEST","ttlMs must be a finite number above zero.")}const t=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 s=Math.min(Math.max(e.ttlMs??15*6e4,3e4),24*36e5);const i=e.runId?this.ownRun(e.runId,e.owner.clientId):this.newRun(e,t.id,s);const a=Date.parse(i.deadlineAt)-Date.now();if(a<3e4)throw new l("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 n={id:`l-${J(4).toString("hex")}`,epoch:1,owner:e.owner,purpose:e.purpose,runId:i.id,templateId:t.id,sims:[],createdAt:new Date().toISOString(),ttlMs:Math.min(s,a)};i.leases.push(n.id);i.lastActivityAt=n.createdAt;if(e.labels?.length)this.labels.set(n.id,e.labels);this.state.leases[n.id]=n;this.hardDeadlines.set(n.id,this.clock.now()+n.ttlMs);this.acquiring.add(n.id);const o=()=>this.deadlines.set(n.id,this.clock.now()+Math.min(n.ttlMs,de));o();const r=performance.now()+(e.waitMs??0);const c=()=>{if(!this.state.leases[n.id])throw new l("LEASE_EXPIRED","The lease ended while sims were being prepared.");if(this.leaseOverdue(n))throw new l("LEASE_EXPIRED","The lease ran out of time while sims were being prepared.")};try{while(n.sims.length<e.count){c();o();const h=await this.claimOne(n,t);c();if(h)continue;if(performance.now()>=r)break;if(!n.sims.length&&i.state==="active"&&!Object.keys(i.lanes).length)i.state="waiting";await $(Math.max(this.pollMs,2e3))}if(n.sims.length<e.count&&!e.allowPartial){const h=await this.budget({ramGB:this.state.settings.slimSimGB,diskGB:0});throw new l(h.code??"HOST_BUSY",`Got ${n.sims.length} of ${e.count} sims. ${h.reason??"Others are in use."}`,"Try again later or ask for fewer.",true)}if(n.sims.length===0)throw new l("HOST_BUSY","No sim is free right now.","Try again later.",true);if(e.launch)await this.work(n.id,async()=>{c();await Promise.all(n.sims.map(h=>this.launchApp(h,t)));o();await this.checkIdentities(n,6e4);c()});o();this.store.event({kind:"lease",message:`${e.owner.label} took ${n.sims.length} sim(s) for ${i.goal}.`,leaseId:n.id,runId:i.id});this.note(i,"farm",`Got ${n.sims.length} sim${n.sims.length===1?"":"s"}.`);this.store.save();return{lease:n,run:i,sims:n.sims.map(h=>this.state.sims[h])}}catch(h){if(this.state.leases[n.id])this.note(i,"farm",`Could not get sims: ${h.message}`);await this.endLease(n.id,"acquire failed","fast","acquire-failed").catch(()=>{});throw h}finally{this.acquiring.delete(n.id);this.labels.delete(n.id)}}async claimOneWork(e,t){const s=Object.values(this.state.sims).filter(o=>o.templateId===t.id&&!o.leaseId&&o.health==="ok"&&!this.busy.has(o.udid));const i=s.find(o=>o.power==="awake")??s.find(o=>o.power==="parked"||o.power==="cold");const a=async o=>{const r=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(!r){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(i?.power==="awake")return a(i);let n=null;try{if(i){n=await a(i)}else{const o=await this.budget({ramGB:t.profile==="slim"?this.state.settings.slimSimGB:this.state.settings.stockSimGB,diskGB:this.state.settings.cloneDiskGB});if(!o.ok)return null;n=await a(await this.cloneSim(t.id))}if(!n)return null;await this.bootSim(n.udid,e.id);return n}catch(o){if(n)this.unclaim(e,n.udid);if(o instanceof l&&(o.code==="RAM_LOW"||o.code==="DISK_LOW"||o.code==="HOST_BUSY"||o.code==="LEASE_EXPIRED")){return null}if(n&&this.state.sims[n.udid]?.health!=="ok"){const r=n.udid;this.background(`rebuild ${r}`,()=>this.rebuildSim(r))}throw o}}unclaim(e,t){e.sims=e.sims.filter(a=>a!==t);const s=e.runId?this.state.runs[e.runId]:void 0;if(s?.lanes[t])delete s.lanes[t];const i=this.state.sims[t];if(i&&i.leaseId===e.id)i.leaseId=void 0;this.store.save()}async checkIdentities(e,t=6e4){const s=this.template(e.templateId);const i=performance.now()+t;const a=new Map;for(;;){for(const o of e.sims){if(a.has(o))continue;const r=await re(await this.dataPath(o),s.bundleId);const c=r?await yt(r):null;if(!c)continue;const h=[...a.entries()].find(([,u])=>u===c);if(h)throw new l("ID_CLASH",`${this.state.sims[o].name} and ${this.state.sims[h[0]].name} share one Buoy ID.`);const p=Object.values(this.state.sims).find(u=>u.udid!==o&&!e.sims.includes(u.udid)&&u.buoyDeviceId===c);if(p)throw new l("ID_CLASH",`${this.state.sims[o].name} and ${p.name} share one Buoy ID.`);a.set(o,c);this.state.sims[o].buoyDeviceId=c}if(a.size===e.sims.length||performance.now()>=i)break;await $(Math.max(this.pollMs,1e3))}const n=e.sims.filter(o=>!a.has(o));if(n.length){this.store.event({kind:"no-id",message:`No Buoy ID yet on ${n.map(o=>this.state.sims[o].name).join(", ")}. Is the app loading? Check Metro.`,leaseId:e.id})}}liveLease(e,t,s){const i=this.state.leases[e];if(!i){const a=this.runOfLease(e);if(a&&(a.endedBy==="user"||a.endedBy==="deadline"))this.assertRunOpen(a);throw new l("LEASE_EXPIRED",`Lease ${e} has ended.`,"Acquire new sims.")}if(i.epoch!==t)throw new l("STALE_EPOCH",`Lease ${e} is at epoch ${i.epoch}, not ${t}.`);if(s&&i.owner.clientId!==s)throw new l("NOT_OWNED",`Lease ${e} belongs to ${i.owner.label}.`);if(this.leaseOverdue(i)){const a=this.overdueEnd(i);this.background(`expire ${i.id}`,()=>this.endLease(i.id,"expired","full",a));if(a==="deadline")throw new l("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 l("LEASE_EXPIRED",`Lease ${e} has expired.`,"Acquire new sims.")}return i}renew(e,t,s){const i=this.liveLease(e,t,s);const a=Math.min(this.clock.now()+Math.min(i.ttlMs,de),this.hardDeadlines.get(i.id)??Infinity);this.deadlines.set(i.id,a);return{expiresInMs:Math.round(a-this.clock.now())}}async release(e,t,s="fast",i,a=false){if(s!=="fast"&&s!=="full")throw new l("BAD_REQUEST","Use fast or full reset.");this.liveLease(e,t,i);return this.endLease(e,"released",s,a?null:"released")}async endLease(e,t,s,i="released"){const a=this.state.leases[e];if(!a)return{};a.epoch++;delete this.state.leases[e];this.deadlines.delete(e);this.hardDeadlines.delete(e);for(const[r,c]of Object.entries(this.state.locks))if(c.leaseId===e)delete this.state.locks[r];const n=new Set(a.sims.filter(r=>{const c=this.state.sims[r];return c?.bootEpoch===0&&c.power==="cold"&&c.health==="ok"}));const o={};for(const r of a.sims){const c=this.state.sims[r];if(!c||c.leaseId!==e)continue;c.leaseId=void 0;c.health="dirty";c.idleSince=new Date().toISOString();this.busy.add(r);o[c.name]="resetting"}this.store.event({kind:"lease-end",message:`${a.owner.label}'s ${a.purpose} ended (${t}).`,leaseId:e,runId:a.runId});this.settleRun(a,i);this.store.save();this.background(`reset after ${e}`,()=>this.work(void 0,async()=>{for(const r of a.sims){if(!this.state.sims[r])continue;try{const c=Date.parse(a.createdAt)-2e3;for(const h of await this.host.clientsOf(r)){if(Date.parse(h.start)>=c)await this.host.kill(h.pid,h.start)}if(s==="full"&&!n.has(r)||this.state.sims[r].failures>0)await this.rebuildSim(r);else await this.fastReset(r)}catch(c){this.store.event({kind:"reset-failed",message:`${this.state.sims[r]?.name??r}: ${c.message}. Rebuilding.`,udid:r});await this.rebuildSim(r).catch(()=>{})}finally{this.busy.delete(r)}}}));return o}dropFromLease(e,t){const s=this.state.leases[e];if(s)s.sims=s.sims.filter(i=>i!==t)}async actionWork(e,t,s,i,a,n){const o=this.liveLease(e,t,n);const r={};for(const c of s){if(!o.sims.includes(c))throw new l("NOT_OWNED",`${c} is not in lease ${e}.`);await this.assertOwned(c);this.liveLease(e,t,n);const h=this.template(o.templateId);switch(i){case"screenshot":{const p=this.state.locks["perf-slot"];if(p&&p.leaseId!==o.id)throw new l("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);const u=b.join(this.store.shotsPath,`${c}.png`);r[c]=await this.host.screenshot(c,u)?u: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.launchApp(c,h);r[c]="launched";break;case"reset":await this.fastReset(c,o.id);r[c]="reset";break;case"rebuild":{const p=await this.rebuildSim(c,o.id);if(!this.state.leases[o.id])throw new l("LEASE_EXPIRED",`Lease ${e} ended during the rebuild.`);p.leaseId=o.id;o.sims=o.sims.map(R=>R===c?p.udid:R);const u=o.runId?this.state.runs[o.runId]:void 0;const E=u?.lanes[c];if(u&&E){delete u.lanes[c];u.lanes[p.udid]={...E,udid:p.udid,name:p.name,state:"setting-up",note:"Rebuilt"}}await this.bootSim(p.udid,o.id);r[c]=p.udid;break}default:throw new l("BAD_REQUEST",`Unknown action ${i}.`)}}this.store.save();return r}async lock(e,t,s,i=0,a){if(typeof e!=="string"||!/^[a-z0-9-]{1,40}$/.test(e))throw new l("BAD_REQUEST","Lock names are short lowercase words.");this.touch(this.liveLease(t,s,a));if(this.state.locks[e]?.leaseId===t)return{held:true};const n=this.lockQueues.get(e)??[];this.lockQueues.set(e,n);const o={};n.push(o);const r=performance.now()+i;try{for(;;){this.store.assertActive();this.liveLease(t,s,a);const c=this.state.locks[e];const h=e==="perf-slot"&&(this.activeWork>0||this.busy.size>0||this.pendingBg>0||Object.values(this.state.leases).some(p=>p.id!==t&&p.sims.length>0));if(n[0]===o&&!c&&!h){this.state.locks[e]={name:e,leaseId:t,since:new Date().toISOString()};this.store.save();return{held:true}}if(performance.now()>=r){return{held:false,holder:c?this.state.leases[c.leaseId]?.owner.label??"someone":"the farm (work is still running)",since:c?.since}}await $(Math.max(1,Math.min(this.pollMs,r-performance.now())))}}finally{n.splice(n.indexOf(o),1);if(!n.length)this.lockQueues.delete(e)}}unlock(e,t,s,i){this.liveLease(t,s,i);if(this.state.locks[e]?.leaseId===t)delete this.state.locks[e];this.store.save();return{held:false}}async shot(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(this.shotsInFlight>=St)throw new l("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 t=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(t.power!=="awake")throw new l("BAD_REQUEST",`${t.name} is ${t.power}.`);const s=b.join(this.store.shotsPath,`${e}.png`);await this.files(()=>T.mkdir(b.dirname(s),{recursive:true}));if(!await this.host.screenshot(e,s,8e3))throw new l("HOST_ERROR",`No still from ${t.name} in time.`,void 0,true);return{file:s,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 t=this.sim(e);if(t.power==="frozen"||t.power==="thawing")throw new l("BAD_REQUEST",`${t.name} is paused. Wake it first.`);if(t.power!=="awake")await this.bootSim(e,t.leaseId);await this.host.showWindow(e);return"shown"}async tick(){if(this.ticking)return;this.ticking=true;try{for(const s of Object.values(this.state.leases)){const i=!await _(s.owner.pid,s.owner.pidStart);if(i)await this.endLease(s.id,"its owner exited","full","owner-exited");else if(this.leaseOverdue(s))await this.endLease(s.id,"no renewal","full",this.overdueEnd(s))}await this.tickRuns();const e=this.state.settings.parkAfterMinutes;if(e>0){for(const s of Object.values(this.state.sims)){if(s.power!=="awake"||s.leaseId||this.busy.has(s.udid)||!s.idleSince)continue;if(Date.now()-Date.parse(s.idleSince)>e*6e4){await this.parkSim(s.udid).catch(i=>this.log(`park ${s.name}: ${i.message}`))}}}const t=Object.keys(this.state.requests);if(t.length>xe)for(const s of t.slice(0,t.length-xe))delete this.state.requests[s]}finally{this.ticking=false}}async drain(e=10*6e4){const t=performance.now()+e;while((this.busy.size>0||this.pendingBg>0||this.activeWork>0)&&performance.now()<t)await $(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,t){this.pendingBg++;this.workContext.exit(t).catch(s=>{this.log(`${e}: ${s.message}`);try{this.store.event({kind:"error",message:`${e}: ${s.message}`})}catch{}}).finally(()=>this.pendingBg--)}async once(e,t,s,i){if(!t)return i();const a=`${e}:${t}`;const n=ot("sha256").update(JSON.stringify(s)).digest("hex").slice(0,16);const o=this.state.requests[a];if(o){if(o.hash!==n)throw new l("REQUEST_REUSED",`Request ${t} was already used for a different call.`);return o.result}const r=this.inflight.get(a);if(r){if(r.hash!==n)throw new l("REQUEST_REUSED",`Request ${t} was already used for a different call.`);return r.promise}const c=i();this.inflight.set(a,{hash:n,promise:c});try{const h=await c;this.state.requests[a]={hash:n,result:h,at:new Date().toISOString()};this.store.save();return h}finally{this.inflight.delete(a)}}newRun(e,t,s){const i=Date.now();const a=e.owner.agent?this.state.settings.runCapMinutes*6e4:24*36e5;const n=(e.goal??"").trim()||e.purpose;const o={id:`r-${J(4).toString("hex")}`,goal:n.slice(0,200),kind:e.kind?.slice(0,40),owner:e.owner,templateId:t,state:"active",createdAt:new Date(i).toISOString(),deadlineAt:new Date(i+Math.min(s,a)).toISOString(),capAt:new Date(i+a).toISOString(),leases:[],lanes:{},notes:[],lastActivityAt:new Date(i).toISOString(),auto:!e.goal};this.state.runs[o.id]=o;return o}touch(e){const t=e.runId?this.state.runs[e.runId]:void 0;if(t)t.lastActivityAt=new Date().toISOString()}runOfLease(e){return Object.values(this.state.runs).find(t=>t.leases.includes(e))}assertRunOpen(e){if(e.state==="stopping"||e.state==="stopped"){if(e.endedBy==="user")throw new l("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 l("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(B.includes(e.state)||e.state==="stopping")throw new l("RUN_ENDED",`This run has ended (${e.state}).`,"Start a new run.")}ownRun(e,t){const s=this.state.runs[e];if(!s)throw new l("NOT_FOUND",`No run ${e}.`,"Use the runId that sim_farm_acquire returned.");if(t&&s.owner.clientId!==t)throw new l("NOT_OWNED",`Run ${e} belongs to ${s.owner.label}.`);this.assertRunOpen(s);return s}note(e,t,s,i){e.notes.push({at:new Date().toISOString(),by:t,text:s.slice(0,500),udid:i});if(e.notes.length>Ne)e.notes.splice(0,e.notes.length-Ne)}addLane(e,t){const s=e.runId?this.state.runs[e.runId]:void 0;if(!s)return;const i=this.labels.get(e.id)?.[e.sims.length-1];s.lanes[t.udid]={udid:t.udid,name:t.name,label:i?.slice(0,80),state:"setting-up",at:new Date().toISOString()};if(s.state==="waiting")s.state="active"}overdueEnd(e){return this.clock.now()>=(this.hardDeadlines.get(e.id)??Infinity)?"deadline":"lost"}settleRun(e,t){const s=e.runId?this.state.runs[e.runId]:void 0;if(!s)return;for(const a of e.sims)if(s.lanes[a])s.lanes[a].gone=true;if(B.includes(s.state)||s.state==="stopping"||t===null)return;const i=s.leases.some(a=>this.state.leases[a]);switch(t){case"released":if(!i)this.endRun(s,this.outcomeOf(s)==="failed"?"failed":"done","released");return;case"acquire-failed":if(!i&&!Object.keys(s.lanes).length)this.endRun(s,"failed","acquire-failed");return;case"user":return this.endRun(s,"stopped","user","You stopped this run.");case"deadline":return this.endRun(s,"stopped","deadline","Ran out of time. The sims were taken back.");case"owner-exited":return this.endRun(s,"interrupted","owner-exited","The chat closed. Cleaning up.");case"lost":return this.endRun(s,"interrupted","lost","Lost touch with the chat. Cleaning up.");case"finished":return this.endRun(s,"done","finished")}}endRun(e,t,s,i,a="full"){if(B.includes(e.state))return;e.state="stopping";e.endedBy=s;if(i)this.note(e,s==="user"?"person":"farm",i);for(const n of e.leases){if(this.state.leases[n])void this.endLease(n,i??s,s==="finished"||s==="released"?a:"full",null)}for(const n of Object.values(e.lanes))n.gone=true;e.state=t;e.endedAt=new Date().toISOString();e.outcome=e.outcome??this.outcomeOf(e);this.store.event({kind:"run-end",message:`${e.owner.label}: "${e.goal}" ${t==="done"?"finished":t}.`,runId:e.id})}outcomeOf(e){const t=Object.values(e.lanes);const s=t.filter(a=>a.state==="passed").length;const i=t.filter(a=>a.state==="failed").length;if(!s&&!i)return"unknown";if(i&&s)return"mixed";return i?"failed":"passed"}report(e,t){const s=this.ownRun(e.runId,t);const i=new Date().toISOString();if(e.sequence!==void 0){if(!Number.isInteger(e.sequence)||e.sequence<0)throw new l("BAD_REQUEST","sequence must be a whole number.");if(s.lastSeq!==void 0&&e.sequence<=s.lastSeq)return{applied:false,reason:"already have this report",...this.timeLeft(s)};s.lastSeq=e.sequence}if(e.state!==void 0&&!je.includes(e.state))throw new l("BAD_REQUEST",`state must be one of ${je.join(", ")}.`);if(e.udid!==void 0){const a=s.lanes[e.udid];if(!a)throw new l("NOT_FOUND",`That sim is not in run ${s.id}.`,"Use a UDID from this run's sims.");if(e.state)a.state=e.state;if(e.state==="passed"||e.state==="failed")a.resultSource=e.resultSource==="test"?"test":"agent";if(e.note!==void 0)a.note=String(e.note).slice(0,200);a.at=i}if(e.counts!==void 0){const a=e.counts;const n=o=>o===void 0||Number.isInteger(o)&&o>=0;if(!a||typeof a!=="object"||!n(a.done)||!n(a.total)||!n(a.passed)||!n(a.failed)||a.done>a.total){throw new l("BAD_REQUEST","counts needs whole numbers, with done no more than total.")}s.counts={done:a.done,total:a.total,passed:a.passed,failed:a.failed}}if(e.note)this.note(s,"agent",String(e.note),e.udid);s.lastReportAt=s.lastActivityAt=i;this.store.save();return{applied:true,...this.timeLeft(s)}}extend(e,t,s){const i=this.ownRun(e,s);if(!Number.isFinite(t)||t<=0)throw new l("BAD_REQUEST","minutes must be above zero.");const a=Date.parse(i.deadlineAt);const n=Date.now()+t*6e4;const o=Math.min(n,Date.parse(i.capAt));if(o<=a){return{granted:false,partly:false,reason:"This run is at its time limit.",...this.timeLeft(i)}}i.deadlineAt=new Date(o).toISOString();i.lastActivityAt=new Date().toISOString();for(const r of i.leases){const c=this.state.leases[r];if(!c)continue;c.ttlMs=o-Date.parse(c.createdAt);this.hardDeadlines.set(r,this.clock.now()+(o-Date.now()))}this.note(i,"farm",`More time: until ${new Date(o).toLocaleTimeString()}.`);this.store.save();return{granted:true,partly:o<n,...this.timeLeft(i)}}finish(e,t,s,i){const a=this.state.runs[e];if(a?.endedBy==="finished"&&a.owner.clientId===i)return{state:a.state,outcome:a.outcome};const n=this.ownRun(e,i);if(t&&!["passed","failed","mixed","unknown"].includes(t))throw new l("BAD_REQUEST","outcome must be passed, failed, mixed or unknown.");n.outcome=t??this.outcomeOf(n);if(s)n.summary=s.slice(0,600);this.endRun(n,n.outcome==="failed"?"failed":"done","finished",void 0,"fast");this.store.save();return{state:n.state,outcome:n.outcome}}stopRun(e,t="you"){const s=this.state.runs[e];if(!s)throw new l("NOT_FOUND",`No run ${e}.`);if(B.includes(s.state))return{state:s.state};this.endRun(s,"stopped","user",t==="you"?"You stopped this run.":`Stopped by ${t}.`);this.store.save();return{state:s.state}}stopAll(e="you"){const t=Object.values(this.state.runs).filter(s=>!B.includes(s.state));for(const s of t)this.stopRun(s.id,e);return{stopped:t.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 a of Object.values(this.state.runs)){if(B.includes(a.state))continue;if(e>=Date.parse(a.deadlineAt)){this.endRun(a,"stopped","deadline","Ran out of time. The sims were taken back.");this.store.save()}else if(!a.leases.some(n=>this.state.leases[n])&&!await _(a.owner.pid,a.owner.pidStart)){this.endRun(a,"interrupted","owner-exited","The chat closed.");this.store.save()}}const t=this.state.settings.keepRunsHours*36e5;const s=Object.values(this.state.runs).filter(a=>B.includes(a.state)).sort((a,n)=>Date.parse(n.endedAt??n.createdAt)-Date.parse(a.endedAt??a.createdAt));let i=false;s.forEach((a,n)=>{if(n>=200||e-Date.parse(a.endedAt??a.createdAt)>t){delete this.state.runs[a.id];i=true}});if(i)this.store.save()}async status(e={}){const t=await this.budget();const s=Object.values(this.state.sims).map(a=>{const n=a.leaseId?this.state.leases[a.leaseId]:void 0;const o=n?.runId?this.state.runs[n.runId]:void 0;return{...a,owner:n?.owner.label,purpose:o?.goal??n?.purpose,runId:o?.id,lane:o?.lanes[a.udid]?.label,busy:this.busy.has(a.udid)}});const i={farmId:this.state.farmId,counts:{pool:s.length,awake:s.filter(a=>a.power==="awake").length,leased:s.filter(a=>a.leaseId).length},budget:{roomForMoreSims:t.roomFor,freeDiskGB:Math.floor(t.stats.freeDiskBytes/P),availableMemGB:Math.floor(t.stats.availableMemBytes/P),memPressure:t.stats.memPressure,loadAvg1:Number(t.stats.loadAvg1.toFixed(1)),cpuCount:t.stats.cpuCount},templates:Object.values(this.state.templates),sims:s,orphans:Object.values(this.state.orphans),leases:Object.values(this.state.leases).map(a=>({...a,expiresInMs:Math.max(0,Math.round(this.leaseDeadline(a)-this.clock.now()))})),runs:Object.values(this.state.runs).sort((a,n)=>Date.parse(n.createdAt)-Date.parse(a.createdAt)).map(a=>({...a,notes:a.notes.slice(-50),secondsLeft:B.includes(a.state)?0:Math.max(0,Math.round((Date.parse(a.deadlineAt)-Date.now())/1e3))})),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.keys(this.state.orphans),...Object.values(this.state.templates).map(n=>n.udid)]);i.outsideSims=(await this.host.listDevices()).filter(n=>!a.has(n.udid)&&n.available).map(n=>({udid:n.udid,name:n.name,state:n.state,runtime:n.runtime}))}return i}setSettings(e){const t=this.state.settings;const s={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]};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 t)||typeof a!==typeof t[i])throw new l("BAD_REQUEST",`Bad setting ${i}.`);const n=s[i];if(n&&(typeof a!=="number"||!Number.isFinite(a)||a<n[0]||a>n[1])){throw new l("BAD_REQUEST",`${i} must be ${n[0]} to ${n[1]}.`)}}for(const[i,a]of Object.entries(e))t[i]=a;this.store.save();return t}};function Ue(e,t=Date.now()-3*6e4){const s=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 n=Date.parse(a[1].replace(" ","T"));if(!(n>=t))continue;const o=s.get(a[2])??[];o.push(n);s.set(a[2],o)}for(const[i,a]of s){a.sort((n,o)=>n-o);for(let n=2;n<a.length;n++)if(a[n]-a[n-2]<=6e4)return i.replace(/^com\.apple\./,"")}return null}import{promises as It}from"fs";import F from"os";import he from"path";var Ce=6e4;var Ge=class{constructor(){this.disabledRoot=L;this.devicesDir=he.join(F.homedir(),"Library/Developer/CoreSimulator/Devices")}simctl(e,t=Ce){return O("xcrun",["simctl",...e],{timeoutMs:t})}async listDevices(){const e=JSON.parse(await this.simctl(["list","devices","--json"],3e4));const t=[];for(const[s,i]of Object.entries(e.devices??{})){for(const a of i){t.push({udid:a.udid,name:a.name,state:a.state,runtime:s,deviceType:a.deviceTypeIdentifier,dataPath:a.dataPath??he.join(this.devicesDir,a.udid,"data"),available:a.isAvailable!==false})}}return t}async create(e,t,s){return(await this.simctl(["create",e,t,s])).trim()}async clone(e,t){return(await this.simctl(["clone",e,t])).trim()}async boot(e){const t=await k("xcrun",["simctl","boot",e],{timeoutMs:Ce});if(t.code!==0&&!/current state: Booted/i.test(t.stderr)){throw new Error(`simctl boot ${e} failed: ${t.stderr.trim().slice(0,300)}`)}}async shutdown(e){const t=await k("xcrun",["simctl","shutdown",e],{timeoutMs:9e4});if(t.code!==0&&!/current state: Shutdown/i.test(t.stderr)){throw new Error(`simctl shutdown ${e} failed: ${t.stderr.trim().slice(0,300)}`)}}async delete(e){await this.simctl(["delete",e],9e4)}async install(e,t){await this.simctl(["install",e,t],18e4)}async launch(e,t){await this.simctl(["launch","--terminate-running-process",e,t],6e4)}async terminate(e,t){await k("xcrun",["simctl","terminate",e,t],{timeoutMs:2e4})}async openUrl(e,t){await this.simctl(["openurl",e,t],2e4)}async screenshot(e,t,s=1e4){const i=await k("xcrun",["simctl","io",e,"screenshot",t],{timeoutMs:s});return i.code===0&&!i.timedOut}spawn(e,t,s=2e4){return k("xcrun",["simctl","spawn",e,...t],{timeoutMs:s})}async appContainer(e,t){const s=await k("xcrun",["simctl","get_app_container",e,t,"data"],{timeoutMs:2e4});return s.code===0?s.stdout.trim()||null:null}async simTree(e){const t=await Oe();const s=t.find(i=>/launchd_sim/.test(i.command)&&i.command.includes(e));return s?Ee(t,s.pid):[]}async signal(e,t){for(const s of e){try{process.kill(s,t)}catch{}}}async launchdLogTail(e,t=256*1024){const s=he.join(this.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`,"launchd.log");try{const i=await It.open(s,"r");try{const{size:a}=await i.stat();const n=Math.max(0,a-t);const o=Buffer.alloc(a-n);await i.read(o,0,o.length,n);return o.toString("utf8")}finally{await i.close()}}catch{return""}}async hostStats(){const[e,t,s,i]=await Promise.all([O("vm_stat",[],{timeoutMs:1e4}),k("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{timeoutMs:5e3}),k("sysctl",["-n","vm.swapusage"],{timeoutMs:5e3}),O("df",["-k",F.homedir()],{timeoutMs:1e4})]);const a=Number(/page size of (\d+)/.exec(e)?.[1]??16384);const n=u=>Number(new RegExp(`${u}:\\s+(\\d+)`).exec(e)?.[1]??0);const o=n("Pages free")+n("Pages inactive")+n("Pages speculative")+n("Pages purgeable");const r=Number(t.stdout.trim());const c=/used = ([\d.]+)M/.exec(s.stdout)?.[1];const h=i.trim().split("\n").pop()??"";const p=Number(h.split(/\s+/)[3]??0);return{totalMemBytes:F.totalmem(),availableMemBytes:o*a,memPressure:r>=4?"critical":r>=2?"warn":"normal",swapUsedBytes:c?Number(c)*1024*1024:0,freeDiskBytes:p*1024,cpuCount:F.cpus().length,loadAvg1:F.loadavg()[0]}}async showWindow(e){const t=await k("open",["-b","com.apple.iphonesimulator","--args","-CurrentDeviceUDID",e],{timeoutMs:2e4});if(t.code===0)return;await O("open",["-b","com.apple.dt.Devices"],{timeoutMs:2e4})}async xcodeBuild(){const e=await k("xcodebuild",["-version"],{timeoutMs:2e4});return e.stdout.trim().replace(/\s+/g," ")||"unknown"}async clientsOf(e){const t=await Oe();const s=new Set((await this.simTree(e)).map(a=>a.pid));const i=new Set(Ee(t,process.pid).map(a=>a.pid));return t.filter(a=>!i.has(a.pid)&&!s.has(a.pid)&&a.command.includes(e)&&!/launchd_sim|CoreSimulator/.test(a.command))}async kill(e,t){if(!await _(e,t))return;try{process.kill(e,"SIGTERM")}catch{return}for(let s=0;s<20;s++){await new Promise(i=>setTimeout(i,100));if(!await _(e,t))return}if(await _(e,t)){try{process.kill(e,"SIGKILL")}catch{}}}};import Dt from"net";import{chmodSync as At,rmSync as Mt,appendFileSync as Tt}from"fs";import{timingSafeEqual as $t}from"crypto";import{promises as Rt,existsSync as ue,mkdirSync as pe,readFileSync as N,renameSync as me,writeFileSync as Y,rmSync as W,openSync as Bt,fsyncSync as _t,closeSync as xt,chmodSync as Nt,readdirSync as jt,statSync as Fe}from"fs";import Lt from"os";import v from"path";import{createHash as Pt,randomBytes as q}from"crypto";function A(){return process.env.BUOY_FARM_HOME||v.join(Lt.homedir(),".buoy","farm")}var M=(e=A())=>({home:e,lock:v.join(e,"owner.lock"),lockOwner:v.join(e,"owner.lock","owner.json"),socket:v.join(e,"farmd.sock"),token:v.join(e,"token"),state:v.join(e,"state.json"),journal:v.join(e,"journal.ndjson"),log:v.join(e,"farmd.log"),shots:v.join(e,"shots")});function fe(e=A()){pe(e,{recursive:true,mode:448});try{Nt(e,448)}catch{}}function We(e=A()){const t=M(e).token;try{const s=N(t,"utf8").trim();if(s)return s}catch{}fe(e);try{Y(t,q(24).toString("hex")+"\n",{mode:384,flag:"wx"})}catch(s){if(s.code!=="EEXIST")throw s}for(let s=0;s<20;s++){const i=N(t,"utf8").trim();if(i)return i;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,25)}throw new Error(`farm token at ${t} is empty`)}async function K(e=A()){try{return JSON.parse(await Rt.readFile(M(e).lockOwner,"utf8"))}catch{return null}}async function V(e){return!!e&&await _(e.pid,e.start)}async function Ut(e,t){const s=v.join(e,"owner.claim");for(let i=0;i<20;i++){try{pe(s,{mode:448});Y(v.join(s,"pid"),`${process.pid}
5
+ ${await G(process.pid)??""}`);try{return await t()}finally{W(s,{recursive:true,force:true})}}catch(a){if(a.code!=="EEXIST")throw a;const n=Fe(s,{throwIfNoEntry:false});if(n&&Date.now()-n.mtimeMs>3e4){let o=true;try{const[r,c]=N(v.join(s,"pid"),"utf8").split("\n");o=!await _(Number(r),c||void 0)}catch{}if(o){try{me(s,v.join(e,`owner.claim-dead-${q(4).toString("hex")}`))}catch{}}}await new Promise(o=>setTimeout(o,100))}}return null}async function Ct(e,t,s=A()){fe(s);const i=M(s);const a=await G(process.pid)??"";for(let n=0;n<3;n++){try{pe(i.lock,{mode:448})}catch(r){if(r.code!=="EEXIST")throw r;const c=await K(s);if(c&&await V(c))return null;if(!c){await new Promise(u=>setTimeout(u,300));const p=await K(s);if(p&&await V(p))return null}const h=await Ut(s,async()=>{const p=await K(s);if(p&&await V(p))return false;if(!ue(i.lock))return true;me(i.lock,v.join(s,`owner.stale-${q(4).toString("hex")}`));return true});if(h===false)return null;continue}const o={pid:process.pid,start:a,version:e,host:t,since:new Date().toISOString()};Y(i.lockOwner,JSON.stringify(o),{mode:384});Gt(s);return{info:o,release(){try{const r=JSON.parse(N(i.lockOwner,"utf8"));if(r.pid!==process.pid)return}catch{return}try{W(i.socket,{force:true})}catch{}W(i.lock,{recursive:true,force:true})}}}return null}function Gt(e){try{for(const t of jt(e)){if(t.startsWith("owner.stale-")||t.startsWith("owner.claim-dead-"))W(v.join(e,t),{recursive:true,force:true})}}catch{}}function qe(){return{version:z,seq:0,farmId:q(6).toString("hex"),nextIndex:1,templates:{},sims:{},orphans:{},leases:{},runs:{},locks:{},requests:{},events:[],settings:{...se}}}var He=e=>Pt("sha256").update(JSON.stringify(e)).digest("hex").slice(0,12);function Qe(e,t,s){const i=Bt(e,s,384);try{Y(i,t);_t(i)}finally{xt(i)}}var ze=class{constructor(e=A()){this.fenced=false;this.pending=[];this.open=new Map;this.p=M(e);fe(e);this.state=this.load()}get shotsPath(){return this.p.shots}fence(){this.fenced=true}assertActive(){if(this.fenced)throw new Error("This farm has stopped.")}load(){let e=qe();if(ue(this.p.state)){const a=JSON.parse(N(this.p.state,"utf8"));if(a.version>z){throw new Error(`farm state is version ${a.version}; this farmd understands ${z}. Update Buoy.`)}e={...qe(),...a,settings:{...se,...a.settings}}}delete e.settings.activeSlots;const t=new Map;const s=new Set;let i=e.seq;if(ue(this.p.journal)){for(const a of N(this.p.journal,"utf8").split("\n")){if(!a.trim())continue;let n;try{n=JSON.parse(a)}catch{continue}const{sum:o,...r}=n;if(He(r)!==o||n.seq<=e.seq)continue;i=Math.max(i,n.seq);if(n.type==="intent")t.set(n.seq,n);else{t.delete(n.seq);s.add(n.seq)}}}for(const a of e.openIntents??[]){if(!s.has(a.seq)&&!t.has(a.seq))t.set(a.seq,{...a,type:"intent",sum:""})}this.pending=[...t.values()].sort((a,n)=>a.seq-n.seq);for(const a of this.pending)this.open.set(a.seq,a);e.seq=i;e.openIntents=[];return e}intent(e,t){this.assertActive();const s=++this.state.seq;const i={seq:s,at:new Date().toISOString(),type:"intent",op:e,data:t};this.append(i);this.open.set(s,{...i,sum:""});return s}result(e,t,s){this.assertActive();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:t,data:s})}append(e){this.assertActive();Qe(this.p.journal,JSON.stringify({...e,sum:He(e)})+"\n","a")}save(){this.assertActive();const e=`${this.p.state}.${q(4).toString("hex")}.tmp`;this.state.openIntents=[...this.open.values()].map(({seq:t,at:s,op:i,data:a})=>({seq:t,at:s,op:i,data:a}));Qe(e,JSON.stringify(this.state,null,1),"w");me(e,this.p.state);try{if(Fe(this.p.journal).size>2*1024*1024)W(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)}};function Ft(e=A()){try{const t=JSON.parse(N(M(e).state,"utf8"));return new Set([...Object.keys(t.sims??{}),...Object.keys(t.orphans??{}),...Object.values(t.templates??{}).map(s=>s.udid)])}catch{return new Set}}var Je=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"]);async function Wt(e={}){const t=e.home??A();const s=M(t);const i=await Ct(D,e.hostLabel??"node",t);if(!i)return null;const a=n=>{try{Tt(s.log,`${new Date().toISOString()} ${n}
6
+ `)}catch{}e.log?.(n)};try{return await qt(e,t,i,a)}catch(n){i.release();throw n}}async function qt(e,t,s,i){const a=M(t);const n=We(t);const o=new ze(t);const r=new Pe({host:e.host??new Ge,store:o,log:i,pollMs:e.pollMs,readyTimeoutMs:e.readyTimeoutMs});await r.recover();Mt(a.socket,{force:true});const c=new Set;let h=Date.now();let p=false;let u=0;const E=Dt.createServer(f=>{c.add(f);f.on("close",()=>c.delete(f));f.on("error",()=>{});let d="";f.on("data",m=>{d+=m.toString("utf8");if(d.length>1024*1024)return f.destroy();let w;while((w=d.indexOf("\n"))>=0){const y=d.slice(0,w);d=d.slice(w+1);if(y.trim())void et(f,y)}})});const R=(f,d,m)=>{if(!f.destroyed)f.write(JSON.stringify({id:d,...m})+"\n")};async function et(f,d){h=Date.now();let m;try{m=JSON.parse(d)}catch{return R(f,-1,{ok:false,error:{code:"BAD_REQUEST",message:"Not JSON."}})}u++;try{if(!m||typeof m!=="object")throw new l("BAD_REQUEST","Use a request object.");if(!Ht(m.token,n))throw new l("UNAUTHORIZED","Bad farm token.");if(!m.client||typeof m.client.clientId!=="string"||!m.client.clientId.trim())throw new l("BAD_REQUEST","Each call needs a clientId.");if(p&&!["hello","status"].includes(m.method)){throw new l("HOST_BUSY","The farm is stopping.","Try again in a few seconds.",true)}if(m.v>D&&Je.has(m.method)){throw new l("FARMD_VERSION",`This farm runs protocol ${D}; the client speaks ${m.v}.`,"Ask it to stop when idle (method shutdown), then start a newer farm.")}if(m.v<D&&Je.has(m.method)){throw new l("READ_ONLY",`This client speaks farm protocol ${m.v}; the farm runs ${D}.`,"Update Buoy to change the farm.")}Qt(m);const w=await tt(m);R(f,m.id,{ok:true,result:w})}catch(w){const y=w instanceof l?w.toJSON():{code:"HOST_ERROR",message:w.message,retryable:false};R(f,m?.id??-1,{ok:false,error:y})}finally{u--}}async function tt(f){const d=f.params??{};const m=f.client;const w=y=>r.once(m.clientId,f.requestId,{m:f.method,q:d},y);switch(f.method){case"hello":return{protocol:D,pid:process.pid,host:s.info.host,farmId:r.state.farmId};case"status":return r.status({outside:d.outside});case"budget":return r.budget({ramGB:d.ramGB??0,diskGB:d.diskGB??0});case"setup":return w(()=>r.createTemplate(d));case"grow":return w(async()=>(await r.grow(d.templateId,d.count)).map(y=>y.name));case"shrink":return w(()=>r.shrink(d.templateId,d.count));case"removeTemplate":return w(()=>r.removeTemplate(d.templateId));case"acquire":return w(async()=>{const{lease:y,run:I,sims:te}=await r.acquire({...d,owner:m});return{runId:I.id,leaseId:y.id,epoch:y.epoch,deadlineAt:I.deadlineAt,capAt:I.capAt,sims:te.map(Q=>({udid:Q.udid,name:Q.name,buoyDeviceId:Q.buoyDeviceId,profile:Q.profile}))}});case"renew":return r.renew(d.leaseId,d.epoch,m.clientId);case"release":return w(()=>r.release(d.leaseId,d.epoch,d.reset,m.clientId,d.keepRun===true));case"action":return w(()=>r.action(d.leaseId,d.epoch,d.udids,d.action,d.arg,m.clientId));case"lock":return r.lock(d.name,d.leaseId,d.epoch,d.waitMs??0,m.clientId);case"unlock":return r.unlock(d.name,d.leaseId,d.epoch,m.clientId);case"park":return w(async()=>(await r.parkSim(d.udid,"asked"),"parked"));case"wake":return w(async()=>(await r.bootSim(d.udid)).power);case"reset":return w(async()=>(await r.fastReset(d.udid),"reset"));case"rebuild":return w(async()=>(await r.rebuildSim(d.udid)).udid);case"delete":return w(async()=>(await r.deleteSim(d.udid),"deleted"));case"endLease":{const y=r.state.leases[d.leaseId];if(!y)return{};if(y.owner.clientId!==m.clientId&&d.force!==true){throw new l("NOT_OWNED",`Lease ${d.leaseId} belongs to ${y.owner.label}.`,"Pass force: true only after a person confirms.")}return w(()=>r.endLease(d.leaseId,`ended by ${m.label}`,"full",y.owner.clientId===m.clientId?"released":"user"))}case"report":return w(async()=>r.report(d,m.clientId));case"extend":return w(async()=>r.extend(d.runId,d.minutes,m.clientId));case"finish":return w(async()=>r.finish(d.runId,d.outcome,d.summary,m.clientId));case"stopRun":{const y=r.state.runs[d.runId];if(y&&y.owner.clientId!==m.clientId&&d.force!==true){throw new l("NOT_OWNED",`Run ${d.runId} belongs to ${y.owner.label}.`,"Pass force: true only after a person confirms.")}return w(async()=>r.stopRun(d.runId))}case"stopAll":if(d.force!==true)throw new l("BAD_REQUEST","stopAll needs force: true, after a person confirms.");return w(async()=>r.stopAll());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!==false&&(!r.idle()||u>1))return{stopping:false,reason:"busy"};void ee("asked to stop");return{stopping:true};default:throw new l("BAD_REQUEST",`Unknown method ${f.method}.`)}}await new Promise((f,d)=>{E.once("error",d);E.listen(a.socket,()=>f())});At(a.socket,384);i(`farmd ${D} up, pid ${process.pid}, host ${e.hostLabel??"node"}`);const ye=setInterval(()=>{void r.tick().catch(d=>i(`tick: ${d.message}`));const f=Date.now()-h;if(e.idleExitMs&&c.size===0&&f>e.idleExitMs){void(async()=>{await r.parkAllIdle();if(r.idle()&&c.size===0&&u===0&&Date.now()-h>e.idleExitMs)await ee("idle")})()}},e.tickMs??5e3);ye.unref?.();let Z=null;function ee(f,d=10*6e4){if(!Z)Z=st(f,d);return Z}async function st(f,d){if(p)return;p=true;clearInterval(ye);i(`farmd stopping: ${f}`);const m=Date.now()+d;while(u>0&&Date.now()<m)await new Promise(I=>setTimeout(I,200));const w=await r.drain(Math.max(0,m-Date.now()));let y=false;if(u>0||!w||ne()>0){o.save();r.fence();y=true;i(`farmd stopping with ${u} call(s) and ${ne()} child process(es) still running; killing them`);lt();const I=Date.now()+5e3;while(ne()>0&&Date.now()<I)await new Promise(te=>setTimeout(te,100))}await r.settleFiles();if(!y&&u===0){try{await r.parkAllIdle()}catch{}}for(const I of c)I.destroy();await new Promise(I=>E.close(()=>I()));if(!y){o.save();r.fence()}s.release();e.onExit?.(f)}return{engine:r,owner:s,stop:ee}}function Ht(e,t){if(typeof e!=="string"||e.length!==t.length)return false;return $t(Buffer.from(e),Buffer.from(t))}function Qt(e){const t=o=>{throw new l("BAD_REQUEST",o)};const s=e.params??{};if(typeof s!=="object"||Array.isArray(s))t("Params must be an object.");const i=(o,r=false)=>{if(r&&s[o]===void 0)return;if(typeof s[o]!=="string"||!s[o].trim())t(`${o} must be text.`)};const a=o=>{if(s[o]!==void 0&&typeof s[o]!=="boolean")t(`${o} must be true or false.`)};const n=(o,r,c,h=false,p=false)=>{const u=s[o];if(p&&u===void 0)return;if(typeof u!=="number"||!Number.isFinite(u)||u<r||u>c||h&&!Number.isInteger(u))t(`${o} must be a number from ${r} to ${c}.`)};switch(e.method){case"status":a("outside");break;case"budget":n("ramGB",0,Number.MAX_VALUE,false,true);n("diskGB",0,Number.MAX_VALUE,false,true);break;case"setup":for(const o of["appPath","deviceType","runtime"])i(o);if(s.seedDir!=null)i("seedDir");i("name",true);i("launchUrl",true);a("warmLaunch");if(!["slim","stock"].includes(s.profile))t("Use slim or stock.");n("metroPort",1,65535,true);break;case"removeTemplate":i("templateId");break;case"grow":case"shrink":case"acquire":i("templateId");n("count",1,64,true);if(e.method==="acquire"){i("purpose");n("ttlMs",1,Number.MAX_VALUE,false,true);n("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);a("launch");a("allowPartial");i("goal",true);i("runId",true);i("kind",true);if(s.labels!==void 0&&(!Array.isArray(s.labels)||s.labels.some(o=>typeof o!=="string")))t("labels must be a list of text.");if(!Number.isInteger(e.client.pid)||e.client.pid<=0)t("The client needs a valid pid.")}break;case"release":if(s.reset!==void 0&&!["fast","full"].includes(s.reset))t("Use fast or full reset.");break;case"action":if(!Array.isArray(s.udids)||s.udids.some(o=>typeof o!=="string"||!o))t("udids must be a list of sim ids.");if(!["reset","rebuild","open-url","screenshot","launch"].includes(s.action))t("Unknown sim action.");i("arg",s.action!=="open-url");break;case"lock":case"unlock":i("name");if(!/^[a-z0-9-]{1,40}$/.test(s.name))t("Lock names are short lowercase words.");n("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);break;case"endLease":i("leaseId");break;case"report":i("runId");i("udid",true);i("note",true);n("sequence",0,Number.MAX_SAFE_INTEGER,true,true);if(s.state!==void 0&&typeof s.state!=="string")t("state must be text.");break;case"extend":i("runId");n("minutes",1,24*60);break;case"finish":i("runId");i("summary",true);i("outcome",true);break;case"stopRun":i("runId");a("force");break;case"stopAll":a("force");break;case"shutdown":a("ifIdle");break}if(["renew","release","action","lock","unlock"].includes(e.method))i("leaseId")}import zt from"net";import{existsSync as Jt}from"fs";import{spawn as Xt}from"child_process";import{randomBytes as Xe}from"crypto";function Yt(e,t=["farmd"]){return()=>{const s=Xt(process.execPath,[...process.execArgv,e,...t],{detached:true,stdio:"ignore",env:{...process.env}});s.unref()}}var Ye=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??A();this.clientIdValue=`c-${Xe(5).toString("hex")}`}get clientId(){return this.owner?.clientId}async connect(){this.token=We(this.home);this.owner={clientId:this.clientIdValue,pid:process.pid,pidStart:await G(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 K(this.home);if(!await V(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 t=Date.now()+(this.opts.startTimeoutMs??2e4);while(Date.now()<t){await $(250);if(await this.tryOpen())return this.checkVersion()}throw new l("HOST_ERROR","The farm did not start in time.",`Check ${M(this.home).log}.`)}async checkVersion(){const e=await this.send("hello",void 0,{});if(e.protocol<D&&this.opts.launcher){const t=await this.send("shutdown",{ifIdle:true},{});if(t.stopping){this.close();await $(500);return this.connect()}}return this}tryOpen(){const e=M(this.home).socket;if(!Jt(e))return Promise.resolve(false);return new Promise(t=>{const s=zt.createConnection(e);s.once("connect",()=>{this.sock=s;s.on("data",i=>this.onData(i));s.on("close",()=>this.onClose());s.on("error",()=>{});t(true)});s.once("error",()=>t(false))})}onData(e){this.buf+=e.toString("utf8");let t;while((t=this.buf.indexOf("\n"))>=0){const s=this.buf.slice(0,t);this.buf=this.buf.slice(t+1);let i;try{i=JSON.parse(s)}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 n=i.error??{};a.reject(new l(n.code??"HOST_ERROR",n.message??"farm error",n.next,n.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,t,s={}){if(!this.sock&&!this.closed)await this.reconnect();return this.send(e,t,s)}send(e,t,s){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:D,token:this.token,client:this.owner,method:e,params:t,requestId:s.requestId};return new Promise((n,o)=>{const r=setTimeout(()=>{this.pending.delete(i);o(new l("HOST_ERROR",`Farm call ${e} timed out.`,void 0,true))},s.timeoutMs??3e4);this.pending.set(i,{resolve:n,reject:o,timer:r});this.sock.write(JSON.stringify(a)+"\n")})}async acquire(e){const{requestId:t,...s}=e;const i=t??`acq-${Xe(4).toString("hex")}`;const a=await this.call("acquire",s,{timeoutMs:15*6e4+(e.waitMs??0),requestId:i});if(this.renewals.has(a.leaseId))return a;const n=setInterval(()=>{this.call("renew",{leaseId:a.leaseId,epoch:a.epoch}).catch(o=>{if(["LEASE_EXPIRED","STALE_EPOCH","RUN_STOPPED","RUN_ENDED"].includes(o.code))this.stopRenew(a.leaseId)})},15e3);n.unref?.();this.renewals.set(a.leaseId,n);return a}stopRenew(e){const t=this.renewals.get(e);if(t)clearInterval(t);this.renewals.delete(e)}async release(e,t,s="fast",i=false){this.stopRenew(e);return this.call("release",{leaseId:e,epoch:t,reset:s,keepRun:i},{timeoutMs:6e4,requestId:`rel-${e}-${t}`})}close(){this.closed=true;for(const e of[...this.renewals.keys()])this.stopRenew(e);this.sock?.end();this.sock=null}};async function Kt(e){return new Ye(e).connect()}import H from"path";import Vt from"os";import{existsSync as we,readdirSync as Zt,statSync as es}from"fs";function ge(e=process.cwd()){let t=e;for(;;){if(we(H.join(t,"buoy.tools.json")))return t;const s=H.dirname(t);if(s===t)return null;t=s}}function Ke(){const e=H.join(Vt.homedir(),"Library/Developer/Xcode/DerivedData");let t=null;try{for(const s of Zt(e)){if(!s.startsWith("exampledevbuild-"))continue;const i=H.join(e,s,"Build/Products/Debug-iphonesimulator/exampledevbuild.app");if(!we(i))continue;const a=es(i).mtimeMs;if(!t||a>t.t)t={p:i,t:a}}}catch{}return t?.p??null}function Ve(e=ge()){return e?H.join(e,"example-dev-build/artifacts/pool-seed"):null}async function ts(e=ge()){const t=Ve(e);let s=[];let i=[];try{const a=JSON.parse(await O("xcrun",["simctl","list","runtimes","--json"],{timeoutMs:3e4}));s=(a.runtimes??[]).filter(o=>o.isAvailable!==false&&/^iOS/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}));const n=JSON.parse(await O("xcrun",["simctl","list","devicetypes","--json"],{timeoutMs:3e4}));i=(n.devicetypes??[]).filter(o=>/iPhone|iPad/.test(o.name??"")).map(o=>({id:o.identifier,name:o.name}))}catch{}return{appPath:Ke(),seedDir:t&&we(t)?t:null,seedProblem:t?oe(t):"Could not find the repo's seed folder.",runtimes:s,deviceTypes:i,slimTurnsOff:ie.map(a=>({name:a.name,downside:a.downside}))}}import ss from"path";var Ze=[{app:"claude",name:"Claude",match:/(^|\/)claude$/i},{app:"codex",name:"Codex",match:/(^|\/)codex$/i},{app:"cursor",name:"Cursor",match:/cursor/i}];async function is(e){const t=await k("ps",["-o","ppid=,comm=","-p",String(e)],{timeoutMs:3e3});const s=/^\s*(\d+)\s+(.+?)\s*$/.exec(t.stdout);return t.code===0&&s?{ppid:Number(s[1]),comm:s[2]}:null}async function as(e){const t=await k("lsof",["-a","-p",String(e),"-d","cwd","-Fn"],{timeoutMs:3e3});const s=t.stdout.split("\n").find(i=>i.startsWith("n"));return s?s.slice(1):null}function ns(e){if(!e)return void 0;return Ze.find(t=>e.toLowerCase().includes(t.app))}async function os(e={}){const t=e.env??process.env;let s=e.pid??process.pid;let i=null;for(let p=0;p<5&&s>1;p++){const u=await is(s).catch(()=>null);if(!u)break;const E=Ze.find(R=>R.match.test(u.comm));if(p>0&&E){i={app:E,pid:s};break}s=u.ppid}const a=i?.app??ns(e.clientName);const n=(i?await as(i.pid).catch(()=>null):null)??process.cwd();const o=(await k("git",["-C",n,"rev-parse","--abbrev-ref","HEAD"],{timeoutMs:3e3}).catch(()=>null))?.stdout.trim();const r=t.CLAUDE_CODE_SESSION_ID?.trim();let c;let h="none";if(r&&(a?.app??"claude")==="claude"){c=`claude:${r}`;h="claude-session"}else if(i){c=`${i.app.app}:${i.pid}:${await G(i.pid)??""}`;h="parent-process"}return{app:a?.app??"other",appName:a?.name??(e.clientName||"Agent"),chatKey:c,chatKeySource:h,project:ss.basename(n)||void 0,projectPath:n,branch:o&&o!=="HEAD"?o:void 0,clientName:e.clientName}}function rs(e){return e.project?`${e.appName} \xB7 ${e.project}`:e.appName}export{D as PROTOCOL_VERSION,z as STATE_VERSION,se as DEFAULT_SETTINGS,l as FarmError,ke as ALWAYS_ENABLED,ie as SLIM_CATEGORIES,ve as slimLabels,U as profileLabels,Se as profileHash,Pe as Engine,Ue as crashLoop,Ge as SimctlHost,A as farmHome,M as paths,ze as Store,Ft as farmOwnedUdids,Wt as startFarmd,Yt as nodeLauncher,Ye as FarmClient,Kt as connectFarm,ge as findRepoRoot,Ke as findExampleBuild,Ve as defaultSeedDir,ts as farmDefaults,os as detectAgent,rs as agentLabel};