npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

simgadget

v3.1.0

Published

Drive iOS simulators from JavaScript: accessibility reads, taps that verify they landed, coordinates that survive rotation — with a current idb_companion resolved for you.

Readme

simgadget

The simulator, as an object. Boot an iOS simulator, read its accessibility tree, and tap controls by name — from TypeScript or JavaScript, with two runtime dependencies and no Python anywhere in the loop.

npm install simgadget

You do not need to install idb or idb_companion. SimGadget brings its own.


Three lines to a booted simulator

import { createSimulator } from "simgadget";

// Creates on the latest installed iOS runtime and waits until the device is
// actually driveable — `simctl boot` returns a minute before accessibility
// answers. Does not throw on a slow boot; ask the handle how it went.
const sim = await createSimulator({ deviceType: "iPhone 16 Pro" });
console.log(sim.udid, sim.lastBoot); // { ready: true, waitedMs: 41000, ... }

await sim.installApp("./build/MyApp.app");
await sim.launchApp("com.example.myapp"); // -> { pid: 18900 }

// Resolve the element, refuse it if disabled or covered, then touch it.
const result = await sim.tap({ label: "Sign Up" });
// { acted: "touch", x: 201, y: 442, count: 1, durationSeconds: 0.1, element: {...} }

await sim.tap({ label: "Email" });
await sim.typeText("[email protected]");

// null is the ordinary "not on screen" answer, not an error.
const banner = await sim.findByLabel("Welcome back");

await sim.screenshot({ path: "/tmp/signup.png" });
await sim.delete();

CommonJS works the same way: const { createSimulator } = require("simgadget").

Nothing in the library ever destroys a simulator except a delete() you wrote yourself.

Requirements

  • macOS on Apple Silicon. iOS simulators are macOS-only and the pinned companion is arm64-only.
  • Xcode, with at least one iOS runtime installed.
  • Node.js 18+.

Every action answers with what happened

There are no success strings. "Tapped successfully" is the bug class this library was reshaped to kill, because a tap that hit the wrong control, a tap that landed 40% of the time and a tap that actuated nothing each reported that same cheerful string.

const result = await sim.tap({ label: "Sound" });
// {
//   acted:   "activation",
//   element: { AXLabel: "Sound", type: "Switch", … },
//   before:  "off",
//   after:   "on",
// }
  • A toggle tells you the state it read back — and when it cannot read it back, it says so rather than claiming success.
  • A touch tells you where it landed and which element it resolved.
  • A rotate tells you which orientation the interface adopted, not which one you asked for. Apps decline orientations; no Face ID iPhone ever adopts upside_down.
  • A failure is a typed error with a code and a payload, so no caller ever regexes a message.
  • "Absent" is an answer, not an exception. findByLabel, findByIdentifier and describePoint return null for a clean miss. Actions that cannot proceed without the element throw.
  • Where there is genuinely nothing to read back — swipe, typeText — the return is void. The companion acks delivery and knows no more than you do.

API

A short summary of the public surface. The authoritative signatures — every option, default and thrown error — are in the generated reference at simgadget.dev/api.

Top-level functions

Everything else hangs off a Simulator handle, which these return.

| Signature | Description | |---|---| | listSimulators(): Promise<SimInfo[]> | Every simulator simctl knows about. | | createSimulator(opts?: CreateOptions): Promise<Simulator> | Creates on the latest available iOS runtime and, by default, boots and waits until actually driveable. Does not throw on a boot that timed out — the simulator exists either way; inspect sim.lastBoot. Throws DeviceTypeNotFoundError carrying the available list. | | attachSimulator(udid: string, opts?: AttachOptions): Promise<Simulator> | Adopts an existing simulator. Verifies it exists; does not probe, does not boot, claims no knowledge of orientation. Call waitReady() next if you need it driveable. | | prefetchCompanion(onProgress?): Promise<string> | Resolves — downloading if necessary — the pinned idb_companion and returns its absolute path. Also exposed as npx simgadget prefetch. |

CreateOptions: deviceType (substring match against simctl devicetypes, newest match wins, default "iPhone"), name, boot (default true), budgetMs (default 55_000), onLog.

Simulator — lifecycle

Nothing implicit ever destroys a simulator; ownership and cleanup policy belong to the caller.

