Skip to content

Commit 83efe54

Browse files
authored
fix: default apps listing to user-installed (#541)
1 parent 1987198 commit 83efe54

23 files changed

Lines changed: 134 additions & 46 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.15.0
44

5+
- Breaking: `apps` discovery and public app-list helpers now default to user-installed apps. Use `--all` or `filter: 'all'` to include system/OEM apps.
56
- Breaking: removed the `agent-device/android-apps` public subpath. Use the Android app helpers from `agent-device/android-adb`.
67
- Breaking: removed the `agent-device/daemon` public subpath. Use `agent-device/contracts` for daemon request/response types.
78
- Breaking: removed public local ADB bypass/selection helpers such as `spawnAndroidAdbBySerial` and `resolveAndroidAdbProvider`; use `createLocalAndroidAdbProvider(device)` or pass providers directly to the helpers from `agent-device/android-adb`.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Try the loop.
114114
```bash
115115
# Find the app.
116116
agent-device apps --platform ios
117+
agent-device apps --platform android
117118

118119
# Start a session.
119120
agent-device open SampleApp --platform ios

src/__tests__/cli-client-commands.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,39 @@ test('open forwards macOS surface to the client apps API', async () => {
508508
assert.equal(observed?.surface, 'menubar');
509509
});
510510

511+
test('apps command defaults to user-installed and prints discovery hint', async () => {
512+
let observedAppsFilter: string | undefined;
513+
const client = createStubClient({
514+
installFromSource: async () => {
515+
throw new Error('unexpected install call');
516+
},
517+
listApps: async (options) => {
518+
observedAppsFilter = options?.appsFilter;
519+
return ['Demo (com.example.demo)'];
520+
},
521+
});
522+
523+
const output = await captureOutput(async () => {
524+
const handled = await tryRunClientBackedCommand({
525+
command: 'apps',
526+
positionals: [],
527+
flags: {
528+
json: false,
529+
help: false,
530+
version: false,
531+
platform: 'android',
532+
},
533+
client,
534+
});
535+
assert.equal(handled, true);
536+
});
537+
538+
assert.equal(observedAppsFilter, 'user-installed');
539+
assert.equal(output.stdout, 'Demo (com.example.demo)\n');
540+
assert.match(output.stderr, /user-installed apps/i);
541+
assert.match(output.stderr, /--all/);
542+
});
543+
511544
test('screenshot reports annotated ref count in non-json mode', async () => {
512545
const client = createStubClient({
513546
installFromSource: async () => {
@@ -834,8 +867,35 @@ async function captureStdout(run: () => Promise<void>): Promise<string> {
834867
return stdout;
835868
}
836869

870+
async function captureOutput(
871+
run: () => Promise<void>,
872+
): Promise<{ stdout: string; stderr: string }> {
873+
let stdout = '';
874+
let stderr = '';
875+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
876+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
877+
(process.stdout as any).write = ((chunk: unknown) => {
878+
stdout += String(chunk);
879+
return true;
880+
}) as typeof process.stdout.write;
881+
(process.stderr as any).write = ((chunk: unknown) => {
882+
stderr += String(chunk);
883+
return true;
884+
}) as typeof process.stderr.write;
885+
886+
try {
887+
await run();
888+
} finally {
889+
process.stdout.write = originalStdoutWrite;
890+
process.stderr.write = originalStderrWrite;
891+
}
892+
893+
return { stdout, stderr };
894+
}
895+
837896
function createStubClient(params: {
838897
installFromSource: AgentDeviceClient['apps']['installFromSource'];
898+
listApps?: AgentDeviceClient['apps']['list'];
839899
prepareMetro?: AgentDeviceClient['metro']['prepare'];
840900
reloadMetro?: AgentDeviceClient['metro']['reload'];
841901
open?: AgentDeviceClient['apps']['open'];
@@ -884,7 +944,7 @@ function createStubClient(params: {
884944
identifiers: { appId: 'com.example.demo' },
885945
}),
886946
installFromSource: params.installFromSource,
887-
list: async () => [],
947+
list: params.listApps ?? (async () => []),
888948
open:
889949
params.open ??
890950
(async () => ({

src/__tests__/runtime-apps.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ test('runtime app commands call typed backend lifecycle primitives', async () =>
3333
const closed = await device.apps.close({ app: 'com.example.app' });
3434
assert.equal(closed.kind, 'appClosed');
3535

36-
const listed = await device.apps.list({ filter: 'user-installed' });
36+
const listed = await device.apps.list();
3737
assert.deepEqual(listed.apps, [
3838
{
3939
id: 'com.example.app',

src/backend.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AndroidSnapshotBackendMetadata } from './platforms/android/snapshot-types.ts';
2+
import type { AppsFilter } from './client-types.ts';
23
import type {
34
Point,
45
ScreenshotOverlayRef,
@@ -198,7 +199,7 @@ export type BackendOpenOptions = {
198199
relaunch?: boolean;
199200
};
200201

201-
export type BackendAppListFilter = 'all' | 'user-installed';
202+
export type BackendAppListFilter = AppsFilter;
202203

203204
export type BackendAppInfo = {
204205
id: string;
@@ -527,7 +528,7 @@ export type AgentDeviceBackend = {
527528
closeApp?(context: BackendCommandContext, app?: string): Promise<BackendActionResult>;
528529
listApps?(
529530
context: BackendCommandContext,
530-
filter?: BackendAppListFilter,
531+
filter: BackendAppListFilter,
531532
): Promise<readonly BackendAppInfo[]>;
532533
getAppState?(context: BackendCommandContext, app: string): Promise<BackendAppState>;
533534
pushFile?(

src/cli/commands/apps.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import { buildSelectionOptions, writeCommandOutput } from './shared.ts';
2+
import { DEFAULT_APPS_FILTER } from '../../client-types.ts';
23
import type { ClientCommandHandler } from './router-types.ts';
34

45
export const appsCommand: ClientCommandHandler = async ({ flags, client }) => {
6+
const appsFilter = flags.appsFilter ?? DEFAULT_APPS_FILTER;
57
const apps = await client.apps.list({
68
...buildSelectionOptions(flags),
7-
appsFilter: flags.appsFilter,
9+
appsFilter,
810
});
911
const data = { apps };
10-
writeCommandOutput(flags, data, () => apps.join('\n'));
12+
writeCommandOutput(flags, data, () => {
13+
if (!flags.json) {
14+
process.stderr.write(
15+
appsFilter === 'all'
16+
? 'Showing all apps, including system apps.\n'
17+
: 'Showing user-installed apps. Use --all to include system apps.\n',
18+
);
19+
}
20+
if (apps.length > 0) return apps.join('\n');
21+
return appsFilter === 'all' ? 'No apps found.' : 'No user-installed apps found.';
22+
});
1123
return true;
1224
};

src/client-types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import type { MetroBridgeScope } from './client-companion-tunnel-contract.ts';
2121
export type { FindLocator } from './utils/finders.ts';
2222
export type { CompanionTunnelScope, MetroBridgeScope } from './client-companion-tunnel-contract.ts';
2323

24+
export type AppsFilter = 'user-installed' | 'all';
25+
export const DEFAULT_APPS_FILTER: AppsFilter = 'user-installed';
26+
2427
type DaemonTransportMode = 'auto' | 'socket' | 'http';
2528
type DaemonServerMode = 'socket' | 'http' | 'dual';
2629
type SessionIsolationMode = 'none' | 'tenant';
@@ -232,7 +235,7 @@ export type AppInstallFromSourceResult = {
232235

233236
export type AppListOptions = AgentDeviceRequestOverrides &
234237
AgentDeviceSelectionOptions & {
235-
appsFilter?: 'all' | 'user-installed';
238+
appsFilter?: AppsFilter;
236239
};
237240

238241
export type MaterializationReleaseOptions = AgentDeviceRequestOverrides & {
@@ -800,7 +803,7 @@ export type InternalRequestOptions = AgentDeviceClientConfig &
800803
metroPort?: number;
801804
bundleUrl?: string;
802805
launchUrl?: string;
803-
appsFilter?: 'all' | 'user-installed';
806+
appsFilter?: AppsFilter;
804807
installSource?: DaemonInstallSource;
805808
retainMaterializedPaths?: boolean;
806809
materializedPathRetentionMs?: number;

src/commands/apps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
} from '../backend.ts';
1010
import type { FileInputRef } from '../io.ts';
1111
import type { AgentDeviceRuntime, CommandContext } from '../runtime-contract.ts';
12+
import { DEFAULT_APPS_FILTER } from '../client-types.ts';
1213
import { AppError } from '../utils/errors.ts';
1314
import { successText } from '../utils/success-text.ts';
1415
import { resolveCommandInput } from './io-policy.ts';
@@ -151,7 +152,7 @@ export const listAppsCommand: RuntimeCommand<
151152

152153
const apps = await runtime.backend.listApps(
153154
toAppBackendContext(runtime, options),
154-
options.filter ?? 'all',
155+
options.filter ?? DEFAULT_APPS_FILTER,
155156
);
156157
return {
157158
kind: 'appsList',

src/commands/session-lifecycle/definition.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
22
import type { CommandCapability } from '../../core/capabilities.ts';
3+
import { DEFAULT_APPS_FILTER } from '../../client-types.ts';
34
import { commandCapabilityMap, commandSchemaMap, defineCommand } from '../command-definition.ts';
45

56
const APP_RUNTIME_CAPABILITY = {
@@ -89,11 +90,11 @@ const installFromSourceCommandDefinition = defineCommand({
8990
const appsCommandDefinition = defineCommand({
9091
name: PUBLIC_COMMANDS.apps,
9192
schema: {
92-
helpDescription: 'List installed apps (includes default/system apps by default)',
93+
helpDescription: 'List user-installed apps; use --all to include system/OEM apps',
9394
summary: 'List installed apps',
9495
positionalArgs: [],
9596
allowedFlags: ['appsFilter'],
96-
defaults: { appsFilter: 'all' },
97+
defaults: { appsFilter: DEFAULT_APPS_FILTER },
9798
},
9899
capability: APP_INVENTORY_CAPABILITY,
99100
});

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ test('apps on macOS uses Apple app listing path', async () => {
13901390

13911391
mockListIosApps.mockImplementation(async (device, filter) => {
13921392
expect(device.platform).toBe('macos');
1393-
expect(filter).toBe('all');
1393+
expect(filter).toBe('user-installed');
13941394
return [{ bundleId: 'com.apple.systempreferences', name: 'System Settings' }];
13951395
});
13961396
const response = await handleSessionCommands({

0 commit comments

Comments
 (0)