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

@run-cloud/sdk

v0.29.0

Published

TypeScript client for run.cloud, including sandbox-provider compatibility adapters

Readme

@run-cloud/sdk

TypeScript client for the run.cloud API.

npm install @run-cloud/sdk
export RUN_CLOUD_API_KEY="rc_live_..."
import { readFile, writeFile } from "node:fs/promises";
import { Client } from "@run-cloud/sdk";

const cloud = new Client();

const ios = await cloud.ios.create({
  displayName: "CI smoke",
  idempotencyKey: "ci-smoke:run-123",
});
await cloud.ios.install(
  ios.id,
  new Blob([await readFile("Newly.app.tar.gz")], { type: "application/gzip" }),
  { filename: "Newly.app.tar.gz" },
);
await cloud.ios.keepAlive(ios.id);
const opened = await cloud.ios.openUrl(ios.id, "https://example.com");
console.log(opened.sessionId, opened.device, opened.leaseId, opened.url);
await cloud.ios.tap(ios.id, { x: 0.5, y: 0.75 });
await cloud.ios.typeText(ios.id, "Hello from iOS!");
await writeFile("ios-screenshot.png", await cloud.ios.screenshot(ios.id));
await cloud.ios.delete(ios.id);

const android = await cloud.android.create({ displayName: "Android smoke" });
await cloud.android.swipe(
  android.id,
  { x: 0.5, y: 0.8 },
  { x: 0.5, y: 0.2 },
  { durationMs: 250 },
);
await cloud.android.pressKey(android.id, "enter");
await writeFile("android-screenshot.png", await cloud.android.screenshot(android.id));
const recording = await cloud.android.startRecording(android.id, {
  idempotencyKey: "android-readme-example",
});
await cloud.android.openUrl(android.id, "https://run.cloud");
await cloud.android.stopRecording(android.id, recording.id);
await writeFile(
  "android-recording.mp4",
  await cloud.android.downloadRecording(android.id, recording.id),
);
await cloud.android.delete(android.id);

Xcode builds

const submitted = await cloud.xcode.build(process.cwd(), {
  project: "MyApp.xcodeproj",
  scheme: "MyApp",
  sdk: "iphonesimulator",
  idempotencyKey: `xcode-${process.env.GITHUB_SHA ?? "local"}`,
});
const completed = await cloud.xcode.wait(submitted.id, {
  onLog: (text) => process.stdout.write(text),
});
if (completed.status !== "succeeded") {
  throw new Error(`${completed.failure?.reason}\n${completed.failure?.action}`);
}
await writeFile("MyApp.tar.gz", await cloud.xcode.downloadArtifact(completed.id));

const rebuilt = await cloud.xcode.rebundle(process.cwd(), completed.id, {
  idempotencyKey: `rebundle-${completed.id}-${process.env.GITHUB_SHA ?? "local"}`,
});
const ready = await cloud.xcode.wait(rebuilt.id);
const launch = await cloud.mobile.launch(
  { buildId: ready.id },
  { idempotencyKey: `launch-${ready.id}`, orgId: ready.orgId },
);
console.log(launch.id, launch.sessionId, launch.status);

Build records, sanitized logs, failure reasons, and artifacts stay available by ID to members of the owning organization. Source packaging honors the root .gitignore, drops common Xcode and dependency output, and supports explicit include, ignore, and additional-file rules. Full options and intentional differences are documented at docs.run.cloud/ios/build-with-xcode. Use cloud.xcode.events for replayable lifecycle delivery and cloud.capabilities.create for short-lived access to selected resources.

Retry-safe simulator creates

Simulator creates are retry-safe when idempotencyKey is set. Reuse the same key after a timeout or lost response to receive the original session instead of leasing another device.

Install and keep active

cloud.ios.install(id, application, options) and cloud.android.install(id, application, options) upload an application as a user-owned asset and install it into an active session. If an asset is already uploaded, use installAsset(id, assetId) to avoid another transfer.

Call keepAlive(id) while a user is actively viewing a session that has an inactivity timeout. A fleet-confirmed missing device is reported as a non-retryable simulator_gone error; temporary transport failures remain retryable and do not claim the device has ended.

Connect local Metro

