You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

agent-device

Package Overview
Dependencies
Maintainers
1
Versions
66
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

agent-device - npm Package Compare versions

Comparing version
0.9.0
to
0.10.0
+20
dist/src/core/click-button.d.ts
import { AppError } from '../utils/errors.ts';
export type ClickButton = 'primary' | 'secondary' | 'middle';
type ClickButtonFlags = {
clickButton?: ClickButton;
};
export declare function resolveClickButton(flags: ClickButtonFlags | undefined): ClickButton;
export declare function getClickButtonValidationError(options: {
commandLabel: string;
platform: string;
button: ClickButton;
count?: number;
intervalMs?: number;
holdMs?: number;
jitterPx?: number;
doubleTap?: boolean;
}): AppError | null;
export declare function buttonTag(button: ClickButton): {} | {
button: ClickButton;
};
export {};
import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
import type { DaemonCommandContext } from '../context.ts';
import type { DaemonRequest } from '../types.ts';
import { SessionStore } from '../session-store.ts';
export type ContextFromFlags = (flags: CommandFlags | undefined, appBundleId?: string, traceLogPath?: string) => DaemonCommandContext;
export type InteractionHandlerParams = {
req: DaemonRequest;
sessionName: string;
sessionStore: SessionStore;
contextFromFlags: ContextFromFlags;
dispatch: typeof dispatchCommand;
};
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export declare function handleFillCommand(params: InteractionHandlerParams): Promise<DaemonResponse>;
import type { CommandFlags } from '../../core/dispatch.ts';
import type { DaemonResponse } from '../types.ts';
export declare function refSnapshotFlagGuardResponse(command: 'press' | 'fill' | 'get' | 'scrollintoview', flags: CommandFlags | undefined): DaemonResponse | null;
export declare function unsupportedRefSnapshotFlags(flags: CommandFlags | undefined): string[];
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export declare function handleGetCommand(params: InteractionHandlerParams): Promise<DaemonResponse>;
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export declare function handleIsCommand(params: InteractionHandlerParams): Promise<DaemonResponse>;
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export declare function handlePressCommand(params: InteractionHandlerParams): Promise<DaemonResponse>;
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export declare function handleScrollIntoViewCommand(params: InteractionHandlerParams): Promise<DaemonResponse | null>;
import { parseSelectorChain, resolveSelectorChain } from '../selectors.ts';
import type { DaemonResponse, SessionState } from '../types.ts';
import type { SessionStore } from '../session-store.ts';
import { captureSnapshotForSession } from './interaction-snapshot.ts';
import type { ContextFromFlags } from './interaction-common.ts';
import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
export declare function resolveSelectorTarget(params: {
command: string;
selectorExpression: string;
session: SessionState;
flags: CommandFlags | undefined;
sessionStore: SessionStore;
contextFromFlags: ContextFromFlags;
interactiveOnly: boolean;
requireRect: boolean;
requireUnique: boolean;
disambiguateAmbiguous: boolean;
dispatch: typeof dispatchCommand;
}): Promise<{
ok: true;
chain: ReturnType<typeof parseSelectorChain>;
snapshot: Awaited<ReturnType<typeof captureSnapshotForSession>>;
resolved: NonNullable<Awaited<ReturnType<typeof resolveSelectorChain>>>;
} | {
ok: false;
response: DaemonResponse;
}>;
import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
import type { SessionStore } from '../session-store.ts';
import type { SessionState } from '../types.ts';
import type { SnapshotState } from '../../utils/snapshot.ts';
import type { ContextFromFlags } from './interaction-common.ts';
export declare function captureSnapshotForSession(session: SessionState, flags: CommandFlags | undefined, sessionStore: SessionStore, contextFromFlags: ContextFromFlags, options: {
interactiveOnly: boolean;
}, dispatch?: typeof dispatchCommand): Promise<SnapshotState>;
import { type Rect, type SnapshotNode } from '../../utils/snapshot.ts';
import type { DaemonResponse, SessionState } from '../types.ts';
export declare function parseCoordinateTarget(positionals: string[]): {
x: number;
y: number;
} | null;
export declare function resolveRefTarget(params: {
session: SessionState;
refInput: string;
fallbackLabel: string;
requireRect: boolean;
invalidRefMessage: string;
notFoundMessage: string;
}): {
ok: true;
target: {
ref: string;
node: SnapshotNode;
snapshotNodes: SnapshotNode[];
};
} | {
ok: false;
response: DaemonResponse;
};
export declare function resolveRectCenter(rect: Rect | undefined): {
x: number;
y: number;
} | null;
import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
import { SessionStore } from '../session-store.ts';
type HandleAlertCommandParams = {
req: DaemonRequest;
logPath: string;
sessionStore: SessionStore;
session: SessionState | undefined;
device: SessionState['device'];
runnerCommand?: typeof runIosRunnerCommand;
};
export declare function handleAlertCommand(params: HandleAlertCommandParams): Promise<DaemonResponse>;
export {};
import { dispatchCommand } from '../../core/dispatch.ts';
import { type RawSnapshotNode, type SnapshotState } from '../../utils/snapshot.ts';
import type { DaemonResponse, DaemonRequest, SessionState } from '../types.ts';
type CaptureSnapshotParams = {
dispatchSnapshotCommand: typeof dispatchCommand;
device: SessionState['device'];
session: SessionState | undefined;
req: DaemonRequest;
logPath: string;
snapshotScope?: string;
};
export declare function captureSnapshot(params: CaptureSnapshotParams): Promise<{
snapshot: SnapshotState;
}>;
export declare function buildSnapshotState(data: {
nodes?: RawSnapshotNode[];
truncated?: boolean;
backend?: 'xctest' | 'android';
}, snapshotRaw: boolean | undefined): SnapshotState;
export declare function resolveSnapshotScope(snapshotScope: string | undefined, session: SessionState | undefined): {
ok: true;
scope?: string;
} | {
ok: false;
response: DaemonResponse;
};
export {};
import type { DaemonRequest, SessionState } from '../types.ts';
import { SessionStore } from '../session-store.ts';
export declare function resolveSessionDevice(sessionStore: SessionStore, sessionName: string, flags: DaemonRequest['flags']): Promise<{
session: SessionState | undefined;
device: import("../../utils/device.ts").DeviceInfo;
}>;
export declare function withSessionlessRunnerCleanup<T>(session: SessionState | undefined, device: SessionState['device'], task: () => Promise<T>): Promise<T>;
export declare function recordIfSession(sessionStore: SessionStore, session: SessionState | undefined, req: DaemonRequest, result: Record<string, unknown>): void;
export declare function buildSnapshotSession(params: {
session: SessionState | undefined;
sessionName: string;
device: SessionState['device'];
snapshot: SessionState['snapshot'];
appBundleId?: string;
}): SessionState;
import { SessionStore } from '../session-store.ts';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
type ParsedSettingsArgs = {
setting: string;
state: string;
permissionTarget?: string;
};
type HandleSettingsCommandParams = {
req: DaemonRequest;
logPath: string;
sessionStore: SessionStore;
session: SessionState | undefined;
device: SessionState['device'];
parsed: ParsedSettingsArgs;
};
export declare function parseSettingsArgs(req: DaemonRequest): {
ok: true;
parsed: ParsedSettingsArgs;
} | {
ok: false;
response: DaemonResponse;
};
export declare function handleSettingsCommand(params: HandleSettingsCommandParams): Promise<DaemonResponse>;
export {};
import { dispatchCommand } from '../../core/dispatch.ts';
import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts';
import { SessionStore } from '../session-store.ts';
import { type SelectorChain } from '../selectors.ts';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
export type WaitParsed = {
kind: 'sleep';
durationMs: number;
} | {
kind: 'ref';
rawRef: string;
timeoutMs: number | null;
} | {
kind: 'selector';
selector: SelectorChain;
selectorExpression: string;
timeoutMs: number | null;
} | {
kind: 'text';
text: string;
timeoutMs: number | null;
};
export declare function parseWaitArgs(args: string[]): WaitParsed | null;
type HandleWaitCommandParams = {
parsed: WaitParsed;
req: DaemonRequest;
sessionName: string;
logPath: string;
sessionStore: SessionStore;
session: SessionState | undefined;
device: SessionState['device'];
dispatchSnapshotCommand?: typeof dispatchCommand;
runnerCommand?: typeof runIosRunnerCommand;
};
export declare function waitNeedsRunnerCleanup(parsed: WaitParsed): boolean;
export declare function handleWaitCommand(params: HandleWaitCommandParams): Promise<DaemonResponse>;
export {};
export declare function resolveAndroidSdkRoots(env?: NodeJS.ProcessEnv): string[];
export declare function ensureAndroidSdkPathConfigured(env?: NodeJS.ProcessEnv): Promise<void>;
import type { IosAppInfo } from './devicectl.ts';
export declare function filterAppleAppsByBundlePrefix(apps: IosAppInfo[], filter: 'user-installed' | 'all'): IosAppInfo[];
import type { DeviceInfo } from '../../utils/device.ts';
import type { IosAppInfo } from './devicectl.ts';
export declare function resolveMacOsApp(app: string): Promise<string>;
export declare function openMacOsApp(_device: DeviceInfo, app: string, options?: {
appBundleId?: string;
url?: string;
}): Promise<void>;
export declare function closeMacOsApp(_device: DeviceInfo, app: string): Promise<void>;
export declare function readMacOsClipboardText(): Promise<string>;
export declare function writeMacOsClipboardText(text: string): Promise<void>;
export declare function setMacOsAppearance(state: string): Promise<void>;
export declare function listMacApps(filter?: 'user-installed' | 'all'): Promise<IosAppInfo[]>;
import type { DeviceInfo } from '../../utils/device.ts';
export declare function repairMacOsRunnerProductsIfNeeded(device: DeviceInfo, productPaths: string[], xctestrunPath: string): Promise<void>;
export declare function isExpectedRunnerRepairFailure(error: unknown): boolean;
export declare function xctestrunReferencesExistingProducts(xctestrunPath: string): boolean;
export declare function resolveExistingXctestrunProductPaths(xctestrunPath: string): string[] | null;

Sorry, the diff of this file is not supported yet

