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
29 changes: 6 additions & 23 deletions src/cli/auth-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from 'node:path';
import { runCmd } from '../utils/exec.ts';
import { AppError } from '../utils/errors.ts';
import type { CliFlags } from '../utils/cli-flags.ts';
import type { EnvMap } from '../utils/env-map.ts';
import { readCloudJsonResponse } from './cloud-response.ts';

const DEFAULT_CLOUD_BASE_URL = 'https://cloud.agent-device.dev';
const DEVICE_AUTH_START_PATH = '/api/control-plane/device-auth/start';
Expand Down Expand Up @@ -31,8 +33,6 @@ export type RemoteAuthResolution = {
source: 'flag' | 'env' | 'cli-session' | 'login' | 'none';
};

type EnvMap = Record<string, string | undefined>;

type DeviceAuthStartResponse = {
deviceCode: string;
userCode: string;
Expand Down Expand Up @@ -455,27 +455,10 @@ async function postJson<T>(options: {
body: JSON.stringify(options.body),
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
});
const text = await response.text();
let parsed: unknown = {};
if (text.trim().length > 0) {
try {
parsed = JSON.parse(text);
} catch (error) {
throw new AppError(
'COMMAND_FAILED',
`Cloud auth endpoint returned invalid JSON (${response.status}).`,
{ status: response.status },
error instanceof Error ? error : undefined,
);
}
}
if (!response.ok) {
throw new AppError('UNAUTHORIZED', `Cloud auth endpoint rejected the request.`, {
status: response.status,
response: parsed,
});
}
return parsed as T;
return await readCloudJsonResponse<T>(response, {
invalidJsonMessage: `Cloud auth endpoint returned invalid JSON (${response.status}).`,
rejectedMessage: 'Cloud auth endpoint rejected the request.',
});
}

function assertDeviceAuthStart(response: DeviceAuthStartResponse): void {
Expand Down
28 changes: 6 additions & 22 deletions src/cli/cloud-connection-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type { RemoteConfigProfile, ResolvedRemoteConfigProfile } from '../remote
import { profileToCliFlags } from '../utils/remote-config.ts';
import { AppError, asAppError } from '../utils/errors.ts';
import type { CliFlags } from '../utils/cli-flags.ts';
import type { EnvMap } from '../utils/env-map.ts';
import { resolveCloudAccessForConnect } from './auth-session.ts';
import { readCloudJsonResponse } from './cloud-response.ts';

const CONNECTION_PROFILE_PATH = '/api/control-plane/connection-profile';
const HTTP_TIMEOUT_MS = 15_000;
Expand All @@ -17,8 +19,6 @@ type CloudConnectionProfileResponse = {
};
};

type EnvMap = Record<string, string | undefined>;

export async function resolveCloudConnectProfile(options: {
flags: CliFlags;
stateDir: string;
Expand Down Expand Up @@ -71,26 +71,10 @@ async function fetchConnectionProfile(options: {
headers: { authorization: `Bearer ${options.accessToken}` },
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
});
const text = await response.text();
let parsed: unknown = {};
if (text.trim()) {
try {
parsed = JSON.parse(text);
} catch (error) {
throw new AppError(
'COMMAND_FAILED',
`Cloud connection profile endpoint returned invalid JSON (${response.status}).`,
{ status: response.status },
error instanceof Error ? error : undefined,
);
}
}
if (!response.ok) {
throw new AppError('UNAUTHORIZED', 'Cloud connection profile endpoint rejected the request.', {
status: response.status,
response: parsed,
});
}
const parsed = await readCloudJsonResponse<unknown>(response, {
invalidJsonMessage: `Cloud connection profile endpoint returned invalid JSON (${response.status}).`,
rejectedMessage: 'Cloud connection profile endpoint rejected the request.',
});
return parseConnectionProfile(parsed);
}

Expand Down
31 changes: 31 additions & 0 deletions src/cli/cloud-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AppError } from '../utils/errors.ts';

export async function readCloudJsonResponse<T>(
response: Response,
options: {
invalidJsonMessage: string;
rejectedMessage: string;
},
): Promise<T> {
const text = await response.text();
let parsed: unknown = {};
if (text.trim().length > 0) {
try {
parsed = JSON.parse(text);
} catch (error) {
throw new AppError(
'COMMAND_FAILED',
options.invalidJsonMessage,
{ status: response.status },
error instanceof Error ? error : undefined,
);
}
}
if (!response.ok) {
throw new AppError('UNAUTHORIZED', options.rejectedMessage, {
status: response.status,
response: parsed,
});
}
return parsed as T;
}
7 changes: 3 additions & 4 deletions src/commands/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { successText } from '../utils/success-text.ts';
import {
toBackendResult,
type BackendResultEnvelope,
type BackendResultVariant,
type RuntimeCommand,
} from './runtime-types.ts';
import { resolveCommandInput } from './io-policy.ts';
Expand Down Expand Up @@ -40,12 +41,10 @@ export type AdminShutdownCommandOptions = CommandContext & {
target?: BackendDeviceTarget;
};

