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,737 @@
|
|
|
1
|
+
"""`mobster ci`: your checks on every pull request or merge request, on a Mac of yours or a hosted CI runner."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .. import __version__, plan_gate, premium_model
|
|
16
|
+
from . import DevError, self_argv
|
|
17
|
+
from .ci_hosts import COMMAND
|
|
18
|
+
|
|
19
|
+
CI_DIR = ".mobster/ci"
|
|
20
|
+
MARKER = "<!-- mobster:test-pr -->"
|
|
21
|
+
SCHEMA = "mobster.ci/1"
|
|
22
|
+
GITHUB_WORKFLOW = ".github/workflows/mobster.yml"
|
|
23
|
+
GITLAB_FILE = ".gitlab-ci.yml"
|
|
24
|
+
GITLAB_INCLUDE = ".gitlab/mobster.gitlab-ci.yml"
|
|
25
|
+
# Hosts whose runners Mobster doesn't see: every job on them is a hosted macOS runner, with no phones.
|
|
26
|
+
HOSTED_HOSTS = ("xcode-cloud", "codemagic", "bitrise", "circleci")
|
|
27
|
+
HOST_FILES = {"xcode-cloud": "ci_scripts/ci_post_xcodebuild.sh", "codemagic": "codemagic.yaml",
|
|
28
|
+
"bitrise": "bitrise.yml", "circleci": ".circleci/config.yml"}
|
|
29
|
+
SNIPPETS = ".mobster/hosts"
|
|
30
|
+
PAGE = 100
|
|
31
|
+
MAX_PAGES = 30
|
|
32
|
+
MIN_FREE_GB, LOW_FREE_GB = 10, 30
|
|
33
|
+
COMMENT_LIMIT = 60_000
|
|
34
|
+
DOCS = "https://docs.mobster.dev"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def listed(text):
|
|
38
|
+
"""A comma- or newline-separated list, folded for comparison."""
|
|
39
|
+
return {item.strip().casefold() for item in re.split(r"[,\n]", text or "") if item.strip()}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def host_of(environ=None):
|
|
43
|
+
"""The CI host of this job ("github", "gitlab", "xcode-cloud", "codemagic", "bitrise", "circleci"), else None (a
|
|
44
|
+
terminal)."""
|
|
45
|
+
environ = os.environ if environ is None else environ
|
|
46
|
+
if environ.get("GITHUB_ACTIONS") == "true":
|
|
47
|
+
return "github"
|
|
48
|
+
if environ.get("GITLAB_CI") == "true":
|
|
49
|
+
return "gitlab"
|
|
50
|
+
if environ.get("CI_XCODE_CLOUD") == "TRUE":
|
|
51
|
+
return "xcode-cloud"
|
|
52
|
+
if environ.get("CM_BUILD_ID"):
|
|
53
|
+
return "codemagic"
|
|
54
|
+
if environ.get("BITRISE_IO") == "true":
|
|
55
|
+
return "bitrise"
|
|
56
|
+
if environ.get("CIRCLECI") == "true":
|
|
57
|
+
return "circleci"
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_hosted(environ=None):
|
|
62
|
+
"""Whether this job runs on a runner the CI host owns, not on a Mac of yours."""
|
|
63
|
+
environ = os.environ if environ is None else environ
|
|
64
|
+
return host_of(environ) in HOSTED_HOSTS or environ.get("RUNNER_ENVIRONMENT") == "github-hosted"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def remote_host(url):
|
|
68
|
+
""""github" or "gitlab" from a remote URL, else None."""
|
|
69
|
+
text = str(url or "").lower()
|
|
70
|
+
if "github" in text:
|
|
71
|
+
return "github"
|
|
72
|
+
if "gitlab" in text:
|
|
73
|
+
return "gitlab"
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _git(root, *args, runner=None):
|
|
78
|
+
run = runner or (lambda argv, cwd: subprocess.run(argv, cwd=cwd, capture_output=True, text=True, timeout=60))
|
|
79
|
+
try:
|
|
80
|
+
done = run(["git", *args], str(root))
|
|
81
|
+
except (OSError, subprocess.SubprocessError):
|
|
82
|
+
return 1, ""
|
|
83
|
+
return done.returncode, (done.stdout or "").strip()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def repo_root(start, runner=None):
|
|
87
|
+
code, out = _git(start, "rev-parse", "--show-toplevel", runner=runner)
|
|
88
|
+
return Path(out) if code == 0 and out else Path(start)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# -- init --------------------------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _flags(devices, testflight):
|
|
94
|
+
return "".join(f" --device {shlex.quote(device)}" for device in devices) + \
|
|
95
|
+
"".join(f" --testflight {shlex.quote(bundle)}" for bundle in testflight)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def github_workflow(*, devices=(), testflight=()):
|
|
99
|
+
flags = _flags(devices, testflight)
|
|
100
|
+
return f"""# Mobster checks on every pull request, on a Mac of yours ({DOCS}/ci).
|
|
101
|
+
# Install uv on the runner (`brew install uv`). `mobster ci doctor` says what else is missing.
|
|
102
|
+
name: mobster
|
|
103
|
+
on: pull_request
|
|
104
|
+
permissions:
|
|
105
|
+
contents: read
|
|
106
|
+
pull-requests: write # the one comment Mobster keeps up to date
|
|
107
|
+
concurrency:
|
|
108
|
+
group: mobster-${{{{ github.event.pull_request.number }}}}
|
|
109
|
+
cancel-in-progress: true
|
|
110
|
+
jobs:
|
|
111
|
+
checks:
|
|
112
|
+
runs-on: [self-hosted, macOS, mobster]
|
|
113
|
+
timeout-minutes: 60
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
with:
|
|
117
|
+
fetch-depth: 0 # the diff needs the base branch
|
|
118
|
+
- name: Test what this PR changed
|
|
119
|
+
run: |
|
|
120
|
+
{COMMAND} ci run{flags}
|
|
121
|
+
env:
|
|
122
|
+
ANTHROPIC_API_KEY: ${{{{ secrets.ANTHROPIC_API_KEY }}}} # only checks with steps use it
|
|
123
|
+
- name: Upload the report
|
|
124
|
+
id: upload
|
|
125
|
+
if: always()
|
|
126
|
+
uses: actions/upload-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: mobster-report
|
|
129
|
+
path: .mobster/ci/
|
|
130
|
+
include-hidden-files: true
|
|
131
|
+
retention-days: 14
|
|
132
|
+
if-no-files-found: ignore
|
|
133
|
+
- name: Comment on the PR
|
|
134
|
+
if: always()
|
|
135
|
+
run: |
|
|
136
|
+
{COMMAND} ci comment
|
|
137
|
+
env:
|
|
138
|
+
GITHUB_TOKEN: ${{{{ github.token }}}}
|
|
139
|
+
MOBSTER_ARTIFACT_URL: ${{{{ steps.upload.outputs.artifact-url }}}}
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def gitlab_job(*, devices=(), testflight=()):
|
|
144
|
+
flags = _flags(devices, testflight)
|
|
145
|
+
return f"""# Mobster checks on every merge request, on a Mac of yours ({DOCS}/ci/gitlab).
|
|
146
|
+
# Register the Mac as a runner with the shell executor and the tag "mobster". On it, `mobster ci doctor` says what's
|
|
147
|
+
# missing. Install uv with `brew install uv`. For the MR comment, add MOBSTER_GITLAB_TOKEN (a project access token
|
|
148
|
+
# with the api scope) as a masked CI/CD variable. For checks with steps, add ANTHROPIC_API_KEY the same way.
|
|
149
|
+
mobster:
|
|
150
|
+
stage: test
|
|
151
|
+
tags: [mobster]
|
|
152
|
+
rules:
|
|
153
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
154
|
+
variables:
|
|
155
|
+
GIT_DEPTH: "0"
|
|
156
|
+
timeout: 1h
|
|
157
|
+
interruptible: true
|
|
158
|
+
script:
|
|
159
|
+
- |
|
|
160
|
+
{COMMAND} ci run{flags}
|
|
161
|
+
after_script:
|
|
162
|
+
- |
|
|
163
|
+
{COMMAND} ci comment
|
|
164
|
+
artifacts:
|
|
165
|
+
when: always
|
|
166
|
+
expire_in: 14 days
|
|
167
|
+
paths:
|
|
168
|
+
- .mobster/ci/
|
|
169
|
+
reports:
|
|
170
|
+
junit: .mobster/ci/junit.xml
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def workflow(host, *, devices=(), testflight=(), hosted=False):
|
|
175
|
+
"""(the file's path in the repository, its text): what `ci init` writes and `ci init --print` prints. Raises
|
|
176
|
+
DevError.
|
|
177
|
+
|
|
178
|
+
``hosted`` means GitHub's hosted macOS runner; the other hosted CI hosts are always hosted."""
|
|
179
|
+
from . import ci_hosts
|
|
180
|
+
if host in HOSTED_HOSTS or (hosted and host == "github"):
|
|
181
|
+
if devices or testflight:
|
|
182
|
+
raise DevError("A hosted runner has no iPhone to test on, so --device and --testflight don't go with it.",
|
|
183
|
+
"Drop them, or use a Mac of yours as the runner.")
|
|
184
|
+
return Path(GITHUB_WORKFLOW if host == "github" else HOST_FILES[host]), ci_hosts.render(host)
|
|
185
|
+
if hosted and host == "gitlab":
|
|
186
|
+
raise DevError("Mobster has no hosted runner setup for GitLab CI yet.",
|
|
187
|
+
"Use a Mac of yours as the runner: https://docs.mobster.dev/ci/gitlab")
|
|
188
|
+
if host == "github":
|
|
189
|
+
return Path(GITHUB_WORKFLOW), github_workflow(devices=devices, testflight=testflight)
|
|
190
|
+
if host == "gitlab":
|
|
191
|
+
return Path(GITLAB_FILE), gitlab_job(devices=devices, testflight=testflight)
|
|
192
|
+
raise DevError("Mobster can't tell where this repository lives.", "Pass --host github or --host gitlab.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def init(root, *, host, devices=(), testflight=(), force=False, hosted=False):
|
|
196
|
+
"""Write the workflow. Returns (path written, extra lines to tell the person). Raises DevError."""
|
|
197
|
+
root = Path(root)
|
|
198
|
+
relative, text = workflow(host, devices=devices, testflight=testflight, hosted=hosted)
|
|
199
|
+
target, notes = root / relative, []
|
|
200
|
+
if host in HOSTED_HOSTS and host != "xcode-cloud" and target.exists() and not force:
|
|
201
|
+
# these files hold a project's other jobs: leave them and put ours beside them to merge
|
|
202
|
+
target = root / SNIPPETS / relative.name
|
|
203
|
+
notes.append(f"{relative} already exists, so Mobster wrote the job to {target.relative_to(root)}. "
|
|
204
|
+
f"Merge it into {relative}.")
|
|
205
|
+
elif host == "gitlab" and target.exists():
|
|
206
|
+
target = root / GITLAB_INCLUDE
|
|
207
|
+
notes.append(f"{GITLAB_FILE} already exists, so the job went to {GITLAB_INCLUDE}. Add it to "
|
|
208
|
+
f"{GITLAB_FILE}:\n\n include:\n - local: {GITLAB_INCLUDE}\n")
|
|
209
|
+
if target.exists() and not force:
|
|
210
|
+
raise DevError(f"{target.relative_to(root)} already exists, so Mobster left it as it is.",
|
|
211
|
+
"Pass --force to replace it.")
|
|
212
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
target.write_text(text, encoding="utf-8")
|
|
214
|
+
if text.startswith("#!"):
|
|
215
|
+
target.chmod(0o755)
|
|
216
|
+
return target, notes
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# -- doctor ------------------------------------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
class Row:
|
|
222
|
+
def __init__(self, name, status, detail, fix=""):
|
|
223
|
+
self.name, self.status, self.detail, self.fix = name, status, detail, fix
|
|
224
|
+
|
|
225
|
+
def public(self):
|
|
226
|
+
return {"name": self.name, "status": self.status, "detail": self.detail, "fix": self.fix or None}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _run(argv, timeout=30):
|
|
230
|
+
try:
|
|
231
|
+
done = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
232
|
+
except (OSError, subprocess.SubprocessError):
|
|
233
|
+
return 127, ""
|
|
234
|
+
return done.returncode, (done.stdout or "") + (done.stderr or "")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def pmset_value(text, name):
|
|
238
|
+
"""The number pmset -g prints for ``name`` (" sleep 0"), or None."""
|
|
239
|
+
match = re.search(rf"^\s*{re.escape(name)}\s+(\d+)", text or "", re.MULTILINE)
|
|
240
|
+
return int(match.group(1)) if match else None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def runner_env(home, environ=None):
|
|
244
|
+
environ = os.environ if environ is None else environ
|
|
245
|
+
found = {key: environ[key] for key in ("MOBSTER_RUNNER_DEVICES", "MOBSTER_RUNNER_TESTFLIGHT") if environ.get(key)}
|
|
246
|
+
if found:
|
|
247
|
+
return found, "the job's environment"
|
|
248
|
+
env_file = Path(home) / "actions-runner" / ".env"
|
|
249
|
+
try:
|
|
250
|
+
for line in env_file.read_text(encoding="utf-8").splitlines():
|
|
251
|
+
key, _, value = line.partition("=")
|
|
252
|
+
if key.strip() in ("MOBSTER_RUNNER_DEVICES", "MOBSTER_RUNNER_TESTFLIGHT"):
|
|
253
|
+
found[key.strip()] = value.strip()
|
|
254
|
+
if found:
|
|
255
|
+
return found, str(env_file)
|
|
256
|
+
except OSError:
|
|
257
|
+
pass
|
|
258
|
+
config = Path(home) / ".gitlab-runner" / "config.toml"
|
|
259
|
+
try:
|
|
260
|
+
text = config.read_text(encoding="utf-8")
|
|
261
|
+
for key in ("MOBSTER_RUNNER_DEVICES", "MOBSTER_RUNNER_TESTFLIGHT"):
|
|
262
|
+
match = re.search(rf'"{key}=([^"]*)"', text)
|
|
263
|
+
if match:
|
|
264
|
+
found[key] = match.group(1)
|
|
265
|
+
if found:
|
|
266
|
+
return found, str(config)
|
|
267
|
+
except OSError:
|
|
268
|
+
pass
|
|
269
|
+
return {}, None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def doctor(*, run=_run, home=None, environ=None, machine=None, plan=None, agent=None, disk=None, hosted=None,
|
|
273
|
+
ci_token=None):
|
|
274
|
+
home = Path(home or Path.home())
|
|
275
|
+
environ = os.environ if environ is None else environ
|
|
276
|
+
hosted = is_hosted(environ) if hosted is None else hosted
|
|
277
|
+
rows = []
|
|
278
|
+
arch = machine if machine is not None else platform.machine()
|
|
279
|
+
rows.append(Row("Apple silicon", "ok" if arch == "arm64" else "warn", f"this Mac is {arch or 'unknown'}",
|
|
280
|
+
"" if arch == "arm64" else "Simulators run fastest on Apple silicon; an Intel Mac works, slower."))
|
|
281
|
+
code, out = run(["xcodebuild", "-version"])
|
|
282
|
+
version = out.splitlines()[0].strip() if code == 0 and out.strip() else ""
|
|
283
|
+
rows.append(Row("Xcode", "ok" if version else "fail", version or "xcodebuild didn't answer",
|
|
284
|
+
"" if version else "Install Xcode from the App Store, open it once, then run "
|
|
285
|
+
"`sudo xcode-select -s /Applications/Xcode.app`."))
|
|
286
|
+
code, out = run(["xcrun", "simctl", "list", "runtimes", "-j"])
|
|
287
|
+
runtimes = []
|
|
288
|
+
try:
|
|
289
|
+
runtimes = [item.get("name") for item in json.loads(out).get("runtimes") or ()
|
|
290
|
+
if item.get("isAvailable") and str(item.get("name", "")).startswith("iOS")]
|
|
291
|
+
except (ValueError, AttributeError):
|
|
292
|
+
pass
|
|
293
|
+
rows.append(Row("iOS Simulator runtime", "ok" if runtimes else "fail",
|
|
294
|
+
", ".join(runtimes[-2:]) if runtimes else "none installed",
|
|
295
|
+
"" if runtimes else "Run `xcodebuild -downloadPlatform iOS`."))
|
|
296
|
+
agent = self_argv()[0][0] if agent is None else agent
|
|
297
|
+
installed = Path(agent).is_file()
|
|
298
|
+
rows.append(Row("Mobster CLI", "ok" if installed else "fail",
|
|
299
|
+
f"version {__version__}" if installed else "executable not found",
|
|
300
|
+
"" if installed else f"Install uv, then run `{COMMAND} ci doctor`."))
|
|
301
|
+
bundled = plan_gate.bundled()
|
|
302
|
+
has_token = bool(environ.get("MOBSTER_CI_TOKEN", "").strip())
|
|
303
|
+
if hosted or ci_token is not None or has_token:
|
|
304
|
+
if ci_token is not None:
|
|
305
|
+
token_row = _token_row(ci_token)
|
|
306
|
+
elif bundled or has_token:
|
|
307
|
+
token_row = hosted_token_row(environ)
|
|
308
|
+
else:
|
|
309
|
+
token_row = Row("CI token", "skip", "not required by the standalone CLI")
|
|
310
|
+
rows.append(token_row)
|
|
311
|
+
if not hosted:
|
|
312
|
+
if bundled or plan is not None:
|
|
313
|
+
problem = (plan or _plan_problem)()
|
|
314
|
+
rows.append(Row("Mobster plan", "ok" if problem is None else "fail",
|
|
315
|
+
"active on this Mac" if problem is None else f"none active ({problem.replace('_', ' ')})",
|
|
316
|
+
"" if problem is None else "Open Mobster, sign in, and start your free trial or choose a "
|
|
317
|
+
"plan."))
|
|
318
|
+
else:
|
|
319
|
+
rows.append(Row("Mobster plan", "skip", "not required by the standalone CLI"))
|
|
320
|
+
if hosted:
|
|
321
|
+
# a hosted runner is a fresh machine for every job, so the rows about keeping one running don't apply
|
|
322
|
+
for name in ("System sleep", "Restart after a power cut", "Automatic login", "Runner service"):
|
|
323
|
+
rows.append(Row(name, "skip", "not needed on a hosted runner"))
|
|
324
|
+
return rows + _hosted_tail(home, disk)
|
|
325
|
+
code, out = run(["pmset", "-g"])
|
|
326
|
+
sleep, restart = pmset_value(out, "sleep"), pmset_value(out, "autorestart")
|
|
327
|
+
rows.append(Row("System sleep", "ok" if sleep == 0 else "fail" if sleep else "warn",
|
|
328
|
+
"never" if sleep == 0 else f"after {sleep} minutes" if sleep else "unknown",
|
|
329
|
+
"" if sleep == 0 else "Run `sudo pmset -a sleep 0 disksleep 0`, so the Mac never sleeps through "
|
|
330
|
+
"a job."))
|
|
331
|
+
rows.append(Row("Restart after a power cut", "ok" if restart == 1 else "warn",
|
|
332
|
+
"on" if restart == 1 else "off",
|
|
333
|
+
"" if restart == 1 else "Run `sudo pmset -a autorestart 1`."))
|
|
334
|
+
code, out = run(["defaults", "read", "/Library/Preferences/com.apple.loginwindow", "autoLoginUser"])
|
|
335
|
+
user = out.strip() if code == 0 else ""
|
|
336
|
+
rows.append(Row("Automatic login", "ok" if user else "warn", f"as {user}" if user else "off",
|
|
337
|
+
"" if user else "Turn it on in System Settings › Users & Groups, so the runner and the "
|
|
338
|
+
"simulators start again after a restart."))
|
|
339
|
+
code, out = run(["launchctl", "list"])
|
|
340
|
+
services = [name for name, needle in (("GitHub Actions runner", "actions.runner"),
|
|
341
|
+
("GitLab Runner", "gitlab-runner")) if needle in out]
|
|
342
|
+
rows.append(Row("Runner service", "ok" if services else "warn", ", ".join(services) or "none running",
|
|
343
|
+
"" if services else f"Install the runner as a service of this user: {DOCS}/ci/mac-runner."))
|
|
344
|
+
free = (disk or (lambda path: shutil.disk_usage(path).free))(str(home))
|
|
345
|
+
gigabytes = free / 1e9
|
|
346
|
+
rows.append(Row("Free disk", "ok" if gigabytes >= LOW_FREE_GB else "warn" if gigabytes >= MIN_FREE_GB else
|
|
347
|
+
"fail", f"{gigabytes:.0f} GB",
|
|
348
|
+
"" if gigabytes >= LOW_FREE_GB else "Free some space: `xcrun simctl delete unavailable` and "
|
|
349
|
+
"Xcode's DerivedData are the usual places."))
|
|
350
|
+
values, source = runner_env(home, environ)
|
|
351
|
+
if source and source.startswith(str(home) + os.sep):
|
|
352
|
+
source = "~" + source[len(str(home)):]
|
|
353
|
+
phones = sorted(listed(values.get("MOBSTER_RUNNER_DEVICES", "")))
|
|
354
|
+
rows.append(Row("Phones this runner allows", "ok",
|
|
355
|
+
f"{', '.join(phones)} (from {source})" if phones else "none: checks run on simulators only",
|
|
356
|
+
"" if phones else "To test on an iPhone, plug it in, set it up in Mobster, and list its name in "
|
|
357
|
+
"MOBSTER_RUNNER_DEVICES in the runner's environment."))
|
|
358
|
+
return rows
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
TOKEN_FIXES = {
|
|
362
|
+
"off": "CI tokens are off in this Mobster. Update Mobster for Mac, or ask Mobster to turn them on for your team.",
|
|
363
|
+
"no_token": "Add your CI token as the secret MOBSTER_CI_TOKEN and pass it to the step that runs `mobster ci run`.",
|
|
364
|
+
"expired": "Create a new CI token and replace the secret.",
|
|
365
|
+
"inactive": "This token's plan is no longer active. Check your plan, then create a new CI token.",
|
|
366
|
+
"plan_required": "The plan behind this token isn't active. Check your plan, then create a new CI token.",
|
|
367
|
+
"revoked": "This token was revoked. Create a new CI token and replace the secret.",
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# The row's word for a verdict that refuses (plan_gate.ci_token_verdict), where its key doesn't read as one.
|
|
372
|
+
VERDICT_DETAILS = {"unchecked": "couldn't be checked", "unsendable": "can't be sent", "plan_required": "plan inactive"}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def hosted_token_row(environ=None):
|
|
376
|
+
"""A hosted runner's token row. With premiumModel on no run needs a token, but one that's there must check out."""
|
|
377
|
+
environ = os.environ if environ is None else environ
|
|
378
|
+
if not premium_model.enabled(environ):
|
|
379
|
+
return _token_row(lambda: plan_gate.ci_token_problem(environ))
|
|
380
|
+
verdict = plan_gate.ci_token_verdict(environ)
|
|
381
|
+
if verdict is None:
|
|
382
|
+
return Row("CI token", "skip", "not needed")
|
|
383
|
+
if verdict == "ok":
|
|
384
|
+
return Row("CI token", "ok", "checked with Mobster")
|
|
385
|
+
return Row("CI token", "fail", VERDICT_DETAILS.get(verdict, verdict), plan_gate.ci_token_refusal(verdict))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _token_row(problem):
|
|
389
|
+
problem = problem()
|
|
390
|
+
if problem is None:
|
|
391
|
+
return Row("CI token", "ok", "valid for simulator runs")
|
|
392
|
+
return Row("CI token", "fail", problem.replace("_", " "),
|
|
393
|
+
TOKEN_FIXES.get(problem,
|
|
394
|
+
"This isn't a CI token Mobster accepts. Create a new one and replace the secret."))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _hosted_tail(home, disk):
|
|
398
|
+
free = (disk or (lambda path: shutil.disk_usage(path).free))(str(home)) / 1e9
|
|
399
|
+
return [Row("Free disk", "ok" if free >= LOW_FREE_GB else "warn" if free >= MIN_FREE_GB else "fail",
|
|
400
|
+
f"{free:.0f} GB", "" if free >= LOW_FREE_GB else "Pick a larger runner, or free space before the job."),
|
|
401
|
+
Row("Phones this runner allows", "skip", "none: a hosted runner has no iPhone, checks run on simulators")]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _plan_problem():
|
|
405
|
+
if plan_gate.gate_turned_off():
|
|
406
|
+
return None
|
|
407
|
+
return plan_gate.PlanGate(plan_gate.app_account_file()).problem()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# -- run ---------------------------------------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
def guard(devices, testflight, environ=None):
|
|
413
|
+
"""In a CI job, refuse the whole run when one phone or TestFlight app isn't on the runner's own lists."""
|
|
414
|
+
environ = os.environ if environ is None else environ
|
|
415
|
+
# outside a CI job the person at the terminal is the runner's owner
|
|
416
|
+
if host_of(environ) is None:
|
|
417
|
+
return None
|
|
418
|
+
allowed = listed(environ.get("MOBSTER_RUNNER_DEVICES", ""))
|
|
419
|
+
refused = [device for device in devices if device.casefold() not in allowed]
|
|
420
|
+
if refused:
|
|
421
|
+
phones = ("the iPhone " if len(refused) == 1 else "the iPhones ") + ", ".join(f'"{name}"' for name in refused)
|
|
422
|
+
raise DevError(f"This runner doesn't allow {phones}, so nothing ran.",
|
|
423
|
+
"Its owner lists the phones it may test in MOBSTER_RUNNER_DEVICES, in the runner's "
|
|
424
|
+
"environment, which a pull request can't change.")
|
|
425
|
+
missing = sorted({bundle.casefold() for bundle in testflight} -
|
|
426
|
+
listed(environ.get("MOBSTER_RUNNER_TESTFLIGHT", "")))
|
|
427
|
+
if missing:
|
|
428
|
+
raise DevError(f"This runner doesn't allow the TestFlight build of {', '.join(missing)}, so nothing ran.",
|
|
429
|
+
"Its owner lists them in MOBSTER_RUNNER_TESTFLIGHT, in the runner's environment.")
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
BASE_VARIABLES = ("GITHUB_BASE_REF", "CI_MERGE_REQUEST_TARGET_BRANCH_NAME", "CI_PULL_REQUEST_TARGET_BRANCH",
|
|
434
|
+
"BITRISEIO_GIT_BRANCH_DEST", "MOBSTER_BASE_BRANCH")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def base_ref(environ=None):
|
|
438
|
+
"""(the branch the PR or MR merges into as origin/<branch>, its name), or (None, None)."""
|
|
439
|
+
environ = os.environ if environ is None else environ
|
|
440
|
+
branch = next((environ[name] for name in BASE_VARIABLES if environ.get(name, "").strip()), "")
|
|
441
|
+
branch = branch.strip()
|
|
442
|
+
return (f"origin/{branch}", branch) if branch else (None, None)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def fetch_base(root, branch, git=None):
|
|
446
|
+
code, out = _git(root, "rev-parse", "--is-shallow-repository", runner=git)
|
|
447
|
+
if code == 0 and out == "true":
|
|
448
|
+
_git(root, "fetch", "--quiet", "--no-tags", "--unshallow", "origin", runner=git)
|
|
449
|
+
if branch:
|
|
450
|
+
_git(root, "fetch", "--quiet", "--no-tags", "origin",
|
|
451
|
+
f"+refs/heads/{branch}:refs/remotes/origin/{branch}", runner=git)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_argv(*, base=None, devices=(), testflight=(), sims=(), video=True, share=False, extra=()):
|
|
455
|
+
argv = ["test", "pr", "--json"]
|
|
456
|
+
if base:
|
|
457
|
+
argv += ["--base", base]
|
|
458
|
+
for sim in sims:
|
|
459
|
+
argv += ["--sim", sim]
|
|
460
|
+
for device in devices:
|
|
461
|
+
argv += ["--device", device]
|
|
462
|
+
for bundle in testflight:
|
|
463
|
+
argv += ["--testflight", bundle]
|
|
464
|
+
if video:
|
|
465
|
+
argv.append("--video")
|
|
466
|
+
if share:
|
|
467
|
+
argv.append("--share")
|
|
468
|
+
return argv + list(extra)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _last_json(text):
|
|
472
|
+
for line in reversed((text or "").strip().splitlines()):
|
|
473
|
+
line = line.strip()
|
|
474
|
+
if line.startswith("{"):
|
|
475
|
+
try:
|
|
476
|
+
value = json.loads(line)
|
|
477
|
+
except ValueError:
|
|
478
|
+
continue
|
|
479
|
+
if isinstance(value, dict):
|
|
480
|
+
return value
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _claim(target):
|
|
485
|
+
"""Clear the last job's copy in ``target``; refuse a folder holding files Mobster didn't write (no ci.json)."""
|
|
486
|
+
if target.exists():
|
|
487
|
+
if not (target / "ci.json").is_file() and any(target.iterdir()):
|
|
488
|
+
raise DevError(f"{target} holds files Mobster didn't write, so it left them there.",
|
|
489
|
+
"Move them, then run it again.")
|
|
490
|
+
shutil.rmtree(target)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def collect(root, suite, target):
|
|
494
|
+
"""Copy the suite folder to ``target`` (.mobster/ci), replacing the last job's copy."""
|
|
495
|
+
source = Path(root) / ".mobster" / "test-results" / str(suite)
|
|
496
|
+
target = Path(target)
|
|
497
|
+
_claim(target)
|
|
498
|
+
if source.is_dir():
|
|
499
|
+
shutil.copytree(source, target)
|
|
500
|
+
else:
|
|
501
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def set_output(name, value, environ=None):
|
|
505
|
+
path = ((os.environ if environ is None else environ).get("GITHUB_OUTPUT") or "").strip()
|
|
506
|
+
if not path:
|
|
507
|
+
return
|
|
508
|
+
with open(path, "a", encoding="utf-8") as handle:
|
|
509
|
+
handle.write(f"{name}={str(value).splitlines()[0] if str(value) else ''}\n")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def run_checks(root, *, devices=(), testflight=(), sims=(), video=True, base=None, extra=(), environ=None,
|
|
513
|
+
runner=None, git=None, progress=None):
|
|
514
|
+
environ = os.environ if environ is None else environ
|
|
515
|
+
progress = progress or (lambda line: None)
|
|
516
|
+
root = Path(root)
|
|
517
|
+
if any(flag.split("=", 1)[0] in ("--device", "--testflight") for flag in extra):
|
|
518
|
+
raise DevError("This CI job can't pass --device or --testflight after --, so nothing ran.",
|
|
519
|
+
"Use mobster ci run --device and --testflight; the runner's allowlist checks those flags.")
|
|
520
|
+
guard(devices, testflight, environ)
|
|
521
|
+
host = host_of(environ)
|
|
522
|
+
if base is None:
|
|
523
|
+
base, branch = base_ref(environ)
|
|
524
|
+
if branch:
|
|
525
|
+
fetch_base(root, branch, git=git)
|
|
526
|
+
argv, env_extra = self_argv()
|
|
527
|
+
# With the team's upload key, the report goes to the team's hosted reports too, and the comment links it.
|
|
528
|
+
share = bool(environ.get("MOBSTER_TEAM_KEY", "").strip())
|
|
529
|
+
argv = argv + test_argv(base=base, devices=devices, testflight=testflight, sims=sims, video=video, share=share,
|
|
530
|
+
extra=extra)
|
|
531
|
+
progress("$ mobster " + " ".join(argv[len(self_argv()[0]):]))
|
|
532
|
+
if runner is None:
|
|
533
|
+
def runner(argv, cwd, env):
|
|
534
|
+
try:
|
|
535
|
+
done = subprocess.run(argv, cwd=cwd, stdout=subprocess.PIPE, text=True, env=env)
|
|
536
|
+
except FileNotFoundError:
|
|
537
|
+
return 127, ""
|
|
538
|
+
return done.returncode, done.stdout
|
|
539
|
+
code, out = runner(argv, str(root), {**environ, **env_extra})
|
|
540
|
+
pr = _last_json(out)
|
|
541
|
+
target = root / CI_DIR
|
|
542
|
+
doc = {"schema": SCHEMA, "host": host, "exitCode": code, "suite": None,
|
|
543
|
+
"headline": "Mobster checks couldn't run"}
|
|
544
|
+
if isinstance(pr, dict) and pr.get("suite"):
|
|
545
|
+
doc.update(suite=pr["suite"], headline=pr.get("headline") or doc["headline"])
|
|
546
|
+
collect(root, pr["suite"], target)
|
|
547
|
+
else:
|
|
548
|
+
_claim(target)
|
|
549
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
550
|
+
(target / "ci.json").write_text(json.dumps(doc, indent=1) + "\n", encoding="utf-8")
|
|
551
|
+
set_output("exit-code", code, environ)
|
|
552
|
+
set_output("headline", doc["headline"], environ)
|
|
553
|
+
return code, doc
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# -- comment -----------------------------------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
def links(host, environ=None, artifact_url=None):
|
|
559
|
+
"""(the report's link, a function from a file's path in the report to its link, or None)."""
|
|
560
|
+
environ = os.environ if environ is None else environ
|
|
561
|
+
if host == "gitlab":
|
|
562
|
+
job, project, job_id = (environ.get(key, "").rstrip("/") for key in ("CI_JOB_URL", "CI_PROJECT_URL",
|
|
563
|
+
"CI_JOB_ID"))
|
|
564
|
+
if not job:
|
|
565
|
+
return artifact_url, None
|
|
566
|
+
browse = f"{job}/artifacts/browse/{CI_DIR}/"
|
|
567
|
+
|
|
568
|
+
def file_url(path):
|
|
569
|
+
quoted = urllib.parse.quote(f"{CI_DIR}/{path}")
|
|
570
|
+
return f"{project}/-/jobs/{job_id}/artifacts/file/{quoted}" if project and job_id else browse
|
|
571
|
+
return artifact_url or browse, file_url
|
|
572
|
+
url = artifact_url or environ.get("MOBSTER_ARTIFACT_URL", "").strip()
|
|
573
|
+
return url or _github_run_url(environ), None
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _github_run_url(environ):
|
|
577
|
+
if not environ.get("GITHUB_RUN_ID"):
|
|
578
|
+
return None
|
|
579
|
+
server = environ.get("GITHUB_SERVER_URL", "https://github.com").rstrip("/")
|
|
580
|
+
return f"{server}/{environ.get('GITHUB_REPOSITORY', '')}/actions/runs/{environ['GITHUB_RUN_ID']}"
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def failed_comment(doc, environ=None):
|
|
584
|
+
environ = os.environ if environ is None else environ
|
|
585
|
+
if host_of(environ) == "gitlab" and environ.get("CI_JOB_URL"):
|
|
586
|
+
log = environ["CI_JOB_URL"]
|
|
587
|
+
else:
|
|
588
|
+
log = _github_run_url(environ)
|
|
589
|
+
where = f"The [job's log]({log}) says why" if log else "The job's log says why"
|
|
590
|
+
return f"{MARKER}\n### Mobster checks couldn't run\n\n{where} (exit code {doc.get('exitCode', '?')}).\n"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def comment_markdown(root, environ=None, artifact_url=None):
|
|
594
|
+
"""The comment for the report in .mobster/ci: pr_report's, with links into this job's artifacts."""
|
|
595
|
+
from ..testkit import pr_report
|
|
596
|
+
environ = os.environ if environ is None else environ
|
|
597
|
+
folder = Path(root) / CI_DIR
|
|
598
|
+
try:
|
|
599
|
+
doc = json.loads((folder / "ci.json").read_text(encoding="utf-8"))
|
|
600
|
+
except (OSError, ValueError):
|
|
601
|
+
raise DevError(f"There's no report in {CI_DIR}, so there's nothing to comment.",
|
|
602
|
+
"Run `mobster ci run` first.") from None
|
|
603
|
+
try:
|
|
604
|
+
pr = json.loads((folder / "pr.json").read_text(encoding="utf-8"))
|
|
605
|
+
except (OSError, ValueError):
|
|
606
|
+
pr = None
|
|
607
|
+
if not isinstance(pr, dict):
|
|
608
|
+
return failed_comment(doc, environ)
|
|
609
|
+
hosted = pr.get("hostedReport")
|
|
610
|
+
if isinstance(hosted, str):
|
|
611
|
+
try:
|
|
612
|
+
return pr_report.comment(pr, artifact_url=hosted, file_url=pr_report.hosted_pages(hosted))
|
|
613
|
+
except ValueError:
|
|
614
|
+
pass # not a report link: the job's artifacts, as without --share
|
|
615
|
+
report, file_url = links(host_of(environ), environ, artifact_url)
|
|
616
|
+
return pr_report.comment(pr, artifact_url=report, file_url=file_url)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _open(request, opener=None, timeout=30):
|
|
620
|
+
with (opener or urllib.request.urlopen)(request, timeout=timeout) as response:
|
|
621
|
+
text = response.read().decode("utf-8")
|
|
622
|
+
return json.loads(text) if text else None
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class GitHub:
|
|
626
|
+
"""The one sticky PR comment, through the REST API with the job's token."""
|
|
627
|
+
|
|
628
|
+
def __init__(self, token, repository, api="https://api.github.com", opener=None):
|
|
629
|
+
self.token, self.repository, self.api, self.opener = token, repository, api.rstrip("/"), opener
|
|
630
|
+
|
|
631
|
+
def call(self, method, path, body=None):
|
|
632
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
633
|
+
headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github+json",
|
|
634
|
+
"X-GitHub-Api-Version": "2022-11-28", "User-Agent": "mobster-ci"}
|
|
635
|
+
if data is not None:
|
|
636
|
+
headers["Content-Type"] = "application/json"
|
|
637
|
+
return _open(urllib.request.Request(f"{self.api}{path}", data=data, method=method, headers=headers),
|
|
638
|
+
self.opener)
|
|
639
|
+
|
|
640
|
+
def sticky(self, number, body):
|
|
641
|
+
base = f"/repos/{self.repository}/issues"
|
|
642
|
+
for page in range(1, MAX_PAGES + 1):
|
|
643
|
+
items = self.call("GET", f"{base}/{number}/comments?per_page={PAGE}&page={page}") or []
|
|
644
|
+
for item in items:
|
|
645
|
+
if MARKER in (item.get("body") or ""):
|
|
646
|
+
done = self.call("PATCH", f"{base}/comments/{item['id']}", {"body": body})
|
|
647
|
+
return "updated", (done or {}).get("html_url") or item.get("html_url")
|
|
648
|
+
if len(items) < PAGE:
|
|
649
|
+
break
|
|
650
|
+
done = self.call("POST", f"{base}/{number}/comments", {"body": body})
|
|
651
|
+
return "created", (done or {}).get("html_url")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class GitLab:
|
|
655
|
+
"""The one sticky MR note, through the REST API with a project access token (api scope)."""
|
|
656
|
+
|
|
657
|
+
def __init__(self, token, project, api, opener=None):
|
|
658
|
+
self.token, self.project, self.api, self.opener = token, str(project), api.rstrip("/"), opener
|
|
659
|
+
|
|
660
|
+
def call(self, method, path, body=None):
|
|
661
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
662
|
+
headers = {"PRIVATE-TOKEN": self.token, "User-Agent": "mobster-ci"}
|
|
663
|
+
if data is not None:
|
|
664
|
+
headers["Content-Type"] = "application/json"
|
|
665
|
+
return _open(urllib.request.Request(f"{self.api}{path}", data=data, method=method, headers=headers),
|
|
666
|
+
self.opener)
|
|
667
|
+
|
|
668
|
+
def sticky(self, iid, body):
|
|
669
|
+
base = f"/projects/{urllib.parse.quote(self.project, safe='')}/merge_requests/{iid}/notes"
|
|
670
|
+
for page in range(1, MAX_PAGES + 1):
|
|
671
|
+
items = self.call("GET", f"{base}?per_page={PAGE}&page={page}&sort=asc") or []
|
|
672
|
+
for item in items:
|
|
673
|
+
if not item.get("system") and MARKER in (item.get("body") or ""):
|
|
674
|
+
self.call("PUT", f"{base}/{item['id']}", {"body": body})
|
|
675
|
+
return "updated", item["id"]
|
|
676
|
+
if len(items) < PAGE:
|
|
677
|
+
break
|
|
678
|
+
done = self.call("POST", base, {"body": body})
|
|
679
|
+
return "created", (done or {}).get("id")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def github_pr_number(environ):
|
|
683
|
+
try:
|
|
684
|
+
with open(environ.get("GITHUB_EVENT_PATH", ""), encoding="utf-8") as handle:
|
|
685
|
+
event = json.load(handle)
|
|
686
|
+
except (OSError, ValueError):
|
|
687
|
+
return None
|
|
688
|
+
number = (event.get("pull_request") or {}).get("number")
|
|
689
|
+
if number is None and (event.get("issue") or {}).get("pull_request"):
|
|
690
|
+
number = event["issue"].get("number")
|
|
691
|
+
return number if isinstance(number, int) else None
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def post(body, environ=None, opener=None):
|
|
695
|
+
environ = os.environ if environ is None else environ
|
|
696
|
+
host = host_of(environ)
|
|
697
|
+
if host == "github":
|
|
698
|
+
number = github_pr_number(environ)
|
|
699
|
+
if number is None:
|
|
700
|
+
return "Not a pull request, so there's no comment; the job's summary has the verdict."
|
|
701
|
+
token = environ.get("GITHUB_TOKEN", "").strip()
|
|
702
|
+
if not token:
|
|
703
|
+
return "No GITHUB_TOKEN, so Mobster didn't comment. Give the step `GITHUB_TOKEN: ${{ github.token }}`."
|
|
704
|
+
client = GitHub(token, environ.get("GITHUB_REPOSITORY", ""),
|
|
705
|
+
environ.get("GITHUB_API_URL", "https://api.github.com"), opener)
|
|
706
|
+
where = number
|
|
707
|
+
elif host == "gitlab":
|
|
708
|
+
iid = environ.get("CI_MERGE_REQUEST_IID", "").strip()
|
|
709
|
+
if not iid:
|
|
710
|
+
return "Not a merge request pipeline, so there's no comment."
|
|
711
|
+
token = environ.get("MOBSTER_GITLAB_TOKEN", "").strip()
|
|
712
|
+
if not token:
|
|
713
|
+
return ("No MOBSTER_GITLAB_TOKEN, so Mobster didn't comment. Add a project access token with the api "
|
|
714
|
+
"scope as a masked CI/CD variable named MOBSTER_GITLAB_TOKEN.")
|
|
715
|
+
client = GitLab(token, environ.get("CI_PROJECT_ID", ""), environ.get("CI_API_V4_URL", ""), opener)
|
|
716
|
+
where = iid
|
|
717
|
+
else:
|
|
718
|
+
return "Not in a CI job, so Mobster posted nothing."
|
|
719
|
+
# a comment never fails the job: every answer from the host becomes one line
|
|
720
|
+
try:
|
|
721
|
+
how, _ = client.sticky(where, body)
|
|
722
|
+
except urllib.error.HTTPError as problem:
|
|
723
|
+
if problem.code in (401, 403, 404):
|
|
724
|
+
return (f"Mobster couldn't comment: {'GitLab' if host == 'gitlab' else 'GitHub'} answered "
|
|
725
|
+
f"{problem.code}. The token can't write to this {'merge request' if host == 'gitlab' else 'PR'}"
|
|
726
|
+
" (a PR from a fork gets a read-only token).")
|
|
727
|
+
return f"Mobster couldn't comment: the host answered {problem.code}."
|
|
728
|
+
except (urllib.error.URLError, OSError, ValueError) as problem:
|
|
729
|
+
return f"Mobster couldn't reach the host to comment ({type(problem).__name__})."
|
|
730
|
+
return f"{how.capitalize()} the {'MR' if host == 'gitlab' else 'PR'} comment."
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def job_summary(body, environ=None):
|
|
734
|
+
path = ((os.environ if environ is None else environ).get("GITHUB_STEP_SUMMARY") or "").strip()
|
|
735
|
+
if path:
|
|
736
|
+
with open(path, "a", encoding="utf-8") as handle:
|
|
737
|
+
handle.write(body.replace(MARKER + "\n", "") + "\n")
|