Skip to content

Commit bc7cf47

Browse files
authored
refactor: add command definition foundation (#513)
* refactor: add command definition foundation * fix: satisfy fallow for command definitions * refactor: tighten command definition foundation
1 parent fceaab9 commit bc7cf47

8 files changed

Lines changed: 105 additions & 23 deletions

File tree

src/cli/commands/generic.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { AgentDeviceClient, CommandRequestResult } from '../../client.ts';
22
import { CLIENT_COMMANDS } from '../../client-command-registry.ts';
33
import type { RecordOptions } from '../../client-types.ts';
44
import { announceReplayTestRun } from '../../cli-test.ts';
5+
import { runTypeCliCommand } from '../../commands/interactions/cli.ts';
6+
import { typeCommandDefinition } from '../../commands/interactions/definition.ts';
57
import { AppError } from '../../utils/errors.ts';
68
import type { CliFlags } from '../../utils/command-schema.ts';
79
import {
@@ -151,14 +153,9 @@ export const genericClientCommandHandlers = {
151153
y: Number(positionals[1]),
152154
}),
153155
),
154-
[CLIENT_COMMANDS.type]: createGenericClientCommandHandler(
155-
CLIENT_COMMANDS.type,
156-
({ client, positionals, flags }) =>
157-
client.interactions.type({
158-
...buildSelectionOptions(flags),
159-
text: positionals.join(' '),
160-
delayMs: flags.delayMs,
161-
}),
156+
[typeCommandDefinition.name]: createGenericClientCommandHandler(
157+
typeCommandDefinition.name,
158+
runTypeCliCommand,
162159
),
163160
[CLIENT_COMMANDS.fill]: createGenericClientCommandHandler(
164161
CLIENT_COMMANDS.fill,

src/commands/command-definition.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { CommandCapability } from '../core/capabilities.ts';
2+
import type { CommandSchema } from '../utils/command-schema.ts';
3+
4+
export type CommandDefinition<TName extends string = string> = {
5+
name: TName;
6+
schema: CommandSchema;
7+
capability: CommandCapability;
8+
};
9+
10+
export function defineCommand<const TDefinition extends CommandDefinition>(
11+
definition: TDefinition,
12+
): TDefinition {
13+
return definition;
14+
}
15+
16+
export function commandSchemaMap<TName extends string>(
17+
definitions: readonly CommandDefinition<TName>[],
18+
): Record<TName, CommandSchema> {
19+
return Object.fromEntries(
20+
definitions.map((definition) => [definition.name, definition.schema]),
21+
) as Record<TName, CommandSchema>;
22+
}
23+
24+
export function commandCapabilityMap<TName extends string>(
25+
definitions: readonly CommandDefinition<TName>[],
26+
): Record<TName, CommandCapability> {
27+
return Object.fromEntries(
28+
definitions.map((definition) => [definition.name, definition.capability]),
29+
) as Record<TName, CommandCapability>;
30+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { getCommandCapability } from '../../../core/capabilities.ts';
4+
import { getCommandSchema } from '../../../utils/command-schema.ts';
5+
import { INTERACTION_COMMAND_DEFINITIONS } from '../definition.ts';
6+
7+
test('interaction command definitions feed schema and capability registries', () => {
8+
for (const definition of INTERACTION_COMMAND_DEFINITIONS) {
9+
assert.deepEqual(getCommandSchema(definition.name), definition.schema);
10+
assert.deepEqual(getCommandCapability(definition.name), definition.capability);
11+
}
12+
});

src/commands/interactions/cli.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { AgentDeviceClient, CommandRequestResult } from '../../client.ts';
2+
import type { CliFlags } from '../../utils/command-schema.ts';
3+
import { buildSelectionOptions } from '../../cli/commands/shared.ts';
4+
5+
export type InteractionCliCommandParams = {
6+
client: AgentDeviceClient;
7+
positionals: string[];
8+
flags: CliFlags;
9+
};
10+
11+
export async function runTypeCliCommand({
12+
client,
13+
positionals,
14+
flags,
15+
}: InteractionCliCommandParams): Promise<CommandRequestResult> {
16+
return await client.interactions.type({
17+
...buildSelectionOptions(flags),
18+
text: positionals.join(' '),
19+
delayMs: flags.delayMs,
20+
});
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
2+
import { commandCapabilityMap, commandSchemaMap, defineCommand } from '../command-definition.ts';
3+
4+
export const typeCommandDefinition = defineCommand({
5+
name: PUBLIC_COMMANDS.type,
6+
schema: {
7+
helpDescription: 'Type text in focused field',
8+
positionalArgs: ['text'],
9+
allowsExtraPositionals: true,
10+
allowedFlags: ['delayMs'],
11+
},
12+
capability: {
13+
apple: { simulator: true, device: true },
14+
android: { emulator: true, device: true, unknown: true },
15+
linux: { device: true },
16+
},
17+
});
18+
19+
export const INTERACTION_COMMAND_DEFINITIONS = [typeCommandDefinition] as const;
20+
21+
export const INTERACTION_COMMAND_SCHEMAS = commandSchemaMap(INTERACTION_COMMAND_DEFINITIONS);
22+
export const INTERACTION_COMMAND_CAPABILITIES = commandCapabilityMap(
23+
INTERACTION_COMMAND_DEFINITIONS,
24+
);

src/core/capabilities.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isApplePlatform, type DeviceInfo } from '../utils/device.ts';
2+
import { INTERACTION_COMMAND_CAPABILITIES } from '../commands/interactions/definition.ts';
23

34
type KindMatrix = {
45
simulator?: boolean;
@@ -7,7 +8,7 @@ type KindMatrix = {
78
unknown?: boolean;
89
};
910

10-
type CommandCapability = {
11+
export type CommandCapability = {
1112
apple?: KindMatrix;
1213
android?: KindMatrix;
1314
linux?: KindMatrix;
@@ -227,11 +228,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
227228
android: { emulator: true, device: true, unknown: true },
228229
linux: LINUX_NONE,
229230
},
230-
type: {
231-
apple: { simulator: true, device: true },
232-
android: { emulator: true, device: true, unknown: true },
233-
linux: LINUX_DEVICE,
234-
},
231+
...INTERACTION_COMMAND_CAPABILITIES,
235232
wait: {
236233
apple: { simulator: true, device: true },
237234
android: { emulator: true, device: true, unknown: true },
@@ -253,6 +250,10 @@ export function isCommandSupportedOnDevice(command: string, device: DeviceInfo):
253250
return byPlatform[kind] === true;
254251
}
255252

253+
export function getCommandCapability(command: string): CommandCapability | undefined {
254+
return COMMAND_CAPABILITY_MATRIX[command];
255+
}
256+
256257
export function listCapabilityCommands(): string[] {
257258
return Object.keys(COMMAND_CAPABILITY_MATRIX).sort();
258259
}

src/daemon/handlers/interaction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createInteractionRuntime } from './interaction-runtime.ts';
88
import { finalizeTouchInteraction } from './interaction-common.ts';
99
import { errorResponse } from './response.ts';
1010
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
11+
import { typeCommandDefinition } from '../../commands/interactions/definition.ts';
1112
import { normalizeError } from '../../utils/errors.ts';
1213
import { successText } from '../../utils/success-text.ts';
1314
import { recoverAndroidBlockingSystemDialog } from '../android-system-dialog.ts';
@@ -27,7 +28,7 @@ export async function handleInteractionCommands(
2728
}
2829

2930
switch (params.req.command) {
30-
case 'type':
31+
case typeCommandDefinition.name:
3132
return await dispatchTypeViaRuntime({
3233
...params,
3334
captureSnapshotForSession,
@@ -49,7 +50,7 @@ async function dispatchTypeViaRuntime(
4950
const { req, sessionName, sessionStore } = params;
5051
const session = sessionStore.get(sessionName);
5152
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
52-
if (!isCommandSupportedOnDevice('type', session.device)) {
53+
if (!isCommandSupportedOnDevice(typeCommandDefinition.name, session.device)) {
5354
return errorResponse('UNSUPPORTED_OPERATION', 'type is not supported on this device');
5455
}
5556
if (session.device.platform === 'android' && session.recording) {

src/utils/command-schema.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SETTINGS_USAGE_OVERRIDE } from '../core/settings-contract.ts';
22
import { SESSION_SURFACES } from '../core/session-surface.ts';
33
import type { DaemonInstallSource } from '../contracts.ts';
4+
import { INTERACTION_COMMAND_SCHEMAS } from '../commands/interactions/definition.ts';
45

56
export type CliFlags = {
67
json: boolean;
@@ -130,7 +131,7 @@ export type FlagDefinition = {
130131
usageDescription?: string;
131132
};
132133

133-
type CommandSchema = {
134+
export type CommandSchema = {
134135
helpDescription: string;
135136
summary?: string;
136137
positionalArgs: readonly string[];
@@ -1779,12 +1780,7 @@ const COMMAND_SCHEMAS: Record<string, CommandSchema> = {
17791780
positionalArgs: ['x', 'y'],
17801781
allowedFlags: [],
17811782
},
1782-
type: {
1783-
helpDescription: 'Type text in focused field',
1784-
positionalArgs: ['text'],
1785-
allowsExtraPositionals: true,
1786-
allowedFlags: ['delayMs'],
1787-
},
1783+
...INTERACTION_COMMAND_SCHEMAS,
17881784
fill: {
17891785
usageOverride: 'fill <x> <y> <text> | fill <@ref|selector> <text>',
17901786
helpDescription: 'Tap then type',

0 commit comments

Comments
 (0)