openMetroTunnel(id, { localPort }) returns a short-lived reverse-tunnel capability that lets the caller connect a loopback Metro server to an active iOS or Android session. Start the tunnel sidecar before opening the development client URL inside the simulator.

const tunnel = await cloud.ios.openMetroTunnel(ios.id, { localPort: 8081 });
await startTrustedTunnelSidecar(tunnel);

The descriptor contains tunnel credentials and must be treated as a secret. It never contains the fleet lease token or private integration URL. The tunnel is fixed to loopback Metro traffic; callers cannot choose another remote service through this method.

URLs and deep links

cloud.ios.openUrl(id, url), cloud.android.openUrl(id, url), and cloud.simulators.openUrl(id, url, { platform }) return a typed SimulatorOpenUrlResult. It contains ok, platform, sessionId, device, leaseId, and the exact input url without decoding or normalization:

const target =
  "runcloudproof://open/items%2F42?message=hello%20world&return=" +
  "https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof";
const result = await cloud.android.openUrl(android.id, target);
if (result.url !== target) throw new Error("deep-link encoding changed");

The target app must be installed and register its custom scheme. See Open URLs and Deep Links for CLI, SDK, and REST examples, platform behavior, validation, and stable error codes. On iOS, the first custom-scheme handoff can show an Open in “App”? system confirmation after openUrl returns; approve it in the viewer or with an authenticated simulator tap before asserting visible app state.

Simulator interactions

Both cloud.ios and cloud.android expose the same interaction methods: tap, swipe, gesture, typeText, pressKey, pressButton, rotate, reload, scroll, toggleSoftwareKeyboard, simulateMemoryWarning, rotateDigitalCrown, and setRenderDebug. Coordinates use inclusive normalized display space: (0, 0) is the top-left corner and (1, 1) is the bottom-right. typeText uses a US keyboard and accepts tabs, line feeds, and printable ASCII. On iOS, home, appSwitcher, and recents are momentary navigation actions; physical buttons honor durationMs. The shared orientation type includes portrait_upside_down for Android. Current iPhone Simulator sessions support portrait, landscape_left, and landscape_right; an upside-down request throws a non-retryable RunCloudError with code unsupported_action and supported-orientation details. Digital Crown input is not supported by current iOS Simulator or Android Emulator sessions; the typed method returns an actionable API error. Android Emulator sessions also report capsLock, numLock, and scrollLock as unsupported semantic keys.

Every method returns a correlated, action-narrowed acknowledgement. Use interact when the action is assembled dynamically:

const result = await cloud.simulators.interact(
  ios.id,
  { action: "tap", x: 0.5, y: 0.5 },
  { platform: "ios", requestId: "open-settings", timeoutMs: 15_000 },
);
console.log(result.requestId, result.action, result.status, result.result.pointCount);

Pass an AbortSignal to stop waiting without releasing the session. The SDK sends the requested timeout to run.cloud and leaves a short response margin so the server's completion or timeout acknowledgement can arrive.

const controller = new AbortController();
process.once("SIGINT", () => controller.abort());
await cloud.android.typeText(android.id, "cancel-safe", {
  signal: controller.signal,
  timeoutMs: 15_000,
});

API failures throw RunCloudError. In addition to status and detail, interaction failures preserve code, retryable, details, requestId, action, and the accepted/completed timing fields when the API provides them. Screenshot options also accept requestId; screenshot timeout, transport, and invalid-image failures retain the same correlation fields with stable screenshot_* codes. isSimulatorCapacityError(error) narrows a failed create to the typed, retry-safe simulator_capacity_unavailable 503 response.

Accessibility trees

Read the current nested accessibility hierarchy from either active platform:

const tree = await cloud.ios.accessibilityTree(ios.id);
for (const root of tree.roots) {
  console.log(root.role, root.label, root.states, root.children.length);
}

SimulatorAccessibilitySnapshot has a versioned cross-platform role, label, value, state, bounds, identifier, and child schema, plus discriminated iOS and Android native fields. iOS coordinates use points; Android coordinates use physical display pixels. Secure text values are always null.

Options accept timeoutMs (20 seconds by default) and signal. Reads are scoped to the authenticated active session. Released, expired, wrong-platform, and inaccessible sessions fail with a typed RunCloudError instead of reading another device.

Simulator recordings

