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

lim

v0.57.2

Published

Use remote XCode, Gradle, iOS Simulator, Android Emulator and more to build and test apps from Linux, Windows or macOS.

Readme

lim

The official command-line interface for Limrun — create and control cloud mobile sandboxes for Android, iOS, and Xcode.

Workload permissions

Workload permissions control which resources an instance can access. Assets are the first supported resource type. Commands that create instances send an explicit spec.role.permissions list. It is empty unless you supply --permission or request a named build upload:

lim xcode build . --upload releases/app.ipa
lim gradle build . --upload releases/app.apk
lim xcode sync . --permission 'asset:name/releases/*:upload'
lim ios create --permission 'asset:name/fixtures/*:read'

--upload and RBE's --auto-upload add the target's create, upload, and update permissions automatically; no --permission is needed for the upload itself. If you supply --permission, those extra grants are added to the upload grants. The daemon uploads to that concrete target after building. No client-side upload URL request is needed. Automatic upload names cannot contain * or ?; use --permission for intentional patterns. Delegating a pattern requires asset:*:<verb> or broader authority for that verb.

Repeat --permission to add grants at creation. It is available on all four create commands, run, Xcode build/test/sync/RBE, and Gradle build/sync. --reuse-if-exists matches labels, regardless of the requested permissions. A reused instance keeps its existing role; --permission applies only if a new instance is created.

A later build --upload keeps your prepared instance. The CLI reads its live permissions and adds create/upload/update for the exact asset name using a conditional update. It preserves other permissions and retries concurrent changes. Your credentials must control the instance and allow delegation of the resulting role. RBE --auto-upload adds permissions when the tunnel starts; manual RBE uploads add them before uploading. Build daemons request fresh workload credentials for every permission-based upload. The CLI never passes your credentials to the build daemon. Removed grants remain usable by already-issued tokens until expiry, up to ten minutes; signed URLs retain their own expiry. Instance selection still uses labels alone. Auxiliary simulators receive an empty list; ios create --xcode gives both newly created workloads the requested permissions.

--signed-upload-url remains supported and adds no workload permissions. Direct client uploads (asset push) and the keychain signed-URL flow continue to use the caller's credentials.

Installation

Installing through npm or npx requires Node.js 20.18.1 or later.

# npm
npm install -g lim

# npx (no install)
npx lim <command>

DotSlash (standalone, no Node.js required)

Every lim-v* GitHub release includes a DotSlash file named lim. It is a small text file you can commit to your repository; the first time it runs, DotSlash fetches, verifies, and caches a self-contained lim build (with a bundled Node.js runtime) for your platform. Linux, macOS, and Windows are supported, each on x86_64 and arm64.

With dotslash on your PATH, on macOS and Linux:

curl -LOSf https://github.com/limrun-inc/typescript-sdk/releases/download/lim-v0.32.1/lim
chmod +x lim
./lim --version

Windows does not use shebangs, so invoke the file through dotslash explicitly (dotslash lim --version) or create a sibling lim.bat containing @dotslash.exe "%~dpn0" %* as described in DotSlash on Windows.

The same releases also carry the underlying per-platform tarballs (e.g. lim-v0.32.1-<sha>-darwin-arm64.tar.gz) if you prefer to extract one manually and run lim/bin/lim (lim\bin\lim.cmd on Windows) directly.

Migrating from @limrun/cli

lim is the canonical npm package for the Limrun CLI. If you already installed the older scoped package, remove it before installing lim so npm does not hit a global lim binary conflict:

npm uninstall -g @limrun/cli
npm install -g lim

If you already have the lim package installed globally, update it with:

npm install -g lim

Authentication

# Log in via browser (stores API key in ~/.lim/config.yaml)
lim login

# Or provide an API key directly
lim --api-key <YOUR_KEY> android list

# Or use an environment variable
export LIM_API_KEY=<YOUR_KEY>
lim android list

# Log out (removes stored API key)
lim logout

The CLI stores configuration in ~/.lim/config.yaml. This file is compatible with the Go-based lim CLI — if you've already logged in with the Go version, the TypeScript CLI will use the same credentials.

Fast First Run

lim run

lim run is the fastest way to get started with Limrun. Run it inside a clear iOS or Expo project root to install the Limrun agent skills for that project and get the next prompt for your coding agent. Run it anywhere else to clone a sample app (Swift, Expo 56, or Bazel — picked interactively, Swift by default), build it on Limrun, and print a cloud simulator URL.

Global Flags

Most commands support these flags (exceptions: lim skills install does not take --api-key because it does not talk to the API):

| Flag | Description | | ------------------- | ---------------------------------------------------------- | | --api-key <value> | API key (also reads LIM_API_KEY env var) | | --json | Output as JSON instead of human-readable tables | | --quiet | Suppress intermediate logs and print only the final result | | --help | Show help for any command |

Command Structure

Commands are organized by resource (noun-first), so you can discover everything available for a platform with --help:

lim ios --help       # All iOS commands
lim android --help   # All Android commands
lim xcode --help     # All Xcode commands
lim asset --help     # All asset commands

Instance ID is optional on interaction commands. When omitted, the CLI uses the last created instance of the matching type:

lim ios create                  # Creates ios_abc123, remembers it
lim ios screenshot test.png     # Uses ios_abc123 automatically
lim ios tap 100 200             # Still uses ios_abc123
lim session start               # Starts session for ios_abc123

You can always provide an ID explicitly to target a specific instance:

lim ios screenshot test.png --id ios_def456

For repeatable scripts and LLM agents, prefer explicit platform commands plus an explicit --id once you have it:

lim ios get ios_abc123 --json
lim ios screenshot screenshot.png --id ios_abc123
lim android tap --id android_abc123 100 200

This avoids relying on locally cached "last created" state and keeps the target platform explicit.

Commands

  • Run — Get started with Limrun on your project or the sample app
  • iOS — Create, manage, and interact with iOS instances
  • Android — Create, manage, and interact with Android instances
  • Xcode — Create and manage Xcode sandbox instances
  • Assets — Upload and download files (APKs, IPAs, etc.)
  • Sessions — Persistent connections for fast, interactive device control
  • Xcode Build Pipeline — Sync code and run xcodebuild remotely
  • Bazel: Build a Bazel workspace on Limrun's build plane
  • Skills — Install Limrun skills for AI coding agents (Claude Code, Cursor, Codex)

Run

lim run logs in if needed, then chooses the fastest useful path:

lim run
  • In a clear native iOS project, it installs the Limrun iOS/Xcode skill into .agents/skills/ and .claude/skills/.
  • In a clear Expo project, it installs the Limrun iOS/Xcode and Expo skills into .agents/skills/ and .claude/skills/.
  • In any other directory, it asks which sample project you want to run — Swift (limrun-inc/sample-native-app), Expo 56 (limrun-inc/sample-expo56-app), or Bazel (limrun-inc/sample-native-bazel-app) — clones it, builds it with a Limrun iOS simulator + Xcode sandbox, and prints a simulator URL. Non-interactive sessions default to the Swift sample.

