mobster-cli 0.4.3__py3-none-any.whl

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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,152 @@
1
+ """`mobster test nightly`: every check overnight on this Mac, and a morning report waiting for you."""
2
+
3
+ import datetime
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import plistlib
8
+ import re
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from . import DevError, agent_argv, self_argv
13
+
14
+ LABEL = "dev.mobster.nightly"
15
+ FOLDER = ".mobster/nightly"
16
+ TIME = re.compile(r"([01]?\d|2[0-3]):([0-5]\d)")
17
+
18
+
19
+ def label_for(root):
20
+ root = Path(root).resolve()
21
+ slug = re.sub(r"[^a-z0-9]+", "-", root.name.lower()).strip("-")[:30] or "project"
22
+ return f"{LABEL}.{slug}-{hashlib.sha256(str(root).encode()).hexdigest()[:8]}"
23
+
24
+
25
+ # A per-user LaunchAgent, not a daemon: it reaches the person's simulators, Keychain and plugged-in iPhones.
26
+ def plist_path(root, home=None):
27
+ return Path(home or Path.home()) / "Library" / "LaunchAgents" / f"{label_for(root)}.plist"
28
+
29
+
30
+ def parse_time(text):
31
+ match = TIME.fullmatch(str(text or "").strip())
32
+ if not match:
33
+ raise DevError(f"--at {text} isn't a time.", "Give it as HH:MM on a 24-hour clock, such as 02:00.")
34
+ return int(match.group(1)), int(match.group(2))
35
+
36
+
37
+ def turn_on(root, at, flags, *, home=None, run=None, uid=None):
38
+ """Write and load the LaunchAgent. Returns its plist path."""
39
+ hour, minute = parse_time(at)
40
+ root = Path(root).resolve()
41
+ argv, env = agent_argv()
42
+ path = plist_path(root, home)
43
+ (root / FOLDER).mkdir(parents=True, exist_ok=True)
44
+ data = {"Label": label_for(root), "ProgramArguments": [*argv, "test", "nightly", "run", *flags],
45
+ "WorkingDirectory": str(root), "StartCalendarInterval": {"Hour": hour, "Minute": minute},
46
+ "StandardOutPath": str(root / FOLDER / "launchd.log"),
47
+ "StandardErrorPath": str(root / FOLDER / "launchd.log"),
48
+ "ProcessType": "Background", "LowPriorityIO": True, "Nice": 5}
49
+ if env:
50
+ data["EnvironmentVariables"] = env
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ path.write_bytes(plistlib.dumps(data))
53
+ run = run or _launchctl
54
+ domain = f"gui/{os.getuid() if uid is None else uid}"
55
+ run(["launchctl", "bootout", domain, str(path)])
56
+ if run(["launchctl", "bootstrap", domain, str(path)]) != 0:
57
+ raise DevError("launchctl didn't load the nightly run.", f"Load it yourself: launchctl bootstrap {domain} "
58
+ f"{path}")
59
+ return path
60
+
61
+
62
+ def turn_off(root, *, home=None, run=None, uid=None):
63
+ path = plist_path(root, home)
64
+ if not path.is_file():
65
+ return False
66
+ (run or _launchctl)(["launchctl", "bootout", f"gui/{os.getuid() if uid is None else uid}", str(path)])
67
+ path.unlink()
68
+ return True
69
+
70
+
71
+ def _launchctl(argv):
72
+ try:
73
+ return subprocess.run(argv, capture_output=True, timeout=30).returncode
74
+ except (OSError, subprocess.SubprocessError):
75
+ return 1
76
+
77
+
78
+ def status(root, *, home=None):
79
+ """{"on", "at", "flags", "last"}: the schedule and last night's latest.json."""
80
+ path = plist_path(root, home)
81
+ out = {"on": path.is_file(), "at": None, "flags": [], "last": None}
82
+ if path.is_file():
83
+ try:
84
+ data = plistlib.loads(path.read_bytes())
85
+ when = data.get("StartCalendarInterval") or {}
86
+ out["at"] = f"{int(when.get('Hour', 0)):02d}:{int(when.get('Minute', 0)):02d}"
87
+ arguments = data.get("ProgramArguments") or []
88
+ out["flags"] = arguments[arguments.index("run") + 1:] if "run" in arguments else []
89
+ except (ValueError, plistlib.InvalidFileException):
90
+ pass
91
+ try:
92
+ out["last"] = json.loads((Path(root) / FOLDER / "latest.json").read_text())
93
+ except (OSError, ValueError):
94
+ pass
95
+ return out
96
+
97
+
98
+ def morning(doc, folder, date):
99
+ """morning.md: the verdict first, then each check that didn't pass with why and the command to run it again."""
100
+ from ..testkit.reports import counts_text
101
+ from ..verify.report import dots
102
+ summary = doc.get("summary") or {}
103
+ lines = [f"# Overnight checks, {date}", "", f"**{counts_text(summary)}**" + (
104
+ f" in {round((doc.get('durationMs') or 0) / 60000)} min" if doc.get("durationMs") else ""), ""]
105
+ bad = [(check, result) for check in doc.get("checks") or () for result in check.get("results") or ()
106
+ if result.get("status") != "passed" or result.get("flaky")]
107
+ for check, result in bad:
108
+ word = "flaky" if result.get("status") == "passed" else str(result.get("status")).replace("_", " ")
109
+ reason = (result.get("reason") or result.get("flakyReason") or {}).get("message") or ""
110
+ lines.append(f"- **{word}**: {check.get('name')} on {dots(result.get('device') or '')}"
111
+ + (f". {reason}" if reason else ""))
112
+ if check.get("file"):
113
+ lines.append(f" Run it again: `mobster test {check['file']}`")
114
+ if not bad:
115
+ lines.append("Every check passed.")
116
+ lines += ["", f"Report: {folder}/index.html"]
117
+ return "\n".join(lines) + "\n"
118
+
119
+
120
+ def run_night(root, flags, *, runner=None, notifier=None, alert=None, today=None, environ=None):
121
+ """One night's run. Returns (exit code, morning.md's text)."""
122
+ environ = os.environ if environ is None else environ
123
+ root = Path(root).resolve()
124
+ date = (today or datetime.date.today()).isoformat()
125
+ folder = root / FOLDER / date
126
+ folder.mkdir(parents=True, exist_ok=True)
127
+ argv, env = self_argv()
128
+ argv = argv + ["test", "--video", "--json", "--html", str(folder), "--junit", str(folder / "junit.xml"), *flags]
129
+ if runner is None:
130
+ def runner(argv, cwd, env):
131
+ done = subprocess.run(argv, cwd=cwd, stdout=subprocess.PIPE, text=True, env=env)
132
+ return done.returncode, done.stdout
133
+ code, out = runner(argv, str(root), {**environ, **env})
134
+ try:
135
+ doc = json.loads(out.strip().splitlines()[-1])
136
+ except (ValueError, IndexError):
137
+ doc = {"summary": {}, "checks": [], "exitCode": code}
138
+ text = morning(doc, str(folder.relative_to(root)), date)
139
+ (folder / "morning.md").write_text(text, encoding="utf-8")
140
+ from ..testkit.reports import counts_text
141
+ headline = counts_text(doc.get("summary") or {}) or "couldn't run"
142
+ (root / FOLDER / "latest.json").write_text(json.dumps({"date": date, "exitCode": code, "headline": headline,
143
+ "morning": str(folder / "morning.md")}, indent=1))
144
+ from .xcode import notify
145
+ (notifier or notify)(f"Overnight checks: {root.name}", headline)
146
+ if code != 0:
147
+ from .. import alerts
148
+ url = alerts.configured_url(environ)
149
+ if url:
150
+ (alert or alerts.deliver)(url, alerts.payload(f"Overnight checks: {root.name}",
151
+ "failed" if code == 1 else "error"))
152
+ return code, text
@@ -0,0 +1,203 @@
1
+ """App Store screenshots from your real flows, as full-resolution PNGs in the folders `fastlane deliver` reads."""
2
+
3
+ import json
4
+ import plistlib
5
+ import re
6
+ import subprocess
7
+ from dataclasses import dataclass, replace
8
+ from pathlib import Path
9
+
10
+ from ..sim import simctl
11
+
12
+ # App Store Connect's screenshot sizes (Screenshot specifications, developer.apple.com/help/app-store-connect,
13
+ # read 14 Oct 2026): the display class, the simulators that have it (newest first), and the portrait pixel sizes
14
+ # Apple accepts for it. 6.9" is required for an iPhone app; 13" for an app that runs on iPad.
15
+ SIZES = {
16
+ "6.9": ("iPhone 6.9-inch", ("iPhone 17 Pro Max", "iPhone 16 Pro Max", "iPhone 16 Plus"),
17
+ ((1320, 2868), (1290, 2796), (1260, 2736))),
18
+ "6.5": ("iPhone 6.5-inch", ("iPhone 14 Plus", "iPhone 11 Pro Max"), ((1284, 2778), (1242, 2688))),
19
+ "6.3": ("iPhone 6.3-inch", ("iPhone 17 Pro", "iPhone 17", "iPhone 16 Pro", "iPhone 16"),
20
+ ((1206, 2622), (1179, 2556))),
21
+ "13": ("iPad 13-inch", ("iPad Pro 13-inch (M5)", "iPad Pro 13-inch (M4)", "iPad Air 13-inch (M3)"),
22
+ ((2064, 2752), (2048, 2732))),
23
+ }
24
+ # The App Store's locale codes for an .lproj with a language only.
25
+ LOCALES = {"en": "en-US", "de": "de-DE", "es": "es-ES", "fr": "fr-FR", "nl": "nl-NL", "pt": "pt-PT", "ar": "ar-SA",
26
+ "nb": "no", "zh-Hans": "zh-Hans", "zh-Hant": "zh-Hant", "zh_CN": "zh-Hans", "zh_TW": "zh-Hant"}
27
+ STATUS_BAR = ("--time", "9:41", "--dataNetwork", "wifi", "--wifiMode", "active", "--wifiBars", "3",
28
+ "--cellularMode", "active", "--cellularBars", "4", "--batteryState", "charged", "--batteryLevel", "100")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Shot:
33
+ index: int
34
+ check: object # verify.checks.Check
35
+ size: str
36
+ device: str
37
+ locale: str
38
+
39
+ @property
40
+ def name(self):
41
+ slug = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", self.check.name.lower())).strip("-")[:40] or "shot"
42
+ return f"{self.index:02d}_{self.size.replace('.', '')}_{slug}.png"
43
+
44
+
45
+ def app_locales(app_path):
46
+ """The App Store locales a built .app ships: its .lproj folders (Base left out), else CFBundleLocalizations."""
47
+ found = []
48
+ folder = Path(app_path or "")
49
+ if folder.is_dir():
50
+ found = sorted(p.stem for p in folder.glob("*.lproj") if p.stem not in ("Base",))
51
+ if not found:
52
+ try:
53
+ with open(folder / "Info.plist", "rb") as handle:
54
+ found = list(plistlib.load(handle).get("CFBundleLocalizations") or [])
55
+ except (OSError, ValueError, plistlib.InvalidFileException):
56
+ found = []
57
+ return [asc_locale(item) for item in found] or ["en-US"]
58
+
59
+
60
+ def asc_locale(code):
61
+ code = str(code).replace("_", "-")
62
+ if code in LOCALES:
63
+ return LOCALES[code]
64
+ return code
65
+
66
+
67
+ def launch_args(locale):
68
+ """``-AppleLanguages (de-DE) -AppleLocale de_DE``: the app runs in that language, the rest of iOS unchanged."""
69
+ language = locale if locale != "no" else "nb"
70
+ region = locale.replace("-", "_") if "-" in locale and not locale.startswith("zh-") else language
71
+ return ("-AppleLanguages", f"({language})", "-AppleLocale", region)
72
+
73
+
74
+ def is_ipad_app(app_path):
75
+ try:
76
+ with open(Path(app_path) / "Info.plist", "rb") as handle:
77
+ family = plistlib.load(handle).get("UIDeviceFamily") or []
78
+ except (OSError, ValueError, plistlib.InvalidFileException, TypeError):
79
+ return False
80
+ return 2 in family
81
+
82
+
83
+ def available_device_types(run=None):
84
+ """Names of the simulator device types this Mac's Xcode has."""
85
+ run = run or (lambda argv: subprocess.run(argv, capture_output=True, text=True, timeout=60).stdout)
86
+ try:
87
+ data = json.loads(run(["xcrun", "simctl", "list", "devicetypes", "-j"]))
88
+ except (ValueError, OSError, subprocess.SubprocessError):
89
+ return []
90
+ return [item.get("name") for item in data.get("devicetypes") or () if item.get("name")]
91
+
92
+
93
+ def device_for(size, available):
94
+ """The first simulator of ``size`` this Mac has, or None."""
95
+ for name in SIZES[size][1]:
96
+ if name in available:
97
+ return name
98
+ return None
99
+
100
+
101
+ def plan_shots(checks, *, sizes, locales, available):
102
+ """Every (check × size × locale) as Shots, and the notes on sizes this Mac can't make."""
103
+ shots, notes = [], []
104
+ for size in sizes:
105
+ if size not in SIZES:
106
+ raise ValueError(f"{size} isn't a screenshot size Mobster knows: {', '.join(SIZES)}")
107
+ device = device_for(size, available)
108
+ if device is None:
109
+ notes.append(f"No {SIZES[size][0]} simulator on this Mac ({', '.join(SIZES[size][1][:2])}), so no "
110
+ f"{size}\" shots. Add one in Xcode › Settings › Components.")
111
+ continue
112
+ for locale in locales:
113
+ for index, check in enumerate(checks, 1):
114
+ shots.append(Shot(index, check, size, device, locale))
115
+ return shots, notes
116
+
117
+
118
+ class StatusBarAndShot:
119
+ """A simulator manager that sets the status bar to 9:41 on each lease and shoots the screen as it goes back."""
120
+
121
+ def __init__(self, manager, path, *, run=simctl.run):
122
+ self._manager, self.path = manager, Path(path)
123
+ self._run = run
124
+ self.taken = False
125
+
126
+ def __getattr__(self, name):
127
+ return getattr(self._manager, name)
128
+
129
+ def acquire(self, *args, **kwargs):
130
+ lease = self._manager.acquire(*args, **kwargs)
131
+ self._run(["xcrun", "simctl", "status_bar", lease.target.udid, "override", *STATUS_BAR])
132
+ return _ShotLease(self, lease)
133
+
134
+
135
+ class _ShotLease:
136
+ def __init__(self, owner, lease):
137
+ self._owner, self._lease, self.target = owner, lease, lease.target
138
+
139
+ def __getattr__(self, name):
140
+ return getattr(self._lease, name)
141
+
142
+ def __enter__(self):
143
+ return self.target
144
+
145
+ def __exit__(self, *exc):
146
+ self.release()
147
+
148
+ def release(self):
149
+ owner, udid = self._owner, self.target.udid
150
+ # the shot is taken as the lease goes back: the run's verdict is decided by then
151
+ if not owner.taken:
152
+ owner.path.parent.mkdir(parents=True, exist_ok=True)
153
+ owner.taken = simctl.screenshot(udid, owner.path, timeout=60, runner=owner._run).ok
154
+ owner._run(["xcrun", "simctl", "status_bar", udid, "clear"])
155
+ self._lease.release()
156
+
157
+
158
+ def png_size(path):
159
+ data = Path(path).read_bytes()[:24]
160
+ if data[:8] != b"\x89PNG\r\n\x1a\n":
161
+ return None
162
+ return int.from_bytes(data[16:20], "big"), int.from_bytes(data[20:24], "big")
163
+
164
+
165
+ def take(shots, *, out, runs_dir, manager=None, verify=None, progress=None):
166
+ """Run every shot. Returns {"shots": [...], "ok": bool} and writes screenshots.json in ``out``."""
167
+ from ..verify.runner import verify as verify_run
168
+ verify = verify or verify_run
169
+ progress = progress or (lambda line: None)
170
+ if manager is None:
171
+ from ..sim import SimulatorManager
172
+ manager = SimulatorManager(progress=progress)
173
+ out = Path(out)
174
+ rows = []
175
+ for number, shot in enumerate(shots, 1):
176
+ target = out / shot.locale / shot.name
177
+ target.unlink(missing_ok=True)
178
+ check = replace(shot.check, device=shot.device, runtime=None,
179
+ launch_args=tuple(shot.check.launch_args) + launch_args(shot.locale))
180
+ progress(f"[{number}/{len(shots)}] {shot.check.name}: {shot.device}, {shot.locale}")
181
+ wrapped = StatusBarAndShot(manager, target)
182
+ try:
183
+ result = verify(check, runs_dir=runs_dir, manager=wrapped)
184
+ except KeyboardInterrupt:
185
+ raise
186
+ except Exception as error: # one shot's failure never stops the rest
187
+ result = {"verdict": "couldnt_run", "summary": f"{type(error).__name__}: {error}"}
188
+ verdict = result.get("verdict") or "couldnt_run"
189
+ if verdict == "couldnt_run" and target.is_file():
190
+ target.unlink()
191
+ size = png_size(target) if target.is_file() else None
192
+ accepted = SIZES[shot.size][2]
193
+ row = {"check": shot.check.name, "size": shot.size, "device": shot.device, "locale": shot.locale,
194
+ "verdict": verdict, "file": str(target.relative_to(out)) if target.is_file() else None,
195
+ "pixels": list(size) if size else None,
196
+ "fits": bool(size and (tuple(size) in accepted or (size[1], size[0]) in accepted)),
197
+ "summary": result.get("summary"), "run": result.get("run_id")}
198
+ rows.append(row)
199
+ doc = {"schema": "mobster.screenshots/1", "shots": rows,
200
+ "ok": bool(rows) and all(row["file"] and row["fits"] for row in rows)}
201
+ out.mkdir(parents=True, exist_ok=True)
202
+ (out / "screenshots.json").write_text(json.dumps(doc, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
203
+ return doc
@@ -0,0 +1,155 @@
1
+ """TestFlight builds for a project: list them, choose one, install it on an iPhone, and record the install."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from .. import env_file
10
+ from ..verify.runs import ensure_gitignore
11
+ from . import DevError, asc
12
+
13
+ RECORD_DIR = ("runs", "testflight")
14
+ READY = frozenset({"IN_BETA_TESTING", "READY_FOR_BETA_TESTING"})
15
+ WHY = {"PROCESSING": "Apple is still processing it", "PROCESSING_EXCEPTION": "Apple couldn't process it",
16
+ "MISSING_EXPORT_COMPLIANCE": "it needs export compliance answered in App Store Connect",
17
+ "IN_EXPORT_COMPLIANCE_REVIEW": "it is in export compliance review",
18
+ "WAITING_FOR_BETA_REVIEW": "it is waiting for beta review", "IN_BETA_REVIEW": "it is in beta review",
19
+ "BETA_REJECTED": "beta review rejected it", "EXPIRED": "it expired"}
20
+ BUNDLE = re.compile(r"[A-Za-z0-9][A-Za-z0-9.\-]{0,200}")
21
+
22
+
23
+ def check_row(row):
24
+ """``row`` (asc.build_row) with ``installable`` and, when it isn't, ``why`` in a sentence."""
25
+ states = [state for state in (row.get("internal"), row.get("external")) if state]
26
+ if row.get("expired") or "EXPIRED" in states:
27
+ why = "it expired (TestFlight builds last 90 days)"
28
+ elif row.get("processing") != "VALID":
29
+ why = WHY.get(row.get("processing")) or "Apple hasn't finished processing it"
30
+ elif not any(state in READY for state in states):
31
+ why = next((WHY[state] for state in states if state in WHY), "no tester group has it")
32
+ else:
33
+ return {**row, "installable": True, "why": ""}
34
+ return {**row, "installable": False, "why": why}
35
+
36
+
37
+ def builds(client, bundle, *, limit=20):
38
+ """(app, rows): the app and its builds, newest first."""
39
+ app = client.app(bundle)
40
+ return app, [check_row(row) for row in client.builds(app["id"], limit=limit)]
41
+
42
+
43
+ def choose(rows, build=None, version=None):
44
+ if build is None and version is None:
45
+ for row in rows:
46
+ if row["installable"]:
47
+ return row
48
+ raise DevError("No build of this app can be installed from TestFlight right now."
49
+ + (f" The newest, {_name(rows[0])}, {rows[0]['why']}." if rows else " It has no builds."),
50
+ "Upload a build, or add it to a tester group in App Store Connect." if not rows else
51
+ "Fix that in App Store Connect, or upload a new build.")
52
+ found = [row for row in rows if (build is None or str(row["build"]) == str(build))
53
+ and (version is None or row["version"] == version)]
54
+ if not found:
55
+ raise DevError(f"TestFlight has no build {version + ' ' if version else ''}{build or ''}".strip()
56
+ + f" in the newest {len(rows)} builds.", "Run `mobster testflight list` to see them.")
57
+ row = found[0]
58
+ if not row["installable"]:
59
+ raise DevError(f"Build {_name(row)} can't be installed: {row['why']}.",
60
+ "Pick another build, or fix it in App Store Connect.")
61
+ return row
62
+
63
+
64
+ def _name(row):
65
+ return f"{row['version'] or '?'} ({row['build']})"
66
+
67
+
68
+ # -- the record ----------------------------------------------------------------------------------------------------
69
+
70
+ def _record_path(root, bundle):
71
+ if not BUNDLE.fullmatch(bundle or ""):
72
+ raise DevError(f"{bundle!r} isn't a bundle ID.", "Pass the app's bundle ID, such as com.example.habits.")
73
+ return Path(root) / ".mobster" / Path(*RECORD_DIR) / f"{bundle}.json"
74
+
75
+
76
+ def read_record(root, bundle):
77
+ """The record of the last install of ``bundle`` in this project, or None."""
78
+ try:
79
+ data = json.loads(_record_path(root, bundle).read_text())
80
+ except FileNotFoundError:
81
+ return None
82
+ except (OSError, ValueError):
83
+ raise DevError("The previous TestFlight install record cannot be read.",
84
+ "Check the iPhone and reconcile the record before installing again.") from None
85
+ if not isinstance(data, dict) or data.get("status") not in {"installing", "installed"}:
86
+ raise DevError("The previous TestFlight install record is invalid.",
87
+ "Check the iPhone and reconcile the record before installing again.")
88
+ return data
89
+
90
+
91
+ def _write_record(root, bundle, data):
92
+ path = _record_path(root, bundle)
93
+ path.parent.mkdir(parents=True, exist_ok=True)
94
+ ensure_gitignore(path.parent.parent)
95
+ temp = path.with_suffix(".tmp")
96
+ with env_file.open_private(temp, "w") as handle:
97
+ handle.write(json.dumps(data, sort_keys=True))
98
+ handle.flush()
99
+ os.fsync(handle.fileno())
100
+ os.replace(temp, path)
101
+ descriptor = os.open(path.parent, os.O_RDONLY)
102
+ try:
103
+ os.fsync(descriptor)
104
+ finally:
105
+ os.close(descriptor)
106
+
107
+
108
+ def recorded_build(root, bundle, *, devices=()):
109
+ try:
110
+ data = read_record(root, bundle)
111
+ except DevError:
112
+ return None # metadata cannot prevent the suite from writing its actual verdict
113
+ # an install whose outcome is unknown names no build: it may not be on the phone
114
+ if (not data or data.get("status") != "installed"
115
+ or (devices and not set(devices).intersection(data.get("devices") or ()))):
116
+ return None
117
+ return {key: data.get(key) for key in ("bundleId", "version", "build", "installedAt")}
118
+
119
+
120
+ # -- the install ---------------------------------------------------------------------------------------------------
121
+
122
+ def install(client, record, wda, bundle, *, root, build=None, version=None, progress=None, now=None, **options):
123
+ with env_file.writer(_record_path(root, bundle)):
124
+ app, rows = builds(client, bundle, limit=50)
125
+ row = choose(rows, build, version)
126
+ newest = next(item for item in rows if item["installable"])
127
+ if row["id"] != newest["id"]:
128
+ raise DevError(
129
+ f"TestFlight installs the newest build, {_name(newest)}, from the app's page, and you asked for "
130
+ f"{_name(row)}. Mobster won't pick from Previous Builds for you.",
131
+ f"On the iPhone, open TestFlight, tap {app['name']}, tap Previous Builds, tap {_name(row)}, then tap "
132
+ f"Install. Then run `mobster test --device NAME --testflight {bundle}`.")
133
+ earlier = read_record(root, bundle)
134
+ unknown = bool(earlier) and earlier.get("status") == "installing"
135
+ if unknown and earlier.get("buildId") != row["id"]:
136
+ raise DevError("An earlier TestFlight install has an unknown outcome.",
137
+ "Check that build on the iPhone before requesting a different build.")
138
+ stamp = (now or (lambda: datetime.now(timezone.utc)))().strftime("%Y-%m-%dT%H:%M:%SZ")
139
+ entry = {"bundleId": bundle, "version": row["version"], "build": row["build"], "buildId": row["id"],
140
+ "startedAt": stamp, "devices": [value for value in (record.get("id"), record.get("name")) if value]}
141
+ _write_record(root, bundle, {**entry, "status": "installing"}) # before the first tap, so a crash is "unknown"
142
+ try:
143
+ result = asc.testflight_install(record, wda, app["id"], progress=progress, tap=not unknown,
144
+ expected=(row["version"], row["build"]), **options)
145
+ except asc.UnknownOutcome:
146
+ raise # the record stays "installing"
147
+ except DevError: # no tap went out (terms, no button, a simulator, or watch-only): the old record stands
148
+ if earlier:
149
+ _write_record(root, bundle, earlier)
150
+ else:
151
+ _record_path(root, bundle).unlink(missing_ok=True)
152
+ raise
153
+ finished = {**entry, "status": "installed", "installedAt": stamp}
154
+ _write_record(root, bundle, finished)
155
+ return {"result": result, "row": row, "app": app, "record": finished}
@@ -0,0 +1,127 @@
1
+ """`mobster testflight`: your app's TestFlight builds, listed and installed on an iPhone."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from . import DevError, main, say, subcommands
8
+
9
+
10
+ def add_arguments(parser, helpers):
11
+ add = subcommands(parser, "testflight_command")
12
+
13
+ key = add("key", help="add, show or remove this project's App Store Connect API key (kept in the Keychain)",
14
+ description="Keep an App Store Connect API key for this project in your login Keychain, readable only "
15
+ "by Mobster's own programs, and never printed. Make one in App Store Connect › Users and "
16
+ "Access › Integrations. The key is this project's: a folder with .mobster/ above the "
17
+ "current one, or the current folder. --shared keeps it for every project instead.")
18
+ key.add_argument("action", choices=("add", "show", "remove"), help="add, show or remove")
19
+ key.add_argument("--key-id", metavar="ID", help="with add: the key's Key ID")
20
+ key.add_argument("--issuer-id", metavar="UUID", help="with add: the Issuer ID (a team key; none for an "
21
+ "individual key)")
22
+ key.add_argument("--p8", metavar="FILE", help="with add: the AuthKey_<Key ID>.p8 file Apple gave you")
23
+ key.add_argument("--shared", action="store_true", help="the key every project uses, not this project's")
24
+
25
+ listing = add("list", help="list the app's builds and whether TestFlight can install each, newest first")
26
+ listing.add_argument("--app", required=True, metavar="BUNDLE", help="the app's bundle ID")
27
+ listing.add_argument("--limit", type=int, default=10, metavar="N", help="how many (default: 10)")
28
+ listing.add_argument("--json", action="store_true", help="print JSON")
29
+
30
+ install = add("install", help="install a TestFlight build on your iPhone through the TestFlight app",
31
+ description="Install the newest installable build, or the one --build names, on your iPhone: "
32
+ "Mobster opens the app's page in TestFlight and taps Install or Update, then waits "
33
+ "for Open. You must be a tester of the app. Mobster never accepts TestFlight's terms "
34
+ "for you, never picks from Previous Builds (it prints the taps instead), and after a "
35
+ "tap it can't confirm it never taps again.",
36
+ epilog="after it: mobster test --device NAME --testflight BUNDLE, whose report names the build")
37
+ install.add_argument("--app", required=True, metavar="BUNDLE", help="the app's bundle ID")
38
+ install.add_argument("--build", metavar="N", help="the build number (default: the newest installable)")
39
+ install.add_argument("--version", metavar="V", help="the version the build belongs to, such as 1.4.2")
40
+ install.add_argument("--device", metavar="NAME", help="the iPhone (default: your iPhone)")
41
+ install.add_argument("--timeout", type=int, default=600, metavar="SECONDS",
42
+ help="how long to wait for TestFlight (default: 600)")
43
+ install.add_argument("--json", action="store_true", help="print JSON")
44
+
45
+
46
+ def run(args, *, client=None, store=None, devices=None, wda=None, cwd=None):
47
+ handlers = {"key": _key, "list": _list, "install": _install}
48
+ return main("testflight", handlers, args, client=client, store=store, devices=devices, wda=wda,
49
+ cwd=Path(cwd or Path.cwd()))
50
+
51
+
52
+ def project_of(cwd):
53
+ from ..verify.checks import project_root
54
+ return Path(project_root(Path(cwd))).resolve()
55
+
56
+
57
+ def _client(client, store, root):
58
+ if client is not None:
59
+ return client
60
+ from . import asc
61
+ return asc.Client(asc.load_key(store=store, project=root))
62
+
63
+
64
+ def _key(args, *, store, cwd, **_):
65
+ from . import asc
66
+ root = None if args.shared else project_of(cwd)
67
+ if args.action == "add":
68
+ if not args.key_id or not args.p8:
69
+ raise DevError("key add needs --key-id and --p8 (and --issuer-id for a team key).")
70
+ try:
71
+ text = Path(args.p8).expanduser().read_text()
72
+ except OSError:
73
+ raise DevError(f"Mobster can't read {args.p8}.", "Pass the path of the .p8 file.") from None
74
+ key = asc.save_key(args.key_id, args.issuer_id, text, store=store, project=root)
75
+ say(f"Saved the {key.public()['kind']} key {key.key_id} in your Keychain for "
76
+ f"{'every project' if root is None else root.name}. You can delete {Path(args.p8).name} now, or keep "
77
+ "it somewhere safe.")
78
+ elif args.action == "remove":
79
+ asc.remove_key(store=store, project=root)
80
+ say("Removed the key from your Keychain.")
81
+ else:
82
+ info = asc.load_key(store=store, project=root).public()
83
+ say(f"Key ID {info['keyId']}\nIssuer ID {info['issuerId'] or 'none (an individual key)'}\n"
84
+ f"From {info['source']}")
85
+ return 0
86
+
87
+
88
+ def _list(args, *, client, store, cwd, **_):
89
+ from . import testflight
90
+ app, rows = testflight.builds(_client(client, store, project_of(cwd)), args.app,
91
+ limit=max(1, min(args.limit, 200)))
92
+ if args.json:
93
+ say(json.dumps({"app": app, "builds": rows}, ensure_ascii=False))
94
+ return 0
95
+ lines = [f"{app['name']} ({args.app}), newest first:"]
96
+ for row in rows:
97
+ when = str(row["uploaded"] or "")[:16].replace("T", " ")
98
+ lines.append(f" {row['version'] or '?'} ({row['build']}) {when} "
99
+ + ("can install" if row["installable"] else f"can't install: {row['why']}"))
100
+ if not rows:
101
+ lines.append(" no builds yet")
102
+ say("\n".join(lines))
103
+ return 0
104
+
105
+
106
+ def _install(args, *, client, store, devices, wda, cwd, **_):
107
+ from . import testflight
108
+ if devices is None or wda is None:
109
+ from ..phone_io import choose_device
110
+ from ..phone_io import wda as phone_wda
111
+ devices, wda = devices or choose_device, wda or phone_wda
112
+ root = project_of(cwd)
113
+ record = devices(args.device)
114
+ done = testflight.install(_client(client, store, root), record, wda, args.app, root=root, build=args.build,
115
+ version=args.version, timeout=max(30, min(args.timeout, 3600)),
116
+ progress=lambda line: say(line, sys.stderr))
117
+ row = done["row"]
118
+ name = f"{row['version'] or '?'} ({row['build']})"
119
+ if args.json:
120
+ say(json.dumps({"device": record.get("name"), "app": args.app, "result": done["result"],
121
+ "build": {"version": row["version"], "build": row["build"]}}))
122
+ elif done["result"] == "already":
123
+ say(f"{args.app} {name} is already on {record.get('name')}: TestFlight says Open.")
124
+ else:
125
+ say(f"Installed {args.app} {name} from TestFlight on {record.get('name')}. Test it with: "
126
+ f"mobster test --device \"{record.get('name')}\" --testflight {args.app}")
127
+ return 0