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,375 @@
|
|
|
1
|
+
"""A project's deep links: where they come from, what `mobster links check` does with them, and what a step can say.
|
|
2
|
+
|
|
3
|
+
Four sources, merged by address (a link in several keeps the first source's name, and takes an expected title from
|
|
4
|
+
whichever has one):
|
|
5
|
+
|
|
6
|
+
1. ``.mobster/links.yaml``: the links you name, with the title each screen should show.
|
|
7
|
+
2. The Expo config: ``scheme`` and ``ios.associatedDomains`` (app.json, or `npx expo config` for an app.config.*).
|
|
8
|
+
3. The Xcode project: ``CFBundleURLTypes`` in an Info.plist (each URL scheme) and ``com.apple.developer.associated-
|
|
9
|
+
domains`` in an .entitlements file (each ``applinks:`` domain).
|
|
10
|
+
4. Links learned: a link a check opened successfully (``learn``, called by the runner) is kept in
|
|
11
|
+
``.mobster/links-learned.json``. The memory store takes over this file when it lands (CLAIMS.md, deep-link
|
|
12
|
+
registry row).
|
|
13
|
+
|
|
14
|
+
links:
|
|
15
|
+
- name: cart
|
|
16
|
+
url: myapp://cart
|
|
17
|
+
title: Your cart # optional: the text the screen shows when the link works
|
|
18
|
+
# or: cart: myapp://cart # name: address
|
|
19
|
+
|
|
20
|
+
A scheme or a domain alone (``myapp://``, ``https://example.com/``) opens the app and no screen in it; it is listed,
|
|
21
|
+
and ``check`` reports that it opened without judging a title.
|
|
22
|
+
|
|
23
|
+
``check_for(link, ...)`` is the check a link becomes: launch the app with the link, expect the title. Run through
|
|
24
|
+
`mobster test`'s engine, so the verdict, the screenshot of a failure and the reports are the ones checks have.
|
|
25
|
+
``expand_steps`` turns a step "open cart" or "open link cart" into "Open the link myapp://cart" for a name in the
|
|
26
|
+
registry, so a step never needs the address.
|
|
27
|
+
|
|
28
|
+
Reading dynamic Expo configuration invokes its CLI; read-only API discovery skips that step. Only a custom scheme
|
|
29
|
+
is learned; web pages and links Mobster never opens (deeplinks.BLOCKED_SCHEMES) are not.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import plistlib
|
|
35
|
+
import re
|
|
36
|
+
from dataclasses import dataclass, replace
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from urllib.parse import urlsplit
|
|
39
|
+
|
|
40
|
+
from ..deeplinks import BLOCKED_SCHEMES, LinkError, risk_of
|
|
41
|
+
from ..env_file import writer
|
|
42
|
+
from ..paths import write_text_atomic
|
|
43
|
+
|
|
44
|
+
FILE = "links.yaml"
|
|
45
|
+
LEARNED = "links-learned.json"
|
|
46
|
+
LEARNED_SCHEMA = "mobster.links-learned/1"
|
|
47
|
+
SOURCES = ("links.yaml", "expo", "xcode", "learned") # in the order a duplicate keeps
|
|
48
|
+
MAX_LINKS = 200
|
|
49
|
+
MAX_LEARNED = 50
|
|
50
|
+
SKIP_DIRS = frozenset({"Pods", "build", "DerivedData", "node_modules", ".git", "Carthage", ".mobster", "vendor"})
|
|
51
|
+
SCAN_DEPTH = 5
|
|
52
|
+
NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9 ._-]{0,59}")
|
|
53
|
+
OPEN_STEP = re.compile(r"^\s*open\s+(?:the\s+)?(?:(?:deep\s*)?link\s+)?[\"']?(.+?)[\"']?\s*\.?\s*$", re.I)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LinksError(Exception):
|
|
57
|
+
"""A links file or an argument can't be used: one sentence that says what to do."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ProjectLink:
|
|
62
|
+
name: str
|
|
63
|
+
url: str
|
|
64
|
+
source: str
|
|
65
|
+
title: str = "" # the text the landing screen shows; "" when nobody said
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def screen(self):
|
|
69
|
+
"""Whether the address names a screen. A scheme or a domain alone opens the app and nothing in it."""
|
|
70
|
+
parts = urlsplit(self.url)
|
|
71
|
+
return bool(parts.path.strip("/") or parts.query or (parts.netloc and parts.scheme not in ("http", "https")))
|
|
72
|
+
|
|
73
|
+
def public(self):
|
|
74
|
+
return {"name": self.name, "url": self.url, "source": self.source, "title": self.title or None,
|
|
75
|
+
"screen": self.screen}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# -- the sources ------------------------------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def _file_links(root):
|
|
81
|
+
path = Path(root) / ".mobster" / FILE
|
|
82
|
+
if not path.is_file():
|
|
83
|
+
return []
|
|
84
|
+
import yaml
|
|
85
|
+
try:
|
|
86
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
87
|
+
except (OSError, UnicodeDecodeError):
|
|
88
|
+
raise LinksError(f".mobster/{FILE} can't be read. Check that it is a text file.") from None
|
|
89
|
+
except yaml.YAMLError:
|
|
90
|
+
raise LinksError(f".mobster/{FILE} isn't valid YAML. Write `links:` as a "
|
|
91
|
+
"list of name, url and title.") from None
|
|
92
|
+
items = data.get("links") if isinstance(data, dict) and set(data) == {"links"} else data
|
|
93
|
+
if isinstance(items, dict):
|
|
94
|
+
items = [{"name": key, **(value if isinstance(value, dict) else {"url": value})} for key,
|
|
95
|
+
value in items.items()]
|
|
96
|
+
if not isinstance(items, list) or len(items) > MAX_LINKS:
|
|
97
|
+
raise LinksError(f".mobster/{FILE} holds `links:` as a list of at most {MAX_LINKS}.")
|
|
98
|
+
out = []
|
|
99
|
+
for index, item in enumerate(items, 1):
|
|
100
|
+
if not isinstance(item, dict) or set(item) - {"name", "url", "title"}:
|
|
101
|
+
raise LinksError(f".mobster/{FILE}, link {index}: use name, url and title.")
|
|
102
|
+
name, url, title = str(item.get("name") or "").strip(), item.get("url"), item.get("title") or ""
|
|
103
|
+
if not NAME.fullmatch(name):
|
|
104
|
+
raise LinksError(f".mobster/{FILE}, link {index}: `name` is 1 to 60 letters, digits, spaces, dots, "
|
|
105
|
+
"dashes or underscores.")
|
|
106
|
+
if not isinstance(url, str) or not isinstance(title, str) or len(title) > 200:
|
|
107
|
+
raise LinksError(f".mobster/{FILE}, link {name}: `url` and `title` are text; a title is at most 200.")
|
|
108
|
+
try:
|
|
109
|
+
risk_of(url)
|
|
110
|
+
except LinkError as error:
|
|
111
|
+
raise LinksError(f".mobster/{FILE}, link {name}: {error}") from None
|
|
112
|
+
out.append(ProjectLink(name, url.strip(), "links.yaml", title.strip()))
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _expo_links(root, runner):
|
|
117
|
+
from . import expo
|
|
118
|
+
root = Path(root)
|
|
119
|
+
if not (root / "package.json").is_file():
|
|
120
|
+
return []
|
|
121
|
+
config = expo.read_config(root, runner=runner)
|
|
122
|
+
schemes = config.get("scheme")
|
|
123
|
+
out = [ProjectLink(scheme, f"{scheme}://", "expo")
|
|
124
|
+
for scheme in ([schemes] if isinstance(schemes, str) else schemes or ())
|
|
125
|
+
if isinstance(scheme, str) and re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]{0,40}", scheme)]
|
|
126
|
+
ios = config.get("ios") if isinstance(config.get("ios"), dict) else {}
|
|
127
|
+
for domain in ios.get("associatedDomains") or ():
|
|
128
|
+
out += _applink(domain, "expo")
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _applink(entry, source):
|
|
133
|
+
kind, _, host = str(entry).partition(":")
|
|
134
|
+
host = host.split("?")[0].strip()
|
|
135
|
+
if kind != "applinks" or not re.fullmatch(r"(\*\.)?[A-Za-z0-9.-]{1,253}", host) or host.startswith("*"):
|
|
136
|
+
return []
|
|
137
|
+
return [ProjectLink(host, f"https://{host}/", source)]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _walk(root):
|
|
141
|
+
root = Path(root)
|
|
142
|
+
for folder, dirs, files in os.walk(root):
|
|
143
|
+
depth = len(Path(folder).relative_to(root).parts)
|
|
144
|
+
dirs[:] = (
|
|
145
|
+
[d for d in sorted(dirs) if d not in SKIP_DIRS and not d.endswith(".xcarchive")]
|
|
146
|
+
if depth < SCAN_DEPTH else [])
|
|
147
|
+
for name in sorted(files):
|
|
148
|
+
if name.endswith("Info.plist") or name.endswith(".entitlements"):
|
|
149
|
+
yield Path(folder) / name
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _xcode_links(root):
|
|
153
|
+
out = []
|
|
154
|
+
for path in _walk(root):
|
|
155
|
+
try:
|
|
156
|
+
data = plistlib.loads(path.read_bytes())
|
|
157
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
158
|
+
continue
|
|
159
|
+
if not isinstance(data, dict):
|
|
160
|
+
continue
|
|
161
|
+
for kind in data.get("CFBundleURLTypes") or ():
|
|
162
|
+
for scheme in (kind.get("CFBundleURLSchemes") if isinstance(kind, dict) else None) or ():
|
|
163
|
+
if isinstance(scheme, str) and re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]{0,40}", scheme) \
|
|
164
|
+
and scheme.casefold() not in BLOCKED_SCHEMES:
|
|
165
|
+
out.append(ProjectLink(scheme, f"{scheme}://", "xcode"))
|
|
166
|
+
for domain in data.get("com.apple.developer.associated-domains") or ():
|
|
167
|
+
out += _applink(domain, "xcode")
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _learned(root):
|
|
172
|
+
path = Path(root) / ".mobster" / LEARNED
|
|
173
|
+
try:
|
|
174
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
175
|
+
except (OSError, ValueError):
|
|
176
|
+
return []
|
|
177
|
+
items = data.get("links") if isinstance(data, dict) and data.get("schema") == LEARNED_SCHEMA else None
|
|
178
|
+
if not isinstance(items, list):
|
|
179
|
+
return []
|
|
180
|
+
return [ProjectLink(str(item["name"])[:60], item["url"], "learned") for item in items[:MAX_LEARNED]
|
|
181
|
+
if isinstance(item, dict) and isinstance(item.get("url"), str) and isinstance(item.get("name"), str)]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def discover(root, *, runner=None, deep=True):
|
|
185
|
+
"""The project's links, deduplicated by address. ``deep`` False skips `npx expo config` (an app.config.* needs Node,
|
|
186
|
+
which takes seconds): app.json is still read. LinksError when .mobster/links.yaml is wrong."""
|
|
187
|
+
root = Path(root)
|
|
188
|
+
found = _file_links(root)
|
|
189
|
+
if deep or not any((root / name).is_file() for name in ("app.config.js", "app.config.ts")):
|
|
190
|
+
found += _expo_links(root, runner)
|
|
191
|
+
found += _xcode_links(root) + _learned(root)
|
|
192
|
+
merged = {}
|
|
193
|
+
for link in found:
|
|
194
|
+
key = link.url
|
|
195
|
+
if key not in merged:
|
|
196
|
+
merged[key] = link
|
|
197
|
+
elif link.title and not merged[key].title:
|
|
198
|
+
merged[key] = replace(merged[key], title=link.title)
|
|
199
|
+
return list(merged.values())[:MAX_LINKS]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def find(links, wanted):
|
|
203
|
+
"""The link a name (case ignored) or an exact address names. LinksError when none or two match."""
|
|
204
|
+
address = str(wanted or "").strip()
|
|
205
|
+
text = " ".join(address.split()).casefold()
|
|
206
|
+
hits = [link for link in links if link.name.casefold() == text] or [link for link in links if link.url == address]
|
|
207
|
+
if not hits:
|
|
208
|
+
names = ", ".join(link.name for link in links[:8])
|
|
209
|
+
raise LinksError(f"No link is called {str(wanted)[:60]}." + (f" You have: {names}." if names else
|
|
210
|
+
" `mobster links list` shows what Mobster found."))
|
|
211
|
+
if len(hits) > 1:
|
|
212
|
+
raise LinksError(f"Several links are called {str(wanted)[:60]}. Use the address.")
|
|
213
|
+
return hits[0]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# -- saving -----------------------------------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def _write(root, items):
|
|
219
|
+
import yaml
|
|
220
|
+
folder = Path(root) / ".mobster"
|
|
221
|
+
folder.mkdir(exist_ok=True)
|
|
222
|
+
rows = [{key: value for key, value in (("name", link.name), ("url", link.url), ("title", link.title)) if value}
|
|
223
|
+
for link in items]
|
|
224
|
+
write_text_atomic(folder / FILE, yaml.safe_dump({"links": rows}, sort_keys=False, allow_unicode=True))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def save(root, name, url, title=""):
|
|
228
|
+
"""Add or replace the link called ``name`` in .mobster/links.yaml.
|
|
229
|
+
LinksError for a name or address that can't be kept."""
|
|
230
|
+
name = " ".join(str(name or "").split())
|
|
231
|
+
if not NAME.fullmatch(name):
|
|
232
|
+
raise LinksError("A link's name is 1 to 60 letters, digits, spaces, dots, dashes or underscores.")
|
|
233
|
+
if not isinstance(url, str):
|
|
234
|
+
raise LinksError("A link needs an address, such as myapp://cart.")
|
|
235
|
+
try:
|
|
236
|
+
risk_of(url)
|
|
237
|
+
except LinkError as error:
|
|
238
|
+
raise LinksError(str(error)) from None
|
|
239
|
+
if not isinstance(title, str) or len(title) > 200:
|
|
240
|
+
raise LinksError("A link's title is text of at most 200 characters.")
|
|
241
|
+
link = ProjectLink(name, url.strip(), "links.yaml", title.strip())
|
|
242
|
+
with writer(Path(root) / ".mobster" / ".links"):
|
|
243
|
+
kept = [item for item in _file_links(root) if item.name.casefold() != name.casefold()]
|
|
244
|
+
if len(kept) >= MAX_LINKS:
|
|
245
|
+
raise LinksError(f"A project keeps at most {MAX_LINKS} links in .mobster/{FILE}.")
|
|
246
|
+
_write(root, kept + [link])
|
|
247
|
+
return link
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def remove(root, name):
|
|
251
|
+
"""Take the link called ``name`` out of .mobster/links.yaml. Returns whether it was there."""
|
|
252
|
+
text = " ".join(str(name or "").split()).casefold()
|
|
253
|
+
with writer(Path(root) / ".mobster" / ".links"):
|
|
254
|
+
items = _file_links(root)
|
|
255
|
+
kept = [item for item in items if item.name.casefold() != text]
|
|
256
|
+
if len(kept) == len(items):
|
|
257
|
+
return False
|
|
258
|
+
_write(root, kept)
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# -- learning ---------------------------------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def learn(root, url, name=None):
|
|
265
|
+
"""Keep ``url`` as learned after a run opened it. Never raises; does nothing without a .mobster folder or for a
|
|
266
|
+
web page or a link Mobster never opens. True when the link is new."""
|
|
267
|
+
try:
|
|
268
|
+
scheme = urlsplit(url).scheme.casefold()
|
|
269
|
+
if not scheme or scheme in BLOCKED_SCHEMES or scheme in ("http", "https") or risk_of(url) != "opens":
|
|
270
|
+
return False
|
|
271
|
+
folder = Path(root) / ".mobster"
|
|
272
|
+
if not folder.is_dir():
|
|
273
|
+
return False
|
|
274
|
+
with writer(Path(root) / ".mobster" / ".links"):
|
|
275
|
+
known = [{"name": link.name, "url": link.url} for link in _learned(root)]
|
|
276
|
+
if any(item["url"] == url for item in known):
|
|
277
|
+
return False
|
|
278
|
+
parts = urlsplit(url)
|
|
279
|
+
label = name or (parts.netloc + parts.path).strip("/").replace("/", " ") or scheme
|
|
280
|
+
known = ([{"name": re.sub(r"[^A-Za-z0-9 ._-]", "", label)[:60].strip() or scheme, "url": url}]
|
|
281
|
+
+ known)[:MAX_LEARNED]
|
|
282
|
+
write_text_atomic(folder / LEARNED, json.dumps({"schema": LEARNED_SCHEMA, "links": known}, indent=1) + "\n")
|
|
283
|
+
return True
|
|
284
|
+
except Exception: # noqa: BLE001 -- what a run learned is a convenience, never a reason to fail it
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# -- steps and checks -------------------------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
def expand_steps(steps, links):
|
|
291
|
+
"""``steps`` with each "open <link name>" or "open link <name>" naming a project link turned into "Open the link
|
|
292
|
+
<address>". Other steps, and "open Settings", are left as written."""
|
|
293
|
+
if not links:
|
|
294
|
+
return tuple(steps)
|
|
295
|
+
out = []
|
|
296
|
+
for step in steps:
|
|
297
|
+
match = OPEN_STEP.match(step)
|
|
298
|
+
hit = None
|
|
299
|
+
if match:
|
|
300
|
+
text = " ".join(match.group(1).split()).casefold()
|
|
301
|
+
if any(link.name.casefold() == text for link in links):
|
|
302
|
+
hit = find(links, match.group(1))
|
|
303
|
+
out.append(f"Open the link {hit.url}" if hit else step)
|
|
304
|
+
return tuple(out)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def checkable(found):
|
|
308
|
+
"""(the links ``check`` opens, the ones it skips): a link that acts as it opens (a call, a shortcut) is skipped."""
|
|
309
|
+
def opens(link):
|
|
310
|
+
try:
|
|
311
|
+
return risk_of(link.url) == "opens"
|
|
312
|
+
except LinkError:
|
|
313
|
+
return False
|
|
314
|
+
chosen = [link for link in found if opens(link)]
|
|
315
|
+
return chosen, [link for link in found if link not in chosen]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def check_data(link, *, bundle=None, app_path=None):
|
|
319
|
+
"""The check file's content for ``link``: launch with the address, expect the title."""
|
|
320
|
+
app = {"path": app_path} if app_path else {"bundle": bundle}
|
|
321
|
+
data = {"name": f"Link: {link.name}", "app": app, "tags": ["links"], "launch": {"url": link.url}}
|
|
322
|
+
if link.title:
|
|
323
|
+
data["expect"] = [{"text": link.title}]
|
|
324
|
+
return data
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def checks_for(links, *, bundle=None, app_path=None):
|
|
328
|
+
"""[(link, Check)] for the links. A link that names no screen is skipped by the caller, not here."""
|
|
329
|
+
from ..verify.checks import check_from_dict
|
|
330
|
+
return [(link, check_from_dict(check_data(link, bundle=bundle, app_path=app_path))) for link in links]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def bundle_id(root, *, runner=None):
|
|
334
|
+
"""The app's bundle ID: the Expo config's ``ios.bundleIdentifier``, else the first literal PRODUCT_BUNDLE_IDENTIFIER
|
|
335
|
+
of an Xcode project (a test target's is skipped). None when neither names one."""
|
|
336
|
+
from . import expo
|
|
337
|
+
root = Path(root)
|
|
338
|
+
if (root / "package.json").is_file():
|
|
339
|
+
ios = expo.read_config(root, runner=runner).get("ios")
|
|
340
|
+
if isinstance(ios, dict) and isinstance(ios.get("bundleIdentifier"), str):
|
|
341
|
+
return ios["bundleIdentifier"]
|
|
342
|
+
for folder, dirs, files in os.walk(root):
|
|
343
|
+
dirs[:] = [d for d in sorted(dirs) if d not in SKIP_DIRS
|
|
344
|
+
and len(Path(folder).relative_to(root).parts) < SCAN_DEPTH]
|
|
345
|
+
if folder.endswith(".xcodeproj") and "project.pbxproj" in files:
|
|
346
|
+
try:
|
|
347
|
+
text = (Path(folder) / "project.pbxproj").read_text(encoding="utf-8", errors="replace")
|
|
348
|
+
except OSError:
|
|
349
|
+
continue
|
|
350
|
+
for found in re.findall(r"PRODUCT_BUNDLE_IDENTIFIER = ([A-Za-z0-9.-]+);", text):
|
|
351
|
+
if not re.search(r"tests?$", found, re.I):
|
|
352
|
+
return found
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def plan_check(root, selected, options, *, bundle=None, app_path=None, named_device=None):
|
|
357
|
+
"""A testkit Planned for ``selected`` links: one check each, run by `mobster test`'s engine. LinksError when the
|
|
358
|
+
app isn't known."""
|
|
359
|
+
from ..testkit.api import Planned
|
|
360
|
+
from ..testkit.discover import Entry
|
|
361
|
+
from ..testkit.targets import resolve
|
|
362
|
+
root = Path(root)
|
|
363
|
+
bundle = bundle or (None if app_path else bundle_id(root))
|
|
364
|
+
if not bundle and not app_path:
|
|
365
|
+
raise LinksError("Mobster can't tell which app the links open. Pass --bundle com.example.app, or --app PATH "
|
|
366
|
+
"to a built .app.")
|
|
367
|
+
entries = []
|
|
368
|
+
for link, check in checks_for(selected, bundle=bundle, app_path=app_path):
|
|
369
|
+
file = f".mobster/{FILE}#{link.name}"
|
|
370
|
+
entry = Entry(path=root / ".mobster" / FILE, file=file, check=check,
|
|
371
|
+
classname="links." + re.sub(r"[^A-Za-z0-9_]+", "_", link.name),
|
|
372
|
+
keys=(link.name.casefold(), link.url.casefold()))
|
|
373
|
+
entries.append(entry)
|
|
374
|
+
targets = resolve(sims=options.sims, devices=options.devices, root=root, named_device=named_device)
|
|
375
|
+
return Planned(root=root, entries=entries, targets=targets, options=options)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""`mobster links`: the deep links a project has, and whether each one still lands where it should (links.py).
|
|
2
|
+
|
|
3
|
+
mobster links list [--json] every link, where it came from, the title its screen should show
|
|
4
|
+
mobster links check [NAME...] [--sim TYPE] open each on a simulator and say what screen it landed on
|
|
5
|
+
|
|
6
|
+
``check`` is `mobster test` for links: each link becomes a check that launches the app with the address and expects
|
|
7
|
+
its title, so the verdict, the failure's screenshot and the reports (.mobster/test-results/) are the ones checks have.
|
|
8
|
+
A link with no title is opened and its landing screen recorded; it reports "needs review" until you add a title to
|
|
9
|
+
.mobster/links.yaml. A link that acts as it opens (a call, a shortcut) is never opened by `check`.
|
|
10
|
+
|
|
11
|
+
Exit codes: 0 every link landed where it should, 1 one didn't, 3 couldn't (usage too).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from . import DevError, main, say, subcommands
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_arguments(parser, helpers):
|
|
22
|
+
add = subcommands(parser, "links_command")
|
|
23
|
+
listed = add("list", help="list the project's deep links and where each came from",
|
|
24
|
+
description="List the project's deep links: the ones in .mobster/links.yaml, the Expo config's "
|
|
25
|
+
"scheme and associated domains, the Xcode project's URL types and associated domains, "
|
|
26
|
+
"and links a check opened successfully. Dynamic Expo config runs `npx expo config`.")
|
|
27
|
+
listed.add_argument("--json", action="store_true", help="print them as JSON")
|
|
28
|
+
check = add("check", help="open each link on a simulator and record the screen it lands on",
|
|
29
|
+
description="Open each link on a simulator, in the app, and say what screen it landed on: passed "
|
|
30
|
+
"when the screen shows the link's title, failed with a screenshot when it doesn't. "
|
|
31
|
+
"Name links to check some; with none, every link is checked.",
|
|
32
|
+
epilog="exit codes: 0 every link landed where it should, 1 one didn't, 3 couldn't\n\n"
|
|
33
|
+
"examples:\n mobster links check\n mobster links check cart profile --sim \"iPhone 17 Pro\"")
|
|
34
|
+
check.add_argument("names", nargs="*", metavar="NAME", help="links to check, by name or address (default: all)")
|
|
35
|
+
check.add_argument("--sim", action="append", metavar="TYPE", help="a simulator, such as "
|
|
36
|
+
"\"iPhone 17 Pro\"; repeatable")
|
|
37
|
+
check.add_argument("--device", action="append", metavar="NAME", help="an iPhone from `mobster devices`; repeatable")
|
|
38
|
+
check.add_argument("--bundle", metavar="ID", help="the app's bundle ID (default: read from the project)")
|
|
39
|
+
check.add_argument("--app", metavar="PATH", help="a built .app to install (default: the app "
|
|
40
|
+
"already on the simulator)")
|
|
41
|
+
check.add_argument("--json", action="store_true", help="print results.json's document")
|
|
42
|
+
for sub in (listed, check):
|
|
43
|
+
sub.add_argument("--project", metavar="DIR", help="the project folder (default: the one you are in)")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run(args, *, cwd=None):
|
|
47
|
+
return main("links", {"list": _list, "check": _check}, args, cwd=Path(cwd or Path.cwd()))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _root(args, cwd):
|
|
51
|
+
from ..verify.checks import project_root
|
|
52
|
+
return project_root(Path(args.project).expanduser() if args.project else cwd)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _links(root):
|
|
56
|
+
from . import links
|
|
57
|
+
try:
|
|
58
|
+
return links.discover(root)
|
|
59
|
+
except links.LinksError as error:
|
|
60
|
+
raise DevError(str(error)) from None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _list(args, cwd):
|
|
64
|
+
found = _links(_root(args, cwd))
|
|
65
|
+
if args.json:
|
|
66
|
+
print(json.dumps({"links": [link.public() for link in found]}, ensure_ascii=False), flush=True)
|
|
67
|
+
return 0
|
|
68
|
+
if not found:
|
|
69
|
+
say("No deep links found. Add some to .mobster/links.yaml:\n\n links:\n - name: "
|
|
70
|
+
"cart\n url: myapp://cart\n"
|
|
71
|
+
" title: Your cart")
|
|
72
|
+
return 0
|
|
73
|
+
width = max(len(link.name) for link in found)
|
|
74
|
+
wide = max(len(link.url) for link in found)
|
|
75
|
+
for link in found:
|
|
76
|
+
say(f"{link.name:<{width}} {link.url:<{wide}} {link.source:<10} "
|
|
77
|
+
+ (f"\"{link.title}\"" if link.title else ""))
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _check(args, cwd):
|
|
82
|
+
from ..style import color_enabled
|
|
83
|
+
from ..testkit.api import Options, UsageError, execute
|
|
84
|
+
from ..testkit.cli import summary_text
|
|
85
|
+
from . import links
|
|
86
|
+
root = _root(args, cwd)
|
|
87
|
+
found = _links(root)
|
|
88
|
+
try:
|
|
89
|
+
chosen = [links.find(found, name) for name in args.names] if args.names else found
|
|
90
|
+
except links.LinksError as error:
|
|
91
|
+
raise DevError(str(error)) from None
|
|
92
|
+
chosen, skipped = links.checkable(chosen)
|
|
93
|
+
for link in skipped:
|
|
94
|
+
say(f"Skipped {link.name}: it acts as it opens, and Mobster doesn't open that one to check it.", sys.stderr)
|
|
95
|
+
if not chosen:
|
|
96
|
+
raise DevError("There are no links to check.", "`mobster links list` shows what Mobster found.")
|
|
97
|
+
options = Options(sims=tuple(args.sim or ()), devices=tuple(args.device or ()), retries=0,
|
|
98
|
+
command="mobster links check")
|
|
99
|
+
try:
|
|
100
|
+
planned = links.plan_check(root, chosen, options, bundle=args.bundle,
|
|
101
|
+
app_path=str(Path(args.app).expanduser().resolve()) if args.app else None)
|
|
102
|
+
doc, paths = execute(planned, progress=lambda line: say(line, sys.stderr))
|
|
103
|
+
except (links.LinksError, UsageError) as error:
|
|
104
|
+
raise DevError(str(error)) from None
|
|
105
|
+
except Exception as error: # TargetError and the engine's own errors say a sentence
|
|
106
|
+
if type(error).__name__ in ("TargetError", "DiscoveryError"):
|
|
107
|
+
raise DevError(str(error)) from None
|
|
108
|
+
raise
|
|
109
|
+
if args.json:
|
|
110
|
+
print(json.dumps(doc, ensure_ascii=False, allow_nan=False), flush=True)
|
|
111
|
+
else:
|
|
112
|
+
say(landings(doc) + "\n" + summary_text(doc, paths, color=color_enabled(sys.stdout)))
|
|
113
|
+
return int(doc.get("exitCode", 3))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def landings(doc):
|
|
117
|
+
"""One line per link and device: the screen it landed on."""
|
|
118
|
+
lines = []
|
|
119
|
+
for check in doc.get("checks") or ():
|
|
120
|
+
for result in check.get("results") or ():
|
|
121
|
+
title = (result.get("screen") or {}).get("title")
|
|
122
|
+
lines.append(f"{check.get('name')}: landed on " + (f"\"{title}\"" if title else "a screen with no title"))
|
|
123
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""A project's deep links over the local API, under /api/tests/links (api_routes owner "testkit"), for the Mac app's
|
|
2
|
+
project page. Logic is links.py's; these routes only name the project and shape the answer.
|
|
3
|
+
|
|
4
|
+
| Route | Auth | Body | Answer |
|
|
5
|
+
|---|---|---|---|
|
|
6
|
+
| GET /api/tests/links?project=/abs/path | token | | {links: [{name, url, source, title, screen, last}]} |
|
|
7
|
+
| POST /api/tests/links | app | {project, name, url, title?} | 201 {link}: saved in .mobster/links.yaml |
|
|
8
|
+
| DELETE /api/tests/links?project=/abs/path&name=… | app | | {removed}: the name taken out of .mobster/links.yaml |
|
|
9
|
+
|
|
10
|
+
``source`` is links.yaml, expo, xcode or learned. ``screen`` is false for a scheme or a domain alone, which opens the
|
|
11
|
+
app and no screen in it. ``last`` is the newest check of the link (`mobster links check`, or Check links on the
|
|
12
|
+
project's page): {status, device, at}, or null when it was never checked. 400 bad_project without an absolute path to a
|
|
13
|
+
folder; 400 bad_link for a name or address that can't be saved. Checking links opens each on a simulator: `mobster links
|
|
14
|
+
check`, or POST /api/projects/{id}/runs {links: true} for a project Mobster knows (projects/runs.py). Saving rewrites
|
|
15
|
+
.mobster/links.yaml as the `links:` list, so a comment in the file is lost.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from ..api_errors import APIError
|
|
21
|
+
from ..api_routes import Response, Route, register
|
|
22
|
+
from ..service_support import needs_app
|
|
23
|
+
from . import links
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _root(request, body):
|
|
27
|
+
value = body.get("project") or (request.query.get("project") or [None])[0]
|
|
28
|
+
path = Path(value) if isinstance(value, str) else None
|
|
29
|
+
if path is None or not path.is_absolute() or not path.is_dir():
|
|
30
|
+
raise APIError("Send project: the absolute path of the project's folder.", 400, "bad_project")
|
|
31
|
+
from ..verify.checks import project_root
|
|
32
|
+
return project_root(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _guard(call):
|
|
36
|
+
try:
|
|
37
|
+
return call()
|
|
38
|
+
except links.LinksError as error:
|
|
39
|
+
raise APIError(str(error), 400, "bad_link") from None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _last_checks(root):
|
|
43
|
+
"""{link name: {status, device, at}}: the newest result of each link's check, from the project's history."""
|
|
44
|
+
from ..testkit.history import report
|
|
45
|
+
prefix = f".mobster/{links.FILE}#"
|
|
46
|
+
newest = {}
|
|
47
|
+
for row in report(root)["checks"]:
|
|
48
|
+
file = row.get("file") or ""
|
|
49
|
+
if not file.startswith(prefix) or row.get("lastStatus") is None:
|
|
50
|
+
continue
|
|
51
|
+
name, at = file[len(prefix):], row.get("lastAt") or 0
|
|
52
|
+
if name not in newest or at > (newest[name]["at"] or 0):
|
|
53
|
+
newest[name] = {"status": row["lastStatus"], "device": row.get("device") or None, "at": row.get("lastAt")}
|
|
54
|
+
return newest
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def listed(request):
|
|
58
|
+
root = _root(request, {})
|
|
59
|
+
found = _guard(lambda: links.discover(root, deep=False))
|
|
60
|
+
last = _last_checks(root)
|
|
61
|
+
return Response.json({"links": [{**link.public(), "last": last.get(link.name)} for link in found]})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def saved(request):
|
|
65
|
+
needs_app(request)
|
|
66
|
+
body = request.body if isinstance(request.body, dict) else {}
|
|
67
|
+
root = _root(request, body)
|
|
68
|
+
link = _guard(lambda: links.save(root, body.get("name"), body.get("url"), body.get("title") or ""))
|
|
69
|
+
return Response.json({"link": link.public()}, 201)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def removed(request):
|
|
73
|
+
needs_app(request)
|
|
74
|
+
root = _root(request, {})
|
|
75
|
+
name = (request.query.get("name") or [None])[0]
|
|
76
|
+
return Response.json({"removed": _guard(lambda: links.remove(root, name))})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def register_routes():
|
|
80
|
+
for route in (Route("testkit", "GET", "/api/tests/links", listed, body="none"),
|
|
81
|
+
Route("testkit", "POST", "/api/tests/links", saved),
|
|
82
|
+
Route("testkit", "DELETE", "/api/tests/links", removed, body="none")):
|
|
83
|
+
register(route)
|