Skip to content

Commit 1f6136f

Browse files
committed
chore: harden command input typing
1 parent 634c513 commit 1f6136f

5 files changed

Lines changed: 103 additions & 18 deletions

File tree

src/commands/cli-grammar/common.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { ElementTarget, InteractionTarget } from '../../client-types.ts';
1+
import type {
2+
ElementTarget,
3+
InteractionTarget,
4+
InternalRequestOptions,
5+
} from '../../client-types.ts';
26
import { splitSelectorFromArgs } from '../../daemon/selectors.ts';
37
import type { CliFlags } from '../../utils/command-schema.ts';
48
import { AppError } from '../../utils/errors.ts';
@@ -25,12 +29,19 @@ export function request(
2529
return { command, positionals, options: normalizeCommonRequestOptions(options) };
2630
}
2731

28-
function normalizeCommonRequestOptions(options: CommandInput): CommandInput {
29-
const normalizedTarget =
30-
options.deviceTarget ?? (typeof options.target === 'string' ? options.target : undefined);
31-
if (normalizedTarget === undefined && options.target === undefined) return options;
32+
function normalizeCommonRequestOptions(options: CommandInput): InternalRequestOptions {
33+
const normalizedTarget = readDeviceTarget(options.deviceTarget ?? options.target);
34+
if (normalizedTarget === undefined && options.target === undefined) {
35+
return options as InternalRequestOptions;
36+
}
3237
const { target: _target, ...rest } = options;
33-
return normalizedTarget === undefined ? rest : { ...rest, target: normalizedTarget };
38+
return (
39+
normalizedTarget === undefined ? rest : { ...rest, target: normalizedTarget }
40+
) as InternalRequestOptions;
41+
}
42+
43+
function readDeviceTarget(value: unknown): InternalRequestOptions['target'] | undefined {
44+
return value === 'mobile' || value === 'tv' || value === 'desktop' ? value : undefined;
3445
}
3546

3647
export function commonInputFromFlags(flags: CliFlags): Record<string, unknown> {

src/commands/cli-grammar/system.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const systemCliReaders = {
4040

4141
export const systemDaemonWriters = {
4242
appstate: direct(PUBLIC_COMMANDS.appState),
43-
back: (input) => request(PUBLIC_COMMANDS.back, [], { ...input, backMode: input.mode }),
43+
back: (input) =>
44+
request(PUBLIC_COMMANDS.back, [], { ...input, backMode: readBackMode(input.mode) }),
4445
home: direct(PUBLIC_COMMANDS.home),
4546
rotate: direct(PUBLIC_COMMANDS.rotate, (input) => [
4647
requiredDaemonString(input.orientation, 'rotate requires orientation'),
@@ -55,6 +56,10 @@ export const systemDaemonWriters = {
5556
]),
5657
} satisfies Record<string, DaemonWriter>;
5758

59+
function readBackMode(value: unknown): 'in-app' | 'system' | undefined {
60+
return value === 'in-app' || value === 'system' ? value : undefined;
61+
}
62+
5863
function clipboardPositionals(input: ClipboardCommandOptions): string[] {
5964
return input.action === 'read' ? ['read'] : ['write', input.text];
6065
}
@@ -83,11 +88,24 @@ function readClipboardInput(positionals: string[]): Record<string, unknown> {
8388
return { action, text: positionals.slice(1).join(' ') };
8489
}
8590

86-
function readKeyboardAction(value: string | undefined): 'status' | 'dismiss' | undefined {
91+
function readKeyboardAction(
92+
value: string | undefined,
93+
): 'status' | 'dismiss' | 'enter' | 'return' | undefined {
8794
const action = value?.toLowerCase();
8895
if (action === 'get') return 'status';
89-
if (action === undefined || action === 'status' || action === 'dismiss') return action;
90-
throw new AppError('INVALID_ARGS', 'keyboard action must be status, get, or dismiss.');
96+
if (
97+
action === undefined ||
98+
action === 'status' ||
99+
action === 'dismiss' ||
100+
action === 'enter' ||
101+
action === 'return'
102+
) {
103+
return action;
104+
}
105+
throw new AppError(
106+
'INVALID_ARGS',
107+
'keyboard action must be status, get, dismiss, enter, or return.',
108+
);
91109
}
92110

93111
function readReactNativeAction(value: string | undefined): 'dismiss-overlay' {

src/commands/cli-grammar/types.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,61 @@ export type DaemonCommandRequest = {
77
options: InternalRequestOptions;
88
};
99

10-
export type CommandInput = InternalRequestOptions & Record<string, any>;
10+
type PointInput = {
11+
x?: number;
12+
y?: number;
13+
};
14+
15+
export type CommandInput = Omit<InternalRequestOptions, 'batchSteps' | 'target'> &
16+
Omit<Partial<CliFlags>, 'batchSteps' | 'target'> & {
17+
target?: InternalRequestOptions['target'] | Record<string, unknown>;
18+
action?: string;
19+
amount?: number;
20+
app?: string;
21+
appPath?: string;
22+
backend?: string;
23+
degrees?: number;
24+
direction?: string;
25+
distance?: number;
26+
durationMs?: number;
27+
dx?: number;
28+
dy?: number;
29+
delta?: PointInput;
30+
env?: string[];
31+
event?: string;
32+
format?: string;
33+
from?: PointInput;
34+
include?: CliFlags['networkInclude'];
35+
kind?: string;
36+
locator?: string;
37+
mode?: 'in-app' | 'system' | 'full' | 'limited';
38+
button?: 'primary' | 'secondary' | 'middle';
39+
first?: boolean;
40+
last?: boolean;
41+
maxSteps?: number;
42+
onError?: 'stop';
43+
origin?: PointInput;
44+
path?: string;
45+
paths?: string[];
46+
payload?: unknown;
47+
permission?: string;
48+
predicate?: string;
49+
query?: string;
50+
retainPaths?: boolean;
51+
retentionMs?: number;
52+
scale?: number;
53+
selector?: string;
54+
source?: InternalRequestOptions['installSource'];
55+
state?: string;
56+
text?: string;
57+
to?: PointInput;
58+
update?: boolean;
59+
url?: string;
60+
value?: string;
61+
velocity?: number;
62+
x?: number;
63+
y?: number;
64+
} & Record<string, unknown>;
1165

1266
export type SelectionOptions = {
1367
platform?: CliFlags['platform'];

src/utils/cli-command-overrides.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ const CLI_COMMAND_OVERRIDES = {
147147
allowsExtraPositionals: true,
148148
},
149149
keyboard: {
150-
usageOverride: 'keyboard [status|get|dismiss]',
151-
helpDescription: 'Inspect Android keyboard visibility/type or dismiss the device keyboard',
152-
summary: 'Inspect or dismiss the device keyboard',
150+
usageOverride: 'keyboard [status|get|dismiss|enter|return]',
151+
helpDescription:
152+
'Inspect Android keyboard visibility/type or press/dismiss the device keyboard',
153+
summary: 'Inspect, press, or dismiss the device keyboard',
153154
positionalArgs: ['action?'],
154155
},
155156
back: {
@@ -197,16 +198,17 @@ const CLI_COMMAND_OVERRIDES = {
197198
},
198199
replay: {
199200
positionalArgs: ['path'],
200-
allowedFlags: ['replayMaestro', ...REPLAY_FLAGS],
201+
allowedFlags: ['replayMaestro', ...REPLAY_FLAGS, 'timeoutMs'],
201202
},
202203
test: {
203204
usageOverride: 'test <path-or-glob>...',
204205
listUsageOverride: 'test <path-or-glob>...',
205-
helpDescription: 'Run one or more .ad scripts as a serial test suite',
206-
summary: 'Run .ad test suites',
206+
helpDescription: 'Run one or more replay scripts as a serial test suite',
207+
summary: 'Run replay test suites',
207208
positionalArgs: ['pathOrGlob'],
208209
allowsExtraPositionals: true,
209210
allowedFlags: [
211+
'replayMaestro',
210212
...REPLAY_FLAGS,
211213
'failFast',
212214
'timeoutMs',

src/utils/cli-flags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [
760760
type: 'boolean',
761761
usageLabel: '--maestro',
762762
usageDescription:
763-
'Replay: treat input as a Maestro YAML compatibility flow. Supported subset: launchApp without state-reset side effects, runFlow file/inline with when.platform, onFlowStart/onFlowComplete, deterministic repeat.times, tapOn, doubleTapOn, longPressOn, inputText, pasteText, openLink, assertVisible, assertNotVisible, assertTrue literal true/false, extendedWaitUntil, scroll, absolute/percentage swipe, takeScreenshot, hideKeyboard, pressKey back/enter/home, back, waitForAnimationToEnd, stopApp/killApp, setAirplaneMode, setLocation, setOrientation, supported setPermissions targets, and startRecording/stopRecording. ' +
763+
'Replay: treat input as a Maestro YAML compatibility flow. Supported subset: launchApp without state-reset side effects, runFlow file/inline with when.platform, onFlowStart/onFlowComplete, deterministic repeat.times, ordered trusted runScript, tapOn, doubleTapOn, longPressOn, inputText, pasteText, openLink, assertVisible, assertNotVisible, assertTrue literal true/false, extendedWaitUntil, scroll, absolute/percentage swipe, takeScreenshot, hideKeyboard, pressKey back/enter/home, back, waitForAnimationToEnd, stopApp/killApp, setAirplaneMode, setLocation, setOrientation, supported setPermissions targets, and startRecording/stopRecording. ' +
764764
'Unsupported syntax fails loudly with a link to https://github.com/callstackincubator/agent-device/issues/558',
765765
},
766766
{

0 commit comments

Comments
 (0)