devicely 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/bin/devicely.js +105 -1
  2. package/lib/androidDeviceDetection.js +276 -1
  3. package/lib/appMappings.js +337 -1
  4. package/lib/deviceDetection.js +394 -1
  5. package/lib/devices.js +54 -1
  6. package/lib/doctor.js +94 -1
  7. package/lib/executor.js +104 -1
  8. package/lib/logger.js +35 -1
  9. package/lib/scriptLoader.js +75 -0
  10. package/lib/server.js +3483 -1
  11. package/package.json +3 -12
  12. package/scripts/compile-shell-scripts.js +208 -0
  13. package/scripts/encrypt-shell-simple.js +75 -0
  14. package/scripts/obfuscate-shell.js +160 -0
  15. package/scripts/shell/android_device_control +0 -0
  16. package/scripts/shell/android_device_control.sh +848 -0
  17. package/scripts/shell/apps_presets.conf +271 -0
  18. package/scripts/shell/connect_android_usb +0 -0
  19. package/scripts/shell/connect_android_usb_multi_final +0 -0
  20. package/scripts/shell/connect_android_usb_multi_final.sh +289 -0
  21. package/scripts/shell/connect_android_wireless +0 -0
  22. package/scripts/shell/connect_android_wireless.sh +58 -0
  23. package/scripts/shell/connect_android_wireless_multi_final +0 -0
  24. package/scripts/shell/connect_android_wireless_multi_final.sh +476 -0
  25. package/scripts/shell/connect_ios_usb +0 -0
  26. package/scripts/shell/connect_ios_usb_multi_final +0 -0
  27. package/scripts/shell/connect_ios_usb_multi_final.sh +4225 -0
  28. package/scripts/shell/connect_ios_wireless_multi_final +0 -0
  29. package/scripts/shell/connect_ios_wireless_multi_final.sh +4167 -0
  30. package/scripts/shell/create_production_scripts +0 -0
  31. package/scripts/shell/create_production_scripts.sh +38 -0
  32. package/scripts/shell/devices.conf +24 -0
  33. package/scripts/shell/diagnose_wireless_ios +0 -0
  34. package/scripts/shell/find_element_coordinates +0 -0
  35. package/scripts/shell/find_wda +0 -0
  36. package/scripts/shell/install_uiautomator2 +0 -0
  37. package/scripts/shell/install_uiautomator2.sh +93 -0
  38. package/scripts/shell/ios_device_control +0 -0
  39. package/scripts/shell/ios_device_control.sh +220 -0
  40. package/scripts/shell/organize_project +0 -0
  41. package/scripts/shell/organize_project.sh +59 -0
  42. package/scripts/shell/pre-publish-check +0 -0
  43. package/scripts/shell/pre-publish-check.sh +238 -0
  44. package/scripts/shell/publish +0 -0
  45. package/scripts/shell/publish-to-npm +0 -0
  46. package/scripts/shell/publish-to-npm.sh +366 -0
  47. package/scripts/shell/publish.sh +100 -0
  48. package/scripts/shell/setup +0 -0
  49. package/scripts/shell/setup.sh +121 -0
  50. package/scripts/shell/setup_android +0 -0
  51. package/scripts/shell/start +0 -0
  52. package/scripts/shell/start.sh +59 -0
  53. package/scripts/shell/sync-to-npm-package-final +0 -0
  54. package/scripts/shell/sync-to-npm-package-final.sh +60 -0
  55. package/scripts/shell/test-local-package.sh +95 -0
  56. package/scripts/shell/test_android_locators +0 -0
  57. package/scripts/shell/test_connect +0 -0
  58. package/scripts/shell/test_device_detection +0 -0
  59. package/scripts/shell/test_fixes +0 -0
  60. package/scripts/shell/test_getlocators_fix +0 -0
  61. package/scripts/shell/test_recording_feature +0 -0
  62. package/scripts/shell/verify-shell-protection +0 -0
  63. package/scripts/shell/verify-shell-protection.sh +73 -0
  64. package/scripts/shell/verify_distribution +0 -0
  65. package/lib/package-lock.json +0 -1678
  66. package/lib/package.json +0 -30
  67. package/lib/screenshots/screenshot_ios_iPhone17_20260205_225900.png +0 -0
  68. package/lib/screenshots/screenshot_ios_iPhone17_20260205_225942.png +0 -0
  69. package/lib/screenshots/screenshot_ios_iPhone17_20260205_231101.png +0 -0
  70. package/lib/screenshots/screenshot_ios_iPhone17_20260205_232911.png +0 -0
  71. package/lib/screenshots/screenshot_ios_iPhone17_20260208_095103.png +0 -0
  72. package/lib/screenshots/screenshot_ios_iPhone17_20260208_095720.png +0 -0
  73. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115040.png +0 -0
  74. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115047.png +0 -0
  75. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115118.png +0 -0
  76. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115125.png +0 -0
  77. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115143.png +0 -0
  78. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120107.png +0 -0
  79. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120118.png +0 -0
  80. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120137.png +0 -0
  81. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120201.png +0 -0
  82. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_134529.png +0 -0
  83. package/scripts/shell/android_device_control.enc +0 -1
  84. package/scripts/shell/connect_android_usb_multi_final.enc +0 -1
  85. package/scripts/shell/connect_android_wireless.enc +0 -1
  86. package/scripts/shell/connect_android_wireless_multi_final.enc +0 -1
  87. package/scripts/shell/connect_ios_usb_multi_final.enc +0 -1
  88. package/scripts/shell/connect_ios_wireless_multi_final.enc +0 -1
  89. package/scripts/shell/ios_device_control.enc +0 -1