After setup, open your coding agent in the project and ask it to build and run the app with Limrun. For manual agent setup, use lim skills install.


iOS

lim ios create          # Create a new iOS instance
lim ios list            # List all ready iOS instances
lim ios get <ID>        # Get details of a specific instance
lim ios delete <ID>     # Delete an instance
lim ios attach-xcode    # Attach an Xcode target to the simulator
lim ios info            # Get device info from a running instance

Create Options

# Basic
lim ios create

# With specific device model
lim ios create --model ipad --rm

# With a specific iOS version
lim ios create --os-version 27

# With pre-installed app from asset storage
lim ios create --install-asset my-app.ipa

# With an Xcode build sandbox created and attached
lim ios create --xcode

# Lock the simulator to an app after it first enters the foreground
lim ios create --force-bundle-id com.example.myapp

# Full options
lim ios create --jurisdiction us --display-name "CI Test" --label env=ci --rm

Flags for ios create:

| Flag | Description | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | --rm | Auto-delete the instance on exit (Ctrl+C) | | --model <iphone\|iphone-duo\|ipad\|watch> | Simulator device model | | --os-version <26\|27> | iOS major version. iPhone and iPad run both, iPhone Duo 27, Watch 26. Default: 26, or 27 on iPhone Duo | | --xcode | Create an Xcode build sandbox and attach the simulator to it | | --jurisdiction <us\|eu\|as> | Jurisdiction the instance must be created in (hard constraint: creation fails when no region there has capacity) | | --region <value> | Deprecated. Region preference (e.g. us-east1), tried first and overflowing to other regions when full. Use --jurisdiction | | --display-name <value> | Human-readable name | | --label <key=value> | Labels (repeatable). Used for filtering and reuse | | --hard-timeout <duration> | Max lifetime (e.g. 1m, 10m, 3h). Default: none | | --inactivity-timeout <duration> | Idle timeout. Default: 3m | | --force-bundle-id <bundle-id> | Lock to an app after it first enters the foreground | | --reuse-if-exists | Reuse an existing instance with exactly the same labels, looked up in the region that handles the create | | --install <file> | Local file to install (auto-uploads, repeatable) | | --install-asset <name> | Asset name to install (repeatable) | | --install-url <url> | Signed download URL of an app to install (repeatable) |

List and Filter

lim ios list                                   # Ready instances
lim ios list --all                             # All states
lim ios list --state creating                  # Filter by state
lim ios list --region us-east1                 # Filter by region
lim ios list --label-selector env=prod         # Filter by labels
lim ios list --json                            # JSON output
lim ios get <ID>                               # Single instance details

Device Interaction

All interaction commands accept an optional --id. When omitted, the last created iOS instance is used.

# Device info
lim ios info
lim ios info --json

# Screenshots
lim ios screenshot screenshot.png
lim ios screenshot app-store.png --format png --size full # Native pixel dimensions, opaque PNG

# Tapping
lim ios tap 100 200
lim ios tap-element --ax-label "Submit"
lim ios tap-element --ax-unique-id btn_ok

# Text input
lim ios type "Hello World"                # real key events, text delegates fire
lim ios type "search query" --enter
lim ios set-text "long text" --focused    # instant, no key events
lim ios set-text "[email protected]" --ax-unique-id email_field
lim ios press-key enter
lim ios press-key "@"
lim ios toggle-keyboard

# Scrolling and swiping
lim ios scroll down --amount 500
lim ios scroll down --amount 500 --momentum 0.4
lim ios scroll down --amount 500 --coordinate 200,400
lim ios swipe --from 200,300 --to 200,600

# Batch multiple actions in one call
lim ios perform --action type=tap,x=100,y=200 --action 'type=typeText,text=Hello World'
lim ios perform --action type=wait,durationMs=500 --action type=pressKey,key=enter
lim ios perform --file ./actions.yaml

# actions.yaml
- type: tap
  x: 100
  y: 200
- type: typeText
  text: "Hello World"

# UI inspection
lim ios element-tree
lim ios element-tree | jq '.'

# Open URLs / deep links
lim ios open-url https://example.com
lim ios open-url myapp://settings

# Low-level simulator access
lim ios simctl -- listapps booted
lim ios xcrun -- --sdk iphonesimulator --show-sdk-version
lim ios xcodebuild -- -version
lim ios push-file ./fixtures/payload.json payload.json
lim ios ls Documents --bundle-id com.example.app --container-type data
lim ios pull-file Documents/photo.jpeg ./photo.jpeg --bundle-id com.example.app --container-type data
lim ios delete-file Documents/photo.jpeg --bundle-id com.example.app --container-type data
lim ios lsof

Debugging with LLDB

Attach LLDB to a running app. LLDB runs next to the simulator, so this works from any machine, including Linux. The app must be a debug build: Xcode adds the get-task-allow entitlement LLDB needs to those.

# One-shot: run commands and exit
lim ios lldb --bundle-id com.example.app -- --batch -o "bt all"

# Load symbols from a separate .dSYM, then break and inspect
lim ios lldb --bundle-id com.example.app --dsym ./MyApp.app.dSYM -- --batch \
  -o "breakpoint set -f ContentView.swift -l 42" -o "continue" -o "frame variable"

# Script from standard input
printf 'bt\nframe variable\n' | lim ios lldb --bundle-id com.example.app

# Live session: type LLDB commands, `quit` or Ctrl-D to detach
lim ios lldb --bundle-id com.example.app

Arguments after -- go to lldb unchanged. Without --batch the session reads standard input until it ends, so pass --batch when nothing will close it. The app keeps running after LLDB detaches.

An app that lim xcode build installed on the simulator gets its debug symbols automatically: the Xcode instance sends them to the simulator when the session starts, so file and line breakpoints and frame variable work with no flag. Use --dsym for an app built anywhere else.

A simulator runs one LLDB session at a time. Backtraces, breakpoints, stepping, frame variable, Objective-C expressions and Python scripting work. Swift expression evaluation (expr, and po on Swift values) is not available yet.

Xcode Attachment

Attach an existing Xcode target so future simulator builds are installed automatically:

# Attach the last used Xcode target to the last used iOS simulator
lim ios attach-xcode

# Select the Xcode target and simulator explicitly
lim ios attach-xcode sandbox_def456 --id ios_abc123

This is the iOS-oriented equivalent of lim xcode attach-simulator ios_abc123 --id sandbox_def456.

App Management (iOS only)

# Install an app (local file auto-uploads, or use URL)
lim ios install-app ./MyApp.ipa
lim ios install-app https://example.com/app.ipa
lim ios install-app https://example.com/app.ipa --md5 <hex-digest>
lim ios install-app ./MyApp.ipa --launch-mode RelaunchIfRunning