# macOS Desktop Automation
Use this reference for host Mac apps such as Finder, TextEdit, System Settings, Preview, or browser apps running as normal desktop windows.
## Mental model
- `snapshot -i` should describe UI that is visible to a human in the current front window.
- Context menus are not ambient UI. Open them explicitly with `click --button secondary`, then re-snapshot.
- Prefer refs for exploration and selectors for deterministic replay/assertions.
- Avoid raw `x y` coordinates unless refs/selectors are impossible.
## Canonical flow
```bash
agent-device open Finder --platform macos
agent-device snapshot -i
agent-device click @e66 --button secondary --platform macos
agent-device snapshot -i
agent-device close
```
## What to expect from snapshots
- `snapshot -i` prioritizes visible window content over dormant menu infrastructure.
- File rows, sidebar items, toolbar controls, search fields, and visible context menus should appear.
- Finder and other native apps may expose duplicate-looking structures such as row wrapper nodes, `cell` nodes, and child `text` or `text-field` nodes.
- Treat those as distinct AX nodes unless you have a stronger selector anchor.
## Context menus
Use secondary click when the app exposes actions only through the contextual menu.
```bash
agent-device click @e66 --button secondary --platform macos
agent-device snapshot -i
```
Expected pattern:
1. Snapshot visible content.
2. Secondary-click the target row/item.
3. Snapshot again.
4. Interact with newly visible `menu-item` nodes.
Do not expect context-menu items to appear before the menu is opened.
## Finder-specific guidance
- `snapshot -i` should still expose visible folder rows even when nothing is selected.
- Unselected folder contents should still be visible in `snapshot -i` through list/table rows.
- A file row may expose multiple nodes with the same label, including a row container, name cell, and child text/text-field.
- For opening a context menu, prefer the outer visible row/cell ref over a nested text child if both exist.
- After secondary click, expect actions such as `Rename`, `Quick Look`, `Copy`, `Compress`, and tag-related items in the next snapshot.
## Raw snapshots
Use `snapshot --raw` only when debugging AX structure or collector issues.
```bash
agent-device snapshot --raw --platform macos
```
- Raw output is larger and less token-efficient.
- It is useful for verifying whether missing UI is absent from the AX tree or only filtered from interactive output.
- Do not use raw output as the default agent loop when `snapshot -i` already shows the visible window content you need.
## Selector guidance
Good macOS selectors usually anchor on one of:
- `label="Downloads"`
- `label="failed-step.json"`
- `role=button label="Search"`
- `role=menu-item label="Rename"`
Prefer exact labels when the desktop UI is stable. Use `id=...` when the AX identifier is clearly app-owned and not a framework-generated `_NS:*` value.
## Things not to rely on
- Mobile-only helpers like `install`, `reinstall`, `push`, `logs`, `network`, or generic `alert`
- Long-press as a substitute for right-click
- Raw coordinate assumptions across runs; macOS windows can move
- Framework-generated `_NS:*` identifiers as stable selectors
## Troubleshooting
- If visible window content is missing from `snapshot -i`, re-snapshot once after the UI settles.
- If the wrong menu opened or no menu appeared, retry secondary-clicking the row/cell wrapper instead of the nested text node.
- If the app has multiple windows, ensure the correct one is frontmost before relying on refs.
+1
-1
import{createRequestId as e,node_path as t,isAgentDeviceDaemonProcess as r,runCmdDetached as a,readVersion as n,findProjectRoot as o,node_https as i,runCmdSync as s,withDiagnosticTimer as l,resolveDaemonTransportPreference as d,resolveUserPath as u,emitDiagnostic as c,spawn as p,AppError as m,node_fs as h,resolveDaemonPaths as f,node_net as I,resolveDaemonServerMode as g,stopProcessForTakeover as w,node_http as b}from"./331.js";async function y(e){let{localPath:r,baseUrl:a,token:n}=e,o=h.statSync(r).isDirectory(),s=t.basename(r),l=new URL("upload",a.endsWith("/")?a:`${a}/`),d="https:"===l.protocol?i:b,u={"x-artifact-type":o?"app-bundle":"file","x-artifact-filename":s,"transfer-encoding":"chunked"};return n&&(u.authorization=`Bearer ${n}`,u["x-agent-device-token"]=n),new Promise((e,a)=>{let n=d.request({protocol:l.protocol,host:l.hostname,port:l.port,method:"POST",path:l.pathname+l.search,headers:u},t=>{let r="";t.setEncoding("utf8"),t.on("data",e=>{r+=e}),t.on("end",()=>{clearTimeout(i);try{let t=JSON.parse(r);if(!t.ok||!t.uploadId)return void a(new m("COMMAND_FAILED",`Upload failed: ${r}`));e(t.uploadId)}catch{a(new m("COMMAND_FAILED",`Invalid upload response: ${r}`))}})}),i=setTimeout(()=>{n.destroy(),a(new m("COMMAND_FAILED","Artifact upload timed out",{timeoutMs:3e5,hint:"The upload to the remote daemon exceeded the 5-minute timeout."}))},3e5);if(n.on("error",e=>{clearTimeout(i),a(new m("COMMAND_FAILED","Failed to upload artifact to remote daemon",{hint:"Verify the remote daemon is reachable and supports artifact uploads."},e))}),o){let e=p("tar",["cf","-","-C",t.dirname(r),t.basename(r)],{stdio:["ignore","pipe","pipe"]});e.stdout.pipe(n),e.on("error",e=>{n.destroy(),a(new m("COMMAND_FAILED","Failed to create tar archive for app bundle",{},e))}),e.on("close",e=>{0!==e&&(n.destroy(),a(new m("COMMAND_FAILED",`tar failed with exit code ${e}`)))})}else{let e=h.createReadStream(r);e.pipe(n),e.on("error",e=>{n.destroy(),a(new m("COMMAND_FAILED","Failed to read local artifact",{},e))})}})}let v=function(e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS){if(!e)return 9e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):9e4}(),A=function(e=process.env.AGENT_DEVICE_DAEMON_STARTUP_TIMEOUT_MS){if(!e)return 15e3;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):15e3}(),D=function(e=process.env.AGENT_DEVICE_DAEMON_STARTUP_ATTEMPTS){if(!e)return 2;let t=Number(e);return Number.isFinite(t)?Math.min(5,Math.max(1,Math.floor(t))):2}(),_=["xcodebuild .*AgentDeviceRunnerUITests/RunnerTests/testCommand","xcodebuild .*AgentDeviceRunner\\.env\\.session-","xcodebuild build-for-testing .*ios-runner/AgentDeviceRunner/AgentDeviceRunner\\.xcodeproj"];async function P(t){let r=t.meta?.requestId??e(),a=!!(t.meta?.debug||t.flags?.verbose),n=function(e){let t=e.flags?.stateDir??process.env.AGENT_DEVICE_STATE_DIR,r=function(e){let t;if(e){try{t=new URL(e)}catch(t){throw new m("INVALID_ARGS","Invalid daemon base URL",{daemonBaseUrl:e},t instanceof Error?t:void 0)}if("http:"!==t.protocol&&"https:"!==t.protocol)throw new m("INVALID_ARGS","Daemon base URL must use http or https",{daemonBaseUrl:e});return t.toString().replace(/\/+$/,"")}}(e.flags?.daemonBaseUrl??process.env.AGENT_DEVICE_DAEMON_BASE_URL),a=e.flags?.daemonAuthToken??process.env.AGENT_DEVICE_DAEMON_AUTH_TOKEN,n=e.flags?.daemonTransport??process.env.AGENT_DEVICE_DAEMON_TRANSPORT,o=d(n);if(r&&"socket"===o)throw new m("INVALID_ARGS","Remote daemon base URL only supports HTTP transport. Remove --daemon-transport socket.",{daemonBaseUrl:r});let i=g(e.flags?.daemonServerMode??process.env.AGENT_DEVICE_DAEMON_SERVER_MODE??("dual"===n?"dual":void 0));return{paths:f(t),transportPreference:o,serverMode:i,remoteBaseUrl:r,remoteAuthToken:a}}(t),o=await l("daemon_startup",async()=>await N(n),{requestId:r,session:t.session}),i=await E(t,o),s={...t,positionals:i.positionals,flags:i.flags,token:o.token,meta:{...t.meta??{},requestId:r,debug:a,cwd:t.meta?.cwd,tenantId:t.meta?.tenantId??t.flags?.tenant,runId:t.meta?.runId??t.flags?.runId,leaseId:t.meta?.leaseId??t.flags?.leaseId,sessionIsolation:t.meta?.sessionIsolation??t.flags?.sessionIsolation,lockPolicy:t.meta?.lockPolicy,lockPlatform:t.meta?.lockPlatform,...i.uploadedArtifactId?{uploadedArtifactId:i.uploadedArtifactId}:{},...i.clientArtifactPaths?{clientArtifactPaths:i.clientArtifactPaths}:{},...i.installSource?{installSource:i.installSource}:{}}};return c({level:"info",phase:"daemon_request_prepare",data:{requestId:r,command:t.command,session:t.session}}),await l("daemon_request",async()=>await j(o,s,n.transportPreference),{requestId:r,command:t.command})}async function E(e,r){let a,n=[...e.positionals??[]],o=e.flags?{...e.flags}:void 0,i=e.meta?.installSource,s={};if(X(r)){let l=function(e,r){if("screenshot"===e.command){let t=M(e,"path",".png");return r[0]?{field:"path",localPath:t,positionalIndex:0,positionalPath:k("screenshot",".png")}:{field:"path",localPath:t,positionalIndex:0,flagPath:k("screenshot",".png")}}if("record"===e.command&&"start"===(r[0]??"").toLowerCase()){let r=M(e,"outPath",".mp4",1);return{field:"outPath",localPath:r,positionalIndex:1,positionalPath:k("recording",t.extname(r)||".mp4")}}return null}(e,n);l&&(void 0!==l.positionalPath&&(n[l.positionalIndex]=l.positionalPath),void 0!==l.flagPath&&((o??={}).out=l.flagPath),s[l.field]=l.localPath);let d=await S(e,r);d&&(i=d.installSource,a=d.uploadedArtifactId??a)}if(!X(r)||"install"!==e.command&&"reinstall"!==e.command||n.length<2)return{positionals:n,flags:o,installSource:i,uploadedArtifactId:a,...Object.keys(s).length>0?{clientArtifactPaths:s}:{}};let l=n[1];if(l.startsWith("remote:"))return n[1]=l.slice(7),{positionals:n,flags:o,...Object.keys(s).length>0?{clientArtifactPaths:s}:{}};let d=t.isAbsolute(l)?l:t.resolve(e.meta?.cwd??process.cwd(),l);return h.existsSync(d)?{positionals:n,flags:o,installSource:i,uploadedArtifactId:a=await y({localPath:d,baseUrl:r.baseUrl,token:r.token}),...Object.keys(s).length>0?{clientArtifactPaths:s}:{}}:{positionals:n,flags:o,...Object.keys(s).length>0?{clientArtifactPaths:s}:{}}}async function S(e,r){let a=e.meta?.installSource;if("install_source"!==e.command||!a||"path"!==a.kind)return null;let n=a.path.trim();if(!n)return{installSource:a};if(n.startsWith("remote:"))return{installSource:{...a,path:n.slice(7)}};let o=t.isAbsolute(n)?n:t.resolve(e.meta?.cwd??process.cwd(),n);if(!h.existsSync(o))return{installSource:{...a,path:o}};let i=await y({localPath:o,baseUrl:r.baseUrl,token:r.token});return{installSource:{...a,path:o},uploadedArtifactId:i}}function M(e,r,a,n=0){let o=e.positionals?.[n]??e.flags?.out,i=`${"path"===r?"screenshot":"recording"}-${Date.now()}${a}`,s=o&&o.trim().length>0?o:i;return t.isAbsolute(s)?s:t.resolve(e.meta?.cwd??process.cwd(),s)}function k(e,r){let a=r.startsWith(".")?r:`.${r}`;return t.posix.join("/tmp",`agent-device-${e}-${Date.now()}-${Math.random().toString(36).slice(2,8)}${a}`)}async function N(e){let a;if(e.remoteBaseUrl){let t={transport:"http",token:e.remoteAuthToken??"",pid:0,baseUrl:e.remoteBaseUrl};if(await q(t,"http"))return t;throw new m("COMMAND_FAILED","Remote daemon is unavailable",{daemonBaseUrl:e.remoteBaseUrl,hint:"Verify AGENT_DEVICE_DAEMON_BASE_URL points to a reachable daemon with GET /health and POST /rpc."})}let i=x(e.paths.infoPath),s=n(),l=function(e,r=o()){try{let a=h.statSync(e),n=t.relative(r,e)||e;return`${n}:${a.size}:${Math.trunc(a.mtimeMs)}`}catch{return"unknown"}}((a=z()).useSrc?a.srcPath:a.distPath,a.root),d=!!i&&await q(i,e.transportPreference);if(i&&i.version===s&&i.codeSignature===l&&d)return i;i&&(i.version!==s||i.codeSignature!==l||!d)&&(await O(i),F(e.paths.infoPath)),function(e){let t=L(e);if(!t.hasLock||t.hasInfo)return;let a=C(e.lockPath);if(!a)return F(e.lockPath);r(a.pid,a.processStartTime)||F(e.lockPath)}(e.paths);let u=0;for(let t=1;t<=D;t+=1){await B(e);let r=await T(A,e);if(r)return r;if(await U(e.paths)){u+=1;continue}let a=L(e.paths);if(!(t<D))break;if(!a.hasInfo&&!a.hasLock){await R(150);continue}}let c=L(e.paths);throw new m("COMMAND_FAILED","Failed to start daemon",{kind:"daemon_startup_failed",infoPath:e.paths.infoPath,lockPath:e.paths.lockPath,startupTimeoutMs:A,startupAttempts:D,lockRecoveryCount:u,metadataState:c,hint:function(e,t=f(process.env.AGENT_DEVICE_STATE_DIR)){return e.hasLock&&!e.hasInfo?`Detected ${t.lockPath} without ${t.infoPath}. If no agent-device daemon process is running, delete ${t.lockPath} and retry.`:e.hasLock&&e.hasInfo?`Daemon metadata may be stale. If no agent-device daemon process is running, delete ${t.infoPath} and ${t.lockPath}, then retry.`:`Daemon metadata is missing or stale. Delete ${t.infoPath} if present and retry.`}(c,e.paths)})}async function T(e,t){let r=Date.now();for(;Date.now()-r<e;){let e=x(t.paths.infoPath);if(e&&await q(e,t.transportPreference))return e;await new Promise(e=>setTimeout(e,100))}return null}async function R(e){await new Promise(t=>setTimeout(t,e))}async function U(e){let t=L(e);if(!t.hasLock||t.hasInfo)return!1;let a=C(e.lockPath);return a&&r(a.pid,a.processStartTime)&&await w(a.pid,{termTimeoutMs:3e3,killTimeoutMs:1e3,expectedStartTime:a.processStartTime}),F(e.lockPath),!0}async function O(e){await w(e.pid,{termTimeoutMs:3e3,killTimeoutMs:1e3,expectedStartTime:e.processStartTime})}function x(e){let t=$(e);if(!t||"object"!=typeof t)return null;let r="string"==typeof t.token&&t.token.length>0?t.token:null;if(!r)return null;let a=Number.isInteger(t.port)&&Number(t.port)>0,n=Number.isInteger(t.httpPort)&&Number(t.httpPort)>0;if(!a&&!n)return null;let o=t.transport,i="string"==typeof t.version?t.version:void 0,s="string"==typeof t.codeSignature?t.codeSignature:void 0,l="string"==typeof t.processStartTime?t.processStartTime:void 0,d=Number.isInteger(t.pid)&&Number(t.pid)>0;return{token:r,port:a?Number(t.port):void 0,httpPort:n?Number(t.httpPort):void 0,transport:"socket"===o||"http"===o||"dual"===o?o:void 0,pid:d?Number(t.pid):0,version:i,codeSignature:s,processStartTime:l}}function C(e){let t=$(e);return t&&"object"==typeof t&&Number.isInteger(t.pid)&&Number(t.pid)>0?{pid:Number(t.pid),processStartTime:"string"==typeof t.processStartTime?t.processStartTime:void 0,startedAt:"number"==typeof t.startedAt?t.startedAt:void 0}:null}function L(e){return{hasInfo:h.existsSync(e.infoPath),hasLock:h.existsSync(e.lockPath)}}function $(e){if(!h.existsSync(e))return null;try{return JSON.parse(h.readFileSync(e,"utf8"))}catch{return null}}function F(e){try{h.existsSync(e)&&h.unlinkSync(e)}catch{}}async function q(e,t){var r;return"http"===G(e,t)?await function(e){let t=e.baseUrl?Y(e.baseUrl,"health"):e.httpPort?`http://127.0.0.1:${e.httpPort}/health`:null;if(!t)return Promise.resolve(!1);let r=new URL(t),a="https:"===r.protocol?i:b,n=e.baseUrl?3e3:500;return new Promise(e=>{let t=a.request({protocol:r.protocol,host:r.hostname,port:r.port,path:r.pathname+r.search,method:"GET",timeout:n},t=>{t.resume(),e((t.statusCode??500)<500)});t.on("timeout",()=>{t.destroy(),e(!1)}),t.on("error",()=>{e(!1)}),t.end()})}(e):await ((r=e.port)?new Promise(e=>{let t=I.createConnection({host:"127.0.0.1",port:r},()=>{t.destroy(),e(!0)});t.on("error",()=>{e(!1)})}):Promise.resolve(!1))}async function B(e){let t=z(),r=t.useSrc?["--experimental-strip-types",t.srcPath]:[t.distPath],n={...process.env,AGENT_DEVICE_STATE_DIR:e.paths.baseDir,AGENT_DEVICE_DAEMON_SERVER_MODE:e.serverMode};a(process.execPath,r,{env:n})}function z(){let e=o(),r=t.join(e,"dist","src","daemon.js"),a=t.join(e,"src","daemon.ts"),n=h.existsSync(r),i=h.existsSync(a);if(!n&&!i)throw new m("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:a});return{root:e,distPath:r,srcPath:a,useSrc:process.execArgv.includes("--experimental-strip-types")?i:!n&&i}}async function j(e,t,r){return"http"===G(e,r)?await J(e,t):await H(e,t)}function G(e,t){if(e.baseUrl){if("socket"===t)throw new m("COMMAND_FAILED","Remote daemon endpoint only supports HTTP transport",{daemonBaseUrl:e.baseUrl});return"http"}if("http"===t||"socket"===t){var r=e,a=t;if(V(r,a))return a;throw new m("COMMAND_FAILED","http"===a?"Daemon HTTP endpoint is unavailable":"Daemon socket endpoint is unavailable")}let n=("socket"===e.transport||"dual"===e.transport?["socket","http"]:["http","socket"]).find(t=>V(e,t));if(n)return n;throw new m("COMMAND_FAILED","Daemon metadata has no reachable transport")}function V(e,t){return"http"===t?!!e.httpPort:!!e.port}async function H(e,t){let r=e.port;if(!r)throw new m("COMMAND_FAILED","Daemon socket endpoint is unavailable");return new Promise((a,n)=>{let o=I.createConnection({host:"127.0.0.1",port:r},()=>{o.write(`${JSON.stringify(t)}
`)}),i=setTimeout(()=>{o.destroy();let r=K(),a=W(e,f(t.flags?.stateDir??process.env.AGENT_DEVICE_STATE_DIR));c({level:"error",phase:"daemon_request_timeout",data:{timeoutMs:v,requestId:t.meta?.requestId,command:t.command,timedOutRunnerPidsTerminated:r.terminated,timedOutRunnerCleanupError:r.error,daemonPidReset:e.pid,daemonPidForceKilled:a.forcedKill}}),n(new m("COMMAND_FAILED","Daemon request timed out",{timeoutMs:v,requestId:t.meta?.requestId,hint:"Retry with --debug and check daemon diagnostics logs. Timed-out iOS runner xcodebuild processes were terminated when detected."}))},v),s="";o.setEncoding("utf8"),o.on("data",e=>{let r=(s+=e).indexOf("\n");if(-1===r)return;let l=s.slice(0,r).trim();if(l)try{let e=JSON.parse(l);o.end(),clearTimeout(i),a(e)}catch(e){clearTimeout(i),n(new m("COMMAND_FAILED","Invalid daemon response",{requestId:t.meta?.requestId,line:l},e instanceof Error?e:void 0))}}),o.on("error",e=>{clearTimeout(i),c({level:"error",phase:"daemon_request_socket_error",data:{requestId:t.meta?.requestId,message:e instanceof Error?e.message:String(e)}}),n(new m("COMMAND_FAILED","Failed to communicate with daemon",{requestId:t.meta?.requestId,hint:"Retry command. If this persists, clean stale daemon metadata and start a fresh session."},e))})})}async function J(t,r){let a=t.baseUrl?new URL(Y(t.baseUrl,"rpc")):t.httpPort?new URL(`http://127.0.0.1:${t.httpPort}/rpc`):null;if(!a)throw new m("COMMAND_FAILED","Daemon HTTP endpoint is unavailable");let n=JSON.stringify({jsonrpc:"2.0",id:r.meta?.requestId??e(),method:"agent_device.command",params:r}),o={"content-type":"application/json","content-length":Buffer.byteLength(n)};return t.baseUrl&&t.token&&(o.authorization=`Bearer ${t.token}`,o["x-agent-device-token"]=t.token),await new Promise((e,s)=>{let l=f(r.flags?.stateDir??process.env.AGENT_DEVICE_STATE_DIR),d=("https:"===a.protocol?i:b).request({protocol:a.protocol,host:a.hostname,port:a.port,method:"POST",path:a.pathname+a.search,headers:o},a=>{let n="";a.setEncoding("utf8"),a.on("data",e=>{n+=e}),a.on("end",()=>{clearTimeout(u);try{let a=JSON.parse(n);if(a.error){let e=a.error.data??{};s(new m(String(e.code??"COMMAND_FAILED"),String(e.message??a.error.message??"Daemon RPC request failed"),{..."object"==typeof e.details&&e.details?e.details:{},hint:"string"==typeof e.hint?e.hint:void 0,diagnosticId:"string"==typeof e.diagnosticId?e.diagnosticId:void 0,logPath:"string"==typeof e.logPath?e.logPath:void 0,requestId:r.meta?.requestId}));return}if(!a.result||"object"!=typeof a.result)return void s(new m("COMMAND_FAILED","Invalid daemon RPC response",{requestId:r.meta?.requestId}));if(t.baseUrl&&a.result.ok)return void Q(t,r,a.result).then(e).catch(s);e(a.result)}catch(e){clearTimeout(u),s(new m("COMMAND_FAILED","Invalid daemon response",{requestId:r.meta?.requestId,line:n},e instanceof Error?e:void 0))}})}),u=setTimeout(()=>{d.destroy();let e=X(t)?{terminated:0}:K(),a=X(t)?{forcedKill:!1}:W(t,l);c({level:"error",phase:"daemon_request_timeout",data:{timeoutMs:v,requestId:r.meta?.requestId,command:r.command,timedOutRunnerPidsTerminated:e.terminated,timedOutRunnerCleanupError:e.error,daemonPidReset:X(t)?void 0:t.pid,daemonPidForceKilled:X(t)?void 0:a.forcedKill,daemonBaseUrl:t.baseUrl}}),s(new m("COMMAND_FAILED","Daemon request timed out",{timeoutMs:v,requestId:r.meta?.requestId,hint:X(t)?"Retry with --debug and verify the remote daemon URL, auth token, and remote host logs.":"Retry with --debug and check daemon diagnostics logs. Timed-out iOS runner xcodebuild processes were terminated when detected."}))},v);d.on("error",e=>{clearTimeout(u),c({level:"error",phase:"daemon_request_socket_error",data:{requestId:r.meta?.requestId,message:e instanceof Error?e.message:String(e)}}),s(new m("COMMAND_FAILED","Failed to communicate with daemon",{requestId:r.meta?.requestId,hint:X(t)?"Retry command. If this persists, verify the remote daemon URL, auth token, and remote host reachability.":"Retry command. If this persists, clean stale daemon metadata and start a fresh session."},e))}),d.write(n),d.end()})}function K(){let e=0;try{for(let t of _){let r=s("pkill",["-f",t],{allowFailure:!0});0===r.exitCode&&(e+=1)}return{terminated:e}}catch(t){return{terminated:e,error:t instanceof Error?t.message:String(t)}}}function W(e,t){let a=!1;try{r(e.pid,e.processStartTime)&&(process.kill(e.pid,"SIGKILL"),a=!0)}catch{w(e.pid,{termTimeoutMs:3e3,killTimeoutMs:1e3,expectedStartTime:e.processStartTime})}finally{F(t.infoPath),F(t.lockPath)}return{forcedKill:a}}function X(e){return"string"==typeof e.baseUrl&&e.baseUrl.length>0}function Y(e,t){return new URL(t,e.endsWith("/")?e:`${e}/`).toString()}async function Q(e,r,a){let n=Array.isArray(a.data?.artifacts)?a.data.artifacts:[];if(0===n.length||!e.baseUrl)return a;let o=a.data?{...a.data}:{},i=[];for(let a of n){if(!a||"object"!=typeof a||"string"!=typeof a.artifactId){i.push(a);continue}let n=function(e,r){if(e.localPath&&e.localPath.trim().length>0)return e.localPath;let a=e.fileName?.trim()||`${e.field}-${Date.now()}`;return t.resolve(r.meta?.cwd??process.cwd(),a)}(a,r);await Z({baseUrl:e.baseUrl,token:e.token,artifactId:a.artifactId,destinationPath:n,requestId:r.meta?.requestId}),o[a.field]=n,i.push({...a,localPath:n})}return o.artifacts=i,{ok:!0,data:o}}async function Z(e){var r,a;let n,o=new URL((r=e.baseUrl,a=e.artifactId,n=r.endsWith("/")?r:`${r}/`,new URL(`upload/${encodeURIComponent(a)}`,n).toString())),s="https:"===o.protocol?i:b;await h.promises.mkdir(t.dirname(e.destinationPath),{recursive:!0}),await new Promise((t,r)=>{let a=!1,n=e.timeoutMs??v,i=n=>{if(!a){if(a=!0,clearTimeout(d),n)return void h.promises.rm(e.destinationPath,{force:!0}).finally(()=>r(n));t()}},l=s.request({protocol:o.protocol,host:o.hostname,port:o.port,method:"GET",path:o.pathname+o.search,headers:e.token?{authorization:`Bearer ${e.token}`,"x-agent-device-token":e.token}:void 0},t=>{if((t.statusCode??500)>=400){let r="";t.setEncoding("utf8"),t.on("data",e=>{r+=e}),t.on("end",()=>{i(new m("COMMAND_FAILED","Failed to download remote artifact",{artifactId:e.artifactId,statusCode:t.statusCode,requestId:e.requestId,body:r}))});return}let r=h.createWriteStream(e.destinationPath);r.on("error",e=>{i(e instanceof Error?e:Error(String(e)))}),t.on("error",e=>{i(e instanceof Error?e:Error(String(e)))}),t.on("aborted",()=>{i(new m("COMMAND_FAILED","Remote artifact download was interrupted",{artifactId:e.artifactId,requestId:e.requestId}))}),r.on("finish",()=>{r.close(()=>i())}),t.pipe(r)}),d=setTimeout(()=>{l.destroy(new m("COMMAND_FAILED","Remote artifact download timed out",{artifactId:e.artifactId,requestId:e.requestId,timeoutMs:n}))},n);l.on("error",t=>{t instanceof m?i(t):i(new m("COMMAND_FAILED","Failed to download remote artifact",{artifactId:e.artifactId,requestId:e.requestId,timeoutMs:n},t instanceof Error?t:void 0))}),l.end()})}function ee(e){return e.replace(/\/+$/,"")}function et(e){return"string"==typeof e&&e.trim()?ee(e.trim()):""}function er(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function ea(e,t,r){return u(e,{env:t,cwd:r})}function en(e){try{return h.accessSync(e,h.constants.F_OK),!0}catch{return!1}}function eo(e,t,r){if(null==e||""===e)return t;let a=Number.parseInt(String(e),10);return Number.isInteger(a)?Math.max(a,r):t}function ei(e,t){let r;return{platform:t,bundleUrl:((r=new URL(`${ee(e)}/index.bundle`)).searchParams.set("platform",t),r.searchParams.set("dev","true"),r.searchParams.set("minify","false"),r.toString())}}function es(e,t){return{platform:t,metroHost:er(e?.metro_host),metroPort:e?.metro_port,bundleUrl:er(e?.metro_bundle_url),launchUrl:er(e?.launch_url)}}function el(e){return`'${e.replace(/'/g,"'\"'\"'")}'`}async function ed(e){await new Promise(t=>setTimeout(t,e))}async function eu(e,t,r={}){try{let a=await fetch(e,{headers:r,signal:AbortSignal.timeout(t)});return{ok:a.ok,status:a.status,body:await a.text()}}catch(r){if(r instanceof Error&&"TimeoutError"===r.name)throw Error(`Timed out fetching ${e} after ${t}ms`);throw r}}async function ec(e,t){try{let r=await eu(e,t);return r.ok&&r.body.includes("packager-status:running")}catch{return!1}}async function ep(e){var t,r,a;let n;try{n=await fetch(`${e.baseUrl}/api/metro/bridge`,{method:"POST",headers:(t=e.baseUrl,r=e.bearerToken,{Authorization:`Bearer ${r}`,"Content-Type":"application/json",...t.includes("ngrok")?{"ngrok-skip-browser-warning":"1"}:{}}),body:JSON.stringify({ios_runtime:e.runtime,timeout_ms:e.timeoutMs}),signal:AbortSignal.timeout(e.timeoutMs)})}catch(t){if(t instanceof Error&&"TimeoutError"===t.name)throw Error(`/api/metro/bridge timed out after ${e.timeoutMs}ms calling ${e.baseUrl}/api/metro/bridge`);throw t}let o=await n.text(),i=o?JSON.parse(o):{};if(!n.ok)throw Error(`/api/metro/bridge failed (${n.status}): ${JSON.stringify(i)}`);return{enabled:(a=i.data??i).enabled,baseUrl:a.base_url,statusUrl:a.status_url,bundleUrl:a.bundle_url,iosRuntime:es(a.ios_runtime,"ios"),androidRuntime:es(a.android_runtime,"android"),upstream:{bundleUrl:a.upstream.bundle_url,host:a.upstream.host,port:a.upstream.port,statusUrl:a.upstream.status_url},probe:{reachable:a.probe.reachable,statusCode:a.probe.status_code,latencyMs:a.probe.latency_ms,detail:a.probe.detail}}}async function em(e,t,r){let a=Date.now()+t;for(;Date.now()<a;){let t=Math.min(r,Math.max(a-Date.now(),1));if(await ec(e,t))return!0;let n=Math.min(500,Math.max(a-Date.now(),0));n>0&&await ed(n)}return!1}async function eh(e={}){let r=e.env??process.env,a=process.cwd(),n=ea(e.projectRoot??a,r,a),o=function(e,r){if("auto"!==r)return r;let a=function(e){let r=t.join(e,"package.json");if(!en(r))throw new m("INVALID_ARGS",`package.json not found at ${r}`);return JSON.parse(h.readFileSync(r,"utf8"))}(e);return"string"==typeof({...a.dependencies??{},...a.devDependencies??{}}).expo?"expo":"react-native"}(n,e.kind??"auto"),i=function(e,t){if(null==e||""===e)return 8081;let r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<1||r>65535)throw new m("INVALID_ARGS",`Invalid Metro port: ${String(e)}. Use 1-65535.`);return r}(e.metroPort??8081,0),l=er(e.listenHost)??"0.0.0.0",d=er(e.statusHost)??"127.0.0.1",u=et(e.publicBaseUrl),c=eo(e.startupTimeoutMs,18e4,3e4),p=eo(e.probeTimeoutMs,1e4,1e3),f=e.reuseExisting??!0,I=e.installDependenciesIfNeeded??!0,g=e.runtimeFilePath?ea(e.runtimeFilePath,r,a):null,w=ea(e.logPath??t.join(n,".agent-device","metro.log"),r,a);if(!u)throw new m("INVALID_ARGS","metro prepare requires --public-base-url <url>.");let{proxyEnabled:b,proxyBaseUrl:y,proxyBearerToken:v}=function(e,t){if(e&&!t)throw new m("INVALID_ARGS","metro prepare requires proxy auth when --proxy-base-url is provided. Pass --bearer-token or set AGENT_DEVICE_PROXY_TOKEN.");if(!e&&t)throw new m("INVALID_ARGS","metro prepare requires --proxy-base-url when proxy auth is provided.");return{proxyEnabled:!!(e&&t),proxyBaseUrl:e,proxyBearerToken:t}}(et(e.proxyBaseUrl),er(e.proxyBearerToken)??""),A=I?function(e,r){if(function(e){try{return h.statSync(e).isDirectory()}catch{return!1}}(t.join(e,"node_modules")))return{installed:!1};let a=en(t.join(e,"pnpm-lock.yaml"))?{command:"pnpm",installArgs:["install"]}:en(t.join(e,"yarn.lock"))?{command:"yarn",installArgs:["install"]}:{command:"npm",installArgs:["install"]};return s(a.command,a.installArgs,{cwd:e,env:r}),{installed:!0,packageManager:a.command}}(n,r):{installed:!1},D=`http://${d}:${i}/status`,_=!1,P=!1,E=0;if(f&&await ec(D,p))P=!0;else if(_=!0,E=function(e,r,a,n,o,i){let l="expo"===r?{command:"npx",installArgs:["expo","start","--host","lan","--port",String(a)]}:{command:"npx",installArgs:["react-native","start","--host",n,"--port",String(a)]};h.mkdirSync(t.dirname(o),{recursive:!0});let d=[el(l.command),...l.installArgs.map(el)].join(" "),u=s("/bin/sh",["-c",`nohup ${d} >> ${el(o)} 2>&1 < /dev/null & echo $!`],{cwd:e,env:i}),c=Number.parseInt(u.stdout.trim(),10);if(!Number.isInteger(c)||c<=0)throw Error(`Failed to start Metro. Expected a child PID in stdout, got "${u.stdout.trim()}".`);return{pid:c}}(n,o,i,l,w,r).pid,!await em(D,c,p))throw Error(`Metro did not become ready at ${D} within ${c}ms. Check ${w}.`);let S=ei(u,"ios"),M=ei(u,"android"),k=null,N=null;if(b)try{k=await ep({baseUrl:y,bearerToken:v,runtime:{metro_bundle_url:S.bundleUrl},timeoutMs:p})}catch(e){N=e instanceof Error?e.message:String(e)}if(b&&(!k||!1===k.probe.reachable)){var T,R;let e;throw Error((T=N,R=k,e=[`Metro bridge is required for this run but could not be configured via ${y}/api/metro/bridge.`],T&&e.push(`bridgeError=${T}`),R?.probe.reachable===!1&&e.push(`bridgeProbe=${R.probe.detail||`unreachable (status ${R.probe.statusCode||0})`}`),e.join(" ")))}let U=k?.iosRuntime??S,O=k?.androidRuntime??M,x={projectRoot:n,kind:o,dependenciesInstalled:A.installed,packageManager:A.packageManager??null,started:_,reused:P,pid:E,logPath:w,statusUrl:D,runtimeFilePath:g,iosRuntime:U,androidRuntime:O,bridge:k};return g&&(h.mkdirSync(t.dirname(g),{recursive:!0}),h.writeFileSync(g,JSON.stringify(x,null,2))),x}function ef(e){let t=e.appId??e.bundleId??e.packageName;return{session:e.session,appId:t,appBundleId:e.bundleId,package:e.packageName}}function eI(e,t,r){return{deviceId:t,deviceName:r,..."ios"===e?{udid:t}:{serial:t}}}function eg(e,t={}){let r=t.includeAndroidSerial??!0;return{platform:e.platform,target:e.target,device:e.name,id:e.id,..."ios"===e.platform?{device_udid:e.ios?.udid??e.id,ios_simulator_device_set:e.ios?.simulatorSetPath??null}:{},..."android"===e.platform&&r?{serial:e.android?.serial??e.id}:{}}}function ew(e){return{name:e.name,...eg(e.device,{includeAndroidSerial:!1}),createdAt:e.createdAt}}function eb(e){return{platform:e.platform,id:e.id,name:e.name,kind:e.kind,target:e.target,..."boolean"==typeof e.booted?{booted:e.booted}:{}}}function ey(e){return{udid:e.udid,device:e.device,runtime:e.runtime,ios_simulator_device_set:e.iosSimulatorDeviceSet??null,created:e.created,booted:e.booted}}function ev(e){return{app:e.app,appPath:e.appPath,platform:e.platform,...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.package?{package:e.package}:{}}}function eA(e){return{launchTarget:e.launchTarget,...e.appName?{appName:e.appName}:{},...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.packageName?{package:e.packageName}:{},...e.installablePath?{installablePath:e.installablePath}:{},...e.archivePath?{archivePath:e.archivePath}:{},...e.materializationId?{materializationId:e.materializationId}:{},...e.materializationExpiresAt?{materializationExpiresAt:e.materializationExpiresAt}:{}}}function eD(e){return{session:e.session,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{},...e.startup?{startup:e.startup}:{},...e.runtime?{runtime:e.runtime}:{},...e.device?eg(e.device):{}}}function e_(e){return{session:e.session,...e.shutdown?{shutdown:e.shutdown}:{}}}function eP(e){return{nodes:e.nodes,truncated:e.truncated,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{}}}function eE(e,t){let r=eO(e,"bundleId"),a=eO(e,"package");return{app:eU(e,"app"),appPath:eU(e,"appPath"),platform:ex(e,"platform"),appId:r??a,bundleId:r,package:a,identifiers:ef({session:t,bundleId:r,packageName:a})}}function eS(e){var t;let r=eT(e),a=ex(r,"platform"),n=eU(r,"id"),o=eU(r,"name");return{platform:a,target:eC(r,"target"),kind:eL(r,t="kind",ez,`Daemon response has invalid "${t}".`),id:n,name:o,booted:"boolean"==typeof r.booted?r.booted:void 0,identifiers:eI(a,n,o),ios:"ios"===a?{udid:n}:void 0,android:"android"===a?{serial:n}:void 0}}function eM(e){var t;let r=eT(e),a=ex(r,"platform"),n=eU(r,"id"),o=eU(r,"name"),i=eC(r,"target"),s=eU(r,"device"),l={session:o,...eI(a,n,s)};return{name:o,createdAt:eL(r,t="createdAt",eq,`Daemon response is missing numeric "${t}".`),device:{platform:a,target:i,id:n,name:s,identifiers:l,ios:"ios"===a?{udid:n,simulatorSetPath:e$(r,"ios_simulator_device_set",eF)}:void 0,android:"android"===a?{serial:n}:void 0},identifiers:l}}function ek(e,t){return t??e??"default"}function eN(e){let t={};for(let[r,a]of Object.entries(e))void 0!==a&&(t[r]=a);return t}function eT(e){if(!eR(e))throw new m("COMMAND_FAILED","Daemon returned an unexpected response shape.",{value:e});return e}function eR(e){return"object"==typeof e&&null!==e}function eU(e,t){return eL(e,t,eF,`Daemon response is missing "${t}".`)}function eO(e,t){return eF(e[t])}function ex(e,t){return eL(e,t,eB,`Daemon response has invalid "${t}".`)}function eC(e,t){return ej(e[t])??"mobile"}function eL(e,t,r,a){let n=r(e[t]);if(void 0===n)throw new m("COMMAND_FAILED",a,{response:e});return n}function e$(e,t,r){let a=e[t];return null===a?null:r(a)}function eF(e){return"string"==typeof e&&e.length>0?e:void 0}function eq(e){return"number"==typeof e&&Number.isFinite(e)?e:void 0}function eB(e){return"ios"===e||"android"===e?e:void 0}function ez(e){return"simulator"===e||"emulator"===e||"device"===e?e:void 0}function ej(e){return"tv"===e?"tv":"mobile"===e?"mobile":void 0}function eG(e={},t={}){let r=t.transport??P,a=async(t,a=[],n={})=>{let o={...e,...n},i=await r({session:ek(e.session,n.session),command:t,positionals:a,flags:eN({stateDir:o.stateDir,daemonBaseUrl:o.daemonBaseUrl,daemonAuthToken:o.daemonAuthToken,daemonTransport:o.daemonTransport,daemonServerMode:o.daemonServerMode,tenant:o.tenant,sessionIsolation:o.sessionIsolation,runId:o.runId,leaseId:o.leaseId,platform:o.platform,target:o.target,device:o.device,udid:o.udid,serial:o.serial,iosSimulatorDeviceSet:o.iosSimulatorDeviceSet,androidDeviceAllowlist:o.androidDeviceAllowlist,runtime:o.simulatorRuntimeId,boot:o.boot,reuseExisting:o.reuseExisting,activity:o.activity,relaunch:o.relaunch,shutdown:o.shutdown,saveScript:o.saveScript,noRecord:o.noRecord,metroHost:o.metroHost,metroPort:o.metroPort,bundleUrl:o.bundleUrl,launchUrl:o.launchUrl,snapshotInteractiveOnly:o.interactiveOnly,snapshotCompact:o.compact,snapshotDepth:o.depth,snapshotScope:o.scope,snapshotRaw:o.raw,verbose:o.debug}),runtime:o.runtime,meta:eN({requestId:o.requestId,cwd:o.cwd,debug:o.debug,lockPolicy:o.lockPolicy,lockPlatform:o.lockPlatform,tenantId:o.tenant,runId:o.runId,leaseId:o.leaseId,sessionIsolation:o.sessionIsolation,installSource:o.installSource,retainMaterializedPaths:o.retainMaterializedPaths,materializedPathRetentionMs:o.materializedPathRetentionMs,materializationId:o.materializationId})});if(!i.ok)throw new m(i.error.code,i.error.message,{...i.error.details??{},hint:i.error.hint,diagnosticId:i.error.diagnosticId,logPath:i.error.logPath});return i.data??{}},n=async(e={})=>{let t=await a("session_list",[],e);return(Array.isArray(t.sessions)?t.sessions:[]).map(eM)};return{devices:{list:async(e={})=>{let t=await a("devices",[],e);return(Array.isArray(t.devices)?t.devices:[]).map(eS)}},sessions:{list:async(e={})=>await n(e),close:async(t={})=>{let r=ek(e.session,t.session),n=(await a("close",[],t)).shutdown;return{session:r,shutdown:"object"==typeof n&&null!==n?n:void 0,identifiers:{session:r}}}},simulators:{ensure:async e=>{let{runtime:t,...r}=e,n=await a("ensure-simulator",[],{...r,simulatorRuntimeId:t}),o=eU(n,"udid"),i=eU(n,"device");return{udid:o,device:i,runtime:eU(n,"runtime"),created:!0===n.created,booted:!0===n.booted,iosSimulatorDeviceSet:e$(n,"ios_simulator_device_set",eF),identifiers:{deviceId:o,deviceName:i,udid:o}}}},apps:{install:async t=>eE(await a("install",[t.app,t.appPath],t),ek(e.session,t.session)),reinstall:async t=>eE(await a("reinstall",[t.app,t.appPath],t),ek(e.session,t.session)),installFromSource:async t=>(function(e,t){let r=eO(e,"bundleId"),a=eO(e,"packageName"),n=r??a??eO(e,"appId"),o=eO(e,"launchTarget")??a??r??n;if(!o)throw new m("COMMAND_FAILED",'Daemon response is missing "launchTarget".',{response:e});return{appName:eO(e,"appName"),appId:n,bundleId:r,packageName:a,launchTarget:o,installablePath:eO(e,"installablePath"),archivePath:eO(e,"archivePath"),materializationId:eO(e,"materializationId"),materializationExpiresAt:eO(e,"materializationExpiresAt"),identifiers:ef({session:t,bundleId:r,packageName:a,appId:n})}})(await a("install_source",[],{...t,installSource:t.source,retainMaterializedPaths:t.retainPaths,materializedPathRetentionMs:t.retentionMs}),ek(e.session,t.session)),open:async t=>{let r=ek(e.session,t.session),n=t.url?[t.app,t.url]:[t.app],o=await a("open",n,t),i=function(e){let t=e.platform,r=eO(e,"id"),a=eO(e,"device");if("ios"!==t&&"android"!==t||!r||!a)return;let n=eC(e,"target"),o=eI(t,r,a);return{platform:t,target:n,id:r,name:a,identifiers:o,ios:"ios"===t?{udid:eO(e,"device_udid")??r,simulatorSetPath:e$(e,"ios_simulator_device_set",eF)}:void 0,android:"android"===t?{serial:eO(e,"serial")??r}:void 0}}(o),s=eO(o,"appBundleId");return{session:r,appName:eO(o,"appName"),appBundleId:s,appId:s,startup:function(e){if(eR(e)&&"number"==typeof e.durationMs&&"string"==typeof e.measuredAt&&"string"==typeof e.method)return{durationMs:e.durationMs,measuredAt:e.measuredAt,method:e.method,appTarget:eO(e,"appTarget"),appBundleId:eO(e,"appBundleId")}}(o.startup),runtime:function(e){if(!eR(e))return;let t=e.platform,r=eO(e,"metroHost"),a="number"==typeof e.metroPort?e.metroPort:void 0;return{platform:"ios"===t||"android"===t?t:void 0,metroHost:r,metroPort:a,bundleUrl:eO(e,"bundleUrl"),launchUrl:eO(e,"launchUrl")}}(o.runtime),device:i,identifiers:{session:r,deviceId:i?.id,deviceName:i?.name,udid:i?.ios?.udid,serial:i?.android?.serial,appId:s,appBundleId:s}}},close:async(t={})=>{let r=ek(e.session,t.session),n=(await a("close",t.app?[t.app]:[],t)).shutdown;return{session:r,closedApp:t.app,shutdown:"object"==typeof n&&null!==n?n:void 0,identifiers:{session:r}}}},materializations:{release:async e=>{var t;return{released:!0===(t=await a("release_materialized_paths",[],{...e,materializationId:e.materializationId})).released,materializationId:eU(t,"materializationId"),identifiers:{}}}},metro:{prepare:async t=>await eh({projectRoot:t.projectRoot??e.cwd,kind:t.kind,publicBaseUrl:t.publicBaseUrl,proxyBaseUrl:t.proxyBaseUrl,proxyBearerToken:t.bearerToken,metroPort:t.port,listenHost:t.listenHost,statusHost:t.statusHost,startupTimeoutMs:t.startupTimeoutMs,probeTimeoutMs:t.probeTimeoutMs,reuseExisting:t.reuseExisting,installDependenciesIfNeeded:t.installDependenciesIfNeeded,runtimeFilePath:t.runtimeFilePath,logPath:t.logPath})},capture:{snapshot:async(t={})=>{var r;let n=ek(e.session,t.session),o=await a("snapshot",[],t),i=eO(o,"appBundleId");return{nodes:Array.isArray(r=o.nodes)?r:[],truncated:!0===o.truncated,appName:eO(o,"appName"),appBundleId:i,identifiers:{session:n,appId:i,appBundleId:i}}},screenshot:async(t={})=>{let r=ek(e.session,t.session);return{path:eU(await a("screenshot",t.path?[t.path]:[],t),"path"),identifiers:{session:r}}}}}}export{eG as createAgentDeviceClient,P as sendToDaemon,e_ as serializeCloseResult,ev as serializeDeployResult,eb as serializeDevice,ey as serializeEnsureSimulatorResult,eA as serializeInstallFromSourceResult,eD as serializeOpenResult,ew as serializeSessionListEntry,eP as serializeSnapshotResult};
`)}),i=setTimeout(()=>{o.destroy();let r=K(),a=W(e,f(t.flags?.stateDir??process.env.AGENT_DEVICE_STATE_DIR));c({level:"error",phase:"daemon_request_timeout",data:{timeoutMs:v,requestId:t.meta?.requestId,command:t.command,timedOutRunnerPidsTerminated:r.terminated,timedOutRunnerCleanupError:r.error,daemonPidReset:e.pid,daemonPidForceKilled:a.forcedKill}}),n(new m("COMMAND_FAILED","Daemon request timed out",{timeoutMs:v,requestId:t.meta?.requestId,hint:"Retry with --debug and check daemon diagnostics logs. Timed-out iOS runner xcodebuild processes were terminated when detected."}))},v),s="";o.setEncoding("utf8"),o.on("data",e=>{let r=(s+=e).indexOf("\n");if(-1===r)return;let l=s.slice(0,r).trim();if(l)try{let e=JSON.parse(l);o.end(),clearTimeout(i),a(e)}catch(e){clearTimeout(i),n(new m("COMMAND_FAILED","Invalid daemon response",{requestId:t.meta?.requestId,line:l},e instanceof Error?e:void 0))}}),o.on("error",e=>{clearTimeout(i),c({level:"error",phase:"daemon_request_socket_error",data:{requestId:t.meta?.requestId,message:e instanceof Error?e.message:String(e)}}),n(new m("COMMAND_FAILED","Failed to communicate with daemon",{requestId:t.meta?.requestId,hint:"Retry command. If this persists, clean stale daemon metadata and start a fresh session."},e))})})}async function J(t,r){let a=t.baseUrl?new URL(Y(t.baseUrl,"rpc")):t.httpPort?new URL(`http://127.0.0.1:${t.httpPort}/rpc`):null;if(!a)throw new m("COMMAND_FAILED","Daemon HTTP endpoint is unavailable");let n=JSON.stringify({jsonrpc:"2.0",id:r.meta?.requestId??e(),method:"agent_device.command",params:r}),o={"content-type":"application/json","content-length":Buffer.byteLength(n)};return t.baseUrl&&t.token&&(o.authorization=`Bearer ${t.token}`,o["x-agent-device-token"]=t.token),await new Promise((e,s)=>{let l=f(r.flags?.stateDir??process.env.AGENT_DEVICE_STATE_DIR),d=("https:"===a.protocol?i:b).request({protocol:a.protocol,host:a.hostname,port:a.port,method:"POST",path:a.pathname+a.search,headers:o},a=>{let n="";a.setEncoding("utf8"),a.on("data",e=>{n+=e}),a.on("end",()=>{clearTimeout(u);try{let a=JSON.parse(n);if(a.error){let e=a.error.data??{};s(new m(String(e.code??"COMMAND_FAILED"),String(e.message??a.error.message??"Daemon RPC request failed"),{..."object"==typeof e.details&&e.details?e.details:{},hint:"string"==typeof e.hint?e.hint:void 0,diagnosticId:"string"==typeof e.diagnosticId?e.diagnosticId:void 0,logPath:"string"==typeof e.logPath?e.logPath:void 0,requestId:r.meta?.requestId}));return}if(!a.result||"object"!=typeof a.result)return void s(new m("COMMAND_FAILED","Invalid daemon RPC response",{requestId:r.meta?.requestId}));if(t.baseUrl&&a.result.ok)return void Q(t,r,a.result).then(e).catch(s);e(a.result)}catch(e){clearTimeout(u),s(new m("COMMAND_FAILED","Invalid daemon response",{requestId:r.meta?.requestId,line:n},e instanceof Error?e:void 0))}})}),u=setTimeout(()=>{d.destroy();let e=X(t)?{terminated:0}:K(),a=X(t)?{forcedKill:!1}:W(t,l);c({level:"error",phase:"daemon_request_timeout",data:{timeoutMs:v,requestId:r.meta?.requestId,command:r.command,timedOutRunnerPidsTerminated:e.terminated,timedOutRunnerCleanupError:e.error,daemonPidReset:X(t)?void 0:t.pid,daemonPidForceKilled:X(t)?void 0:a.forcedKill,daemonBaseUrl:t.baseUrl}}),s(new m("COMMAND_FAILED","Daemon request timed out",{timeoutMs:v,requestId:r.meta?.requestId,hint:X(t)?"Retry with --debug and verify the remote daemon URL, auth token, and remote host logs.":"Retry with --debug and check daemon diagnostics logs. Timed-out iOS runner xcodebuild processes were terminated when detected."}))},v);d.on("error",e=>{clearTimeout(u),c({level:"error",phase:"daemon_request_socket_error",data:{requestId:r.meta?.requestId,message:e instanceof Error?e.message:String(e)}}),s(new m("COMMAND_FAILED","Failed to communicate with daemon",{requestId:r.meta?.requestId,hint:X(t)?"Retry command. If this persists, verify the remote daemon URL, auth token, and remote host reachability.":"Retry command. If this persists, clean stale daemon metadata and start a fresh session."},e))}),d.write(n),d.end()})}function K(){let e=0;try{for(let t of _){let r=s("pkill",["-f",t],{allowFailure:!0});0===r.exitCode&&(e+=1)}return{terminated:e}}catch(t){return{terminated:e,error:t instanceof Error?t.message:String(t)}}}function W(e,t){let a=!1;try{r(e.pid,e.processStartTime)&&(process.kill(e.pid,"SIGKILL"),a=!0)}catch{w(e.pid,{termTimeoutMs:3e3,killTimeoutMs:1e3,expectedStartTime:e.processStartTime})}finally{F(t.infoPath),F(t.lockPath)}return{forcedKill:a}}function X(e){return"string"==typeof e.baseUrl&&e.baseUrl.length>0}function Y(e,t){return new URL(t,e.endsWith("/")?e:`${e}/`).toString()}async function Q(e,r,a){let n=Array.isArray(a.data?.artifacts)?a.data.artifacts:[];if(0===n.length||!e.baseUrl)return a;let o=a.data?{...a.data}:{},i=[];for(let a of n){if(!a||"object"!=typeof a||"string"!=typeof a.artifactId){i.push(a);continue}let n=function(e,r){if(e.localPath&&e.localPath.trim().length>0)return e.localPath;let a=e.fileName?.trim()||`${e.field}-${Date.now()}`;return t.resolve(r.meta?.cwd??process.cwd(),a)}(a,r);await Z({baseUrl:e.baseUrl,token:e.token,artifactId:a.artifactId,destinationPath:n,requestId:r.meta?.requestId}),o[a.field]=n,i.push({...a,localPath:n})}return o.artifacts=i,{ok:!0,data:o}}async function Z(e){var r,a;let n,o=new URL((r=e.baseUrl,a=e.artifactId,n=r.endsWith("/")?r:`${r}/`,new URL(`upload/${encodeURIComponent(a)}`,n).toString())),s="https:"===o.protocol?i:b;await h.promises.mkdir(t.dirname(e.destinationPath),{recursive:!0}),await new Promise((t,r)=>{let a=!1,n=e.timeoutMs??v,i=n=>{if(!a){if(a=!0,clearTimeout(d),n)return void h.promises.rm(e.destinationPath,{force:!0}).finally(()=>r(n));t()}},l=s.request({protocol:o.protocol,host:o.hostname,port:o.port,method:"GET",path:o.pathname+o.search,headers:e.token?{authorization:`Bearer ${e.token}`,"x-agent-device-token":e.token}:void 0},t=>{if((t.statusCode??500)>=400){let r="";t.setEncoding("utf8"),t.on("data",e=>{r+=e}),t.on("end",()=>{i(new m("COMMAND_FAILED","Failed to download remote artifact",{artifactId:e.artifactId,statusCode:t.statusCode,requestId:e.requestId,body:r}))});return}let r=h.createWriteStream(e.destinationPath);r.on("error",e=>{i(e instanceof Error?e:Error(String(e)))}),t.on("error",e=>{i(e instanceof Error?e:Error(String(e)))}),t.on("aborted",()=>{i(new m("COMMAND_FAILED","Remote artifact download was interrupted",{artifactId:e.artifactId,requestId:e.requestId}))}),r.on("finish",()=>{r.close(()=>i())}),t.pipe(r)}),d=setTimeout(()=>{l.destroy(new m("COMMAND_FAILED","Remote artifact download timed out",{artifactId:e.artifactId,requestId:e.requestId,timeoutMs:n}))},n);l.on("error",t=>{t instanceof m?i(t):i(new m("COMMAND_FAILED","Failed to download remote artifact",{artifactId:e.artifactId,requestId:e.requestId,timeoutMs:n},t instanceof Error?t:void 0))}),l.end()})}function ee(e){return e.replace(/\/+$/,"")}function et(e){return"string"==typeof e&&e.trim()?ee(e.trim()):""}function er(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function ea(e,t,r){return u(e,{env:t,cwd:r})}function en(e){try{return h.accessSync(e,h.constants.F_OK),!0}catch{return!1}}function eo(e,t,r){if(null==e||""===e)return t;let a=Number.parseInt(String(e),10);return Number.isInteger(a)?Math.max(a,r):t}function ei(e,t){let r;return{platform:t,bundleUrl:((r=new URL(`${ee(e)}/index.bundle`)).searchParams.set("platform",t),r.searchParams.set("dev","true"),r.searchParams.set("minify","false"),r.toString())}}function es(e,t){return{platform:t,metroHost:er(e?.metro_host),metroPort:e?.metro_port,bundleUrl:er(e?.metro_bundle_url),launchUrl:er(e?.launch_url)}}function el(e){return`'${e.replace(/'/g,"'\"'\"'")}'`}async function ed(e){await new Promise(t=>setTimeout(t,e))}async function eu(e,t,r={}){try{let a=await fetch(e,{headers:r,signal:AbortSignal.timeout(t)});return{ok:a.ok,status:a.status,body:await a.text()}}catch(r){if(r instanceof Error&&"TimeoutError"===r.name)throw Error(`Timed out fetching ${e} after ${t}ms`);throw r}}async function ec(e,t){try{let r=await eu(e,t);return r.ok&&r.body.includes("packager-status:running")}catch{return!1}}async function ep(e){var t,r,a;let n;try{n=await fetch(`${e.baseUrl}/api/metro/bridge`,{method:"POST",headers:(t=e.baseUrl,r=e.bearerToken,{Authorization:`Bearer ${r}`,"Content-Type":"application/json",...t.includes("ngrok")?{"ngrok-skip-browser-warning":"1"}:{}}),body:JSON.stringify({ios_runtime:e.runtime,timeout_ms:e.timeoutMs}),signal:AbortSignal.timeout(e.timeoutMs)})}catch(t){if(t instanceof Error&&"TimeoutError"===t.name)throw Error(`/api/metro/bridge timed out after ${e.timeoutMs}ms calling ${e.baseUrl}/api/metro/bridge`);throw t}let o=await n.text(),i=o?JSON.parse(o):{};if(!n.ok)throw Error(`/api/metro/bridge failed (${n.status}): ${JSON.stringify(i)}`);return{enabled:(a=i.data??i).enabled,baseUrl:a.base_url,statusUrl:a.status_url,bundleUrl:a.bundle_url,iosRuntime:es(a.ios_runtime,"ios"),androidRuntime:es(a.android_runtime,"android"),upstream:{bundleUrl:a.upstream.bundle_url,host:a.upstream.host,port:a.upstream.port,statusUrl:a.upstream.status_url},probe:{reachable:a.probe.reachable,statusCode:a.probe.status_code,latencyMs:a.probe.latency_ms,detail:a.probe.detail}}}async function em(e,t,r){let a=Date.now()+t;for(;Date.now()<a;){let t=Math.min(r,Math.max(a-Date.now(),1));if(await ec(e,t))return!0;let n=Math.min(500,Math.max(a-Date.now(),0));n>0&&await ed(n)}return!1}async function eh(e={}){let r=e.env??process.env,a=process.cwd(),n=ea(e.projectRoot??a,r,a),o=function(e,r){if("auto"!==r)return r;let a=function(e){let r=t.join(e,"package.json");if(!en(r))throw new m("INVALID_ARGS",`package.json not found at ${r}`);return JSON.parse(h.readFileSync(r,"utf8"))}(e);return"string"==typeof({...a.dependencies??{},...a.devDependencies??{}}).expo?"expo":"react-native"}(n,e.kind??"auto"),i=function(e,t){if(null==e||""===e)return 8081;let r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<1||r>65535)throw new m("INVALID_ARGS",`Invalid Metro port: ${String(e)}. Use 1-65535.`);return r}(e.metroPort??8081,0),l=er(e.listenHost)??"0.0.0.0",d=er(e.statusHost)??"127.0.0.1",u=et(e.publicBaseUrl),c=eo(e.startupTimeoutMs,18e4,3e4),p=eo(e.probeTimeoutMs,1e4,1e3),f=e.reuseExisting??!0,I=e.installDependenciesIfNeeded??!0,g=e.runtimeFilePath?ea(e.runtimeFilePath,r,a):null,w=ea(e.logPath??t.join(n,".agent-device","metro.log"),r,a);if(!u)throw new m("INVALID_ARGS","metro prepare requires --public-base-url <url>.");let{proxyEnabled:b,proxyBaseUrl:y,proxyBearerToken:v}=function(e,t){if(e&&!t)throw new m("INVALID_ARGS","metro prepare requires proxy auth when --proxy-base-url is provided. Pass --bearer-token or set AGENT_DEVICE_PROXY_TOKEN.");if(!e&&t)throw new m("INVALID_ARGS","metro prepare requires --proxy-base-url when proxy auth is provided.");return{proxyEnabled:!!(e&&t),proxyBaseUrl:e,proxyBearerToken:t}}(et(e.proxyBaseUrl),er(e.proxyBearerToken)??""),A=I?function(e,r){if(function(e){try{return h.statSync(e).isDirectory()}catch{return!1}}(t.join(e,"node_modules")))return{installed:!1};let a=en(t.join(e,"pnpm-lock.yaml"))?{command:"pnpm",installArgs:["install"]}:en(t.join(e,"yarn.lock"))?{command:"yarn",installArgs:["install"]}:{command:"npm",installArgs:["install"]};return s(a.command,a.installArgs,{cwd:e,env:r}),{installed:!0,packageManager:a.command}}(n,r):{installed:!1},D=`http://${d}:${i}/status`,_=!1,P=!1,E=0;if(f&&await ec(D,p))P=!0;else if(_=!0,E=function(e,r,a,n,o,i){let l="expo"===r?{command:"npx",installArgs:["expo","start","--host","lan","--port",String(a)]}:{command:"npx",installArgs:["react-native","start","--host",n,"--port",String(a)]};h.mkdirSync(t.dirname(o),{recursive:!0});let d=[el(l.command),...l.installArgs.map(el)].join(" "),u=s("/bin/sh",["-c",`nohup ${d} >> ${el(o)} 2>&1 < /dev/null & echo $!`],{cwd:e,env:i}),c=Number.parseInt(u.stdout.trim(),10);if(!Number.isInteger(c)||c<=0)throw Error(`Failed to start Metro. Expected a child PID in stdout, got "${u.stdout.trim()}".`);return{pid:c}}(n,o,i,l,w,r).pid,!await em(D,c,p))throw Error(`Metro did not become ready at ${D} within ${c}ms. Check ${w}.`);let S=ei(u,"ios"),M=ei(u,"android"),k=null,N=null;if(b)try{k=await ep({baseUrl:y,bearerToken:v,runtime:{metro_bundle_url:S.bundleUrl},timeoutMs:p})}catch(e){N=e instanceof Error?e.message:String(e)}if(b&&(!k||!1===k.probe.reachable)){var T,R;let e;throw Error((T=N,R=k,e=[`Metro bridge is required for this run but could not be configured via ${y}/api/metro/bridge.`],T&&e.push(`bridgeError=${T}`),R?.probe.reachable===!1&&e.push(`bridgeProbe=${R.probe.detail||`unreachable (status ${R.probe.statusCode||0})`}`),e.join(" ")))}let U=k?.iosRuntime??S,O=k?.androidRuntime??M,x={projectRoot:n,kind:o,dependenciesInstalled:A.installed,packageManager:A.packageManager??null,started:_,reused:P,pid:E,logPath:w,statusUrl:D,runtimeFilePath:g,iosRuntime:U,androidRuntime:O,bridge:k};return g&&(h.mkdirSync(t.dirname(g),{recursive:!0}),h.writeFileSync(g,JSON.stringify(x,null,2))),x}function ef(e){let t=e.appId??e.bundleId??e.packageName;return{session:e.session,appId:t,appBundleId:e.bundleId,package:e.packageName}}function eI(e,t,r){return{deviceId:t,deviceName:r,..."android"===e?{serial:t}:"ios"===e?{udid:t}:{}}}function eg(e,t={}){let r=t.includeAndroidSerial??!0;return{platform:e.platform,target:e.target,device:e.name,id:e.id,..."ios"===e.platform?{device_udid:e.ios?.udid??e.id,ios_simulator_device_set:e.ios?.simulatorSetPath??null}:{},..."android"===e.platform&&r?{serial:e.android?.serial??e.id}:{}}}function ew(e){return{name:e.name,...eg(e.device,{includeAndroidSerial:!1}),createdAt:e.createdAt}}function eb(e){return{platform:e.platform,id:e.id,name:e.name,kind:e.kind,target:e.target,..."boolean"==typeof e.booted?{booted:e.booted}:{}}}function ey(e){return{udid:e.udid,device:e.device,runtime:e.runtime,ios_simulator_device_set:e.iosSimulatorDeviceSet??null,created:e.created,booted:e.booted}}function ev(e){return{app:e.app,appPath:e.appPath,platform:e.platform,...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.package?{package:e.package}:{}}}function eA(e){return{launchTarget:e.launchTarget,...e.appName?{appName:e.appName}:{},...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.packageName?{package:e.packageName}:{},...e.installablePath?{installablePath:e.installablePath}:{},...e.archivePath?{archivePath:e.archivePath}:{},...e.materializationId?{materializationId:e.materializationId}:{},...e.materializationExpiresAt?{materializationExpiresAt:e.materializationExpiresAt}:{}}}function eD(e){return{session:e.session,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{},...e.startup?{startup:e.startup}:{},...e.runtime?{runtime:e.runtime}:{},...e.device?eg(e.device):{}}}function e_(e){return{session:e.session,...e.shutdown?{shutdown:e.shutdown}:{}}}function eP(e){return{nodes:e.nodes,truncated:e.truncated,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{}}}function eE(e,t){let r=eO(e,"bundleId"),a=eO(e,"package");return{app:eU(e,"app"),appPath:eU(e,"appPath"),platform:ex(e,"platform"),appId:r??a,bundleId:r,package:a,identifiers:ef({session:t,bundleId:r,packageName:a})}}function eS(e){var t;let r=eT(e),a=ex(r,"platform"),n=eU(r,"id"),o=eU(r,"name");return{platform:a,target:eC(r,"target"),kind:eL(r,t="kind",ez,`Daemon response has invalid "${t}".`),id:n,name:o,booted:"boolean"==typeof r.booted?r.booted:void 0,identifiers:eI(a,n,o),ios:"ios"===a?{udid:n}:void 0,android:"android"===a?{serial:n}:void 0}}function eM(e){var t;let r=eT(e),a=ex(r,"platform"),n=eU(r,"id"),o=eU(r,"name"),i=eC(r,"target"),s=eU(r,"device"),l={session:o,...eI(a,n,s)};return{name:o,createdAt:eL(r,t="createdAt",eq,`Daemon response is missing numeric "${t}".`),device:{platform:a,target:i,id:n,name:s,identifiers:l,ios:"ios"===a?{udid:n,simulatorSetPath:e$(r,"ios_simulator_device_set",eF)}:void 0,android:"android"===a?{serial:n}:void 0},identifiers:l}}function ek(e,t){return t??e??"default"}function eN(e){let t={};for(let[r,a]of Object.entries(e))void 0!==a&&(t[r]=a);return t}function eT(e){if(!eR(e))throw new m("COMMAND_FAILED","Daemon returned an unexpected response shape.",{value:e});return e}function eR(e){return"object"==typeof e&&null!==e}function eU(e,t){return eL(e,t,eF,`Daemon response is missing "${t}".`)}function eO(e,t){return eF(e[t])}function ex(e,t){return eL(e,t,eB,`Daemon response has invalid "${t}".`)}function eC(e,t){return ej(e[t])??"mobile"}function eL(e,t,r,a){let n=r(e[t]);if(void 0===n)throw new m("COMMAND_FAILED",a,{response:e});return n}function e$(e,t,r){let a=e[t];return null===a?null:r(a)}function eF(e){return"string"==typeof e&&e.length>0?e:void 0}function eq(e){return"number"==typeof e&&Number.isFinite(e)?e:void 0}function eB(e){return"ios"===e||"macos"===e||"android"===e?e:void 0}function ez(e){return"simulator"===e||"emulator"===e||"device"===e?e:void 0}function ej(e){return"tv"===e||"mobile"===e||"desktop"===e?e:void 0}function eG(e={},t={}){let r=t.transport??P,a=async(t,a=[],n={})=>{let o={...e,...n},i=await r({session:ek(e.session,n.session),command:t,positionals:a,flags:eN({stateDir:o.stateDir,daemonBaseUrl:o.daemonBaseUrl,daemonAuthToken:o.daemonAuthToken,daemonTransport:o.daemonTransport,daemonServerMode:o.daemonServerMode,tenant:o.tenant,sessionIsolation:o.sessionIsolation,runId:o.runId,leaseId:o.leaseId,platform:o.platform,target:o.target,device:o.device,udid:o.udid,serial:o.serial,iosSimulatorDeviceSet:o.iosSimulatorDeviceSet,androidDeviceAllowlist:o.androidDeviceAllowlist,runtime:o.simulatorRuntimeId,boot:o.boot,reuseExisting:o.reuseExisting,activity:o.activity,relaunch:o.relaunch,shutdown:o.shutdown,saveScript:o.saveScript,noRecord:o.noRecord,metroHost:o.metroHost,metroPort:o.metroPort,bundleUrl:o.bundleUrl,launchUrl:o.launchUrl,snapshotInteractiveOnly:o.interactiveOnly,snapshotCompact:o.compact,snapshotDepth:o.depth,snapshotScope:o.scope,snapshotRaw:o.raw,verbose:o.debug}),runtime:o.runtime,meta:eN({requestId:o.requestId,cwd:o.cwd,debug:o.debug,lockPolicy:o.lockPolicy,lockPlatform:o.lockPlatform,tenantId:o.tenant,runId:o.runId,leaseId:o.leaseId,sessionIsolation:o.sessionIsolation,installSource:o.installSource,retainMaterializedPaths:o.retainMaterializedPaths,materializedPathRetentionMs:o.materializedPathRetentionMs,materializationId:o.materializationId})});if(!i.ok)throw new m(i.error.code,i.error.message,{...i.error.details??{},hint:i.error.hint,diagnosticId:i.error.diagnosticId,logPath:i.error.logPath});return i.data??{}},n=async(e={})=>{let t=await a("session_list",[],e);return(Array.isArray(t.sessions)?t.sessions:[]).map(eM)};return{devices:{list:async(e={})=>{let t=await a("devices",[],e);return(Array.isArray(t.devices)?t.devices:[]).map(eS)}},sessions:{list:async(e={})=>await n(e),close:async(t={})=>{let r=ek(e.session,t.session),n=(await a("close",[],t)).shutdown;return{session:r,shutdown:"object"==typeof n&&null!==n?n:void 0,identifiers:{session:r}}}},simulators:{ensure:async e=>{let{runtime:t,...r}=e,n=await a("ensure-simulator",[],{...r,simulatorRuntimeId:t}),o=eU(n,"udid"),i=eU(n,"device");return{udid:o,device:i,runtime:eU(n,"runtime"),created:!0===n.created,booted:!0===n.booted,iosSimulatorDeviceSet:e$(n,"ios_simulator_device_set",eF),identifiers:{deviceId:o,deviceName:i,udid:o}}}},apps:{install:async t=>eE(await a("install",[t.app,t.appPath],t),ek(e.session,t.session)),reinstall:async t=>eE(await a("reinstall",[t.app,t.appPath],t),ek(e.session,t.session)),installFromSource:async t=>(function(e,t){let r=eO(e,"bundleId"),a=eO(e,"packageName"),n=r??a??eO(e,"appId"),o=eO(e,"launchTarget")??a??r??n;if(!o)throw new m("COMMAND_FAILED",'Daemon response is missing "launchTarget".',{response:e});return{appName:eO(e,"appName"),appId:n,bundleId:r,packageName:a,launchTarget:o,installablePath:eO(e,"installablePath"),archivePath:eO(e,"archivePath"),materializationId:eO(e,"materializationId"),materializationExpiresAt:eO(e,"materializationExpiresAt"),identifiers:ef({session:t,bundleId:r,packageName:a,appId:n})}})(await a("install_source",[],{...t,installSource:t.source,retainMaterializedPaths:t.retainPaths,materializedPathRetentionMs:t.retentionMs}),ek(e.session,t.session)),open:async t=>{let r=ek(e.session,t.session),n=t.url?[t.app,t.url]:[t.app],o=await a("open",n,t),i=function(e){let t=e.platform,r=eO(e,"id"),a=eO(e,"device");if("ios"!==t&&"macos"!==t&&"android"!==t||!r||!a)return;let n=eC(e,"target"),o=eI(t,r,a);return{platform:t,target:n,id:r,name:a,identifiers:o,ios:"ios"===t?{udid:eO(e,"device_udid")??r,simulatorSetPath:e$(e,"ios_simulator_device_set",eF)}:void 0,android:"android"===t?{serial:eO(e,"serial")??r}:void 0}}(o),s=eO(o,"appBundleId");return{session:r,appName:eO(o,"appName"),appBundleId:s,appId:s,startup:function(e){if(eR(e)&&"number"==typeof e.durationMs&&"string"==typeof e.measuredAt&&"string"==typeof e.method)return{durationMs:e.durationMs,measuredAt:e.measuredAt,method:e.method,appTarget:eO(e,"appTarget"),appBundleId:eO(e,"appBundleId")}}(o.startup),runtime:function(e){if(!eR(e))return;let t=e.platform,r=eO(e,"metroHost"),a="number"==typeof e.metroPort?e.metroPort:void 0;return{platform:"ios"===t||"android"===t?t:void 0,metroHost:r,metroPort:a,bundleUrl:eO(e,"bundleUrl"),launchUrl:eO(e,"launchUrl")}}(o.runtime),device:i,identifiers:{session:r,deviceId:i?.id,deviceName:i?.name,udid:i?.ios?.udid,serial:i?.android?.serial,appId:s,appBundleId:s}}},close:async(t={})=>{let r=ek(e.session,t.session),n=(await a("close",t.app?[t.app]:[],t)).shutdown;return{session:r,closedApp:t.app,shutdown:"object"==typeof n&&null!==n?n:void 0,identifiers:{session:r}}}},materializations:{release:async e=>{var t;return{released:!0===(t=await a("release_materialized_paths",[],{...e,materializationId:e.materializationId})).released,materializationId:eU(t,"materializationId"),identifiers:{}}}},metro:{prepare:async t=>await eh({projectRoot:t.projectRoot??e.cwd,kind:t.kind,publicBaseUrl:t.publicBaseUrl,proxyBaseUrl:t.proxyBaseUrl,proxyBearerToken:t.bearerToken,metroPort:t.port,listenHost:t.listenHost,statusHost:t.statusHost,startupTimeoutMs:t.startupTimeoutMs,probeTimeoutMs:t.probeTimeoutMs,reuseExisting:t.reuseExisting,installDependenciesIfNeeded:t.installDependenciesIfNeeded,runtimeFilePath:t.runtimeFilePath,logPath:t.logPath})},capture:{snapshot:async(t={})=>{var r;let n=ek(e.session,t.session),o=await a("snapshot",[],t),i=eO(o,"appBundleId");return{nodes:Array.isArray(r=o.nodes)?r:[],truncated:!0===o.truncated,appName:eO(o,"appName"),appBundleId:i,identifiers:{session:n,appId:i,appBundleId:i}}},screenshot:async(t={})=>{let r=ek(e.session,t.session);return{path:eU(await a("screenshot",t.path?[t.path]:[],t),"path"),identifiers:{session:r}}}}}}export{eG as createAgentDeviceClient,P as sendToDaemon,e_ as serializeCloseResult,ev as serializeDeployResult,eb as serializeDevice,ey as serializeEnsureSimulatorResult,eA as serializeInstallFromSourceResult,eD as serializeOpenResult,ew as serializeSessionListEntry,eP as serializeSnapshotResult};

@@ -1,5 +0,5 @@

let e,t,s,r,o,a,i;import{PNG as n}from"pngjs";import{formatSnapshotLine as l,SETTINGS_USAGE_OVERRIDE as p,styleText as u,parseBatchStepsJson as c,buildSnapshotDisplayLines as d}from"./274.js";import{createRequestId as m,node_path as g,normalizeError as f,resolveUserPath as h,readVersion as y,getDiagnosticsMeta as w,emitDiagnostic as v,promises as b,asAppError as D,expandUserHomePath as k,pathToFileURL as S,AppError as A,node_fs as $,node_os as I,withDiagnosticsScope as L,flushDiagnosticsToSessionFile as x,resolveDaemonPaths as N}from"./331.js";import{serializeOpenResult as O,sendToDaemon as E,serializeSessionListEntry as R,serializeDeployResult as P,serializeSnapshotResult as _,serializeCloseResult as F,serializeEnsureSimulatorResult as C,serializeInstallFromSourceResult as j,serializeDevice as T,createAgentDeviceClient as V}from"./224.js";let M=["snapshotInteractiveOnly","snapshotCompact","snapshotDepth","snapshotScope","snapshotRaw"],G=["snapshotDepth","snapshotScope","snapshotRaw"],U=[{key:"config",names:["--config"],type:"string",usageLabel:"--config <path>",usageDescription:"Load CLI defaults from a specific config file"},{key:"remoteConfig",names:["--remote-config"],type:"string",usageLabel:"--remote-config <path>",usageDescription:"Load remote host + Metro workflow settings from a specific profile file"},{key:"stateDir",names:["--state-dir"],type:"string",usageLabel:"--state-dir <path>",usageDescription:"Daemon state directory (defaults to ~/.agent-device)"},{key:"daemonBaseUrl",names:["--daemon-base-url"],type:"string",usageLabel:"--daemon-base-url <url>",usageDescription:"Explicit remote HTTP daemon base URL (skip local daemon discovery/startup)"},{key:"daemonAuthToken",names:["--daemon-auth-token"],type:"string",usageLabel:"--daemon-auth-token <token>",usageDescription:"Remote HTTP daemon auth token (sent as request token and bearer header)"},{key:"daemonTransport",names:["--daemon-transport"],type:"enum",enumValues:["auto","socket","http"],usageLabel:"--daemon-transport auto|socket|http",usageDescription:"Daemon client transport preference"},{key:"daemonServerMode",names:["--daemon-server-mode"],type:"enum",enumValues:["socket","http","dual"],usageLabel:"--daemon-server-mode socket|http|dual",usageDescription:"Daemon server mode used when spawning daemon"},{key:"tenant",names:["--tenant"],type:"string",usageLabel:"--tenant <id>",usageDescription:"Tenant scope identifier for isolated daemon sessions"},{key:"sessionIsolation",names:["--session-isolation"],type:"enum",enumValues:["none","tenant"],usageLabel:"--session-isolation none|tenant",usageDescription:"Session isolation strategy (tenant prefixes session namespace)"},{key:"runId",names:["--run-id"],type:"string",usageLabel:"--run-id <id>",usageDescription:"Run identifier used for tenant lease admission checks"},{key:"leaseId",names:["--lease-id"],type:"string",usageLabel:"--lease-id <id>",usageDescription:"Lease identifier bound to tenant/run admission scope"},{key:"sessionLock",names:["--session-lock"],type:"enum",enumValues:["reject","strip"],usageLabel:"--session-lock reject|strip",usageDescription:"Lock bound-session device routing for this CLI invocation and nested batch steps"},{key:"sessionLocked",names:["--session-locked"],type:"boolean",usageLabel:"--session-locked",usageDescription:"Deprecated alias for --session-lock reject"},{key:"sessionLockConflicts",names:["--session-lock-conflicts"],type:"enum",enumValues:["reject","strip"],usageLabel:"--session-lock-conflicts reject|strip",usageDescription:"Deprecated alias for --session-lock"},{key:"platform",names:["--platform"],type:"enum",enumValues:["ios","android","apple"],usageLabel:"--platform ios|android|apple",usageDescription:"Platform to target (`apple` aliases the iOS/tvOS backend)"},{key:"target",names:["--target"],type:"enum",enumValues:["mobile","tv"],usageLabel:"--target mobile|tv",usageDescription:"Device target class to match"},{key:"device",names:["--device"],type:"string",usageLabel:"--device <name>",usageDescription:"Device name to target"},{key:"udid",names:["--udid"],type:"string",usageLabel:"--udid <udid>",usageDescription:"iOS device UDID"},{key:"serial",names:["--serial"],type:"string",usageLabel:"--serial <serial>",usageDescription:"Android device serial"},{key:"headless",names:["--headless"],type:"boolean",usageLabel:"--headless",usageDescription:"Boot: launch Android emulator without a GUI window"},{key:"runtime",names:["--runtime"],type:"string",usageLabel:"--runtime <id>",usageDescription:"ensure-simulator: CoreSimulator runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-18-0)"},{key:"metroHost",names:["--metro-host"],type:"string",usageLabel:"--metro-host <host>",usageDescription:"Session-scoped Metro/debug host hint"},{key:"metroPort",names:["--metro-port"],type:"int",min:1,max:65535,usageLabel:"--metro-port <port>",usageDescription:"Session-scoped Metro/debug port hint"},{key:"metroProjectRoot",names:["--project-root"],type:"string",usageLabel:"--project-root <path>",usageDescription:"metro prepare: React Native project root (default: cwd)"},{key:"metroKind",names:["--kind"],type:"enum",enumValues:["auto","react-native","expo"],usageLabel:"--kind auto|react-native|expo",usageDescription:"metro prepare: detect or force the Metro launcher kind"},{key:"metroPublicBaseUrl",names:["--public-base-url"],type:"string",usageLabel:"--public-base-url <url>",usageDescription:"metro prepare: public base URL used to build bundle hints"},{key:"metroProxyBaseUrl",names:["--proxy-base-url"],type:"string",usageLabel:"--proxy-base-url <url>",usageDescription:"metro prepare: optional remote host bridge base URL for Metro access"},{key:"metroBearerToken",names:["--bearer-token"],type:"string",usageLabel:"--bearer-token <token>",usageDescription:"metro prepare: host bridge bearer token (prefer AGENT_DEVICE_PROXY_TOKEN or AGENT_DEVICE_METRO_BEARER_TOKEN)"},{key:"metroPreparePort",names:["--port"],type:"int",min:1,max:65535,usageLabel:"--port <port>",usageDescription:"metro prepare: local Metro port (default: 8081)"},{key:"metroListenHost",names:["--listen-host"],type:"string",usageLabel:"--listen-host <host>",usageDescription:"metro prepare: host Metro listens on (default: 0.0.0.0)"},{key:"metroStatusHost",names:["--status-host"],type:"string",usageLabel:"--status-host <host>",usageDescription:"metro prepare: host used for local /status polling (default: 127.0.0.1)"},{key:"metroStartupTimeoutMs",names:["--startup-timeout-ms"],type:"int",min:1,usageLabel:"--startup-timeout-ms <ms>",usageDescription:"metro prepare: timeout while waiting for Metro to become ready"},{key:"metroProbeTimeoutMs",names:["--probe-timeout-ms"],type:"int",min:1,usageLabel:"--probe-timeout-ms <ms>",usageDescription:"metro prepare: timeout for /status and proxy bridge calls"},{key:"metroRuntimeFile",names:["--runtime-file"],type:"string",usageLabel:"--runtime-file <path>",usageDescription:"metro prepare: optional file path to persist the JSON result"},{key:"metroNoReuseExisting",names:["--no-reuse-existing"],type:"boolean",usageLabel:"--no-reuse-existing",usageDescription:"metro prepare: always start a fresh Metro process"},{key:"metroNoInstallDeps",names:["--no-install-deps"],type:"boolean",usageLabel:"--no-install-deps",usageDescription:"metro prepare: skip package-manager install when node_modules is missing"},{key:"bundleUrl",names:["--bundle-url"],type:"string",usageLabel:"--bundle-url <url>",usageDescription:"Session-scoped bundle URL hint"},{key:"launchUrl",names:["--launch-url"],type:"string",usageLabel:"--launch-url <url>",usageDescription:"Session-scoped deep link / launch URL hint"},{key:"boot",names:["--boot"],type:"boolean",usageLabel:"--boot",usageDescription:"ensure-simulator: boot the simulator after ensuring it exists"},{key:"reuseExisting",names:["--reuse-existing"],type:"boolean",usageLabel:"--reuse-existing",usageDescription:"ensure-simulator: reuse an existing simulator (default: true)"},{key:"iosSimulatorDeviceSet",names:["--ios-simulator-device-set"],type:"string",usageLabel:"--ios-simulator-device-set <path>",usageDescription:"Scope iOS simulator discovery/commands to this simulator device set"},{key:"androidDeviceAllowlist",names:["--android-device-allowlist"],type:"string",usageLabel:"--android-device-allowlist <serials>",usageDescription:"Comma/space separated Android serial allowlist for discovery/selection"},{key:"activity",names:["--activity"],type:"string",usageLabel:"--activity <component>",usageDescription:"Android app launch activity (package/Activity); not for URL opens"},{key:"header",names:["--header"],type:"string",multiple:!0,usageLabel:"--header <name:value>",usageDescription:"install-from-source: repeatable HTTP header for URL downloads"},{key:"session",names:["--session"],type:"string",usageLabel:"--session <name>",usageDescription:"Named session"},{key:"count",names:["--count"],type:"int",min:1,max:200,usageLabel:"--count <n>",usageDescription:"Repeat count for press/swipe series"},{key:"fps",names:["--fps"],type:"int",min:1,max:120,usageLabel:"--fps <n>",usageDescription:"Record: target frames per second (iOS physical device runner)"},{key:"intervalMs",names:["--interval-ms"],type:"int",min:0,max:1e4,usageLabel:"--interval-ms <ms>",usageDescription:"Delay between press iterations"},{key:"holdMs",names:["--hold-ms"],type:"int",min:0,max:1e4,usageLabel:"--hold-ms <ms>",usageDescription:"Press hold duration for each iteration"},{key:"jitterPx",names:["--jitter-px"],type:"int",min:0,max:100,usageLabel:"--jitter-px <n>",usageDescription:"Deterministic coordinate jitter radius for press"},{key:"doubleTap",names:["--double-tap"],type:"boolean",usageLabel:"--double-tap",usageDescription:"Use double-tap gesture per press iteration"},{key:"pauseMs",names:["--pause-ms"],type:"int",min:0,max:1e4,usageLabel:"--pause-ms <ms>",usageDescription:"Delay between swipe iterations"},{key:"pattern",names:["--pattern"],type:"enum",enumValues:["one-way","ping-pong"],usageLabel:"--pattern one-way|ping-pong",usageDescription:"Swipe repeat pattern"},{key:"verbose",names:["--debug","--verbose","-v"],type:"boolean",usageLabel:"--debug, --verbose, -v",usageDescription:"Enable debug diagnostics and stream daemon/runner logs"},{key:"json",names:["--json"],type:"boolean",usageLabel:"--json",usageDescription:"JSON output"},{key:"help",names:["--help","-h"],type:"boolean",usageLabel:"--help, -h",usageDescription:"Print help and exit"},{key:"version",names:["--version","-V"],type:"boolean",usageLabel:"--version, -V",usageDescription:"Print version and exit"},{key:"saveScript",names:["--save-script"],type:"booleanOrString",usageLabel:"--save-script [path]",usageDescription:"Save session script (.ad) on close; optional custom output path"},{key:"shutdown",names:["--shutdown"],type:"boolean",usageLabel:"--shutdown",usageDescription:"close: shutdown associated iOS simulator after ending session"},{key:"relaunch",names:["--relaunch"],type:"boolean",usageLabel:"--relaunch",usageDescription:"open: terminate app process before launching it"},{key:"restart",names:["--restart"],type:"boolean",usageLabel:"--restart",usageDescription:"logs clear: stop active stream, clear logs, then start streaming again"},{key:"retainPaths",names:["--retain-paths"],type:"boolean",usageLabel:"--retain-paths",usageDescription:"install-from-source: keep materialized artifact paths after install"},{key:"retentionMs",names:["--retention-ms"],type:"int",min:1,usageLabel:"--retention-ms <ms>",usageDescription:"install-from-source: retention TTL for materialized artifact paths"},{key:"noRecord",names:["--no-record"],type:"boolean",usageLabel:"--no-record",usageDescription:"Do not record this action"},{key:"replayUpdate",names:["--update","-u"],type:"boolean",usageLabel:"--update, -u",usageDescription:"Replay: update selectors and rewrite replay file in place"},{key:"steps",names:["--steps"],type:"string",usageLabel:"--steps <json>",usageDescription:"Batch: JSON array of steps"},{key:"stepsFile",names:["--steps-file"],type:"string",usageLabel:"--steps-file <path>",usageDescription:"Batch: read steps JSON from file"},{key:"batchOnError",names:["--on-error"],type:"enum",enumValues:["stop"],usageLabel:"--on-error stop",usageDescription:"Batch: stop when a step fails"},{key:"batchMaxSteps",names:["--max-steps"],type:"int",min:1,max:1e3,usageLabel:"--max-steps <n>",usageDescription:"Batch: maximum number of allowed steps"},{key:"appsFilter",names:["--user-installed"],type:"enum",setValue:"user-installed",usageLabel:"--user-installed",usageDescription:"Apps: list user-installed apps"},{key:"appsFilter",names:["--all"],type:"enum",setValue:"all",usageLabel:"--all",usageDescription:"Apps: list all apps (include system/default apps)"},{key:"snapshotInteractiveOnly",names:["-i"],type:"boolean",usageLabel:"-i",usageDescription:"Snapshot: interactive elements only"},{key:"snapshotCompact",names:["-c"],type:"boolean",usageLabel:"-c",usageDescription:"Snapshot: compact output (drop empty structure)"},{key:"snapshotDepth",names:["--depth","-d"],type:"int",min:0,usageLabel:"--depth, -d <depth>",usageDescription:"Snapshot: limit snapshot depth"},{key:"snapshotScope",names:["--scope","-s"],type:"string",usageLabel:"--scope, -s <scope>",usageDescription:"Snapshot: scope snapshot to label/identifier"},{key:"snapshotRaw",names:["--raw"],type:"boolean",usageLabel:"--raw",usageDescription:"Snapshot: raw node output"},{key:"out",names:["--out"],type:"string",usageLabel:"--out <path>",usageDescription:"Output path"},{key:"baseline",names:["--baseline","-b"],type:"string",usageLabel:"--baseline, -b <path>",usageDescription:"Diff screenshot: path to baseline image file"},{key:"threshold",names:["--threshold"],type:"string",usageLabel:"--threshold <0-1>",usageDescription:"Diff screenshot: color distance threshold (default 0.1)"}],B=new Set(["json","config","remoteConfig","stateDir","daemonBaseUrl","daemonAuthToken","daemonTransport","daemonServerMode","tenant","sessionIsolation","runId","leaseId","sessionLock","sessionLocked","sessionLockConflicts","help","version","verbose","platform","target","device","udid","serial","iosSimulatorDeviceSet","androidDeviceAllowlist","session","noRecord"]),H={boot:{helpDescription:"Ensure target device/simulator is booted and ready",summary:"Boot target device/simulator",positionalArgs:[],allowedFlags:["headless"]},open:{helpDescription:"Boot device/simulator; optionally launch app or deep link URL",summary:"Open an app, deep link or URL, save replays",positionalArgs:["appOrUrl?","url?"],allowedFlags:["activity","saveScript","relaunch"]},close:{helpDescription:"Close app or just end session",summary:"Close app or end session",positionalArgs:["app?"],allowedFlags:["saveScript","shutdown"]},reinstall:{helpDescription:"Uninstall + install app from binary path",summary:"Reinstall app from binary path",positionalArgs:["app","path"],allowedFlags:[]},install:{helpDescription:"Install app from binary path without uninstalling first",summary:"Install app from binary path",positionalArgs:["app","path"],allowedFlags:[]},"install-from-source":{helpDescription:"Install app from a URL source through the normal daemon artifact flow",summary:"Install app from a URL source",positionalArgs:["url"],allowedFlags:["header","retainPaths","retentionMs"]},push:{helpDescription:"Simulate push notification payload delivery",summary:"Deliver push payload",positionalArgs:["bundleOrPackage","payloadOrJson"],allowedFlags:[]},snapshot:{helpDescription:"Capture accessibility tree",positionalArgs:[],allowedFlags:[...M]},diff:{usageOverride:"diff snapshot | diff screenshot --baseline <path> [--out <diff.png>] [--threshold <0-1>]",helpDescription:"Diff accessibility snapshot or compare screenshots pixel-by-pixel",summary:"Diff snapshot or screenshot",positionalArgs:["kind"],allowedFlags:[...M,"baseline","threshold","out"]},"ensure-simulator":{helpDescription:"Ensure an iOS simulator exists in a device set (create if missing)",summary:"Ensure iOS simulator exists",positionalArgs:[],allowedFlags:["runtime","boot","reuseExisting"],skipCapabilityCheck:!0},devices:{helpDescription:"List available devices",positionalArgs:[],allowedFlags:[],skipCapabilityCheck:!0},apps:{helpDescription:"List installed apps (includes default/system apps by default)",summary:"List installed apps",positionalArgs:[],allowedFlags:["appsFilter"],defaults:{appsFilter:"all"}},appstate:{helpDescription:"Show foreground app/activity",positionalArgs:[],allowedFlags:[],skipCapabilityCheck:!0},metro:{usageOverride:"metro prepare --public-base-url <url> [--project-root <path>] [--port <port>] [--kind auto|react-native|expo]",listUsageOverride:"metro prepare --public-base-url <url>",helpDescription:"Prepare a local Metro runtime and optionally bridge it through a remote host",summary:"Prepare local Metro runtime",positionalArgs:["prepare"],allowedFlags:["metroProjectRoot","metroKind","metroPublicBaseUrl","metroProxyBaseUrl","metroBearerToken","metroPreparePort","metroListenHost","metroStatusHost","metroStartupTimeoutMs","metroProbeTimeoutMs","metroRuntimeFile","metroNoReuseExisting","metroNoInstallDeps"],skipCapabilityCheck:!0},clipboard:{usageOverride:"clipboard read | clipboard write <text>",listUsageOverride:"clipboard read | clipboard write <text>",helpDescription:"Read or write device clipboard text",positionalArgs:["read|write","text?"],allowsExtraPositionals:!0,allowedFlags:[]},keyboard:{usageOverride:"keyboard [status|get|dismiss]",helpDescription:"Inspect Android keyboard visibility/type or dismiss it",summary:"Inspect or dismiss Android keyboard",positionalArgs:["action?"],allowedFlags:[]},perf:{helpDescription:"Show session performance metrics (startup timing)",summary:"Show startup metrics",positionalArgs:[],allowedFlags:[]},back:{helpDescription:"Navigate back (where supported)",summary:"Go back",positionalArgs:[],allowedFlags:[]},home:{helpDescription:"Go to home screen (where supported)",summary:"Go home",positionalArgs:[],allowedFlags:[]},"app-switcher":{helpDescription:"Open app switcher (where supported)",summary:"Open app switcher",positionalArgs:[],allowedFlags:[]},wait:{usageOverride:"wait <ms>|text <text>|@ref|<selector> [timeoutMs]",helpDescription:"Wait for duration, text, ref, or selector to appear",summary:"Wait for time, text, ref, or selector",positionalArgs:["durationOrSelector","timeoutMs?"],allowsExtraPositionals:!0,allowedFlags:[...G]},alert:{usageOverride:"alert [get|accept|dismiss|wait] [timeout]",helpDescription:"Inspect or handle alert (iOS simulator)",summary:"Inspect or handle iOS alert",positionalArgs:["action?","timeout?"],allowedFlags:[]},click:{usageOverride:"click <x y|@ref|selector>",helpDescription:"Tap/click by coordinates, snapshot ref, or selector",summary:"Tap by coordinates, ref, or selector",positionalArgs:["target"],allowsExtraPositionals:!0,allowedFlags:["count","intervalMs","holdMs","jitterPx","doubleTap",...G]},get:{usageOverride:"get text|attrs <@ref|selector>",helpDescription:"Return element text/attributes by ref or selector",summary:"Get text or attrs by ref or selector",positionalArgs:["subcommand","target"],allowedFlags:[...G]},replay:{helpDescription:"Replay a recorded session",positionalArgs:["path"],allowedFlags:["replayUpdate"],skipCapabilityCheck:!0},batch:{usageOverride:"batch [--steps <json> | --steps-file <path>]",listUsageOverride:"batch --steps <json> | --steps-file <path>",helpDescription:"Execute multiple commands in one daemon request",summary:"Run multiple commands",positionalArgs:[],allowedFlags:["steps","stepsFile","batchOnError","batchMaxSteps","out"],skipCapabilityCheck:!0},press:{usageOverride:"press <x y|@ref|selector>",helpDescription:"Tap/press by coordinates, snapshot ref, or selector (supports repeated series)",summary:"Press by coordinates, ref, or selector",positionalArgs:["targetOrX","y?"],allowsExtraPositionals:!0,allowedFlags:["count","intervalMs","holdMs","jitterPx","doubleTap",...G]},longpress:{helpDescription:"Long press by coordinates (iOS and Android)",summary:"Long press by coordinates",positionalArgs:["x","y","durationMs?"],allowedFlags:[]},swipe:{helpDescription:"Swipe coordinates with optional repeat pattern",summary:"Swipe coordinates",positionalArgs:["x1","y1","x2","y2","durationMs?"],allowedFlags:["count","pauseMs","pattern"]},focus:{helpDescription:"Focus input at coordinates",positionalArgs:["x","y"],allowedFlags:[]},type:{helpDescription:"Type text in focused field",positionalArgs:["text"],allowsExtraPositionals:!0,allowedFlags:[]},fill:{usageOverride:"fill <x> <y> <text> | fill <@ref|selector> <text>",helpDescription:"Tap then type",positionalArgs:["targetOrX","yOrText","text?"],allowsExtraPositionals:!0,allowedFlags:[...G]},scroll:{helpDescription:"Scroll in direction (0-1 amount)",summary:"Scroll in a direction",positionalArgs:["direction","amount?"],allowedFlags:[]},scrollintoview:{usageOverride:"scrollintoview <text|@ref>",helpDescription:"Scroll until text appears or a snapshot ref is brought into view",summary:"Scroll until text or ref is visible",positionalArgs:["target"],allowsExtraPositionals:!0,allowedFlags:[]},pinch:{helpDescription:"Pinch/zoom gesture (iOS simulator)",positionalArgs:["scale","x?","y?"],allowedFlags:[]},screenshot:{helpDescription:"Capture screenshot",positionalArgs:["path?"],allowedFlags:["out"]},"trigger-app-event":{usageOverride:"trigger-app-event <event> [payloadJson]",helpDescription:"Trigger app-defined event hook via deep link template",summary:"Trigger app event hook",positionalArgs:["event","payloadJson?"],allowedFlags:[]},record:{usageOverride:"record start [path] [--fps <n>] | record stop",listUsageOverride:"record start [path] | record stop",helpDescription:"Start/stop screen recording",summary:"Start or stop screen recording",positionalArgs:["start|stop","path?"],allowedFlags:["fps"]},trace:{usageOverride:"trace start [path] | trace stop [path]",listUsageOverride:"trace start [path] | trace stop",helpDescription:"Start/stop trace log capture",summary:"Start or stop trace capture",positionalArgs:["start|stop","path?"],allowedFlags:[],skipCapabilityCheck:!0},logs:{usageOverride:"logs path | logs start | logs stop | logs clear [--restart] | logs doctor | logs mark [message...]",helpDescription:"Session app log info, start/stop streaming, diagnostics, and markers",summary:"Manage session app logs",positionalArgs:["path|start|stop|clear|doctor|mark","message?"],allowsExtraPositionals:!0,allowedFlags:["restart"]},network:{usageOverride:"network dump [limit] [summary|headers|body|all] | network log [limit] [summary|headers|body|all]",helpDescription:"Dump recent HTTP(s) traffic parsed from the session app log",summary:"Show recent HTTP traffic",positionalArgs:["dump|log","limit?","include?"],allowedFlags:[]},find:{usageOverride:"find <locator|text> <action> [value]",helpDescription:"Find by text/label/value/role/id and run action",summary:"Find an element and act",positionalArgs:["query","action","value?"],allowsExtraPositionals:!0,allowedFlags:["snapshotDepth","snapshotRaw"]},is:{helpDescription:"Assert UI state (visible|hidden|exists|editable|selected|text)",summary:"Assert UI state",positionalArgs:["predicate","selector","value?"],allowsExtraPositionals:!0,allowedFlags:[...G]},settings:{usageOverride:p,listUsageOverride:"settings [area] [options]",helpDescription:"Toggle OS settings, appearance, and app permissions (session app scope for permission actions)",summary:"Change OS settings and app permissions",positionalArgs:["setting","state","target?","mode?"],allowedFlags:[]},session:{usageOverride:"session list",helpDescription:"List active sessions",positionalArgs:["list?"],allowedFlags:[],skipCapabilityCheck:!0}},q=new Map,J=new Map;for(let e of U){for(let t of e.names)q.set(t,e);let t=J.get(e.key);t?t.push(e):J.set(e.key,[e])}function K(e){if(e)return H[e]}function W(){return Object.keys(H)}function z(e){let t=e.endsWith("?"),s=t?e.slice(0,-1):e;return t?`[${s}]`:`<${s}>`}let X=(e=`agent-device <command> [args] [--json]
let e,t,s,r,o,a,i;import{PNG as n}from"pngjs";import{formatSnapshotLine as l,SETTINGS_USAGE_OVERRIDE as p,styleText as u,parseBatchStepsJson as c,buildSnapshotDisplayLines as d}from"./274.js";import{createRequestId as m,node_path as g,normalizeError as f,resolveUserPath as h,readVersion as y,getDiagnosticsMeta as w,emitDiagnostic as v,promises as b,asAppError as D,expandUserHomePath as k,pathToFileURL as S,AppError as A,node_fs as $,node_os as I,withDiagnosticsScope as L,flushDiagnosticsToSessionFile as x,resolveDaemonPaths as N}from"./331.js";import{serializeOpenResult as O,sendToDaemon as E,serializeSessionListEntry as R,serializeDeployResult as P,serializeSnapshotResult as _,serializeCloseResult as F,serializeEnsureSimulatorResult as C,serializeInstallFromSourceResult as j,serializeDevice as T,createAgentDeviceClient as V}from"./224.js";let M=["snapshotInteractiveOnly","snapshotCompact","snapshotDepth","snapshotScope","snapshotRaw"],G=["snapshotDepth","snapshotScope","snapshotRaw"],U=[{key:"config",names:["--config"],type:"string",usageLabel:"--config <path>",usageDescription:"Load CLI defaults from a specific config file"},{key:"remoteConfig",names:["--remote-config"],type:"string",usageLabel:"--remote-config <path>",usageDescription:"Load remote host + Metro workflow settings from a specific profile file"},{key:"stateDir",names:["--state-dir"],type:"string",usageLabel:"--state-dir <path>",usageDescription:"Daemon state directory (defaults to ~/.agent-device)"},{key:"daemonBaseUrl",names:["--daemon-base-url"],type:"string",usageLabel:"--daemon-base-url <url>",usageDescription:"Explicit remote HTTP daemon base URL (skip local daemon discovery/startup)"},{key:"daemonAuthToken",names:["--daemon-auth-token"],type:"string",usageLabel:"--daemon-auth-token <token>",usageDescription:"Remote HTTP daemon auth token (sent as request token and bearer header)"},{key:"daemonTransport",names:["--daemon-transport"],type:"enum",enumValues:["auto","socket","http"],usageLabel:"--daemon-transport auto|socket|http",usageDescription:"Daemon client transport preference"},{key:"daemonServerMode",names:["--daemon-server-mode"],type:"enum",enumValues:["socket","http","dual"],usageLabel:"--daemon-server-mode socket|http|dual",usageDescription:"Daemon server mode used when spawning daemon"},{key:"tenant",names:["--tenant"],type:"string",usageLabel:"--tenant <id>",usageDescription:"Tenant scope identifier for isolated daemon sessions"},{key:"sessionIsolation",names:["--session-isolation"],type:"enum",enumValues:["none","tenant"],usageLabel:"--session-isolation none|tenant",usageDescription:"Session isolation strategy (tenant prefixes session namespace)"},{key:"runId",names:["--run-id"],type:"string",usageLabel:"--run-id <id>",usageDescription:"Run identifier used for tenant lease admission checks"},{key:"leaseId",names:["--lease-id"],type:"string",usageLabel:"--lease-id <id>",usageDescription:"Lease identifier bound to tenant/run admission scope"},{key:"sessionLock",names:["--session-lock"],type:"enum",enumValues:["reject","strip"],usageLabel:"--session-lock reject|strip",usageDescription:"Lock bound-session device routing for this CLI invocation and nested batch steps"},{key:"sessionLocked",names:["--session-locked"],type:"boolean",usageLabel:"--session-locked",usageDescription:"Deprecated alias for --session-lock reject"},{key:"sessionLockConflicts",names:["--session-lock-conflicts"],type:"enum",enumValues:["reject","strip"],usageLabel:"--session-lock-conflicts reject|strip",usageDescription:"Deprecated alias for --session-lock"},{key:"platform",names:["--platform"],type:"enum",enumValues:["ios","macos","android","apple"],usageLabel:"--platform ios|macos|android|apple",usageDescription:"Platform to target (`apple` aliases the Apple automation backend)"},{key:"target",names:["--target"],type:"enum",enumValues:["mobile","tv","desktop"],usageLabel:"--target mobile|tv|desktop",usageDescription:"Device target class to match"},{key:"device",names:["--device"],type:"string",usageLabel:"--device <name>",usageDescription:"Device name to target"},{key:"udid",names:["--udid"],type:"string",usageLabel:"--udid <udid>",usageDescription:"iOS device UDID"},{key:"serial",names:["--serial"],type:"string",usageLabel:"--serial <serial>",usageDescription:"Android device serial"},{key:"headless",names:["--headless"],type:"boolean",usageLabel:"--headless",usageDescription:"Boot: launch Android emulator without a GUI window"},{key:"runtime",names:["--runtime"],type:"string",usageLabel:"--runtime <id>",usageDescription:"ensure-simulator: CoreSimulator runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-18-0)"},{key:"metroHost",names:["--metro-host"],type:"string",usageLabel:"--metro-host <host>",usageDescription:"Session-scoped Metro/debug host hint"},{key:"metroPort",names:["--metro-port"],type:"int",min:1,max:65535,usageLabel:"--metro-port <port>",usageDescription:"Session-scoped Metro/debug port hint"},{key:"metroProjectRoot",names:["--project-root"],type:"string",usageLabel:"--project-root <path>",usageDescription:"metro prepare: React Native project root (default: cwd)"},{key:"metroKind",names:["--kind"],type:"enum",enumValues:["auto","react-native","expo"],usageLabel:"--kind auto|react-native|expo",usageDescription:"metro prepare: detect or force the Metro launcher kind"},{key:"metroPublicBaseUrl",names:["--public-base-url"],type:"string",usageLabel:"--public-base-url <url>",usageDescription:"metro prepare: public base URL used to build bundle hints"},{key:"metroProxyBaseUrl",names:["--proxy-base-url"],type:"string",usageLabel:"--proxy-base-url <url>",usageDescription:"metro prepare: optional remote host bridge base URL for Metro access"},{key:"metroBearerToken",names:["--bearer-token"],type:"string",usageLabel:"--bearer-token <token>",usageDescription:"metro prepare: host bridge bearer token (prefer AGENT_DEVICE_PROXY_TOKEN or AGENT_DEVICE_METRO_BEARER_TOKEN)"},{key:"metroPreparePort",names:["--port"],type:"int",min:1,max:65535,usageLabel:"--port <port>",usageDescription:"metro prepare: local Metro port (default: 8081)"},{key:"metroListenHost",names:["--listen-host"],type:"string",usageLabel:"--listen-host <host>",usageDescription:"metro prepare: host Metro listens on (default: 0.0.0.0)"},{key:"metroStatusHost",names:["--status-host"],type:"string",usageLabel:"--status-host <host>",usageDescription:"metro prepare: host used for local /status polling (default: 127.0.0.1)"},{key:"metroStartupTimeoutMs",names:["--startup-timeout-ms"],type:"int",min:1,usageLabel:"--startup-timeout-ms <ms>",usageDescription:"metro prepare: timeout while waiting for Metro to become ready"},{key:"metroProbeTimeoutMs",names:["--probe-timeout-ms"],type:"int",min:1,usageLabel:"--probe-timeout-ms <ms>",usageDescription:"metro prepare: timeout for /status and proxy bridge calls"},{key:"metroRuntimeFile",names:["--runtime-file"],type:"string",usageLabel:"--runtime-file <path>",usageDescription:"metro prepare: optional file path to persist the JSON result"},{key:"metroNoReuseExisting",names:["--no-reuse-existing"],type:"boolean",usageLabel:"--no-reuse-existing",usageDescription:"metro prepare: always start a fresh Metro process"},{key:"metroNoInstallDeps",names:["--no-install-deps"],type:"boolean",usageLabel:"--no-install-deps",usageDescription:"metro prepare: skip package-manager install when node_modules is missing"},{key:"bundleUrl",names:["--bundle-url"],type:"string",usageLabel:"--bundle-url <url>",usageDescription:"Session-scoped bundle URL hint"},{key:"launchUrl",names:["--launch-url"],type:"string",usageLabel:"--launch-url <url>",usageDescription:"Session-scoped deep link / launch URL hint"},{key:"boot",names:["--boot"],type:"boolean",usageLabel:"--boot",usageDescription:"ensure-simulator: boot the simulator after ensuring it exists"},{key:"reuseExisting",names:["--reuse-existing"],type:"boolean",usageLabel:"--reuse-existing",usageDescription:"ensure-simulator: reuse an existing simulator (default: true)"},{key:"iosSimulatorDeviceSet",names:["--ios-simulator-device-set"],type:"string",usageLabel:"--ios-simulator-device-set <path>",usageDescription:"Scope iOS simulator discovery/commands to this simulator device set"},{key:"androidDeviceAllowlist",names:["--android-device-allowlist"],type:"string",usageLabel:"--android-device-allowlist <serials>",usageDescription:"Comma/space separated Android serial allowlist for discovery/selection"},{key:"activity",names:["--activity"],type:"string",usageLabel:"--activity <component>",usageDescription:"Android app launch activity (package/Activity); not for URL opens"},{key:"header",names:["--header"],type:"string",multiple:!0,usageLabel:"--header <name:value>",usageDescription:"install-from-source: repeatable HTTP header for URL downloads"},{key:"session",names:["--session"],type:"string",usageLabel:"--session <name>",usageDescription:"Named session"},{key:"count",names:["--count"],type:"int",min:1,max:200,usageLabel:"--count <n>",usageDescription:"Repeat count for press/swipe series"},{key:"fps",names:["--fps"],type:"int",min:1,max:120,usageLabel:"--fps <n>",usageDescription:"Record: target frames per second (iOS physical device runner)"},{key:"intervalMs",names:["--interval-ms"],type:"int",min:0,max:1e4,usageLabel:"--interval-ms <ms>",usageDescription:"Delay between press iterations"},{key:"holdMs",names:["--hold-ms"],type:"int",min:0,max:1e4,usageLabel:"--hold-ms <ms>",usageDescription:"Press hold duration for each iteration"},{key:"jitterPx",names:["--jitter-px"],type:"int",min:0,max:100,usageLabel:"--jitter-px <n>",usageDescription:"Deterministic coordinate jitter radius for press"},{key:"doubleTap",names:["--double-tap"],type:"boolean",usageLabel:"--double-tap",usageDescription:"Use double-tap gesture per press iteration"},{key:"clickButton",names:["--button"],type:"enum",enumValues:["primary","secondary","middle"],usageLabel:"--button primary|secondary|middle",usageDescription:"Click: choose mouse button (middle reserved for future macOS support)"},{key:"pauseMs",names:["--pause-ms"],type:"int",min:0,max:1e4,usageLabel:"--pause-ms <ms>",usageDescription:"Delay between swipe iterations"},{key:"pattern",names:["--pattern"],type:"enum",enumValues:["one-way","ping-pong"],usageLabel:"--pattern one-way|ping-pong",usageDescription:"Swipe repeat pattern"},{key:"verbose",names:["--debug","--verbose","-v"],type:"boolean",usageLabel:"--debug, --verbose, -v",usageDescription:"Enable debug diagnostics and stream daemon/runner logs"},{key:"json",names:["--json"],type:"boolean",usageLabel:"--json",usageDescription:"JSON output"},{key:"help",names:["--help","-h"],type:"boolean",usageLabel:"--help, -h",usageDescription:"Print help and exit"},{key:"version",names:["--version","-V"],type:"boolean",usageLabel:"--version, -V",usageDescription:"Print version and exit"},{key:"saveScript",names:["--save-script"],type:"booleanOrString",usageLabel:"--save-script [path]",usageDescription:"Save session script (.ad) on close; optional custom output path"},{key:"shutdown",names:["--shutdown"],type:"boolean",usageLabel:"--shutdown",usageDescription:"close: shutdown associated iOS simulator after ending session"},{key:"relaunch",names:["--relaunch"],type:"boolean",usageLabel:"--relaunch",usageDescription:"open: terminate app process before launching it"},{key:"restart",names:["--restart"],type:"boolean",usageLabel:"--restart",usageDescription:"logs clear: stop active stream, clear logs, then start streaming again"},{key:"retainPaths",names:["--retain-paths"],type:"boolean",usageLabel:"--retain-paths",usageDescription:"install-from-source: keep materialized artifact paths after install"},{key:"retentionMs",names:["--retention-ms"],type:"int",min:1,usageLabel:"--retention-ms <ms>",usageDescription:"install-from-source: retention TTL for materialized artifact paths"},{key:"noRecord",names:["--no-record"],type:"boolean",usageLabel:"--no-record",usageDescription:"Do not record this action"},{key:"replayUpdate",names:["--update","-u"],type:"boolean",usageLabel:"--update, -u",usageDescription:"Replay: update selectors and rewrite replay file in place"},{key:"steps",names:["--steps"],type:"string",usageLabel:"--steps <json>",usageDescription:"Batch: JSON array of steps"},{key:"stepsFile",names:["--steps-file"],type:"string",usageLabel:"--steps-file <path>",usageDescription:"Batch: read steps JSON from file"},{key:"batchOnError",names:["--on-error"],type:"enum",enumValues:["stop"],usageLabel:"--on-error stop",usageDescription:"Batch: stop when a step fails"},{key:"batchMaxSteps",names:["--max-steps"],type:"int",min:1,max:1e3,usageLabel:"--max-steps <n>",usageDescription:"Batch: maximum number of allowed steps"},{key:"appsFilter",names:["--user-installed"],type:"enum",setValue:"user-installed",usageLabel:"--user-installed",usageDescription:"Apps: list user-installed apps"},{key:"appsFilter",names:["--all"],type:"enum",setValue:"all",usageLabel:"--all",usageDescription:"Apps: list all apps (include system/default apps)"},{key:"snapshotInteractiveOnly",names:["-i"],type:"boolean",usageLabel:"-i",usageDescription:"Snapshot: interactive elements only"},{key:"snapshotCompact",names:["-c"],type:"boolean",usageLabel:"-c",usageDescription:"Snapshot: compact output (drop empty structure)"},{key:"snapshotDepth",names:["--depth","-d"],type:"int",min:0,usageLabel:"--depth, -d <depth>",usageDescription:"Snapshot: limit snapshot depth"},{key:"snapshotScope",names:["--scope","-s"],type:"string",usageLabel:"--scope, -s <scope>",usageDescription:"Snapshot: scope snapshot to label/identifier"},{key:"snapshotRaw",names:["--raw"],type:"boolean",usageLabel:"--raw",usageDescription:"Snapshot: raw node output"},{key:"out",names:["--out"],type:"string",usageLabel:"--out <path>",usageDescription:"Output path"},{key:"baseline",names:["--baseline","-b"],type:"string",usageLabel:"--baseline, -b <path>",usageDescription:"Diff screenshot: path to baseline image file"},{key:"threshold",names:["--threshold"],type:"string",usageLabel:"--threshold <0-1>",usageDescription:"Diff screenshot: color distance threshold (default 0.1)"}],B=new Set(["json","config","remoteConfig","stateDir","daemonBaseUrl","daemonAuthToken","daemonTransport","daemonServerMode","tenant","sessionIsolation","runId","leaseId","sessionLock","sessionLocked","sessionLockConflicts","help","version","verbose","platform","target","device","udid","serial","iosSimulatorDeviceSet","androidDeviceAllowlist","session","noRecord"]),H={boot:{helpDescription:"Ensure target device/simulator is booted and ready",summary:"Boot target device/simulator",positionalArgs:[],allowedFlags:["headless"]},open:{helpDescription:"Boot device/simulator; optionally launch app or deep link URL",summary:"Open an app, deep link or URL, save replays",positionalArgs:["appOrUrl?","url?"],allowedFlags:["activity","saveScript","relaunch"]},close:{helpDescription:"Close app or just end session",summary:"Close app or end session",positionalArgs:["app?"],allowedFlags:["saveScript","shutdown"]},reinstall:{helpDescription:"Uninstall + install app from binary path",summary:"Reinstall app from binary path",positionalArgs:["app","path"],allowedFlags:[]},install:{helpDescription:"Install app from binary path without uninstalling first",summary:"Install app from binary path",positionalArgs:["app","path"],allowedFlags:[]},"install-from-source":{helpDescription:"Install app from a URL source through the normal daemon artifact flow",summary:"Install app from a URL source",positionalArgs:["url"],allowedFlags:["header","retainPaths","retentionMs"]},push:{helpDescription:"Simulate push notification payload delivery",summary:"Deliver push payload",positionalArgs:["bundleOrPackage","payloadOrJson"],allowedFlags:[]},snapshot:{helpDescription:"Capture accessibility tree",positionalArgs:[],allowedFlags:[...M]},diff:{usageOverride:"diff snapshot | diff screenshot --baseline <path> [--out <diff.png>] [--threshold <0-1>]",helpDescription:"Diff accessibility snapshot or compare screenshots pixel-by-pixel",summary:"Diff snapshot or screenshot",positionalArgs:["kind"],allowedFlags:[...M,"baseline","threshold","out"]},"ensure-simulator":{helpDescription:"Ensure an iOS simulator exists in a device set (create if missing)",summary:"Ensure iOS simulator exists",positionalArgs:[],allowedFlags:["runtime","boot","reuseExisting"],skipCapabilityCheck:!0},devices:{helpDescription:"List available devices",positionalArgs:[],allowedFlags:[],skipCapabilityCheck:!0},apps:{helpDescription:"List installed apps (includes default/system apps by default)",summary:"List installed apps",positionalArgs:[],allowedFlags:["appsFilter"],defaults:{appsFilter:"all"}},appstate:{helpDescription:"Show foreground app/activity",positionalArgs:[],allowedFlags:[],skipCapabilityCheck:!0},metro:{usageOverride:"metro prepare --public-base-url <url> [--project-root <path>] [--port <port>] [--kind auto|react-native|expo]",listUsageOverride:"metro prepare --public-base-url <url>",helpDescription:"Prepare a local Metro runtime and optionally bridge it through a remote host",summary:"Prepare local Metro runtime",positionalArgs:["prepare"],allowedFlags:["metroProjectRoot","metroKind","metroPublicBaseUrl","metroProxyBaseUrl","metroBearerToken","metroPreparePort","metroListenHost","metroStatusHost","metroStartupTimeoutMs","metroProbeTimeoutMs","metroRuntimeFile","metroNoReuseExisting","metroNoInstallDeps"],skipCapabilityCheck:!0},clipboard:{usageOverride:"clipboard read | clipboard write <text>",listUsageOverride:"clipboard read | clipboard write <text>",helpDescription:"Read or write device clipboard text",positionalArgs:["read|write","text?"],allowsExtraPositionals:!0,allowedFlags:[]},keyboard:{usageOverride:"keyboard [status|get|dismiss]",helpDescription:"Inspect Android keyboard visibility/type or dismiss it",summary:"Inspect or dismiss Android keyboard",positionalArgs:["action?"],allowedFlags:[]},perf:{helpDescription:"Show session performance metrics (startup timing)",summary:"Show startup metrics",positionalArgs:[],allowedFlags:[]},back:{helpDescription:"Navigate back (where supported)",summary:"Go back",positionalArgs:[],allowedFlags:[]},home:{helpDescription:"Go to home screen (where supported)",summary:"Go home",positionalArgs:[],allowedFlags:[]},"app-switcher":{helpDescription:"Open app switcher (where supported)",summary:"Open app switcher",positionalArgs:[],allowedFlags:[]},wait:{usageOverride:"wait <ms>|text <text>|@ref|<selector> [timeoutMs]",helpDescription:"Wait for duration, text, ref, or selector to appear",summary:"Wait for time, text, ref, or selector",positionalArgs:["durationOrSelector","timeoutMs?"],allowsExtraPositionals:!0,allowedFlags:[...G]},alert:{usageOverride:"alert [get|accept|dismiss|wait] [timeout]",helpDescription:"Inspect or handle alert (iOS simulator)",summary:"Inspect or handle iOS alert",positionalArgs:["action?","timeout?"],allowedFlags:[]},click:{usageOverride:"click <x y|@ref|selector>",helpDescription:"Tap/click by coordinates, snapshot ref, or selector",summary:"Tap by coordinates, ref, or selector",positionalArgs:["target"],allowsExtraPositionals:!0,allowedFlags:["count","intervalMs","holdMs","jitterPx","doubleTap","clickButton",...G]},get:{usageOverride:"get text|attrs <@ref|selector>",helpDescription:"Return element text/attributes by ref or selector",summary:"Get text or attrs by ref or selector",positionalArgs:["subcommand","target"],allowedFlags:[...G]},replay:{helpDescription:"Replay a recorded session",positionalArgs:["path"],allowedFlags:["replayUpdate"],skipCapabilityCheck:!0},batch:{usageOverride:"batch [--steps <json> | --steps-file <path>]",listUsageOverride:"batch --steps <json> | --steps-file <path>",helpDescription:"Execute multiple commands in one daemon request",summary:"Run multiple commands",positionalArgs:[],allowedFlags:["steps","stepsFile","batchOnError","batchMaxSteps","out"],skipCapabilityCheck:!0},press:{usageOverride:"press <x y|@ref|selector>",helpDescription:"Tap/press by coordinates, snapshot ref, or selector (supports repeated series)",summary:"Press by coordinates, ref, or selector",positionalArgs:["targetOrX","y?"],allowsExtraPositionals:!0,allowedFlags:["count","intervalMs","holdMs","jitterPx","doubleTap",...G]},longpress:{helpDescription:"Long press by coordinates (iOS and Android)",summary:"Long press by coordinates",positionalArgs:["x","y","durationMs?"],allowedFlags:[]},swipe:{helpDescription:"Swipe coordinates with optional repeat pattern",summary:"Swipe coordinates",positionalArgs:["x1","y1","x2","y2","durationMs?"],allowedFlags:["count","pauseMs","pattern"]},focus:{helpDescription:"Focus input at coordinates",positionalArgs:["x","y"],allowedFlags:[]},type:{helpDescription:"Type text in focused field",positionalArgs:["text"],allowsExtraPositionals:!0,allowedFlags:[]},fill:{usageOverride:"fill <x> <y> <text> | fill <@ref|selector> <text>",helpDescription:"Tap then type",positionalArgs:["targetOrX","yOrText","text?"],allowsExtraPositionals:!0,allowedFlags:[...G]},scroll:{helpDescription:"Scroll in direction (0-1 amount)",summary:"Scroll in a direction",positionalArgs:["direction","amount?"],allowedFlags:[]},scrollintoview:{usageOverride:"scrollintoview <text|@ref>",helpDescription:"Scroll until text appears or a snapshot ref is brought into view",summary:"Scroll until text or ref is visible",positionalArgs:["target"],allowsExtraPositionals:!0,allowedFlags:[]},pinch:{helpDescription:"Pinch/zoom gesture (iOS simulator)",positionalArgs:["scale","x?","y?"],allowedFlags:[]},screenshot:{helpDescription:"Capture screenshot",positionalArgs:["path?"],allowedFlags:["out"]},"trigger-app-event":{usageOverride:"trigger-app-event <event> [payloadJson]",helpDescription:"Trigger app-defined event hook via deep link template",summary:"Trigger app event hook",positionalArgs:["event","payloadJson?"],allowedFlags:[]},record:{usageOverride:"record start [path] [--fps <n>] | record stop",listUsageOverride:"record start [path] | record stop",helpDescription:"Start/stop screen recording",summary:"Start or stop screen recording",positionalArgs:["start|stop","path?"],allowedFlags:["fps"]},trace:{usageOverride:"trace start [path] | trace stop [path]",listUsageOverride:"trace start [path] | trace stop",helpDescription:"Start/stop trace log capture",summary:"Start or stop trace capture",positionalArgs:["start|stop","path?"],allowedFlags:[],skipCapabilityCheck:!0},logs:{usageOverride:"logs path | logs start | logs stop | logs clear [--restart] | logs doctor | logs mark [message...]",helpDescription:"Session app log info, start/stop streaming, diagnostics, and markers",summary:"Manage session app logs",positionalArgs:["path|start|stop|clear|doctor|mark","message?"],allowsExtraPositionals:!0,allowedFlags:["restart"]},network:{usageOverride:"network dump [limit] [summary|headers|body|all] | network log [limit] [summary|headers|body|all]",helpDescription:"Dump recent HTTP(s) traffic parsed from the session app log",summary:"Show recent HTTP traffic",positionalArgs:["dump|log","limit?","include?"],allowedFlags:[]},find:{usageOverride:"find <locator|text> <action> [value]",helpDescription:"Find by text/label/value/role/id and run action",summary:"Find an element and act",positionalArgs:["query","action","value?"],allowsExtraPositionals:!0,allowedFlags:["snapshotDepth","snapshotRaw"]},is:{helpDescription:"Assert UI state (visible|hidden|exists|editable|selected|text)",summary:"Assert UI state",positionalArgs:["predicate","selector","value?"],allowsExtraPositionals:!0,allowedFlags:[...G]},settings:{usageOverride:p,listUsageOverride:"settings [area] [options]",helpDescription:"Toggle OS settings, appearance, and app permissions (macOS supports only settings appearance; permission actions use the active session app)",summary:"Change OS settings and app permissions",positionalArgs:["setting","state","target?","mode?"],allowedFlags:[]},session:{usageOverride:"session list",helpDescription:"List active sessions",positionalArgs:["list?"],allowedFlags:[],skipCapabilityCheck:!0}},q=new Map,J=new Map;for(let e of U){for(let t of e.names)q.set(t,e);let t=J.get(e.key);t?t.push(e):J.set(e.key,[e])}function K(e){if(e)return H[e]}function W(){return Object.keys(H)}function z(e){let t=e.endsWith("?"),s=t?e.slice(0,-1):e;return t?`[${s}]`:`<${s}>`}let X=(e=`agent-device <command> [args] [--json]
CLI to control iOS and Android devices for AI agents.
`,t=Q("Commands:",W().map(e=>{let t=H[e];if(!t)throw Error(`Missing command schema for ${e}`);return{name:e,schema:t,usage:function(e,t){if(t.listUsageOverride)return t.listUsageOverride;let s=t.positionalArgs.map(s=>{var r,o,a;let i,n,l,p;return r=e,o=t,n=(i=(a=s).endsWith("?"))?a.slice(0,-1):a,p=(l=/^[a-z-]+(?:\|[a-z-]+)+$/i.test(n))||void 0!==o.usageOverride&&o.usageOverride.startsWith(`${r} ${n}`),i?l?`[${n}]`:p?n:`[${n}]`:p?n:`<${n}>`});return[e,...s].join(" ")}(e,t)}}).map(e=>({label:e.usage,description:e.schema.summary??e.schema.helpDescription}))),s=Y("Flags:",Z(B)),r=[Q("Agent Skills:",[{label:"agent-device",description:"Canonical mobile automation flows"},{label:"dogfood",description:"Exploratory QA and bug hunts"}]),"See `skills/<name>/SKILL.md` in the installed package."].join("\n\n"),o=ee("Configuration:",["Default config files: ~/.agent-device/config.json, ./agent-device.json","Use --config <path> or AGENT_DEVICE_CONFIG to load one explicit config file."]),a=Q("Environment:",[{label:"AGENT_DEVICE_SESSION",description:"Default session name"},{label:"AGENT_DEVICE_PLATFORM",description:"Default platform binding"},{label:"AGENT_DEVICE_SESSION_LOCK",description:"Bound-session conflict mode"},{label:"AGENT_DEVICE_DAEMON_BASE_URL",description:"Connect to remote daemon"}]),i=ee("Examples:",["agent-device open Settings --platform ios","agent-device snapshot -i",'agent-device fill @e3 "test@example.com"',"agent-device replay ./session.ad"]),`${e}
`,t=Q("Commands:",W().map(e=>{let t=H[e];if(!t)throw Error(`Missing command schema for ${e}`);return{name:e,schema:t,usage:function(e,t){if(t.listUsageOverride)return t.listUsageOverride;let s=t.positionalArgs.map(s=>{var r,o,a;let i,n,l,p;return r=e,o=t,n=(i=(a=s).endsWith("?"))?a.slice(0,-1):a,p=(l=/^[a-z-]+(?:\|[a-z-]+)+$/i.test(n))||void 0!==o.usageOverride&&o.usageOverride.startsWith(`${r} ${n}`),i?l?`[${n}]`:p?n:`[${n}]`:p?n:`<${n}>`});return[e,...s].join(" ")}(e,t)}}).map(e=>({label:e.usage,description:e.schema.summary??e.schema.helpDescription}))),s=Y("Flags:",Z(B)),r=[Q("Agent Skills:",[{label:"agent-device",description:"Canonical mobile automation flows"},{label:"dogfood",description:"Exploratory QA and bug hunts"}]),"See `skills/<name>/SKILL.md` in the installed package."].join("\n\n"),o=ee("Configuration:",["Default config files: ~/.agent-device/config.json, ./agent-device.json","Use --config <path> or AGENT_DEVICE_CONFIG to load one explicit config file."]),a=Q("Environment:",[{label:"AGENT_DEVICE_SESSION",description:"Default session name"},{label:"AGENT_DEVICE_PLATFORM",description:"Default platform binding"},{label:"AGENT_DEVICE_SESSION_LOCK",description:"Bound-session conflict mode"},{label:"AGENT_DEVICE_DAEMON_BASE_URL",description:"Connect to remote daemon"}]),i=ee("Examples:",["agent-device open Settings --platform ios","agent-device open TextEdit --platform macos","agent-device snapshot -i",'agent-device fill @e3 "test@example.com"',"agent-device replay ./session.ad"]),`${e}
${t}

@@ -6,0 +6,0 @@

@@ -1,3 +0,3 @@

import type { DeviceInfo } from '../utils/device.ts';
import { type DeviceInfo } from '../utils/device.ts';
export declare function isCommandSupportedOnDevice(command: string, device: DeviceInfo): boolean;
export declare function listCapabilityCommands(): string[];

@@ -1,2 +0,2 @@

import { resolveDevice, type DeviceInfo } from '../utils/device.ts';
import { resolveDevice, type DeviceInfo, type PlatformSelector } from '../utils/device.ts';
import { findBootableIosSimulator } from '../platforms/ios/devices.ts';

@@ -6,4 +6,4 @@ import type { CliFlags } from '../utils/command-schema.ts';

type ResolveDeviceFlags = Pick<CliFlags, 'platform' | 'target' | 'device' | 'udid' | 'serial' | 'iosSimulatorDeviceSet' | 'androidDeviceAllowlist'>;
type IosDeviceSelector = {
platform?: 'ios';
type AppleDeviceSelector = {
platform?: Exclude<PlatformSelector, 'android'>;
target?: DeviceTarget;

@@ -14,3 +14,3 @@ deviceName?: string;

};
type ResolveIosDeviceDeps = {
type ResolveAppleDeviceDeps = {
resolveDevice: typeof resolveDevice;

@@ -26,6 +26,7 @@ findBootableSimulator: typeof findBootableIosSimulator;

*/
export declare function resolveIosDevice(devices: DeviceInfo[], selector: IosDeviceSelector, context: {
export declare function resolveAppleDevice(devices: DeviceInfo[], selector: AppleDeviceSelector, context: {
simulatorSetPath?: string;
}, deps: ResolveIosDeviceDeps): Promise<DeviceInfo>;
}, deps: ResolveAppleDeviceDeps): Promise<DeviceInfo>;
export declare const resolveIosDevice: typeof resolveAppleDevice;
export declare function resolveTargetDevice(flags: ResolveDeviceFlags): Promise<DeviceInfo>;
export {};

@@ -32,4 +32,5 @@ import type { DeviceInfo } from '../utils/device.ts';

doubleTap?: boolean;
clickButton?: 'primary' | 'secondary' | 'middle';
pauseMs?: number;
pattern?: 'one-way' | 'ping-pong';
}): Promise<Record<string, unknown> | void>;

@@ -19,2 +19,3 @@ import type { CommandFlags } from '../core/dispatch.ts';

doubleTap?: boolean;
clickButton?: 'primary' | 'secondary' | 'middle';
pauseMs?: number;

@@ -21,0 +22,0 @@ pattern?: 'one-way' | 'ping-pong';

@@ -1,14 +0,7 @@

import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
import type { DaemonCommandContext } from '../context.ts';
import type { DaemonRequest, DaemonResponse } from '../types.ts';
import { SessionStore } from '../session-store.ts';
type ContextFromFlags = (flags: CommandFlags | undefined, appBundleId?: string, traceLogPath?: string) => DaemonCommandContext;
export declare function handleInteractionCommands(params: {
req: DaemonRequest;
sessionName: string;
sessionStore: SessionStore;
contextFromFlags: ContextFromFlags;
import { dispatchCommand } from '../../core/dispatch.ts';
import type { DaemonResponse } from '../types.ts';
import type { InteractionHandlerParams } from './interaction-common.ts';
export { unsupportedRefSnapshotFlags } from './interaction-flags.ts';
export declare function handleInteractionCommands(params: Omit<InteractionHandlerParams, 'dispatch'> & {
dispatch?: typeof dispatchCommand;
}): Promise<DaemonResponse | null>;
export declare function unsupportedRefSnapshotFlags(flags: CommandFlags | undefined): string[];
export {};

@@ -20,2 +20,3 @@ import { type DeviceInfo } from '../../utils/device.ts';

}): Promise<DeviceInfo>;
export declare function refreshSessionDeviceIfNeeded(device: DeviceInfo, resolveTargetDeviceFn: typeof resolveTargetDevice): Promise<DeviceInfo>;
export declare function resolveAndroidEmulatorAvdName(params: {

@@ -22,0 +23,0 @@ flags: DaemonRequest['flags'] | undefined;

@@ -6,4 +6,6 @@ import type { DeviceInfo } from '../../utils/device.ts';

import { type clearRuntimeHintsFromApp } from '../runtime-hints.ts';
type RuntimePlatform = NonNullable<SessionRuntimeHints['platform']>;
export declare function countConfiguredRuntimeHints(runtime: SessionRuntimeHints | undefined): number;
export declare function buildRuntimeHints(flags: CommandFlags | undefined, platform?: 'ios' | 'android'): SessionRuntimeHints;
export declare function toRuntimePlatform(platform: CommandFlags['platform'] | DeviceInfo['platform'] | 'apple' | undefined): RuntimePlatform | undefined;
export declare function buildRuntimeHints(flags: CommandFlags | undefined, platform?: RuntimePlatform): SessionRuntimeHints;
export declare function mergeRuntimeHints(current: SessionRuntimeHints | undefined, next: SessionRuntimeHints): SessionRuntimeHints;

@@ -10,0 +12,0 @@ export declare function setSessionRuntimeHintsForOpen(sessionStore: SessionStore, sessionName: string, runtime: SessionRuntimeHints | undefined): SessionRuntimeHints | undefined;

@@ -18,2 +18,4 @@ import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts';

}) => Promise<DeviceInfo>;
type ListAndroidDevices = typeof import('../../platforms/android/devices.ts').listAndroidDevices;
type ListAppleDevices = typeof import('../../platforms/ios/devices.ts').listAppleDevices;
export declare function handleSessionCommands(params: {

@@ -42,3 +44,9 @@ req: DaemonRequest;

shutdownAndroidEmulator?: ShutdownAndroidEmulatorFn;
listAndroidDevices?: ListAndroidDevices;
listAppleDevices?: ListAppleDevices;
listAppleApps?: (device: DeviceInfo, filter: 'user-installed' | 'all') => Promise<Array<{
bundleId: string;
name?: string;
}>>;
}): Promise<DaemonResponse | null>;
export {};

@@ -5,3 +5,5 @@ import { dispatchCommand } from '../../core/dispatch.ts';

import { SessionStore } from '../session-store.ts';
import { type SelectorChain } from '../selectors.ts';
import { withSessionlessRunnerCleanup } from './snapshot-session.ts';
import { parseWaitArgs } from './snapshot-wait.ts';
export { parseWaitArgs };
export declare function handleSnapshotCommands(params: {

@@ -14,21 +16,3 @@ req: DaemonRequest;

runnerCommand?: typeof runIosRunnerCommand;
sessionlessRunnerCleanup?: typeof withSessionlessRunnerCleanup;
}): Promise<DaemonResponse | null>;
type WaitParsed = {
kind: 'sleep';
durationMs: number;
} | {
kind: 'ref';
rawRef: string;
timeoutMs: number | null;
} | {
kind: 'selector';
selector: SelectorChain;
selectorExpression: string;
timeoutMs: number | null;
} | {
kind: 'text';
text: string;
timeoutMs: number | null;
};
export declare function parseWaitArgs(args: string[]): WaitParsed | null;
export {};

@@ -0,1 +1,2 @@

import type { Platform } from '../utils/device.ts';
import type { SnapshotState } from '../utils/snapshot.ts';

@@ -8,3 +9,3 @@ type IsPredicate = 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'text';

expectedText?: string;
platform: 'ios' | 'android';
platform: Platform;
}): {

@@ -11,0 +12,0 @@ pass: boolean;

@@ -0,4 +1,5 @@

import type { Platform } from '../utils/device.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';
export declare function buildSelectorChainForNode(node: SnapshotNode, _platform: 'ios' | 'android', options?: {
export declare function buildSelectorChainForNode(node: SnapshotNode, _platform: Platform, options?: {
action?: 'click' | 'fill' | 'get';
}): string[];

@@ -0,5 +1,6 @@

import type { Platform } from '../utils/device.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';
import type { Selector } from './selectors-parse.ts';
export declare function matchesSelector(node: SnapshotNode, selector: Selector, platform: 'ios' | 'android'): boolean;
export declare function matchesSelector(node: SnapshotNode, selector: Selector, platform: Platform): boolean;
export declare function isNodeVisible(node: SnapshotNode): boolean;
export declare function isNodeEditable(node: SnapshotNode, platform: 'ios' | 'android'): boolean;
export declare function isNodeEditable(node: SnapshotNode, platform: Platform): boolean;

@@ -0,1 +1,2 @@

import type { Platform } from '../utils/device.ts';
import type { SnapshotNode, SnapshotState } from '../utils/snapshot.ts';

@@ -15,3 +16,3 @@ import type { Selector, SelectorChain } from './selectors-parse.ts';

export declare function resolveSelectorChain(nodes: SnapshotState['nodes'], chain: SelectorChain, options: {
platform: 'ios' | 'android';
platform: Platform;
requireRect?: boolean;

@@ -22,3 +23,3 @@ requireUnique?: boolean;

export declare function findSelectorChainMatch(nodes: SnapshotState['nodes'], chain: SelectorChain, options: {
platform: 'ios' | 'android';
platform: Platform;
requireRect?: boolean;

@@ -25,0 +26,0 @@ }): {

@@ -0,1 +1,2 @@

import type { Platform } from '../utils/device.ts';
import type { RawSnapshotNode, SnapshotState } from '../utils/snapshot.ts';

@@ -6,4 +7,4 @@ export declare function findNodeByLabel(nodes: SnapshotState['nodes'], label: string): import("../index.ts").SnapshotNode | null;

export declare function normalizeType(type: string): string;
export declare function isFillableType(type: string, platform: 'ios' | 'android'): boolean;
export declare function isFillableType(type: string, platform: Platform): boolean;
export declare function findNearestHittableAncestor(nodes: SnapshotState['nodes'], node: SnapshotState['nodes'][number]): SnapshotState['nodes'][number] | null;
export declare function extractNodeText(node: SnapshotState['nodes'][number]): string;
import type { MaterializeInstallSource } from '../platforms/install-source.ts';
import type { CommandFlags } from '../core/dispatch.ts';
import type { DeviceInfo } from '../utils/device.ts';
import type { DeviceInfo, Platform, PlatformSelector } from '../utils/device.ts';
import type { ExecResult } from '../utils/exec.ts';

@@ -32,3 +32,3 @@ import type { SnapshotState } from '../utils/snapshot.ts';

lockPolicy?: DaemonLockPolicy;
lockPlatform?: 'ios' | 'android' | 'apple';
lockPlatform?: PlatformSelector;
};

@@ -89,10 +89,10 @@ };

} | {
platform: 'ios-device-runner';
platform: 'ios-device-runner' | 'macos-runner';
outPath: string;
clientOutPath?: string;
remotePath: string;
remotePath?: string;
};
/** Session-scoped app log stream; logs written to outPath for agent to grep */
appLog?: {
platform: 'ios' | 'android';
platform: Platform;
backend: 'ios-simulator' | 'ios-device' | 'android';

@@ -99,0 +99,0 @@ outPath: string;

@@ -0,1 +1,2 @@

import { runCmd } from '../../utils/exec.ts';
import type { DeviceInfo } from '../../utils/device.ts';

@@ -5,2 +6,5 @@ type AndroidDeviceDiscoveryOptions = {

};
type AndroidAdbRunner = typeof runCmd;
export declare function parseAndroidEmulatorAvdNameOutput(rawOutput: string): string | undefined;
export declare function resolveAndroidEmulatorAvdName(serial: string, runAdb?: AndroidAdbRunner): Promise<string | undefined>;
export declare function parseAndroidTargetFromCharacteristics(rawOutput: string): 'tv' | null;

@@ -7,0 +11,0 @@ export declare function parseAndroidFeatureListForTv(rawOutput: string): boolean;

@@ -38,3 +38,4 @@ import type { DeviceInfo, DeviceTarget } from '../../utils/device.ts';

export declare function findBootableIosSimulator(options?: FindBootableSimulatorOptions): Promise<DeviceInfo | null>;
export declare function listIosDevices(options?: IosDeviceDiscoveryOptions): Promise<DeviceInfo[]>;
export declare function listAppleDevices(options?: IosDeviceDiscoveryOptions): Promise<DeviceInfo[]>;
export declare const listIosDevices: typeof listAppleDevices;
export {};
import type { DeviceInfo } from '../../utils/device.ts';
import type { ClickButton } from '../../core/click-button.ts';
export type RunnerCommand = {
command: 'tap' | 'tapSeries' | 'longPress' | 'drag' | 'dragSeries' | 'type' | 'swipe' | 'findText' | 'snapshot' | 'screenshot' | 'back' | 'home' | 'appSwitcher' | 'alert' | 'pinch' | 'recordStart' | 'recordStop' | 'shutdown';
command: 'tap' | 'mouseClick' | 'tapSeries' | 'longPress' | 'drag' | 'dragSeries' | 'type' | 'swipe' | 'findText' | 'snapshot' | 'screenshot' | 'back' | 'home' | 'appSwitcher' | 'alert' | 'pinch' | 'recordStart' | 'recordStop' | 'shutdown';
appBundleId?: string;

@@ -9,2 +10,3 @@ text?: string;

y?: number;
button?: ClickButton;
count?: number;

@@ -11,0 +13,0 @@ intervalMs?: number;

import { type DeviceInfo } from '../../utils/device.ts';
export { xctestrunReferencesExistingProducts } from './runner-xctestrun-products.ts';
export declare const runnerPrepProcesses: Set<import("child_process").ChildProcess>;
export declare const IOS_RUNNER_CONTAINER_BUNDLE_IDS: string[];
type EnsureXctestrunDeps = {
findProjectRoot: () => string;
findXctestrun: (root: string) => string | null;
xctestrunReferencesProjectRoot: (xctestrunPath: string, projectRoot: string) => boolean;
resolveExistingXctestrunProductPaths: (xctestrunPath: string) => string[] | null;
repairRunnerProductsIfNeeded: (device: DeviceInfo, productPaths: string[], xctestrunPath: string) => Promise<void>;
assertSafeDerivedCleanup: (derivedPath: string) => void;
cleanRunnerDerivedArtifacts: (derivedPath: string) => void;
buildRunnerXctestrun: (device: DeviceInfo, projectPath: string, derived: string, options: {
verbose?: boolean;
logPath?: string;
traceLogPath?: string;
}) => Promise<void>;
};
export declare function ensureXctestrun(device: DeviceInfo, options: {

@@ -8,3 +23,5 @@ verbose?: boolean;

traceLogPath?: string;
}): Promise<string>;
}, deps?: EnsureXctestrunDeps): Promise<string>;
export declare function shouldDeleteRunnerDerivedRootEntry(entryName: string): boolean;
export declare function xctestrunReferencesProjectRoot(xctestrunPath: string, projectRoot: string): boolean;
export declare function prepareXctestrunWithEnv(xctestrunPath: string, envVars: Record<string, string>, suffix: string): Promise<{

@@ -17,4 +34,5 @@ xctestrunPath: string;

export declare function resolveRunnerMaxConcurrentDestinationsFlag(device: DeviceInfo): string;
export declare function resolveRunnerSigningBuildSettings(env?: NodeJS.ProcessEnv, forDevice?: boolean): string[];
export declare function resolveRunnerSigningBuildSettings(env?: NodeJS.ProcessEnv, forDevice?: boolean, platform?: DeviceInfo['platform']): string[];
export declare function resolveRunnerBundleBuildSettings(env?: NodeJS.ProcessEnv): string[];
export declare function resolveRunnerPerformanceBuildSettings(): string[];
export declare function assertSafeDerivedCleanup(derivedPath: string, env?: NodeJS.ProcessEnv): void;

@@ -17,4 +17,4 @@ export type CliFlags = {

sessionLockConflicts?: 'reject' | 'strip';
platform?: 'ios' | 'android' | 'apple';
target?: 'mobile' | 'tv';
platform?: 'ios' | 'macos' | 'android' | 'apple';
target?: 'mobile' | 'tv' | 'desktop';
device?: string;

@@ -62,2 +62,3 @@ udid?: string;

doubleTap?: boolean;
clickButton?: 'primary' | 'secondary' | 'middle';
pauseMs?: number;

@@ -64,0 +65,0 @@ pattern?: 'one-way' | 'ping-pong';

@@ -1,5 +0,6 @@

export type Platform = 'ios' | 'android';
export type ApplePlatform = 'ios' | 'macos';
export type Platform = ApplePlatform | 'android';
export type PlatformSelector = Platform | 'apple';
export type DeviceKind = 'simulator' | 'emulator' | 'device';
export type DeviceTarget = 'mobile' | 'tv';
export type DeviceTarget = 'mobile' | 'tv' | 'desktop';
export type DeviceInfo = {

@@ -15,3 +16,3 @@ platform: Platform;

type DeviceSelector = {
platform?: Platform;
platform?: PlatformSelector;
target?: DeviceTarget;

@@ -25,5 +26,12 @@ deviceName?: string;

};
export declare function normalizePlatformSelector(platform: PlatformSelector | undefined): Platform | undefined;
export declare function resolveApplePlatformName(target: DeviceTarget | undefined): 'iOS' | 'tvOS';
export declare function normalizePlatformSelector(platform: PlatformSelector | undefined): PlatformSelector | undefined;
export declare function isApplePlatform(platform: Platform | PlatformSelector | undefined): platform is ApplePlatform | 'apple';
export declare function matchesPlatformSelector(platform: Platform, selector: PlatformSelector | undefined): boolean;
export declare function resolveApplePlatformName(platformOrTarget: ApplePlatform | DeviceTarget | undefined): 'iOS' | 'tvOS' | 'macOS';
export declare function resolveAppleSimulatorSetPathForSelector(params: {
simulatorSetPath?: string;
platform?: PlatformSelector;
target?: DeviceTarget;
}): string | undefined;
export declare function resolveDevice(devices: DeviceInfo[], selector: DeviceSelector, context?: DeviceSelectionContext): Promise<DeviceInfo>;
export {};

@@ -10,2 +10,9 @@ //

import Network
#if canImport(UIKit)
import UIKit
typealias RunnerImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias RunnerImage = NSImage
#endif

@@ -89,7 +96,3 @@ final class RunnerTests: XCTestCase {

NSLog("AGENT_DEVICE_RUNNER_DESIRED_PORT=%d", desiredPort)
if desiredPort > 0, let port = NWEndpoint.Port(rawValue: desiredPort) {
listener = try NWListener(using: .tcp, on: port)
} else {
listener = try NWListener(using: .tcp)
}
listener = try makeRunnerListener(desiredPort: desiredPort)
listener?.stateUpdateHandler = { [weak self] state in

@@ -128,2 +131,16 @@ switch state {

}
private func makeRunnerListener(desiredPort: UInt16) throws -> NWListener {
if desiredPort > 0, let port = NWEndpoint.Port(rawValue: desiredPort) {
#if os(macOS)
let parameters = NWParameters.tcp
parameters.allowLocalEndpointReuse = true
parameters.requiredLocalEndpoint = .hostPort(host: "127.0.0.1", port: port)
return try NWListener(using: parameters)
#else
return try NWListener(using: .tcp, on: port)
#endif
}
return try NWListener(using: .tcp)
}
}

@@ -220,2 +220,12 @@ import XCTest

return Response(ok: false, error: ErrorPayload(message: "tap requires text or x/y"))
case .mouseClick:
guard let x = command.x, let y = command.y else {
return Response(ok: false, error: ErrorPayload(message: "mouseClick requires x and y"))
}
do {
try mouseClickAt(app: activeApp, x: x, y: y, button: command.button ?? "primary")
return Response(ok: true, data: DataPayload(message: "clicked"))
} catch {
return Response(ok: false, error: ErrorPayload(message: error.localizedDescription))
}
case .tapSeries:

@@ -332,3 +342,3 @@ guard let x = command.x, let y = command.y else {

let screenshot = XCUIScreen.main.screenshot()
guard let pngData = screenshot.image.pngData() else {
guard let pngData = runnerPngData(for: screenshot.image) else {
return Response(ok: false, error: ErrorPayload(message: "Failed to encode screenshot as PNG"))

@@ -343,4 +353,8 @@ }

}
#if os(macOS)
return Response(ok: true, data: DataPayload(message: filePath))
#else
// Return path relative to app container root (tmp/ maps to NSTemporaryDirectory)
return Response(ok: true, data: DataPayload(message: "tmp/\(fileName)"))
#endif
case .back:

@@ -350,11 +364,26 @@ if tapNavigationBack(app: activeApp) {

}
#if os(macOS)
return Response(ok: false, error: ErrorPayload(message: "back button is not available on macOS"))
#else
performBackGesture(app: activeApp)
return Response(ok: true, data: DataPayload(message: "back"))
#endif
case .home:
#if os(macOS)
return Response(ok: false, error: ErrorPayload(message: "home is not supported on macOS"))
#else
pressHomeButton()
return Response(ok: true, data: DataPayload(message: "home"))
#endif
case .appSwitcher:
#if os(macOS)
return Response(ok: false, error: ErrorPayload(message: "appSwitcher is not supported on macOS"))
#else
performAppSwitcherGesture(app: activeApp)
return Response(ok: true, data: DataPayload(message: "appSwitcher"))
#endif
case .alert:
#if os(macOS)
return Response(ok: false, error: ErrorPayload(message: "alert is not supported on macOS"))
#else
let action = (command.action ?? "get").lowercased()

@@ -377,3 +406,7 @@ let alert = activeApp.alerts.firstMatch

return Response(ok: true, data: DataPayload(message: alert.label, items: buttonLabels))
#endif
case .pinch:
#if os(macOS)
return Response(ok: false, error: ErrorPayload(message: "pinch is not supported on macOS"))
#else
guard let scale = command.scale, scale > 0 else {

@@ -384,4 +417,5 @@ return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))

return Response(ok: true, data: DataPayload(message: "pinched"))
#endif
}
}
}

@@ -7,2 +7,9 @@ import XCTest

func tapNavigationBack(app: XCUIApplication) -> Bool {
#if os(macOS)
if let back = macOSNavigationBackElement(app: app) {
tapElementCenter(app: app, element: back)
return true
}
return false
#else
let buttons = app.navigationBars.buttons.allElementsBoundByIndex

@@ -14,2 +21,3 @@ if let back = buttons.first(where: { $0.isHittable }) {

return pressTvRemoteMenuIfAvailable()
#endif
}

@@ -38,2 +46,5 @@

func pressHomeButton() {
#if os(macOS)
return
#else
if pressTvRemoteHomeIfAvailable() {

@@ -43,2 +54,3 @@ return

XCUIDevice.shared.press(.home)
#endif
}

@@ -147,10 +159,38 @@

func tapAt(app: XCUIApplication, x: Double, y: Double) {
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
let coordinate = interactionCoordinate(app: app, x: x, y: y)
coordinate.tap()
}
func mouseClickAt(app: XCUIApplication, x: Double, y: Double, button: String) throws {
let coordinate = interactionCoordinate(app: app, x: x, y: y)
#if os(macOS)
switch button {
case "primary":
coordinate.tap()
case "secondary":
coordinate.rightClick()
case "middle":
throw NSError(
domain: "AgentDeviceRunner",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "middle mouse button is not supported"]
)
default:
throw NSError(
domain: "AgentDeviceRunner",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "unsupported mouse button: \(button)"]
)
}
#else
throw NSError(
domain: "AgentDeviceRunner",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "mouseClick is only supported on macOS"]
)
#endif
}
func doubleTapAt(app: XCUIApplication, x: Double, y: Double) {
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
let coordinate = interactionCoordinate(app: app, x: x, y: y)
coordinate.doubleTap()

@@ -160,4 +200,3 @@ }

func longPressAt(app: XCUIApplication, x: Double, y: Double, duration: TimeInterval) {
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
let coordinate = interactionCoordinate(app: app, x: x, y: y)
coordinate.press(forDuration: duration)

@@ -174,5 +213,4 @@ }

) {
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let start = origin.withOffset(CGVector(dx: x, dy: y))
let end = origin.withOffset(CGVector(dx: x2, dy: y2))
let start = interactionCoordinate(app: app, x: x, y: y)
let end = interactionCoordinate(app: app, x: x2, y: y2)
start.press(forDuration: holdDuration, thenDragTo: end)

@@ -265,2 +303,38 @@ }

private func interactionRoot(app: XCUIApplication) -> XCUIElement {
let windows = app.windows.allElementsBoundByIndex
if let window = windows.first(where: { $0.exists && !$0.frame.isEmpty }) {
return window
}
return app
}
private func interactionCoordinate(app: XCUIApplication, x: Double, y: Double) -> XCUICoordinate {
let root = interactionRoot(app: app)
let origin = root.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let rootFrame = root.frame
let offsetX = x - Double(rootFrame.origin.x)
let offsetY = y - Double(rootFrame.origin.y)
return origin.withOffset(CGVector(dx: offsetX, dy: offsetY))
}
private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
let frame = element.frame
if !frame.isEmpty {
tapAt(app: app, x: frame.midX, y: frame.midY)
return
}
element.tap()
}
private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
let predicate = NSPredicate(
format: "identifier == %@ OR label == %@",
"go back",
"Back"
)
let element = app.descendants(matching: .any).matching(predicate).firstMatch
return element.exists ? element : nil
}
}
import XCTest
import UIKit
#if canImport(AppKit)
import AppKit
#endif
func runnerPngData(for image: RunnerImage) -> Data? {
#if canImport(UIKit)
return image.pngData()
#elseif canImport(AppKit)
guard let cgImage = runnerCGImage(from: image) else { return nil }
let bitmap = NSBitmapImageRep(cgImage: cgImage)
return bitmap.representation(using: .png, properties: [:])
#endif
}
func runnerCGImage(from image: RunnerImage) -> CGImage? {
#if canImport(UIKit)
return image.cgImage
#elseif canImport(AppKit)
return image.cgImage(forProposedRect: nil, context: nil, hints: nil)
#endif
}
extension RunnerTests {
// MARK: - Recording
func captureRunnerFrame() -> UIImage? {
var image: UIImage?
func captureRunnerFrame() -> RunnerImage? {
var image: RunnerImage?
let capture = {

@@ -32,2 +52,7 @@ let screenshot = XCUIScreen.main.screenshot()

func resolveRecordingOutPath(_ requestedOutPath: String) -> String {
#if os(macOS)
if requestedOutPath.hasPrefix("/") {
return requestedOutPath
}
#endif
let fileName = URL(fileURLWithPath: requestedOutPath).lastPathComponent

@@ -42,8 +67,15 @@ let fallbackName = "agent-device-recording-\(Int(Date().timeIntervalSince1970 * 1000)).mp4"

func targetNeedsActivation(_ target: XCUIApplication) -> Bool {
switch target.state {
case .unknown, .notRunning, .runningBackground, .runningBackgroundSuspended:
let state = target.state
#if os(macOS)
if state == .unknown || state == .notRunning || state == .runningBackground {
return true
default:
return false
}
#else
if state == .unknown || state == .notRunning || state == .runningBackground
|| state == .runningBackgroundSuspended
{
return true
}
#endif
return false
}

@@ -50,0 +82,0 @@

@@ -5,2 +5,3 @@ // MARK: - Wire Models

case tap
case mouseClick
case tapSeries

@@ -40,2 +41,3 @@ case longPress

let y: Double?
let button: String?
let count: Double?

@@ -42,0 +44,0 @@ let intervalMs: Double?

import AVFoundation
import CoreVideo
import UIKit

@@ -36,3 +35,3 @@ extension RunnerTests {

func start(captureFrame: @escaping () -> UIImage?) throws {
func start(captureFrame: @escaping () -> RunnerImage?) throws {
let url = URL(fileURLWithPath: outputPath)

@@ -50,6 +49,6 @@ let directory = url.deletingLastPathComponent()

var dimensions: CGSize = .zero
var bootstrapImage: UIImage?
var bootstrapImage: RunnerImage?
let bootstrapDeadline = Date().addingTimeInterval(2.0)
while Date() < bootstrapDeadline {
if let image = captureFrame(), let cgImage = image.cgImage {
if let image = captureFrame(), let cgImage = runnerCGImage(from: image) {
bootstrapImage = image

@@ -188,4 +187,4 @@ dimensions = CGSize(width: cgImage.width, height: cgImage.height)

private func append(image: UIImage) {
guard let cgImage = image.cgImage else { return }
private func append(image: RunnerImage) {
guard let cgImage = runnerCGImage(from: image) else { return }
lock.lock()

@@ -192,0 +191,0 @@ defer { lock.unlock() }

import XCTest
extension RunnerTests {
private struct SnapshotTraversalContext {
let rootSnapshot: XCUIElementSnapshot
let viewport: CGRect
let flatSnapshots: [XCUIElementSnapshot]
let snapshotRanges: [ObjectIdentifier: (Int, Int)]
let maxDepth: Int
}
private struct SnapshotEvaluation {
let label: String
let identifier: String
let valueText: String?
let hittable: Bool
let visible: Bool
}
// MARK: - Snapshot Entry

@@ -51,46 +67,15 @@

guard let context = makeSnapshotTraversalContext(app: app, options: options) else {
return DataPayload(nodes: [], truncated: false)
}
var nodes: [SnapshotNode] = []
var truncated = false
let maxDepth = options.depth ?? Int.max
let viewport = app.frame
let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
let rootSnapshot: XCUIElementSnapshot
do {
rootSnapshot = try queryRoot.snapshot()
} catch {
return DataPayload(nodes: nodes, truncated: truncated)
}
let (flatSnapshots, snapshotRanges) = flattenedSnapshots(rootSnapshot)
let rootLaterNodes = laterSnapshots(
for: rootSnapshot,
in: flatSnapshots,
ranges: snapshotRanges
)
let rootLabel = aggregatedLabel(for: rootSnapshot) ?? rootSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
let rootIdentifier = rootSnapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
let rootValue = snapshotValueText(rootSnapshot)
let rootHittable = computedSnapshotHittable(rootSnapshot, viewport: viewport, laterNodes: rootLaterNodes)
let rootEvaluation = evaluateSnapshot(context.rootSnapshot, in: context)
nodes.append(
SnapshotNode(
index: 0,
type: elementTypeName(rootSnapshot.elementType),
label: rootLabel.isEmpty ? nil : rootLabel,
identifier: rootIdentifier.isEmpty ? nil : rootIdentifier,
value: rootValue,
rect: SnapshotRect(
x: Double(rootSnapshot.frame.origin.x),
y: Double(rootSnapshot.frame.origin.y),
width: Double(rootSnapshot.frame.size.width),
height: Double(rootSnapshot.frame.size.height)
),
enabled: rootSnapshot.isEnabled,
hittable: rootHittable,
depth: 0
)
makeSnapshotNode(snapshot: context.rootSnapshot, evaluation: rootEvaluation, depth: 0, index: 0)
)
var seen = Set<String>()
var stack: [(XCUIElementSnapshot, Int, Int)] = rootSnapshot.children.map { ($0, 1, 1) }
var stack: [(XCUIElementSnapshot, Int, Int)] = context.rootSnapshot.children.map { ($0, 1, 1) }

@@ -104,26 +89,14 @@ while let (snapshot, depth, visibleDepth) = stack.popLast() {

let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
let valueText = snapshotValueText(snapshot)
let laterNodes = laterSnapshots(
for: snapshot,
in: flatSnapshots,
ranges: snapshotRanges
)
let hittable = computedSnapshotHittable(snapshot, viewport: viewport, laterNodes: laterNodes)
let hasContent = !label.isEmpty || !identifier.isEmpty || (valueText != nil)
if !isVisibleInViewport(snapshot.frame, viewport) && !hasContent {
continue
}
let evaluation = evaluateSnapshot(snapshot, in: context)
let include = shouldInclude(
snapshot: snapshot,
label: label,
identifier: identifier,
valueText: valueText,
label: evaluation.label,
identifier: evaluation.identifier,
valueText: evaluation.valueText,
options: options,
hittable: hittable
hittable: evaluation.hittable,
visible: evaluation.visible
)
let key = "\(snapshot.elementType)-\(label)-\(identifier)-\(snapshot.frame.origin.x)-\(snapshot.frame.origin.y)"
let key = "\(snapshot.elementType)-\(evaluation.label)-\(evaluation.identifier)-\(snapshot.frame.origin.x)-\(snapshot.frame.origin.y)"
let isDuplicate = seen.contains(key)

@@ -134,3 +107,3 @@ if !isDuplicate {

if depth < maxDepth {
if depth < context.maxDepth {
let nextVisibleDepth = include && !isDuplicate ? visibleDepth + 1 : visibleDepth

@@ -145,17 +118,7 @@ for child in snapshot.children.reversed() {

nodes.append(
SnapshotNode(
index: nodes.count,
type: elementTypeName(snapshot.elementType),
label: label.isEmpty ? nil : label,
identifier: identifier.isEmpty ? nil : identifier,
value: valueText,
rect: SnapshotRect(
x: Double(snapshot.frame.origin.x),
y: Double(snapshot.frame.origin.y),
width: Double(snapshot.frame.size.width),
height: Double(snapshot.frame.size.height)
),
enabled: snapshot.isEnabled,
hittable: hittable,
depth: min(maxDepth, visibleDepth)
makeSnapshotNode(
snapshot: snapshot,
evaluation: evaluation,
depth: min(context.maxDepth, visibleDepth),
index: nodes.count
)

@@ -174,16 +137,9 @@ )

let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
guard let context = makeSnapshotTraversalContext(app: app, options: options) else {
return DataPayload(nodes: [], truncated: false)
}
var nodes: [SnapshotNode] = []
var truncated = false
let viewport = app.frame
let rootSnapshot: XCUIElementSnapshot
do {
rootSnapshot = try queryRoot.snapshot()
} catch {
return DataPayload(nodes: nodes, truncated: truncated)
}
let (flatSnapshots, snapshotRanges) = flattenedSnapshots(rootSnapshot)
func walk(_ snapshot: XCUIElementSnapshot, depth: Int) {

@@ -195,33 +151,15 @@ if nodes.count >= maxSnapshotElements {

if let limit = options.depth, depth > limit { return }
if !isVisibleInViewport(snapshot.frame, viewport) { return }
let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
let valueText = snapshotValueText(snapshot)
let laterNodes = laterSnapshots(
for: snapshot,
in: flatSnapshots,
ranges: snapshotRanges
)
let hittable = computedSnapshotHittable(snapshot, viewport: viewport, laterNodes: laterNodes)
let evaluation = evaluateSnapshot(snapshot, in: context)
if shouldInclude(
snapshot: snapshot,
label: label,
identifier: identifier,
valueText: valueText,
label: evaluation.label,
identifier: evaluation.identifier,
valueText: evaluation.valueText,
options: options,
hittable: hittable
hittable: evaluation.hittable,
visible: evaluation.visible
) {
nodes.append(
SnapshotNode(
index: nodes.count,
type: elementTypeName(snapshot.elementType),
label: label.isEmpty ? nil : label,
identifier: identifier.isEmpty ? nil : identifier,
value: valueText,
rect: snapshotRect(from: snapshot.frame),
enabled: snapshot.isEnabled,
hittable: hittable,
depth: depth
)
makeSnapshotNode(snapshot: snapshot, evaluation: evaluation, depth: depth, index: nodes.count)
)

@@ -237,3 +175,3 @@ }

walk(rootSnapshot, depth: 0)
walk(context.rootSnapshot, depth: 0)
return DataPayload(nodes: nodes, truncated: truncated)

@@ -259,3 +197,4 @@ }

options: SnapshotOptions,
hittable: Bool
hittable: Bool,
visible: Bool
) -> Bool {

@@ -268,2 +207,7 @@ let type = snapshot.elementType

if options.interactiveOnly {
#if os(macOS)
if !visible && type != .application {
return false
}
#endif
if interactiveTypes.contains(type) { return true }

@@ -299,2 +243,66 @@ if hittable && type != .other { return true }

private func makeSnapshotTraversalContext(
app: XCUIApplication,
options: SnapshotOptions
) -> SnapshotTraversalContext? {
let viewport = snapshotViewport(app: app)
let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
let rootSnapshot: XCUIElementSnapshot
do {
rootSnapshot = try queryRoot.snapshot()
} catch {
return nil
}
let (flatSnapshots, snapshotRanges) = flattenedSnapshots(rootSnapshot)
return SnapshotTraversalContext(
rootSnapshot: rootSnapshot,
viewport: viewport,
flatSnapshots: flatSnapshots,
snapshotRanges: snapshotRanges,
maxDepth: options.depth ?? Int.max
)
}
private func evaluateSnapshot(
_ snapshot: XCUIElementSnapshot,
in context: SnapshotTraversalContext
) -> SnapshotEvaluation {
let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
let valueText = snapshotValueText(snapshot)
let laterNodes = laterSnapshots(
for: snapshot,
in: context.flatSnapshots,
ranges: context.snapshotRanges
)
return SnapshotEvaluation(
label: label,
identifier: identifier,
valueText: valueText,
hittable: computedSnapshotHittable(snapshot, viewport: context.viewport, laterNodes: laterNodes),
visible: isVisibleInViewport(snapshot.frame, context.viewport)
)
}
private func makeSnapshotNode(
snapshot: XCUIElementSnapshot,
evaluation: SnapshotEvaluation,
depth: Int,
index: Int
) -> SnapshotNode {
return SnapshotNode(
index: index,
type: elementTypeName(snapshot.elementType),
label: evaluation.label.isEmpty ? nil : evaluation.label,
identifier: evaluation.identifier.isEmpty ? nil : evaluation.identifier,
value: evaluation.valueText,
rect: snapshotRect(from: snapshot.frame),
enabled: snapshot.isEnabled,
hittable: evaluation.hittable,
depth: depth
)
}
private func isOccludingType(_ type: XCUIElement.ElementType) -> Bool {

@@ -352,2 +360,14 @@ switch type {

private func snapshotViewport(app: XCUIApplication) -> CGRect {
let windows = app.windows.allElementsBoundByIndex
if let window = windows.first(where: { $0.exists && !$0.frame.isNull && !$0.frame.isEmpty }) {
return window.frame
}
let appFrame = app.frame
if !appFrame.isNull && !appFrame.isEmpty {
return appFrame
}
return .infinite
}
private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {

@@ -354,0 +374,0 @@ if depth > 4 { return nil }

@@ -7,2 +7,5 @@ import XCTest

func blockingSystemAlertSnapshot() -> DataPayload? {
#if os(macOS)
return nil
#else
guard let modal = firstBlockingSystemModal(in: springboard) else {

@@ -44,2 +47,3 @@ return nil

return DataPayload(nodes: nodes, truncated: false)
#endif
}

@@ -46,0 +50,0 @@

{
"name": "agent-device",
"version": "0.9.0",
"version": "0.10.0",
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",

@@ -26,4 +26,5 @@ "license": "MIT",

"build:node": "pnpm build && pnpm clean:daemon",
"build:xcuitest": "pnpm build:xcuitest:ios",
"build:xcuitest": "pnpm build:xcuitest:ios && pnpm build:xcuitest:macos",
"build:xcuitest:ios": "rm -rf ~/.agent-device/ios-runner/derived/device && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
"build:xcuitest:macos": "rm -rf ~/.agent-device/ios-runner/derived/macos && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"platform=macOS,arch=$(uname -m)\" -derivedDataPath ~/.agent-device/ios-runner/derived/macos CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=\"\" DEVELOPMENT_TEAM=\"\" COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO",
"build:xcuitest:tvos": "rm -rf ~/.agent-device/ios-runner/derived/tvos && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=tvOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived/tvos",

@@ -30,0 +31,0 @@ "build:all": "pnpm build:node && pnpm build:xcuitest",

+103
-19

@@ -11,17 +11,54 @@ <a href="https://www.callstack.com/open-source?utm_campaign=generic&utm_source=github&utm_medium=referral&utm_content=agent-device" align="center">

CLI to control iOS and Android devices for AI agents influenced by Vercel’s [agent-browser](https://github.com/vercel-labs/agent-browser).
`agent-device` is a CLI for UI automation on iOS, tvOS, macOS, Android, and AndroidTV. It is designed for agent-driven workflows: inspect the UI, act on it deterministically, and keep that work session-aware and replayable.
The project is in early development and considered experimental. Pull requests are welcome!
If you know Vercel's [agent-browser](https://github.com/vercel-labs/agent-browser), this project applies the same broad idea to mobile apps and devices.
## Features
- Platforms: iOS/tvOS (simulator + physical device core automation) and Android/AndroidTV (emulator + device).
- Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`, `install`, `install-from-source`, `reinstall`, `push`, `trigger-app-event`.
- Inspection commands: `snapshot` (accessibility tree), `diff snapshot` (structural baseline diff), `appstate`, `apps`, `devices`.
- Clipboard commands: `clipboard read`, `clipboard write <text>`.
- Keyboard commands: `keyboard status|get|dismiss` (Android).
- Performance command: `perf` (alias: `metrics`) returns a metrics JSON blob for the active session; startup timing is currently sampled.
- App logs and traffic inspection: `logs path` returns session log metadata; `logs start` / `logs stop` stream app output; `logs clear` truncates session app logs; `logs clear --restart` resets and restarts stream in one step; `logs doctor` checks readiness; `logs mark` writes timeline markers; `network dump` parses recent HTTP(s) entries from session logs.
- Device tooling: `adb` (Android), `simctl`/`devicectl` (iOS via Xcode).
- Minimal dependencies; TypeScript executed directly on Node 22+ (no build step).
<video src="https://github.com/user-attachments/assets/db81d164-c179-4e68-97fa-53f06e467211" controls muted playsinline></video>
## Project Goals
- Give agents a practical way to understand mobile UI state through structured snapshots.
- Keep automation flows token-efficient enough for real agent loops.
- Make common interactions reliable enough for repeated automation runs.
- Keep automation grounded in sessions, selectors, and replayable flows instead of one-off scripts.
## Core Ideas
- Sessions: open a target once, interact within that session, then close it cleanly.
- Snapshots: inspect the current accessibility tree in a compact form and get stable refs for exploration.
- Refs vs selectors: use refs for discovery, use selectors for durable replay and assertions.
- Human docs vs agent skills: docs explain the system for people; skills provide compact operating guidance for agents.
## Command Flow
The canonical loop is:
```bash
agent-device open SampleApp --platform ios
agent-device snapshot -i
agent-device press @e3
agent-device diff snapshot -i
agent-device fill @e5 "test"
agent-device close
```
In practice, most work follows the same pattern:
1. `open` a target app or URL.
2. `snapshot -i` to inspect the current screen.
3. `press`, `fill`, `scroll`, `get`, or `wait` using refs or selectors.
4. `diff snapshot` or re-snapshot after UI changes.
5. `close` when the session is finished.
## Where To Go Next
For people:
- [Website](https://agent-device.dev/)
- [Docs](https://incubator.callstack.com/agent-device/docs/introduction)
For agents:
- [agent-device skill](skills/agent-device/SKILL.md)
- [dogfood skill](skills/dogfood/SKILL.md)
## Install

@@ -117,2 +154,3 @@

Config file lookup order:
- `~/.agent-device/config.json`

@@ -138,2 +176,3 @@ - `./agent-device.json`

Notes:
- Config keys use the existing camelCase flag names, for example `stateDir`, `daemonAuthToken`, `iosSimulatorDeviceSet`, and `androidDeviceAllowlist`.

@@ -165,2 +204,3 @@ - Environment overrides use `AGENT_DEVICE_*` uppercase snake case names, for example `AGENT_DEVICE_SESSION`, `AGENT_DEVICE_DAEMON_BASE_URL`, and `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET`.

Coordinates:
- All coordinate-based commands (`press`, `longpress`, `swipe`, `focus`, `fill`) use device coordinates with origin at top-left.

@@ -170,2 +210,3 @@ - X increases to the right, Y increases downward.

- `click` is an equivalent alias and accepts the same targets (`x y`, `@ref`, selector) and flags.
- `click --secondary` performs a secondary click on macOS, which is useful for opening context menus before a follow-up `snapshot -i`.

@@ -184,2 +225,3 @@ Gesture series examples:

## Command Index
- `boot`, `open`, `close`, `install`, `reinstall`, `home`, `back`, `app-switcher`

@@ -194,3 +236,3 @@ - `push`

- `logs path`, `logs start`, `logs stop`, `logs clear`, `logs clear --restart`, `logs doctor`, `logs mark` (session app log file for grep; iOS simulator + iOS device + Android)
- `clipboard read`, `clipboard write <text>` (iOS simulator + Android)
- `clipboard read`, `clipboard write <text>` (macOS + iOS simulator + Android)
- `keyboard [status|get|dismiss]` (Android emulator/device)

@@ -221,2 +263,3 @@ - `network dump [limit] [summary|headers|body|all]`, `network log ...` (best-effort HTTP(s) parsing from session app log)

Payload notes:
- iOS uses `xcrun simctl push <device> <bundle> <payload>` and requires APNs-style JSON object (for example `{"aps":{"alert":"..."}}`).

@@ -236,2 +279,3 @@ - Android uses `adb shell am broadcast` with payload JSON shape:

- `trigger-app-event` requires either an active session or explicit device selectors (`--platform`, `--device`, `--udid`, `--serial`).
- On macOS, use `AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE` to override the desktop deep-link template.
- On iOS physical devices, custom-scheme deep links require active app context (open the app in-session first).

@@ -241,2 +285,3 @@ - Configure one of:

- `AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE`
- `AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE`
- `AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE`

@@ -252,2 +297,3 @@ - Template placeholders: `{event}`, `{payload}`, `{platform}`.

Notes:
- iOS snapshots use XCTest on simulators and physical devices.

@@ -259,2 +305,3 @@ - Scope snapshots with `-s "<label>"` or `-s @ref`.

Diff snapshots:
- Run `diff snapshot` once to initialize baseline for the current session.

@@ -265,2 +312,3 @@ - Run `diff snapshot` again after UI changes to get unified-style output (`-` removed, `+` added, unchanged context).

Efficient snapshot usage:
- Default to `snapshot -i` for iterative agent loops.

@@ -274,5 +322,6 @@ - Add `-s "<label>"` (or `-s @ref`) for screen-local work to reduce payload size.

Flags:
- `--version, -V` print version and exit
- `--platform ios|android|apple` (`apple` aliases the iOS/tvOS backend)
- `--target mobile|tv` select device class within platform (requires `--platform`; for example AndroidTV/tvOS)
- `--platform ios|macos|android|apple` (`apple` aliases the Apple automation backend)
- `--target mobile|tv|desktop` select device class within platform (requires `--platform`; for example AndroidTV/tvOS/macOS)
- `--device <name>`

@@ -309,2 +358,3 @@ - `--udid <udid>` (iOS)

Isolation precedence:
- Discovery scope (`--ios-simulator-device-set`, `--android-device-allowlist`) is applied before selector matching (`--device`, `--udid`, `--serial`).

@@ -315,2 +365,3 @@ - If a selector points outside the scoped set/allowlist, command resolution fails with `DEVICE_NOT_FOUND` (no host-global fallback).

TV targets:
- Use `--target tv` together with `--platform ios|android|apple`.

@@ -323,3 +374,12 @@ - TV target selection supports both simulator/emulator and connected physical devices (AppleTV + AndroidTV).

Desktop targets:
- Use `--platform macos` for the host Mac, or `--platform apple --target desktop` when selecting through the Apple family alias.
- macOS uses the same runner-driven interaction/snapshot flow as iOS/tvOS for `open`, `appstate`, `snapshot`, `press`, `fill`, `scroll`, `back`, `screenshot`, `record`, and selector-based commands.
- macOS also supports `clipboard read|write`, `trigger-app-event`, and only `settings appearance light|dark|toggle`.
- Prefer selector or `@ref`-driven interactions on macOS. Window position is not stable, so raw x/y commands are more fragile than snapshot-derived refs.
- Mobile-only helpers remain unsupported on macOS: `boot`, `home`, `app-switcher`, `install`, `reinstall`, `install-from-source`, `push`, `logs`, and `network`.
Examples:
- `agent-device open YouTube --platform android --target tv`

@@ -329,4 +389,7 @@ - `agent-device apps --platform android --target tv`

- `agent-device screenshot ./apple-tv.png --platform ios --target tv`
- `agent-device open TextEdit --platform macos`
- `agent-device snapshot -i --platform apple --target desktop`
Pinch:
- `pinch` is supported on iOS simulators (including tvOS simulator targets).

@@ -336,2 +399,3 @@ - On Android, `pinch` currently returns `UNSUPPORTED_OPERATION` in the adb backend.

Swipe timing:
- `swipe` accepts optional `durationMs` (default `250`, range `16..10000`).

@@ -343,2 +407,3 @@ - Android uses requested swipe duration directly.

## Skills
Install the automation skills listed in [SKILL.md](skills/agent-device/SKILL.md).

@@ -351,2 +416,3 @@

Sessions:
- `open` starts a session. Without args boots/activates the target device/simulator without launching an app.

@@ -364,2 +430,3 @@ - All interaction commands require an open session.

Navigation helpers:
- `boot --platform ios|android|apple` ensures the target is ready without launching an app.

@@ -384,2 +451,3 @@ - Use `boot` mainly when starting a new session and `open` fails because no booted simulator/emulator is available.

Deep links:
- `open <url>` supports deep links with `scheme://...`.

@@ -400,2 +468,3 @@ - `open <app> <url>` opens a deep link on iOS.

Find (semantic):
- `find <text> <action> [value]` finds by any text (label/value/identifier) using a scoped snapshot.

@@ -406,2 +475,3 @@ - `find text|label|value|role|id <value> <action> [value]` for specific locators.

Assertions:
- `is` predicates: `visible`, `hidden`, `exists`, `editable`, `selected`, `text`.

@@ -411,2 +481,3 @@ - `is text` uses exact equality.

Performance metrics:
- `perf` (or `metrics`) requires an active session and returns a JSON metrics blob.

@@ -431,2 +502,3 @@ - Current metric: `startup` sampled from the elapsed wall-clock time around each session `open` command dispatch (`open-command-roundtrip`), unit `ms`.

Replay update:
- `replay <path>` runs deterministic replay from `.ad` scripts.

@@ -459,2 +531,3 @@ - `replay -u <path>` attempts selector updates on failures and atomically rewrites the same file.

Android fill reliability:
- `fill` clears the current value, then enters text.

@@ -474,6 +547,7 @@ - `type` enters text into the focused field without clearing.

Settings helpers:
- `settings wifi on|off`
- `settings airplane on|off`
- `settings location on|off` (iOS uses per-app permission for the current session app)
- `settings appearance light|dark|toggle` (iOS simulator appearance + Android night mode)
- `settings appearance light|dark|toggle` (macOS appearance + iOS simulator appearance + Android night mode)
- `settings faceid|touchid match|nonmatch|enroll|unenroll` (iOS simulator only)

@@ -483,3 +557,3 @@ - `settings fingerprint match|nonmatch` (Android emulator/device where supported)

- `settings permission grant|deny|reset <camera|microphone|photos|contacts|notifications> [full|limited]` (session app required)
Note: iOS supports these only on simulators. iOS wifi/airplane toggles status bar indicators, not actual network state. Airplane off clears status bar overrides.
Note: iOS supports these only on simulators. On macOS, only `settings appearance` is supported. iOS wifi/airplane toggles status bar indicators, not actual network state. Airplane off clears status bar overrides.
- iOS permission targets map to `simctl privacy`: `camera`, `microphone`, `photos` (`full` => `photos`, `limited` => `photos-add`), `contacts`, `notifications`.

@@ -490,2 +564,3 @@ - Android permission targets: `camera`, `microphone`, `photos`, `contacts` use `pm grant|revoke` (`reset` maps to `pm revoke`); `notifications` uses `appops set POST_NOTIFICATION allow|deny|default`.

App state:
- `appstate` shows the foreground app/activity (Android).

@@ -496,8 +571,10 @@ - On iOS, `appstate` returns the currently tracked session app (`source: session`) and requires an active session on the selected device.

Clipboard:
- `clipboard read` returns current clipboard text.
- `clipboard write <text>` sets clipboard text (`clipboard write ""` clears it).
- Supported on Android emulator/device and iOS simulator.
- Supported on macOS, Android emulator/device, and iOS simulator.
- iOS physical devices currently return `UNSUPPORTED_OPERATION` for clipboard commands.
Keyboard:
- `keyboard status` (or `keyboard get`) reports Android keyboard visibility and best-effort input type classification (`text`, `number`, `email`, `phone`, `password`, `datetime`).

@@ -527,2 +604,3 @@ - `keyboard dismiss` issues Android back keyevent only when keyboard is visible, then verifies hidden state.

Boot diagnostics:
- Boot failures include normalized reason codes in `error.details.reason` (JSON mode) and verbose logs.

@@ -538,2 +616,3 @@ - Reason codes: `IOS_BOOT_TIMEOUT`, `IOS_RUNNER_CONNECT_TIMEOUT`, `ANDROID_BOOT_TIMEOUT`, `ADB_TRANSPORT_UNAVAILABLE`, `CI_RESOURCE_STARVATION_SUSPECTED`, `BOOT_COMMAND_FAILED`, `UNKNOWN`.

Diagnostics files:
- Failed commands persist diagnostics in `~/.agent-device/logs/<session>/<date>/<timestamp>-<diagnosticId>.ndjson`.

@@ -544,2 +623,3 @@ - `--debug` persists diagnostics for successful commands too and streams live diagnostic events.

## App resolution
- Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).

@@ -550,2 +630,3 @@ - Human-readable names are resolved when possible (e.g., `Settings`).

## iOS notes
- Core runner commands: `snapshot`, `wait`, `click`, `fill`, `get`, `is`, `find`, `press`, `longpress`, `focus`, `type`, `scroll`, `scrollintoview`, `back`, `home`, `app-switcher`.

@@ -585,2 +666,3 @@ - Simulator-only commands: `alert`, `pinch`, `settings`.

Environment selectors:
- `ANDROID_DEVICE=Pixel_9_Pro_XL` or `ANDROID_SERIAL=emulator-5554`

@@ -627,2 +709,3 @@ - `IOS_DEVICE="iPhone 17 Pro"` or `IOS_UDID=<udid>`

Test screenshots are written to:
- `test/screenshots/android-settings.png`

@@ -632,6 +715,7 @@ - `test/screenshots/ios-settings.png`

## Contributing
See `CONTRIBUTING.md`.
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Made at Callstack
agent-device is an open source project and will always remain free to use. Callstack is a group of React and React Native geeks. Contact us at hello@callstack.com if you need any help with these technologies or just want to say hi.

@@ -1,2 +0,2 @@

# Snapshot Refs and Selectors (Mobile)
# Snapshot Refs and Selectors

@@ -7,2 +7,3 @@ ## Purpose

For tap interactions, `press` is canonical; `click` is an equivalent alias.
For host Mac desktop apps, pair this reference with [macos-desktop.md](macos-desktop.md) because context menus and native list/table structures need desktop-specific handling.

@@ -33,2 +34,9 @@ ## Snapshot

On macOS, if actions live in a context menu, use:
```bash
agent-device click @e5 --button secondary --platform macos
agent-device snapshot -i
```
## Using selectors (deterministic)

@@ -76,3 +84,4 @@

- Ref not found: re-snapshot.
- If XCTest returns 0 nodes, foreground app state may have changed. Re-open the app or retry after state is stable.
- If `snapshot` returns 0 nodes, foreground app state or accessibility state may have changed. Re-open the app or retry after state is stable.
- On macOS, use `snapshot --raw --platform macos` to distinguish collector filtering from truly missing AX content.

@@ -79,0 +88,0 @@ ## Stop Conditions

---
name: agent-device
description: Automates interactions for iOS simulators/devices and Android emulators/devices. Use when navigating apps, taking snapshots/screenshots, tapping, typing, scrolling, or extracting UI info on mobile targets.
description: Automates interactions for Apple-platform apps (iOS, tvOS, macOS) and Android devices. Use when navigating apps, taking snapshots/screenshots, tapping, typing, scrolling, or extracting UI info across mobile, TV, and desktop targets.
---
# Mobile Automation with agent-device
# Apple and Android Automation with agent-device

@@ -25,7 +25,8 @@ For exploration, use snapshot refs. For deterministic replay, use selectors.

- No target context yet: `devices` -> pick target -> `open`.
- Normal UI task: `open` -> `snapshot -i` -> `press/fill` -> `diff snapshot -i` -> `close`
- Debug/crash: `open <app>` -> `logs clear --restart` -> reproduce -> `network dump` -> `logs path` -> targeted `grep`
- Normal UI task: `open` -> `snapshot -i` -> `press/click/fill` -> `diff snapshot -i` -> `close`
- Debug/crash (iOS/Android): `open <app>` -> `logs clear --restart` -> reproduce -> `network dump` -> `logs path` -> targeted `grep`
- Replay drift: `replay -u <path>` -> verify updated selectors
- Remote multi-tenant run: allocate lease -> point client at remote daemon base URL -> run commands with tenant isolation flags -> heartbeat/release lease
- Device-scope isolation run: set iOS simulator set / Android allowlist -> run selectors within scope only
- macOS desktop task: run the macOS desktop flow, then open [references/macos-desktop.md](references/macos-desktop.md) if context menus, Finder rows, or desktop-specific snapshot behavior matters

@@ -36,2 +37,3 @@ ## Target Selection Rules

- iOS local QA in mixed simulator/device environments: run `ensure-simulator` first and pass `--device`, `--udid`, or `--ios-simulator-device-set` on later commands.
- macOS desktop app automation: use `--platform macos`, or `--platform apple --target desktop` when the caller wants one Apple-family selector path.
- Android local QA: use `install` or `reinstall` for `.apk`/`.aab` files, then relaunch by installed package name.

@@ -108,2 +110,16 @@ - Android React Native + Metro flows: prefer `open <package> --remote-config <path> --relaunch`.

### 1e) macOS Desktop Flow
```bash
agent-device open TextEdit --platform macos
agent-device snapshot -i
agent-device fill @e3 "desktop smoke test"
agent-device screenshot /tmp/macos-textedit.png
agent-device close
```
Use this for host Mac desktop apps. Prefer the Apple runner interaction flow (`open`, `snapshot`, `press`, `click`, `fill`, `scroll`, `back`, `record`, `screenshot`). macOS also supports `clipboard read|write`, `trigger-app-event` when a desktop deep-link template is configured, and only `settings appearance light|dark|toggle` under the `settings` command. Do not rely on mobile-only helpers like `install`, `push`, `logs`, or `network` on macOS.
Prefer selectors or snapshot refs (`@e...`) over raw x/y commands on macOS because the window origin can move between runs.
Open [references/macos-desktop.md](references/macos-desktop.md) when you need Finder-style list traversal, context-menu flows, or macOS-specific snapshot expectations.
### 2) Debug/Crash Flow

@@ -230,2 +246,4 @@

`press` is canonical tap command; `click` is an alias.
On macOS, use `click --button secondary <@ref|selector>` to open a context menu before the next `snapshot -i`.
For desktop-specific heuristics and Finder guidance, see [references/macos-desktop.md](references/macos-desktop.md).

@@ -277,3 +295,3 @@ ### Utilities

- iOS `open` responses include `device_udid` and `ios_simulator_device_set` to confirm which simulator handled the session.
- Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on Android and iOS simulators; iOS physical devices are not supported yet.
- Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on macOS, Android, and iOS simulators; iOS physical devices are not supported yet.
- Android keyboard helpers: `keyboard status|get|dismiss` report keyboard visibility/type and dismiss via keyevent when visible.

@@ -287,2 +305,3 @@ - `network dump` is best-effort and parses HTTP(s) entries from the session app log file.

- `trigger-app-event` requires app-defined deep-link hooks and URL template configuration (`AGENT_DEVICE_APP_EVENT_URL_TEMPLATE` or platform-specific variants).
- On macOS, set `AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE` when the desktop app uses a different deep-link template than iOS/Android.
- `trigger-app-event` requires an active session or explicit selectors (`--platform`, `--device`, `--udid`, `--serial`); on iOS physical devices, custom-scheme triggers require active app context.

@@ -327,2 +346,3 @@ - Canonical trigger behavior and caveats are documented in [`website/docs/docs/commands.md`](../../website/docs/docs/commands.md) under **App event triggers**.

- [references/snapshot-refs.md](references/snapshot-refs.md)
- [references/macos-desktop.md](references/macos-desktop.md)
- [references/logs-and-debug.md](references/logs-and-debug.md)

@@ -329,0 +349,0 @@ - [references/session-management.md](references/session-management.md)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet