How the last boot/waitReady went; set by createSimulator (through
boot()), boot() and waitReady(). Undefined on a fresh attach. A
getter rather than a readonly field because the boot ladder finishes
long after the constructor has run, and a readonly field cannot be
assigned outside the constructor that declares it.
Current simctl state. Cheap; hits simctl list.
Boots and waits until driveable — the whole sequence BOOT_BUG.md taught:
simctl boot, open -a Simulator.app (DECISIONS.md #1), then
waitUntilDriveable's bootstatus wait, settle and poll. This method owns
the whole sequence; createSimulatorWith calls it rather than repeating
the steps and writing the result back into the handle afterwards.
Does not throw on timeout — the simulator exists either way, and a throw
would discard the handle and the udid with it; inspect the result.
No-op boot (already booted) still performs the wait: simctl boot fails
"Unable to boot device in current state: Booted" for one, and that one
shape is swallowed so the wait still runs. Any other simctl boot
failure — a genuinely bad udid, a corrupted device — still propagates.
Optionalopts: { budgetMs?: number }Brings the Simulator.app window to the front for this simulator, without booting or waiting for anything.
Separate from boot() because the MCP's resume path wants exactly this
and nothing else: a session reconnecting to a simulator that is already
up needs its window visible again, and boot() — the only other place
that opens the app (DECISIONS.md #1) — would charge it the whole
driveability ladder, whose settle is unconditional. Sub-second here,
eight seconds and change there, on the call an agent makes most often
after a disconnect.
open -a raises whatever Simulator.app is showing rather than choosing a
device, which is why this takes no argument and promises no more than it
does: with several simulators booted, the frontmost window is not
necessarily this one. Booting is what makes a device frontmost, and this
method is deliberately not that.
Waits (without booting) until an accessibility read answers with a real
frame. This is what the MCP's attach_simulator does after adopting.
Same ladder as boot()'s wait, so "costs nothing when already up" is as
true here as it is there: simctl bootstatus on an already-booted
device returns immediately.
Optionalopts: { budgetMs?: number }Tolerates an already-shut-down simulator, as today's cleanup does
(index.ts:481): a blanket swallow, not just the one shape boot()
matches, because there is nothing else useful to report from a shutdown
that "failed" only because there was nothing left to shut down.
Shuts down and deletes the simulator. Ports destroy_simulator's
sequence (index.ts:1337-1351):
closeCompanion(udid) — before any simctl call. simctl shutdown/delete takes seconds, and without the block a concurrent
call for this simulator would see its companion channel die and
spawn a replacement against a simulator that is about to stop
existing (see CompanionManager.close's header).simctl shutdown, tolerating "already shut down" the same way
shutdown() does.simctl delete — not swallowed; a real failure here means the
simulator was not actually deleted, so the handle must not be marked
stale for it.forgetSimulator, index.ts:888).Before any of that, a recording this handle started is stopped. Deleting
the device out from under simctl io ... recordVideo does not stop it:
observed leaving a recorder running six minutes later against a udid that
no longer existed. Stopping first is also the only ordering that can
finalize the file, since after the delete there is nothing left to record.
.app directory or .ipa. Throws app-bundle-not-found before calling
simctl if the path does not exist — simctl does not support -- as an
option terminator, and this existsSync check is what makes passing a
caller-supplied path through safe.
The screen as a caller should see it. Absorbs today's ui_describe_all
tool body (index.ts:1548-1560): when the pruned read comes back degenerate,
run describeAll's whole ladder of cures — restart our companion, then the
simulator's bridge — and ask again if it brings the screen back. Returning
the screen beats returning an error that tells the caller to retry the call
themselves.
Logical screen dimensions from the cheap (~13ms) unpruned read — today's
getScreenDimensions (index.ts:603) without the caching, which is now
noteRootFrame's business. This replaces public access to the raw tree,
which no caller of the old code ever consumed for anything else
(SIMGADGET.md's Decisions register).
Resolves an element by its accessibility identifier — exact, where a label
is a substring that can drift onto something else as a screen changes.
Ports findByIdentifier (index.ts:180).
No AXBridge fallback: this exists to re-read an element the caller has already found, so a miss means it has genuinely gone rather than that a backend cannot see it, and paying ~300ms to confirm a disappearance helps nobody.
Resolves a single element by the text a caller knows it by. Ports
findByLabel (index.ts:232).
Cheap path first: the companion matches a marker server-side and returns just that element, roughly half a kilobyte against several for a whole tree, in ~13ms. Most lookups end there.
When it misses, the fallback reads the screen and matches here. That covers three separate failures the marker query cannot:
AXLabel only, but a control's visible text is not
always its label — search fields in particular have a null label and
their text in AXValue, making them unnameable.One fallback rather than a chain of marker retries: it is a single round trip (~350ms against ~300ms for another marker query), and matching here means the comparison is ours to fix rather than the companion's to be exact about.
The element at a logical-space point, or null when nothing is there.
Absorbs the ui_describe_point tool body (index.ts:2110-2152).
A frame nowhere near the point it was found at is the telltale sign of a remote-hosted view: the hit-test is right, the frame is measured from the hosting window rather than the screen. The tree read that corrects it costs ~300ms, so it is paid only here, on the reads that are otherwise wrong — and only to replace the rectangle, never the identity, which the point read established by hit-testing.
Rotates the device, waits out the animation, then detects what the
interface adopted and reports both. Ports the rotate tool body
(index.ts:1454).
Detected, not assumed. An app is free to decline an orientation — and one
always does: no Face ID iPhone will adopt upside-down portrait, whatever
its Info.plist says. Reporting the request back as though it had been
obeyed would leave every later coordinate wrong, silently, which is exactly
the failure the returned adopted exists to make visible.
Probes the current orientation and refreshes this handle's hint. Ports the
detect_rotation tool body (index.ts:1516).
The cached logical dimensions go first: a rotation swaps them, and a probe that read the stale pair would compute every candidate position in the space the screen just left.
Tap by label or by coordinate. Two verbs under one name, because callers think of them as one:
{x, y} is a literal touch at the caller's coordinates: no resolution,
no verification, delivered with the 0.1s floor. Coordinates are the
caller saying where, and are taken at their word. Answers
acted: "touch" with no element.{label} is "find this thing and operate it", and the order below is the
specification: resolve, refuse a disabled control, activate a toggle
through accessibility (falling back to a real touch when the action API
cannot reach it), refuse a hold or multi-tap aimed at a toggle, take the
centre of the frame, transform it, hit-test it, and only then touch.Every branch of that order exists because a tap once silently did the wrong
thing and reported success; the reasons are on the pieces
(./ax/tap.ts's decideTapVerb, activateToggle and the hit-test below).
Optionalopts: TapOptionsTypes printable ASCII and newline as key events.
The refusal is here, at the library boundary, rather than left to the idb
client's own — which raises an IdbError naming keycodes. Checking twice
costs a set membership per character and buys the thing design rule 2 asks
for: the typed error is what escapes, with the distinct offending
characters on it, and no caller has to read a message to find out what they
were. Before any event goes out, either way: half a string typed into an
app is not a failure a caller can undo.
Exclusive, so another caller's taps cannot land mid-string.
A swipe between two points in logical space.
Void because there is genuinely nothing to read back: the companion acks delivery and knows no more than we do about what the app made of it.
Both endpoints go through one resolved transform, so they cannot end up in
different coordinate spaces (see portraitTransform). delta and
durationSeconds pass through as given — the client already substitutes 0
for undefined, and the defaults a caller sees belong to whatever host is
asking (DECISIONS.md #15).
Exclusive: a swipe is a stream of events, and another caller's input landing between them scrambles the gesture.
Optionalopts: { durationSeconds?: number; delta?: number }A hardware button. HOME is the only way to leave an app without launching another, which is the first thing a short script needs.
Optionalopts: { durationSeconds?: number }A screenshot of the screen, rotated to match the interface.
simctl captures in physical portrait pixel orientation whatever the device
is doing, so a landscape capture arrives on its side; every caller of this
library would then have to know that and undo it. Rotating here is a
deliberate change from today's screenshot tool, which saves the raw
capture — the spec's Screenshot type mandates it, and orientation says
which way up the returned image is.
resizeTo: "points" is the option ui_view was built out of: the image
comes back in the coordinate space the caller's own taps live in, which is
both far smaller than native pixels and directly comparable with anything
a describe reported.
Starts recording video to path, and resolves once it is under way.
One recording per handle: a second call throws rather than silently replacing the first, because the process it would abandon holds the only reference to a file that never gets finalized.
Stops the recording and returns where it was written.
The path comes from the handle rather than from the caller: startRecording
resolved it, and answering with anything else would hand back a relative
path the caller would have to resolve the same way to use.
Restarts the guest's CoreSimulator bridge (the wedge cure). Step 3's
recovery machinery calls restartSimulatorBridge (./lifecycle.ts)
itself, best-effort; this is the same command, public for a host that
wants to force it, with a real failure surfaced rather than swallowed.
Stops this simulator's companion process. The exit hook
(CompanionManager's process.on("exit")) does this anyway on process
exit; long-lived hosts get tidier teardown by calling it themselves. The
simulator itself keeps running, state intact.
The device type this simulator was created as —
{identifier, name}, wherenameis the model a person says ("iPhone 16 Pro") andidentifieris whatsimctl createtakes.undefinedon an attached handle, deliberately.createSimulatorresolves the device type on its way tosimctl createand so knows it for free;attachSimulatoradopts a udid and never looks one up. A caller that needs it for an attached simulator can pay alistSimulators()for the identifier — which is why this is a field a creator fills rather than a lookup the handle performs.Exists because the answer to
start_simulatornames the model: an agent asks for "iPhone" and the reply is the only place it learns which iPhone it got.