# Launch — streams the app's logs until the app exits or Ctrl+C
lim ios launch-app com.example.myapp
lim ios launch-app com.example.myapp --mode RelaunchIfRunning

# Launch and exit immediately without streaming logs
lim ios launch-app com.example.myapp --detach

# Terminate
lim ios terminate-app com.example.myapp

# List installed apps
lim ios list-apps

Log Streaming (iOS only)

# Tail recent logs
lim ios app-log com.example.myapp --tail 50

# Stream logs continuously (Ctrl+C to stop)
lim ios app-log com.example.myapp --follow

# Stream full simulator syslog
lim ios syslog
lim ios syslog --json

App Performance (iOS only)

The instance samples the CPU and memory of every app you install once a second and keeps the last ten minutes, so lim ios perf answers at once.

# CPU, memory and its peak, threads, disk rates and busiest threads of each app
lim ios perf

# Average and highest CPU and the highest memory over the last 2 minutes
lim ios perf --app com.example.myapp --since 2m

# Stream one line per app per second (Ctrl+C to stop)
lim ios perf --follow

# Save the samples of a whole session, then list the saved files
lim ios create --perf --persist-ttl 24h
lim ios perf-captures

Video Recording

On iOS, recordings include touch ripples and drag trails by default. Use lim ios record start --no-touch-indicators to omit them. Indicators also appear in live views and screenshots while recording, and turn off when recording stops or fails.

lim ios record start
lim ios record start --quality 8
lim ios record stop
lim ios record stop -o recording.mp4
lim ios record stop --presigned-url https://example.com/upload

Built App Sync (iOS only)

# Sync a built .app bundle to the current iOS instance
lim ios sync ./Build/Products/Debug-iphonesimulator/MyApp.app

# Re-sync on changes and relaunch if the app is already running
lim ios sync ./MyApp.app --watch --launch-mode RelaunchIfRunning

# Use a custom delta-sync cache
lim ios sync ./MyApp.app --basis-cache-dir ./.limsync-cache

Forward a simulator port

Reach a server running inside the simulator through a local TCP port:

lim ios forward 8100 --id <instance-ID>
# In another terminal, while WebDriverAgent is running in the simulator:
curl http://127.0.0.1:8100/status

# Local port 8080 forwards to simulator port 3000.
lim ios forward 8080:3000 --id <instance-ID>

# Choose an available local port and print it as JSON.
lim ios forward 0:8100 --id <instance-ID> --json

A single port uses that port on both sides. The local listener binds to 127.0.0.1. The command stays running until Ctrl+C, including with --json, and closes its connections when it exits. Start the server inside the simulator separately and bind it to the simulator's assigned IP or 0.0.0.0. A server bound only to 127.0.0.1 inside the simulator is not reachable through this forward.

HTTP and WebSocket clients connect without adding a Limrun authorization header. The forward carries TCP protocols where the client sends data first; UDP and protocols where the server sends first are not supported. lim ios tunnel and lim ios reverse expose services on your machine to the simulator instead.

Maestro (iOS only)

Run the locally installed Maestro CLI against a remote Limrun simulator. The command installs and launches the Maestro XCTest runner on the instance when needed, then wires maestro test to it transparently.

lim ios maestro test flow.yaml
lim ios maestro test flows/

# Extra maestro flags go after --
lim ios maestro -- test flow.yaml --include-tags smoke --test-output-dir artifacts

# Creating the instance with the runner preinstalled skips the install step
lim ios create --install-asset appstore/maestro-ios-runner-2.5.1.tar.gz

Android

lim android create       # Create a new Android instance
lim android list         # List all ready Android instances
lim android get <ID>     # Get details of a specific instance
lim android delete <ID>  # Delete an instance

Create Options

# Basic (prints a Console URL you can open in the browser)
lim android create

# With apps pre-installed
lim android create --install ./my-app.apk --install ./another.apk

# With a specific Android OS version and device model
lim android create --os-version 15 --model tablet

# Create without opening an ADB tunnel
lim android create --no-connect

# Full options
lim android create --jurisdiction us --display-name "CI Test" --label env=ci --rm

Android-specific flags:

| Flag | Description | | ------------------------ | ----------------------------------------------- | | --os-version <version> | Android OS major version (14 or 15) | | --model <model> | Android device model (phone or tablet) | | --[no-]connect | Start an ADB tunnel immediately (default: true) | | --adb-path <path> | Path to adb binary (default: adb) |

lim android create always prints a Console URL such as https://console.limrun.com/stream/android_... that you can open in the browser for live viewing. For automation, --no-connect is usually the safest default.

Device Interaction

All interaction commands accept an optional --id. When omitted, the last created Android instance is used.

# Screenshots
lim android screenshot screenshot.png

# Tapping
lim android tap 100 200
lim android tap-element --resource-id com.example:id/button
lim android tap-element --content-desc "Sign In button"
lim android tap-element --text "Sign In"
lim android find-element --resource-id com.example:id/button --json

# Text input
lim android type "Hello World"
lim android type "Hello World" --resource-id com.example:id/search_input
lim android press-key enter

# Scrolling
lim android scroll down --amount 500
lim android scroll down --resource-id com.example:id/list --amount 500

# UI inspection
lim android element-tree

# Install app
lim android install-app ./app.apk

# Open URL
lim android open-url https://example.com

# Wi-Fi bandwidth limits
lim android set-wifi-bandwidth --down-kbps 1000
lim android set-wifi-bandwidth --up-kbps 1000
lim android set-wifi-bandwidth --up-kbps 0

# Video recording
lim android record start
lim android record start --quality 8
lim android record stop
lim android record stop -o recording.mp4
lim android record stop --presigned-url https://example.com/upload

# Logcat (arguments after `--` are forwarded to logcat verbatim)
lim android logcat
lim android logcat -- -t 50
lim android logcat -- -T 50 *:E

ADB Tunnel

Connect to a running Android instance for direct adb access:

lim android connect
lim android connect --id android_abc123 --adb-path /usr/local/bin/adb

The tunnel stays open until you press Ctrl+C. While connected, you can use adb commands in another terminal.


Xcode

Standalone Xcode build sandboxes for remote compilation.

lim xcode create          # Create a new Xcode sandbox
lim xcode create --ios    # Create an Xcode sandbox plus a fresh simulator and attach them
lim xcode list            # List all ready Xcode instances
lim xcode get <ID>        # Get details of a specific instance
lim xcode delete <ID>     # Delete an instance
lim xcode attach-simulator <IOS_ID> --id <XCODE_ID>
lim xcode version         # Xcode the sandbox builds with
lim xcode use xcode@27    # Prefer an Xcode version for this workspace (switches the sandbox now)
lim xcode version list    # Xcode versions the sandbox can build with

Disk snapshots