@@ -0,0 +1,4225 @@
1
+ #!/bin/bash
2
+ # iOS Multi-Device Control - USB VERSION WITH AI
3
+ # Features: Revolutionary AI-powered click detection + Trust-preserving cleanup
4
+ # Usage:
5
+ # ./connect_ios_usb_multi_final.sh # Interactive mode
6
+ # ./connect_ios_usb_multi_final.sh "text" # Send text to all
7
+ # ./connect_ios_usb_multi_final.sh -d device "text" # Send to specific device
8
+ # ./connect_ios_usb_multi_final.sh "click 100,200" # Click at coordinates
9
+ # ./connect_ios_usb_multi_final.sh "click 'Button'" # AI-powered click on element
10
+ # ./connect_ios_usb_multi_final.sh "longpress 'Menu'" # Long press on element
11
+ #
12
+ # Revolutionary Features:
13
+ # 🚀 AI-powered element detection - Pre-caches all screen elements
14
+ # 🧠 Smart fuzzy matching - Finds elements like human would
15
+ # 🎯 Multi-strategy fallback - Professional automation approaches
16
+ # 📸 Screen analysis caching - Instant element lookup after first scan
17
+ # 🔄 Self-refreshing cache - Updates when screen changes detected
18
+ #
19
+ # Commands:
20
+ # click <x,y> or click <text> - AI-powered click detection
21
+ # longpress <x,y> or longpress <text> - AI-powered long press
22
+ # getLocators - Get all locators from current screen
23
+ # refresh - Force refresh screen element cache
24
+ # forcecleanup - Force terminate WDA (may affect device trust)
25
+ #
26
+ # Trust Preservation:
27
+ # Normal 'quit' preserves device trust by keeping WDA processes running
28
+
29
+ # Load the revolutionary AI click system
30
+ SCRIPT_DIR="$(dirname "$0")"
31
+ source "$SCRIPT_DIR/advanced_click_system.sh" 2>/dev/null || echo "⚠️ AI system not loaded, using fallback"
32
+ source "$SCRIPT_DIR/ios_click_fix.sh" 2>/dev/null || echo "⚠️ Non-AI click helper not loaded"
33
+
34
+ WDA_PORT=8100
35
+ WDA_PORT_BASE=8100
36
+ SCRIPT_DIR="$(dirname "$0")"
37
+
38
+ # Function to find config files in multiple locations
39
+ find_config() {
40
+ local filename="$1"
41
+ local search_paths=(
42
+ "$SCRIPT_DIR/$filename"
43
+ "$SCRIPT_DIR/../../config/$filename"
44
+ "$SCRIPT_DIR/../config/$filename"
45
+ "$HOME/.devicely/$filename"
46
+ "/opt/homebrew/lib/node_modules/devicely/config/$filename"
47
+ "$(npm root -g 2>/dev/null)/devicely/config/$filename"
48
+ )
49
+
50
+ for path in "${search_paths[@]}"; do
51
+ if [ -f "$path" ]; then
52
+ echo "$path"
53
+ return 0
54
+ fi
55
+ done
56
+
57
+ return 1
58
+ }
59
+
60
+ # Find config files
61
+ CONFIG_FILE=$(find_config "devices.conf")
62
+ if [ -z "$CONFIG_FILE" ]; then
63
+ if [ -f "$HOME/.devicely/devices.conf" ]; then
64
+ CONFIG_FILE="$HOME/.devicely/devices.conf"
65
+ else
66
+ echo "❌ Required file not found: devices.conf"
67
+ echo " Searched in: script dir, config/, ~/.devicely/, global npm"
68
+ exit 1
69
+ fi
70
+ fi
71
+
72
+ APPS_PRESETS_FILE=$(find_config "apps_presets.conf")
73
+ if [ -z "$APPS_PRESETS_FILE" ]; then
74
+ if [ -f "$HOME/.devicely/apps_presets.conf" ]; then
75
+ APPS_PRESETS_FILE="$HOME/.devicely/apps_presets.conf"
76
+ else
77
+ echo "❌ Required file not found: apps_presets.conf"
78
+ echo " Searched in: script dir, config/, ~/.devicely/, global npm"
79
+ exit 1
80
+ fi
81
+ fi
82
+
83
+ # Track iproxy processes for cleanup (using file-based tracking for bash 3.2 compatibility)
84
+ IPROXY_PIDS_FILE="/tmp/ios_multi_iproxy_pids.$$"
85
+ DEVICE_PORT_MAP_FILE="/tmp/ios_multi_device_ports.$$"
86
+ DEVICE_CONNECTION_TYPE_FILE="/tmp/ios_multi_connection_types.$$"
87
+
88
+ # Command history variables (like original script)
89
+ HISTORY_FILE="/tmp/.ios_multi_history"
90
+ HISTORY_MAX=50
91
+ declare -a TEXT_HISTORY
92
+ HISTORY_INDEX=-1
93
+ CURRENT_INPUT=""
94
+
95
+ # Disable bash history to avoid showing shell commands (like original script)
96
+ set +o history
97
+ unset HISTFILE
98
+
99
+ # Colors
100
+ RED='\033[0;31m'
101
+ GREEN='\033[0;32m'
102
+ YELLOW='\033[1;33m'
103
+ BLUE='\033[0;34m'
104
+ CYAN='\033[0;36m'
105
+ NC='\033[0m'
106
+
107
+ print_color() {
108
+ echo -e "$1$2${NC}"
109
+ }
110
+
111
+ # Global session tracking (use fixed filename so sessions persist across command invocations)
112
+ DEVICE_SESSIONS_FILE="/tmp/ios_multi_sessions"
113
+
114
+ # History management functions
115
+ load_text_history() {
116
+ # Ensure history file exists and is clean
117
+ if [ ! -f "$HISTORY_FILE" ]; then
118
+ touch "$HISTORY_FILE"
119
+ fi
120
+ }
121
+
122
+ # Add text to history
123
+ add_to_history() {
124
+ local text="$1"
125
+ # Skip empty text, commands, and special keys
126
+ if [ -z "$text" ] || [[ "$text" == /* ]] || [[ "$text" == "{*}" ]] || [[ "$text" == "\\n" ]] || [[ "$text" == "quit" ]] || [[ "$text" == "exit" ]] || [[ "$text" == "help" ]]; then
127
+ return
128
+ fi
129
+
130
+ # Add to history file (remove duplicates and keep last entries)
131
+ grep -v "^$(printf '%s\n' "$text" | sed 's/[[\.*^$()+?{|]/\\&/g')$" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" 2>/dev/null || true
132
+ echo "$text" >> "${HISTORY_FILE}.tmp"
133
+ tail -n "$HISTORY_MAX" "${HISTORY_FILE}.tmp" > "$HISTORY_FILE"
134
+ rm -f "${HISTORY_FILE}.tmp"
135
+ }
136
+
137
+ # Parse arguments
138
+ DEVICE_NAME_REQUEST=""
139
+ TEXT=""
140
+ APP_BUNDLE=""
141
+
142
+ if [ "$1" = "-d" ]; then
143
+ DEVICE_NAME_REQUEST="$2"
144
+ TEXT="$3"
145
+ elif [ -n "$1" ]; then
146
+ TEXT="$1"
147
+ fi
148
+
149
+ # Check files
150
+ for file in "$CONFIG_FILE" "$APPS_PRESETS_FILE"; do
151
+ if [ ! -f "$file" ]; then
152
+ echo "❌ Required file not found: $file"
153
+ exit 1
154
+ fi
155
+ done
156
+
157
+ # Setup single USB device (for -d mode)
158
+ setup_single_usb_device() {
159
+ local device_name="$1"
160
+ local details=$(get_device_details "$device_name")
161
+
162
+ if [ $? -ne 0 ]; then
163
+ return 1
164
+ fi
165
+
166
+ local port=$(echo "$details" | cut -d',' -f1)
167
+ local udid=$(echo "$details" | cut -d',' -f2)
168
+
169
+ # Check if iproxy is already running for this port
170
+ if ! pgrep -f "iproxy.*${port}:8100" >/dev/null 2>&1; then
171
+ print_color "$CYAN" " 🔌 Starting iproxy: localhost:$port -> device:8100"
172
+ iproxy ${port} 8100 -u "$udid" >/dev/null 2>&1 &
173
+ local iproxy_pid=$!
174
+ echo "$device_name:$iproxy_pid" >> "$IPROXY_PIDS_FILE"
175
+ sleep 0.5
176
+ else
177
+ print_color "$GREEN" " ✅ iproxy already running on port $port"
178
+ fi
179
+
180
+ # Quick connection test (skip ping, go straight to WDA)
181
+ print_color "$CYAN" " 🔍 Testing WDA connection..."
182
+ if curl -s --connect-timeout 2 --max-time 3 "http://localhost:${port}/status" 2>/dev/null | grep -q '"state"'; then
183
+ print_color "$GREEN" " ✅ WDA ready"
184
+ return 0
185
+ else
186
+ print_color "$YELLOW" " ⚠️ WDA not responding, attempting auto-start..."
187
+
188
+ # Try to auto-start WDA
189
+ if start_wda_usb "$device_name" "$port" "$udid"; then
190
+ print_color "$GREEN" " ✅ WDA auto-started"
191
+ return 0
192
+ else
193
+ print_color "$RED" " ❌ Failed to start WDA"
194
+ return 1
195
+ fi
196
+ fi
197
+ }
198
+
199
+ # Get connected USB devices
200
+ get_all_device_names() {
201
+ local udids=($(idevice_id -l 2>/dev/null))
202
+ if [ ${#udids[@]} -eq 0 ]; then
203
+ return 1
204
+ fi
205
+
206
+ # For each UDID, try to get device name
207
+ local devices=()
208
+ for udid in "${udids[@]}"; do
209
+ local device_name=$(ideviceinfo -u "$udid" -k DeviceName 2>/dev/null | head -n1 | tr -d '\r\n')
210
+ if [ -z "$device_name" ]; then
211
+ device_name="$udid"
212
+ fi
213
+ devices+=("$device_name")
214
+ done
215
+ printf '%s\n' "${devices[@]}"
216
+ }
217
+
218
+ # Check if device is connected via USB
219
+ is_usb_connected() {
220
+ local udid="$1"
221
+ if command -v idevice_id >/dev/null 2>&1; then
222
+ idevice_id -l 2>/dev/null | grep -q "^${udid}$"
223
+ return $?
224
+ fi
225
+ return 1
226
+ }
227
+
228
+ # Get USB device name from UDID
229
+ get_usb_device_name() {
230
+ local udid="$1"
231
+ if command -v idevicename >/dev/null 2>&1; then
232
+ idevicename -u "$udid" 2>/dev/null
233
+ elif command -v ideviceinfo >/dev/null 2>&1; then
234
+ ideviceinfo -u "$udid" -k DeviceName 2>/dev/null | head -n1 | tr -d '\r\n'
235
+ fi
236
+ }
237
+
238
+ # Get device details (port,udid) for USB devices
239
+ get_device_details() {
240
+ local device_name="$1"
241
+
242
+ # Check cache first for consistent port assignment
243
+ if [ -f "$DEVICE_PORT_MAP_FILE" ]; then
244
+ local cached=$(grep "^$device_name:" "$DEVICE_PORT_MAP_FILE" 2>/dev/null | cut -d':' -f2-)
245
+ if [ -n "$cached" ]; then
246
+ echo "$cached"
247
+ return 0
248
+ fi
249
+ fi
250
+
251
+ # Not in cache, calculate and cache it
252
+ local udids=($(idevice_id -l 2>/dev/null))
253
+ local port_offset=0
254
+
255
+ for udid in "${udids[@]}"; do
256
+ local name=$(ideviceinfo -u "$udid" -k DeviceName 2>/dev/null | head -n1 | tr -d '\r\n')
257
+ if [ -z "$name" ]; then
258
+ name="$udid"
259
+ fi
260
+
261
+ if [ "$name" = "$device_name" ] || [ "$udid" = "$device_name" ]; then
262
+ local port=$((WDA_PORT_BASE + port_offset))
263
+ local result="$port,$udid"
264
+ # Cache it for consistent lookups
265
+ echo "$device_name:$result" >> "$DEVICE_PORT_MAP_FILE"
266
+ echo "$result"
267
+ return 0
268
+ fi
269
+ ((port_offset++))
270
+ done
271
+ return 1
272
+ }
273
+
274
+ # Resolve app name to bundle ID
275
+ resolve_app_bundle() {
276
+ local input="$1"
277
+
278
+ # If already a bundle ID, return as-is
279
+ if [[ "$input" == *"."* ]] && [[ "$input" != *" "* ]]; then
280
+ echo "$input"
281
+ return
282
+ fi
283
+
284
+ # Look up in presets
285
+ if [ -f "$APPS_PRESETS_FILE" ]; then
286
+ local input_lower=$(echo "$input" | tr '[:upper:]' '[:lower:]')
287
+ while IFS=',' read -r name bundle rest; do
288
+ [[ "$name" =~ ^# ]] || [[ -z "$name" ]] && continue
289
+ if [ "$(echo "$name" | tr '[:upper:]' '[:lower:]')" = "$input_lower" ]; then
290
+ echo "$bundle"
291
+ return
292
+ fi
293
+ done < "$APPS_PRESETS_FILE"
294
+ fi
295
+
296
+ # Default bundle IDs for common apps
297
+ case "$input_lower" in
298
+ "safari") echo "com.apple.mobilesafari" ;;
299
+ "settings") echo "com.apple.Preferences" ;;
300
+ "messages") echo "com.apple.MobileSMS" ;;
301
+ "camera") echo "com.apple.camera" ;;
302
+ "photos") echo "com.apple.mobileslideshow" ;;
303
+ "notes") echo "com.apple.mobilenotes" ;;
304
+ *) echo "$input" ;;
305
+ esac
306
+ }
307
+
308
+ # Check device connectivity (USB version with auto-WDA start)
309
+ check_device_connectivity() {
310
+ local device_name="$1"
311
+ local device_details
312
+ device_details=$(get_device_details "$device_name")
313
+
314
+ if [ $? -ne 0 ]; then
315
+ print_color "$RED" "❌ Device not found: $device_name"
316
+ return 1
317
+ fi
318
+
319
+ # Get device info
320
+ local udid=$(echo "$device_details" | awk -F',' '{print $1}')
321
+ local port=$(echo "$device_details" | awk -F',' '{print $2}')
322
+
323
+ # Check if device is still connected
324
+ if ! idevice_id -l 2>/dev/null | grep -q "^${udid}$"; then
325
+ print_color "$RED" "❌ Device disconnected: $device_name"
326
+ return 1
327
+ fi
328
+
329
+ # Test localhost connectivity
330
+ echo "🌐 Ping test to localhost..."
331
+ if ping -c 1 -W 2000 localhost >/dev/null 2>&1; then
332
+ echo "✅ Network reachable"
333
+ else
334
+ echo "❌ Network unreachable"
335
+ return 1
336
+ fi
337
+
338
+ # Test WDA connection
339
+ echo "🔍 Testing WDA connection..."
340
+ if curl -s --connect-timeout 2 --max-time 3 "http://localhost:$port/status" 2>/dev/null | grep -q '"state"'; then
341
+ echo "✅ WDA ready"
342
+ return 0
343
+ else
344
+ echo "⚠️ WDA not ready, attempting auto-start..."
345
+ start_wda_usb "$device_name" "$udid" "$port"
346
+ return $?
347
+ fi
348
+ }
349
+
350
+ create_persistent_session() {
351
+ local device_name="$1"
352
+ local device_details
353
+ device_details=$(get_device_details "$device_name")
354
+ local port=$(echo "$device_details" | cut -d',' -f1)
355
+
356
+ # Create ONE session per device for the entire interactive session
357
+ # Don't launch any specific app - just create a session to the home screen
358
+ local session=$(curl -s -X POST "http://localhost:${port}/session" \
359
+ -H 'Content-Type: application/json' \
360
+ -d '{"capabilities":{"alwaysMatch":{}}}' | \
361
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sessionId','') or d.get('value',{}).get('sessionId',''))" 2>/dev/null)
362
+
363
+ if [ -n "$session" ]; then
364
+ # Use a lock file for atomic writes when running in parallel
365
+ local lock_file="${DEVICE_SESSIONS_FILE}.lock"
366
+ while ! mkdir "$lock_file" 2>/dev/null; do
367
+ sleep 0.01
368
+ done
369
+ echo "$device_name:$session" >> "$DEVICE_SESSIONS_FILE"
370
+ rmdir "$lock_file"
371
+ echo "$session"
372
+ else
373
+ echo ""
374
+ fi
375
+ }
376
+
377
+ # Get existing session for device
378
+ get_device_session() {
379
+ local device_name="$1"
380
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
381
+ grep "^$device_name:" "$DEVICE_SESSIONS_FILE" | cut -d':' -f2 | tail -1
382
+ fi
383
+ }
384
+
385
+ # Helper function to find element using multiple strategies (like Appium)
386
+ find_element_by_text() {
387
+ local session="$1"
388
+ local port="$2"
389
+ local text="$3"
390
+ local device_name="$4"
391
+
392
+ local element_id=""
393
+ local found_strategy=""
394
+
395
+ # Strategy 1: Accessibility ID (most reliable for iOS)
396
+ echo "[$device_name] Trying accessibility ID..."
397
+ local elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
398
+ -H "Content-Type: application/json" \
399
+ -d "{\"using\": \"accessibility id\", \"value\": \"$text\"}" 2>/dev/null)
400
+
401
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
402
+ element_id=$(echo "$elements_result" | python3 -c "
403
+ import sys, json
404
+ try:
405
+ data = json.load(sys.stdin)
406
+ elements = data.get('value', [])
407
+ if elements and len(elements) > 0:
408
+ print(elements[0].get('ELEMENT', ''))
409
+ except:
410
+ pass" 2>/dev/null)
411
+ found_strategy="accessibility id"
412
+ fi
413
+
414
+ # Strategy 2: Name attribute
415
+ if [ -z "$element_id" ]; then
416
+ echo "[$device_name] Trying name attribute..."
417
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
418
+ -H "Content-Type: application/json" \
419
+ -d "{\"using\": \"name\", \"value\": \"$text\"}" 2>/dev/null)
420
+
421
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
422
+ element_id=$(echo "$elements_result" | python3 -c "
423
+ import sys, json
424
+ try:
425
+ data = json.load(sys.stdin)
426
+ elements = data.get('value', [])
427
+ if elements and len(elements) > 0:
428
+ print(elements[0].get('ELEMENT', ''))
429
+ except:
430
+ pass" 2>/dev/null)
431
+ found_strategy="name"
432
+ fi
433
+ fi
434
+
435
+ # Strategy 3: Predicate string (iOS specific - very powerful)
436
+ if [ -z "$element_id" ]; then
437
+ echo "[$device_name] Trying predicate string..."
438
+ local predicate="name CONTAINS '$text' OR label CONTAINS '$text' OR value CONTAINS '$text'"
439
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
440
+ -H "Content-Type: application/json" \
441
+ -d "{\"using\": \"predicate string\", \"value\": \"$predicate\"}" 2>/dev/null)
442
+
443
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
444
+ element_id=$(echo "$elements_result" | python3 -c "
445
+ import sys, json
446
+ try:
447
+ data = json.load(sys.stdin)
448
+ elements = data.get('value', [])
449
+ if elements and len(elements) > 0:
450
+ print(elements[0].get('ELEMENT', ''))
451
+ except:
452
+ pass" 2>/dev/null)
453
+ found_strategy="predicate string"
454
+ fi
455
+ fi
456
+
457
+ # Strategy 4: Class chain for buttons (iOS specific)
458
+ if [ -z "$element_id" ]; then
459
+ echo "[$device_name] Trying class chain for buttons..."
460
+ local class_chain="**/XCUIElementTypeButton[\`name CONTAINS '$text'\`]"
461
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
462
+ -H "Content-Type: application/json" \
463
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
464
+
465
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
466
+ element_id=$(echo "$elements_result" | python3 -c "
467
+ import sys, json
468
+ try:
469
+ data = json.load(sys.stdin)
470
+ elements = data.get('value', [])
471
+ if elements and len(elements) > 0:
472
+ print(elements[0].get('ELEMENT', ''))
473
+ except:
474
+ pass" 2>/dev/null)
475
+ found_strategy="class chain (button)"
476
+ fi
477
+ fi
478
+
479
+ # Strategy 5: Class chain for any element (broader search)
480
+ if [ -z "$element_id" ]; then
481
+ echo "[$device_name] Trying class chain for any element..."
482
+ local class_chain="**/*[\`name CONTAINS '$text'\`]"
483
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
484
+ -H "Content-Type: application/json" \
485
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
486
+
487
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
488
+ element_id=$(echo "$elements_result" | python3 -c "
489
+ import sys, json
490
+ try:
491
+ data = json.load(sys.stdin)
492
+ elements = data.get('value', [])
493
+ if elements and len(elements) > 0:
494
+ print(elements[0].get('ELEMENT', ''))
495
+ except:
496
+ pass" 2>/dev/null)
497
+ found_strategy="class chain (any)"
498
+ fi
499
+ fi
500
+
501
+ # Strategy 6: Table cell search (common in Settings app)
502
+ if [ -z "$element_id" ]; then
503
+ echo "[$device_name] Trying table cell search..."
504
+ local class_chain="**/XCUIElementTypeCell[\`name CONTAINS '$text'\`]"
505
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
506
+ -H "Content-Type: application/json" \
507
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
508
+
509
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
510
+ element_id=$(echo "$elements_result" | python3 -c "
511
+ import sys, json
512
+ try:
513
+ data = json.load(sys.stdin)
514
+ elements = data.get('value', [])
515
+ if elements and len(elements) > 0:
516
+ print(elements[0].get('ELEMENT', ''))
517
+ except:
518
+ pass" 2>/dev/null)
519
+ found_strategy="class chain (cell)"
520
+ fi
521
+ fi
522
+
523
+ # Strategy 7: Static text search (for text labels)
524
+ if [ -z "$element_id" ]; then
525
+ echo "[$device_name] Trying static text search..."
526
+ local class_chain="**/XCUIElementTypeStaticText[\`name CONTAINS '$text'\`]"
527
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
528
+ -H "Content-Type: application/json" \
529
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
530
+
531
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
532
+ element_id=$(echo "$elements_result" | python3 -c "
533
+ import sys, json
534
+ try:
535
+ data = json.load(sys.stdin)
536
+ elements = data.get('value', [])
537
+ if elements and len(elements) > 0:
538
+ print(elements[0].get('ELEMENT', ''))
539
+ except:
540
+ pass" 2>/dev/null)
541
+ found_strategy="class chain (static text)"
542
+ fi
543
+ fi
544
+
545
+ # Strategy 8: Partial match with case-insensitive search
546
+ if [ -z "$element_id" ]; then
547
+ echo "[$device_name] Trying case-insensitive partial match..."
548
+ local predicate="name CONTAINS[c] '$text' OR label CONTAINS[c] '$text' OR value CONTAINS[c] '$text'"
549
+ elements_result=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
550
+ -H "Content-Type: application/json" \
551
+ -d "{\"using\": \"predicate string\", \"value\": \"$predicate\"}" 2>/dev/null)
552
+
553
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
554
+ element_id=$(echo "$elements_result" | python3 -c "
555
+ import sys, json
556
+ try:
557
+ data = json.load(sys.stdin)
558
+ elements = data.get('value', [])
559
+ if elements and len(elements) > 0:
560
+ print(elements[0].get('ELEMENT', ''))
561
+ except:
562
+ pass" 2>/dev/null)
563
+ found_strategy="predicate (case-insensitive)"
564
+ fi
565
+ fi
566
+
567
+ # Output results
568
+ if [ -n "$element_id" ]; then
569
+ echo "$element_id|$found_strategy"
570
+ else
571
+ echo "|"
572
+ fi
573
+ }
574
+
575
+ # Helper function to get element center coordinates with multiple methods
576
+ get_element_center() {
577
+ local session="$1"
578
+ local port="$2"
579
+ local element_id="$3"
580
+
581
+ # Method 1: Try /rect endpoint
582
+ local rect_response=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/rect" \
583
+ -H 'Content-Type: application/json' 2>/dev/null)
584
+
585
+ echo "DEBUG: Rect response: '$rect_response'" >&2
586
+
587
+ if echo "$rect_response" | grep -q '"x"' && echo "$rect_response" | grep -q '"y"'; then
588
+ local x=$(echo "$rect_response" | python3 -c "
589
+ import sys, json
590
+ try:
591
+ data = json.load(sys.stdin)
592
+ rect = data.get('value', {})
593
+ x = rect.get('x', 0)
594
+ width = rect.get('width', 0)
595
+ center_x = int(x + width/2)
596
+ print(center_x)
597
+ except Exception as e:
598
+ print('0')" 2>/dev/null)
599
+
600
+ local y=$(echo "$rect_response" | python3 -c "
601
+ import sys, json
602
+ try:
603
+ data = json.load(sys.stdin)
604
+ rect = data.get('value', {})
605
+ y = rect.get('y', 0)
606
+ height = rect.get('height', 0)
607
+ center_y = int(y + height/2)
608
+ print(center_y)
609
+ except Exception as e:
610
+ print('0')" 2>/dev/null)
611
+
612
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
613
+ echo "$x,$y"
614
+ return
615
+ fi
616
+ fi
617
+
618
+ # Method 2: Try /location endpoint
619
+ echo "DEBUG: Trying location endpoint..." >&2
620
+ local location_response=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/location" \
621
+ -H 'Content-Type: application/json' 2>/dev/null)
622
+
623
+ echo "DEBUG: Location response: '$location_response'" >&2
624
+
625
+ if echo "$location_response" | grep -q '"x"' && echo "$location_response" | grep -q '"y"'; then
626
+ local x=$(echo "$location_response" | python3 -c "
627
+ import sys, json
628
+ try:
629
+ data = json.load(sys.stdin)
630
+ location = data.get('value', {})
631
+ print(int(location.get('x', 0)))
632
+ except:
633
+ print('0')" 2>/dev/null)
634
+
635
+ local y=$(echo "$location_response" | python3 -c "
636
+ import sys, json
637
+ try:
638
+ data = json.load(sys.stdin)
639
+ location = data.get('value', {})
640
+ print(int(location.get('y', 0)))
641
+ except:
642
+ print('0')" 2>/dev/null)
643
+
644
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
645
+ echo "$x,$y"
646
+ return
647
+ fi
648
+ fi
649
+
650
+ # Method 3: Try WebDriverAgent specific endpoint
651
+ echo "DEBUG: Trying WDA accessibilityContainer endpoint..." >&2
652
+ local wda_response=$(curl -s -X GET "http://localhost:${port}/session/$session/wda/element/$element_id/accessibilityContainer" \
653
+ -H 'Content-Type: application/json' 2>/dev/null)
654
+
655
+ echo "DEBUG: WDA response: '$wda_response'" >&2
656
+
657
+ # Method 4: Use element attribute to get frame info
658
+ echo "DEBUG: Trying element attribute..." >&2
659
+ local attr_response=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/attribute/frame" \
660
+ -H 'Content-Type: application/json' 2>/dev/null)
661
+
662
+ echo "DEBUG: Attribute response: '$attr_response'" >&2
663
+
664
+ # If all methods fail, return 0,0
665
+ echo "0,0"
666
+ }
667
+
668
+ cleanup_wda_session() {
669
+ local device_name="$1"
670
+ local preserve_trust="${2:-true}"
671
+ local session=""
672
+
673
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
674
+ session=$(grep "^$device_name:" "$DEVICE_SESSIONS_FILE" | cut -d':' -f2 | tail -1)
675
+ fi
676
+
677
+ if [ -n "$session" ]; then
678
+ local device_details
679
+ device_details=$(get_device_details "$device_name")
680
+ local port=$(echo "$device_details" | cut -d',' -f1)
681
+
682
+ curl -s -X DELETE "http://localhost:${port}/session/$session" >/dev/null 2>&1
683
+
684
+ # Remove session from tracking
685
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
686
+ grep -v "^$device_name:" "$DEVICE_SESSIONS_FILE" > "${DEVICE_SESSIONS_FILE}.tmp" 2>/dev/null || true
687
+ mv "${DEVICE_SESSIONS_FILE}.tmp" "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
688
+ fi
689
+
690
+ if [ "$preserve_trust" = "true" ] && [ "${preserve_trust}" != "false" ]; then
691
+ print_color "$GREEN" "✅ Session closed for $device_name (WebDriverAgent preserved)"
692
+ fi
693
+ fi
694
+ }
695
+
696
+ # Helper function to find clickable parent element
697
+ # When StaticText is found, we need to click its parent Cell or Button
698
+ find_clickable_element() {
699
+ local session="$1"
700
+ local port="$2"
701
+ local element_id="$3"
702
+ local device_name="$4"
703
+
704
+ # Get element type to determine if it's clickable
705
+ local element_type=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/attribute/type" 2>/dev/null | \
706
+ python3 -c "import sys,json; print(json.load(sys.stdin).get('value', ''))" 2>/dev/null)
707
+
708
+ echo "[$device_name] Element type: $element_type" >&2
709
+
710
+ # If it's StaticText, try to find parent Cell/Button (common in Settings app)
711
+ if [[ "$element_type" == *"StaticText"* ]]; then
712
+ echo "[$device_name] StaticText found, searching for clickable parent..." >&2
713
+
714
+ # Try to get parent cell using xpath
715
+ local parent=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/element" \
716
+ -H 'Content-Type: application/json' \
717
+ -d '{"using":"xpath","value":"./ancestor::XCUIElementTypeCell[1]"}' 2>/dev/null | \
718
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('value',{}).get('ELEMENT',''))" 2>/dev/null)
719
+
720
+ if [ -n "$parent" ]; then
721
+ echo "[$device_name] ✅ Found parent Cell: $parent" >&2
722
+ echo "$parent"
723
+ return 0
724
+ fi
725
+
726
+ # Try button parent
727
+ parent=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/element" \
728
+ -H 'Content-Type: application/json' \
729
+ -d '{"using":"xpath","value":"./ancestor::XCUIElementTypeButton[1]"}' 2>/dev/null | \
730
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('value',{}).get('ELEMENT',''))" 2>/dev/null)
731
+
732
+ if [ -n "$parent" ]; then
733
+ echo "[$device_name] ✅ Found parent Button: $parent" >&2
734
+ echo "$parent"
735
+ return 0
736
+ fi
737
+
738
+ echo "[$device_name] ⚠️ No clickable parent found, using original element" >&2
739
+ fi
740
+
741
+ # Return original element
742
+ echo "$element_id"
743
+ }
744
+
745
+ # Execute WDA command directly
746
+ execute_wda_command() {
747
+ local device_name="$1"
748
+ local command="$2"
749
+ local log_prefix="[${device_name}]"
750
+ local device_details
751
+ device_details=$(get_device_details "$device_name")
752
+ local port=$(echo "$device_details" | cut -d',' -f1)
753
+
754
+ # Parse command
755
+ local cmd_type=$(echo "$command" | awk '{print $1}')
756
+ local cmd_args=$(echo "$command" | sed "s/^$cmd_type *//" | xargs)
757
+
758
+ # Show what we parsed for debugging
759
+ echo "${log_prefix} Processing command: '$cmd_type' with args: '$cmd_args'"
760
+
761
+ # Check the actual values with explicit debugging
762
+ echo "${log_prefix} cmd_type length: ${#cmd_type}"
763
+ echo "${log_prefix} cmd_type hex: $(echo -n "$cmd_type" | xxd -p)"
764
+
765
+ case "$cmd_type" in
766
+ "launch"|"open")
767
+ echo "${log_prefix} CASE: Launch/open matched"
768
+ local bundle_id=$(resolve_app_bundle "$cmd_args")
769
+ echo "${log_prefix} Launching: $cmd_args → $bundle_id"
770
+
771
+ # Clean up existing session and create new one for the target app
772
+ cleanup_wda_session "$device_name" false # Don't show message during launch
773
+
774
+ # Create new session with target app (this launches it)
775
+ local session=$(curl -s -X POST "http://localhost:${port}/session" \
776
+ -H 'Content-Type: application/json' \
777
+ -d "{\"capabilities\":{\"alwaysMatch\":{\"bundleId\":\"$bundle_id\"}}}" | \
778
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sessionId','') or d.get('value',{}).get('sessionId',''))" 2>/dev/null)
779
+
780
+ if [ -n "$session" ]; then
781
+ # Store new session
782
+ echo "$device_name:$session" >> "$DEVICE_SESSIONS_FILE"
783
+ echo "${log_prefix} ✅ App launched"
784
+ else
785
+ echo "${log_prefix} ❌ Launch failed"
786
+ fi
787
+ ;;
788
+
789
+ "kill"|"close"|"terminate")
790
+ local bundle_id=$(resolve_app_bundle "$cmd_args")
791
+ echo "${log_prefix} Terminating: $cmd_args → $bundle_id"
792
+
793
+ # Method 1: Try WDA terminate endpoint
794
+ local session=$(get_device_session "$device_name")
795
+ local success=false
796
+
797
+ if [ -n "$session" ]; then
798
+ echo "${log_prefix} → Trying WDA terminate endpoint..."
799
+ local response=$(curl -s -X POST "http://localhost:${port}/wda/apps/terminate" \
800
+ -H 'Content-Type: application/json' \
801
+ -d "{\"bundleId\":\"$bundle_id\"}")
802
+
803
+ # Debug: Show response for troubleshooting
804
+ # echo "${log_prefix} DEBUG: Response: $response"
805
+
806
+ if echo "$response" | grep -q '"value".*true' && ! echo "$response" | grep -q '"error"'; then
807
+ echo "${log_prefix} ✅ App terminated via WDA"
808
+ success=true
809
+ elif echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
810
+ echo "${log_prefix} ✅ App terminated via WDA"
811
+ success=true
812
+ else
813
+ echo "${log_prefix} → WDA terminate failed, trying alternatives..."
814
+ fi
815
+ fi
816
+
817
+ # Method 2: Try alternative WDA endpoint if first method failed
818
+ if [ "$success" = false ] && [ -n "$session" ]; then
819
+ echo "${log_prefix} → Trying session-based terminate method..."
820
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/apps/terminate" \
821
+ -H 'Content-Type: application/json' \
822
+ -d "{\"bundleId\":\"$bundle_id\"}")
823
+
824
+ if echo "$response" | grep -q '"value".*true' || (echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'); then
825
+ echo "${log_prefix} ✅ App terminated via session API"
826
+ success=true
827
+ else
828
+ echo "${log_prefix} → Session terminate also failed..."
829
+ fi
830
+ fi
831
+
832
+ # Method 3: Try using xcrun devicectl (iOS 17+)
833
+ if [ "$success" = false ]; then
834
+ echo "${log_prefix} → Trying xcrun devicectl..."
835
+ local device_details
836
+ device_details=$(get_device_details "$device_name")
837
+ local udid=$(echo "$device_details" | cut -d',' -f2)
838
+
839
+ if [ -n "$udid" ] && command -v xcrun >/dev/null 2>&1; then
840
+ # Try process kill first
841
+ local kill_result=$(xcrun devicectl device process kill --device "$udid" "$bundle_id" 2>&1)
842
+ if [ $? -eq 0 ]; then
843
+ echo "${log_prefix} ✅ App terminated via xcrun devicectl (process kill)"
844
+ success=true
845
+ else
846
+ # Try app terminate
847
+ local terminate_result=$(xcrun devicectl device apps terminate --device "$udid" "$bundle_id" 2>&1)
848
+ if [ $? -eq 0 ]; then
849
+ echo "${log_prefix} ✅ App terminated via xcrun devicectl (app terminate)"
850
+ success=true
851
+ fi
852
+ fi
853
+ fi
854
+ fi
855
+
856
+ # Method 4: Try launching home screen to background the app
857
+ if [ "$success" = false ]; then
858
+ echo "${log_prefix} → Backgrounding app (launching home screen)..."
859
+ local response=$(curl -s -X POST "http://localhost:${port}/wda/homescreen" \
860
+ -H 'Content-Type: application/json' -d '{}')
861
+
862
+ if ! echo "$response" | grep -q '"error"'; then
863
+ echo "${log_prefix} ✅ App backgrounded (sent to home screen)"
864
+ success=true
865
+ fi
866
+ fi
867
+
868
+ if [ "$success" = false ]; then
869
+ echo "${log_prefix} ❌ All terminate methods failed"
870
+ echo "${log_prefix} 💡 Try manually closing the app or use 'home' command"
871
+ fi
872
+ ;;
873
+
874
+ "home")
875
+ echo "${log_prefix} Going to home screen"
876
+ # Use the same endpoint as original script (no session ID needed)
877
+ local response=$(curl -s -X POST "http://localhost:${port}/wda/homescreen" \
878
+ -H 'Content-Type: application/json' -d '{}')
879
+
880
+ if ! echo "$response" | grep -q '"error"'; then
881
+ echo "${log_prefix} ✅ Went to home"
882
+ else
883
+ echo "${log_prefix} ❌ Home failed"
884
+ fi
885
+ ;;
886
+
887
+ "url")
888
+ local url="$cmd_args"
889
+ if ! echo "$url" | grep -qE '^[a-z]+://'; then
890
+ url="https://$url"
891
+ fi
892
+ echo "${log_prefix} Opening URL: $url"
893
+
894
+ # Use existing session - first activate Safari, then navigate
895
+ local session=$(get_device_session "$device_name")
896
+ if [ -n "$session" ]; then
897
+ # Activate Safari first
898
+ curl -s -X POST "http://localhost:${port}/wda/apps/activate" \
899
+ -H 'Content-Type: application/json' \
900
+ -d '{"bundleId":"com.apple.mobilesafari"}' >/dev/null 2>&1
901
+ sleep 1
902
+ # Navigate to URL
903
+ curl -s -X POST "http://localhost:${port}/session/$session/url" \
904
+ -H 'Content-Type: application/json' \
905
+ -d "{\"url\":\"$url\"}" >/dev/null 2>&1
906
+ echo "${log_prefix} ✅ URL opened in Safari"
907
+ else
908
+ echo "${log_prefix} ❌ URL open failed"
909
+ fi
910
+ ;;
911
+
912
+ "screenshot")
913
+ echo "${log_prefix} Taking screenshot"
914
+ local session=$(get_device_session "$device_name")
915
+ if [ -n "$session" ]; then
916
+ # Save to webapp screenshots directory
917
+ local screenshots_dir="$SCRIPT_DIR/webapp/backend/screenshots"
918
+ mkdir -p "$screenshots_dir"
919
+ local out_file="${screenshots_dir}/screenshot_ios_${device_name}_$(date +%Y%m%d_%H%M%S).png"
920
+ local response=$(curl -s -X GET "http://localhost:${port}/screenshot")
921
+
922
+ if echo "$response" | python3 -c "
923
+ import sys,base64,json
924
+ try:
925
+ j=json.load(sys.stdin)
926
+ img_b64=j.get('value','')
927
+ if img_b64:
928
+ with open('$out_file','wb') as f:
929
+ f.write(base64.b64decode(img_b64))
930
+ print('✅ Screenshot saved: $out_file')
931
+ else:
932
+ print('❌ Screenshot failed')
933
+ except:
934
+ print('❌ Screenshot error')
935
+ " 2>/dev/null; then
936
+ echo "${log_prefix} Screenshot completed"
937
+ fi
938
+ else
939
+ echo "${log_prefix} ❌ Screenshot failed"
940
+ fi
941
+ ;;
942
+
943
+ "lock")
944
+ echo "${log_prefix} Locking device"
945
+ local response=$(curl -s -X POST "http://localhost:${port}/wda/lock" \
946
+ -H 'Content-Type: application/json' -d '{}')
947
+
948
+ if ! echo "$response" | grep -q '"error"'; then
949
+ echo "${log_prefix} ✅ Device locked"
950
+ else
951
+ echo "${log_prefix} ❌ Lock failed"
952
+ fi
953
+ ;;
954
+
955
+ "unlock")
956
+ echo "${log_prefix} Unlocking device"
957
+ local response=$(curl -s -X POST "http://localhost:${port}/wda/unlock" \
958
+ -H 'Content-Type: application/json' -d '{}')
959
+
960
+ if ! echo "$response" | grep -q '"error"'; then
961
+ echo "${log_prefix} ✅ Device unlocked"
962
+ else
963
+ echo "${log_prefix} ❌ Unlock failed"
964
+ fi
965
+ ;;
966
+
967
+ "back"|"goback")
968
+ echo "${log_prefix} Going back"
969
+ # iOS doesn't have a universal back button like Android
970
+ # Use swipe from left edge gesture which is the iOS equivalent
971
+ local session=$(get_device_session "$device_name")
972
+ if [ -n "$session" ]; then
973
+ # Swipe from left edge (back gesture)
974
+ local swipe_payload='{
975
+ "actions": [
976
+ {
977
+ "type": "pointer",
978
+ "id": "finger1",
979
+ "parameters": {"pointerType": "touch"},
980
+ "actions": [
981
+ {"type": "pointerMove", "duration": 0, "x": 10, "y": 400},
982
+ {"type": "pointerDown", "button": 0},
983
+ {"type": "pointerMove", "duration": 300, "x": 200, "y": 400},
984
+ {"type": "pointerUp", "button": 0}
985
+ ]
986
+ }
987
+ ]
988
+ }'
989
+
990
+ curl -s -X POST "http://localhost:${port}/session/$session/actions" \
991
+ -H 'Content-Type: application/json' \
992
+ -d "$swipe_payload" >/dev/null 2>&1
993
+
994
+ echo "${log_prefix} ✅ Back gesture performed"
995
+ else
996
+ echo "${log_prefix} ❌ Back failed"
997
+ fi
998
+ ;;
999
+
1000
+ "install")
1001
+ local app_path="$cmd_args"
1002
+ echo "${log_prefix} Installing app: $app_path"
1003
+
1004
+ # Check if file exists
1005
+ if [ ! -f "$app_path" ]; then
1006
+ echo "${log_prefix} ❌ App file not found: $app_path"
1007
+ return 1
1008
+ fi
1009
+
1010
+ # Check if it's an IPA file
1011
+ if [[ ! "$app_path" =~ \.ipa$ ]]; then
1012
+ echo "${log_prefix} ❌ File must be a .ipa file: $app_path"
1013
+ return 1
1014
+ fi
1015
+
1016
+ # Get device details for wireless installation
1017
+ local device_details
1018
+ device_details=$(get_device_details "$device_name")
1019
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1020
+
1021
+ if [ -n "$udid" ]; then
1022
+ echo "${log_prefix} 📦 Installing app via wireless-compatible tools..."
1023
+
1024
+ # For wireless devices, prioritize xcrun devicectl
1025
+ if command -v xcrun >/dev/null 2>&1; then
1026
+ # Use xcrun devicectl (works with wireless devices on iOS 17+)
1027
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless installation..."
1028
+ local install_result=$(xcrun devicectl device install app --device "$udid" "$app_path" 2>&1)
1029
+ if [ $? -eq 0 ]; then
1030
+ echo "${log_prefix} ✅ App installed successfully via xcrun devicectl"
1031
+ else
1032
+ echo "${log_prefix} ❌ Installation failed via xcrun devicectl: $install_result"
1033
+ echo "${log_prefix} 💡 For wireless devices, ensure:"
1034
+ echo "${log_prefix} • Device is paired and trusted in Xcode"
1035
+ echo "${log_prefix} • iOS 17+ for best wireless support"
1036
+ fi
1037
+ elif command -v ios-deploy >/dev/null 2>&1; then
1038
+ # Try ios-deploy with wireless flag
1039
+ echo "${log_prefix} 🔄 Trying ios-deploy with wireless support..."
1040
+ local install_result=$(ios-deploy --id "$udid" --bundle "$app_path" 2>&1)
1041
+ if [ $? -eq 0 ]; then
1042
+ echo "${log_prefix} ✅ App installed successfully via ios-deploy"
1043
+ else
1044
+ echo "${log_prefix} ❌ Installation failed via ios-deploy: $install_result"
1045
+ fi
1046
+ elif command -v ideviceinstaller >/dev/null 2>&1; then
1047
+ # ideviceinstaller may not work with wireless, but try anyway
1048
+ echo "${log_prefix} 🔄 Trying ideviceinstaller (may require USB)..."
1049
+ local install_result=$(ideviceinstaller -u "$udid" install "$app_path" 2>&1)
1050
+ if [ $? -eq 0 ]; then
1051
+ echo "${log_prefix} ✅ App installed successfully via ideviceinstaller"
1052
+ else
1053
+ echo "${log_prefix} ❌ Installation failed via ideviceinstaller (wireless not supported): $install_result"
1054
+ fi
1055
+ else
1056
+ echo "${log_prefix} ❌ No wireless-compatible installation tools found"
1057
+ echo "${log_prefix} 💡 For wireless installation:"
1058
+ echo "${log_prefix} • Install via Xcode → Window → Devices and Simulators"
1059
+ echo "${log_prefix} • Or use: xcrun devicectl device install app --device $udid $app_path"
1060
+ fi
1061
+ else
1062
+ echo "${log_prefix} ❌ Device UDID not available"
1063
+ fi
1064
+ ;;
1065
+
1066
+ "uninstall")
1067
+ local bundle_id="$cmd_args"
1068
+ echo "${log_prefix} Uninstalling app: $bundle_id"
1069
+
1070
+ # Get device UDID for wireless uninstallation
1071
+ local device_details
1072
+ device_details=$(get_device_details "$device_name")
1073
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1074
+
1075
+ if [ -n "$udid" ]; then
1076
+ echo "${log_prefix} 🗑️ Uninstalling app via wireless-compatible tools..."
1077
+
1078
+ # For wireless devices, prioritize xcrun devicectl
1079
+ if command -v xcrun >/dev/null 2>&1; then
1080
+ # Use xcrun devicectl (works with wireless devices on iOS 17+)
1081
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless uninstallation..."
1082
+ local uninstall_result=$(xcrun devicectl device uninstall app --device "$udid" "$bundle_id" 2>&1)
1083
+ if [ $? -eq 0 ]; then
1084
+ echo "${log_prefix} ✅ App uninstalled successfully via xcrun devicectl"
1085
+ else
1086
+ echo "${log_prefix} ❌ Uninstallation failed via xcrun devicectl: $uninstall_result"
1087
+ echo "${log_prefix} 💡 For wireless devices, ensure:"
1088
+ echo "${log_prefix} • Device is paired and trusted in Xcode"
1089
+ echo "${log_prefix} • App bundle ID is correct: $bundle_id"
1090
+ fi
1091
+ elif command -v ideviceinstaller >/dev/null 2>&1; then
1092
+ # ideviceinstaller may not work with wireless, but try anyway
1093
+ echo "${log_prefix} 🔄 Trying ideviceinstaller (may require USB)..."
1094
+ local uninstall_result=$(ideviceinstaller -u "$udid" uninstall "$bundle_id" 2>&1)
1095
+ if [ $? -eq 0 ]; then
1096
+ echo "${log_prefix} ✅ App uninstalled successfully via ideviceinstaller"
1097
+ else
1098
+ echo "${log_prefix} ❌ Uninstallation failed via ideviceinstaller (wireless not supported): $uninstall_result"
1099
+ fi
1100
+ else
1101
+ echo "${log_prefix} ❌ No wireless-compatible uninstallation tools found"
1102
+ echo "${log_prefix} 💡 For wireless uninstallation:"
1103
+ echo "${log_prefix} • Uninstall manually on device (long-press app icon)"
1104
+ echo "${log_prefix} • Or use Xcode → Window → Devices and Simulators"
1105
+ echo "${log_prefix} • Or use: xcrun devicectl device uninstall app --device $udid $bundle_id"
1106
+ fi
1107
+ else
1108
+ echo "${log_prefix} ❌ Device UDID not available"
1109
+ fi
1110
+ ;;
1111
+
1112
+ "listapps"|"apps")
1113
+ local search_term="$cmd_args"
1114
+ if [ -n "$search_term" ]; then
1115
+ echo "${log_prefix} 🔍 Searching installed apps for: '$search_term'"
1116
+ echo "${log_prefix} 💡 Searching by app name, bundle ID, and display name..."
1117
+ else
1118
+ echo "${log_prefix} Listing all installed apps..."
1119
+ fi
1120
+
1121
+ # For wireless devices, use WebDriverAgent or xcrun devicectl
1122
+ local device_details
1123
+ device_details=$(get_device_details "$device_name")
1124
+ local port=$(echo "$device_details" | cut -d',' -f1)
1125
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1126
+
1127
+ echo "${log_prefix} 📱 Using wireless device tools for app listing..."
1128
+
1129
+ # Method 1: Try xcrun devicectl for wireless devices (iOS 17+)
1130
+ if command -v xcrun >/dev/null 2>&1 && [ -n "$udid" ]; then
1131
+ echo "${log_prefix} 🔄 Trying xcrun devicectl (wireless-compatible)..."
1132
+
1133
+ if [ -n "$search_term" ]; then
1134
+ # First try exact bundle ID match
1135
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --bundle-id "$search_term" 2>&1)
1136
+ local found_exact=false
1137
+ if [ $? -eq 0 ] && [ -n "$list_result" ] && ! echo "$list_result" | grep -q "No matching apps found"; then
1138
+ echo "${log_prefix} ✅ Found exact bundle ID match: $search_term"
1139
+ echo "$list_result" | while read line; do
1140
+ echo "${log_prefix} $line"
1141
+ done
1142
+ found_exact=true
1143
+ fi
1144
+
1145
+ # Always also search by app name/partial match for better user experience
1146
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --include-all-apps 2>&1)
1147
+ if [ $? -eq 0 ]; then
1148
+ # Enhanced search: look for matches in app name, display name, and bundle ID
1149
+ local filtered_apps=$(echo "$list_result" | grep -i "$search_term" 2>/dev/null)
1150
+ if [ -n "$filtered_apps" ]; then
1151
+ if [ "$found_exact" = false ]; then
1152
+ echo "${log_prefix} ✅ Found matching apps:"
1153
+ else
1154
+ echo "${log_prefix} ✅ Additional matches found:"
1155
+ fi
1156
+ echo "$filtered_apps" | while read line; do
1157
+ echo "${log_prefix} $line"
1158
+ done
1159
+ return
1160
+ elif [ "$found_exact" = false ]; then
1161
+ echo "${log_prefix} ❌ No apps found matching: '$search_term'"
1162
+ fi
1163
+ fi
1164
+ else
1165
+ # List all apps
1166
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --include-all-apps 2>&1)
1167
+ if [ $? -eq 0 ]; then
1168
+ echo "${log_prefix} ✅ App list retrieved via xcrun devicectl"
1169
+ echo "${log_prefix} ✅ Installed apps:"
1170
+ echo "$list_result" | head -20 | while read line; do
1171
+ echo "${log_prefix} $line"
1172
+ done
1173
+ local app_count=$(echo "$list_result" | wc -l)
1174
+ if [ "$app_count" -gt 20 ]; then
1175
+ echo "${log_prefix} ... and $((app_count - 20)) more apps"
1176
+ fi
1177
+ return
1178
+ else
1179
+ echo "${log_prefix} ❌ xcrun devicectl failed: $list_result"
1180
+ fi
1181
+ fi
1182
+ fi
1183
+
1184
+ # Method 2: Use WebDriverAgent to get basic app info
1185
+ local session=$(get_device_session "$device_name")
1186
+ if [ -n "$session" ] && [ -n "$host" ]; then
1187
+ echo "${log_prefix} 🔄 Trying WebDriverAgent for basic app detection..."
1188
+
1189
+ # Test a few common apps to demonstrate the approach
1190
+ local test_apps=(
1191
+ "com.apple.mobilesafari"
1192
+ "com.apple.mobilemail"
1193
+ "com.apple.MobileSMS"
1194
+ "com.apple.camera"
1195
+ "com.apple.mobilenotes"
1196
+ "com.apple.Preferences"
1197
+ "com.mobileiron.enterprise.anyware.ios"
1198
+ "com.facebook.Facebook"
1199
+ "com.google.chrome.ios"
1200
+ "com.microsoft.Office.Outlook"
1201
+ )
1202
+
1203
+ echo "${log_prefix} 📱 Checking common/enterprise apps via WebDriverAgent..."
1204
+ local found_apps=()
1205
+
1206
+ for bundle_id in "${test_apps[@]}"; do
1207
+ local app_state_response=$(curl -s -X GET "http://localhost:${port}/wda/apps/state" \
1208
+ -H 'Content-Type: application/json' \
1209
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
1210
+
1211
+ if echo "$app_state_response" | grep -q '"value"' && ! echo "$app_state_response" | grep -q '"error"'; then
1212
+ local state_value=$(echo "$app_state_response" | python3 -c "
1213
+ import sys, json
1214
+ try:
1215
+ data = json.load(sys.stdin)
1216
+ state = data.get('value', 0)
1217
+ if state > 0: # App is installed if state > 0
1218
+ print('installed')
1219
+ else:
1220
+ print('not_installed')
1221
+ except:
1222
+ print('error')" 2>/dev/null)
1223
+
1224
+ if [ "$state_value" = "installed" ]; then
1225
+ found_apps+=("$bundle_id")
1226
+ fi
1227
+ fi
1228
+ done
1229
+
1230
+ if [ ${#found_apps[@]} -gt 0 ]; then
1231
+ echo "${log_prefix} ✅ Found installed apps via WebDriverAgent:"
1232
+ local matches=0
1233
+ for app in "${found_apps[@]}"; do
1234
+ if [ -z "$search_term" ] || echo "$app" | grep -qi "$search_term"; then
1235
+ echo "${log_prefix} $app - installed"
1236
+ ((matches++))
1237
+ fi
1238
+ done
1239
+
1240
+ if [ -n "$search_term" ] && [ $matches -eq 0 ]; then
1241
+ echo "${log_prefix} ❌ No apps found matching: '$search_term'"
1242
+ elif [ -n "$search_term" ]; then
1243
+ echo "${log_prefix} 🎯 Found $matches app(s) matching: '$search_term'"
1244
+ fi
1245
+ else
1246
+ echo "${log_prefix} ❌ No apps detected via WebDriverAgent"
1247
+ fi
1248
+ else
1249
+ echo "${log_prefix} ❌ No WebDriverAgent session available"
1250
+ fi
1251
+
1252
+ # Method 3: Suggest manual verification
1253
+ echo "${log_prefix} 💡 For complete app listing on wireless devices:"
1254
+ echo "${log_prefix} • Use Xcode → Window → Devices and Simulators"
1255
+ echo "${log_prefix} • Or check specific apps with: appinfo <bundle_id>"
1256
+ ;;
1257
+
1258
+ "debug"|"test")
1259
+ echo "${log_prefix} 🔍 Running debug tests..."
1260
+
1261
+ # Get device UDID
1262
+ local device_details
1263
+ device_details=$(get_device_details "$device_name")
1264
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1265
+
1266
+ echo "${log_prefix} Device: $device_name"
1267
+ echo "${log_prefix} UDID: $udid"
1268
+
1269
+ if [ -n "$udid" ]; then
1270
+ echo "${log_prefix} 🧪 Testing idevice tools..."
1271
+
1272
+ # Test 1: Check if device is detected
1273
+ echo "${log_prefix} Test 1: Device detection"
1274
+ if command -v idevice_id >/dev/null 2>&1; then
1275
+ local detected_devices=$(idevice_id -l 2>/dev/null)
1276
+ if echo "$detected_devices" | grep -q "$udid"; then
1277
+ echo "${log_prefix} ✅ Device detected by idevice_id"
1278
+ else
1279
+ echo "${log_prefix} ❌ Device NOT detected by idevice_id"
1280
+ echo "${log_prefix} Available devices: $detected_devices"
1281
+ fi
1282
+ else
1283
+ echo "${log_prefix} ❌ idevice_id not found"
1284
+ fi
1285
+
1286
+ # Test 2: Raw ideviceinstaller command
1287
+ echo "${log_prefix} Test 2: Raw ideviceinstaller"
1288
+ local raw_command="ideviceinstaller -u '$udid' list --user"
1289
+ echo "${log_prefix} Command: $raw_command"
1290
+ local raw_result=$(eval $raw_command 2>&1)
1291
+ local raw_exit=$?
1292
+ echo "${log_prefix} Exit code: $raw_exit"
1293
+ if [ $raw_exit -eq 0 ]; then
1294
+ local app_count=$(echo "$raw_result" | wc -l)
1295
+ echo "${log_prefix} ✅ Success! Found $app_count apps"
1296
+ echo "${log_prefix} Sample apps:"
1297
+ echo "$raw_result" | head -3 | while read line; do
1298
+ echo "${log_prefix} $line"
1299
+ done
1300
+ else
1301
+ echo "${log_prefix} ❌ Failed: $raw_result"
1302
+ fi
1303
+
1304
+ # Test 3: Your specific app search
1305
+ echo "${log_prefix} Test 3: MobileIron app search"
1306
+ local mobileiron_search=$(eval $raw_command 2>/dev/null | grep -i mobileiron)
1307
+ if [ -n "$mobileiron_search" ]; then
1308
+ echo "${log_prefix} ✅ Found MobileIron apps:"
1309
+ echo "$mobileiron_search" | while read line; do
1310
+ echo "${log_prefix} $line"
1311
+ done
1312
+ else
1313
+ echo "${log_prefix} ❌ No MobileIron apps found"
1314
+ fi
1315
+
1316
+ else
1317
+ echo "${log_prefix} ❌ No UDID available for testing"
1318
+ fi
1319
+ ;;
1320
+
1321
+ "appinfo"|"version")
1322
+ local bundle_id="$cmd_args"
1323
+ echo "${log_prefix} Getting app info: $bundle_id"
1324
+
1325
+ # Get device UDID for app info
1326
+ local device_details
1327
+ device_details=$(get_device_details "$device_name")
1328
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1329
+
1330
+ if [ -n "$udid" ]; then
1331
+ echo "${log_prefix} 📱 Checking app info via iOS tools..."
1332
+
1333
+ # Try different methods to get app info
1334
+ if command -v ideviceinstaller >/dev/null 2>&1; then
1335
+ # Method 1: Search by exact bundle ID
1336
+ echo "${log_prefix} 🔍 Searching for exact bundle ID: $bundle_id"
1337
+ local app_info=$(ideviceinstaller -u "$udid" list --bundle-identifier "$bundle_id" 2>/dev/null)
1338
+
1339
+ if [ -n "$app_info" ]; then
1340
+ echo "${log_prefix} ✅ App found (exact match):"
1341
+ echo "${log_prefix} $app_info"
1342
+ else
1343
+ # Method 2: List all apps and grep for partial matches
1344
+ echo "${log_prefix} 🔍 Searching all installed apps for pattern..."
1345
+ local all_apps=$(ideviceinstaller -u "$udid" list --all 2>/dev/null)
1346
+ local found_apps=$(echo "$all_apps" | grep -i "$bundle_id" 2>/dev/null)
1347
+
1348
+ if [ -n "$found_apps" ]; then
1349
+ echo "${log_prefix} ✅ Found matching apps:"
1350
+ echo "$found_apps" | while read line; do
1351
+ echo "${log_prefix} $line"
1352
+ done
1353
+ else
1354
+ # Method 3: Search by partial bundle ID (useful for enterprise apps)
1355
+ local partial_search=$(echo "$bundle_id" | sed 's/.*\.//') # Get last part after dot
1356
+ local partial_apps=$(echo "$all_apps" | grep -i "$partial_search" 2>/dev/null)
1357
+
1358
+ if [ -n "$partial_apps" ]; then
1359
+ echo "${log_prefix} ✅ Found apps with similar names:"
1360
+ echo "$partial_apps" | while read line; do
1361
+ echo "${log_prefix} $line"
1362
+ done
1363
+ else
1364
+ echo "${log_prefix} ❌ App not found: $bundle_id"
1365
+ fi
1366
+ fi
1367
+ fi
1368
+ elif command -v xcrun >/dev/null 2>&1; then
1369
+ # Use xcrun devicectl to get app info (iOS 17+)
1370
+ echo "${log_prefix} 🔄 Checking via xcrun devicectl..."
1371
+ local app_info=$(xcrun devicectl device info apps --device "$udid" --bundle-id "$bundle_id" 2>&1)
1372
+
1373
+ if [ $? -eq 0 ] && [ -n "$app_info" ] && ! echo "$app_info" | grep -q "No matching apps found"; then
1374
+ echo "${log_prefix} ✅ App info:"
1375
+ echo "$app_info" | while read line; do
1376
+ echo "${log_prefix} $line"
1377
+ done
1378
+ else
1379
+ echo "${log_prefix} ❌ App not found or info unavailable: $bundle_id"
1380
+ echo "${log_prefix} Response: $app_info"
1381
+ fi
1382
+ else
1383
+ echo "${log_prefix} ❌ No iOS app info tools found"
1384
+ echo "${log_prefix} Please install ideviceinstaller or use Xcode"
1385
+ fi
1386
+
1387
+ # Alternative: Try to get app info through WebDriverAgent if available
1388
+ local session=$(get_device_session "$device_name")
1389
+ if [ -n "$session" ]; then
1390
+ echo "${log_prefix} 🔍 Checking if app is installed via WDA..."
1391
+ local wda_response=$(curl -s -X GET "http://localhost:${port}/wda/apps/state" \
1392
+ -H 'Content-Type: application/json' \
1393
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
1394
+
1395
+ if echo "$wda_response" | grep -q '"value"' && ! echo "$wda_response" | grep -q '"error"'; then
1396
+ local app_state=$(echo "$wda_response" | python3 -c "
1397
+ import sys, json
1398
+ try:
1399
+ data = json.load(sys.stdin)
1400
+ state = data.get('value', 0)
1401
+ states = {0: 'Not installed', 1: 'Not running', 2: 'Running in background', 3: 'Running in foreground', 4: 'Running but suspended'}
1402
+ print(f'App State: {states.get(state, f\"Unknown ({state})\")}')" 2>/dev/null)
1403
+
1404
+ if [ -n "$app_state" ]; then
1405
+ echo "${log_prefix} ✅ $app_state"
1406
+ fi
1407
+ fi
1408
+ fi
1409
+ else
1410
+ echo "${log_prefix} ❌ Device UDID not available"
1411
+ fi
1412
+ ;;
1413
+
1414
+ "darkmode"|"lightmode"|"theme"|"appearance")
1415
+ if [ "$cmd_type" = "appearance" ] && [ -z "$cmd_args" ]; then
1416
+ # Just check current appearance
1417
+ echo "${log_prefix} Checking current appearance..."
1418
+ check_appearance_mode "$device_name"
1419
+ else
1420
+ local mode=""
1421
+ case "$cmd_type" in
1422
+ "darkmode") mode="dark" ;;
1423
+ "lightmode") mode="light" ;;
1424
+ "theme") mode="$cmd_args" ;;
1425
+ "appearance") mode="$cmd_args" ;;
1426
+ esac
1427
+ if [ -z "$mode" ]; then
1428
+ mode="toggle"
1429
+ fi
1430
+ echo "${log_prefix} Setting theme to: $mode"
1431
+
1432
+ # Try multiple methods for theme switching
1433
+ if ! toggle_dark_mode "$device_name" "$mode"; then
1434
+ # Fallback: Try direct URL scheme approach
1435
+ echo "${log_prefix} Trying alternative method..."
1436
+ local session=$(get_device_session "$device_name")
1437
+ if [ -n "$session" ]; then
1438
+ case "$mode" in
1439
+ "dark"|"light")
1440
+ # Use Settings URL scheme
1441
+ local url_result=$(curl -s -X POST "http://localhost:${port}/session/$session/url" \
1442
+ -H "Content-Type: application/json" \
1443
+ -d '{"url": "prefs:root=DISPLAY"}' 2>/dev/null)
1444
+
1445
+ if echo "$url_result" | grep -q '"sessionId"'; then
1446
+ echo "${log_prefix} ✅ Opened Display settings - manual selection may be needed"
1447
+ sleep 3
1448
+ execute_wda_command "$device_name" "home" >/dev/null
1449
+ else
1450
+ echo "${log_prefix} 💡 Please manually go to Settings > Display & Brightness > Appearance"
1451
+ fi
1452
+ ;;
1453
+ esac
1454
+ fi
1455
+ fi
1456
+ fi
1457
+ ;;
1458
+
1459
+ "rotate"|"rotation")
1460
+ local orientation="$cmd_args"
1461
+ if [ -z "$orientation" ]; then
1462
+ orientation="toggle"
1463
+ fi
1464
+ echo "${log_prefix} Rotating screen to: $orientation"
1465
+ rotate_screen "$device_name" "$orientation"
1466
+ ;;
1467
+
1468
+ "rotationlock"|"lockrotation")
1469
+ local lock_state="$cmd_args"
1470
+ if [ -z "$lock_state" ]; then
1471
+ lock_state="toggle"
1472
+ fi
1473
+ echo "${log_prefix} Setting rotation lock: $lock_state"
1474
+ toggle_rotation_lock "$device_name" "$lock_state"
1475
+ ;;
1476
+
1477
+ "restart"|"reboot")
1478
+ echo "${log_prefix} 🔄 Attempting device restart..."
1479
+
1480
+ # Get device UDID for wireless restart
1481
+ local device_details
1482
+ device_details=$(get_device_details "$device_name")
1483
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1484
+
1485
+ if [ -n "$udid" ]; then
1486
+ echo "${log_prefix} 🔄 Restarting device via wireless-compatible tools..."
1487
+
1488
+ # Method 1: Try xcrun devicectl (works with wireless devices on iOS 17+)
1489
+ if command -v xcrun >/dev/null 2>&1; then
1490
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless restart..."
1491
+
1492
+ # First try with hardware UDID
1493
+ local restart_result=$(xcrun devicectl device reboot --device "$udid" --timeout 10 2>&1)
1494
+ if [ $? -eq 0 ]; then
1495
+ echo "${log_prefix} ✅ Restart command sent via xcrun devicectl (UDID)"
1496
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1497
+ else
1498
+ # Try to find device by name in devicectl list and use that identifier
1499
+ echo "${log_prefix} 🔍 Searching for device in CoreDevice list..."
1500
+ local coredevice_info=$(xcrun devicectl list devices --timeout 5 2>/dev/null | grep -v "Name\|--" | head -20)
1501
+
1502
+ # Look for connected devices first, then any device
1503
+ local target_device=""
1504
+ if [ -n "$coredevice_info" ]; then
1505
+ # First try to find a connected device
1506
+ target_device=$(echo "$coredevice_info" | grep "connected" | head -1 | awk '{print $1}')
1507
+
1508
+ # If no connected device, try any available device
1509
+ if [ -z "$target_device" ]; then
1510
+ target_device=$(echo "$coredevice_info" | head -1 | awk '{print $1}')
1511
+ fi
1512
+ fi
1513
+
1514
+ if [ -n "$target_device" ]; then
1515
+ echo "${log_prefix} 🎯 Found target device: $target_device"
1516
+ local retry_result=$(xcrun devicectl device reboot --device "$target_device" --timeout 10 2>&1)
1517
+ if [ $? -eq 0 ]; then
1518
+ echo "${log_prefix} ✅ Restart command sent via xcrun devicectl (device name)"
1519
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1520
+ else
1521
+ echo "${log_prefix} ❌ Restart failed with both UDID and device name"
1522
+ echo "${log_prefix} 💡 Error: $retry_result"
1523
+
1524
+ # Final fallback to USB method
1525
+ echo "${log_prefix} 🔄 Trying USB fallback..."
1526
+ local device_control_script="$SCRIPT_DIR/ios_device_control.sh"
1527
+ if [ -f "$device_control_script" ] && "$device_control_script" restart "$device_name" 2>&1 | grep -q "✅"; then
1528
+ echo "${log_prefix} ✅ Restart command sent via USB fallback"
1529
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1530
+ else
1531
+ echo "${log_prefix} ❌ All restart methods failed"
1532
+ echo "${log_prefix} 💡 Try connecting device via USB cable"
1533
+ fi
1534
+ fi
1535
+ else
1536
+ echo "${log_prefix} ❌ No devices found in CoreDevice list"
1537
+ echo "${log_prefix} 💡 Ensure device is paired and trusted in Xcode"
1538
+ fi
1539
+ fi
1540
+ elif command -v idevicediagnostics >/dev/null 2>&1; then
1541
+ # Fallback to USB-based restart
1542
+ echo "${log_prefix} 🔄 xcrun not available, trying USB restart..."
1543
+ if idevicediagnostics -u "$udid" restart 2>/dev/null; then
1544
+ echo "${log_prefix} ✅ Restart command sent via USB"
1545
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1546
+ else
1547
+ echo "${log_prefix} ❌ Restart failed - device must be USB connected"
1548
+ echo "${log_prefix} 💡 For wireless devices, ensure iOS 17+ and Xcode pairing"
1549
+ fi
1550
+ else
1551
+ echo "${log_prefix} ❌ No restart tools available"
1552
+ echo "${log_prefix} 💡 Install Xcode command line tools or libimobiledevice"
1553
+ fi
1554
+ else
1555
+ echo "${log_prefix} ❌ Device UDID not available"
1556
+ fi
1557
+ ;;
1558
+
1559
+ "shutdown"|"poweroff")
1560
+ echo "${log_prefix} 🔌 Attempting device shutdown..."
1561
+ echo "${log_prefix} ⚠️ WARNING: Wireless shutdown not supported - will attempt restart instead!"
1562
+
1563
+ # Get device UDID for wireless operations
1564
+ local device_details
1565
+ device_details=$(get_device_details "$device_name")
1566
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1567
+
1568
+ if [ -n "$udid" ]; then
1569
+ echo "${log_prefix} 🔄 Note: xcrun devicectl doesn't support shutdown, using restart..."
1570
+
1571
+ # Method 1: Try xcrun devicectl reboot (closest alternative for wireless)
1572
+ if command -v xcrun >/dev/null 2>&1; then
1573
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless restart (shutdown not supported)..."
1574
+
1575
+ # First try with hardware UDID
1576
+ local restart_result=$(xcrun devicectl device reboot --device "$udid" --timeout 10 2>&1)
1577
+ if [ $? -eq 0 ]; then
1578
+ echo "${log_prefix} ⚠️ Device will restart instead of shutdown (wireless limitation)"
1579
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1580
+ else
1581
+ # Try to find device by name in devicectl list
1582
+ echo "${log_prefix} 🔍 Searching for device in CoreDevice list..."
1583
+ local coredevice_info=$(xcrun devicectl list devices --timeout 5 2>/dev/null | grep -v "Name\|--" | head -20)
1584
+
1585
+ # Look for connected devices first
1586
+ local target_device=""
1587
+ if [ -n "$coredevice_info" ]; then
1588
+ target_device=$(echo "$coredevice_info" | grep "connected" | head -1 | awk '{print $1}')
1589
+ if [ -z "$target_device" ]; then
1590
+ target_device=$(echo "$coredevice_info" | head -1 | awk '{print $1}')
1591
+ fi
1592
+ fi
1593
+
1594
+ if [ -n "$target_device" ]; then
1595
+ echo "${log_prefix} 🎯 Found target device: $target_device"
1596
+ local retry_result=$(xcrun devicectl device reboot --device "$target_device" --timeout 10 2>&1)
1597
+ if [ $? -eq 0 ]; then
1598
+ echo "${log_prefix} ⚠️ Device will restart instead of shutdown (wireless limitation)"
1599
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1600
+ else
1601
+ echo "${log_prefix} ❌ Wireless restart failed, trying USB shutdown..."
1602
+
1603
+ # Final fallback to USB shutdown
1604
+ local device_control_script="$SCRIPT_DIR/ios_device_control.sh"
1605
+ if [ -f "$device_control_script" ]; then
1606
+ # Auto-confirm for batch operations
1607
+ if echo "y" | "$device_control_script" shutdown "$device_name" 2>&1 | grep -q "✅"; then
1608
+ echo "${log_prefix} ✅ Shutdown command sent via USB fallback"
1609
+ echo "${log_prefix} ⏳ Device will power off in a few seconds..."
1610
+ else
1611
+ echo "${log_prefix} ❌ Both wireless and USB methods failed"
1612
+ echo "${log_prefix} 💡 Try connecting via USB or manually power off device"
1613
+ fi
1614
+ fi
1615
+ fi
1616
+ else
1617
+ echo "${log_prefix} ❌ No devices found for wireless operation"
1618
+ echo "${log_prefix} 💡 Ensure device is paired and trusted in Xcode"
1619
+ fi
1620
+ fi
1621
+ elif command -v idevicediagnostics >/dev/null 2>&1; then
1622
+ # Fallback to USB-based shutdown
1623
+ echo "${log_prefix} 🔄 xcrun not available, trying USB shutdown..."
1624
+ if idevicediagnostics -u "$udid" shutdown 2>/dev/null; then
1625
+ echo "${log_prefix} ✅ Shutdown command sent via USB"
1626
+ echo "${log_prefix} ⏳ Device will power off in a few seconds..."
1627
+ else
1628
+ echo "${log_prefix} ❌ Shutdown failed - device must be USB connected"
1629
+ echo "${log_prefix} 💡 For wireless devices, only restart is supported"
1630
+ fi
1631
+ else
1632
+ echo "${log_prefix} ❌ No device control tools available"
1633
+ echo "${log_prefix} 💡 Install Xcode command line tools or libimobiledevice"
1634
+ fi
1635
+ else
1636
+ echo "${log_prefix} ❌ Device UDID not available"
1637
+ fi
1638
+ ;;
1639
+
1640
+ "back"|"goback"|"navigate back"|"←")
1641
+ echo "${log_prefix} CASE: Back navigation matched!"
1642
+ local back_context="$cmd_args"
1643
+
1644
+ local session=$(get_device_session "$device_name")
1645
+ if [ -n "$session" ]; then
1646
+ echo "${log_prefix} 🔙 Executing smart back navigation..."
1647
+
1648
+ # Method 1: Try back button click first (most reliable)
1649
+ echo "${log_prefix} Trying Method 1: Back button click..."
1650
+ local back_click_payload='{
1651
+ "actions": [
1652
+ {
1653
+ "type": "pointer",
1654
+ "id": "finger1",
1655
+ "parameters": {"pointerType": "touch"},
1656
+ "actions": [
1657
+ {"type": "pointerMove", "duration": 0, "x": 40, "y": 110},
1658
+ {"type": "pointerDown", "button": 0},
1659
+ {"type": "pause", "duration": 150},
1660
+ {"type": "pointerUp", "button": 0}
1661
+ ]
1662
+ }
1663
+ ]
1664
+ }'
1665
+
1666
+ local click_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
1667
+ -H 'Content-Type: application/json' \
1668
+ -d "$back_click_payload" 2>/dev/null)
1669
+
1670
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
1671
+ echo "${log_prefix} ✅ Back navigation via button click"
1672
+ else
1673
+ # Method 2: Try improved iOS native back gesture (proper edge swipe)
1674
+ echo "${log_prefix} Trying Method 2: iOS back gesture (proper edge swipe)..."
1675
+ local swipe_payload='{
1676
+ "actions": [
1677
+ {
1678
+ "type": "pointer",
1679
+ "id": "finger1",
1680
+ "parameters": {"pointerType": "touch"},
1681
+ "actions": [
1682
+ {"type": "pointerMove", "duration": 0, "x": 0, "y": 400},
1683
+ {"type": "pointerDown", "button": 0},
1684
+ {"type": "pointerMove", "duration": 800, "x": 250, "y": 400},
1685
+ {"type": "pointerUp", "button": 0}
1686
+ ]
1687
+ }
1688
+ ]
1689
+ }'
1690
+
1691
+ local swipe_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
1692
+ -H 'Content-Type: application/json' \
1693
+ -d "$swipe_payload" 2>/dev/null)
1694
+
1695
+ if echo "$swipe_response" | grep -q '"sessionId"' && ! echo "$swipe_response" | grep -q '"error"'; then
1696
+ echo "${log_prefix} ✅ Back navigation via edge swipe"
1697
+ else
1698
+ # Method 3: Try WebDriverAgent drag method as fallback
1699
+ echo "${log_prefix} Trying Method 3: WDA drag method..."
1700
+ local drag_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
1701
+ -H 'Content-Type: application/json' \
1702
+ -d '{"fromX": 0, "fromY": 400, "toX": 250, "toY": 400, "duration": 0.8}' 2>/dev/null)
1703
+
1704
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
1705
+ echo "${log_prefix} ✅ Back navigation via drag method"
1706
+ else
1707
+ # Method 4: Try context-specific back navigation
1708
+ echo "${log_prefix} Trying Method 4: Context-specific navigation..."
1709
+ case "${back_context,,}" in
1710
+ "settings"|"preferences")
1711
+ # Settings app back - try Settings-specific position
1712
+ local settings_back='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":35,"y":94},{"type":"pointerDown","button":0},{"type":"pause","duration":100},{"type":"pointerUp","button":0}]}]}'
1713
+ curl -s -X POST "http://localhost:${port}/session/$session/actions" -H 'Content-Type: application/json' -d "$settings_back" >/dev/null
1714
+ echo "${log_prefix} ✅ Settings back navigation"
1715
+ ;;
1716
+ "safari"|"browser"|"web")
1717
+ # Safari back navigation
1718
+ local safari_back='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":44,"y":94},{"type":"pointerDown","button":0},{"type":"pause","duration":100},{"type":"pointerUp","button":0}]}]}'
1719
+ curl -s -X POST "http://localhost:${port}/session/$session/actions" -H 'Content-Type: application/json' -d "$safari_back" >/dev/null
1720
+ echo "${log_prefix} ✅ Safari back navigation"
1721
+ ;;
1722
+ *)
1723
+ # Method 5: Hardware home button simulation (universal fallback)
1724
+ echo "${log_prefix} Trying Method 5: Home button fallback..."
1725
+ local home_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/homescreen" \
1726
+ -H 'Content-Type: application/json' -d '{}' 2>/dev/null)
1727
+
1728
+ if echo "$home_response" | grep -q '"sessionId"' && ! echo "$home_response" | grep -q '"error"'; then
1729
+ echo "${log_prefix} ✅ Navigated to home screen"
1730
+ else
1731
+ echo "${log_prefix} ❌ All back navigation methods failed"
1732
+ fi
1733
+ ;;
1734
+ esac
1735
+ fi
1736
+ fi
1737
+ fi
1738
+ else
1739
+ echo "${log_prefix} ❌ No session available"
1740
+ fi
1741
+ ;;
1742
+
1743
+ "click"|"tap")
1744
+ echo "${log_prefix} CASE: Accessibility-first Click Activated"
1745
+ local coords="$cmd_args"
1746
+
1747
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
1748
+ # Direct coordinates - use precise W3C Actions API
1749
+ local x=$(echo "$coords" | cut -d',' -f1)
1750
+ local y=$(echo "$coords" | cut -d',' -f2)
1751
+ echo "${log_prefix} 🎯 Precise coordinate click: ($x, $y)"
1752
+
1753
+ local session=$(get_device_session "$device_name")
1754
+ if [ -n "$session" ]; then
1755
+ local actions_payload='{
1756
+ "actions": [
1757
+ {
1758
+ "type": "pointer",
1759
+ "id": "finger1",
1760
+ "parameters": {"pointerType": "touch"},
1761
+ "actions": [
1762
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
1763
+ {"type": "pointerDown", "button": 0},
1764
+ {"type": "pause", "duration": 100},
1765
+ {"type": "pointerUp", "button": 0}
1766
+ ]
1767
+ }
1768
+ ]
1769
+ }'
1770
+
1771
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
1772
+ -H 'Content-Type: application/json' \
1773
+ -d "$actions_payload" 2>/dev/null)
1774
+
1775
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
1776
+ echo "${log_prefix} ✅ Coordinate click successful at ($x, $y)"
1777
+ else
1778
+ echo "${log_prefix} ❌ Coordinate click failed"
1779
+ echo "${log_prefix} Response: $response"
1780
+ fi
1781
+ else
1782
+ echo "${log_prefix} ❌ No session available"
1783
+ fi
1784
+ else
1785
+ # Non-AI click: Inline accessibility-first implementation
1786
+ local text="$coords"
1787
+ echo "${log_prefix} 🔎 Accessibility search: '$text'"
1788
+ local session=$(get_device_session "$device_name")
1789
+ if [ -n "$session" ]; then
1790
+ # Method 1: Try accessibility ID
1791
+ local acc_elements=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
1792
+ -H 'Content-Type: application/json' \
1793
+ -d '{"using":"accessibility id","value":"'"$text"'"}' 2>/dev/null)
1794
+
1795
+ local element_id=$(echo "$acc_elements" | python3 -c "
1796
+ import sys, json
1797
+ try:
1798
+ data = json.load(sys.stdin)
1799
+ elements = data.get('value', [])
1800
+ if elements and 'ELEMENT' in elements[0]:
1801
+ print(elements[0]['ELEMENT'])
1802
+ except: pass
1803
+ " 2>/dev/null)
1804
+
1805
+ if [ -z "$element_id" ]; then
1806
+ # Method 2: Try name attribute
1807
+ local name_elements=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
1808
+ -H 'Content-Type: application/json' \
1809
+ -d '{"using":"name","value":"'"$text"'"}' 2>/dev/null)
1810
+ element_id=$(echo "$name_elements" | python3 -c "
1811
+ import sys, json
1812
+ try:
1813
+ data = json.load(sys.stdin)
1814
+ elements = data.get('value', [])
1815
+ if elements and 'ELEMENT' in elements[0]:
1816
+ print(elements[0]['ELEMENT'])
1817
+ except: pass
1818
+ " 2>/dev/null)
1819
+ fi
1820
+
1821
+ if [ -n "$element_id" ]; then
1822
+ echo "${log_prefix} ✅ Found element via accessibility"
1823
+ # Find clickable parent if element is StaticText or Cell
1824
+ local clickable_element=$(find_clickable_element "$session" "$port" "$element_id" "$device_name")
1825
+ if [ -n "$clickable_element" ]; then
1826
+ element_id="$clickable_element"
1827
+ fi
1828
+ # Get element rect for coordinates
1829
+ local rect_response=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/rect" 2>/dev/null)
1830
+ local elem_x=$(echo "$rect_response" | python3 -c "
1831
+ import sys, json
1832
+ try:
1833
+ data = json.load(sys.stdin)
1834
+ value = data.get('value', {})
1835
+ print(int(value.get('x', 0) + value.get('width', 0) / 2))
1836
+ except: print(0)
1837
+ " 2>/dev/null)
1838
+ local elem_y=$(echo "$rect_response" | python3 -c "
1839
+ import sys, json
1840
+ try:
1841
+ data = json.load(sys.stdin)
1842
+ value = data.get('value', {})
1843
+ print(int(value.get('y', 0) + value.get('height', 0) / 2))
1844
+ except: print(0)
1845
+ " 2>/dev/null)
1846
+
1847
+ if [ "$elem_x" -gt 0 ] && [ "$elem_y" -gt 0 ]; then
1848
+ echo "${log_prefix} 🎯 Element center: ($elem_x,$elem_y)"
1849
+ local elem_actions='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":'$elem_x',"y":'$elem_y'},{"type":"pointerDown","button":0},{"type":"pause","duration":150},{"type":"pointerUp","button":0}]}]}'
1850
+ local elem_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
1851
+ -H 'Content-Type: application/json' -d "$elem_actions" 2>/dev/null)
1852
+
1853
+ if echo "$elem_response" | grep -q '"sessionId"' && ! echo "$elem_response" | grep -q '"error"'; then
1854
+ echo "${log_prefix} ✅ Clicked '$text' at ($elem_x,$elem_y)"
1855
+ else
1856
+ echo "${log_prefix} ❌ Click failed"
1857
+ fi
1858
+ else
1859
+ # Fallback: Direct element click
1860
+ local click_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/click" \
1861
+ -H 'Content-Type: application/json' -d '{}' 2>/dev/null)
1862
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
1863
+ echo "${log_prefix} ✅ Clicked '$text' via element click"
1864
+ else
1865
+ echo "${log_prefix} ❌ Click failed"
1866
+ fi
1867
+ fi
1868
+ else
1869
+ echo "${log_prefix} ❌ Element '$text' not found"
1870
+ fi
1871
+ else
1872
+ echo "${log_prefix} ❌ No session available"
1873
+ fi
1874
+ fi
1875
+ ;;
1876
+
1877
+ "refresh"|"refresh_screen"|"reload_cache")
1878
+ echo "${log_prefix} 🔄 REFRESHING SCREEN CACHE..."
1879
+ refresh_screen_cache "$device_name"
1880
+ echo "${log_prefix} ✅ Screen cache refreshed. Next click will re-analyze screen."
1881
+ ;;
1882
+
1883
+ "longpress"|"longclick")
1884
+ local coords="$cmd_args"
1885
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
1886
+ local x=$(echo "$coords" | cut -d',' -f1)
1887
+ local y=$(echo "$coords" | cut -d',' -f2)
1888
+ echo "${log_prefix} 🎯 Precise long press at ($x, $y)"
1889
+ local session=$(get_device_session "$device_name")
1890
+ if [ -n "$session" ]; then
1891
+ local actions_payload='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":'$x',"y":'$y'},{"type":"pointerDown","button":0},{"type":"pause","duration":800},{"type":"pointerUp","button":0}]}]}'
1892
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" -H 'Content-Type: application/json' -d "$actions_payload" 2>/dev/null)
1893
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
1894
+ echo "${log_prefix} ✅ Long press successful at ($x, $y)"
1895
+ else
1896
+ echo "${log_prefix} ❌ Long press failed"
1897
+ echo "${log_prefix} Response: $response"
1898
+ fi
1899
+ else
1900
+ echo "${log_prefix} ❌ No session available"
1901
+ fi
1902
+ else
1903
+ # Search for element by text - GENIUS SCALABLE APPROACH!
1904
+ local text="$coords"
1905
+ echo "${log_prefix} Searching for element: '$text'"
1906
+
1907
+ local session=$(get_device_session "$device_name")
1908
+ if [ -n "$session" ]; then
1909
+ # Step 1: Try dynamic element detection first (like long press does)
1910
+ echo "${log_prefix} 🔍 Attempting dynamic element detection..."
1911
+ local search_result=$(find_element_by_text "$session" "$host" "$text" "$device_name")
1912
+ local element_id=$(echo "$search_result" | cut -d'|' -f1)
1913
+ local found_strategy=$(echo "$search_result" | cut -d'|' -f2)
1914
+
1915
+ if [ -n "$element_id" ]; then
1916
+ echo "${log_prefix} ✅ Found element dynamically using: $found_strategy"
1917
+ echo "${log_prefix} Element ID: $element_id"
1918
+
1919
+ # Get element center coordinates
1920
+ local coordinates=$(get_element_center "$session" "$host" "$element_id")
1921
+ local x=$(echo "$coordinates" | cut -d',' -f1)
1922
+ local y=$(echo "$coordinates" | cut -d',' -f2)
1923
+
1924
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
1925
+ echo "${log_prefix} 🎯 Dynamic element center: ($x, $y)"
1926
+
1927
+ # Use W3C Actions API for precise click
1928
+ echo "${log_prefix} Trying Method 1: W3C Actions API (dynamic)..."
1929
+ local actions_payload='{
1930
+ "actions": [
1931
+ {
1932
+ "type": "pointer",
1933
+ "id": "finger1",
1934
+ "parameters": {"pointerType": "touch"},
1935
+ "actions": [
1936
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
1937
+ {"type": "pointerDown", "button": 0},
1938
+ {"type": "pause", "duration": 100},
1939
+ {"type": "pointerUp", "button": 0}
1940
+ ]
1941
+ }
1942
+ ]
1943
+ }'
1944
+
1945
+ local tap_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
1946
+ -H 'Content-Type: application/json' \
1947
+ -d "$actions_payload" 2>/dev/null)
1948
+
1949
+ echo "${log_prefix} Method 1 Response: '$tap_response'"
1950
+
1951
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
1952
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Dynamic Detection"
1953
+ echo "${log_prefix} Completed"
1954
+ return
1955
+ fi
1956
+ fi
1957
+ fi
1958
+
1959
+ # Step 2: Fallback to device-specific coordinate mapping
1960
+ echo "${log_prefix} 🔄 Falling back to coordinate mapping..."
1961
+ local text_lower=$(echo "$text" | tr '[:upper:]' '[:lower:]')
1962
+ local x y found=false
1963
+
1964
+ # Get device screen dimensions for coordinate adaptation
1965
+ local screen_width screen_height
1966
+ case "$device_name" in
1967
+ "iphone16p")
1968
+ screen_width=430; screen_height=932
1969
+ ;;
1970
+ "iphone17")
1971
+ screen_width=402; screen_height=874
1972
+ ;;
1973
+ "ipad")
1974
+ screen_width=834; screen_height=1210
1975
+ ;;
1976
+ *)
1977
+ screen_width=400; screen_height=800 # fallback
1978
+ ;;
1979
+ esac
1980
+
1981
+ # Common app icons (home screen)
1982
+ case "$text_lower" in
1983
+ "settings")
1984
+ x=200; y=300; found=true
1985
+ ;;
1986
+ "safari")
1987
+ x=75; y=300; found=true
1988
+ ;;
1989
+ "camera")
1990
+ x=325; y=300; found=true
1991
+ ;;
1992
+ "photos")
1993
+ x=75; y=425; found=true
1994
+ ;;
1995
+ "messages")
1996
+ x=200; y=425; found=true
1997
+ ;;
1998
+ "mail")
1999
+ x=325; y=425; found=true
2000
+ ;;
2001
+ "phone")
2002
+ x=75; y=550; found=true
2003
+ ;;
2004
+ "facetime")
2005
+ x=200; y=550; found=true
2006
+ ;;
2007
+ # Settings app elements
2008
+ "general")
2009
+ x=200; y=200; found=true
2010
+ ;;
2011
+ "wi-fi"|"wifi")
2012
+ x=200; y=150; found=true
2013
+ ;;
2014
+ "bluetooth")
2015
+ x=200; y=200; found=true
2016
+ ;;
2017
+ "cellular")
2018
+ x=200; y=250; found=true
2019
+ ;;
2020
+ "personal hotspot"|"hotspot")
2021
+ x=200; y=300; found=true
2022
+ ;;
2023
+ "privacy & security"|"privacy")
2024
+ x=200; y=400; found=true
2025
+ ;;
2026
+ # Navigation elements - COMPREHENSIVE BACK SYSTEM (device-aware)
2027
+ "settings back"|"settings <"|"< settings")
2028
+ # Back button in Settings app
2029
+ x=$(echo "$screen_width * 0.0875" | bc); y=$(echo "$screen_height * 0.117" | bc); found=true
2030
+ ;;
2031
+ "safari back"|"safari <"|"< safari")
2032
+ # Back button in Safari
2033
+ x=$(echo "$screen_width * 0.11" | bc); y=$(echo "$screen_height * 0.117" | bc); found=true
2034
+ ;;
2035
+ "app back"|"navigation back")
2036
+ # Generic app back button
2037
+ x=$(echo "$screen_width * 0.08" | bc); y=$(echo "$screen_height * 0.1225" | bc); found=true
2038
+ ;;
2039
+ "cancel"|"cancel button")
2040
+ # Top-left area
2041
+ x=$(echo "$screen_width * 0.125" | bc); y=$(echo "$screen_width * 0.125" | bc); found=true
2042
+ ;;
2043
+ "next"|"next button")
2044
+ # Top-right
2045
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2046
+ ;;
2047
+ "edit"|"edit button")
2048
+ # Top-right
2049
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2050
+ ;;
2051
+ "save"|"save button")
2052
+ x=350; y=100; found=true
2053
+ ;;
2054
+ # Tab Bar Navigation (bottom of screen)
2055
+ "tab back"|"previous tab")
2056
+ x=100; y=680; found=true
2057
+ ;;
2058
+ "home tab"|"house"|"🏠")
2059
+ x=200; y=680; found=true
2060
+ ;;
2061
+ "search tab"|"🔍"|"magnifying glass")
2062
+ x=300; y=680; found=true
2063
+ ;;
2064
+ # Browser Navigation
2065
+ "browser back"|"web back")
2066
+ x=44; y=650; found=true
2067
+ ;;
2068
+ "browser forward"|"web forward")
2069
+ x=100; y=650; found=true
2070
+ ;;
2071
+ "refresh"|"reload"|"↻")
2072
+ x=200; y=650; found=true
2073
+ ;;
2074
+ # Control Center elements
2075
+ "wifi toggle"|"wifi button")
2076
+ x=100; y=200; found=true
2077
+ ;;
2078
+ "bluetooth toggle"|"bluetooth button")
2079
+ x=200; y=200; found=true
2080
+ ;;
2081
+ "airplane mode")
2082
+ x=50; y=200; found=true
2083
+ ;;
2084
+ # Common UI Elements - Device-aware coordinates
2085
+ "learn more"|"learn more >")
2086
+ # Typically at bottom half of screen
2087
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2088
+ ;;
2089
+ "continue"|"continue button")
2090
+ # Typically at bottom of screen (75% down)
2091
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.75" | bc); found=true
2092
+ ;;
2093
+ "get started"|"start"|"begin")
2094
+ # Usually in lower half
2095
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2096
+ ;;
2097
+ "sign in"|"log in"|"login")
2098
+ # Mid-lower screen
2099
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.56" | bc); found=true
2100
+ ;;
2101
+ "sign up"|"register"|"create account")
2102
+ # Lower screen
2103
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2104
+ ;;
2105
+ "skip"|"skip for now"|"maybe later")
2106
+ # Top area, left-ish
2107
+ x=$(echo "$screen_width * 0.2" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2108
+ ;;
2109
+ "allow"|"allow access"|"ok"|"okay")
2110
+ # Dialog right side
2111
+ x=$(echo "$screen_width * 0.75" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2112
+ ;;
2113
+ "don't allow"|"deny"|"not now")
2114
+ # Dialog left side
2115
+ x=$(echo "$screen_width * 0.25" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2116
+ ;;
2117
+ "agree"|"accept"|"i agree")
2118
+ # Bottom area
2119
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2120
+ ;;
2121
+ "try again"|"retry")
2122
+ # Mid screen
2123
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.56" | bc); found=true
2124
+ ;;
2125
+ # Navigation elements with device-aware coordinates
2126
+ "back"|"< back"|"back button"|"←"|"<")
2127
+ # Top-left corner
2128
+ x=$(echo "$screen_width * 0.075" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2129
+ ;;
2130
+ "close"|"×"|"x"|"close button")
2131
+ # Top-right corner
2132
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2133
+ ;;
2134
+ "done"|"done button")
2135
+ # Top-right
2136
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2137
+ ;;
2138
+ "share"|"share button")
2139
+ x=350; y=100; found=true
2140
+ ;;
2141
+ "add"|"+ add"|"add item")
2142
+ x=350; y=100; found=true
2143
+ ;;
2144
+ "delete"|"remove"|"trash")
2145
+ x=350; y=100; found=true
2146
+ ;;
2147
+ "search"|"🔍 search")
2148
+ x=200; y=150; found=true
2149
+ ;;
2150
+ "more"|"more options"|"⋯"|"...")
2151
+ x=350; y=100; found=true
2152
+ ;;
2153
+ "info"|"ⓘ"|"information")
2154
+ x=350; y=100; found=true
2155
+ ;;
2156
+ *)
2157
+ found=false
2158
+ ;;
2159
+ esac
2160
+
2161
+ if [ "$found" = "true" ]; then
2162
+ echo "${log_prefix} ✅ Using known coordinates for '$text': ($x, $y)"
2163
+
2164
+ # Use CORRECT WebDriverAgent endpoint - try multiple methods
2165
+ echo "${log_prefix} Trying Method 1: W3C Actions API..."
2166
+ local actions_payload='{
2167
+ "actions": [
2168
+ {
2169
+ "type": "pointer",
2170
+ "id": "finger1",
2171
+ "parameters": {"pointerType": "touch"},
2172
+ "actions": [
2173
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2174
+ {"type": "pointerDown", "button": 0},
2175
+ {"type": "pause", "duration": 100},
2176
+ {"type": "pointerUp", "button": 0}
2177
+ ]
2178
+ }
2179
+ ]
2180
+ }'
2181
+
2182
+ local tap_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
2183
+ -H 'Content-Type: application/json' \
2184
+ -d "$actions_payload" 2>/dev/null)
2185
+
2186
+ echo "${log_prefix} Method 1 Response: '$tap_response'"
2187
+
2188
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
2189
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - W3C Actions"
2190
+ else
2191
+ # Method 2: Try WebDriverAgent touch endpoint
2192
+ echo "${log_prefix} Trying Method 2: WDA touch endpoint..."
2193
+ local touch_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/touch/perform" \
2194
+ -H 'Content-Type: application/json' \
2195
+ -d '[{"action": "press", "options": {"x": '$x', "y": '$y'}}, {"action": "wait", "options": {"ms": 100}}, {"action": "release"}]' 2>/dev/null)
2196
+
2197
+ echo "${log_prefix} Method 2 Response: '$touch_response'"
2198
+
2199
+ if echo "$touch_response" | grep -q '"sessionId"' && ! echo "$touch_response" | grep -q '"error"'; then
2200
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - WDA Touch"
2201
+ else
2202
+ # Method 3: Try dragFromToForDuration with same point (acts as tap)
2203
+ echo "${log_prefix} Trying Method 3: Drag endpoint as tap..."
2204
+ local drag_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
2205
+ -H 'Content-Type: application/json' \
2206
+ -d '{"fromX": '$x', "fromY": '$y', "toX": '$x', "toY": '$y', "duration": 0.1}' 2>/dev/null)
2207
+
2208
+ echo "${log_prefix} Method 3 Response: '$drag_response'"
2209
+
2210
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
2211
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Drag Method"
2212
+ else
2213
+ # Method 4: Try element-based approach with known coordinates
2214
+ echo "${log_prefix} Trying Method 4: Element at coordinates..."
2215
+ local element_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element" \
2216
+ -H 'Content-Type: application/json' \
2217
+ -d '{"using": "xpath", "value": "//XCUIElementTypeApplication"}' 2>/dev/null)
2218
+
2219
+ if echo "$element_response" | grep -q '"ELEMENT"'; then
2220
+ local element_id=$(echo "$element_response" | python3 -c "
2221
+ import sys, json
2222
+ try:
2223
+ data = json.load(sys.stdin)
2224
+ print(data.get('value', {}).get('ELEMENT', ''))
2225
+ except: pass" 2>/dev/null)
2226
+
2227
+ if [ -n "$element_id" ]; then
2228
+ local click_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/click" \
2229
+ -H 'Content-Type: application/json' \
2230
+ -d "{\"x\": $x, \"y\": $y}" 2>/dev/null)
2231
+
2232
+ echo "${log_prefix} Method 4 Response: '$click_response'"
2233
+
2234
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
2235
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Element Click"
2236
+ else
2237
+ echo "${log_prefix} ❌ All click methods failed for '$text' at ($x, $y)"
2238
+ echo "${log_prefix} 💡 WebDriverAgent version may not support standard tap endpoints"
2239
+ echo "${log_prefix} 🔍 Debug info - WDA Response: $tap_response"
2240
+ fi
2241
+ fi
2242
+ else
2243
+ echo "${log_prefix} ❌ All click methods failed - no supported endpoints found"
2244
+ echo "${log_prefix} 💡 Check WebDriverAgent version and capabilities"
2245
+ fi
2246
+ fi
2247
+ fi
2248
+ fi
2249
+ else
2250
+ # Advanced fallback: Try element detection with multiple strategies
2251
+ echo "${log_prefix} Unknown element '$text' - using advanced detection..."
2252
+
2253
+ # Strategy 1: Try direct accessibility ID (most common)
2254
+ local element_found=false
2255
+ local element_response=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
2256
+ -H "Content-Type: application/json" \
2257
+ -d "{\"using\": \"accessibility id\", \"value\": \"$text\"}" 2>/dev/null)
2258
+
2259
+ if echo "$element_response" | grep -q '"ELEMENT"'; then
2260
+ echo "${log_prefix} ✅ Found via accessibility ID"
2261
+ # Extract element ID and try multiple interaction methods
2262
+ local element_id=$(echo "$element_response" | python3 -c "
2263
+ import sys, json
2264
+ try:
2265
+ data = json.load(sys.stdin)
2266
+ elements = data.get('value', [])
2267
+ if elements: print(elements[0].get('ELEMENT', ''))
2268
+ except: pass" 2>/dev/null)
2269
+
2270
+ if [ -n "$element_id" ]; then
2271
+ echo "${log_prefix} Element ID: $element_id"
2272
+
2273
+ # Method 1: Try direct element click
2274
+ echo "${log_prefix} Trying element click method..."
2275
+ local click_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/click" \
2276
+ -H 'Content-Type: application/json' \
2277
+ -d '{}' 2>/dev/null)
2278
+
2279
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
2280
+ echo "${log_prefix} ✅ Successfully clicked '$text' (Element click method)"
2281
+ element_found=true
2282
+ else
2283
+ # Method 2: Get element coordinates and use W3C Actions
2284
+ echo "${log_prefix} Trying coordinate extraction..."
2285
+ local rect_response=$(curl -s -X GET "http://localhost:${port}/session/$session/element/$element_id/rect" \
2286
+ -H 'Content-Type: application/json' 2>/dev/null)
2287
+
2288
+ local center_x=$(echo "$rect_response" | python3 -c "
2289
+ import sys, json
2290
+ try:
2291
+ data = json.load(sys.stdin)
2292
+ rect = data.get('value', {})
2293
+ x = rect.get('x', 0)
2294
+ width = rect.get('width', 0)
2295
+ print(int(x + width/2))
2296
+ except: print('0')" 2>/dev/null)
2297
+
2298
+ local center_y=$(echo "$rect_response" | python3 -c "
2299
+ import sys, json
2300
+ try:
2301
+ data = json.load(sys.stdin)
2302
+ rect = data.get('value', {})
2303
+ y = rect.get('y', 0)
2304
+ height = rect.get('height', 0)
2305
+ print(int(y + height/2))
2306
+ except: print('0')" 2>/dev/null)
2307
+
2308
+ if [[ "$center_x" =~ ^[0-9]+$ ]] && [[ "$center_y" =~ ^[0-9]+$ ]] && [ "$center_x" -gt 0 ] && [ "$center_y" -gt 0 ]; then
2309
+ echo "${log_prefix} Element center: ($center_x, $center_y)"
2310
+
2311
+ # Use W3C Actions with extracted coordinates
2312
+ local actions_payload='{
2313
+ "actions": [
2314
+ {
2315
+ "type": "pointer",
2316
+ "id": "finger1",
2317
+ "parameters": {"pointerType": "touch"},
2318
+ "actions": [
2319
+ {"type": "pointerMove", "duration": 0, "x": '$center_x', "y": '$center_y'},
2320
+ {"type": "pointerDown", "button": 0},
2321
+ {"type": "pause", "duration": 100},
2322
+ {"type": "pointerUp", "button": 0}
2323
+ ]
2324
+ }
2325
+ ]
2326
+ }'
2327
+
2328
+ local tap_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
2329
+ -H 'Content-Type: application/json' \
2330
+ -d "$actions_payload" 2>/dev/null)
2331
+
2332
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
2333
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($center_x, $center_y) (Coordinate method)"
2334
+ element_found=true
2335
+ else
2336
+ # Method 3: Fallback to Enter key (for text fields)
2337
+ echo "${log_prefix} Trying Enter key fallback..."
2338
+ local key_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/value" \
2339
+ -H 'Content-Type: application/json' \
2340
+ -d '{"value":["\n"]}' 2>/dev/null)
2341
+
2342
+ if echo "$key_response" | grep -q '"sessionId"' && ! echo "$key_response" | grep -q '"error"'; then
2343
+ echo "${log_prefix} ✅ Successfully activated '$text' (Enter key method)"
2344
+ element_found=true
2345
+ fi
2346
+ fi
2347
+ else
2348
+ echo "${log_prefix} ❌ Could not extract valid coordinates from element"
2349
+ fi
2350
+ fi
2351
+ else
2352
+ echo "${log_prefix} ❌ Could not extract element ID"
2353
+ fi
2354
+ fi
2355
+
2356
+ # Strategy 2: Try partial name matching if not found
2357
+ if [ "$element_found" = "false" ]; then
2358
+ local name_response=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
2359
+ -H "Content-Type: application/json" \
2360
+ -d "{\"using\": \"name\", \"value\": \"$text\"}" 2>/dev/null)
2361
+
2362
+ if echo "$name_response" | grep -q '"ELEMENT"'; then
2363
+ echo "${log_prefix} ✅ Found via name attribute"
2364
+ local element_id=$(echo "$name_response" | python3 -c "
2365
+ import sys, json
2366
+ try:
2367
+ data = json.load(sys.stdin)
2368
+ elements = data.get('value', [])
2369
+ if elements: print(elements[0].get('ELEMENT', ''))
2370
+ except: pass" 2>/dev/null)
2371
+
2372
+ if [ -n "$element_id" ]; then
2373
+ local key_response=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/value" \
2374
+ -H 'Content-Type: application/json' \
2375
+ -d '{"value":["\n"]}' 2>/dev/null)
2376
+
2377
+ if echo "$key_response" | grep -q '"sessionId"' && ! echo "$key_response" | grep -q '"error"'; then
2378
+ echo "${log_prefix} ✅ Successfully activated '$text' (Enter key method)"
2379
+ element_found=true
2380
+ fi
2381
+ fi
2382
+ fi
2383
+ fi
2384
+
2385
+ # Strategy 3: Suggest coordinate-based approach if all fails
2386
+ if [ "$element_found" = "false" ]; then
2387
+ echo "${log_prefix} ❌ Element '$text' not found with any method"
2388
+ echo "${log_prefix} 💡 Try using coordinates: click 200,300"
2389
+ echo "${log_prefix} 💡 Or add '$text' to the coordinate mapping system"
2390
+ fi
2391
+ fi
2392
+ else
2393
+ echo "${log_prefix} ❌ No session available"
2394
+ fi
2395
+ fi
2396
+ ;;
2397
+
2398
+ "longpress"|"longclick")
2399
+ echo "${log_prefix} CASE: Longpress matched!"
2400
+ local coords="$cmd_args"
2401
+ local duration="1.5" # Default 1.5 seconds
2402
+
2403
+ # Check if duration is specified (e.g., "100,200,2.0")
2404
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+,[0-9.]+$ ]]; then
2405
+ duration=$(echo "$coords" | cut -d',' -f3)
2406
+ coords=$(echo "$coords" | cut -d',' -f1,2)
2407
+ fi
2408
+
2409
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
2410
+ # Direct coordinates - use W3C Actions API for more reliable long press
2411
+ local x=$(echo "$coords" | cut -d',' -f1)
2412
+ local y=$(echo "$coords" | cut -d',' -f2)
2413
+ echo "${log_prefix} Long pressing at coordinates: ($x, $y) for ${duration}s"
2414
+
2415
+ local session=$(get_device_session "$device_name")
2416
+ if [ -n "$session" ]; then
2417
+ # Use W3C Actions API for precise long press
2418
+ local duration_ms=$(python3 -c "print(int(float('$duration') * 1000))" 2>/dev/null || echo "1500")
2419
+ local actions_payload='{
2420
+ "actions": [
2421
+ {
2422
+ "type": "pointer",
2423
+ "id": "finger1",
2424
+ "parameters": {"pointerType": "touch"},
2425
+ "actions": [
2426
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2427
+ {"type": "pointerDown", "button": 0},
2428
+ {"type": "pause", "duration": '$duration_ms'},
2429
+ {"type": "pointerUp", "button": 0}
2430
+ ]
2431
+ }
2432
+ ]
2433
+ }'
2434
+
2435
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
2436
+ -H 'Content-Type: application/json' \
2437
+ -d "$actions_payload")
2438
+
2439
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2440
+ echo "${log_prefix} ✅ Long pressed at ($x, $y) for ${duration}s"
2441
+ else
2442
+ # Fallback to touchAndHold endpoint
2443
+ echo "${log_prefix} Trying fallback touchAndHold..."
2444
+ response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/touchAndHold" \
2445
+ -H 'Content-Type: application/json' \
2446
+ -d "{\"x\":$x,\"y\":$y,\"duration\":$duration}")
2447
+
2448
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2449
+ echo "${log_prefix} ✅ Long pressed at ($x, $y)"
2450
+ else
2451
+ echo "${log_prefix} ❌ Long press failed"
2452
+ echo "${log_prefix} Response: $response"
2453
+ fi
2454
+ fi
2455
+ else
2456
+ echo "${log_prefix} ❌ No session available"
2457
+ fi
2458
+ else
2459
+ # Search for element by text - PROFESSIONAL APPIUM WAY
2460
+ local text="$coords"
2461
+ echo "${log_prefix} Searching for element to long press: '$text'"
2462
+
2463
+ local session=$(get_device_session "$device_name")
2464
+ if [ -n "$session" ]; then
2465
+ # Use helper function to find element
2466
+ local search_result=$(find_element_by_text "$session" "$host" "$text" "$device_name")
2467
+ local element_id=$(echo "$search_result" | cut -d'|' -f1)
2468
+ local found_strategy=$(echo "$search_result" | cut -d'|' -f2)
2469
+
2470
+ if [ -n "$element_id" ]; then
2471
+ echo "${log_prefix} ✅ Found element using: $found_strategy"
2472
+
2473
+ # Get element center coordinates
2474
+ local coordinates=$(get_element_center "$session" "$host" "$element_id")
2475
+ local x=$(echo "$coordinates" | cut -d',' -f1)
2476
+ local y=$(echo "$coordinates" | cut -d',' -f2)
2477
+
2478
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
2479
+ echo "${log_prefix} Element center: ($x, $y)"
2480
+
2481
+ # Use W3C Actions API for precise long press
2482
+ local duration_ms=$(python3 -c "print(int(float('$duration') * 1000))" 2>/dev/null || echo "1500")
2483
+ local actions_payload='{
2484
+ "actions": [
2485
+ {
2486
+ "type": "pointer",
2487
+ "id": "finger1",
2488
+ "parameters": {"pointerType": "touch"},
2489
+ "actions": [
2490
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2491
+ {"type": "pointerDown", "button": 0},
2492
+ {"type": "pause", "duration": '$duration_ms'},
2493
+ {"type": "pointerUp", "button": 0}
2494
+ ]
2495
+ }
2496
+ ]
2497
+ }'
2498
+
2499
+ local longpress_response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
2500
+ -H 'Content-Type: application/json' \
2501
+ -d "$actions_payload")
2502
+
2503
+ if echo "$longpress_response" | grep -q '"sessionId"' && ! echo "$longpress_response" | grep -q '"error"'; then
2504
+ echo "${log_prefix} ✅ Successfully long pressed '$text' for ${duration}s"
2505
+ else
2506
+ echo "${log_prefix} ❌ Long press failed on '$text'"
2507
+ echo "${log_prefix} Response: $longpress_response"
2508
+ fi
2509
+ else
2510
+ echo "${log_prefix} ❌ Could not get valid element coordinates"
2511
+ fi
2512
+ else
2513
+ echo "${log_prefix} ❌ Element '$text' not found with any locator strategy"
2514
+ fi
2515
+ else
2516
+ echo "${log_prefix} ❌ No session available"
2517
+ fi
2518
+ fi
2519
+ ;;
2520
+
2521
+ "swipe"|"swipe_up"|"swipe_down"|"swipe_left"|"swipe_right"|"scroll")
2522
+ echo "${log_prefix} CASE: Swipe gesture matched!"
2523
+ local swipe_params="$cmd_args"
2524
+ local direction=""
2525
+ local start_x="" start_y="" end_x="" end_y=""
2526
+ local duration="300" # Default 300ms
2527
+
2528
+ # Get device screen dimensions for optimal coordinates
2529
+ local screen_width screen_height
2530
+
2531
+ # Device-specific coordinate profiles (fallback when session detection fails)
2532
+ case "$device_name" in
2533
+ "iphone16p")
2534
+ screen_width=430; screen_height=932 # iPhone 16 Pro actual resolution
2535
+ echo "${log_prefix} 📱 Using iPhone 16 Pro profile: ${screen_width}×${screen_height}"
2536
+ ;;
2537
+ "iphone17")
2538
+ screen_width=402; screen_height=874 # iPhone 17 actual resolution
2539
+ echo "${log_prefix} 📱 Using iPhone 17 profile: ${screen_width}×${screen_height}"
2540
+ ;;
2541
+ "ipad")
2542
+ screen_width=834; screen_height=1210 # iPad actual resolution
2543
+ echo "${log_prefix} 📱 Using iPad profile: ${screen_width}×${screen_height}"
2544
+ ;;
2545
+ *)
2546
+ # Try to get from session first, then fallback
2547
+ if [ -n "$session" ]; then
2548
+ local screen_info=$(curl -s "http://localhost:${port}/session/$session/window/size" 2>/dev/null)
2549
+ if echo "$screen_info" | grep -q '"width"' 2>/dev/null; then
2550
+ screen_width=$(echo "$screen_info" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['value']['width']) if 'value' in data else print('400')" 2>/dev/null || echo "400")
2551
+ screen_height=$(echo "$screen_info" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['value']['height']) if 'value' in data else print('800')" 2>/dev/null || echo "800")
2552
+ echo "${log_prefix} 📱 Detected via session: ${screen_width}×${screen_height}"
2553
+ fi
2554
+ fi
2555
+ ;;
2556
+ esac
2557
+
2558
+ # Default fallback dimensions
2559
+ screen_width=${screen_width:-400}
2560
+ screen_height=${screen_height:-800}
2561
+
2562
+ # Parse swipe parameters
2563
+ if [[ "$swipe_params" =~ ^(up|down|left|right)$ ]]; then
2564
+ # Device-optimized directional swipes based on actual screen dimensions
2565
+ direction="$swipe_params"
2566
+ local center_x=$((screen_width / 2))
2567
+ local quarter_height=$((screen_height / 4))
2568
+ local three_quarter_height=$((screen_height * 3 / 4))
2569
+ local left_edge=$((screen_width / 8))
2570
+ local right_edge=$((screen_width * 7 / 8))
2571
+ local mid_height=$((screen_height / 2))
2572
+
2573
+ case "$direction" in
2574
+ "up")
2575
+ # Scroll up - start from 75% height, swipe to 25% height
2576
+ start_x=$center_x; start_y=$three_quarter_height; end_x=$center_x; end_y=$quarter_height
2577
+ duration="600" # Slower for better recognition
2578
+ echo "${log_prefix} 👆 Optimized swipe up for ${screen_width}×${screen_height}"
2579
+ ;;
2580
+ "down")
2581
+ # Scroll down - start from 25% height, swipe to 75% height
2582
+ start_x=$center_x; start_y=$quarter_height; end_x=$center_x; end_y=$three_quarter_height
2583
+ duration="600" # Slower for better recognition
2584
+ echo "${log_prefix} 👇 Optimized swipe down for ${screen_width}×${screen_height}"
2585
+ ;;
2586
+ "left")
2587
+ # Swipe left - from right edge to left edge at mid height
2588
+ start_x=$right_edge; start_y=$mid_height; end_x=$left_edge; end_y=$mid_height
2589
+ duration="500" # Medium speed for page transitions
2590
+ echo "${log_prefix} 👈 Optimized swipe left for ${screen_width}×${screen_height}"
2591
+ ;;
2592
+ "right")
2593
+ # Swipe right - from left edge to right edge at mid height
2594
+ start_x=$left_edge; start_y=$mid_height; end_x=$right_edge; end_y=$mid_height
2595
+ duration="500" # Medium speed for page transitions
2596
+ echo "${log_prefix} 👉 Optimized swipe right for ${screen_width}×${screen_height}"
2597
+ ;;
2598
+ esac
2599
+ elif [[ "$swipe_params" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]]; then
2600
+ # Custom coordinates: "x1,y1,x2,y2"
2601
+ start_x=$(echo "$swipe_params" | cut -d',' -f1)
2602
+ start_y=$(echo "$swipe_params" | cut -d',' -f2)
2603
+ end_x=$(echo "$swipe_params" | cut -d',' -f3)
2604
+ end_y=$(echo "$swipe_params" | cut -d',' -f4)
2605
+ direction="custom"
2606
+ elif [[ "$swipe_params" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]]; then
2607
+ # Custom coordinates with duration: "x1,y1,x2,y2,duration_ms"
2608
+ start_x=$(echo "$swipe_params" | cut -d',' -f1)
2609
+ start_y=$(echo "$swipe_params" | cut -d',' -f2)
2610
+ end_x=$(echo "$swipe_params" | cut -d',' -f3)
2611
+ end_y=$(echo "$swipe_params" | cut -d',' -f4)
2612
+ duration=$(echo "$swipe_params" | cut -d',' -f5)
2613
+ direction="custom"
2614
+ else
2615
+ echo "${log_prefix} ❌ Invalid swipe format. Use: swipe up/down/left/right OR swipe x1,y1,x2,y2[,duration]"
2616
+ echo "${log_prefix} Examples: swipe up, swipe 100,200,300,400, swipe 50,50,350,600,500"
2617
+ continue
2618
+ fi
2619
+
2620
+ local session=$(get_device_session "$device_name")
2621
+ if [ -n "$session" ]; then
2622
+ if [ "$direction" = "custom" ]; then
2623
+ echo "${log_prefix} 👆 Swiping custom from ($start_x,$start_y) to ($end_x,$end_y) [${duration}ms]"
2624
+ else
2625
+ echo "${log_prefix} 👆 Swiping ${direction} from ($start_x,$start_y) to ($end_x,$end_y) [${duration}ms] - Screen ${screen_width}×${screen_height}"
2626
+ fi
2627
+
2628
+ # Use W3C Actions API for precise swipe gesture
2629
+ local swipe_payload='{
2630
+ "actions": [
2631
+ {
2632
+ "type": "pointer",
2633
+ "id": "finger1",
2634
+ "parameters": {"pointerType": "touch"},
2635
+ "actions": [
2636
+ {"type": "pointerMove", "duration": 0, "x": '$start_x', "y": '$start_y'},
2637
+ {"type": "pointerDown", "button": 0},
2638
+ {"type": "pointerMove", "duration": '$duration', "x": '$end_x', "y": '$end_y'},
2639
+ {"type": "pointerUp", "button": 0}
2640
+ ]
2641
+ }
2642
+ ]
2643
+ }'
2644
+
2645
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/actions" \
2646
+ -H 'Content-Type: application/json' \
2647
+ -d "$swipe_payload" 2>/dev/null)
2648
+
2649
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2650
+ echo "${log_prefix} ✅ Swipe gesture successful"
2651
+ else
2652
+ # Fallback: Try WebDriverAgent drag endpoint
2653
+ echo "${log_prefix} Trying fallback drag method..."
2654
+ local duration_sec=$(python3 -c "print(float('$duration')/1000)" 2>/dev/null || echo "0.3")
2655
+ local drag_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
2656
+ -H 'Content-Type: application/json' \
2657
+ -d "{\"fromX\": $start_x, \"fromY\": $start_y, \"toX\": $end_x, \"toY\": $end_y, \"duration\": $duration_sec}" 2>/dev/null)
2658
+
2659
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
2660
+ echo "${log_prefix} ✅ Swipe gesture successful (drag method)"
2661
+ else
2662
+ echo "${log_prefix} ❌ Swipe gesture failed"
2663
+ echo "${log_prefix} Response: $response"
2664
+ fi
2665
+ fi
2666
+ else
2667
+ echo "${log_prefix} ❌ No session available"
2668
+ fi
2669
+ ;;
2670
+
2671
+ "getLocators"|"getlocators"|"locators")
2672
+ echo "${log_prefix} 🔍 Getting all locators from current screen"
2673
+
2674
+ local session=$(get_device_session "$device_name")
2675
+ if [ -n "$session" ]; then
2676
+ # Check what app is currently active (don't disrupt it)
2677
+ echo "${log_prefix} 📱 Checking active app..."
2678
+ local active_app_info=$(curl -s -X GET "http://localhost:$port/wda/activeAppInfo" 2>/dev/null)
2679
+ local bundle_id=$(echo "$active_app_info" | python3 -c "
2680
+ import sys, json
2681
+ try:
2682
+ data = json.load(sys.stdin)
2683
+ print(data.get('value', {}).get('bundleId', 'com.apple.springboard'))
2684
+ except:
2685
+ print('com.apple.springboard')
2686
+ " 2>/dev/null)
2687
+
2688
+ if [ "$bundle_id" = "com.apple.springboard" ]; then
2689
+ echo "${log_prefix} ℹ️ Currently on Home Screen"
2690
+ else
2691
+ echo "${log_prefix} ℹ️ Active app: $bundle_id"
2692
+ fi
2693
+
2694
+ echo "${log_prefix} 📱 Fetching page source..."
2695
+ local page_source=$(curl -s -X GET "http://localhost:$port/session/$session/source" 2>/dev/null)
2696
+
2697
+ if [ -n "$page_source" ] && ! echo "$page_source" | grep -q '"error"'; then
2698
+ echo "${log_prefix} 🔍 Extracting locators..."
2699
+
2700
+ # Extract all locator information using Python
2701
+ local locators=$(echo "$page_source" | python3 -c "
2702
+ import sys
2703
+ import json
2704
+ import xml.etree.ElementTree as ET
2705
+
2706
+ try:
2707
+ data = json.load(sys.stdin)
2708
+ xml_str = data.get('value', '')
2709
+
2710
+ if not xml_str:
2711
+ print('❌ No page source found')
2712
+ sys.exit(1)
2713
+
2714
+ root = ET.fromstring(xml_str)
2715
+ locators = []
2716
+
2717
+ # Extract locators from all elements
2718
+ for elem in root.iter():
2719
+ locator_info = {}
2720
+
2721
+ # Get element type
2722
+ elem_type = elem.tag
2723
+ locator_info['type'] = elem_type
2724
+
2725
+ # Get name/label
2726
+ name = elem.get('name', '')
2727
+ label = elem.get('label', '')
2728
+ value = elem.get('value', '')
2729
+
2730
+ if name:
2731
+ locator_info['name'] = name
2732
+ if label:
2733
+ locator_info['label'] = label
2734
+ if value:
2735
+ locator_info['value'] = value
2736
+
2737
+ # Get accessible property
2738
+ accessible = elem.get('accessible', 'false')
2739
+ locator_info['accessible'] = accessible
2740
+
2741
+ # Get visible property
2742
+ visible = elem.get('visible', 'false')
2743
+ locator_info['visible'] = visible
2744
+
2745
+ # Get enabled property
2746
+ enabled = elem.get('enabled', 'true')
2747
+ locator_info['enabled'] = enabled
2748
+
2749
+ # Get coordinates
2750
+ x = elem.get('x', '')
2751
+ y = elem.get('y', '')
2752
+ width = elem.get('width', '')
2753
+ height = elem.get('height', '')
2754
+
2755
+ if x and y and width and height:
2756
+ locator_info['coordinates'] = f'({x},{y}) {width}x{height}'
2757
+
2758
+ # Only add if we have meaningful locator information
2759
+ if name or label or value:
2760
+ locators.append(locator_info)
2761
+
2762
+ # Print formatted output
2763
+ if locators:
2764
+ print(f'\\n📋 Found {len(locators)} interactive elements:\\n')
2765
+ for i, loc in enumerate(locators, 1):
2766
+ # Build single line output with Name first, better formatting
2767
+ parts = []
2768
+
2769
+ # Start with number and Name (most important for clicking)
2770
+ name = loc.get('name', '')
2771
+ if name:
2772
+ parts.append(f'{i:3d}. 📍 {name:40s}')
2773
+ else:
2774
+ parts.append(f'{i:3d}. 📍 {\"(no name)\":40s}')
2775
+
2776
+ # Add Type (shortened for readability)
2777
+ elem_type = loc.get('type', 'unknown').replace('XCUIElementType', '')
2778
+ parts.append(f'[{elem_type:15s}]')
2779
+
2780
+ # Add Label if different from Name
2781
+ label = loc.get('label', '')
2782
+ if label and label != name:
2783
+ parts.append(f'Label:{label:30s}' if len(label) <= 30 else f'Label:{label[:27]}...')
2784
+
2785
+ # Add Value if present
2786
+ value = loc.get('value', '')
2787
+ if value and value != name and value != label:
2788
+ parts.append(f'Val:{value:20s}' if len(value) <= 20 else f'Val:{value[:17]}...')
2789
+
2790
+ # Add Position
2791
+ if loc.get('coordinates'):
2792
+ parts.append(f'@ {loc[\"coordinates\"]}')
2793
+
2794
+ # Add status flags (compact)
2795
+ flags = []
2796
+ if loc.get('visible') == 'true':
2797
+ flags.append('✓Vis')
2798
+ if loc.get('enabled') == 'true':
2799
+ flags.append('✓En')
2800
+ if flags:
2801
+ parts.append(' '.join(flags))
2802
+
2803
+ print(' │ '.join(parts))
2804
+ else:
2805
+ print('❌ No interactive elements found with locator information')
2806
+
2807
+ except Exception as e:
2808
+ print(f'❌ Error parsing page source: {e}')
2809
+ sys.exit(1)
2810
+ " 2>&1)
2811
+
2812
+ if [ $? -eq 0 ]; then
2813
+ echo "${log_prefix} $locators"
2814
+ echo "${log_prefix} ✅ Locators extraction complete"
2815
+ else
2816
+ echo "${log_prefix} ❌ Failed to extract locators: $locators"
2817
+ fi
2818
+ else
2819
+ echo "${log_prefix} ❌ Failed to get page source"
2820
+ fi
2821
+ else
2822
+ echo "${log_prefix} ❌ No session available"
2823
+ fi
2824
+ ;;
2825
+
2826
+ *)
2827
+ # Text input - use existing persistent session (like original script)
2828
+ echo "${log_prefix} CASE: Default text input"
2829
+ echo "${log_prefix} Sending text: $command"
2830
+
2831
+ # Convert text to JSON array format (properly escape for shell safety)
2832
+ local json_array=$(python3 -c "
2833
+ import json
2834
+ import sys
2835
+ text = sys.argv[1] if len(sys.argv) > 1 else ''
2836
+ print(json.dumps([text]))" "$command" 2>/dev/null)
2837
+
2838
+ if [ -n "$json_array" ]; then
2839
+ # Use the persistent session for this device
2840
+ local session=$(get_device_session "$device_name")
2841
+
2842
+ if [ -n "$session" ]; then
2843
+ # Send text using existing session (preserves current app context)
2844
+ local response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/keys" \
2845
+ -H 'Content-Type: application/json' \
2846
+ -d "{\"value\":$json_array}")
2847
+
2848
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2849
+ echo "${log_prefix} ✅ Text sent"
2850
+
2851
+ # Auto-press Enter after text with minimal delay
2852
+ sleep 0.1
2853
+ local enter_array=$(python3 -c "import json; print(json.dumps(['\\n']))" 2>/dev/null)
2854
+ local enter_response=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/keys" \
2855
+ -H 'Content-Type: application/json' \
2856
+ -d "{\"value\":$enter_array}")
2857
+
2858
+ if echo "$enter_response" | grep -q '"sessionId"' && ! echo "$enter_response" | grep -q '"error"'; then
2859
+ echo "${log_prefix} ✅ Auto-pressed Enter"
2860
+ fi
2861
+ else
2862
+ echo "${log_prefix} ❌ Text send failed"
2863
+ fi
2864
+
2865
+ # DO NOT clean up session - keep it for future commands!
2866
+ else
2867
+ echo "${log_prefix} ❌ No session available"
2868
+ fi
2869
+ else
2870
+ echo "${log_prefix} ❌ JSON encoding failed"
2871
+ fi
2872
+ ;;
2873
+ esac
2874
+ }
2875
+
2876
+ # Check current appearance mode (helper function)
2877
+ check_appearance_mode() {
2878
+ local device="$1"
2879
+
2880
+ local device_details
2881
+ device_details=$(get_device_details "$device")
2882
+ if [ $? -ne 0 ]; then
2883
+ print_color "$RED" "❌ Device not found: $device"
2884
+ return 1
2885
+ fi
2886
+
2887
+ local port=$(echo "$device_details" | cut -d',' -f1)
2888
+ local session=$(get_device_session "$device")
2889
+
2890
+ if [ -n "$session" ]; then
2891
+ # Try to get current appearance info via WDA
2892
+ local appearance_info=$(curl -s -X GET "http://localhost:${port}/session/$session/appium/device/current_activity" 2>/dev/null)
2893
+
2894
+ if echo "$appearance_info" | grep -q "dark\|Dark"; then
2895
+ print_color "$BLUE" "🌙 $device: Currently in Dark Mode"
2896
+ elif echo "$appearance_info" | grep -q "light\|Light"; then
2897
+ print_color "$YELLOW" "☀️ $device: Currently in Light Mode"
2898
+ else
2899
+ print_color "$CYAN" "❓ $device: Appearance mode unknown"
2900
+ fi
2901
+ else
2902
+ print_color "$RED" "❌ $device: No session available to check appearance"
2903
+ fi
2904
+ }
2905
+
2906
+ # Dark Mode / Light Mode Toggle
2907
+ toggle_dark_mode() {
2908
+ local device="$1"
2909
+ local mode="${2:-toggle}" # toggle, dark, light
2910
+
2911
+ local device_details
2912
+ device_details=$(get_device_details "$device")
2913
+ if [ $? -ne 0 ]; then
2914
+ print_color "$RED" "❌ Device not found: $device"
2915
+ return 1
2916
+ fi
2917
+
2918
+ local port=$(echo "$device_details" | cut -d',' -f1)
2919
+ local udid=$(echo "$device_details" | cut -d',' -f2)
2920
+
2921
+ # Method 1: Try using xcrun devicectl (iOS 17+)
2922
+ if command -v xcrun >/dev/null 2>&1; then
2923
+ print_color "$CYAN" "$device: Trying xcrun devicectl for appearance..."
2924
+
2925
+ case "$mode" in
2926
+ "dark")
2927
+ local result=$(xcrun devicectl device install app --device "$udid" --json 2>/dev/null || echo "not_supported")
2928
+ if [ "$result" != "not_supported" ]; then
2929
+ # Try using xcrun to set appearance (if supported)
2930
+ xcrun simctl ui "$udid" appearance dark 2>/dev/null && {
2931
+ print_color "$GREEN" "✅ $device: Set to Dark Mode via xcrun"
2932
+ return 0
2933
+ }
2934
+ fi
2935
+ ;;
2936
+ "light")
2937
+ xcrun simctl ui "$udid" appearance light 2>/dev/null && {
2938
+ print_color "$GREEN" "✅ $device: Set to Light Mode via xcrun"
2939
+ return 0
2940
+ }
2941
+ ;;
2942
+ esac
2943
+ fi
2944
+
2945
+ # Method 2: Try using idevice tools with terminal commands
2946
+ if command -v idevice-app-runner >/dev/null 2>&1 || command -v ios-deploy >/dev/null 2>&1; then
2947
+ print_color "$CYAN" "$device: Trying iOS terminal commands..."
2948
+
2949
+ # Use shortcuts if available (requires Shortcuts app)
2950
+ local shortcuts_bundle="com.apple.shortcuts"
2951
+ local session=$(get_device_session "$device")
2952
+
2953
+ if [ -n "$session" ]; then
2954
+ # Try to create a simple shortcut command via WDA
2955
+ local shortcut_url="http://localhost:${port}/session/$session/url"
2956
+
2957
+ case "$mode" in
2958
+ "dark")
2959
+ # Use URL scheme to trigger shortcuts or settings
2960
+ local result=$(curl -s -X POST "$shortcut_url" \
2961
+ -H "Content-Type: application/json" \
2962
+ -d '{"url": "prefs:root=DISPLAY&path=APPEARANCE"}' 2>/dev/null)
2963
+
2964
+ if echo "$result" | grep -q '"sessionId"'; then
2965
+ sleep 1
2966
+ # Try to tap the Dark option
2967
+ local tap_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
2968
+ -H "Content-Type: application/json" \
2969
+ -d '{"x": 100, "y": 400}' 2>/dev/null)
2970
+
2971
+ print_color "$GREEN" "✅ $device: Attempted Dark Mode via Settings URL"
2972
+ execute_wda_command "$device" "home" >/dev/null
2973
+ return 0
2974
+ fi
2975
+ ;;
2976
+ "light")
2977
+ local result=$(curl -s -X POST "$shortcut_url" \
2978
+ -H "Content-Type: application/json" \
2979
+ -d '{"url": "prefs:root=DISPLAY&path=APPEARANCE"}' 2>/dev/null)
2980
+
2981
+ if echo "$result" | grep -q '"sessionId"'; then
2982
+ sleep 1
2983
+ # Try to tap the Light option
2984
+ local tap_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
2985
+ -H "Content-Type: application/json" \
2986
+ -d '{"x": 100, "y": 300}' 2>/dev/null)
2987
+
2988
+ print_color "$GREEN" "✅ $device: Attempted Light Mode via Settings URL"
2989
+ execute_wda_command "$device" "home" >/dev/null
2990
+ return 0
2991
+ fi
2992
+ ;;
2993
+ esac
2994
+ fi
2995
+ fi
2996
+
2997
+ # Method 3: Enhanced WDA approach with better element detection
2998
+ local session=$(get_device_session "$device")
2999
+ if [ -n "$session" ]; then
3000
+ print_color "$CYAN" "$device: Using enhanced WDA method..."
3001
+
3002
+ # Open Settings using URL scheme first
3003
+ local settings_result=$(curl -s -X POST "http://localhost:${port}/session/$session/url" \
3004
+ -H "Content-Type: application/json" \
3005
+ -d '{"url": "App-prefs:DISPLAY"}' 2>/dev/null)
3006
+
3007
+ if ! echo "$settings_result" | grep -q '"sessionId"'; then
3008
+ # Fallback: Launch Settings app manually
3009
+ execute_wda_command "$device" "launch com.apple.Preferences" >/dev/null
3010
+ sleep 2
3011
+
3012
+ # Try to find and tap Display & Brightness
3013
+ local search_url="http://localhost:${port}/session/$session/elements"
3014
+ local elements_result=$(curl -s -X POST "$search_url" \
3015
+ -H "Content-Type: application/json" \
3016
+ -d '{"using": "partial link text", "value": "Display"}' 2>/dev/null)
3017
+
3018
+ if ! echo "$elements_result" | grep -q '"ELEMENT"'; then
3019
+ # Try alternative search methods
3020
+ elements_result=$(curl -s -X POST "$search_url" \
3021
+ -H "Content-Type: application/json" \
3022
+ -d '{"using": "accessibility id", "value": "Display & Brightness"}' 2>/dev/null)
3023
+ fi
3024
+
3025
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
3026
+ local element_id=$(echo "$elements_result" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3027
+ curl -s -X POST "http://localhost:${port}/session/$session/element/$element_id/click" >/dev/null
3028
+ sleep 1
3029
+ fi
3030
+ fi
3031
+
3032
+ sleep 2
3033
+
3034
+ # Now try to find and click the appearance option
3035
+ case "$mode" in
3036
+ "dark")
3037
+ # Look for Dark mode option with multiple strategies
3038
+ local dark_elements=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
3039
+ -H "Content-Type: application/json" \
3040
+ -d '{"using": "accessibility id", "value": "Dark"}' 2>/dev/null)
3041
+
3042
+ if echo "$dark_elements" | grep -q '"ELEMENT"'; then
3043
+ local dark_id=$(echo "$dark_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3044
+ curl -s -X POST "http://localhost:${port}/session/$session/element/$dark_id/click" >/dev/null
3045
+ print_color "$GREEN" "✅ $device: Switched to Dark Mode"
3046
+ else
3047
+ # Fallback: Try coordinates (approximate location of Dark option)
3048
+ curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
3049
+ -H "Content-Type: application/json" \
3050
+ -d '{"x": 60, "y": 450}' >/dev/null
3051
+ print_color "$YELLOW" "⚠️ $device: Attempted Dark Mode (coordinate fallback)"
3052
+ fi
3053
+ ;;
3054
+ "light")
3055
+ local light_elements=$(curl -s -X POST "http://localhost:${port}/session/$session/elements" \
3056
+ -H "Content-Type: application/json" \
3057
+ -d '{"using": "accessibility id", "value": "Light"}' 2>/dev/null)
3058
+
3059
+ if echo "$light_elements" | grep -q '"ELEMENT"'; then
3060
+ local light_id=$(echo "$light_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3061
+ curl -s -X POST "http://localhost:${port}/session/$session/element/$light_id/click" >/dev/null
3062
+ print_color "$GREEN" "✅ $device: Switched to Light Mode"
3063
+ else
3064
+ # Fallback: Try coordinates (approximate location of Light option)
3065
+ curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
3066
+ -H "Content-Type: application/json" \
3067
+ -d '{"x": 60, "y": 350}' >/dev/null
3068
+ print_color "$YELLOW" "⚠️ $device: Attempted Light Mode (coordinate fallback)"
3069
+ fi
3070
+ ;;
3071
+ *)
3072
+ # Toggle mode - try to detect current and switch
3073
+ print_color "$YELLOW" "⚠️ $device: Toggle mode - manual selection may be needed"
3074
+ ;;
3075
+ esac
3076
+
3077
+ # Go back to home
3078
+ sleep 1
3079
+ execute_wda_command "$device" "home" >/dev/null
3080
+ return 0
3081
+ else
3082
+ print_color "$RED" "❌ $device: No WDA session available"
3083
+ return 1
3084
+ fi
3085
+ }
3086
+
3087
+ # Screen Rotation Functions
3088
+ rotate_screen() {
3089
+ local device="$1"
3090
+ local orientation="${2:-toggle}" # portrait, landscape-left, landscape-right, portrait-upside-down, toggle
3091
+
3092
+ local device_details
3093
+ device_details=$(get_device_details "$device")
3094
+ if [ $? -ne 0 ]; then
3095
+ print_color "$RED" "❌ Device not found: $device"
3096
+ return 1
3097
+ fi
3098
+
3099
+ local port=$(echo "$device_details" | cut -d',' -f1)
3100
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3101
+
3102
+ # Method 1: Try using xcrun devicectl for rotation (iOS 17+)
3103
+ if command -v xcrun >/dev/null 2>&1; then
3104
+ case "$orientation" in
3105
+ "portrait"|"1")
3106
+ if xcrun simctl status_bar "$udid" override --orientation portrait 2>/dev/null; then
3107
+ print_color "$GREEN" "✅ $device: Rotated to Portrait via xcrun"
3108
+ return 0
3109
+ fi
3110
+ ;;
3111
+ "landscape-left"|"landscape"|"3")
3112
+ if xcrun simctl status_bar "$udid" override --orientation landscapeLeft 2>/dev/null; then
3113
+ print_color "$GREEN" "✅ $device: Rotated to Landscape Left via xcrun"
3114
+ return 0
3115
+ fi
3116
+ ;;
3117
+ "landscape-right"|"4")
3118
+ if xcrun simctl status_bar "$udid" override --orientation landscapeRight 2>/dev/null; then
3119
+ print_color "$GREEN" "✅ $device: Rotated to Landscape Right via xcrun"
3120
+ return 0
3121
+ fi
3122
+ ;;
3123
+ esac
3124
+ fi
3125
+
3126
+ # Method 2: Use WDA orientation API
3127
+ local session=$(get_device_session "$device")
3128
+ if [ -n "$session" ]; then
3129
+ local orientation_url="http://localhost:${port}/session/$session/orientation"
3130
+
3131
+ # Get current orientation first
3132
+ local current_orientation=$(curl -s -X GET "$orientation_url" | grep -o '"value":"[^"]*"' | cut -d'"' -f4)
3133
+
3134
+ # Map orientation names to WDA values
3135
+ local orientation_value
3136
+ case "$orientation" in
3137
+ "portrait"|"1")
3138
+ orientation_value="PORTRAIT"
3139
+ ;;
3140
+ "landscape-left"|"landscape"|"3")
3141
+ orientation_value="LANDSCAPE"
3142
+ ;;
3143
+ "landscape-right"|"4")
3144
+ orientation_value="UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT"
3145
+ ;;
3146
+ "portrait-upside-down"|"2")
3147
+ orientation_value="UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN"
3148
+ ;;
3149
+ "toggle"|*)
3150
+ # Toggle between portrait and landscape
3151
+ case "$current_orientation" in
3152
+ "PORTRAIT") orientation_value="LANDSCAPE" ;;
3153
+ "LANDSCAPE") orientation_value="PORTRAIT" ;;
3154
+ *) orientation_value="LANDSCAPE" ;; # Default to landscape if unknown
3155
+ esac
3156
+ ;;
3157
+ esac
3158
+
3159
+ # Set orientation via WDA
3160
+ local response=$(curl -s -X POST "$orientation_url" \
3161
+ -H "Content-Type: application/json" \
3162
+ -d "{\"orientation\": \"$orientation_value\"}")
3163
+
3164
+ if echo "$response" | grep -q '"value":null\|"sessionId"'; then
3165
+ print_color "$GREEN" "✅ $device: Rotated to $orientation_value"
3166
+ return 0
3167
+ else
3168
+ print_color "$YELLOW" "⚠️ $device: Rotation may not be supported or device is locked"
3169
+ fi
3170
+ fi
3171
+
3172
+ # Method 3: Try using idevice tools if available
3173
+ if command -v idevice-app-runner >/dev/null 2>&1; then
3174
+ print_color "$CYAN" "$device: Trying idevice rotation tools..."
3175
+ # Note: Most idevice tools don't support rotation directly
3176
+ print_color "$YELLOW" "⚠️ $device: idevice rotation not directly supported"
3177
+ fi
3178
+
3179
+ print_color "$RED" "❌ $device: Could not rotate screen - may need manual rotation"
3180
+ return 1
3181
+ }
3182
+
3183
+ # Lock/Unlock Rotation
3184
+ toggle_rotation_lock() {
3185
+ local device="$1"
3186
+ local lock_state="${2:-toggle}" # lock, unlock, toggle
3187
+
3188
+ local device_details
3189
+ device_details=$(get_device_details "$device")
3190
+ if [ $? -ne 0 ]; then
3191
+ print_color "$RED" "❌ Device not found: $device"
3192
+ return 1
3193
+ fi
3194
+
3195
+ local port=$(echo "$device_details" | cut -d',' -f1)
3196
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3197
+
3198
+ # Method 1: Try using terminal/xcrun commands
3199
+ if command -v xcrun >/dev/null 2>&1; then
3200
+ # Note: xcrun doesn't directly support rotation lock for real devices
3201
+ print_color "$CYAN" "$device: Rotation lock via terminal not directly supported for physical devices"
3202
+ fi
3203
+
3204
+ # Method 2: Use WDA to access Control Center
3205
+ local session=$(get_device_session "$device")
3206
+ if [ -n "$session" ]; then
3207
+ print_color "$CYAN" "$device: Opening Control Center to toggle rotation lock..."
3208
+
3209
+ # Open Control Center by swiping from top-right (iOS 12+) or bottom (older iOS)
3210
+ # Try modern swipe first (from top-right corner)
3211
+ local swipe_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
3212
+ -H "Content-Type: application/json" \
3213
+ -d '{"fromX": 350, "fromY": 10, "toX": 350, "toY": 200, "duration": 0.5}' 2>/dev/null)
3214
+
3215
+ if ! echo "$swipe_result" | grep -q '"sessionId"'; then
3216
+ # Fallback: Try swipe up from bottom (older iOS)
3217
+ swipe_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
3218
+ -H "Content-Type: application/json" \
3219
+ -d '{"fromX": 200, "fromY": 800, "toX": 200, "toY": 400, "duration": 0.5}' 2>/dev/null)
3220
+ fi
3221
+
3222
+ sleep 2
3223
+
3224
+ # Look for rotation lock button with multiple strategies
3225
+ local search_url="http://localhost:${port}/session/$session/elements"
3226
+
3227
+ # Strategy 1: Find by accessibility identifier
3228
+ local rotation_elements=$(curl -s -X POST "$search_url" \
3229
+ -H "Content-Type: application/json" \
3230
+ -d '{"using": "accessibility id", "value": "portrait-orientation-lock"}' 2>/dev/null)
3231
+
3232
+ if ! echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3233
+ # Strategy 2: Find by name
3234
+ rotation_elements=$(curl -s -X POST "$search_url" \
3235
+ -H "Content-Type: application/json" \
3236
+ -d '{"using": "name", "value": "Portrait Orientation Lock"}' 2>/dev/null)
3237
+ fi
3238
+
3239
+ if ! echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3240
+ # Strategy 3: Find by partial name match
3241
+ rotation_elements=$(curl -s -X POST "$search_url" \
3242
+ -H "Content-Type: application/json" \
3243
+ -d '{"using": "partial link text", "value": "orientation"}' 2>/dev/null)
3244
+ fi
3245
+
3246
+ if echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3247
+ local rotation_id=$(echo "$rotation_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3248
+ local tap_result=$(curl -s -X POST "http://localhost:${port}/session/$session/element/$rotation_id/click" 2>/dev/null)
3249
+
3250
+ if echo "$tap_result" | grep -q '"sessionId"'; then
3251
+ print_color "$GREEN" "✅ $device: Toggled rotation lock"
3252
+ else
3253
+ print_color "$YELLOW" "⚠️ $device: Rotation lock button found but tap may have failed"
3254
+ fi
3255
+ else
3256
+ # Fallback: Try common coordinates for rotation lock button
3257
+ print_color "$YELLOW" "$device: Using coordinate fallback for rotation lock..."
3258
+
3259
+ # Common positions for rotation lock in Control Center
3260
+ local coords=("60,300" "60,350" "80,320" "100,340")
3261
+
3262
+ for coord in "${coords[@]}"; do
3263
+ local x=$(echo "$coord" | cut -d',' -f1)
3264
+ local y=$(echo "$coord" | cut -d',' -f2)
3265
+
3266
+ local tap_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
3267
+ -H "Content-Type: application/json" \
3268
+ -d "{\"x\": $x, \"y\": $y}" 2>/dev/null)
3269
+
3270
+ if echo "$tap_result" | grep -q '"sessionId"'; then
3271
+ print_color "$YELLOW" "⚠️ $device: Attempted rotation lock toggle at ($x,$y)"
3272
+ break
3273
+ fi
3274
+ done
3275
+ fi
3276
+
3277
+ # Close Control Center by tapping outside or swiping down
3278
+ sleep 1
3279
+ local close_result=$(curl -s -X POST "http://localhost:${port}/session/$session/wda/tap/0" \
3280
+ -H "Content-Type: application/json" \
3281
+ -d '{"x": 200, "y": 600}' 2>/dev/null)
3282
+
3283
+ # Alternative: Swipe down to close Control Center
3284
+ if ! echo "$close_result" | grep -q '"sessionId"'; then
3285
+ curl -s -X POST "http://localhost:${port}/session/$session/wda/dragfromtoforduration" \
3286
+ -H "Content-Type: application/json" \
3287
+ -d '{"fromX": 200, "fromY": 200, "toX": 200, "toY": 600, "duration": 0.3}' >/dev/null
3288
+ fi
3289
+
3290
+ return 0
3291
+ else
3292
+ print_color "$RED" "❌ $device: No WDA session available for rotation lock"
3293
+ return 1
3294
+ fi
3295
+ }
3296
+
3297
+ # Auto-start WDA on USB device
3298
+ start_wda_usb() {
3299
+ local device_name="$1"
3300
+ local port="$2"
3301
+ local udid="$3"
3302
+
3303
+ print_color "$YELLOW" " 🔧 Auto-starting WDA for $device_name..."
3304
+
3305
+ # Try multiple common WDA paths
3306
+ local WDA_PATHS=(
3307
+ "$HOME/Downloads/WebDriverAgent-10.2.1"
3308
+ "$HOME/Downloads/WebDriverAgent"
3309
+ "$HOME/WebDriverAgent"
3310
+ "$(dirname "$0")/WebDriverAgent"
3311
+ "/tmp/WebDriverAgent"
3312
+ )
3313
+
3314
+ local WDA_PATH=""
3315
+ for path in "${WDA_PATHS[@]}"; do
3316
+ if [ -d "$path" ] && [ -f "$path/WebDriverAgent.xcodeproj/project.pbxproj" ]; then
3317
+ WDA_PATH="$path"
3318
+ break
3319
+ fi
3320
+ done
3321
+
3322
+ if [ -z "$WDA_PATH" ]; then
3323
+ print_color "$RED" " ❌ WebDriverAgent not found in common locations"
3324
+ print_color "$YELLOW" " 📝 Searched:"
3325
+ for path in "${WDA_PATHS[@]}"; do
3326
+ echo " - $path"
3327
+ done
3328
+ return 1
3329
+ fi
3330
+
3331
+ print_color "$CYAN" " → Using WDA: $WDA_PATH"
3332
+
3333
+ # Check if already building for this device
3334
+ if pgrep -f "xcodebuild.*${udid}" >/dev/null 2>&1; then
3335
+ print_color "$YELLOW" " ⚠️ WDA build already in progress for this device"
3336
+ print_color "$YELLOW" " ⏳ Waiting for existing build..."
3337
+ else
3338
+ # Start WDA build in background
3339
+ local wda_log="/tmp/wda_${device_name}_$$.log"
3340
+ (
3341
+ cd "$WDA_PATH" && \
3342
+ xcodebuild -project WebDriverAgent.xcodeproj \
3343
+ -scheme WebDriverAgentRunner \
3344
+ -destination "platform=iOS,id=$udid" \
3345
+ test > "$wda_log" 2>&1
3346
+ ) &
3347
+ print_color "$CYAN" " → WDA build started (log: $wda_log)"
3348
+ fi
3349
+
3350
+ # Wait for WDA to become available
3351
+ print_color "$YELLOW" " ⏳ Waiting for WDA to start (max 60s)..."
3352
+ local waited=0
3353
+ for i in {1..60}; do
3354
+ if curl -s --connect-timeout 1 --max-time 2 "http://localhost:${port}/status" 2>/dev/null | grep -q '"state"'; then
3355
+ print_color "$GREEN" " ✅ WDA started successfully in ${i}s!"
3356
+ return 0
3357
+ fi
3358
+ sleep 1
3359
+ waited=$i
3360
+ if [ $((i % 15)) -eq 0 ]; then
3361
+ print_color "$YELLOW" " Still waiting... (${i}s elapsed)"
3362
+ fi
3363
+ done
3364
+
3365
+ print_color "$RED" " ❌ WDA failed to start after ${waited}s"
3366
+ print_color "$YELLOW" " 💡 Check log: tail -50 /tmp/wda_${device_name}_$$.log"
3367
+ return 1
3368
+ }
3369
+
3370
+ # Discover and connect USB devices
3371
+ discover_and_connect_devices() {
3372
+ print_color "$BLUE" "🔍 Discovering USB iOS devices..."
3373
+ echo ""
3374
+
3375
+ local ALL_DEVICES=($(get_all_device_names))
3376
+
3377
+ if [ ${#ALL_DEVICES[@]} -eq 0 ]; then
3378
+ print_color "$RED" "❌ No USB iOS devices found"
3379
+ print_color "$YELLOW" " Make sure:"
3380
+ print_color "$YELLOW" " - Devices are connected via USB"
3381
+ print_color "$YELLOW" " - Devices are unlocked"
3382
+ print_color "$YELLOW" " - You trust this computer on the devices"
3383
+ exit 1
3384
+ fi
3385
+
3386
+ print_color "$YELLOW" "Found USB devices:"
3387
+ local port_offset=0
3388
+ for device in "${ALL_DEVICES[@]}"; do
3389
+ local port=$((WDA_PORT_BASE + port_offset))
3390
+ print_color "$CYAN" " • $device (port: $port)"
3391
+ ((port_offset++))
3392
+ done
3393
+ echo ""
3394
+
3395
+ print_color "$YELLOW" "Setting up USB connections..."
3396
+ echo ""
3397
+
3398
+ CONNECTED_DEVICES=()
3399
+ local failed_devices=()
3400
+
3401
+ # Start all iproxy processes in parallel first
3402
+ print_color "$YELLOW" " 🔌 Starting iproxy for all devices..."
3403
+ for device in "${ALL_DEVICES[@]}"; do
3404
+ local details=$(get_device_details "$device")
3405
+ local port=$(echo "$details" | cut -d',' -f1)
3406
+ local udid=$(echo "$details" | cut -d',' -f2)
3407
+ print_color "$CYAN" " $device -> localhost:$port (UDID: ${udid:0:20}...)"
3408
+ pkill -f "iproxy.*${port}:8100" 2>/dev/null
3409
+ iproxy ${port} 8100 -u "$udid" >/dev/null 2>&1 &
3410
+ local iproxy_pid=$!
3411
+ echo "$device:$iproxy_pid" >> "$IPROXY_PIDS_FILE"
3412
+ done
3413
+
3414
+ # Give all iproxy instances time to start
3415
+ sleep 1
3416
+
3417
+ # Now test all devices in parallel
3418
+ for device in "${ALL_DEVICES[@]}"; do
3419
+ local details=$(get_device_details "$device")
3420
+ local port=$(echo "$details" | cut -d',' -f1)
3421
+ local udid=$(echo "$details" | cut -d',' -f2)
3422
+ print_color "$CYAN" "📱 Setting up $device (USB port: $port):"
3423
+
3424
+ # Test WDA connection
3425
+ print_color "$YELLOW" " 🔍 Testing WDA connection..."
3426
+ if curl -s --connect-timeout 2 --max-time 3 "http://localhost:${port}/status" 2>/dev/null | grep -q '"state"'; then
3427
+ CONNECTED_DEVICES+=("$device")
3428
+ print_color "$GREEN" " ✅ $device ready (USB)"
3429
+ else
3430
+ print_color "$YELLOW" " ⚠️ WDA not responding, attempting auto-start..."
3431
+
3432
+ # Try to auto-start WDA
3433
+ if start_wda_usb "$device" "$port" "$udid"; then
3434
+ CONNECTED_DEVICES+=("$device")
3435
+ print_color "$GREEN" " ✅ $device ready (USB - WDA auto-started)"
3436
+ else
3437
+ failed_devices+=("$device")
3438
+ print_color "$RED" " ❌ $device - Failed to start WDA"
3439
+ print_color "$YELLOW" " 💡 You may need to manually start WebDriverAgent on this device"
3440
+ fi
3441
+ fi
3442
+ echo ""
3443
+ done
3444
+
3445
+ print_color "$YELLOW" "📋 Connection Summary:"
3446
+ echo ""
3447
+
3448
+ if [ ${#CONNECTED_DEVICES[@]} -gt 0 ]; then
3449
+ print_color "$GREEN" "🎉 ${#CONNECTED_DEVICES[@]} device(s) ready via USB!"
3450
+ print_color "$CYAN" "Connected: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3451
+ if [ ${#failed_devices[@]} -gt 0 ]; then
3452
+ print_color "$YELLOW" "⚠️ Not ready: $(IFS=', '; echo "${failed_devices[*]}")"
3453
+ print_color "$YELLOW" " 💡 Continuing with available devices..."
3454
+ fi
3455
+ else
3456
+ print_color "$RED" "❌ No devices are ready"
3457
+ print_color "$YELLOW" ""
3458
+ print_color "$YELLOW" "🔧 Troubleshooting:"
3459
+ print_color "$YELLOW" " 1. Make sure devices are unlocked"
3460
+ print_color "$YELLOW" " 2. Trust this computer on your devices"
3461
+ print_color "$YELLOW" " 3. Check WebDriverAgent is in one of these locations:"
3462
+ print_color "$CYAN" " • ~/Downloads/WebDriverAgent-10.2.1"
3463
+ print_color "$CYAN" " • ~/Downloads/WebDriverAgent"
3464
+ print_color "$CYAN" " • ~/WebDriverAgent"
3465
+ print_color "$YELLOW" " 4. Check WDA logs: tail -50 /tmp/wda_*.log"
3466
+ exit 1
3467
+ fi
3468
+
3469
+ echo ""
3470
+ }
3471
+
3472
+ # Send command to multiple devices
3473
+ send_to_all_devices() {
3474
+ local devices=("$@")
3475
+ local pids=()
3476
+
3477
+ print_color "$YELLOW" "🚀 Executing on ${#devices[@]} device(s)..."
3478
+ echo ""
3479
+
3480
+ for device in "${devices[@]}"; do
3481
+ {
3482
+ execute_wda_command "$device" "$TEXT"
3483
+ echo "[${device}] Completed"
3484
+ } &
3485
+ pids+=($!)
3486
+ print_color "$CYAN" "📱 Started: $device (PID: $!)"
3487
+ done
3488
+
3489
+ echo ""
3490
+ print_color "$YELLOW" "⏳ Waiting for completion..."
3491
+
3492
+ for pid in "${pids[@]}"; do
3493
+ wait "$pid"
3494
+ done
3495
+
3496
+ echo ""
3497
+ print_color "$GREEN" "✅ All devices completed!"
3498
+ }
3499
+
3500
+ # Cleanup all sessions and terminate iproxy processes
3501
+ # Graceful cleanup function for USB
3502
+ graceful_cleanup() {
3503
+ local preserve_trust="${1:-true}"
3504
+
3505
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
3506
+ print_color "$YELLOW" "🧹 Gracefully closing sessions..."
3507
+
3508
+ # Only close WebDriver sessions, don't terminate WDA processes
3509
+ while IFS=':' read -r device session; do
3510
+ if [ -n "$device" ] && [ -n "$session" ]; then
3511
+ local device_details
3512
+ device_details=$(get_device_details "$device")
3513
+ local port=$(echo "$device_details" | cut -d',' -f1)
3514
+
3515
+ # Just close the WebDriver session, keep WDA running
3516
+ curl -s -X DELETE "http://localhost:${port}/session/$session" >/dev/null 2>&1 || true
3517
+ echo " ✅ Session closed: $device"
3518
+ fi
3519
+ done < "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3520
+
3521
+ rm -f "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3522
+ fi
3523
+
3524
+ # Kill iproxy processes
3525
+ print_color "$YELLOW" "🔌 Stopping USB port forwarding..."
3526
+ if [ -f "$IPROXY_PIDS_FILE" ]; then
3527
+ while IFS=':' read -r device pid; do
3528
+ if [ -n "$pid" ]; then
3529
+ kill -9 "$pid" 2>/dev/null || true
3530
+ echo " ✅ iproxy stopped: $device"
3531
+ fi
3532
+ done < "$IPROXY_PIDS_FILE"
3533
+ rm -f "$IPROXY_PIDS_FILE"
3534
+ fi
3535
+
3536
+ # Clean up port mapping cache
3537
+ rm -f "$DEVICE_PORT_MAP_FILE" 2>/dev/null || true
3538
+
3539
+ if [ "$preserve_trust" = "true" ]; then
3540
+ print_color "$GREEN" "✅ USB connections closed gracefully"
3541
+ print_color "$CYAN" "💡 WebDriverAgent processes left running on devices to preserve trust"
3542
+ print_color "$YELLOW" " Note: Devices should remain trusted and ready for reconnection"
3543
+ else
3544
+ # Call the full cleanup if explicitly requested
3545
+ cleanup_all_sessions_full
3546
+ fi
3547
+ }
3548
+
3549
+ # Full cleanup function (original behavior - may affect trust)
3550
+ cleanup_all_sessions_full() {
3551
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
3552
+ print_color "$YELLOW" "🧹 Cleaning up sessions and stopping WDA on devices..."
3553
+
3554
+ # Collect device UDIDs for WDA termination
3555
+ local device_udids=()
3556
+
3557
+ while IFS=':' read -r device session; do
3558
+ if [ -n "$device" ] && [ -n "$session" ]; then
3559
+ local device_details
3560
+ device_details=$(get_device_details "$device")
3561
+ local port=$(echo "$device_details" | cut -d',' -f1)
3562
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3563
+
3564
+ # Close WebDriver session first
3565
+ curl -s -X DELETE "http://localhost:${port}/session/$session" >/dev/null 2>&1 || true
3566
+ echo " ✅ Session closed: $device"
3567
+
3568
+ # Collect UDID for WDA process termination
3569
+ if [ -n "$udid" ]; then
3570
+ device_udids+=("$udid")
3571
+ fi
3572
+ fi
3573
+ done < "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3574
+
3575
+ rm -f "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3576
+
3577
+ # Terminate WDA processes for the connected devices
3578
+ print_color "$CYAN" "🛑 Terminating WDA processes on respective devices..."
3579
+
3580
+ if [ ${#device_udids[@]} -gt 0 ]; then
3581
+ for udid in "${device_udids[@]}"; do
3582
+ # Kill WDA process for this specific device UDID
3583
+ local killed_count=$(pkill -f "WebDriverAgentRunner.*$udid" 2>/dev/null; echo $?)
3584
+ if [ $killed_count -eq 0 ]; then
3585
+ echo " ✅ WDA terminated on device: ${udid:0:8}..."
3586
+ else
3587
+ echo " ℹ️ No WDA process found for: ${udid:0:8}..."
3588
+ fi
3589
+ done
3590
+ else
3591
+ # Fallback: stop all WDA processes if no specific devices found
3592
+ print_color "$YELLOW" " → No specific devices found, stopping all WDA processes..."
3593
+ pkill -f "WebDriverAgentRunner" 2>/dev/null || true
3594
+ fi
3595
+
3596
+ # Also terminate any lingering WDA processes (in case other scripts left them)
3597
+ print_color "$CYAN" "🧹 Cleaning up any remaining WDA processes..."
3598
+ local remaining=$(pgrep -f "WebDriverAgentRunner" 2>/dev/null | wc -l)
3599
+ if [ "$remaining" -gt 0 ]; then
3600
+ pkill -TERM -f "WebDriverAgentRunner" 2>/dev/null || true
3601
+ sleep 1
3602
+ pkill -KILL -f "WebDriverAgentRunner" 2>/dev/null || true
3603
+ echo " ✅ Cleaned up $remaining remaining WDA processes"
3604
+ else
3605
+ echo " ✅ No remaining WDA processes found"
3606
+ fi
3607
+
3608
+ print_color "$GREEN" "✅ All WDA sessions and processes terminated"
3609
+ print_color "$CYAN" "💡 Ready for manual testing - completely clean environment"
3610
+ print_color "$YELLOW" " Note: You may need to re-establish wireless connections in Xcode"
3611
+ fi
3612
+ }
3613
+
3614
+ # Legacy cleanup function name (redirects to graceful cleanup)
3615
+ cleanup_all_sessions() {
3616
+ graceful_cleanup true
3617
+ }
3618
+
3619
+ # Signal cleanup function (preserves device trust on interruption)
3620
+ cleanup_on_signal() {
3621
+ echo ""
3622
+ print_color "$YELLOW" "⚠️ Script interrupted - cleaning up gracefully..."
3623
+ graceful_cleanup true
3624
+ exit 0
3625
+ }
3626
+
3627
+ # Signal handlers for proper cleanup (only on interruption, not normal exit)
3628
+ trap cleanup_on_signal INT
3629
+ trap cleanup_on_signal TERM
3630
+
3631
+ # Main execution
3632
+ if [ -n "$DEVICE_NAME_REQUEST" ]; then
3633
+ # Single device mode
3634
+ if ! get_device_details "$DEVICE_NAME_REQUEST" >/dev/null; then
3635
+ print_color "$RED" "❌ Device '$DEVICE_NAME_REQUEST' not found"
3636
+ exit 1
3637
+ fi
3638
+
3639
+ print_color "$BLUE" "🎯 Single device: $DEVICE_NAME_REQUEST"
3640
+ echo ""
3641
+
3642
+ # Setup USB device (iproxy + WDA)
3643
+ if setup_single_usb_device "$DEVICE_NAME_REQUEST"; then
3644
+ if [ -n "$TEXT" ]; then
3645
+ # Single command mode - execute and exit
3646
+ # Check if session already exists (e.g., from a launched app)
3647
+ existing_session=$(get_device_session "$DEVICE_NAME_REQUEST")
3648
+ session="$existing_session"
3649
+
3650
+ if [ -z "$session" ]; then
3651
+ # No existing session, create new SpringBoard session
3652
+ session=$(create_persistent_session "$DEVICE_NAME_REQUEST")
3653
+ else
3654
+ echo " ℹ️ Using existing session: $session"
3655
+ fi
3656
+
3657
+ if [ -n "$session" ]; then
3658
+ execute_wda_command "$DEVICE_NAME_REQUEST" "$TEXT"
3659
+ fi
3660
+ # DON'T cleanup session - keep app running!
3661
+ # cleanup_wda_session "$DEVICE_NAME_REQUEST"
3662
+ exit 0
3663
+ else
3664
+ # Single device interactive mode
3665
+ print_color "$YELLOW" "🔧 Initializing interactive session..."
3666
+ session=$(create_persistent_session "$DEVICE_NAME_REQUEST")
3667
+ if [ -n "$session" ]; then
3668
+ print_color "$GREEN" " ✅ Session ready: $DEVICE_NAME_REQUEST"
3669
+ else
3670
+ print_color "$RED" " ❌ Session failed: $DEVICE_NAME_REQUEST"
3671
+ exit 1
3672
+ fi
3673
+
3674
+ echo ""
3675
+ load_text_history
3676
+
3677
+ print_color "$CYAN" "🔄 Single-device Interactive Mode: $DEVICE_NAME_REQUEST"
3678
+ print_color "$YELLOW" " Commands: launch/kill/home/url/screenshot + text input"
3679
+ print_color "$YELLOW" " Type 'help' for commands • 'quit' to exit"
3680
+ echo ""
3681
+
3682
+ while true; do
3683
+ # Enable readline for this input with custom history
3684
+ set -o history
3685
+ HISTFILE="$HISTORY_FILE"
3686
+ HISTSIZE="$HISTORY_MAX"
3687
+ HISTFILESIZE="$HISTORY_MAX"
3688
+
3689
+ # Load history from file for this session
3690
+ if [ -f "$HISTORY_FILE" ]; then
3691
+ history -r "$HISTORY_FILE"
3692
+ fi
3693
+
3694
+ read -e -p "$DEVICE_NAME_REQUEST > " INPUT_TEXT
3695
+
3696
+ # Add any non-empty input to history (except quit commands)
3697
+ if [ -n "$INPUT_TEXT" ] && [[ "$INPUT_TEXT" != "quit" ]] && [[ "$INPUT_TEXT" != "exit" ]] && [[ "$INPUT_TEXT" != "q" ]] && [[ "$INPUT_TEXT" != "help" ]]; then
3698
+ add_to_history "$INPUT_TEXT"
3699
+ fi
3700
+
3701
+ # Disable history again to avoid shell command pollution
3702
+ set +o history
3703
+ unset HISTFILE
3704
+
3705
+ case "$INPUT_TEXT" in
3706
+ "quit"|"q"|"exit")
3707
+ print_color "$YELLOW" "👋 Goodbye!"
3708
+ cleanup_wda_session "$DEVICE_NAME_REQUEST"
3709
+ exit 0
3710
+ ;;
3711
+ "forcecleanup"|"killwda")
3712
+ print_color "$YELLOW" "🛑 Force cleaning up - this may affect device trust..."
3713
+ cleanup_all_sessions_full
3714
+ print_color "$YELLOW" "👋 Goodbye!"
3715
+ exit 0
3716
+ ;;
3717
+ "help"|"commands")
3718
+ print_color "$YELLOW" "📱 Available Commands:"
3719
+ echo ""
3720
+ print_color "$CYAN" "🚀 App Control:"
3721
+ print_color "$YELLOW" " launch <app> - Launch application"
3722
+ print_color "$YELLOW" " open <app> - Same as launch"
3723
+ print_color "$YELLOW" " kill <app> - Terminate application"
3724
+ print_color "$YELLOW" " close <app> - Same as kill"
3725
+ print_color "$YELLOW" " install <path> - Install IPA file (requires iOS tools)"
3726
+ print_color "$YELLOW" " uninstall <id> - Uninstall app by bundle ID (requires iOS tools)"
3727
+ print_color "$YELLOW" " appinfo <id> - Check app version/info by bundle ID"
3728
+ print_color "$YELLOW" " version <id> - Same as appinfo"
3729
+ print_color "$YELLOW" " listapps [word] - List all apps or search by name/word"
3730
+ print_color "$YELLOW" " apps [word] - Same as listapps (search any installed app)"
3731
+ print_color "$YELLOW" " debug - Test device connection and tools"
3732
+ echo ""
3733
+ print_color "$CYAN" "🌐 Navigation:"
3734
+ print_color "$YELLOW" " home - Go to home screen"
3735
+ print_color "$YELLOW" " back [context] - Smart back navigation (button/swipe/context)"
3736
+ print_color "$YELLOW" " goback - Same as back"
3737
+ print_color "$YELLOW" " url <url> - Open URL in Safari"
3738
+ print_color "$YELLOW" " screenshot - Take screenshot"
3739
+ print_color "$YELLOW" " click <element> - Click on element or coordinates"
3740
+ print_color "$YELLOW" " longpress <el> - Long press on element"
3741
+ print_color "$YELLOW" " swipe <dir> - Swipe up/down/left/right or custom coordinates"
3742
+ print_color "$YELLOW" " scroll <dir> - Same as swipe"
3743
+ echo ""
3744
+ print_color "$CYAN" "⚙️ Device Control:"
3745
+ print_color "$YELLOW" " restart - Restart device (wireless + USB supported)"
3746
+ print_color "$YELLOW" " reboot - Same as restart"
3747
+ print_color "$YELLOW" " shutdown - Shutdown device (USB only, wireless does restart)"
3748
+ print_color "$YELLOW" " poweroff - Same as shutdown"
3749
+ echo ""
3750
+ print_color "$CYAN" "🎨 Display & Appearance:"
3751
+ print_color "$YELLOW" " darkmode - Switch to dark mode"
3752
+ print_color "$YELLOW" " lightmode - Switch to light mode"
3753
+ print_color "$YELLOW" " theme <mode> - Set theme (dark/light/toggle)"
3754
+ print_color "$YELLOW" " appearance - Check current appearance mode"
3755
+ print_color "$CYAN" " Note: May open Settings for manual selection"
3756
+ echo ""
3757
+ print_color "$CYAN" "🔄 Screen Rotation:"
3758
+ print_color "$YELLOW" " rotate - Toggle orientation (portrait/landscape)"
3759
+ print_color "$YELLOW" " rotate <orient> - Set orientation (portrait/landscape-left/landscape-right)"
3760
+ print_color "$YELLOW" " rotationlock - Toggle rotation lock (opens Control Center)"
3761
+ print_color "$YELLOW" " lockrotation - Toggle rotation lock (same as rotationlock)"
3762
+ print_color "$CYAN" " Note: May require manual interaction in Control Center"
3763
+ echo ""
3764
+ print_color "$CYAN" "📝 Text & Control:"
3765
+ print_color "$YELLOW" " any text - Send to active field"
3766
+ print_color "$YELLOW" " click <x,y> - Click at coordinates (e.g., click 100,200)"
3767
+ print_color "$YELLOW" " click <text> - Click on button/text element"
3768
+ print_color "$YELLOW" " tap <x,y> - Same as click"
3769
+ print_color "$YELLOW" " longpress <x,y> - Long press at coordinates (1.5s default)"
3770
+ print_color "$YELLOW" " longpress <text>- Long press on text element"
3771
+ print_color "$YELLOW" " longclick <x,y> - Same as longpress"
3772
+ print_color "$YELLOW" " getLocators - Get all UI element locators from current screen"
3773
+ print_color "$YELLOW" " -d device cmd - Run command on specific device only"
3774
+ print_color "$YELLOW" " help - Show this help"
3775
+ print_color "$YELLOW" " quit - Exit and cleanup (preserves device trust)"
3776
+ print_color "$YELLOW" " forcecleanup - Force kill WDA processes (may affect trust)"
3777
+ echo ""
3778
+ print_color "$CYAN" "💡 Examples:"
3779
+ print_color "$YELLOW" " launch safari"
3780
+ print_color "$YELLOW" " url google.com"
3781
+ print_color "$YELLOW" " click 200,300"
3782
+ print_color "$YELLOW" " click 'Sign In'"
3783
+ print_color "$YELLOW" " longpress 'Settings'"
3784
+ print_color "$YELLOW" " longpress 100,150,2.0"
3785
+ print_color "$YELLOW" " getLocators"
3786
+ print_color "$YELLOW" " -d iPhone17 getLocators # Get locators only from iPhone17"
3787
+ print_color "$YELLOW" " darkmode"
3788
+ print_color "$YELLOW" " rotate landscape-left"
3789
+ print_color "$YELLOW" " rotationlock"
3790
+ print_color "$YELLOW" " restart"
3791
+ print_color "$YELLOW" " shutdown"
3792
+ print_color "$YELLOW" " install /path/to/app.ipa"
3793
+ print_color "$YELLOW" " uninstall com.example.app"
3794
+ print_color "$YELLOW" " appinfo com.example.app"
3795
+ print_color "$YELLOW" " listapps mobileiron"
3796
+ print_color "$YELLOW" " Hello World"
3797
+ print_color "$YELLOW" " kill safari"
3798
+ continue
3799
+ ;;
3800
+ "")
3801
+ continue
3802
+ ;;
3803
+ *)
3804
+ execute_wda_command "$DEVICE_NAME_REQUEST" "$INPUT_TEXT"
3805
+ echo ""
3806
+ ;;
3807
+ esac
3808
+ done
3809
+ fi
3810
+ else
3811
+ # Setup failed
3812
+ print_color "$RED" "❌ Failed to connect to device: $DEVICE_NAME_REQUEST"
3813
+ print_color "$YELLOW" ""
3814
+ print_color "$YELLOW" "💡 Troubleshooting:"
3815
+ print_color "$YELLOW" " 1. Make sure device is unlocked"
3816
+ print_color "$YELLOW" " 2. Trust this computer on your device"
3817
+ print_color "$YELLOW" " 3. Check WebDriverAgent is installed"
3818
+ print_color "$YELLOW" " 4. Try: idevice_id -l (to verify USB connection)"
3819
+ exit 1
3820
+ fi
3821
+ exit 0
3822
+ fi
3823
+
3824
+ # Multi-device mode
3825
+ discover_and_connect_devices
3826
+
3827
+ if [ -z "$TEXT" ]; then
3828
+ # Interactive mode - create persistent sessions for all devices in parallel
3829
+ print_color "$YELLOW" "🔧 Initializing persistent sessions..."
3830
+
3831
+ session_pids=()
3832
+ for device in "${CONNECTED_DEVICES[@]}"; do
3833
+ {
3834
+ session=$(create_persistent_session "$device")
3835
+ if [ -n "$session" ]; then
3836
+ print_color "$GREEN" " ✅ Session ready: $device"
3837
+ else
3838
+ print_color "$RED" " ❌ Session failed: $device"
3839
+ fi
3840
+ } &
3841
+ session_pids+=($!)
3842
+ done
3843
+
3844
+ # Wait for all session creations to complete
3845
+ for pid in "${session_pids[@]}"; do
3846
+ wait "$pid"
3847
+ done
3848
+
3849
+ echo ""
3850
+ # Load history at startup
3851
+ load_text_history
3852
+
3853
+ # Initialize device selection (start with all devices selected)
3854
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
3855
+
3856
+ print_color "$CYAN" "🔄 Multi-device Interactive Mode"
3857
+ print_color "$YELLOW" " Commands: launch/kill/home/url/screenshot + text input"
3858
+ print_color "$YELLOW" " Type 'help' for commands • 'select' for device targeting • 'quit' to exit"
3859
+ print_color "$CYAN" " Available: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3860
+ print_color "$GREEN" " Active: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
3861
+ echo ""
3862
+
3863
+ while true; do
3864
+ # Enable readline for this input with custom history
3865
+ set -o history
3866
+ HISTFILE="$HISTORY_FILE"
3867
+ HISTSIZE="$HISTORY_MAX"
3868
+ HISTFILESIZE="$HISTORY_MAX"
3869
+
3870
+ # Load history from file for this session
3871
+ if [ -f "$HISTORY_FILE" ]; then
3872
+ history -r "$HISTORY_FILE"
3873
+ fi
3874
+
3875
+ read -e -p "Multi-device [${#SELECTED_DEVICES[@]}/${#CONNECTED_DEVICES[@]}] > " INPUT_TEXT
3876
+
3877
+ # Add any non-empty input to history (except quit commands)
3878
+ if [ -n "$INPUT_TEXT" ] && [[ "$INPUT_TEXT" != "quit" ]] && [[ "$INPUT_TEXT" != "exit" ]] && [[ "$INPUT_TEXT" != "q" ]] && [[ "$INPUT_TEXT" != "help" ]]; then
3879
+ add_to_history "$INPUT_TEXT"
3880
+ fi
3881
+
3882
+ # Disable history again to avoid shell command pollution
3883
+ set +o history
3884
+ unset HISTFILE
3885
+
3886
+ case "$INPUT_TEXT" in
3887
+ "quit"|"q"|"exit")
3888
+ print_color "$YELLOW" "👋 Goodbye!"
3889
+ cleanup_all_sessions
3890
+ exit 0
3891
+ ;;
3892
+ "forcecleanup"|"killwda")
3893
+ print_color "$YELLOW" "🛑 Force cleaning up - this may affect device trust..."
3894
+ cleanup_all_sessions_full
3895
+ print_color "$YELLOW" "👋 Goodbye!"
3896
+ exit 0
3897
+ ;;
3898
+ "help"|"commands")
3899
+ print_color "$YELLOW" "📱 Available Commands:"
3900
+ echo ""
3901
+ print_color "$CYAN" "🚀 App Control:"
3902
+ print_color "$YELLOW" " launch <app> - Launch application"
3903
+ print_color "$YELLOW" " open <app> - Same as launch"
3904
+ print_color "$YELLOW" " kill <app> - Terminate application"
3905
+ print_color "$YELLOW" " close <app> - Same as kill"
3906
+ print_color "$YELLOW" " install <path> - Install IPA file (requires iOS tools)"
3907
+ print_color "$YELLOW" " uninstall <id> - Uninstall app by bundle ID (requires iOS tools)"
3908
+ print_color "$YELLOW" " appinfo <id> - Check app version/info by bundle ID"
3909
+ print_color "$YELLOW" " version <id> - Same as appinfo"
3910
+ print_color "$YELLOW" " listapps [word] - List all apps or search by name/word"
3911
+ print_color "$YELLOW" " apps [word] - Same as listapps (search any installed app)"
3912
+ print_color "$YELLOW" " debug - Test device connection and tools"
3913
+ echo ""
3914
+ print_color "$CYAN" "🌐 Navigation:"
3915
+ print_color "$YELLOW" " home - Go to home screen"
3916
+ print_color "$YELLOW" " back [context] - Smart back navigation (button/swipe/context)"
3917
+ print_color "$YELLOW" " goback - Same as back"
3918
+ print_color "$YELLOW" " url <url> - Open URL in Safari"
3919
+ print_color "$YELLOW" " screenshot - Take screenshot"
3920
+ print_color "$YELLOW" " click <element> - Click on element or coordinates"
3921
+ print_color "$YELLOW" " longpress <el> - Long press on element"
3922
+ print_color "$YELLOW" " swipe <dir> - Swipe up/down/left/right or custom coordinates"
3923
+ print_color "$YELLOW" " scroll <dir> - Same as swipe"
3924
+ echo ""
3925
+ print_color "$CYAN" "⚙️ Device Control:"
3926
+ print_color "$YELLOW" " restart - Restart device (wireless + USB supported)"
3927
+ print_color "$YELLOW" " reboot - Same as restart"
3928
+ print_color "$YELLOW" " shutdown - Shutdown device (USB only, wireless does restart)"
3929
+ print_color "$YELLOW" " poweroff - Same as shutdown"
3930
+ echo ""
3931
+ print_color "$CYAN" "🎨 Display & Appearance:"
3932
+ print_color "$YELLOW" " darkmode - Switch to dark mode"
3933
+ print_color "$YELLOW" " lightmode - Switch to light mode"
3934
+ print_color "$YELLOW" " theme <mode> - Set theme (dark/light/toggle)"
3935
+ print_color "$YELLOW" " appearance - Check current appearance mode"
3936
+ print_color "$CYAN" " Note: May open Settings for manual selection"
3937
+ echo ""
3938
+ print_color "$CYAN" "🔄 Screen Rotation:"
3939
+ print_color "$YELLOW" " rotate - Toggle orientation (portrait/landscape)"
3940
+ print_color "$YELLOW" " rotate <orient> - Set orientation (portrait/landscape-left/landscape-right)"
3941
+ print_color "$YELLOW" " rotationlock - Toggle rotation lock (opens Control Center)"
3942
+ print_color "$YELLOW" " lockrotation - Toggle rotation lock (same as rotationlock)"
3943
+ print_color "$CYAN" " Note: May require manual interaction in Control Center"
3944
+ echo ""
3945
+ print_color "$CYAN" "📝 Text & Control:"
3946
+ print_color "$YELLOW" " any text - Send to active field"
3947
+ print_color "$YELLOW" " click <x,y> - Click at coordinates (e.g., click 100,200)"
3948
+ print_color "$YELLOW" " click <text> - Click on button/text element"
3949
+ print_color "$YELLOW" " tap <x,y> - Same as click"
3950
+ print_color "$YELLOW" " longpress <x,y> - Long press at coordinates (1.5s default)"
3951
+ print_color "$YELLOW" " longpress <text>- Long press on text element"
3952
+ print_color "$YELLOW" " longclick <x,y> - Same as longpress"
3953
+ print_color "$YELLOW" " help - Show this help"
3954
+ print_color "$YELLOW" " quit - Exit and cleanup (preserves device trust)"
3955
+ print_color "$YELLOW" " forcecleanup - Force kill WDA processes (may affect trust)"
3956
+ echo ""
3957
+ print_color "$CYAN" "🎯 Device Targeting:"
3958
+ print_color "$YELLOW" " select - Choose which devices to target"
3959
+ print_color "$YELLOW" " status - Show connected devices"
3960
+ echo ""
3961
+ print_color "$CYAN" "💡 Examples:"
3962
+ print_color "$YELLOW" " launch safari"
3963
+ print_color "$YELLOW" " url google.com"
3964
+ print_color "$YELLOW" " click 200,300"
3965
+ print_color "$YELLOW" " click 'Sign In'"
3966
+ print_color "$YELLOW" " longpress 'Settings'"
3967
+ print_color "$YELLOW" " longpress 100,150,2.0"
3968
+ print_color "$YELLOW" " darkmode"
3969
+ print_color "$YELLOW" " rotate landscape-left"
3970
+ print_color "$YELLOW" " rotationlock"
3971
+ print_color "$YELLOW" " restart"
3972
+ print_color "$YELLOW" " shutdown"
3973
+ print_color "$YELLOW" " install /path/to/app.ipa"
3974
+ print_color "$YELLOW" " uninstall com.example.app"
3975
+ print_color "$YELLOW" " appinfo com.example.app"
3976
+ print_color "$YELLOW" " listapps mobileiron"
3977
+ print_color "$YELLOW" " Hello World"
3978
+ print_color "$YELLOW" " kill safari"
3979
+ continue
3980
+ ;;
3981
+ "status"|"devices")
3982
+ print_color "$CYAN" "📱 Connected: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3983
+ continue
3984
+ ;;
3985
+ "")
3986
+ continue
3987
+ ;;
3988
+ "select"|"devices"|"target")
3989
+ print_color "$CYAN" "📱 Device Selection Menu:"
3990
+ echo ""
3991
+ print_color "$YELLOW" "Available devices:"
3992
+ for i in "${!CONNECTED_DEVICES[@]}"; do
3993
+ local device="${CONNECTED_DEVICES[$i]}"
3994
+ local is_selected=""
3995
+ if [[ " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
3996
+ is_selected="✅"
3997
+ else
3998
+ is_selected="⬜"
3999
+ fi
4000
+ echo " $((i+1)). $is_selected $device"
4001
+ done
4002
+ echo ""
4003
+ print_color "$CYAN" "Commands:"
4004
+ print_color "$YELLOW" " all - Select all devices"
4005
+ print_color "$YELLOW" " none - Deselect all devices"
4006
+ print_color "$YELLOW" " 1,2,3 - Select devices by numbers"
4007
+ print_color "$YELLOW" " +1,2 - Add devices to selection"
4008
+ print_color "$YELLOW" " -1,2 - Remove devices from selection"
4009
+ print_color "$YELLOW" " toggle 1,2 - Toggle device selection"
4010
+ print_color "$YELLOW" " list - Show current selection"
4011
+ print_color "$YELLOW" " done - Finish selection"
4012
+ echo ""
4013
+
4014
+ while true; do
4015
+ read -e -p "Device Selection > " DEVICE_CMD
4016
+
4017
+ case "$DEVICE_CMD" in
4018
+ "done"|"exit"|"")
4019
+ break
4020
+ ;;
4021
+ "all")
4022
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
4023
+ print_color "$GREEN" "✅ All devices selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4024
+ ;;
4025
+ "none"|"clear")
4026
+ SELECTED_DEVICES=()
4027
+ print_color "$YELLOW" "🔄 All devices deselected"
4028
+ ;;
4029
+ "list"|"show")
4030
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
4031
+ print_color "$RED" "❌ No devices selected"
4032
+ else
4033
+ print_color "$GREEN" "✅ Selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4034
+ fi
4035
+ ;;
4036
+ +*)
4037
+ # Add devices
4038
+ local add_nums="${DEVICE_CMD:1}"
4039
+ IFS=',' read -ra NUMS <<< "$add_nums"
4040
+ for num in "${NUMS[@]}"; do
4041
+ num=$(echo "$num" | xargs) # trim whitespace
4042
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4043
+ local device="${CONNECTED_DEVICES[$((num-1))]}"
4044
+ if [[ ! " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
4045
+ SELECTED_DEVICES+=("$device")
4046
+ print_color "$GREEN" "✅ Added: $device"
4047
+ fi
4048
+ fi
4049
+ done
4050
+ ;;
4051
+ -*)
4052
+ # Remove devices
4053
+ local remove_nums="${DEVICE_CMD:1}"
4054
+ IFS=',' read -ra NUMS <<< "$remove_nums"
4055
+ for num in "${NUMS[@]}"; do
4056
+ num=$(echo "$num" | xargs)
4057
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4058
+ local device="${CONNECTED_DEVICES[$((num-1))]}"
4059
+ local new_selected=()
4060
+ for selected in "${SELECTED_DEVICES[@]}"; do
4061
+ if [ "$selected" != "$device" ]; then
4062
+ new_selected+=("$selected")
4063
+ fi
4064
+ done
4065
+ SELECTED_DEVICES=("${new_selected[@]}")
4066
+ print_color "$YELLOW" "➖ Removed: $device"
4067
+ fi
4068
+ done
4069
+ ;;
4070
+ toggle*)
4071
+ # Toggle devices
4072
+ local toggle_nums="${DEVICE_CMD#toggle }"
4073
+ IFS=',' read -ra NUMS <<< "$toggle_nums"
4074
+ for num in "${NUMS[@]}"; do
4075
+ num=$(echo "$num" | xargs)
4076
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4077
+ local device="${CONNECTED_DEVICES[$((num-1))]}"
4078
+ if [[ " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
4079
+ # Remove from selection
4080
+ local new_selected=()
4081
+ for selected in "${SELECTED_DEVICES[@]}"; do
4082
+ if [ "$selected" != "$device" ]; then
4083
+ new_selected+=("$selected")
4084
+ fi
4085
+ done
4086
+ SELECTED_DEVICES=("${new_selected[@]}")
4087
+ print_color "$YELLOW" "⬜ Deselected: $device"
4088
+ else
4089
+ # Add to selection
4090
+ SELECTED_DEVICES+=("$device")
4091
+ print_color "$GREEN" "✅ Selected: $device"
4092
+ fi
4093
+ fi
4094
+ done
4095
+ ;;
4096
+ *)
4097
+ # Direct number selection (replace existing selection)
4098
+ if [[ "$DEVICE_CMD" =~ ^[0-9,[:space:]]+$ ]]; then
4099
+ SELECTED_DEVICES=()
4100
+ IFS=',' read -ra NUMS <<< "$DEVICE_CMD"
4101
+ for num in "${NUMS[@]}"; do
4102
+ num=$(echo "$num" | xargs)
4103
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4104
+ local device="${CONNECTED_DEVICES[$((num-1))]}"
4105
+ if [[ ! " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
4106
+ SELECTED_DEVICES+=("$device")
4107
+ fi
4108
+ fi
4109
+ done
4110
+ if [ ${#SELECTED_DEVICES[@]} -gt 0 ]; then
4111
+ print_color "$GREEN" "✅ Selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4112
+ else
4113
+ print_color "$RED" "❌ No valid device numbers provided"
4114
+ fi
4115
+ else
4116
+ print_color "$RED" "❌ Unknown command: $DEVICE_CMD"
4117
+ fi
4118
+ ;;
4119
+ esac
4120
+ done
4121
+
4122
+ # Update the prompt to show selected devices
4123
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
4124
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
4125
+ print_color "$YELLOW" "⚠️ No devices selected, using all devices"
4126
+ fi
4127
+
4128
+ print_color "$CYAN" "🎯 Active devices: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4129
+ continue
4130
+ ;;
4131
+ *)
4132
+ # Check if this is a runtime device selection: -d devicename command
4133
+ if [[ "$INPUT_TEXT" =~ ^-d[[:space:]]+([^[:space:]]+)[[:space:]]+(.+)$ ]]; then
4134
+ runtime_device="${BASH_REMATCH[1]}"
4135
+ runtime_command="${BASH_REMATCH[2]}"
4136
+
4137
+ # Validate device exists in connected devices
4138
+ device_found=false
4139
+ for device in "${CONNECTED_DEVICES[@]}"; do
4140
+ if [ "$device" = "$runtime_device" ]; then
4141
+ device_found=true
4142
+ break
4143
+ fi
4144
+ done
4145
+
4146
+ if [ "$device_found" = true ]; then
4147
+ print_color "$YELLOW" "🎯 Executing on single device: $runtime_device"
4148
+ print_color "$CYAN" " Command: $runtime_command"
4149
+ echo ""
4150
+
4151
+ execute_wda_command "$runtime_device" "$runtime_command"
4152
+ echo ""
4153
+ print_color "$GREEN" "✅ Completed on $runtime_device"
4154
+ else
4155
+ print_color "$RED" "❌ Device '$runtime_device' not found in connected devices"
4156
+ print_color "$YELLOW" " Available devices: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
4157
+ fi
4158
+ echo ""
4159
+ continue
4160
+ fi
4161
+
4162
+ # Check if this is a command (starts with known command keywords)
4163
+ first_word="${INPUT_TEXT%% *}" # More reliable word extraction
4164
+ # echo "DEBUG: Input text: '$INPUT_TEXT'"
4165
+ # echo "DEBUG: First word: '$first_word'"
4166
+
4167
+ case "$first_word" in
4168
+ "launch"|"open"|"kill"|"close"|"home"|"url"|"screenshot"|"restart"|"reboot"|"shutdown"|"poweroff"|"darkmode"|"lightmode"|"theme"|"appearance"|"rotate"|"rotationlock"|"lockrotation"|"install"|"uninstall"|"appinfo"|"version"|"listapps"|"apps"|"debug"|"click"|"tap"|"longpress"|"longclick"|"back"|"goback"|"swipe"|"scroll"|"getLocators"|"getlocators"|"locators")
4169
+ # This is a command - send to execute_wda_command for each device
4170
+ # echo "DEBUG: Command detected!"
4171
+ print_color "$YELLOW" "🚀 Executing command on ${#SELECTED_DEVICES[@]} device(s): $INPUT_TEXT"
4172
+ echo ""
4173
+
4174
+ pids=()
4175
+ for device in "${SELECTED_DEVICES[@]}"; do
4176
+ {
4177
+ execute_wda_command "$device" "$INPUT_TEXT"
4178
+ echo "[${device}] Completed"
4179
+ } &
4180
+ pids+=($!)
4181
+ print_color "$CYAN" "📱 Started: $device (PID: $!)"
4182
+ done
4183
+
4184
+ echo ""
4185
+ print_color "$YELLOW" "⏳ Waiting for completion..."
4186
+
4187
+ for pid in "${pids[@]}"; do
4188
+ wait "$pid"
4189
+ done
4190
+
4191
+ echo ""
4192
+ print_color "$GREEN" "✅ All devices completed!"
4193
+ ;;
4194
+ *)
4195
+ # This is regular text - send as text input
4196
+ # echo "DEBUG: Treating as text input"
4197
+ TEXT="$INPUT_TEXT"
4198
+ # Use selected devices instead of all devices
4199
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
4200
+ send_to_all_devices "${CONNECTED_DEVICES[@]}"
4201
+ else
4202
+ send_to_all_devices "${SELECTED_DEVICES[@]}"
4203
+ fi
4204
+ TEXT=""
4205
+ ;;
4206
+ esac
4207
+ echo ""
4208
+ ;;
4209
+ esac
4210
+ done
4211
+ else
4212
+ # Single command mode - create temporary sessions
4213
+ print_color "$YELLOW" "🎯 Command: $TEXT"
4214
+ print_color "$CYAN" " Targets: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
4215
+ echo ""
4216
+
4217
+ # Create sessions for all devices
4218
+ for device in "${CONNECTED_DEVICES[@]}"; do
4219
+ create_persistent_session "$device" >/dev/null
4220
+ done
4221
+
4222
+ send_to_all_devices "${CONNECTED_DEVICES[@]}"
4223
+ fi
4224
+ # Cleanup on normal exit
4225
+ graceful_cleanup true