{"id":14030,"plugin_id":"plugin_asdk_app_6aa95107d094819192324cfe1acf2985","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:08:40.721Z","digest":"c463eadbffec307bd9ee16d8823204dbb00df49bffeb35202edd17f22ab4af14","against":null,"payload":{"name":"limrun-ios-simulator","description":"Drive an app running on a Limrun cloud iOS simulator: launch, tap, type, read the accessibility element tree, screenshot, record video, connect the app to local services, play a video file as the camera, and run timed action chains. Use after a build (from any builder) when the user wants to see, test, or interact with their app on a simulator, or says 'show me a screenshot', 'tap', 'run the UI test', 'record a video', 'connect localhost', 'reach my local server from the simulator', 'mock the camera', or 'launch on simulator'. To build the app first, use limrun-xcode-bazel (Bazel workspaces) or limrun-xcode (xcodebuild projects).","included_files":[],"skill_md_contents":"---\nname: limrun-ios-simulator\ndescription: \"Drive an app running on a Limrun cloud iOS simulator: launch, tap, type, read the accessibility element tree, screenshot, record video, connect the app to local services, play a video file as the camera, and run timed action chains. Use after a build (from any builder) when the user wants to see, test, or interact with their app on a simulator, or says 'show me a screenshot', 'tap', 'run the UI test', 'record a video', 'connect localhost', 'reach my local server from the simulator', 'mock the camera', or 'launch on simulator'. To build the app first, use limrun-xcode-bazel (Bazel workspaces) or limrun-xcode (xcodebuild projects).\"\nuser-invocable: true\neffort: high\n---\n\n# Limrun iOS Simulator\n\nInteract with an app running on a Limrun cloud iOS simulator, from any\nenvironment (Linux, Windows, macOS, VM, container). This skill is build-agnostic:\nit assumes the app was already built and installed by a build skill\n(`limrun-xcode-bazel` for Bazel, `limrun-xcode` for xcodebuild). Keep build\nconcerns in those skills; this one is about driving the running simulator.\n\nNever use local Xcode, local simulators, or local macOS tools.\n\n## Auth and CLI\n\nInstall if needed: `npm install --global lim`. Auth is `lim login` or\n`LIM_API_KEY` (it may be set outside the project, so don't ask for it just\nbecause it's missing from `.env` or the shell). The CLI is the source of truth:\nthe commands in this skill are verified, but if a flag errors or you need one\nnot shown here, check `lim ios <subcommand> --help` instead of guessing.\n\n## Installing an app bundle\n\nYou can either use Limrun remote Bazel or Xcode services to build the app bundle and have\nit installed to the simulator automatically or you can sync a pre-built local `.ipa`\nfile or `.app` folder to the simulator. The main requirement is that it must be\nbuilt for simulator.\n\n### Build and install\n\nA build skill usually attaches the simulator for you (`lim xcode rbe --ios`, or\n`lim xcode build .` then attach). Check what's already there:\n\n```bash\nlim xcode get      # is a simulator attached to the current build target?\nlim ios list       # all running iOS instances\n```\n\nIf none is attached, create one. It installs the last build immediately, so you\ndon't need to rebuild:\n\n```bash\nlim ios create --attach\n```\n\nIf the create (or `lim xcode rbe --ios`) output includes a signed stream URL,\nshare it with the user as a Markdown link, like\n`[Live simulator](<signed-stream-url>)`. If you have a browser the user can see,\nopen the URL there and tell them. Otherwise pass `--no-open` to `create`: it\nskips opening the URL locally and still prints it for sharing.\n\n`lim xcode get` prints a Limrun console URL instead. It opens the same live\nview but requires a console login, so prefer the signed stream URL for sharing.\nIf the console URL is all you have, share it and mention it needs login.\n\n### Install app from local\n\nCreate a new simulator:\n\n```bash\nlim ios create\n```\n\nShare the signed stream URL with the user as a Markdown link, like\n`[Live simulator](<signed-stream-url>)`. If you have a browser the user can see,\nopen the URL there and tell them.\n\nYou can then run the following command to upload a bundle from local:\n\n```bash\nlim ios sync <path to .ipa file or .app folder>\n```\n\nYou can run the same command every time you need to install a new version of the\nbundle. It will patch with the difference and reload it in the simulator.\n\n## Targeting the right instance\n\nMost `lim ios` commands default to the last created instance and resolve the\n\"current\" one from the **git repo / worktree** of your cwd. So even when a\nsimulator is attached and `lim xcode get` shows it, a `lim ios` command can still\nreport `No instance ID provided and no recent ios instance found`, because your\ncwd isn't the git worktree where the instance was created (or isn't a git repo at\nall). This bites most often right after `lim xcode rbe --ios` in a fresh project.\n\nThe reliable recipe when that happens:\n\n```bash\nlim xcode get                          # shows the attached simulator's ID\nlim ios element-tree --id <that-id>    # pass --id to EVERY lim ios command\n```\n\n`lim xcode get` is the dependable source for the attached simulator's ID\n(`lim ios list` also works). Once you have it, pass `--id <ios-instance-id>` to\nall `lim ios` calls for the rest of the session (screenshot, tap, type,\nelement-tree, record). Alternatively, `git init` the project so the workspace\nresolves on its own. When controlling multiple instances, always pass `--id`.\n\n## Reaching services on the local machine\n\nDestination tunnels let an iPhone simulator app keep calling its normal\ndestinations while the CLI dials them from the machine running `lim`. Select\nexact `localhost:port` or literal `IP:port` destinations, or domains that only\nyour machine or VPN can reach:\n\n```bash\nlim ios tunnel \\\n  --id <ios-instance-id> \\\n  --selector localhost:3000 \\\n  --selector localhost:8081 \\\n  --selector \"*.staging.example\" \\\n  --detach\n```\n\nUse the app's normal URLs, such as `http://localhost:3000`. Declaring\n`localhost:3000` also captures loopback forms such as `127.0.0.1:3000` and\n`[::1]:3000`, plus `[::ffff:127.0.0.1]:3000`. Domain selectors (exact\n`api.corp.example` or label-bound wildcard `\"*.staging.example\"`) are\nintercepted on the simulator and dialed from your machine whether or not the\nname resolves on public DNS, so your DNS and VPN apply and TLS stays end to\nend. Apps that resolve DNS themselves over HTTPS bypass domain interception.\nA tunnel carries TCP only: up to ten exact selectors and 64 domain selectors,\nports 1-65535 except 53; CIDRs and UDP are not supported. Start the tunnel\nbefore launching the app: connections opened earlier keep their original route.\n\nOne instance accepts one active destination tunnel, and its selector set is\nimmutable. To add or remove a destination, stop the tunnel and start it again\nwith the complete selector list:\n\n```bash\nlim ios tunnel status --id <ios-instance-id> --json\nlim ios tunnel stop --id <ios-instance-id>\n```\n\nIf the simulator attempts a route while its local service is stopped, the\ntunnel remains active and reports `connection_refused`; restart the service\nwithout recreating the simulator or tunnel.\n\n## Launching the app\n\nThe build skills reinstall and relaunch the app after every successful build,\nso you usually don't need to launch it yourself. When the app is closed (a\nfresh attach to an old build, or after a terminate), launch it by bundle ID:\n\n```bash\nlim ios launch-app <bundle-id>                            # foregrounds it if already running\nlim ios launch-app <bundle-id> --mode RelaunchIfRunning   # restart for a clean state\nlim ios terminate-app <bundle-id>                         # stop it, e.g. to reset app state\n```\n\nIf you don't know the bundle ID, run `lim ios list-apps`.\n\nThe `lim ios launch-app` will stream the logs from the app in realtime for you\nto debug. If you'd like to launch and forget, you can use `--detach` flag.\n\n## Testing changes\n\nWhen simulator interaction is part of the task, test new or changed\nfunctionality with the interaction commands after each build. Focus on what\nchanged, plus a quick smoke test of core flows. Start by reading the element\ntree to see what's on screen before acting:\n\n```bash\nlim ios element-tree\n```\n\n## Interacting with the app\n\nPrefer tapping by accessibility id, then by label, then coordinates as a last\nresort:\n\n```bash\nlim ios tap-element --ax-unique-id startButton\nlim ios tap-element --ax-label \"Save\"\nlim ios tap 201 450\n```\n\n`tap-element` taps with a real synthesized touch. Elements the accessibility\ntree can see are scrolled into view automatically. A selector that matches\nnothing in the tree fails in about a second; iOS creates list rows lazily, so\na below-the-fold row often isn't in the tree at all. For those, pass\n`--scroll-search`: the CLI pages the screen (a few pages down, then up)\nretrying the tap until the row materializes, which can take ~10s. Pass\n`--activate ax` to use an accessibility press instead of a touch (no\nscrolling, works on elements without a usable frame).\n\n**Toolbar / nav-bar items usually can't be tapped by id.** SwiftUI collapses\ntoolbar children into a single nav-bar group, and those items report\n`AXUniqueId: null` even when you set `.accessibilityIdentifier(...)` (regular\ncontent `Button`s do expose it). So `tap-element --ax-unique-id` finds nothing\nfor a nav-bar button. Set an `.accessibilityLabel` / `.accessibilityIdentifier`\nanyway for documentation, but to actually tap it, read its `AXFrame` from the\nelement tree and tap the center by coordinate:\n\n```bash\nlim ios element-tree --id <id> | grep -i -A6 -B2 moon   # find the item's AXFrame\nlim ios tap <x> <y> --id <id>                           # tap the frame's center\n```\n\nFor text input, focus a field first (tap it), then type:\n\n```bash\nlim ios type \"hello world\"     # real key events; errors if no field is focused\nlim ios type \"hi\" --no-require-focus  # skip the focus check: for fields focused by coordinate taps when the accessibility focus scan is unreliable\nlim ios press-key backspace\nlim ios press-key @            # shifted symbols work directly\n```\n\n`type` presses real keys, so text delegates fire and the field's own keyboard\nbehavior applies (a default text field autocapitalizes the first letter, for\nexample). To set a value verbatim with no keyboard behavior, use `set-text`:\n\n```bash\nlim ios set-text \"P@ssw0rd!\" --focused                 # into the focused field\nlim ios set-text \"hello\" --ax-unique-id emailField     # by selector\n```\n\nFor scrolling and drags:\n\n```bash\nlim ios scroll down --amount 300                       # from the screen center\nlim ios scroll down --amount 300 --coordinate 200,400  # from a specific point\nlim ios swipe --from 200,600 --to 200,200              # explicit drag; --duration 800 for a slower, precise one\n```\n\nAfter every interaction, re-run `element-tree` to confirm the UI transitioned.\nNo sleep is needed between a tap and `element-tree`; the tap blocks until done.\n\n```bash\nlim ios element-tree\n```\n\nChain multiple actions with precise timing via `perform`:\n\n```bash\nlim ios perform --action type=tap,x=100,y=200 --action \"type=typeText,text=Hello World\"\nlim ios perform --action type=wait,durationMs=1000 --action type=pressKey,key=enter\nlim ios perform --file ./actions.yaml\n```\n\nRun `lim ios perform --help` for the full action grammar.\n\n## Screenshots and video\n\nScreenshot takes a **positional path** (not `-o`):\n\n```bash\nlim ios screenshot screenshot.png\nlim ios screenshot screenshot.png --id <ios-instance-id>\n```\n\nUse the element tree for functional assertions (element existence, labels, state\nchanges) and screenshots only for visual properties. For anything involving\nmotion (animations, gameplay, streaming UI), prefer video:\n\n```bash\nlim ios record start                       # non-blocking\nlim ios record stop -o /tmp/recording.mp4\n```\n\nFor UI changes, include a demo video in the pull request so the user can see it.\n\n## App container files\n\nList an app's data container before pulling a file so you use the exact path the\napp created. Keep the same `--bundle-id` and `--container-type` flags for list,\npull, push, and delete:\n\n```bash\nlim ios ls Documents --bundle-id com.example.app --container-type data\nlim ios pull-file Documents/recording.mov ./recording.mov \\\n  --bundle-id com.example.app --container-type data\nlim ios push-file ./fixture.json Documents/fixture.json \\\n  --bundle-id com.example.app --container-type data\nlim ios delete-file Documents/fixture.json \\\n  --bundle-id com.example.app --container-type data\n```\n\n`lim ios ls` defaults to the staging-folder root. With `--bundle-id`, it\ndefaults to the app bundle (`--container-type app`); use `data` for the app's\nwritable `Documents`, `Library`, and `tmp` directories. Paths in `ls` output are\nrelative to the selected root and can be copied directly into the other file\ncommands.\n\n## Simulate the camera with a video\n\nFor camera-driven flows (QR-code scanning, document capture, video calls),\nplay a local video file as the simulator's camera. The app sees the frames\nthrough its normal capture pipeline:\n\n```bash\nlim ios camera play ./fixtures/qr-scan.mp4            # loops by default\nlim ios camera play ./fixtures/intro.mp4 --no-loop    # play once, freeze on last frame\nlim ios camera clear                                  # restore the default camera\n```\n\nAny AVFoundation-decodable file works (H.264/HEVC in `.mp4`/`.mov`). Use\n`--no-loop` when the app must observe the end of the clip exactly once (the\nfeed freezes on the last frame rather than stalling).\n\n## Preview URL for humans\n\nUpload the `.ipa` file directly or targz archive of the `.app` folder\nto Limrun Asset Storage and return a preview URL for the user to open it and\ntest the app manually in the browser.\n\nHere is how to upload it:\n```bash\nexport ASSET_NAME=myapp.tar.gz # can be any name\nlim asset push ${ASSET_NAME}\n\necho \"https://console.limrun.com/preview?asset=${ASSET_NAME}&platform=ios\"\n```\n\nOnce the command finishes, you can give the following URL to the user to\nclick to see a simulator where this bundle is pre-installed.\n\n```\nhttps://console.limrun.com/preview?asset=${ASSET_NAME}&platform=ios\n```\n\n## Cleanup\n\nWhen the work is completed, you can delete the iOS simulator.\n\n```bash\nlim ios delete\n```\n\n## Gotchas\n\n- **Instance resolution can miss in a non-git dir.** See \"Targeting the right\n  instance\" above; pass `--id` when in doubt.\n- **`element-tree` can be large.** Pipe through `grep` / `jq` to extract what you\n  need rather than dumping the whole tree into context.\n- **`type` / `perform typeText` may not drive SwiftUI (or React Native) state.**\n  Automated text injection sets the field's value through accessibility, which\n  does **not** always fire a SwiftUI `@Binding` / `onChange` the way a real\n  keystroke does. Symptom: the text appears in the field (and in `element-tree`),\n  but reactive UI tied to it doesn't update (a send button stays disabled, a\n  character counter doesn't move) and submit handlers see empty state. A real\n  keyboard on the live stream works. When automating, drive submit through a\n  tappable control (a button, a suggestion chip) rather than relying on text\n  bound to reactive state, or have the app expose a test affordance.\n- **Toolbar / nav-bar items aren't tappable by id.** See \"Interacting with the\n  app\" above: read the `AXFrame` from `element-tree` and tap by coordinate.\n- **Bundle ID discovery.** If you don't know the bundle ID, run\n  `lim ios list-apps` after a successful install.\n- **Build errors are the build skill's job.** If the app isn't installing, the\n  failure is upstream; go back to `limrun-xcode-bazel` / `limrun-xcode`.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}