diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index f4242e9c2..b699ec5cb 100644 --- a/src/__tests__/cli-client-commands.test.ts +++ b/src/__tests__/cli-client-commands.test.ts @@ -670,6 +670,7 @@ function createStubClient(params: { identifiers: { appId: 'com.example.demo' }, }), installFromSource: params.installFromSource, + list: async () => [], open: params.open ?? (async () => ({ diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 90790422c..3be91da30 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -294,6 +294,30 @@ test('apps.installFromSource derives Android launchTarget from packageName when }); }); +test('apps.list forwards filters and returns daemon app names', async () => { + const setup = createTransport(async () => ({ + ok: true, + data: { + apps: ['Settings (com.apple.Preferences)', 'Demo (com.example.demo)', { ignored: true }], + }, + })); + const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); + + const apps = await client.apps.list({ + platform: 'ios', + device: 'iPhone 16', + appsFilter: 'user-installed', + }); + + assert.equal(setup.calls.length, 1); + assert.equal(setup.calls[0]?.command, 'apps'); + assert.deepEqual(setup.calls[0]?.positionals, []); + assert.equal(setup.calls[0]?.flags?.platform, 'ios'); + assert.equal(setup.calls[0]?.flags?.device, 'iPhone 16'); + assert.equal(setup.calls[0]?.flags?.appsFilter, 'user-installed'); + assert.deepEqual(apps, ['Settings (com.apple.Preferences)', 'Demo (com.example.demo)']); +}); + test('materializations.release forwards materialization identity through the daemon request', async () => { const setup = createTransport(async () => ({ ok: true, diff --git a/src/__tests__/close-remote-metro.test.ts b/src/__tests__/close-remote-metro.test.ts index 681db3b5d..232e27d5a 100644 --- a/src/__tests__/close-remote-metro.test.ts +++ b/src/__tests__/close-remote-metro.test.ts @@ -61,6 +61,9 @@ test('close with remote-config stops the managed Metro companion for that projec installFromSource: async () => { throw new Error('unexpected call'); }, + list: async () => { + throw new Error('unexpected call'); + }, open: async () => { throw new Error('unexpected call'); }, @@ -153,6 +156,9 @@ test('close with remote-config still stops the managed Metro companion when clos installFromSource: async () => { throw new Error('unexpected call'); }, + list: async () => { + throw new Error('unexpected call'); + }, open: async () => { throw new Error('unexpected call'); }, @@ -251,6 +257,9 @@ test('close app with remote-config stops the managed Metro companion for that se installFromSource: async () => { throw new Error('unexpected call'); }, + list: async () => { + throw new Error('unexpected call'); + }, open: async () => { throw new Error('unexpected call'); }, @@ -348,6 +357,9 @@ test('close with remote-config still succeeds when the config file is gone befor installFromSource: async () => { throw new Error('unexpected call'); }, + list: async () => { + throw new Error('unexpected call'); + }, open: async () => { throw new Error('unexpected call'); }, diff --git a/src/client-normalizers.ts b/src/client-normalizers.ts index 100001f6f..2b67b4fdd 100644 --- a/src/client-normalizers.ts +++ b/src/client-normalizers.ts @@ -267,6 +267,7 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags { snapshotScope: options.scope, snapshotRaw: options.raw, overlayRefs: options.overlayRefs, + appsFilter: options.appsFilter, verbose: options.debug, }) as CommandFlags; } diff --git a/src/client-types.ts b/src/client-types.ts index 061f665ed..d92079268 100644 --- a/src/client-types.ts +++ b/src/client-types.ts @@ -211,6 +211,11 @@ export type AppInstallFromSourceResult = { identifiers: AgentDeviceIdentifiers; }; +export type AppListOptions = AgentDeviceRequestOverrides & + AgentDeviceSelectionOptions & { + appsFilter?: 'all' | 'user-installed'; + }; + export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & { materializationId: string; }; @@ -295,6 +300,7 @@ export type InternalRequestOptions = AgentDeviceClientConfig & depth?: number; scope?: string; raw?: boolean; + appsFilter?: 'all' | 'user-installed'; installSource?: DaemonInstallSource; retainMaterializedPaths?: boolean; materializedPathRetentionMs?: number; @@ -322,6 +328,7 @@ export type AgentDeviceClient = { installFromSource: ( options: AppInstallFromSourceOptions, ) => Promise; + list: (options?: AppListOptions) => Promise; open: (options: AppOpenOptions) => Promise; close: (options?: AppCloseOptions) => Promise; }; diff --git a/src/client.ts b/src/client.ts index d3beb2f0a..f5bd8db76 100644 --- a/src/client.ts +++ b/src/client.ts @@ -26,6 +26,7 @@ import type { AppCloseOptions, AppDeployOptions, AppInstallFromSourceOptions, + AppListOptions, AppOpenOptions, CaptureScreenshotOptions, CaptureSnapshotOptions, @@ -142,6 +143,12 @@ export function createAgentDeviceClient( }), resolveSessionName(config.session, options.session), ), + list: async (options: AppListOptions = {}) => { + const data = await execute('apps', [], options); + return Array.isArray(data.apps) + ? data.apps.filter((app): app is string => typeof app === 'string') + : []; + }, open: async (options: AppOpenOptions) => { const session = resolveSessionName(config.session, options.session); const positionals = options.url ? [options.app, options.url] : [options.app]; @@ -268,6 +275,7 @@ export type { AppDeployResult, AppInstallFromSourceOptions, AppInstallFromSourceResult, + AppListOptions, AppOpenOptions, AppOpenResult, CaptureScreenshotOptions, diff --git a/src/utils/__tests__/cli-option-schema.test.ts b/src/utils/__tests__/cli-option-schema.test.ts index 4e8958ca1..702a2cbdd 100644 --- a/src/utils/__tests__/cli-option-schema.test.ts +++ b/src/utils/__tests__/cli-option-schema.test.ts @@ -54,7 +54,10 @@ test('remote config schema stays aligned with CLI option metadata', () => { assert.equal(definition.type, field.type); assert.equal(definition.min, 'min' in field ? field.min : undefined); assert.equal(definition.max, 'max' in field ? field.max : undefined); - assert.deepEqual(definition.enumValues ?? [], 'enumValues' in field ? field.enumValues ?? [] : []); + assert.deepEqual( + definition.enumValues ?? [], + 'enumValues' in field ? (field.enumValues ?? []) : [], + ); } }); diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index d17a80211..06c4496a2 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -36,6 +36,7 @@ const client = createAgentDeviceClient({ }); const devices = await client.devices.list({ platform: 'ios' }); +const apps = await client.apps.list({ platform: 'ios', appsFilter: 'user-installed' }); const ensured = await client.simulators.ensure({ device: 'iPhone 16', boot: true,