Both platform clients expose startRecording, listRecordings, getRecording, stopRecording, and downloadRecording. Start accepts an optional idempotency key. Status includes lifecycle events, actionable failure metadata, and the retention deadline; download returns a validated MP4 without exposing backing-storage credentials. Ready recordings remain available after the simulator session is released.

Linux sandboxes

Firecracker microVMs. The default reservation is 0.125 vCPU / 128 MiB — CPU bursts above it when the host is uncontended, memory does not.

const sandbox = await cloud.sandboxes.create({
  image: "runcloud/agent-base",
  cpu: 4,
  memory: 8192,
  disk: 40,
});

const result = await cloud.sandboxes.exec(sandbox.id, "npm install && npm test", {
  timeoutSeconds: 600,
  onStdout: (chunk) => process.stdout.write(chunk),
  onStderr: (chunk) => process.stderr.write(chunk),
});
console.log(result.exitCode, result.stdout);
await writeFile(
  "sandbox-artifact.tar.gz",
  await cloud.sandboxes.readFile(sandbox.id, "/tmp/artifact.tar.gz"),
);

await cloud.sandboxes.destroy(sandbox.id);

Open a short-lived public URL for a service without making the sandbox persistent:

await cloud.sandboxes.exec(
  sandbox.id,
  "nohup npm run dev -- --host 0.0.0.0 >/tmp/dev.log 2>&1 &",
);
const tunnel = await cloud.sandboxes.openTunnel(sandbox.id, 3000, {
  ttlSeconds: 900,
});
await fetch(tunnel.url);
await cloud.sandboxes.closeTunnel(sandbox.id, tunnel.id);

Tunnel hostnames are random bearer capabilities. Do not write them to public logs. They expire automatically and are also removed when the tunnel or sandbox is deleted. Opening a tunnel does not modify idlePauseSeconds. Active tunnels are limited to 8 per sandbox and 100 per organization.

Cloudflare KV is eventually consistent, so a URL may remain reachable briefly after closeTunnel returns. Do not treat close as instantaneous global revocation. Stop the service or destroy the sandbox when access must end immediately.

Ask for the exact resources you need:

await cloud.sandboxes.create({ cpu: 2, memory: 4096, disk: 40 });

exec takes a string (run via /bin/sh -c) or an argv array to skip the shell. onStdout and onStderr receive bytes as they arrive over the authenticated WebSocket. A non-zero exit code is returned, not thrown — only transport and API errors raise RunCloudError.

Creates are retry-safe with an idempotency key; a repeat returns the original sandbox instead of spawning — and billing — a second one:

await cloud.sandboxes.create({ image: "runcloud/agent-base", idempotencyKey: jobId });

A sandbox's name is a label you own: set it at create, change it later, and filter on it. Pausing keeps the disk and memory and stops compute billing, so naming plus resume is how you hand the same sandbox back to the same piece of work instead of booting a new one:

const [existing] = await cloud.sandboxes.list({ name: `project-${projectId}` });
const sandbox = existing
  ? await cloud.sandboxes.resume(existing.id)
  : await cloud.sandboxes.create({ name: `project-${projectId}` });

await cloud.sandboxes.rename(sandbox.id, `project-${projectId}-v2`);
await cloud.sandboxes.pause(sandbox.id);

Names are not unique and mean nothing to the platform, so check state on a match before using it. Renaming works in any state but destroyed.

Snapshot a sandbox and restore it later:

const snap = await cloud.sandboxes.snapshot(sandbox.id, { label: "deps-installed" });
const restored = await cloud.snapshots.restore(snap.id);

Sandbox fields are returned in both the API's snake_case and camelCase (milli_cpu and milliCpu are both present).

Read fleet history and per-sandbox resource metrics, or control a desktop-image sandbox through its short-lived viewer and computer-use endpoints:

const overview = await cloud.overview();
const history = await cloud.sandboxes.history({ range: "24h" });
const metrics = await cloud.sandboxes.metrics(sandbox.id, { range: "1h" });
const desktop = await cloud.sandboxes.desktop(sandbox.id, { viewOnly: true });
const screenshot = await cloud.sandboxes.computer(sandbox.id, {
  action: "screenshot",
});

Use credential() for a lightweight token/product-scope probe and account() for the caller's full profile and organization memberships. An API key can revoke itself:

const scope = await cloud.credential();
const profile = await cloud.account();
await cloud.revokeCurrentApiKey();