| Member | Description | |---|---| | readonly udid: string | The simulator's UDID. | | readonly name: string | Its simctl device name. | | readonly lastBoot?: ReadyResult | How the last boot or waitReady went. Undefined on a fresh attach. | | state(): Promise<SimulatorState> | Current simctl state. Cheap. | | boot(opts?): Promise<ReadyResult> | Boots and waits until driveable. Does not throw on timeout. An already-booted simulator still performs the wait. | | waitReady(opts?): Promise<ReadyResult> | Waits, without booting, until an accessibility read answers with a real frame. Costs nothing when already up. | | showWindow(): Promise<void> | Opens the Simulator app onto this device. | | shutdown(): Promise<void> | Shuts down. The simulator still exists. | | delete(): Promise<void> | Shuts down and deletes. Stops the companion first and blocks respawn for this udid. The handle is stale afterwards; every method then throws SimulatorNotFoundError. |

Simulator — apps

| Member | Description | |---|---| | installApp(appPath: string): Promise<void> | An .app directory or an .ipa. Throws app-bundle-not-found before calling simctl if the path does not exist. | | launchApp(bundleId, opts?): Promise<{ pid: number \| null }> | opts.terminateRunning relaunches an app that is already running. |

Simulator — reading

Absent is null, not a throw.

| Member | Description | |---|---| | describeScreen(): Promise<ScreenRead> | The complete tree — AXBridge backend, so tab bars, nav bars and toolbars have their contents — with remote-hosted subtrees rebased into screen coordinates, pruned to elements you can act on. ~350 ms. Runs the full recovery ladder internally and throws AccessibilityUnreadableError only when both cures failed. | | screenSize(): Promise<{ width, height }> | Logical screen dimensions from the cheap (~13 ms) read. Refreshes the orientation aspect hint as a side effect. | | findByLabel(label): Promise<AXElement \| null> | Resolves one element by the text you know it by. Fast marker query first (~13 ms), then identifier, then the AXBridge tree walk with typography folding — curly quotes, dashes, non-breaking spaces. | | findByIdentifier(identifier): Promise<AXElement \| null> | Exact match on the accessibility identifier. | | describePoint(x, y): Promise<AXElement \| null> | The element at a logical-space point. Hit-tests (~10 ms). Corrects remote-hosted frames internally. |

Simulator — acting

tap is two different verbs under one name, because callers think of them as one.

| Member | Description | |---|---| | tap(target: TapTarget, opts?: TapOptions): Promise<TapResult> | {x, y} is a literal touch at your coordinates, delivered with the 0.1 s floor. No resolution, no verification — coordinates are you saying where.{label} is "find this and operate it": resolve (ElementNotFoundError), refuse disabled controls (ElementDisabledError), route toggles through accessibility activation with state read-back, refuse hold and multi-tap on toggles (ToggleGestureError), hit-test the centre and refuse if the touch would not land (TapObstructedError, naming the obstruction), then touch. | | swipe(from, to, opts?): Promise<void> | Logical-space swipe. Void because the companion acks delivery and knows no more than you do. opts: durationSeconds, delta. | | typeText(text: string): Promise<void> | Printable ASCII plus newline, as key events. Throws UntypeableTextError listing the offending characters before any event goes out — never a half-typed string. | | pressButton(button, opts?): Promise<void> | "home" | "lock" | "side-button" | "siri" | "apple-pay". home is the only way to leave an app without launching another. |

TapOptions: durationSeconds — a floor of 0.1 s is always applied, so passing less changes nothing; above ~0.5 s UIKit reads it as a long press. count — 2 is a double-tap.

Simulator — orientation

| Member | Description | |---|---| | rotate(to: Orientation): Promise<RotateResult> | Device vocabulary, as the Simulator's own menus use it; the crossed mapping to idb's interface vocabulary is internal. Waits out the animation, then detects what the interface adopted. The result is authoritative for the coordinate space. | | detectOrientation(): Promise<Orientation> | Probes the current orientation (a few hundred ms) and refreshes the hint. Call after something external rotated the simulator. |

Simulator — capture

| Member | Description | |---|---| | screenshot(opts?: ScreenshotOptions): Promise<Screenshot> | Always rotated to match the interface orientation — simctl captures physical portrait regardless. resizeTo: "points" returns the logical dimensions your coordinates live in. | | startRecording(path, opts?): Promise<void> | One recording per handle. Throws recording-already-active. | | stopRecording(): Promise<{ path: string }> | Stops and finalizes. Throws no-active-recording. |

Simulator — low level

You should never need these.

| Member | Description | |---|---| | restartBridge(): Promise<void> | Restarts the guest's CoreSimulator bridge — the wedge cure. The recovery machinery calls this itself; it is public for hosts that want to force it. | | releaseCompanion(): Promise<void> | Stops this simulator's companion process. The exit hook does this anyway; long-lived hosts get tidier teardown. The simulator keeps running, state intact. |


Types

AXElement keeps Apple's key names deliberately — it is the vocabulary of the source data. It is a closed type: no index signature, so a caller reading element.role learns from the compiler that there is no such field.

