@@ -18,12 +18,50 @@ interface SimulatorDevice {
1818 udid : string ;
1919 state : string ;
2020 isAvailable : boolean ;
21+ runtime ?: string ;
2122}
2223
2324interface SimulatorData {
2425 devices : Record < string , SimulatorDevice [ ] > ;
2526}
2627
28+ // Parse text output as fallback for Apple simctl JSON bugs (e.g., duplicate runtime IDs)
29+ function parseTextOutput ( textOutput : string ) : SimulatorDevice [ ] {
30+ const devices : SimulatorDevice [ ] = [ ] ;
31+ const lines = textOutput . split ( '\n' ) ;
32+ let currentRuntime = '' ;
33+
34+ for ( const line of lines ) {
35+ // Match runtime headers like "-- iOS 26.0 --" or "-- iOS 18.6 --"
36+ const runtimeMatch = line . match ( / ^ - - ( [ \w \s . ] + ) - - $ / ) ;
37+ if ( runtimeMatch ) {
38+ currentRuntime = runtimeMatch [ 1 ] ;
39+ continue ;
40+ }
41+
42+ // Match device lines like " iPhone 17 Pro (UUID) (Booted)"
43+ // UUID pattern is flexible to handle test UUIDs like "test-uuid-123"
44+ const deviceMatch = line . match (
45+ / ^ \s { 4 } ( .+ ?) \s + \( ( [ ^ ) ] + ) \) \s + \( ( B o o t e d | S h u t d o w n | B o o t i n g | S h u t t i n g D o w n ) \) (?: \s + \( u n a v a i l a b l e .* \) ) ? $ / i,
46+ ) ;
47+ if ( deviceMatch && currentRuntime ) {
48+ const [ , name , udid , state ] = deviceMatch ;
49+ const isUnavailable = line . includes ( 'unavailable' ) ;
50+ if ( ! isUnavailable ) {
51+ devices . push ( {
52+ name : name . trim ( ) ,
53+ udid,
54+ state,
55+ isAvailable : true ,
56+ runtime : currentRuntime ,
57+ } ) ;
58+ }
59+ }
60+ }
61+
62+ return devices ;
63+ }
64+
2765function isSimulatorData ( value : unknown ) : value is SimulatorData {
2866 if ( ! value || typeof value !== 'object' ) {
2967 return false ;
@@ -68,79 +106,99 @@ export async function list_simsLogic(
68106 log ( 'info' , 'Starting xcrun simctl list devices request' ) ;
69107
70108 try {
71- const command = [ 'xcrun' , 'simctl' , 'list' , 'devices' , 'available' , '--json' ] ;
72- const result = await executor ( command , 'List Simulators' , true ) ;
109+ // Try JSON first for structured data
110+ const jsonCommand = [ 'xcrun' , 'simctl' , 'list' , 'devices' , '--json' ] ;
111+ const jsonResult = await executor ( jsonCommand , 'List Simulators (JSON)' , true ) ;
73112
74- if ( ! result . success ) {
113+ if ( ! jsonResult . success ) {
75114 return {
76115 content : [
77116 {
78117 type : 'text' ,
79- text : `Failed to list simulators: ${ result . error } ` ,
118+ text : `Failed to list simulators: ${ jsonResult . error } ` ,
80119 } ,
81120 ] ,
82121 } ;
83122 }
84123
124+ // Parse JSON output
125+ let jsonDevices : Record < string , SimulatorDevice [ ] > = { } ;
85126 try {
86- const parsedData : unknown = JSON . parse ( result . output ) ;
87-
88- if ( ! isSimulatorData ( parsedData ) ) {
89- return {
90- content : [
91- {
92- type : 'text' ,
93- text : 'Failed to parse simulator data: Invalid format' ,
94- } ,
95- ] ,
96- } ;
127+ const parsedData : unknown = JSON . parse ( jsonResult . output ) ;
128+ if ( isSimulatorData ( parsedData ) ) {
129+ jsonDevices = parsedData . devices ;
97130 }
131+ } catch {
132+ log ( 'warn' , 'Failed to parse JSON output, falling back to text parsing' ) ;
133+ }
98134
99- const simulatorsData : SimulatorData = parsedData ;
100- let responseText = 'Available iOS Simulators:\n\n' ;
101-
102- for ( const runtime in simulatorsData . devices ) {
103- const devices = simulatorsData . devices [ runtime ] ;
135+ // Fallback to text parsing for Apple simctl bugs (duplicate runtime IDs in iOS 26.0 beta)
136+ const textCommand = [ 'xcrun' , 'simctl' , 'list' , 'devices' ] ;
137+ const textResult = await executor ( textCommand , 'List Simulators (Text)' , true ) ;
104138
105- if ( devices . length === 0 ) continue ;
139+ const textDevices = textResult . success ? parseTextOutput ( textResult . output ) : [ ] ;
106140
107- responseText += `${ runtime } :\n` ;
141+ // Merge JSON and text devices, preferring JSON but adding any missing from text
142+ const allDevices : Record < string , SimulatorDevice [ ] > = { ...jsonDevices } ;
143+ const jsonUUIDs = new Set < string > ( ) ;
108144
109- for ( const device of devices ) {
110- if ( device . isAvailable ) {
111- responseText += `- ${ device . name } (${ device . udid } )${ device . state === 'Booted' ? ' [Booted]' : '' } \n` ;
112- }
145+ // Collect all UUIDs from JSON
146+ for ( const runtime in jsonDevices ) {
147+ for ( const device of jsonDevices [ runtime ] ) {
148+ if ( device . isAvailable ) {
149+ jsonUUIDs . add ( device . udid ) ;
113150 }
151+ }
152+ }
114153
115- responseText += '\n' ;
154+ // Add devices from text that aren't in JSON (handles Apple's duplicate runtime ID bug)
155+ for ( const textDevice of textDevices ) {
156+ if ( ! jsonUUIDs . has ( textDevice . udid ) ) {
157+ const runtime = textDevice . runtime ?? 'Unknown Runtime' ;
158+ if ( ! allDevices [ runtime ] ) {
159+ allDevices [ runtime ] = [ ] ;
160+ }
161+ allDevices [ runtime ] . push ( textDevice ) ;
162+ log (
163+ 'info' ,
164+ `Added missing device from text parsing: ${ textDevice . name } (${ textDevice . udid } )` ,
165+ ) ;
116166 }
167+ }
117168
118- responseText += 'Next Steps:\n' ;
119- responseText += "1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })\n" ;
120- responseText += '2. Open the simulator UI: open_sim({})\n' ;
121- responseText +=
122- "3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })\n" ;
123- responseText +=
124- "4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })" ;
169+ // Format output
170+ let responseText = 'Available iOS Simulators:\n\n' ;
125171
126- return {
127- content : [
128- {
129- type : 'text' ,
130- text : responseText ,
131- } ,
132- ] ,
133- } ;
134- } catch {
135- return {
136- content : [
137- {
138- type : 'text' ,
139- text : result . output ,
140- } ,
141- ] ,
142- } ;
172+ for ( const runtime in allDevices ) {
173+ const devices = allDevices [ runtime ] . filter ( ( d ) => d . isAvailable ) ;
174+
175+ if ( devices . length === 0 ) continue ;
176+
177+ responseText += `${ runtime } :\n` ;
178+
179+ for ( const device of devices ) {
180+ responseText += `- ${ device . name } (${ device . udid } )${ device . state === 'Booted' ? ' [Booted]' : '' } \n` ;
181+ }
182+
183+ responseText += '\n' ;
143184 }
185+
186+ responseText += 'Next Steps:\n' ;
187+ responseText += "1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })\n" ;
188+ responseText += '2. Open the simulator UI: open_sim({})\n' ;
189+ responseText +=
190+ "3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })\n" ;
191+ responseText +=
192+ "4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })" ;
193+
194+ return {
195+ content : [
196+ {
197+ type : 'text' ,
198+ text : responseText ,
199+ } ,
200+ ] ,
201+ } ;
144202 } catch ( error ) {
145203 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
146204 log ( 'error' , `Error listing simulators: ${ errorMessage } ` ) ;
0 commit comments