export type AdminShutdownCommandResult = {
export type AdminShutdownCommandResult = BackendResultVariant<{
kind: 'deviceShutdown';
target?: BackendDeviceTarget;
backendResult?: Record<string, unknown>;
message?: string;
};
}>;

export type AdminInstallCommandOptions = CommandContext & {
app: string;
Expand Down
25 changes: 9 additions & 16 deletions src/commands/cli-grammar/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type {
RecordOptions,
} from '../../client-types.ts';
import { AppError } from '../../utils/errors.ts';
import type { NetworkIncludeMode } from '../../contracts.ts';
import type { LogAction } from '../log-command-contract.ts';
import { NETWORK_INCLUDE_MODES, type NetworkIncludeMode } from '../../contracts.ts';
import { parseStringMember } from '../../utils/string-enum.ts';
import { LOG_ACTION_VALUES, type LogAction } from '../log-command-contract.ts';
import {
isPerfAction,
isPerfArea,
Expand Down Expand Up @@ -123,17 +124,9 @@ function readPerfAction(

function readLogsAction(value: string | undefined): LogAction | undefined {
if (value === undefined) return undefined;
if (
value === 'path' ||
value === 'start' ||
value === 'stop' ||
value === 'doctor' ||
value === 'mark' ||
value === 'clear'
) {
return value;
}
throw new AppError('INVALID_ARGS', 'logs requires path, start, stop, doctor, mark, or clear');
return parseStringMember(LOG_ACTION_VALUES, value, {
message: 'logs requires path, start, stop, doctor, mark, or clear',
});
}

function readNetworkAction(value: string | undefined): 'dump' | 'log' | undefined {
Expand All @@ -144,7 +137,7 @@ function readNetworkAction(value: string | undefined): 'dump' | 'log' | undefine

function readNetworkInclude(value: string | undefined): NetworkIncludeMode | undefined {
if (value === undefined) return undefined;
if (value === 'summary' || value === 'headers' || value === 'body' || value === 'all')
return value;
throw new AppError('INVALID_ARGS', 'network include mode must be summary, headers, body, or all');
return parseStringMember(NETWORK_INCLUDE_MODES, value, {
message: 'network include mode must be summary, headers, body, or all',
});
}
3 changes: 1 addition & 2 deletions src/commands/client-command-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import type {
import { defineExecutableCommand } from './command-contract.ts';
import { optionalEnum } from './command-input.ts';
import { clientCommandMetadata } from './client-command-metadata.ts';

const WAIT_KIND_VALUES = ['duration', 'text', 'ref', 'selector'] as const;
import { WAIT_KIND_VALUES } from './wait-command-contract.ts';

type ClientCommandMetadata = (typeof clientCommandMetadata)[number];
type ClientCommandName = ClientCommandMetadata['name'];
Expand Down
7 changes: 3 additions & 4 deletions src/commands/client-command-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MetroPrepareOptions, RecordOptions } from '../client-types.ts';
import type { DaemonInstallSource } from '../contracts.ts';
import { NETWORK_INCLUDE_MODES, type DaemonInstallSource } from '../contracts.ts';
import { ALERT_ACTIONS } from '../alert-contract.ts';
import { BACK_MODES } from '../core/back-mode.ts';
import { DEVICE_ROTATIONS } from '../core/device-rotation.ts';
Expand All @@ -24,11 +24,10 @@ import {
} from './command-input.ts';
import { defineFieldCommandMetadata } from './field-command-contract.ts';
import { PERF_ACTION_VALUES, PERF_AREA_VALUES } from './perf-command-contract.ts';
import { WAIT_KIND_VALUES } from './wait-command-contract.ts';

const WAIT_KIND_VALUES = ['duration', 'text', 'ref', 'selector'] as const;
const CLIPBOARD_ACTION_VALUES = ['read', 'write'] as const;
const NETWORK_ACTION_VALUES = ['dump', 'log'] as const;
const NETWORK_INCLUDE_VALUES = ['summary', 'headers', 'body', 'all'] as const;
const START_STOP_VALUES = ['start', 'stop'] as const;
const REACT_NATIVE_ACTION_VALUES = ['dismiss-overlay'] as const;
const METRO_ACTION_VALUES = ['prepare', 'reload'] as const;
Expand Down Expand Up @@ -189,7 +188,7 @@ export const clientCommandMetadata = [
defineClientCommandMetadata('network', {
action: enumField(NETWORK_ACTION_VALUES),
limit: integerField(),
include: enumField(NETWORK_INCLUDE_VALUES),
include: enumField(NETWORK_INCLUDE_MODES),
}),
defineClientCommandMetadata('record', {
action: requiredField(enumField(START_STOP_VALUES)),
Expand Down
18 changes: 8 additions & 10 deletions src/commands/command-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import type {
ElementTarget,
InteractionTarget,
} from '../client-types.ts';
import { DEVICE_TARGETS, type DeviceTarget, type PlatformSelector } from '../utils/device.ts';
import {
DEVICE_TARGETS,
PLATFORM_SELECTORS,
type DeviceTarget,
type PlatformSelector,
} from '../utils/device.ts';
import type { JsonSchema } from './command-contract.ts';

const PLATFORM_VALUES = [
'ios',
'android',
'macos',
'linux',
'apple',
] as const satisfies readonly PlatformSelector[];
const INTERACTION_TARGET_KINDS = ['ref', 'selector', 'point'] as const;

export type CommonCommandInput = Pick<
Expand Down Expand Up @@ -279,7 +277,7 @@ export function readCommonInput(
): CommonCommandInput {
return {
session: optionalString(record, 'session'),
platform: optionalEnum(record, 'platform', PLATFORM_VALUES),
platform: optionalEnum(record, 'platform', PLATFORM_SELECTORS),
deviceTarget: readDeviceTarget(record, options),
device: optionalString(record, 'device'),
udid: optionalString(record, 'udid'),
Expand Down Expand Up @@ -564,7 +562,7 @@ function commonProperties(): Record<string, JsonSchema> {
session: { type: 'string', description: 'Agent-device session name.' },
platform: {
type: 'string',
enum: PLATFORM_VALUES,
enum: PLATFORM_SELECTORS,
description: 'Platform selector used to resolve a device.',
},
deviceTarget: {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/interaction-command-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type SwipePreset,
} from '../core/scroll-gesture.ts';
import { SCROLL_INPUT_DIRECTIONS } from './interaction-gestures.ts';
import { FIND_LOCATORS } from '../utils/finders.ts';

const FIND_ACTION_VALUES = [
'click',
Expand All @@ -47,7 +48,6 @@ const FIND_ACTION_VALUES = [
'fill',
'type',
] as const;
const FIND_LOCATOR_VALUES = ['any', 'text', 'label', 'value', 'role', 'id'] as const;

const clickFields = {
target: requiredField(interactionTargetField()),
Expand Down Expand Up @@ -116,7 +116,7 @@ const isFields = {
};

const findFields = {
locator: enumField(FIND_LOCATOR_VALUES),
locator: enumField(FIND_LOCATORS),
query: requiredField(stringField()),
action: enumField(FIND_ACTION_VALUES),
value: stringField(),
Expand Down
25 changes: 12 additions & 13 deletions src/commands/interaction-gestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import {
toBackendResult,
type BackendResultEnvelope,
type BackendResultVariant,
type RuntimeCommand,
} from './runtime-types.ts';
import {
Expand Down Expand Up @@ -67,25 +68,23 @@ export type ScrollCommandOptions = CommandContext & {
};

export type ScrollCommandResult =
| {
| BackendResultVariant<{
kind: 'viewport';
direction: GestureDirection;
edge?: 'top' | 'bottom';
passes?: number;
amount?: number;
pixels?: number;
backendResult?: Record<string, unknown>;
message?: string;
}
| (ResolvedInteractionTarget & {
direction: GestureDirection;
edge?: 'top' | 'bottom';
passes?: number;
amount?: number;
pixels?: number;
backendResult?: Record<string, unknown>;
message?: string;
});
}>
| BackendResultVariant<
ResolvedInteractionTarget & {
direction: GestureDirection;
edge?: 'top' | 'bottom';
passes?: number;
amount?: number;
pixels?: number;
}
>;

type ResolvedScrollTarget = { kind: 'viewport' } | ResolvedInteractionTarget;

Expand Down
16 changes: 8 additions & 8 deletions src/commands/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { toBackendContext } from './selector-read-utils.ts';
import {
toBackendResult,
type BackendResultEnvelope,
type BackendResultVariant,
type RuntimeCommand,
} from './runtime-types.ts';
import type { RepeatedInput } from './command-input.ts';
Expand Down Expand Up @@ -55,21 +56,20 @@ export type PressCommandOptions = CommandContext &

export type ClickCommandOptions = PressCommandOptions;

export type PressCommandResult = ResolvedInteractionTarget & {
backendResult?: Record<string, unknown>;
};
export type PressCommandResult = BackendResultVariant<ResolvedInteractionTarget>;

export type FillCommandOptions = CommandContext & {
target: InteractionTarget;
text: string;
delayMs?: number;
};

export type FillCommandResult = ResolvedInteractionTarget & {
text: string;
warning?: string;
backendResult?: Record<string, unknown>;
};
export type FillCommandResult = BackendResultVariant<
ResolvedInteractionTarget & {
text: string;
warning?: string;
}
>;

export type TypeTextCommandOptions = CommandContext & {
text: string;
Expand Down
Loading
Loading