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,802 @@
1
+ """Maestro flows as checks; the YAML is read with safe_load only and never reaches a model as free text."""
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from ..verify.checks import LIMITS, CheckError, check_from_dict, check_to_dict
9
+
10
+ FILE_LIMIT = 1_000_000
11
+ MAX_DEPTH = 8
12
+ TEXT_LIMIT = 120 # a quoted value in a step
13
+ INPUT_LIMIT = 300 # typed text
14
+ MAX_FLOWS = 500
15
+ REGEX_CHARS = set("*+?[](){}|^$\\")
16
+ ENV_REF = re.compile(r"(?<!\\)\$\{([^}]*)\}")
17
+ ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
18
+ PERCENT_POINT = re.compile(r"^\s*(\d{1,3}(?:\.\d+)?)%\s*,\s*(\d{1,3}(?:\.\d+)?)%\s*$")
19
+ ABS_POINT = re.compile(r"^\s*(\d{1,5}(?:\.\d+)?)\s*,\s*(\d{1,5}(?:\.\d+)?)\s*$")
20
+ HEADER_KEYS = ("appId", "name", "tags", "env", "onFlowStart", "onFlowComplete", "properties", "jsEngine")
21
+ DIRECTIONS = ("UP", "DOWN", "LEFT", "RIGHT")
22
+ # Options that only tune timing or retries: Mobster waits for the screen itself and verifies each action.
23
+ TIMING = ("retryTapIfNoChange", "waitToSettleTimeoutMs", "duration", "speed", "timeout", "label",
24
+ "visibilityPercentage", "centerElement", "delay", "waitUntilVisible")
25
+ RELATIVE = {"below": "below", "above": "above", "leftOf": "to the left of", "rightOf": "to the right of",
26
+ "childOf": "inside", "containsChild": "that contains", "containsDescendants": "that contains"}
27
+ KEYS = {"home": "Press the Home button", "enter": "Press the Return key on the keyboard",
28
+ "backspace": "Press the delete key on the keyboard"}
29
+
30
+ # Commands Mobster doesn't run: (why, what to do instead). An unsupported assertion refuses the whole flow.
31
+ UNSUPPORTED = {
32
+ "runScript": ("runs JavaScript, which Mobster doesn't run",
33
+ "move the logic into the app or the launch environment, "
34
+ "or write that part as a Mobster check by hand"),
35
+ "evalScript": ("runs JavaScript, which Mobster doesn't run", "set the value in the flow's env instead"),
36
+ "assertTrue": ("judges a JavaScript condition, which Mobster doesn't run",
37
+ "write the condition as assertVisible or assertNotVisible on what the screen shows"),
38
+ "assertWithAI": ("asks a model to judge the screen, and Mobster's verdicts never come from a model",
39
+ "write the assertion as assertVisible or assertNotVisible"),
40
+ "assertNoDefectsWithAI": ("asks a model to judge the screen, and Mobster's verdicts never come from a model",
41
+ "write the assertion as assertVisible or assertNotVisible"),
42
+ "assertScreenshot": ("compares pixels, and Mobster checks the accessibility tree",
43
+ "assert the text or element the screenshot proves with assertVisible"),
44
+ "repeat": ("loops, and a check is a fixed list of steps", "write the loop out as steps, or use scrollUntilVisible"),
45
+ "retry": ("retries commands, and a check is a fixed list of steps",
46
+ "remove the retry; `mobster test --retries N` retries a whole check"),
47
+ "takeScreenshot": ("Mobster records a frame for every step of every run", "read the frames in the report"),
48
+ "startRecording": ("Mobster records video itself", "run `mobster test --video`"),
49
+ "stopRecording": ("Mobster records video itself", "run `mobster test --video`"),
50
+ "setLocation": ("changing the simulated location isn't a check step",
51
+ "set it before the run with `xcrun simctl location`"),
52
+ "setPermissions": ("granting permissions isn't a check step",
53
+ "grant them before the run with `xcrun simctl privacy`"),
54
+ "travel": ("moving the simulated location isn't a check step",
55
+ "set it before the run with `xcrun simctl location`"),
56
+ "setAirplaneMode": ("changing network state isn't a check step", "set it on the device before the run"),
57
+ "toggleAirplaneMode": ("changing network state isn't a check step", "set it on the device before the run"),
58
+ "setOrientation": ("rotating the device isn't a check step", "rotate the device before the run"),
59
+ "setClipboard": ("the clipboard isn't a check step", "run `mobster phone clipboard set`"),
60
+ "copyTextFrom": ("copies screen text into a variable, and a check has no variables",
61
+ "assert the text with assertVisible"),
62
+ "pasteText": ("pastes a variable, and a check has no variables", "type the text with inputText"),
63
+ "addMedia": ("adding photos isn't a check step", "add them before the run with `xcrun simctl addmedia`"),
64
+ "clearKeychain": ("clearing the keychain isn't a check step", "run `mobster sim erase` before the run"),
65
+ "clearState": ("clearing app data mid-flow isn't a check step",
66
+ "put `clearState: true` on the first launchApp, or set `reset: data` in the check"),
67
+ "stopApp": ("stopping the app mid-flow isn't a check step", "use launchApp to start it again"),
68
+ "killApp": ("killing the app mid-flow isn't a check step", "use launchApp to start it again"),
69
+ "openBrowser": ("Android only", "use openLink"),
70
+ "inputRandomText": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
71
+ "inputRandomNumber": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
72
+ "inputRandomEmail": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
73
+ "inputRandomPersonName": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
74
+ "defineVariables": ("sets variables, and a check has none", "put the values in the flow's env"),
75
+ "pauseTest": ("pauses for a person", "remove it"),
76
+ "setDarkMode": ("changing appearance isn't a check step", "set it before the run with `xcrun simctl ui`"),
77
+ }
78
+ ASSERTIONS = ("assertTrue", "assertWithAI", "assertNoDefectsWithAI", "assertScreenshot")
79
+
80
+
81
+ class MaestroError(Exception):
82
+ """The file can't be read as a flow at all."""
83
+
84
+
85
+ @dataclass
86
+ class Problem:
87
+ where: str # "command 4 (runScript)", with the file for a subflow
88
+ command: str
89
+ why: str
90
+ instead: str = ""
91
+ assertion: bool = False # an assertion is never dropped
92
+
93
+
94
+ @dataclass
95
+ class FlowResult:
96
+ source: str # path shown to the user, relative to the import root
97
+ outcome: str = "imported" # imported | refused | skipped
98
+ checks: list = field(default_factory=list) # of (file stem, check dict)
99
+ problems: list = field(default_factory=list) # of Problem: why a flow was refused
100
+ placeholders: list = field(default_factory=list) # of Problem: became a marked step for the first run
101
+ same: list = field(default_factory=list) # of file stems: already on disk exactly as imported
102
+ converted: int = 0 # commands that became steps or expectations as written
103
+ dropped: list = field(default_factory=list) # of Problem: dropped on request
104
+ notes: list = field(default_factory=list) # of str: kept, with a caveat
105
+ reason: str = "" # for skipped, or a flow that can't be read
106
+
107
+ def to_dict(self):
108
+ return {"source": self.source, "outcome": self.outcome, "reason": self.reason or None,
109
+ "checks": [{"file": f"{stem}.yaml", "steps": len(data.get("steps") or ()),
110
+ "expect": len(data["expect"])} for stem, data in self.checks],
111
+ "refused": [vars(p) for p in self.problems] if self.outcome == "refused" else [],
112
+ "converted": self.converted, "needsFirstRun": [vars(p) for p in self.placeholders],
113
+ "dropped": [vars(p) for p in self.dropped], "notes": list(self.notes)}
114
+
115
+
116
+ class _Ctx:
117
+ def __init__(self, root, skip_unsupported):
118
+ self.root, self.skip = Path(root), skip_unsupported
119
+ self.problems, self.dropped, self.notes, self.placeholders = [], [], [], []
120
+ self.converted = 0
121
+ self.ops = [] # ("act", text) | ("assert", dict, name) | ("launch", {...}) | ("url", url)
122
+ self.launch = {}
123
+ self.loaded = None
124
+
125
+ def problem(self, where, command, why, instead="", assertion=False):
126
+ assertion = assertion or command.startswith("assert") or command == "extendedWaitUntil"
127
+ item = Problem(where, command, why, instead, assertion)
128
+ if self.skip and not assertion:
129
+ self.dropped.append(item)
130
+ else:
131
+ self.problems.append(item)
132
+
133
+ def unsupported(self, where, command, why, instead, assertion=False):
134
+ """A command Mobster can't run: refused when it is an assertion, dropped on request, else a marked step."""
135
+ if assertion or self.skip:
136
+ return self.problem(where, command, why, instead, assertion)
137
+ self.placeholders.append(Problem(where, command, why, instead))
138
+ name = command if ENV_NAME.fullmatch(command) and len(command) <= 40 else "unknown"
139
+ self.ops.append(("act", f"TODO (Maestro {name}, not imported): {why}. Look at the screen and do what this "
140
+ "point in the flow needs, or nothing if it needs nothing"))
141
+
142
+ def started(self):
143
+ return any(op[0] in ("act", "assert") for op in self.ops)
144
+
145
+
146
+ def read_flow(path):
147
+ """(config dict, commands list) from a Maestro flow file. Raises MaestroError on a file that isn't one."""
148
+ import yaml
149
+ path = Path(path)
150
+ try:
151
+ if path.stat().st_size > FILE_LIMIT:
152
+ raise MaestroError("larger than 1 MB")
153
+ text = path.read_text(encoding="utf-8")
154
+ except OSError as error:
155
+ raise MaestroError(f"can't be read ({error.strerror or type(error).__name__})") from None
156
+ except UnicodeDecodeError:
157
+ raise MaestroError("isn't UTF-8 text") from None
158
+ try:
159
+ docs = [doc for doc in yaml.safe_load_all(text)]
160
+ except yaml.YAMLError as error:
161
+ mark = getattr(error, "problem_mark", None)
162
+ raise MaestroError(f"line {mark.line + 1}: {getattr(error, 'problem', None) or 'not valid YAML'}"
163
+ if mark is not None else "isn't valid YAML") from None
164
+ except (RecursionError, ValueError):
165
+ raise MaestroError("is nested too deeply") from None
166
+ docs = [doc for doc in docs if doc is not None]
167
+ if len(docs) == 1 and isinstance(docs[0], list):
168
+ return {}, docs[0]
169
+ if len(docs) == 1 and isinstance(docs[0], dict):
170
+ return docs[0], []
171
+ if len(docs) == 2 and isinstance(docs[0], dict) and isinstance(docs[1], list):
172
+ return docs[0], docs[1]
173
+ if not docs:
174
+ raise MaestroError("is empty")
175
+ raise MaestroError("isn't a Maestro flow: expected a config, then `---`, then a list of commands")
176
+
177
+
178
+ def discover(paths):
179
+ """The flow files in ``paths`` as [(file, root)], sorted and de-duplicated."""
180
+ found = {}
181
+ for path in paths:
182
+ path = Path(path).expanduser()
183
+ if path.is_dir():
184
+ root = path.resolve()
185
+ for file in sorted(path.iterdir()):
186
+ if file.suffix.lower() in (".yaml", ".yml") and file.is_file() and not file.is_symlink() \
187
+ and not file.name.startswith("."):
188
+ found.setdefault(file.resolve(), root)
189
+ elif path.is_file():
190
+ found.setdefault(path.resolve(), path.resolve().parent)
191
+ else:
192
+ raise MaestroError(f"{path} doesn't exist")
193
+ return sorted(found.items())[:MAX_FLOWS]
194
+
195
+
196
+ def _substitute(value, env, ctx, where, command):
197
+ """``${NAME}`` from the flow's env. A name that isn't set, and any JavaScript expression, is a problem."""
198
+ if not isinstance(value, str):
199
+ return value
200
+
201
+ def sub(found):
202
+ name = found.group(1).strip()
203
+ if not ENV_NAME.fullmatch(name):
204
+ ctx.problem(where, command, f"uses the JavaScript expression ${{{name[:40]}}}, which Mobster doesn't run",
205
+ "write the value out, or set it in the flow's env")
206
+ failed.append(name)
207
+ return ""
208
+ if name not in env:
209
+ ctx.problem(where, command, f"uses ${{{name}}}, which the flow's env doesn't set",
210
+ f"add {name} to the flow's env, or write the value out")
211
+ failed.append(name)
212
+ return ""
213
+ return str(env[name])
214
+ failed = []
215
+ out = ENV_REF.sub(sub, value).replace("\\${", "${")
216
+ return None if failed else out
217
+
218
+
219
+ def _quote(text, limit=TEXT_LIMIT):
220
+ """A value inside a step: JSON-quoted on one line, so it reads as data."""
221
+ text = str(text)
222
+ if len(text) > limit:
223
+ raise ValueError(f"is longer than {limit} characters")
224
+ return json.dumps(text, ensure_ascii=False)
225
+
226
+
227
+ def _scalar(value):
228
+ if isinstance(value, bool) or value is None:
229
+ return None
230
+ return str(value) if isinstance(value, (str, int, float)) else None
231
+
232
+
233
+ def _regex(text):
234
+ return any(char in REGEX_CHARS for char in text)
235
+
236
+
237
+ def _literal(text):
238
+ """``text`` with every regex character escaped and nothing else touched."""
239
+ return re.sub(r"([\\.^$*+?()\[\]{}|/])", r"\\\1", text)
240
+
241
+
242
+ def _matcher(text, *, fold=True):
243
+ # Ids stay exact (fold=False): that can fail a flow Maestro would pass, never pass one Maestro would fail.
244
+ if _regex(text):
245
+ return f"/^(?:{text})$/i"
246
+ if fold and text.lower() != text.upper():
247
+ return f"/^{_literal(text)}$/i"
248
+ return text
249
+
250
+
251
+ @dataclass
252
+ class Sel:
253
+ text: str = None
254
+ id: str = None
255
+ enabled: bool = None
256
+ selected: bool = None
257
+ index: int = None
258
+ point: str = None
259
+ optional: bool = False
260
+ relative: list = field(default_factory=list) # of (word, Sel)
261
+
262
+ def describe(self):
263
+ """The selector in plain English, every value quoted."""
264
+ if self.point is not None:
265
+ return f"the screen point {self.point}"
266
+ parts = [] if self.text is not None or self.id is not None else ["the element"]
267
+ if self.text is not None:
268
+ parts.append(f"the element whose text matches the pattern {_quote(self.text)}" if _regex(self.text)
269
+ else _quote(self.text))
270
+ if self.id is not None:
271
+ kind = "an accessibility id that matches the pattern" if _regex(self.id) else "accessibility id"
272
+ parts.append(f"{'with' if parts else 'the element with'} {kind} {_quote(self.id)}")
273
+ out = " ".join(parts)
274
+ for state, name in ((self.enabled, "enabled"), (self.selected, "selected")):
275
+ if state is not None:
276
+ out += f" that is {'' if state else 'not '}{name}"
277
+ for word, other in self.relative:
278
+ out += f" {RELATIVE[word]} {other.describe()}"
279
+ if self.index is not None:
280
+ out += f" (match number {self.index + 1}, counting from the top left)"
281
+ return out
282
+
283
+
284
+ def _selector(value, where, command, ctx, env, *, tap=False, nested=False):
285
+ """A Sel from a Maestro selector (a string, or an object), or None after naming the problem."""
286
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
287
+ value = str(value)
288
+ if isinstance(value, str):
289
+ value = {"text": value}
290
+ if not isinstance(value, dict):
291
+ ctx.problem(where, command, "has a selector Mobster can't read", "give it as text, id or point")
292
+ return None
293
+ sel, bad = Sel(), False
294
+ for key, item in value.items():
295
+ if key in TIMING:
296
+ continue
297
+ if key in ("text", "id"):
298
+ if _scalar(item) is None or not _scalar(item).strip():
299
+ ctx.problem(where, command, f"has an empty or non-text {key}", "give the text or id to match")
300
+ bad = True
301
+ else:
302
+ item = _substitute(_scalar(item), env, ctx, where, command)
303
+ if item is None:
304
+ bad = True
305
+ else:
306
+ setattr(sel, key, item)
307
+ elif key in ("enabled", "selected") and isinstance(item, bool):
308
+ setattr(sel, key, item)
309
+ elif key == "index" and isinstance(item, int) and not isinstance(item, bool) and item >= 0 and tap:
310
+ sel.index = item
311
+ elif key == "optional" and isinstance(item, bool):
312
+ sel.optional = item
313
+ elif key == "longPress" and tap and isinstance(item, bool):
314
+ continue
315
+ elif key == "repeat" and tap:
316
+ ctx.problem(where, command, "repeats the tap", "write the taps out as steps")
317
+ bad = True
318
+ elif key == "point" and tap and not nested:
319
+ point = _substitute(_scalar(item), env, ctx, where, command)
320
+ if point is None:
321
+ bad = True
322
+ elif point and (PERCENT_POINT.match(point) or ABS_POINT.match(point)):
323
+ sel.point = point.replace(" ", "")
324
+ else:
325
+ ctx.problem(where, command, f"has the point {str(item)[:30]!r}, which isn't x,y or x%,y%",
326
+ "write it as 50%,90%")
327
+ bad = True
328
+ elif key in RELATIVE and tap and not nested:
329
+ other = _selector(item, where, command, ctx, env, nested=True)
330
+ if other is None:
331
+ bad = True
332
+ elif other.relative or other.index is not None:
333
+ ctx.problem(where, command, f"nests {key} more than one level",
334
+ "describe the element by its own text or id")
335
+ bad = True
336
+ else:
337
+ sel.relative.append((key, other))
338
+ else:
339
+ ctx.problem(where, command, f"uses the selector option {key}, which Mobster can't match",
340
+ "select by text or id" if tap else "assert on text or id; Mobster can't judge this option",
341
+ assertion=not tap)
342
+ bad = True
343
+ if not bad and sel.point is None and sel.text is None and sel.id is None and not (tap and sel.relative):
344
+ ctx.problem(where, command, "has no text, id or point", "give the text or id to match", assertion=not tap)
345
+ bad = True
346
+ if not bad and sel.point is not None and (sel.text or sel.id or sel.relative):
347
+ ctx.problem(where, command, "mixes a point with a text or id", "use one or the other")
348
+ bad = True
349
+ return None if bad else sel
350
+
351
+
352
+ def _expect(sel, present, name, ctx, where, command):
353
+ """The Mobster assertion for a Maestro assertVisible / assertNotVisible selector."""
354
+ if sel.index is not None or sel.point is not None or sel.relative or sel.optional:
355
+ what = "optional" if sel.optional else "a relative position, index or point"
356
+ ctx.problem(where, command, f"asserts with {what}, which an assertion on the accessibility tree can't carry",
357
+ "assert on the element's text or id", assertion=True)
358
+ return None
359
+ selector = {}
360
+ if sel.text is not None:
361
+ selector["label"] = _matcher(sel.text)
362
+ if sel.id is not None:
363
+ selector["id"] = _matcher(sel.id, fold=False)
364
+ if sel.enabled is not None:
365
+ selector["enabled"] = sel.enabled
366
+ if sel.selected is not None:
367
+ selector["selected"] = sel.selected
368
+ verb = "visible" if present else "not visible"
369
+ shown = sel.text if sel.text is not None else f"id {sel.id}"
370
+ label = f"{shown} is {verb}"
371
+ return {"name": label[:120], ("visible" if present else "absent"): selector}
372
+
373
+
374
+ def _commands(commands, env, ctx, base, label, depth, chain):
375
+ if not isinstance(commands, list):
376
+ ctx.problem(label, "flow", "has no list of commands", "write the commands as a YAML list after `---`")
377
+ return
378
+ for number, command in enumerate(commands, 1):
379
+ name, arg = _split(command)
380
+ where = f"{label}command {number} ({name or '?'})"
381
+ if name is None:
382
+ ctx.problem(where, "?", "isn't a command Mobster can read", "write one command per list item")
383
+ continue
384
+ _command(name, arg, env, ctx, base, where, depth, chain)
385
+
386
+
387
+ def _split(command):
388
+ if isinstance(command, str):
389
+ return command, None
390
+ if isinstance(command, dict) and len(command) == 1:
391
+ name, arg = next(iter(command.items()))
392
+ return (name, arg) if isinstance(name, str) else (None, None)
393
+ return None, None
394
+
395
+
396
+ def _act(ctx, text):
397
+ ctx.ops.append(("act", text))
398
+
399
+
400
+ def _command(name, arg, env, ctx, base, where, depth, chain):
401
+ def sub(value):
402
+ return _substitute(value, env, ctx, where, name)
403
+
404
+ before = len(ctx.problems) + len(ctx.placeholders) + len(ctx.dropped)
405
+ try:
406
+ _dispatch(name, arg, env, ctx, base, where, depth, chain, sub)
407
+ except ValueError as error:
408
+ ctx.problem(where, name, f"has a value that {error}", "shorten it, or split the flow")
409
+ if name != "runFlow" and before == len(ctx.problems) + len(ctx.placeholders) + len(ctx.dropped):
410
+ ctx.converted += 1
411
+
412
+
413
+ def _dispatch(name, arg, env, ctx, base, where, depth, chain, sub):
414
+ if name == "launchApp":
415
+ return _launch(arg, env, ctx, where, sub)
416
+ if name in ("tapOn", "doubleTapOn", "longPressOn"):
417
+ sel = _selector(arg, where, name, ctx, env, tap=True)
418
+ if sel is None:
419
+ return
420
+ verb = {"tapOn": "Tap", "doubleTapOn": "Double-tap", "longPressOn": "Press and hold"}[name]
421
+ if isinstance(arg, dict) and (arg.get("longPress") is True):
422
+ verb = "Press and hold"
423
+ target = sel.describe()
424
+ if sel.optional:
425
+ return _act(ctx, f"If {target} is on screen, {verb[0].lower() + verb[1:]} it; otherwise skip this step")
426
+ if sel.point is not None and ABS_POINT.match(sel.point):
427
+ ctx.notes.append(f"{where}: the point {sel.point} is in screen points, so it depends on the device; "
428
+ "write it as 50%,90% to work on any size")
429
+ return _act(ctx, f"{verb} {target}")
430
+ if name in ("assertVisible", "assertNotVisible"):
431
+ sel = _selector(arg, where, name, ctx, env)
432
+ if sel is not None:
433
+ found = _expect(sel, name == "assertVisible", name, ctx, where, name)
434
+ if found is not None:
435
+ ctx.ops.append(("assert", found))
436
+ return
437
+ if name == "extendedWaitUntil":
438
+ return _wait_until(arg, env, ctx, where)
439
+ if name == "inputText":
440
+ if not _scalar(arg):
441
+ return ctx.problem(where, name, "has no text to type", "give the text as a string")
442
+ text = sub(_scalar(arg))
443
+ if text is None:
444
+ return
445
+ return _act(ctx, f"Type {_quote(text, INPUT_LIMIT)} into the focused text field")
446
+ if name == "eraseText":
447
+ count = arg.get("charactersToErase") if isinstance(arg, dict) else arg
448
+ if count is None:
449
+ return _act(ctx, "Erase the text in the focused text field")
450
+ if isinstance(count, int) and not isinstance(count, bool) and 0 < count <= 1000:
451
+ return _act(ctx, f"Delete {count} characters from the focused text field")
452
+ return ctx.problem(where, name, "has a character count Mobster can't read", "give a whole number")
453
+ if name == "hideKeyboard":
454
+ return _act(ctx, "Dismiss the keyboard")
455
+ if name == "scroll":
456
+ return _act(ctx, "Scroll down")
457
+ if name == "scrollUntilVisible":
458
+ return _scroll_until(arg, env, ctx, where)
459
+ if name == "swipe":
460
+ return _swipe(arg, env, ctx, where)
461
+ if name == "back":
462
+ ctx.notes.append(f"{where}: iOS has no back key, so the step taps the Back button on screen or swipes from "
463
+ "the left edge")
464
+ return _act(ctx, "Go back one screen with the Back button on screen, or by swiping from the left edge")
465
+ if name == "pressKey":
466
+ key = str(arg).strip().lower() if _scalar(arg) else ""
467
+ if key in KEYS:
468
+ return _act(ctx, KEYS[key])
469
+ return ctx.problem(where, name, f"presses {str(arg)[:30]!r}, a key iOS doesn't have",
470
+ "use Home, Enter or Backspace")
471
+ if name == "openLink":
472
+ link = sub(_scalar(arg.get("link")) if isinstance(arg, dict) else _scalar(arg))
473
+ if link is None and "${" in str(arg):
474
+ return
475
+ if not link or not re.match(r"[A-Za-z][A-Za-z0-9+.\-]*:", link) or len(link) > LIMITS["url"]:
476
+ return ctx.problem(where, name, "has a link Mobster can't read",
477
+ "write the full link, such as yourapp://home")
478
+ if not ctx.started() and "url" not in ctx.launch:
479
+ ctx.launch["url"] = link
480
+ return
481
+ return _act(ctx, f"Open the link {_quote(link, LIMITS['url'])}")
482
+ if name == "waitForAnimationToEnd":
483
+ return _act(ctx, "Wait for animations on screen to finish")
484
+ if name == "runFlow":
485
+ return _run_flow(arg, env, ctx, base, where, depth, chain)
486
+ if name in UNSUPPORTED:
487
+ why, instead = UNSUPPORTED[name]
488
+ return ctx.unsupported(where, name, why, instead, assertion=name in ASSERTIONS)
489
+ ctx.unsupported(where, name, "isn't a Maestro command Mobster knows",
490
+ "check the spelling, or run this step by hand", assertion=name.startswith("assert"))
491
+
492
+
493
+ def _launch(arg, env, ctx, where, sub):
494
+ options = {} if arg is None else arg
495
+ if not isinstance(options, dict):
496
+ return ctx.problem(where, "launchApp", "has options Mobster can't read", "use clearState or nothing")
497
+ initial = not ctx.started() and not ctx.launch.get("seen")
498
+ app = options.get("appId")
499
+ resolved = sub(_scalar(app)) if app is not None else None
500
+ if app is not None and resolved is None:
501
+ return
502
+ if app is not None and resolved != ctx.launch.get("appId"):
503
+ return ctx.problem(where, "launchApp", "launches a different app", "write one flow per app")
504
+ for key in options:
505
+ if key in ("appId", "stopApp", "clearState"):
506
+ continue
507
+ why = {"arguments": "passes launch arguments, whose iOS form Mobster can't confirm",
508
+ "permissions": "sets permissions, which isn't a check step"}.get(key, f"uses the option {key}")
509
+ ctx.problem(where, "launchApp", why, "set it before the run, or in the check's launch.args by hand")
510
+ clear = options.get("clearState") is True
511
+ if initial:
512
+ ctx.launch["seen"] = True
513
+ ctx.launch["reset"] = "data" if clear else "none"
514
+ return
515
+ if clear:
516
+ return ctx.problem(where, "launchApp", "clears app data in the middle of the flow",
517
+ "put clearState on the first launchApp, or split the flow")
518
+ _act(ctx, "Bring the app to the front" if options.get("stopApp") is False
519
+ else "Force-quit the app, then open it again")
520
+
521
+
522
+ def _wait_until(arg, env, ctx, where):
523
+ if not isinstance(arg, dict) or ("visible" in arg) == ("notVisible" in arg):
524
+ return ctx.problem(where, "extendedWaitUntil", "doesn't have exactly one of visible and notVisible",
525
+ "give one", assertion=True)
526
+ present = "visible" in arg
527
+ sel = _selector(arg["visible" if present else "notVisible"], where, "extendedWaitUntil", ctx, env)
528
+ if sel is None or arg.get("optional") is True:
529
+ if sel is not None:
530
+ ctx.problem(where, "extendedWaitUntil", "is optional, so it asserts nothing", "remove optional",
531
+ assertion=True)
532
+ return
533
+ found = _expect(sel, present, "extendedWaitUntil", ctx, where, "extendedWaitUntil")
534
+ if found is None:
535
+ return
536
+ timeout = arg.get("timeout")
537
+ if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 5000:
538
+ ctx.notes.append(f"{where}: Maestro waits up to {timeout / 1000:g} s; Mobster waits --assert-timeout "
539
+ "(default 5) for the screen to settle, then judges")
540
+ ctx.ops.append(("assert", found))
541
+
542
+
543
+ def _scroll_until(arg, env, ctx, where):
544
+ if not isinstance(arg, dict) or "element" not in arg:
545
+ return ctx.problem(where, "scrollUntilVisible", "has no element to scroll to", "give element: with text or id")
546
+ sel = _selector(arg["element"], where, "scrollUntilVisible", ctx, env, tap=True)
547
+ direction = str(arg.get("direction", "DOWN")).upper()
548
+ if direction not in DIRECTIONS:
549
+ return ctx.problem(where, "scrollUntilVisible", f"scrolls {direction[:20]!r}, not UP, DOWN, LEFT or RIGHT",
550
+ "fix the direction")
551
+ if sel is not None:
552
+ tail = "; continue if it never appears" if arg.get("optional") is True else ""
553
+ _act(ctx, f"Scroll {direction.lower()} until {sel.describe()} is visible{tail}")
554
+
555
+
556
+ def _swipe(arg, env, ctx, where):
557
+ arg = arg if isinstance(arg, dict) else {}
558
+ if "direction" in arg and "from" not in arg:
559
+ direction = str(arg["direction"]).upper()
560
+ if direction in DIRECTIONS:
561
+ return _act(ctx, f"Swipe {direction.lower()} across the screen")
562
+ elif "direction" in arg and "from" in arg:
563
+ sel = _selector(arg["from"], where, "swipe", ctx, env, tap=True)
564
+ direction = str(arg["direction"]).upper()
565
+ if direction in DIRECTIONS and sel is not None:
566
+ return _act(ctx, f"Swipe {direction.lower()} starting on {sel.describe()}")
567
+ if sel is not None:
568
+ return ctx.problem(where, "swipe", "has a direction Mobster can't read", "use UP, DOWN, LEFT or RIGHT")
569
+ return
570
+ elif "start" in arg and "end" in arg:
571
+ points = []
572
+ for key in ("start", "end"):
573
+ text = _scalar(arg[key])
574
+ if text and (PERCENT_POINT.match(text) or ABS_POINT.match(text)):
575
+ points.append(text.replace(" ", ""))
576
+ if len(points) == 2:
577
+ return _act(ctx, f"Swipe from the screen point {points[0]} to {points[1]}")
578
+ ctx.problem(where, "swipe", "isn't a swipe Mobster can read",
579
+ "use direction, or start and end as 50%,80%")
580
+
581
+
582
+ def _run_flow(arg, env, ctx, base, where, depth, chain):
583
+ if isinstance(arg, str):
584
+ arg = {"file": arg}
585
+ if not isinstance(arg, dict):
586
+ return ctx.problem(where, "runFlow", "has options Mobster can't read", "give a file or commands")
587
+ if "when" in arg:
588
+ return ctx.problem(where, "runFlow", "runs only when a condition holds, and a check is a fixed list of steps",
589
+ "write the branch you want as its own flow")
590
+ inner = dict(env)
591
+ given = arg.get("env") or {}
592
+ if not isinstance(given, dict):
593
+ return ctx.problem(where, "runFlow", "has an env Mobster can't read", "write env as KEY: value")
594
+ for key, value in given.items():
595
+ inner[str(key)] = _substitute(str(value), env, ctx, where, "runFlow") or ""
596
+ if depth >= MAX_DEPTH:
597
+ return ctx.problem(where, "runFlow", f"nests more than {MAX_DEPTH} flows deep", "flatten the flows")
598
+ if "commands" in arg:
599
+ return _commands(arg["commands"], inner, ctx, base, f"{where} > ", depth + 1, chain)
600
+ target = _scalar(arg.get("file"))
601
+ if not target:
602
+ return ctx.problem(where, "runFlow", "has neither a file nor commands", "give one")
603
+ target = _substitute(target, env, ctx, where, "runFlow")
604
+ if target is None:
605
+ return
606
+ path = (base / target).resolve()
607
+ try:
608
+ path.relative_to(ctx.root)
609
+ except ValueError:
610
+ return ctx.problem(where, "runFlow", f"reads {target[:60]}, outside the folder being imported",
611
+ "keep subflows inside the folder you import")
612
+ if path in chain:
613
+ return ctx.problem(where, "runFlow", "runs a flow that runs it again", "remove the loop")
614
+ try:
615
+ config, commands = read_flow(path)
616
+ except MaestroError as error:
617
+ return ctx.problem(where, "runFlow", f"{target[:60]} {error}", "fix the file or the path")
618
+ for key, value in (config.get("env") or {}).items() if isinstance(config.get("env"), dict) else ():
619
+ if str(key) in inner and inner[str(key)] != str(value):
620
+ return ctx.problem(where, "runFlow", f"{target[:60]} sets {key} to a different value than the flow that "
621
+ "runs it, and Mobster can't tell which wins", "set it in one place")
622
+ inner[str(key)] = str(value)
623
+ app = config.get("appId")
624
+ if app is not None and _substitute(str(app), inner, ctx, where, "runFlow") not in (None, ctx.launch.get("appId")):
625
+ return ctx.problem(where, "runFlow", f"{target[:60]} is for a different app", "write one flow per app")
626
+ _commands(commands, inner, ctx, path.parent, f"{path.relative_to(ctx.root)} ", depth + 1, chain | {path})
627
+
628
+
629
+ def _pack(steps):
630
+ """At most LIMITS['steps'] steps: join neighbours with ", then" when there are more."""
631
+ cap, limit = LIMITS["steps"], LIMITS["step"]
632
+ if len(steps) <= cap:
633
+ return steps
634
+ size = -(-len(steps) // cap)
635
+ out = [", then ".join(s[0].lower() + s[1:] if i else s for i, s in enumerate(steps[i:i + size]))
636
+ for i in range(0, len(steps), size)]
637
+ if any(len(step) > limit for step in out):
638
+ raise ValueError("long")
639
+ return out
640
+
641
+
642
+ def _segments(ops):
643
+ """Cumulative segments: [(steps so far, assertions of this group)], one per assertion group that more actions
644
+ follow, then the last."""
645
+ steps, group, out = [], [], []
646
+ for op in ops:
647
+ if op[0] == "act":
648
+ if group:
649
+ out.append((list(steps), group))
650
+ group = []
651
+ steps.append(op[1])
652
+ elif op[0] == "assert":
653
+ group.append(op[1])
654
+ trailing = 0
655
+ if group or not out:
656
+ out.append((list(steps), group))
657
+ else:
658
+ trailing = len(steps) - len(out[-1][0])
659
+ return out, trailing
660
+
661
+
662
+ def convert_flow(file, root, *, bundle=None, app_path=None, skip_unsupported=False, stem=None, shown=None):
663
+ """The FlowResult for one Maestro flow file."""
664
+ file, root = Path(file).resolve(), Path(root).resolve()
665
+ result = FlowResult(shown or str(file.relative_to(root)))
666
+ stem = stem or file.stem
667
+ try:
668
+ config, commands = read_flow(file)
669
+ except MaestroError as error:
670
+ result.outcome, result.reason = "refused", f"{file.name} {error}"
671
+ return result
672
+ ctx = _Ctx(root, skip_unsupported)
673
+ unknown = [key for key in config if key not in HEADER_KEYS]
674
+ for key in unknown:
675
+ ctx.problem("config", str(key), "isn't a flow setting Mobster reads",
676
+ "remove it, or it belongs to a workspace config, not a flow")
677
+ env = {}
678
+ raw_env = config.get("env") or {}
679
+ if isinstance(raw_env, dict):
680
+ env = {str(key): str(value) for key, value in raw_env.items()}
681
+ for hook in ("onFlowStart", "onFlowComplete"):
682
+ if hook in config:
683
+ ctx.problem("config", hook, "runs commands before or after the flow",
684
+ "put those commands at the start or end of the flow")
685
+ app_id = _substitute(_scalar(config.get("appId")), env, ctx, "config", "appId") if config.get("appId") else None
686
+ app_id = bundle or app_id
687
+ if not app_id and ctx.problems:
688
+ result.outcome, result.problems = "refused", ctx.problems
689
+ return result
690
+ if not app_id:
691
+ result.outcome = "skipped"
692
+ result.reason = "no appId, so it is a subflow or can't name its app; pass --bundle to import it as a flow"
693
+ return result
694
+ ctx.launch["appId"] = app_id
695
+ if not commands:
696
+ result.outcome, result.reason = "refused", "has no commands"
697
+ return result
698
+ _commands(commands, env, ctx, file.parent, "", 0, frozenset({file}))
699
+ result.notes, result.dropped = ctx.notes, ctx.dropped
700
+ result.placeholders, result.converted = ctx.placeholders, ctx.converted
701
+ if ctx.placeholders:
702
+ result.notes.append(f"{len(ctx.placeholders)} command{'' if len(ctx.placeholders) == 1 else 's'} became a "
703
+ "step marked TODO: the first run (Smart) fills it, then replay keeps what it did; read "
704
+ "that run's report before you trust the check")
705
+ if ctx.problems:
706
+ result.outcome, result.problems = "refused", ctx.problems
707
+ return result
708
+ if not any(op[0] == "assert" for op in ctx.ops):
709
+ result.notes.append("the flow asserts nothing, so the check ends needs_review; add an assertVisible")
710
+ name = _text_name(config.get("name"), stem)
711
+ segments, trailing = _segments(ctx.ops)
712
+ if trailing:
713
+ result.notes.append(f"the last {trailing} step{'' if trailing == 1 else 's'} come after the final "
714
+ "assertion, so no check runs them; nothing judges them")
715
+ tags = _tags(config.get("tags"), result)
716
+ for index, (steps, asserts) in enumerate(segments, 1):
717
+ try:
718
+ steps = _pack(steps)
719
+ except ValueError:
720
+ result.outcome = "refused"
721
+ result.problems.append(Problem("flow", "steps", f"has more than {LIMITS['steps']} steps that can't be "
722
+ "joined into one check", "split the flow with runFlow into shorter flows"))
723
+ return result
724
+ data = {"version": 1, "name": name if len(segments) == 1 else f"{name} (part {index} of {len(segments)})",
725
+ "app": {"bundle": app_id}}
726
+ data["reset"] = ctx.launch.get("reset", "none")
727
+ data["tags"] = tags
728
+ if ctx.launch.get("url"):
729
+ data["launch"] = {"url": ctx.launch["url"]}
730
+ if steps:
731
+ data["steps"] = steps
732
+ data["expect"] = asserts
733
+ try:
734
+ canonical = check_to_dict(check_from_dict(data, base_dir=root))
735
+ except CheckError as error:
736
+ result.outcome = "refused"
737
+ result.problems.append(Problem("flow", "check", f"makes a check Mobster rejects: {error}",
738
+ "fix the flow, or write the check by hand"))
739
+ return result
740
+ if app_path:
741
+ canonical["app"]["path"] = str(app_path)
742
+ result.checks.append((stem if len(segments) == 1 else f"{stem}-part-{index}", canonical))
743
+ if len(segments) > 1:
744
+ result.notes.append(f"assertions in the middle of the flow became {len(segments)} checks; each one replays "
745
+ "the steps up to its assertions")
746
+ return result
747
+
748
+
749
+ def _text_name(value, stem):
750
+ text = value.strip() if isinstance(value, str) and value.strip() else stem.replace("_", " ").replace("-", " ")
751
+ return text[:LIMITS["name"]]
752
+
753
+
754
+ def _tags(value, result):
755
+ out = ["maestro"]
756
+ for tag in value if isinstance(value, list) else ():
757
+ word = re.sub(r"[^a-z0-9_-]+", "-", str(tag).lower()).strip("-")[:40]
758
+ if word and re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,39}", word):
759
+ if word not in out and len(out) < LIMITS["tags"]:
760
+ out.append(word)
761
+ else:
762
+ result.notes.append(f"the tag {str(tag)[:30]!r} isn't a Mobster tag (a-z, 0-9, - and _), "
763
+ "so it was left out")
764
+ return out
765
+
766
+
767
+ def convert_tree(paths, *, bundle=None, app_path=None, skip_unsupported=False):
768
+ """[FlowResult] for every flow under ``paths``."""
769
+ files = discover(paths)
770
+ if not files:
771
+ raise MaestroError("no .yaml or .yml files found")
772
+ shown_names = _unique_stems(files)
773
+ results = []
774
+ for (file, root), (stem, shown) in zip(files, shown_names):
775
+ if file.name.lower() in ("config.yaml", "config.yml"):
776
+ item = FlowResult(shown, "skipped", reason="a workspace config, not a flow; its includeTags, excludeTags "
777
+ "and executionOrder are not applied")
778
+ else:
779
+ item = convert_flow(file, root, bundle=bundle, app_path=app_path, skip_unsupported=skip_unsupported,
780
+ stem=stem, shown=shown)
781
+ results.append(item)
782
+ return results
783
+
784
+
785
+ def _unique_stems(files):
786
+ """(stem, shown path) per file: the file name, or the folder path joined with - when two share a name."""
787
+ out, seen = [], {}
788
+ for file, root in files:
789
+ rel = file.relative_to(root)
790
+ stem = re.sub(r"[^A-Za-z0-9_.-]+", "-", rel.with_suffix("").as_posix().replace("/", "-")).strip("-.") or "flow"
791
+ count = seen.get(stem.lower(), 0)
792
+ seen[stem.lower()] = count + 1
793
+ out.append((stem if not count else f"{stem}-{count + 1}", rel.as_posix()))
794
+ return out
795
+
796
+
797
+ def render(check, source):
798
+ """A check dict as YAML text with a header that names the Maestro flow it came from."""
799
+ import yaml
800
+ header = (f"# Imported from the Maestro flow {source}.\n"
801
+ "# Run it with: mobster test <this file>\n")
802
+ return header + yaml.safe_dump(check, sort_keys=False, allow_unicode=True, default_flow_style=False, width=100)