Skip to content

Commit 3621b1d

Browse files
committed
feat: expose daemon embedding and Android ADB APIs
1 parent 72ba612 commit 3621b1d

14 files changed

Lines changed: 444 additions & 12 deletions

File tree

.fallowrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"src/bin.ts",
1818
"src/companion-tunnel.ts",
1919
"src/daemon.ts",
20+
"src/daemon-embedding.ts",
2021
"src/utils/update-check-entry.ts",
2122
"test/scripts/metro-prepare-packaged-smoke.mjs",
2223
"test/integration/*.test.ts",

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,16 @@ Snapshots assign refs like `@e1`, `@e2`, and `@e3` to current-screen elements. R
9494

9595
`agent-device` runs session-aware commands through platform backends: XCTest for iOS and tvOS, ADB plus the Android snapshot helper for Android, a local helper for macOS desktop automation, and AT-SPI for Linux desktop targets. See [Introduction](https://incubator.callstack.com/agent-device/docs/introduction) and [Commands](https://incubator.callstack.com/agent-device/docs/commands) for platform details.
9696

97+
Node consumers can use the typed client and public subpaths for bridge integrations. `agent-device/android-adb` exposes the Android ADB provider contract and reusable helpers for ADB-backed app listing and foreground state. `agent-device/daemon` exposes the supported daemon embedding surface for integrations that intentionally reuse the upstream request router.
98+
9799
## Used By
98100

99101
Used by teams and developers at Callstack, Expensify, Shopify, Kindred, Total Wine & More, LegendList, HerLyfe, App & Flow, and more.
100102

101103
## Documentation
102104

103105
- [Installation](https://incubator.callstack.com/agent-device/docs/installation)
106+
- [Typed Client](https://incubator.callstack.com/agent-device/docs/client-api)
104107
- [Commands](https://incubator.callstack.com/agent-device/docs/commands)
105108
- [Replay & E2E](https://incubator.callstack.com/agent-device/docs/replay-e2e)
106109
- [Known limitations](https://incubator.callstack.com/agent-device/docs/known-limitations)

fallow-baselines/dupes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"src/client.ts:632-658|src/index.ts:79-105",
104104
"src/commands/admin.ts:306-313|src/commands/apps.ts:286-295",
105105
"src/commands/capture-snapshot.ts:104-110|src/commands/selector-read-shared.ts:42-47",
106-
"src/commands/interactions.ts:207-215|src/core/dispatch.ts:875-883",
106+
"src/commands/interactions.ts:207-215|src/core/dispatch.ts:876-884",
107107
"src/commands/selector-read.ts:169-178|src/commands/selector-read.ts:403-410",
108108
"src/contracts.ts:422-427|src/contracts.ts:457-462",
109109
"src/core/__tests__/dispatch-open.test.ts:17-25|src/core/__tests__/dispatch-push.test.ts:12-20",
@@ -625,4 +625,4 @@
625625
"test/integration/smoke-open-remote-config.test.ts:146-170|test/scripts/metro-prepare-packaged-smoke.mjs:200-214",
626626
"test/scripts/metro-prepare-packaged-smoke.mjs:217-226|test/scripts/metro-prepare-packaged-smoke.mjs:245-254"
627627
]
628-
}
628+
}

fallow-baselines/health.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@
217217
"src/core/dispatch-resolve.ts": {
218218
"crap_high": {
219219
"count": 1
220+
},
221+
"crap_moderate": {
222+
"count": 1
220223
}
221224
},
222225
"src/core/dispatch.ts": {
@@ -1080,4 +1083,4 @@
10801083
"src/daemon/screenshot-overlay.ts:untested risk",
10811084
"src/utils/screenshot-diff-non-text.ts:complexity"
10821085
]
1083-
}
1086+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757
"import": "./dist/src/android-snapshot-helper.js",
5858
"types": "./dist/src/android-snapshot-helper.d.ts"
5959
},
60+
"./daemon": {
61+
"import": "./dist/src/daemon-embedding.js",
62+
"types": "./dist/src/daemon-embedding.d.ts"
63+
},
6064
"./contracts": {
6165
"import": "./dist/src/contracts.js",
6266
"types": "./dist/src/contracts.d.ts"

rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default defineConfig({
2626
'android-apps': 'src/android-apps.ts',
2727
'android-adb': 'src/android-adb.ts',
2828
'android-snapshot-helper': 'src/android-snapshot-helper.ts',
29+
'daemon-embedding': 'src/daemon-embedding.ts',
2930
contracts: 'src/contracts.ts',
3031
selectors: 'src/selectors.ts',
3132
finders: 'src/finders.ts',

src/android-adb.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ export {
99
type AndroidAdbProvider,
1010
type AndroidAdbSpawner,
1111
} from './platforms/android/adb-executor.ts';
12+
export {
13+
getAndroidAppStateWithAdb,
14+
listAndroidAppsWithAdb,
15+
} from './platforms/android/app-helpers.ts';
16+
export type {
17+
AndroidAppListFilter,
18+
AndroidAppListOptions,
19+
AndroidAppListTarget,
20+
} from './platforms/android/app-helpers.ts';

src/core/__tests__/dispatch-resolve.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ vi.mock('../../platforms/ios/devices.ts', async (importOriginal) => {
1818
import {
1919
resolveIosDevice,
2020
resolveTargetDevice,
21+
withDeviceInventoryProvider,
2122
withResolveTargetDeviceCacheScope,
2223
} from '../dispatch-resolve.ts';
2324
import type { DeviceInfo } from '../../utils/device.ts';
@@ -173,3 +174,28 @@ test('resolveTargetDevice reuses cache across nested request scopes', async () =
173174

174175
assert.equal(mockListAppleDevices.mock.calls.length, 1);
175176
});
177+
178+
test('resolveTargetDevice uses injected device inventory without local discovery', async () => {
179+
const result = await withDeviceInventoryProvider(
180+
async (request) => {
181+
assert.equal(request.platform, 'ios');
182+
assert.equal(request.deviceName, 'Remote iPhone');
183+
return [{ ...bootedSimulator, id: 'remote-ios-1', name: 'Remote iPhone' }];
184+
},
185+
async () => await resolveTargetDevice({ platform: 'ios', device: 'Remote iPhone' }),
186+
);
187+
188+
assert.equal(result.id, 'remote-ios-1');
189+
assert.equal(mockListAppleDevices.mock.calls.length, 0);
190+
});
191+
192+
test('resolveTargetDevice treats empty injected inventory as authoritative', async () => {
193+
const err = await withDeviceInventoryProvider(
194+
async () => [],
195+
async () => await resolveTargetDevice({ platform: 'ios' }),
196+
).catch((error) => error);
197+
198+
assert.ok(err instanceof AppError);
199+
assert.equal(err.code, 'DEVICE_NOT_FOUND');
200+
assert.equal(mockListAppleDevices.mock.calls.length, 0);
201+
});

src/core/dispatch-resolve.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ type ResolveDeviceFlags = Pick<
3030
>;
3131

3232
const resolveTargetDeviceCacheScope = new AsyncLocalStorage<Map<string, DeviceInfo>>();
33+
const deviceInventoryProviderScope = new AsyncLocalStorage<DeviceInventoryProvider>();
34+
35+
export type DeviceInventoryRequest = {
36+
platform?: PlatformSelector;
37+
target?: DeviceTarget;
38+
deviceName?: string;
39+
udid?: string;
40+
serial?: string;
41+
iosSimulatorSetPath?: string;
42+
androidSerialAllowlist?: string[];
43+
};
44+
45+
export type DeviceInventoryProvider = (
46+
request: DeviceInventoryRequest,
47+
) => Promise<DeviceInfo[] | null | undefined>;
3348

3449
type AppleDeviceSelector = {
3550
platform?: Exclude<PlatformSelector, 'android'>;
@@ -134,6 +149,20 @@ export async function resolveTargetDevice(flags: ResolveDeviceFlags): Promise<De
134149
);
135150
}
136151

152+
const injectedDevices = await readInjectedDeviceInventory({
153+
...selector,
154+
iosSimulatorSetPath,
155+
androidSerialAllowlist: androidSerialAllowlist
156+
? Array.from(androidSerialAllowlist).sort()
157+
: undefined,
158+
});
159+
if (injectedDevices) {
160+
return cacheResolvedTargetDevice(
161+
cacheKey,
162+
await resolveDevice(injectedDevices, selector, { simulatorSetPath: iosSimulatorSetPath }),
163+
);
164+
}
165+
137166
if (selector.platform === 'linux') {
138167
const devices = await listLinuxDevices();
139168
return cacheResolvedTargetDevice(cacheKey, await resolveDevice(devices, selector));
@@ -181,6 +210,34 @@ export async function withResolveTargetDeviceCacheScope<T>(task: () => Promise<T
181210
return await resolveTargetDeviceCacheScope.run(new Map(), task);
182211
}
183212

213+
export async function withDeviceInventoryProvider<T>(
214+
provider: DeviceInventoryProvider | undefined,
215+
task: () => Promise<T>,
216+
): Promise<T> {
217+
if (!provider) return await task();
218+
return await deviceInventoryProviderScope.run(provider, task);
219+
}
220+
221+
export async function withTargetDeviceResolutionScope<T>(
222+
provider: DeviceInventoryProvider | undefined,
223+
task: () => Promise<T>,
224+
): Promise<T> {
225+
return await withDeviceInventoryProvider(
226+
provider,
227+
async () => await withResolveTargetDeviceCacheScope(task),
228+
);
229+
}
230+
231+
async function readInjectedDeviceInventory(
232+
request: DeviceInventoryRequest,
233+
): Promise<DeviceInfo[] | null> {
234+
const provider = deviceInventoryProviderScope.getStore();
235+
if (!provider) return null;
236+
const devices = await provider(request);
237+
if (devices === undefined || devices === null) return null;
238+
return devices.map((device) => ({ ...device }));
239+
}
240+
184241
function readResolveTargetDeviceCache(cacheKey: string): DeviceInfo | undefined {
185242
const cache = resolveTargetDeviceCacheScope.getStore();
186243
const cached = cache?.get(cacheKey);

src/daemon-embedding.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export { createRequestHandler } from './daemon/request-router.ts';
2+
export type { AndroidAdbProviderResolver, RequestRouterDeps } from './daemon/request-router.ts';
3+
export { withDeviceInventoryProvider } from './core/dispatch-resolve.ts';
4+
export type { DeviceInventoryProvider, DeviceInventoryRequest } from './core/dispatch-resolve.ts';
5+
export { SessionStore } from './daemon/session-store.ts';
6+
export { LeaseRegistry } from './daemon/lease-registry.ts';
7+
export type {
8+
AdmissionRequest,
9+
AllocateLeaseRequest,
10+
HeartbeatLeaseRequest,
11+
LeaseRegistryOptions,
12+
ReleaseLeaseRequest,
13+
SimulatorLease,
14+
} from './daemon/lease-registry.ts';
15+
export {
16+
cleanupDownloadableArtifact,
17+
cleanupUploadedArtifact,
18+
prepareDownloadableArtifact,
19+
prepareUploadedArtifact,
20+
trackDownloadableArtifact,
21+
trackUploadedArtifact,
22+
} from './daemon/artifact-tracking.ts';
23+
export type {
24+
DaemonArtifact,
25+
DaemonInstallSource,
26+
DaemonRequest,
27+
DaemonResponse,
28+
DaemonResponseData,
29+
SessionRuntimeHints,
30+
SessionState,
31+
} from './daemon/types.ts';
32+
export type { DeviceInfo, Platform, PlatformSelector } from './utils/device.ts';
33+
export type {
34+
AndroidAdbExecutor,
35+
AndroidAdbExecutorOptions,
36+
AndroidAdbExecutorResult,
37+
AndroidAdbProvider,
38+
} from './android-adb.ts';

0 commit comments

Comments
 (0)