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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- 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
|