If a snapshot cannot be restored, including a checksum mismatch or download failure, the CLI warns and continues with a cold workspace. The instance stays available and the build can regenerate its dependencies and outputs. A lost snapshot-status connection also leaves the build free to continue.

New Xcode instances automatically restore and save a disk snapshot using the Git origin host/repository and project subdirectory, for example github.com/acme/monorepo/apps/ios. SSH and HTTPS origins produce the same key; branches and commits share it. The key alone controls both restore and save. Discovery reads repository-local Git metadata directly, including worktree and submodule pointers, so it does not require the Git executable. Global Git settings and URL rewrites are not used; config includes require an explicit key.

build, test, and sync use their project path; create uses the current directory. lim run uses its detected project, and lim ios create --xcode uses the current directory. Existing instances keep their original snapshot settings. Without a usable Git origin, pass --snapshot-key explicitly. Use --no-snapshot to create an uncached instance.

From the project directory:

XCODE_ID=$(lim xcode create --quiet)
lim xcode build . --id "$XCODE_ID" --scheme MyApp
lim xcode delete "$XCODE_ID" --wait-snapshot

The first build starts cold. Termination publishes the workspace, and the next instance restores it. Override with --snapshot-key when separate branches or Xcode versions need independent saved state. Use --snapshot-restore-keys "myapp-pr51,myapp-main" at creation for ordered exact-key and literal-prefix fallbacks. Leave --snapshot-paths unset to save the whole workspace. On existing instances, restore keys and paths stay fixed; build --snapshot-key only binds a publication key to a workspace already prepared for snapshots.

The legacy --cache-key, --cache-restore-keys, --cache-paths, and --wait-cache flags remain supported. --basis-cache-dir still controls the separate local source-upload cache.

# Create with options
lim xcode create --rm --jurisdiction us --hard-timeout 1h

# Build (automatically syncs the project path first)
lim xcode build ./MyProject --scheme MyApp --workspace MyApp.xcworkspace

# Run a project command (syncs the current directory first)
lim xcode run -- make api

# Build and upload artifact
lim xcode build ./MyProject --scheme MyApp --upload my-app-build

# Build with app config values available as Xcode build settings
lim xcode build ./MyProject --scheme MyApp --build-setting 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) LIMRUN' --build-setting APP_CONFIG_DEV_LOGIN_SECRET="$DEV_LOGIN_SECRET"

# Signed device build
lim xcode build ./MyProject --scheme MyApp --certificate-p12 ./certificate.p12 --certificate-password "$P12_PASSWORD" --provisioning-profile ./profile.mobileprovision --upload signed-device-build.ipa

# Get an HTTP callback with the result and a build-log URL when the build finishes
lim xcode build ./MyProject --webhook-url https://ci.example.com/hooks/limrun --webhook-header Authorization="Bearer $HOOK_SECRET"

# Headless one-shot build: return after submission and reap the fresh instance shortly after completion
lim xcode build ./MyProject --detach --inactivity-timeout 3s

# Snapshot the active build (or show the latest completed build)
lim xcode logs

# Replay the active build and follow it to completion
lim xcode logs --follow

# Tag the callback so your endpoint can tell builds apart; labels come back verbatim in the payload's "labels" field
lim xcode build ./MyProject --detach --webhook-url https://ci.example.com/hooks/limrun --webhook-label pipeline=release --webhook-label commit="$GIT_SHA"

# Same callback, configured through the environment instead of the command line
LIM_WEBHOOK_URL=https://ci.example.com/hooks/limrun \
  LIM_WEBHOOK_HEADERS="Authorization=Bearer $HOOK_SECRET" \
  lim xcode build ./MyProject --detach

# Attach an existing simulator so builds auto-install there
lim xcode attach-simulator ios_abc123 --id sandbox_def456

# Pick the Xcode for this workspace once; builds, tests, runs, syncs, RBE and new sandboxes follow it.
# A bare major selects that major's GA release; a major.minor pins an exact version, typically a beta.
lim xcode use xcode@27
lim xcode build ./MyProject           # builds with the Xcode 27 GA
lim xcode build ./MyProject --xcode-version 27.1   # one-off override to the 27.1 beta, not remembered
lim xcode version unset               # forget the preference; the sandbox goes back to the node default

# Tune sync cache or ignore additional paths
lim xcode sync ./MyProject --watch --basis-cache-dir ./.limsync-cache
lim xcode sync ./MyProject --ignore "\\.xcuserdata/" --ignore "^DerivedData/"
lim xcode sync ./MyProject --ignore "^\\.env($|\\.)" --ignore "\\.xcconfig$"

Sync precedence is: the client basis cache is always excluded; --include wins over --ignore; --ignore wins over built-in .env and .xcconfig force-includes; built-in exclusions and .gitignore rules then apply. Match an ignored parent directory to prune its subtree. Without --ignore, Xcode sync continues to include gitignored .env, .env.*, and .xcconfig files.


Assets

Assets are files (APKs, IPAs, configs, etc.) stored in Limrun's cloud storage for use with instances.

# Upload a file
lim asset push ./my-app.apk
lim asset push ./my-app.ipa -n custom-name

# Download a file
lim asset pull asset_abc123
lim asset pull asset_abc123 -o ./downloads
lim asset list --name my-app.apk --json
lim asset pull asset_abc123

# List assets
lim asset list
lim asset list --name my-app
lim asset list --name-prefix builds/
lim asset list --download-url
lim asset list --include-app-store

# Get a specific asset
lim asset list asset_abc123

# Delete an asset
lim asset delete asset_abc123

Sessions

Sessions keep a persistent WebSocket connection to an instance in the background, making all interaction commands near-instant (~50ms instead of ~2s per command).

Why Sessions?

Without a session, every command creates a new connection:

lim ios screenshot screenshot.png # ~2s (connect + auth + screenshot + disconnect)
lim ios tap 100 200             # ~2s (connect + auth + tap + disconnect)
lim ios element-tree            # ~2s (connect + auth + fetch + disconnect)
# Total: ~6s for 3 commands

With a session, the connection is created once and reused:

lim session start               # ~2s (one-time connection setup)
lim ios screenshot screenshot.png # ~50ms (reuses connection)
lim ios tap 100 200             # ~50ms (reuses connection)
lim ios element-tree            # ~50ms (reuses connection)
lim session stop                # instant cleanup
# Total: ~2.15s for 3 commands

This makes sessions essential for interactive workflows, AI agent loops, and any scenario where you run multiple commands against the same instance.

Session Commands

# Start a session (defaults to the last remembered iOS instance, otherwise Android, then Xcode)
lim session start

# Or specify an instance explicitly
lim session start --id ios_abc123

# Multiple sessions can run simultaneously
lim session start --id ios_abc123
lim session start --id android_def456

# Check all active sessions
lim session status
lim session status --json

# Stop a specific session
lim session stop --id ios_abc123

# Stop all sessions at once
lim session stop --all