interface Frame { x: number; y: number; width: number; height: number }

interface AXElement {
  AXLabel?: string;
  AXValue?: string | number;
  AXUniqueId?: string;
  type?: string;        // normalised role: "Button", "Switch", "SearchField", …
  enabled?: boolean;
  frame?: Frame;
  children?: AXElement[];
}

type Orientation = "portrait" | "upside_down"
                 | "landscape_left" | "landscape_right" | string;

type SimulatorState = "Booted" | "Shutdown" | "Booting"
                    | "Shutting Down" | "Creating" | string;

interface SimInfo {
  udid: string; name: string; state: SimulatorState;
  deviceTypeIdentifier: string; runtimeIdentifier: string;
}

interface ReadyResult {
  ready: boolean; waitedMs: number;
  recoveryTried: boolean; recovered: boolean;
}

interface ScreenRead {
  elements: AXElement[];              // [0] is the screen root
  screen: { width: number; height: number };
}

type TapTarget = { x: number; y: number } | { label: string };

type TapResult =
  | { acted: "touch"; x: number; y: number;
      count: number; durationSeconds: number; element?: AXElement }
  | { acted: "activation"; element: AXElement;
      before?: string | number; after?: string | number };

interface RotateResult { requested: Orientation; adopted: Orientation }

interface Screenshot {
  data: Buffer; format: string;
  width: number; height: number;      // pixels of the returned image
  orientation: Orientation;
}

Errors

One base class, SimGadgetError, with a code you branch on. Subclasses exist only where there is a payload to carry. Messages are host-agnostic: they never name a tool, a URL, or remediation that assumes a particular caller.

| code | Class and payload | |---|---| | unsupported-architecture | UnsupportedArchitectureError — message names the architecture | | companion-download-failed | CompanionDownloadError — HTTP failure or checksum mismatch | | companion-start-failed | CompanionStartError · stderrTail: string[] | | simulator-not-found | SimulatorNotFoundError · udid | | device-type-not-found | DeviceTypeNotFoundError · keyword, available: string[] | | no-ios-runtime | SimGadgetError | | not-answering | SimulatorNotAnsweringError · recoveryTried — the wedge, after recovery was tried or suppressed by cooldown | | accessibility-unreadable | AccessibilityUnreadableError · verdict: "booting" \| "unrecoverable" | | element-not-found | ElementNotFoundError · query | | element-disabled | ElementDisabledError · element | | element-unusable-frame | SimGadgetError — resolved, but no frame to aim at | | tap-obstructed | TapObstructedError · element, obstruction, point | | toggle-needs-plain-tap | ToggleGestureError · element, gesture: "hold" \| "multi-tap" | | untypeable-text | UntypeableTextError · characters: string[] | | recording-already-active | SimGadgetError | | no-active-recording | SimGadgetError | | app-bundle-not-found | SimGadgetError |

Coordinates

Every coordinate crossing this API is a logical point in the current interface orientation — the same space the accessibility tree reports, and what you see on screen. The portrait-space translation the companion actually requires is applied inside tap, swipe and describePoint, and never leaks out: a landscape tap at (162, 352) is reported back as (162, 352), not as the portrait pair that went over the wire.

Screenshots are the exception you have to know about, and they are handled: simctl captures in physical portrait regardless of rotation, so screenshot() always rotates the image to match the interface, and reports the orientation it matched. Pixels are still pixels, though — use resizeTo: "points" if you want to compare an image against coordinates.

Configuration

Two environment variables, both about the companion:

| Variable | Description | Default | |---|---|---| | SIMGADGET_COMPANION_PATH | Custom path to the idb_companion binary, used verbatim and ahead of everything else | — | | SIMGADGET_COMPANION_CACHE | Cache root for the downloaded companion | ~/Library/Caches/simgadget (or $XDG_CACHE_HOME/simgadget) |

The former IOS_SIMULATOR_MCP_* spelling of each still works, with one deprecation line on stderr per variable. That fallback goes away two releases after the rename. SIMGADGET_IDB_PATH is a tombstone and throws — there is no Python idb CLI to point at any more.

More

  • simgadget.dev/api — the generated API reference: every signature, option, default and thrown error, with a search box
  • TROUBLESHOOTING.md — common issues and their solutions
  • TESTING_LIBRARY.md — the end-to-end suite: what it covers and what it deliberately does not
  • CONTRIBUTING.md — development setup and the vendored idb submodule
  • simgadget-mcp — the MCP server built on this library, for driving simulators from AI agents

License

MIT. Forked from joshuayoes/ios-simulator-mcp — all foundational work by Joshua Yoes.