openuispec 0.2.9 → 0.2.11

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 (232) hide show
  1. package/README.md +3 -1
  2. package/cli/index.ts +80 -2
  3. package/cli/init.ts +23 -8
  4. package/docs/cli.md +42 -7
  5. package/drift/index.ts +41 -15
  6. package/mcp-server/index.ts +247 -117
  7. package/mcp-server/screenshot-android.ts +185 -44
  8. package/mcp-server/screenshot-ios.ts +242 -30
  9. package/mcp-server/screenshot.ts +96 -1
  10. package/package.json +5 -2
  11. package/prepare/index.ts +16 -0
  12. package/scripts/take-all-screenshots.ts +507 -0
  13. package/status/index.ts +2 -2
  14. package/examples/social-app/.mcp.json +0 -10
  15. package/examples/social-app/AGENTS.md +0 -124
  16. package/examples/social-app/CLAUDE.md +0 -124
  17. package/examples/social-app/backend/.gitkeep +0 -1
  18. package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
  19. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
  20. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
  21. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
  22. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
  23. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
  24. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
  40. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
  41. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
  42. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
  43. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
  44. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
  45. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
  46. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
  47. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
  48. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
  49. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
  50. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
  51. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
  52. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  53. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
  54. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
  55. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
  56. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
  57. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
  58. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
  59. package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
  60. package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
  61. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
  62. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
  63. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
  64. package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
  65. package/examples/social-app/generated/android/social-app/gradlew +0 -248
  66. package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
  67. package/examples/social-app/generated/web/social-app/index.html +0 -12
  68. package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
  69. package/examples/social-app/generated/web/social-app/package.json +0 -27
  70. package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
  71. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
  72. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
  73. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
  74. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
  75. package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
  76. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
  77. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
  78. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
  79. package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
  80. package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
  81. package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
  82. package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
  83. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
  84. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
  85. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
  86. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
  87. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
  88. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
  89. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
  90. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
  91. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
  92. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
  93. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
  94. package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
  95. package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
  96. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
  97. package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
  98. package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
  99. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
  100. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
  101. package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
  102. package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
  103. package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
  104. package/examples/social-app/package.json +0 -13
  105. package/examples/social-app/take-web-screenshots.ts +0 -97
  106. package/examples/taskflow/.codex/config.toml +0 -4
  107. package/examples/taskflow/.mcp.json +0 -10
  108. package/examples/taskflow/AGENTS.md +0 -124
  109. package/examples/taskflow/CLAUDE.md +0 -124
  110. package/examples/taskflow/backend/.gitkeep +0 -1
  111. package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
  112. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
  113. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
  114. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
  115. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
  116. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
  117. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
  118. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
  119. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
  120. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
  121. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
  122. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
  123. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
  124. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
  125. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
  126. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
  127. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
  128. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
  129. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
  130. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
  131. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
  132. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  133. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
  134. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
  135. package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
  136. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
  137. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
  138. package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
  139. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
  140. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
  141. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
  142. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
  143. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
  144. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
  145. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
  146. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
  147. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
  148. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
  149. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
  150. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
  151. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
  152. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
  153. package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -26
  154. package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
  155. package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
  156. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
  157. package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
  158. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
  159. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
  160. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
  161. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
  162. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
  163. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
  164. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
  165. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
  166. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
  167. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
  168. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
  169. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
  170. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
  171. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
  172. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
  173. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
  174. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
  175. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
  176. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
  177. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
  178. package/examples/todo-orbit/.codex/config.toml +0 -4
  179. package/examples/todo-orbit/.mcp.json +0 -10
  180. package/examples/todo-orbit/AGENTS.md +0 -124
  181. package/examples/todo-orbit/CLAUDE.md +0 -124
  182. package/examples/todo-orbit/backend/.gitkeep +0 -1
  183. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
  184. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
  185. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
  186. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
  187. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
  188. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
  189. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
  190. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
  191. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
  192. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
  193. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
  194. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
  195. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
  196. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
  197. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
  198. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
  199. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
  200. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
  201. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  202. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
  203. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
  204. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
  205. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
  206. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
  207. package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
  208. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
  209. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
  210. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
  211. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
  212. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
  213. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
  214. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
  215. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
  216. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
  217. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
  218. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
  219. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
  220. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
  221. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
  222. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -400
  223. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  224. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -25
  225. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
  226. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
  227. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
  228. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
  229. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
  230. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
  231. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
  232. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +0 -6
@@ -17,6 +17,7 @@ import {
17
17
  } from "./screenshot-shared.js";
18
18
 
19
19
  const exec = promisify(execCb);
20
+ const androidScreenshotQueues = new Map<string, Promise<void>>();
20
21
 
21
22
  // ── types ───────────────────────────────────────────────────────────
22
23
 
@@ -31,6 +32,21 @@ export interface AndroidScreenshotOptions {
31
32
  module?: string;
32
33
  }
33
34
 
35
+ export interface AndroidBatchCapture {
36
+ screen: string;
37
+ route?: string;
38
+ nav?: string[];
39
+ wait_for?: number;
40
+ }
41
+
42
+ export interface AndroidScreenshotBatchOptions {
43
+ captures: AndroidBatchCapture[];
44
+ theme?: "light" | "dark";
45
+ output_dir?: string;
46
+ project_dir?: string;
47
+ module?: string;
48
+ }
49
+
34
50
  // ── constants ───────────────────────────────────────────────────────
35
51
 
36
52
  const ADB_SCREENSHOT_PATH = "/sdcard/openuispec_screenshot.png";
@@ -282,6 +298,27 @@ export async function installAndLaunch(
282
298
  }
283
299
  }
284
300
 
301
+ export async function launchInstalledApp(
302
+ adb: string,
303
+ serial: string,
304
+ appInfo: AppInfo,
305
+ route?: string,
306
+ ): Promise<void> {
307
+ await adbShell(adb, serial, `am force-stop ${appInfo.applicationId}`);
308
+ // Clear saved nav state so deep links route correctly
309
+ try { await adbShell(adb, serial, `pm clear ${appInfo.applicationId}`); } catch { /* ignore */ }
310
+ if (route) {
311
+ await adbShell(
312
+ adb,
313
+ serial,
314
+ `am start -W -a android.intent.action.VIEW -d '${route}' ` +
315
+ `${appInfo.applicationId}/${appInfo.launchActivity}`,
316
+ );
317
+ } else {
318
+ await adbShell(adb, serial, `am start -W -n ${appInfo.applicationId}/${appInfo.launchActivity}`);
319
+ }
320
+ }
321
+
285
322
  // ── theme control ───────────────────────────────────────────────────
286
323
 
