@@ -20,7 +20,8 @@ import {
2020 describeElement ,
2121 computeSwipeEnd ,
2222} from '../utils/element.js' ;
23- import { execStrict , exec , isCommandAvailable } from '../utils/exec.js' ;
23+ import { execStrict , exec } from '../utils/exec.js' ;
24+ import { resolveIdbPath } from '../utils/platform.js' ;
2425
2526export class IdbBackend implements DeviceBackend {
2627 readonly platform = 'ios' as const ;
@@ -29,6 +30,8 @@ export class IdbBackend implements DeviceBackend {
2930
3031 readonly #udid: string ;
3132
33+ #idbPath = 'idb' ;
34+
3235 #recordingProcess: ChildProcess | null = null ;
3336
3437 #recordingPath: string | null = null ;
@@ -42,20 +45,14 @@ export class IdbBackend implements DeviceBackend {
4245 return ;
4346 }
4447
45- if ( ! ( await isCommandAvailable ( 'idb' ) ) ) {
46- throw new Error (
47- 'idb is not installed.\n' +
48- 'Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb' ,
49- ) ;
50- }
51-
52- await exec ( 'idb' , [ 'connect' , this . #udid] ) ;
48+ this . #idbPath = await resolveIdbPath ( ) ;
49+ await exec ( this . #idbPath, [ 'connect' , this . #udid] ) ;
5350 this . #connected = true ;
5451 }
5552
5653 async getDeviceInfo ( ) : Promise < DeviceInfo > {
5754 await this . ensureConnected ( ) ;
58- const raw = await execStrict ( 'idb' , [
55+ const raw = await execStrict ( this . #idbPath , [
5956 'describe' ,
6057 '--udid' ,
6158 this . #udid,
@@ -80,7 +77,7 @@ export class IdbBackend implements DeviceBackend {
8077
8178 async snapshot ( ) : Promise < SnapshotResult > {
8279 await this . ensureConnected ( ) ;
83- const raw = await execStrict ( 'idb' , [
80+ const raw = await execStrict ( this . #idbPath , [
8481 'ui' ,
8582 'describe-all' ,
8683 '--udid' ,
@@ -120,7 +117,7 @@ export class IdbBackend implements DeviceBackend {
120117
121118 async tapCoordinates ( x : number , y : number ) : Promise < void > {
122119 await this . ensureConnected ( ) ;
123- await execStrict ( 'idb' , [
120+ await execStrict ( this . #idbPath , [
124121 'ui' ,
125122 'tap' ,
126123 String ( x ) ,
@@ -132,16 +129,16 @@ export class IdbBackend implements DeviceBackend {
132129
133130 async typeText ( text : string ) : Promise < void > {
134131 await this . ensureConnected ( ) ;
135- await exec ( 'idb' , [
132+ await exec ( this . #idbPath , [
136133 'ui' ,
137134 'key-sequence' ,
138135 '--' ,
139136 'cmd+a' ,
140137 '--udid' ,
141138 this . #udid,
142139 ] ) ;
143- await exec ( 'idb' , [ 'ui' , 'key' , 'DELETE' , '--udid' , this . #udid] ) ;
144- await execStrict ( 'idb' , [ 'ui' , 'text' , text , '--udid' , this . #udid] ) ;
140+ await exec ( this . #idbPath , [ 'ui' , 'key' , 'DELETE' , '--udid' , this . #udid] ) ;
141+ await execStrict ( this . #idbPath , [ 'ui' , 'text' , text , '--udid' , this . #udid] ) ;
145142 }
146143
147144 async swipe (
@@ -156,7 +153,7 @@ export class IdbBackend implements DeviceBackend {
156153 const sy = startY ?? 400 ;
157154 const [ endX , endY ] = computeSwipeEnd ( sx , sy , direction , d ) ;
158155
159- await execStrict ( 'idb' , [
156+ await execStrict ( this . #idbPath , [
160157 'ui' ,
161158 'swipe' ,
162159 String ( sx ) ,
@@ -189,38 +186,72 @@ export class IdbBackend implements DeviceBackend {
189186
190187 async getAppState ( bundleId : string ) : Promise < AppStateResult > {
191188 await this . ensureConnected ( ) ;
192- const raw = await execStrict ( 'idb' , [ 'list-apps' , '--udid' , this . #udid] ) ;
193-
194- for ( const line of raw . trim ( ) . split ( '\n' ) ) {
195- if ( line . includes ( bundleId ) ) {
196- const parts = line . split ( '|' ) . map ( ( s ) => s . trim ( ) ) ;
197- return {
198- bundleId,
199- state : parts [ 2 ] ?? 'Unknown' ,
200- pid : parts [ 3 ] ? parseInt ( parts [ 3 ] , 10 ) : undefined ,
201- } ;
189+ try {
190+ const raw = await execStrict ( this . #idbPath, [
191+ 'list-apps' ,
192+ '--udid' ,
193+ this . #udid,
194+ ] ) ;
195+
196+ for ( const line of raw . trim ( ) . split ( '\n' ) ) {
197+ if ( line . includes ( bundleId ) ) {
198+ const parts = line . split ( '|' ) . map ( ( s ) => s . trim ( ) ) ;
199+ return {
200+ bundleId,
201+ state : parts [ 2 ] ?? 'Unknown' ,
202+ pid : parts [ 3 ] ? parseInt ( parts [ 3 ] , 10 ) : undefined ,
203+ } ;
204+ }
202205 }
206+
207+ return { bundleId, state : 'Not Installed' } ;
208+ } catch {
209+ return this . #getAppStateViaSimctl( bundleId ) ;
203210 }
211+ }
204212
213+ async #getAppStateViaSimctl( bundleId : string ) : Promise < AppStateResult > {
214+ const result = await exec ( 'xcrun' , [ 'simctl' , 'listapps' , this . #udid] ) ;
215+ if ( result . exitCode === 0 && result . stdout . includes ( bundleId ) ) {
216+ return { bundleId, state : 'Running' } ;
217+ }
205218 return { bundleId, state : 'Not Installed' } ;
206219 }
207220
208221 async screenshot ( outputPath ?: string ) : Promise < ScreenshotResult > {
209222 await this . ensureConnected ( ) ;
210223 const path = outputPath ?? `/tmp/device-mcp-screenshot-${ Date . now ( ) } .png` ;
211- await execStrict ( 'idb' , [ 'screenshot' , path , '--udid' , this . #udid] ) ;
224+ await execStrict ( this . #idbPath , [ 'screenshot' , path , '--udid' , this . #udid] ) ;
212225 const data = await execStrict ( 'base64' , [ path ] ) ;
213226 return { data : data . trim ( ) , format : 'png' , path } ;
214227 }
215228
216229 async openApp ( bundleId : string ) : Promise < void > {
217230 await this . ensureConnected ( ) ;
218- await execStrict ( 'idb' , [ 'launch' , bundleId , '--udid' , this . #udid] ) ;
231+ try {
232+ await execStrict ( this . #idbPath, [
233+ 'launch' ,
234+ bundleId ,
235+ '--udid' ,
236+ this . #udid,
237+ ] ) ;
238+ } catch {
239+ await execStrict ( 'xcrun' , [ 'simctl' , 'launch' , this . #udid, bundleId ] ) ;
240+ }
219241 }
220242
221243 async closeApp ( bundleId : string ) : Promise < void > {
222244 await this . ensureConnected ( ) ;
223- await execStrict ( 'idb' , [ 'terminate' , bundleId , '--udid' , this . #udid] ) ;
245+ try {
246+ await execStrict ( this . #idbPath, [
247+ 'terminate' ,
248+ bundleId ,
249+ '--udid' ,
250+ this . #udid,
251+ ] ) ;
252+ } catch {
253+ await execStrict ( 'xcrun' , [ 'simctl' , 'terminate' , this . #udid, bundleId ] ) ;
254+ }
224255 }
225256
226257 async pressButton ( button : DeviceButton ) : Promise < void > {
@@ -231,7 +262,7 @@ export class IdbBackend implements DeviceBackend {
231262 back : 'HOME' ,
232263 enter : 'RETURN' ,
233264 } ;
234- await execStrict ( 'idb' , [
265+ await execStrict ( this . #idbPath , [
235266 'ui' ,
236267 'key' ,
237268 keyMap [ button ] ,
@@ -242,7 +273,13 @@ export class IdbBackend implements DeviceBackend {
242273
243274 async dismissKeyboard ( ) : Promise < void > {
244275 await this . ensureConnected ( ) ;
245- await execStrict ( 'idb' , [ 'ui' , 'key' , 'RETURN' , '--udid' , this . #udid] ) ;
276+ await execStrict ( this . #idbPath, [
277+ 'ui' ,
278+ 'key' ,
279+ 'RETURN' ,
280+ '--udid' ,
281+ this . #udid,
282+ ] ) ;
246283 }
247284
248285 async dismissAlert ( accept : boolean ) : Promise < void > {
@@ -276,7 +313,7 @@ export class IdbBackend implements DeviceBackend {
276313 if ( filter ) {
277314 args . push ( '--predicate' , `eventMessage CONTAINS "${ filter } "` ) ;
278315 }
279- const raw = await execStrict ( 'idb' , args ) ;
316+ const raw = await execStrict ( this . #idbPath , args ) ;
280317 const entries = raw
281318 . trim ( )
282319 . split ( '\n' )
@@ -302,7 +339,7 @@ export class IdbBackend implements DeviceBackend {
302339
303340 const cx = Math . round ( element . frame . x + element . frame . width / 2 ) ;
304341 const cy = Math . round ( element . frame . y + element . frame . height / 2 ) ;
305- await execStrict ( 'idb' , [
342+ await execStrict ( this . #idbPath , [
306343 'ui' ,
307344 'tap' ,
308345 String ( cx ) ,
@@ -362,7 +399,7 @@ export class IdbBackend implements DeviceBackend {
362399
363400 async getWindowSize ( ) : Promise < WindowSize > {
364401 await this . ensureConnected ( ) ;
365- const raw = await execStrict ( 'idb' , [
402+ const raw = await execStrict ( this . #idbPath , [
366403 'describe' ,
367404 '--udid' ,
368405 this . #udid,
@@ -427,7 +464,7 @@ export class IdbBackend implements DeviceBackend {
427464 }
428465 const path = outputPath ?? `/tmp/device-mcp-recording-${ Date . now ( ) } .mp4` ;
429466 this . #recordingPath = path ;
430- this . #recordingProcess = spawn ( 'idb' , [
467+ this . #recordingProcess = spawn ( this . #idbPath , [
431468 'record-video' ,
432469 path ,
433470 '--udid' ,
0 commit comments