Distributed tracing

Every SDK-owned HTTP, multipart, byte-download, SSE, and WebSocket transport injects W3C trace context. An active OpenTelemetry context is propagated when one exists; otherwise the SDK starts the request with a fresh sampled traceparent. WebSocket exec carries the same context in both the upgrade headers and its first command frame.

Applications that already use OpenTelemetry can install their preferred global propagator and start a span normally:

import { trace } from "@opentelemetry/api";

await trace.getTracer("worker").startActiveSpan("provision", async (span) => {
  try {
    await cloud.sandboxes.create({ name: "traceable-job" });
  } finally {
    span.end();
  }
});

The client reads RUN_CLOUD_API_KEY first, then RUN_CLOUD_API_TOKEN. Override the API origin with RUN_CLOUD_API_URL or by passing new Client({ apiUrl }).

Compatibility adapters

Provider-shaped migration adapters ship in this same package:

| Provider | Import | | --- | --- | | Modal | @run-cloud/sdk/compat/modal | | E2B | @run-cloud/sdk/compat/e2b | | Daytona | @run-cloud/sdk/compat/daytona | | Vercel Sandbox | @run-cloud/sdk/compat/vercel | | Blaxel | @run-cloud/sdk/compat/blaxel | | Fly Sprites | @run-cloud/sdk/compat/sprites | | Cloudflare Sandbox | @run-cloud/sdk/compat/cloudflare | | CodeSandbox | @run-cloud/sdk/compat/codesandbox | | Fly Machines | @run-cloud/sdk/compat/fly | | Remotion Lambda | @run-cloud/sdk/compat/remotion-lambda |

// import { Sandbox } from "@vercel/sandbox";
import { Sandbox } from "@run-cloud/sdk/compat/vercel";

const box = await Sandbox.create();
const result = await box.runCommand("node", ["-e", "console.log('hello')"]);
console.log(await result.stdout());
await box.stop();

The adapters preserve core creation, lookup/listing, synchronous command execution, pause/resume where the provider exposes it, and destruction. Provider-specific features that do not have a public run.cloud equivalent throw UnsupportedCompatibilityFeatureError with a migration hint. In particular, public port capabilities and filesystem mutation are not silently emulated.

Remotion renders with customer-owned S3

run.cloud can replace the Lambda compute used for a Remotion render without owning the output bucket. Supply a presigned S3-compatible PUT URL; the sandbox renders the video, uploads it directly to that URL, and is destroyed when render() returns.

import { Client } from "@run-cloud/sdk";

const cloud = new Client();
const result = await cloud.remotion.render({
  serveUrl: "https://example.com/remotion-bundle",
  composition: "ProductDemo",
  remotionVersion: "4.0.507",
  inputProps: { customer: "Acme" },
  codec: "h264",
  output: {
    uploadUrl: presignedPutUrl,
    downloadUrl: finalObjectUrl,
    contentType: "video/mp4",
  },
});

console.log(result.outputFile);

remotionVersion selects Remotion's matching public runtime image and installs that exact renderer during sandbox startup; all Remotion packages in a project should use the same version. You can pass image instead to use a custom runtime that already includes the renderer. Signed URLs are written to a private guest file and are not included in command logs.

For repeated renders, create a version-pinned snapshot once and persist its ID:

const runtime = await cloud.remotion.createRuntimeSnapshot({
  remotionVersion: "4.0.507",
});

const result = await cloud.remotion.render({
  serveUrl: "https://example.com/remotion-bundle",
  composition: "ProductDemo",
  snapshotId: runtime.id,
  codec: "h264",
  output: { uploadUrl: presignedPutUrl, downloadUrl: finalObjectUrl },
});

The snapshot is private to the caller's run.cloud organization. Create one per Remotion version and delete obsolete snapshots with cloud.snapshots.delete().

For a smaller migration from @remotion/lambda/client, switch the import and add s3Output; existing region and functionName fields may remain while compute moves to run.cloud:

import {
  getRenderProgress,
  renderMediaOnLambda,
} from "@run-cloud/sdk/compat/remotion-lambda";

