Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
"import": "./dist/src/android-adb.js",
"types": "./dist/src/android-adb.d.ts"
},
"./limrun": {
"import": "./dist/src/limrun.js",
"types": "./dist/src/limrun.d.ts"
},
"./contracts": {
"import": "./dist/src/contracts.js",
"types": "./dist/src/contracts.d.ts"
Expand Down
28 changes: 10 additions & 18 deletions src/__tests__/limrun-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, test, vi } from 'vitest';
import { LimrunRuntime } from '../providers/limrun/runtime.ts';
import { LimrunRuntime } from '../sdk/limrun.ts';
import { createExpiredProviderLeaseReleaser } from '../daemon/provider-lease-expiry.ts';
import type { SimulatorLease } from '../daemon/lease-registry.ts';
import type { DeviceInfo } from '../kernel/device.ts';
import { runCmd } from '../utils/exec.ts';
import { readVersion } from '../utils/version.ts';

type LimrunInstancePage = {
getPaginatedItems: () => Array<{ metadata: { id: string } }>;
Expand Down Expand Up @@ -118,7 +119,6 @@ afterEach(() => {
test('Limrun runtime identifies direct CLI usage to the Limrun API', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});

const lease: SimulatorLease = {
Expand All @@ -139,7 +139,7 @@ test('Limrun runtime identifies direct CLI usage to the Limrun API', async () =>

assert.deepEqual(limrunMockState.constructorOptions[0]?.defaultHeaders, {
'x-agent-device-client': 'agent-device-cli',
'x-agent-device-version': '9.9.9-test',
'x-agent-device-version': readVersion(),
});
const iosCreateCalls = limrunMockState.iosCreate.mock.calls as unknown as Array<
[{ metadata?: { labels?: Record<string, string> } }]
Expand All @@ -157,7 +157,7 @@ test('Limrun runtime identifies direct CLI usage to the Limrun API', async () =>
});

test('Limrun iOS uses shared deep-link classification', async () => {
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' });

try {
const device = await allocateLimrunDevice(runtime, {
Expand All @@ -179,7 +179,7 @@ test('Limrun iOS uses shared deep-link classification', async () => {
});

test('Limrun reclaims a labeled iOS instance without an in-memory session', async () => {
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' });
const lease: SimulatorLease = {
leaseId: 'lease-recovered-ios',
tenantId: 'team-a',
Expand Down Expand Up @@ -220,7 +220,7 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as
heartbeatAt: 1,
expiresAt: 60_001,
};
const firstRuntime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const firstRuntime = new LimrunRuntime({ apiKey: 'lim_test_key' });
const firstReleaser = createExpiredProviderLeaseReleaser({
recoverExpiredLease: firstRuntime.recoverExpiredLease,
recoverableProviderIds: ['limrun'],
Expand All @@ -239,7 +239,7 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as
assert.deepEqual(limrunMockState.iosDelete.mock.calls[0], ['ios-instance-1']);
firstReleaser.shutdown();

recoveredRuntime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
recoveredRuntime = new LimrunRuntime({ apiKey: 'lim_test_key' });
recoveredReleaser = createExpiredProviderLeaseReleaser({
recoverExpiredLease: recoveredRuntime.recoverExpiredLease,
recoverableProviderIds: ['limrun'],
Expand All @@ -266,7 +266,6 @@ test('Limrun recovers a failed expired lease release after a daemon restart', as
test('Limrun Android reverses localhost URL ports through the persistent ADB tunnel', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});

try {
Expand Down Expand Up @@ -358,7 +357,6 @@ function assertAndroidTunnelLifecycle(openUrl: string): void {
test('Limrun Android installs direct local artifacts through Limrun assets', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-android',
Expand Down Expand Up @@ -410,7 +408,7 @@ test('Limrun Android installs direct local artifacts through Limrun assets', asy
});

test('Limrun Android shares an in-flight ADB tunnel across concurrent port reverse requests', async () => {
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' });

try {
await allocateLimrunDevice(runtime, androidLease());
Expand Down Expand Up @@ -441,7 +439,6 @@ test('Limrun iOS installs direct local artifacts through Limrun assets', async (
fs.writeFileSync(ipaPath, 'demo');
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-ios',
Expand Down Expand Up @@ -505,7 +502,7 @@ test('Limrun iOS reads the bundle display name before uploading an app bundle',
'</dict></plist>',
].join(''),
);
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' });

try {
const device = await allocateLimrunDevice(runtime, {
Expand All @@ -529,7 +526,6 @@ test('Limrun iOS reads the bundle display name before uploading an app bundle',
test('Limrun iOS maps supported orientation and rejects unsupported upside-down orientation', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});

try {
Expand All @@ -556,7 +552,6 @@ test('Limrun iOS maps supported orientation and rejects unsupported upside-down
test('Limrun Android configures an explicit port reverse', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-android',
Expand Down Expand Up @@ -601,7 +596,7 @@ test('Limrun Android configures an explicit port reverse', async () => {
});

test('Limrun Android preserves canonical ADB failure classification', async () => {
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key', version: '9.9.9-test' });
const runtime = new LimrunRuntime({ apiKey: 'lim_test_key' });

try {
await allocateLimrunDevice(runtime, androidLease());
Expand Down Expand Up @@ -638,7 +633,6 @@ test('Limrun deletes iOS instance when post-create validation fails', async () =
} as never);
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-ios',
Expand Down Expand Up @@ -671,7 +665,6 @@ test('Limrun deletes Android instance when post-create validation fails', async
} as never);
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-android',
Expand All @@ -697,7 +690,6 @@ test('Limrun deletes Android instance when post-create validation fails', async
test('Limrun keeps session tracked when release fails so release can be retried', async () => {
const runtime = new LimrunRuntime({
apiKey: 'lim_test_key',
version: '9.9.9-test',
});
const lease: SimulatorLease = {
leaseId: 'lease-android',
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/package-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const supportedSubpaths = [
'./remote-config',
'./install-source',
'./android-adb',
'./limrun',
'./contracts',
'./selectors',
'./finders',
Expand Down
14 changes: 10 additions & 4 deletions src/provider-device-runtimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ export async function createDefaultProviderDeviceRuntimes(
env: DefaultProviderDeviceRuntimeEnv = process.env,
): Promise<ProviderDeviceRuntime[]> {
const runtimes = createDefaultCloudWebDriverProviderRuntimes(env);
if (!env.LIMRUN_API_KEY?.trim()) return runtimes;
const apiKey = env.LIMRUN_API_KEY?.trim();
if (!apiKey) return runtimes;

const { createLimrunRuntimeFromEnv } = await import('./providers/limrun/runtime.ts');
const limrunRuntime = createLimrunRuntimeFromEnv(env);
return limrunRuntime ? [...runtimes, limrunRuntime] : runtimes;
const { LimrunRuntime } = await import('./providers/limrun/runtime.ts');
return [
...runtimes,
new LimrunRuntime({
apiKey,
region: env.LIMRUN_REGION?.trim() || undefined,
}),
];
}
15 changes: 2 additions & 13 deletions src/providers/limrun/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,13 @@ type LimrunInstance = {

type LimrunRuntimeSession = LimrunIosSession | LimrunAndroidSession;

type LimrunRuntimeOptions = {
export type LimrunRuntimeOptions = {
apiKey: string;
region?: string;
version?: string;
};

const LIMRUN_CLIENT_HEADER = 'agent-device-cli';

export function createLimrunRuntimeFromEnv(env: NodeJS.ProcessEnv): LimrunRuntime | undefined {
const apiKey = env.LIMRUN_API_KEY?.trim();
if (!apiKey) return undefined;
return new LimrunRuntime({
apiKey,
region: env.LIMRUN_REGION?.trim() || undefined,
version: readVersion(),
});
}

export class LimrunRuntime implements ProviderDeviceRuntime {
private readonly limrun: Limrun;
private readonly sessions = new Map<string, LimrunRuntimeSession>();
Expand Down Expand Up @@ -101,7 +90,7 @@ export class LimrunRuntime implements ProviderDeviceRuntime {
apiKey: options.apiKey,
defaultHeaders: {
'x-agent-device-client': LIMRUN_CLIENT_HEADER,
'x-agent-device-version': options.version ?? readVersion(),
'x-agent-device-version': readVersion(),
},
});
}
Expand Down
1 change: 1 addition & 0 deletions src/sdk/limrun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LimrunRuntime, type LimrunRuntimeOptions } from '../providers/limrun/runtime.ts';
6 changes: 6 additions & 0 deletions test/integration/installed-package-metro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi
'./finders': (mod) => mod.findBestMatchesByLocator([], 'text', 'anything').matches.length,
'./install-source': (mod) => typeof mod.isTrustedInstallSourceUrl('https://example.test/app.apk'),
'./io': (mod) => typeof mod.createLocalArtifactAdapter({ cwd: process.cwd() }).reserveOutput,
'./limrun': async (mod) => {
const runtime = new mod.LimrunRuntime({ apiKey: 'lim_test_key' });
await runtime.shutdown();
return runtime.provider;
},
'./metro': (mod) => mod.buildBundleUrl('https://public.example.test', 'ios'),
'./remote-config': (mod) => typeof mod,
'./selectors': (mod) => mod.isSelectorToken('||') && typeof mod.parseSelectorChain === 'function',
Expand Down Expand Up @@ -388,6 +393,7 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi
'./finders': 0,
'./install-source': 'boolean',
'./io': 'function',
'./limrun': 'limrun',
// Type-only subpath: resolving the module from the packed exports map is
// the entire runtime check.
'./remote-config': 'object',
Expand Down
1 change: 1 addition & 0 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default defineConfig({
'remote-config': 'src/sdk/remote-config.ts',
'install-source': 'src/sdk/install-source.ts',
'android-adb': 'src/sdk/android-adb.ts',
limrun: 'src/sdk/limrun.ts',
contracts: 'src/sdk/contracts.ts',
selectors: 'src/sdk/selectors.ts',
finders: 'src/sdk/finders.ts',
Expand Down
5 changes: 4 additions & 1 deletion website/docs/docs/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ Public subpath API exposed for Node consumers:
- `listAndroidAppsWithAdb(executor)`
- `getAndroidAppStateWithAdb(executor)`
- types: `AndroidAdbExecutor`, `AndroidAdbExecutorOptions`, `AndroidPortReverseEndpoint`
- `agent-device/limrun`
- `new LimrunRuntime(options)`
- types: `LimrunRuntimeOptions`

The `contracts`, `selectors`, `finders`, `install-source`, `android-adb`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths are the supported Node entry points. The former compatibility subpaths `agent-device/android-apps` and `agent-device/daemon`, plus hosted-runtime subpaths `agent-device/cloud-webdriver`, `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability`, are not published.
The `contracts`, `selectors`, `finders`, `install-source`, `android-adb`, `limrun`, `artifacts`, `batch`, `metro`, `remote-config`, and `io` subpaths are the supported Node entry points. The former compatibility subpaths `agent-device/android-apps` and `agent-device/daemon`, plus hosted-runtime subpaths `agent-device/cloud-webdriver`, `agent-device/commands`, `agent-device/backend`, `agent-device/testing/conformance`, and `agent-device/observability`, are not published.

## Basic usage

Expand Down
19 changes: 18 additions & 1 deletion website/docs/docs/device-clouds.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,24 @@ const client = createAgentDeviceClient({
});
```

The JavaScript client does not publish provider SDK subpaths. Use the normal typed client methods; provider implementation details stay internal. Limrun uses the same client shape with `leaseProvider: 'limrun'`, `platform: 'android'` or `platform: 'ios'`, and `LIMRUN_API_KEY` in the daemon environment.
Use the normal typed client methods when agent-device owns the daemon. Limrun uses the same client
shape with `leaseProvider: 'limrun'`, `platform: 'android'` or `platform: 'ios'`, and
`LIMRUN_API_KEY` in the daemon environment.

The first-party agent-device-cloud bridge hosts the provider runtime itself and can reuse
agent-device's Limrun implementation:

```ts
import { LimrunRuntime } from 'agent-device/limrun';

const apiKey = process.env.LIMRUN_API_KEY;
if (!apiKey) throw new Error('LIMRUN_API_KEY is required');

const runtime = new LimrunRuntime({
apiKey,
region: process.env.LIMRUN_REGION,
});
```

## MCP Experience

Expand Down
Loading