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,248 @@
|
|
|
1
|
+
"""Mobster Checks: `mobster test pr`'s verdict as a GitHub check run. A post never changes the verdict or exit code."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from . import pr_report
|
|
12
|
+
from .pr import run_git
|
|
13
|
+
|
|
14
|
+
MODES = ("check", "comment", "off")
|
|
15
|
+
GITHUB_MODES_ERROR = "--github is check, comment or off"
|
|
16
|
+
NAME = "Mobster"
|
|
17
|
+
LABEL = "Mobster Checks"
|
|
18
|
+
API = "https://api.github.com"
|
|
19
|
+
TOKEN_ROUTE = "/api/connectors/github-checks/token"
|
|
20
|
+
REPO = re.compile(r"[A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}")
|
|
21
|
+
SHA = re.compile(r"[0-9a-f]{40}")
|
|
22
|
+
REMOTE = re.compile(r"(?:git@github\.com:|ssh://git@github\.com/|https://(?:[^@/\s]+@)?github\.com/)"
|
|
23
|
+
r"([A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}?)(?:\.git)?/?")
|
|
24
|
+
EARLY_S = 60 # a token this close to its expiry is asked for again
|
|
25
|
+
TIMEOUT_S = 20
|
|
26
|
+
SUMMARY_LIMIT = 65_000 # GitHub refuses a check run's summary over 65,535 characters
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Refused(Exception):
|
|
30
|
+
"""Mobster couldn't get a token for the repository: the sentence says why and what to do."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ask_mobster(repo):
|
|
34
|
+
"""{token, expiresAt, repo} from the running Mobster (connectors/github_app.py). Refused when it gives none."""
|
|
35
|
+
from ..threads.client import Client, NoService, ServiceError
|
|
36
|
+
try:
|
|
37
|
+
return Client.connect(origin="cli").call("POST", TOKEN_ROUTE, {"repo": repo}, timeout=30)
|
|
38
|
+
except NoService:
|
|
39
|
+
raise Refused("Open Mobster, so it can post the verdict on GitHub. Then run the checks again.") from None
|
|
40
|
+
except ServiceError as error:
|
|
41
|
+
install = error.data.get("installUrl")
|
|
42
|
+
raise Refused(f"{error} {install}" if isinstance(install, str) else str(error)) from None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def repository(root, environ, git=run_git):
|
|
46
|
+
"""owner/name on github.com: GITHUB_REPOSITORY in a GitHub Actions job, else the ``origin`` remote's."""
|
|
47
|
+
if environ.get("GITHUB_ACTIONS") == "true":
|
|
48
|
+
repo = environ.get("GITHUB_REPOSITORY", "")
|
|
49
|
+
return repo if REPO.fullmatch(repo) else None
|
|
50
|
+
code, url = git(["remote", "get-url", "origin"], root)
|
|
51
|
+
match = REMOTE.fullmatch(url.strip()) if code == 0 else None
|
|
52
|
+
return match.group(1) if match else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pull_request(environ):
|
|
56
|
+
"""(head commit, number) of the pull request a GitHub Actions job runs for, else (None, None)."""
|
|
57
|
+
if environ.get("GITHUB_ACTIONS") != "true":
|
|
58
|
+
return None, None
|
|
59
|
+
try:
|
|
60
|
+
with open(environ.get("GITHUB_EVENT_PATH", ""), encoding="utf-8") as handle:
|
|
61
|
+
event = json.load(handle)
|
|
62
|
+
except (OSError, ValueError):
|
|
63
|
+
return None, None
|
|
64
|
+
pull = event.get("pull_request") if isinstance(event, dict) else None
|
|
65
|
+
if not isinstance(pull, dict):
|
|
66
|
+
return None, None
|
|
67
|
+
sha = (pull.get("head") or {}).get("sha") if isinstance(pull.get("head"), dict) else None
|
|
68
|
+
number = pull.get("number")
|
|
69
|
+
return (sha if isinstance(sha, str) and SHA.fullmatch(sha) else None,
|
|
70
|
+
number if isinstance(number, int) else None)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def conclusion(result):
|
|
74
|
+
"""success, failure, or neutral when no check covered the PR: mobster test's exit code decides."""
|
|
75
|
+
if result.exit_code:
|
|
76
|
+
return "failure"
|
|
77
|
+
counts = result.pr.get("counts") or {}
|
|
78
|
+
ran = any(counts.get(key) for key in ("passed", "failed", "needs_review", "couldnt_run"))
|
|
79
|
+
return "success" if ran else "neutral"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def output(pr, artifact_url=None):
|
|
83
|
+
title = str(pr.get("headline") or "Mobster checks").removeprefix("Mobster checks: ")
|
|
84
|
+
summary = pr_report.comment(pr, artifact_url=artifact_url).replace(pr_report.MARKER + "\n", "", 1)
|
|
85
|
+
return {"title": title[:200], "summary": summary[:SUMMARY_LIMIT]}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _seconds(text):
|
|
89
|
+
try:
|
|
90
|
+
return datetime.strptime(str(text), "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp()
|
|
91
|
+
except ValueError:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Poster:
|
|
96
|
+
def __init__(self, root, mode="check", progress=None, *, environ=None, git=run_git, mint=ask_mobster,
|
|
97
|
+
opener=None, clock=time.time):
|
|
98
|
+
self.root, self.mode = root, mode if mode in MODES else "check"
|
|
99
|
+
self.progress = progress or (lambda line: None)
|
|
100
|
+
self.environ = os.environ if environ is None else environ
|
|
101
|
+
self.git, self.mint, self.opener, self.clock = git, mint, opener or urllib.request.urlopen, clock
|
|
102
|
+
self.state = "off" if self.mode == "off" else "ready" # ready, started, done or off
|
|
103
|
+
self.repo = self.sha = self.number = self.check_id = None
|
|
104
|
+
self._token, self._expires = None, 0.0
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def actions(self):
|
|
108
|
+
return self.environ.get("GITHUB_ACTIONS") == "true"
|
|
109
|
+
|
|
110
|
+
def say(self, text):
|
|
111
|
+
self.progress(f"{LABEL}: {text}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# The installation token stays in memory, asked for again before it expires; nothing here logs or prints it.
|
|
115
|
+
def _bearer(self):
|
|
116
|
+
"""The token, asked for again when there's none or it's within EARLY_S of expiring. Refused."""
|
|
117
|
+
if self._token is not None and self.clock() < self._expires - EARLY_S:
|
|
118
|
+
return self._token
|
|
119
|
+
self._token = None
|
|
120
|
+
answer = self.mint(self.repo)
|
|
121
|
+
answer = answer if isinstance(answer, dict) else {}
|
|
122
|
+
token, expires, repo = answer.get("token"), _seconds(answer.get("expiresAt")), answer.get("repo")
|
|
123
|
+
if not isinstance(token, str) or not token or expires is None or self.clock() >= expires - EARLY_S:
|
|
124
|
+
raise Refused("Mobster's server gave no usable token. Run the checks again.")
|
|
125
|
+
if isinstance(repo, str) and REPO.fullmatch(repo):
|
|
126
|
+
self.repo = repo # GitHub's own spelling, after a rename
|
|
127
|
+
self._token, self._expires = token, expires
|
|
128
|
+
return token
|
|
129
|
+
|
|
130
|
+
def _github(self, method, path, body=None):
|
|
131
|
+
token = self._bearer()
|
|
132
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
133
|
+
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
|
|
134
|
+
"X-GitHub-Api-Version": "2022-11-28", "User-Agent": "mobster-checks"}
|
|
135
|
+
if data is not None:
|
|
136
|
+
headers["Content-Type"] = "application/json"
|
|
137
|
+
request = urllib.request.Request(f"{API}{path}", data=data, method=method, headers=headers)
|
|
138
|
+
try:
|
|
139
|
+
with self.opener(request, timeout=TIMEOUT_S) as response:
|
|
140
|
+
text = response.read().decode("utf-8")
|
|
141
|
+
finally:
|
|
142
|
+
token = headers = request = None # noqa: F841 -- the token's other references go with this frame
|
|
143
|
+
return json.loads(text) if text else None
|
|
144
|
+
|
|
145
|
+
def _now(self):
|
|
146
|
+
return datetime.fromtimestamp(self.clock(), timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
147
|
+
|
|
148
|
+
def _failed(self, error, doing):
|
|
149
|
+
"""One line for a request that failed: what GitHub or Mobster said, and what to do."""
|
|
150
|
+
if isinstance(error, Refused):
|
|
151
|
+
return self.say(f"nothing posted. {error}")
|
|
152
|
+
if isinstance(error, urllib.error.HTTPError):
|
|
153
|
+
if error.code == 422 and doing == "start":
|
|
154
|
+
return self.say(f"nothing posted: GitHub doesn't have {self.sha[:7]} yet. Push it, then run the "
|
|
155
|
+
"checks again.")
|
|
156
|
+
return self.say(f"GitHub refused ({error.code}) when Mobster {doing_words(doing)}. Run the checks again.")
|
|
157
|
+
return self.say(f"GitHub didn't answer when Mobster {doing_words(doing)}, so Mobster didn't send it again. "
|
|
158
|
+
"Run the checks again.")
|
|
159
|
+
|
|
160
|
+
def start(self, changes):
|
|
161
|
+
"""The check run "Mobster", in progress on the commit the checks test."""
|
|
162
|
+
if self.state != "ready":
|
|
163
|
+
return
|
|
164
|
+
self.state = "done"
|
|
165
|
+
repo = repository(self.root, self.environ, self.git)
|
|
166
|
+
if repo is None:
|
|
167
|
+
return
|
|
168
|
+
head, self.number = pull_request(self.environ)
|
|
169
|
+
sha = head or getattr(changes, "head", None)
|
|
170
|
+
if not isinstance(sha, str) or not SHA.fullmatch(sha):
|
|
171
|
+
return
|
|
172
|
+
self.repo, self.sha = repo, sha
|
|
173
|
+
if getattr(changes, "uncommitted", False) and head is None:
|
|
174
|
+
return self.say(f"nothing posted: these checks run on uncommitted changes, so their verdict isn't "
|
|
175
|
+
f"{sha[:7]}'s. Commit and push, then run them again.")
|
|
176
|
+
try:
|
|
177
|
+
self._bearer()
|
|
178
|
+
run = self._github("POST", f"/repos/{self.repo}/check-runs", {
|
|
179
|
+
"name": NAME, "head_sha": sha, "status": "in_progress", "started_at": self._now(),
|
|
180
|
+
"output": {"title": "Testing the screens this PR changed",
|
|
181
|
+
"summary": "Mobster is building the app and running the checks that cover them."}})
|
|
182
|
+
except (Refused, urllib.error.URLError, OSError, ValueError) as error:
|
|
183
|
+
return self._failed(error, "start")
|
|
184
|
+
if not isinstance(run, dict) or not isinstance(run.get("id"), int):
|
|
185
|
+
return self.say("nothing posted: GitHub answered without a check run.")
|
|
186
|
+
self.check_id, self.state = run["id"], "started"
|
|
187
|
+
self.say(f"in progress on {self.repo} at {sha[:7]}")
|
|
188
|
+
|
|
189
|
+
def finish(self, result):
|
|
190
|
+
"""The check run completed with the verdict; with ``comment`` (outside GitHub Actions), the one comment."""
|
|
191
|
+
if self.state != "started":
|
|
192
|
+
return
|
|
193
|
+
self.state = "done"
|
|
194
|
+
artifact_url = details = None
|
|
195
|
+
if self.actions:
|
|
196
|
+
from ..devkit.ci import links
|
|
197
|
+
artifact_url, _ = links("github", self.environ)
|
|
198
|
+
details = artifact_url
|
|
199
|
+
verdict = output(result.pr, artifact_url)
|
|
200
|
+
body = {"status": "completed", "conclusion": conclusion(result), "completed_at": self._now(),
|
|
201
|
+
"output": verdict, **({"details_url": details} if details else {})}
|
|
202
|
+
try:
|
|
203
|
+
self._github("PATCH", f"/repos/{self.repo}/check-runs/{self.check_id}", body)
|
|
204
|
+
except (Refused, urllib.error.URLError, OSError, ValueError) as error:
|
|
205
|
+
return self._failed(error, "finish")
|
|
206
|
+
line = f"posted “{verdict['title']}” on {self.repo} at {self.sha[:7]}"
|
|
207
|
+
if self.mode == "comment" and not self.actions:
|
|
208
|
+
line += self._comment(result.pr)
|
|
209
|
+
self.say(line)
|
|
210
|
+
|
|
211
|
+
def abandon(self):
|
|
212
|
+
"""The run stopped before a verdict (an error, ctrl+c): the check run says so instead of staying in progress."""
|
|
213
|
+
if self.state != "started":
|
|
214
|
+
return
|
|
215
|
+
self.state = "done"
|
|
216
|
+
try:
|
|
217
|
+
self._github("PATCH", f"/repos/{self.repo}/check-runs/{self.check_id}", {
|
|
218
|
+
"status": "completed", "conclusion": "cancelled", "completed_at": self._now(),
|
|
219
|
+
"output": {"title": "Stopped", "summary": "The checks stopped before a verdict. Run them again."}})
|
|
220
|
+
except (Refused, urllib.error.URLError, OSError, ValueError) as error:
|
|
221
|
+
self._failed(error, "abandon")
|
|
222
|
+
|
|
223
|
+
def _comment(self, pr):
|
|
224
|
+
try:
|
|
225
|
+
number = self.number or self._open_pull()
|
|
226
|
+
if number is None:
|
|
227
|
+
return f"; no open pull request has {self.sha[:7]} as its head, so there's no comment"
|
|
228
|
+
from ..devkit.ci import GitHub
|
|
229
|
+
how, _ = GitHub(self._bearer(), self.repo, API, self.opener).sticky(number, pr_report.comment(pr))
|
|
230
|
+
except Refused as error:
|
|
231
|
+
return f"; no comment: {error}"
|
|
232
|
+
except urllib.error.HTTPError as error:
|
|
233
|
+
return f"; GitHub answered {error.code} to the comment"
|
|
234
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
235
|
+
return "; GitHub didn't answer about the comment, so Mobster didn't send it again"
|
|
236
|
+
return f", and {how} the comment on #{number}"
|
|
237
|
+
|
|
238
|
+
def _open_pull(self):
|
|
239
|
+
pulls = self._github("GET", f"/repos/{self.repo}/commits/{self.sha}/pulls") or []
|
|
240
|
+
for pull in pulls if isinstance(pulls, list) else ():
|
|
241
|
+
if pull.get("state") == "open" and (pull.get("head") or {}).get("sha") == self.sha:
|
|
242
|
+
return pull.get("number") if isinstance(pull.get("number"), int) else None
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def doing_words(doing):
|
|
247
|
+
return {"start": "started the check run", "finish": "posted the verdict",
|
|
248
|
+
"abandon": "marked the check run stopped"}.get(doing, "posted")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The branch and commit a suite tested: `git {branch, head}` in results.json, JUnit's properties and the Mac app's
|
|
2
|
+
runs by branch.
|
|
3
|
+
|
|
4
|
+
checkout(root) # {"branch": "onboarding-v2", "head": "217873d1…"}, or None outside a git checkout
|
|
5
|
+
|
|
6
|
+
A CI checkout is usually a detached HEAD; its branch then comes from the CI's own variable (ENV_BRANCHES), the pull
|
|
7
|
+
request's head branch first. Outside a git checkout git never runs, so a Mac without the developer tools never sees
|
|
8
|
+
macOS offer to install them. git runs with no prompt and no optional locks, for at most 3 seconds, and a value that
|
|
9
|
+
isn't a branch name or a commit is left out.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
TIMEOUT = 3
|
|
18
|
+
HEAD = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}")
|
|
19
|
+
# The CI variables that name the branch a detached checkout is on: GitHub (a pull request's head, then a push), GitLab,
|
|
20
|
+
# Bitrise, Buildkite, CircleCI, Xcode Cloud.
|
|
21
|
+
ENV_BRANCHES = ("GITHUB_HEAD_REF", "GITHUB_REF_NAME", "CI_COMMIT_REF_NAME", "BITRISE_GIT_BRANCH", "BUILDKITE_BRANCH",
|
|
22
|
+
"CIRCLE_BRANCH", "CI_BRANCH")
|
|
23
|
+
MAX_BRANCH = 255
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _repository(root):
|
|
27
|
+
"""Whether ``root`` or a folder above it holds .git (a folder, or a worktree's file)."""
|
|
28
|
+
try:
|
|
29
|
+
start = Path(root).resolve()
|
|
30
|
+
except OSError:
|
|
31
|
+
return False
|
|
32
|
+
return any((folder / ".git").exists() for folder in (start, *start.parents))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run(argv, cwd):
|
|
36
|
+
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_OPTIONAL_LOCKS": "0"}
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(["git", "-c", "core.fsmonitor=false", *argv], cwd=str(cwd), capture_output=True,
|
|
39
|
+
timeout=TIMEOUT, stdin=subprocess.DEVNULL, env=env)
|
|
40
|
+
except (OSError, subprocess.SubprocessError):
|
|
41
|
+
return 1, ""
|
|
42
|
+
return result.returncode, result.stdout.decode("utf-8", errors="replace")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _branch(text):
|
|
46
|
+
text = (text or "").strip()
|
|
47
|
+
if not text or text == "HEAD" or len(text) > MAX_BRANCH or any(ord(c) < 32 or c == "\x7f" for c in text):
|
|
48
|
+
return None
|
|
49
|
+
return text.removeprefix("refs/heads/")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def checkout(root, *, env=None, run=None):
|
|
53
|
+
"""{branch, head} of the git checkout ``root`` is in, or None outside one (or when git can't say). ``env`` is
|
|
54
|
+
os.environ and ``run(argv, cwd)`` git's runner (tests pass fakes)."""
|
|
55
|
+
if not _repository(root):
|
|
56
|
+
return None
|
|
57
|
+
env = os.environ if env is None else env
|
|
58
|
+
code, out = (run or _run)(["rev-parse", "HEAD", "--abbrev-ref", "HEAD"], root) # the commit, then the branch
|
|
59
|
+
lines = out.splitlines() if code == 0 else []
|
|
60
|
+
head = lines[0].strip() if lines and HEAD.fullmatch(lines[0].strip()) else None
|
|
61
|
+
branch = _branch(lines[1]) if len(lines) > 1 else None
|
|
62
|
+
if branch is None and not (env.get("GITHUB_REF_TYPE") and env.get("GITHUB_REF_TYPE") != "branch"):
|
|
63
|
+
branch = next((value for value in (_branch(env.get(name)) for name in ENV_BRANCHES) if value), None)
|
|
64
|
+
if branch is None and head is None:
|
|
65
|
+
return None
|
|
66
|
+
return {"branch": branch, "head": head}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Heals waiting for review (replay_core/heal.py writes and settles them), for `review_heals` and the Mac app's Tests
|
|
2
|
+
page, under /api/tests/heals (api_routes owner "testkit").
|
|
3
|
+
|
|
4
|
+
| Route | Auth | Answer |
|
|
5
|
+
|---|---|---|
|
|
6
|
+
| GET /api/tests/heals[?project=/abs/path] | token | {projects: [{project, root, problem, checks}]} |
|
|
7
|
+
| POST /api/tests/heals {project, action, checks?} | app | {action, settled: [{name, file, steps}]} |
|
|
8
|
+
| GET /api/tests/heals/frame?project=/abs/path&path=.mobster/runs/… | token | the frame, PNG or JPEG |
|
|
9
|
+
|
|
10
|
+
A check is {name, file, diff, app, ios, device, heals}, and a heal is {step, from, to, why, frames: {before, after},
|
|
11
|
+
run, at}. ``action`` is accept (the healed steps stay) or reject (the recorded steps come back and fail when their
|
|
12
|
+
screen changes again). ``checks`` names check files relative to the project, default every check with a heal waiting. A
|
|
13
|
+
frame is served only when a waiting heal names it. The routes answer 404 tests_page_off until
|
|
14
|
+
MOBSTER_EXPERIMENTAL_TESTS_PAGE=1.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..replay_core import heal as heals
|
|
20
|
+
|
|
21
|
+
FRAME_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg"}
|
|
22
|
+
FRAME_MAX = 20 * 1024 * 1024
|
|
23
|
+
MAX_CHECKS = 50
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pending(root, paths=()):
|
|
27
|
+
"""[(Entry, heal document)] for each check under ``paths`` (default the project's checks) with a heal waiting.
|
|
28
|
+
DiscoveryError for a path that isn't a check; HealError for a heal file that can't be read."""
|
|
29
|
+
from .discover import discover
|
|
30
|
+
found = []
|
|
31
|
+
for entry in discover(list(paths), root):
|
|
32
|
+
document = heals.read(entry.path)
|
|
33
|
+
if document is not None:
|
|
34
|
+
found.append((entry, document))
|
|
35
|
+
return found
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def settle(root, found, action):
|
|
39
|
+
"""Accept or reject each heal in ``found``; [(Entry, steps settled)]."""
|
|
40
|
+
from ..replay_core.checks import cache_dir_for
|
|
41
|
+
cache = cache_dir_for(Path(root) / ".mobster" / "runs")
|
|
42
|
+
settle_one = heals.accept if action == "accept" else heals.reject
|
|
43
|
+
return [(entry, settle_one(entry.path, cache)) for entry, _ in found]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rows(found):
|
|
47
|
+
"""Each check's heals: the step, what it was, what it became, why, and the frames that show it."""
|
|
48
|
+
return [{"check": str(entry.path), "file": entry.file, "name": entry.name, "diff": str(heals.heal_file(entry.path)),
|
|
49
|
+
"heals": [{key: item.get(key) for key in ("step", "from", "to", "why", "frames", "run")}
|
|
50
|
+
for item in document["heals"] if isinstance(item, dict)]}
|
|
51
|
+
for entry, document in found]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _steps(document):
|
|
55
|
+
return [{"step": item.get("step"), "from": item.get("from"), "to": item.get("to"), "why": item.get("why"),
|
|
56
|
+
"frames": {name: (item.get("frames") or {}).get(name) for name in ("before", "after")},
|
|
57
|
+
"run": item.get("run"), "at": item.get("at")}
|
|
58
|
+
for item in document["heals"] if isinstance(item, dict)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _relative(path, root):
|
|
62
|
+
try:
|
|
63
|
+
return Path(path).resolve().relative_to(Path(root).resolve()).as_posix()
|
|
64
|
+
except ValueError:
|
|
65
|
+
return str(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def project_listing(root):
|
|
69
|
+
"""One project's waiting heals as GET /api/tests/heals answers them, and ``problem``, the sentence for a heal file
|
|
70
|
+
that can't be read (then none are listed). A project with no checks has none."""
|
|
71
|
+
from .discover import DiscoveryError
|
|
72
|
+
problem = None
|
|
73
|
+
try:
|
|
74
|
+
found = pending(root)
|
|
75
|
+
except DiscoveryError:
|
|
76
|
+
found = []
|
|
77
|
+
except heals.HealError as error:
|
|
78
|
+
found, problem = [], f"{error}. Run the check again, or delete the file."
|
|
79
|
+
checks = []
|
|
80
|
+
for entry, document in found:
|
|
81
|
+
app = document.get("app") if isinstance(document.get("app"), dict) else {}
|
|
82
|
+
checks.append({"name": entry.name, "file": entry.file, "diff": _relative(heals.heal_file(entry.path), root),
|
|
83
|
+
"app": app.get("version"), "ios": document.get("ios"), "device": document.get("device"),
|
|
84
|
+
"heals": _steps(document)})
|
|
85
|
+
return {"project": Path(root).name, "root": str(root), "checks": checks, "problem": problem}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _frames(found):
|
|
89
|
+
return {frame for _, document in found for item in _steps(document) for frame in item["frames"].values() if frame}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# -- routes ------------------------------------------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def listed(request):
|
|
95
|
+
from ..api_routes import Response
|
|
96
|
+
from .register import page_off, project_roots
|
|
97
|
+
off = page_off()
|
|
98
|
+
if off is not None:
|
|
99
|
+
return off
|
|
100
|
+
roots, bad = project_roots(request)
|
|
101
|
+
if bad is not None:
|
|
102
|
+
return bad
|
|
103
|
+
return Response.json({"projects": [project_listing(root) for root in roots]})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def settled(request):
|
|
107
|
+
from ..api_routes import Response
|
|
108
|
+
from .discover import DiscoveryError
|
|
109
|
+
from .register import app_only, page_off, project_roots
|
|
110
|
+
off = page_off() or app_only(request, "Open Mobster to accept or reject a heal.")
|
|
111
|
+
if off is not None:
|
|
112
|
+
return off
|
|
113
|
+
body = request.body if isinstance(request.body, dict) else {}
|
|
114
|
+
checks = body.get("checks", [])
|
|
115
|
+
if set(body) - {"project", "action", "checks"} or not isinstance(body.get("project"), str) \
|
|
116
|
+
or body.get("action") not in ("accept", "reject") or not isinstance(checks, list) \
|
|
117
|
+
or len(checks) > MAX_CHECKS or not all(isinstance(item, str) and item and not Path(item).is_absolute()
|
|
118
|
+
for item in checks):
|
|
119
|
+
return Response.error("Send project, action (accept or reject) and, if you like, checks: the check files "
|
|
120
|
+
"relative to the project.", 400, "bad_request")
|
|
121
|
+
roots, bad = project_roots(request, body)
|
|
122
|
+
if bad is not None:
|
|
123
|
+
return bad
|
|
124
|
+
root = roots[0]
|
|
125
|
+
try:
|
|
126
|
+
found = pending(root)
|
|
127
|
+
except (DiscoveryError, heals.HealError):
|
|
128
|
+
found = []
|
|
129
|
+
if checks:
|
|
130
|
+
by_file = {entry.file: (entry, document) for entry, document in found}
|
|
131
|
+
missing = [item for item in checks if item not in by_file]
|
|
132
|
+
if missing:
|
|
133
|
+
return Response.error(f"{missing[0]} has no heal waiting for review.", 404, "heal_not_found")
|
|
134
|
+
found = [by_file[item] for item in checks]
|
|
135
|
+
try:
|
|
136
|
+
done = settle(root, found, body["action"])
|
|
137
|
+
except heals.HealError as error:
|
|
138
|
+
return Response.error(f"{error}. Run the check again, or delete the file.", 409, "bad_heal")
|
|
139
|
+
return Response.json({"action": body["action"],
|
|
140
|
+
"settled": [{"name": entry.name, "file": entry.file, "steps": steps}
|
|
141
|
+
for entry, steps in done]})
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def frame(request):
|
|
145
|
+
from ..api_routes import Response
|
|
146
|
+
from .discover import DiscoveryError
|
|
147
|
+
from .register import page_off, project_roots
|
|
148
|
+
off = page_off()
|
|
149
|
+
if off is not None:
|
|
150
|
+
return off
|
|
151
|
+
wanted = (request.query.get("path") or [None])[0]
|
|
152
|
+
roots, bad = project_roots(request)
|
|
153
|
+
if bad is not None:
|
|
154
|
+
return bad
|
|
155
|
+
if (request.query.get("project") or [None])[0] is None or not isinstance(wanted, str):
|
|
156
|
+
return Response.error("Send project and path: a frame a waiting heal names.", 400, "bad_request")
|
|
157
|
+
root = roots[0]
|
|
158
|
+
try:
|
|
159
|
+
named = _frames(pending(root))
|
|
160
|
+
except (DiscoveryError, heals.HealError):
|
|
161
|
+
named = set()
|
|
162
|
+
file = (root / wanted).resolve() if wanted in named else None
|
|
163
|
+
runs = (root / ".mobster" / "runs").resolve()
|
|
164
|
+
kind = FRAME_TYPES.get(file.suffix.lower()) if file is not None else None
|
|
165
|
+
if file is None or kind is None or not file.is_relative_to(runs) or not file.is_file() \
|
|
166
|
+
or file.stat().st_size > FRAME_MAX:
|
|
167
|
+
return Response.error("No waiting heal has that frame.", 404, "frame_not_found")
|
|
168
|
+
return Response.bytes(file.read_bytes(), kind)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def register_routes():
|
|
172
|
+
from ..api_routes import Route, register
|
|
173
|
+
# POST /api/tests/heals is the editor's route (editor.heals), which hands the app's bodies to ``settled``.
|
|
174
|
+
for route in (Route("testkit", "GET", "/api/tests/heals", listed, body="none"),
|
|
175
|
+
Route("testkit", "GET", "/api/tests/heals/frame", frame, body="none")):
|
|
176
|
+
register(route)
|