const render = await renderMediaOnLambda({
  region: "us-east-1",
  functionName: "remotion-render-4-0-507-mem2048mb-disk2048mb-240sec",
  serveUrl: "https://example.com/remotion-bundle",
  composition: "ProductDemo",
  codec: "h264",
  s3Output: {
    bucketName: "customer-video-output",
    uploadUrl: presignedPutUrl,
    downloadUrl: finalObjectUrl,
  },
});

const progress = await getRenderProgress({ renderId: render.renderId });

The adapter derives the exact Remotion image version from the standard Lambda function name. By default, concurrencyPerLambda controls Remotion workers inside one sandbox.

To split one video across multiple sandboxes, add a customer-owned temporary chunk location. concurrency chooses the target maximum sandbox count, while framesPerLambda can derive it after the SDK selects the composition. Each allocated sandbox renders one frame range. For a sufficiently long video, concurrency: 8 creates up to eight sandboxes:

const render = await renderMediaOnLambda({
  functionName: "remotion-render-4-0-507-mem2048mb-disk2048mb-240sec",
  serveUrl: "https://example.com/remotion-bundle",
  composition: "ProductDemo",
  codec: "h264",
  concurrency: 8,
  concurrencyPerLambda: 1,
  s3Output: {
    bucketName: "customer-video-output",
    uploadUrl: finalPresignedPutUrl,
    downloadUrl: finalObjectUrl,
  },
  runCloud: {
    distributed: {
      createChunkOutput: async ({ renderId, index }) => {
        const prefix = `remotion-chunks/${renderId}/${index}`;
        return {
          video: await presignTemporaryObject(`${prefix}.ts`),
          audio: await presignTemporaryObject(`${prefix}.aac`),
        };
      },
    },
  },
});

console.log(render.sandboxCount); // exact number actually allocated

presignTemporaryObject() returns {uploadUrl, downloadUrl, headers?, contentType?}. Both URLs point to the same temporary object. Signed URLs stay out of shell commands, and the temporary objects are used to produce the final artifact in customer S3. Configure an S3 lifecycle rule for the temporary prefix.

Chunks must be equal-sized except for the last one, so very short or awkwardly divisible compositions can use fewer sandboxes than the target. run.cloud still does not create or host an S3 bucket, deploy the Remotion site, store render metadata, or add a queue/webhook layer. render.sandboxCount and progress.runCloud.sandboxCount report the exact fan-out. deleteRender() or cloud.remotion.cancel() destroys every sandbox associated with the render; finite sandbox timeouts remain the cleanup backstop.

cloud.assets.upload(blob, options) verifies the uploaded size and MD5 before returning a ready asset. Re-uploading the same bytes reuses the verified object. cloud.assets.uploadDirect(blob, options) uses the API's buffered multipart endpoint when a signed-storage transfer is not suitable, and cloud.assets.download(id) returns the owned asset as a Uint8Array:

const asset = await cloud.assets.uploadDirect(blob, { filename: "app.tar.gz" });
await writeFile("app.tar.gz", await cloud.assets.download(asset.id));

For multipart archives, cloud.assets.uploadBatch(parts, { uploadBatchId, uploadRunId }) numbers the parts, records per-part transfer telemetry, and limits direct storage transfers to six at a time.

cloud.ios.screenshot(sessionId) and cloud.android.screenshot(sessionId) return PNG bytes as a Uint8Array. Save the bytes directly with node:fs/promises.writeFile, attach them to test artifacts, or pass them to an image-processing library.

cloud.ios.logs(sessionId, { tail: 200 }) and cloud.android.logs(sessionId, { tail: 200 }) return bounded entries from the current lease. Use the async iterable followLogs(sessionId, { signal }) on either client to process new entries until the signal is aborted or the session ends.

cloud.ios.uploadVideo(sessionId, videoBlob, { filename }) accepts MP4 and QuickTime blobs and imports them into Photos. The video is stored as a user-owned run.cloud asset and remains available through cloud.assets.

cloud.ios.uploadCameraVideo(...) and cloud.android.uploadCameraVideo(...) accept MP4 and QuickTime blobs and loop them through the target app camera. cloud.ios.uploadMicrophoneAudio(...) and cloud.android.uploadMicrophoneAudio(...) accept AAC, M4A, MP3, MP4 audio, and WAV and deliver decoded samples through the target app microphone. Pass bundleId to name the target app. Both results contain the retained uploaded asset; delete it through cloud.assets when it is no longer needed.