287
324
  export async function setTheme(adb: string, serial: string, theme: "light" | "dark"): Promise<void> {
@@ -358,9 +395,12 @@ export async function captureScreenshot(
358
395
  serial: string,
359
396
  localPath: string,
360
397
  ): Promise<void> {
361
- await adbShell(adb, serial, `screencap -p ${ADB_SCREENSHOT_PATH}`);
362
- await adbExec(adb, serial, `pull ${ADB_SCREENSHOT_PATH} "${localPath}"`);
363
- await adbShell(adb, serial, `rm ${ADB_SCREENSHOT_PATH}`);
398
+ try {
399
+ await exec(`${adb} -s ${serial} exec-out screencap -p > "${localPath}"`, { timeout: 60_000, shell: "/bin/bash" });
400
+ } catch (err: any) {
401
+ const output = ((err.stderr ?? "") + "\n" + (err.stdout ?? "")).trim();
402
+ throw new Error(`Android screenshot capture failed${output ? `:\n${output}` : "."}`);
403
+ }
364
404
  }
365
405
 
366
406
  // ── wait for app ready ──────────────────────────────────────────────
@@ -395,6 +435,47 @@ async function waitForAppReady(
395
435
  await new Promise(r => setTimeout(r, waitMs));
396
436
  }
397
437
 
438
+ async function takeSingleAndroidCapture(
439
+ adb: string,
440
+ serial: string,
441
+ androidDir: string,
442
+ appInfo: AppInfo,
443
+ capture: AndroidBatchCapture,
444
+ theme: "light" | "dark" | undefined,
445
+ defaultOutputDir: string | undefined,
446
+ ): Promise<{ screen: string; path: string; data: string }> {
447
+ await launchInstalledApp(adb, serial, appInfo, capture.route);
448
+ await waitForAppReady(adb, serial, appInfo.applicationId, capture.wait_for ?? 3000);
449
+
450
+ if (capture.nav && capture.nav.length > 0) {
451
+ await navigateByTaps(adb, serial, capture.nav);
452
+ }
453
+
454
+ const themeLabel = theme ?? "default";
455
+ const filename = `${capture.screen}_${themeLabel}.png`;
456
+ const tmpPath = join(androidDir, `.openuispec-screenshot-${capture.screen}.png`);
457
+ await captureScreenshot(adb, serial, tmpPath);
458
+
459
+ let savedPath = filename;
460
+ if (defaultOutputDir) {
461
+ const outDir = resolve(androidDir, defaultOutputDir);
462
+ mkdirSync(outDir, { recursive: true });
463
+ savedPath = join(outDir, filename);
464
+ copyFileSync(tmpPath, savedPath);
465
+ }
466
+
467
+ try {
468
+ const data = readFileSync(tmpPath).toString("base64");
469
+ return {
470
+ screen: capture.screen,
471
+ path: savedPath,
472
+ data,
473
+ };
474
+ } finally {
475
+ try { unlinkSync(tmpPath); } catch { /* ignore */ }
476
+ }
477
+ }
478
+
398
479
  // ── main entry point ────────────────────────────────────────────────
399
480
 
400
481
  export async function takeAndroidScreenshot(
@@ -420,61 +501,121 @@ export async function takeAndroidScreenshot(
420
501
  const adb = findAdb();
421
502
  const serial = await getConnectedEmulator(adb);
422
503
 
423
- // 3. Free emulator storage before build/install
424
- await cleanEmulatorStorage(adb, serial);
504
+ const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
505
+ let releaseQueue: (() => void) | undefined;
506
+ const currentRun = new Promise<void>((resolve) => {
507
+ releaseQueue = resolve;
508
+ });
509
+ androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
425
510
 
426
- // 4. Build APK
427
- const apkPath = await buildApk(androidDir, appInfo.moduleName);
511
+ await previousRun;
428
512
 
429
- // 5. Set theme if requested
430
- if (theme) {
431
- await setTheme(adb, serial, theme);
432
- }
513
+ try {
514
+ // 3. Free emulator storage before build/install
515
+ await cleanEmulatorStorage(adb, serial);
433
516
 
434
- // 6. Install and launch
435
- await installAndLaunch(adb, serial, apkPath, appInfo, route);
517
+ // 4. Build APK
518
+ const apkPath = await buildApk(androidDir, appInfo.moduleName);
436
519
 
437
- // 7. Wait for app to be ready and content to load
438
- await waitForAppReady(adb, serial, appInfo.applicationId, wait_for);
520
+ // 5. Set theme if requested
521
+ if (theme) {
522
+ await setTheme(adb, serial, theme);
523
+ }
439
524
 
440
- // 8. Navigate via UI taps if specified
441
- if (nav && nav.length > 0) {
442
- await navigateByTaps(adb, serial, nav);
443
- }
525
+ // 6. Install fresh once, then capture
526
+ await installAndLaunch(adb, serial, apkPath, appInfo, route);
527
+
528
+ const snapshot = await takeSingleAndroidCapture(
529
+ adb,
530
+ serial,
531
+ androidDir,
532
+ appInfo,
533
+ { screen: screen ?? "main", route, nav, wait_for },
534
+ theme,
535
+ output_dir,
536
+ );
444
537
 
445
- // 9. Capture screenshot
446
- const screenLabel = screen ?? "main";
447
- const themeLabel = theme ?? "default";
448
- const filename = `${screenLabel}_${themeLabel}.png`;
449
- const tmpPath = join(androidDir, ".openuispec-screenshot.png");
450
- await captureScreenshot(adb, serial, tmpPath);
538
+ return buildScreenshotResponse([snapshot], (s) => ({
539
+ screen: s.screen,
540
+ path: snapshot.path ?? null,
541
+ emulator: serial,
542
+ theme: theme ?? "default",
543
+ applicationId: appInfo.applicationId,
544
+ }));
545
+ } finally {
546
+ releaseQueue?.();
547
+ if (androidScreenshotQueues.get(serial) === currentRun) {
548
+ androidScreenshotQueues.delete(serial);
549
+ }
550
+ }
551
+ }
451
552
 
452
- // 9. Save to output_dir if specified
453
- let savedPath: string | undefined;
454
- if (output_dir) {
455
- const outDir = resolve(androidDir, output_dir);
456
- mkdirSync(outDir, { recursive: true });
457
- savedPath = join(outDir, filename);
458
- copyFileSync(tmpPath, savedPath);
553
+ export async function takeAndroidScreenshotBatch(
554
+ projectCwd: string,
555
+ options: AndroidScreenshotBatchOptions,
556
+ ): Promise<ScreenshotResult> {
557
+ const { captures, theme, output_dir, project_dir, module } = options;
558
+ if (captures.length === 0) {
559
+ return {
560
+ content: [{ type: "text", text: "No Android captures specified." }],
561
+ isError: true,
562
+ };
459
563
  }
460
564
 
461
- // 10. Read and return
565
+ const androidDir = findAndroidAppDir(projectCwd, project_dir);
566
+ const appInfo = extractAppInfo(androidDir, module);
567
+ const adb = findAdb();
568
+ const serial = await getConnectedEmulator(adb);
569
+
570
+ const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
571
+ let releaseQueue: (() => void) | undefined;
572
+ const currentRun = new Promise<void>((resolve) => {
573
+ releaseQueue = resolve;
574
+ });
575
+ androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
576
+
577
+ await previousRun;
578
+
462
579
  try {
463
- const data = readFileSync(tmpPath).toString("base64");
464
- const snapshots = [{
465
- screen: screenLabel,
466
- path: savedPath ?? filename,
467
- data,
468
- }];
580
+ await cleanEmulatorStorage(adb, serial);
581
+ const apkPath = await buildApk(androidDir, appInfo.moduleName);
469
582
 
470
- return buildScreenshotResponse(snapshots, (s) => ({
471
- screen: s.screen,
472
- path: savedPath ?? null,
583
+ if (theme) {
584
+ await setTheme(adb, serial, theme);
585
+ }
586
+
587
+ await installAndLaunch(adb, serial, apkPath, appInfo);
588
+
589
+ // Pre-create output dir once
590
+ if (output_dir) mkdirSync(resolve(androidDir, output_dir), { recursive: true });
591
+
592
+ const snapshots = [];
593
+ for (let index = 0; index < captures.length; index += 1) {
594
+ const capture = captures[index];
595
+ snapshots.push(
596
+ await takeSingleAndroidCapture(
597
+ adb,
598
+ serial,
599
+ androidDir,
600
+ appInfo,
601
+ capture,
602
+ theme,
603
+ output_dir,
604
+ ),
605
+ );
606
+ }
607
+
608
+ return buildScreenshotResponse(snapshots, (snapshot) => ({
609
+ screen: snapshot.screen,
610
+ path: snapshot.path,
473
611
  emulator: serial,
474
- theme: themeLabel,
612
+ theme: theme ?? "default",
475
613
  applicationId: appInfo.applicationId,
476
614
  }));
477
615
  } finally {
478
- try { unlinkSync(tmpPath); } catch { /* ignore */ }
616
+ releaseQueue?.();
617
+ if (androidScreenshotQueues.get(serial) === currentRun) {
618
+ androidScreenshotQueues.delete(serial);
619
+ }
479
620
  }
480
621
  }
@@ -233,6 +233,45 @@ async function setAppearance(udid: string, theme: "light" | "dark"): Promise<voi
233
233
  const UITEST_TARGET = "ScreenshotUITests";
234
234
  const UITEST_DIR = ".screenshot-uitest";
235
235
 
236
+ export function generateUITestTargetYml(
237
+ appInfo: IOSAppInfo,
238
+ sourcePath: string,
239
+ includeProductName = false,
240
+ ): string {
241
+ const productLines = includeProductName
242
+ ? `\n PRODUCT_NAME: ${UITEST_TARGET}\n PRODUCT_MODULE_NAME: ${UITEST_TARGET}`
243
+ : "";
244
+ return ` ${UITEST_TARGET}:
245
+ type: bundle.ui-testing
246
+ platform: iOS
247
+ deploymentTarget: "${appInfo.deploymentTarget}"
248
+ sources:
249
+ - path: ${sourcePath}
250
+ dependencies:
251
+ - target: ${appInfo.schemeName}
252
+ embed: false
253
+ settings:
254
+ base:${productLines}
255
+ TEST_TARGET_NAME: ${appInfo.schemeName}
256
+ PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
257
+ GENERATE_INFOPLIST_FILE: YES`;
258
+ }
259
+
260
+ export function insertUITestTarget(yml: string, targetYml: string): string {
261
+ if (yml.includes("\nschemes:")) {
262
+ return yml.replace("\nschemes:", `\n${targetYml}\nschemes:`);
263
+ }
264
+ return yml + "\n" + targetYml + "\n";
265
+ }
266
+
267
+ export function ensureInfoPlistFlag(yml: string): string {
268
+ if (yml.includes("GENERATE_INFOPLIST_FILE")) return yml;
269
+ return yml.replace(
270
+ /(PRODUCT_BUNDLE_IDENTIFIER:[^\n]+\n)/,
271
+ "$1 GENERATE_INFOPLIST_FILE: YES\n",
272
+ );
273
+ }
274
+
236
275
  function generateUITestSwift(
237
276
  bundleId: string,
238
277
  navSteps: string[],
@@ -322,40 +361,13 @@ async function runXCUITest(
322
361
 
323
362
  if (appInfo.hasXcodegen) {
324
363
  originalProjectYml = readFileSync(projectYmlPath, "utf-8");
364
+ let modifiedYml = ensureInfoPlistFlag(originalProjectYml);
365
+ modifiedYml = insertUITestTarget(modifiedYml, generateUITestTargetYml(appInfo, `${UITEST_DIR}/Sources`));
366
+ writeFileSync(projectYmlPath, modifiedYml);
325
367
 
326
- // Ensure main target has GENERATE_INFOPLIST_FILE and append UI test target
327
- let modifiedYml = originalProjectYml;
328
- if (!modifiedYml.includes("GENERATE_INFOPLIST_FILE")) {
329
- // Add after the first PRODUCT_BUNDLE_IDENTIFIER line
330
- modifiedYml = modifiedYml.replace(
331
- /(PRODUCT_BUNDLE_IDENTIFIER:[^\n]+\n)/,
332
- "$1 GENERATE_INFOPLIST_FILE: YES\n",
333
- );
334
- }
335
-
336
- const uitestConfig = `
337
- ${UITEST_TARGET}:
338
- type: bundle.ui-testing
339
- platform: iOS
340
- deploymentTarget: "${appInfo.deploymentTarget}"
341
- sources:
342
- - path: ${UITEST_DIR}/Sources
343
- dependencies:
344
- - target: ${appInfo.schemeName}
345
- embed: false
346
- settings:
347
- base:
348
- TEST_TARGET_NAME: ${appInfo.schemeName}
349
- PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
350
- GENERATE_INFOPLIST_FILE: YES
351
- `;
352
- writeFileSync(projectYmlPath, modifiedYml + uitestConfig);
353
-
354
- // Regenerate Xcode project
355
368
  try {
356
369
  await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 });
357
370
  } catch (err: any) {
358
- // Restore original project.yml
359
371
  writeFileSync(projectYmlPath, originalProjectYml);
360
372
  throw new Error(`xcodegen failed: ${((err.stderr ?? "") + (err.stdout ?? "")).slice(-300)}`);
361
373
  }
@@ -539,3 +551,203 @@ export async function takeIOSScreenshot(
539
551
  try { unlinkSync(tmpPath); } catch { /* ignore */ }
540
552
  }
541
553
  }
554
+
555
+ // ── batch types ──────────────────────────────────────────────────────
556
+
557
+ export interface IOSBatchCapture {
558
+ screen: string;
559
+ nav?: string[];
560
+ wait_for?: number;
561
+ }
562
+
563
+ export interface IOSScreenshotBatchOptions {
564
+ captures: IOSBatchCapture[];
565
+ device?: string;
566
+ theme?: "light" | "dark";
567
+ output_dir?: string;
568
+ project_dir?: string;
569
+ scheme?: string;
570
+ bundle_id?: string;
571
+ }
572
+
573
+ // ── batch screenshot ─────────────────────────────────────────────────
574
+
575
+ export async function takeIOSScreenshotBatch(
576
+ projectCwd: string,
577
+ options: IOSScreenshotBatchOptions,
578
+ ): Promise<ScreenshotResult> {
579
+ const { captures, device, theme, output_dir, project_dir, scheme, bundle_id } = options;
580
+
581
+ if (captures.length === 0) {
582
+ return { content: [{ type: "text", text: "No iOS captures specified." }], isError: true };
583
+ }
584
+
585
+ const iosDir = findIOSAppDir(projectCwd, project_dir);
586
+ const appInfo = extractAppInfo(iosDir, { scheme, bundle_id });
587
+ const sim = findSimulator(device);
588
+ await ensureSimulatorBooted(sim.udid);
589
+
590
+ if (theme) {
591
+ await setAppearance(sim.udid, theme);
592
+ }
593
+
594
+ const themeLabel = theme ?? "default";
595
+ const snapshots: Array<{ screen: string; path: string; data: string }> = [];
596
+
597
+ // Separate captures: no-nav (simctl screenshot) vs nav (XCUITest batch)
598
+ const noNavCaptures = captures.filter((c) => !c.nav || c.nav.length === 0);
599
+ const navCaptures = captures.filter((c) => c.nav && c.nav.length > 0);
600
+
601
+ // Build + install once for all captures
602
+ const appBundlePath = await buildApp(iosDir, appInfo, sim.udid);
603
+ await installAndLaunch(sim.udid, appBundlePath, appInfo.bundleId);
604
+
605
+ // Pre-create output dir once
606
+ if (output_dir) mkdirSync(resolve(iosDir, output_dir), { recursive: true });
607
+
608
+ // No-nav captures: relaunch, wait, simctl screenshot
609
+ for (const capture of noNavCaptures) {
610
+ // Relaunch without reinstalling
611
+ try { await exec(`xcrun simctl terminate ${sim.udid} ${appInfo.bundleId}`); } catch { /* not running */ }
612
+ await exec(`xcrun simctl launch ${sim.udid} ${appInfo.bundleId}`, { timeout: 30_000 });
613
+ await waitForAppReady(sim.udid, appInfo.bundleId, capture.wait_for ?? 3000);
614
+
615
+ const filename = `${capture.screen}_${themeLabel}.png`;
616
+ const tmpPath = join(iosDir, `.openuispec-screenshot-${capture.screen}.png`);
617
+ await captureScreenshot(sim.udid, tmpPath);
618
+
619
+ if (!existsSync(tmpPath)) continue;
620
+
621
+ let savedPath = filename;
622
+ if (output_dir) {
623
+ savedPath = join(resolve(iosDir, output_dir), filename);
624
+ copyFileSync(tmpPath, savedPath);
625
+ }
626
+
627
+ snapshots.push({ screen: capture.screen, path: savedPath, data: readFileSync(tmpPath).toString("base64") });
628
+ try { unlinkSync(tmpPath); } catch { /* ignore */ }
629
+ }
630
+
631
+ // Nav captures: batch into a single XCUITest run
632
+ if (navCaptures.length > 0) {
633
+ const uitestDir = join(iosDir, ".screenshot-uitest");
634
+ const sourcesDir = join(uitestDir, "Sources");
635
+ mkdirSync(sourcesDir, { recursive: true });
636
+
637
+ // Build output paths map
638
+ const outputPaths: Record<string, string> = {};
639
+ for (const capture of navCaptures) {
640
+ const filename = `${capture.screen}_${themeLabel}.png`;
641
+ if (output_dir) {
642
+ const outDir = resolve(iosDir, output_dir);
643
+ mkdirSync(outDir, { recursive: true });
644
+ outputPaths[capture.screen] = join(outDir, filename);
645
+ } else {
646
+ outputPaths[capture.screen] = join(iosDir, `.openuispec-screenshot-${capture.screen}.png`);
647
+ }
648
+ }
649
+
650
+ // Generate multi-test Swift file
651
+ const testCases = navCaptures.map((capture, i) => {
652
+ const taps = (capture.nav ?? []).map((step, j) => {
653
+ const escaped = step.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
654
+ return `
655
+ let target_${i}_${j} = app.descendants(matching: .any).matching(NSPredicate(format: "label ==[c] %@ OR title ==[c] %@", "${escaped}", "${escaped}")).firstMatch
656
+ if target_${i}_${j}.waitForExistence(timeout: 5) {
657
+ target_${i}_${j}.tap()
658
+ Thread.sleep(forTimeInterval: 0.8)
659
+ }`;
660
+ }).join("\n");
661
+
662
+ const outPath = outputPaths[capture.screen].replace(/"/g, '\\"');
663
+ return `
664
+ func test_${String(i + 1).padStart(2, "0")}_${capture.screen}() {
665
+ let app = XCUIApplication()
666
+ app.launchArguments = ["-AppleLanguages", "(en)"]
667
+ app.launch()
668
+ Thread.sleep(forTimeInterval: ${((capture.wait_for ?? 3000) / 1000).toFixed(1)})
669
+ ${taps}
670
+ Thread.sleep(forTimeInterval: 0.5)
671
+ let screenshot = XCUIScreen.main.screenshot()
672
+ try! screenshot.pngRepresentation.write(to: URL(fileURLWithPath: "${outPath}"))
673
+ }`;
674
+ }).join("\n");
675
+
676
+ writeFileSync(join(sourcesDir, "ScreenshotUITest.swift"),
677
+ `import XCTest\n\nfinal class ScreenshotUITest: XCTestCase {\n${testCases}\n}\n`);
678
+
679
+ // Set up xcodegen
680
+ const UITEST_TARGET = "ScreenshotUITests";
681
+ const hasXcodegen = existsSync(join(iosDir, "project.yml"));
682
+ const projectYmlPath = join(iosDir, "project.yml");
683
+ let originalProjectYml: string | null = null;
684
+ const buildDir = join(iosDir, ".build", "screenshot");
685
+
686
+ if (hasXcodegen) {
687
+ originalProjectYml = readFileSync(projectYmlPath, "utf-8");
688
+ let modifiedYml = ensureInfoPlistFlag(originalProjectYml);
689
+ modifiedYml = insertUITestTarget(modifiedYml, generateUITestTargetYml(appInfo, ".screenshot-uitest/Sources", true));
690
+ writeFileSync(projectYmlPath, modifiedYml);
691
+ await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 });
692
+ } else {
693
+ writeFileSync(join(uitestDir, "project.yml"), `name: ${UITEST_TARGET}
694
+ targets:
695
+ ${UITEST_TARGET}:
696
+ type: bundle.ui-testing
697
+ platform: iOS
698
+ deploymentTarget: "${appInfo.deploymentTarget}"
699
+ sources:
700
+ - path: Sources
701
+ settings:
702
+ base:
703
+ TEST_TARGET_NAME: ${appInfo.schemeName}
704
+ PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
705
+ GENERATE_INFOPLIST_FILE: YES
706
+ `);
707
+ await exec(`xcodegen generate`, { cwd: uitestDir, timeout: 30_000 });
708
+ }
709
+
710
+ const testProjectFlag = hasXcodegen
711
+ ? (appInfo.xcodeproj ? `-project "${join(iosDir, appInfo.xcodeproj)}"` : "")
712
+ : `-project "${join(uitestDir, `${UITEST_TARGET}.xcodeproj`)}"`;
713
+ const testCwd = hasXcodegen ? iosDir : uitestDir;
714
+
715
+ try {
716
+ await exec(
717
+ `xcodebuild test ${testProjectFlag} -scheme "${UITEST_TARGET}" -destination "id=${sim.udid}" -derivedDataPath "${buildDir}" -only-testing:${UITEST_TARGET}/ScreenshotUITest 2>&1`,
718
+ { cwd: testCwd, timeout: 300_000 },
719
+ );
720
+ } catch {
721
+ // Tests may "fail" but still produce screenshots
722
+ } finally {
723
+ if (originalProjectYml) {
724
+ writeFileSync(projectYmlPath, originalProjectYml);
725
+ try { await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 }); } catch { /* best effort */ }
726
+ }
727
+ }
728
+
729
+ // Collect results
730
+ for (const capture of navCaptures) {
731
+ const outPath = outputPaths[capture.screen];
732
+ if (existsSync(outPath)) {
733
+ snapshots.push({
734
+ screen: capture.screen,
735
+ path: output_dir ? outPath : `${capture.screen}_${themeLabel}.png`,
736
+ data: readFileSync(outPath).toString("base64"),
737
+ });
738
+ if (!output_dir) { try { unlinkSync(outPath); } catch { /* ignore */ } }
739
+ }
740
+ }
741
+ }
742
+
743
+ if (snapshots.length === 0) {
744
+ return { content: [{ type: "text", text: "No screenshots were captured. Check Xcode and Simulator output." }], isError: true };
745
+ }
746
+
747
+ const content: ScreenshotResult["content"] = [];
748
+ for (const s of snapshots) {
749
+ content.push({ type: "image" as const, data: s.data, mimeType: "image/png" });
750
+ content.push({ type: "text" as const, text: JSON.stringify({ screen: s.screen, path: s.path, simulator: sim.name, theme: themeLabel, bundleId: appInfo.bundleId }, null, 2) });
751
+ }
752
+ return { content };
753
+ }