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
7 changes: 6 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isCommandCodec,
settingsCommandCodec,
} from './command-codecs.ts';
import { typeCommandCodec } from './commands/interactions/definition.ts';
import { throwDaemonError } from './daemon-error.ts';
import {
buildFlags,
Expand Down Expand Up @@ -386,7 +387,11 @@ export function createAgentDeviceClient(
options,
),
type: async (options) =>
await executeCommandRequest(PUBLIC_COMMANDS.type, [options.text], options),
await executeCommandRequest(
PUBLIC_COMMANDS.type,
typeCommandCodec.encode(options),
options,
),
fill: async (options) =>
await executeCommandRequest(
PUBLIC_COMMANDS.fill,
Expand Down
12 changes: 9 additions & 3 deletions src/commands/command-definition.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import type { CommandCapability } from '../core/capabilities.ts';
import type { CommandSchema } from '../utils/command-schema.ts';
import type { CliFlags, CommandSchema } from '../utils/command-schema.ts';

export const ALL_DEVICE_COMMAND_CAPABILITY = {
apple: { simulator: true, device: true },
android: { emulator: true, device: true, unknown: true },
linux: { device: true },
} as const satisfies CommandCapability;

export type CommandDefinition<TName extends string = string> = {
export type CommandCodec<TOptions = unknown> = {
decode(positionals: string[], flags?: Partial<CliFlags>): TOptions;
encode(options: TOptions): string[];
};

export type CommandDefinition<TName extends string = string, TOptions = unknown> = {
name: TName;
schema: CommandSchema;
capability: CommandCapability;
codec?: CommandCodec<TOptions>;
};

export function defineCommand<const TDefinition extends CommandDefinition>(
export function defineCommand<const TDefinition extends CommandDefinition<string, unknown>>(
definition: TDefinition,
): TDefinition {
return definition;
Expand Down
37 changes: 35 additions & 2 deletions src/commands/interactions/__tests__/definition.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import type { AgentDeviceClient } from '../../../client.ts';
import { getCommandCapability } from '../../../core/capabilities.ts';
import { getCommandSchema } from '../../../utils/command-schema.ts';
import { getCommandSchema, type CliFlags } from '../../../utils/command-schema.ts';
import { CAPTURE_COMMAND_DEFINITIONS } from '../../capture-definition.ts';
import { SELECTOR_COMMAND_DEFINITIONS } from '../../selectors-definition.ts';
import { SESSION_LIFECYCLE_COMMAND_DEFINITIONS } from '../../session-lifecycle/definition.ts';
import { INTERACTION_COMMAND_DEFINITIONS } from '../definition.ts';
import { runTypeCliCommand } from '../cli.ts';
import { INTERACTION_COMMAND_DEFINITIONS, typeCommandDefinition } from '../definition.ts';

test('command definitions feed schema and capability registries', () => {
for (const definition of [
Expand All @@ -18,3 +20,34 @@ test('command definitions feed schema and capability registries', () => {
assert.deepEqual(getCommandCapability(definition.name), definition.capability);
}
});

test('type command definition exposes its positional codec', () => {
assert.deepEqual(typeCommandDefinition.codec.decode(['hello', 'world'], { delayMs: 25 }), {
text: 'hello world',
delayMs: 25,
});
assert.deepEqual(typeCommandDefinition.codec.encode({ text: 'hello world' }), ['hello world']);
});

test('type CLI command routes through the definition codec', async () => {
let received: unknown;
const client = {
interactions: {
type: async (options: unknown) => {
received = options;
return {};
},
},
} as AgentDeviceClient;

await runTypeCliCommand({
client,
positionals: ['hello', 'world'],
flags: { platform: 'ios', delayMs: 25 } as CliFlags,
});

const options = received as Record<string, unknown>;
assert.equal(options.platform, 'ios');
assert.equal(options.text, 'hello world');
assert.equal(options.delayMs, 25);
});
5 changes: 3 additions & 2 deletions src/commands/interactions/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AgentDeviceClient, CommandRequestResult } from '../../client.ts';
import type { CliFlags } from '../../utils/command-schema.ts';
import { buildSelectionOptions } from '../../cli/commands/shared.ts';
import { typeCommandCodec } from './definition.ts';

export type InteractionCliCommandParams = {
client: AgentDeviceClient;
Expand All @@ -13,9 +14,9 @@ export async function runTypeCliCommand({
positionals,
flags,
}: InteractionCliCommandParams): Promise<CommandRequestResult> {
const decoded = typeCommandCodec.decode(positionals, flags);
return await client.interactions.type({
...buildSelectionOptions(flags),
text: positionals.join(' '),
delayMs: flags.delayMs,
...decoded,
});
}
16 changes: 16 additions & 0 deletions src/commands/interactions/definition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
import {
ALL_DEVICE_COMMAND_CAPABILITY,
type CommandCodec,
commandCapabilityMap,
commandSchemaMap,
defineCommand,
} from '../command-definition.ts';

type TypeCommandCodecOptions = {
text: string;
delayMs?: number;
};

export const typeCommandCodec = {
decode: (positionals, flags) => ({
text: positionals.join(' '),
delayMs: flags?.delayMs,
}),
// `delayMs` is encoded through flags, so positionals only carry text.
encode: (options) => [options.text],
} satisfies CommandCodec<TypeCommandCodecOptions>;

export const typeCommandDefinition = defineCommand({
name: PUBLIC_COMMANDS.type,
schema: {
Expand All @@ -15,6 +30,7 @@ export const typeCommandDefinition = defineCommand({
allowedFlags: ['delayMs'],
},
capability: ALL_DEVICE_COMMAND_CAPABILITY,
codec: typeCommandCodec,
});

export const INTERACTION_COMMAND_DEFINITIONS = [typeCommandDefinition] as const;
Expand Down
Loading