If only one session is active, lim session stop (no ID) stops it automatically. For scripts and agents, prefer --id explicitly so the target instance is unambiguous.

How It Works

Each lim session start spawns an independent background daemon that:

  • Holds a persistent WebSocket connection to that specific instance
  • Listens on its own Unix socket under ~/.lim/sessions/<hashed-instance-id>/
  • All interaction commands automatically detect the matching session and route through it
  • Multiple sessions run in parallel with no shared state

Example: Interactive Testing

lim ios create --model iphone
lim session start

# Fast interaction loop — each command takes ~50ms
lim ios launch-app com.example.myapp --detach
lim ios element-tree | jq '.tree'
lim ios tap-element --ax-label "Login"
lim ios type "[email protected]"
lim ios tap-element --ax-label "Submit"
lim ios screenshot after-login.png

lim session stop
lim ios delete ios_abc123

Example: Multi-Device AI Agent

# Create two instances and start sessions for both
lim ios create --model iphone
lim ios create --model ipad
lim session start --id ios_phone_123
lim session start --id ios_tablet_456

# Agent controls both devices in parallel — ~50ms per command
lim ios launch-app com.example.myapp --id ios_phone_123 --detach
lim ios launch-app com.example.myapp --id ios_tablet_456 --detach

lim ios screenshot phone.png --id ios_phone_123
lim ios screenshot tablet.png --id ios_tablet_456

lim ios tap 200 400 --id ios_phone_123
lim ios element-tree --id ios_tablet_456 --json > tablet-tree.json

# Clean up all sessions
lim session stop --all
lim ios delete ios_phone_123
lim ios delete ios_tablet_456

Example: Automated Test Matrix

# Spin up devices, start sessions, run tests, tear down
DEVICES=("iphone" "ipad")
IDS=()

for model in "${DEVICES[@]}"; do
  ID=$(lim ios create --model $model --json | jq -r '.metadata.id')
  lim session start --id $ID
  IDS+=($ID)
done

# Run tests against all devices
for ID in "${IDS[@]}"; do
  lim ios launch-app com.example.myapp --id $ID --detach
  lim ios screenshot "test_${ID}.png" --id $ID
done

# Tear down
lim session stop --all
for ID in "${IDS[@]}"; do
  lim ios delete $ID
done

Xcode Build Pipeline

Build and test iOS apps remotely using cloud Xcode sandboxes. Instances are created separately and attached: the Xcode sandbox builds, and any simulator attached to it receives the builds automatically.

Option A: Build First, Attach a Simulator (Recommended)

Build on a standalone Xcode instance, then attach a simulator when you need to see the app. The attach installs the latest successful build immediately, and every later successful build reinstalls automatically. Building before creating the simulator also means the simulator doesn't sit idle (burning its inactivity timeout) during a long build.

# 1. Build — creates or reuses an Xcode instance and syncs your project code first
lim xcode build ./MyProject --scheme MyApp --workspace MyApp.xcworkspace

# 2. Attach a simulator; it installs the latest successful build immediately
lim ios create --attach

# 3. Start a session for fast device interaction
lim session start

# 4. Test the built app on the simulator (~50ms per command)
lim ios launch-app com.example.myapp --detach
lim ios element-tree | jq '.'
lim ios screenshot built-app.png

# 5. Clean up
lim session stop
lim ios delete <ios-instance-ID>
lim xcode delete <xcode-instance-ID>

Note: lim xcode build already syncs the project path you pass before invoking xcodebuild, so you do not need to call lim xcode sync first. To create everything up front instead, lim ios create --xcode creates a simulator plus an Xcode instance and attaches them, and lim xcode create --ios does the same starting from the Xcode side.

Option B: Standalone Xcode Instance

Use this when you only need to build (no simulator needed).

# 1. Create a standalone Xcode instance
lim xcode create --rm

# 2. Optionally attach an existing simulator by ID
lim xcode attach-simulator ios_abc123 --id xcode_def456

# 3. Build (automatically syncs the project path first)
lim xcode build ./MyProject --scheme MyApp --workspace MyApp.xcworkspace

# 4. Upload build artifact
lim xcode build ./MyProject --scheme MyApp --upload my-app-build

# Signed device builds default to --sdk iphoneos when signing assets are provided
lim xcode build ./MyProject --scheme MyApp --certificate-p12 ./certificate.p12 --certificate-password "$P12_PASSWORD" --provisioning-profile ./profile.mobileprovision --upload signed-device-build.ipa

# 5. Download the artifact
lim asset pull my-app-build -o ./build-output

Build Behavior

lim xcode build [PATH] automatically performs a one-shot code sync for the given project path before invoking xcodebuild. The sync step automatically ignores build artifacts (build/, DerivedData/, .build/), dependency folders (Pods/, Carthage/Build/, .swiftpm/), and user-specific files (xcuserdata/, .dSYM/).

For headless CI-style builds, pass --detach. The command still creates or resolves the instance and syncs the project, but returns as soon as limbuild accepts the build rather than holding an SSE log stream open until completion. The detached summary includes the exact lim xcode logs <exec-id> --id <instance-id> command and a Console URL for observing the build; optionally pass --webhook-url <url> to receive the terminal result automatically. Pass --inactivity-timeout <duration> (for example 3s) to skip any cached Xcode target and create a fresh one-shot instance with that timeout; it cannot be combined with --id. Active builds continually report activity, so a short timeout only starts expiring once build work stops. The inactivity controller checks approximately every 15 seconds, so actual teardown can lag the configured timeout by that interval.

lim xcode logs and lim gradle logs print a point-in-time snapshot of the active build on the remembered instance, falling back to its latest retained or persisted build when none is active. Pass the build exec ID to select a specific build, --id <instance-id> to override the remembered instance, or --follow to replay an active build and continue streaming until it finishes:

lim xcode logs build-1776140344112378000 --id sandbox_abc123
lim gradle logs --follow

The build-capture flags are also readable from the environment (including a local .env file), so a wrapper or CI system can attach a callback without editing the command line — and without putting a bearer token or a signed URL into argv, where ps and parse-error output can expose it. This applies to both lim xcode build and lim gradle build:

| Variable | Flag | Notes | | ----------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | LIM_WEBHOOK_URL | --webhook-url | Optional build-completion callback. | | LIM_WEBHOOK_HEADERS | --webhook-header | Several headers separated by commas, e.g. Authorization=Bearer x,X-Trace=abc. A literal comma inside a value is written \,. | | LIM_WEBHOOK_LABELS | --webhook-label | Several labels separated by commas, e.g. pipeline=release,commit=9f3a1c. A literal comma inside a value is written \,. | | LIM_SIGNED_UPLOAD_URL | --signed-upload-url | An explicit --upload takes precedence and the variable is ignored, so a wrapper's ambient URL never collides with a chosen asset name. |

