Skip to content
Closed
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
3 changes: 2 additions & 1 deletion src/cli-client-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { CliFlags } from './utils/command-schema.ts';
import type { CommandName } from './core/command-names.ts';
import { formatScreenshotDiffText, formatSnapshotText, printJson } from './utils/output.ts';
import { AppError } from './utils/errors.ts';
import {
Expand Down Expand Up @@ -247,7 +248,7 @@ const clientCommandHandlers: Partial<Record<string, ClientCommandHandler>> = {
}
return true;
},
};
} satisfies Partial<Record<CommandName, ClientCommandHandler>>;

async function runDeployCommand(
command: 'install' | 'reinstall',
Expand Down
3 changes: 2 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sendToDaemon } from './daemon-client.ts';
import { prepareMetroRuntime } from './client-metro.ts';
import { AppError } from './utils/errors.ts';
import type { CommandName } from './core/command-names.ts';
import {
buildFlags,
buildMeta,
Expand Down Expand Up @@ -44,7 +45,7 @@ export function createAgentDeviceClient(
const transport = deps.transport ?? sendToDaemon;

const execute = async (
command: string,
command: CommandName,
positionals: string[] = [],
options: InternalRequestOptions = {},
): Promise<Record<string, unknown>> => {
Expand Down
7 changes: 4 additions & 3 deletions src/core/batch.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { AppError } from '../utils/errors.ts';
import type { BatchStep, CommandFlags } from './dispatch.ts';
import type { CommandName } from './command-names.ts';

export const DEFAULT_BATCH_MAX_STEPS = 100;
const BATCH_BLOCKED_COMMANDS = new Set(['batch', 'replay']);

export type NormalizedBatchStep = {
command: string;
command: CommandName;
positionals: string[];
flags: Partial<CommandFlags>;
runtime?: unknown;
};

export type BatchStepResult = {
step: number;
command: string;
command: CommandName;
ok: true;
data: Record<string, unknown>;
durationMs: number;
Expand Down Expand Up @@ -82,7 +83,7 @@ export function validateAndNormalizeBatchSteps(
throw new AppError('INVALID_ARGS', `Batch step ${index + 1} runtime must be an object.`);
}
normalized.push({
command,
command: command as CommandName,
positionals: positionals as string[],
flags: (step.flags ?? {}) as Partial<CommandFlags>,
Comment on lines 85 to 88
Comment on lines 85 to 88
Comment on lines 84 to 88
runtime: step.runtime,
Expand Down
3 changes: 2 additions & 1 deletion src/core/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DeviceInfo } from '../utils/device.ts';
import type { CommandName } from './command-names.ts';

type KindMatrix = {
simulator?: boolean;
Expand Down Expand Up @@ -144,7 +145,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
ios: { simulator: true, device: true },
android: { emulator: true, device: true, unknown: true },
},
};
} satisfies Partial<Record<CommandName, CommandCapability>>;

export function isCommandSupportedOnDevice(command: string, device: DeviceInfo): boolean {
const capability = COMMAND_CAPABILITY_MATRIX[command];
Expand Down
31 changes: 31 additions & 0 deletions src/core/command-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Canonical command name type used throughout agent-device.
*
* CLI command names are derived from `COMMAND_SCHEMAS` keys.
* Daemon-internal names are listed as a small separate union.
*
* `DaemonRequest.command` intentionally stays `string` because it represents
* untrusted input from wire boundaries. The value of `CommandName` comes from
* typed lookup-table keys and internal function parameters.
*/

import type { CliCommandName } from '../utils/command-schema.ts';

const DAEMON_INTERNAL_COMMANDS = [
'install_source',
'lease_allocate',
'lease_heartbeat',
'lease_release',
'release_materialized_paths',
'session_list',
] as const;

type DaemonInternalCommandName = (typeof DAEMON_INTERNAL_COMMANDS)[number];

export type CommandName = CliCommandName | DaemonInternalCommandName;

/** CLI aliases that are normalized before reaching command dispatch. */
export const COMMAND_ALIASES: Partial<Record<string, CommandName>> = {
'long-press': 'longpress',
metrics: 'perf',
};
5 changes: 3 additions & 2 deletions src/core/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
import pathModule from 'node:path';
import { AppError } from '../utils/errors.ts';
import type { DeviceInfo } from '../utils/device.ts';
import type { CommandName } from './command-names.ts';
import {
dismissAndroidKeyboard,
getAndroidKeyboardState,
Expand Down Expand Up @@ -30,7 +31,7 @@ export { resolveTargetDevice } from './dispatch-resolve.ts';
export { shouldUseIosTapSeries, shouldUseIosDragSeries };

export type BatchStep = {
command: string;
command: CommandName;
positionals?: string[];
flags?: Partial<CommandFlags>;
runtime?: unknown;
Expand All @@ -42,7 +43,7 @@ export type CommandFlags = Omit<CliFlags, 'json' | 'help' | 'version' | 'batchSt

export async function dispatchCommand(
device: DeviceInfo,
command: string,
command: CommandName,
positionals: string[],
Comment on lines 44 to 47
outPath?: string,
context?: {
Expand Down
3 changes: 2 additions & 1 deletion src/daemon/handlers/session-close.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { normalizeError } from '../../utils/errors.ts';
import { runCmd } from '../../utils/exec.ts';
import type { DeviceInfo } from '../../utils/device.ts';
import type { CommandName } from '../../core/command-names.ts';
import { contextFromFlags } from '../context.ts';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
import { SessionStore } from '../session-store.ts';
Expand Down Expand Up @@ -77,7 +78,7 @@ export async function handleCloseCommand(params: {
sessionStore: SessionStore;
dispatch: (
device: DeviceInfo,
command: string,
command: CommandName,
positionals: string[],
out?: string,
context?: Record<string, unknown>,
Expand Down
3 changes: 2 additions & 1 deletion src/daemon/handlers/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type BatchStep,
type CommandFlags,
} from '../../core/dispatch.ts';
import type { CommandName } from '../../core/command-names.ts';
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
import { AppError, asAppError, normalizeError } from '../../utils/errors.ts';
import { normalizePlatformSelector, type DeviceInfo } from '../../utils/device.ts';
Expand Down Expand Up @@ -117,7 +118,7 @@ async function runSessionOrSelectorDispatch(params: {
ensureReady: typeof ensureDeviceReady;
resolveDevice: typeof resolveTargetDevice;
dispatch: typeof dispatchCommand;
command: string;
command: CommandName;
positionals: string[];
recordPositionals?: string[];
deriveNextSession?: (
Expand Down
3 changes: 2 additions & 1 deletion src/daemon/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import http, { type IncomingHttpHeaders } from 'node:http';
import fs from 'node:fs';
import { AppError, normalizeError } from '../utils/errors.ts';
import type { DaemonInstallSource, DaemonRequest, DaemonResponse } from './types.ts';
import type { CommandName } from '../core/command-names.ts';
import { normalizeTenantId } from './config.ts';
import {
clearRequestCanceled,
Expand Down Expand Up @@ -78,7 +79,7 @@ const LEASE_RPC_METHOD_TO_COMMAND: Record<
'agent-device.lease.heartbeat': 'lease_heartbeat',
'agent_device.lease.release': 'lease_release',
'agent-device.lease.release': 'lease_release',
};
} satisfies Record<string, CommandName>;
const SUPPORTED_RPC_METHODS = new Set([
...COMMAND_RPC_METHODS,
...INSTALL_FROM_SOURCE_RPC_METHODS,
Expand Down
11 changes: 6 additions & 5 deletions src/daemon/request-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,23 @@ import {
} from '../utils/diagnostics.ts';
import { resolveLeaseScope } from './lease-context.ts';
import type { LeaseRegistry } from './lease-registry.ts';
import type { CommandName } from '../core/command-names.ts';

const selectorValidationExemptCommands = new Set([
const selectorValidationExemptCommands = new Set<string>([
'session_list',
'devices',
'ensure-simulator',
'release_materialized_paths',
]);
const leaseAdmissionExemptCommands = new Set([
] as const satisfies readonly CommandName[]);
const leaseAdmissionExemptCommands = new Set<string>([
'session_list',
'devices',
'ensure-simulator',
'release_materialized_paths',
'lease_allocate',
'lease_heartbeat',
'lease_release',
]);
] as const satisfies readonly CommandName[]);

function contextFromFlags(
logPath: string,
Expand Down Expand Up @@ -344,7 +345,7 @@ export function createRequestHandler(
command === 'screenshot' && resolvedOut
? { ...(lockedReq.flags ?? {}), out: resolvedOut }
: (lockedReq.flags ?? {});
const data = await dispatch(session.device, command, resolvedPositionals, resolvedOut, {
const data = await dispatch(session.device, command as CommandName, resolvedPositionals, resolvedOut, {
...contextFromFlags(
Comment on lines +348 to 349
Comment on lines +348 to 349
Comment on lines 346 to 349
logPath,
lockedReq.flags,
Expand Down
5 changes: 2 additions & 3 deletions src/utils/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type FlagKey,
} from './command-schema.ts';
import { isFlagSupportedForCommand } from './cli-option-schema.ts';
import { COMMAND_ALIASES } from '../core/command-names.ts';

type ParsedArgs = {
command: string | null;
Expand Down Expand Up @@ -304,9 +305,7 @@ export function usageForCommand(command: string): string | null {
}

function normalizeCommandAlias(command: string): string {
if (command === 'long-press') return 'longpress';
if (command === 'metrics') return 'perf';
return command;
return COMMAND_ALIASES[command] ?? command;
}

function mergeDefinedFlags<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
Expand Down
8 changes: 6 additions & 2 deletions src/utils/command-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ export const GLOBAL_FLAG_KEYS = new Set<FlagKey>([
'noRecord',
]);

const COMMAND_SCHEMAS: Record<string, CommandSchema> = {
const commandSchemaEntries = {
boot: {
description: 'Ensure target device/simulator is booted and ready',
positionalArgs: [],
Expand Down Expand Up @@ -1049,7 +1049,11 @@ const COMMAND_SCHEMAS: Record<string, CommandSchema> = {
allowedFlags: [],
skipCapabilityCheck: true,
},
};
} satisfies Record<string, CommandSchema>;

export type CliCommandName = keyof typeof commandSchemaEntries;

const COMMAND_SCHEMAS: Record<string, CommandSchema> = commandSchemaEntries;

const flagDefinitionByName = new Map<string, FlagDefinition>();
const flagDefinitionsByKey = new Map<FlagKey, FlagDefinition[]>();
Expand Down
Loading