agent-device
Advanced tools
| 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}; |
+2
-2
@@ -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 @@ |
+3
-2
| { | ||
| "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
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 53 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 53 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
963382
5.35%219
11.73%4647
8.4%683
14.02%100
12.36%