A flag passed on the command line always wins over its variable, and env-sourced values are validated and sent exactly like flag-sourced ones. For --webhook-header and --webhook-label that means replacement, not merging: one --webhook-header on the command line drops every header LIM_WEBHOOK_HEADERS would have set (and likewise for labels), so repeat the ambient ones alongside yours if you need both.

Run project commands

lim xcode run [RELATIVE_CWD] -- <COMMAND...> syncs the current directory and runs a one-shot command in the Xcode sandbox. The remote working directory defaults to the synced workspace root:

lim xcode run -- make api
lim xcode run apps/api -- make generate
lim xcode run --env API_ENV=development -- npm run generate
lim xcode run --no-sync -- node --version

Commands stream stdout and stderr and return the remote exit code, but do not provide an interactive terminal.

Select developer tools for shell commands and managed builds with mise preferences:

lim xcode use node@24 pnpm@10 [email protected]
lim xcode tools install
lim xcode tools
lim xcode run -- mise use --pin [email protected]

lim xcode tools inspects an existing sandbox without syncing or creating an instance. Use lim xcode tools --sync to upload local changes first, and --cwd apps/mobile to inspect a nested project. Both forms require an existing sandbox; use --id to choose one.

lim xcode tools install syncs the project and runs mise install in that sandbox. Use --no-sync to install its current selections without syncing, --cwd apps/mobile for a nested project, and --id to choose an existing sandbox. The command never creates or replaces an instance.

lim xcode use xcode@27 is equivalent to lim xcode version set 27: it remembers the workspace Xcode version and switches the existing sandbox. With no sandbox, it saves the preference for the next one. You can combine requests, for example lim xcode use xcode@27 node@24; only the other tools go into mise. Xcode takes a major (27, the GA of that major) or a major.minor (27.1, an exact version). --cwd applies only to mise tools; --workspace chooses the Limrun workspace preference.

lim xcode use selects tools in an existing sandbox after project sync. Mise installs a requested version if needed; use lim xcode tools install to install tools declared by synced project files. Use --cwd apps/mobile for a nested project. Sandbox selections take precedence over synced project tool requests and apply to later runs and builds.

Each run or build resolves mise env --json once. Synced project tool declarations override package-manager detection and image defaults. The first operation with project tool declarations installs them once; later changes require lim xcode tools install. Project numeric requests retain the major, except Ruby, Python, Go, Flutter, Dart and pre-1.0 tools retain major.minor. Limrun imports the [tools] declarations from project mise files. Use an explicit sandbox mise use --pin command to select an exact release.

Both Node 22 and 24 are preinstalled, with Node 22 as the default. Image tools stay outside disk snapshots. User-installed versions under .limbuild-sandbox/home/.mise/ can be saved when the configured snapshot paths cover them, after a successful managed build. If a restored user gem references its old sandbox path, reinstall it explicitly, for example lim xcode run -- mise install --force bundler. Homebrew and Apple tools have separate management; select Xcode with lim xcode use xcode@27 and inspect it with lim xcode version.

For XcodeGen projects whose generated .xcodeproj is gitignored, the server generates it from your synced project.yml automatically before the build — it looks next to a pinned --project/--workspace path, at the synced folder root, and one directory level down. If your spec has a different name or location, pin it with --xcodegen-spec <path>, optionally control the output directory with --xcodegen-project <dir>, and anchor relative paths in the spec with --xcodegen-project-root <dir>; all paths are relative to the synced folder root and mirror xcodegen generate --spec/--project/--project-root. Passing any of these flags always regenerates the project on the server:

lim xcode build ./MyProject --xcodegen-spec specs/app.yml --xcodegen-project ios

Pass --build-setting KEY=VALUE to set Xcode build settings on the build. Any environment-style key is accepted and replaces the Limrun default with the same key. Keys are passed to xcodebuild verbatim; use $(inherited) to append rather than replace, e.g. --build-setting 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) LIMRUN' (single-quote it so your shell does not evaluate $(inherited)) enables #if LIMRUN. An APP_CONFIG_DEV_LOGIN_SECRET value is referenced in Info.plist as <string>$(APP_CONFIG_DEV_LOGIN_SECRET)</string> and read at runtime with Bundle.main; its value is redacted in build logs.

Provide --certificate-p12, --certificate-password, and --provisioning-profile together to sign a real-device build. When signing assets are provided without --sdk, the CLI builds with iphoneos; pass --sdk watchos, --sdk appletvos, or --sdk xros for signed watchOS, tvOS, or visionOS device builds. tvOS and visionOS require an explicit SDK and scheme; their simulator SDKs are not supported yet.

Cloud signing supports the same device SDKs. --upload-to-appstore supports iphoneos, appletvos, and xros:

lim xcode build ./MyProject --scheme TVApp --sdk appletvos --configuration Release --signing-method app-store-connect --team-id "$TEAM_ID" --asc-key-id "$ASC_KEY_ID" --asc-issuer-id "$ASC_ISSUER_ID" --asc-key ./AuthKey.p8 --upload-to-appstore
lim xcode build ./MyProject --scheme VisionApp --sdk xros --configuration Release --signing-method app-store-connect --team-id "$TEAM_ID" --asc-key-id "$ASC_KEY_ID" --asc-issuer-id "$ASC_ISSUER_ID" --asc-key ./AuthKey.p8 --upload-to-appstore

You can override automatic App Store app selection with --asc-app-id <app-id> when using --upload-to-appstore.


Bazel

lim bazel setup points a Bazel workspace at Limrun's build plane. Apple actions run on Limrun's Macs, Android actions on its Linux workers, and every build in your organization shares one cache. Run it in the workspace root, next to MODULE.bazel:

lim bazel setup
bazel build --config=limrun //app:target

Setup writes a gitignored .limrun/ directory and adds try-import %workspace%/.limrun/bazelrc to .bazelrc. Rerun it after upgrading the CLI or when the workspace starts using Apple or Android rules.

For an Android workspace, setup also adds the modules Linux workers need to the root MODULE.bazel. It shows the lines and asks first; --yes adds them without asking.

Bazel gets its credentials from lim-bazel-credentials, which ships with the npm package and uses your lim login or LIM_API_KEY. Nothing secret is written to the workspace. Setup needs Bazel 8 or later.

| Flag | Description | | ------------- | ------------------------------------------------------------- | | --region | Use the build plane in this region instead of the closest one | | --yes, -y | Add the Android lines to MODULE.bazel without asking |

Skills

By default, fetch the latest Limrun skills from limrun-inc/skills@main and install them into the native skills directory of AI coding agents (Claude Code, Cursor, Codex). After installation, the agent auto-discovers the skill and triggers it when you ask things like "build the iOS app" or "show me a screenshot."

# Install all skills for all agents (Expo/Bazel skills only when the folder scan finds matching clues)
lim skills install

# Pin a skills release or an exact commit
lim skills install --ref 0.1.17
lim skills install --ref <full-commit-sha>

# Narrow the install with flags
lim skills install --agents claude --agents cursor
lim skills install --agents codex --scope global
lim skills install --agents cursor --skills limrun-expo-development

# Keep existing skill directories that differ instead of updating them
lim skills install --keep-existing

# Machine-readable output for scripts
lim skills install --json

Flags:

| Flag | Description | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --ref <version-or-sha> | Install a skills release version, such as 0.1.17 or v0.1.17, or a full 40-character commit SHA. Branch names are not accepted. | | --agents <id> | Target agent. Repeat to select multiple. One of: claude, cursor, codex. Defaults to agents with an existing skills directory, or all agents when none exists. | | --skills <name> | Limrun skill to install. Repeat to select multiple. Defaults to all skills; in project scope, Bazel/Detox skills are included only when the folder scan finds clues. | | --scope <project\|global> | project (default) writes into the current directory; global writes into the user's home directory. | | --keep-existing | Keep existing skill directories that differ from the fetched version instead of updating them. | | --json | Emit structured JSON instead of the human summary. | | --quiet | Suppress non-result output. |

The human summary and JSON source.commit field report the resolved commit. Save it and pass it to --ref to install the same source again, even if a release tag moves. Missing versions or commits fail without falling back to main. Skill and agent selection still follow the flags and project scan; use explicit --skills and --agents selections to keep those fixed too. --keep-existing can preserve local content that differs from the pinned source.

Available skills:

  • limrun-xcode: Build and launch iOS apps with remote Xcode.
  • limrun-ios-simulator: Control and test apps on Limrun iOS simulators.
  • limrun-expo-development: Iterate on Expo / React Native apps with remote iOS dev-client workflows.
  • limrun-detox-testing (conditional): Configure, run, and debug Detox against Limrun iOS simulators.
  • limrun-xcode-bazel (conditional): Build Bazel-based iOS apps with remote Xcode.

Install paths:

| Agent | Project | Global | | ----------- | ------------------------- | -------------------------------------------------------------------------- | | Claude Code | .claude/skills/<skill>/ | $CLAUDE_CONFIG_DIR/skills/<skill>/ (default ~/.claude/skills/<skill>/) | | Cursor | .agents/skills/<skill>/ | ~/.agents/skills/<skill>/ | | Codex | .codex/skills/<skill>/ | $CODEX_HOME/skills/<skill>/ (default ~/.codex/skills/<skill>/) |

Behavior:

  • Without flags, the command installs every skill for every agent. In project scope, two skills are conditional: limrun-xcode-bazel is only included when the folder scan finds Bazel workspace markers (WORKSPACE, MODULE.bazel, .bazelrc, ...), and limrun-detox-testing when it finds a detox dependency, a detox section in package.json, or a Detox config file (.detoxrc*, detox.config.*). Global installs are not tied to a project folder, so they skip the scan and install every skill. Pass --skills to override.
  • If an existing skills structure is found for the chosen scope (e.g. .claude/skills/ or .cursor/skills/ already exists), the install adopts it: skills are only installed for the agents that already have a skills directory, instead of creating directories for every agent. Pass --agents to override.
  • The command fetches limrun-inc/skills@main at runtime, so skill updates do not require a new lim release.
  • The command compares fetched vs existing skill directories byte-for-byte. Identical content is reported as Unchanged (no writes).
  • Different content is updated in place by default and reported as Updated. The previous content stays reviewable in your VCS diff, so you can inspect the change or ask your agent to reconcile local edits. Pass --keep-existing to leave differing skill directories untouched (reported as Skipped).

Cursor reads .agents/skills/ natively, so we prefer installing there rather than .cursor/skills/ (an existing .cursor/skills/ directory is adopted instead of creating a second structure). As a bonus, the .agents/skills/ install reaches OpenCode and any other tool that follows the AGENTS.md convention - no extra menu options needed.


Configuration

The CLI reads configuration from multiple sources (in order of precedence):

  1. Command-line flags (--api-key)
  2. Environment variables (LIM_API_KEY, LIM_API_ENDPOINT, LIM_CONSOLE_ENDPOINT), including values loaded from a local .env file
  3. Config file (~/.lim/config.yaml)

Config file keys:

| Key | Default | Description | | ------------------ | ---------------------------- | ----------------------- | | api-key | — | Your Limrun API key | | api-endpoint | https://api.limrun.com | API base URL | | console-endpoint | https://console.limrun.com | Console URL (for login) |

Telemetry

The CLI sends privacy-safe product events for authentication, skill installation, the lim run lifecycle, instance creation intent, and build intent. It never sends command arguments, file or repository paths, hostnames, email addresses, API keys, tokens, or raw error messages.

Before login, events use a random installation ID stored in ~/.lim/telemetry.json. Login links that ID to the authenticated account so the journey remains continuous. Set LIM_DISABLE_TELEMETRY=1 to disable collection.


JSON Output

Commands that advertise --json provide a machine-readable CLI contract. Finite commands return one JSON value; streams and watch modes use newline-delimited JSON. Human-readable diagnostics go to stderr. Always check the exit status. See the JSON output contract for response shapes, stream framing, and errors:

# Get instance details as JSON
lim ios get ios_abc123 --json

# Parse with jq
lim android list --json | jq '.[].metadata.id'

# Use in scripts
INSTANCE_ID=$(lim ios create --json | jq -r '.metadata.id')
lim ios screenshot test.png --id $INSTANCE_ID
lim ios delete $INSTANCE_ID

Workflows

CI Testing: Install and Verify an App

# Create instance and start session for fast commands
lim ios create --install ./build/MyApp.ipa
lim session start

# Verify — each command takes ~50ms with session
lim ios launch-app com.example.myapp --detach
sleep 2
lim ios element-tree | grep "Welcome"
lim ios screenshot test-result.png

# Clean up
lim session stop
lim ios delete ios_abc123

AI Agent Automation

# Create instance
INSTANCE=$(lim ios create --model iphone --json)
ID=$(echo $INSTANCE | jq -r '.metadata.id')

# Start session — all commands now run in ~50ms
lim session start

# Agent can interact at high speed
lim ios tap 200 400
lim ios type "[email protected]"
lim ios tap-element --ax-label "Sign In"
lim ios screenshot result.png
lim ios element-tree --json > ui-state.json

# Tail logs (non-streaming works through session too)
lim ios app-log com.example.myapp --tail 20

# Clean up
lim session stop
lim ios delete $ID

Remote Build + Test on iOS Simulator

# Build first (creates or reuses an Xcode instance, syncs the project path automatically)
lim xcode build ./MyiOSProject --scheme MyApp --workspace MyApp.xcworkspace

# Attach a simulator; it installs the latest successful build immediately
ID=$(lim ios create --attach --json | jq -r '.metadata.id')

# Verify the built app on the simulator
lim session start
lim ios launch-app com.example.myapp --detach
sleep 2
lim ios element-tree | grep "Welcome"
lim ios screenshot test-result.png
lim session stop

lim ios delete $ID

Build-Only with Artifact Upload

lim xcode create --rm --reuse-if-exists --label project=myapp

lim xcode build ./MyiOSProject --scheme MyApp --workspace MyApp.xcworkspace --upload myapp-latest
lim asset list --name myapp-latest --json
lim asset pull asset_abc123 -o ./build-output

Development

Setup

cd packages/cli
npm ci
npm run build

This package is managed with npm (CI runs npm ci against packages/cli/package-lock.json), even though the root SDK and packages/ui use yarn. Use npm here, not yarn, so you do not create a stray yarn.lock. The CLI's build still shells out to yarn to build the root SDK first, so keep yarn installed.

Run commands during development

# After making changes, rebuild and run
npm run build && node bin/run.js <command>

# Or use watch mode in one terminal, run in another
npx tsc --watch            # Terminal 1
node bin/run.js ios list   # Terminal 2

Link globally

npm link

# Now `lim` works anywhere on your machine
lim --help
lim android list

# Unlink when done
npm unlink -g lim

End-to-end testing with local SDK changes (limx)

Most CLI changes also touch the SDK (@limrun/api). npm link only links the CLI, so it keeps running the published SDK from the registry. The limx-from helper builds both the SDK and the CLI from a checkout, links the locally built SDK into the CLI, and installs a separate limx binary that runs that build, so it never clashes with a real lim on your PATH.

# From the repo root (first run also installs the global `limx-from`)
./hack/limx-from

# limx now runs the local SDK + CLI build
limx ios list
limx --where          # prints which checkout limx points at

# Point limx at another worktree (builds + relinks it)
limx-from /path/to/other-worktree

# Stop using limx
limx-from --unlink

Iterating after the initial link:

  • SDK change: yarn build (the link tracks dist/, no relink needed)
  • CLI change: yarn --cwd packages/cli build

Note: any yarn install inside packages/cli restores the published copy of @limrun/api. Re-run limx-from to re-link the local build. limx --where always tells you the current state. (limx --version and --help still self-identify as lim; that's expected.)

Select tools and run commands

Use lim gradle sync [path] to upload source without building; the path defaults to the current directory. Add --watch to keep syncing changes until Ctrl+C.

The sandbox includes these mise-managed tools:

| Tool | Included compatibility lines | Default | | -------------------------- | ---------------------------- | ---------- | | Node.js (with npm and npx) | 22, 24 | 22 | | pnpm | 9, 10, 11 | 10 | | Yarn | 1, 4 | 1 | | Bun | 1 (stable) | 1 | | Java | Temurin 17 | Temurin 17 | | bundletool | 1 | 1 |

Builds use your project's Gradle wrapper. Android SDK, NDK, and CMake packages remain managed by sdkmanager.

Select tool versions and install missing versions explicitly:

lim gradle use node@24 java@temurin-17 pnpm@10
lim gradle tools install
lim gradle tools

lim gradle tools inspects an existing sandbox without syncing or creating an instance. Use lim gradle tools --sync to upload local changes first, and --cwd apps/mobile to inspect a nested project. Both forms require an existing sandbox; use --id to choose one.

lim gradle tools install syncs the project and runs mise install in that sandbox. Use --no-sync to install its current selections without syncing, --cwd apps/mobile for a nested project, and --id to choose an existing sandbox. The command never creates or replaces an instance.

lim gradle use selects tools in an existing sandbox after project sync. Mise installs a requested version if needed. Use --cwd apps/mobile for a nested project, and the same directory with lim gradle tools install --cwd apps/mobile when installing its synced project tools.

Limrun imports [tools] declarations from synced project mise files. These declarations override package-manager detection and image defaults. Most numeric project requests retain the major version; Ruby, Python, Go, Flutter, Dart, and pre-1.0 tools retain major.minor. Limrun can update patch and minor releases within those lines independently. latest opts out of a fixed line.

Each build or run resolves its mise environment once. The first sandbox operation that reads a project mise file runs mise install before exporting that environment. This happens once per sandbox. Without a project mise file, commands use image defaults. Later changes require lim gradle tools install. A failed or cancelled first install stops that operation and also requires an explicit retry. To select an exact release, run lim gradle run -- mise use --pin [email protected]. Subsequent operations respect that override. lim gradle use node@24 replaces it with a Node 24 selection in the sandbox.

Run a command with the selected tools, or supply environment variables to a build:

lim gradle run -- node --version
lim gradle run --env APP_ENV=staging -- npm run generate
lim gradle build . --env APP_ENV=staging

lim gradle build, sync, and run accept --additional-file localPath=remotePath to add files outside the source tree. Repeat the flag for multiple files. Remote paths starting with ~/ target the sandbox home used by builds and tool commands; other paths are relative to the workspace. Create a secret container, then save its value:

lim secret create packages/npm --type generic --metadata '{"registry":"npm"}'
lim secret write packages/npm "$NPM_TOKEN"
lim secret create packages/npmrc
lim secret write packages/npmrc --file .npmrc
printf %s "$NPM_TOKEN" | lim secret write packages/npm --stdin
lim secret get packages/npm
lim secret update packages/npm --metadata '{"registry":"npm","purpose":"builds"}'
lim secret read packages/npm
lim secret delete packages/npm

create creates metadata only and returns a conflict if the name already exists. get returns metadata; without a name, it lists visible secrets using --limit and --offset. update replaces the metadata object without reading or writing the value. Names and types cannot be changed. Metadata is stored by Limrun and must not contain credentials.

write NAME VALUE saves the value argument by default. Use exactly one source: VALUE, --file PATH, or --stdin. File and stdin input preserve all bytes, including trailing newlines. read writes exact bytes to stdout; --output PATH creates a file with mode 0600 and refuses to overwrite an existing file. Values must contain 1 byte to 64 KiB. Reads and writes request a short-lived grant, then transfer values directly between this computer and Google Secret Manager.

Commands accept a secret TID, a name such as packages/npm, or name/packages/npm. create always takes the literal name. Creation, metadata updates, deletion, reads, and writes use their corresponding secret permissions. Read-only and write-only credentials do not need permission to fetch metadata before accessing a value.

For private dependencies, bind secrets that the workload can read:

lim gradle build --file-from-secret '~/.npmrc=packages/npmrc'
lim gradle run --file-from-secret '~/.netrc=github/netrc' -- \
  git -c credential.helper= clone https://github.com/OWNER/REPOSITORY.git vendor/private-library
lim gradle set file-from-secret '~/.npmrc=packages/npmrc'
lim gradle set file-from-secret --unset '~/.npmrc'
lim gradle build --env-from-secret NPM_TOKEN=packages/npm

The same flags and set commands work with lim xcode. Store complete .npmrc, .netrc, or other credential files as generic secrets. Quote the entire file binding so ~/ resolves to the sandbox user's home. Other secret file pa